]>
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'])()
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
:
97 if config
.has_option(account
, "preauthtunnel"):
98 tunnels
[account
] = config
.get(account
, "preauthtunnel")
99 elif config
.has_option(account
, "remotepass"):
100 passwords
[account
] = config
.get(account
, "remotepass")
101 elif config
.has_option(account
, "remotepassfile"):
102 passfile
= open(os
.path
.expanduser(config
.get(account
, "remotepassfile")))
103 passwords
[account
] = passfile
.readline().strip()
106 passwords
[account
] = ui
.getpass(account
, config
)
107 for instancename
in ["FOLDER_" + account
, "MSGCOPY_" + account
]:
109 threadutil
.initInstanceLimit(instancename
, 1)
111 threadutil
.initInstanceLimit(instancename
,
112 config
.getint(account
, "maxconnections"))
117 def syncaccount(accountname
, *args
):
118 # We don't need an account lock because syncitall() goes through
119 # each account once, then waits for all to finish.
122 accountmetadata
= os
.path
.join(metadatadir
, accountname
)
123 if not os
.path
.exists(accountmetadata
):
124 os
.mkdir(accountmetadata
, 0700)
127 if accountname
in servers
:
128 server
= servers
[accountname
]
130 server
= imapserver
.ConfigedIMAPServer(config
, accountname
, passwords
)
131 servers
[accountname
] = server
133 remoterepos
= repository
.IMAP
.IMAPRepository(config
, accountname
, server
)
135 # Connect to the Maildirs.
136 localrepos
= repository
.Maildir
.MaildirRepository(os
.path
.expanduser(config
.get(accountname
, "localfolders")))
138 # Connect to the local cache.
139 statusrepos
= repository
.LocalStatus
.LocalStatusRepository(accountmetadata
)
141 ui
.syncfolders(remoterepos
, localrepos
)
142 remoterepos
.syncfoldersto(localrepos
)
145 for remotefolder
in remoterepos
.getfolders():
146 thread
= InstanceLimitedThread(\
147 instancename
= 'FOLDER_' + accountname
,
149 name
= "Folder sync %s[%s]" % \
150 (accountname
, remotefolder
.getvisiblename()),
151 args
= (accountname
, remoterepos
, remotefolder
, localrepos
,
155 folderthreads
.append(thread
)
156 threadutil
.threadsreset(folderthreads
)
157 if not (config
.has_option(accountname
, 'holdconnectionopen') and \
158 config
.getboolean(accountname
, 'holdconnectionopen')):
163 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
166 localfolder
= localrepos
.\
167 getfolder(remotefolder
.getvisiblename().\
168 replace(remoterepos
.getsep(), localrepos
.getsep()))
169 # Write the mailboxes
170 mailboxes
.append({'accountname': accountname
,
171 'foldername': localfolder
.getvisiblename()})
173 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
174 ui
.loadmessagelist(localrepos
, localfolder
)
175 localfolder
.cachemessagelist()
176 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
178 # Load status folder.
179 statusfolder
= statusrepos
.getfolder(remotefolder
.getvisiblename().\
180 replace(remoterepos
.getsep(),
181 statusrepos
.getsep()))
182 statusfolder
.cachemessagelist()
185 # If either the local or the status folder has messages and
186 # there is a UID validity problem, warn and abort.
187 # If there are no messages, UW IMAPd loses UIDVALIDITY.
188 # But we don't really need it if both local folders are empty.
189 # So, in that case, save it off.
190 if (len(localfolder
.getmessagelist()) or \
191 len(statusfolder
.getmessagelist())) and \
192 not localfolder
.isuidvalidityok(remotefolder
):
193 ui
.validityproblem(remotefolder
)
196 localfolder
.saveuidvalidity(remotefolder
.getuidvalidity())
198 # Load remote folder.
199 ui
.loadmessagelist(remoterepos
, remotefolder
)
200 remotefolder
.cachemessagelist()
201 ui
.messagelistloaded(remoterepos
, remotefolder
,
202 len(remotefolder
.getmessagelist().keys()))
207 if not statusfolder
.isnewfolder():
208 # Delete local copies of remote messages. This way,
209 # if a message's flag is modified locally but it has been
210 # deleted remotely, we'll delete it locally. Otherwise, we
211 # try to modify a deleted message's flags! This step
212 # need only be taken if a statusfolder is present; otherwise,
213 # there is no action taken *to* the remote repository.
215 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
217 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
218 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
220 # Synchronize remote changes.
221 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
222 remotefolder
.syncmessagesto(localfolder
)
224 # Make sure the status folder is up-to-date.
225 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
226 localfolder
.syncmessagesto(statusfolder
)
232 mailboxes
= [] # Reset.
234 for accountname
in accounts
:
235 thread
= InstanceLimitedThread(instancename
= 'ACCOUNTLIMIT',
236 target
= syncaccount
,
237 name
= "Account sync %s" % accountname
,
238 args
= (accountname
,))
241 threads
.append(thread
)
242 # Wait for the threads to finish.
243 threadutil
.threadsreset(threads
)
244 mbnames
.genmbnames(config
, mailboxes
)
246 def sync_with_timer():
247 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
249 if config
.has_option('general', 'autorefresh'):
250 refreshperiod
= config
.getint('general', 'autorefresh') * 60
252 # Set up keep-alives.
255 for accountname
in accounts
:
256 if config
.has_option(accountname
, 'holdconnectionopen') and \
257 config
.getboolean(accountname
, 'holdconnectionopen') and \
258 config
.has_option(accountname
, 'keepalive'):
260 kaevents
[accountname
] = event
261 thread
= ExitNotifyThread(target
= servers
[accountname
].keepalive
,
262 name
= "Keep alive " + accountname
,
263 args
= (config
.getint(accountname
, 'keepalive'), event
))
266 kathreads
[accountname
] = thread
267 if ui
.sleep(refreshperiod
) == 2:
268 # Cancel keep-alives, but don't bother terminating threads
269 for event
in kaevents
.values():
273 # Cancel keep-alives and wait for threads to terminate.
274 for event
in kaevents
.values():
276 for thread
in kathreads
.values():
280 def threadexited(thread
):
281 if thread
.getExitCause() == 'EXCEPTION':
282 if isinstance(thread
.getExitException(), SystemExit):
283 # Bring a SystemExit into the main thread.
284 # Do not send it back to UI layer right now.
285 # Maybe later send it to ui.terminate?
287 ui
.threadException(thread
) # Expected to terminate
288 sys
.exit(100) # Just in case...
290 elif thread
.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
296 ui
.threadExited(thread
)
298 threadutil
.initexitnotify()
299 t
= ExitNotifyThread(target
=sync_with_timer
,
304 threadutil
.exitnotifymonitorloop(threadexited
)
308 ui
.mainException() # Also expected to terminate.