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