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