]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/init.py
/offlineimap/head: changeset 259
[offlineimap] / offlineimap / head / offlineimap / init.py
1 # OfflineIMAP initialization code
2 # Copyright (C) 2002 John Goerzen
3 # <jgoerzen@complete.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
19 from offlineimap import imaplib, imapserver, repository, folder, mbnames, threadutil, version
20 from offlineimap.localeval import LocalEval
21 from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
22 from offlineimap.ui import UIBase
23 import re, os, os.path, offlineimap, sys
24 from ConfigParser import ConfigParser
25 from threading import *
26 from getopt import getopt
27
28 def startup(revno):
29 assert revno == version.revno, "Revision of main program (%d) does not match that of library (%d). Please double-check your PYTHONPATH and installation locations." % (revno, version.revno)
30 options = {}
31 if '--help' in sys.argv[1:]:
32 sys.stdout.write(version.cmdhelp + "\n")
33 sys.exit(0)
34
35 for optlist in getopt(sys.argv[1:], 'P:1oa:c:d:u:h')[0]:
36 options[optlist[0]] = optlist[1]
37
38 if '-h' in options:
39 sys.stdout.write(version.cmdhelp)
40 sys.stdout.write("\n")
41 sys.exit(0)
42 configfilename = os.path.expanduser("~/.offlineimaprc")
43 if '-c' in options:
44 configfilename = options['-c']
45 if '-P' in options:
46 if not '-1' in options:
47 sys.stderr.write("FATAL: profile mode REQUIRES -1\n")
48 sys.exit(100)
49 profiledir = options['-P']
50 os.mkdir(profiledir)
51 threadutil.setprofiledir(profiledir)
52 sys.stderr.write("WARNING: profile mode engaged;\nPotentially large data will be created in " + profiledir + "\n")
53
54 config = ConfigParser()
55 if not os.path.exists(configfilename):
56 sys.stderr.write(" *** Config file %s does not exist; aborting!\n" % configfilename)
57 sys.exit(1)
58
59 config.read(configfilename)
60
61 if config.has_option("general", "pythonfile"):
62 path=os.path.expanduser(config.get("general", "pythonfile"))
63 else:
64 path=None
65 localeval = LocalEval(path)
66
67 ui = offlineimap.ui.detector.findUI(config, localeval, options.get('-u'))
68 ui.init_banner()
69 UIBase.setglobalui(ui)
70 print "UI is", UIBase.getglobalui()
71
72 if '-d' in options:
73 for debugtype in options['-d'].split(','):
74 ui.add_debug(debugtype.strip())
75 if debugtype == 'imap':
76 imaplib.Debug = 5
77
78 if '-o' in options and config.has_option("general", "autorefresh"):
79 config.remove_option("general", "autorefresh")
80
81 metadatadir = os.path.expanduser(config.get("general", "metadata"))
82 if not os.path.exists(metadatadir):
83 os.mkdir(metadatadir, 0700)
84
85 accounts = config.get("general", "accounts")
86 if '-a' in options:
87 accounts = options['-a']
88 accounts = accounts.replace(" ", "")
89 accounts = accounts.split(",")
90
91 server = None
92 remoterepos = None
93 localrepos = None
94 passwords = {}
95 tunnels = {}
96
97 if '-1' in options:
98 threadutil.initInstanceLimit("ACCOUNTLIMIT", 1)
99 else:
100 threadutil.initInstanceLimit("ACCOUNTLIMIT",
101 config.getint("general", "maxsyncaccounts"))
102
103 # We have to gather passwords here -- don't want to have two threads
104 # asking for passwords simultaneously.
105
106 for account in accounts:
107 print "Processing account", account
108 #if '.' in account:
109 # raise ValueError, "Account '%s' contains a dot; dots are not " \
110 # "allowed in account names." % account
111 if config.has_option(account, "preauthtunnel"):
112 tunnels[account] = config.get(account, "preauthtunnel")
113 elif config.has_option(account, "remotepass"):
114 passwords[account] = config.get(account, "remotepass")
115 elif config.has_option(account, "remotepassfile"):
116 passfile = open(os.path.expanduser(config.get(account, "remotepassfile")))
117 passwords[account] = passfile.readline().strip()
118 passfile.close()
119 else:
120 passwords[account] = ui.getpass(account, config)
121 for instancename in ["FOLDER_" + account, "MSGCOPY_" + account]:
122 if '-1' in options:
123 threadutil.initInstanceLimit(instancename, 1)
124 else:
125 threadutil.initInstanceLimit(instancename,
126 config.getint(account, "maxconnections"))
127
128 mailboxes = []
129 servers = {}
130
131 threadutil.initexitnotify()
132 t = ExitNotifyThread(target=sync_with_timer,
133 name='Sync Runner')
134 t.setDaemon(1)
135 t.start()
136 try:
137 threadutil.exitnotifymonitorloop(threadexited)
138 except SystemExit:
139 raise
140 except:
141 ui.mainException() # Also expected to terminate.
142
143 def syncaccount(accountname, *args):
144 ui = UIBase.getglobalui()
145 # We don't need an account lock because syncitall() goes through
146 # each account once, then waits for all to finish.
147 try:
148 ui.acct(accountname)
149 accountmetadata = os.path.join(metadatadir, accountname)
150 if not os.path.exists(accountmetadata):
151 os.mkdir(accountmetadata, 0700)
152
153 server = None
154 if accountname in servers:
155 server = servers[accountname]
156 else:
157 server = imapserver.ConfigedIMAPServer(config, accountname, passwords)
158 servers[accountname] = server
159
160 remoterepos = repository.IMAP.IMAPRepository(config, localeval, accountname, server)
161
162 # Connect to the Maildirs.
163 localrepos = repository.Maildir.MaildirRepository(os.path.expanduser(config.get(accountname, "localfolders")), accountname, config)
164
165 # Connect to the local cache.
166 statusrepos = repository.LocalStatus.LocalStatusRepository(accountmetadata)
167
168 ui.syncfolders(remoterepos, localrepos)
169 remoterepos.syncfoldersto(localrepos)
170 ui.acct(accountname)
171
172 folderthreads = []
173 for remotefolder in remoterepos.getfolders():
174 thread = InstanceLimitedThread(\
175 instancename = 'FOLDER_' + accountname,
176 target = syncfolder,
177 name = "Folder sync %s[%s]" % \
178 (accountname, remotefolder.getvisiblename()),
179 args = (accountname, remoterepos, remotefolder, localrepos,
180 statusrepos))
181 thread.setDaemon(1)
182 thread.start()
183 folderthreads.append(thread)
184 threadutil.threadsreset(folderthreads)
185 if not (config.has_option(accountname, 'holdconnectionopen') and \
186 config.getboolean(accountname, 'holdconnectionopen')):
187 server.close()
188 finally:
189 pass
190
191 def syncfolder(accountname, remoterepos, remotefolder, localrepos,
192 statusrepos):
193 ui = UIBase.getglobalui()
194 # Load local folder.
195 localfolder = localrepos.\
196 getfolder(remotefolder.getvisiblename().\
197 replace(remoterepos.getsep(), localrepos.getsep()))
198 # Write the mailboxes
199 mailboxes.append({'accountname': accountname,
200 'foldername': localfolder.getvisiblename()})
201 # Load local folder
202 ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
203 ui.loadmessagelist(localrepos, localfolder)
204 localfolder.cachemessagelist()
205 ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
206
207
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()
216
217 statusfolder.cachemessagelist()
218
219
220 # If either the local or the status folder has messages and
221 # there is a UID validity problem, warn and abort.
222 # If there are no messages, UW IMAPd loses UIDVALIDITY.
223 # But we don't really need it if both local folders are empty.
224 # So, in that case, save it off.
225 if (len(localfolder.getmessagelist()) or \
226 len(statusfolder.getmessagelist())) and \
227 not localfolder.isuidvalidityok(remotefolder):
228 ui.validityproblem(remotefolder)
229 return
230 else:
231 localfolder.saveuidvalidity(remotefolder.getuidvalidity())
232
233 # Load remote folder.
234 ui.loadmessagelist(remoterepos, remotefolder)
235 remotefolder.cachemessagelist()
236 ui.messagelistloaded(remoterepos, remotefolder,
237 len(remotefolder.getmessagelist().keys()))
238
239
240 #
241
242 if not statusfolder.isnewfolder():
243 # Delete local copies of remote messages. This way,
244 # if a message's flag is modified locally but it has been
245 # deleted remotely, we'll delete it locally. Otherwise, we
246 # try to modify a deleted message's flags! This step
247 # need only be taken if a statusfolder is present; otherwise,
248 # there is no action taken *to* the remote repository.
249
250 remotefolder.syncmessagesto_delete(localfolder, [localfolder,
251 statusfolder])
252 ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
253 localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder])
254
255 # Synchronize remote changes.
256 ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
257 remotefolder.syncmessagesto(localfolder)
258
259 # Make sure the status folder is up-to-date.
260 ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
261 localfolder.syncmessagesto(statusfolder)
262 statusfolder.save()
263
264
265
266 def syncitall():
267 ui = UIBase.getglobalui()
268 global mailboxes
269 mailboxes = [] # Reset.
270 threads = []
271 for accountname in accounts:
272 thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT',
273 target = syncaccount,
274 name = "Account sync %s" % accountname,
275 args = (accountname,))
276 thread.setDaemon(1)
277 thread.start()
278 threads.append(thread)
279 # Wait for the threads to finish.
280 threadutil.threadsreset(threads)
281 mbnames.genmbnames(config, localeval, mailboxes)
282
283 def sync_with_timer():
284 ui = UIBase.getglobalui()
285 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
286 syncitall()
287 if config.has_option('general', 'autorefresh'):
288 refreshperiod = config.getint('general', 'autorefresh') * 60
289 while 1:
290 # Set up keep-alives.
291 kaevents = {}
292 kathreads = {}
293 for accountname in accounts:
294 if config.has_option(accountname, 'holdconnectionopen') and \
295 config.getboolean(accountname, 'holdconnectionopen') and \
296 config.has_option(accountname, 'keepalive'):
297 event = Event()
298 kaevents[accountname] = event
299 thread = ExitNotifyThread(target = servers[accountname].keepalive,
300 name = "Keep alive " + accountname,
301 args = (config.getint(accountname, 'keepalive'), event))
302 thread.setDaemon(1)
303 thread.start()
304 kathreads[accountname] = thread
305 if ui.sleep(refreshperiod) == 2:
306 # Cancel keep-alives, but don't bother terminating threads
307 for event in kaevents.values():
308 event.set()
309 break
310 else:
311 # Cancel keep-alives and wait for threads to terminate.
312 for event in kaevents.values():
313 event.set()
314 for thread in kathreads.values():
315 thread.join()
316 syncitall()
317
318 def threadexited(thread):
319 ui = UIBase.getglobalui()
320 if thread.getExitCause() == 'EXCEPTION':
321 if isinstance(thread.getExitException(), SystemExit):
322 # Bring a SystemExit into the main thread.
323 # Do not send it back to UI layer right now.
324 # Maybe later send it to ui.terminate?
325 raise SystemExit
326 ui.threadException(thread) # Expected to terminate
327 sys.exit(100) # Just in case...
328 os._exit(100)
329 elif thread.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
330 ui.terminate()
331 # Just in case...
332 sys.exit(100)
333 os._exit(100)
334 else:
335 ui.threadExited(thread)