#!/usr/bin/perl
# @(#) newspost Version 1.0
# publish a file of news articles to a news server.
#
# The NNTP protocol can be found in RFC977
# RFC1036 describes the format of the messages.
#
# GNU Copyright (C) June 24 1996 Gaspar Sinai <gsinai@gol.com>
# GNU Copyright (C) Feb   6 1996 Gaspar Sinai <gsinai@gol.com>
#
# I dont want to make the compication long: please read the separate
# file called COPYING
#

$VERBOSE=0;

$ALARM=300;
#
# Command line arguments 
#
while ($#ARGV >=0)
{
	if( $#ARGV > 0 && $ARGV[0] eq "-s")
	{
		$SERVER=$ARGV[1];shift;shift;
	}
	elsif( $#ARGV > 0 && $ARGV[0] eq "-p")
	{
		$PASSWORD_FILE=$ARGV[1];shift;shift;
	}
	elsif( $#ARGV > 0 && $ARGV[0] eq "-t")
	{
		$ALARM=$ARGV[1];shift;shift;
	}
	elsif( $#ARGV > 0 && $ARGV[0] eq "-f")
	{
		$OUTGOING=$ARGV[1];shift;shift;
	}
	elsif( $#ARGV > 0 && $ARGV[0] eq "-g")
	{
		@NEWSGROUPS=(@NEWSGROUPS, $ARGV[1]);shift;shift;
	}
	elsif( $#ARGV > 0 && $ARGV[0] eq "-S")
	{
		$SUBJECT=$ARGV[1];shift;shift;
	}
	elsif( $#ARGV > 0 && $ARGV[0] eq "-i")
	{
		$NEWSPORT=$ARGV[1];shift;shift;
	}
	elsif( $ARGV[0] eq "-n")
	{
		$NNTPFILTER=1;shift;
	}
	elsif( $ARGV[0] eq "-r")
	{
		$REJECTMAIL=1;shift;
	}
	elsif( $ARGV[0] eq "-a")
	{
		$ACKMAIL=1;shift;
	}
	elsif( $ARGV[0] eq "-v")
	{
		$VERBOSE++;shift;
	}
	else
	{
		last;
	}
}

$NEWSPORT=nntp unless (defined ($NEWSPORT));

if (defined (@NEWSGROUPS) && !defined ($SUBJECT))
{ 
	&NWUsage;
	exit (255);
}
elsif (!defined (@NEWSGROUPS) && defined ($SUBJECT))
{
	&NWUsage;
	exit (255);
}

if (defined ($SUBJECT))
{
	unless (defined ($SERVER))
	{
		&NWUsage;
		exit (255);
	}
}
elsif (!defined ($SERVER) || !defined($OUTGOING))
{
	&NWUsage;
	exit (255);
}

if (defined ($SUBJECT))
{
	if ($#ARGV >= 0)
	{
		$MYADDRESS="@ARGV";
	}
}
elsif ($#ARGV >=0)
{
	@ALLOWED=@ARGV;
}

$NWREJECTCOUNT=0;
$NWPOSTCOUNT=0;
$NWUNPOSTCOUNT=0;

&NWAddSignals ('NWDie');

$UID = (getpwuid($<))[2];
$LOCK = "/tmp/loadnews.$UID";

unless (defined ($SUBJECT))
{
	unless (open (NWOUT, "< $OUTGOING"))
	{
		print STDERR  "$0: Can not open $OUTGOING.\n";
		exit 255;
	}

	while (<NWOUT>)
	{
		chop;
		@NWARTLIST=(@NWARTLIST, $_);
	}
	close (NWOUT);
}
else
{
	$TMPFILE="/tmp/newspost$$";
	unless (open (TMP, "> $TMPFILE"))
	{
		print STDERR  "$0: Can not open tmpfile $TMPFILE.\n";
		exit 255;
	}
	$MYADDRESS=&NWMyAddress($MYADDRESS);
	($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdsr) = gmtime(time);
	$ascdate = sprintf("%02d%02d%02d%02d%02d",$year, 
		$mon+1,$mday,$hour,$min);
	print TMP "Message-Id: <$ascdate.NP$$\@$HOSTNAME>\n";
	print TMP "X-Newsposter: newspost-1.0\n";
	print TMP "Newsgroups: @NEWSGROUPS\n";
	print TMP "From: $MYADDRESS\n";
	print TMP "Subject: $SUBJECT\n";
	print TMP "\n";
	@NWARTLIST=$TMPFILE;
	$LINE=0;
	if (-t STDIN)
	{
		print "Enter article. Finish it with a single \".\" ";
		print "or <Ctrl><D> at start of line:\n";
	}
	while (<STDIN>)
	{
		last if ( $_ =~ /^\.[\r\n]*$/);
		print TMP "$_";
		$LINE++;
	}
	if ($LINE==0)
	{
		print STDERR "Empty input - not posted.\n";
		unlink $TMPFILE if (defined($TMPFILE));
		exit (255);
	}
	close (TMP);
}

if (!defined(@NWARTLIST))
{
	print STDERR  "$0: $OUTGOING is empty.\n";
	exit 255;
}

if (defined ($PASSWORD_FILE))
{
	unless ( open (PASSFILE, "< $PASSWORD_FILE"))
	{
		print STDERR  "$0: Can not open $PASSWORD_FILE.\n";
		exit (255);
	}

	while (<PASSFILE>)
	{
		chop;
		next if ( /^#.*/o );
		next unless (split == 3);
		next unless ( "$SERVER" eq $_[0] );
		$USER=$_[1];
		$PASS=$_[2];
		last;
	}
}

#
# If the program is stuck for more than 5 minutes than bailing out.
#
alarm ($ALARM);

if ( defined ($USER) && defined ($PASSWORD_FILE) &&  defined ($PASS))
{
	&NWOpen ("$SERVER", "$NEWSPORT", $USER, $PASS);
}
else
{
	&NWOpen ("$SERVER", "$NEWSPORT");
}


#
# Read articles from input and post them one by one.
#

foreach $ARTREF (@NWARTLIST)
{
	if( &WGetArticleByRef ($ARTREF) == 0)
	{
		print STDERR "$0: $ARTREF not posted.\n";
		next;
	}
	($status, $smsg) = &NWCommand ("POST");
	if ($status!=340 )
	{
		$NWREJECTCOUNT++;
		print STDERR "$0: Can not post $ARTREF.\n";
		print STDERR "$status $smsg\n";
		next;
	}
	alarm ($ALARM);
	print NW @ARTICLE;
	($status, $smsg) = &NWCommand (".");
	if ($status != 240) # posted ok
	{
		if ($status == 441) # No such group
		{
			$NWUNPOSTS{"$ARTREF"}=1;
		}
		$NWREJECTCOUNT++;
		print STDERR "$0: Rejected $ARTREF article.\n";
		print STDERR "$status $smsg\n";
		if ( $REJECTMAIL && $FROM ne "")
		{
			next unless (open (MAIL, "| mail -s 'Rejected News Post' $FROM"));
			print MAIL "Your News Post was rejected by $SERVER.\n";
			print MAIL "Reason: $status, $smsg\n\n";
			print MAIL "   ----- Unsent message follows -----\n";
			print MAIL @ARTICLE;
			close (MAIL);
		}
		next;
	}
	if ($VERBOSE > 0)
	{
		print "$ARTREF $MESSAGEID From: $FROM Posted.\n";
	}
	$NWPOSTCOUNT++;
	$NWPOSTS { "$ARTREF" } = 1;
	if ( $ACKMAIL && $FROM ne "")
	{
		next unless (open (MAIL, "| mail -s 'Successful News Post' $FROM"));
		print MAIL "Your News Post was delivered to $SERVER.\n\n";
		print MAIL "   ----- Sent message follows -----\n";
		print MAIL @ARTICLE;
		close (MAIL);
	}
}

if (defined ($SUBJECT))
{
	unlink $TMPFILE if (defined ($TMPFILE));
}
else
{
	&NWSaveRejects();
}

print "$0: Normal exit\n" if ($VERBOSE > 0);
print "$0: Rejected: $NWREJECTCOUNT, Posted: $NWPOSTCOUNT, Discarded: $NWUNPOSTCOUNT\n";

($status, $smsg) = &NWCommand ("QUIT");

exit(0);

#-------------------------------------------------------------------------------
#  S U B R O U T I N E S
#-------------------------------------------------------------------------------

# 
# Save the rejects file
#
sub
NWSaveRejects
{
	#
	# build the reject list 
	#
	foreach $ARTREF (@NWARTLIST)
	{
		next if (defined ($NWPOSTS{"$ARTREF"}));
		next if (defined ($NWUNPOSTS{"$ARTREF"}));
		@REJECTS= (@REJECTS, $ARTREF . "\n");
	}
	unless ( open (NWREJFILE, "> $OUTGOING"))
	{
		print STDERR  "$0: Can not open $OUTGOING.\n";
		unlink $TMPFILE if (defined ($TMPFILE));
		exit 255;
	}
	print "$0: Saving new $OUTGOING\n";
	if ($VERBOSE > 0)
	{
		print  @REJECTS;
	}
	print NWREJFILE @REJECTS;
	close (NWREJFILE);
}

#
# The connection is broken with the news. Update files and exit.
#
sub 
NWDie 
{
	local($msg) = $_[0];

	print STDERR "An error occurred: $msg\n";
	&NWSaveRejects() unless (defined ($SUBJECT));

	print "$0: Rejected: $NWREJECTCOUNT, Posted: $NWPOSTCOUNT, Discarded: $NWUNPOSTCOUNT\n";
	unlink $TMPFILE if (defined ($TMPFILE));
	exit (255);
}


#
# Send a command to the server and wait for the reply
# Two strings are returned. The first is the reply number
# and the second is the reply string. 
#
sub
NWCommand
{
	print NW "$_[0]\r\n" if ($_[0] ne "NOP");
	$_=<NW>;
	return ($1,$2) if( /(\d\d\d) (.*)\r\n/);
	return ($1) if( /(\d\d\d)\r\n/);
	return "ERR";
}

#
# usage: NWOpen server port 
#
# Open an NNTP server with a port number.
# If user and password is omitted no authentication is done.
# port can be specified by its name like "nntp"
#
sub 
NWOpen 
{
	local($server, $port, $user, $pass) = @_;

	$sockaddr = 'S n a4 x8';

	if ($server =~ /^(\d+)+\.(\d+)\.(\d+)\.(\d+)$/)
	{
		$serveraddr = pack('C4', $1, $2, $3, $4);
	}
	else
	{
    		&NWDie ("$0: Unkown host: $server.") 
			unless (local(@arr) = gethostbyname($server));
		$serveraddr = $arr[4];
	}

    	unless (($name, $aliases, $proto) = getprotobyname('tcp'))
	{
		&NWDie ("$0: Unkown protocol 'tcp'");
	}
	if ($port !~ /^\d+$/ )
	{
		unless (($name, $aliases, $np) = getservbyname($port,'tcp'))
		{
			&NWDie ("$0: Unkown service '$port'");
		}
		$port=$np;
	}

	$serverproc = pack($sockaddr, 2, $port, $serveraddr);

	#
	# Hardwire to AF_SOCKET, $SOCK_STREAM, 'tcp'
	# Well, it used to be constant on all platforms but now, 
        # on Solaris $SOCK_STREAM is 2 that's why the second "try"
        # I dont want to include any perl files for only this...
	&NWDie ("$0: Can not make socket.") 
		unless (socket(NW, 2, 1, 6) || socket(NW, 2, 2, 6));
	
	unless (connect(NW, $serverproc))
	{
		close (NW);
		&NWDie ("$0: Can not connect socket.");
	}

	select((select(NW), $| = 1)[0]);

	($status, $msg) = &NWCommand ( "NOP" );
	unless ($status == 200 || $status == 201)
	{
		&NWDie ( "Could not contact nntpd. $msg");
	}

	($status, $smsg) = &NWCommand ("MODE READER");
	if ( defined ($user) && defined ($pass))
	{
		($status, $msg) = &NWCommand ( "AUTHINFO USER $user" );
		&NWDie ("Could not Authenticate $user. $status $msg") unless ($status == 381);
		($status, $msg) = &NWCommand ( "AUTHINFO PASS $pass" );
		&NWDie ("Could not Authenticate. $status $msg") unless ($status == 281);
	}
}

#
# Print out usage message
#
sub
NWUsage
{
print <<EOTXT;
newspost version 1.0 usage: 
post news folders: newspost -s server -f output-batchfile [-p password-file] \\
                   [-t timeout] [-i port] [-n] [-r] [-a] [-v] [user1 [user2..]]
post from stdin:   newspost -g newsgroup -S subject -s server [-r] [-a] [-v] \\
                   [-p password-file] [-t timeout] [-i port] [from-address]
EOTXT
}

#
# Get a long text buffer into list.
#
sub
WGetArticleByRef
{
	$_ = $_[0];
	$FILENAME = $_;
	split;
	$REAL_FILENAME=$_[0];
	unless ( open (INPUT,  "< $REAL_FILENAME"))
	{
		print STDERR "$0: Can not open $REAL_FILENAME.";
		$NWUNPOSTCOUNT++;
		@NWUNPOSTS {"$FILENAME"} = 1;
		return 0;
	}
	($QALLOWED) = &NWParsePost();
	close (INPUT);
	if ($QALLOWED == 0)
	{
		print STDERR "$0: $FROM is not allowed to post:$FILENAME\n";
		$NWUNPOSTCOUNT++;
		$NWUNPOSTS{"$FILENAME"}=1;
		
	}
	unless (defined (@ARTICLE))
	{
		print STDERR "$0: Cant parse header in $FILENAME.\n";
		$NWUNPOSTCOUNT++;
		$NWUNPOSTS{"$FILENAME"}=1;
		return 0;
	}
	return $QALLOWED;
}

#
# Get article from INPUT file and put in into global @ARTICLE
# return 0 if guy is not allowed to post, undef @ARTICLE on error.
# Multi-line headers are fine. Return-Path: and From: are both parsed.
# Return-Path is of higher priority.
#
sub
NWParsePost
{
	local ($MessageId, $From, $ReturnPath, $MultiLine);
	
	$MessageId = $From = $ReturnPath = "";
	undef (@ARTICLE);

	while (<INPUT>)
	{
		# We should skip nntp filter normally
		# this can not be multi-line
		if (/^NNTP-Posting-Host:[ \t]+(.*)$/)
		{
			next unless (defined ($NNTPFILTER));
		}

		@ARTICLE=(@ARTICLE, $_);
		chop;
		s/\r$//;
		last if ($_ eq "");

		if ($_ =~ /^[ \t]+(.*)$/ )
		{
			$MultiLine = "$MultiLine $_";
			$_=$MultiLine;
		}
		else
		{
			$MultiLine=$_;
		}

                #
		# Lets allow for some discrepency
		#
		if ( /^Message-ID:[ \t]+(<.*\@.*>)$/
			|| /^Message-Id:[ \t]+(<.*\@.*>)$/ )
		{
			$MessageId=$1;
			next;
		}

		# Now many variants of From: header
		next unless (/^([A-Z][-A-Za-z]+):[ \t]+.*<(.*@.*)>$/ 
			|| /^([A-Z][-A-Za-z]+):[ \t]+<(.*@.*)>$/
			 || /^([A-Z][-A-Za-z]+):[ \t]+(.*@.*)[ \t]+\(.*\)$/ 
			|| /^([A-Z][-A-Za-z]+):[ \t]+(.*@.*)$/ );

		$From = $2 if ($1 eq "From");
		$ReturnPath = $2 if ($1 eq "Return-Path");
	}
	while (<INPUT>)
	{
		@ARTICLE=(@ARTICLE, $_);
	}
	$FROM=$From;
	$FROM=RetrunPath if ( "$RetrunPath" ne "");
	$MESSAGEID=$MessageId;

	if ( "$MessageId" eq "")
	{
		undef (@ARTICLE);
		return 1;
	}
	if ( defined (@ALLOWED))
	{
		foreach $FELLOW (@ALLOWED)
		{
			return 1 if ($FROM =~ /$FELLOW/);
		}
		return 0;
	}
	return 1;
}

#
# Find out what my address is.
#
sub
NWMyAddress
{
	local ($Name, $FullName, @pwent);

	@pwent = getpwuid($<);

	$Name = $pwent[0];
	$FullName = $pwent[6]; 

	chop ($HOSTNAME=`hostname`);

	unless (defined ($_[0]))
	{
		$MYADDRESS="$FullName <$Name\@$HOSTNAME>";
		return $MYADDRESS;
	}
	$_=$_[0];
	return $_ if ( /^.*[ \t]+<.*@.*>$/);
	return $_ if ( /^.*@.*[ \t]+\(.*\)$/ );
	return "$FullName $_" if ( /^<.*@.*>$/);
	return "$FullName <$_>" if ( /^.*@.*$/);
	return "$FullName <$Name\@$_>" if ( /^.*\..*$/);
	return undefined;
}

#
# Assign signal handlers for some signals
#
sub
NWAddSignals
{
	local ($signal) = $_[0];
	if ($signal eq 'IGNORE')
	{
		$SIG{'INT'} = 'NWSigSave';
	}
	elsif (defined ($SIGCAUGHT))
	{
		&NWDie ($SIGCAUGHT);
	}
	else 
	{
		$SIG{'INT'} = $signal;
		$SIG{'QUIT'} = $signal;
		$SIG{'TERM'} = $signal;
		$SIG{'ALRM'} = $signal;
	}
}

#
# This handler is invoked when we don't want any signal handler.
# It saves the last incoming signal in $SIGCAUGHT
#
sub
NWSigSave
{
	$SIGCAUGHT=$_[0];
}

