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