]>
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;\nPotentially 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 " \
99 # "allowed in account names." % account
100 if config
.has_option(account
, "preauthtunnel"):
101 tunnels
[account
] = config
.get(account
, "preauthtunnel")
102 elif config
.has_option(account
, "remotepass"):
103 passwords
[account
] = config
.get(account
, "remotepass")
104 elif config
.has_option(account
, "remotepassfile"):
105 passfile
= open(os
.path
.expanduser(config
.get(account
, "remotepassfile")))
106 passwords
[account
] = passfile
.readline().strip()
109 passwords
[account
] = ui
.getpass(account
, config
)
110 for instancename
in ["FOLDER_" + account
, "MSGCOPY_" + account
]:
112 threadutil
.initInstanceLimit(instancename
, 1)
114 threadutil
.initInstanceLimit(instancename
,
115 config
.getint(account
, "maxconnections"))
120 def syncaccount(accountname
, *args
):
121 # We don't need an account lock because syncitall() goes through
122 # each account once, then waits for all to finish.
125 accountmetadata
= os
.path
.join(metadatadir
, accountname
)
126 if not os
.path
.exists(accountmetadata
):
127 os
.mkdir(accountmetadata
, 0700)
130 if accountname
in servers
:
131 server
= servers
[accountname
]
133 server
= imapserver
.ConfigedIMAPServer(config
, accountname
, passwords
)
134 servers
[accountname
] = server
136 remoterepos
= repository
.IMAP
.IMAPRepository(config
, accountname
, server
)
138 # Connect to the Maildirs.
139 localrepos
= repository
.Maildir
.MaildirRepository(os
.path
.expanduser(config
.get(accountname
, "localfolders")))
141 # Connect to the local cache.
142 statusrepos
= repository
.LocalStatus
.LocalStatusRepository(accountmetadata
)
144 ui
.syncfolders(remoterepos
, localrepos
)
145 remoterepos
.syncfoldersto(localrepos
)
149 for remotefolder
in remoterepos
.getfolders():
150 thread
= InstanceLimitedThread(\
151 instancename
= 'FOLDER_' + accountname
,
153 name
= "Folder sync %s[%s]" % \
154 (accountname
, remotefolder
.getvisiblename()),
155 args
= (accountname
, remoterepos
, remotefolder
, localrepos
,
159 folderthreads
.append(thread
)
160 threadutil
.threadsreset(folderthreads
)
161 if not (config
.has_option(accountname
, 'holdconnectionopen') and \
162 config
.getboolean(accountname
, 'holdconnectionopen')):
167 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
170 localfolder
= localrepos
.\
171 getfolder(remotefolder
.getvisiblename().\
172 replace(remoterepos
.getsep(), localrepos
.getsep()))
173 # Write the mailboxes
174 mailboxes
.append({'accountname': accountname
,
175 'foldername': localfolder
.getvisiblename()})
177 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
178 ui
.loadmessagelist(localrepos
, localfolder
)
179 localfolder
.cachemessagelist()
180 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
183 # Load status folder.
184 statusfolder
= statusrepos
.getfolder(remotefolder
.getvisiblename().\
185 replace(remoterepos
.getsep(),
186 statusrepos
.getsep()))
187 if localfolder
.getuidvalidity() == None:
188 # This is a new folder, so delete the status cache to be sure
189 # we don't have a conflict.
190 statusfolder
.deletemessagelist()
192 statusfolder
.cachemessagelist()
195 # If either the local or the status folder has messages and
196 # there is a UID validity problem, warn and abort.
197 # If there are no messages, UW IMAPd loses UIDVALIDITY.
198 # But we don't really need it if both local folders are empty.
199 # So, in that case, save it off.
200 if (len(localfolder
.getmessagelist()) or \
201 len(statusfolder
.getmessagelist())) and \
202 not localfolder
.isuidvalidityok(remotefolder
):
203 ui
.validityproblem(remotefolder
)
206 localfolder
.saveuidvalidity(remotefolder
.getuidvalidity())
208 # Load remote folder.
209 ui
.loadmessagelist(remoterepos
, remotefolder
)
210 remotefolder
.cachemessagelist()
211 ui
.messagelistloaded(remoterepos
, remotefolder
,
212 len(remotefolder
.getmessagelist().keys()))
217 if not statusfolder
.isnewfolder():
218 # Delete local copies of remote messages. This way,
219 # if a message's flag is modified locally but it has been
220 # deleted remotely, we'll delete it locally. Otherwise, we
221 # try to modify a deleted message's flags! This step
222 # need only be taken if a statusfolder is present; otherwise,
223 # there is no action taken *to* the remote repository.
225 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
227 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
228 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
230 # Synchronize remote changes.
231 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
232 remotefolder
.syncmessagesto(localfolder
)
234 # Make sure the status folder is up-to-date.
235 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
236 localfolder
.syncmessagesto(statusfolder
)
242 mailboxes
= [] # Reset.
244 for accountname
in accounts
:
245 thread
= InstanceLimitedThread(instancename
= 'ACCOUNTLIMIT',
246 target
= syncaccount
,
247 name
= "Account sync %s" % accountname
,
248 args
= (accountname
,))
251 threads
.append(thread
)
252 # Wait for the threads to finish.
253 threadutil
.threadsreset(threads
)
254 mbnames
.genmbnames(config
, mailboxes
)
256 def sync_with_timer():
257 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
259 if config
.has_option('general', 'autorefresh'):
260 refreshperiod
= config
.getint('general', 'autorefresh') * 60
262 # Set up keep-alives.
265 for accountname
in accounts
:
266 if config
.has_option(accountname
, 'holdconnectionopen') and \
267 config
.getboolean(accountname
, 'holdconnectionopen') and \
268 config
.has_option(accountname
, 'keepalive'):
270 kaevents
[accountname
] = event
271 thread
= ExitNotifyThread(target
= servers
[accountname
].keepalive
,
272 name
= "Keep alive " + accountname
,
273 args
= (config
.getint(accountname
, 'keepalive'), event
))
276 kathreads
[accountname
] = thread
277 if ui
.sleep(refreshperiod
) == 2:
278 # Cancel keep-alives, but don't bother terminating threads
279 for event
in kaevents
.values():
283 # Cancel keep-alives and wait for threads to terminate.
284 for event
in kaevents
.values():
286 for thread
in kathreads
.values():
290 def threadexited(thread
):
291 if thread
.getExitCause() == 'EXCEPTION':
292 if isinstance(thread
.getExitException(), SystemExit):
293 # Bring a SystemExit into the main thread.
294 # Do not send it back to UI layer right now.
295 # Maybe later send it to ui.terminate?
297 ui
.threadException(thread
) # Expected to terminate
298 sys
.exit(100) # Just in case...
300 elif thread
.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
306 ui
.threadExited(thread
)
308 threadutil
.initexitnotify()
309 t
= ExitNotifyThread(target
=sync_with_timer
,
314 threadutil
.exitnotifymonitorloop(threadexited
)
318 ui
.mainException() # Also expected to terminate.