aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG3
-rw-r--r--Makefile4
-rwxr-xr-xarchivemail.py133
-rw-r--r--archivemail.sgml14
-rwxr-xr-xsetup.py2
5 files changed, 144 insertions, 12 deletions
diff --git a/CHANGELOG b/CHANGELOG
index f723ea5..3bba92b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,7 @@
+Version 0.6.0 - 3 October 2002
+ * Added IMAP mailbox support. (Thanks Mark Roach)
+
Version 0.5.1 - 18 September 2002
* Fixed a bug where when running archivemail as root on a non-root mailbox,
the temporary container directory would be created as root before the
diff --git a/Makefile b/Makefile
index 601f156..173dbc8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
-VERSION=0.5.1
+VERSION=0.6.0
VERSION_TAG=v$(subst .,_,$(VERSION))
TARFILE=archivemail-$(VERSION).tar.gz
@@ -37,7 +37,7 @@ upload:
doc: archivemail.1 archivemail.html
archivemail.1: archivemail.sgml
- nsgmls archivemail.sgml | sgmlspl docbook2man-spec.pl
+ docbook2man archivemail.sgml
chmod 644 archivemail.1
archivemail.html: archivemail.sgml
diff --git a/archivemail.py b/archivemail.py
index a4a8046..4b2d069 100755
--- a/archivemail.py
+++ b/archivemail.py
@@ -22,7 +22,7 @@ Website: http://archivemail.sourceforge.net/
"""
# global administrivia
-__version__ = "archivemail v0.5.1"
+__version__ = "archivemail v0.6.0"
__cvs_id__ = "$Id$"
__copyright__ = """Copyright (C) 2002 Paul Rodger <paul@paulrodger.com>
This is free software; see the source for copying conditions. There is NO
@@ -32,8 +32,8 @@ import sys
def check_python_version():
"""Abort if we are running on python < v2.0"""
- too_old_error = """This program requires python v2.0 or greater.
-Your version of python is: %s""" % sys.version
+ too_old_error = "This program requires python v2.0 or greater. " + \
+ "Your version of python is:\n%s""" % sys.version
try:
version = sys.version_info # we might not even have this function! :)
if (version[0] < 2):
@@ -575,7 +575,7 @@ def main(args = sys.argv[1:]):
# this usage message is longer than 24 lines -- bad idea?
usage = """Usage: %s [options] mailbox [mailbox...]
-Moves old mail in mbox, MH or maildir-format mailboxes to an mbox-format
+Moves old mail in IMAP, mbox, MH or maildir-format mailboxes to an mbox-format
mailbox compressed with gzip.
Options are as follows:
@@ -601,6 +601,9 @@ Example: %s linux-kernel
mailbox. If the 'linux-kernel_archive.gz' mailbox already exists, the
newly archived messages are appended.
+To archive IMAP mailboxes, format your mailbox argument like this:
+ imap://username:password@server/mailbox
+
Website: http://archivemail.sourceforge.net/ """ % \
(options.script_name, options.days_old_max, options.archive_suffix,
options.script_name, options.days_old_max)
@@ -768,6 +771,38 @@ def add_status_headers(message):
vprint("converting maildir status into X-Status header '%s'" % x_status)
message['X-Status'] = x_status
+def add_status_headers_imap(message, flags):
+ """Add Status and X-Status headers to a message from an imap mailbox."""
+ status = ""
+ x_status = ""
+ flags = list(flags) # convert from tuple
+ for flag in flags:
+ if flag == "\\Draft": # (draft): the user considers this message a draft
+ pass # does this make any sense in mbox?
+ elif flag == "\\Flagged": # (flagged): user-defined 'important' flag
+ x_status = x_status + "F"
+ elif flag == "\\Answered": # (replied): the user has replied to this message
+ x_status = x_status + "A"
+ elif flag == "\\Seen": # (seen): the user has viewed this message
+ status = status + "R"
+ elif flag == "\\Deleted": # (trashed): user has moved this message to trash
+ pass # is this Status: D ?
+ else:
+ pass # no whingeing here, although it could be a good experiment
+ if flags.count("\\Seen") == 0:
+ if flags.count("\\Recent") == 1:
+ status = status + "N"
+ else:
+ status = status + "O"
+
+ # As with maildir folders, preserve Status and X-Status headers
+ # if they exist (they shouldn't)
+ if not message.get('Status') and status:
+ vprint("converting imap status into Status header '%s'" % status)
+ message['Status'] = status
+ if not message.get('X-Status') and x_status:
+ vprint("converting imap status into X-Status header '%s'" % x_status)
+ message['X-Status'] = x_status
def is_flagged(message):
"""return true if the message is flagged important, false otherwise"""
@@ -909,6 +944,30 @@ def is_older_than_days(time_message, max_days):
return 1
return 0
+def build_imap_filter():
+ """Return an imap filter string"""
+
+ filter = []
+ old = 0
+ if options.date_old_max == None:
+ time_now = time.time()
+ secs_old_max = (options.days_old_max * 24 * 60 * 60)
+ time_old = time.gmtime(time_now - secs_old_max)
+ time_str = time.strftime('%d-%b-%Y', time_old)
+ filter.append("BEFORE %s" % time_str)
+ else:
+ time_old = time.gmtime(options.date_old_max)
+ time_str = time.strftime('%d-%b-%Y', time_old)
+ filter.append("BEFORE %s" % time_str)
+
+ if not options.include_flagged:
+ filter.append("UNFLAGGED")
+ if options.min_size:
+ filter.append("BIGGER %d" % options.min_size)
+ if options.preserve_unread:
+ filter.append("SEEN")
+
+ return '(' + string.join(filter, ' ') + ')'
############### mailbox operations ###############
@@ -937,7 +996,10 @@ def archive(mailbox_name):
parsed_suffix = time.strftime(options.archive_suffix,
time.localtime(time.time()))
- final_archive_name = mailbox_name + parsed_suffix
+ if mailbox_name[:7].lower() == 'imap://':
+ final_archive_name = mailbox_name.split('/')[-1] + parsed_suffix
+ else:
+ final_archive_name = mailbox_name + parsed_suffix
if options.output_dir:
final_archive_name = os.path.join(options.output_dir,
os.path.basename(final_archive_name))
@@ -966,6 +1028,9 @@ def archive(mailbox_name):
if os.path.islink(mailbox_name):
unexpected_error("'%s' is a symbolic link -- I feel nervous!" %
mailbox_name)
+ elif mailbox_name[:7].lower() == 'imap://':
+ vprint("guessing mailbox is of type: imap")
+ _archive_imap(mailbox_name, final_archive_name)
elif os.path.isfile(mailbox_name):
vprint("guessing mailbox is of type: mbox")
_archive_mbox(mailbox_name, final_archive_name)
@@ -1128,7 +1193,65 @@ def _archive_dir(mailbox_name, final_archive_name, type):
if not options.quiet:
stats.display()
+def _archive_imap(mailbox_name, final_archive_name):
+ """Archive an imap mailbox - used by archive_mailbox()"""
+ assert(mailbox_name)
+ assert(final_archive_name)
+ import imaplib
+ import cStringIO
+ archive = None
+ stats = Stats(mailbox_name, final_archive_name)
+ imap_str = mailbox_name[7:]
+ filter = build_imap_filter()
+ vprint("imap filter: '%s'" % filter)
+ try:
+ imap_username, imap_str = imap_str.split(':', 1)
+ imap_password, imap_str = imap_str.split('@', 1)
+ imap_server, imap_folder = imap_str.split('/', 1)
+ except:
+ unexpected_error("you must provide a properly formatted \
+ IMAP connection string")
+ imap_srv = imaplib.IMAP4(imap_server)
+ vprint("connected to server %s" % imap_server)
+ result, response = imap_srv.login(imap_username, imap_password)
+ if result != 'OK': unexpected_error("authentication failure")
+ vprint("logged in to server as %s" % imap_username)
+ result, response = imap_srv.select(imap_folder)
+ if result != 'OK': unexpected_error("cannot select imap folder")
+ vprint("selected imap folder %s" % imap_folder)
+ result, response = imap_srv.search(None, filter)
+ if result != 'OK': unexpected_error("imap search failed")
+ message_list_str = response[0]
+ message_list = response[0].split()
+ vprint("%d messages found matching filter" % len(message_list))
+
+ for msg_id in message_list:
+ result, response = imap_srv.fetch(msg_id, '(RFC822 FLAGS)')
+ if result != 'OK': unexpected_error("Failed to fetch message")
+ msg_str = response[0][1]
+ msg_flags = imaplib.ParseFlags(response[1])
+ msg = rfc822.Message(cStringIO.StringIO(msg_str))
+ add_status_headers_imap(msg, msg_flags)
+ vprint("processing message '%s'" % msg.get('Message-ID'))
+ if options.warn_duplicates:
+ cache.warn_if_dupe(msg)
+ if not options.dry_run:
+ if not archive:
+ archive = ArchiveMbox(final_archive_name)
+ archive.write(msg)
+ stats.another_archived()
+
+ if not options.dry_run:
+ if archive:
+ archive.close()
+ archive.finalise()
+ vprint("Deleting messages")
+ imap_srv.store(string.join(message_list, ','),
+ '+FLAGS.SILENT', '\\Deleted')
+ imap_srv.close()
+ imap_srv.logout()
+
############### misc functions ###############
diff --git a/archivemail.sgml b/archivemail.sgml
index 593c882..1807c2f 100644
--- a/archivemail.sgml
+++ b/archivemail.sgml
@@ -70,10 +70,10 @@ with &gzip;.
</Para>
<Para>
-<Command/archivemail/ supports reading <application/Maildir/,
-<application/MH/ and <application/mbox/-format mailboxes, but it will
-always write archive files to <application/mbox/-format mailboxes that
-are compressed with &gzip;.
+<Command/archivemail/ supports reading <application/IMAP/,
+<application/Maildir/, <application/MH/ and <application/mbox/-format
+mailboxes, but it will always write archive files to <application/mbox/-format
+mailboxes that are compressed with &gzip;.
</Para>
<Para>
@@ -310,6 +310,12 @@ Display brief summary information about how to run <Command/archivemail/.
</Para>
<Para>
+To archive an <application/IMAP/-format mailbox, use the the format
+<command>imap://username:password@server/mailbox</command> to specify the
+mailbox.
+</Para>
+
+<Para>
When reading an <application/mbox/-format mailbox, <command/archivemail/ will
create a lockfile with the extension <filename>.lock</filename> so that
procmail will not deliver to the mailbox while it is being processed. It will
diff --git a/setup.py b/setup.py
index b4efe84..07f3628 100755
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@ check_python_version() # define & run this early - 'distutils.core' is new
from distutils.core import setup
setup(name="archivemail",
- version="0.5.1",
+ version="0.6.0",
description="archive and compress old email",
license="GNU GPL",
url="http://archivemail.sourceforge.net/",