]>
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., 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 ui
= UIBase
.getglobalui()
108 imapobj
= self
.imapserver
.acquireconnection()
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")
117 self
.imapserver
.releaseconnection(imapobj
)
119 def getmessageflags(self
, uid
):
120 return self
.messagelist
[uid
]['flags']
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
)
131 def savemessage_addheader(self
, content
, headername
, headervalue
):
132 ui
= UIBase
.getglobalui()
134 'savemessage_addheader: called to add %s: %s' % (headername
,
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:
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
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])
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
)
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.
166 ui
.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids
))
168 matchinguids
= matchinguids
.split(' ')
169 ui
.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \
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
))
174 return long(matchinguids
[0])
176 def savemessage(self
, uid
, content
, flags
):
177 imapobj
= self
.imapserver
.acquireconnection()
178 ui
= UIBase
.getglobalui()
179 ui
.debug('imap', 'savemessage: called')
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.
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.
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()
198 if datetuple
[0] < 1981:
200 # This could raise a value error if it's not a valid format.
201 date
= imaplib
.Time2Internaldate(datetuple
)
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())
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
))
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
,
218 ui
.debug('imap', 'savemessage: new content is: ' + repr(content
))
219 ui
.debug('imap', 'savemessage: new content length is ' + \
222 assert(imapobj
.append(self
.getfullname(),
223 imaputil
.flagsmaildir2imap(flags
),
224 date
, content
)[0] == 'OK')
226 # Checkpoint. Let it write out the messages, etc.
227 assert(imapobj
.check()[0] == 'OK')
229 # Keep trying until we get the UID.
231 ui
.debug('imap', 'savemessage: first attempt to get new UID')
232 uid
= self
.savemessage_searchforheader(imapobj
, headername
,
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
,
240 self
.imapserver
.releaseconnection(imapobj
)
242 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
}
243 ui
.debug('imap', 'savemessage: returning %d' % uid
)
246 def savemessageflags(self
, uid
, flags
):
247 imapobj
= self
.imapserver
.acquireconnection()
250 imapobj
.select(self
.getfullname())
251 except imapobj
.readonly
:
252 UIBase
.getglobalui().flagstoreadonly(self
, [uid
], flags
)
254 result
= imapobj
.uid('store', '%d' % uid
, 'FLAGS',
255 imaputil
.flagsmaildir2imap(flags
))
256 assert result
[0] == 'OK', 'Error with store: ' + r
[1]
258 self
.imapserver
.releaseconnection(imapobj
)
259 result
= result
[1][0]
261 self
.messagelist
[uid
]['flags'] = flags
263 flags
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])['FLAGS']
264 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
266 def addmessageflags(self
, uid
, flags
):
267 self
.addmessagesflags([uid
], flags
)
269 def addmessagesflags_noconvert(self
, uidlist
, flags
):
270 self
.processmessagesflags('+', uidlist
, flags
)
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
)
278 def deletemessageflags(self
, uid
, flags
):
279 self
.deletemessagesflags([uid
], flags
)
281 def deletemessagesflags(self
, uidlist
, flags
):
282 self
.processmessagesflags('-', uidlist
, flags
)
284 def processmessagesflags(self
, operation
, uidlist
, flags
):
285 imapobj
= self
.imapserver
.acquireconnection()
288 imapobj
.select(self
.getfullname())
289 except imapobj
.readonly
:
290 UIBase
.getglobalui().flagstoreadonly(self
, uidlist
, flags
)
292 r
= imapobj
.uid('store',
293 imaputil
.listjoin(uidlist
),
295 imaputil
.flagsmaildir2imap(flags
))
296 assert r
[0] == 'OK', 'Error with store: ' + r
[1]
299 self
.imapserver
.releaseconnection(imapobj
)
300 # Some IMAP servers do not always return a result. Therefore,
301 # only update the ones that it talks about, and manually fix
303 needupdate
= copy(uidlist
)
306 # Compensate for servers that don't return anything from
309 attributehash
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])
310 if not ('UID' in attributehash
and 'FLAGS' in attributehash
):
311 # Compensate for servers that don't return a UID attribute.
313 flags
= attributehash
['FLAGS']
314 uid
= long(attributehash
['UID'])
315 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
317 needupdate
.remove(uid
)
318 except ValueError: # Let it slide if it's not in the list
320 for uid
in needupdate
:
323 if not flag
in self
.messagelist
[uid
]['flags']:
324 self
.messagelist
[uid
]['flags'].append(flag
)
325 self
.messagelist
[uid
]['flags'].sort()
326 elif operation
== '-':
328 if flag
in self
.messagelist
[uid
]['flags']:
329 self
.messagelist
[uid
]['flags'].remove(flag
)
331 def deletemessage(self
, uid
):
332 self
.deletemessages_noconvert([uid
])
334 def deletemessages(self
, uidlist
):
335 self
.deletemessages_noconvert(uidlist
)
337 def deletemessages_noconvert(self
, uidlist
):
338 # Weed out ones not in self.messagelist
339 uidlist
= [uid
for uid
in uidlist
if uid
in self
.messagelist
]
343 self
.addmessagesflags_noconvert(uidlist
, ['T'])
344 imapobj
= self
.imapserver
.acquireconnection()
347 imapobj
.select(self
.getfullname())
348 except imapobj
.readonly
:
349 UIBase
.getglobalui().deletereadonly(self
, uidlist
)
352 assert(imapobj
.expunge()[0] == 'OK')
354 self
.imapserver
.releaseconnection(imapobj
)
356 del self
.messagelist
[uid
]