inject: New posting system.
[newsgate] / bin / inject
diff --git a/bin/inject b/bin/inject
new file mode 100755 (executable)
index 0000000..62a75fd
--- /dev/null
@@ -0,0 +1,183 @@
+#! /usr/bin/python
+
+import sre as RX
+import os as OS
+import time as T
+import socket as S
+from getopt import getopt, GetoptError
+from sys import stdin, stdout, stderr, argv, exit
+from cStringIO import StringIO
+
+prog = argv[0]
+
+def bad(msg):
+  print >>stderr, '%s (fatal): %s' % (prog, msg)
+  exit(100)
+def die(msg):
+  print >>stderr, '%s: %s' % (prog, msg)
+  exit(111)
+def usage():
+  print >>stderr, \
+    ('Usage: %s [-d DIST] [-h HOST] [-r REMOTE] [-p PATH] GROUP <MESSAGE' %
+     prog)
+  exit(111)
+
+def headers(file):
+  h = None
+  while True:
+    line = file.next()
+    if line == '' or line == '\n':
+      break
+    if line[0].isspace():
+      if h is None:
+        bad('unexpected continuation')
+      h += line
+    else:
+      if h: yield h
+      h = line
+  if h: yield h
+
+def hdrsplit(h):
+  v = h.split(':', 1)
+  if len(v) != 2:
+    bad('failed to parse header')
+  return v[0].strip().lower(), v[1].strip()
+
+remote = ('localhost', 119)
+approved = None
+try:
+  host = OS.popen('hostname -f').read().strip()
+except:
+  host = 'localhost'
+dist = 'mail'
+path = 'newsgate'
+group = None
+
+def opts():
+  global approved, remote, host, dist, path, group
+  try:
+    opts, args = getopt(argv[1:], 'a:d:h:r:p:',
+                        ['approved=', 'distribution=',
+                         'hostname=', 'remote=', 'path='])
+  except GetoptError:
+    usage()
+  for o, a in opts:
+    if o in ('-a', '--approved'):
+      approved = a
+    elif o in ('-d', '--distribution'):
+      dist = a
+    elif o in ('-h', '--hostname'):
+      host = a
+    elif o in ('-r', '--remote'):
+      remote = (lambda addr, port = 119: (addr, int(port)))(*a.split(':'))
+  if len(args) != 1:
+    usage()
+  group, = args
+
+rx_msgid = RX.compile(r'^\<\S+@\S+\>$')
+
+class NNTP (object):
+  def __init__(me, addr):
+    me.sk = S.socket(S.AF_INET, S.SOCK_STREAM)
+    me.sk.connect(remote)
+    me.f = me.sk.makefile()
+    rc, msg = me.reply()
+    if rc != '200':
+      die('unable to contact server: %s %s' % (rc, msg))
+  def write(me, stuff):
+    me.f.write(stuff)
+  def flush(me):
+    me.f.flush()
+  def cmd(me, stuff):
+    me.f.write(stuff + '\r\n')
+    me.f.flush()
+  def reply(me):
+    rc, msg = (lambda rc, msg = '.': (rc, msg.strip())) \
+                (*me.f.readline().split(None, 1))
+    if rc.startswith('5'):
+      die('server hated me: %s %s' % (rc, msg))
+    return rc, msg.strip()
+
+def send():
+  hdr = StringIO()
+  hdr.write('Path: newsgate\r\n'
+            'Distribution: mail\r\n'
+            'Newsgroups: %s\r\n'
+            'Approved: %s\r\n'
+            % (group, approved or 'newsgate@%s' % host))
+  xify = {}
+  for h in '''
+    lines xref newsgroups path distribution approved received
+  '''.split():
+    xify[h] = 1
+  seen = {}
+  for h in headers(stdin):
+    n, c = hdrsplit(h)
+    if n in xify:
+      h = 'X-Newsgate-' + h
+    elif h.startswith('.'):
+      h = '.' + h
+    seen[n] = c
+    if h.endswith('\r\n'):
+      pass
+    elif h.endswith('\n'):
+      h = h[:-1] + '\r\n'
+    else:
+      h += '\r\n'
+    hdr.write(h)
+  if 'message-id' not in seen:
+    seen['message-id'] = ('<newsgate-%s@%s>'
+                          % (OS.popen('gorp 128').read().strip(),
+                             host))
+    hdr.write('Message-ID: %s\r\n' % seen['message-id'])
+  if 'date' not in seen:
+    hdr.write('Date: %s\r\n'
+              % (T.strftime('%a, %d %b %Y %H:%M:%S %Z')))
+  if 'subject' not in seen:
+    hdr.write('Subject: (no subject)\r\n')
+  hdr.write('\r\n')
+
+  msgid = seen['message-id']
+  if not rx_msgid.match(msgid):
+    bad('invalid message-id %s' % msgid)
+
+  nntp = NNTP(remote)
+  nntp.cmd('IHAVE %s' % msgid)
+  rc, msg = nntp.reply()
+  if rc == '335':
+    nntp.write(hdr.getvalue())
+    for i in stdin:
+      if i.startswith('.'):
+        i = '.' + i
+      if i.endswith('\r\n'):
+        pass
+      elif i.endswith('\n'):
+        i = i[:-1] + '\r\n'
+      else:
+        i = i + '\r\n'
+      nntp.write(i)
+    nntp.write('.\r\n')
+    nntp.flush()
+    rc, msg = nntp.reply()
+  if rc == '435':
+    ## doesn't want my article; pretend all is fine: I don't care
+    pass
+  elif rc == '436':
+    die('failed to send article: %s %s' % (rc, msg))
+  elif rc == '437':
+    bad('server rejected article: %s %s' % (rc, msg))
+  elif not rc.startswith('2'):
+    die('unexpected response from server: %s %s' % (rc, msg))
+  nntp.cmd('QUIT')
+  nntp.reply()
+
+def main():
+  try:
+    opts()
+    send()
+  except SystemExit:
+    raise
+#  except Exception, exc:
+#    die('unhandled exception: %s, %s' % (exc.__class__.__name__,
+#                                         exc.args))
+main()