]>
code.delx.au - offlineimap/blob - offlineimap/folder/IMAP.py
2 # Copyright (C) 2002-2004 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 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
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 getaccountname(self
):
44 return self
.accountname
46 def suggeststhreads(self
):
49 def waitforthread(self
):
50 self
.imapserver
.connectionwait()
52 def getcopyinstancelimit(self
):
53 return 'MSGCOPY_' + self
.repository
.getname()
55 def getvisiblename(self
):
56 return self
.visiblename
58 def getuidvalidity(self
):
59 imapobj
= self
.imapserver
.acquireconnection()
61 # Primes untagged_responses
62 imapobj
.select(self
.getfullname(), readonly
= 1)
63 return long(imapobj
.untagged_responses
['UIDVALIDITY'][0])
65 self
.imapserver
.releaseconnection(imapobj
)
67 def cachemessagelist(self
):
68 imapobj
= self
.imapserver
.acquireconnection()
72 # Primes untagged_responses
73 imapobj
.select(self
.getfullname(), readonly
= 1, force
= 1)
75 # Some mail servers do not return an EXISTS response if
76 # the folder is empty.
77 maxmsgid
= long(imapobj
.untagged_responses
['EXISTS'][0])
84 # Now, get the flags and UIDs for these.
85 # We could conceivably get rid of maxmsgid and just say
87 response
= imapobj
.fetch('1:%d' % maxmsgid
, '(FLAGS UID INTERNALDATE)')[1]
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' %\
99 uid
= long(options
['UID'])
100 flags
= imaputil
.flagsimap2maildir(options
['FLAGS'])
101 rtime
= imaplib
.Internaldate2epoch(messagestr
)
102 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
, 'time': rtime
}
104 def getmessagelist(self
):
105 return self
.messagelist
107 def getmessage(self
, uid
):
108 ui
= UIBase
.getglobalui()
109 imapobj
= self
.imapserver
.acquireconnection()
111 imapobj
.select(self
.getfullname(), readonly
= 1)
112 initialresult
= imapobj
.uid('fetch', '%d' % uid
, '(BODY.PEEK[])')
113 ui
.debug('imap', 'Returned object from fetching %d: %s' % \
114 (uid
, str(initialresult
)))
115 return initialresult
[1][0][1].replace("\r\n", "\n")
118 self
.imapserver
.releaseconnection(imapobj
)
120 def getmessagetime(self
, uid
):
121 return self
.messagelist
[uid
]['time']
123 def getmessageflags(self
, uid
):
124 return self
.messagelist
[uid
]['flags']
126 def savemessage_getnewheader(self
, content
):
127 headername
= 'X-OfflineIMAP-%s-' % str(binascii
.crc32(content
)).replace('-', 'x')
128 headername
+= binascii
.hexlify(self
.repository
.getname()) + '-'
129 headername
+= binascii
.hexlify(self
.getname())
130 headervalue
= '%d-' % long(time
.time())
131 headervalue
+= str(self
.randomgenerator
.random()).replace('.', '')
132 headervalue
+= '-v' + versionstr
133 return (headername
, headervalue
)
135 def savemessage_addheader(self
, content
, headername
, headervalue
):
136 ui
= UIBase
.getglobalui()
138 'savemessage_addheader: called to add %s: %s' % (headername
,
140 insertionpoint
= content
.find("\r\n")
141 ui
.debug('imap', 'savemessage_addheader: insertionpoint = %d' % insertionpoint
)
142 leader
= content
[0:insertionpoint
]
143 ui
.debug('imap', 'savemessage_addheader: leader = %s' % repr(leader
))
144 if insertionpoint
== 0 or insertionpoint
== -1:
149 newline
+= "%s: %s" % (headername
, headervalue
)
150 ui
.debug('imap', 'savemessage_addheader: newline = ' + repr(newline
))
151 trailer
= content
[insertionpoint
:]
152 ui
.debug('imap', 'savemessage_addheader: trailer = ' + repr(trailer
))
153 return leader
+ newline
+ trailer
155 def savemessage_searchforheader(self
, imapobj
, headername
, headervalue
):
156 if imapobj
.untagged_responses
.has_key('APPENDUID'):
157 return long(imapobj
.untagged_responses
['APPENDUID'][-1].split(' ')[1])
159 ui
= UIBase
.getglobalui()
160 ui
.debug('imap', 'savemessage_searchforheader called for %s: %s' % \
161 (headername
, headervalue
))
162 # Now find the UID it got.
163 headervalue
= imapobj
._quote
(headervalue
)
165 matchinguids
= imapobj
.uid('search', 'HEADER', headername
, headervalue
)[1][0]
166 except imapobj
.error
, err
:
167 # IMAP server doesn't implement search or had a problem.
168 ui
.debug('imap', "savemessage_searchforheader: got IMAP error '%s' while attempting to UID SEARCH for message with header %s" % (err
, headername
))
170 ui
.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids
))
172 if matchinguids
== '':
173 ui
.debug('imap', "savemessage_searchforheader: UID SEARCH for message with header %s yielded no results" % headername
)
176 matchinguids
= matchinguids
.split(' ')
177 ui
.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \
179 if len(matchinguids
) != 1 or matchinguids
[0] == None:
180 raise ValueError, "While attempting to find UID for message with header %s, got wrong-sized matchinguids of %s" % (headername
, str(matchinguids
))
182 return long(matchinguids
[0])
184 def savemessage(self
, uid
, content
, flags
, rtime
):
185 imapobj
= self
.imapserver
.acquireconnection()
186 ui
= UIBase
.getglobalui()
187 ui
.debug('imap', 'savemessage: called')
190 imapobj
.select(self
.getfullname()) # Needed for search
191 except imapobj
.readonly
:
192 ui
.msgtoreadonly(self
, uid
, content
, flags
)
193 # Return indicating message taken, but no UID assigned.
197 # This backend always assigns a new uid, so the uid arg is ignored.
198 # In order to get the new uid, we need to save off the message ID.
200 message
= rfc822
.Message(StringIO(content
))
201 datetuple_msg
= rfc822
.parsedate(message
.getheader('Date'))
202 # Will be None if missing or not in a valid format.
204 # If time isn't known
205 if rtime
== None and datetuple_msg
== None:
206 datetuple
= time
.localtime()
208 datetuple
= datetuple_msg
210 datetuple
= time
.localtime(rtime
)
213 if datetuple
[0] < 1981:
215 # This could raise a value error if it's not a valid format.
216 date
= imaplib
.Time2Internaldate(datetuple
)
217 except (ValueError, OverflowError):
218 # Argh, sometimes it's a valid format but year is 0102
219 # or something. Argh. It seems that Time2Internaldate
220 # will rause a ValueError if the year is 0102 but not 1902,
221 # but some IMAP servers nonetheless choke on 1902.
222 date
= imaplib
.Time2Internaldate(time
.localtime())
224 ui
.debug('imap', 'savemessage: using date ' + str(date
))
225 content
= re
.sub("(?<!\r)\n", "\r\n", content
)
226 ui
.debug('imap', 'savemessage: initial content is: ' + repr(content
))
228 (headername
, headervalue
) = self
.savemessage_getnewheader(content
)
229 ui
.debug('imap', 'savemessage: new headers are: %s: %s' % \
230 (headername
, headervalue
))
231 content
= self
.savemessage_addheader(content
, headername
,
233 ui
.debug('imap', 'savemessage: new content is: ' + repr(content
))
234 ui
.debug('imap', 'savemessage: new content length is ' + \
237 assert(imapobj
.append(self
.getfullname(),
238 imaputil
.flagsmaildir2imap(flags
),
239 date
, content
)[0] == 'OK')
241 # Checkpoint. Let it write out the messages, etc.
242 assert(imapobj
.check()[0] == 'OK')
244 # Keep trying until we get the UID.
245 ui
.debug('imap', 'savemessage: first attempt to get new UID')
246 uid
= self
.savemessage_searchforheader(imapobj
, headername
,
248 # See docs for savemessage in Base.py for explanation of this and other return values
250 ui
.debug('imap', 'savemessage: first attempt to get new UID failed. Going to run a NOOP and try again.')
251 assert(imapobj
.noop()[0] == 'OK')
252 uid
= self
.savemessage_searchforheader(imapobj
, headername
,
255 self
.imapserver
.releaseconnection(imapobj
)
257 if uid
: # avoid UID FETCH 0 crash happening later on
258 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
}
260 ui
.debug('imap', 'savemessage: returning %d' % uid
)
263 def savemessageflags(self
, uid
, flags
):
264 imapobj
= self
.imapserver
.acquireconnection()
267 imapobj
.select(self
.getfullname())
268 except imapobj
.readonly
:
269 UIBase
.getglobalui().flagstoreadonly(self
, [uid
], flags
)
271 result
= imapobj
.uid('store', '%d' % uid
, 'FLAGS',
272 imaputil
.flagsmaildir2imap(flags
))
273 assert result
[0] == 'OK', 'Error with store: ' + r
[1]
275 self
.imapserver
.releaseconnection(imapobj
)
276 result
= result
[1][0]
278 self
.messagelist
[uid
]['flags'] = flags
280 flags
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])['FLAGS']
281 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
283 def addmessageflags(self
, uid
, flags
):
284 self
.addmessagesflags([uid
], flags
)
286 def addmessagesflags_noconvert(self
, uidlist
, flags
):
287 self
.processmessagesflags('+', uidlist
, flags
)
289 def addmessagesflags(self
, uidlist
, flags
):
290 """This is here for the sake of UIDMaps.py -- deletemessages must
291 add flags and get a converted UID, and if we don't have noconvert,
292 then UIDMaps will try to convert it twice."""
293 self
.addmessagesflags_noconvert(uidlist
, flags
)
295 def deletemessageflags(self
, uid
, flags
):
296 self
.deletemessagesflags([uid
], flags
)
298 def deletemessagesflags(self
, uidlist
, flags
):
299 self
.processmessagesflags('-', uidlist
, flags
)
301 def processmessagesflags(self
, operation
, uidlist
, flags
):
302 if len(uidlist
) > 101:
303 # Hack for those IMAP ervers with a limited line length
304 self
.processmessagesflags(operation
, uidlist
[:100], flags
)
305 self
.processmessagesflags(operation
, uidlist
[100:], flags
)
308 imapobj
= self
.imapserver
.acquireconnection()
311 imapobj
.select(self
.getfullname())
312 except imapobj
.readonly
:
313 UIBase
.getglobalui().flagstoreadonly(self
, uidlist
, flags
)
315 r
= imapobj
.uid('store',
316 imaputil
.listjoin(uidlist
),
318 imaputil
.flagsmaildir2imap(flags
))
319 assert r
[0] == 'OK', 'Error with store: ' + r
[1]
322 self
.imapserver
.releaseconnection(imapobj
)
323 # Some IMAP servers do not always return a result. Therefore,
324 # only update the ones that it talks about, and manually fix
326 needupdate
= copy(uidlist
)
329 # Compensate for servers that don't return anything from
332 attributehash
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])
333 if not ('UID' in attributehash
and 'FLAGS' in attributehash
):
334 # Compensate for servers that don't return a UID attribute.
336 flags
= attributehash
['FLAGS']
337 uid
= long(attributehash
['UID'])
338 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
340 needupdate
.remove(uid
)
341 except ValueError: # Let it slide if it's not in the list
343 for uid
in needupdate
:
346 if not flag
in self
.messagelist
[uid
]['flags']:
347 self
.messagelist
[uid
]['flags'].append(flag
)
348 self
.messagelist
[uid
]['flags'].sort()
349 elif operation
== '-':
351 if flag
in self
.messagelist
[uid
]['flags']:
352 self
.messagelist
[uid
]['flags'].remove(flag
)
354 def deletemessage(self
, uid
):
355 self
.deletemessages_noconvert([uid
])
357 def deletemessages(self
, uidlist
):
358 self
.deletemessages_noconvert(uidlist
)
360 def deletemessages_noconvert(self
, uidlist
):
361 # Weed out ones not in self.messagelist
362 uidlist
= [uid
for uid
in uidlist
if uid
in self
.messagelist
]
366 self
.addmessagesflags_noconvert(uidlist
, ['T'])
367 imapobj
= self
.imapserver
.acquireconnection()
370 imapobj
.select(self
.getfullname())
371 except imapobj
.readonly
:
372 UIBase
.getglobalui().deletereadonly(self
, uidlist
)
375 assert(imapobj
.expunge()[0] == 'OK')
377 self
.imapserver
.releaseconnection(imapobj
)
379 del self
.messagelist
[uid
]