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