# -*- 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 ({})"'.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)