]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/folder/IMAP.py
/offlineimap/head: changeset 308
[offlineimap] / offlineimap / head / offlineimap / folder / IMAP.py
1 # IMAP folder support
2 # Copyright (C) 2002 John Goerzen
3 # <jgoerzen@complete.org>
4 #
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.
9 #
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.
14 #
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
18
19 from Base import BaseFolder
20 from offlineimap import imaputil, imaplib
21 from offlineimap.ui import UIBase
22 import rfc822, time, string
23 from StringIO import StringIO
24 from copy import copy
25
26
27 class IMAPFolder(BaseFolder):
28 def __init__(self, imapserver, name, visiblename, accountname, repository):
29 self.config = imapserver.config
30 self.expunge = 1
31 if self.config.has_option(accountname, 'expunge'):
32 self.expunge = self.config.getboolean(accountname, 'expunge')
33 self.name = imaputil.dequote(name)
34 self.root = None # imapserver.root
35 self.sep = imapserver.delim
36 self.imapserver = imapserver
37 self.messagelist = None
38 self.visiblename = visiblename
39 self.accountname = accountname
40 self.repository = repository
41
42 def getaccountname(self):
43 return self.accountname
44
45 def suggeststhreads(self):
46 return 1
47
48 def waitforthread(self):
49 self.imapserver.connectionwait()
50
51 def getcopyinstancelimit(self):
52 return 'MSGCOPY_' + self.accountname
53
54 def getvisiblename(self):
55 return self.visiblename
56
57 def getuidvalidity(self):
58 imapobj = self.imapserver.acquireconnection()
59 try:
60 # Primes untagged_responses
61 imapobj.select(self.getfullname(), readonly = 1)
62 return long(imapobj.untagged_responses['UIDVALIDITY'][0])
63 finally:
64 self.imapserver.releaseconnection(imapobj)
65
66 def cachemessagelist(self):
67 imapobj = self.imapserver.acquireconnection()
68 self.messagelist = {}
69
70 try:
71 # Primes untagged_responses
72 imapobj.select(self.getfullname(), readonly = 1)
73 maxmsgid = long(imapobj.untagged_responses['EXISTS'][0])
74 if maxmsgid < 1:
75 # No messages; return
76 return
77
78 # Now, get the flags and UIDs for these.
79 # We could conceivably get rid of maxmsgid and just say
80 # '1:*' here.
81 response = imapobj.fetch('1:%d' % maxmsgid, '(FLAGS UID)')[1]
82 finally:
83 self.imapserver.releaseconnection(imapobj)
84 for messagestr in response:
85 # Discard the message number.
86 messagestr = string.split(messagestr, maxsplit = 1)[1]
87 options = imaputil.flags2hash(messagestr)
88 if not options.has_key('UID'):
89 UIBase.getglobalui().warn('No UID in message with options %s' %\
90 str(options),
91 minor = 1)
92 else:
93 uid = long(options['UID'])
94 flags = imaputil.flagsimap2maildir(options['FLAGS'])
95 self.messagelist[uid] = {'uid': uid, 'flags': flags}
96
97 def getmessagelist(self):
98 return self.messagelist
99
100 def getmessage(self, uid):
101 imapobj = self.imapserver.acquireconnection()
102 try:
103 imapobj.select(self.getfullname(), readonly = 1)
104 return imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])')[1][0][1].replace("\r\n", "\n")
105 finally:
106 self.imapserver.releaseconnection(imapobj)
107
108 def getmessageflags(self, uid):
109 return self.messagelist[uid]['flags']
110
111 def savemessage(self, uid, content, flags):
112 imapobj = self.imapserver.acquireconnection()
113 try:
114 try:
115 imapobj.select(self.getfullname()) # Needed for search
116 except imapobj.readonly:
117 UIBase.getglobalui().msgtoreadonly(self, uid, content, flags)
118 # Return indicating message taken, but no UID assigned.
119 # Fudge it.
120 return 0
121
122 # This backend always assigns a new uid, so the uid arg is ignored.
123 # In order to get the new uid, we need to save off the message ID.
124
125 message = rfc822.Message(StringIO(content))
126 mid = message.getheader('Message-Id')
127 if mid != None:
128 mid = imapobj._quote(mid)
129 datetuple = rfc822.parsedate(message.getheader('Date'))
130 # Will be None if missing or not in a valid format.
131 if datetuple == None:
132 datetuple = time.localtime()
133 try:
134 if datetuple[0] < 1981:
135 raise ValueError
136 # This could raise a value error if it's not a valid format.
137 date = imaplib.Time2Internaldate(datetuple)
138 except ValueError:
139 # Argh, sometimes it's a valid format but year is 0102
140 # or something. Argh. It seems that Time2Internaldate
141 # will rause a ValueError if the year is 0102 but not 1902,
142 # but some IMAP servers nonetheless choke on 1902.
143 date = imaplib.Time2Internaldate(time.localtime())
144
145 if content.find("\r\n") == -1: # Convert line endings if not already
146 content = content.replace("\n", "\r\n")
147
148 assert(imapobj.append(self.getfullname(),
149 imaputil.flagsmaildir2imap(flags),
150 date, content)[0] == 'OK')
151 # Checkpoint. Let it write out the messages, etc.
152 assert(imapobj.check()[0] == 'OK')
153 if mid == None:
154 # No message ID in original message -- no sense trying to
155 # search for it.
156 return 0
157 # Now find the UID it got.
158 try:
159 matchinguids = imapobj.uid('search', None,
160 '(HEADER Message-Id %s)' % mid)[1][0]
161 except imapobj.error:
162 # IMAP server doesn't implement search or had a problem.
163 return 0
164 matchinguids = matchinguids.split(' ')
165 if len(matchinguids) != 1 or matchinguids[0] == None:
166 return 0
167 matchinguids.sort()
168 try:
169 uid = long(matchinguids[-1])
170 except ValueError:
171 return 0
172 self.messagelist[uid] = {'uid': uid, 'flags': flags}
173 return uid
174 finally:
175 self.imapserver.releaseconnection(imapobj)
176
177 def savemessageflags(self, uid, flags):
178 imapobj = self.imapserver.acquireconnection()
179 try:
180 try:
181 imapobj.select(self.getfullname())
182 except imapobj.readonly:
183 UIBase.getglobalui().flagstoreadonly(self, [uid], flags)
184 return
185 result = imapobj.uid('store', '%d' % uid, 'FLAGS',
186 imaputil.flagsmaildir2imap(flags))
187 assert result[0] == 'OK', 'Error with store: ' + r[1]
188 finally:
189 self.imapserver.releaseconnection(imapobj)
190 result = result[1][0]
191 if not result:
192 self.messagelist[uid]['flags'] = flags
193 else:
194 flags = imaputil.flags2hash(imaputil.imapsplit(result)[1])['FLAGS']
195 self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
196
197 def addmessageflags(self, uid, flags):
198 self.addmessagesflags([uid], flags)
199
200 def addmessagesflags(self, uidlist, flags):
201 imapobj = self.imapserver.acquireconnection()
202 try:
203 try:
204 imapobj.select(self.getfullname())
205 except imapobj.readonly:
206 UIBase.getglobalui().flagstoreadonly(self, uidlist, flags)
207 return
208 r = imapobj.uid('store',
209 imaputil.listjoin(uidlist),
210 '+FLAGS',
211 imaputil.flagsmaildir2imap(flags))
212 assert r[0] == 'OK', 'Error with store: ' + r[1]
213 r = r[1]
214 finally:
215 self.imapserver.releaseconnection(imapobj)
216 # Some IMAP servers do not always return a result. Therefore,
217 # only update the ones that it talks about, and manually fix
218 # the others.
219 needupdate = copy(uidlist)
220 for result in r:
221 if result == None:
222 # Compensate for servers that don't return anything from
223 # STORE.
224 continue
225 attributehash = imaputil.flags2hash(imaputil.imapsplit(result)[1])
226 if not ('UID' in attributehash and 'FLAGS' in attributehash):
227 # Compensate for servers that don't return a UID attribute.
228 continue
229 flags = attributehash['FLAGS']
230 uid = long(attributehash['UID'])
231 self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
232 try:
233 needupdate.remove(uid)
234 except ValueError: # Let it slide if it's not in the list
235 pass
236 for uid in needupdate:
237 for flag in flags:
238 if not flag in self.messagelist[uid]['flags']:
239 self.messagelist[uid]['flags'].append(flag)
240 self.messagelist[uid]['flags'].sort()
241
242 def deletemessage(self, uid):
243 self.deletemessages([uid])
244
245 def deletemessages(self, uidlist):
246 # Weed out ones not in self.messagelist
247 uidlist = [uid for uid in uidlist if uid in self.messagelist]
248 if not len(uidlist):
249 return
250
251 self.addmessagesflags(uidlist, ['T'])
252 imapobj = self.imapserver.acquireconnection()
253 try:
254 try:
255 imapobj.select(self.getfullname())
256 except imapobj.readonly:
257 UIBase.getglobalui().deletereadonly(self, uidlist)
258 return
259 if self.expunge:
260 assert(imapobj.expunge()[0] == 'OK')
261 finally:
262 self.imapserver.releaseconnection(imapobj)
263 for uid in uidlist:
264 del self.messagelist[uid]
265
266