1
0
mirror of https://github.com/OfflineIMAP/offlineimap.git synced 2024-06-25 07:27:44 +02:00

Add IDLE support from James Bunton

Merge branch 'bunton'
This commit is contained in:
John Goerzen 2009-02-12 20:22:32 -06:00
commit 9e08556529
9 changed files with 2210 additions and 113 deletions

View File

@ -307,6 +307,19 @@ remoteuser = username
# #
# reference = Mail # reference = Mail
# In between synchronisations, OfflineIMAP can monitor mailboxes for new
# messages using the IDLE command. If you want to enable this, specify here
# the folders you wish to monitor. Note that the IMAP protocol requires a
# separate connection for each folder monitored in this way, so setting
# this option will force settings for:
# maxconnections - to be at least the number of folders you give
# holdconnectionopen - to be true
# keepalive - to be 29 minutes unless you specify otherwise
# This option should return a Python list. For example
#
# idlefolders = ['INBOX', 'INBOX.Alerts']
#
# OfflineIMAP can use multiple connections to the server in order # OfflineIMAP can use multiple connections to the server in order
# to perform multiple synchronization actions simultaneously. # to perform multiple synchronization actions simultaneously.
# This may place a higher burden on the server. In most cases, # This may place a higher burden on the server. In most cases,

View File

@ -20,8 +20,7 @@
""" """
from IMAP import IMAPFolder from IMAP import IMAPFolder
import imaplib from offlineimap import imaplib2, imaputil, imaplibutil
from offlineimap import imaputil, imaplibutil
from offlineimap.ui import UIBase from offlineimap.ui import UIBase
from copy import copy from copy import copy

View File

@ -17,8 +17,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from Base import BaseFolder from Base import BaseFolder
import imaplib from offlineimap import imaplib2, imaputil, imaplibutil
from offlineimap import imaputil, imaplibutil
from offlineimap.ui import UIBase from offlineimap.ui import UIBase
from offlineimap.version import versionstr from offlineimap.version import versionstr
import rfc822, time, string, random, binascii, re import rfc822, time, string, random, binascii, re
@ -271,13 +270,13 @@ class IMAPFolder(BaseFolder):
raise ValueError raise ValueError
# This could raise a value error if it's not a valid format. # This could raise a value error if it's not a valid format.
date = imaplib.Time2Internaldate(datetuple) date = imaplib2.Time2Internaldate(datetuple)
except (ValueError, OverflowError): except (ValueError, OverflowError):
# Argh, sometimes it's a valid format but year is 0102 # Argh, sometimes it's a valid format but year is 0102
# or something. Argh. It seems that Time2Internaldate # or something. Argh. It seems that Time2Internaldate
# will rause a ValueError if the year is 0102 but not 1902, # will rause a ValueError if the year is 0102 but not 1902,
# but some IMAP servers nonetheless choke on 1902. # but some IMAP servers nonetheless choke on 1902.
date = imaplib.Time2Internaldate(time.localtime()) date = imaplib2.Time2Internaldate(time.localtime())
ui.debug('imap', 'savemessage: using date ' + str(date)) ui.debug('imap', 'savemessage: using date ' + str(date))
content = re.sub("(?<!\r)\n", "\r\n", content) content = re.sub("(?<!\r)\n", "\r\n", content)

