From 78b4923832a9cb469d4782b5eb1d2bca971254a7 Mon Sep 17 00:00:00 2001 From: Nikolaus Schulz Date: Tue, 8 Apr 2008 15:38:51 +0000 Subject: IMAP: be NAMESPACE-aware; improved probing for guessed mailbox names. * Automatically add NAMESPACE prefix to the mailbox path if necessary, * Explicitely check for guessed mailbox names with LIST instead of just trying to SELECT them. * Updated documentation about NAMESPACE handling. --- CHANGELOG | 1 + archivemail.1 | 11 ++++--- archivemail.py | 91 +++++++++++++++++++++++++++++++++++++++++++++----------- archivemail.sgml | 13 ++++---- 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f342ee7..e17b4b8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ Version 0.7.3 - UNRELEASED "Aidant") Closes: #1878940. * New option --all to archive all messages in a mailbox. Closes: #1764846. * Fixed a crash when archiving maildirs with --days=0. (Thanks John Goerzen) + * IMAP: automatically add NAMESPACE prefix to a mailbox path if necessary. Version 0.7.2 - 9 November 2007 diff --git a/archivemail.1 b/archivemail.1 index 69b0d48..90c3393 100644 --- a/archivemail.1 +++ b/archivemail.1 @@ -314,12 +314,11 @@ username or password. Note that quoting only a substring will not work, and be aware that your shell will probably remove unprotected quotes or backslashes. .PP -\fBIMAP\fR servers supporting subfolders may use any character as a -mailbox path separator, that is, as an equivalent to the slash character on Unix -systems. -If you are archiving an IMAP subfolder, first \fBarchivemail\fR will try -to open a given mailbox name unchanged; if this fails, it will interpret any -slashes in the URL as path separators and try again. +\fBarchivemail\fR tries to be smart when handling mailbox paths. +In particular, it will automatically add an IMAP NAMESPACE +prefix to the mailbox path if necessary; and if you are archiving a subfolder, +you can use the slash as a path separator instead of the IMAP server's +internal representation. .SH "EXAMPLES" .PP .PP diff --git a/archivemail.py b/archivemail.py index 45a0cdc..afd532a 100755 --- a/archivemail.py +++ b/archivemail.py @@ -1371,6 +1371,7 @@ def _archive_imap(mailbox_name, final_archive_name): user_error("imap server %s has login disabled (hint: " "try ssl/imaps)" % imap_server) + imap_folder = imap_find_mailbox(imap_srv, imap_folder) roflag = options.dry_run or options.copy_old_mail # Work around python bug #1277098 (still pending in python << 2.5) if not roflag: @@ -1379,25 +1380,10 @@ def _archive_imap(mailbox_name, final_archive_name): vprint("examining imap folder '%s' read-only" % imap_folder) else: vprint("selecting imap folder '%s'" % imap_folder) - # First try the given folder name, if this doesn't work, try to fix it. result, response = imap_srv.select(imap_folder, roflag) if result != 'OK': - errmsg = "cannot select imap folder; server says '%s'" % response[0] - if not os.path.sep in imap_folder: - unexpected_error(errmsg) - vprint("Selecting '%s' failed; server says: '%s'.\nTrying to " - "fix mailbox path..." % (imap_folder, response[0])) - delim = imap_getdelim(imap_srv) - if not delim: - unexpected_error(errmsg) - imap_folder = imap_folder.replace(os.path.sep, delim) - vprint("Selecting '%s'" % imap_folder) - result, response = imap_srv.select(imap_folder, roflag) - if result == 'OK': - vprint("successfully selected imap folder %s" % imap_folder) - else: - # Report original mailbox path. - unexpected_error(errmsg) + unexpected_error("selecting '%s' failed; server says: '%s'." \ + % (imap_folder, response[0])) # response is e.g. ['1016'] for 1016 messages in folder total_msg_count = int(response[0]) vprint("folder has %d message(s)" % total_msg_count) @@ -1549,6 +1535,77 @@ def imap_getdelim(imap_server): return delim +def imap_get_namespace(srv): + """Return the IMAP namespace prefixes and hierarchy delimiters.""" + assert('NAMESPACE' in srv.capabilities) + result, response = srv.namespace() + if result != 'OK': + unexpected_error("Cannot retrieve IMAP namespace; server says: '%s'" + % response[0]) + vprint("NAMESPACE response: %s" % repr(response[0])) + # Typical response is e.g. + # ['(("INBOX." ".")) NIL (("#shared." ".")("shared." "."))'] or + # ['(("" ".")) NIL NIL'], see RFC 2342. + # Make a reasonable guess parsing this beast. + ns = re.findall(r'\("([^"]*)" (?:"(.)"|NIL)', response[0]) + assert(ns) + return ns + + +def imap_find_mailbox(srv, mailbox): + """Find the given mailbox on the IMAP server, correcting an invalid + mailbox path if possible. Return the found mailbox name.""" + for curbox in imap_guess_mailboxnames(srv, mailbox): + vprint("Looking for mailbox '%s'..." % curbox) + result, response = srv.list(pattern=curbox) + if result != 'OK': + unexpected_error("LIST command failed; " \ + "server says: '%s'" % response[0]) + # Say we queried for the mailbox "foo". + # Upon success, response is e.g. ['(\\HasChildren) "." "foo"']. + # Upon failure, response is [None]. Funky imaplib! + if response[0] != None: + break + else: + user_error("Cannot find mailbox '%s' on server." % mailbox) + vprint("Found mailbox '%s'" % curbox) + # Catch \NoSelect here to avoid misleading errors later. + m = re.match(r'\((?P[^\)]*)\)', response[0]) + if '\\noselect' in m.group('attrs').lower().split(): + user_error("Server indicates that mailbox '%s' is not selectable" \ + % curbox) + return curbox + + +def imap_guess_mailboxnames(srv, mailbox): + """Return a list of possible real IMAP mailbox names in descending order + of preference, compiled by prepending an IMAP namespace prefix if necessary, + and by translating hierarchy delimiters.""" + if 'NAMESPACE' in srv.capabilities: + namespace_response = imap_get_namespace(srv) + for nsprefix, hdelim in namespace_response: + if mailbox.startswith(nsprefix): + mailbox = mailbox[len(nsprefix):] + break + else: + # mailbox doesn't start with a namespace prefix; + # choose private namespace, which is the first one. + nsprefix, hdelim = namespace_response[0] + else: + vprint("Server doesn't support NAMESPACE command.") + nsprefix = "" + hdelim = imap_getdelim(srv) + vprint("IMAP namespace prefix: '%s', hierarchy delimiter: '%s'" % \ + (nsprefix, hdelim)) + boxnames = [nsprefix + mailbox] + if os.path.sep in mailbox: + mailbox = mailbox.replace(os.path.sep, hdelim) + boxnames.append(mailbox) # could have a valid namespace prefix now + if nsprefix: + boxnames.append(nsprefix + mailbox) + return boxnames + + ############### misc functions ############### diff --git a/archivemail.sgml b/archivemail.sgml index dff716a..93db525 100644 --- a/archivemail.sgml +++ b/archivemail.sgml @@ -35,7 +35,7 @@ -15 March 2008 +8 April 2008 archivemail @@ -479,12 +479,11 @@ Note that quoting only a substring will not work, and be aware that your shell will probably remove unprotected quotes or backslashes. - -- cgit v1.2.3