Viewing file: mailmail.py (10.33 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# -*- 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)
|