offlineimap/offlineimap/head/offlineimap.py

327 lines
12 KiB
Python
Raw Normal View History

#!/usr/bin/env python2.2
2002-06-19 04:21:27 +02:00
# Copyright (C) 2002 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from offlineimap import imaplib, imaputil, imapserver, repository, folder, mbnames, threadutil, version, localeval
from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
from offlineimap.ui import UIBase
import re, os, os.path, offlineimap, sys
2002-06-20 04:55:24 +02:00
from ConfigParser import ConfigParser
from threading import *
from getopt import getopt
2002-06-19 04:21:27 +02:00
options = {}
if '--help' in sys.argv[1:]:
sys.stdout.write(version.cmdhelp + "\n")
sys.exit(0)
for optlist in getopt(sys.argv[1:], 'P:1oa:c:d:u:h')[0]:
options[optlist[0]] = optlist[1]
if '-h' in options:
sys.stdout.write(version.cmdhelp)
sys.stdout.write("\n")
sys.exit(0)
configfilename = os.path.expanduser("~/.offlineimaprc")
if '-c' in options:
configfilename = options['-c']
if '-P' in options:
if not '-1' in options:
sys.stderr.write("FATAL: profile mode REQUIRES -1\n")
sys.exit(100)
profiledir = options['-P']
os.mkdir(profiledir)
threadutil.setprofiledir(profiledir)
sys.stderr.write("WARNING: profile mode engaged;\nPotentially large data will be created in " + profiledir + "\n")
2002-06-20 04:55:24 +02:00
config = ConfigParser()
2002-06-21 07:54:27 +02:00
if not os.path.exists(configfilename):
sys.stderr.write(" *** Config file %s does not exist; aborting!\n" % configfilename)
sys.exit(1)
config.read(configfilename)
if config.has_option("general", "pythonfile"):
path=os.path.expanduser(config.get("general", "pythonfile"))
else:
path=None
localeval = localeval.LocalEval(path)
ui = offlineimap.ui.detector.findUI(config, localeval, options.get('-u'))
ui.init_banner()
if '-d' in options:
for debugtype in options['-d'].split(','):
ui.add_debug(debugtype.strip())
if debugtype == 'imap':
imaplib.Debug = 5
if '-o' in options and config.has_option("general", "autorefresh"):
config.remove_option("general", "autorefresh")
2002-06-20 04:55:24 +02:00
metadatadir = os.path.expanduser(config.get("general", "metadata"))
if not os.path.exists(metadatadir):
os.mkdir(metadatadir, 0700)
2002-06-19 04:21:27 +02:00
2002-06-20 04:55:24 +02:00
accounts = config.get("general", "accounts")
if '-a' in options:
accounts = options['-a']
2002-06-20 04:55:24 +02:00
accounts = accounts.replace(" ", "")
accounts = accounts.split(",")
server = None
remoterepos = None
localrepos = None
passwords = {}
tunnels = {}
2002-07-04 05:59:19 +02:00
if '-1' in options:
threadutil.initInstanceLimit("ACCOUNTLIMIT", 1)
else:
threadutil.initInstanceLimit("ACCOUNTLIMIT",
config.getint("general", "maxsyncaccounts"))
# We have to gather passwords here -- don't want to have two threads
# asking for passwords simultaneously.
for account in accounts:
#if '.' in account:
# raise ValueError, "Account '%s' contains a dot; dots are not " \
# "allowed in account names." % account
if config.has_option(account, "preauthtunnel"):
tunnels[account] = config.get(account, "preauthtunnel")
elif config.has_option(account, "remotepass"):
2002-07-04 03:58:12 +02:00
passwords[account] = config.get(account, "remotepass")
elif config.has_option(account, "remotepassfile"):
passfile = open(os.path.expanduser(config.get(account, "remotepassfile")))
passwords[account] = passfile.readline().strip()
passfile.close()
else:
2002-07-04 03:58:12 +02:00
passwords[account] = ui.getpass(account, config)
2002-07-04 05:59:19 +02:00
for instancename in ["FOLDER_" + account, "MSGCOPY_" + account]:
if '-1' in options:
threadutil.initInstanceLimit(instancename, 1)
else:
threadutil.initInstanceLimit(instancename,
config.getint(account, "maxconnections"))
mailboxes = []
servers = {}
2002-07-04 03:58:12 +02:00
def syncaccount(accountname, *args):
# We don't need an account lock because syncitall() goes through
# each account once, then waits for all to finish.
try:
2002-06-21 09:25:24 +02:00
ui.acct(accountname)
accountmetadata = os.path.join(metadatadir, accountname)
if not os.path.exists(accountmetadata):
os.mkdir(accountmetadata, 0700)
server = None
if accountname in servers:
server = servers[accountname]
else:
server = imapserver.ConfigedIMAPServer(config, accountname, passwords)
servers[accountname] = server
remoterepos = repository.IMAP.IMAPRepository(config, localeval, accountname, server)
2002-06-21 09:25:24 +02:00
# Connect to the Maildirs.
localrepos = repository.Maildir.MaildirRepository(os.path.expanduser(config.get(accountname, "localfolders")), accountname, config)
2002-06-21 09:25:24 +02:00
# Connect to the local cache.
statusrepos = repository.LocalStatus.LocalStatusRepository(accountmetadata)
ui.syncfolders(remoterepos, localrepos)
remoterepos.syncfoldersto(localrepos)
ui.acct(accountname)
2002-06-21 09:25:24 +02:00
2002-07-04 03:58:12 +02:00
folderthreads = []
2002-06-21 09:25:24 +02:00
for remotefolder in remoterepos.getfolders():
2002-07-04 05:59:19 +02:00
thread = InstanceLimitedThread(\
instancename = 'FOLDER_' + accountname,
target = syncfolder,
name = "Folder sync %s[%s]" % \
2002-07-04 05:59:19 +02:00
(accountname, remotefolder.getvisiblename()),
args = (accountname, remoterepos, remotefolder, localrepos,
statusrepos))
thread.setDaemon(1)
2002-07-04 03:58:12 +02:00
thread.start()
folderthreads.append(thread)
threadutil.threadsreset(folderthreads)
if not (config.has_option(accountname, 'holdconnectionopen') and \
config.getboolean(accountname, 'holdconnectionopen')):
server.close()
finally:
2002-07-04 05:59:19 +02:00
pass
2002-06-21 09:25:24 +02:00
2002-07-04 03:58:12 +02:00
def syncfolder(accountname, remoterepos, remotefolder, localrepos,
statusrepos):
# Load local folder.
2002-07-05 02:07:02 +02:00
localfolder = localrepos.\
getfolder(remotefolder.getvisiblename().\
replace(remoterepos.getsep(), localrepos.getsep()))
# Write the mailboxes
mailboxes.append({'accountname': accountname,
'foldername': localfolder.getvisiblename()})
# Load local folder
2002-07-04 03:58:12 +02:00
ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
ui.loadmessagelist(localrepos, localfolder)
localfolder.cachemessagelist()
ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
# Load status folder.
statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
replace(remoterepos.getsep(),
statusrepos.getsep()))
if localfolder.getuidvalidity() == None:
# This is a new folder, so delete the status cache to be sure
# we don't have a conflict.
statusfolder.deletemessagelist()
statusfolder.cachemessagelist()
# If either the local or the status folder has messages and
# there is a UID validity problem, warn and abort.
# If there are no messages, UW IMAPd loses UIDVALIDITY.
# But we don't really need it if both local folders are empty.
# So, in that case, save it off.
if (len(localfolder.getmessagelist()) or \
len(statusfolder.getmessagelist())) and \
not localfolder.isuidvalidityok(remotefolder):
ui.validityproblem(remotefolder)
return
else:
localfolder.saveuidvalidity(remotefolder.getuidvalidity())
2002-07-04 03:58:12 +02:00
# Load remote folder.
ui.loadmessagelist(remoterepos, remotefolder)
remotefolder.cachemessagelist()
ui.messagelistloaded(remoterepos, remotefolder,
len(remotefolder.getmessagelist().keys()))
#
if not statusfolder.isnewfolder():
# Delete local copies of remote messages. This way,
# if a message's flag is modified locally but it has been
# deleted remotely, we'll delete it locally. Otherwise, we
# try to modify a deleted message's flags! This step
# need only be taken if a statusfolder is present; otherwise,
# there is no action taken *to* the remote repository.
remotefolder.syncmessagesto_delete(localfolder, [localfolder,
statusfolder])
ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder])
# Synchronize remote changes.
ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
remotefolder.syncmessagesto(localfolder)
# Make sure the status folder is up-to-date.
ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
localfolder.syncmessagesto(statusfolder)
statusfolder.save()
def syncitall():
global mailboxes
mailboxes = [] # Reset.
threads = []
2002-07-04 08:44:51 +02:00
for accountname in accounts:
2002-07-04 05:59:19 +02:00
thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT',
target = syncaccount,
name = "Account sync %s" % accountname,
2002-07-04 05:59:19 +02:00
args = (accountname,))
thread.setDaemon(1)
thread.start()
threads.append(thread)
# Wait for the threads to finish.
2002-07-04 03:58:12 +02:00
threadutil.threadsreset(threads)
mbnames.genmbnames(config, localeval, mailboxes)
2002-06-21 09:25:24 +02:00
def sync_with_timer():
currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
syncitall()
if config.has_option('general', 'autorefresh'):
refreshperiod = config.getint('general', 'autorefresh') * 60
while 1:
# Set up keep-alives.
kaevents = {}
kathreads = {}
for accountname in accounts:
if config.has_option(accountname, 'holdconnectionopen') and \
config.getboolean(accountname, 'holdconnectionopen') and \
config.has_option(accountname, 'keepalive'):
event = Event()
kaevents[accountname] = event
thread = ExitNotifyThread(target = servers[accountname].keepalive,
name = "Keep alive " + accountname,
args = (config.getint(accountname, 'keepalive'), event))
thread.setDaemon(1)
thread.start()
kathreads[accountname] = thread
if ui.sleep(refreshperiod) == 2:
# Cancel keep-alives, but don't bother terminating threads
for event in kaevents.values():
event.set()
break
else:
# Cancel keep-alives and wait for threads to terminate.
for event in kaevents.values():
event.set()
for thread in kathreads.values():
thread.join()
syncitall()
2002-06-20 04:55:24 +02:00
def threadexited(thread):
if thread.getExitCause() == 'EXCEPTION':
if isinstance(thread.getExitException(), SystemExit):
# Bring a SystemExit into the main thread.
# Do not send it back to UI layer right now.
# Maybe later send it to ui.terminate?
raise SystemExit
ui.threadException(thread) # Expected to terminate
sys.exit(100) # Just in case...
os._exit(100)
elif thread.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
ui.terminate()
# Just in case...
sys.exit(100)
os._exit(100)
else:
ui.threadExited(thread)
threadutil.initexitnotify()
t = ExitNotifyThread(target=sync_with_timer,
name='Sync Runner')
t.setDaemon(1)
t.start()
try:
threadutil.exitnotifymonitorloop(threadexited)
2002-07-05 04:59:08 +02:00
except SystemExit:
raise
except:
ui.mainException() # Also expected to terminate.