Mercurial > p > roundup > code
changeset 3176:18ad9d702a5b
added "imapServer.py" script (patch [SF#934567])
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Mon, 14 Feb 2005 06:09:14 +0000 |
| parents | e2a590ecc5e4 |
| children | 88dbe6b3d891 |
| files | CHANGES.txt scripts/README.txt scripts/imapServer.py |
| diffstat | 3 files changed, 372 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/CHANGES.txt Mon Feb 14 05:56:23 2005 +0000 +++ b/CHANGES.txt Mon Feb 14 06:09:14 2005 +0000 @@ -24,6 +24,7 @@ - fixed class "help" listing paging (sf bug 1106329) - nicer error looking up values of None (response to sf bug 1108697) - fallback for (list) popups if javascript disabled (sf patch 1101626) +- added "imapServer.py" script (sf patch 934567) 2005-01-13 0.8.0b2
--- a/scripts/README.txt Mon Feb 14 05:56:23 2005 +0000 +++ b/scripts/README.txt Mon Feb 14 06:09:14 2005 +0000 @@ -23,3 +23,7 @@ Offers start, stop and restart commands and integrates with the Debian init process. +imapServer.py + This IMAP server script that runs in the background and checks for new + email from a variety of mailboxes. +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/imapServer.py Mon Feb 14 06:09:14 2005 +0000 @@ -0,0 +1,367 @@ +#!/usr/bin/env python2.3 +# arch-tag: f2d1fd6e-df72-4188-a3b4-a9dbbb0807b9 +# vim: filetype=python ts=4 sw=4 noexpandtab si +"""\ +This script is a wrapper around the mailgw.py script that exists in roundup. +It runs as service instead of running as a one-time shot. +It also connects to a secure IMAP server. The main reasons for this script are: + +1) The roundup-mailgw script isn't designed to run as a server. It expects that you + either run it by hand, and enter the password each time, or you supply the + password on the command line. I prefer to run a server that I initialize with + the password, and then it just runs. I don't want to have to pass it on the + command line, so running through crontab isn't a possibility. (This wouldn't + be a problem on a local machine running through a mailspool.) +2) mailgw.py somehow screws up SSL support so IMAP4_SSL doesn't work. So hopefully + running that work outside of the mailgw will allow it to work. +3) I wanted to be able to check multiple projects at the same time. roundup-mailgw is + only for 1 mailbox and 1 project. + + +*TODO*: + For the first round, the program spawns a new roundup-mailgw for each imap message + that it finds and pipes the result in. In the future it might be more practical to + actually include the roundup files and run the appropriate commands using python. + +*TODO*: + Look into supporting a logfile instead of using 2>/logfile + +*TODO*: + Add an option for changing the uid/gid of the running process. +""" + +import logging +logging.basicConfig() +log = logging.getLogger('IMAPServer') + +version = '0.1.2' + +class RoundupMailbox: + """This contains all the info about each mailbox. + Username, Password, server, security, roundup database + """ + def __init__(self, dbhome='', username=None, password=None, mailbox=None + , server=None, protocol='imaps'): + self.username = username + self.password = password + self.mailbox = mailbox + self.server = server + self.protocol = protocol + self.dbhome = dbhome + + try: + if not self.dbhome: + import os + self.dbhome = raw_input('Tracker home: ') + if not os.path.exists(self.dbhome): + raise ValueError, 'Invalid home address directory does not exist. %s' % self.dbhome + + if not self.server: + self.server = raw_input('Server: ') + if not self.server: + raise ValueError, 'No Servername supplied' + protocol = raw_input('protocol [imaps]? ') + self.protocol = protocol + + if not self.username: + self.username = raw_input('Username: ') + if not self.username: + raise ValueError, 'Invalid Username' + + if not self.password: + import getpass + print 'For server %s, user %s' % (self.server, self.username) + self.password = getpass.getpass() + # password can be empty because it could be superceeded by a later + # entry + + #if self.mailbox is None: + # self.mailbox = raw_input('Mailbox [INBOX]: ') + # # We allow an empty mailbox because that will + # # select the INBOX, whatever it is called + + except (KeyboardInterrupt, EOFError): + raise ValueError, 'Canceled by User' + + def __str__(self): + return """Mailbox{ server:%(server)s, protocol:%(protocol)s, username:%(username)s, mailbox:%(mailbox)s, dbhome:%(dbhome)s }""" % self.__dict__ + + + +class IMAPServer: + """This class runs as a server process. It is configured with a list of + mailboxes to connect to, along with the roundup database directories that correspond + with each email address. + It then connects to each mailbox at a specified interval, and if there are new messages + it reads them, and sends the result to the roundup.mailgw. + + *TODO*: + Try to be smart about how you access the mailboxes so that you can connect once, and + access multiple mailboxes and possibly multiple usernames. + + *NOTE*: + This assumes that if you are using the same user on the same server, you are using + the same password. (the last one supplied is used.) Empty passwords are ignored. + Only the last protocol supplied is used. + """ + + def __init__(self, pidfile=None, delay=5, daemon=False): + #This is sorted by servername, then username, then mailboxes + self.mailboxes = {} + self.delay = float(delay) + self.pidfile = pidfile + self.daemon = daemon + + def setDelay(self, delay): + self.delay = delay + + def addMailbox(self, mailbox): + """ The linkage is as follows: + servers -- users - mailbox:dbhome + So there can be multiple servers, each with multiple users. + Each username can be associated with multiple mailboxes. + each mailbox is associated with 1 database home + """ + log.info('Adding mailbox %s', mailbox) + if not self.mailboxes.has_key(mailbox.server): + self.mailboxes[mailbox.server] = {'protocol':'imaps', 'users':{}} + server = self.mailboxes[mailbox.server] + if mailbox.protocol: + server['protocol'] = mailbox.protocol + + if not server['users'].has_key(mailbox.username): + server['users'][mailbox.username] = {'password':'', 'mailboxes':{}} + user = server['users'][mailbox.username] + if mailbox.password: + user['password'] = mailbox.password + + if user['mailboxes'].has_key(mailbox.mailbox): + raise ValueError, 'Mailbox is already defined' + + user['mailboxes'][mailbox.mailbox] = mailbox.dbhome + + def _process(self, message, dbhome): + """Actually process one of the email messages""" + import os, sys + child = os.popen('roundup-mailgw %s' % dbhome, 'wb') + child.write(message) + child.close() + #print message + + def _getMessages(self, serv, count, dbhome): + """This assumes that you currently have a mailbox open, and want to + process all messages that are inside. + """ + for n in range(1, count+1): + (t, data) = serv.fetch(n, '(RFC822)') + if t == 'OK': + self._process(data[0][1], dbhome) + serv.store(n, '+FLAGS', r'(\Deleted)') + + def checkBoxes(self): + """This actually goes out and does all the checking. + Returns False if there were any errors, otherwise returns true. + """ + import imaplib + noErrors = True + for server in self.mailboxes: + log.info('Connecting to server: %s', server) + s_vals = self.mailboxes[server] + + try: + for user in s_vals['users']: + u_vals = s_vals['users'][user] + # TODO: As near as I can tell, you can only + # login with 1 username for each connection to a server. + protocol = s_vals['protocol'].lower() + if protocol == 'imaps': + serv = imaplib.IMAP4_SSL(server) + elif protocol == 'imap': + serv = imaplib.IMAP4(server) + else: + raise ValueError, 'Unknown protocol %s' % protocol + + password = u_vals['password'] + + try: + log.info('Connecting as user: %s', user) + serv.login(user, password) + + for mbox in u_vals['mailboxes']: + dbhome = u_vals['mailboxes'][mbox] + log.info('Using mailbox: %s, home: %s', mbox, dbhome) + #access a specific mailbox + if mbox: + (t, data) = serv.select(mbox) + else: + # Select the default mailbox (INBOX) + (t, data) = serv.select() + try: + nMessages = int(data[0]) + except ValueError: + nMessages = 0 + + log.info('Found %s messages', nMessages) + + if nMessages: + self._getMessages(serv, nMessages, dbhome) + serv.expunge() + + # We are done with this mailbox + serv.close() + except: + log.exception('Exception with server %s user %s', server, user) + noErrors = False + + serv.logout() + serv.shutdown() + del serv + except: + log.exception('Exception while connecting to %s', server) + noErrors = False + return noErrors + + + def makeDaemon(self): + """This forks a couple of times, and otherwise makes this run as a daemon.""" + ''' Turn this process into a daemon. + - make our parent PID 1 + + Write our new PID to the pidfile. + + From A.M. Kuuchling (possibly originally Greg Ward) with + modification from Oren Tirosh, and finally a small mod from me. + Originally taken from roundup.scripts.roundup_server.py + ''' + log.info('Running as Daemon') + import os + # Fork once + if os.fork() != 0: + os._exit(0) + + # Create new session + os.setsid() + + # Second fork to force PPID=1 + pid = os.fork() + if pid: + if self.pidfile: + pidfile = open(self.pidfile, 'w') + pidfile.write(str(pid)) + pidfile.close() + os._exit(0) + + #os.chdir("/") + #os.umask(0) + + def run(self): + """This spawns itself as a daemon, and then runs continually, just sleeping inbetween checks. + It is recommended that you run checkBoxes once first before you select run. That way you can + know if there were any failures. + """ + import time + if self.daemon: + self.makeDaemon() + while True: + + time.sleep(self.delay * 60.0) + log.info('Time: %s', time.strftime('%Y-%m-%d %H:%M:%S')) + self.checkBoxes() + +def getItems(s): + """Parse a string looking for userame@server""" + import re + myRE = re.compile( + r'((?P<proto>[^:]+)://)?'#You can supply a protocol if you like + r'(' #The username part is optional + r'(?P<user>[^:]+)' #You can supply the password as + r'(:(?P<pass>.+))?' #username:password@server + r'@)?' + r'(?P<server>[^/]+)' + r'(/(?P<mailbox>.+))?$' + ) + m = myRE.match(s) + if m: + return {'username':m.group('user'), 'password':m.group('pass') + , 'server':m.group('server'), 'protocol':m.group('proto') + , 'mailbox':m.group('mailbox') + } + +def main(): + """This is what is called if run at the prompt""" + import optparse, os + parser = optparse.OptionParser( + version=('%prog ' + version) + , usage="""usage: %prog [options] (home server)... +So each entry has a home, and then the server configuration. home is just a path to the +roundup issue tracker. The server is something of the form: + imaps://user:password@server/mailbox +If you don't supply the protocol, imaps is assumed. Without user or password, you will be +prompted for them. The server must be supplied. Without mailbox the INBOX is used. + +Examples: + %prog /home/roundup/trackers/test imaps://test@imap.example.com/test + %prog /home/roundup/trackers/test imap.example.com /home/roundup/trackers/test2 imap.example.com/test2 +""" + ) + parser.add_option('-d', '--delay', dest='delay', type='float', metavar='<sec>' + , default=5 + , help="Set the delay between checks in minutes. (default 5)" + ) + parser.add_option('-p', '--pid-file', dest='pidfile', metavar='<file>' + , default=None + , help="The pid of the server process will be written to <file>" + ) + parser.add_option('-n', '--no-daemon', dest='daemon', action='store_false' + , default=True + , help="Do not fork into the background after running the first check." + ) + parser.add_option('-v', '--verbose', dest='verbose', action='store_const' + , const=logging.INFO + , help="Be more verbose in letting you know what is going on." + " Enables informational messages." + ) + parser.add_option('-V', '--very-verbose', dest='verbose', action='store_const' + , const=logging.DEBUG + , help="Be very verbose in letting you know what is going on." + " Enables debugging messages." + ) + parser.add_option('-q', '--quiet', dest='verbose', action='store_const' + , const=logging.ERROR + , help="Be less verbose. Ignores warnings, only prints errors." + ) + parser.add_option('-Q', '--very-quiet', dest='verbose', action='store_const' + , const=logging.CRITICAL + , help="Be much less verbose. Ignores warnings and errors." + " Only print CRITICAL messages." + ) + + (opts, args) = parser.parse_args() + if (len(args) == 0) or (len(args) % 2 == 1): + parser.error('Invalid number of arguments. Each site needs a home and a server.') + + log.setLevel(opts.verbose) + myServer = IMAPServer(delay=opts.delay, pidfile=opts.pidfile, daemon=opts.daemon) + for i in range(0,len(args),2): + home = args[i] + server = args[i+1] + if not os.path.exists(home): + parser.error('Home: "%s" does not exist' % home) + + info = getItems(server) + if not info: + parser.error('Invalid server string: "%s"' % server) + + myServer.addMailbox( + RoundupMailbox(dbhome=home, mailbox=info['mailbox'] + , username=info['username'], password=info['password'] + , server=info['server'], protocol=info['protocol'] + ) + ) + + if myServer.checkBoxes(): + myServer.run() + +if __name__ == '__main__': + main() +
