"Fossies" - the Fresh Open Source Software Archive

Member "roundup-2.0.0/scripts/imapServer.py" (26 Aug 2019, 14297 Bytes) of package /linux/www/roundup-2.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "imapServer.py": 1.6.1_vs_2.0.0.

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