]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap.py
/offlineimap/head: changeset 183
[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;\n{otentially 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 allowed in account names."
99 if config.has_option(account, "preauthtunnel"):
100 tunnels[account] = config.get(account, "preauthtunnel")
101 elif config.has_option(account, "remotepass"):
102 passwords[account] = config.get(account, "remotepass")
103 elif config.has_option(account, "remotepassfile"):
104 passfile = open(os.path.expanduser(config.get(account, "remotepassfile")))
105 passwords[account] = passfile.readline().strip()
106 passfile.close()
107 else:
108 passwords[account] = ui.getpass(account, config)
109 for instancename in ["FOLDER_" + account, "MSGCOPY_" + account]:
110 if '-1' in options:
111 threadutil.initInstanceLimit(instancename, 1)
112 else:
113 threadutil.initInstanceLimit(instancename,
114 config.getint(account, "maxconnections"))
115
116 mailboxes = []
117 servers = {}
118
119 def syncaccount(accountname, *args):
120 # We don't need an account lock because syncitall() goes through
121 # each account once, then waits for all to finish.
122 try:
123 ui.acct(accountname)
124 accountmetadata = os.path.join(metadatadir, accountname)
125 if not os.path.exists(accountmetadata):
126 os.mkdir(accountmetadata, 0700)
127
128 server = None
129 if accountname in servers:
130 server = servers[accountname]
131 else:
132 server = imapserver.ConfigedIMAPServer(config, accountname, passwords)
133 servers[accountname] = server
134
135 remoterepos = repository.IMAP.IMAPRepository(config, accountname, server)
136
137 # Connect to the Maildirs.
138 localrepos = repository.Maildir.MaildirRepository(os.path.expanduser(config.get(accountname, "localfolders")))
139
140 # Connect to the local cache.
141 statusrepos = repository.LocalStatus.LocalStatusRepository(accountmetadata)
142
143 ui.syncfolders(remoterepos, localrepos)
144 remoterepos.syncfoldersto(localrepos)
145 ui.acct(accountname)
146
147 folderthreads = []
148 for remotefolder in remoterepos.getfolders():
149 thread = InstanceLimitedThread(\
150 instancename = 'FOLDER_' + accountname,
151 target = syncfolder,
152 name = "Folder sync %s[%s]" % \
153 (accountname, remotefolder.getvisiblename()),
154 args = (accountname, remoterepos, remotefolder, localrepos,
155 statusrepos))
156 thread.setDaemon(1)
157 thread.start()
158 folderthreads.append(thread)
159 threadutil.threadsreset(folderthreads)
160 if not (config.has_option(accountname, 'holdconnectionopen') and \
161 config.getboolean(accountname, 'holdconnectionopen')):
162 server.close()
163 finally:
164 pass
165
166 def syncfolder(accountname, remoterepos, remotefolder, localrepos,
167 statusrepos):
168 # Load local folder.
169 localfolder = localrepos.\
170 getfolder(remotefolder.getvisiblename().\
171 replace(remoterepos.getsep(), localrepos.getsep()))
172 # Write the mailboxes
173 mailboxes.append({'accountname': accountname,
174 'foldername': localfolder.getvisiblename()})
175 # Load local folder
176 ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
177 ui.loadmessagelist(localrepos, localfolder)
178 localfolder.cachemessagelist()
179 ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
180
181 # Load status folder.
182 statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
183 replace(remoterepos.getsep(),
184 statusrepos.getsep()))
185 statusfolder.cachemessagelist()
186
187
188 # If either the local or the status folder has messages and
189 # there is a UID validity problem, warn and abort.
190 # If there are no messages, UW IMAPd loses UIDVALIDITY.
191 # But we don't really need it if both local folders are empty.
192 # So, in that case, save it off.
193 if (len(localfolder.getmessagelist()) or \
194 len(statusfolder.getmessagelist())) and \
195 not localfolder.isuidvalidityok(remotefolder):
196 ui.validityproblem(remotefolder)
197 return
198 else:
199 localfolder.saveuidvalidity(remotefolder.getuidvalidity())
200
201 # Load remote folder.
202 ui.loadmessagelist(remoterepos, remotefolder)
203 remotefolder.cachemessagelist()
204 ui.messagelistloaded(remoterepos, remotefolder,
205 len(remotefolder.getmessagelist().keys()))
206
207
208 #
209
210 if not statusfolder.isnewfolder():
211 # Delete local copies of remote messages. This way,
212 # if a message's flag is modified locally but it has been
213 # deleted remotely, we'll delete it locally. Otherwise, we
214 # try to modify a deleted message's flags! This step
215 # need only be taken if a statusfolder is present; otherwise,
216 # there is no action taken *to* the remote repository.
217
218 remotefolder.syncmessagesto_delete(localfolder, [localfolder,
219 statusfolder])
220 ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
221 localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder])
222
223 # Synchronize remote changes.
224 ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
225 remotefolder.syncmessagesto(localfolder)
226
227 # Make sure the status folder is up-to-date.
228 ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
229 localfolder.syncmessagesto(statusfolder)
230 statusfolder.save()
231
232
233 def syncitall():
234 global mailboxes
235 mailboxes = [] # Reset.
236 threads = []
237 for accountname in accounts:
238 thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT',
239 target = syncaccount,
240 name = "Account sync %s" % accountname,
241 args = (accountname,))
242 thread.setDaemon(1)
243 thread.start()
244 threads.append(thread)
245 # Wait for the threads to finish.
246 threadutil.threadsreset(threads)
247 mbnames.genmbnames(config, mailboxes)
248
249 def sync_with_timer():
250 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
251 syncitall()
252 if config.has_option('general', 'autorefresh'):
253 refreshperiod = config.getint('general', 'autorefresh') * 60
254 while 1:
255 # Set up keep-alives.
256 kaevents = {}
257 kathreads = {}
258 for accountname in accounts:
259 if config.has_option(accountname, 'holdconnectionopen') and \
260 config.getboolean(accountname, 'holdconnectionopen') and \
261 config.has_option(accountname, 'keepalive'):
262 event = Event()
263 kaevents[accountname] = event
264 thread = ExitNotifyThread(target = servers[accountname].keepalive,
265 name = "Keep alive " + accountname,
266 args = (config.getint(accountname, 'keepalive'), event))
267 thread.setDaemon(1)
268 thread.start()
269 kathreads[accountname] = thread
270 if ui.sleep(refreshperiod) == 2:
271 # Cancel keep-alives, but don't bother terminating threads
272 for event in kaevents.values():
273 event.set()
274 break
275 else:
276 # Cancel keep-alives and wait for threads to terminate.
277 for event in kaevents.values():
278 event.set()
279 for thread in kathreads.values():
280 thread.join()
281 syncitall()
282
283 def threadexited(thread):
284 if thread.getExitCause() == 'EXCEPTION':
285 if isinstance(thread.getExitException(), SystemExit):
286 # Bring a SystemExit into the main thread.
287 # Do not send it back to UI layer right now.
288 # Maybe later send it to ui.terminate?
289 raise SystemExit
290 ui.threadException(thread) # Expected to terminate
291 sys.exit(100) # Just in case...
292 os._exit(100)
293 elif thread.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
294 ui.terminate()
295 # Just in case...
296 sys.exit(100)
297 os._exit(100)
298 else:
299 ui.threadExited(thread)
300
301 threadutil.initexitnotify()
302 t = ExitNotifyThread(target=sync_with_timer,
303 name='Sync Runner')
304 t.setDaemon(1)
305 t.start()
306 try:
307 threadutil.exitnotifymonitorloop(threadexited)
308 except SystemExit:
309 raise
310 except:
311 ui.mainException() # Also expected to terminate.