| 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() |