chiark / gitweb /
mail: Send mail with the correct envelope sender.
[newsgate] / bin / inject
1 #! /usr/bin/python
2
3 import sre as RX
4 import os as OS
5 import time as T
6 import socket as S
7 from getopt import getopt, GetoptError
8 from sys import stdin, stdout, stderr, argv, exit
9 from cStringIO import StringIO
10 env = OS.environ
11
12 prog = argv[0]
13
14 def bad(msg):
15   print >>stderr, '%s (fatal): %s' % (prog, msg)
16   exit(100)
17 def die(msg):
18   print >>stderr, '%s: %s' % (prog, msg)
19   exit(111)
20 def usage():
21   print >>stderr, \
22     ('Usage: %s [-d DIST] [-h HOST] [-r REMOTE] [-p PATH] GROUP <MESSAGE' %
23      prog)
24   exit(111)
25
26 def headers(file):
27   h = None
28   while True:
29     line = file.next()
30     if line == '' or line == '\n':
31       break
32     if line[0].isspace():
33       if h is None:
34         bad('unexpected continuation')
35       h += line
36     else:
37       if h: yield h
38       h = line
39   if h: yield h
40
41 def hdrsplit(h):
42   v = h.split(':', 1)
43   if len(v) != 2:
44     bad('failed to parse header')
45   return v[0].strip().lower(), v[1].strip()
46
47 remote = ('localhost', 119)
48 approved = None
49 try:
50   host = OS.popen('hostname -f').read().strip()
51 except:
52   host = 'localhost'
53 dist = 'mail'
54 path = 'newsgate'
55 sender = env.get('SENDER')
56 recip = env.get('RECIPIENT')
57 group = None
58
59 def opts():
60   global approved, remote, host, dist, path, group, sender, recip
61   try:
62     opts, args = getopt(argv[1:], 'a:d:h:r:p:S:R:',
63                         ['approved=', 'distribution=',
64                          'sender=', 'recipient=',
65                          'hostname=', 'remote=', 'path='])
66   except GetoptError:
67     usage()
68   for o, a in opts:
69     if o in ('-a', '--approved'):
70       approved = a
71     elif o in ('-d', '--distribution'):
72       dist = a
73     elif o in ('-h', '--hostname'):
74       host = a
75     elif o in ('-r', '--remote'):
76       remote = (lambda addr, port = 119: (addr, int(port)))(*a.split(':'))
77     elif o in ('-R', '--recipient'):
78       recip = a
79     elif o in ('-S', '--sender'):
80       sender = a
81   if len(args) != 1:
82     usage()
83   group, = args
84
85 rx_msgid = RX.compile(r'^\<\S+@\S+\>$')
86
87 class NNTP (object):
88   def __init__(me, addr):
89     me.sk = S.socket(S.AF_INET, S.SOCK_STREAM)
90     me.sk.connect(remote)
91     me.f = me.sk.makefile()
92     rc, msg = me.reply()
93     if rc != '200':
94       die('unable to contact server: %s %s' % (rc, msg))
95   def write(me, stuff):
96     me.f.write(stuff)
97   def flush(me):
98     me.f.flush()
99   def cmd(me, stuff):
100     me.f.write(stuff + '\r\n')
101     me.f.flush()
102   def reply(me):
103     rc, msg = (lambda rc, msg = '.': (rc, msg.strip())) \
104                 (*me.f.readline().split(None, 1))
105     if rc.startswith('5'):
106       die('server hated me: %s %s' % (rc, msg))
107     return rc, msg.strip()
108
109 def send():
110   hdr = StringIO()
111   body = StringIO()
112   hdr.write('Path: newsgate\r\n'
113             'Distribution: mail\r\n'
114             'Newsgroups: %s\r\n'
115             % group)
116   if approved: hdr.write('Approved: %s\r\n' % approved)
117   if sender: hdr.write('Return-Path: <%s>\r\n' % sender)
118   if recip: hdr.write('Delivered-To: %s\r\n' % recip)
119   xify = {}
120   for h in '''
121     lines xref newsgroups path distribution approved received
122   '''.split():
123     xify[h] = 1
124   seen = {}
125   for h in headers(stdin):
126     n, c = hdrsplit(h)
127     if n in xify:
128       h = 'X-Newsgate-' + h
129     elif h.startswith('.'):
130       h = '.' + h
131     seen[n] = c
132     if h.endswith('\r\n'):
133       pass
134     elif h.endswith('\n'):
135       h = h[:-1] + '\r\n'
136     else:
137       h += '\r\n'
138     hdr.write(h)
139   if 'message-id' not in seen:
140     seen['message-id'] = ('<newsgate-%s@%s>'
141                           % (OS.popen('gorp 128').read().strip(),
142                              host))
143     hdr.write('Message-ID: %s\r\n' % seen['message-id'])
144   if 'date' not in seen:
145     hdr.write('Date: %s\r\n'
146               % (T.strftime('%a, %d %b %Y %H:%M:%S %Z')))
147   if 'subject' not in seen:
148     hdr.write('Subject: (no subject)\r\n')
149
150   msgid = seen['message-id']
151   if not rx_msgid.match(msgid):
152     bad('invalid message-id %s' % msgid)
153
154   nntp = NNTP(remote)
155   nntp.cmd('IHAVE %s' % msgid)
156   rc, msg = nntp.reply()
157   if rc == '335':
158     n = 0
159     for i in stdin:
160       if i.startswith('.'):
161         i = '.' + i
162       if i.endswith('\r\n'):
163         pass
164       elif i.endswith('\n'):
165         i = i[:-1] + '\r\n'
166       else:
167         i = i + '\r\n'
168       body.write(i)
169       n += 1
170     hdr.write('Lines: %d\r\n' % n)
171     hdr.write('\r\n')
172     nntp.write(hdr.getvalue())
173     nntp.write(body.getvalue())
174     nntp.write('.\r\n')
175     nntp.flush()
176     rc, msg = nntp.reply()
177   if rc == '435':
178     ## doesn't want my article; pretend all is fine: I don't care
179     pass
180   elif rc == '436':
181     die('failed to send article: %s %s' % (rc, msg))
182   elif rc == '437':
183     bad('server rejected article: %s %s' % (rc, msg))
184   elif not rc.startswith('2'):
185     die('unexpected response from server: %s %s' % (rc, msg))
186   nntp.cmd('QUIT')
187   nntp.reply()
188
189 def main():
190   try:
191     opts()
192     send()
193   except SystemExit:
194     raise
195 #  except Exception, exc:
196 #    die('unhandled exception: %s, %s' % (exc.__class__.__name__,
197 #                                         exc.args))
198 main()