Script also works with python 2.7
[imap-fix-internaldate] / src / mail_iterator.py
... / ...
CommitLineData
1'''
2mail_iterator.py - The module contains the MailIterator class.
3
4Copyright (c) 2012 Intra2net AG
5Author: Plamen Dimitrov and Thomas Jarosch
6
7This program is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12This program is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16'''
17
18import imaplib
19import re
20import time
21import logging
22
23MAILBOX_RESP = re.compile(r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
24UIDVAL_RESP = re.compile(r'(?P<name>.*) \(UIDVALIDITY (?P<uidval>.*)\)')
25
26#imaplib.Debug = 4
27
28class 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 self.skip_shared_folders = skip_shared_folders
44
45 # connect to server
46 try:
47 self.mail_con = imaplib.IMAP4_SSL(server)
48 except Exception as ex:
49 raise UserWarning("Could not connect to host %s: %s" % (server, ex))
50
51 # log in
52 try:
53 self.mail_con.login(username, password)
54 logging.info("Logged in as %s.", username)
55 except:
56 self.logged_in = False
57 raise UserWarning("Could not log in as user " + username)
58 self.logged_in = True
59
60 # list mailboxes
61 try:
62 _result, self.mailboxes = self.mail_con.list()
63 except (self.mail_con.error):
64 raise UserWarning("Could not retrieve mailboxes for user " + username)
65
66 def __del__(self):
67 """Closes the connection and the user session."""
68 if(self.logged_in):
69 self.mail_con.close()
70 self.mail_con.logout()
71
72 def __iter__(self):
73 """Iterates through all mailboxes, returns (uidval,name)."""
74 for mailbox in self.mailboxes:
75 logging.debug("Checking mailbox %s.", mailbox)
76 mailbox = MAILBOX_RESP.match(mailbox.decode('iso-8859-1')).groups()
77 # detect if mailbox is shared and if skip flag is set iterate further
78 if(self.skip_shared_folders and mailbox[2].split(mailbox[1])[0] == '"user'):
79 logging.info("Mailbox %s is shared and therefore skipped.", mailbox[2])
80 continue
81 # retrieve uidvalidity
82 try:
83 # Work around unsolicited server responses in imaplib by clearing them
84 self.mail_con.response('STATUS')
85 _result, data = self.mail_con.status(mailbox[2], '(UIDVALIDITY)')
86 except (self.mail_con.error):
87 raise UserWarning("Could not retrieve mailbox uidvalidity.")
88 uidval = UIDVAL_RESP.match(data[0].decode('iso-8859-1')).groups()
89 logging.debug("Extracted mailbox info is %s %s.", data[0], uidval)
90 # select mailbox if writable
91 try:
92 self.mail_con.select(mailbox[2])
93 except self.mail_con.readonly:
94 logging.warning("Mailbox %s is not writable and therefore skipped.", mailbox[2])
95 continue
96 yield (mailbox[2], uidval[1])
97
98 def fetch_messages(self):
99 """Fetches the messages from the current mailbox, return list of uids."""
100 try:
101 # Work around unsolicited server responses in imaplib by clearing them
102 self.mail_con.response('SEARCH')
103 _result, data = self.mail_con.uid('search', None, "ALL")
104 except (self.mail_con.error):
105 raise UserWarning("Could not fetch messages.")
106 mailid_list = data[0].split()
107 return mailid_list
108
109 def fetch_internal_date(self, mid):
110 """Fetches the internal date of a message, returns a time tuple."""
111 try:
112 # Work around unsolicited server responses in imaplib by clearing them
113 self.mail_con.response('FETCH')
114 _result, data = self.mail_con.uid('fetch', mid, '(INTERNALDATE)')
115 except (self.mail_con.error):
116 raise UserWarning("Could not fetch the internal date of message " + mid.decode('iso-8859-1') + ".")
117 internal_date = imaplib.Internaldate2tuple(data[0])
118 return internal_date
119
120 def fetch_received_date(self, mid):
121 """Fetches the received date of a message, returns bytes reponse."""
122 try:
123 # Work around unsolicited server responses in imaplib by clearing them
124 self.mail_con.response('FETCH')
125 _result, data = self.mail_con.uid('fetch', mid, '(BODY.PEEK[HEADER.FIELDS (RECEIVED)])')
126 except (self.mail_con.error):
127 raise UserWarning("Could not fetch the received header of message " + mid.decode('iso-8859-1') + ".")
128 return data[0][1].decode('iso-8859-1')
129
130 def fetch_basic_date(self, mid):
131 """Fetches the basic date of a message, returns bytes reponse."""
132 try:
133 # Work around unsolicited server responses in imaplib by clearing them
134 self.mail_con.response('FETCH')
135 _result, data = self.mail_con.uid('fetch', mid, '(BODY.PEEK[HEADER.FIELDS (DATE)])')
136 except (self.mail_con.error):
137 raise UserWarning("Could not fetch the date header of message " + mid.decode('iso-8859-1') + ".")
138 return data[0][1].decode('iso-8859-1')
139
140 def update_message(self, mid, mailbox, internal_date):
141 """Replaces a message with one with correct internal date."""
142 internal_date_sec = time.mktime(internal_date.timetuple())
143 try:
144 # Work around unsolicited server responses in imaplib by clearing them
145 self.mail_con.response('FETCH')
146 result, data = self.mail_con.uid('fetch', mid, '(RFC822)')
147 #logging.debug("Entire e-mail is: %s", data[0][1])
148
149 # Work around unsolicited server responses in imaplib by clearing them
150 self.mail_con.response('FETCH')
151 # retrieve and select flags to upload
152 fetched_flags = self.mail_con.uid('fetch', mid, '(FLAGS)')[1][0]
153 parsed_flags = imaplib.ParseFlags(fetched_flags)
154 selected_flags = ()
155 for flag in parsed_flags:
156 if(flag != b'\\Recent'):
157 selected_flags += (flag,)
158 logging.debug("Selected flags %s from parsed flags %s.", selected_flags, parsed_flags)
159 flags_str = " ".join(flag.decode('iso-8859-1') for flag in selected_flags)
160
161 # Work around unsolicited server responses in imaplib by clearing them
162 self.mail_con.response('APPEND')
163 # upload message copy and delete old one
164 result, data = self.mail_con.append(mailbox, flags_str,
165 internal_date_sec, data[0][1])
166 logging.debug("Adding corrected copy of the message reponse: %s %s", result, data)
167 except (self.mail_con.error):
168 raise UserWarning("Could not replace the e-mail " + mid.decode('iso-8859-1') + ".")
169 try:
170 # Work around unsolicited server responses in imaplib by clearing them
171 self.mail_con.response('STORE')
172 result, data = self.mail_con.uid('STORE', mid, '+FLAGS', r'(\Deleted)')
173 logging.debug("Removing old copy of the message reponse: %s %s", result, data)
174 except (self.mail_con.error):
175 raise UserWarning("Could not delete the e-mail " + mid.decode('iso-8859-1') + ".")
176 self.mail_con.expunge()
177 return