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