1 # Copyright (C) 2003 John Goerzen
2 # <jgoerzen@complete.org>
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.
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.
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
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 subprocess
import Popen
, PIPE
23 from threading
import Event
, Lock
25 from Queue
import Queue
, Empty
27 class SigListener(Queue
):
29 self
.folderlock
= Lock()
31 Queue
.__init
__(self
, 20)
32 def put_nowait(self
, sig
):
33 self
.folderlock
.acquire()
36 if self
.folders
is None or not self
.autorefreshes
:
37 # folders haven't yet been added, or this account is once-only; drop signal
40 for foldernr
in range(len(self
.folders
)):
42 self
.folders
[foldernr
][1] = True
45 # else folders have already been cleared, put signal...
47 self
.folderlock
.release()
48 Queue
.put_nowait(self
, sig
)
49 def addfolders(self
, remotefolders
, autorefreshes
, quick
):
50 self
.folderlock
.acquire()
54 self
.autorefreshes
= autorefreshes
55 for folder
in remotefolders
:
56 # new folders are queued
57 self
.folders
.append([folder
, True])
59 self
.folderlock
.release()
60 def clearfolders(self
):
61 self
.folderlock
.acquire()
63 for folder
, queued
in self
.folders
:
65 # some folders still in queue
70 self
.folderlock
.release()
71 def queuedfolders(self
):
72 self
.folderlock
.acquire()
77 for foldernr
, (folder
, queued
) in enumerate(self
.folders
):
79 # mark folder as no longer queued
80 self
.folders
[foldernr
][1] = False
83 self
.folderlock
.release()
85 self
.folderlock
.acquire()
87 self
.folderlock
.release()
89 def getaccountlist(customconfig
):
90 return customconfig
.getsectionlist('Account')
92 def AccountListGenerator(customconfig
):
93 return [Account(customconfig
, accountname
)
94 for accountname
in getaccountlist(customconfig
)]
96 def AccountHashGenerator(customconfig
):
98 for item
in AccountListGenerator(customconfig
):
99 retval
[item
.getname()] = item
104 class Account(CustomConfig
.ConfigHelperMixin
):
105 def __init__(self
, config
, name
):
108 self
.metadatadir
= config
.getmetadatadir()
109 self
.localeval
= config
.getlocaleval()
110 self
.ui
= UIBase
.getglobalui()
111 self
.refreshperiod
= self
.getconffloat('autorefresh', 0.0)
112 self
.quickrefreshcount
= self
.getconfint('quick', 0)
114 if self
.refreshperiod
== 0.0:
115 self
.refreshperiod
= None
117 def getlocaleval(self
):
118 return self
.localeval
126 def getsection(self
):
127 return 'Account ' + self
.getname()
129 def sleeper(self
, siglistener
):
130 """Sleep handler. Returns same value as UIBase.sleep:
131 0 if timeout expired, 1 if there was a request to cancel the timer,
132 and 2 if there is a request to abort the program.
134 Also, returns 100 if configured to not sleep at all."""
136 if not self
.refreshperiod
:
141 if hasattr(self
, 'localrepos'):
142 kaobjs
.append(self
.localrepos
)
143 if hasattr(self
, 'remoterepos'):
144 kaobjs
.append(self
.remoterepos
)
147 item
.startkeepalive()
149 sleeptime
= int(self
.refreshperiod
* 60)
150 if (self
.quickrefreshcount
> 0):
151 sleeptime
= int(sleeptime
/ self
.quickrefreshcount
)
154 # sleepresult = siglistener.get_nowait()
155 # # retrieved signal before sleep started
156 # if sleepresult == 1:
157 # # catching signal 1 here means folders were cleared before signal was posted
160 # sleepresult = self.ui.sleep(sleeptime, siglistener)
161 sleepresult
= self
.ui
.sleep(sleeptime
, siglistener
)
170 class AccountSynchronizationMixin
:
171 def syncrunner(self
, siglistener
):
172 self
.ui
.registerthread(self
.name
)
173 self
.ui
.acct(self
.name
)
174 accountmetadata
= self
.getaccountmeta()
175 if not os
.path
.exists(accountmetadata
):
176 os
.mkdir(accountmetadata
, 0700)
178 self
.remoterepos
= offlineimap
.repository
.Base
.LoadRepository(self
.getconf('remoterepository'), self
, 'remote')
180 # Connect to the local repository.
181 self
.localrepos
= offlineimap
.repository
.Base
.LoadRepository(self
.getconf('localrepository'), self
, 'local')
183 # Connect to the local cache.
184 self
.statusrepos
= offlineimap
.repository
.LocalStatus
.LocalStatusRepository(self
.getconf('localrepository'), self
)
186 if not self
.refreshperiod
:
187 self
.sync(siglistener
)
188 self
.ui
.acctdone(self
.name
)
192 self
.sync(siglistener
)
193 looping
= self
.sleeper(siglistener
) != 2
194 self
.ui
.acctdone(self
.name
)
196 def getaccountmeta(self
):
197 return os
.path
.join(self
.metadatadir
, 'Account-' + self
.name
)
199 def sync(self
, siglistener
):
200 # We don't need an account lock because syncitall() goes through
201 # each account once, then waits for all to finish.
203 hook
= self
.getconf('presynchook', '')
206 quickconfig
= self
.getconfint('quick', 0)
209 elif quickconfig
> 0:
210 if self
.quicknum
== 0 or self
.quicknum
> quickconfig
:
214 self
.quicknum
= self
.quicknum
+ 1
220 remoterepos
= self
.remoterepos
221 localrepos
= self
.localrepos
222 statusrepos
= self
.statusrepos
223 self
.ui
.syncfolders(remoterepos
, localrepos
)
224 remoterepos
.syncfoldersto(localrepos
, [statusrepos
])
226 siglistener
.addfolders(remoterepos
.getfolders(), bool(self
.refreshperiod
), quick
)
230 for remotefolder
, quick
in siglistener
.queuedfolders():
231 thread
= InstanceLimitedThread(\
232 instancename
= 'FOLDER_' + self
.remoterepos
.getname(),
234 name
= "Folder sync %s[%s]" % \
235 (self
.name
, remotefolder
.getvisiblename()),
236 args
= (self
.name
, remoterepos
, remotefolder
, localrepos
,
240 folderthreads
.append(thread
)
241 threadutil
.threadsreset(folderthreads
)
242 if siglistener
.clearfolders():
244 mbnames
.sort(self
.name
, remoterepos
.foldersort
)
246 localrepos
.forgetfolders()
247 remoterepos
.forgetfolders()
248 localrepos
.holdordropconnections()
249 remoterepos
.holdordropconnections()
253 hook
= self
.getconf('postsynchook', '')
256 def callhook(self
, cmd
):
260 self
.ui
.callhook("Calling hook: " + cmd
)
261 p
= Popen(cmd
, shell
=True,
262 stdin
=PIPE
, stdout
=PIPE
, stderr
=PIPE
,
265 self
.ui
.callhook("Hook stdout: %s\nHook stderr:%s\n" % r
)
266 self
.ui
.callhook("Hook return code: %d" % p
.returncode
)
268 self
.ui
.warn("Exception occured while calling hook")
270 class SyncableAccount(Account
, AccountSynchronizationMixin
):
273 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
276 ui
= UIBase
.getglobalui()
277 ui
.registerthread(accountname
)
279 localfolder
= localrepos
.\
280 getfolder(remotefolder
.getvisiblename().\
281 replace(remoterepos
.getsep(), localrepos
.getsep()))
282 # Write the mailboxes
283 mbnames
.add(accountname
, localfolder
.getvisiblename())
285 # Load status folder.
286 statusfolder
= statusrepos
.getfolder(remotefolder
.getvisiblename().\
287 replace(remoterepos
.getsep(),
288 statusrepos
.getsep()))
289 if localfolder
.getuidvalidity() == None:
290 # This is a new folder, so delete the status cache to be sure
291 # we don't have a conflict.
292 statusfolder
.deletemessagelist()
294 statusfolder
.cachemessagelist()
297 if not localfolder
.quickchanged(statusfolder
) \
298 and not remotefolder
.quickchanged(statusfolder
):
299 ui
.skippingfolder(remotefolder
)
300 localrepos
.restore_atime()
304 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
305 ui
.loadmessagelist(localrepos
, localfolder
)
306 localfolder
.cachemessagelist()
307 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
309 # If either the local or the status folder has messages and there is a UID
310 # validity problem, warn and abort. If there are no messages, UW IMAPd
311 # loses UIDVALIDITY. But we don't really need it if both local folders are
312 # empty. So, in that case, just save it off.
313 if len(localfolder
.getmessagelist()) or len(statusfolder
.getmessagelist()):
314 if not localfolder
.isuidvalidityok():
315 ui
.validityproblem(localfolder
)
316 localrepos
.restore_atime()
318 if not remotefolder
.isuidvalidityok():
319 ui
.validityproblem(remotefolder
)
320 localrepos
.restore_atime()
323 localfolder
.saveuidvalidity()
324 remotefolder
.saveuidvalidity()
326 # Load remote folder.
327 ui
.loadmessagelist(remoterepos
, remotefolder
)
328 remotefolder
.cachemessagelist()
329 ui
.messagelistloaded(remoterepos
, remotefolder
,
330 len(remotefolder
.getmessagelist().keys()))
335 if not statusfolder
.isnewfolder():
336 # Delete local copies of remote messages. This way,
337 # if a message's flag is modified locally but it has been
338 # deleted remotely, we'll delete it locally. Otherwise, we
339 # try to modify a deleted message's flags! This step
340 # need only be taken if a statusfolder is present; otherwise,
341 # there is no action taken *to* the remote repository.
343 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
345 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
346 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
348 # Synchronize remote changes.
349 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
350 remotefolder
.syncmessagesto(localfolder
, [localfolder
, statusfolder
])
352 # Make sure the status folder is up-to-date.
353 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
354 localfolder
.syncmessagesto(statusfolder
)
356 localrepos
.restore_atime()