]>
code.delx.au - offlineimap/blob - 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:], '1a: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']
46 config
= ConfigParser()
47 if not os
.path
.exists(configfilename
):
48 sys
.stderr
.write(" *** Config file %s does not exist; aborting!\n" % configfilename
)
51 config
.read(configfilename
)
54 ui
= offlineimap
.ui
.detector
.getUImod(options
['-u'])()
56 ui
= offlineimap
.ui
.detector
.findUI(config
)
60 metadatadir
= os
.path
.expanduser(config
.get("general", "metadata"))
61 if not os
.path
.exists(metadatadir
):
62 os
.mkdir(metadatadir
, 0700)
64 accounts
= config
.get("general", "accounts")
66 accounts
= options
['-a']
67 accounts
= accounts
.replace(" ", "")
68 accounts
= accounts
.split(",")
77 threadutil
.initInstanceLimit("ACCOUNTLIMIT", 1)
79 threadutil
.initInstanceLimit("ACCOUNTLIMIT",
80 config
.getint("general", "maxsyncaccounts"))
82 # We have to gather passwords here -- don't want to have two threads
83 # asking for passwords simultaneously.
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()
95 passwords
[account
] = ui
.getpass(account
, config
)
96 for instancename
in ["FOLDER_" + account
, "MSGCOPY_" + account
]:
98 threadutil
.initInstanceLimit(instancename
, 1)
100 threadutil
.initInstanceLimit(instancename
,
101 config
.getint(account
, "maxconnections"))
107 def addmailbox(accountname
, remotefolder
):
108 mailboxlock
.acquire()
109 mailboxes
.append({'accountname' : accountname
,
110 'foldername': remotefolder
.getvisiblename()})
111 mailboxlock
.release()
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.
118 accountmetadata
= os
.path
.join(metadatadir
, accountname
)
119 if not os
.path
.exists(accountmetadata
):
120 os
.mkdir(accountmetadata
, 0700)
123 if accountname
in servers
:
124 server
= servers
[accountname
]
126 server
= imapserver
.ConfigedIMAPServer(config
, accountname
, passwords
)
127 servers
[accountname
] = server
129 remoterepos
= repository
.IMAP
.IMAPRepository(config
, accountname
, server
)
131 # Connect to the Maildirs.
132 localrepos
= repository
.Maildir
.MaildirRepository(os
.path
.expanduser(config
.get(accountname
, "localfolders")))
134 # Connect to the local cache.
135 statusrepos
= repository
.LocalStatus
.LocalStatusRepository(accountmetadata
)
137 ui
.syncfolders(remoterepos
, localrepos
)
138 remoterepos
.syncfoldersto(localrepos
)
141 for remotefolder
in remoterepos
.getfolders():
142 thread
= InstanceLimitedThread(\
143 instancename
= 'FOLDER_' + accountname
,
145 name
= "Folder sync %s[%s]" % \
146 (accountname
, remotefolder
.getvisiblename()),
147 args
= (accountname
, remoterepos
, remotefolder
, localrepos
,
151 folderthreads
.append(thread
)
152 threadutil
.threadsreset(folderthreads
)
153 if not (config
.has_option(accountname
, 'holdconnectionopen') and \
154 config
.getboolean(accountname
, 'holdconnectionopen')):
159 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
161 mailboxes
.append({'accountname': accountname
,
162 'foldername': remotefolder
.getvisiblename()})
164 localfolder
= localrepos
.\
165 getfolder(remotefolder
.getvisiblename().\
166 replace(remoterepos
.getsep(), localrepos
.getsep()))
168 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
169 ui
.loadmessagelist(localrepos
, localfolder
)
170 localfolder
.cachemessagelist()
171 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
173 # Load status folder.
174 statusfolder
= statusrepos
.getfolder(remotefolder
.getvisiblename().\
175 replace(remoterepos
.getsep(),
176 statusrepos
.getsep()))
177 statusfolder
.cachemessagelist()
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
)
191 localfolder
.saveuidvalidity(remotefolder
.getuidvalidity())
193 # Load remote folder.
194 ui
.loadmessagelist(remoterepos
, remotefolder
)
195 remotefolder
.cachemessagelist()
196 ui
.messagelistloaded(remoterepos
, remotefolder
,
197 len(remotefolder
.getmessagelist().keys()))
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.
210 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
212 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
213 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
215 # Synchronize remote changes.
216 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
217 remotefolder
.syncmessagesto(localfolder
)
219 # Make sure the status folder is up-to-date.
220 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
221 localfolder
.syncmessagesto(statusfolder
)
227 mailboxes
= [] # Reset.
229 for accountname
in accounts
:
230 thread
= InstanceLimitedThread(instancename
= 'ACCOUNTLIMIT',
231 target
= syncaccount
,
232 name
= "Account sync %s" % accountname
,
233 args
= (accountname
,))
236 threads
.append(thread
)
237 # Wait for the threads to finish.
238 threadutil
.threadsreset(threads
)
239 mbnames
.genmbnames(config
, mailboxes
)
241 def sync_with_timer():
242 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
244 if config
.has_option('general', 'autorefresh'):
245 refreshperiod
= config
.getint('general', 'autorefresh') * 60
247 # Set up keep-alives.
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'):
255 kaevents
[accountname
] = event
256 thread
= ExitNotifyThread(target
= servers
[accountname
].keepalive
,
257 name
= "Keep alive " + accountname
,
258 args
= (config
.getint(accountname
, 'keepalive'), event
))
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():
268 # Cancel keep-alives and wait for threads to terminate.
269 for event
in kaevents
.values():
271 for thread
in kathreads
.values():
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?
282 ui
.threadException(thread
) # Expected to terminate
283 sys
.exit(100) # Just in case...
285 elif thread
.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
291 ui
.threadExited(thread
)
293 threadutil
.initexitnotify()
294 t
= ExitNotifyThread(target
=sync_with_timer
,
299 threadutil
.exitnotifymonitorloop(threadexited
)
303 ui
.mainException() # Also expected to terminate.