2072
offlineimap/imaplib2.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -16,12 +16,12 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import re, string, types, binascii, socket, time, random, subprocess, sys, os import os, re, socket, time, subprocess, sys, threading
from offlineimap.ui import UIBase from offlineimap.ui import UIBase
from imaplib import * from offlineimap.imaplib2 import *
# Import the symbols we need that aren't exported by default # Import the symbols we need that aren't exported by default
from imaplib import IMAP4_PORT, IMAP4_SSL_PORT, InternalDate, Mon2num from offlineimap.imaplib2 import IMAP4_PORT, IMAP4_SSL_PORT, InternalDate, Mon2num
class IMAP4_Tunnel(IMAP4): class IMAP4_Tunnel(IMAP4):
@ -40,12 +40,10 @@ class IMAP4_Tunnel(IMAP4):
self.process = subprocess.Popen(host, shell=True, close_fds=True, self.process = subprocess.Popen(host, shell=True, close_fds=True,
stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdin=subprocess.PIPE, stdout=subprocess.PIPE)
(self.outfd, self.infd) = (self.process.stdin, self.process.stdout) (self.outfd, self.infd) = (self.process.stdin, self.process.stdout)
self.read_fd = self.infd.fileno()
def read(self, size): def read(self, size):
retval = '' return os.read(self.read_fd, size)
while len(retval) < size:
retval += self.infd.read(size - len(retval))
return retval
def readline(self): def readline(self):
return self.infd.readline() return self.infd.readline()
@ -97,78 +95,22 @@ class sslwrapper:
else: else:
retval += linebuf retval += linebuf
def new_mesg(self, s, secs=None): def new_mesg(self, s, tn=None, secs=None):
if secs is None: if secs is None:
secs = time.time() secs = time.time()
if tn is None:
tn = threading.currentThread().getName()
tm = time.strftime('%M:%S', time.localtime(secs)) tm = time.strftime('%M:%S', time.localtime(secs))
UIBase.getglobalui().debug('imap', ' %s.%02d %s' % (tm, (secs*100)%100, s)) UIBase.getglobalui().debug('imap', ' %s.%02d %s %s' % (tm, (secs*100)%100, tn, s))
class WrappedIMAP4_SSL(IMAP4_SSL): class WrappedIMAP4_SSL(IMAP4_SSL):
def open(self, host = '', port = IMAP4_SSL_PORT): def open(self, host=None, port=None):
IMAP4_SSL.open(self, host, port) IMAP4_SSL.open(self, host, port)
self.sslobj = sslwrapper(self.sslobj) self.sslobj = sslwrapper(self.sslobj)
def readline(self): def readline(self):
return self.sslobj.readline() return self.sslobj.readline()
def new_open(self, host = '', port = IMAP4_PORT):
"""Setup connection to remote server on "host:port"
(default: localhost:standard IMAP4 port).
This connection will be used by the routines:
read, readline, send, shutdown.
"""
self.host = host
self.port = port
res = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)
# Try each address returned by getaddrinfo in turn until we
# manage to connect to one.
# Try all the addresses in turn until we connect()
last_error = 0
for remote in res:
af, socktype, proto, canonname, sa = remote
self.sock = socket.socket(af, socktype, proto)
last_error = self.sock.connect_ex(sa)
if last_error == 0:
break
else:
self.sock.close()
if last_error != 0:
# FIXME
raise socket.error(last_error)
self.file = self.sock.makefile('rb')
def new_open_ssl(self, host = '', port = IMAP4_SSL_PORT):
"""Setup connection to remote server on "host:port".
(default: localhost:standard IMAP4 SSL port).
This connection will be used by the routines:
read, readline, send, shutdown.
"""
self.host = host
self.port = port
#This connects to the first ip found ipv4/ipv6
#Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
#example from the python documentation:
#http://www.python.org/doc/lib/socket-example.html
res = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)
# Try all the addresses in turn until we connect()
last_error = 0
for remote in res:
af, socktype, proto, canonname, sa = remote
self.sock = socket.socket(af, socktype, proto)
last_error = self.sock.connect_ex(sa)
if last_error == 0:
break
else:
self.sock.close()
if last_error != 0:
# FIXME
raise socket.error(last_error)
self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
self.sslobj = sslwrapper(self.sslobj)
mustquote = re.compile(r"[^\w!#$%&'+,.:;<=>?^`|~-]") mustquote = re.compile(r"[^\w!#$%&'+,.:;<=>?^`|~-]")
def Internaldate2epoch(resp): def Internaldate2epoch(resp):

View File

