]> code.delx.au - offlineimap/blob - head/offlineimap.py
/head: changeset 97
[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
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
26 if '-d' in sys.argv:
27 imaplib.Debug = 5
28
29 #ui = offlineimap.ui.TTY.TTYUI()
30 ui = offlineimap.ui.Tk.TkUI()
31 ui.init_banner()
32
33 config = ConfigParser()
34 configfilename = os.path.expanduser("~/.offlineimaprc")
35 if not os.path.exists(configfilename):
36 sys.stderr.write(" *** Config file %s does not exist; aborting!\n" % configfilename)
37 sys.exit(1)
38
39 config.read(configfilename)
40
41 metadatadir = os.path.expanduser(config.get("general", "metadata"))
42 if not os.path.exists(metadatadir):
43 os.mkdir(metadatadir, 0700)
44
45 accounts = config.get("general", "accounts")
46 accounts = accounts.replace(" ", "")
47 accounts = accounts.split(",")
48
49 server = None
50 remoterepos = None
51 localrepos = None
52 passwords = {}
53 tunnels = {}
54
55 threadutil.initInstanceLimit("ACCOUNTLIMIT", config.getint("general",
56 "maxsyncaccounts"))
57
58 # We have to gather passwords here -- don't want to have two threads
59 # asking for passwords simultaneously.
60
61 for account in accounts:
62 if config.has_option(account, "preauthtunnel"):
63 tunnels[account] = config.get(account, "preauthtunnel")
64 elif config.has_option(account, "remotepass"):
65 passwords[account] = config.get(account, "remotepass")
66 elif config.has_option(account, "remotepassfile"):
67 passfile = os.path.expanduser(config.get(account, "remotepassfile"))
68 passwords[account] = passfile.readline().strip()
69 passfile.close()
70 else:
71 passwords[account] = ui.getpass(account, config)
72 for instancename in ["FOLDER_" + account, "MSGCOPY_" + account]:
73 threadutil.initInstanceLimit(instancename,
74 config.getint(account, "maxconnections"))
75
76 mailboxes = []
77 mailboxlock = Lock()
78 servers = {}
79
80 def addmailbox(accountname, remotefolder):
81 mailboxlock.acquire()
82 mailboxes.append({'accountname' : accountname,
83 'foldername': remotefolder.getvisiblename()})
84 mailboxlock.release()
85
86 def syncaccount(accountname, *args):
87 # We don't need an account lock because syncitall() goes through
88 # each account once, then waits for all to finish.
89 try:
90 ui.acct(accountname)
91 accountmetadata = os.path.join(metadatadir, accountname)
92 if not os.path.exists(accountmetadata):
93 os.mkdir(accountmetadata, 0700)
94
95 server = None
96 if accountname in servers:
97 server = servers[accountname]
98 else:
99 server = imapserver.ConfigedIMAPServer(config, accountname, passwords)
100 servers[accountname] = server
101
102 remoterepos = repository.IMAP.IMAPRepository(config, accountname, server)
103
104 # Connect to the Maildirs.
105 localrepos = repository.Maildir.MaildirRepository(os.path.expanduser(config.get(accountname, "localfolders")))
106
107 # Connect to the local cache.
108 statusrepos = repository.LocalStatus.LocalStatusRepository(accountmetadata)
109
110 ui.syncfolders(remoterepos, localrepos)
111 remoterepos.syncfoldersto(localrepos)
112
113 folderthreads = []
114 for remotefolder in remoterepos.getfolders():
115 thread = InstanceLimitedThread(\
116 instancename = 'FOLDER_' + accountname,
117 target = syncfolder,
118 name = "syncfolder-%s-%s" % \
119 (accountname, remotefolder.getvisiblename()),
120 args = (accountname, remoterepos, remotefolder, localrepos,
121 statusrepos))
122 thread.setDaemon(1)
123 thread.start()
124 folderthreads.append(thread)
125 threadutil.threadsreset(folderthreads)
126 if not (config.has_option(accountname, 'holdconnectionopen') and \
127 config.getboolean(accountname, 'holdconnectionopen')):
128 server.close()
129 finally:
130 pass
131
132 def syncfolder(accountname, remoterepos, remotefolder, localrepos,
133 statusrepos):
134 mailboxes.append({'accountname': accountname,
135 'foldername': remotefolder.getvisiblename()})
136 # Load local folder.
137 localfolder = localrepos.\
138 getfolder(remotefolder.getvisiblename().\
139 replace(remoterepos.getsep(), localrepos.getsep()))
140 if not localfolder.isuidvalidityok(remotefolder):
141 ui.validityproblem(remotefolder)
142 return
143 ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
144 ui.loadmessagelist(localrepos, localfolder)
145 localfolder.cachemessagelist()
146 ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
147
148 # Load remote folder.
149 ui.loadmessagelist(remoterepos, remotefolder)
150 remotefolder.cachemessagelist()
151 ui.messagelistloaded(remoterepos, remotefolder,
152 len(remotefolder.getmessagelist().keys()))
153
154 # Load status folder.
155 statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
156 replace(remoterepos.getsep(),
157 statusrepos.getsep()))
158 statusfolder.cachemessagelist()
159
160 #
161
162 if not statusfolder.isnewfolder():
163 # Delete local copies of remote messages. This way,
164 # if a message's flag is modified locally but it has been
165 # deleted remotely, we'll delete it locally. Otherwise, we
166 # try to modify a deleted message's flags! This step
167 # need only be taken if a statusfolder is present; otherwise,
168 # there is no action taken *to* the remote repository.
169
170 remotefolder.syncmessagesto_delete(localfolder, [localfolder,
171 statusfolder])
172 ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
173 localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder])
174
175 # Synchronize remote changes.
176 ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
177 remotefolder.syncmessagesto(localfolder)
178
179 # Make sure the status folder is up-to-date.
180 ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
181 localfolder.syncmessagesto(statusfolder)
182 statusfolder.save()
183
184
185 def syncitall():
186 global mailboxes
187 mailboxes = [] # Reset.
188 threads = []
189 for accountname in accounts:
190 thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT',
191 target = syncaccount,
192 name = "syncaccount-%s" % accountname,
193 args = (accountname,))
194 thread.setDaemon(1)
195 thread.start()
196 threads.append(thread)
197 # Wait for the threads to finish.
198 threadutil.threadsreset(threads)
199 mbnames.genmbnames(config, mailboxes)
200
201 def sync_with_timer():
202 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
203 syncitall()
204 if config.has_option('general', 'autorefresh'):
205 refreshperiod = config.getint('general', 'autorefresh') * 60
206 while 1:
207 # Set up keep-alives.
208 kaevents = {}
209 kathreads = {}
210 for accountname in accounts:
211 if config.has_option(accountname, 'holdconnectionopen') and \
212 config.getboolean(accountname, 'holdconnectionopen') and \
213 config.has_option(accountname, 'keepalive'):
214 event = Event()
215 kaevents[accountname] = event
216 thread = ExitNotifyThread(target = servers[accountname].keepalive,
217 args = (config.getint(accountname, 'keepalive'), event))
218 thread.setDaemon(1)
219 thread.start()
220 kathreads[accountname] = thread
221 if ui.sleep(refreshperiod) == 2:
222 # Cancel keep-alives, but don't bother terminating threads
223 for event in kaevents.values():
224 event.set()
225 break
226 else:
227 # Cancel keep-alives and wait for threads to terminate.
228 for event in kaevents.values():
229 event.set()
230 for thread in kathreads.values():
231 thread.join()
232 syncitall()
233
234 def threadexited(thread):
235 if thread.getExitCause() == 'EXCEPTION':
236 ui.threadException(thread) # Expected to terminate
237 sys.exit(100) # Just in case...
238 os._exit(100)
239 elif thread.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
240 ui.terminate()
241 # Just in case...
242 sys.exit(100)
243 os._exit(100)
244 else:
245 ui.threadExited(thread)
246
247 threadutil.initexitnotify()
248 t = ExitNotifyThread(target=sync_with_timer, name='sync_with_timer')
249 t.setDaemon(1)
250 t.start()
251 try:
252 threadutil.exitnotifymonitorloop(threadexited)
253 except SystemExit:
254 raise
255 except:
256 ui.mainException() # Also expected to terminate.
257
258
259