1 # Copyright (C) 2003 John Goerzen
2 # <jgoerzen@complete.org>
4 # Portions Copyright (C) 2007 David Favro <offlineimap@meta-dynamic.com>
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.
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.
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
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
27 def getaccountlist(customconfig
):
28 return customconfig
.getsectionlist('Account')
30 def AccountListGenerator(customconfig
):
31 return [Account(customconfig
, accountname
)
32 for accountname
in getaccountlist(customconfig
)]
34 def AccountHashGenerator(customconfig
):
36 for item
in AccountListGenerator(customconfig
):
37 retval
[item
.getname()] = item
42 class Account(CustomConfig
.ConfigHelperMixin
):
43 def __init__(self
, config
, 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
53 def getlocaleval(self
):
63 return 'Account ' + self
.getname()
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.
70 Also, returns 100 if configured to not sleep at all."""
72 if not self
.refreshperiod
:
77 if hasattr(self
, 'localrepos'):
78 kaobjs
.append(self
.localrepos
)
79 if hasattr(self
, 'remoterepos'):
80 kaobjs
.append(self
.remoterepos
)
85 refreshperiod
= int(self
.refreshperiod
* 60)
86 sleepresult
= self
.ui
.sleep(refreshperiod
)
88 # Cancel keep-alive, but don't bother terminating threads
90 item
.stopkeepalive(abrupt
= 1)
93 # Cancel keep-alive and wait for thread to terminate.
95 item
.stopkeepalive(abrupt
= 0)
98 class AccountSynchronizationMixin
:
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)
106 self
.remoterepos
= offlineimap
.repository
.Base
.LoadRepository(self
.getconf('remoterepository'), self
, 'remote')
108 # Connect to the local repository.
109 self
.localrepos
= offlineimap
.repository
.Base
.LoadRepository(self
.getconf('localrepository'), self
, 'local')
111 # Connect to the local cache.
112 self
.statusrepos
= offlineimap
.repository
.LocalStatus
.LocalStatusRepository(self
.getconf('localrepository'), self
)
114 if not self
.refreshperiod
:
116 self
.ui
.acctdone(self
.name
)
121 looping
= self
.sleeper() != 2
122 self
.ui
.acctdone(self
.name
)
124 def getaccountmeta(self
):
125 return os
.path
.join(self
.metadatadir
, 'Account-' + self
.name
)
128 # We don't need an account lock because syncitall() goes through
129 # each account once, then waits for all to finish.
131 remoterepos
= self
.remoterepos
132 localrepos
= self
.localrepos
133 statusrepos
= self
.statusrepos
134 self
.ui
.syncfolders(remoterepos
, localrepos
)
135 remoterepos
.syncfoldersto(localrepos
)
138 for remotefolder
in remoterepos
.getfolders():
139 thread
= InstanceLimitedThread(\
140 instancename
= 'FOLDER_' + self
.remoterepos
.getname(),
142 name
= "Folder sync %s[%s]" % \
143 (self
.name
, remotefolder
.getvisiblename()),
144 args
= (self
.name
, remoterepos
, remotefolder
, localrepos
,
148 folderthreads
.append(thread
)
149 threadutil
.threadsreset(folderthreads
)
151 localrepos
.holdordropconnections()
152 remoterepos
.holdordropconnections()
156 class SyncableAccount(Account
, AccountSynchronizationMixin
):
159 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
162 ui
= UIBase
.getglobalui()
163 ui
.registerthread(accountname
)
165 localfolder
= localrepos
.\
166 getfolder(remotefolder
.getvisiblename().\
167 replace(remoterepos
.getsep(), localrepos
.getsep()))
168 # Write the mailboxes
169 mbnames
.add(accountname
, localfolder
.getvisiblename())
171 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
172 ui
.loadmessagelist(localrepos
, localfolder
)
173 localfolder
.cachemessagelist()
174 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
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()
186 statusfolder
.cachemessagelist()
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()
197 if not remotefolder
.isuidvalidityok():
198 ui
.validityproblem(remotefolder
)
199 localrepos
.restore_atime()
202 localfolder
.saveuidvalidity()
203 remotefolder
.saveuidvalidity()
205 # Load remote folder.
206 ui
.loadmessagelist(remoterepos
, remotefolder
)
207 remotefolder
.cachemessagelist()
208 ui
.messagelistloaded(remoterepos
, remotefolder
,
209 len(remotefolder
.getmessagelist().keys()))
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.
222 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
224 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
225 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
227 # Synchronize remote changes.
228 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
229 remotefolder
.syncmessagesto(localfolder
, [localfolder
, statusfolder
])
231 # Make sure the status folder is up-to-date.
232 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
233 localfolder
.syncmessagesto(statusfolder
)
235 localrepos
.restore_atime()