#! /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 , # Marc Brockschmidt # # 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", $>, 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 to \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 [B] E I =head1 DESCRIPTION B reads an article on STDIN, signs it via L or L 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 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 (, '\n'). Use --B to convert from to just . =head1 OPTIONS X =over 4 =item -B<4> | --B X<-4> X<--iv4> Force connecting via IPv4 to the remote NNTP server. =item -B C | --B C X<-a> X<--approved> Set the article header field Approved: to the given value. =item -B C | --B C X<-c> X<--control> Set the article header field Control: to the given value. =item -B C | --B C X<-d> X<--distribution> Set the article header field Distribution: to the given value. =item -B C | --B C X<-e> X<--expires> Set the article header field Expires: to the given value. =item -B C | --B C X<-f> X<--from> Set the article header field From: to the given value. =item -B F
| --B F
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 C | --B C X<-m> X<--message-id> Set the article header field Message-ID: to the given value. =item -B C | --B C X<-n> X<--newsgroups> Set the article header field Newsgroups: to the given value. =item -B C | --B C X<-o> X<--organization> Set the article header field Organization: to the given value. =item -B

C | --B C X<-p> X<--port> use C as NNTP-port =item -B C | --B C X<-r> X<--reply-to> Set the article header field Reply-To: to the given value. =item -B F | --B F X<-s> X<--savedir> Save signed article to directory F instead of posting. =item -B C | --B C X<-t> X<--subject> Set the article header field Subject: to the given value. =item -B | --B 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 C | --B C X<-w> X<--followup-to> Set the article header field Followup-To: to the given value. =item -B C | --B C X<-x> X<--path> Prepend the article header field Path: with the given value. =item -B | -B | --B X<-D> X<-N> X<--debug> Set L 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 | --B X<-E> X<--discard-empty> Silently discard an empty article. =item -B | --B X<-F> X<--references> Set the article header field References: to the given value. =item -B | --B X<-H> X<--help> Show help-page. =item -B | --B X<-I> X<--no-injection-date> Do not add Injection-Date: header. =item -B | --B X<-L> X<--no-canlock> Do not add Cancel-Lock: / Cancel-Key: headers. =item -B | --B X<-O> X<--no-organization> Do not add Organization: header. =item -B | --B X<-R> X<--no-control> Restricted mode, disallow control-messages. =item -B | --B X<-s> X<--no-signature> Do not append F<$HOME/.signature>. =item -B C | --B C X<-T> X<--timeout> Override the connection timeout setting. Default is 120 seconds. =item -B | --B X<-X> X<--no-sign> Do not sign the article. =item -B | --B X<-Y> X<--force-auth> Force authentication on connect even if not required by the server. =item --B C X<--canlock-algorithm> Digest algorithm used for Cancel-Lock: / Cancel-Key: headers. Supported algorithms are sha1, sha256 and sha512. Default is sha1. =item --B | --B X<--ssl> X<--nntps> Use NNTPS (via port 563) if available. This requires a recent version of L and L. Be aware that no SSL verification will be done. =item --B X<--transform> Convert network line endings () to unix line endings (). =item --B X<--version> Show version. =item -B -B -B X<-A> X<-V> X<-W> These options are accepted for compatibility reasons but ignored. =item -B | --B 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 An error occurred. =back =head1 ENVIRONMENT X =over 4 =item B<$NEWSHOST> X<$NEWSHOST> X 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 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 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

' command-line option overrides B<$NNTPPORT>. =item B<$PGPPASS> X<$PGPPASS> X Set to override the passphrase configured in the source (used for L-2.6.3). =item B<$PGPPASSFILE> X<$PGPPASSFILE> X Passphrase file used for L or L. =item B<$SIGNER> X<$SIGNER> X 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 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' command-line option overrides B<$REPLYTO>. =item B<$ORGANIZATION> X<$ORGANIZATION> X 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' command-line option overrides B<$ORGANIZATION>, The '-B' command-line option disables it. =item B<$DISTRIBUTION> X<$DISTRIBUTION> X 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' command-line option overrides B<$DISTRIBUTION>. =back =head1 FILES =over 4 =item F Temporary file used to store the reformatted article. =item F Temporary file used to store the reformatted and signed article. =item F<$PGPPASSFILE> The passphrase file to be used for L or L. =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 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 is designed to be used with L-2.6.3, L-5, L-6, L and L. B requires the following standard modules to be installed: L, L, L and L. NNTPS (NNTP with implicit TLS; RFC 4642 and RFC 8143) may be unavailable if L is too old or L is missing on the system. B 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, L or L and L. sha256 and sha512 as algorithms for B are only available with L. L users may need to set B<$GPG_TTY>, i.e. GPG_TTY=$(tty) export GPG_TTY before using B. See L for details. B does not do any MIME encoding, its input should be already properly encoded and have all relevant headers set. =head1 AUTHOR Urs Janssen Eurs@tin.orgE, Marc Brockschmidt Emarc@marcbrockschmidt.deE =head1 SEE ALSO L, L, L, L, L, L, L, L, L, L, L, L, L =cut