#!/usr/local/bin/python

# current status (950301):
#  fixed the uid problem using majordomo's 'wrapper' program.
#  this allows me to use INN's private NNTP socket, and to
#  spool articles for later handling if the server is throttled.
#
# current status (950111):
# works ok with inews, but still no way to know whether the post
#   suceeded.
# talking to the private port doesn't work (can't setuid from daemon)
# talking to NNTP port directly is cool, and works.
#
# current status (950104):
# works ok with inews, but still have the problem of not know
#   what inews is returning...
# I would like to have this talk to INN's private NNTP socket,
#   but this doesn't work on all machines.  May just do the same
#   workaround as for linux.
# 
#---------------------------------------------------------------------------

import sys
import string
import regex
import regsub
import getopt
import os
import candate
import parsdate
import socket
import header

program = 'mail2news.py'
# man page for inews claimes that -h is required with headers
inews_program = '/usr/local/news/inews -h'

# mandatory: date, from, mid, subject, newsgroups, path.
# son-of-1036 says to & cc should not appear.
# date headers missing parts should be filled in with defaults (0's)
# bad from headers: gateway MUST transform via %-hack.

irt_h = regex.compile('[ \t]*\(<[^>]+>\).*', regex.casefold)

# message _must_ have these headers
mandatory_headers = ['Date', 'From', 'Message-ID', 'Subject']

# USE THIS FOR A MAIL-ONLY GATEWAY
# the presence of these headers will cause a rejection [any more?]
#reject_if_present_headers = ['NNTP-Posting-Host', 'Newsgroups', 'Path', 'Control']
#unwanted_headers = ['Received']

# USE THIS FOR A 'MODERATED' GATEWAY.
# these headers will be tossed
# TODO: need to add a 'transform_headers', which at the very least will do a X-Unwanted:
unwanted_headers = ['Received', 'Newsgroups', 'To', 'NNTP-Posting-Host']
reject_if_present_headers = ['Control']
backup_headers = ['To', 'CC']

MAILER = '/bin/mail'
INCOMING_DIR = '/var/spool/news/in.coming'

def tweak_message (m, newsgroups):
	reject_reasons = []

	# this is temporary, need to put this behaviour into a derived
	# class.
	if m.getheader('subject') == None:
		m['Subject'] = '<none>'

	# make sure all mandatory headers are present
	for x in mandatory_headers:
		if m.getheader(x) == None:
			reject_reasons.append ('Missing '+x+': header')

	# backup headers - i.e., transform 'To' to 'X-Original-To'

	for x in backup_headers:
		while 1:
			hd = m.getheader(x)
			if hd != None:
				m['X-Original-'+x] = hd
				m.remove_first (x)
			else:
				break

	for x in reject_if_present_headers:
		if m.getheader(x) != None:
			reject_reasons.append ('Unwanted '+x+': header')

	# if we're ok at this point, go ahead and remove the unwanted
	# headers
	if reject_reasons == []:
		for x in unwanted_headers:
			m.remove_all (x)

	# add a newsgroups header
	if newsgroups:
		m['Newsgroups'] = newsgroups

	# transform the in-reply-to header to a references header
	irt = m.getheader('in-reply-to')
	if irt:
		if irt_h.match(irt) != -1:
		# this should be made smarter.
			m['References'] = irt_h.group(1)
			m.remove_all('In-Reply-To')

	# make the date header safe for news
	ds = m['Date']
	cds = candate.canonicalize_date (ds)
	if ds != cds:
		# do we really need to back it up?  hmmm...
		m['X-Original-Date'] = ds
		m.remove_all ('Date')
		m['Date'] = cds

	m['Approved'] = program
	return reject_reasons

def send_rejection_mail (lines, recipient, message=None):
	fd = os.popen (MAILER+' '+recipient, 'w')
	fd.write ('From: '+program+'\n')
	fd.write ('Subject: message unfit for inews\n')
	fd.write ('To: '+recipient+'\n')
	fd.write ('\n')
	# write out the body of the rejection notice
	for x in lines:
		fd.write (x+'\n')
	# write out the bad message
	fd.write ("message follows (this should really be MIME'd)\n")
	fd.write ('-'*50+'\n')
	if message:
		message.rewindbody()
		for x in message.headers:
			fd.write (x)
		while 1:
			line = message.fp.readline()
			if line == '':
				break
			fd.write (line)
	fd.close()

