"Fossies" - the Fresh Open Source Software Archive

Member "getmail-5.16/getmailcore/destinations.py" (31 Oct 2021, 44147 Bytes) of package /linux/misc/getmail-5.16.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. For more information about "destinations.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 5.15_vs_5.16.

    1 #!/usr/bin/env python2
    2 '''Classes implementing destinations (files, directories, or programs getmail
    3 can deliver mail to).
    4 
    5 Currently implemented:
    6 
    7   Maildir
    8   Mboxrd
    9   MDA_qmaillocal (deliver though qmail-local as external MDA)
   10   MDA_external (deliver through an arbitrary external MDA)
   11   MultiSorter (deliver to a selection of maildirs/mbox files based on matching
   12     recipient address patterns)
   13 '''
   14 
   15 __all__ = [
   16     'DeliverySkeleton',
   17     'Maildir',
   18     'Mboxrd',
   19     'MDA_qmaillocal',
   20     'MDA_external',
   21     'MultiDestinationBase',
   22     'MultiDestination',
   23     'MultiSorterBase',
   24     'MultiSorter',
   25 ]
   26 
   27 import os
   28 import re
   29 import tempfile
   30 import types
   31 import email.Utils
   32 
   33 import pwd
   34 
   35 from getmailcore.exceptions import *
   36 from getmailcore.utilities import *
   37 from getmailcore.baseclasses import *
   38 
   39 #######################################
   40 class DeliverySkeleton(ConfigurableBase):
   41     '''Base class for implementing message-delivery classes.
   42 
   43     Sub-classes should provide the following data attributes and methods:
   44 
   45       _confitems - a tuple of dictionaries representing the parameters the class
   46                    takes.  Each dictionary should contain the following key,
   47                    value pairs:
   48                      - name - parameter name
   49                      - type - a type function to compare the parameter value
   50                      against (i.e. str, int, bool)
   51                      - default - optional default value.  If not preseent, the
   52                      parameter is required.
   53 
   54       __str__(self) - return a simple string representing the class instance.
   55 
   56       showconf(self) - log a message representing the instance and configuration
   57                        from self._confstring().
   58 
   59       initialize(self) - process instantiation parameters from self.conf.
   60                          Raise getmailConfigurationError on errors.  Do any
   61                          other validation necessary, and set self.__initialized
   62                          when done.
   63 
   64       retriever_info(self, retriever) - extract information from retriever and
   65                         store it for use in message deliveries.
   66 
   67       _deliver_message(self, msg, delivered_to, received) - accept the message
   68                         and deliver it, returning a string describing the
   69                         result.
   70 
   71     See the Maildir class for a good, simple example.
   72     '''
   73     def __init__(self, **args):
   74         ConfigurableBase.__init__(self, **args)
   75         try:
   76             self.initialize()
   77         except KeyError, o:
   78             raise getmailConfigurationError(
   79                 'missing required configuration parameter %s' % o
   80             )
   81         self.received_from = None
   82         self.received_with = None
   83         self.received_by = None
   84         self.retriever = None
   85         self.log.trace('done\n')
   86 
   87     def retriever_info(self, retriever):
   88         self.log.trace()
   89         self.received_from = retriever.received_from
   90         self.received_with = retriever.received_with
   91         self.received_by = retriever.received_by
   92         self.retriever = retriever
   93 
   94     def deliver_message(self, msg, delivered_to=True, received=True):
   95         self.log.trace()
   96         msg.received_from = self.received_from
   97         msg.received_with = self.received_with
   98         msg.received_by = self.received_by
   99         return self._deliver_message(msg, delivered_to, received)
  100 
  101 #######################################
  102 class Maildir(DeliverySkeleton, ForkingBase):
  103     '''Maildir destination.
  104 
  105     Parameters:
  106 
  107       path - path to maildir, which will be expanded for leading '~/' or
  108       '~USER/', as well as environment variables.
  109     '''
  110     _confitems = (
  111         ConfInstance(name='configparser', required=False),
  112         ConfMaildirPath(name='path'),
  113         ConfString(name='user', required=False, default=None),
  114         ConfString(name='filemode', required=False, default='0600'),
  115     )
  116 
  117     def initialize(self):
  118         self.log.trace()
  119         self.hostname = localhostname()
  120         self.dcount = 0
  121         try:
  122             self.conf['filemode'] = int(self.conf['filemode'], 8)
  123         except ValueError, o:
  124             raise getmailConfigurationError('filemode %s not valid: %s'
  125                                             % (self.conf['filemode'], o))
  126 
  127     def __str__(self):
  128         self.log.trace()
  129         return 'Maildir %s' % self.conf['path']
  130 
  131     def showconf(self):
  132         self.log.info('Maildir(%s)\n' % self._confstring())
  133 
  134     def __deliver_message_maildir(self, uid, gid, msg, delivered_to, received,
  135                                   stdout, stderr):
  136         '''Delivery method run in separate child process.
  137         '''
  138         try:
  139             if os.name == 'posix':
  140                 if uid:
  141                     change_uidgid(None, uid, gid)
  142                 if os.geteuid() == 0:
  143                     raise getmailConfigurationError(
  144                         'refuse to deliver mail as root'
  145                     )
  146                 if os.getegid() == 0:
  147                     raise getmailConfigurationError(
  148                         'refuse to deliver mail as GID 0'
  149                     )
  150             f = deliver_maildir(
  151                 self.conf['path'], msg.flatten(delivered_to, received),
  152                 self.hostname, self.dcount, self.conf['filemode']
  153             )
  154             stdout.write(f)
  155             stdout.flush()
  156             os.fsync(stdout.fileno())
  157             os._exit(0)
  158         except StandardError, o:
  159             # Child process; any error must cause us to exit nonzero for parent
  160             # to detect it
  161             stderr.write('maildir delivery process failed (%s)' % o)
  162             stderr.flush()
  163             os.fsync(stderr.fileno())
  164             os._exit(127)
  165 
  166     def _deliver_message(self, msg, delivered_to, received):
  167         self.log.trace()
  168         uid = None
  169         gid = None
  170         user = self.conf['user']
  171         if os.name == 'posix':
  172             if user and uid_of_user(user) != os.geteuid():
  173                 # Config specifies delivery as user other than current UID
  174                 uid = uid_of_user(user)
  175                 gid = gid_of_uid(uid)
  176                 if uid == 0:
  177                     raise getmailConfigurationError(
  178                         'refuse to deliver mail as root'
  179                     )
  180                 if gid == 0:
  181                     raise getmailConfigurationError(
  182                         'refuse to deliver mail as GID 0'
  183                     )
  184         self._prepare_child()
  185         stdout = tempfile.TemporaryFile()
  186         stderr = tempfile.TemporaryFile()
  187         childpid = os.fork()
  188 
  189         if not childpid:
  190             # Child
  191             self.__deliver_message_maildir(uid, gid, msg, delivered_to,
  192                                            received, stdout, stderr)
  193         self.log.debug('spawned child %d\n' % childpid)
  194 
  195         # Parent
  196         exitcode = self._wait_for_child(childpid)
  197 
  198         stdout.seek(0)
  199         stderr.seek(0)
  200         out = stdout.read().strip()
  201         err = stderr.read().strip()
  202 
  203         self.log.debug('maildir delivery process %d exited %d\n'
  204                        % (childpid, exitcode))
  205 
  206         if exitcode or err:
  207             raise getmailDeliveryError('maildir delivery %d error (%d, %s)'
  208                                        % (childpid, exitcode, err))
  209 
  210         self.dcount += 1
  211         self.log.debug('maildir file %s' % out)
  212         return self
  213 
  214 #######################################
  215 class Mboxrd(DeliverySkeleton, ForkingBase):
  216     '''mboxrd destination with fcntl-style locking.
  217 
  218     Parameters:
  219 
  220       path - path to mboxrd file, which will be expanded for leading '~/'
  221       or '~USER/', as well as environment variables.
  222 
  223     Note the differences between various subtypes of mbox format (mboxrd, mboxo,
  224     mboxcl, mboxcl2) and differences in locking; see the following for details:
  225     http://qmail.org/man/man5/mbox.html
  226     http://groups.google.com/groups?selm=4ivk9s%24bok%40hustle.rahul.net
  227     '''
  228     _confitems = (
  229         ConfInstance(name='configparser', required=False),
  230         ConfMboxPath(name='path'),
  231         ConfString(name='locktype', required=False, default='lockf'),
  232         ConfString(name='user', required=False, default=None),
  233     )
  234 
  235     def initialize(self):
  236         self.log.trace()
  237         if self.conf['locktype'] not in ('lockf', 'flock'):
  238             raise getmailConfigurationError('unknown mbox lock type: %s'
  239                                             % self.conf['locktype'])
  240 
  241     def __str__(self):
  242         self.log.trace()
  243         return 'Mboxrd %s' % self.conf['path']
  244 
  245     def showconf(self):
  246         self.log.info('Mboxrd(%s)\n' % self._confstring())
  247 
  248     def __deliver_message_mbox(self, uid, gid, msg, delivered_to, received,
  249                                stdout, stderr):
  250         '''Delivery method run in separate child process.
  251         '''
  252         try:
  253             if os.name == 'posix':
  254                 if uid:
  255                     change_uidgid(None, uid, gid)
  256                 if os.geteuid() == 0:
  257                     raise getmailConfigurationError(
  258                         'refuse to deliver mail as root'
  259                     )
  260                 if os.getegid() == 0:
  261                     raise getmailConfigurationError(
  262                         'refuse to deliver mail as GID 0'
  263                     )
  264 
  265             if not os.path.exists(self.conf['path']):
  266                 raise getmailDeliveryError('mboxrd does not exist (%s)'
  267                                            % self.conf['path'])
  268             if not os.path.isfile(self.conf['path']):
  269                 raise getmailDeliveryError('not an mboxrd file (%s)'
  270                                            % self.conf['path'])
  271 
  272             # Open mbox file, refusing to create it if it doesn't exist
  273             fd = os.open(self.conf['path'], os.O_RDWR)
  274             status_old = os.fstat(fd)
  275             f = os.fdopen(fd, 'r+b')
  276             lock_file(f, self.conf['locktype'])
  277             # Check if it _is_ an mbox file.  mbox files must start with "From "
  278             # in their first line, or are 0-length files.
  279             f.seek(0, 0)
  280             first_line = f.readline()
  281             if first_line and not first_line.startswith('From '):
  282                 # Not an mbox file; abort here
  283                 unlock_file(f, self.conf['locktype'])
  284                 raise getmailDeliveryError('not an mboxrd file (%s)'
  285                                            % self.conf['path'])
  286             # Seek to end
  287             f.seek(0, 2)
  288             try:
  289                 # Write out message plus blank line with native EOL
  290                 f.write(msg.flatten(delivered_to, received, include_from=True,
  291                                     mangle_from=True) + os.linesep)
  292                 f.flush()
  293                 os.fsync(fd)
  294                 status_new = os.fstat(fd)
  295                 # Reset atime
  296                 try:
  297                     os.utime(self.conf['path'], (status_old.st_atime,
  298                              status_new.st_mtime))
  299                 except OSError, o:
  300                     # Not root or owner; readers will not be able to reliably
  301                     # detect new mail.  But you shouldn't be delivering to
  302                     # other peoples' mboxes unless you're root, anyways.
  303                     stdout.write('failed to updated mtime/atime of mbox')
  304                     stdout.flush()
  305                     os.fsync(stdout.fileno())
  306 
  307                 unlock_file(f, self.conf['locktype'])
  308 
  309             except IOError, o:
  310                 try:
  311                     if not f.closed:
  312                         # If the file was opened and we know how long it was,
  313                         # try to truncate it back to that length
  314                         # If it's already closed, or the error occurred at
  315                         # close(), then there's not much we can do.
  316                         f.truncate(status_old.st_size)
  317                 except KeyboardInterrupt:
  318                     raise
  319                 except StandardError:
  320                     pass
  321                 raise getmailDeliveryError(
  322                     'failure writing message to mbox file "%s" (%s)'
  323                     % (self.conf['path'], o)
  324                 )
  325 
  326             os._exit(0)
  327 
  328         except StandardError, o:
  329             # Child process; any error must cause us to exit nonzero for parent
  330             # to detect it
  331             stderr.write('mbox delivery process failed (%s)' % o)
  332             stderr.flush()
  333             os.fsync(stderr.fileno())
  334             os._exit(127)
  335 
  336     def _deliver_message(self, msg, delivered_to, received):
  337         self.log.trace()
  338         uid = None
  339         gid = None
  340         # Get user & group of mbox file
  341         st_mbox = os.stat(self.conf['path'])
  342         user = self.conf['user']
  343         if os.name == 'posix':
  344             if user and uid_of_user(user) != os.geteuid():
  345                 # Config specifies delivery as user other than current UID
  346                 uid = uid_of_user(user)
  347                 gid = gid_of_uid(uid)
  348             if uid == 0:
  349                 raise getmailConfigurationError(
  350                     'refuse to deliver mail as root'
  351                 )
  352             if gid == 0:
  353                 raise getmailConfigurationError(
  354                     'refuse to deliver mail as GID 0'
  355                 )
  356         self._prepare_child()
  357         stdout = tempfile.TemporaryFile()
  358         stderr = tempfile.TemporaryFile()
  359         childpid = os.fork()
  360 
  361         if not childpid:
  362             # Child
  363             self.__deliver_message_mbox(uid, gid, msg, delivered_to, received,
  364                                         stdout, stderr)
  365         self.log.debug('spawned child %d\n' % childpid)
  366 
  367         # Parent
  368         exitcode = self._wait_for_child(childpid)
  369 
  370         stdout.seek(0)
  371         stderr.seek(0)
  372         out = stdout.read().strip()
  373         err = stderr.read().strip()
  374 
  375         self.log.debug('mboxrd delivery process %d exited %d\n'
  376                        % (childpid, exitcode))
  377 
  378         if exitcode or err:
  379             raise getmailDeliveryError('mboxrd delivery %d error (%d, %s)'
  380                                        % (childpid, exitcode, err))
  381 
  382         if out:
  383             self.log.debug('mbox delivery: %s' % out)
  384 
  385         return self
  386 
  387 #######################################
  388 class MDA_qmaillocal(DeliverySkeleton, ForkingBase):
  389     '''qmail-local MDA destination.
  390 
  391     Passes the message to qmail-local for delivery.  qmail-local is invoked as:
  392 
  393       qmail-local -nN user homedir local dash ext domain sender defaultdelivery
  394 
  395     Parameters (all optional):
  396 
  397       qmaillocal - complete path to the qmail-local binary.  Defaults
  398                 to "/var/qmail/bin/qmail-local".
  399 
  400       user - username supplied to qmail-local as the "user" argument.  Defaults
  401             to the login name of the current effective user ID.  If supplied,
  402             getmail will also change the effective UID to that of the user
  403             before running qmail-local.
  404 
  405       group - If supplied, getmail will change the effective GID to that of the
  406             named group before running qmail-local.
  407 
  408       homedir - complete path to the directory supplied to qmail-local as the
  409             "homedir" argument. Defaults to the home directory of the current
  410             effective user ID.
  411 
  412       localdomain - supplied to qmail-local as the "domain" argument.  Defaults
  413             to localhostname().
  414 
  415       defaultdelivery - supplied to qmail-local as the "defaultdelivery"
  416             argument.  Defaults to "./Maildir/".
  417 
  418       conf-break - supplied to qmail-local as the "dash" argument and used to
  419             calculate ext from local.  Defaults to "-".
  420 
  421       localpart_translate - a string representing a Python 2-tuple of strings
  422             (i.e. "('foo', 'bar')"). If supplied, the retrieved message
  423             recipient address will have any leading instance of "foo" replaced
  424             with "bar" before being broken into "local" and "ext" for qmail-
  425             local (according to the values of "conf-break" and "user").  This
  426             can be used to add or remove a prefix of the address.
  427 
  428       strip_delivered_to - if set, existing Delivered-To: header fields will be
  429             removed from the message before processing by qmail-local.  This may
  430             be necessary to prevent qmail-local falsely detecting a looping
  431             message if (for instance) the system retrieving messages otherwise
  432             believes it has the same domain name as the POP server.
  433             Inappropriate use, however, may cause message loops.
  434 
  435       allow_root_commands (boolean, optional) - if set, external commands are
  436             allowed when running as root.  The default is not to allow such
  437             behaviour.
  438 
  439     For example, if getmail is run as user "exampledotorg", which has virtual
  440     domain "example.org" delegated to it with a virtualdomains entry of
  441     "example.org:exampledotorg", and messages are retrieved with envelope
  442     recipients like "trimtext-localpart@example.org", the messages could be
  443     properly passed to qmail-local with a localpart_translate value of
  444     "('trimtext-', '')" (and perhaps a defaultdelivery value of
  445     "./Maildirs/postmaster/" or similar).
  446     '''
  447 
  448     _confitems = (
  449         ConfInstance(name='configparser', required=False),
  450         ConfFile(name='qmaillocal', required=False,
  451                  default='/var/qmail/bin/qmail-local'),
  452         ConfString(name='user', required=False,
  453                    default=pwd.getpwuid(os.geteuid()).pw_name),
  454         ConfString(name='group', required=False, default=None),
  455         ConfDirectory(name='homedir', required=False,
  456                       default=pwd.getpwuid(os.geteuid()).pw_dir),
  457         ConfString(name='localdomain', required=False, default=localhostname()),
  458         ConfString(name='defaultdelivery', required=False,
  459                    default='./Maildir/'),
  460         ConfString(name='conf-break', required=False, default='-'),
  461         ConfTupleOfStrings(name='localpart_translate', required=False,
  462                            default="('', '')"),
  463         ConfBool(name='strip_delivered_to', required=False, default=False),
  464         ConfBool(name='allow_root_commands', required=False, default=False),
  465     )
  466 
  467     def initialize(self):
  468         self.log.trace()
  469 
  470     def __str__(self):
  471         self.log.trace()
  472         return 'MDA_qmaillocal %s' % self._confstring()
  473 
  474     def showconf(self):
  475         self.log.info('MDA_qmaillocal(%s)\n' % self._confstring())
  476 
  477     def _deliver_qmaillocal(self, msg, msginfo, delivered_to, received, stdout,
  478             stderr):
  479         try:
  480             args = (
  481                 self.conf['qmaillocal'], self.conf['qmaillocal'],
  482                 '--', self.conf['user'], self.conf['homedir'],
  483                 msginfo['local'], msginfo['dash'], msginfo['ext'],
  484                 self.conf['localdomain'], msginfo['sender'],
  485                 self.conf['defaultdelivery']
  486             )
  487             self.log.debug('about to execl() with args %s\n' % str(args))
  488             # Modify message
  489             if self.conf['strip_delivered_to']:
  490                 msg.remove_header('delivered-to')
  491                 # Also don't insert a Delivered-To: header.
  492                 delivered_to = None
  493             # Write out message
  494             msgfile = tempfile.TemporaryFile()
  495             msgfile.write(msg.flatten(delivered_to, received))
  496             msgfile.flush()
  497             os.fsync(msgfile.fileno())
  498             # Rewind
  499             msgfile.seek(0)
  500             # Set stdin to read from this file
  501             os.dup2(msgfile.fileno(), 0)
  502             # Set stdout and stderr to write to files
  503             os.dup2(stdout.fileno(), 1)
  504             os.dup2(stderr.fileno(), 2)
  505             change_usergroup(self.log, self.conf['user'], self.conf['group'])
  506             # At least some security...
  507             if ((os.geteuid() == 0 or os.getegid() == 0)
  508                     and not self.conf['allow_root_commands']):
  509                 raise getmailConfigurationError(
  510                     'refuse to invoke external commands as root '
  511                     'or GID 0 by default'
  512                 )
  513 
  514             os.execl(*args)
  515         except StandardError, o:
  516             # Child process; any error must cause us to exit nonzero for parent
  517             # to detect it
  518             stderr.write('exec of qmail-local failed (%s)' % o)
  519             stderr.flush()
  520             os.fsync(stderr.fileno())
  521             os._exit(127)
  522 
  523     def _deliver_message(self, msg, delivered_to, received):
  524         self.log.trace()
  525         self._prepare_child()
  526         if msg.recipient == None:
  527             raise getmailConfigurationError(
  528                 'MDA_qmaillocal destination requires a message source that '
  529                 'preserves the message envelope'
  530             )
  531         msginfo = {
  532             'sender' : msg.sender,
  533             'local' : '@'.join(msg.recipient.lower().split('@')[:-1])
  534         }
  535 
  536         self.log.debug('recipient: extracted local-part "%s"\n'
  537                        % msginfo['local'])
  538         xlate_from, xlate_to = self.conf['localpart_translate']
  539         if xlate_from or xlate_to:
  540             if msginfo['local'].startswith(xlate_from):
  541                 self.log.debug('recipient: translating "%s" to "%s"\n'
  542                                % (xlate_from, xlate_to))
  543                 msginfo['local'] = xlate_to + msginfo['local'][len(xlate_from):]
  544             else:
  545                 self.log.debug('recipient: does not start with xlate_from '
  546                                '"%s"\n' % xlate_from)
  547         self.log.debug('recipient: translated local-part "%s"\n'
  548                        % msginfo['local'])
  549         if self.conf['conf-break'] in msginfo['local']:
  550             msginfo['dash'] = self.conf['conf-break']
  551             msginfo['ext'] = self.conf['conf-break'].join(
  552                 msginfo['local'].split(self.conf['conf-break'])[1:]
  553             )
  554         else:
  555             msginfo['dash'] = ''
  556             msginfo['ext'] = ''
  557         self.log.debug('recipient: set dash to "%s", ext to "%s"\n'
  558                        % (msginfo['dash'], msginfo['ext']))
  559 
  560         stdout = tempfile.TemporaryFile()
  561         stderr = tempfile.TemporaryFile()
  562         childpid = os.fork()
  563 
  564         if not childpid:
  565             # Child
  566             self._deliver_qmaillocal(msg, msginfo, delivered_to, received,
  567                                      stdout, stderr)
  568         self.log.debug('spawned child %d\n' % childpid)
  569 
  570         # Parent
  571         exitcode = self._wait_for_child(childpid)
  572 
  573         stdout.seek(0)
  574         stderr.seek(0)
  575         out = stdout.read().strip()
  576         err = stderr.read().strip()
  577 
  578         self.log.debug('qmail-local %d exited %d\n' % (childpid, exitcode))
  579 
  580         if exitcode == 111:
  581             raise getmailDeliveryError('qmail-local %d temporary error (%s)'
  582                                        % (childpid, err))
  583         elif exitcode:
  584             raise getmailDeliveryError('qmail-local %d error (%d, %s)'
  585                                        % (childpid, exitcode, err))
  586 
  587         if out and err:
  588             info = '%s:%s' % (out, err)
  589         else:
  590             info = out or err
  591 
  592         return 'MDA_qmaillocal (%s)' % info
  593 
  594 #######################################
  595 class MDA_external(DeliverySkeleton, ForkingBase):
  596     '''Arbitrary external MDA destination.
  597 
  598     Parameters:
  599 
  600       path - path to the external MDA binary.
  601 
  602       unixfrom - (boolean) whether to include a Unix From_ line at the beginning
  603                  of the message.  Defaults to False.
  604 
  605       arguments - a valid Python tuple of strings to be passed as arguments to
  606                   the command.  The following replacements are available if
  607                   supported by the retriever:
  608 
  609                     %(sender) - envelope return path
  610                     %(recipient) - recipient address
  611                     %(domain) - domain-part of recipient address
  612                     %(local) - local-part of recipient address
  613                     %(mailbox) - for IMAP retrievers, the name of the 
  614                         server-side mailbox/folder the message was retrieved
  615                         from.  Will be empty for POP.
  616 
  617                   Warning: the text of these replacements is taken from the
  618                   message and is therefore under the control of a potential
  619                   attacker. DO NOT PASS THESE VALUES TO A SHELL -- they may
  620                   contain unsafe shell metacharacters or other hostile
  621                   constructions.
  622 
  623                   example:
  624 
  625                     path = /path/to/mymda
  626                     arguments = ('--demime', '-f%(sender)', '--', '%(recipient)')
  627 
  628       user (string, optional) - if provided, the external command will be run as
  629             the specified user.  This requires that the main getmail process
  630             have permission to change the effective user ID.
  631 
  632       group (string, optional) -  if provided, the external command will be run
  633             with the specified group ID.  This requires that the main getmail
  634             process have permission to change the effective group ID.
  635 
  636       allow_root_commands (boolean, optional) - if set, external commands are
  637             allowed when running as root.  The default is not to allow such
  638             behaviour.
  639 
  640       ignore_stderr (boolean, optional) - if set, getmail will not consider the
  641             program writing to stderr to be an error.  The default is False.
  642     '''
  643     _confitems = (
  644         ConfInstance(name='configparser', required=False),
  645         ConfFile(name='path'),
  646         ConfTupleOfStrings(name='arguments', required=False, default="()"),
  647         ConfString(name='user', required=False, default=None),
  648         ConfString(name='group', required=False, default=None),
  649         ConfBool(name='allow_root_commands', required=False, default=False),
  650         ConfBool(name='unixfrom', required=False, default=False),
  651         ConfBool(name='ignore_stderr', required=False, default=False),
  652     )
  653 
  654     def initialize(self):
  655         self.log.trace()
  656         self.conf['command'] = os.path.basename(self.conf['path'])
  657         if not os.access(self.conf['path'], os.X_OK):
  658             raise getmailConfigurationError('%s not executable'
  659                                             % self.conf['path'])
  660         if type(self.conf['arguments']) != tuple:
  661             raise getmailConfigurationError(
  662                 'incorrect arguments format; see documentation (%s)'
  663                 % self.conf['arguments']
  664             )
  665 
  666     def __str__(self):
  667         self.log.trace()
  668         return 'MDA_external %s (%s)' % (self.conf['command'],
  669                                          self._confstring())
  670 
  671     def showconf(self):
  672         self.log.info('MDA_external(%s)\n' % self._confstring())
  673 
  674     def _deliver_command(self, msg, msginfo, delivered_to, received,
  675                          stdout, stderr):
  676         try:
  677             # Write out message with native EOL convention
  678             msgfile = tempfile.TemporaryFile()
  679             msgfile.write(msg.flatten(delivered_to, received,
  680                                       include_from=self.conf['unixfrom']))
  681             msgfile.flush()
  682             os.fsync(msgfile.fileno())
  683             # Rewind
  684             msgfile.seek(0)
  685             # Set stdin to read from this file
  686             os.dup2(msgfile.fileno(), 0)
  687             # Set stdout and stderr to write to files
  688             os.dup2(stdout.fileno(), 1)
  689             os.dup2(stderr.fileno(), 2)
  690             change_usergroup(self.log, self.conf['user'], self.conf['group'])
  691             # At least some security...
  692             if ((os.geteuid() == 0 or os.getegid() == 0)
  693                     and not self.conf['allow_root_commands']):
  694                 raise getmailConfigurationError(
  695                     'refuse to invoke external commands as root '
  696                     'or GID 0 by default'
  697                 )
  698             args = [self.conf['path'], self.conf['path']]
  699             msginfo['mailbox'] = (self.retriever.mailbox_selected 
  700                                   or '').encode('utf-8')
  701             for arg in self.conf['arguments']:
  702                 arg = expand_user_vars(arg)
  703                 for (key, value) in msginfo.items():
  704                     arg = arg.replace('%%(%s)' % key, value)
  705                 args.append(arg)
  706             self.log.debug('about to execl() with args %s\n' % str(args))
  707             os.execl(*args)
  708         except StandardError, o:
  709             # Child process; any error must cause us to exit nonzero for parent
  710             # to detect it
  711             stderr.write('exec of command %s failed (%s)'
  712                          % (self.conf['command'], o))
  713             stderr.flush()
  714             os.fsync(stderr.fileno())
  715             os._exit(127)
  716 
  717     def _deliver_message(self, msg, delivered_to, received):
  718         self.log.trace()
  719         self._prepare_child()
  720         msginfo = {}
  721         msginfo['sender'] = msg.sender
  722         if msg.recipient != None:
  723             msginfo['recipient'] = msg.recipient
  724             msginfo['domain'] = msg.recipient.lower().split('@')[-1]
  725             msginfo['local'] = '@'.join(msg.recipient.split('@')[:-1])
  726         self.log.debug('msginfo "%s"\n' % msginfo)
  727 
  728         stdout = tempfile.TemporaryFile()
  729         stderr = tempfile.TemporaryFile()
  730         childpid = os.fork()
  731 
  732         if not childpid:
  733             # Child
  734             self._deliver_command(msg, msginfo, delivered_to, received,
  735                                   stdout, stderr)
  736         self.log.debug('spawned child %d\n' % childpid)
  737 
  738         # Parent
  739         exitcode = self._wait_for_child(childpid)
  740 
  741         stdout.seek(0)
  742         stderr.seek(0)
  743         out = stdout.read().strip()
  744         err = stderr.read().strip()
  745 
  746         self.log.debug('command %s %d exited %d\n'
  747                        % (self.conf['command'], childpid, exitcode))
  748 
  749         if exitcode:
  750             raise getmailDeliveryError(
  751                 'command %s %d error (%d, %s)'
  752                 % (self.conf['command'], childpid, exitcode, err)
  753             )
  754         elif err:
  755             if not self.conf['ignore_stderr']:
  756                 raise getmailDeliveryError(
  757                     'command %s %d wrote to stderr: %s'
  758                     % (self.conf['command'], childpid, err)
  759                 )
  760             #else:
  761             # User said to ignore stderr, just log it.
  762             self.log.info('command %s: %s' % (self, err))
  763 
  764         return 'MDA_external command %s (%s)' % (self.conf['command'], out)
  765 
  766 #######################################
  767 class MultiDestinationBase(DeliverySkeleton):
  768     '''Base class for destinations which hand messages off to other
  769     destinations.
  770 
  771     Sub-classes must provide the following attributes and methods:
  772 
  773       conf - standard ConfigurableBase configuration dictionary
  774 
  775       log - getmailcore.logging.Logger() instance
  776 
  777     In addition, sub-classes must populate the following list provided by
  778     this base class:
  779 
  780       _destinations - a list of all destination objects messages could be
  781                       handed to by this class.
  782     '''
  783 
  784     def _get_destination(self, path):
  785         p = expand_user_vars(path)
  786         if p.startswith('[') and p.endswith(']'):
  787             destsectionname = p[1:-1]
  788             if not destsectionname in self.conf['configparser'].sections():
  789                 raise getmailConfigurationError(
  790                     'destination specifies section name %s which does not exist'
  791                     % path
  792                 )
  793             # Construct destination instance
  794             self.log.debug('  getting destination for %s\n' % path)
  795             destination_type = self.conf['configparser'].get(destsectionname,
  796                                                              'type')
  797             self.log.debug('    type="%s"\n' % destination_type)
  798             destination_func = globals().get(destination_type, None)
  799             if not callable(destination_func):
  800                 raise getmailConfigurationError(
  801                     'configuration file section %s specifies incorrect '
  802                     'destination type (%s)'
  803                     % (destsectionname, destination_type)
  804                 )
  805             destination_args = {'configparser' : self.conf['configparser']}
  806             for (name, value) in self.conf['configparser'].items(destsectionname):
  807                 if name in ('type', 'configparser'):
  808                     continue
  809                 self.log.debug('    parameter %s="%s"\n' % (name, value))
  810                 destination_args[name] = value
  811             self.log.debug('    instantiating destination %s with args %s\n'
  812                            % (destination_type, destination_args))
  813             dest = destination_func(**destination_args)
  814         elif (p.startswith('/') or p.startswith('.')) and p.endswith('/'):
  815             dest = Maildir(path=p)
  816         elif (p.startswith('/') or p.startswith('.')):
  817             dest = Mboxrd(path=p)
  818         else:
  819             raise getmailConfigurationError(
  820                 'specified destination %s not of recognized type' % p
  821             )
  822         return dest
  823 
  824     def initialize(self):
  825         self.log.trace()
  826         self._destinations = []
  827 
  828     def retriever_info(self, retriever):
  829         '''Override base class to pass this to the encapsulated destinations.
  830         '''
  831         self.log.trace()
  832         DeliverySkeleton.retriever_info(self, retriever)
  833         # Pass down to all destinations
  834         for destination in self._destinations:
  835             destination.retriever_info(retriever)
  836 
  837 #######################################
  838 class MultiDestination(MultiDestinationBase):
  839     '''Send messages to one or more other destination objects unconditionally.
  840 
  841     Parameters:
  842 
  843       destinations - a tuple of strings, each specifying a destination that
  844                 messages should be delivered to.  These strings will be expanded
  845                 for leading "~/" or "~user/" and environment variables,
  846                 then interpreted as maildir/mbox/other-destination-section.
  847     '''
  848     _confitems = (
  849         ConfInstance(name='configparser', required=False),
  850         ConfTupleOfStrings(name='destinations'),
  851     )
  852 
  853     def initialize(self):
  854         self.log.trace()
  855         MultiDestinationBase.initialize(self)
  856         dests = [expand_user_vars(item) for item in self.conf['destinations']]
  857         for item in dests:
  858             try:
  859                 dest = self._get_destination(item)
  860             except getmailConfigurationError, o:
  861                 raise getmailConfigurationError('%s destination error %s'
  862                                                 % (item, o))
  863             self._destinations.append(dest)
  864         if not self._destinations:
  865             raise getmailConfigurationError('no destinations specified')
  866 
  867     def _confstring(self):
  868         '''Override the base class implementation.
  869         '''
  870         self.log.trace()
  871         confstring = ''
  872         for dest in self._destinations:
  873             if confstring:
  874                 confstring += ', '
  875             confstring += '%s' % dest
  876         return confstring
  877 
  878     def __str__(self):
  879         self.log.trace()
  880         return 'MultiDestination (%s)' % self._confstring()
  881 
  882     def showconf(self):
  883         self.log.info('MultiDestination(%s)\n' % self._confstring())
  884 
  885     def _deliver_message(self, msg, delivered_to, received):
  886         self.log.trace()
  887         for dest in self._destinations:
  888             dest.deliver_message(msg, delivered_to, received)
  889         return self
  890 
  891 #######################################
  892 class MultiSorterBase(MultiDestinationBase):
  893     '''Base class for multiple destinations with address matching.
  894     '''
  895 
  896     def initialize(self):
  897         self.log.trace()
  898         MultiDestinationBase.initialize(self)
  899         self.default = self._get_destination(self.conf['default'])
  900         self._destinations.append(self.default)
  901         self.targets = []
  902         try:
  903             _locals = self.conf['locals']
  904             # Special case for convenience if user supplied one base 2-tuple
  905             if (len(_locals) == 2 and type(_locals[0]) == str
  906                     and type(_locals[1]) == str):
  907                 _locals = (_locals, )
  908             for item in _locals:
  909                 if not (type(item) == tuple and len(item) == 2
  910                         and type(item[0]) == str and type(item[1]) == str):
  911                     raise getmailConfigurationError(
  912                         'invalid syntax for locals; see documentation'
  913                     )
  914             for (pattern, path) in _locals:
  915                 try:
  916                     dest = self._get_destination(path)
  917                 except getmailConfigurationError, o:
  918                     raise getmailConfigurationError(
  919                         'pattern %s destination error %s' % (pattern, o)
  920                     )
  921                 self.targets.append((re.compile(pattern, re.IGNORECASE), dest))
  922                 self._destinations.append(dest)
  923         except re.error, o:
  924             raise getmailConfigurationError('invalid regular expression %s' % o)
  925 
  926     def _confstring(self):
  927         '''
  928         Override the base class implementation; locals isn't readable that way.
  929         '''
  930         self.log.trace()
  931         confstring = 'default=%s' % self.default
  932         for (pattern, destination) in self.targets:
  933             confstring += ', %s->%s' % (pattern.pattern, destination)
  934         return confstring
  935 
  936 #######################################
  937 class MultiSorter(MultiSorterBase):
  938     '''Multiple destination with envelope recipient address matching.
  939 
  940     Parameters:
  941 
  942       default - the default destination.  Messages not matching any
  943                 "local" patterns (see below) will be delivered here.
  944 
  945       locals - an optional tuple of items, each being a 2-tuple of quoted
  946             strings. Each quoted string pair is a regular expression and a
  947             maildir/mbox/other destination. In the general case, an email
  948             address is a valid regular expression. Each pair is on a separate
  949             line; the second and subsequent lines need to have leading
  950             whitespace to be considered a continuation of the "locals"
  951             configuration.  If the recipient address matches a given pattern, it
  952             will be delivered to the corresponding destination.  A destination
  953             is assumed to be a maildir if it starts with a dot or slash and ends
  954             with a slash. A destination is assumed to be an mboxrd file if it
  955             starts with a dot or a slash and does not end with a slash.  A
  956             destination may also be specified by section name, i.e.
  957             "[othersectionname]". Multiple patterns may match a given recipient
  958             address; the message will be delivered to /all/ destinations with
  959             matching patterns.  Patterns are matched case-insensitively.
  960 
  961             example:
  962 
  963              default = /home/kellyw/Mail/postmaster/
  964              locals = (
  965                ("jason@example.org", "/home/jasonk/Maildir/"),
  966                ("sales@example.org", "/home/karlyk/Mail/sales"),
  967                ("abuse@(example.org|example.net)", "/home/kellyw/Mail/abuse/"),
  968                ("^(jeff|jefferey)(\.s(mith)?)?@.*$", "[jeff-mail-delivery]"),
  969                ("^.*@(mail.)?rapinder.example.org$", "/home/rapinder/Maildir/")
  970                )
  971 
  972                In it's simplest form, locals is merely a list of pairs of email
  973                addresses and corresponding maildir/mbox paths.  Don't worry
  974                about the details of regular expressions if you aren't familiar
  975                with them.
  976     '''
  977     _confitems = (
  978         ConfInstance(name='configparser', required=False),
  979         ConfString(name='default'),
  980         ConfTupleOfTupleOfStrings(name='locals', required=False, default="()"),
  981     )
  982 
  983     def __str__(self):
  984         self.log.trace()
  985         return 'MultiSorter (%s)' % self._confstring()
  986 
  987     def showconf(self):
  988         self.log.info('MultiSorter(%s)\n' % self._confstring())
  989 
  990     def _deliver_message(self, msg, delivered_to, received):
  991         self.log.trace()
  992         matched = []
  993         if msg.recipient == None and self.targets:
  994             raise getmailConfigurationError(
  995                 'MultiSorter recipient matching requires a retriever (message '
  996                 'source) that preserves the message envelope'
  997             )
  998         for (pattern, dest) in self.targets:
  999             self.log.debug('checking recipient %s against pattern %s\n'
 1000                            % (msg.recipient, pattern.pattern))
 1001             if pattern.search(msg.recipient):
 1002                 self.log.debug('recipient %s matched target %s\n'
 1003                                % (msg.recipient, dest))
 1004                 dest.deliver_message(msg, delivered_to, received)
 1005                 matched.append(str(dest))
 1006         if not matched:
 1007             if self.targets:
 1008                 self.log.debug('recipient %s not matched; using default %s\n'
 1009                                % (msg.recipient, self.default))
 1010             else:
 1011                 self.log.debug('using default %s\n' % self.default)
 1012             return 'MultiSorter (default %s)' % self.default.deliver_message(
 1013                 msg, delivered_to, received
 1014             )
 1015         return 'MultiSorter (%s)' % matched
 1016 
 1017 #######################################
 1018 class MultiGuesser(MultiSorterBase):
 1019     '''Multiple destination with header field address matching.
 1020 
 1021     Parameters:
 1022 
 1023       default - see MultiSorter for definition.
 1024 
 1025       locals - see MultiSorter for definition.
 1026 
 1027     '''
 1028     _confitems = (
 1029         ConfInstance(name='configparser', required=False),
 1030         ConfString(name='default'),
 1031         ConfTupleOfTupleOfStrings(name='locals', required=False, default="()"),
 1032     )
 1033 
 1034     def __str__(self):
 1035         self.log.trace()
 1036         return 'MultiGuesser (%s)' % self._confstring()
 1037 
 1038     def showconf(self):
 1039         self.log.info('MultiGuesser(%s)\n' % self._confstring())
 1040 
 1041     def _deliver_message(self, msg, delivered_to, received):
 1042         self.log.trace()
 1043         matched = []
 1044         header_addrs = []
 1045         fieldnames = (
 1046             ('delivered-to', ),
 1047             ('envelope-to', ),
 1048             ('x-envelope-to', ),
 1049             ('apparently-to', ),
 1050             ('resent-to', 'resent-cc', 'resent-bcc'),
 1051             ('to', 'cc', 'bcc'),
 1052         )
 1053         for fields in fieldnames:
 1054             for field in fields:
 1055                 self.log.debug(
 1056                     'looking for addresses in %s header fields\n' % field
 1057                 )
 1058                 header_addrs.extend(
 1059                     [addr for (name, addr) in email.Utils.getaddresses(
 1060                         msg.get_all(field, [])
 1061                      ) if addr]
 1062                 )
 1063             if header_addrs:
 1064                 # Got some addresses, quit here
 1065                 self.log.debug('found total of %d addresses (%s)\n'
 1066                                % (len(header_addrs), header_addrs))
 1067                 break
 1068             else:
 1069                 self.log.debug('no addresses found, continuing\n')
 1070 
 1071         for (pattern, dest) in self.targets:
 1072             for addr in header_addrs:
 1073                 self.log.debug('checking address %s against pattern %s\n'
 1074                                % (addr, pattern.pattern))
 1075                 if pattern.search(addr):
 1076                     self.log.debug('address %s matched target %s\n'
 1077                                    % (addr, dest))
 1078                     dest.deliver_message(msg, delivered_to, received)
 1079                     matched.append(str(dest))
 1080                     # Only deliver once to each destination; this one matched,
 1081                     # so we don't need to check any remaining addresses against
 1082                     # this pattern
 1083                     break
 1084         if not matched:
 1085             if self.targets:
 1086                 self.log.debug('no addresses matched; using default %s\n'
 1087                                % self.default)
 1088             else:
 1089                 self.log.debug('using default %s\n' % self.default)
 1090             return 'MultiGuesser (default %s)' % self.default.deliver_message(
 1091                 msg, delivered_to, received
 1092             )
 1093         return 'MultiGuesser (%s)' % matched