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