@ -16,9 +16,9 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import imaplib from offlineimap import imaplib2, imaplibutil, imaputil, threadutil
from offlineimap import imaplibutil, imaputil, threadutil
from offlineimap.ui import UIBase from offlineimap.ui import UIBase
from offlineimap.accounts import syncfolder
from threading import * from threading import *
import thread, hmac, os, time import thread, hmac, os, time
import base64 import base64
@ -56,13 +56,10 @@ class UsefulIMAPMixIn:
else: else:
self.selectedfolder = None self.selectedfolder = None
def _mesg(self, s, secs=None): def _mesg(self, s, tn=None, secs=None):
imaplibutil.new_mesg(self, s, secs) imaplibutil.new_mesg(self, s, tn, secs)
class UsefulIMAP4(UsefulIMAPMixIn, imaplib.IMAP4):
def open(self, host = '', port = imaplib.IMAP4_PORT):
imaplibutil.new_open(self, host, port)
class UsefulIMAP4(UsefulIMAPMixIn, imaplib2.IMAP4):
# This is a hack around Darwin's implementation of realloc() (which # This is a hack around Darwin's implementation of realloc() (which
# Python uses inside the socket code). On Darwin, we split the # Python uses inside the socket code). On Darwin, we split the
# message into 100k chunks, which should be small enough - smaller # message into 100k chunks, which should be small enough - smaller
@ -73,17 +70,17 @@ class UsefulIMAP4(UsefulIMAPMixIn, imaplib.IMAP4):
read = 0 read = 0
io = StringIO() io = StringIO()
while read < size: while read < size:
data = imaplib.IMAP4.read (self, min(size-read,8192)) sz = min(size-read, 8192)
data = imaplib2.IMAP4.read (self, sz)
read += len(data) read += len(data)
io.write(data) io.write(data)
if len(data) < sz:
break
return io.getvalue() return io.getvalue()
else: else:
return imaplib.IMAP4.read (self, size) return imaplib2.IMAP4.read (self, size)
class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplibutil.WrappedIMAP4_SSL): class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplibutil.WrappedIMAP4_SSL):
def open(self, host = '', port = imaplib.IMAP4_SSL_PORT):
imaplibutil.new_open_ssl(self, host, port)
# This is the same hack as above, to be used in the case of an SSL # This is the same hack as above, to be used in the case of an SSL
# connexion. # connexion.
@ -92,9 +89,12 @@ class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplibutil.WrappedIMAP4_SSL):
read = 0 read = 0
io = StringIO() io = StringIO()
while read < size: while read < size:
data = imaplibutil.WrappedIMAP4_SSL.read (self, min(size-read,8192)) sz = min(size-read,8192)
data = imaplibutil.WrappedIMAP4_SSL.read (self, sz)
read += len(data) read += len(data)
io.write(data) io.write(data)
if len(data) < sz:
break
return io.getvalue() return io.getvalue()
else: else:
return imaplibutil.WrappedIMAP4_SSL.read (self,size) return imaplibutil.WrappedIMAP4_SSL.read (self,size)
@ -107,7 +107,8 @@ class IMAPServer:
def __init__(self, config, reposname, def __init__(self, config, reposname,
username = None, password = None, hostname = None, username = None, password = None, hostname = None,
port = None, ssl = 1, maxconnections = 1, tunnel = None, port = None, ssl = 1, maxconnections = 1, tunnel = None,
reference = '""', sslclientcert = None, sslclientkey = None): reference = '""', sslclientcert = None, sslclientkey = None,
idlefolders = []):
self.reposname = reposname self.reposname = reposname
self.config = config self.config = config
self.username = username self.username = username
@ -134,6 +135,7 @@ class IMAPServer:
self.semaphore = BoundedSemaphore(self.maxconnections) self.semaphore = BoundedSemaphore(self.maxconnections)
self.connectionlock = Lock() self.connectionlock = Lock()
self.reference = reference self.reference = reference
self.idlefolders = idlefolders
self.gss_step = self.GSS_STATE_STEP self.gss_step = self.GSS_STATE_STEP
self.gss_vc = None self.gss_vc = None
self.gssapi = False self.gssapi = False
@ -344,8 +346,6 @@ class IMAPServer:
ui.debug('imap', 'keepalive thread started') ui.debug('imap', 'keepalive thread started')
while 1: while 1:
ui.debug('imap', 'keepalive: top of loop') ui.debug('imap', 'keepalive: top of loop')
time.sleep(timeout)
ui.debug('imap', 'keepalive: after wait')
if event.isSet(): if event.isSet():
ui.debug('imap', 'keepalive: event is set; exiting') ui.debug('imap', 'keepalive: event is set; exiting')
return return
@ -356,32 +356,91 @@ class IMAPServer:
self.connectionlock.release() self.connectionlock.release()
ui.debug('imap', 'keepalive: connectionlock released') ui.debug('imap', 'keepalive: connectionlock released')
threads = [] threads = []
imapobjs = []
for i in range(numconnections): for i in range(numconnections):
ui.debug('imap', 'keepalive: processing connection %d of %d' % (i, numconnections)) ui.debug('imap', 'keepalive: processing connection %d of %d' % (i, numconnections))
imapobj = self.acquireconnection() if len(self.idlefolders) > i:
ui.debug('imap', 'keepalive: connection %d acquired' % i) idler = IdleThread(self, self.idlefolders[i])
imapobjs.append(imapobj) else:
thr = threadutil.ExitNotifyThread(target = imapobj.noop) idler = IdleThread(self)
thr.setDaemon(1) idler.start()
thr.start() threads.append(idler)
threads.append(thr)
ui.debug('imap', 'keepalive: thread started') ui.debug('imap', 'keepalive: thread started')
ui.debug('imap', 'keepalive: waiting for timeout')
event.wait(timeout)
ui.debug('imap', 'keepalive: joining threads') ui.debug('imap', 'keepalive: joining threads')
for thr in threads: for idler in threads:
# Make sure all the commands have completed. # Make sure all the commands have completed.
thr.join() idler.stop()
idler.join()
ui.debug('imap', 'keepalive: releasing connections')
for imapobj in imapobjs:
self.releaseconnection(imapobj)
ui.debug('imap', 'keepalive: bottom of loop') ui.debug('imap', 'keepalive: bottom of loop')
class IdleThread(object):
def __init__(self, parent, folder=None):
self.parent = parent
self.folder = folder
self.event = Event()
if folder is None:
self.thread = Thread(target=self.noop)
else:
self.thread = Thread(target=self.idle)
self.thread.setDaemon(1)
def start(self):
self.thread.start()
def stop(self):
self.event.set()
def join(self):
self.thread.join()
def noop(self):
imapobj = self.parent.acquireconnection()
imapobj.noop()
self.event.wait()
self.parent.releaseconnection(imapobj)
def dosync(self):
remoterepos = self.parent.repos
account = remoterepos.account
localrepos = account.localrepos
remoterepos = account.remoterepos
statusrepos = account.statusrepos
remotefolder = remoterepos.getfolder(self.folder)
syncfolder(account.name, remoterepos, remotefolder, localrepos, statusrepos, quick=False)
ui = UIBase.getglobalui()
ui.unregisterthread(currentThread())
def idle(self):
imapobj = self.parent.acquireconnection()
imapobj.select(self.folder)
self.parent.releaseconnection(imapobj)
while True:
if self.event.isSet():
return
self.needsync = False
def callback(args):
if not self.event.isSet():
self.needsync = True
self.event.set()
imapobj = self.parent.acquireconnection()
if "IDLE" in imapobj.capabilities:
imapobj.idle(callback=callback)
else:
imapobj.noop()
self.event.wait()
if self.event.isSet():
imapobj.noop()
self.parent.releaseconnection(imapobj)
if self.needsync:
self.event.clear()
self.dosync()
class ConfigedIMAPServer(IMAPServer): class ConfigedIMAPServer(IMAPServer):
"""This class is designed for easier initialization given a ConfigParser """This class is designed for easier initialization given a ConfigParser
object and an account name. The passwordhash is used if object and an account name. The passwordhash is used if
@ -401,6 +460,7 @@ class ConfigedIMAPServer(IMAPServer):
sslclientcert = self.repos.getsslclientcert() sslclientcert = self.repos.getsslclientcert()
sslclientkey = self.repos.getsslclientkey() sslclientkey = self.repos.getsslclientkey()
reference = self.repos.getreference() reference = self.repos.getreference()
idlefolders = self.repos.getidlefolders()
server = None server = None
password = None password = None
@ -412,6 +472,7 @@ class ConfigedIMAPServer(IMAPServer):
IMAPServer.__init__(self, self.config, self.repos.getname(), IMAPServer.__init__(self, self.config, self.repos.getname(),
tunnel = usetunnel, tunnel = usetunnel,
reference = reference, reference = reference,
idlefolders = idlefolders,
maxconnections = self.repos.getmaxconnections()) maxconnections = self.repos.getmaxconnections())
else: else:
if not password: if not password:
@ -420,5 +481,6 @@ class ConfigedIMAPServer(IMAPServer):
user, password, host, port, ssl, user, password, host, port, ssl,
self.repos.getmaxconnections(), self.repos.getmaxconnections(),
reference = reference, reference = reference,
idlefolders = idlefolders,
sslclientcert = sslclientcert, sslclientcert = sslclientcert,
sslclientkey = sslclientkey) sslclientkey = sslclientkey)

