# Twisted, the Framework of Your Internet
# Copyright (C) 2001 Matthew W. Lefkowitz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Implementation module for the `newtexaco` command.
The name is preliminary and subject to change.
"""
import os
import sys
import rfc822
import socket
import getpass
from ConfigParser import ConfigParser
try:
import cStringIO as StringIO
except:
import StringIO
from twisted.internet import reactor
from twisted.protocols import smtp
from twisted.mail import bounce
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.
"""
def log(message, *args):
sys.stderr.write(str(message) % args + '\n')
class Options:
"""
@type to: C{list} of C{str}
@ivar to: The addresses to which to deliver this message.
@type sender: C{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()
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
# -bp lists queue information. Screw that.
if '-bp' in argv:
raise ValueError, "Unsupported option"
# -bs makes sendmail use stdin/stdout as its transport. Screw that.
if '-bs' in argv:
raise ValueError, "Unsupported option"
# -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 ValueError, "Unsupported option"
# -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 ValueError, "Unsupported option"
# -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': [],
}
headers = []
buffer = StringIO.StringIO()
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([
a[1] for a in rfc822.AddressList(hdrs[1]).addresslist
])
if hdr == 'bcc':
write = 0
elif hdr == 'from':
o.sender = rfc822.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: %s\r\n' % (o.sender,))
if not requiredHeaders['to']:
if not o.to:
raise ValueError, "No recipients specified"
buffer.write('To: %s\r\n' % (', '.join(o.to),))
if not requiredHeaders['date']:
buffer.write('Date: %s\r\n' % (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 = StringIO.StringIO(buffer.getvalue() + sys.stdin.read())
return o
class Configuration:
"""
@ivar allowUIDs
@ivar allowGIDs
@ivar denyUIDs
@ivar denyGIDs
@ivar useraccess
@ivar groupaccess
@ivar identities
@ivar smarthost
@ivar domain
@ivar defaultAccess
"""
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 id in p.get(section, mode).split(','):
try:
id = int(id)
except ValueError:
log("Illegal %sID in [%s] section: %s", section[0].upper(), section, id)
else:
L.append(id)
order = p.get(section, 'order')
order = map(str.split, map(str.lower, 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("Illegal entry in [identity] section: %s", up)
continue
p.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):
from twisted.protocols.smtp import sendmail
d = 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 (%s)"<postmaster@%s>' % (sys.argv[0], smtp.DNSNAME)
error = StringIO.StringIO()
failure.printTraceback(file=error)
body = StringIO.StringIO(ERROR_FMT % error.getvalue())
from twisted.protocols.smtp import sendmail
d = 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("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)
syntax highlighted by Code2HTML, v. 0.9.1