]>
code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/init.py
1 # OfflineIMAP initialization code
2 # Copyright (C) 2002 John Goerzen
3 # <jgoerzen@complete.org>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 from offlineimap
import imaplib
, imapserver
, repository
, folder
, mbnames
, threadutil
, version
20 from offlineimap
. localeval
import LocalEval
21 from offlineimap
. threadutil
import InstanceLimitedThread
, ExitNotifyThread
22 from offlineimap
. ui
import UIBase
23 import re
, os
, os
. path
, offlineimap
, sys
24 from ConfigParser
import ConfigParser
25 from threading
import *
26 from getopt
import getopt
29 assert revno
== version
. revno
, "Revision of main program ( %d ) does not match that of library ( %d ). Please double-check your PYTHONPATH and installation locations." % ( revno
, version
. revno
)
31 if '--help' in sys
. argv
[ 1 :]:
32 sys
. stdout
. write ( version
. cmdhelp
+ " \n " )
35 for optlist
in getopt ( sys
. argv
[ 1 :], 'P:1oa:c:d:u:h' )[ 0 ]:
36 options
[ optlist
[ 0 ]] = optlist
[ 1 ]
39 sys
. stdout
. write ( version
. cmdhelp
)
40 sys
. stdout
. write ( " \n " )
42 configfilename
= os
. path
. expanduser ( "~/.offlineimaprc" )
44 configfilename
= options
[ '-c' ]
46 if not '-1' in options
:
47 sys
. stderr
. write ( "FATAL: profile mode REQUIRES -1 \n " )
49 profiledir
= options
[ '-P' ]
51 threadutil
. setprofiledir ( profiledir
)
52 sys
. stderr
. write ( "WARNING: profile mode engaged; \n Potentially large data will be created in " + profiledir
+ " \n " )
54 config
= ConfigParser ()
55 if not os
. path
. exists ( configfilename
):
56 sys
. stderr
. write ( " *** Config file %s does not exist; aborting! \n " % configfilename
)
59 config
. read ( configfilename
)
61 if config
. has_option ( "general" , "pythonfile" ):
62 path
= os
. path
. expanduser ( config
. get ( "general" , "pythonfile" ))
65 localeval
= LocalEval ( path
)
67 ui
= offlineimap
. ui
. detector
. findUI ( config
, localeval
, options
. get ( '-u' ))
69 UIBase
. setglobalui ( ui
)
70 print "UI is" , UIBase
. getglobalui ()
73 for debugtype
in options
[ '-d' ]. split ( ',' ):
74 ui
. add_debug ( debugtype
. strip ())
75 if debugtype
== 'imap' :
78 if '-o' in options
and config
. has_option ( "general" , "autorefresh" ):
79 config
. remove_option ( "general" , "autorefresh" )
81 metadatadir
= os
. path
. expanduser ( config
. get ( "general" , "metadata" ))
82 if not os
. path
. exists ( metadatadir
):
83 os
. mkdir ( metadatadir
, 0700 )
85 accounts
= config
. get ( "general" , "accounts" )
87 accounts
= options
[ '-a' ]
88 accounts
= accounts
. replace ( " " , "" )
89 accounts
= accounts
. split ( "," )
98 threadutil
. initInstanceLimit ( "ACCOUNTLIMIT" , 1 )
100 threadutil
. initInstanceLimit ( "ACCOUNTLIMIT" ,
101 config
. getint ( "general" , "maxsyncaccounts" ))
103 # We have to gather passwords here -- don't want to have two threads
104 # asking for passwords simultaneously.
106 for account
in accounts
:
107 print "Processing account" , account
109 # raise ValueError, "Account '%s' contains a dot; dots are not " \
110 # "allowed in account names." % account
111 if config
. has_option ( account
, "preauthtunnel" ):
112 tunnels
[ account
] = config
. get ( account
, "preauthtunnel" )
113 elif config
. has_option ( account
, "remotepass" ):
114 passwords
[ account
] = config
. get ( account
, "remotepass" )
115 elif config
. has_option ( account
, "remotepassfile" ):
116 passfile
= open ( os
. path
. expanduser ( config
. get ( account
, "remotepassfile" )))
117 passwords
[ account
] = passfile
. readline (). strip ()
120 passwords
[ account
] = ui
. getpass ( account
, config
)
121 for instancename
in [ "FOLDER_" + account
, "MSGCOPY_" + account
]:
123 threadutil
. initInstanceLimit ( instancename
, 1 )
125 threadutil
. initInstanceLimit ( instancename
,
126 config
. getint ( account
, "maxconnections" ))
131 threadutil
. initexitnotify ()
132 t
= ExitNotifyThread ( target
= sync_with_timer
,
137 threadutil
. exitnotifymonitorloop ( threadexited
)
141 ui
. mainException () # Also expected to terminate.
143 def syncaccount ( accountname
, * args
):
144 ui
= UIBase
. getglobalui ()
145 # We don't need an account lock because syncitall() goes through
146 # each account once, then waits for all to finish.
149 accountmetadata
= os
. path
. join ( metadatadir
, accountname
)
150 if not os
. path
. exists ( accountmetadata
):
151 os
. mkdir ( accountmetadata
, 0700 )
154 if accountname
in servers
:
155 server
= servers
[ accountname
]
157 server
= imapserver
. ConfigedIMAPServer ( config
, accountname
, passwords
)
158 servers
[ accountname
] = server
160 remoterepos
= repository
. IMAP
. IMAPRepository ( config
, localeval
, accountname
, server
)
162 # Connect to the Maildirs.
163 localrepos
= repository
. Maildir
. MaildirRepository ( os
. path
. expanduser ( config
. get ( accountname
, "localfolders" )), accountname
, config
)
165 # Connect to the local cache.
166 statusrepos
= repository
. LocalStatus
. LocalStatusRepository ( accountmetadata
)
168 ui
. syncfolders ( remoterepos
, localrepos
)
169 remoterepos
. syncfoldersto ( localrepos
)
173 for remotefolder
in remoterepos
. getfolders ():
174 thread
= InstanceLimitedThread ( \
175 instancename
= 'FOLDER_' + accountname
,
177 name
= "Folder sync %s [ %s ]" % \
178 ( accountname
, remotefolder
. getvisiblename ()),
179 args
= ( accountname
, remoterepos
, remotefolder
, localrepos
,
183 folderthreads
. append ( thread
)
184 threadutil
. threadsreset ( folderthreads
)
185 if not ( config
. has_option ( accountname
, 'holdconnectionopen' ) and \
186 config
. getboolean ( accountname
, 'holdconnectionopen' )):
191 def syncfolder ( accountname
, remoterepos
, remotefolder
, localrepos
,
193 ui
= UIBase
. getglobalui ()
195 localfolder
= localrepos
. \
196 getfolder ( remotefolder
. getvisiblename (). \
197 replace ( remoterepos
. getsep (), localrepos
. getsep ()))
198 # Write the mailboxes
199 mailboxes
. append ({ 'accountname' : accountname
,
200 'foldername' : localfolder
. getvisiblename ()})
202 ui
. syncingfolder ( remoterepos
, remotefolder
, localrepos
, localfolder
)
203 ui
. loadmessagelist ( localrepos
, localfolder
)
204 localfolder
. cachemessagelist ()
205 ui
. messagelistloaded ( localrepos
, localfolder
, len ( localfolder
. getmessagelist (). keys ()))
208 # Load status folder.
209 statusfolder
= statusrepos
. getfolder ( remotefolder
. getvisiblename (). \
210 replace ( remoterepos
. getsep (),
211 statusrepos
. getsep ()))
212 if localfolder
. getuidvalidity () == None :
213 # This is a new folder, so delete the status cache to be sure
214 # we don't have a conflict.
215 statusfolder
. deletemessagelist ()
217 statusfolder
. cachemessagelist ()
220 # If either the local or the status folder has messages and
221 # there is a UID validity problem, warn and abort.
222 # If there are no messages, UW IMAPd loses UIDVALIDITY.
223 # But we don't really need it if both local folders are empty.
224 # So, in that case, save it off.
225 if ( len ( localfolder
. getmessagelist ()) or \
226 len ( statusfolder
. getmessagelist ())) and \
227 not localfolder
. isuidvalidityok ( remotefolder
):
228 ui
. validityproblem ( remotefolder
)
231 localfolder
. saveuidvalidity ( remotefolder
. getuidvalidity ())
233 # Load remote folder.
234 ui
. loadmessagelist ( remoterepos
, remotefolder
)
235 remotefolder
. cachemessagelist ()
236 ui
. messagelistloaded ( remoterepos
, remotefolder
,
237 len ( remotefolder
. getmessagelist (). keys ()))
242 if not statusfolder
. isnewfolder ():
243 # Delete local copies of remote messages. This way,
244 # if a message's flag is modified locally but it has been
245 # deleted remotely, we'll delete it locally. Otherwise, we
246 # try to modify a deleted message's flags! This step
247 # need only be taken if a statusfolder is present; otherwise,
248 # there is no action taken *to* the remote repository.
250 remotefolder
. syncmessagesto_delete ( localfolder
, [ localfolder
,
252 ui
. syncingmessages ( localrepos
, localfolder
, remoterepos
, remotefolder
)
253 localfolder
. syncmessagesto ( statusfolder
, [ remotefolder
, statusfolder
])
255 # Synchronize remote changes.
256 ui
. syncingmessages ( remoterepos
, remotefolder
, localrepos
, localfolder
)
257 remotefolder
. syncmessagesto ( localfolder
)
259 # Make sure the status folder is up-to-date.
260 ui
. syncingmessages ( localrepos
, localfolder
, statusrepos
, statusfolder
)
261 localfolder
. syncmessagesto ( statusfolder
)
267 ui
= UIBase
. getglobalui ()
269 mailboxes
= [] # Reset.
271 for accountname
in accounts
:
272 thread
= InstanceLimitedThread ( instancename
= 'ACCOUNTLIMIT' ,
273 target
= syncaccount
,
274 name
= "Account sync %s " % accountname
,
275 args
= ( accountname
,))
278 threads
. append ( thread
)
279 # Wait for the threads to finish.
280 threadutil
. threadsreset ( threads
)
281 mbnames
. genmbnames ( config
, localeval
, mailboxes
)
283 def sync_with_timer ():
284 ui
= UIBase
. getglobalui ()
285 currentThread (). setExitMessage ( 'SYNC_WITH_TIMER_TERMINATE' )
287 if config
. has_option ( 'general' , 'autorefresh' ):
288 refreshperiod
= config
. getint ( 'general' , 'autorefresh' ) * 60
290 # Set up keep-alives.
293 for accountname
in accounts
:
294 if config
. has_option ( accountname
, 'holdconnectionopen' ) and \
295 config
. getboolean ( accountname
, 'holdconnectionopen' ) and \
296 config
. has_option ( accountname
, 'keepalive' ):
298 kaevents
[ accountname
] = event
299 thread
= ExitNotifyThread ( target
= servers
[ accountname
]. keepalive
,
300 name
= "Keep alive " + accountname
,
301 args
= ( config
. getint ( accountname
, 'keepalive' ), event
))
304 kathreads
[ accountname
] = thread
305 if ui
. sleep ( refreshperiod
) == 2 :
306 # Cancel keep-alives, but don't bother terminating threads
307 for event
in kaevents
. values ():
311 # Cancel keep-alives and wait for threads to terminate.
312 for event
in kaevents
. values ():
314 for thread
in kathreads
. values ():
318 def threadexited ( thread
):
319 ui
= UIBase
. getglobalui ()
320 if thread
. getExitCause () == 'EXCEPTION' :
321 if isinstance ( thread
. getExitException (), SystemExit ):
322 # Bring a SystemExit into the main thread.
323 # Do not send it back to UI layer right now.
324 # Maybe later send it to ui.terminate?
326 ui
. threadException ( thread
) # Expected to terminate
327 sys
. exit ( 100 ) # Just in case...
329 elif thread
. getExitMessage () == 'SYNC_WITH_TIMER_TERMINATE' :
335 ui
. threadExited ( thread
)