''' mail_iterator.py - The module contains the MailIterator class. Copyright (c) 2012 Intra2net AG Author: Plamen Dimitrov and Thomas Jarosch This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ''' import imaplib import re import time import logging MAILBOX_RESP = re.compile(r'\((?P.*?)\) "(?P.*)" (?P.*)') UIDVAL_RESP = re.compile(r'(?P.*) \(UIDVALIDITY (?P.*)\)') #imaplib.Debug = 4 class MailIterator: """This class communicates with the e-mail server.""" # class attributes # IMAP4_SSL for connection with an IMAP server mail_con = None # list of tuples (uidvalidity, mailboxname) for the retrieved mailboxes mailboxes = None # logged in status logged_in = None # skip shared folders skip_shared_folders = None def __init__(self, server, username, password, skip_shared_folders = False): """Creates a connection and a user session.""" self.skip_shared_folders = skip_shared_folders # connect to server try: self.mail_con = imaplib.IMAP4_SSL(server) except Exception as ex: raise UserWarning("Could not connect to host %s: %s" % (server, ex)) # log in try: self.mail_con.login(username, password) logging.info("Logged in as %s.", username) except: self.logged_in = False raise UserWarning("Could not log in as user " + username) self.logged_in = True # list mailboxes try: _result, self.mailboxes = self.mail_con.list() except (self.mail_con.error): raise UserWarning("Could not retrieve mailboxes for user " + username) def __del__(self): """Closes the connection and the user session.""" if(self.logged_in): self.mail_con.close() self.mail_con.logout() def __iter__(self): """Iterates through all mailboxes, returns (uidval,name).""" for mailbox in self.mailboxes: logging.debug("Checking mailbox %s.", mailbox) mailbox = MAILBOX_RESP.match(mailbox.decode('iso-8859-1')).groups() # detect if mailbox is shared and if skip flag is set iterate further if(self.skip_shared_folders and mailbox[2].split(mailbox[1])[0] == '"user'): logging.info("Mailbox %s is shared and therefore skipped.", mailbox[2]) continue # retrieve uidvalidity try: # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('STATUS') _result, data = self.mail_con.status(mailbox[2], '(UIDVALIDITY)') except (self.mail_con.error): raise UserWarning("Could not retrieve mailbox uidvalidity.") uidval = UIDVAL_RESP.match(data[0].decode('iso-8859-1')).groups() logging.debug("Extracted mailbox info is %s %s.", data[0], uidval) # select mailbox if writable try: self.mail_con.select(mailbox[2]) except self.mail_con.readonly: logging.warning("Mailbox %s is not writable and therefore skipped.", mailbox[2]) continue yield (mailbox[2], uidval[1]) def fetch_messages(self): """Fetches the messages from the current mailbox, return list of uids.""" try: # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('SEARCH') _result, data = self.mail_con.uid('search', None, "ALL") except (self.mail_con.error): raise UserWarning("Could not fetch messages.") mailid_list = data[0].split() return mailid_list def fetch_internal_date(self, mid): """Fetches the internal date of a message, returns a time tuple.""" try: # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('FETCH') _result, data = self.mail_con.uid('fetch', mid, '(INTERNALDATE)') except (self.mail_con.error): raise UserWarning("Could not fetch the internal date of message " + mid.decode('iso-8859-1') + ".") internal_date = imaplib.Internaldate2tuple(data[0]) return internal_date def fetch_received_date(self, mid): """Fetches the received date of a message, returns bytes reponse.""" try: # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('FETCH') _result, data = self.mail_con.uid('fetch', mid, '(BODY.PEEK[HEADER.FIELDS (RECEIVED)])') except (self.mail_con.error): raise UserWarning("Could not fetch the received header of message " + mid.decode('iso-8859-1') + ".") return data[0][1].decode('iso-8859-1') def fetch_basic_date(self, mid): """Fetches the basic date of a message, returns bytes reponse.""" try: # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('FETCH') _result, data = self.mail_con.uid('fetch', mid, '(BODY.PEEK[HEADER.FIELDS (DATE)])') except (self.mail_con.error): raise UserWarning("Could not fetch the date header of message " + mid.decode('iso-8859-1') + ".") return data[0][1].decode('iso-8859-1') def update_message(self, mid, mailbox, internal_date): """Replaces a message with one with correct internal date.""" internal_date_sec = time.mktime(internal_date.timetuple()) try: # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('FETCH') result, data = self.mail_con.uid('fetch', mid, '(RFC822)') #logging.debug("Entire e-mail is: %s", data[0][1]) # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('FETCH') # retrieve and select flags to upload fetched_flags = self.mail_con.uid('fetch', mid, '(FLAGS)')[1][0] parsed_flags = imaplib.ParseFlags(fetched_flags) selected_flags = () for flag in parsed_flags: if(flag != b'\\Recent'): selected_flags += (flag,) logging.debug("Selected flags %s from parsed flags %s.", selected_flags, parsed_flags) flags_str = " ".join(flag.decode('iso-8859-1') for flag in selected_flags) # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('APPEND') # upload message copy and delete old one result, data = self.mail_con.append(mailbox, flags_str, internal_date_sec, data[0][1]) logging.debug("Adding corrected copy of the message reponse: %s %s", result, data) except (self.mail_con.error): raise UserWarning("Could not replace the e-mail " + mid.decode('iso-8859-1') + ".") try: # Work around unsolicited server responses in imaplib by clearing them self.mail_con.response('STORE') result, data = self.mail_con.uid('STORE', mid, '+FLAGS', r'(\Deleted)') logging.debug("Removing old copy of the message reponse: %s %s", result, data) except (self.mail_con.error): raise UserWarning("Could not delete the e-mail " + mid.decode('iso-8859-1') + ".") self.mail_con.expunge() return