]>
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
21 from offlineimap
.threadutil
import InstanceLimitedThread
, ExitNotifyThread
22 import re
, os
, os
.path
, offlineimap
, sys
23 from ConfigParser
import ConfigParser
24 from threading
import *
29 #ui = offlineimap.ui.TTY.TTYUI()
30 ui
= offlineimap
.ui
.Tk
.TkUI()
33 config
= ConfigParser()
34 configfilename
= os
.path
.expanduser("~/.offlineimaprc")
35 if not os
.path
.exists(configfilename
):
36 sys
.stderr
.write(" *** Config file %s does not exist; aborting!\n" % configfilename
)
39 config
.read(configfilename
)
41 metadatadir
= os
.path
.expanduser(config
.get("general", "metadata"))
42 if not os
.path
.exists(metadatadir
):
43 os
.mkdir(metadatadir
, 0700)
45 accounts
= config
.get("general", "accounts")
46 accounts
= accounts
.replace(" ", "")
47 accounts
= accounts
.split(",")
55 threadutil
.initInstanceLimit("ACCOUNTLIMIT", config
.getint("general",
58 # We have to gather passwords here -- don't want to have two threads
59 # asking for passwords simultaneously.
61 for account
in accounts
:
62 if config
.has_option(account
, "preauthtunnel"):
63 tunnels
[account
] = config
.get(account
, "preauthtunnel")
64 elif config
.has_option(account
, "remotepass"):
65 passwords
[account
] = config
.get(account
, "remotepass")
66 elif config
.has_option(account
, "remotepassfile"):
67 passfile
= os
.path
.expanduser(config
.get(account
, "remotepassfile"))
68 passwords
[account
] = passfile
.readline().strip()
71 passwords
[account
] = ui
.getpass(account
, config
)
72 for instancename
in ["FOLDER_" + account
, "MSGCOPY_" + account
]:
73 threadutil
.initInstanceLimit(instancename
,
74 config
.getint(account
, "maxconnections"))
80 def addmailbox(accountname
, remotefolder
):
82 mailboxes
.append({'accountname' : accountname
,
83 'foldername': remotefolder
.getvisiblename()})
86 def syncaccount(accountname
, *args
):
87 # We don't need an account lock because syncitall() goes through
88 # each account once, then waits for all to finish.
91 accountmetadata
= os
.path
.join(metadatadir
, accountname
)
92 if not os
.path
.exists(accountmetadata
):
93 os
.mkdir(accountmetadata
, 0700)
96 if accountname
in servers
:
97 server
= servers
[accountname
]
99 server
= imapserver
.ConfigedIMAPServer(config
, accountname
, passwords
)
100 servers
[accountname
] = server
102 remoterepos
= repository
.IMAP
.IMAPRepository(config
, accountname
, server
)
104 # Connect to the Maildirs.
105 localrepos
= repository
.Maildir
.MaildirRepository(os
.path
.expanduser(config
.get(accountname
, "localfolders")))
107 # Connect to the local cache.
108 statusrepos
= repository
.LocalStatus
.LocalStatusRepository(accountmetadata
)
110 ui
.syncfolders(remoterepos
, localrepos
)
111 remoterepos
.syncfoldersto(localrepos
)
114 for remotefolder
in remoterepos
.getfolders():
115 thread
= InstanceLimitedThread(\
116 instancename
= 'FOLDER_' + accountname
,
118 name
= "syncfolder-%s-%s" % \
119 (accountname
, remotefolder
.getvisiblename()),
120 args
= (accountname
, remoterepos
, remotefolder
, localrepos
,
124 folderthreads
.append(thread
)
125 threadutil
.threadsreset(folderthreads
)
126 if not (config
.has_option(accountname
, 'holdconnectionopen') and \
127 config
.getboolean(accountname
, 'holdconnectionopen')):
132 def syncfolder(accountname
, remoterepos
, remotefolder
, localrepos
,
134 mailboxes
.append({'accountname': accountname
,
135 'foldername': remotefolder
.getvisiblename()})
137 localfolder
= localrepos
.\
138 getfolder(remotefolder
.getvisiblename().\
139 replace(remoterepos
.getsep(), localrepos
.getsep()))
140 if not localfolder
.isuidvalidityok(remotefolder
):
141 ui
.validityproblem(remotefolder
)
143 ui
.syncingfolder(remoterepos
, remotefolder
, localrepos
, localfolder
)
144 ui
.loadmessagelist(localrepos
, localfolder
)
145 localfolder
.cachemessagelist()
146 ui
.messagelistloaded(localrepos
, localfolder
, len(localfolder
.getmessagelist().keys()))
148 # Load remote folder.
149 ui
.loadmessagelist(remoterepos
, remotefolder
)
150 remotefolder
.cachemessagelist()
151 ui
.messagelistloaded(remoterepos
, remotefolder
,
152 len(remotefolder
.getmessagelist().keys()))
154 # Load status folder.
155 statusfolder
= statusrepos
.getfolder(remotefolder
.getvisiblename().\
156 replace(remoterepos
.getsep(),
157 statusrepos
.getsep()))
158 statusfolder
.cachemessagelist()
162 if not statusfolder
.isnewfolder():
163 # Delete local copies of remote messages. This way,
164 # if a message's flag is modified locally but it has been
165 # deleted remotely, we'll delete it locally. Otherwise, we
166 # try to modify a deleted message's flags! This step
167 # need only be taken if a statusfolder is present; otherwise,
168 # there is no action taken *to* the remote repository.
170 remotefolder
.syncmessagesto_delete(localfolder
, [localfolder
,
172 ui
.syncingmessages(localrepos
, localfolder
, remoterepos
, remotefolder
)
173 localfolder
.syncmessagesto(statusfolder
, [remotefolder
, statusfolder
])
175 # Synchronize remote changes.
176 ui
.syncingmessages(remoterepos
, remotefolder
, localrepos
, localfolder
)
177 remotefolder
.syncmessagesto(localfolder
)
179 # Make sure the status folder is up-to-date.
180 ui
.syncingmessages(localrepos
, localfolder
, statusrepos
, statusfolder
)
181 localfolder
.syncmessagesto(statusfolder
)
187 mailboxes
= [] # Reset.
189 for accountname
in accounts
:
190 thread
= InstanceLimitedThread(instancename
= 'ACCOUNTLIMIT',
191 target
= syncaccount
,
192 name
= "syncaccount-%s" % accountname
,
193 args
= (accountname
,))
196 threads
.append(thread
)
197 # Wait for the threads to finish.
198 threadutil
.threadsreset(threads
)
199 mbnames
.genmbnames(config
, mailboxes
)
201 def sync_with_timer():
202 currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
204 if config
.has_option('general', 'autorefresh'):
205 refreshperiod
= config
.getint('general', 'autorefresh') * 60
207 # Set up keep-alives.
210 for accountname
in accounts
:
211 if config
.has_option(accountname
, 'holdconnectionopen') and \
212 config
.getboolean(accountname
, 'holdconnectionopen') and \
213 config
.has_option(accountname
, 'keepalive'):
215 kaevents
[accountname
] = event
216 thread
= ExitNotifyThread(target
= servers
[accountname
].keepalive
,
217 args
= (config
.getint(accountname
, 'keepalive'), event
))
220 kathreads
[accountname
] = thread
221 if ui
.sleep(refreshperiod
) == 2:
222 # Cancel keep-alives, but don't bother terminating threads
223 for event
in kaevents
.values():
227 # Cancel keep-alives and wait for threads to terminate.
228 for event
in kaevents
.values():
230 for thread
in kathreads
.values():
234 def threadexited(thread
):
235 if thread
.getExitCause() == 'EXCEPTION':
236 ui
.threadException(thread
) # Expected to terminate
237 sys
.exit(100) # Just in case...
239 elif thread
.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
245 ui
.threadExited(thread
)
247 threadutil
.initexitnotify()
248 t
= ExitNotifyThread(target
=sync_with_timer
, name
='sync_with_timer')
252 threadutil
.exitnotifymonitorloop(threadexited
)
256 ui
.mainException() # Also expected to terminate.