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 threading
import Event
25 def getaccountlist(customconfig
):
26 return customconfig
.getsectionlist('Account')
28 def AccountListGenerator(customconfig
):
29 return [Account(customconfig
, accountname
)
30 for accountname
in getaccountlist(customconfig
)]
32 def AccountHashGenerator(customconfig
):
34 for item
in AccountListGenerator(customconfig
):
35 retval
[item
.getname()] = item
40 class Account(CustomConfig
.ConfigHelperMixin
):
41 def __init__(self
, config
, name
):
44 self
.metadatadir
= config
.getmetadatadir()
45 self
.localeval
= config
.getlocaleval()
46 self
.ui
= UIBase
.getglobalui()
47 self
.refreshperiod
= self
.getconffloat('autorefresh', 0.0)
49 if self
.refreshperiod
== 0.0:
50 self
.refreshperiod
= None
52 def getlocaleval(self
):
62 return 'Account ' + self
.getname()
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.
69 Also, returns 100 if configured to not sleep at all."""
71 if not self
.refreshperiod
:
76 if hasattr(self
, 'localrepos'):
77 kaobjs
.append(self
.localrepos
)
78 if hasattr(self
, 'remoterepos'):
79 kaobjs
.append(self
.remoterepos
)
84 refreshperiod
= int(self
.refreshperiod
* 60)
85 sleepresult
= self
.ui
.sleep(refreshperiod
)
87 # Cancel keep-alive, but don't bother terminating threads
89 item
.stopkeepalive(abrupt
= 1)
92 # Cancel keep-alive and wait for thread to terminate.
94 item
.stopkeepalive(abrupt
= 0)
97 class AccountSynchronizationMixin
:
98 def __init__(self
, config
, name
, folderhash
, folderhashlock
):
99 Account
.__init
__(self
, config
, name
)
100 self
.folderhash
= folderhash
101 self
.folderhashlock
= folderhashlock
102 self
.folderhashlock
.acquire()
104 self
.folderhash
[name
] = {}
106 self
.folderhashlock
.release()
108 def syncrunner(self
):
109 self
.ui
.registerthread(self
.name
)
110 self
.ui
.acct(self
.name
)
111 accountmetadata
= self
.getaccountmeta()
112 if not os
.path
.exists(accountmetadata
):
113 os
.mkdir(accountmetadata
, 0700)
115 self
.remoterepos
= offlineimap
.repository
.Base
.LoadRepository(self
.getconf('remoterepository'), self
, 'remote')
117 # Connect to the local repository.
118 self
.localrepos
= offlineimap
.repository
.Base
.LoadRepository(self
.getconf('localrepository'), self
, 'local')
120 # Connect to the local cache.
121 self
.statusrepos
= offlineimap
.repository
.LocalStatus
.LocalStatusRepository(self
.getconf('localrepository'), self
)
123 # FIXME: need new UI here?
124 self
.ui
.syncfolders(self
.remoterepos
, self
.localrepos
)
125 srcfolders
= self
.remoterepos
.getfolders()
126 destfolders
= self
.localrepos
.getfolders()
128 self
.folderhashlock
.acquire()
130 self
.folderhash
[name
] = {'src': srcfolders
, 'dest': destfolders
}
131 self
.folderhash
['___sem'].release()
133 self
.folderhashlock
.release()
135 if not self
.refreshperiod
:
137 self
.ui
.acctdone(self
.name
)
142 looping
= self
.sleeper() != 2
143 self
.ui
.acctdone(self
.name
)
145 def getaccountmeta(self
):
146 return os
.path
.join(self
.metadatadir
, 'Account-' + self
.name
)
149 # We don't need an account lock because syncitall() goes through
150 # each account once, then waits for all to finish.
152 quickconfig
= self
.getconfint('quick', 0)
155 elif quickconfig
> 0:
156 if self
.quicknum
== 0 or self
.quicknum
> quickconfig
:
160 self
.quicknum
= self
.quicknum
+ 1
166 remoterepos
= self
.remoterepos
167 localrepos
= self
.localrepos
168 statusrepos
= self
.statusrepos
169 self
.ui
.syncfolders(remoterepos
, localrepos
)
170 remoterepos
.syncfoldersto(localrepos
, [statusrepos
])
173 for remotefolder
in remoterepos
.getfolders():
174 thread
= InstanceLimitedThread(\
175 instancename
= 'FOLDER_' + self
.remoterepos
.getname(),
177 name
= "Folder sync %s[%s]" % \
178 (self
.name
, remotefolder
.getvisiblename()),
179 args
= (self
.name
, remoterepos
, remotefolder
, localrepos
,
183 folderthreads
.append(thread
)
184 threadutil
.threadsreset(folderthreads
)
186 localrepos
.forgetfolders()
187 remoterepos
.forgetfolders()
188 localrepos
.holdordropconnections()
189 remoterepos
.holdordropconnections()
193 class SyncableAccount(Account
, AccountSynchronizationMixin
):
196 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
199 ui
= UIBase
.getglobalui()
200 ui
.registerthread(accountname
)
202 localfolder
= localrepos
.\
203 getfolder(remotefolder
.getvisiblename().\
204 replace(remoterepos
.getsep(), localrepos
.getsep()))
205 # Write the mailboxes
206 mbnames
.add(accountname
, localfolder
.getvisiblename())
208 # Load status folder.
209 statusfolder
= statusrepos
.getfolder(remotefolder
.getvisiblename().\
210 replace(remoterepos
.getsep(),
211 statusrepos
.getsep()))
212 if localfolder
.getuidvalidity() == None:
213 # This is a new folder, so delete the status cache to be sure
214 # we don't have a conflict.
215 statusfolder
.deletemessagelist()
217 statusfolder
.cachemessagelist()
220 if not localfolder
.quickchanged(statusfolder
) \
221 and not remotefolder
.quickchanged(statusfolder
):
222 ui
.skippingfolder(remotefolder
)
223 localrepos
.restore_atime()
227 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
228 ui
.loadmessagelist(localrepos
, localfolder
)
229 localfolder
.cachemessagelist()
230 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
232 # If either the local or the status folder has messages and there is a UID
233 # validity problem, warn and abort. If there are no messages, UW IMAPd
234 # loses UIDVALIDITY. But we don't really need it if both local folders are
235 # empty. So, in that case, just save it off.
236 if len(localfolder
.getmessagelist()) or len(statusfolder
.getmessagelist()):
237 if not localfolder
.isuidvalidityok():
238 ui
.validityproblem(localfolder
)
239 localrepos
.restore_atime()
241 if not remotefolder
.isuidvalidityok():
242 ui
.validityproblem(remotefolder
)
243 localrepos
.restore_atime()
246 localfolder
.saveuidvalidity()
247 remotefolder
.saveuidvalidity()
249 # Load remote folder.
250 ui
.loadmessagelist(remoterepos
, remotefolder
)
251 remotefolder
.cachemessagelist()
252 ui
.messagelistloaded(remoterepos
, remotefolder
,
253 len(remotefolder
.getmessagelist().keys()))
258 if not statusfolder
.isnewfolder():
259 # Delete local copies of remote messages. This way,
260 # if a message's flag is modified locally but it has been
261 # deleted remotely, we'll delete it locally. Otherwise, we
262 # try to modify a deleted message's flags! This step
263 # need only be taken if a statusfolder is present; otherwise,
264 # there is no action taken *to* the remote repository.
266 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
268 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
269 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
271 # Synchronize remote changes.
272 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
273 remotefolder
.syncmessagesto(localfolder
, [localfolder
, statusfolder
])
275 # Make sure the status folder is up-to-date.
276 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
277 localfolder
.syncmessagesto(statusfolder
)
279 localrepos
.restore_atime()