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