]>
code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/folder/IMAP.py
1f36ece08c38109654a6d4240066d0f5dba1aa81
2 # Copyright (C) 2002 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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)')[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 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
}
103 def getmessagelist(self
):
104 return self
.messagelist
106 def getmessage(self
, uid
):
107 imapobj
= self
.imapserver
.acquireconnection()
109 imapobj
.select(self
.getfullname(), readonly
= 1)
110 return imapobj
.uid('fetch', '%d' % uid
, '(BODY.PEEK[])')[1][0][1].replace("\r\n", "\n")
112 self
.imapserver
.releaseconnection(imapobj
)
114 def getmessageflags(self
, uid
):
115 return self
.messagelist
[uid
]['flags']
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
)
126 def savemessage_addheader(self
, content
, headername
, headervalue
):
127 ui
= UIBase
.getglobalui()
129 'savemessage_addheader: called to add %s: %s' % (headername
,
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:
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
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])
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
)
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.
161 ui
.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids
))
163 matchinguids
= matchinguids
.split(' ')
164 ui
.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \
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
))
169 return long(matchinguids
[0])
171 def savemessage(self
, uid
, content
, flags
):
172 imapobj
= self
.imapserver
.acquireconnection()
173 ui
= UIBase
.getglobalui()
174 ui
.debug('imap', 'savemessage: called')
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.
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.
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()
193 if datetuple
[0] < 1981:
195 # This could raise a value error if it's not a valid format.
196 date
= imaplib
.Time2Internaldate(datetuple
)
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())
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
))
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
,
213 ui
.debug('imap', 'savemessage: new content is: ' + repr(content
))
214 ui
.debug('imap', 'savemessage: new content length is ' + \
217 assert(imapobj
.append(self
.getfullname(),
218 imaputil
.flagsmaildir2imap(flags
),
219 date
, content
)[0] == 'OK')
221 # Checkpoint. Let it write out the messages, etc.
222 assert(imapobj
.check()[0] == 'OK')
224 # Keep trying until we get the UID.
226 ui
.debug('imap', 'savemessage: first attempt to get new UID')
227 uid
= self
.savemessage_searchforheader(imapobj
, headername
,
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
,
235 self
.imapserver
.releaseconnection(imapobj
)
237 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
}
238 ui
.debug('imap', 'savemessage: returning %d' % uid
)
241 def savemessageflags(self
, uid
, flags
):
242 imapobj
= self
.imapserver
.acquireconnection()
245 imapobj
.select(self
.getfullname())
246 except imapobj
.readonly
:
247 UIBase
.getglobalui().flagstoreadonly(self
, [uid
], flags
)
249 result
= imapobj
.uid('store', '%d' % uid
, 'FLAGS',
250 imaputil
.flagsmaildir2imap(flags
))
251 assert result
[0] == 'OK', 'Error with store: ' + r
[1]
253 self
.imapserver
.releaseconnection(imapobj
)
254 result
= result
[1][0]
256 self
.messagelist
[uid
]['flags'] = flags
258 flags
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])['FLAGS']
259 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
261 def addmessageflags(self
, uid
, flags
):
262 self
.addmessagesflags([uid
], flags
)
264 def addmessagesflags_noconvert(self
, uidlist
, flags
):
265 self
.processmessagesflags('+', uidlist
, flags
)
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
)
273 def deletemessageflags(self
, uid
, flags
):
274 self
.deletemessagesflags([uid
], flags
)
276 def deletemessagesflags(self
, uidlist
, flags
):
277 self
.processmessagesflags('-', uidlist
, flags
)
279 def processmessagesflags(self
, operation
, uidlist
, flags
):
280 imapobj
= self
.imapserver
.acquireconnection()
283 imapobj
.select(self
.getfullname())
284 except imapobj
.readonly
:
285 UIBase
.getglobalui().flagstoreadonly(self
, uidlist
, flags
)
287 r
= imapobj
.uid('store',
288 imaputil
.listjoin(uidlist
),
290 imaputil
.flagsmaildir2imap(flags
))
291 assert r
[0] == 'OK', 'Error with store: ' + r
[1]
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
298 needupdate
= copy(uidlist
)
301 # Compensate for servers that don't return anything from
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.
308 flags
= attributehash
['FLAGS']
309 uid
= long(attributehash
['UID'])
310 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
312 needupdate
.remove(uid
)
313 except ValueError: # Let it slide if it's not in the list
315 for uid
in needupdate
:
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
== '-':
323 if flag
in self
.messagelist
[uid
]['flags']:
324 self
.messagelist
[uid
]['flags'].remove(flag
)
326 def deletemessage(self
, uid
):
327 self
.deletemessages_noconvert([uid
])
329 def deletemessages(self
, uidlist
):
330 self
.deletemessages_noconvert(uidlist
)
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
]
338 self
.addmessagesflags_noconvert(uidlist
, ['T'])
339 imapobj
= self
.imapserver
.acquireconnection()
342 imapobj
.select(self
.getfullname())
343 except imapobj
.readonly
:
344 UIBase
.getglobalui().deletereadonly(self
, uidlist
)
347 assert(imapobj
.expunge()[0] == 'OK')
349 self
.imapserver
.releaseconnection(imapobj
)
351 del self
.messagelist
[uid
]