Fallback to DATE header to correct messages without RECEIVED header as new option...
[imap-fix-internaldate] / src / mail_iterator.py
1 '''
2 mail_iterator.py - The module contains the MailIterator class.
3
4 Copyright (c) 2012 Intra2net AG
5 Author: Plamen Dimitrov
6
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 GNU General Public License for more details.
16 '''
17
18 import imaplib
19 import re
20 import time
21 import logging
22
23 MAILBOX_RESP = re.compile(r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
24 UIDVAL_RESP = re.compile(r'(?P<name>.*) \(UIDVALIDITY (?P<uidval>.*)\)')
25
26 class MailIterator:
27     """This class communicates with the e-mail server."""
28
29     # class attributes
30     # IMAP4_SSL for connection with an IMAP server
31     mail_con = None
32     # list of tuples (uidvalidity, mailboxname) for the retrieved mailboxes
33     mailboxes = None
34     # logged in status
35     logged_in = None
36
37     def __init__(self, server, username, password):
38         """Creates a connection and a user session."""
39         try:
40             self.mail_con = imaplib.IMAP4_SSL(server)
41             self.mail_con.login(username, password)
42             logging.info("Logged in as %s.", username)
43         except:
44             self.logged_in = False
45             raise UserWarning("Could not log in as user " + username + ".")
46         self.logged_in = True
47         
48         try:
49             result, self.mailboxes = self.mail_con.list()
50         except:
51             raise UserWarning("Could not retrieve mailboxes for user " + username + ".")
52
53     def __del__(self):
54         """Closes the connection and the user session."""
55         if(self.logged_in):
56             try:
57                 self.mail_con.close()
58                 self.mail_con.logout()
59             except:
60                 pass
61
62     def __iter__(self):
63         """Iterates through all mailboxes, returns (uidval,name)."""
64         for mailbox in self.mailboxes:
65             logging.debug("Checking mailbox %s.", mailbox)
66             mailbox = MAILBOX_RESP.match(mailbox.decode('iso-8859-1')).groups()
67             try:
68                 result, data = self.mail_con.status(mailbox[2], '(UIDVALIDITY)')
69             except:
70                 raise UserWarning("Could not retrieve mailbox uidvalidity.")
71             uidval = UIDVAL_RESP.match(data[0].decode('iso-8859-1')).groups()
72             logging.debug("Extracted mailbox info is %s %s.", data[0], uidval)
73             self.mail_con.select(mailbox[2])
74             yield (mailbox[2], uidval[1])
75
76     def fetch_messages(self):
77         """Fetches the messages from the current mailbox, return list of uids."""
78         try:
79             result, data = self.mail_con.uid('search', None, "ALL")
80         except:
81             raise UserWarning("Could not fetch messages.")
82         mailid_list = data[0].split()
83         return mailid_list
84
85     def fetch_internal_date(self, mid):
86         """Fetches the internal date of a message, returns a time tuple."""
87         try:
88             result, data = self.mail_con.uid('fetch', mid, '(INTERNALDATE)')
89         except:
90             raise UserWarning("Could not fetch the internal date of message" + mid.decode('iso-8859-1') + ".")
91         internal_date = imaplib.Internaldate2tuple(data[0])
92         return internal_date
93
94     def fetch_received_date(self, mid):
95         """Fetches the received date of a message, returns bytes reponse."""
96         try:
97             result, data = self.mail_con.uid('fetch', mid, '(BODY.PEEK[HEADER.FIELDS (RECEIVED)])')
98         except:
99             raise UserWarning("Could not fetch the received header of message" + mid.decode('iso-8859-1') + ".")
100         return data[0][1].decode('iso-8859-1')
101
102     def fetch_basic_date(self, mid):
103         """Fetches the basic date of a message, returns bytes reponse."""
104         try:
105             result, data = self.mail_con.uid('fetch', mid, '(BODY.PEEK[HEADER.FIELDS (DATE)])')
106         except:
107             raise UserWarning("Could not fetch the date header of message" + mid.decode('iso-8859-1') + ".")
108         return data[0][1].decode('iso-8859-1')        
109
110     def update_message(self, mid, mailbox, internal_date):
111         """Replaces a message with one with correct internal date."""
112         internal_date_seconds = time.mktime(internal_date.timetuple())
113         internal_date_str = imaplib.Time2Internaldate(internal_date_seconds)
114         try:
115             result, data = self.mail_con.uid('fetch', mid, '(RFC822)')
116             #logging.debug("Entire e-mail is: %s", data[0][1])
117
118             fetched_flags = self.mail_con.uid('fetch', mid, '(FLAGS)')[1][0]
119             parsed_flags = imaplib.ParseFlags(fetched_flags)
120             flags_str = " ".join(flag.decode('iso-8859-1') for flag in parsed_flags)
121             result, data = self.mail_con.append(mailbox, flags_str,
122                                                 internal_date_str, data[0][1])
123             logging.debug("Adding corrected copy of the message reponse: %s %s", result, data)
124         except:
125             raise UserWarning("Could not replace the e-mail" + mid.decode('iso-8859-1') + ".")
126         try:
127             result, data = self.mail_con.uid('STORE', mid, '+FLAGS', r'(\Deleted)')
128             logging.debug("Removing old copy of the message reponse: %s %s", result, data)
129         except:
130             raise UserWarning("Could not delete the e-mail" + mid.decode('iso-8859-1') + ".")
131         self.mail_con.expunge()
132         return