|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- # -*- test-case-name: twisted.mail.test.test_mailmail -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
-
- """
- Implementation module for the I{mailmail} command.
- """
-
- from __future__ import print_function
-
- import email.utils
- import os
- import sys
- import getpass
- try:
- # Python 3
- from configparser import ConfigParser
- except ImportError:
- # Python 2
- from ConfigParser import ConfigParser
-
- from twisted.copyright import version
- from twisted.internet import reactor
- from twisted.logger import Logger, textFileLogObserver
- from twisted.mail import smtp
- from twisted.python.compat import NativeStringIO
-
- GLOBAL_CFG = "/etc/mailmail"
- LOCAL_CFG = os.path.expanduser("~/.twisted/mailmail")
- SMARTHOST = '127.0.0.1'
-
- ERROR_FMT = """\
- Subject: Failed Message Delivery
-
- Message delivery failed. The following occurred:
-
- %s
- --
- The Twisted sendmail application.
- """
-
- _logObserver = textFileLogObserver(sys.stderr)
- _log = Logger(observer=_logObserver)
-
-
-
- class Options:
- """
- Store the values of the parsed command-line options to the I{mailmail}
- script.
-
- @type to: L{list} of L{str}
- @ivar to: The addresses to which to deliver this message.
-
- @type sender: L{str}
- @ivar sender: The address from which this message is being sent.
-
- @type body: C{file}
- @ivar body: The object from which the message is to be read.
- """
-
-
-
- def getlogin():
- try:
- return os.getlogin()
- except:
- return getpass.getuser()
-
-
- _unsupportedOption = SystemExit("Unsupported option.")
-
-
-
- def parseOptions(argv):
- o = Options()
- o.to = [e for e in argv if not e.startswith('-')]
- o.sender = getlogin()
-
- # Just be very stupid
-
- # Skip -bm -- it is the default
-
- # Add a non-standard option for querying the version of this tool.
- if '--version' in argv:
- print('mailmail version:', version)
- raise SystemExit()
-
- # -bp lists queue information. Screw that.
- if '-bp' in argv:
- raise _unsupportedOption
-
- # -bs makes sendmail use stdin/stdout as its transport. Screw that.
- if '-bs' in argv:
- raise _unsupportedOption
-
- # -F sets who the mail is from, but is overridable by the From header
- if '-F' in argv:
- o.sender = argv[argv.index('-F') + 1]
- o.to.remove(o.sender)
-
- # -i and -oi makes us ignore lone "."
- if ('-i' in argv) or ('-oi' in argv):
- raise _unsupportedOption
-
- # -odb is background delivery
- if '-odb' in argv:
- o.background = True
- else:
- o.background = False
-
- # -odf is foreground delivery
- if '-odf' in argv:
- o.background = False
- else:
- o.background = True
-
- # -oem and -em cause errors to be mailed back to the sender.
- # It is also the default.
-
- # -oep and -ep cause errors to be printed to stderr
- if ('-oep' in argv) or ('-ep' in argv):
- o.printErrors = True
- else:
- o.printErrors = False
-
- # -om causes a copy of the message to be sent to the sender if the sender
- # appears in an alias expansion. We do not support aliases.
- if '-om' in argv:
- raise _unsupportedOption
-
- # -t causes us to pick the recipients of the message from
- # the To, Cc, and Bcc headers, and to remove the Bcc header
- # if present.
- if '-t' in argv:
- o.recipientsFromHeaders = True
- o.excludeAddresses = o.to
- o.to = []
- else:
- o.recipientsFromHeaders = False
- o.exludeAddresses = []
-
- requiredHeaders = {
- 'from': [],
- 'to': [],
- 'cc': [],
- 'bcc': [],
- 'date': [],
- }
-
- buffer = NativeStringIO()
- while 1:
- write = 1
- line = sys.stdin.readline()
- if not line.strip():
- break
-
- hdrs = line.split(': ', 1)
-
- hdr = hdrs[0].lower()
- if o.recipientsFromHeaders and hdr in ('to', 'cc', 'bcc'):
- o.to.extend([
- email.utils.parseaddr(hdrs[1])[1]
- ])
- if hdr == 'bcc':
- write = 0
- elif hdr == 'from':
- o.sender = email.utils.parseaddr(hdrs[1])[1]
-
- if hdr in requiredHeaders:
- requiredHeaders[hdr].append(hdrs[1])
-
- if write:
- buffer.write(line)
-
- if not requiredHeaders['from']:
- buffer.write('From: {}\r\n'.format(o.sender))
- if not requiredHeaders['to']:
- if not o.to:
- raise SystemExit("No recipients specified.")
- buffer.write('To: {}\r\n'.format(', '.join(o.to)))
- if not requiredHeaders['date']:
- buffer.write('Date: {}\r\n'.format(smtp.rfc822date()))
-
- buffer.write(line)
-
- if o.recipientsFromHeaders:
- for a in o.excludeAddresses:
- try:
- o.to.remove(a)
- except:
- pass
-
- buffer.seek(0, 0)
- o.body = NativeStringIO(buffer.getvalue() + sys.stdin.read())
- return o
-
-
-
- class Configuration:
- """
-
- @ivar allowUIDs: A list of UIDs which are allowed to send mail.
- @ivar allowGIDs: A list of GIDs which are allowed to send mail.
- @ivar denyUIDs: A list of UIDs which are not allowed to send mail.
- @ivar denyGIDs: A list of GIDs which are not allowed to send mail.
-
- @type defaultAccess: L{bool}
- @ivar defaultAccess: L{True} if access will be allowed when no other access
- control rule matches or L{False} if it will be denied in that case.
-
- @ivar useraccess: Either C{'allow'} to check C{allowUID} first
- or C{'deny'} to check C{denyUID} first.
-
- @ivar groupaccess: Either C{'allow'} to check C{allowGID} first or
- C{'deny'} to check C{denyGID} first.
-
- @ivar identities: A L{dict} mapping hostnames to credentials to use when
- sending mail to that host.
-
- @ivar smarthost: L{None} or a hostname through which all outgoing mail will
- be sent.
-
- @ivar domain: L{None} or the hostname with which to identify ourselves when
- connecting to an MTA.
- """
- def __init__(self):
- self.allowUIDs = []
- self.denyUIDs = []
- self.allowGIDs = []
- self.denyGIDs = []
- self.useraccess = 'deny'
- self.groupaccess = 'deny'
-
- self.identities = {}
- self.smarthost = None
- self.domain = None
-
- self.defaultAccess = True
-
-
-
- def loadConfig(path):
- # [useraccess]
- # allow=uid1,uid2,...
- # deny=uid1,uid2,...
- # order=allow,deny
- # [groupaccess]
- # allow=gid1,gid2,...
- # deny=gid1,gid2,...
- # order=deny,allow
- # [identity]
- # host1=username:password
- # host2=username:password
- # [addresses]
- # smarthost=a.b.c.d
- # default_domain=x.y.z
-
- c = Configuration()
-
- if not os.access(path, os.R_OK):
- return c
-
- p = ConfigParser()
- p.read(path)
-
- au = c.allowUIDs
- du = c.denyUIDs
- ag = c.allowGIDs
- dg = c.denyGIDs
- for (section, a, d) in (('useraccess', au, du), ('groupaccess', ag, dg)):
- if p.has_section(section):
- for (mode, L) in (('allow', a), ('deny', d)):
- if p.has_option(section, mode) and p.get(section, mode):
- for sectionID in p.get(section, mode).split(','):
- try:
- sectionID = int(sectionID)
- except ValueError:
- _log.error(
- "Illegal {prefix}ID in "
- "[{section}] section: {sectionID}",
- prefix=section[0].upper(),
- section=section, sectionID=sectionID)
- else:
- L.append(sectionID)
- order = p.get(section, 'order')
- order = [s.split()
- for s in [s.lower()
- for s in order.split(',')]]
- if order[0] == 'allow':
- setattr(c, section, 'allow')
- else:
- setattr(c, section, 'deny')
-
- if p.has_section('identity'):
- for (host, up) in p.items('identity'):
- parts = up.split(':', 1)
- if len(parts) != 2:
- _log.error("Illegal entry in [identity] section: {section}",
- section=up)
- continue
- c.identities[host] = parts
-
- if p.has_section('addresses'):
- if p.has_option('addresses', 'smarthost'):
- c.smarthost = p.get('addresses', 'smarthost')
- if p.has_option('addresses', 'default_domain'):
- c.domain = p.get('addresses', 'default_domain')
-
- return c
-
-
-
- def success(result):
- reactor.stop()
-
-
-
- failed = None
- def failure(f):
- global failed
- reactor.stop()
- failed = f
-
-
-
- def sendmail(host, options, ident):
- d = smtp.sendmail(host, options.sender, options.to, options.body)
- d.addCallbacks(success, failure)
- reactor.run()
-
-
-
- def senderror(failure, options):
- recipient = [options.sender]
- sender = '"Internally Generated Message ({})"<postmaster@{}>'.format(
- sys.argv[0], smtp.DNSNAME.decode("ascii"))
- error = NativeStringIO()
- failure.printTraceback(file=error)
- body = NativeStringIO(ERROR_FMT % error.getvalue())
- d = smtp.sendmail('localhost', sender, recipient, body)
- d.addBoth(lambda _: reactor.stop())
-
-
-
- def deny(conf):
- uid = os.getuid()
- gid = os.getgid()
-
- if conf.useraccess == 'deny':
- if uid in conf.denyUIDs:
- return True
- if uid in conf.allowUIDs:
- return False
- else:
- if uid in conf.allowUIDs:
- return False
- if uid in conf.denyUIDs:
- return True
-
- if conf.groupaccess == 'deny':
- if gid in conf.denyGIDs:
- return True
- if gid in conf.allowGIDs:
- return False
- else:
- if gid in conf.allowGIDs:
- return False
- if gid in conf.denyGIDs:
- return True
-
- return not conf.defaultAccess
-
-
-
- def run():
- o = parseOptions(sys.argv[1:])
- gConf = loadConfig(GLOBAL_CFG)
- lConf = loadConfig(LOCAL_CFG)
-
- if deny(gConf) or deny(lConf):
- _log.error("Permission denied")
- return
-
- host = lConf.smarthost or gConf.smarthost or SMARTHOST
-
- ident = gConf.identities.copy()
- ident.update(lConf.identities)
-
- if lConf.domain:
- smtp.DNSNAME = lConf.domain
- elif gConf.domain:
- smtp.DNSNAME = gConf.domain
-
- sendmail(host, o, ident)
-
- if failed:
- if o.printErrors:
- failed.printTraceback(file=sys.stderr)
- raise SystemExit(1)
- else:
- senderror(failed, o)
|