]>
code.delx.au - offlineimap/blob - offlineimap/folder/IMAP.py
2 # Copyright (C) 2002-2007 John Goerzen
3 # <jgoerzen@complete.org>
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.
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.
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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 from Base
import BaseFolder
20 from offlineimap
import imaplib2
, imaputil
, imaplibutil
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
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
)
43 def selectro(self
, imapobj
):
44 """Select this folder when we do not need write access.
45 Prefer SELECT to EXAMINE if we can, since some servers
46 (Courier) do not stabilize UID validity until the folder is
49 imapobj
.select(self
.getfullname())
50 except imapobj
.readonly
:
51 imapobj
.select(self
.getfullname(), readonly
= 1)
53 def getaccountname(self
):
54 return self
.accountname
56 def suggeststhreads(self
):
59 def waitforthread(self
):
60 self
.imapserver
.connectionwait()
62 def getcopyinstancelimit(self
):
63 return 'MSGCOPY_' + self
.repository
.getname()
65 def getvisiblename(self
):
66 return self
.visiblename
68 def getuidvalidity(self
):
69 imapobj
= self
.imapserver
.acquireconnection()
71 # Primes untagged_responses
72 self
.selectro(imapobj
)
73 return long(imapobj
.untagged_responses
['UIDVALIDITY'][0])
75 self
.imapserver
.releaseconnection(imapobj
)
77 def quickchanged(self
, statusfolder
):
78 # An IMAP folder has definitely changed if the number of
79 # messages or the UID of the last message have changed. Otherwise
80 # only flag changes could have occurred.
81 imapobj
= self
.imapserver
.acquireconnection()
83 # Primes untagged_responses
84 imapobj
.select(self
.getfullname(), readonly
= 1, force
= 1)
86 # Some mail servers do not return an EXISTS response if
87 # the folder is empty.
88 maxmsgid
= long(imapobj
.untagged_responses
['EXISTS'][0])
92 # Different number of messages than last time?
93 if maxmsgid
!= len(statusfolder
.getmessagelist()):
100 # Now, get the UID for the last message.
101 response
= imapobj
.fetch('%d' % maxmsgid
, '(UID)')[1]
103 self
.imapserver
.releaseconnection(imapobj
)
105 # Discard the message number.
106 messagestr
= string
.split(response
[0], maxsplit
= 1)[1]
107 options
= imaputil
.flags2hash(messagestr
)
108 if not options
.has_key('UID'):
110 uid
= long(options
['UID'])
111 saveduids
= statusfolder
.getmessagelist().keys()
113 if uid
!= saveduids
[-1]:
118 def cachemessagelist(self
):
119 imapobj
= self
.imapserver
.acquireconnection()
120 self
.messagelist
= {}
123 # Primes untagged_responses
124 imapobj
.select(self
.getfullname(), readonly
= 1, force
= 1)
126 # Some mail servers do not return an EXISTS response if
127 # the folder is empty.
128 maxmsgid
= long(imapobj
.untagged_responses
['EXISTS'][0])
132 # No messages; return
135 # Now, get the flags and UIDs for these.
136 # We could conceivably get rid of maxmsgid and just say
138 response
= imapobj
.fetch('1:%d' % maxmsgid
, '(FLAGS UID INTERNALDATE)')[1]
140 self
.imapserver
.releaseconnection(imapobj
)
141 for messagestr
in response
:
142 # Discard the message number.
143 messagestr
= string
.split(messagestr
, maxsplit
= 1)[1]
144 options
= imaputil
.flags2hash(messagestr
)
145 if not options
.has_key('UID'):
146 UIBase
.getglobalui().warn('No UID in message with options %s' %\
150 uid
= long(options
['UID'])
151 flags
= imaputil
.flagsimap2maildir(options
['FLAGS'])
152 rtime
= imaplibutil
.Internaldate2epoch(messagestr
)
153 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
, 'time': rtime
}
155 def getmessagelist(self
):
156 return self
.messagelist
158 def getmessage(self
, uid
):
159 ui
= UIBase
.getglobalui()
160 imapobj
= self
.imapserver
.acquireconnection()
162 imapobj
.select(self
.getfullname(), readonly
= 1)
163 initialresult
= imapobj
.uid('fetch', '%d' % uid
, '(BODY.PEEK[])')
164 ui
.debug('imap', 'Returned object from fetching %d: %s' % \
165 (uid
, str(initialresult
)))
166 return initialresult
[1][0][1].replace("\r\n", "\n")
169 self
.imapserver
.releaseconnection(imapobj
)
171 def getmessagetime(self
, uid
):
172 return self
.messagelist
[uid
]['time']
174 def getmessageflags(self
, uid
):
175 return self
.messagelist
[uid
]['flags']
177 def savemessage_getnewheader(self
, content
):
178 headername
= 'X-OfflineIMAP-%s-' % str(binascii
.crc32(content
)).replace('-', 'x')
179 headername
+= binascii
.hexlify(self
.repository
.getname()) + '-'
180 headername
+= binascii
.hexlify(self
.getname())
181 headervalue
= '%d-' % long(time
.time())
182 headervalue
+= str(self
.randomgenerator
.random()).replace('.', '')
183 headervalue
+= '-v' + versionstr
184 return (headername
, headervalue
)
186 def savemessage_addheader(self
, content
, headername
, headervalue
):
187 ui
= UIBase
.getglobalui()
189 'savemessage_addheader: called to add %s: %s' % (headername
,
191 insertionpoint
= content
.find("\r\n")
192 ui
.debug('imap', 'savemessage_addheader: insertionpoint = %d' % insertionpoint
)
193 leader
= content
[0:insertionpoint
]
194 ui
.debug('imap', 'savemessage_addheader: leader = %s' % repr(leader
))
195 if insertionpoint
== 0 or insertionpoint
== -1:
200 newline
+= "%s: %s" % (headername
, headervalue
)
201 ui
.debug('imap', 'savemessage_addheader: newline = ' + repr(newline
))
202 trailer
= content
[insertionpoint
:]
203 ui
.debug('imap', 'savemessage_addheader: trailer = ' + repr(trailer
))
204 return leader
+ newline
+ trailer
206 def savemessage_searchforheader(self
, imapobj
, headername
, headervalue
):
207 if imapobj
.untagged_responses
.has_key('APPENDUID'):
208 return long(imapobj
.untagged_responses
['APPENDUID'][-1].split(' ')[1])
210 ui
= UIBase
.getglobalui()
211 ui
.debug('imap', 'savemessage_searchforheader called for %s: %s' % \
212 (headername
, headervalue
))
213 # Now find the UID it got.
214 headervalue
= imapobj
._quote
(headervalue
)
216 matchinguids
= imapobj
.uid('search', 'HEADER', headername
, headervalue
)[1][0]
217 except imapobj
.error
, err
:
218 # IMAP server doesn't implement search or had a problem.
219 ui
.debug('imap', "savemessage_searchforheader: got IMAP error '%s' while attempting to UID SEARCH for message with header %s" % (err
, headername
))
221 ui
.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids
))
223 if matchinguids
== '':
224 ui
.debug('imap', "savemessage_searchforheader: UID SEARCH for message with header %s yielded no results" % headername
)
227 matchinguids
= matchinguids
.split(' ')
228 ui
.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \
230 if len(matchinguids
) != 1 or matchinguids
[0] == None:
231 raise ValueError, "While attempting to find UID for message with header %s, got wrong-sized matchinguids of %s" % (headername
, str(matchinguids
))
233 return long(matchinguids
[0])
235 def savemessage(self
, uid
, content
, flags
, rtime
):
236 imapobj
= self
.imapserver
.acquireconnection()
237 ui
= UIBase
.getglobalui()
238 ui
.debug('imap', 'savemessage: called')
241 imapobj
.select(self
.getfullname()) # Needed for search
242 except imapobj
.readonly
:
243 ui
.msgtoreadonly(self
, uid
, content
, flags
)
244 # Return indicating message taken, but no UID assigned.
248 # This backend always assigns a new uid, so the uid arg is ignored.
249 # In order to get the new uid, we need to save off the message ID.
251 message
= rfc822
.Message(StringIO(content
))
252 datetuple_msg
= rfc822
.parsedate(message
.getheader('Date'))
253 # Will be None if missing or not in a valid format.
255 # If time isn't known
256 if rtime
== None and datetuple_msg
== None:
257 datetuple
= time
.localtime()
259 datetuple
= datetuple_msg
261 datetuple
= time
.localtime(rtime
)
264 if datetuple
[0] < 1981:
267 # Check for invalid date
268 datetuple_check
= time
.localtime(time
.mktime(datetuple
))
269 if datetuple
[:2] != datetuple_check
[:2]:
272 # This could raise a value error if it's not a valid format.
273 date
= imaplib2
.Time2Internaldate(datetuple
)
274 except (ValueError, OverflowError):
275 # Argh, sometimes it's a valid format but year is 0102
276 # or something. Argh. It seems that Time2Internaldate
277 # will rause a ValueError if the year is 0102 but not 1902,
278 # but some IMAP servers nonetheless choke on 1902.
279 date
= imaplib2
.Time2Internaldate(time
.localtime())
281 ui
.debug('imap', 'savemessage: using date ' + str(date
))
282 content
= re
.sub("(?<!\r)\n", "\r\n", content
)
283 ui
.debug('imap', 'savemessage: initial content is: ' + repr(content
))
285 (headername
, headervalue
) = self
.savemessage_getnewheader(content
)
286 ui
.debug('imap', 'savemessage: new headers are: %s: %s' % \
287 (headername
, headervalue
))
288 content
= self
.savemessage_addheader(content
, headername
,
290 ui
.debug('imap', 'savemessage: new content is: ' + repr(content
))
291 ui
.debug('imap', 'savemessage: new content length is ' + \
294 assert(imapobj
.append(self
.getfullname(),
295 imaputil
.flagsmaildir2imap(flags
),
296 date
, content
)[0] == 'OK')
298 # Checkpoint. Let it write out the messages, etc.
299 assert(imapobj
.check()[0] == 'OK')
301 # Keep trying until we get the UID.
302 ui
.debug('imap', 'savemessage: first attempt to get new UID')
303 uid
= self
.savemessage_searchforheader(imapobj
, headername
,
305 # See docs for savemessage in Base.py for explanation of this and other return values
307 ui
.debug('imap', 'savemessage: first attempt to get new UID failed. Going to run a NOOP and try again.')
308 assert(imapobj
.noop()[0] == 'OK')
309 uid
= self
.savemessage_searchforheader(imapobj
, headername
,
312 self
.imapserver
.releaseconnection(imapobj
)
314 if uid
: # avoid UID FETCH 0 crash happening later on
315 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
}
317 ui
.debug('imap', 'savemessage: returning %d' % uid
)
320 def savemessageflags(self
, uid
, flags
):
321 imapobj
= self
.imapserver
.acquireconnection()
324 imapobj
.select(self
.getfullname())
325 except imapobj
.readonly
:
326 UIBase
.getglobalui().flagstoreadonly(self
, [uid
], flags
)
328 result
= imapobj
.uid('store', '%d' % uid
, 'FLAGS',
329 imaputil
.flagsmaildir2imap(flags
))
330 assert result
[0] == 'OK', 'Error with store: ' + '. '.join(r
[1])
332 self
.imapserver
.releaseconnection(imapobj
)
333 result
= result
[1][0]
335 self
.messagelist
[uid
]['flags'] = flags
337 flags
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])['FLAGS']
338 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
340 def addmessageflags(self
, uid
, flags
):
341 self
.addmessagesflags([uid
], flags
)
343 def addmessagesflags_noconvert(self
, uidlist
, flags
):
344 self
.processmessagesflags('+', uidlist
, flags
)
346 def addmessagesflags(self
, uidlist
, flags
):
347 """This is here for the sake of UIDMaps.py -- deletemessages must
348 add flags and get a converted UID, and if we don't have noconvert,
349 then UIDMaps will try to convert it twice."""
350 self
.addmessagesflags_noconvert(uidlist
, flags
)
352 def deletemessageflags(self
, uid
, flags
):
353 self
.deletemessagesflags([uid
], flags
)
355 def deletemessagesflags(self
, uidlist
, flags
):
356 self
.processmessagesflags('-', uidlist
, flags
)
358 def processmessagesflags(self
, operation
, uidlist
, flags
):
359 if len(uidlist
) > 101:
360 # Hack for those IMAP ervers with a limited line length
361 self
.processmessagesflags(operation
, uidlist
[:100], flags
)
362 self
.processmessagesflags(operation
, uidlist
[100:], flags
)
365 imapobj
= self
.imapserver
.acquireconnection()
368 imapobj
.select(self
.getfullname())
369 except imapobj
.readonly
:
370 UIBase
.getglobalui().flagstoreadonly(self
, uidlist
, flags
)
372 r
= imapobj
.uid('store',
373 imaputil
.listjoin(uidlist
),
375 imaputil
.flagsmaildir2imap(flags
))
376 assert r
[0] == 'OK', 'Error with store: ' + '. '.join(r
[1])
379 self
.imapserver
.releaseconnection(imapobj
)
380 # Some IMAP servers do not always return a result. Therefore,
381 # only update the ones that it talks about, and manually fix
383 needupdate
= copy(uidlist
)
386 # Compensate for servers that don't return anything from
389 attributehash
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])
390 if not ('UID' in attributehash
and 'FLAGS' in attributehash
):
391 # Compensate for servers that don't return a UID attribute.
393 lflags
= attributehash
['FLAGS']
394 uid
= long(attributehash
['UID'])
395 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(lflags
)
397 needupdate
.remove(uid
)
398 except ValueError: # Let it slide if it's not in the list
400 for uid
in needupdate
:
403 if not flag
in self
.messagelist
[uid
]['flags']:
404 self
.messagelist
[uid
]['flags'].append(flag
)
405 self
.messagelist
[uid
]['flags'].sort()
406 elif operation
== '-':
408 if flag
in self
.messagelist
[uid
]['flags']:
409 self
.messagelist
[uid
]['flags'].remove(flag
)
411 def deletemessage(self
, uid
):
412 self
.deletemessages_noconvert([uid
])
414 def deletemessages(self
, uidlist
):
415 self
.deletemessages_noconvert(uidlist
)
417 def deletemessages_noconvert(self
, uidlist
):
418 # Weed out ones not in self.messagelist
419 uidlist
= [uid
for uid
in uidlist
if uid
in self
.messagelist
]
423 self
.addmessagesflags_noconvert(uidlist
, ['T'])
424 imapobj
= self
.imapserver
.acquireconnection()
427 imapobj
.select(self
.getfullname())
428 except imapobj
.readonly
:
429 UIBase
.getglobalui().deletereadonly(self
, uidlist
)
432 assert(imapobj
.expunge()[0] == 'OK')
434 self
.imapserver
.releaseconnection(imapobj
)
436 del self
.messagelist
[uid
]