]> code.delx.au - offlineimap/blob - offlineimap/accounts.py
8e96347944f58c37fcada83fd98fb18dca16e23a
[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 syncrunner(self):
99 self.ui.registerthread(self.name)
100 self.ui.acct(self.name)
101 accountmetadata = self.getaccountmeta()
102 if not os.path.exists(accountmetadata):
103 os.mkdir(accountmetadata, 0700)
104
105 self.remoterepos = offlineimap.repository.Base.LoadRepository(self.getconf('remoterepository'), self, 'remote')
106
107 # Connect to the local repository.
108 self.localrepos = offlineimap.repository.Base.LoadRepository(self.getconf('localrepository'), self, 'local')
109
110 # Connect to the local cache.
111 self.statusrepos = offlineimap.repository.LocalStatus.LocalStatusRepository(self.getconf('localrepository'), self)
112
113 if not self.refreshperiod:
114 self.sync()
115 self.ui.acctdone(self.name)
116 return
117 looping = 1
118 while looping:
119 self.sync()
120 looping = self.sleeper() != 2
121 self.ui.acctdone(self.name)
122
123 def getaccountmeta(self):
124 return os.path.join(self.metadatadir, 'Account-' + self.name)
125
126 def sync(self):
127 # We don't need an account lock because syncitall() goes through
128 # each account once, then waits for all to finish.
129
130 quickconfig = self.getconfint('quick', 0)
131 if quickconfig < 0:
132 quick = True
133 elif quickconfig > 0:
134 if self.quicknum == 0 or self.quicknum > quickconfig:
135 self.quicknum = 1
136 quick = False
137 else:
138 self.quicknum = self.quicknum + 1
139 quick = True
140 else:
141 quick = False
142
143 try:
144 remoterepos = self.remoterepos
145 localrepos = self.localrepos
146 statusrepos = self.statusrepos
147 self.ui.syncfolders(remoterepos, localrepos)
148 remoterepos.syncfoldersto(localrepos, [statusrepos])
149
150 folderthreads = []
151 for remotefolder in remoterepos.getfolders():
152 thread = InstanceLimitedThread(\
153 instancename = 'FOLDER_' + self.remoterepos.getname(),
154 target = syncfolder,
155 name = "Folder sync %s[%s]" % \
156 (self.name, remotefolder.getvisiblename()),
157 args = (self.name, remoterepos, remotefolder, localrepos,
158 statusrepos, quick))
159 thread.setDaemon(1)
160 thread.start()
161 folderthreads.append(thread)
162 threadutil.threadsreset(folderthreads)
163 mbnames.write()
164 localrepos.forgetfolders()
165 remoterepos.forgetfolders()
166 localrepos.holdordropconnections()
167 remoterepos.holdordropconnections()
168 finally:
169 pass
170
171 class SyncableAccount(Account, AccountSynchronizationMixin):
172 pass
173
174 def syncfolder(accountname, remoterepos, remotefolder, localrepos,
175 statusrepos, quick):
176 global mailboxes
177 ui = UIBase.getglobalui()
178 ui.registerthread(accountname)
179 # Load local folder.
180 localfolder = localrepos.\
181 getfolder(remotefolder.getvisiblename().\
182 replace(remoterepos.getsep(), localrepos.getsep()))
183 # Write the mailboxes
184 mbnames.add(accountname, localfolder.getvisiblename())
185
186 # Load status folder.
187 statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
188 replace(remoterepos.getsep(),
189 statusrepos.getsep()))
190 if localfolder.getuidvalidity() == None:
191 # This is a new folder, so delete the status cache to be sure
192 # we don't have a conflict.
193 statusfolder.deletemessagelist()
194
195 statusfolder.cachemessagelist()
196
197 if quick:
198 if not localfolder.quickchanged(statusfolder) \
199 and not remotefolder.quickchanged(statusfolder):
200 ui.skippingfolder(remotefolder)
201 localrepos.restore_atime()
202 return
203
204 # Load local folder
205 ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
206 ui.loadmessagelist(localrepos, localfolder)
207 localfolder.cachemessagelist()
208 ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
209
210 # If either the local or the status folder has messages and there is a UID
211 # validity problem, warn and abort. If there are no messages, UW IMAPd
212 # loses UIDVALIDITY. But we don't really need it if both local folders are
213 # empty. So, in that case, just save it off.
214 if len(localfolder.getmessagelist()) or len(statusfolder.getmessagelist()):
215 if not localfolder.isuidvalidityok():
216 ui.validityproblem(localfolder)
217 localrepos.restore_atime()
218 return
219 if not remotefolder.isuidvalidityok():
220 ui.validityproblem(remotefolder)
221 localrepos.restore_atime()
222 return
223 else:
224 localfolder.saveuidvalidity()
225 remotefolder.saveuidvalidity()
226
227 # Load remote folder.
228 ui.loadmessagelist(remoterepos, remotefolder)
229 remotefolder.cachemessagelist()
230 ui.messagelistloaded(remoterepos, remotefolder,
231 len(remotefolder.getmessagelist().keys()))
232
233
234 #
235
236 if not statusfolder.isnewfolder():
237 # Delete local copies of remote messages. This way,
238 # if a message's flag is modified locally but it has been
239 # deleted remotely, we'll delete it locally. Otherwise, we
240 # try to modify a deleted message's flags! This step
241 # need only be taken if a statusfolder is present; otherwise,
242 # there is no action taken *to* the remote repository.
243
244 remotefolder.syncmessagesto_delete(localfolder, [localfolder,
245 statusfolder])
246 ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
247 localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder])
248
249 # Synchronize remote changes.
250 ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
251 remotefolder.syncmessagesto(localfolder, [localfolder, statusfolder])
252
253 # Make sure the status folder is up-to-date.
254 ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
255 localfolder.syncmessagesto(statusfolder)
256 statusfolder.save()
257 localrepos.restore_atime()
258