View File

@ -16,8 +16,7 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import imaplib from offlineimap import imaplib2, imapserver, repository, folder, mbnames, threadutil, version, syncmaster, accounts
from offlineimap import imapserver, repository, folder, mbnames, threadutil, version, syncmaster, accounts
from offlineimap.localeval import LocalEval from offlineimap.localeval import LocalEval
from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
from offlineimap.ui import UIBase from offlineimap.ui import UIBase
@ -103,7 +102,7 @@ def startup(versionno):
for debugtype in options['-d'].split(','): for debugtype in options['-d'].split(','):
ui.add_debug(debugtype.strip()) ui.add_debug(debugtype.strip())
if debugtype == 'imap': if debugtype == 'imap':
imaplib.Debug = 5 imaplib2.Debug = 5
if debugtype == 'thread': if debugtype == 'thread':
threading._VERBOSE = 1 threading._VERBOSE = 1

View File

@ -74,10 +74,16 @@ class IMAPRepository(BaseRepository):
self.imapserver.close() self.imapserver.close()
def getholdconnectionopen(self): def getholdconnectionopen(self):
if self.getidlefolders():
return 1
return self.getconfboolean("holdconnectionopen", 0) return self.getconfboolean("holdconnectionopen", 0)
def getkeepalive(self): def getkeepalive(self):
return self.getconfint("keepalive", 0) num = self.getconfint("keepalive", 0)
if num == 0 and self.getidlefolders():
return 29*60
else:
return num
def getsep(self): def getsep(self):
return self.imapserver.delim return self.imapserver.delim
@ -145,8 +151,14 @@ class IMAPRepository(BaseRepository):
def getreference(self): def getreference(self):
return self.getconf('reference', '""') return self.getconf('reference', '""')
def getidlefolders(self):
localeval = self.localeval
return localeval.eval(self.getconf('idlefolders', '[]'))
def getmaxconnections(self): def getmaxconnections(self):
return self.getconfint('maxconnections', 1) num1 = len(self.getidlefolders())
num2 = self.getconfint('maxconnections', 1)
return max(num1, num2)
def getexpunge(self): def getexpunge(self):
return self.getconfboolean('expunge', 1) return self.getconfboolean('expunge', 1)

View File

@ -16,8 +16,7 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import imaplib from offlineimap import imaplib2, imapserver, repository, folder, mbnames, threadutil, version
from offlineimap import imapserver, repository, folder, mbnames, threadutil, version
from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
import offlineimap.accounts import offlineimap.accounts
from offlineimap.accounts import SyncableAccount, SigListener from offlineimap.accounts import SyncableAccount, SigListener