From 6a4ea48e7fb38a671fbf406bd5ac536e22bb131d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Klein?= Date: Mon, 11 May 2015 18:55:55 +0200 Subject: [PATCH 1/2] Fix https://github.com/modoboa/modoboa-sievefilters/issues/4 append imaputils to modoboa main repository --- modoboa/core/app_settings.py | 20 +++ modoboa/lib/exceptions.py | 9 ++ modoboa/lib/fetch_parser.py | 157 ++++++++++++++++++ modoboa/lib/imaputils.py | 306 +++++++++++++++++++++++++++++++++++ 4 files changed, 492 insertions(+) create mode 100644 modoboa/lib/fetch_parser.py create mode 100644 modoboa/lib/imaputils.py diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 66f1d0c59..9a6378894 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -235,6 +235,26 @@ class GeneralParametersForm(parameters.AdminParametersForm): widget=forms.Select(attrs={"class": "form-control"}) ) + sep4 = SeparatorField(label=_("IMAP settings")) + + imap_server = forms.CharField( + label=_("Server address"), + initial="127.0.0.1", + help_text=_("Address of your IMAP server") + ) + + imap_secured = YesNoField( + label=_("Use a secured connection"), + initial="no", + help_text=_("Use a secured connection to access IMAP server") + ) + + imap_port = forms.IntegerField( + label=_("Server port"), + initial=143, + help_text=_("Listening port of your IMAP server") + ) + # Visibility rules visibility_rules = { "ldap_sep": "authentication_type=ldap", diff --git a/modoboa/lib/exceptions.py b/modoboa/lib/exceptions.py index 82c1135f0..aa2bb26c4 100644 --- a/modoboa/lib/exceptions.py +++ b/modoboa/lib/exceptions.py @@ -66,3 +66,12 @@ class PermDeniedException(ModoboaException): if self.msg: return _("Permission denied: %s" % self.msg) return _("Permission denied") + + +class ImapError(ModoboaException): + + def __init__(self, reason): + self.reason = reason + + def __str__(self): + return str(self.reason) diff --git a/modoboa/lib/fetch_parser.py b/modoboa/lib/fetch_parser.py new file mode 100644 index 000000000..61a765f02 --- /dev/null +++ b/modoboa/lib/fetch_parser.py @@ -0,0 +1,157 @@ +# coding: utf-8 +""" +:mod:`fetch_parser` --- Simple parser for FETCH responses +--------------------------------------------------------- + +The ``imaplib`` module doesn't parse IMAP responses, it returns raw +values. This is pretty annoying when one FETCH command is issued to +retrieve multiple attributes of a message. + +This simple module tries to fix that *problem*. +""" + + +class ParseError(Exception): + pass + + +class Token(object): + + def __init__(self, value): + self.value = value + + +class Literal(Token): + + def __init__(self, value): + super(Literal, self).__init__(value) + self.next_token_len = int(self.value[1:-1]) + + +def parse_next_token(buf): + """Look for the next *token* + + By *token*, I mean: *literal*, *quoted* or anything else until the + next ' ' or ')' character (number, NIL and others should fall into + this last category). + + :param buf: the buffer to parse + :return: the position of the token's last caracter into ``buf`` + """ + end = -1 + klass = Token + if buf[0] == '{': + # Literal + end = buf.find('}') + klass = Literal + elif buf[0] == '"': + # quoted + end = buf.find('"', 1) + else: + for pos, c in enumerate(buf): + if c in [' ', ')']: + end = pos - 1 + break + if end == -1: + raise ParseError( + "End of buffer reached while looking for a token end") + end += 1 + token = klass(buf[:end]) + return token, end + + +def parse_fetch_response(data): + """Parse a FETCH response, previously issued by a UID command + + We extract the message number, the UID and its value and consider + what remains as the response data. + + :param data: the data returned by the ``imaplib`` command + :return: a dictionnary + """ + result = {} + cpt = 0 + while cpt < len(data): + content = () + while cpt < len(data) and data[cpt] != ')': + if isinstance(data[cpt], str): + # FIXME : probably an unsolicited response + cpt += 1 + continue + content += data[cpt] + cpt += 1 + cpt += 1 + + buf = "".join(content) + parts = buf.split(' ', 3) + msgdef = result[int(parts[2])] = {} + response = parts[3] + + while len(response): + if response.startswith('BODY') and response[4] == '[': + end = response.find(']', 5) + if response[end + 1] == '<': + end = response.find('>', end + 1) + end += 1 + else: + end = response.find(' ') + cmdname = response[:end] + response = response[end + 1:] + + end = 0 + if cmdname in ['BODY', 'BODYSTRUCTURE', 'FLAGS']: + parendepth = 0 + instring = False + for pos, c in enumerate(response): + if not instring and c == '"': + instring = True + continue + if instring and c == '"': + if pos and response[pos - 1] != '\\': + instring = False + continue + if not instring and c == '(': + parendepth += 1 + continue + if not instring and c == ')': + parendepth -= 1 + if parendepth == 0: + end = pos + 1 + break + else: + token, end = parse_next_token(response) + if isinstance(token, Literal): + response = response[end:] + end = token.next_token_len + + msgdef[cmdname] = response[:end] + response = response[end + 1:] + try: + func = globals()["parse_%s" % cmdname.lower()] + msgdef[cmdname] = func(msgdef[cmdname])[0] + except KeyError: + pass + + return result + + +if __name__ == "__main__": +# resp = [('855 (UID 46931 BODYSTRUCTURE ((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 886 32 NIL NIL NIL NIL)("text" "html" ("charset" "us-ascii") NIL NIL "quoted-printable" 1208 16 NIL NIL NIL NIL) "alternative" ("boundary" "----=_NextPart_001_0003_01CCC564.B2F64FF0") NIL NIL NIL)("application" "octet-stream" ("name" "Carte Verte_2.pdf") NIL NIL "base64" 285610 NIL ("attachment" ("filename" "Carte Verte_2.pdf")) NIL NIL) "mixed" ("boundary" "----=_NextPart_000_0002_01CCC564.B2F64FF0") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {153}', 'From: \r\nTo: \r\nCc: \r\nSubject: Notre contact du 28/12/2011 - 192175092\r\nDate: Wed, 28 Dec 2011 13:29:17 +0100\r\n\r\n'), ')'] + +# resp = [('856 (UID 46936 BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 724 22 NIL NIL NIL NIL)("text" "html" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 2662 48 NIL NIL NIL NIL) "alternative" ("boundary" "----=_Part_1326887_254624357.1325083973970") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {258}', 'Date: Wed, 28 Dec 2011 15:52:53 +0100 (CET)\r\nFrom: =?ISO-8859-1?Q?Malakoff_M=E9d=E9ric?= \r\nTo: Antoine Nguyen \r\nSubject: =?ISO-8859-1?Q?Votre_inscription_au_grand_Jeu_Malakoff_M=E9d=E9ric?=\r\n\r\n'), ')'] + + resp = [ + ('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)', + ), + ')'] + +# resp = [('19 (UID 19 FLAGS (\\Seen) BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1" "format" "flowed") NIL NIL "7bit" 2 1 NIL NIL NIL NIL)("message" "rfc822" ("name*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74") NIL NIL "8bit" 3632 ("Wed, 13 Dec 2006 20:30:02 +0100" {70}', +# "[INSCRIPTION] R\xe9c\xe9ption de votre dossier d'inscription Free Haut D\xe9bit"), +# (' (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) ((NIL NIL "hautdebit" "freetelecom.fr")) ((NIL NIL "nguyen.antoine" "wanadoo.fr")) NIL NIL NIL "<20061213193125.9DA0919AC@dgroup2-2.proxad.net>") ("text" "plain" ("charset" "iso-8859-1") NIL NIL "8bit" 1428 38 NIL ("inline" NIL) NIL NIL) 76 NIL ("inline" ("filename*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74")) NIL NIL) "mixed" ("boundary" "------------040706080908000209030901") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {266}', +# 'Date: Tue, 19 Dec 2006 19:50:13 +0100\r\nFrom: Antoine Nguyen \r\nTo: Antoine Nguyen \r\nSubject: [Fwd: [INSCRIPTION] =?ISO-8859-1?Q?R=E9c=E9ption_de_votre_?=\r\n =?ISO-8859-1?Q?dossier_d=27inscription_Free_Haut_D=E9bit=5D?=\r\n\r\n'), ')'] + + # resp = [('123 (UID 3 BODYSTRUCTURE (((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1266 30 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 8830 227 NIL NIL NIL NIL) "alternative" ("boundary" "_000_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL NIL NIL)("image" "png" ("name" "image005.png") "" "image005.png" "base64" 7464 NIL ("inline" ("filename" "image005.png" "size" "5453" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL)("image" "jpeg" ("name" "image006.jpg") "" "image006.jpg" "base64" 2492 NIL ("inline" ("filename" "image006.jpg" "size" "1819" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL) "related" ("boundary" "_006_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_" "type" "multipart/alternative") NIL NIL NIL)("application" "pdf" ("name" "bilan assurance CIC.PDF") NIL "bilan assurance CIC.PDF" "base64" 459532 NIL ("attachment" ("filename" "bilan assurance CIC.PDF" "size" "335811" "creation-date" "Fri, 16 Sep 2011 12:45:23 GMT" "modification-date" "Fri, 16 Sep 2011 12:45:23 GMT")) NIL NIL)(("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 1389 29 NIL NIL NIL NIL)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 1457 27 NIL NIL NIL NIL) "alternative" ("boundary" "===============0775904800==") ("inline" NIL) NIL NIL) "mixed" ("boundary" "_007_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL ("fr-FR") NIL)',), ')'] + + # resp = [('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "(26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)',), ')'] + + print parse_fetch_response(resp) diff --git a/modoboa/lib/imaputils.py b/modoboa/lib/imaputils.py new file mode 100644 index 000000000..c46bbd7a4 --- /dev/null +++ b/modoboa/lib/imaputils.py @@ -0,0 +1,306 @@ +# coding: utf-8 +""" +:mod:`imaputils` --- Extra IMAPv4 utilities +------------------------------------------- +""" +import imaplib +import re +import ssl +from functools import wraps + +from django.utils.translation import ugettext as _ + +from modoboa.lib import parameters, imap_utf7 # NOQA +from modoboa.lib.connections import ConnectionsManager +from modoboa.lib.exceptions import InternalError + +from .exceptions import ImapError +from .fetch_parser import parse_fetch_response + +# imaplib.Debug = 4 + + +class capability(object): + + """ + Simple decorator to check if the server presents the required + capability. If not, a fallback method is called instead. + + :param name: the capability name (upper case) + :param fallback_method: a method's name + """ + + def __init__(self, name, fallback_method): + self.name = name + self.fallback_method = fallback_method + + def __call__(self, method): + @wraps(method) + def wrapped_func(cls, *args, **kwargs): + if self.name in cls.capabilities: + return method(cls, *args, **kwargs) + return getattr(cls, self.fallback_method)(cls, **kwargs) + + return wrapped_func + + +class IMAPconnector(object): + + """The IMAPv4 connector.""" + + __metaclass__ = ConnectionsManager + + list_base_pattern = ( + r'\((?P.*?)\) "(?P.*)" "?(?P[^"]*)"?' + ) + list_response_pattern_literal = re.compile( + r'\((?P.*?)\) "(?P.*)" \{(?P\d+)\}') + list_response_pattern = re.compile(list_base_pattern) + listextended_response_pattern = \ + re.compile(list_base_pattern + r'\s*(?P.*)') + + def __init__(self, user=None, password=None): + self.__hdelimiter = None + self.address = parameters.get_admin("IMAP_SERVER", "core") + self.port = int(parameters.get_admin("IMAP_PORT", "core")) + self.login(user, password) + + def _cmd(self, name, *args, **kwargs): + """IMAP command wrapper + + To simplify errors handling, this wrapper calls the + appropriate method (``uid`` or FIXME) and then check the + return code. If an error has occured, an ``ImapError`` + exception is raised. + + For specific commands commands (FETCH, ...), the result is + parsed using the IMAPclient module before being returned. + + :param name: the command's name + :return: the command's result + """ + if name in ['FETCH', 'SORT', 'STORE', 'COPY', 'SEARCH']: + try: + typ, data = self.m.uid(name, *args) + except imaplib.IMAP4.error as e: + raise ImapError(e) + if typ == "NO": + raise ImapError(data) + if name == 'FETCH': + return parse_fetch_response(data) + return data + + try: + typ, data = self.m._simple_command(name, *args) + except imaplib.IMAP4.error as e: + raise ImapError(e) + if typ == "NO": + raise ImapError(data) + if 'responses' not in kwargs: + if name not in self.m.untagged_responses: + return None + return self.m.untagged_responses.pop(name) + res = [] + for r in kwargs['responses']: + if r not in self.m.untagged_responses: + return None + res.append(self.m.untagged_responses.pop(r)) + return res + + @property + def hdelimiter(self): + """Return the default hierachy delimiter. + + This is a simple way to retrieve the default delimiter (see + http://www.imapwiki.org/ClientImplementation/MailboxList). + + :return: a string + """ + if self.__hdelimiter is None: + data = self._cmd("LIST", "", "") + m = self.list_response_pattern.match(data[0]) + if m is None: + raise InternalError(_("Failed to retrieve hierarchy delimiter")) + self.__hdelimiter = m.group('delimiter') + return self.__hdelimiter + + def refresh(self, user, password): + """Check if current connection needs a refresh + + Is it really secure? + """ + if self.m is not None: + try: + self._cmd("NOOP") + except ImapError: + if hasattr(self, "current_mailbox"): + del self.current_mailbox + else: + return + + self.login(user, password) + + def login(self, user, passwd): + """Custom login method + + We connect to the server, issue a LOGIN command. If + successfull, we try to record a eventuel CAPABILITY untagged + response. Otherwise, we issue the command. + + :param user: username + :param passwd: password + """ + import socket + + if isinstance(user, unicode): + user = user.encode("utf-8") + if isinstance(passwd, unicode): + passwd = passwd.encode("utf-8") + try: + secured = parameters.get_admin("IMAP_SECURED", "core") + if secured == "yes": + self.m = imaplib.IMAP4_SSL(self.address, self.port) + else: + self.m = imaplib.IMAP4(self.address, self.port) + except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error: + raise ImapError(_("Connection to IMAP server failed: %s" % error)) + + data = self._cmd("LOGIN", user, passwd) + self.m.state = "AUTH" + if "CAPABILITY" in self.m.untagged_responses: + self.capabilities = \ + self.m.untagged_responses.pop('CAPABILITY')[0].split() + else: + data = self._cmd("CAPABILITY") + self.capabilities = data[0].split() + + def logout(self): + """Logout from server.""" + try: + self._cmd("CHECK") + except ImapError: + pass + self._cmd("LOGOUT") + del self.m + self.m = None + if hasattr(self, "current_mailbox"): + del self.current_mailbox + + def _listmboxes_simple(self, topmailbox='INBOX', mailboxes=None, **kwargs): + # data = self._cmd("LIST", "", "*") + if not mailboxes: + mailboxes = [] + (status, data) = self.m.list() + newmboxes = [] + for mb in data: + flags, delimiter, name = self.list_response_pattern.match( + mb).groups() + name = name.strip('"').decode("imap4-utf-7") + mdm_found = False + for idx, mdm in enumerate(mailboxes): + if mdm["name"] == name: + mdm_found = True + descr = mailboxes[idx] + break + if not mdm_found: + descr = dict(name=name) + newmboxes += [descr] + + if re.search("\%s" % delimiter, name): + parts = name.split(delimiter) + if "path" not in descr: + descr["path"] = parts[0] + descr["sub"] = [] + if self._parse_mailbox_name(descr["sub"], parts[0], delimiter, + parts[1:]): + descr["class"] = "subfolders" + continue + + from operator import itemgetter + mailboxes += sorted(newmboxes, key=itemgetter("name")) + + @capability('LIST-EXTENDED', '_listmboxes_simple') + def _listmboxes(self, topmailbox, mailboxes): + """Retrieve mailboxes list.""" + pattern = ( + "{0}{1}%".format(topmailbox.encode("imap4-utf-7"), self.hdelimiter) + if topmailbox else "%" + ) + resp = self._cmd("LIST", "", pattern, "RETURN", "(CHILDREN)") + newmboxes = [] + for mb in resp: + if not mb: + continue + if type(mb) in [str, unicode]: + flags, delimiter, name, childinfo = \ + self.listextended_response_pattern.match(mb).groups() + else: + flags, delimiter, namelen = ( + self.list_response_pattern_literal.match(mb[0]).groups() + ) + name = mb[1][0:int(namelen)] + flags = flags.split(' ') + name = name.decode("imap4-utf-7") + mdm_found = False + for idx, mdm in enumerate(mailboxes): + if mdm["name"] == name: + mdm_found = True + descr = mailboxes[idx] + break + if not mdm_found: + descr = dict(name=name) + newmboxes += [descr] + + if r'\Marked' in flags or r'\UnMarked' not in flags: + descr["send_status"] = True + if r'\HasChildren' in flags: + if r'\NonExistent' in flags: + descr["removed"] = True + descr["path"] = name + descr["sub"] = [] + + from operator import itemgetter + mailboxes += sorted(newmboxes, key=itemgetter("name")) + + def getmboxes( + self, user, topmailbox=''): + """Returns a list of mailboxes for a particular user + + By default, only the first level of mailboxes under + ``topmailbox`` is returned. + + :param user: a ``User`` instance + :param topmailbox: the mailbox where to start in the tree + :return: a list + """ + if topmailbox: + md_mailboxes = [] + else: + md_mailboxes = [ + {"name": "INBOX", "class": "fa fa-inbox"}, + {"name": parameters.get_user(user, "DRAFTS_FOLDER"), + "class": "fa fa-file"}, + {"name": 'Junk', "class": "fa fa-fire"}, + {"name": parameters.get_user(user, "SENT_FOLDER"), + "class": "fa fa-envelope"}, + {"name": parameters.get_user(user, "TRASH_FOLDER"), + "class": "fa fa-trash"} + ] + self._listmboxes(topmailbox, md_mailboxes) + + return md_mailboxes + + +def get_imapconnector(request): + """Simple shortcut to create a connector + + :param request: a ``Request`` object + """ + imapc = IMAPconnector(user=request.user.username, + password=request.session["password"]) + return imapc + + +if __name__ == "__main__": + import doctest + doctest.testmod() From 4be6ed6747666cd680eada6704c43c3309b4bc20 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 18 May 2015 11:27:59 +0200 Subject: [PATCH 2/2] Cleanup. --- modoboa/core/app_settings.py | 20 ----- modoboa/lib/fetch_parser.py | 157 ----------------------------------- modoboa/lib/imaputils.py | 6 +- 3 files changed, 1 insertion(+), 182 deletions(-) delete mode 100644 modoboa/lib/fetch_parser.py diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 9a6378894..66f1d0c59 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -235,26 +235,6 @@ class GeneralParametersForm(parameters.AdminParametersForm): widget=forms.Select(attrs={"class": "form-control"}) ) - sep4 = SeparatorField(label=_("IMAP settings")) - - imap_server = forms.CharField( - label=_("Server address"), - initial="127.0.0.1", - help_text=_("Address of your IMAP server") - ) - - imap_secured = YesNoField( - label=_("Use a secured connection"), - initial="no", - help_text=_("Use a secured connection to access IMAP server") - ) - - imap_port = forms.IntegerField( - label=_("Server port"), - initial=143, - help_text=_("Listening port of your IMAP server") - ) - # Visibility rules visibility_rules = { "ldap_sep": "authentication_type=ldap", diff --git a/modoboa/lib/fetch_parser.py b/modoboa/lib/fetch_parser.py deleted file mode 100644 index 61a765f02..000000000 --- a/modoboa/lib/fetch_parser.py +++ /dev/null @@ -1,157 +0,0 @@ -# coding: utf-8 -""" -:mod:`fetch_parser` --- Simple parser for FETCH responses ---------------------------------------------------------- - -The ``imaplib`` module doesn't parse IMAP responses, it returns raw -values. This is pretty annoying when one FETCH command is issued to -retrieve multiple attributes of a message. - -This simple module tries to fix that *problem*. -""" - - -class ParseError(Exception): - pass - - -class Token(object): - - def __init__(self, value): - self.value = value - - -class Literal(Token): - - def __init__(self, value): - super(Literal, self).__init__(value) - self.next_token_len = int(self.value[1:-1]) - - -def parse_next_token(buf): - """Look for the next *token* - - By *token*, I mean: *literal*, *quoted* or anything else until the - next ' ' or ')' character (number, NIL and others should fall into - this last category). - - :param buf: the buffer to parse - :return: the position of the token's last caracter into ``buf`` - """ - end = -1 - klass = Token - if buf[0] == '{': - # Literal - end = buf.find('}') - klass = Literal - elif buf[0] == '"': - # quoted - end = buf.find('"', 1) - else: - for pos, c in enumerate(buf): - if c in [' ', ')']: - end = pos - 1 - break - if end == -1: - raise ParseError( - "End of buffer reached while looking for a token end") - end += 1 - token = klass(buf[:end]) - return token, end - - -def parse_fetch_response(data): - """Parse a FETCH response, previously issued by a UID command - - We extract the message number, the UID and its value and consider - what remains as the response data. - - :param data: the data returned by the ``imaplib`` command - :return: a dictionnary - """ - result = {} - cpt = 0 - while cpt < len(data): - content = () - while cpt < len(data) and data[cpt] != ')': - if isinstance(data[cpt], str): - # FIXME : probably an unsolicited response - cpt += 1 - continue - content += data[cpt] - cpt += 1 - cpt += 1 - - buf = "".join(content) - parts = buf.split(' ', 3) - msgdef = result[int(parts[2])] = {} - response = parts[3] - - while len(response): - if response.startswith('BODY') and response[4] == '[': - end = response.find(']', 5) - if response[end + 1] == '<': - end = response.find('>', end + 1) - end += 1 - else: - end = response.find(' ') - cmdname = response[:end] - response = response[end + 1:] - - end = 0 - if cmdname in ['BODY', 'BODYSTRUCTURE', 'FLAGS']: - parendepth = 0 - instring = False - for pos, c in enumerate(response): - if not instring and c == '"': - instring = True - continue - if instring and c == '"': - if pos and response[pos - 1] != '\\': - instring = False - continue - if not instring and c == '(': - parendepth += 1 - continue - if not instring and c == ')': - parendepth -= 1 - if parendepth == 0: - end = pos + 1 - break - else: - token, end = parse_next_token(response) - if isinstance(token, Literal): - response = response[end:] - end = token.next_token_len - - msgdef[cmdname] = response[:end] - response = response[end + 1:] - try: - func = globals()["parse_%s" % cmdname.lower()] - msgdef[cmdname] = func(msgdef[cmdname])[0] - except KeyError: - pass - - return result - - -if __name__ == "__main__": -# resp = [('855 (UID 46931 BODYSTRUCTURE ((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 886 32 NIL NIL NIL NIL)("text" "html" ("charset" "us-ascii") NIL NIL "quoted-printable" 1208 16 NIL NIL NIL NIL) "alternative" ("boundary" "----=_NextPart_001_0003_01CCC564.B2F64FF0") NIL NIL NIL)("application" "octet-stream" ("name" "Carte Verte_2.pdf") NIL NIL "base64" 285610 NIL ("attachment" ("filename" "Carte Verte_2.pdf")) NIL NIL) "mixed" ("boundary" "----=_NextPart_000_0002_01CCC564.B2F64FF0") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {153}', 'From: \r\nTo: \r\nCc: \r\nSubject: Notre contact du 28/12/2011 - 192175092\r\nDate: Wed, 28 Dec 2011 13:29:17 +0100\r\n\r\n'), ')'] - -# resp = [('856 (UID 46936 BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 724 22 NIL NIL NIL NIL)("text" "html" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 2662 48 NIL NIL NIL NIL) "alternative" ("boundary" "----=_Part_1326887_254624357.1325083973970") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {258}', 'Date: Wed, 28 Dec 2011 15:52:53 +0100 (CET)\r\nFrom: =?ISO-8859-1?Q?Malakoff_M=E9d=E9ric?= \r\nTo: Antoine Nguyen \r\nSubject: =?ISO-8859-1?Q?Votre_inscription_au_grand_Jeu_Malakoff_M=E9d=E9ric?=\r\n\r\n'), ')'] - - resp = [ - ('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)', - ), - ')'] - -# resp = [('19 (UID 19 FLAGS (\\Seen) BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1" "format" "flowed") NIL NIL "7bit" 2 1 NIL NIL NIL NIL)("message" "rfc822" ("name*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74") NIL NIL "8bit" 3632 ("Wed, 13 Dec 2006 20:30:02 +0100" {70}', -# "[INSCRIPTION] R\xe9c\xe9ption de votre dossier d'inscription Free Haut D\xe9bit"), -# (' (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) ((NIL NIL "hautdebit" "freetelecom.fr")) ((NIL NIL "nguyen.antoine" "wanadoo.fr")) NIL NIL NIL "<20061213193125.9DA0919AC@dgroup2-2.proxad.net>") ("text" "plain" ("charset" "iso-8859-1") NIL NIL "8bit" 1428 38 NIL ("inline" NIL) NIL NIL) 76 NIL ("inline" ("filename*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74")) NIL NIL) "mixed" ("boundary" "------------040706080908000209030901") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {266}', -# 'Date: Tue, 19 Dec 2006 19:50:13 +0100\r\nFrom: Antoine Nguyen \r\nTo: Antoine Nguyen \r\nSubject: [Fwd: [INSCRIPTION] =?ISO-8859-1?Q?R=E9c=E9ption_de_votre_?=\r\n =?ISO-8859-1?Q?dossier_d=27inscription_Free_Haut_D=E9bit=5D?=\r\n\r\n'), ')'] - - # resp = [('123 (UID 3 BODYSTRUCTURE (((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1266 30 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 8830 227 NIL NIL NIL NIL) "alternative" ("boundary" "_000_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL NIL NIL)("image" "png" ("name" "image005.png") "" "image005.png" "base64" 7464 NIL ("inline" ("filename" "image005.png" "size" "5453" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL)("image" "jpeg" ("name" "image006.jpg") "" "image006.jpg" "base64" 2492 NIL ("inline" ("filename" "image006.jpg" "size" "1819" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL) "related" ("boundary" "_006_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_" "type" "multipart/alternative") NIL NIL NIL)("application" "pdf" ("name" "bilan assurance CIC.PDF") NIL "bilan assurance CIC.PDF" "base64" 459532 NIL ("attachment" ("filename" "bilan assurance CIC.PDF" "size" "335811" "creation-date" "Fri, 16 Sep 2011 12:45:23 GMT" "modification-date" "Fri, 16 Sep 2011 12:45:23 GMT")) NIL NIL)(("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 1389 29 NIL NIL NIL NIL)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 1457 27 NIL NIL NIL NIL) "alternative" ("boundary" "===============0775904800==") ("inline" NIL) NIL NIL) "mixed" ("boundary" "_007_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL ("fr-FR") NIL)',), ')'] - - # resp = [('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "(26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)',), ')'] - - print parse_fetch_response(resp) diff --git a/modoboa/lib/imaputils.py b/modoboa/lib/imaputils.py index c46bbd7a4..14e19bd63 100644 --- a/modoboa/lib/imaputils.py +++ b/modoboa/lib/imaputils.py @@ -15,7 +15,6 @@ from modoboa.lib.connections import ConnectionsManager from modoboa.lib.exceptions import InternalError from .exceptions import ImapError -from .fetch_parser import parse_fetch_response # imaplib.Debug = 4 @@ -86,8 +85,6 @@ class IMAPconnector(object): raise ImapError(e) if typ == "NO": raise ImapError(data) - if name == 'FETCH': - return parse_fetch_response(data) return data try: @@ -262,8 +259,7 @@ class IMAPconnector(object): from operator import itemgetter mailboxes += sorted(newmboxes, key=itemgetter("name")) - def getmboxes( - self, user, topmailbox=''): + def getmboxes(self, user, topmailbox=""): """Returns a list of mailboxes for a particular user By default, only the first level of mailboxes under