]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/folder/IMAP.py
/offlineimap/head: changeset 445
[offlineimap] / offlineimap / head / offlineimap / folder / IMAP.py
1 # IMAP folder support
2 # Copyright (C) 2002 John Goerzen
3 # <jgoerzen@complete.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
19 from Base import BaseFolder
20 from offlineimap import imaputil, imaplib
21 from offlineimap.ui import UIBase
22 import rfc822, time, string, random, binascii
23 from StringIO import StringIO
24 from copy import copy
25
26
27 class IMAPFolder(BaseFolder):
28 def __init__(self, imapserver, name, visiblename, accountname, repository):
29 self.config = imapserver.config
30 self.expunge = repository.getexpunge()
31 self.name = imaputil.dequote(name)
32 self.root = None # imapserver.root
33 self.sep = imapserver.delim
34 self.imapserver = imapserver
35 self.messagelist = None
36 self.visiblename = visiblename
37 self.accountname = accountname
38 self.repository = repository
39 self.randomgenerator = random.Random()
40 BaseFolder.__init__(self)
41
42 def getaccountname(self):
43 return self.accountname
44
45 def suggeststhreads(self):
46 return 1
47
48 def waitforthread(self):
49 self.imapserver.connectionwait()
50
51 def getcopyinstancelimit(self):
52 return 'MSGCOPY_' + self.repository.getname()
53
54 def getvisiblename(self):
55 return self.visiblename
56
57 def getuidvalidity(self):
58 imapobj = self.imapserver.acquireconnection()
59 try:
60 # Primes untagged_responses
61 imapobj.select(self.getfullname(), readonly = 1)
62 return long(imapobj.untagged_responses['UIDVALIDITY'][0])
63 finally:
64 self.imapserver.releaseconnection(imapobj)
65
66 def cachemessagelist(self):
67 imapobj = self.imapserver.acquireconnection()
68 self.messagelist = {}
69
70 try:
71 # Primes untagged_responses
72 assert(imapobj.select(self.getfullname(), readonly = 1)[0] == 'OK')
73 try:
74 # Some mail servers do not return an EXISTS response if
75 # the folder is empty.
76 maxmsgid = long(imapobj.untagged_responses['EXISTS'][0])
77 except KeyError:
78 return
79 if maxmsgid < 1:
80 # No messages; return
81 return
82
83 # Now, get the flags and UIDs for these.
84 # We could conceivably get rid of maxmsgid and just say
85 # '1:*' here.
86 response = imapobj.fetch('1:%d' % maxmsgid, '(FLAGS UID)')[1]
87 finally:
88 self.imapserver.releaseconnection(imapobj)
89 for messagestr in response:
90 # Discard the message number.
91 messagestr = string.split(messagestr, maxsplit = 1)[1]
92 options = imaputil.flags2hash(messagestr)
93 if not options.has_key('UID'):
94 UIBase.getglobalui().warn('No UID in message with options %s' %\
95 str(options),
96 minor = 1)
97 else:
98 uid = long(options['UID'])
99 flags = imaputil.flagsimap2maildir(options['FLAGS'])
100 self.messagelist[uid] = {'uid': uid, 'flags': flags}
101
102 def getmessagelist(self):
103 return self.messagelist
104
105 def getmessage(self, uid):
106 imapobj = self.imapserver.acquireconnection()
107 try:
108 imapobj.select(self.getfullname(), readonly = 1)
109 return imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])')[1][0][1].replace("\r\n", "\n")
110 finally:
111 self.imapserver.releaseconnection(imapobj)
112
113 def getmessageflags(self, uid):
114 return self.messagelist[uid]['flags']
115
116 def savemessage_getnewheader(self, content):
117 headername = 'X-OfflineIMAP-%s-' % str(binascii.crc32(content)).replace('-', 'x')
118 headername += binascii.hexlify(self.repository.getname()) + '-'
119 headername += binascii.hexlify(self.getname())
120 headervalue= '%d-' % long(time.time())
121 headervalue += str(self.randomgenerator.random()).replace('.', '')
122 return (headername, headervalue)
123
124 def savemessage_addheader(self, content, headername, headervalue):
125 insertionpoint = content.find("\r\n")
126 leader = content[0:insertionpoint]
127 newline = "\r\n%s: %s" % (headername, headervalue)
128 trailer = content[insertionpoint:]
129 return leader + newline + trailer
130
131 def savemessage_searchforheader(self, imapobj, headername, headervalue):
132 # Now find the UID it got.
133 headervalue = imapobj._quote(headervalue)
134 try:
135 matchinguids = imapobj.uid('search', None,
136 '(HEADER %s %s)' % (headername, headervalue))[1][0]
137 except imapobj.error:
138 # IMAP server doesn't implement search or had a problem.
139 return 0
140 matchinguids = matchinguids.split(' ')
141 if len(matchinguids) != 1 or matchinguids[0] == None:
142 raise ValueError, "While attempting to find UID for message with header %s, got wrong-sized matchinguids of %s" % (headername, str(matchinguids))
143 matchinguids.sort()
144 return long(matchinguids[0])
145
146 def savemessage(self, uid, content, flags):
147 imapobj = self.imapserver.acquireconnection()
148 try:
149 try:
150 imapobj.select(self.getfullname()) # Needed for search
151 except imapobj.readonly:
152 UIBase.getglobalui().msgtoreadonly(self, uid, content, flags)
153 # Return indicating message taken, but no UID assigned.
154 # Fudge it.
155 return 0
156
157 # This backend always assigns a new uid, so the uid arg is ignored.
158 # In order to get the new uid, we need to save off the message ID.
159
160 message = rfc822.Message(StringIO(content))
161 datetuple = rfc822.parsedate(message.getheader('Date'))
162 # Will be None if missing or not in a valid format.
163 if datetuple == None:
164 datetuple = time.localtime()
165 try:
166 if datetuple[0] < 1981:
167 raise ValueError
168 # This could raise a value error if it's not a valid format.
169 date = imaplib.Time2Internaldate(datetuple)
170 except ValueError:
171 # Argh, sometimes it's a valid format but year is 0102
172 # or something. Argh. It seems that Time2Internaldate
173 # will rause a ValueError if the year is 0102 but not 1902,
174 # but some IMAP servers nonetheless choke on 1902.
175 date = imaplib.Time2Internaldate(time.localtime())
176
177 content = re.sub("[^\r]\n", "\r\n", content)
178
179 (headername, headervalue) = self.savemessage_getnewheader(content)
180 content = self.savemessage_addheader(content, headername,
181 headervalue)
182
183 assert(imapobj.append(self.getfullname(),
184 imaputil.flagsmaildir2imap(flags),
185 date, content)[0] == 'OK')
186
187 # Checkpoint. Let it write out the messages, etc.
188 assert(imapobj.check()[0] == 'OK')
189
190 # Keep trying until we get the UID.
191 try:
192 uid = self.savemessage_searchforheader(imapobj, headername,
193 headervalue)
194 except ValueError:
195 assert(imapobj.noop()[0] == 'OK')
196 uid = self.savemessage_searchforheader(imapobj, headername,
197 headervalue)
198 finally:
199 self.imapserver.releaseconnection(imapobj)
200
201 self.messagelist[uid] = {'uid': uid, 'flags': flags}
202 return uid
203
204 def savemessageflags(self, uid, flags):
205 imapobj = self.imapserver.acquireconnection()
206 try:
207 try:
208 imapobj.select(self.getfullname())
209 except imapobj.readonly:
210 UIBase.getglobalui().flagstoreadonly(self, [uid], flags)
211 return
212 result = imapobj.uid('store', '%d' % uid, 'FLAGS',
213 imaputil.flagsmaildir2imap(flags))
214 assert result[0] == 'OK', 'Error with store: ' + r[1]
215 finally:
216 self.imapserver.releaseconnection(imapobj)
217 result = result[1][0]
218 if not result:
219 self.messagelist[uid]['flags'] = flags
220 else:
221 flags = imaputil.flags2hash(imaputil.imapsplit(result)[1])['FLAGS']
222 self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
223
224 def addmessageflags(self, uid, flags):
225 self.addmessagesflags([uid], flags)
226
227 def addmessagesflags_noconvert(self, uidlist, flags):
228 self.processmessagesflags('+', uidlist, flags)
229
230 def addmessagesflags(self, uidlist, flags):
231 """This is here for the sake of UIDMaps.py -- deletemessages must
232 add flags and get a converted UID, and if we don't have noconvert,
233 then UIDMaps will try to convert it twice."""
234 self.addmessagesflags_noconvert(uidlist, flags)
235
236 def deletemessageflags(self, uid, flags):
237 self.deletemessagesflags([uid], flags)
238
239 def deletemessagesflags(self, uidlist, flags):
240 self.processmessagesflags('-', uidlist, flags)
241
242 def processmessagesflags(self, operation, uidlist, flags):
243 imapobj = self.imapserver.acquireconnection()
244 try:
245 try:
246 imapobj.select(self.getfullname())
247 except imapobj.readonly:
248 UIBase.getglobalui().flagstoreadonly(self, uidlist, flags)
249 return
250 r = imapobj.uid('store',
251 imaputil.listjoin(uidlist),
252 operation + 'FLAGS',
253 imaputil.flagsmaildir2imap(flags))
254 assert r[0] == 'OK', 'Error with store: ' + r[1]
255 r = r[1]
256 finally:
257 self.imapserver.releaseconnection(imapobj)
258 # Some IMAP servers do not always return a result. Therefore,
259 # only update the ones that it talks about, and manually fix
260 # the others.
261 needupdate = copy(uidlist)
262 for result in r:
263 if result == None:
264 # Compensate for servers that don't return anything from
265 # STORE.
266 continue
267 attributehash = imaputil.flags2hash(imaputil.imapsplit(result)[1])
268 if not ('UID' in attributehash and 'FLAGS' in attributehash):
269 # Compensate for servers that don't return a UID attribute.
270 continue
271 flags = attributehash['FLAGS']
272 uid = long(attributehash['UID'])
273 self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
274 try:
275 needupdate.remove(uid)
276 except ValueError: # Let it slide if it's not in the list
277 pass
278 for uid in needupdate:
279 if operation == '+':
280 for flag in flags:
281 if not flag in self.messagelist[uid]['flags']:
282 self.messagelist[uid]['flags'].append(flag)
283 self.messagelist[uid]['flags'].sort()
284 elif operation == '-':
285 for flag in flags:
286 if flag in self.messagelist[uid]['flags']:
287 self.messagelist[uid]['flags'].remove(flag)
288
289 def deletemessage(self, uid):
290 self.deletemessages_noconvert([uid])
291
292 def deletemessages(self, uidlist):
293 self.deletemessages_noconvert(uidlist)
294
295 def deletemessages_noconvert(self, uidlist):
296 # Weed out ones not in self.messagelist
297 uidlist = [uid for uid in uidlist if uid in self.messagelist]
298 if not len(uidlist):
299 return
300
301 self.addmessagesflags_noconvert(uidlist, ['T'])
302 imapobj = self.imapserver.acquireconnection()
303 try:
304 try:
305 imapobj.select(self.getfullname())
306 except imapobj.readonly:
307 UIBase.getglobalui().deletereadonly(self, uidlist)
308 return
309 if self.expunge:
310 assert(imapobj.expunge()[0] == 'OK')
311 finally:
312 self.imapserver.releaseconnection(imapobj)
313 for uid in uidlist:
314 del self.messagelist[uid]
315
316