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