Poor man's typo fix
[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 #imaplib.Debug = 4
27
28 class MailIterator:
29     """This class communicates with the e-mail server."""
30
31     # class attributes
32     # IMAP4_SSL for connection with an IMAP server
33     mail_con = None
34     # list of tuples (uidvalidity, mailboxname) for the retrieved mailboxes
35     mailboxes = None
36     # logged in status
37     logged_in = None
38     # skip shared folders
39     skip_shared_folders = None
40
41     def __init__(self, server, username, password, skip_shared_folders = False):
42         """Creates a connection and a user session."""
43         try:
44             self.mail_con = imaplib.IMAP4_SSL(server)
45             self.mail_con.login(username, password)
46             logging.info("Logged in as %s.", username)
47         except:
48             self.logged_in = False
49             raise UserWarning("Could not log in as user " + username + ".")
50         self.logged_in = True
51         try:
52             result, self.mailboxes = self.mail_con.list()
53         except:
54             raise UserWarning("Could not retrieve mailboxes for user " + username + ".")
55         self.skip_shared_folders = skip_shared_folders
56
57     def __del__(self):
58         """Closes the connection and the user session."""
59         if(self.logged_in):
60             try:
61                 self.mail_con.close()
62                 self.mail_con.logout()
63             except:
64                 pass
65
66     def __iter__(self):
67         """Iterates through all mailboxes, returns (uidval,name)."""
68         for mailbox in self.mailboxes:
69             logging.debug("Checking mailbox %s.", mailbox)
70             mailbox = MAILBOX_RESP.match(mailbox.decode('iso-8859-1')).groups()
71             # detect if mailbox is shared and if skip flag is set iterate further
72             if(self.skip_shared_folders and mailbox[2].split(mailbox[1])[0] == '"user'):
73                 logging.info("Mailbox %s is shared and therefore skipped.", mailbox[2])
74                 continue
75             # retrieve uidvalidity
76             try:
77                 result, data = self.mail_con.status(mailbox[2], '(UIDVALIDITY)')
78             except:
79                 raise UserWarning("Could not retrieve mailbox uidvalidity.")
80             uidval = UIDVAL_RESP.match(data[0].decode('iso-8859-1')).groups()
81             logging.debug("Extracted mailbox info is %s %s.", data[0], uidval)
82             # select mailbox if writable
83             try:
84                 self.mail_con.select(mailbox[2])
85             except self.mail_con.readonly:
86                 logging.warning("Mailbox %s is not writable and therefore skipped.", mailbox[2])               
87                 continue
88             yield (mailbox[2], uidval[1])
89
90     def fetch_messages(self):
91         """Fetches the messages from the current mailbox, return list of uids."""
92         try:
93             result, data = self.mail_con.uid('search', None, "ALL")
94         except:
95             raise UserWarning("Could not fetch messages.")
96         mailid_list = data[0].split()
97         return mailid_list
98
99     def fetch_internal_date(self, mid):
100         """Fetches the internal date of a message, returns a time tuple."""
101         try:
102             result, data = self.mail_con.uid('fetch', mid, '(INTERNALDATE)')
103         except:
104             raise UserWarning("Could not fetch the internal date of message " + mid.decode('iso-8859-1') + ".")
105         internal_date = imaplib.Internaldate2tuple(data[0])
106         return internal_date
107
108     def fetch_received_date(self, mid):
109         """Fetches the received date of a message, returns bytes reponse."""
110         try:
111             result, data = self.mail_con.uid('fetch', mid, '(BODY.PEEK[HEADER.FIELDS (RECEIVED)])')
112         except:
113             raise UserWarning("Could not fetch the received header of message " + mid.decode('iso-8859-1') + ".")
114         return data[0][1].decode('iso-8859-1')
115
116     def fetch_basic_date(self, mid):
117         """Fetches the basic date of a message, returns bytes reponse."""
118         try:
119             result, data = self.mail_con.uid('fetch', mid, '(BODY.PEEK[HEADER.FIELDS (DATE)])')
120         except:
121             raise UserWarning("Could not fetch the date header of message " + mid.decode('iso-8859-1') + ".")
122         return data[0][1].decode('iso-8859-1')
123
124     def update_message(self, mid, mailbox, internal_date):
125         """Replaces a message with one with correct internal date."""
126         internal_date_sec = time.mktime(internal_date.timetuple())
127         try:
128             result, data = self.mail_con.uid('fetch', mid, '(RFC822)')
129             #logging.debug("Entire e-mail is: %s", data[0][1])
130
131             # retrieve and select flags to upload
132             fetched_flags = self.mail_con.uid('fetch', mid, '(FLAGS)')[1][0]
133             parsed_flags = imaplib.ParseFlags(fetched_flags)
134             try:
135                 selected_flags = ()
136                 for flag in parsed_flags:
137                     if(flag != b'\\Recent'):
138                         selected_flags += (flag,)
139                 logging.debug("Selected flags %s from parsed flags %s.", selected_flags, parsed_flags)                    
140             except Exception as ex:
141                 logging.error(ex)
142             flags_str = " ".join(flag.decode('iso-8859-1') for flag in parsed_flags)
143
144             # upload message copy and delete old one
145             result, data = self.mail_con.append(mailbox, flags_str,
146                                                 internal_date_sec, data[0][1])
147             logging.debug("Adding corrected copy of the message reponse: %s %s", result, data)
148         except:
149             raise UserWarning("Could not replace the e-mail " + mid.decode('iso-8859-1') + ".")
150         try:
151             result, data = self.mail_con.uid('STORE', mid, '+FLAGS', r'(\Deleted)')
152             logging.debug("Removing old copy of the message reponse: %s %s", result, data)
153         except:
154             raise UserWarning("Could not delete the e-mail " + mid.decode('iso-8859-1') + ".")
155         self.mail_con.expunge()
156         return