]> code.delx.au - offlineimap/blob - offlineimap/accounts.py
UID validity diagnostics improvement
[offlineimap] / offlineimap / accounts.py
1 # Copyright (C) 2003 John Goerzen
2 # <jgoerzen@complete.org>
3 #
4 # Portions Copyright (C) 2007 David Favro <offlineimap@meta-dynamic.com>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
20 from offlineimap import threadutil, mbnames, CustomConfig
21 import offlineimap.repository.Base, offlineimap.repository.LocalStatus
22 from offlineimap.ui import UIBase
23 from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
24 from threading import Event
25 import os
26
27 def getaccountlist(customconfig):
28 return customconfig.getsectionlist('Account')
29
30 def AccountListGenerator(customconfig):
31 return [Account(customconfig, accountname)
32 for accountname in getaccountlist(customconfig)]
33
34 def AccountHashGenerator(customconfig):
35 retval = {}
36 for item in AccountListGenerator(customconfig):
37 retval[item.getname()] = item
38 return retval
39
40 mailboxes = []
41
42 class Account(CustomConfig.ConfigHelperMixin):
43 def __init__(self, config, name):
44 self.config = config
45 self.name = name
46 self.metadatadir = config.getmetadatadir()
47 self.localeval = config.getlocaleval()
48 self.ui = UIBase.getglobalui()
49 self.refreshperiod = self.getconffloat('autorefresh', 0.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):
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 sleepresult = self.ui.sleep(refreshperiod)
87 if sleepresult == 2:
88 # Cancel keep-alive, but don't bother terminating threads
89 for item in kaobjs:
90 item.stopkeepalive(abrupt = 1)
91 return sleepresult
92 else:
93 # Cancel keep-alive and wait for thread to terminate.
94 for item in kaobjs:
95 item.stopkeepalive(abrupt = 0)
96 return sleepresult
97
98 class AccountSynchronizationMixin:
99 def syncrunner(self):
100 self.ui.registerthread(self.name)
101 self.ui.acct(self.name)
102 accountmetadata = self.getaccountmeta()
103 if not os.path.exists(accountmetadata):
104 os.mkdir(accountmetadata, 0700)
105
106 self.remoterepos = offlineimap.repository.Base.LoadRepository(self.getconf('remoterepository'), self, 'remote')
107
108 # Connect to the local repository.
109 self.localrepos = offlineimap.repository.Base.LoadRepository(self.getconf('localrepository'), self, 'local')
110
111 # Connect to the local cache.
112 self.statusrepos = offlineimap.repository.LocalStatus.LocalStatusRepository(self.getconf('localrepository'), self)
113
114 if not self.refreshperiod:
115 self.sync()
116 self.ui.acctdone(self.name)
117 return
118 looping = 1
119 while looping:
120 self.sync()
121 looping = self.sleeper() != 2
122 self.ui.acctdone(self.name)
123
124 def getaccountmeta(self):
125 return os.path.join(self.metadatadir, 'Account-' + self.name)
126
127 def sync(self):
128 # We don't need an account lock because syncitall() goes through
129 # each account once, then waits for all to finish.
130 try:
131 remoterepos = self.remoterepos
132 localrepos = self.localrepos
133 statusrepos = self.statusrepos
134 self.ui.syncfolders(remoterepos, localrepos)
135 remoterepos.syncfoldersto(localrepos)
136
137 folderthreads = []
138 for remotefolder in remoterepos.getfolders():
139 thread = InstanceLimitedThread(\
140 instancename = 'FOLDER_' + self.remoterepos.getname(),
141 target = syncfolder,
142 name = "Folder sync %s[%s]" % \
143 (self.name, remotefolder.getvisiblename()),
144 args = (self.name, remoterepos, remotefolder, localrepos,
145 statusrepos))
146 thread.setDaemon(1)
147 thread.start()
148 folderthreads.append(thread)
149 threadutil.threadsreset(folderthreads)
150 mbnames.write()
151 localrepos.holdordropconnections()
152 remoterepos.holdordropconnections()
153 finally:
154 pass
155
156 class SyncableAccount(Account, AccountSynchronizationMixin):
157 pass
158
159 def syncfolder(accountname, remoterepos, remotefolder, localrepos,
160 statusrepos):
161 global mailboxes
162 ui = UIBase.getglobalui()
163 ui.registerthread(accountname)
164 # Load local folder.
165 localfolder = localrepos.\
166 getfolder(remotefolder.getvisiblename().\
167 replace(remoterepos.getsep(), localrepos.getsep()))
168 # Write the mailboxes
169 mbnames.add(accountname, localfolder.getvisiblename())
170 # Load local folder
171 ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
172 ui.loadmessagelist(localrepos, localfolder)
173 localfolder.cachemessagelist()
174 ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
175
176
177 # Load status folder.
178 statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
179 replace(remoterepos.getsep(),
180 statusrepos.getsep()))
181 if localfolder.getuidvalidity() == None:
182 # This is a new folder, so delete the status cache to be sure
183 # we don't have a conflict.
184 statusfolder.deletemessagelist()
185
186 statusfolder.cachemessagelist()
187
188 # If either the local or the status folder has messages and there is a UID
189 # validity problem, warn and abort. If there are no messages, UW IMAPd
190 # loses UIDVALIDITY. But we don't really need it if both local folders are
191 # empty. So, in that case, just save it off.
192 if len(localfolder.getmessagelist()) or len(statusfolder.getmessagelist()):
193 if not localfolder.isuidvalidityok():
194 ui.validityproblem(localfolder)
195 localrepos.restore_atime()
196 return
197 if not remotefolder.isuidvalidityok():
198 ui.validityproblem(remotefolder)
199 localrepos.restore_atime()
200 return
201 else:
202 localfolder.saveuidvalidity()
203 remotefolder.saveuidvalidity()
204
205 # Load remote folder.
206 ui.loadmessagelist(remoterepos, remotefolder)
207 remotefolder.cachemessagelist()
208 ui.messagelistloaded(remoterepos, remotefolder,
209 len(remotefolder.getmessagelist().keys()))
210
211
212 #
213
214 if not statusfolder.isnewfolder():
215 # Delete local copies of remote messages. This way,
216 # if a message's flag is modified locally but it has been
217 # deleted remotely, we'll delete it locally. Otherwise, we
218 # try to modify a deleted message's flags! This step
219 # need only be taken if a statusfolder is present; otherwise,
220 # there is no action taken *to* the remote repository.
221
222 remotefolder.syncmessagesto_delete(localfolder, [localfolder,
223 statusfolder])
224 ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
225 localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder])
226
227 # Synchronize remote changes.
228 ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
229 remotefolder.syncmessagesto(localfolder, [localfolder, statusfolder])
230
231 # Make sure the status folder is up-to-date.
232 ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
233 localfolder.syncmessagesto(statusfolder)
234 statusfolder.save()
235 localrepos.restore_atime()
236