]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/folder/IMAP.py
1f36ece08c38109654a6d4240066d0f5dba1aa81
[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 from offlineimap.version import versionstr
23 import rfc822, time, string, random, binascii, re
24 from StringIO import StringIO
25 from copy import copy
26
27
28 class IMAPFolder(BaseFolder):
29 def __init__(self, imapserver, name, visiblename, accountname, repository):
30 self.config = imapserver.config
31 self.expunge = repository.getexpunge()
32 self.name = imaputil.dequote(name)
33 self.root = None # imapserver.root
34 self.sep = imapserver.delim
35 self.imapserver = imapserver
36 self.messagelist = None
37 self.visiblename = visiblename
38 self.accountname = accountname
39 self.repository = repository
40 self.randomgenerator = random.Random()
41 BaseFolder.__init__(self)
42
43 def getaccountname(self):
44 return self.accountname
45
46 def suggeststhreads(self):
47 return 1
48
49 def waitforthread(self):
50 self.imapserver.connectionwait()
51
52 def getcopyinstancelimit(self):
53 return 'MSGCOPY_' + self.repository.getname()
54
55 def getvisiblename(self):
56 return self.visiblename
57
58 def getuidvalidity(self):
59 imapobj = self.imapserver.acquireconnection()
60 try:
61 # Primes untagged_responses
62 imapobj.select(self.getfullname(), readonly = 1)
63 return long(imapobj.untagged_responses['UIDVALIDITY'][0])
64 finally:
65 self.imapserver.releaseconnection(imapobj)
66
67 def cachemessagelist(self):
68 imapobj = self.imapserver.acquireconnection()
69 self.messagelist = {}
70
71 try:
72 # Primes untagged_responses
73 imapobj.select(self.getfullname(), readonly = 1, force = 1)
74 try:
75 # Some mail servers do not return an EXISTS response if
76 # the folder is empty.
77 maxmsgid = long(imapobj.untagged_responses['EXISTS'][0])
78 except KeyError:
79 return
80 if maxmsgid < 1:
81 # No messages; return
82 return
83
84 # Now, get the flags and UIDs for these.
85 # We could conceivably get rid of maxmsgid and just say
86 # '1:*' here.
87 response = imapobj.fetch('1:%d' % maxmsgid, '(FLAGS UID)')[1]
88 finally:
89 self.imapserver.releaseconnection(imapobj)
90 for messagestr in response:
91 # Discard the message number.
92 messagestr = string.split(messagestr, maxsplit = 1)[1]
93 options = imaputil.flags2hash(messagestr)
94 if not options.has_key('UID'):
95 UIBase.getglobalui().warn('No UID in message with options %s' %\
96 str(options),
97 minor = 1)
98 else:
99 uid = long(options['UID'])
100 flags = imaputil.flagsimap2maildir(options['FLAGS'])
101 self.messagelist[uid] = {'uid': uid, 'flags': flags}
102
103 def getmessagelist(self):
104 return self.messagelist
105
106 def getmessage(self, uid):
107 imapobj = self.imapserver.acquireconnection()
108 try:
109 imapobj.select(self.getfullname(), readonly = 1)
110 return imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])')[1][0][1].replace("\r\n", "\n")
111 finally:
112 self.imapserver.releaseconnection(imapobj)
113
114 def getmessageflags(self, uid):
115 return self.messagelist[uid]['flags']
116
117 def savemessage_getnewheader(self, content):
118 headername = 'X-OfflineIMAP-%s-' % str(binascii.crc32(content)).replace('-', 'x')
119 headername += binascii.hexlify(self.repository.getname()) + '-'
120 headername += binascii.hexlify(self.getname())
121 headervalue= '%d-' % long(time.time())
122 headervalue += str(self.randomgenerator.random()).replace('.', '')
123 headervalue += '-v' + versionstr
124 return (headername, headervalue)
125
126 def savemessage_addheader(self, content, headername, headervalue):
127 ui = UIBase.getglobalui()
128 ui.debug('imap',
129 'savemessage_addheader: called to add %s: %s' % (headername,
130 headervalue))
131 insertionpoint = content.find("\r\n")
132 ui.debug('imap', 'savemessage_addheader: insertionpoint = %d' % insertionpoint)
133 leader = content[0:insertionpoint]
134 ui.debug('imap', 'savemessage_addheader: leader = %s' % repr(leader))
135 if insertionpoint == 0 or insertionpoint == -1:
136 newline = ''
137 insertionpoint = 0
138 else:
139 newline = "\r\n"
140 newline += "%s: %s" % (headername, headervalue)
141 ui.debug('imap', 'savemessage_addheader: newline = ' + repr(newline))
142 trailer = content[insertionpoint:]
143 ui.debug('imap', 'savemessage_addheader: trailer = ' + repr(trailer))
144 return leader + newline + trailer
145
146 def savemessage_searchforheader(self, imapobj, headername, headervalue):
147 if imapobj.untagged_responses.has_key('APPENDUID'):
148 return long(imapobj.untagged_responses['APPENDUID'][0].split(' ')[1])
149
150 ui = UIBase.getglobalui()
151 ui.debug('imap', 'savemessage_searchforheader called for %s: %s' % \
152 (headername, headervalue))
153 # Now find the UID it got.
154 headervalue = imapobj._quote(headervalue)
155 try:
156 matchinguids = imapobj.uid('search', None,
157 '(HEADER %s %s)' % (headername, headervalue))[1][0]
158 except imapobj.error:
159 # IMAP server doesn't implement search or had a problem.
160 return 0
161 ui.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids))
162
163 matchinguids = matchinguids.split(' ')
164 ui.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \
165 repr(matchinguids))
166 if len(matchinguids) != 1 or matchinguids[0] == None:
167 raise ValueError, "While attempting to find UID for message with header %s, got wrong-sized matchinguids of %s" % (headername, str(matchinguids))
168 matchinguids.sort()
169 return long(matchinguids[0])
170
171 def savemessage(self, uid, content, flags):
172 imapobj = self.imapserver.acquireconnection()
173 ui = UIBase.getglobalui()
174 ui.debug('imap', 'savemessage: called')
175 try:
176 try:
177 imapobj.select(self.getfullname()) # Needed for search
178 except imapobj.readonly:
179 ui.msgtoreadonly(self, uid, content, flags)
180 # Return indicating message taken, but no UID assigned.
181 # Fudge it.
182 return 0
183
184 # This backend always assigns a new uid, so the uid arg is ignored.
185 # In order to get the new uid, we need to save off the message ID.
186
187 message = rfc822.Message(StringIO(content))
188 datetuple = rfc822.parsedate(message.getheader('Date'))
189 # Will be None if missing or not in a valid format.
190 if datetuple == None:
191 datetuple = time.localtime()
192 try:
193 if datetuple[0] < 1981:
194 raise ValueError
195 # This could raise a value error if it's not a valid format.
196 date = imaplib.Time2Internaldate(datetuple)
197 except ValueError:
198 # Argh, sometimes it's a valid format but year is 0102
199 # or something. Argh. It seems that Time2Internaldate
200 # will rause a ValueError if the year is 0102 but not 1902,
201 # but some IMAP servers nonetheless choke on 1902.
202 date = imaplib.Time2Internaldate(time.localtime())
203
204 ui.debug('imap', 'savemessage: using date ' + str(date))
205 content = re.sub("(?<!\r)\n", "\r\n", content)
206 ui.debug('imap', 'savemessage: initial content is: ' + repr(content))
207
208 (headername, headervalue) = self.savemessage_getnewheader(content)
209 ui.debug('imap', 'savemessage: new headers are: %s: %s' % \
210 (headername, headervalue))
211 content = self.savemessage_addheader(content, headername,
212 headervalue)
213 ui.debug('imap', 'savemessage: new content is: ' + repr(content))
214 ui.debug('imap', 'savemessage: new content length is ' + \
215 str(len(content)))
216
217 assert(imapobj.append(self.getfullname(),
218 imaputil.flagsmaildir2imap(flags),
219 date, content)[0] == 'OK')
220
221 # Checkpoint. Let it write out the messages, etc.
222 assert(imapobj.check()[0] == 'OK')
223
224 # Keep trying until we get the UID.
225 try:
226 ui.debug('imap', 'savemessage: first attempt to get new UID')
227 uid = self.savemessage_searchforheader(imapobj, headername,
228 headervalue)
229 except ValueError:
230 ui.debug('imap', 'savemessage: first attempt to get new UID failed. Going to run a NOOP and try again.')
231 assert(imapobj.noop()[0] == 'OK')
232 uid = self.savemessage_searchforheader(imapobj, headername,
233 headervalue)
234 finally:
235 self.imapserver.releaseconnection(imapobj)
236
237 self.messagelist[uid] = {'uid': uid, 'flags': flags}
238 ui.debug('imap', 'savemessage: returning %d' % uid)
239 return uid
240
241 def savemessageflags(self, uid, flags):
242 imapobj = self.imapserver.acquireconnection()
243 try:
244 try:
245 imapobj.select(self.getfullname())
246 except imapobj.readonly:
247 UIBase.getglobalui().flagstoreadonly(self, [uid], flags)
248 return
249 result = imapobj.uid('store', '%d' % uid, 'FLAGS',
250 imaputil.flagsmaildir2imap(flags))
251 assert result[0] == 'OK', 'Error with store: ' + r[1]
252 finally:
253 self.imapserver.releaseconnection(imapobj)
254 result = result[1][0]
255 if not result:
256 self.messagelist[uid]['flags'] = flags
257 else:
258 flags = imaputil.flags2hash(imaputil.imapsplit(result)[1])['FLAGS']
259 self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
260
261 def addmessageflags(self, uid, flags):
262 self.addmessagesflags([uid], flags)
263
264 def addmessagesflags_noconvert(self, uidlist, flags):
265 self.processmessagesflags('+', uidlist, flags)
266
267 def addmessagesflags(self, uidlist, flags):
268 """This is here for the sake of UIDMaps.py -- deletemessages must
269 add flags and get a converted UID, and if we don't have noconvert,
270 then UIDMaps will try to convert it twice."""
271 self.addmessagesflags_noconvert(uidlist, flags)
272
273 def deletemessageflags(self, uid, flags):
274 self.deletemessagesflags([uid], flags)
275
276 def deletemessagesflags(self, uidlist, flags):
277 self.processmessagesflags('-', uidlist, flags)
278
279 def processmessagesflags(self, operation, uidlist, flags):
280 imapobj = self.imapserver.acquireconnection()
281 try:
282 try:
283 imapobj.select(self.getfullname())
284 except imapobj.readonly:
285 UIBase.getglobalui().flagstoreadonly(self, uidlist, flags)
286 return
287 r = imapobj.uid('store',
288 imaputil.listjoin(uidlist),
289 operation + 'FLAGS',
290 imaputil.flagsmaildir2imap(flags))
291 assert r[0] == 'OK', 'Error with store: ' + r[1]
292 r = r[1]
293 finally:
294 self.imapserver.releaseconnection(imapobj)
295 # Some IMAP servers do not always return a result. Therefore,
296 # only update the ones that it talks about, and manually fix
297 # the others.
298 needupdate = copy(uidlist)
299 for result in r:
300 if result == None:
301 # Compensate for servers that don't return anything from
302 # STORE.
303 continue
304 attributehash = imaputil.flags2hash(imaputil.imapsplit(result)[1])
305 if not ('UID' in attributehash and 'FLAGS' in attributehash):
306 # Compensate for servers that don't return a UID attribute.
307 continue
308 flags = attributehash['FLAGS']
309 uid = long(attributehash['UID'])
310 self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
311 try:
312 needupdate.remove(uid)
313 except ValueError: # Let it slide if it's not in the list
314 pass
315 for uid in needupdate:
316 if operation == '+':
317 for flag in flags:
318 if not flag in self.messagelist[uid]['flags']:
319 self.messagelist[uid]['flags'].append(flag)
320 self.messagelist[uid]['flags'].sort()
321 elif operation == '-':
322 for flag in flags:
323 if flag in self.messagelist[uid]['flags']:
324 self.messagelist[uid]['flags'].remove(flag)
325
326 def deletemessage(self, uid):
327 self.deletemessages_noconvert([uid])
328
329 def deletemessages(self, uidlist):
330 self.deletemessages_noconvert(uidlist)
331
332 def deletemessages_noconvert(self, uidlist):
333 # Weed out ones not in self.messagelist
334 uidlist = [uid for uid in uidlist if uid in self.messagelist]
335 if not len(uidlist):
336 return
337
338 self.addmessagesflags_noconvert(uidlist, ['T'])
339 imapobj = self.imapserver.acquireconnection()
340 try:
341 try:
342 imapobj.select(self.getfullname())
343 except imapobj.readonly:
344 UIBase.getglobalui().deletereadonly(self, uidlist)
345 return
346 if self.expunge:
347 assert(imapobj.expunge()[0] == 'OK')
348 finally:
349 self.imapserver.releaseconnection(imapobj)
350 for uid in uidlist:
351 del self.messagelist[uid]
352
353