]>
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:], '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']
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
)
59 if '-o' in options
and config
.has_option("general", "autorefresh"):
60 config
.remove_option("general", "autorefresh")
62 metadatadir
= os
.path
.expanduser(config
.get("general", "metadata"))
63 if not os
.path
.exists(metadatadir
):
64 os
.mkdir(metadatadir
, 0700)
66 accounts
= config
.get("general", "accounts")
68 accounts
= options
['-a']
69 accounts
= accounts
.replace(" ", "")
70 accounts
= accounts
.split(",")
79 threadutil
.initInstanceLimit("ACCOUNTLIMIT", 1)
81 threadutil
.initInstanceLimit("ACCOUNTLIMIT",
82 config
.getint("general", "maxsyncaccounts"))
84 # We have to gather passwords here -- don't want to have two threads
85 # asking for passwords simultaneously.
87 for account
in accounts
:
88 if config
.has_option(account
, "preauthtunnel"):
89 tunnels
[account
] = config
.get(account
, "preauthtunnel")
90 elif config
.has_option(account
, "remotepass"):
91 passwords
[account
] = config
.get(account
, "remotepass")
92 elif config
.has_option(account
, "remotepassfile"):
93 passfile
= open(os
.path
.expanduser(config
.get(account
, "remotepassfile")))
94 passwords
[account
] = passfile
.readline().strip()
97 passwords
[account
] = ui
.getpass(account
, config
)
98 for instancename
in ["FOLDER_" + account
, "MSGCOPY_" + account
]:
100 threadutil
.initInstanceLimit(instancename
, 1)
102 threadutil
.initInstanceLimit(instancename
,
103 config
.getint(account
, "maxconnections"))
108 def syncaccount(accountname
, *args
):
109 # We don't need an account lock because syncitall() goes through
110 # each account once, then waits for all to finish.
113 accountmetadata
= os
.path
.join(metadatadir
, accountname
)
114 if not os
.path
.exists(accountmetadata
):
115 os
.mkdir(accountmetadata
, 0700)
118 if accountname
in servers
:
119 server
= servers
[accountname
]
121 server
= imapserver
.ConfigedIMAPServer(config
, accountname
, passwords
)
122 servers
[accountname
] = server
124 remoterepos
= repository
.IMAP
.IMAPRepository(config
, accountname
, server
)
126 # Connect to the Maildirs.
127 localrepos
= repository
.Maildir
.MaildirRepository(os
.path
.expanduser(config
.get(accountname
, "localfolders")))
129 # Connect to the local cache.
130 statusrepos
= repository
.LocalStatus
.LocalStatusRepository(accountmetadata
)
132 ui
.syncfolders(remoterepos
, localrepos
)
133 remoterepos
.syncfoldersto(localrepos
)
136 for remotefolder
in remoterepos
.getfolders():
137 thread
= InstanceLimitedThread(\
138 instancename
= 'FOLDER_' + accountname
,
140 name
= "Folder sync %s[%s]" % \
141 (accountname
, remotefolder
.getvisiblename()),
142 args
= (accountname
, remoterepos
, remotefolder
, localrepos
,
146 folderthreads
.append(thread
)
147 threadutil
.threadsreset(folderthreads
)
148 if not (config
.has_option(accountname
, 'holdconnectionopen') and \
149 config
.getboolean(accountname
, 'holdconnectionopen')):
154 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
157 localfolder
= localrepos
.\
158 getfolder(remotefolder
.getvisiblename().\
159 replace(remoterepos
.getsep(), localrepos
.getsep()))
160 # Write the mailboxes
161 mailboxes
.append({'accountname': accountname
,
162 'foldername': localfolder
.getvisiblename()})
164 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
165 ui
.loadmessagelist(localrepos
, localfolder
)
166 localfolder
.cachemessagelist()
167 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
169 # Load status folder.
170 statusfolder
= statusrepos
.getfolder(remotefolder
.getvisiblename().\
171 replace(remoterepos
.getsep(),
172 statusrepos
.getsep()))
173 statusfolder
.cachemessagelist()
176 # If either the local or the status folder has messages and
177 # there is a UID validity problem, warn and abort.
178 # If there are no messages, UW IMAPd loses UIDVALIDITY.
179 # But we don't really need it if both local folders are empty.
180 # So, in that case, save it off.
181 if (len(localfolder
.getmessagelist()) or \
182 len(statusfolder
.getmessagelist())) and \
183 not localfolder
.isuidvalidityok(remotefolder
):
184 ui
.validityproblem(remotefolder
)
187 localfolder
.saveuidvalidity(remotefolder
.getuidvalidity())
189 # Load remote folder.
190 ui
.loadmessagelist(remoterepos
, remotefolder
)
191 remotefolder
.cachemessagelist()
192 ui
.messagelistloaded(remoterepos
, remotefolder
,
193 len(remotefolder
.getmessagelist().keys()))
198 if not statusfolder
.isnewfolder():
199 # Delete local copies of remote messages. This way,
200 # if a message's flag is modified locally but it has been
201 # deleted remotely, we'll delete it locally. Otherwise, we
202 # try to modify a deleted message's flags! This step
203 # need only be taken if a statusfolder is present; otherwise,
204 # there is no action taken *to* the remote repository.
206 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
208 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
209 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
211 # Synchronize remote changes.
212 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
213 remotefolder
.syncmessagesto(localfolder
)
215 # Make sure the status folder is up-to-date.
216 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
217 localfolder
.syncmessagesto(statusfolder
)
223 mailboxes
= [] # Reset.
225 for accountname
in accounts
:
226 thread
= InstanceLimitedThread(instancename
= 'ACCOUNTLIMIT',
227 target
= syncaccount
,
228 name
= "Account sync %s" % accountname
,
229 args
= (accountname
,))
232 threads
.append(thread
)
233 # Wait for the threads to finish.
234 threadutil
.threadsreset(threads
)
235 mbnames
.genmbnames(config
, mailboxes
)
237 def sync_with_timer():
238 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
240 if config
.has_option('general', 'autorefresh'):
241 refreshperiod
= config
.getint('general', 'autorefresh') * 60
243 # Set up keep-alives.
246 for accountname
in accounts
:
247 if config
.has_option(accountname
, 'holdconnectionopen') and \
248 config
.getboolean(accountname
, 'holdconnectionopen') and \
249 config
.has_option(accountname
, 'keepalive'):
251 kaevents
[accountname
] = event
252 thread
= ExitNotifyThread(target
= servers
[accountname
].keepalive
,
253 name
= "Keep alive " + accountname
,
254 args
= (config
.getint(accountname
, 'keepalive'), event
))
257 kathreads
[accountname
] = thread
258 if ui
.sleep(refreshperiod
) == 2:
259 # Cancel keep-alives, but don't bother terminating threads
260 for event
in kaevents
.values():
264 # Cancel keep-alives and wait for threads to terminate.
265 for event
in kaevents
.values():
267 for thread
in kathreads
.values():
271 def threadexited(thread
):
272 if thread
.getExitCause() == 'EXCEPTION':
273 if isinstance(thread
.getExitException(), SystemExit):
274 # Bring a SystemExit into the main thread.
275 # Do not send it back to UI layer right now.
276 # Maybe later send it to ui.terminate?
278 ui
.threadException(thread
) # Expected to terminate
279 sys
.exit(100) # Just in case...
281 elif thread
.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
287 ui
.threadExited(thread
)
289 threadutil
.initexitnotify()
290 t
= ExitNotifyThread(target
=sync_with_timer
,
295 threadutil
.exitnotifymonitorloop(threadexited
)
299 ui
.mainException() # Also expected to terminate.