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