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 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;. - @@ -309,6 +309,12 @@ Display brief summary information about how to run + +To archive an imap://username:password@server/mailbox to specify the +mailbox. + + When reading an .lock so that 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/",