Add idlemail.py which emulates IDLE IMAP for offlineimap
authorJulien Valroff <julien@kirya.net>
Sun, 10 Jul 2011 08:12:46 +0000 (10:12 +0200)
committerJulien Valroff <julien@kirya.net>
Sun, 10 Jul 2011 08:12:46 +0000 (10:12 +0200)
idlemail.py [new file with mode: 0755]

diff --git a/idlemail.py b/idlemail.py
new file mode 100755 (executable)
index 0000000..be2eacd
--- /dev/null
@@ -0,0 +1,205 @@
+#!/usr/bin/python
+
+# Original source by Sam Burnett <sburnett@cc.gatech.edu>
+# http://www.cc.gatech.edu/~sburnett/posts/2010-11-21-imap-idle.html
+#
+# Amended by Julien Valroff <julien@kirya.net> to support getting passwords from 
+# GNOME keyring and falling back to using netrc file
+# This is done through ~/.offlineimap.py from
+# http://git.kirya.net/?p=dotfiles/offlineimap.git;a=blob_plain;f=.offlineimap.py;hb=master
+#
+# requires: 
+#  * offlineimap
+#  * python-twisted-core
+#  * python-openssl
+
+from OpenSSL import SSL
+import sys
+import os.path
+import subprocess
+import re
+import imp
+import ConfigParser
+from optparse import OptionParser
+
+from twisted.internet.protocol import ReconnectingClientFactory
+from twisted.protocols.basic import LineOnlyReceiver
+from twisted.internet import ssl, reactor
+from twisted.protocols.policies import TimeoutMixin
+from twisted.python import log
+
+class ImapIdleClient(LineOnlyReceiver, TimeoutMixin):
+    def __init__(self, username, password, mailbox, account, idle_timeout):
+        self.init_state = 'init'
+        self.state_machine = { 'init': ('login', self.wait_for_hello)
+                             , 'login': ('examine', self.login)
+                             , 'examine': ('idle', self.examine)
+                             , 'idle': ('done', self.idle)
+                             , 'done': ('idle', self.done_idle)
+                             }
+
+        self.state = self.init_state
+        self.username = username
+        self.password = password
+        self.mailbox = mailbox
+        self.account = account
+        self.idle_timeout = idle_timeout
+
+    def connectionMade(self):
+        self.factory.resetDelay()
+
+    def connectionLost(self, reason):
+        print 'connection for %s:%s lost (%s)' % (self.account, self.mailbox, reason)
+
+    def lineReceived(self, line):
+        log.msg('[%s:%s] (%s) %s' % (self.account, self.mailbox, self.state, line))
+
+        (next_state, action) = self.state_machine[self.state]
+        if action(line):
+            self.state = next_state
+            log.msg('[%s:%s] Transitioning to state %s' % (self.account, self.mailbox, self.state))
+
+    def wait_for_hello(self, line):
+        if re.match(r'\* OK', line) is not None:
+            self.sendLine('. login %s "%s"' % (self.username, self.password))
+            return True
+            print 'Logging in'
+        else:
+            return False
+
+    def login(self, line):
+        if re.match(r'\. OK', line) is not None:
+            self.sendLine('. examine %s' % self.mailbox)
+            return True
+            print 'Examining mailbox'
+        else:
+            return False
+
+    def examine(self, line):
+        if re.match(r'\. OK', line) is not None:
+            self.sendLine('. idle')
+            self.setTimeout(self.idle_timeout)
+            return True
+            print 'Going idle'
+        else:
+            return False
+
+    def idle(self, line):
+        if re.match(r'\* \d+ (EXISTS|EXPUNGE)', line) is not None:
+            subprocess.Popen(['offlineimap', '-a', self.account, '-f', self.mailbox, '-o', '-k', 'mbnames:enabled=no'])
+            self.sendLine('DONE')
+            return True
+            print 'Done idling (new mail)'
+        else:
+            return False
+
+    def done_idle(self, line):
+        if re.match(r'\. OK', line) is not None:
+            self.sendLine('. idle')
+            self.setTimeout(self.idle_timeout)
+            return True
+            print 'Idling again'
+        else:
+            return False
+
+    def timeoutConnection(self):
+        self.state = 'done'
+        self.sendLine('DONE')
+        print 'Done idle (timeout)'
+
+class ImapClientFactory(ReconnectingClientFactory):
+    def __init__(self, server, mailbox, account, timeout):
+        path = os.path.expanduser("~/.offlineimap.py")
+        pythonfile = imp.load_source("pythonfile", path)
+
+        self.username = pythonfile.get_username(server)
+        self.password = pythonfile.get_password(server)
+        self.mailbox = mailbox
+        self.account = account
+        self.timeout = 60*timeout
+
+    def buildProtocol(self, addr):
+        proto = ImapIdleClient(self.username, self.password, self.mailbox, self.account, self.timeout)
+        proto.factory = self
+        return proto
+
+class AccountOptions(object):
+    server = None
+    port = 993
+    mailboxes = None
+    timeout = 29
+
+def parse_options():
+    usage = "usage: %s [options] [accounts to sync (overrides config file)]"
+    parser = OptionParser(usage=usage)
+    parser.set_defaults(config=None)
+    parser.add_option('-c', '--configuration', dest='config',
+                      action='store', type='string',
+                      help='Alternate location of configuration file')
+    (options, args) = parser.parse_args()
+
+    if options.config is not None:
+        config_path = options.config
+    else:
+        config_path = os.path.expanduser('~/.idlemailrc')
+
+    if not os.path.isfile(config_path):
+        parser.error("Configuration file %s doesn't exist" % config_path)
+
+    if len(args) > 0:
+        accounts = args
+    else:
+        accounts = None
+
+    cfg = ConfigParser.ConfigParser()
+    cfg.read(config_path)
+
+    if cfg.has_section('general'):
+        if cfg.has_option('general', 'accounts') and accounts is None:
+            accounts = re.split('[, \t]+', cfg.get('general', 'accounts'))
+
+    account_settings = {}
+
+    for section in cfg.sections():
+        if section.split()[0].lower() != 'account':
+            continue
+
+        name = section.split(None, 1)[1].strip()
+        if accounts is not None and name not in accounts:
+            continue
+
+        account = AccountOptions()
+        account.server = cfg.get(section, 'server')
+        if cfg.has_option(section, 'port'):
+            account.port = cfg.getint(section, 'port')
+        boxes = cfg.get(section, 'mailboxes')
+        account.mailboxes = re.split('[, \t]+', boxes)
+        if cfg.has_option(section, 'timeout'):
+            account.timeout = cfg.getfloat(section, 'timeout')
+
+        account_settings[name] = account
+
+    if accounts is not None:
+        for account in accounts:
+            if account not in account_settings:
+                parser.error('Account not found: %s' % account)
+
+    if len(account_settings.keys()) == 0:
+        parser.error('No accounts defined')
+
+    return account_settings
+
+def main():
+    accounts = parse_options()
+
+    log.startLogging(sys.stdout)
+
+    for (name, settings) in accounts.items():
+        for mailbox in settings.mailboxes:
+            factory = ImapClientFactory(settings.server, mailbox, name, settings.timeout)
+            reactor.connectSSL(settings.server, settings.port, factory, ssl.ClientContextFactory())
+
+    reactor.run()
+
+if __name__ == '__main__':
+    main()