]> code.delx.au - offlineimap/blob - offlineimap/accounts.py
Sort mbnames for each account before writing
[offlineimap] / offlineimap / accounts.py
1 # Copyright (C) 2003 John Goerzen
2 # <jgoerzen@complete.org>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17
18 from offlineimap import threadutil, mbnames, CustomConfig
19 import offlineimap.repository.Base, offlineimap.repository.LocalStatus
20 from offlineimap.ui import UIBase
21 from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
22 from subprocess import Popen, PIPE
23 from threading import Event, Lock
24 import os
25 from Queue import Queue, Empty
26
27 class SigListener(Queue):
28 def __init__(self):
29 self.folderlock = Lock()
30 self.folders = None
31 Queue.__init__(self, 20)
32 def put_nowait(self, sig):
33 self.folderlock.acquire()
34 try:
35 if sig == 1:
36 if self.folders is None or not self.autorefreshes:
37 # folders haven't yet been added, or this account is once-only; drop signal
38 return
39 elif self.folders:
40 for folder in self.folders:
41 # requeue folder
42 self.folders[folder] = True
43 self.quick = False
44 return
45 # else folders have already been cleared, put signal...
46 finally:
47 self.folderlock.release()
48 Queue.put_nowait(self, sig)
49 def addfolders(self, remotefolders, autorefreshes, quick):
50 self.folderlock.acquire()
51 try:
52 self.folders = {}
53 self.quick = quick
54 self.autorefreshes = autorefreshes
55 for folder in remotefolders:
56 # new folders are queued
57 self.folders[folder] = True
58 finally:
59 self.folderlock.release()
60 def clearfolders(self):
61 self.folderlock.acquire()
62 try:
63 for folder in self.folders:
64 if self.folders[folder]:
65 # some folders still in queue
66 return False
67 self.folders.clear()
68 return True
69 finally:
70 self.folderlock.release()
71 def queuedfolders(self):
72 self.folderlock.acquire()
73 try:
74 dirty = True
75 while dirty:
76 dirty = False
77 for folder in self.folders:
78 if self.folders[folder]:
79 # mark folder as no longer queued
80 self.folders[folder] = False
81 dirty = True
82 quick = self.quick
83 self.folderlock.release()
84 yield (folder, quick)
85 self.folderlock.acquire()
86 finally:
87 self.folderlock.release()
88
89 def getaccountlist(customconfig):
90 return customconfig.getsectionlist('Account')
91
92 def AccountListGenerator(customconfig):
93 return [Account(customconfig, accountname)
94 for accountname in getaccountlist(customconfig)]
95
96 def AccountHashGenerator(customconfig):
97 retval = {}
98 for item in AccountListGenerator(customconfig):
99 retval[item.getname()] = item
100 return retval
101
102 mailboxes = []
103
104 class Account(CustomConfig.ConfigHelperMixin):
105 def __init__(self, config, name):
106 self.config = config
107 self.name = name
108 self.metadatadir = config.getmetadatadir()
109 self.localeval = config.getlocaleval()
110 self.ui = UIBase.getglobalui()
111 self.refreshperiod = self.getconffloat('autorefresh', 0.0)
112 self.quicknum = 0
113 if self.refreshperiod == 0.0:
114 self.refreshperiod = None
115
116 def getlocaleval(self):
117 return self.localeval
118
119 def getconfig(self):
120 return self.config
121
122 def getname(self):
123 return self.name
124
125 def getsection(self):
126 return 'Account ' + self.getname()
127
128 def sleeper(self, siglistener):
129 """Sleep handler. Returns same value as UIBase.sleep:
130 0 if timeout expired, 1 if there was a request to cancel the timer,
131 and 2 if there is a request to abort the program.
132
133 Also, returns 100 if configured to not sleep at all."""
134
135 if not self.refreshperiod:
136 return 100
137
138 kaobjs = []
139
140 if hasattr(self, 'localrepos'):
141 kaobjs.append(self.localrepos)
142 if hasattr(self, 'remoterepos'):
143 kaobjs.append(self.remoterepos)
144
145 for item in kaobjs:
146 item.startkeepalive()
147
148 refreshperiod = int(self.refreshperiod * 60)
149 # try:
150 # sleepresult = siglistener.get_nowait()
151 # # retrieved signal before sleep started
152 # if sleepresult == 1:
153 # # catching signal 1 here means folders were cleared before signal was posted
154 # pass
155 # except Empty:
156 # sleepresult = self.ui.sleep(refreshperiod, siglistener)
157 sleepresult = self.ui.sleep(refreshperiod, siglistener)
158 if sleepresult == 1:
159 self.quicknum = 0
160
161 # Cancel keepalive
162 for item in kaobjs:
163 item.stopkeepalive()
164 return sleepresult
165
166 class AccountSynchronizationMixin:
167 def syncrunner(self, siglistener):
168 self.ui.registerthread(self.name)
169 self.ui.acct(self.name)
170 accountmetadata = self.getaccountmeta()
171 if not os.path.exists(accountmetadata):
172 os.mkdir(accountmetadata, 0700)
173
174 self.remoterepos = offlineimap.repository.Base.LoadRepository(self.getconf('remoterepository'), self, 'remote')
175
176 # Connect to the local repository.
177 self.localrepos = offlineimap.repository.Base.LoadRepository(self.getconf('localrepository'), self, 'local')
178
179 # Connect to the local cache.
180 self.statusrepos = offlineimap.repository.LocalStatus.LocalStatusRepository(self.getconf('localrepository'), self)
181
182 if not self.refreshperiod:
183 self.sync(siglistener)
184 self.ui.acctdone(self.name)
185 return
186 looping = 1
187 while looping:
188 self.sync(siglistener)
189 looping = self.sleeper(siglistener) != 2
190 self.ui.acctdone(self.name)
191
192 def getaccountmeta(self):
193 return os.path.join(self.metadatadir, 'Account-' + self.name)
194
195 def sync(self, siglistener):
196 # We don't need an account lock because syncitall() goes through
197 # each account once, then waits for all to finish.
198
199 hook = self.getconf('presynchook', '')
200 self.callhook(hook)
201
202 quickconfig = self.getconfint('quick', 0)
203 if quickconfig < 0:
204 quick = True
205 elif quickconfig > 0:
206 if self.quicknum == 0 or self.quicknum > quickconfig:
207 self.quicknum = 1
208 quick = False
209 else:
210 self.quicknum = self.quicknum + 1
211 quick = True
212 else:
213 quick = False
214
215 try:
216 remoterepos = self.remoterepos
217 localrepos = self.localrepos
218 statusrepos = self.statusrepos
219 self.ui.syncfolders(remoterepos, localrepos)
220 remoterepos.syncfoldersto(localrepos, [statusrepos])
221
222 siglistener.addfolders(remoterepos.getfolders(), bool(self.refreshperiod), quick)
223
224 while True:
225 folderthreads = []
226 for remotefolder, quick in siglistener.queuedfolders():
227 thread = InstanceLimitedThread(\
228 instancename = 'FOLDER_' + self.remoterepos.getname(),
229 target = syncfolder,
230 name = "Folder sync %s[%s]" % \
231 (self.name, remotefolder.getvisiblename()),
232 args = (self.name, remoterepos, remotefolder, localrepos,
233 statusrepos, quick))
234 thread.setDaemon(1)
235 thread.start()
236 folderthreads.append(thread)
237 threadutil.threadsreset(folderthreads)
238 if siglistener.clearfolders():
239 break
240 mbnames.sort(self.name, remoterepos.foldersort)
241 mbnames.write()
242 localrepos.forgetfolders()
243 remoterepos.forgetfolders()
244 localrepos.holdordropconnections()
245 remoterepos.holdordropconnections()
246 finally:
247 pass
248
249 hook = self.getconf('postsynchook', '')
250 self.callhook(hook)
251
252 def callhook(self, cmd):
253 if not cmd:
254 return
255 try:
256 self.ui.callhook("Calling hook: " + cmd)
257 p = Popen(cmd, shell=True,
258 stdin=PIPE, stdout=PIPE, stderr=PIPE,
259 close_fds=True)
260 r = p.communicate()
261 self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n" % r)
262 self.ui.callhook("Hook return code: %d" % p.returncode)
263 except:
264 self.ui.warn("Exception occured while calling hook")
265
266 class SyncableAccount(Account, AccountSynchronizationMixin):
267 pass
268
269 def syncfolder(accountname, remoterepos, remotefolder, localrepos,
270 statusrepos, quick):
271 global mailboxes
272 ui = UIBase.getglobalui()
273 ui.registerthread(accountname)
274 # Load local folder.
275 localfolder = localrepos.\
276 getfolder(remotefolder.getvisiblename().\
277 replace(remoterepos.getsep(), localrepos.getsep()))
278 # Write the mailboxes
279 mbnames.add(accountname, localfolder.getvisiblename())
280
281 # Load status folder.
282 statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
283 replace(remoterepos.getsep(),
284 statusrepos.getsep()))
285 if localfolder.getuidvalidity() == None:
286 # This is a new folder, so delete the status cache to be sure
287 # we don't have a conflict.
288 statusfolder.deletemessagelist()
289
290 statusfolder.cachemessagelist()
291
292 if quick:
293 if not localfolder.quickchanged(statusfolder) \
294 and not remotefolder.quickchanged(statusfolder):
295 ui.skippingfolder(remotefolder)
296 localrepos.restore_atime()
297 return
298
299 # Load local folder
300 ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
301 ui.loadmessagelist(localrepos, localfolder)
302 localfolder.cachemessagelist()
303 ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
304
305 # If either the local or the status folder has messages and there is a UID
306 # validity problem, warn and abort. If there are no messages, UW IMAPd
307 # loses UIDVALIDITY. But we don't really need it if both local folders are
308 # empty. So, in that case, just save it off.
309 if len(localfolder.getmessagelist()) or len(statusfolder.getmessagelist()):
310 if not localfolder.isuidvalidityok():
311 ui.validityproblem(localfolder)
312 localrepos.restore_atime()
313 return
314 if not remotefolder.isuidvalidityok():
315 ui.validityproblem(remotefolder)
316 localrepos.restore_atime()
317 return
318 else:
319 localfolder.saveuidvalidity()
320 remotefolder.saveuidvalidity()
321
322 # Load remote folder.
323 ui.loadmessagelist(remoterepos, remotefolder)
324 remotefolder.cachemessagelist()
325 ui.messagelistloaded(remoterepos, remotefolder,
326 len(remotefolder.getmessagelist().keys()))
327
328
329 #
330
331 if not statusfolder.isnewfolder():
332 # Delete local copies of remote messages. This way,
333 # if a message's flag is modified locally but it has been
334 # deleted remotely, we'll delete it locally. Otherwise, we
335 # try to modify a deleted message's flags! This step
336 # need only be taken if a statusfolder is present; otherwise,
337 # there is no action taken *to* the remote repository.
338
339 remotefolder.syncmessagesto_delete(localfolder, [localfolder,
340 statusfolder])
341 ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
342 localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder])
343
344 # Synchronize remote changes.
345 ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
346 remotefolder.syncmessagesto(localfolder, [localfolder, statusfolder])
347
348 # Make sure the status folder is up-to-date.
349 ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
350 localfolder.syncmessagesto(statusfolder)
351 statusfolder.save()
352 localrepos.restore_atime()
353