Premiere version : mise en route du suivi.
[auf_roundup.git] / scripts / .svn / text-base / imapServer.py.svn-base
1 #!/usr/bin/env python
2 """\
3 This script is a wrapper around the mailgw.py script that exists in roundup.
4 It runs as service instead of running as a one-time shot.
5 It also connects to a secure IMAP server. The main reasons for this script are:
6
7 1) The roundup-mailgw script isn't designed to run as a server. It
8     expects that you either run it by hand, and enter the password each
9     time, or you supply the password on the command line. I prefer to
10     run a server that I initialize with the password, and then it just
11     runs. I don't want to have to pass it on the command line, so
12     running through crontab isn't a possibility. (This wouldn't be a
13     problem on a local machine running through a mailspool.)
14 2) mailgw.py somehow screws up SSL support so IMAP4_SSL doesn't work. So
15     hopefully running that work outside of the mailgw will allow it to work.
16 3) I wanted to be able to check multiple projects at the same time.
17     roundup-mailgw is only for 1 mailbox and 1 project.
18
19
20 *TODO*:
21   For the first round, the program spawns a new roundup-mailgw for
22   each imap message that it finds and pipes the result in. In the
23   future it might be more practical to actually include the roundup
24   files and run the appropriate commands using python.
25
26 *TODO*:
27   Look into supporting a logfile instead of using 2>/logfile
28
29 *TODO*:
30   Add an option for changing the uid/gid of the running process.
31 """
32
33 import getpass
34 import logging
35 import imaplib
36 import optparse
37 import os
38 import re
39 import time
40
41 logging.basicConfig()
42 log = logging.getLogger('IMAPServer')
43
44 version = '0.1.2'
45
46 class RoundupMailbox:
47     """This contains all the info about each mailbox.
48     Username, Password, server, security, roundup database
49     """
50     def __init__(self, dbhome='', username=None, password=None, mailbox=None
51         , server=None, protocol='imaps'):
52         self.username = username
53         self.password = password
54         self.mailbox = mailbox
55         self.server = server
56         self.protocol = protocol
57         self.dbhome = dbhome
58
59         try:
60             if not self.dbhome:
61                 self.dbhome = raw_input('Tracker home: ')
62                 if not os.path.exists(self.dbhome):
63                     raise ValueError, 'Invalid home address: ' \
64                         'directory "%s" does not exist.' % self.dbhome
65
66             if not self.server:
67                 self.server = raw_input('Server: ')
68                 if not self.server:
69                     raise ValueError, 'No Servername supplied'
70                 protocol = raw_input('protocol [imaps]? ')
71                 self.protocol = protocol
72
73             if not self.username:
74                 self.username = raw_input('Username: ')
75                 if not self.username:
76                     raise ValueError, 'Invalid Username'
77
78             if not self.password:
79                 print 'For server %s, user %s' % (self.server, self.username)
80                 self.password = getpass.getpass()
81                 # password can be empty because it could be superceeded
82                 # by a later entry
83
84             #if self.mailbox is None:
85             #   self.mailbox = raw_input('Mailbox [INBOX]: ')
86             #   # We allow an empty mailbox because that will
87             #   # select the INBOX, whatever it is called
88
89         except (KeyboardInterrupt, EOFError):
90             raise ValueError, 'Canceled by User'
91
92     def __str__(self):
93         return 'Mailbox{ server:%(server)s, protocol:%(protocol)s, ' \
94             'username:%(username)s, mailbox:%(mailbox)s, ' \
95             'dbhome:%(dbhome)s }' % self.__dict__
96
97
98 # [als] class name is misleading.  this is imap client, not imap server
99 class IMAPServer:
100
101     """IMAP mail gatherer.
102
103     This class runs as a server process. It is configured with a list of
104     mailboxes to connect to, along with the roundup database directories
105     that correspond with each email address.  It then connects to each
106     mailbox at a specified interval, and if there are new messages it
107     reads them, and sends the result to the roundup.mailgw.
108
109     *TODO*:
110       Try to be smart about how you access the mailboxes so that you can
111       connect once, and access multiple mailboxes and possibly multiple
112       usernames.
113
114     *NOTE*:
115       This assumes that if you are using the same user on the same
116       server, you are using the same password. (the last one supplied is
117       used.) Empty passwords are ignored.  Only the last protocol
118       supplied is used.
119     """
120
121     def __init__(self, pidfile=None, delay=5, daemon=False):
122         #This is sorted by servername, then username, then mailboxes
123         self.mailboxes = {}
124         self.delay = float(delay)
125         self.pidfile = pidfile
126         self.daemon = daemon
127
128     def setDelay(self, delay):
129         self.delay = delay
130
131     def addMailbox(self, mailbox):
132         """ The linkage is as follows:
133         servers -- users - mailbox:dbhome
134         So there can be multiple servers, each with multiple users.
135         Each username can be associated with multiple mailboxes.
136         each mailbox is associated with 1 database home
137         """
138         log.info('Adding mailbox %s', mailbox)
139         if not self.mailboxes.has_key(mailbox.server):
140             self.mailboxes[mailbox.server] = {'protocol':'imaps', 'users':{}}
141         server = self.mailboxes[mailbox.server]
142         if mailbox.protocol:
143             server['protocol'] = mailbox.protocol
144
145         if not server['users'].has_key(mailbox.username):
146             server['users'][mailbox.username] = {'password':'', 'mailboxes':{}}
147         user = server['users'][mailbox.username]
148         if mailbox.password:
149             user['password'] = mailbox.password
150
151         if user['mailboxes'].has_key(mailbox.mailbox):
152             raise ValueError, 'Mailbox is already defined'
153
154         user['mailboxes'][mailbox.mailbox] = mailbox.dbhome
155
156     def _process(self, message, dbhome):
157         """Actually process one of the email messages"""
158         child = os.popen('roundup-mailgw %s' % dbhome, 'wb')
159         child.write(message)
160         child.close()
161         #print message
162
163     def _getMessages(self, serv, count, dbhome):
164         """This assumes that you currently have a mailbox open, and want to
165         process all messages that are inside.
166         """
167         for n in range(1, count+1):
168             (t, data) = serv.fetch(n, '(RFC822)')
169             if t == 'OK':
170                 self._process(data[0][1], dbhome)
171                 serv.store(n, '+FLAGS', r'(\Deleted)')
172
173     def checkBoxes(self):
174         """This actually goes out and does all the checking.
175         Returns False if there were any errors, otherwise returns true.
176         """
177         noErrors = True
178         for server in self.mailboxes:
179             log.info('Connecting to server: %s', server)
180             s_vals = self.mailboxes[server]
181
182             try:
183                 for user in s_vals['users']:
184                     u_vals = s_vals['users'][user]
185                     # TODO: As near as I can tell, you can only
186                     # login with 1 username for each connection to a server.
187                     protocol = s_vals['protocol'].lower()
188                     if protocol == 'imaps':
189                         serv = imaplib.IMAP4_SSL(server)
190                     elif protocol == 'imap':
191                         serv = imaplib.IMAP4(server)
192                     else:
193                         raise ValueError, 'Unknown protocol %s' % protocol
194
195                     password = u_vals['password']
196
197                     try:
198                         log.info('Connecting as user: %s', user)
199                         serv.login(user, password)
200
201                         for mbox in u_vals['mailboxes']:
202                             dbhome = u_vals['mailboxes'][mbox]
203                             log.info('Using mailbox: %s, home: %s',
204                                 mbox, dbhome)
205                             #access a specific mailbox
206                             if mbox:
207                                 (t, data) = serv.select(mbox)
208                             else:
209                                 # Select the default mailbox (INBOX)
210                                 (t, data) = serv.select()
211                             try:
212                                 nMessages = int(data[0])
213                             except ValueError:
214                                 nMessages = 0
215
216                             log.info('Found %s messages', nMessages)
217
218                             if nMessages:
219                                 self._getMessages(serv, nMessages, dbhome)
220                                 serv.expunge()
221
222                             # We are done with this mailbox
223                             serv.close()
224                     except:
225                         log.exception('Exception with server %s user %s',
226                             server, user)
227                         noErrors = False
228
229                     serv.logout()
230                     serv.shutdown()
231                     del serv
232             except:
233                 log.exception('Exception while connecting to %s', server)
234                 noErrors = False
235         return noErrors
236
237
238     def makeDaemon(self):
239         """Turn this process into a daemon.
240
241         - make our parent PID 1
242
243         Write our new PID to the pidfile.
244
245         From A.M. Kuuchling (possibly originally Greg Ward) with
246         modification from Oren Tirosh, and finally a small mod from me.
247         Originally taken from roundup.scripts.roundup_server.py
248         """
249         log.info('Running as Daemon')
250         # Fork once
251         if os.fork() != 0:
252             os._exit(0)
253
254         # Create new session
255         os.setsid()
256
257         # Second fork to force PPID=1
258         pid = os.fork()
259         if pid:
260             if self.pidfile:
261                 pidfile = open(self.pidfile, 'w')
262                 pidfile.write(str(pid))
263                 pidfile.close()
264             os._exit(0)
265
266     def run(self):
267         """Run email gathering daemon.
268
269         This spawns itself as a daemon, and then runs continually, just
270         sleeping inbetween checks.  It is recommended that you run
271         checkBoxes once first before you select run. That way you can
272         know if there were any failures.
273         """
274         if self.daemon:
275             self.makeDaemon()
276         while True:
277
278             time.sleep(self.delay * 60.0)
279             log.info('Time: %s', time.strftime('%Y-%m-%d %H:%M:%S'))
280             self.checkBoxes()
281
282 def getItems(s):
283     """Parse a string looking for userame@server"""
284     myRE = re.compile(
285         r'((?P<protocol>[^:]+)://)?'#You can supply a protocol if you like
286         r'('                        #The username part is optional
287          r'(?P<username>[^:]+)'     #You can supply the password as
288          r'(:(?P<password>.+))?'    #username:password@server
289         r'@)?'
290         r'(?P<server>[^/]+)'
291         r'(/(?P<mailbox>.+))?$'
292     )
293     m = myRE.match(s)
294     if m:
295         return m.groupdict()
296     else:
297         return None
298
299 def main():
300     """This is what is called if run at the prompt"""
301     parser = optparse.OptionParser(
302         version=('%prog ' + version),
303         usage="""usage: %prog [options] (home server)...
304
305 So each entry has a home, and then the server configuration. Home is just
306 a path to the roundup issue tracker. The server is something of the form:
307
308     imaps://user:password@server/mailbox
309
310 If you don't supply the protocol, imaps is assumed. Without user or
311 password, you will be prompted for them. The server must be supplied.
312 Without mailbox the INBOX is used.
313
314 Examples:
315   %prog /home/roundup/trackers/test imaps://test@imap.example.com/test
316   %prog /home/roundup/trackers/test imap.example.com \
317 /home/roundup/trackers/test2 imap.example.com/test2
318 """
319     )
320     parser.add_option('-d', '--delay', dest='delay', type='float',
321         metavar='<sec>', default=5,
322         help="Set the delay between checks in minutes. (default 5)"
323     )
324     parser.add_option('-p', '--pid-file', dest='pidfile',
325         metavar='<file>', default=None,
326         help="The pid of the server process will be written to <file>"
327     )
328     parser.add_option('-n', '--no-daemon', dest='daemon',
329         action='store_false', default=True,
330         help="Do not fork into the background after running the first check."
331     )
332     parser.add_option('-v', '--verbose', dest='verbose',
333         action='store_const', const=logging.INFO,
334         help="Be more verbose in letting you know what is going on."
335         " Enables informational messages."
336     )
337     parser.add_option('-V', '--very-verbose', dest='verbose',
338         action='store_const', const=logging.DEBUG,
339         help="Be very verbose in letting you know what is going on."
340             " Enables debugging messages."
341     )
342     parser.add_option('-q', '--quiet', dest='verbose',
343         action='store_const', const=logging.ERROR,
344         help="Be less verbose. Ignores warnings, only prints errors."
345     )
346     parser.add_option('-Q', '--very-quiet', dest='verbose',
347         action='store_const', const=logging.CRITICAL,
348         help="Be much less verbose. Ignores warnings and errors."
349             " Only print CRITICAL messages."
350     )
351
352     (opts, args) = parser.parse_args()
353     if (len(args) == 0) or (len(args) % 2 == 1):
354         parser.error('Invalid number of arguments. '
355             'Each site needs a home and a server.')
356
357     log.setLevel(opts.verbose)
358     myServer = IMAPServer(delay=opts.delay, pidfile=opts.pidfile,
359         daemon=opts.daemon)
360     for i in range(0,len(args),2):
361         home = args[i]
362         server = args[i+1]
363         if not os.path.exists(home):
364             parser.error('Home: "%s" does not exist' % home)
365
366         info = getItems(server)
367         if not info:
368             parser.error('Invalid server string: "%s"' % server)
369
370         myServer.addMailbox(
371             RoundupMailbox(dbhome=home, mailbox=info['mailbox']
372             , username=info['username'], password=info['password']
373             , server=info['server'], protocol=info['protocol']
374             )
375         )
376
377     if myServer.checkBoxes():
378         myServer.run()
379
380 if __name__ == '__main__':
381     main()
382
383 # vim: et ft=python si sts=4 sw=4