def feed_to_inews (m):
	fd = os.popen (inews_program, 'w')
	for x in m.headers:	
		fd.write (x+'\n')
	fd.write('\n')
	while 1:
		line = m.fp.readline()
		if line == '':
			break
		fd.write (line)
	fd.close()

def feed_to_innd (m):
	import socket
	import posix
	s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
	try:
		s.connect('',119)
	except:
		return "couldn't connect to INND"
#	f = s.makefile ('a+')				# How make work?
	resp = s.recv(512)
	if resp[:3] != '200':
		return "unexpected response from INN: "+resp
	s.send ('IHAVE '+m['message-id']+'\r\n')
	resp = s.recv(512)
	if resp[:3] != '335':
		return "unexpected response from IHAVE command: "+resp
	for x in m.headers:
		s.send(x[:-1]+'\r\n')
	s.send('\r\n')
	while 1:
		line = m.fp.readline()
		if not line:
			break
		if line[0] == '.':
			s.send('.')
		s.send(line[:-1]+'\r\n')
	s.send('.\r\n')
	resp = s.recv (512)
	if resp[:3] != '235':
		return "post failed: "+resp
	s.close()
	return ''

# For testing...
# def feed_to_innd (m):
# 	f = sys.stdout
# 	for x in m.headers:
# 		f.write(x[:-1]+'\r\n')
# 	f.write('\r\n')
# 	while 1:
# 		line = m.fp.readline()
# 		if not line:
# 			break
# 		if line[0] == '.':
# 			f.write('.')
# 		f.write(line[:-1]+'\r\n')
# 	f.write('.\r\n')
# 	return ''

def spool_to_innd (mesg):
	# we want to spool it for later
	# but we have no idea if it will be rejected
	# later on... <sigh>
	filename = os.path.join (INCOMING_DIR, 'm2n.'+str(int(time.time())%10000))
	fd = open (filename, 'w')
	for x in mesg.headers:
		fd.write (x+'\n')
	while 1:
		line = mesg.fp.readline()
		if line == '':
			break
		fd.write (line)
	fd.close()

def feed_message (mesg, newsgroups, list_owner=None):
	mesg['Path'] = 'mail2news.py'
	rejections = tweak_message (mesg, newsgroups)
	if rejections != []:
		rej = ['the attached message could not be forwarded to news',
			   'for the following reasons:']
		for x in rejections:
			rej.append (x)
		if list_owner:
			send_rejection_mail (rej, list_owner, mesg)
		return None
#  	feed_to_inews (mesg)
	result = feed_to_innd (mesg)
	if result != '':
		if string.find (result, 'throttled') == -1:
			if list_owner:
				send_rejection_mail(["direct innd feed failed with", result],
									list_owner, mesg)
			return None
		spool_to_innd (mesg)
	return mesg

def main():
	def get_option (which):
		for opt, arg in getopt_result[0]:
			if opt == which:
				return arg
		return None
	def usage ():
		return program+' -n newsgroups -o list_owner'
	getopt_result = getopt.getopt (sys.argv[1:], 'n:o:')
	list_owner = get_option ('-o')
	newsgroups = get_option ('-n')
	# make sure we were called correctly!
	if not list_owner or not newsgroups:
		send_rejection_mail (["program was called with '"+repr(sys.argv)+"'",
							  "usage: "+usage()],
							 'usenet')
	mesg = header.Message (sys.stdin)
	# ignore junk mail! (should probably be a list of regex's)
	if regex.match ('MAILER-DAEMON@.*', mesg['from']) == -1:
		feed_message (mesg, newsgroups, list_owner)

if __name__ == '__main__':
	main ()

# this is for emacs
# Local variables:
# py-indent-offset: 4
# tab-width: 4
# end:
