newsstats/contrib/tinews.pl
Thomas Hochstein cff76a3c65 Set executable bit for new scripts.
Signed-off-by: Thomas Hochstein <thh@thh.name>
2025-06-01 11:03:05 +02:00

1507 lines
50 KiB
Prolog
Executable file

#! /usr/bin/perl
#
# reads an article on STDIN, mails any copies if required,
# signs the article and posts it.
#
#
# Copyright (c) 2002-2024 Urs Janssen <urs@tin.org>,
# Marc Brockschmidt <marc@marcbrockschmidt.de>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
#
# TODO: - extend debug mode to not delete tmp-files and be more verbose
# - add pid to pgptmpf to allow multiple simultaneous instances
# - check for /etc/nntpserver (and /etc/news/server)
# - add $PGPOPTS, $PGPPATH and $GNUPGHOME support
# - cleanup and remove duplicated code
# - quote inpupt properly before passing to shell
# - $ENV{'NEWSHOST'} / $ENV{'NNTPSERVER'} and $ENV{'NNTPPORT'}
# do have higher precedence than settings in the script and
# config-file, but config-settig SSL may override $ENV{'NNTPPORT'}
# - if (!defined $ENV{'GPG_TTY'}) {if (open(my $T,'-|','tty')) {
# chomp(my $tty=<$T>); close($T);
# $ENV{'GPG_TTY'}=$tty if($tty =~ m/^\//)}}
# for gpg?
# - option to break long header lines?
# - option to trim References
# - option to foce connection via AF_INET6 (-6)
# ...
#
# cmd-line options used in other inews:
# inews-xt (Olaf Titz):
# -C accepted for historic reasons and errors out
# inews (inn)
# -P don't add Sender
# inews (Eric S. Raymond; 1989)
# -C create grp
# -d Distribution:
# -p [file] ; run rnews mode
# -M moderator option
# -v print returned ID
require 5.004;
use strict;
use warnings;
# version Number
my $version = "1.1.70";
my %config;
# configuration, may be overwritten via ~/.tinewsrc
$config{'nntp-server'} = 'news'; # your NNTP servers name, may be set via $NNTPSERVER
$config{'nntp-port'} = 119; # NNTP-port, may be set via $NNTPPORT
$config{'nntp-user'} = ''; # username for nntp-auth, may be set via ~/.newsauth or ~/.nntpauth
$config{'nntp-pass'} = ''; # password for nntp-auth, may be set via ~/.newsauth or ~/.nntpauth
$config{'ssl'} = 0; # set to 1 to use NNTPS if possible
$config{'pgp-signer'} = ''; # sign as who?
$config{'pgp-pass'} = ''; # pgp2 only
$config{'path-to-pgp-pass'}= ''; # pgp2, pgp5, pgp6 and gpg
$config{'pgp-pass-fd'} = 9; # file descriptor used for input redirection of path-to-pgp-pass; GPG1, GPG2, PGP5 and PGP6 only
$config{'pgp'} = '/usr/bin/pgp'; # path to pgp
$config{'pgp-version'} = '2'; # Use 2 for 2.X, 5 for PGP5, 6 for PGP6, GPG or GPG1 for GPG1 and GPG2 for GPG2
$config{'digest-algo'} = 'MD5';# Digest Algorithm for GPG. Must be supported by your installation
$config{'interactive'} = 'yes';# allow interactive usage
$config{'verbose'} = 0; # set to 1 to get warning messages
$config{'debug'} = 0; # set to 1 to get some debug output
$config{'sig-path'} = glob('~/.signature'); # path to signature
$config{'add-signature'}= 'yes';# Add $config{'sig-path'} to posting if there is no sig
$config{'sig-max-lines'}= 4; # max number of signatures lines
$config{'max-header-length'} = 998; # RFC 5536
$config{'sendmail'} = '/usr/sbin/sendmail -i -t'; # set to '' to disable mail-actions
$config{'pgptmpf'} = 'pgptmp'; # temporary file for PGP.
$config{'pgpheader'} = 'X-PGP-Sig';
$config{'pgpbegin'} = '-----BEGIN PGP SIGNATURE-----'; # Begin of PGP-Signature
$config{'pgpend'} = '-----END PGP SIGNATURE-----'; # End of PGP-Signature
$config{'canlock-algorithm'} = 'sha1'; # Digest algorithm used for cancel-lock and cancel-key; sha1, sha256 and sha512 are supported
# $config{'canlock-secret'} = '~/.cancelsecret'; # Path to canlock secret file
# $config{'ignore-headers'} = ''; # headers to be ignored during signing
$config{'pgp-sign-headers'} = [
'From', 'Newsgroups', 'Subject', 'Control', 'Supersedes', 'Followup-To',
'Date', 'Injection-Date', 'Sender', 'Approved', 'Message-ID', 'Reply-To',
'Cancel-Key', 'Also-Control', 'Distribution' ];
$config{'pgp-order-headers'} = [
'from', 'newsgroups', 'subject', 'control', 'supersedes', 'followup-To',
'date', 'injection-date', 'organization', 'lines', 'sender', 'approved',
'distribution', 'message-id', 'references', 'reply-to', 'mime-version',
'content-type', 'content-transfer-encoding', 'summary', 'keywords',
'cancel-lock', 'cancel-key', 'also-control', 'x-pgp', 'user-agent' ];
################################################################################
use Getopt::Long qw(GetOptions);
use Net::NNTP;
use IO::Socket qw(AF_INET PF_INET);
use Time::Local;
use Term::ReadLine;
(my $pname = $0) =~ s#^.*/##;
# read config file (first match counts) from
# $XDG_CONFIG_HOME/tinewsrc
# ~/.config/tinewsrc
# ~/.tinewsrc
# if present
my $TINEWSRC = undef;
my (@try, %seen);
if ($ENV{'XDG_CONFIG_HOME'}) {
push(@try, (glob("$ENV{'XDG_CONFIG_HOME'}/tinewsrc"))[0]);
}
push(@try, (glob('~/.config/tinewsrc'))[0], (glob('~/.tinewsrc'))[0]);
foreach (grep { ! $seen{$_}++ } @try) { # uniq @try
last if (open($TINEWSRC, '<', $_));
$TINEWSRC = undef;
}
if (defined($TINEWSRC)) {
my $changes = 0;
while (defined($_ = <$TINEWSRC>)) {
if (m/^([^#\s=]+)\s*=\s*(\S[^#]+)/io) {
# rename pre 1.1.56 tinewsrc-var names
my $key = $1;
my $val = $2;
$key =~ s#^followupto#follow-to# && $changes++;
$key =~ s#^replyto#reply-to# && $changes++;
$key =~ s#^NNTP(?!\-).#NNTP-# && $changes++;
$key =~ s#^PathtoPGPPass#path-to-pgp-pass# && $changes++;
$key =~ s#^PGPorderheaders#pgp-order-headers# && $changes++;
$key =~ s#^PGPPassFD#pgp-pass-fd# && $changes++;
$key =~ s#^PGPSignHeaders#pgp-sign-headers# && $changes++;
$key =~ s#^PGP(?!\-).#PGP-# && $changes++;
$key =~ s#_#-# && $changes++;
chomp($config{lc($key)} = $val);
}
}
close($TINEWSRC);
print "Old style tinewsrc option names found, you should adjust them.\n" if ($changes && ($config{'verbose'} || $config{'debug'}));
}
# as of tinews 1.1.51 we use 3 args open() to pipe to sendmail
# thus we remove any leading '|' to avoid syntax errors;
# for redirections use cat etc.pp., eg. 'cat > /tmp/foo'
$config{'sendmail'} =~ s/^\s*\|\s*//io;
# digest-algo is case sensitive and should be all uppercase
$config{'digest-algo'} = uc($config{'digest-algo'});
# these env-vars have higher priority (order is important)
$config{'nntp-server'} = $ENV{'NEWSHOST'} if ($ENV{'NEWSHOST'});
$config{'nntp-server'} = $ENV{'NNTPSERVER'} if ($ENV{'NNTPSERVER'});
$config{'nntp-port'} = $ENV{'NNTPPORT'} if ($ENV{'NNTPPORT'});
# Get options
Getopt::Long::Configure ("bundling", "no_ignore_case");
my $oret = GetOptions(
'A|V|W|h|headers' => [], # do nothing
'debug|D|N' => \$config{'debug'},
'port|p=i' => \$config{'nntp-port'},
'no-sign|X' => \$config{'no-sign'},
'no-control|R' => \$config{'no-control'},
'no-signature|S' => \$config{'no-signature'},
'no-canlock|L' => \$config{'no-canlock'},
'no-injection-date|I' => \$config{'no-injection-date'},
'no-organization|O' => \$config{'no-organization'},
'force-auth|Y' => \$config{'force-auth'},
'approved|a=s' => \$config{'approved'},
'control|c=s' => \$config{'control'},
'canlock-algorithm=s' => \$config{'canlock-algorithm'},
'distribution|d=s' => \$config{'distribution'},
'discard-empty|E' => \$config{'discard-empty'},
'expires|e=s' => \$config{'expires'},
'from|f=s' => \$config{'from'},
'ignore-headers|i=s' => \$config{'ignore-headers'},
'followup-to|w=s' => \$config{'followup-to'},
'message-id|m=s' => \$config{'message-id'},
'newsgroups|n=s' => \$config{'newsgroups'},
'reply-to|r=s' => \$config{'reply-to'},
'savedir|s=s' => \$config{'savedir'},
'ssl|nntps' => \$config{'ssl'},
'subject|t=s' => \$config{'subject'},
'references|F=s' => \$config{'references'},
'organization|o=s' => \$config{'organization'},
'path|x=s' => \$config{'path'},
'timeout|T=i' => \$config{'timeout'},
'ipv4|4' => \$config{'ipv4'},
'help|H' => \$config{'help'},
'transform' => \$config{'transform'},
'verbose|v' => \$config{'verbose'},
'version' => \$config{'version'},
'man' => \$config{'man'}
);
usage() unless $oret;
if ($config{'version'}) {
version();
exit 0;
}
usage() if ($config{'help'});
# not listed in usage() or man-page as it may not work
if ($config{'man'}) {
if (eval { require Pod::Usage;1; } != 1) {
$config{'man'} = 0;
print STDERR "Unknown option: man.\n";
usage();
} else {
use Pod::Usage;
pod2usage(-verbose => 3, -exit => 0);
}
}
# check if SSL support is available
if ($config{'ssl'}) {
eval "Net::NNTP->can_ssl";
if ($@) {
warn "Your Net::NNTP doesn't support SSL.\n" if ($config{'debug'} || $config{'verbose'});
$config{'ssl'} = 0;
}
}
# and now adjust default port depending on SSL requested and
# available or not
if ($config{'ssl'}) {
$config{'nntp-port'} = 563 if ($config{'nntp-port'} == 119);
} else {
$config{'nntp-port'} = 119 if ($config{'nntp-port'} == 563);
}
# Cancel-Locks require some more modules
my $sha_mod = undef;
if ($config{'canlock-secret'} && !$config{'no-canlock'}) {
$config{'canlock-algorithm'} = lc($config{'canlock-algorithm'});
# we support sha1, sha256 and sha512, fallback to sha1 if something else is given
if (!($config{'canlock-algorithm'} =~ /^sha(1|256|512)$/)) {
warn "Digest algorithm " . $config{'canlock-algorithm'} . " not supported. Falling back to sha1.\n" if ($config{'debug'} || $config{'verbose'});
$config{'canlock-algorithm'} = 'sha1';
}
if ($config{'canlock-algorithm'} eq 'sha1') {
foreach ('Digest::SHA qw(sha1)', 'Digest::SHA1()') {
eval "use $_";
if (!$@) {
($sha_mod = $_) =~ s#( qw\(sha1\)|\(\))##;
last;
}
}
foreach ('MIME::Base64()', 'Digest::HMAC_SHA1()') {
eval "use $_";
if ($@ || !defined($sha_mod)) {
$config{'no-canlock'} = 1;
warn "Cancel-Locks disabled: Can't locate ".$_." (".__FILE__.":".__LINE__.")\n" if ($config{'debug'} || $config{'verbose'});
last;
}
}
} elsif ($config{'canlock-algorithm'} eq 'sha256') {
foreach ('MIME::Base64()', 'Digest::SHA qw(sha256 hmac_sha256)') {
eval "use $_";
if ($@) {
$config{'no-canlock'} = 1;
warn "Cancel-Locks disabled: Can't locate ".$_." (".__FILE__.":".__LINE__.")\n" if ($config{'debug'} || $config{'verbose'});
last;
}
}
} else {
foreach ('MIME::Base64()', 'Digest::SHA qw(sha512 hmac_sha512)') {
eval "use $_";
if ($@) {
$config{'no-canlock'} = 1;
warn "Cancel-Locks disabled: Can't locate ".$_." (".__FILE__.":".__LINE__.")\n" if ($config{'debug'} || $config{'verbose'});
last;
}
}
}
}
my $term = Term::ReadLine->new('tinews');
my $attribs = $term->Attribs;
my $in_header = 1;
my (%Header, @Body, $PGPCommand);
if (! $config{'no-sign'}) {
$config{'pgp-signer'} = $ENV{'SIGNER'} if ($ENV{'SIGNER'});
$config{'path-to-pgp-pass'} = $ENV{'PGPPASSFILE'} if ($ENV{'PGPPASSFILE'});
if ($config{'path-to-pgp-pass'}) {
open(my $pgppass, '<', (glob($config{'path-to-pgp-pass'}))[0]) or
$config{'interactive'} && die("$0: Can't open ".$config{'path-to-pgp-pass'}.": $!");
chomp($config{'pgp-pass'} = <$pgppass>);
close($pgppass);
}
if ($config{'pgp-version'} eq '2' && $ENV{'PGPPASS'}) {
$config{'pgp-pass'} = $ENV{'PGPPASS'};
}
}
# Remove unwanted headers from pgp-sign-headers
if (${config{'ignore-headers'}}) {
my @hdr_to_ignore = split(/,/, ${config{'ignore-headers'}});
foreach my $hdr (@hdr_to_ignore) {
@{$config{'pgp-sign-headers'}} = map {lc($_) eq lc($hdr) ? () : $_} @{$config{'pgp-sign-headers'}};
}
}
# Read the message and split the header
readarticle(\%Header, \@Body);
# empty @Body
if (scalar @Body == 0) {
warn("Empty article\n") if ($config{'verbose'});
exit 0 if ($config{'discard-empty'});
}
# Add signature if there is none
if (!$config{'no-signature'}) {
if ($config{'add-signature'} && !grep {/^-- /} @Body) {
if (-r glob($config{'sig-path'})) {
my $l = 0;
push @Body, "-- \n";
open(my $SIGNATURE, '<', glob($config{'sig-path'})) or die("Can't open " . $config{'sig-path'} . ": $!");
while (<$SIGNATURE>) {
die $config{'sig-path'} . " longer than " . $config{'sig-max-lines'}. " lines!" if (++$l > $config{'sig-max-lines'});
push @Body, $_;
}
close($SIGNATURE);
} else {
warn "Tried to add " . $config{'sig-path'} . ", but it is unreadable.\n" if ($config{'debug'} || $config{'verbose'});
}
}
}
# import headers set in the environment
if (!defined($Header{'reply-to'})) {
if ($ENV{'REPLYTO'}) {
chomp($Header{'reply-to'} = "Reply-To: " . $ENV{'REPLYTO'});
$Header{'reply-to'} .= "\n";
}
}
foreach ('DISTRIBUTION', 'ORGANIZATION') {
if (!defined($Header{lc($_)}) && $ENV{$_}) {
chomp($Header{lc($_)} = ucfirst($_).": " . $ENV{$_});
$Header{lc($_)} .= "\n";
}
}
# overwrite headers if specified via cmd-line
foreach ('Approved', 'Control', 'Distribution', 'Expires',
'From', 'Followup-To', 'Message-ID', 'Newsgroups', 'Reply-To',
'Subject', 'References', 'Organization') {
next if (!defined($config{lc($_)}));
chomp($Header{lc($_)} = $_ . ": " . $config{lc($_)});
$Header{lc($_)} .= "\n";
}
# -x doesn't overwrite but prefixes
if (defined($config{'path'})) {
if (defined($Header{'path'})) {
(my $pbody = $Header{'path'}) =~ s#^Path: ##i;
chomp($Header{'path'} = "Path: " . $config{'path'} . "!" . $pbody);
} else {
chomp($Header{'path'} = "Path: " . $config{'path'});
}
$Header{'path'} .= "\n";
}
# verify/add/remove headers
foreach ('From', 'Subject') {
die("$0: No $_:-header defined.") if (!defined($Header{lc($_)}));
}
$Header{'date'} = "Date: ".getdate()."\n" if (!defined($Header{'date'}) || $Header{'date'} !~ m/^[^\s:]+: .+/o);
$Header{'injection-date'} = "Injection-Date: ".getdate()."\n" if (!$config{'no-injection-date'});
if (defined($Header{'user-agent'})) {
chomp $Header{'user-agent'};
$Header{'user-agent'} = $Header{'user-agent'}." ".$pname."/".$version."\n";
}
delete $Header{'x-pgp-key'} if (!$config{'no-sign'} && defined($Header{'x-pgp-key'}));
delete $Header{'organization'} if ($config{'no-organization'} && defined($Header{'organization'}));
# No control. No control. You have no control.
if ($config{'no-control'} and $Header{control}) {
print STDERR "No control messages allowed.\n";
exit 1;
}
# various checks
if ($config{'debug'} || $config{'verbose'}) {
foreach (keys %Header) {
warn "Raw 8-bit data in the following header:\n$Header{$_}\n" if ($Header{$_} =~ m/[\x80-\xff]/o);
}
# do not check for CTE as it's not required for miltipart/*
if (!defined($Header{'mime-version'}) || !defined($Header{'content-type'})) {
warn "8bit body without MIME-headers\n" if (grep {/[\x80-\xff]/} @Body);
}
}
# try ~/.newsauth if no $config{'nntp-pass'} was set
if (!$config{'nntp-pass'}) {
my ($l, $server, $pass, $user);
if (-r (glob("~/.newsauth"))[0]) {
open (my $NEWSAUTH, '<', (glob("~/.newsauth"))[0]) or die("Can't open ~/.newsauth: $!");
while ($l = <$NEWSAUTH>) {
next if ($l =~ m/^([#\s]|$)/);
chomp $l;
$user = $pass = $server = undef;
if ($l =~ m/^
(\S+)\s+ # server
("(?:[^"]+)"|(?:\S+)) # password
\s+("(?:[^"]+)"|(?:\S+)) # user
/x) {
$server = $1;
$pass = $2;
$user = $3;
if ($pass =~ m/^"([^"]+)"/) { # strip enclising "
$pass = $1;
}
if ($user =~ m/^"([^"]+)"/) { # likewise
$user = $1;
}
} else { # server passwrd
if ($l =~ m/^(\S+)\s+("(?:[^"]+)"|(?:\S+))/) {
$server = $1;
$pass = $2;
if ($pass =~ m/^"([^"]+)"/) { # likewise
$pass = $1;
}
}
}
last if ($server =~ m/\Q$config{'nntp-server'}\E/);
}
close($NEWSAUTH);
if ($pass && $server =~ m/\Q$config{'nntp-server'}\E/) {
$config{'nntp-pass'} = $pass;
$config{'nntp-user'} = $user || getlogin || getpwuid($<) || $ENV{USER};
} else {
$pass = $user = "";
}
}
# try ~/.nntpauth if we still got no password
if (!$pass) {
if (-r (glob("~/.nntpauth"))[0]) {
open (my $NNTPAUTH, '<', (glob("~/.nntpauth"))[0]) or die("Can't open ~/.nntpauth: $!");
while ($l = <$NNTPAUTH>) {
chomp $l;
next if ($l =~ m/(^[#\s]|)/);
($server, $user, $pass) = split(/\s+\b/, $l);
last if ($server =~ m/\Q$config{'nntp-server'}\E/);
}
close($NNTPAUTH);
if ($pass && $server =~ m/\Q$config{'nntp-server'}\E/) {
$config{'nntp-pass'} = $pass;
$config{'nntp-user'} = $user || getlogin || getpwuid($<) || $ENV{USER};
}
}
}
}
# instead of abort posting just to prefetch a Messsage-ID we should (try
# to) keep the session open instead
if (!($config{'no-sign'} && $config{'no-canlock'})) {
if (! $config{'savedir'} && defined($Header{'newsgroups'}) && !defined($Header{'message-id'})) {
my $Server = AuthonNNTP();
my $ServerMsg = $Server->message();
$Header{'message-id'} = "Message-ID: $1\n" if ($ServerMsg =~ m/(<\S+\@\S+>)/o);
#$Server->datasend('.'); # dataend() already sends "."
$Server->dataend();
$Server->quit();
}
if (!defined($Header{'message-id'})) {
my $hname;
if (eval { require Sys::Hostname;1; } != 1) {
chomp($hname = `hostname`);
} else {
use Sys::Hostname;
$hname = hostname();
}
my ($hostname,) = gethostbyname($hname);
if (defined($hostname) && $hostname =~ m/\./io) {
$Header{'message-id'} = "Message-ID: " . sprintf("<N%xI%xT%x@%s>\n", $>, timelocal(localtime), $$, $hostname);
}
}
}
# add Cancel-Lock (and Cancel-Key) header(s) if requested
if ($config{'canlock-secret'} && !$config{'no-canlock'} && defined($Header{'message-id'})) {
open(my $CANLock, '<', (glob($config{'canlock-secret'}))[0]) or die("$0: Can't open " . $config{'canlock-secret'} . ": $!");
chomp(my $key = <$CANLock>);
close($CANLock);
(my $data = $Header{'message-id'}) =~ s#^Message-ID: ##i;
chomp $data;
my $cancel_key = buildcancelkey($data, $key);
my $cancel_lock = buildcancellock($cancel_key, $sha_mod);
if (defined($Header{'cancel-lock'})) {
chomp $Header{'cancel-lock'};
$Header{'cancel-lock'} .= " " . $config{'canlock-algorithm'} . ":" . $cancel_lock . "\n";
} else {
$Header{'cancel-lock'} = "Cancel-Lock: " . $config{'canlock-algorithm'} . ":" . $cancel_lock . "\n";
}
if ((defined($Header{'supersedes'}) && $Header{'supersedes'} =~ m/^Supersedes:\s+<\S+>\s*$/i) || (defined($Header{'control'}) && $Header{'control'} =~ m/^Control:\s+cancel\s+<\S+>\s*$/i) ||(defined($Header{'also-control'}) && $Header{'also-control'} =~ m/^Also-Control:\s+cancel\s+<\S+>\s*$/i)) {
if (defined($Header{'also-control'}) && $Header{'also-control'} =~ m/^Also-Control:\s+cancel\s+/i) {
($data = $Header{'also-control'}) =~ s#^Also-Control:\s+cancel\s+##i;
chomp $data;
$cancel_key = buildcancelkey($data, $key);
} else {
if (defined($Header{'control'}) && $Header{'control'} =~ m/^Control: cancel /i) {
($data = $Header{'control'})=~ s#^Control:\s+cancel\s+##i;
chomp $data;
$cancel_key = buildcancelkey($data, $key);
} else {
if (defined($Header{'supersedes'})) {
($data = $Header{'supersedes'}) =~ s#^Supersedes: ##i;
chomp $data;
$cancel_key = buildcancelkey($data, $key);
}
}
}
if (defined($Header{'cancel-key'})) {
chomp $Header{'cancel-key'};
$Header{'cancel-key'} .= " " . $config{'canlock-algorithm'} . ":" . $cancel_key . "\n";
} else {
$Header{'cancel-key'} = "Cancel-Key: " . $config{'canlock-algorithm'} . ":" . $cancel_key . "\n";
}
}
}
# set Posted-And-Mailed if we send a mailcopy to someone else
if ($config{'sendmail'} && defined($Header{'newsgroups'}) && (defined($Header{'to'}) || defined($Header{'cc'}) || defined($Header{'bcc'}))) {
foreach ('to', 'bcc', 'cc') {
if (defined($Header{$_}) && $Header{$_} ne $Header{'from'}) {
$Header{'posted-and-mailed'} = "Posted-And-Mailed: yes\n";
last;
}
}
}
if (! $config{'no-sign'}) {
if (!$config{'pgp-signer'}) {
chomp($config{'pgp-signer'} = $Header{'from'});
$config{'pgp-signer'} =~ s/^[^\s:]+: (.*)/$1/;
}
$PGPCommand = getpgpcommand($config{'pgp-version'});
}
# exit with error if neither $Newsgroups nor any of $To, $Cc or $Bcc are set
my $required = 0;
foreach ('Newsgroups', 'To', 'Cc', 'Bcc') {
$required++ if (defined($Header{lc($_)}));
last if $required;
}
die("$0: neither Newsgroups: nor any of To:, Cc:, or Bcc: present.\n") if (!$required);
# (re)move mail-headers
my ($To, $Cc, $Bcc, $Newsgroups) = '';
$To = $Header{'to'} if (defined($Header{'to'}));
$Cc = $Header{'cc'} if (defined($Header{'cc'}));
$Bcc = $Header{'bcc'} if (defined($Header{'bcc'}));
delete $Header{$_} foreach ('to', 'cc', 'bcc');
$Newsgroups = $Header{'newsgroups'} if (defined($Header{'newsgroups'}));
my $MessageR = [];
if ($config{'no-sign'}) {
# don't sign article
push @$MessageR, $Header{$_} for (keys %Header);
push @$MessageR, "\n", @Body;
} else {
# sign article
$MessageR = signarticle(\%Header, \@Body);
}
# post or save article
if (! $config{'savedir'}) {
postarticle($MessageR) if ($Newsgroups);
} else {
savearticle($MessageR) if ($Newsgroups);
}
# mail article
if (($To || $Cc || $Bcc) && $config{'sendmail'}) {
open(my $MAIL, '|-', $config{'sendmail'}) || die("$!");
unshift @$MessageR, "$To" if ($To);
unshift @$MessageR, "$Cc" if ($Cc);
unshift @$MessageR, "$Bcc" if ($Bcc);
print($MAIL @$MessageR);
close($MAIL);
}
# Game over. Insert new coin.
exit;
#-------- sub readarticle
#
sub readarticle {
my ($HeaderR, $BodyR) = @_;
my $currentheader;
my $l = 0;
while (defined($_ = <>)) {
s#\r\n$#\n# if ($config{'transform'});
if ($in_header) {
use bytes;
if (m/^$/o) { #end of header
$in_header = 0;
} elsif (m/^([^\s:]+): (.*)$/s) {
$currentheader = lc($1);
$$HeaderR{$currentheader} = "$1: $2";
$l = length($_);
print "" . $1 . ":-header exceeds line length limit " . $l . " > " . $config{'max-header-length'} . " octets.\n" if (($config{'verbose'} || $config{'debug'}) && length($_) > $config{'max-header-length'});
} elsif (m/^[ \t]/o) {
$$HeaderR{$currentheader} .= $_;
$l = length($_);
print "Part of continued " . ucfirst($currentheader) . ":-header exceeds line length limit " . $l . " > " . $config{'max-header-length'} . " octets.\n" if (($config{'verbose'} || $config{'debug'}) && $l > $config{'max-header-length'});
# } elsif (m/^([^\s:]+):$/) { # skip over empty headers
# next;
} else {
chomp($_);
# TODO: quote esc. sequences?
die("'$_' is not a correct header-line");
}
} else {
push @$BodyR, $_;
}
}
return;
}
#-------- sub getdate
# getdate generates a date and returns it.
#
sub getdate {
my @time = localtime;
my $ss = ($time[0]<10) ? "0".$time[0] : $time[0];
my $mm = ($time[1]<10) ? "0".$time[1] : $time[1];
my $hh = ($time[2]<10) ? "0".$time[2] : $time[2];
my $day = $time[3];
# my $month = ($time[4]+1 < 10) ? "0".($time[4]+1) : $time[4]+1; # 01...12; unused
my $monthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$time[4]];
my $wday = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$time[6]];
my $year = $time[5] + 1900;
my $offset = timelocal(localtime) - timelocal(gmtime);
my $sign ="+";
if ($offset < 0) {
$sign ="-";
$offset *= -1;
}
my $offseth = int($offset/3600);
my $offsetm = int(($offset - $offseth*3600)/60);
my $tz = sprintf("%s%0.2d%0.2d", $sign, $offseth, $offsetm);
return "$wday, $day $monthN $year $hh:$mm:$ss $tz";
}
#-------- sub AuthonNNTP
# AuthonNNTP opens the connection to a Server and returns a Net::NNTP-Object.
#
# User, Password and Server are defined before as elements
# of the global hash %config. If no values for user or password
# are defined, the sub will try to ask the user (only if
# $config{'interactive'} is != 0).
sub AuthonNNTP {
my $Server = Net::NNTP->new(
Host => $config{'nntp-server'},
Reader => 1,
Debug => $config{'debug'},
Port => $config{'nntp-port'},
Timeout => $config{'timeout'},
Domain => ($config{'ipv4'} ? AF_INET : undef),
SSL => $config{'ssl'},
SSL_verify_mode => 0
) or die("$0: Can't connect to ".$config{'nntp-server'}.":".$config{'nntp-port'}."!\n");
if ($config{'debug'}) {
printf("Connected to : ".$Server->peerhost.":".$Server->peerport." [%s]\n", ($Server->sockdomain == PF_INET) ? "IPv4" : "IPv6");
if ($config{'ssl'}) {
printf("SSL_fingerprint: %s %s\n", split(/\$/, $Server->get_fingerprint));
}
}
my $ServerMsg = $Server->message();
my $ServerCod = $Server->code();
# no read and/or write access - give up
if ($ServerCod < 200 || $ServerCod > 201) {
$Server->quit();
die($0.": ".$ServerCod." ".$ServerMsg."\n");
}
# read access - try auth
if ($ServerCod == 201 || $config{'force-auth'}) {
if ($config{'nntp-pass'} eq "") {
if ($config{'interactive'}) {
$config{'nntp-user'} = $term->readline("Your Username at ".$config{'nntp-server'}.": ");
$attribs->{redisplay_function} = $attribs->{shadow_redisplay};
$config{'nntp-pass'} = $term->readline("Password for ".$config{'nntp-user'}." at ".$config{'nntp-server'}.": ");
} else {
$Server->quit();
die($0.": ".$ServerCod." ".$ServerMsg."\n");
}
}
$Server->authinfo($config{'nntp-user'}, $config{'nntp-pass'});
$ServerCod = $Server->code();
$ServerMsg = $Server->message();
if ($ServerCod != 281) { # auth failed
$Server->quit();
die $0.": ".$ServerCod." ".$ServerMsg."\n";
}
}
$Server->post();
$ServerCod = $Server->code();
if ($ServerCod == 480) {
if ($config{'nntp-pass'} eq "") {
if ($config{'interactive'}) {
$config{'nntp-user'} = $term->readline("Your Username at ".$config{'nntp-server'}.": ");
$attribs->{redisplay_function} = $attribs->{shadow_redisplay};
$config{'nntp-pass'} = $term->readline("Password for ".$config{'nntp-user'}." at ".$config{'nntp-server'}.": ");
} else {
$ServerMsg = $Server->message();
$Server->quit();
die($0.": ".$ServerCod." ".$ServerMsg."\n");
}
}
$Server->authinfo($config{'nntp-user'}, $config{'nntp-pass'});
$Server->post();
}
return $Server;
}
#-------- sub getpgpcommand
# getpgpcommand generates the command to sign the message and returns it.
#
# Receives:
# - $pgpversion: A scalar holding the pgp-version
sub getpgpcommand {
my ($pgpversion) = @_;
my $found = 0;
if ($config{'pgp'} !~ /^\//) {
foreach(split(/:/, $ENV{'PATH'})) {
if (-x $_."/".$config{'pgp'}) {
$found++;
last;
}
}
}
if (!-x $config{'pgp'} && ! $found) {
warn "PGP signing disabled: Can't locate executable ".$config{'pgp'}."\n" if ($config{'debug'} || $config{'verbose'});
$config{'no-sign'} = 1;
}
if ($pgpversion eq '2') {
if ($config{'pgp-pass'}) {
$PGPCommand = "PGPPASS=\"".$config{'pgp-pass'}."\" ".$config{'pgp'}." -z -u \"".$config{'pgp-signer'}."\" +verbose=0 language='en' -saft <".$config{'pgptmpf'}.".txt >".$config{'pgptmpf'}.".txt.asc";
} elsif ($config{'interactive'}) {
$PGPCommand = $config{'pgp'}." -z -u \"".$config{'pgp-signer'}."\" +verbose=0 language='en' -saft <".$config{'pgptmpf'}.".txt >".$config{'pgptmpf'}.".txt.asc";
} else {
die("$0: Passphrase is unknown!\n");
}
} elsif ($pgpversion eq '5') {
if ($config{'path-to-pgp-pass'}) {
$PGPCommand = "PGPPASSFD=".$config{'pgp-pass-fd'}." ".$config{'pgp'}."s -u \"".$config{'pgp-signer'}."\" -t --armor -o ".$config{'pgptmpf'}.".txt.asc -z -f < ".$config{'pgptmpf'}.".txt ".$config{'pgp-pass-fd'}."<".$config{'path-to-pgp-pass'};
} elsif ($config{'interactive'}) {
$PGPCommand = $config{'pgp'}."s -u \"".$config{'pgp-signer'}."\" -t --armor -o ".$config{'pgptmpf'}.".txt.asc -z -f < ".$config{'pgptmpf'}.".txt";
} else {
die("$0: Passphrase is unknown!\n");
}
} elsif ($pgpversion eq '6') { # this is untested
if ($config{'path-to-pgp-pass'}) {
$PGPCommand = "PGPPASSFD=".$config{'pgp-pass-fd'}." ".$config{'pgp'}." -u \"".$config{'pgp-signer'}."\" -saft -o ".$config{'pgptmpf'}.".txt.asc < ".$config{'pgptmpf'}.".txt ".$config{'pgp-pass-fd'}."<".$config{'path-to-pgp-pass'};
} elsif ($config{'interactive'}) {
$PGPCommand = $config{'pgp'}." -u \"".$config{'pgp-signer'}."\" -saft -o ".$config{'pgptmpf'}.".txt.asc < ".$config{'pgptmpf'}.".txt";
} else {
die("$0: Passphrase is unknown!\n");
}
} elsif ($pgpversion =~ m/GPG1?$/io) {
if ($config{'path-to-pgp-pass'}) {
$PGPCommand = $config{'pgp'}." --emit-version --digest-algo $config{'digest-algo'} -a -u \"".$config{'pgp-signer'}."\" -o ".$config{'pgptmpf'}.".txt.asc --no-tty --batch --passphrase-fd ".$config{'pgp-pass-fd'}." ".$config{'pgp-pass-fd'}."<".$config{'path-to-pgp-pass'}." --clearsign ".$config{'pgptmpf'}.".txt";
} elsif ($config{'interactive'}) {
$PGPCommand = $config{'pgp'}." --emit-version --digest-algo $config{'digest-algo'} -a -u \"".$config{'pgp-signer'}."\" -o ".$config{'pgptmpf'}.".txt.asc --no-secmem-warning --no-batch --clearsign ".$config{'pgptmpf'}.".txt";
} else {
die("$0: Passphrase is unknown!\n");
}
} elsif ($pgpversion =~ m/GPG2$/io) {
if ($config{'path-to-pgp-pass'}) {
$PGPCommand = $config{'pgp'}." --pinentry-mode loopback --emit-version --digest-algo $config{'digest-algo'} -a -u \"".$config{'pgp-signer'}."\" -o ".$config{'pgptmpf'}.".txt.asc --no-tty --batch --passphrase-fd ".$config{'pgp-pass-fd'}." ".$config{'pgp-pass-fd'}."<".$config{'path-to-pgp-pass'}." --clearsign ".$config{'pgptmpf'}.".txt";
} elsif ($config{'interactive'}) {
$PGPCommand = $config{'pgp'}." --emit-version --digest-algo $config{'digest-algo'} -a -u \"".$config{'pgp-signer'}."\" -o ".$config{'pgptmpf'}.".txt.asc --no-secmem-warning --no-batch --clearsign ".$config{'pgptmpf'}.".txt";
} else {
die("$0: Passphrase is unknown!\n");
}
} else {
die("$0: Unknown PGP-Version $pgpversion!");
}
return $PGPCommand;
}
#-------- sub postarticle
# postarticle posts your article to your Newsserver.
#
# Receives:
# - $ArticleR: A reference to an array containing the article
sub postarticle {
my ($ArticleR) = @_;
my $Server = AuthonNNTP();
my $ServerCod = $Server->code();
my $ServerMsg = $Server->message();
if ($ServerCod == 340) {
$Server->datasend(@$ArticleR);
## buggy Net::Cmd < 2.31
$Server->set_status(200, "");
$Server->dataend();
$ServerCod = $Server->code();
$ServerMsg = $Server->message();
if (! $Server->ok()) {
$Server->quit();
die("\n$0: Posting failed! Response from news server:\n", $ServerCod, ' ', $ServerMsg);
}
$Server->quit();
} else {
die("\n$0: Posting failed! Response from news server:\n", $ServerCod, ' ', $ServerMsg);
}
return;
}
#-------- sub savearticle
# savearticle saves your article to the directory $config{'savedir'}
#
# Receives:
# - $ArticleR: A reference to an array containing the article
sub savearticle {
my ($ArticleR) = @_;
my $timestamp = timelocal(localtime);
(my $ng = $Newsgroups) =~ s#^Newsgroups:\s*([^,\s]+).*#$1#i;
my $gn = join "", map { substr($_,0,1) } (split(/\./, $ng));
my $filename = $config{'savedir'}."/".$timestamp."-".$gn."-".$$;
open(my $SH, '>', $filename) or die("$0: can't open $filename: $!\n");
print $SH @$ArticleR;
close($SH) or warn "$0: Couldn't close: $!\n";
return;
}
#-------- sub signarticle
# signarticle signs an article and returns a reference to an array
# containing the whole signed Message.
#
# Receives:
# - $HeaderR: A reference to a hash containing the articles headers.
# - $BodyR: A reference to an array containing the body.
#
# Returns:
# - $MessageRef: A reference to an array containing the whole message.
sub signarticle {
my ($HeaderR, $BodyR) = @_;
my (@pgp_head, @pgp_body, @sign_headers, $pgphead, $pgpbody, $signheaders);
foreach (@{$config{'pgp-sign-headers'}}) {
if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
push @sign_headers, $_;
}
}
$pgpbody = join("", @$BodyR);
# Delete and create the temporary pgp-Files
unlink $config{'pgptmpf'}.".txt";
unlink $config{'pgptmpf'}.".txt.asc";
$signheaders = join(",", @sign_headers);
$pgphead = "X-Signed-Headers: $signheaders\n";
foreach my $header (@sign_headers) {
if ($$HeaderR{lc($header)} =~ m/^[^\s:]+: (.+?)\n?$/so) {
$pgphead .= $header.": ".$1."\n";
}
}
unless (substr($pgpbody, -1, 1) =~ /\n/) {$pgpbody .= "\n"};
open(my $FH, '>', $config{'pgptmpf'} . ".txt") or die("$0: can't open ".$config{'pgptmpf'}.": $!\n");
print $FH $pgphead, "\n", $pgpbody;
print $FH "\n" if ($config{'pgp-version'} =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
close($FH) or warn "$0: Couldn't close TMP: $!\n";
# Start PGP, then read the signature;
`$PGPCommand`;
open($FH, '<', $config{'pgptmpf'} . ".txt.asc") or die("$0: can't open ".$config{'pgptmpf'}.".txt.asc: $!\n");
local $/ = "\n".$config{'pgpbegin'}."\n";
$_ = <$FH>;
unless (m/\Q$config{'pgpbegin'}\E$/o) {
unlink $config{'pgptmpf'} . ".txt";
unlink $config{'pgptmpf'} . ".txt.asc";
close($FH);
die("$0: ".$config{'pgpbegin'}." not found in ".$config{'pgptmpf'}.".txt.asc\n");
}
unlink($config{'pgptmpf'} . ".txt") or warn "$0: Couldn't unlink ".$config{'pgptmpf'}.".txt: $!\n";
local $/ = "\n";
$_ = <$FH>;
unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
unlink $config{'pgptmpf'} . ".txt.asc";
close($FH);
die("$0: didn't find PGP Version line where expected.\n");
}
if (defined($2)) {
$$HeaderR{$config{'pgpheader'}} = $1."-".$2." ".$signheaders;
} else {
$$HeaderR{$config{'pgpheader'}} = $1." ".$signheaders;
}
do { # skip other pgp headers like
$_ = <$FH>; # "charset:"||"comment:" until empty line
} while ! /^$/;
while (<$FH>) {
chomp;
last if /^\Q$config{'pgpend'}\E$/;
$$HeaderR{$config{'pgpheader'}} .= "\n\t$_";
}
$$HeaderR{$config{'pgpheader'}} .= "\n" unless ($$HeaderR{$config{'pgpheader'}} =~ /\n$/s);
$_ = <$FH>;
unless (eof($FH)) {
unlink $config{'pgptmpf'} . ".txt.asc";
close($FH);
die("$0: unexpected data following ".$config{'pgpend'}."\n");
}
close($FH);
unlink $config{'pgptmpf'} . ".txt.asc";
my $tmppgpheader = $config{'pgpheader'} . ": " . $$HeaderR{$config{'pgpheader'}};
delete $$HeaderR{$config{'pgpheader'}};
@pgp_head = ();
foreach my $header (@{$config{'pgp-order-headers'}}) {
if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
push(@pgp_head, "$$HeaderR{$header}");
delete $$HeaderR{$header};
}
}
foreach my $header (keys %$HeaderR) {
if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
push(@pgp_head, "$$HeaderR{$header}");
delete $$HeaderR{$header};
}
}
push @pgp_head, ("X-PGP-Hash: " . $config{'digest-algo'} . "\n") if (defined($config{'digest-algo'}));
push @pgp_head, ("X-PGP-Key: " . $config{'pgp-signer'} . "\n"), $tmppgpheader;
undef $tmppgpheader;
@pgp_body = split(/$/m, $pgpbody);
my @pgpmessage = (@pgp_head, "\n", @pgp_body);
return \@pgpmessage;
}
#-------- sub buildcancelkey
# buildcancelkey builds the cancel-key based on the configured HASH algorithm.
#
# Receives:
# - $data: The input data.
# - $key: The secret key to be used.
#
# Returns:
# - $cancel_key: The calculated cancel-key.
sub buildcancelkey {
my ($data, $key) = @_;
my $cancel_key;
if ($config{'canlock-algorithm'} eq 'sha1') {
$cancel_key = MIME::Base64::encode(Digest::HMAC_SHA1::hmac_sha1($data, $key), '');
} elsif ($config{'canlock-algorithm'} eq 'sha256') {
$cancel_key = MIME::Base64::encode(Digest::SHA::hmac_sha256($data, $key), '');
} else {
$cancel_key = MIME::Base64::encode(Digest::SHA::hmac_sha512($data, $key), '');
}
return $cancel_key;
}
#-------- sub buildcancellock
# buildcancellock builds the cancel-lock based on the configured HASH algorithm
# and the given cancel-key.
#
# Receives:
# - $sha_mod: A hint which module to be used for sha1.
# - $cancel_key: The cancel-key for which the lock has to be calculated.
#
# Returns:
# - $cancel_lock: The calculated cancel-lock.
sub buildcancellock {
my ($cancel_key, $sha_module) = @_;
my $cancel_lock;
if ($config{'canlock-algorithm'} eq 'sha1') {
if ($sha_module =~ m/SHA1/) {
$cancel_lock = MIME::Base64::encode(Digest::SHA1::sha1($cancel_key, ''), '');
} else {
$cancel_lock = MIME::Base64::encode(Digest::SHA::sha1($cancel_key, ''), '');
}
} elsif ($config{'canlock-algorithm'} eq 'sha256') {
$cancel_lock = MIME::Base64::encode(Digest::SHA::sha256($cancel_key, ''), '');
} else {
$cancel_lock = MIME::Base64::encode(Digest::SHA::sha512($cancel_key, ''), '');
}
return $cancel_lock;
}
sub version {
print "".$pname." ".$version."\n";
return;
}
sub usage {
version();
print "Usage: ".$pname." [OPTS] < article\n";
print " -4 force connecting via IPv4\n";
print " -a string set Approved:-header to string\n";
print " -c string set Control:-header to string\n";
print " -d string set Distribution:-header to string\n";
print " -e string set Expires:-header to string\n";
print " -f string set From:-header to string\n";
print " -i string list of headers to be ignored for signing\n";
print " -m string set Message-ID:-header to string\n";
print " -n string set Newsgroups:-header to string\n";
print " -o string set Organization:-header to string\n";
print " -p port use port as NNTP port [default=".$config{'nntp-port'}."]\n";
print " -r string set Reply-To:-header to string\n";
print " -s string save signed article to directory string instead of posting\n";
print " -t string set Subject:-header to string\n";
print " -v show warnings about missing/disabled features\n";
print " -w string set Followup-To:-header to string\n";
print " -x string prepend Path:-header with string\n";
print " -D enable debugging\n";
print " -E silently discard empty article\n";
print " -F string set References:-header to string\n";
print " -H show help\n";
print " -I do not add Injection-Date: header\n";
print " -L do not add Cancel-Lock: / Cancel-Key: headers\n";
print " -O do not add Organization:-header\n";
print " -R disallow control messages\n";
print " -S do not append " . $config{'sig-path'} . "\n";
print " -T seconds set connection timeout to seconds\n";
print " -X do not sign article\n";
print " -Y force authentication on connect\n";
print " --canlock-algorithm string\n";
print " digest algorithm for Cancel-Lock (sha1, sha256 or sha512)\n";
print " --ssl use NNTPS (via port 563) if available\n";
print " --transform convert <CR><LF> to <LF>\n";
print " --version show version\n";
printf("\nAvailable tinewsrc-vars: %s\n", join(", ", sort keys %config)) if ($config{'verbose'} || $config{'debug'});
exit 0;
}
__END__
=head1 NAME
tinews.pl - Post and sign an article via NNTP
=head1 SYNOPSIS
B<tinews.pl> [B<OPTIONS>] E<lt> I<input>
=head1 DESCRIPTION
B<tinews.pl> reads an article on STDIN, signs it via L<pgp(1)> or
L<gpg(1)> and posts it to a news server.
The article shall not contain any raw 8-bit data or it needs to
already have the relevant MIME-headers as B<tinews.pl> will not
add any MIME-headers nor encode its input.
If the article contains To:, Cc: or Bcc: headers and mail-actions are
configured it will automatically add a "Posted-And-Mailed: yes" header
to the article and send out the mail-copies.
If a Cancel-Lock secret file is defined it will automatically add a
Cancel-Lock: (and Cancel-Key: if required) header.
The input should have unix line endings (<LF>, '\n'). Use --B<transform>
to convert from <CR><LF> to just <LF>.
=head1 OPTIONS
X<tinews, command-line options>
=over 4
=item -B<4> | --B<ipv4>
X<-4> X<--iv4>
Force connecting via IPv4 to the remote NNTP server.
=item -B<a> C<Approved> | --B<approved> C<Approved>
X<-a> X<--approved>
Set the article header field Approved: to the given value.
=item -B<c> C<Control> | --B<control> C<Control>
X<-c> X<--control>
Set the article header field Control: to the given value.
=item -B<d> C<Distribution> | --B<distribution> C<Distribution>
X<-d> X<--distribution>
Set the article header field Distribution: to the given value.
=item -B<e> C<Expires> | --B<expires> C<Expires>
X<-e> X<--expires>
Set the article header field Expires: to the given value.
=item -B<f> C<From> | --B<from> C<From>
X<-f> X<--from>
Set the article header field From: to the given value.
=item -B<i> F<header> | --B<ignore-headers> F<header>
X<-i> X<--ignore-headers>
Comma separated list of headers that will be ignored during signing.
Usually the following headers will be signed if present:
From, Newsgroups, Subject, Control, Supersedes, Followup-To,
Date, Injection-Date, Sender, Approved, Message-ID, Reply-To,
Cancel-Key, Also-Control and Distribution.
Some of them may be altered on the Server (i.e. Cancel-Key) which would
invalid the signature, this option can be used the exclude such headers
if required.
=item -B<m> C<Message-ID> | --B<message-id> C<Message-ID>
X<-m> X<--message-id>
Set the article header field Message-ID: to the given value.
=item -B<n> C<Newsgroups> | --B<newsgroups> C<Newsgroups>
X<-n> X<--newsgroups>
Set the article header field Newsgroups: to the given value.
=item -B<o> C<Organization> | --B<organization> C<Organization>
X<-o> X<--organization>
Set the article header field Organization: to the given value.
=item -B<p> C<port> | --B<port> C<port>
X<-p> X<--port>
use C<port> as NNTP-port
=item -B<r> C<Reply-To> | --B<reply-to> C<Reply-To>
X<-r> X<--reply-to>
Set the article header field Reply-To: to the given value.
=item -B<s> F<directory> | --B<savedir> F<directory>
X<-s> X<--savedir>
Save signed article to directory F<directory> instead of posting.
=item -B<t> C<Subject> | --B<subject> C<Subject>
X<-t> X<--subject>
Set the article header field Subject: to the given value.
=item -B<v> | --B<verbose>
X<-v> X<--verbose>
Warn about disabled options due to lacking perl-modules or executables and
unreadable files and enable warnings about raw 8-bit data.
=item -B<w> C<Followup-To> | --B<followup-to> C<Followup-To>
X<-w> X<--followup-to>
Set the article header field Followup-To: to the given value.
=item -B<x> C<Path> | --B<path> C<Path>
X<-x> X<--path>
Prepend the article header field Path: with the given value.
=item -B<D> | -B<N> | --B<debug>
X<-D> X<-N> X<--debug>
Set L<Net::NNTP(3pm)> to debug mode, enable warnings about raw 8-bit data,
warn about disabled options due to lacking perl-modules or executables and
unreadable files.
=item -B<E> | --B<discard-empty>
X<-E> X<--discard-empty>
Silently discard an empty article.
=item -B<F> | --B<references>
X<-F> X<--references>
Set the article header field References: to the given value.
=item -B<H> | --B<help>
X<-H> X<--help>
Show help-page.
=item -B<I> | --B<no-injection-date>
X<-I> X<--no-injection-date>
Do not add Injection-Date: header.
=item -B<L> | --B<no-canlock>
X<-L> X<--no-canlock>
Do not add Cancel-Lock: / Cancel-Key: headers.
=item -B<O> | --B<no-organization>
X<-O> X<--no-organization>
Do not add Organization: header.
=item -B<R> | --B<no-control>
X<-R> X<--no-control>
Restricted mode, disallow control-messages.
=item -B<S> | --B<no-signature>
X<-s> X<--no-signature>
Do not append F<$HOME/.signature>.
=item -B<T> C<seconds> | --B<timeout> C<seconds>
X<-T> X<--timeout>
Override the connection timeout setting. Default is 120 seconds.
=item -B<X> | --B<no-sign>
X<-X> X<--no-sign>
Do not sign the article.
=item -B<Y> | --B<force-auth>
X<-Y> X<--force-auth>
Force authentication on connect even if not required by the server.
=item --B<canlock-algorithm> C<Algorithm>
X<--canlock-algorithm>
Digest algorithm used for Cancel-Lock: / Cancel-Key: headers.
Supported algorithms are sha1, sha256 and sha512. Default is sha1.
=item --B<ssl> | --B<nntps>
X<--ssl> X<--nntps>
Use NNTPS (via port 563) if available. This requires a recent version
of L<Net::NNTP(3pm)> and L<IO::Socket::SSL(3pm)>. Be aware that no SSL
verification will be done.
=item --B<transform>
X<--transform>
Convert network line endings (<CR><LF>) to unix line endings (<LF>).
=item --B<version>
X<--version>
Show version.
=item -B<A> -B<V> -B<W>
X<-A> X<-V> X<-W>
These options are accepted for compatibility reasons but ignored.
=item -B<h> | --B<headers>
X<-h> X<--headers>
These options are accepted for compatibility reasons but ignored.
=back
=head1 EXIT STATUS
The following exit values are returned:
=over 4
=item S< 0>
Successful completion.
=item S<!=0>
An error occurred.
=back
=head1 ENVIRONMENT
X<tinews, environment variables>
=over 4
=item B<$NEWSHOST>
X<$NEWSHOST> X<NEWSHOST>
Set to override the NNTP server configured in the source or config-file.
It has lower priority than B<$NNTPSERVER> and should be avoided.
=item B<$NNTPSERVER>
X<$NNTPSERVER> X<NNTPSERVER>
Set to override the NNTP server configured in the source or config-file.
This has higher priority than B<$NEWSHOST>.
=item B<$NNTPPORT>
X<$NNTPPORT> X<NNTPPORT>
The NNTP TCP-port to post news to. This variable only needs to be set if the
TCP-port is not 119 (the default). The '-B<p>' command-line option overrides
B<$NNTPPORT>.
=item B<$PGPPASS>
X<$PGPPASS> X<PGPPASS>
Set to override the passphrase configured in the source (used for
L<pgp(1)>-2.6.3).
=item B<$PGPPASSFILE>
X<$PGPPASSFILE> X<PGPPASSFILE>
Passphrase file used for L<pgp(1)> or L<gpg(1)>.
=item B<$SIGNER>
X<$SIGNER> X<SIGNER>
Set to override the user-id for signing configured in the source. If you
neither set B<$SIGNER> nor configure it in the source the contents of the
From:-field will be used.
=item B<$REPLYTO>
X<$REPLYTO> X<REPLYTO>
Set the article header field Reply-To: to the return address specified by
the variable if there isn't already a Reply-To: header in the article.
The '-B<r>' command-line option overrides B<$REPLYTO>.
=item B<$ORGANIZATION>
X<$ORGANIZATION> X<ORGANIZATION>
Set the article header field Organization: to the contents of the variable
if there isn't already an Organization: header in the article. The '-B<o>'
command-line option overrides B<$ORGANIZATION>, The '-B<O>' command-line
option disables it.
=item B<$DISTRIBUTION>
X<$DISTRIBUTION> X<DISTRIBUTION>
Set the article header field Distribution: to the contents of the variable
if there isn't already a Distribution: header in the article. The '-B<d>'
command-line option overrides B<$DISTRIBUTION>.
=back
=head1 FILES
=over 4
=item F<pgptmp.txt>
Temporary file used to store the reformatted article.
=item F<pgptmp.txt.asc>
Temporary file used to store the reformatted and signed article.
=item F<$PGPPASSFILE>
The passphrase file to be used for L<pgp(1)> or L<gpg(1)>.
=item F<$HOME/.signature>
Signature file which will be automatically included.
=item F<$HOME/.cancelsecret>
The passphrase file to be used for Cancel-Locks. This feature is turned
off by default.
=item F<$HOME/.newsauth>
"nntpserver password [user]" pairs or triples for NNTP servers that require
authorization. First match counts. Any line that starts with "#" is a
comment. Blank lines are ignored. This file should be readable only for the
user as it contains the user's unencrypted password for reading news. If no
matching entry is found F<$HOME/.nntpauth> is checked.
=item F<$HOME/.nntpauth>
"nntpserver user password" triples for NNTP servers that require
authorization. First match counts. Lines starting with "#" are skipped and
blank lines are ignored. This file should be readable only for the user as
it contains the user's unencrypted password for reading news.
F<$HOME/.newsauth> is checked first.
=item F<$XDG_CONFIG_HOME/tinewsrc> F<$HOME/.config/tinewsrc> F<$HOME/.tinewsrc>
"option=value" configuration pairs, last match counts and only
"value" is case sensitive. Lines that start with "#" are ignored. If the
file contains unencrypted passwords (e.g. nntp-pass or pgp-pass), it
should be readable for the user only. Use -B<vH> to get a full list of
all available configuration options.
=back
=head1 SECURITY
If you've configured or entered a password, even if the variable that
contained that password has been erased, it may be possible for someone to
find that password, in plaintext, in a core dump. In short, if serious
security is an issue, don't use this script.
Be aware that even if NNTPS is used still no SSL verification will be done.
=head1 NOTES
B<tinews.pl> is designed to be used with L<pgp(1)>-2.6.3,
L<pgp(1)>-5, L<pgp(1)>-6, L<gpg(1)> and L<gpg2(1)>.
B<tinews.pl> requires the following standard modules to be installed:
L<Getopt::Long(3pm)>, L<Net::NNTP(3pm)>, L<Time::Local(3pm)> and
L<Term::Readline(3pm)>.
NNTPS (NNTP with implicit TLS; RFC 4642 and RFC 8143) may be unavailable
if L<Net::NNTP(3pm)> is too old or L<IO::Socket::SSL(3pm)> is missing on
the system. B<tinews.pl> will fallback to unencrypted NNTP in that case.
If the Cancel-Lock feature (RFC 8315) is enabled the following additional
modules must be installed: L<MIME::Base64(3pm)>, L<Digest::SHA(3pm)> or
L<Digest::SHA1(3pm)> and L<Digest::HMAC_SHA1(3pm)>. sha256 and sha512 as
algorithms for B<canlock-algorithm> are only available with L<Digest::SHA(3pm)>.
L<gpg2(1)> users may need to set B<$GPG_TTY>, i.e.
GPG_TTY=$(tty)
export GPG_TTY
before using B<tinews.pl>. See L<https://www.gnupg.org/> for details.
B<tinews.pl> does not do any MIME encoding, its input should be already
properly encoded and have all relevant headers set.
=head1 AUTHOR
Urs Janssen E<lt>urs@tin.orgE<gt>,
Marc Brockschmidt E<lt>marc@marcbrockschmidt.deE<gt>
=head1 SEE ALSO
L<pgp(1)>, L<gpg(1)>, L<gpg2(1)>, L<pgps(1)>, L<Digest::HMAC_SHA1(3pm)>,
L<Digest::SHA(3pm)>, L<Digest::SHA1(3pm)>, L<Getopt::Long(3pm)>,
L<IO::Socket::SSL(3pm)>, L<MIME::Base64(3pm)>, L<Net::NNTP(3pm)>,
L<Time::Local(3pm)>, L<Term::Readline(3pm)>
=cut