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