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