]>
code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/folder/IMAP.py
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 import rfc822
, time
, string
, random
, binascii
23 from StringIO
import StringIO
27 class IMAPFolder(BaseFolder
):
28 def __init__(self
, imapserver
, name
, visiblename
, accountname
, repository
):
29 self
.config
= imapserver
.config
30 self
.expunge
= repository
.getexpunge()
31 self
.name
= imaputil
.dequote(name
)
32 self
.root
= None # imapserver.root
33 self
.sep
= imapserver
.delim
34 self
.imapserver
= imapserver
35 self
.messagelist
= None
36 self
.visiblename
= visiblename
37 self
.accountname
= accountname
38 self
.repository
= repository
39 self
.randomgenerator
= random
.Random()
40 BaseFolder
.__init
__(self
)
42 def getaccountname(self
):
43 return self
.accountname
45 def suggeststhreads(self
):
48 def waitforthread(self
):
49 self
.imapserver
.connectionwait()
51 def getcopyinstancelimit(self
):
52 return 'MSGCOPY_' + self
.repository
.getname()
54 def getvisiblename(self
):
55 return self
.visiblename
57 def getuidvalidity(self
):
58 imapobj
= self
.imapserver
.acquireconnection()
60 # Primes untagged_responses
61 imapobj
.select(self
.getfullname(), readonly
= 1)
62 return long(imapobj
.untagged_responses
['UIDVALIDITY'][0])
64 self
.imapserver
.releaseconnection(imapobj
)
66 def cachemessagelist(self
):
67 imapobj
= self
.imapserver
.acquireconnection()
71 # Primes untagged_responses
72 assert(imapobj
.select(self
.getfullname(), readonly
= 1)[0] == 'OK')
74 # Some mail servers do not return an EXISTS response if
75 # the folder is empty.
76 maxmsgid
= long(imapobj
.untagged_responses
['EXISTS'][0])
83 # Now, get the flags and UIDs for these.
84 # We could conceivably get rid of maxmsgid and just say
86 response
= imapobj
.fetch('1:%d' % maxmsgid
, '(FLAGS UID)')[1]
88 self
.imapserver
.releaseconnection(imapobj
)
89 for messagestr
in response
:
90 # Discard the message number.
91 messagestr
= string
.split(messagestr
, maxsplit
= 1)[1]
92 options
= imaputil
.flags2hash(messagestr
)
93 if not options
.has_key('UID'):
94 UIBase
.getglobalui().warn('No UID in message with options %s' %\
98 uid
= long(options
['UID'])
99 flags
= imaputil
.flagsimap2maildir(options
['FLAGS'])
100 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
}
102 def getmessagelist(self
):
103 return self
.messagelist
105 def getmessage(self
, uid
):
106 imapobj
= self
.imapserver
.acquireconnection()
108 imapobj
.select(self
.getfullname(), readonly
= 1)
109 return imapobj
.uid('fetch', '%d' % uid
, '(BODY.PEEK[])')[1][0][1].replace("\r\n", "\n")
111 self
.imapserver
.releaseconnection(imapobj
)
113 def getmessageflags(self
, uid
):
114 return self
.messagelist
[uid
]['flags']
116 def savemessage_getnewheader(self
, content
):
117 headername
= 'X-OfflineIMAP-%s-' % str(binascii
.crc32(content
)).replace('-', 'x')
118 headername
+= binascii
.hexlify(self
.repository
.getname()) + '-'
119 headername
+= binascii
.hexlify(self
.getname())
120 headervalue
= '%d-' % long(time
.time())
121 headervalue
+= str(self
.randomgenerator
.random()).replace('.', '')
122 return (headername
, headervalue
)
124 def savemessage_addheader(self
, content
, headername
, headervalue
):
125 insertionpoint
= content
.find("\r\n")
126 leader
= content
[0:insertionpoint
]
127 newline
= "\r\n%s: %s" % (headername
, headervalue
)
128 trailer
= content
[insertionpoint
:]
129 return leader
+ newline
+ trailer
131 def savemessage_searchforheader(self
, imapobj
, headername
, headervalue
):
132 # Now find the UID it got.
133 headervalue
= imapobj
._quote
(headervalue
)
135 matchinguids
= imapobj
.uid('search', None,
136 '(HEADER %s %s)' % (headername
, headervalue
))[1][0]
137 except imapobj
.error
:
138 # IMAP server doesn't implement search or had a problem.
140 matchinguids
= matchinguids
.split(' ')
141 if len(matchinguids
) != 1 or matchinguids
[0] == None:
142 raise ValueError, "While attempting to find UID for message with header %s, got wrong-sized matchinguids of %s" % (headername
, str(matchinguids
))
144 return long(matchinguids
[0])
146 def savemessage(self
, uid
, content
, flags
):
147 imapobj
= self
.imapserver
.acquireconnection()
150 imapobj
.select(self
.getfullname()) # Needed for search
151 except imapobj
.readonly
:
152 UIBase
.getglobalui().msgtoreadonly(self
, uid
, content
, flags
)
153 # Return indicating message taken, but no UID assigned.
157 # This backend always assigns a new uid, so the uid arg is ignored.
158 # In order to get the new uid, we need to save off the message ID.
160 message
= rfc822
.Message(StringIO(content
))
161 datetuple
= rfc822
.parsedate(message
.getheader('Date'))
162 # Will be None if missing or not in a valid format.
163 if datetuple
== None:
164 datetuple
= time
.localtime()
166 if datetuple
[0] < 1981:
168 # This could raise a value error if it's not a valid format.
169 date
= imaplib
.Time2Internaldate(datetuple
)
171 # Argh, sometimes it's a valid format but year is 0102
172 # or something. Argh. It seems that Time2Internaldate
173 # will rause a ValueError if the year is 0102 but not 1902,
174 # but some IMAP servers nonetheless choke on 1902.
175 date
= imaplib
.Time2Internaldate(time
.localtime())
177 content
= re
.sub("[^\r]\n", "\r\n", content
)
179 (headername
, headervalue
) = self
.savemessage_getnewheader(content
)
180 content
= self
.savemessage_addheader(content
, headername
,
183 assert(imapobj
.append(self
.getfullname(),
184 imaputil
.flagsmaildir2imap(flags
),
185 date
, content
)[0] == 'OK')
187 # Checkpoint. Let it write out the messages, etc.
188 assert(imapobj
.check()[0] == 'OK')
190 # Keep trying until we get the UID.
192 uid
= self
.savemessage_searchforheader(imapobj
, headername
,
195 assert(imapobj
.noop()[0] == 'OK')
196 uid
= self
.savemessage_searchforheader(imapobj
, headername
,
199 self
.imapserver
.releaseconnection(imapobj
)
201 self
.messagelist
[uid
] = {'uid': uid
, 'flags': flags
}
204 def savemessageflags(self
, uid
, flags
):
205 imapobj
= self
.imapserver
.acquireconnection()
208 imapobj
.select(self
.getfullname())
209 except imapobj
.readonly
:
210 UIBase
.getglobalui().flagstoreadonly(self
, [uid
], flags
)
212 result
= imapobj
.uid('store', '%d' % uid
, 'FLAGS',
213 imaputil
.flagsmaildir2imap(flags
))
214 assert result
[0] == 'OK', 'Error with store: ' + r
[1]
216 self
.imapserver
.releaseconnection(imapobj
)
217 result
= result
[1][0]
219 self
.messagelist
[uid
]['flags'] = flags
221 flags
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])['FLAGS']
222 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
224 def addmessageflags(self
, uid
, flags
):
225 self
.addmessagesflags([uid
], flags
)
227 def addmessagesflags_noconvert(self
, uidlist
, flags
):
228 self
.processmessagesflags('+', uidlist
, flags
)
230 def addmessagesflags(self
, uidlist
, flags
):
231 """This is here for the sake of UIDMaps.py -- deletemessages must
232 add flags and get a converted UID, and if we don't have noconvert,
233 then UIDMaps will try to convert it twice."""
234 self
.addmessagesflags_noconvert(uidlist
, flags
)
236 def deletemessageflags(self
, uid
, flags
):
237 self
.deletemessagesflags([uid
], flags
)
239 def deletemessagesflags(self
, uidlist
, flags
):
240 self
.processmessagesflags('-', uidlist
, flags
)
242 def processmessagesflags(self
, operation
, uidlist
, flags
):
243 imapobj
= self
.imapserver
.acquireconnection()
246 imapobj
.select(self
.getfullname())
247 except imapobj
.readonly
:
248 UIBase
.getglobalui().flagstoreadonly(self
, uidlist
, flags
)
250 r
= imapobj
.uid('store',
251 imaputil
.listjoin(uidlist
),
253 imaputil
.flagsmaildir2imap(flags
))
254 assert r
[0] == 'OK', 'Error with store: ' + r
[1]
257 self
.imapserver
.releaseconnection(imapobj
)
258 # Some IMAP servers do not always return a result. Therefore,
259 # only update the ones that it talks about, and manually fix
261 needupdate
= copy(uidlist
)
264 # Compensate for servers that don't return anything from
267 attributehash
= imaputil
.flags2hash(imaputil
.imapsplit(result
)[1])
268 if not ('UID' in attributehash
and 'FLAGS' in attributehash
):
269 # Compensate for servers that don't return a UID attribute.
271 flags
= attributehash
['FLAGS']
272 uid
= long(attributehash
['UID'])
273 self
.messagelist
[uid
]['flags'] = imaputil
.flagsimap2maildir(flags
)
275 needupdate
.remove(uid
)
276 except ValueError: # Let it slide if it's not in the list
278 for uid
in needupdate
:
281 if not flag
in self
.messagelist
[uid
]['flags']:
282 self
.messagelist
[uid
]['flags'].append(flag
)
283 self
.messagelist
[uid
]['flags'].sort()
284 elif operation
== '-':
286 if flag
in self
.messagelist
[uid
]['flags']:
287 self
.messagelist
[uid
]['flags'].remove(flag
)
289 def deletemessage(self
, uid
):
290 self
.deletemessages_noconvert([uid
])
292 def deletemessages(self
, uidlist
):
293 self
.deletemessages_noconvert(uidlist
)
295 def deletemessages_noconvert(self
, uidlist
):
296 # Weed out ones not in self.messagelist
297 uidlist
= [uid
for uid
in uidlist
if uid
in self
.messagelist
]
301 self
.addmessagesflags_noconvert(uidlist
, ['T'])
302 imapobj
= self
.imapserver
.acquireconnection()
305 imapobj
.select(self
.getfullname())
306 except imapobj
.readonly
:
307 UIBase
.getglobalui().deletereadonly(self
, uidlist
)
310 assert(imapobj
.expunge()[0] == 'OK')
312 self
.imapserver
.releaseconnection(imapobj
)
314 del self
.messagelist
[uid
]