]> code.delx.au - offlineimap/blob - offlineimap/folder/IMAP.py
Added code to limit command line length for very large mailboxes
[offlineimap] / offlineimap / folder / IMAP.py
1 # IMAP folder support
2 # Copyright (C) 2002-2004 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 ui = UIBase.getglobalui()
108 imapobj = self.imapserver.acquireconnection()
109 try:
110 imapobj.select(self.getfullname(), readonly = 1)
111 initialresult = imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])')
112 ui.debug('imap', 'Returned object from fetching %d: %s' % \
113 (uid, str(initialresult)))
114 return initialresult[1][0][1].replace("\r\n", "\n")
115
116 finally:
117 self.imapserver.releaseconnection(imapobj)
118
119 def getmessageflags(self, uid):
120 return self.messagelist[uid]['flags']
121
122 def savemessage_getnewheader(self, content):
123 headername = 'X-OfflineIMAP-%s-' % str(binascii.crc32(content)).replace('-', 'x')
124 headername += binascii.hexlify(self.repository.getname()) + '-'
125 headername += binascii.hexlify(self.getname())
126 headervalue= '%d-' % long(time.time())
127 headervalue += str(self.randomgenerator.random()).replace('.', '')
128 headervalue += '-v' + versionstr
129 return (headername, headervalue)
130
131 def savemessage_addheader(self, content, headername, headervalue):
132 ui = UIBase.getglobalui()
133 ui.debug('imap',
134 'savemessage_addheader: called to add %s: %s' % (headername,
135 headervalue))
136 insertionpoint = content.find("\r\n")
137 ui.debug('imap', 'savemessage_addheader: insertionpoint = %d' % insertionpoint)
138 leader = content[0:insertionpoint]
139 ui.debug('imap', 'savemessage_addheader: leader = %s' % repr(leader))
140 if insertionpoint == 0 or insertionpoint == -1:
141 newline = ''
142 insertionpoint = 0
143 else:
144 newline = "\r\n"
145 newline += "%s: %s" % (headername, headervalue)
146 ui.debug('imap', 'savemessage_addheader: newline = ' + repr(newline))
147 trailer = content[insertionpoint:]
148 ui.debug('imap', 'savemessage_addheader: trailer = ' + repr(trailer))
149 return leader + newline + trailer
150
151 def savemessage_searchforheader(self, imapobj, headername, headervalue):
152 if imapobj.untagged_responses.has_key('APPENDUID'):
153 return long(imapobj.untagged_responses['APPENDUID'][-1].split(' ')[1])
154
155 ui = UIBase.getglobalui()
156 ui.debug('imap', 'savemessage_searchforheader called for %s: %s' % \
157 (headername, headervalue))
158 # Now find the UID it got.
159 headervalue = imapobj._quote(headervalue)
160 try:
161 matchinguids = imapobj.uid('search', None,
162 '(HEADER %s %s)' % (headername, headervalue))[1][0]
163 except imapobj.error:
164 # IMAP server doesn't implement search or had a problem.
165 return 0
166 ui.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids))
167
168 matchinguids = matchinguids.split(' ')
169 ui.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \
170 repr(matchinguids))
171 if len(matchinguids) != 1 or matchinguids[0] == None:
172 raise ValueError, "While attempting to find UID for message with header %s, got wrong-sized matchinguids of %s" % (headername, str(matchinguids))
173 matchinguids.sort()
174 return long(matchinguids[0])
175
176 def savemessage(self, uid, content, flags):
177 imapobj = self.imapserver.acquireconnection()
178 ui = UIBase.getglobalui()
179 ui.debug('imap', 'savemessage: called')
180 try:
181 try:
182 imapobj.select(self.getfullname()) # Needed for search
183 except imapobj.readonly:
184 ui.msgtoreadonly(self, uid, content, flags)
185 # Return indicating message taken, but no UID assigned.
186 # Fudge it.
187 return 0
188
189 # This backend always assigns a new uid, so the uid arg is ignored.
190 # In order to get the new uid, we need to save off the message ID.
191
192 message = rfc822.Message(StringIO(content))
193 datetuple = rfc822.parsedate(message.getheader('Date'))
194 # Will be None if missing or not in a valid format.
195 if datetuple == None:
196 datetuple = time.localtime()
197 try:
198 if datetuple[0] < 1981:
199 raise ValueError
200 # This could raise a value error if it's not a valid format.
201 date = imaplib.Time2Internaldate(datetuple)
202 except ValueError:
203 # Argh, sometimes it's a valid format but year is 0102
204 # or something. Argh. It seems that Time2Internaldate
205 # will rause a ValueError if the year is 0102 but not 1902,
206 # but some IMAP servers nonetheless choke on 1902.
207 date = imaplib.Time2Internaldate(time.localtime())
208
209 ui.debug('imap', 'savemessage: using date ' + str(date))
210 content = re.sub("(?<!\r)\n", "\r\n", content)
211 ui.debug('imap', 'savemessage: initial content is: ' + repr(content))
212
213 (headername, headervalue) = self.savemessage_getnewheader(content)
214 ui.debug('imap', 'savemessage: new headers are: %s: %s' % \
215 (headername, headervalue))
216 content = self.savemessage_addheader(content, headername,
217 headervalue)
218 ui.debug('imap', 'savemessage: new content is: ' + repr(content))
219 ui.debug('imap', 'savemessage: new content length is ' + \
220 str(len(content)))
221
222 assert(imapobj.append(self.getfullname(),
223 imaputil.flagsmaildir2imap(flags),
224 date, content)[0] == 'OK')
225
226 # Checkpoint. Let it write out the messages, etc.
227 assert(imapobj.check()[0] == 'OK')
228
229 # Keep trying until we get the UID.
230 try:
231 ui.debug('imap', 'savemessage: first attempt to get new UID')
232 uid = self.savemessage_searchforheader(imapobj, headername,
233 headervalue)
234 except ValueError:
235 ui.debug('imap', 'savemessage: first attempt to get new UID failed. Going to run a NOOP and try again.')
236 assert(imapobj.noop()[0] == 'OK')
237 uid = self.savemessage_searchforheader(imapobj, headername,
238 headervalue)
239 finally:
240 self.imapserver.releaseconnection(imapobj)
241
242 self.messagelist[uid] = {'uid': uid, 'flags': flags}
243 ui.debug('imap', 'savemessage: returning %d' % uid)
244 return uid
245
246 def savemessageflags(self, uid, flags):
247 imapobj = self.imapserver.acquireconnection()
248 try:
249 try:
250 imapobj.select(self.getfullname())
251 except imapobj.readonly:
252 UIBase.getglobalui().flagstoreadonly(self, [uid], flags)
253 return
254 result = imapobj.uid('store', '%d' % uid, 'FLAGS',
255 imaputil.flagsmaildir2imap(flags))
256 assert result[0] == 'OK', 'Error with store: ' + r[1]
257 finally:
258 self.imapserver.releaseconnection(imapobj)
259 result = result[1][0]
260 if not result:
261 self.messagelist[uid]['flags'] = flags
262 else:
263 flags = imaputil.flags2hash(imaputil.imapsplit(result)[1])['FLAGS']
264 self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
265
266 def addmessageflags(self, uid, flags):
267 self.addmessagesflags([uid], flags)
268
269 def addmessagesflags_noconvert(self, uidlist, flags):
270 self.processmessagesflags('+', uidlist, flags)
271
272 def addmessagesflags(self, uidlist, flags):
273 """This is here for the sake of UIDMaps.py -- deletemessages must
274 add flags and get a converted UID, and if we don't have noconvert,
275 then UIDMaps will try to convert it twice."""
276 self.addmessagesflags_noconvert(uidlist, flags)
277
278 def deletemessageflags(self, uid, flags):
279 self.deletemessagesflags([uid], flags)
280
281 def deletemessagesflags(self, uidlist, flags):
282 self.processmessagesflags('-', uidlist, flags)
283
284 def processmessagesflags(self, operation, uidlist, flags):
285 if len(uidlist) > 101:
286 # Hack for those IMAP ervers with a limited line length
287 self.processmessagesflags(operation, uidlist[:100], flags)
288 self.processmessagesflags(operation, uidlist[100:], flags)
289 return
290
291 imapobj = self.imapserver.acquireconnection()
292 try:
293 try:
294 imapobj.select(self.getfullname())
295 except imapobj.readonly:
296 UIBase.getglobalui().flagstoreadonly(self, uidlist, flags)
297 return
298 r = imapobj.uid('store',
299 imaputil.listjoin(uidlist),
300 operation + 'FLAGS',
301 imaputil.flagsmaildir2imap(flags))
302 assert r[0] == 'OK', 'Error with store: ' + r[1]
303 r = r[1]
304 finally:
305 self.imapserver.releaseconnection(imapobj)
306 # Some IMAP servers do not always return a result. Therefore,
307 # only update the ones that it talks about, and manually fix
308 # the others.
309 needupdate = copy(uidlist)
310 for result in r:
311 if result == None:
312 # Compensate for servers that don't return anything from
313 # STORE.
314 continue
315 attributehash = imaputil.flags2hash(imaputil.imapsplit(result)[1])
316 if not ('UID' in attributehash and 'FLAGS' in attributehash):
317 # Compensate for servers that don't return a UID attribute.
318 continue
319 flags = attributehash['FLAGS']
320 uid = long(attributehash['UID'])
321 self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
322 try:
323 needupdate.remove(uid)
324 except ValueError: # Let it slide if it's not in the list
325 pass
326 for uid in needupdate:
327 if operation == '+':
328 for flag in flags:
329 if not flag in self.messagelist[uid]['flags']:
330 self.messagelist[uid]['flags'].append(flag)
331 self.messagelist[uid]['flags'].sort()
332 elif operation == '-':
333 for flag in flags:
334 if flag in self.messagelist[uid]['flags']:
335 self.messagelist[uid]['flags'].remove(flag)
336
337 def deletemessage(self, uid):
338 self.deletemessages_noconvert([uid])
339
340 def deletemessages(self, uidlist):
341 self.deletemessages_noconvert(uidlist)
342
343 def deletemessages_noconvert(self, uidlist):
344 # Weed out ones not in self.messagelist
345 uidlist = [uid for uid in uidlist if uid in self.messagelist]
346 if not len(uidlist):
347 return
348
349 self.addmessagesflags_noconvert(uidlist, ['T'])
350 imapobj = self.imapserver.acquireconnection()
351 try:
352 try:
353 imapobj.select(self.getfullname())
354 except imapobj.readonly:
355 UIBase.getglobalui().deletereadonly(self, uidlist)
356 return
357 if self.expunge:
358 assert(imapobj.expunge()[0] == 'OK')
359 finally:
360 self.imapserver.releaseconnection(imapobj)
361 for uid in uidlist:
362 del self.messagelist[uid]
363
364