IMAP: correctly handle IMAP `literal' and unquoted `astring' in LIST reply
The LIST reply handling in imap_find_mailboxes() was buggy in several ways. It expected a non-NIL hierarchy delimiter, and it assumed that the mailbox name in a LIST reply is always quoted; but it can be an astring, a quoted string, or a literal (see RFC 3501 for the definitions of these tokens). These variants should now all be interpreted correctly. In addition, quoted mailbox names are now handled more strictly to the letter of the RFC; this only affects mailbox names containing a " or a backslash, though.
This commit is contained in:
parent
b7091e90ea
commit
eb8bc7a4ec
41
archivemail
41
archivemail
|
@ -1410,6 +1410,21 @@ def _archive_imap(mailbox_name):
|
||||||
|
|
||||||
############### IMAP functions ###############
|
############### IMAP functions ###############
|
||||||
|
|
||||||
|
def imap_quote(astring):
|
||||||
|
"""Quote an IMAP `astring' string (see RFC 3501, section "Formal Syntax")."""
|
||||||
|
if astring.startswith('"') and astring.endswith('"'):
|
||||||
|
quoted = astring
|
||||||
|
else:
|
||||||
|
quoted = '"' + astring.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
||||||
|
return quoted
|
||||||
|
|
||||||
|
def imap_unquote(quoted):
|
||||||
|
"""Un-quote a `quoted' IMAP string (see RFC 3501, section "Formal Syntax")."""
|
||||||
|
if not (quoted.startswith('"') and quoted.endswith('"')):
|
||||||
|
unquoted = quoted
|
||||||
|
else:
|
||||||
|
unquoted = re.sub(r'\\(\\|")', r'\1', quoted[1:-1])
|
||||||
|
return unquoted
|
||||||
|
|
||||||
def parse_imap_url(url):
|
def parse_imap_url(url):
|
||||||
"""Parse IMAP URL and return username, password (if appliciable), servername
|
"""Parse IMAP URL and return username, password (if appliciable), servername
|
||||||
|
@ -1462,7 +1477,7 @@ def imap_getdelim(imap_server):
|
||||||
"server says '%s'" % response[0])
|
"server says '%s'" % response[0])
|
||||||
|
|
||||||
# Response should be a list of strings like
|
# Response should be a list of strings like
|
||||||
# '(\\Noselect \\HasChildren) "." "boxname"'
|
# '(\\Noselect \\HasChildren) "." boxname'
|
||||||
# We parse only the first list item and just grab the delimiter.
|
# We parse only the first list item and just grab the delimiter.
|
||||||
m = re.match(r'\([^\)]*\) (?P<delim>"."|NIL)', response[0])
|
m = re.match(r'\([^\)]*\) (?P<delim>"."|NIL)', response[0])
|
||||||
if not m:
|
if not m:
|
||||||
|
@ -1505,7 +1520,7 @@ def imap_smart_select(srv, mailbox):
|
||||||
vprint("examining imap folder '%s' read-only" % mailbox)
|
vprint("examining imap folder '%s' read-only" % mailbox)
|
||||||
else:
|
else:
|
||||||
vprint("selecting imap folder '%s'" % mailbox)
|
vprint("selecting imap folder '%s'" % mailbox)
|
||||||
result, response = srv.select(mailbox, roflag)
|
result, response = srv.select(imap_quote(mailbox), roflag)
|
||||||
if result != 'OK':
|
if result != 'OK':
|
||||||
unexpected_error("selecting '%s' failed; server says: '%s'." \
|
unexpected_error("selecting '%s' failed; server says: '%s'." \
|
||||||
% (mailbox, response[0]))
|
% (mailbox, response[0]))
|
||||||
|
@ -1533,12 +1548,12 @@ def imap_find_mailboxes(srv, mailbox):
|
||||||
vprint("Looking for mailboxes matching '%s'..." % curbox)
|
vprint("Looking for mailboxes matching '%s'..." % curbox)
|
||||||
else:
|
else:
|
||||||
vprint("Looking for mailbox '%s'..." % curbox)
|
vprint("Looking for mailbox '%s'..." % curbox)
|
||||||
result, response = srv.list(pattern=curbox)
|
result, response = srv.list(pattern=imap_quote(curbox))
|
||||||
if result != 'OK':
|
if result != 'OK':
|
||||||
unexpected_error("LIST command failed; " \
|
unexpected_error("LIST command failed; " \
|
||||||
"server says: '%s'" % response[0])
|
"server says: '%s'" % response[0])
|
||||||
# Say we queried for the mailbox "foo".
|
# Say we queried for the mailbox "foo".
|
||||||
# Upon success, response is e.g. ['(\\HasChildren) "." "foo"'].
|
# Upon success, response is e.g. ['(\\HasChildren) "." foo'].
|
||||||
# Upon failure, response is [None]. Funky imaplib!
|
# Upon failure, response is [None]. Funky imaplib!
|
||||||
if response[0] != None:
|
if response[0] != None:
|
||||||
break
|
break
|
||||||
|
@ -1546,8 +1561,22 @@ def imap_find_mailboxes(srv, mailbox):
|
||||||
user_error("Cannot find mailbox '%s' on server." % mailbox)
|
user_error("Cannot find mailbox '%s' on server." % mailbox)
|
||||||
mailboxes = []
|
mailboxes = []
|
||||||
for mailbox_data in response:
|
for mailbox_data in response:
|
||||||
m = re.match(r'\((.*?)\) "." "(.*?)"', mailbox_data)
|
if not mailbox_data: # imaplib sometimes returns an empty string
|
||||||
attrs, name = m.groups()
|
continue
|
||||||
|
try:
|
||||||
|
m = re.match(r'\((.*?)\) (?:"."|NIL) (.+)', mailbox_data)
|
||||||
|
except TypeError:
|
||||||
|
# May be a literal. For literals, imaplib returns a tuple like
|
||||||
|
# ('(\\HasNoChildren) "." {12}', 'with "quote"').
|
||||||
|
m = re.match(r'\((.*?)\) (?:"."|NIL) \{\d+\}$', mailbox_data[0])
|
||||||
|
if m is None:
|
||||||
|
unexpected_error("cannot parse LIST reply %s" %
|
||||||
|
(mailbox_data,))
|
||||||
|
attrs = m.group(1)
|
||||||
|
name = mailbox_data[1]
|
||||||
|
else:
|
||||||
|
attrs, name = m.groups()
|
||||||
|
name = imap_unquote(name)
|
||||||
if '\\noselect' in attrs.lower().split():
|
if '\\noselect' in attrs.lower().split():
|
||||||
vprint("skipping not selectable mailbox '%s'" % name)
|
vprint("skipping not selectable mailbox '%s'" % name)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -617,6 +617,26 @@ class TestParseIMAPUrl(unittest.TestCase):
|
||||||
archivemail.options.verbose = False
|
archivemail.options.verbose = False
|
||||||
archivemail.options.pwfile = None
|
archivemail.options.pwfile = None
|
||||||
|
|
||||||
|
########## quoting and un-quoting of IMAP strings ##########
|
||||||
|
|
||||||
|
class TestIMAPQuoting(unittest.TestCase):
|
||||||
|
stringlist = (
|
||||||
|
('{braces} and space', '"{braces} and space"'),
|
||||||
|
('\\backslash', '"\\\\backslash"'),
|
||||||
|
('with "quotes" inbetween', '"with \\"quotes\\" inbetween"'),
|
||||||
|
('ending with "quotes"', '"ending with \\"quotes\\""'),
|
||||||
|
('\\"backslash before quote', '"\\\\\\"backslash before quote"')
|
||||||
|
)
|
||||||
|
|
||||||
|
def testQuote(self):
|
||||||
|
for unquoted, quoted in self.stringlist:
|
||||||
|
self.assertEqual(archivemail.imap_quote(unquoted), quoted)
|
||||||
|
|
||||||
|
def testUnquote(self):
|
||||||
|
for unquoted, quoted in self.stringlist:
|
||||||
|
self.assertEqual(unquoted, archivemail.imap_unquote(quoted))
|
||||||
|
|
||||||
|
|
||||||
########## acceptance testing ###########
|
########## acceptance testing ###########
|
||||||
|
|
||||||
class TestArchive(TestCaseInTempdir):
|
class TestArchive(TestCaseInTempdir):
|
||||||
|
|
Loading…
Reference in New Issue