Autocommit of file /home/julien/scripts/autosync-xdg-launcher.sh changed on host...
[dotfiles/scripts.git] / idlemail.py
1 #!/usr/bin/python
2
3 # Original source by Sam Burnett <sburnett@cc.gatech.edu>
4 # http://www.cc.gatech.edu/~sburnett/posts/2010-11-21-imap-idle.html
5 #
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
10 #
11 # requires: 
12 #  * offlineimap
13 #  * python-twisted-core
14 #  * python-openssl
15
16 from OpenSSL import SSL
17 import sys
18 import os.path
19 import subprocess
20 import re
21 import imp
22 import ConfigParser
23 from optparse import OptionParser
24
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
30
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)
39                              }
40
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
47
48     def connectionMade(self):
49         self.factory.resetDelay()
50
51     def connectionLost(self, reason):
52         print 'connection for %s:%s lost (%s)' % (self.account, self.mailbox, reason)
53
54     def lineReceived(self, line):
55         log.msg('[%s:%s] (%s) %s' % (self.account, self.mailbox, self.state, line))
56
57         (next_state, action) = self.state_machine[self.state]
58         if action(line):
59             self.state = next_state
60             log.msg('[%s:%s] Transitioning to state %s' % (self.account, self.mailbox, self.state))
61
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))
65             return True
66             print 'Logging in'
67         else:
68             return False
69
70     def login(self, line):
71         if re.match(r'\. OK', line) is not None:
72             self.sendLine('. examine %s' % self.mailbox)
73             return True
74             print 'Examining mailbox'
75         else:
76             return False
77
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)
82             return True
83             print 'Going idle'
84         else:
85             return False
86
87     def idle(self, line):
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'])
90             self.sendLine('DONE')
91             return True
92             print 'Done idleing (new mail)'
93         else:
94             return False
95
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)
100             return True
101             print 'Idling again'
102         else:
103             return False
104
105     def timeoutConnection(self):
106         self.state = 'done'
107         self.sendLine('DONE')
108         print 'Done idle (timeout)'
109
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)
114
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
120
121     def buildProtocol(self, addr):
122         proto = ImapIdleClient(self.username, self.password, self.mailbox, self.account, self.timeout)
123         proto.factory = self
124         return proto
125
126 class AccountOptions(object):
127     server = None
128     port = 993
129     mailboxes = None
130     timeout = 29
131
132 def parse_options():
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()
140
141     if options.config is not None:
142         config_path = options.config
143     else:
144         config_path = os.path.expanduser('~/.idlemailrc')
145
146     if not os.path.isfile(config_path):
147         parser.error("Configuration file %s doesn't exist" % config_path)
148
149     if len(args) > 0:
150         accounts = args
151     else:
152         accounts = None
153
154     cfg = ConfigParser.ConfigParser()
155     cfg.read(config_path)
156
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'))
160
161     account_settings = {}
162
163     for section in cfg.sections():
164         if section.split()[0].lower() != 'account':
165             continue
166
167         name = section.split(None, 1)[1].strip()
168         if accounts is not None and name not in accounts:
169             continue
170
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')
179
180         account_settings[name] = account
181
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)
186
187     if len(account_settings.keys()) == 0:
188         parser.error('No accounts defined')
189
190     return account_settings
191
192 def main():
193     accounts = parse_options()
194
195     log.startLogging(sys.stdout)
196
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())
201
202     reactor.run()
203
204 if __name__ == '__main__':
205     main()