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