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 |