]>
code.delx.au - offlineimap/blob - offlineimap/head/offlineimap.py
3 # Copyright (C) 2002 John Goerzen
4 # <jgoerzen@complete.org>
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.
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.
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
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
28 if '--help' in sys
.argv
[1:]:
29 sys
.stdout
.write(version
.cmdhelp
+ "\n")
32 for optlist
in getopt(sys
.argv
[1:], 'P:1oa:c:du:h')[0]:
33 options
[optlist
[0]] = optlist
[1]
38 sys
.stdout
.write(version
.cmdhelp
)
39 sys
.stdout
.write("\n")
41 configfilename
= os
.path
.expanduser("~/.offlineimaprc")
43 configfilename
= options
['-c']
45 if not '-1' in options
:
46 sys
.stderr
.write("FATAL: profile mode REQUIRES -1\n")
48 profiledir
= options
['-P']
50 threadutil
.setprofiledir(profiledir
)
51 sys
.stderr
.write("WARNING: profile mode engaged;\n{otentially large data will be created in " + profiledir
+ "\n")
55 config
= ConfigParser()
56 if not os
.path
.exists(configfilename
):
57 sys
.stderr
.write(" *** Config file %s does not exist; aborting!\n" % configfilename
)
60 config
.read(configfilename
)
63 ui
= offlineimap
.ui
.detector
.getUImod(options
['-u'])(config
)
65 ui
= offlineimap
.ui
.detector
.findUI(config
)
68 if '-o' in options
and config
.has_option("general", "autorefresh"):
69 config
.remove_option("general", "autorefresh")
71 metadatadir
= os
.path
.expanduser(config
.get("general", "metadata"))
72 if not os
.path
.exists(metadatadir
):
73 os
.mkdir(metadatadir
, 0700)
75 accounts
= config
.get("general", "accounts")
77 accounts
= options
['-a']
78 accounts
= accounts
.replace(" ", "")
79 accounts
= accounts
.split(",")
88 threadutil
.initInstanceLimit("ACCOUNTLIMIT", 1)
90 threadutil
.initInstanceLimit("ACCOUNTLIMIT",
91 config
.getint("general", "maxsyncaccounts"))
93 # We have to gather passwords here -- don't want to have two threads
94 # asking for passwords simultaneously.
96 for account
in accounts
:
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()
108 passwords
[account
] = ui
.getpass(account
, config
)
109 for instancename
in ["FOLDER_" + account
, "MSGCOPY_" + account
]:
111 threadutil
.initInstanceLimit(instancename
, 1)
113 threadutil
.initInstanceLimit(instancename
,
114 config
.getint(account
, "maxconnections"))
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.
124 accountmetadata
= os
.path
.join(metadatadir
, accountname
)
125 if not os
.path
.exists(accountmetadata
):
126 os
.mkdir(accountmetadata
, 0700)
129 if accountname
in servers
:
130 server
= servers
[accountname
]
132 server
= imapserver
.ConfigedIMAPServer(config
, accountname
, passwords
)
133 servers
[accountname
] = server
135 remoterepos
= repository
.IMAP
.IMAPRepository(config
, accountname
, server
)
137 # Connect to the Maildirs.
138 localrepos
= repository
.Maildir
.MaildirRepository(os
.path
.expanduser(config
.get(accountname
, "localfolders")))
140 # Connect to the local cache.
141 statusrepos
= repository
.LocalStatus
.LocalStatusRepository(accountmetadata
)
143 ui
.syncfolders(remoterepos
, localrepos
)
144 remoterepos
.syncfoldersto(localrepos
)
148 for remotefolder
in remoterepos
.getfolders():
149 thread
= InstanceLimitedThread(\
150 instancename
= 'FOLDER_' + accountname
,
152 name
= "Folder sync %s[%s]" % \
153 (accountname
, remotefolder
.getvisiblename()),
154 args
= (accountname
, remoterepos
, remotefolder
, localrepos
,
158 folderthreads
.append(thread
)
159 threadutil
.threadsreset(folderthreads
)
160 if not (config
.has_option(accountname
, 'holdconnectionopen') and \
161 config
.getboolean(accountname
, 'holdconnectionopen')):
166 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
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()})
176 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
177 ui
.loadmessagelist(localrepos
, localfolder
)
178 localfolder
.cachemessagelist()
179 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
181 # Load status folder.
182 statusfolder
= statusrepos
.getfolder(remotefolder
.getvisiblename().\
183 replace(remoterepos
.getsep(),
184 statusrepos
.getsep()))
185 statusfolder
.cachemessagelist()
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
)
199 localfolder
.saveuidvalidity(remotefolder
.getuidvalidity())
201 # Load remote folder.
202 ui
.loadmessagelist(remoterepos
, remotefolder
)
203 remotefolder
.cachemessagelist()
204 ui
.messagelistloaded(remoterepos
, remotefolder
,
205 len(remotefolder
.getmessagelist().keys()))
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.
218 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
220 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
221 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
223 # Synchronize remote changes.
224 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
225 remotefolder
.syncmessagesto(localfolder
)
227 # Make sure the status folder is up-to-date.
228 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
229 localfolder
.syncmessagesto(statusfolder
)
235 mailboxes
= [] # Reset.
237 for accountname
in accounts
:
238 thread
= InstanceLimitedThread(instancename
= 'ACCOUNTLIMIT',
239 target
= syncaccount
,
240 name
= "Account sync %s" % accountname
,
241 args
= (accountname
,))
244 threads
.append(thread
)
245 # Wait for the threads to finish.
246 threadutil
.threadsreset(threads
)
247 mbnames
.genmbnames(config
, mailboxes
)
249 def sync_with_timer():
250 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
252 if config
.has_option('general', 'autorefresh'):
253 refreshperiod
= config
.getint('general', 'autorefresh') * 60
255 # Set up keep-alives.
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'):
263 kaevents
[accountname
] = event
264 thread
= ExitNotifyThread(target
= servers
[accountname
].keepalive
,
265 name
= "Keep alive " + accountname
,
266 args
= (config
.getint(accountname
, 'keepalive'), event
))
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():
276 # Cancel keep-alives and wait for threads to terminate.
277 for event
in kaevents
.values():
279 for thread
in kathreads
.values():
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?
290 ui
.threadException(thread
) # Expected to terminate
291 sys
.exit(100) # Just in case...
293 elif thread
.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
299 ui
.threadExited(thread
)
301 threadutil
.initexitnotify()
302 t
= ExitNotifyThread(target
=sync_with_timer
,
307 threadutil
.exitnotifymonitorloop(threadexited
)
311 ui
.mainException() # Also expected to terminate.