3 # Original source by Sam Burnett <sburnett@cc.gatech.edu>
4 # http://www.cc.gatech.edu/~sburnett/posts/2010-11-21-imap-idle.html
6 # Amended by Julien Valroff <julien@kirya.net> to support getting passwords from
7 # GNOME keyring and falling back to using netrc file
8 # This is done through ~/.offlineimap.py from
9 # http://git.kirya.net/?p=dotfiles/offlineimap.git;a=blob_plain;f=.offlineimap.py;hb=master
13 # * python-twisted-core
16 from OpenSSL import SSL
23 from optparse import OptionParser
25 from twisted.internet.protocol import ReconnectingClientFactory
26 from twisted.protocols.basic import LineOnlyReceiver
27 from twisted.internet import ssl, reactor
28 from twisted.protocols.policies import TimeoutMixin
29 from twisted.python import log
31 class ImapIdleClient(LineOnlyReceiver, TimeoutMixin):
32 def __init__(self, username, password, mailbox, account, idle_timeout):
33 self.init_state = 'init'
34 self.state_machine = { 'init': ('login', self.wait_for_hello)
35 , 'login': ('examine', self.login)
36 , 'examine': ('idle', self.examine)
37 , 'idle': ('done', self.idle)
38 , 'done': ('idle', self.done_idle)
41 self.state = self.init_state
42 self.username = username
43 self.password = password
44 self.mailbox = mailbox
45 self.account = account
46 self.idle_timeout = idle_timeout
48 def connectionMade(self):
49 self.factory.resetDelay()
51 def connectionLost(self, reason):
52 print 'connection for %s:%s lost (%s)' % (self.account, self.mailbox, reason)
54 def lineReceived(self, line):
55 log.msg('[%s:%s] (%s) %s' % (self.account, self.mailbox, self.state, line))
57 (next_state, action) = self.state_machine[self.state]
59 self.state = next_state
60 log.msg('[%s:%s] Transitioning to state %s' % (self.account, self.mailbox, self.state))
62 def wait_for_hello(self, line):
63 if re.match(r'\* OK', line) is not None:
64 self.sendLine('. login %s "%s"' % (self.username, self.password))
70 def login(self, line):
71 if re.match(r'\. OK', line) is not None:
72 self.sendLine('. examine %s' % self.mailbox)
74 print 'Examining mailbox'
78 def examine(self, line):
79 if re.match(r'\. OK', line) is not None:
80 self.sendLine('. idle')
81 self.setTimeout(self.idle_timeout)
88 if re.match(r'\* \d+ (EXISTS|EXPUNGE)', line) is not None:
89 subprocess.Popen(['offlineimap', '-a', self.account, '-f', self.mailbox, '-o', '-k', 'mbnames:enabled=no'])
92 print 'Done idleing (new mail)'
96 def done_idle(self, line):
97 if re.match(r'\. OK', line) is not None:
98 self.sendLine('. idle')
99 self.setTimeout(self.idle_timeout)
105 def timeoutConnection(self):
107 self.sendLine('DONE')
108 print 'Done idle (timeout)'
110 class ImapClientFactory(ReconnectingClientFactory):
111 def __init__(self, server, mailbox, account, timeout):
112 path = os.path.expanduser("~/.offlineimap.py")
113 pythonfile = imp.load_source("pythonfile", path)
115 self.username = pythonfile.get_username(server)
116 self.password = pythonfile.get_password(server)
117 self.mailbox = mailbox
118 self.account = account
119 self.timeout = 60*timeout
121 def buildProtocol(self, addr):
122 proto = ImapIdleClient(self.username, self.password, self.mailbox, self.account, self.timeout)
126 class AccountOptions(object):
133 usage = "usage: %s [options] [accounts to sync (overrides config file)]"
134 parser = OptionParser(usage=usage)
135 parser.set_defaults(config=None)
136 parser.add_option('-c', '--configuration', dest='config',
137 action='store', type='string',
138 help='Alternate location of configuration file')
139 (options, args) = parser.parse_args()
141 if options.config is not None:
142 config_path = options.config
144 config_path = os.path.expanduser('~/.idlemailrc')
146 if not os.path.isfile(config_path):
147 parser.error("Configuration file %s doesn't exist" % config_path)
154 cfg = ConfigParser.ConfigParser()
155 cfg.read(config_path)
157 if cfg.has_section('general'):
158 if cfg.has_option('general', 'accounts') and accounts is None:
159 accounts = re.split('[, \t]+', cfg.get('general', 'accounts'))
161 account_settings = {}
163 for section in cfg.sections():
164 if section.split()[0].lower() != 'account':
167 name = section.split(None, 1)[1].strip()
168 if accounts is not None and name not in accounts:
171 account = AccountOptions()
172 account.server = cfg.get(section, 'server')
173 if cfg.has_option(section, 'port'):
174 account.port = cfg.getint(section, 'port')
175 boxes = cfg.get(section, 'mailboxes')
176 account.mailboxes = re.split('[, \t]+', boxes)
177 if cfg.has_option(section, 'timeout'):
178 account.timeout = cfg.getfloat(section, 'timeout')
180 account_settings[name] = account
182 if accounts is not None:
183 for account in accounts:
184 if account not in account_settings:
185 parser.error('Account not found: %s' % account)
187 if len(account_settings.keys()) == 0:
188 parser.error('No accounts defined')
190 return account_settings
193 accounts = parse_options()
195 log.startLogging(sys.stdout)
197 for (name, settings) in accounts.items():
198 for mailbox in settings.mailboxes:
199 factory = ImapClientFactory(settings.server, mailbox, name, settings.timeout)
200 reactor.connectSSL(settings.server, settings.port, factory, ssl.ClientContextFactory())
204 if __name__ == '__main__':