]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/folder/Base.py
/offlineimap/head: changeset 367
[offlineimap] / offlineimap / head / offlineimap / folder / Base.py
1 # Base 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 threading import *
20 from offlineimap import threadutil
21 from offlineimap.threadutil import InstanceLimitedThread
22 from offlineimap.ui import UIBase
23
24 class BaseFolder:
25 def getname(self):
26 """Returns name"""
27 return self.name
28
29 def suggeststhreads(self):
30 """Returns true if this folder suggests using threads for actions;
31 false otherwise. Probably only IMAP will return true."""
32 return 0
33
34 def waitforthread(self):
35 """For threading folders, waits until there is a resource available
36 before firing off a thread. For all others, returns immediately."""
37 pass
38
39 def getcopyinstancelimit(self):
40 """For threading folders, returns the instancelimitname for
41 InstanceLimitedThreads."""
42 raise NotImplementedException
43
44 def storesmessages(self):
45 """Should be true for any backend that actually saves message bodies.
46 (Almost all of them). False for the LocalStatus backend. Saves
47 us from having to slurp up messages just for localstatus purposes."""
48 return 1
49
50 def getvisiblename(self):
51 return self.name
52
53 def getrepository(self):
54 """Returns the repository object that this folder is within."""
55 return self.repository
56
57 def getroot(self):
58 """Returns the root of the folder, in a folder-specific fashion."""
59 return self.root
60
61 def getsep(self):
62 """Returns the separator for this folder type."""
63 return self.sep
64
65 def getfullname(self):
66 if self.getroot():
67 return self.getroot() + self.getsep() + self.getname()
68 else:
69 return self.getname()
70
71 def isuidvalidityok(self, remotefolder):
72 raise NotImplementedException
73
74 def getuidvalidity(self):
75 raise NotImplementedException
76
77 def saveuidvalidity(self, newval):
78 raise NotImplementedException
79
80 def cachemessagelist(self):
81 """Reads the message list from disk or network and stores it in
82 memory for later use. This list will not be re-read from disk or
83 memory unless this function is called again."""
84 raise NotImplementedException
85
86 def getmessagelist(self):
87 """Gets the current message list.
88 You must call cachemessagelist() before calling this function!"""
89 raise NotImplementedException
90
91 def getmessage(self, uid):
92 """Returns the content of the specified message."""
93 raise NotImplementedException
94
95 def savemessage(self, uid, content, flags):
96 """Writes a new message, with the specified uid.
97 If the uid is < 0, the backend should assign a new uid and return it.
98
99 If the backend cannot assign a new uid, it returns the uid passed in
100 WITHOUT saving the message.
101
102 If the backend CAN assign a new uid, but cannot find out what this UID
103 is (as is the case with many IMAP servers), it returns 0 but DOES save
104 the message.
105
106 IMAP backend should be the only one that can assign a new uid.
107
108 If the uid is > 0, the backend should set the uid to this, if it can.
109 If it cannot set the uid to that, it will save it anyway.
110 It will return the uid assigned in any case.
111 """
112 raise NotImplementedException
113
114 def getmessageflags(self, uid):
115 """Returns the flags for the specified message."""
116 raise NotImplementedException
117
118 def savemessageflags(self, uid, flags):
119 """Sets the specified message's flags to the given set."""
120 raise NotImplementedException
121
122 def addmessageflags(self, uid, flags):
123 """Adds the specified flags to the message's flag set. If a given
124 flag is already present, it will not be duplicated."""
125 newflags = self.getmessageflags(uid)
126 for flag in flags:
127 if not flag in newflags:
128 newflags.append(flag)
129 newflags.sort()
130 self.savemessageflags(uid, newflags)
131
132 def addmessagesflags(self, uidlist, flags):
133 for uid in uidlist:
134 self.addmessageflags(uid, flags)
135
136 def deletemessageflags(self, uid, flags):
137 """Removes each flag given from the message's flag set. If a given
138 flag is already removed, no action will be taken for that flag."""
139 newflags = self.getmessageflags(uid)
140 for flag in flags:
141 if flag in newflags:
142 newflags.remove(flag)
143 newflags.sort()
144 self.savemessageflags(uid, newflags)
145
146 def deletemessagesflags(self, uidlist, flags):
147 for uid in uidlist:
148 self.deletemessageflags(uid, flags)
149
150 def deletemessage(self, uid):
151 raise NotImplementedException
152
153 def deletemessages(self, uidlist):
154 for uid in uidlist:
155 self.deletemessage(uid)
156
157 def syncmessagesto_neguid_msg(self, uid, dest, applyto, register = 1):
158 if register:
159 UIBase.getglobalui().registerthread(self.getaccountname())
160 UIBase.getglobalui().copyingmessage(uid, self, applyto)
161 successobject = None
162 successuid = None
163 message = self.getmessage(uid)
164 flags = self.getmessageflags(uid)
165 for tryappend in applyto:
166 successuid = tryappend.savemessage(uid, message, flags)
167 if successuid >= 0:
168 successobject = tryappend
169 break
170 # Did we succeed?
171 if successobject != None:
172 if successuid: # Only if IMAP actually assigned a UID
173 # Copy the message to the other remote servers.
174 for appendserver in \
175 [x for x in applyto if x != successobject]:
176 appendserver.savemessage(successuid, message, flags)
177 # Copy to its new name on the local server and delete
178 # the one without a UID.
179 self.savemessage(successuid, message, flags)
180 self.deletemessage(uid) # It'll be re-downloaded.
181 else:
182 # Did not find any server to take this message. Ignore.
183 pass
184
185
186 def syncmessagesto_neguid(self, dest, applyto):
187 """Pass 1 of folder synchronization.
188
189 Look for messages in self with a negative uid. These are messages in
190 Maildirs that were not added by us. Try to add them to the dests,
191 and once that succeeds, get the UID, add it to the others for real,
192 add it to local for real, and delete the fake one."""
193
194 uidlist = [uid for uid in self.getmessagelist().keys() if uid < 0]
195 threads = []
196
197 usethread = None
198 if applyto != None:
199 usethread = applyto[0]
200
201 for uid in uidlist:
202 if usethread and usethread.suggeststhreads():
203 usethread.waitforthread()
204 thread = InstanceLimitedThread(\
205 usethread.getcopyinstancelimit(),
206 target = self.syncmessagesto_neguid_msg,
207 name = "New msg sync from %s" % self.getvisiblename(),
208 args = (uid, dest, applyto))
209 thread.setDaemon(1)
210 thread.start()
211 threads.append(thread)
212 else:
213 self.syncmessagesto_neguid_msg(uid, dest, applyto, register = 0)
214 for thread in threads:
215 thread.join()
216
217 def copymessageto(self, uid, applyto, register = 1):
218 # Sometimes, it could be the case that if a sync takes awhile,
219 # a message might be deleted from the maildir before it can be
220 # synced to the status cache. This is only a problem with
221 # self.getmessage(). So, don't call self.getmessage unless
222 # really needed.
223 if register:
224 UIBase.getglobalui().registerthread(self.getaccountname())
225 UIBase.getglobalui().copyingmessage(uid, self, applyto)
226 message = ''
227 # If any of the destinations actually stores the message body,
228 # load it up.
229 for object in applyto:
230 if object.storesmessages():
231 message = self.getmessage(uid)
232 break
233 flags = self.getmessageflags(uid)
234 for object in applyto:
235 newuid = object.savemessage(uid, message, flags)
236 if newuid > 0 and newuid != uid:
237 # Change the local uid.
238 self.savemessage(newuid, message, flags)
239 self.deletemessage(uid)
240 uid = newuid
241
242
243 def syncmessagesto_copy(self, dest, applyto):
244 """Pass 2 of folder synchronization.
245
246 Look for messages present in self but not in dest. If any, add
247 them to dest."""
248 threads = []
249
250 for uid in self.getmessagelist().keys():
251 if uid < 0: # Ignore messages that pass 1 missed.
252 continue
253 if not uid in dest.getmessagelist():
254 if self.suggeststhreads():
255 self.waitforthread()
256 thread = InstanceLimitedThread(\
257 self.getcopyinstancelimit(),
258 target = self.copymessageto,
259 name = "Copy message %d from %s" % (uid,
260 self.getvisiblename()),
261 args = (uid, applyto))
262 thread.setDaemon(1)
263 thread.start()
264 threads.append(thread)
265 else:
266 self.copymessageto(uid, applyto, register = 0)
267 for thread in threads:
268 thread.join()
269
270 def syncmessagesto_delete(self, dest, applyto):
271 """Pass 3 of folder synchronization.
272
273 Look for message present in dest but not in self.
274 If any, delete them."""
275 deletelist = []
276 for uid in dest.getmessagelist().keys():
277 if uid < 0:
278 continue
279 if not uid in self.getmessagelist():
280 deletelist.append(uid)
281 if len(deletelist):
282 UIBase.getglobalui().deletingmessages(deletelist, applyto)
283 for object in applyto:
284 object.deletemessages(deletelist)
285
286 def syncmessagesto_flags(self, dest, applyto):
287 """Pass 4 of folder synchronization.
288
289 Look for any flag matching issues -- set dest message to have the
290 same flags that we have."""
291
292 # As an optimization over previous versions, we store up which flags
293 # are being used for an add or a delete. For each flag, we store
294 # a list of uids to which it should be added. Then, we can call
295 # addmessagesflags() to apply them in bulk, rather than one
296 # call per message as before. This should result in some significant
297 # performance improvements.
298
299 addflaglist = {}
300 delflaglist = {}
301
302 for uid in self.getmessagelist().keys():
303 if uid < 0: # Ignore messages missed by pass 1
304 continue
305 selfflags = self.getmessageflags(uid)
306 destflags = dest.getmessageflags(uid)
307
308 addflags = [x for x in selfflags if x not in destflags]
309
310 for flag in addflags:
311 if not flag in addflaglist:
312 addflaglist[flag] = []
313 addflaglist[flag].append(uid)
314
315 delflags = [x for x in destflags if x not in selfflags]
316 for flag in delflags:
317 if not flag in delflaglist:
318 delflaglist[flag] = []
319 delflaglist[flag].append(uid)
320
321 for object in applyto:
322 for flag in addflaglist.keys():
323 UIBase.getglobalui().addingflags(addflaglist[flag], flag, [object])
324 object.addmessagesflags(addflaglist[flag], [flag])
325 for flag in delflaglist.keys():
326 UIBase.getglobalui().deletingflags(delflaglist[flag], flag, [object])
327 object.deletemessagesflags(delflaglist[flag], [flag])
328
329 def syncmessagesto(self, dest, applyto = None):
330 """Syncs messages in this folder to the destination.
331 If applyto is specified, it should be a list of folders (don't forget
332 to include dest!) to which all write actions should be applied.
333 It defaults to [dest] if not specified. It is important that
334 the UID generator be listed first in applyto; that is, the other
335 applyto ones should be the ones that "copy" the main action."""
336 if applyto == None:
337 applyto = [dest]
338
339 self.syncmessagesto_neguid(dest, applyto)
340 self.syncmessagesto_copy(dest, applyto)
341 self.syncmessagesto_delete(dest, applyto)
342
343 # Now, the message lists should be identical wrt the uids present.
344 # (except for potential negative uids that couldn't be placed
345 # anywhere)
346
347 self.syncmessagesto_flags(dest, applyto)
348
349