]> code.delx.au - offlineimap/blob - offlineimap/accounts.py
93f5f1548befc5e638a25cd06f04851bcaed3695
[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.write()
241 localrepos.forgetfolders()
242 remoterepos.forgetfolders()
243 localrepos.holdordropconnections()
244 remoterepos.holdordropconnections()
245 finally:
246 pass
247
248 hook = self.getconf('postsynchook', '')
249 self.callhook(hook)
250
251 def callhook(self, cmd):
252 if not cmd:
253 return
254 try:
255 self.ui.callhook("Calling hook: " + cmd)
256 p = Popen(cmd, shell=True,
257 stdin=PIPE, stdout=PIPE, stderr=PIPE,
258 close_fds=True)
259 r = p.communicate()
260 self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n" % r)
261 self.ui.callhook("Hook return code: %d" % p.returncode)
262 except:
263 self.ui.warn("Exception occured while calling hook")
264
265 class SyncableAccount(Account, AccountSynchronizationMixin):
266 pass
267
268 def syncfolder(accountname, remoterepos, remotefolder, localrepos,
269 statusrepos, quick):
270 global mailboxes
271 ui = UIBase.getglobalui()
272 ui.registerthread(accountname)
273 # Load local folder.
274 localfolder = localrepos.\
275 getfolder(remotefolder.getvisiblename().\
276 replace(remoterepos.getsep(), localrepos.getsep()))
277 # Write the mailboxes
278 mbnames.add(accountname, localfolder.getvisiblename())
279
280 # Load status folder.
281 statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
282 replace(remoterepos.getsep(),
283 statusrepos.getsep()))
284 if localfolder.getuidvalidity() == None:
285 # This is a new folder, so delete the status cache to be sure
286 # we don't have a conflict.
287 statusfolder.deletemessagelist()
288
289 statusfolder.cachemessagelist()
290
291 if quick:
292 if not localfolder.quickchanged(statusfolder) \
293 and not remotefolder.quickchanged(statusfolder):
294 ui.skippingfolder(remotefolder)
295 localrepos.restore_atime()
296 return
297
298 # Load local folder
299 ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
300 ui.loadmessagelist(localrepos, localfolder)
301 localfolder.cachemessagelist()
302 ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
303
304 # If either the local or the status folder has messages and there is a UID
305 # validity problem, warn and abort. If there are no messages, UW IMAPd
306 # loses UIDVALIDITY. But we don't really need it if both local folders are
307 # empty. So, in that case, just save it off.
308 if len(localfolder.getmessagelist()) or len(statusfolder.getmessagelist()):
309 if not localfolder.isuidvalidityok():
310 ui.validityproblem(localfolder)
311 localrepos.restore_atime()
312 return
313 if not remotefolder.isuidvalidityok():
314 ui.validityproblem(remotefolder)
315 localrepos.restore_atime()
316 return
317 else:
318 localfolder.saveuidvalidity()
319 remotefolder.saveuidvalidity()
320
321 # Load remote folder.
322 ui.loadmessagelist(remoterepos, remotefolder)
323 remotefolder.cachemessagelist()
324 ui.messagelistloaded(remoterepos, remotefolder,
325 len(remotefolder.getmessagelist().keys()))
326
327
328 #
329
330 if not statusfolder.isnewfolder():
331 # Delete local copies of remote messages. This way,
332 # if a message's flag is modified locally but it has been
333 # deleted remotely, we'll delete it locally. Otherwise, we
334 # try to modify a deleted message's flags! This step
335 # need only be taken if a statusfolder is present; otherwise,
336 # there is no action taken *to* the remote repository.
337
338 remotefolder.syncmessagesto_delete(localfolder, [localfolder,
339 statusfolder])
340 ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
341 localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder])
342
343 # Synchronize remote changes.
344 ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
345 remotefolder.syncmessagesto(localfolder, [localfolder, statusfolder])
346
347 # Make sure the status folder is up-to-date.
348 ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
349 localfolder.syncmessagesto(statusfolder)
350 statusfolder.save()
351 localrepos.restore_atime()
352