| OLD | NEW |
| (Empty) |
| 1 | |
| 2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
| 3 # See LICENSE for details. | |
| 4 | |
| 5 # | |
| 6 | |
| 7 """ | |
| 8 Implementation module for the `newtexaco` command. | |
| 9 | |
| 10 The name is preliminary and subject to change. | |
| 11 """ | |
| 12 | |
| 13 import os | |
| 14 import sys | |
| 15 import rfc822 | |
| 16 import socket | |
| 17 import getpass | |
| 18 from ConfigParser import ConfigParser | |
| 19 | |
| 20 try: | |
| 21 import cStringIO as StringIO | |
| 22 except: | |
| 23 import StringIO | |
| 24 | |
| 25 from twisted.internet import reactor | |
| 26 from twisted.mail import bounce, smtp | |
| 27 | |
| 28 GLOBAL_CFG = "/etc/mailmail" | |
| 29 LOCAL_CFG = os.path.expanduser("~/.twisted/mailmail") | |
| 30 SMARTHOST = '127.0.0.1' | |
| 31 | |
| 32 ERROR_FMT = """\ | |
| 33 Subject: Failed Message Delivery | |
| 34 | |
| 35 Message delivery failed. The following occurred: | |
| 36 | |
| 37 %s | |
| 38 -- | |
| 39 The Twisted sendmail application. | |
| 40 """ | |
| 41 | |
| 42 def log(message, *args): | |
| 43 sys.stderr.write(str(message) % args + '\n') | |
| 44 | |
| 45 class Options: | |
| 46 """ | |
| 47 @type to: C{list} of C{str} | |
| 48 @ivar to: The addresses to which to deliver this message. | |
| 49 | |
| 50 @type sender: C{str} | |
| 51 @ivar sender: The address from which this message is being sent. | |
| 52 | |
| 53 @type body: C{file} | |
| 54 @ivar body: The object from which the message is to be read. | |
| 55 """ | |
| 56 | |
| 57 def getlogin(): | |
| 58 try: | |
| 59 return os.getlogin() | |
| 60 except: | |
| 61 return getpass.getuser() | |
| 62 | |
| 63 | |
| 64 def parseOptions(argv): | |
| 65 o = Options() | |
| 66 o.to = [e for e in argv if not e.startswith('-')] | |
| 67 o.sender = getlogin() | |
| 68 | |
| 69 # Just be very stupid | |
| 70 | |
| 71 # Skip -bm -- it is the default | |
| 72 | |
| 73 # -bp lists queue information. Screw that. | |
| 74 if '-bp' in argv: | |
| 75 raise ValueError, "Unsupported option" | |
| 76 | |
| 77 # -bs makes sendmail use stdin/stdout as its transport. Screw that. | |
| 78 if '-bs' in argv: | |
| 79 raise ValueError, "Unsupported option" | |
| 80 | |
| 81 # -F sets who the mail is from, but is overridable by the From header | |
| 82 if '-F' in argv: | |
| 83 o.sender = argv[argv.index('-F') + 1] | |
| 84 o.to.remove(o.sender) | |
| 85 | |
| 86 # -i and -oi makes us ignore lone "." | |
| 87 if ('-i' in argv) or ('-oi' in argv): | |
| 88 raise ValueError, "Unsupported option" | |
| 89 | |
| 90 # -odb is background delivery | |
| 91 if '-odb' in argv: | |
| 92 o.background = True | |
| 93 else: | |
| 94 o.background = False | |
| 95 | |
| 96 # -odf is foreground delivery | |
| 97 if '-odf' in argv: | |
| 98 o.background = False | |
| 99 else: | |
| 100 o.background = True | |
| 101 | |
| 102 # -oem and -em cause errors to be mailed back to the sender. | |
| 103 # It is also the default. | |
| 104 | |
| 105 # -oep and -ep cause errors to be printed to stderr | |
| 106 if ('-oep' in argv) or ('-ep' in argv): | |
| 107 o.printErrors = True | |
| 108 else: | |
| 109 o.printErrors = False | |
| 110 | |
| 111 # -om causes a copy of the message to be sent to the sender if the sender | |
| 112 # appears in an alias expansion. We do not support aliases. | |
| 113 if '-om' in argv: | |
| 114 raise ValueError, "Unsupported option" | |
| 115 | |
| 116 # -t causes us to pick the recipients of the message from the To, Cc, and Bc
c | |
| 117 # headers, and to remove the Bcc header if present. | |
| 118 if '-t' in argv: | |
| 119 o.recipientsFromHeaders = True | |
| 120 o.excludeAddresses = o.to | |
| 121 o.to = [] | |
| 122 else: | |
| 123 o.recipientsFromHeaders = False | |
| 124 o.exludeAddresses = [] | |
| 125 | |
| 126 requiredHeaders = { | |
| 127 'from': [], | |
| 128 'to': [], | |
| 129 'cc': [], | |
| 130 'bcc': [], | |
| 131 'date': [], | |
| 132 } | |
| 133 | |
| 134 headers = [] | |
| 135 buffer = StringIO.StringIO() | |
| 136 while 1: | |
| 137 write = 1 | |
| 138 line = sys.stdin.readline() | |
| 139 if not line.strip(): | |
| 140 break | |
| 141 | |
| 142 hdrs = line.split(': ', 1) | |
| 143 | |
| 144 hdr = hdrs[0].lower() | |
| 145 if o.recipientsFromHeaders and hdr in ('to', 'cc', 'bcc'): | |
| 146 o.to.extend([ | |
| 147 a[1] for a in rfc822.AddressList(hdrs[1]).addresslist | |
| 148 ]) | |
| 149 if hdr == 'bcc': | |
| 150 write = 0 | |
| 151 elif hdr == 'from': | |
| 152 o.sender = rfc822.parseaddr(hdrs[1])[1] | |
| 153 | |
| 154 if hdr in requiredHeaders: | |
| 155 requiredHeaders[hdr].append(hdrs[1]) | |
| 156 | |
| 157 if write: | |
| 158 buffer.write(line) | |
| 159 | |
| 160 if not requiredHeaders['from']: | |
| 161 buffer.write('From: %s\r\n' % (o.sender,)) | |
| 162 if not requiredHeaders['to']: | |
| 163 if not o.to: | |
| 164 raise ValueError, "No recipients specified" | |
| 165 buffer.write('To: %s\r\n' % (', '.join(o.to),)) | |
| 166 if not requiredHeaders['date']: | |
| 167 buffer.write('Date: %s\r\n' % (smtp.rfc822date(),)) | |
| 168 | |
| 169 buffer.write(line) | |
| 170 | |
| 171 if o.recipientsFromHeaders: | |
| 172 for a in o.excludeAddresses: | |
| 173 try: | |
| 174 o.to.remove(a) | |
| 175 except: | |
| 176 pass | |
| 177 | |
| 178 buffer.seek(0, 0) | |
| 179 o.body = StringIO.StringIO(buffer.getvalue() + sys.stdin.read()) | |
| 180 return o | |
| 181 | |
| 182 class Configuration: | |
| 183 """ | |
| 184 @ivar allowUIDs: A list of UIDs which are allowed to send mail. | |
| 185 @ivar allowGIDs: A list of GIDs which are allowed to send mail. | |
| 186 @ivar denyUIDs: A list of UIDs which are not allowed to send mail. | |
| 187 @ivar denyGIDs: A list of GIDs which are not allowed to send mail. | |
| 188 | |
| 189 @type defaultAccess: C{bool} | |
| 190 @ivar defaultAccess: C{True} if access will be allowed when no other access | |
| 191 control rule matches or C{False} if it will be denied in that case. | |
| 192 | |
| 193 @ivar useraccess: Either C{'allow'} to check C{allowUID} first | |
| 194 or C{'deny'} to check C{denyUID} first. | |
| 195 | |
| 196 @ivar groupaccess: Either C{'allow'} to check C{allowGID} first or | |
| 197 C{'deny'} to check C{denyGID} first. | |
| 198 | |
| 199 @ivar identities: A C{dict} mapping hostnames to credentials to use when | |
| 200 sending mail to that host. | |
| 201 | |
| 202 @ivar smarthost: C{None} or a hostname through which all outgoing mail will | |
| 203 be sent. | |
| 204 | |
| 205 @ivar domain: C{None} or the hostname with which to identify ourselves when | |
| 206 connecting to an MTA. | |
| 207 """ | |
| 208 def __init__(self): | |
| 209 self.allowUIDs = [] | |
| 210 self.denyUIDs = [] | |
| 211 self.allowGIDs = [] | |
| 212 self.denyGIDs = [] | |
| 213 self.useraccess = 'deny' | |
| 214 self.groupaccess= 'deny' | |
| 215 | |
| 216 self.identities = {} | |
| 217 self.smarthost = None | |
| 218 self.domain = None | |
| 219 | |
| 220 self.defaultAccess = True | |
| 221 | |
| 222 | |
| 223 def loadConfig(path): | |
| 224 # [useraccess] | |
| 225 # allow=uid1,uid2,... | |
| 226 # deny=uid1,uid2,... | |
| 227 # order=allow,deny | |
| 228 # [groupaccess] | |
| 229 # allow=gid1,gid2,... | |
| 230 # deny=gid1,gid2,... | |
| 231 # order=deny,allow | |
| 232 # [identity] | |
| 233 # host1=username:password | |
| 234 # host2=username:password | |
| 235 # [addresses] | |
| 236 # smarthost=a.b.c.d | |
| 237 # default_domain=x.y.z | |
| 238 | |
| 239 c = Configuration() | |
| 240 | |
| 241 if not os.access(path, os.R_OK): | |
| 242 return c | |
| 243 | |
| 244 p = ConfigParser() | |
| 245 p.read(path) | |
| 246 | |
| 247 au = c.allowUIDs | |
| 248 du = c.denyUIDs | |
| 249 ag = c.allowGIDs | |
| 250 dg = c.denyGIDs | |
| 251 for (section, a, d) in (('useraccess', au, du), ('groupaccess', ag, dg)): | |
| 252 if p.has_section(section): | |
| 253 for (mode, L) in (('allow', a), ('deny', d)): | |
| 254 if p.has_option(section, mode) and p.get(section, mode): | |
| 255 for id in p.get(section, mode).split(','): | |
| 256 try: | |
| 257 id = int(id) | |
| 258 except ValueError: | |
| 259 log("Illegal %sID in [%s] section: %s", section[0].u
pper(), section, id) | |
| 260 else: | |
| 261 L.append(id) | |
| 262 order = p.get(section, 'order') | |
| 263 order = map(str.split, map(str.lower, order.split(','))) | |
| 264 if order[0] == 'allow': | |
| 265 setattr(c, section, 'allow') | |
| 266 else: | |
| 267 setattr(c, section, 'deny') | |
| 268 | |
| 269 if p.has_section('identity'): | |
| 270 for (host, up) in p.items('identity'): | |
| 271 parts = up.split(':', 1) | |
| 272 if len(parts) != 2: | |
| 273 log("Illegal entry in [identity] section: %s", up) | |
| 274 continue | |
| 275 p.identities[host] = parts | |
| 276 | |
| 277 if p.has_section('addresses'): | |
| 278 if p.has_option('addresses', 'smarthost'): | |
| 279 c.smarthost = p.get('addresses', 'smarthost') | |
| 280 if p.has_option('addresses', 'default_domain'): | |
| 281 c.domain = p.get('addresses', 'default_domain') | |
| 282 | |
| 283 return c | |
| 284 | |
| 285 def success(result): | |
| 286 reactor.stop() | |
| 287 | |
| 288 failed = None | |
| 289 def failure(f): | |
| 290 global failed | |
| 291 reactor.stop() | |
| 292 failed = f | |
| 293 | |
| 294 def sendmail(host, options, ident): | |
| 295 d = smtp.sendmail(host, options.sender, options.to, options.body) | |
| 296 d.addCallbacks(success, failure) | |
| 297 reactor.run() | |
| 298 | |
| 299 def senderror(failure, options): | |
| 300 recipient = [options.sender] | |
| 301 sender = '"Internally Generated Message (%s)"<postmaster@%s>' % (sys.argv[0]
, smtp.DNSNAME) | |
| 302 error = StringIO.StringIO() | |
| 303 failure.printTraceback(file=error) | |
| 304 body = StringIO.StringIO(ERROR_FMT % error.getvalue()) | |
| 305 | |
| 306 d = smtp.sendmail('localhost', sender, recipient, body) | |
| 307 d.addBoth(lambda _: reactor.stop()) | |
| 308 | |
| 309 def deny(conf): | |
| 310 uid = os.getuid() | |
| 311 gid = os.getgid() | |
| 312 | |
| 313 if conf.useraccess == 'deny': | |
| 314 if uid in conf.denyUIDs: | |
| 315 return True | |
| 316 if uid in conf.allowUIDs: | |
| 317 return False | |
| 318 else: | |
| 319 if uid in conf.allowUIDs: | |
| 320 return False | |
| 321 if uid in conf.denyUIDs: | |
| 322 return True | |
| 323 | |
| 324 if conf.groupaccess == 'deny': | |
| 325 if gid in conf.denyGIDs: | |
| 326 return True | |
| 327 if gid in conf.allowGIDs: | |
| 328 return False | |
| 329 else: | |
| 330 if gid in conf.allowGIDs: | |
| 331 return False | |
| 332 if gid in conf.denyGIDs: | |
| 333 return True | |
| 334 | |
| 335 return not conf.defaultAccess | |
| 336 | |
| 337 def run(): | |
| 338 o = parseOptions(sys.argv[1:]) | |
| 339 gConf = loadConfig(GLOBAL_CFG) | |
| 340 lConf = loadConfig(LOCAL_CFG) | |
| 341 | |
| 342 if deny(gConf) or deny(lConf): | |
| 343 log("Permission denied") | |
| 344 return | |
| 345 | |
| 346 host = lConf.smarthost or gConf.smarthost or SMARTHOST | |
| 347 | |
| 348 ident = gConf.identities.copy() | |
| 349 ident.update(lConf.identities) | |
| 350 | |
| 351 if lConf.domain: | |
| 352 smtp.DNSNAME = lConf.domain | |
| 353 elif gConf.domain: | |
| 354 smtp.DNSNAME = gConf.domain | |
| 355 | |
| 356 sendmail(host, o, ident) | |
| 357 | |
| 358 if failed: | |
| 359 if o.printErrors: | |
| 360 failed.printTraceback(file=sys.stderr) | |
| 361 raise SystemExit(1) | |
| 362 else: | |
| 363 senderror(failed, o) | |
| OLD | NEW |