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