#! /usr/bin/perl -w # # yapfaq by Thomas Hochstein # (Original author: Marc Brockschmidt) # # containing some code from tinews.pl # Copyright (c) 2002-2024 Urs Janssen , # Marc Brockschmidt # containing some code from pgpverify.pl # Written April 1996, (David C Lawrence) # Currently maintained by Russ Allbery # # This script posts articles (e.g. FAQs) to Usenet newsgroups. # Most people will use it in combination with cron(8). # # Copyright (C) 2003 Marc Brockschmidt # Copyright (c) 2010-2017, 2026 Thomas Hochstein # # It can be redistributed and/or modified under the same terms under # which Perl itself is published. my $VERSION = "1.0.0"; (my $NAME = $0) =~ s#^.*/##; use utf8; use strict; use POSIX qw(strftime); use Net::Domain qw(hostfqdn); use Net::NNTP; use DateTime; # CPAN use Path::Tiny; # CPAN use Getopt::Long qw(GetOptions); Getopt::Long::config ('bundling'); use Data::Dumper; # configuration ####################### # may be overwritten via ~/.yapfaqrc or command line my %Config; $Config{'datadir'} = 'data/'; # path to data files (FAQs, ...) $Config{'nntp-server'} = 'news'; # your NNTP server name, may be set via $NNTPSERVER $Config{'nntp-port'} = 119; # NNTP-port, may be set via $NNTPPORT $Config{'nntp-user'} = ''; # username for AUTHINFO $Config{'nntp-pass'} = ''; # password for AUTHINFO $Config{'force-auth'} = 0; # set to 1 to force authentication $Config{'starttls'} = 0; # set to 1 to use STARTTLS if possible $Config{'verbose'} = 0; # set to 1 to get status messages $Config{'debug'} = 0; # set to 1 to get some debug output, # set to 2 for NNTP debug output # Main program ######################## ### read configuration # from (first match counts) # $XDG_CONFIG_HOME/yapfaqrc # ~/.config/yapfaqrc # ~/.yapfaqrc # if present # taken and modified from tinews.pl my $RCFILE = undef; my (@try, %seen); if ($ENV{'XDG_CONFIG_HOME'}) { push(@try, (glob("$ENV{'XDG_CONFIG_HOME'}/yapfaqrc"))[0]); } push(@try, (glob('~/.config/yapfaqrc'))[0], (glob('~/.yapfaqrc'))[0]); foreach (grep { ! $seen{$_}++ } @try) { # uniq @try last if (open($RCFILE, '<', $_)); $RCFILE = undef; } if (defined($RCFILE)) { while (defined($_ = <$RCFILE>)) { if (m/^([^#\s=]+)\s*=\s*(\S[^#]+)/io) { chomp($Config{lc($1)} = $2); } } close($RCFILE); } # these env-vars have higher priority (order is important) # taken from tinews.pl $Config{'nntp-server'} = $ENV{'NEWSHOST'} if ($ENV{'NEWSHOST'}); $Config{'nntp-server'} = $ENV{'NNTPSERVER'} if ($ENV{'NNTPSERVER'}); $Config{'nntp-port'} = $ENV{'NNTPPORT'} if ($ENV{'NNTPPORT'}); ### read commandline options my ($OptProject,$OptForce,$OptTest,$OptNewsgroup,$OptOutput,$OptSimulation); GetOptions ('p|project=s' => \$OptProject, 'f|force' => \$OptForce, 't|test' => \$OptTest, 'n|newsgroup=s' => \$OptNewsgroup, 'o|output' => \$OptOutput, 's|simulation' => \$OptSimulation, 'datadir=s' => \$Config{'datadir'}, 'nntp-server=s' => \$Config{'nntp-server'}, 'nntp-port=s' => \$Config{'nntp-port'}, 'nntp-user=s' => \$Config{'nntp-user'}, 'nntp-pass=s' => \$Config{'nntp-pass'}, 'starttls!' => \$Config{'starttls'}, 'force-auth!' => \$Config{'force-auth'}, 'v|verbose!' => \$Config{'verbose'}, 'd|debug!' => \$Config{'debug'}, 'c|config' => \&ShowConf, 'h|help' => \&ShowPOD, 'V|version' => \&ShowVersion) or &ShowUsage; # -s implies -t and -v if ($OptSimulation) { $OptTest = 1; $Config{'verbose'} = 1; } ### create list of @Projects from $Config{'datadir'} unless -p is set my @Projects; if (!$OptProject) { die "E: Data directory '" . $Config{'datadir'} . "' does not exist.\n" unless (-d $Config{'datadir'}); @Projects = glob $Config{'datadir'} . '*.hdr'; foreach (@Projects) { $_ =~ s#^.*/##; $_ =~ s/\.hdr$//; } } else { push @Projects, $OptProject; } ### iterate over @Projects print "- Test mode, no status updates.\n" if $Config{'debug'}; foreach (@Projects) { # check for existence of project my $HeaderFile = $Config{'datadir'} . "$_.hdr"; if (not -r $HeaderFile) { warn "W: Project '$_' does not exist ('$HeaderFile' not found).\n"; next; } print "Project '$_' ...\n" if $Config{'verbose'} or $Config{'debug'}; # generate posting and check for due date # @Posting will be empty ('') if not due my @Posting = &BuildPosting($_); next if !$#Posting; next if $OptSimulation; # save Message-ID my $LastMID; foreach (@Posting) { if (/^Message-ID: /) { ($LastMID = $_) =~ s/^Message-ID:\s+//; chomp ($LastMID); last; } } # sent to STDOUT due to --output if ($OptOutput) { print "- Print to STDTOUT.\n----->----->----->-----\n" if $Config{'debug'}; foreach (@Posting) { print $_ }; print "-----<-----<-----<-----\n" if $Config{'debug'}; # otherwise: post } else { next if !&PostNNTP(@Posting); } # update status &UpdateStatus($_, $LastMID) if !$OptTest; } ### we're done exit(0); # subroutines ######################### ### ------------------------------------------------------------------ ### display version information and exit sub ShowVersion { print "$NAME v$VERSION\n"; print "Copyright (C) 2003 Marc Brockschmidt \n"; print "Copyright (c) 2010-2017, 2026 Thomas Hochstein \n"; print "This program is free software; you may redistribute it ". "and/or modify it under the same terms as Perl itself.\n"; exit(0); }; ### ------------------------------------------------------------------ ### feed myself to perldoc and exit sub ShowPOD { exec('perldoc', $0); exit(0); }; ### ------------------------------------------------------------------ ### Show current configuration sub ShowConf { print "$NAME v$VERSION\n"; print "Current configuration:\n"; foreach my $config (sort keys %Config) { printf("- %s: %s\n", $config, $Config{$config}) if $Config{$config}; } }; ### ------------------------------------------------------------------ ### display short usage information sub ShowUsage { print "$NAME v$VERSION\n"; print "Usage: " . $NAME . " [OPTIONS]\n"; print " -p project run on project only, don't use all projects\n"; print " -f post unconditionally, even if project(s) is/are not due\n"; print " -t don't update project status (test)\n"; print " -n newsgroup post only to newsgroup (for testing)\n"; print " -o print to STDOUT (for testing or to pipe into inews)\n"; print " --datadir path override \$datadir\n"; print " --nntp-server name override \$nntp-server\n"; print " --nntp-port port override \$nntp-port\n"; print " --nntp-user user override \$nntp-user\n"; print " --nntp-pass passwd override \$nntp-pass\n"; print " --[no-]starttls override \$starttls\n"; print " --[no-]force-auth override \$force-auth\n"; print " -v | --[no-]verbose override \$verbose\n"; print " -d | --[no-]debug override \$debug\n"; print " -c show current configuration\n"; print " -h show documentation\n"; print " -V show version and copyright\n"; exit 0; } ### ------------------------------------------------------------------ ### parse a YYYY-MM-DD construct to a DateTime object sub ParseDate { my $Date = shift; die "E: '$Date' is not a valid date format.\n" unless $Date =~ /^\d\d\d\d-\d\d-\d\d$/; my ($Year, $Month, $Day) = split /-/, $Date; return DateTime->new(year => $Year, month => $Month, day => $Day, ); } ### ------------------------------------------------------------------ ### add a duration (in d,w,m,y) to a DateTime object sub AddDuration { my($Date, $Duration) = @_; $Duration =~ /(\d+)(.)/; my ($Amount, $Timespan) = ($1, lc($2)); if ($Timespan eq 'd') { $Date->add(days => $Amount); } elsif ($Timespan eq 'w') { $Date->add(days => $Amount * 7); } elsif ($Timespan eq 'm') { $Date->add(months => $Amount); } elsif ($Timespan eq 'y') { $Date->add(years => $Amount); } return $Date; } ### ------------------------------------------------------------------ ### return a hash of all headers (ignoring duplicate headers) # taken and modified from tinews.pl sub ParseHeaders { my @Headers = @_; my (%Header, $Label, $Value); foreach (@Headers) { s/\r?\n$//; last if /^$/; if (/^(\S+):[ \t](.+)/) { ($Label, $Value) = ($1, $2); # discard all duplicate headers next if $Header{lc($Label)}; $Header{lc($Label)} = $Value; } elsif (/^\s/) { # continuation lines if ($Label) { s/^\s+/ /; $Header{lc($Label)} .= $_; } else { warn (sprintf("W: Non-header line: %s\n",$_)); } } else { warn (sprintf("W: Non-header line: %s\n",$_)); } } return %Header; }; ### ------------------------------------------------------------------ ### open NNTP connection, authenticate and return a Net::NNTP-Object # taken and modified from tinews.pl sub ConnectNNTP { my $NNTP = Net::NNTP->new( Host => $Config{'nntp-server'}, Reader => 1, Debug => $Config{'debug'}, Port => $Config{'nntp-port'}, SSL_verify_mode => 0, ) or die("E: Can't connect to ".$Config{'nntp-server'}.":".$Config{'nntp-port'}.".\n"); my $NNTPMsg = $NNTP->message(); my $NNTPCode = $NNTP->code(); if ($Config{'starttls'} && $NNTP->can_ssl()) { $NNTP->starttls; } if ($Config{'debug'}) { print '- Connected to ' . $NNTP->peerhost . ':' . $NNTP->peerport . "\n"; if ($Config{'starttls'}) { printf(" SSL-Fingerprint: %s %s\n", split(/\$/, $NNTP->get_fingerprint)); } } # no read and/or write access - give up if ($NNTPCode < 200 || $NNTPCode > 201) { $NNTP->quit(); } # read access - try to authenticate if ($NNTPCode == 201 || $Config{'force-auth'}) { # no user/password if (!$Config{'nntp-user'} || !$Config{'nntp-pass'}) { $NNTP->quit(); die('E: ' . $NNTPCode . ' ' . $NNTPMsg . "\n"); } $NNTP = &AuthNNTP($NNTP); } # try posting; on failure, try to authenticate $NNTP->post(); $NNTPCode = $NNTP->code(); if ($NNTPCode == 480) { $NNTP = &AuthNNTP($NNTP); $NNTP->post(); } return $NNTP; } ### ------------------------------------------------------------------ ### do AUTHINFO on a Net::NNTP-Object, die on failure # taken and modified from tinews.pl sub AuthNNTP { my $NNTP = shift; $NNTP->authinfo($Config{'nntp-user'}, $Config{'nntp-pass'}); my $NNTPMsg = $NNTP->message(); my $NNTPCode = $NNTP->code(); if ($NNTPCode != 281) { # auth failed $NNTP->quit(); die('E: ' . $NNTPCode . ' ' . $NNTPMsg . "\n"); } return $NNTP; } ### ------------------------------------------------------------------ ### build posting # read and parser header and body from files # read status file, check due date sub BuildPosting { my $Project = shift; my $StatusFile = $Config{'datadir'} . "$Project.cfg"; my $HeaderFile = $Config{'datadir'} . "$Project.hdr"; my $BodyFile = $Config{'datadir'} . "$Project.txt"; if (not -r $BodyFile) { warn "W: '$BodyFile' not found.\n"; return ''; } # read status file, if available my($LastPosted, $LastMID); if (-r $StatusFile) { print "- Reading status ($Project.cfg).\n" if $Config{'debug'}; my @Status = path($StatusFile)->lines; foreach (@Status) { # convert Windows line-endings to Unix s/\r//; if (/^Last-posted: /i) { chomp; ($LastPosted = $_) =~ s/^Last-posted:\s+//i; } elsif (/^Last-Message-ID: /i) { chomp; ($LastMID = $_) =~ s/^Last-Message-ID:\s+//i; } } } else { print "- No status file ($Project.cfg).\n" if $Config{'debug'}; } print "- Reading headers ($Project.hdr) and body ($Project.txt).\n" if $Config{'debug'}; my @Headers = path($HeaderFile)->lines; my @Body = path($BodyFile)->lines; my %Header = &ParseHeaders(@Headers); # check for mandatory headers if (!$Header{'from'} or !$Header{'subject'} or !$Header{'newsgroups'}) { warn "W: From, Subject or Newsgroups header missing from '$HeaderFile'.\n"; return ''; } # add Date: push @Headers, 'Date: ' . DateTime->now->strftime('%a, %d %b %Y %H:%M:%S %z') . "\n"; # add missing Message-ID: push @Headers, 'Message-ID: <%n-%y-%m-%d@' . hostfqdn. ">\n" if (!$Header{'message-id'}); # add User-Agent push @Headers, "User-Agent: $NAME/$VERSION\n"; # parse pseudo headers from body my ($InRealBody,$LastModified,$PostingFrequency); foreach (@Body) { # convert Windows line-endings to Unix s/\r//; next if $InRealBody; $InRealBody++ if /^$/; $LastModified = $1 if /^Last-modified:\s*(\S+)\s*$/i; $PostingFrequency = $1 if /^Posting-Frequency:\s*(\S+)\s*$/i; } # parse Posting-Frequency from pseudo-header if ($PostingFrequency) { print "- Posting-Frequency set to $PostingFrequency from pseudo-header.\n" if $Config{'debug'}; if ($PostingFrequency eq 'daily') { $PostingFrequency = '1d'; } elsif ($PostingFrequency eq 'weekly') { $PostingFrequency = '1w'; } elsif ($PostingFrequency =~ /bi-?weekly/) { $PostingFrequency = '2w'; } elsif ($PostingFrequency eq 'monthly') { $PostingFrequency = '1m'; } elsif ($PostingFrequency =~ /bi-?monthly/) { $PostingFrequency = '2m'; } } # parse placeholders in headers foreach (@Headers) { # convert Windows line-endings to Unix s/\r//; # drop empty header $_ = '' if /^$/; # Replace %LM placeholder in Subject: with the Last-modified: pseudo-header if (/^Subject: /) { if ($LastModified) { $_ =~ s/\%LM/$LastModified/g; } else { $_ =~ s/ ?[<\[{\(]?\%LM[>\]}\)]? ?//; } } # Replace placeholders in Message-ID: # %n project name # %y current year # %m current month # %d current day # %p PID if (/^Message-ID: /i) { my $TDY = DateTime->now->strftime('%Y'); my $TDM = DateTime->now->strftime('%m'); my $TDD = DateTime->now->strftime('%d'); $_ =~ s/\%n/$Project/g; $_ =~ s/\%y/$TDY/g; $_ =~ s/\%m/$TDM/g; $_ =~ s/\%d/$TDD/g; $_ =~ s/\%p/$$/g; # add random part in test mode if ($OptTest) { my $random = sprintf("%08X", rand(0xFFFFFFFF)); $_ =~ s/now,$_)->strftime('%a, %d %b %Y %H:%M:%S %z') . "\n"; } # add Supersedes: if set if (/^Supersedes: /) { if ($LastMID && !$OptTest) { $_= "Supersedes: $LastMID\n"; } else { $_ = ''; } } # overwrite Newsgroups: if --newsgroup is set if ($OptNewsgroup && /^Newsgroups: /) { print "- 'Newsgroups: $OptNewsgroup' has been set.\n" if $Config{'debug'}; $_= "Newsgroups: $OptNewsgroup\n"; } # get Posting-Frequency if (/^Posting-Frequency: /i) { chomp; $_ =~ s/^Posting-Frequency:\s+//i; $PostingFrequency = $_; $_ = ''; print "- Posting-Frequency set to $PostingFrequency.\n" if $Config{'debug'}; } } # not due if Posting-Freqency is "none" if ($PostingFrequency =~ /none/) { print "... is disabled.\n" if $Config{'verbose'} or $Config{'debug'}; return ''; } # default to 1 month if no (valid) Posting-Frequency is set $PostingFrequency = '1m' if $PostingFrequency !~ /^\d+[dwmy]$/; my $NextPosted = &AddDuration(&ParseDate($LastPosted),$PostingFrequency) if $LastPosted; # check if posting is due print "- Posting has been forced.\n" if $Config{'debug'} && $OptForce; if ($OptForce or (!$LastPosted) or ($LastPosted && $NextPosted <= DateTime->now)) { print "... is due and will be posted.\n" if $Config{'verbose'} or $Config{'debug'}; } else { printf ("... is not due (next post at %s).\n", $NextPosted->strftime('%Y-%m-%d')) if $Config{'verbose'} or $Config{'debug'}; return ''; } # return posting return @Headers, "\n", @Body; } ### ------------------------------------------------------------------ ### post via NNTP # taken and modified from tinews.pl sub PostNNTP { my @Posting = @_; my $NNTP = ConnectNNTP(); my $NNTPMsg = $NNTP->message(); my $NNTPCode = $NNTP->code(); print "- Post article.\n" if $Config{'debug'}; if ($NNTPCode == 340) { $NNTP->datasend(@Posting); ## buggy Net::Cmd < 2.31 $NNTP->set_status(200, ""); $NNTP->dataend(); $NNTPMsg = $NNTP->message(); $NNTPCode = $NNTP->code(); if (! $NNTP->ok()) { $NNTP->quit(); warn("W: Posting failed! Response from server:\n", $NNTPCode, ' ', $NNTPMsg); return 0; } } else { $NNTP->quit(); warn("W: Posting failed! Response from server:\n", $NNTPCode, ' ', $NNTPMsg); return 0; } $NNTP->quit(); print "- Done.\n" if $Config{'debug'}; return 1; } ### ------------------------------------------------------------------ ### update status (last posted, last mid) sub UpdateStatus { my ($Project, $LastMID) = @_; my $StatusFile = $Config{'datadir'} . "$Project.cfg"; my @Status; push @Status, "Last-Posted: " . DateTime->now->strftime('%Y-%m-%d') . "\n"; push @Status, "Last-Message-ID: $LastMID\n"; $StatusFile = path($StatusFile); $StatusFile->spew(@Status); print "- Status updated.\n" if $Config{'debug'}; return; } __END__ ################################ Documentation ################################# =head1 NAME yapfaq - Post FAQs to Usenet I<(yet another postfaq)> =head1 SYNOPSIS B [B<-cfhotsV>] [B<-p> I[B<-n> I] [OPTIONS] =head1 REQUIREMENTS =over 2 =item - Perl 5.8 or later with core modules =item - DateTime =item - Path::Tiny =back Furthermore you need access to a news server to actually post FAQs. =head1 DESCRIPTION B can post (one or more) FAQs (or other texts) to Usenet every n days, weeks, months or years. The content (article body) for each text will be read from a project file, and headers (with some placeholders) will be read from another project file. Posting frequency can be defined as header, or in the body in form of a news.answers pseudo-header. Project status (last time posted, last used I) will be tracked in a config file for each project. Configuration can be done by modifying the source (disapproved), by adding a config file in your home directory or by overriding those options on the command line. =head2 Runtime configuration Options for B can be set by modifying the I section of the source or by using a config file located at F<$XDG_CONFIG_HOME/yapfaqrc>, F<$HOME/.config/yapfaqrc> or F<$HOME/.yapfaqrc> (in order of precedence). Options in config files will override options in source. Both can be overridden by using command line options. =over 2 =item B = I Path to the directory for all project files. Each project needs a F.hdr> file with all headers and a F.txt> file containing the content (body) to be posted. B will track the project status in a F.cfg> file. You can override this option on the command line by using B<--datadir> I. =item B = I<0|1> Display some status messages on STDOUT if set to I<1>. You can override this option on the command line by using B<--verbose> (B<-v>) or B<--noverbose> accordingly. Don't use B if you want to pipe your text to another program! =item B = I<0|1> Display debug messages (and NNTP dialogue) on STDOUT if set to I<1>. You can override this option on the command line by using B<--debug> (B<-d>) or B<--nodebug> accordingly. Don't use B if you want to pipe your text to another program! =item B = I News (NNTP) server to connect to. Can be overridden by setting I<$NNTPSERVER> or I<$NEWSHOST> in your environment. You can override this option on the command line by using B<--nntp-server> I. =item B = I Port on B to connect to. Default is 119. B can't use NNTPS on port 563, but can use STARTTLS if available. Can be overridden by setting I<$NNTPPORT> in your environment. You can override this option on the command line by using B<--nntp-port> I. =item B = I User name for AUTHINFO authentication. You can override this option on the command line by using B<--nntp-user> I. =item B = I Password for AUTHINFO authentication. You can override this option on the command line by using B<--nntp-pass> I. =item B = I<0|1> Force AUTHINFO authentication, even if the server reports that you may post. Necessary for some servers. You can override this option on the command line by using B<--force-auth> or B<--noforce-auth> accordingly. =item B = I<0|1> Use a TLS encrypted connection (via STARTTLS) if available. You can override this option on the command line by using B<--starttls> or B<--nostarttls> accordingly. =back =head2 Project files Each project needs a F.hdr> and a F.text> file, and will get a F.cfg> file after the first posting. These files need to be in B. =head3 Headers file Needs to have at least I, I and I and can contain all other headers that the posting should have. Headers must conform to RFC 5536 and RFC 5322 and use MIME encoded words for 8bit characters. B won't convert headers. I may contain a I<%LM> placeholder that will be replaced with the I pseudo-header from the text file (see below), if present. If no I pseudo-header is found, the placeholder (and surrounding brackets, angle brackets or curly brackets and spaces) is removed. If a I header is present, placeholders in that header will be replaced: I<%n> with the project name, I<%y> with the current year (YYYY), I<%m> with the current month (MM), I<%d> with the current day (DD) and I<%p> with the current process ID (PID) of B. If no I header is present, the I will be generated with the hostname of the system B is running on and I<%n-%y-%m-%d> as template for the left hand side. If the I header in the headers file does not contain placeholders, the next repost will most probably fail. If an I header is present, it must contain a time period of n days, weeks, months or years in the form of a number followed by I, I, I or I, e.g. I for four weeks: If no such I header is present, no such header will be set. If a I header is present (e.g. I, it will be replaced with a I header containing the I of the last posted article. If a I header is present, it must contain a time period in the same way as for I, e.g. I for a monthly posting, or the keyword B. The I header will be removed after parsing. Alternatively a I pseudo-header in the text file may be used (see below). If no I is set anywhere, the default ist one month (I<1m>). I disables automatic postings. B From: John Doe Reply-To: Subject: <%LM> FAQ for alt.example.discussions Newsgroups: alt.example.discussions Message-ID: <%n-%y-%m-%d@my-domain.example> Posting-frequency: 1w Expires: 1m Supersedes: yes Mime-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit =head3 Text file The content (body) of your FAQ or other text. It may contain pseudo-headers, starting on the first line and separated from the reamining content by a blank line. I and I will be evaluated by B. If your content contains 8bit characters, you'll need suitable MIME headers in your headers file. B Archive-name: alt-example/discussions-faq Posting-frequency: weekly Last-modified: 2025-12-15 URL: https://doe.example/faqs/alt-example-discussions-faq.txt This is a list of frequently asked questions (FAQs) and their answers for the alt.example.discussions newsgroup. 1. What is the topic of alt.example.discussions? We discuss examples. That's quite enought, isn't it? =head1 OPTIONS =over 4 =item B<-c>, B<--config> Display current runtime configuration from source or config file. =item B<-f>, B<--force> Post text unconditionally, even if not due according to the defined posting frequency. This refers either to all projects or just one defined by B<--project>. =item B<-n>, B<--newsgroup> I Override the I header for all texts posted. Intended for testing purposes. Combine with B<--test> to avoid updating project status and to get a unique I (and no I header). =item B<-o>, B<--output> Don't post via NNTP, but print to STDOUT. Combine with B<--test> to avoid updating project status. Intended for testing purposes or to pipe in another program like I or I. If you want to pipe the output to another program, neither B<--verbose> nor B<--debug> should be set. =item B<-p>, B<--project> I Run for just one project (FAQ, text). Default is running for all projects. =item B<-h>, B<--help> Display this man page and exit. =item B<-s>, B<--simulation> Simulation mode. Don't post, just show which projects would be due. Implies B<--test> and B<--verbose>. =item B<-t>, B<--test> Test mode. Don't update project status (time and Message-ID of last posting), dont' add a I header and modify the I with a random part. The text(s) will still be posted if due or forced by B<--force>. Combine with B<--output> to redirect output to STDOUT or with B<--newsgroup> to override the I header. =item B<-V>, B<--version> Display version and copyright information and exit. =item B You can override all runtime configuration options set in the source or a config file from the command line, as described above. =back =head1 EXAMPLES Post all FAQs that are due for posting: yapfaq.pl You may run this command daily from B. Do a dry run, showing which FAQs would be posted and print them on STDOUT: yapfaq.pl -t -v -o (or yapfaq.pl -tvo) The same, with debugging output: yapfaq.pl -tdo Force a test post of your I text to I, even if the text is not due to be posted: yapfaq.pl -t -f -n alt.test The same, with debugging output: yapfaq.pl -tfdn alt.test Pipe all FAQs (that are due for posting) to I from INN: yapfaq.pl -o | inews You may run this command daily from B, too. =head1 ENVIRONMENT =over 2 =item B<$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. The B<--nntp-server> command line option overrides B<$NEWSHOST>. =item B<$NNTPSERVER> Set to override the NNTP server configured in the source or config file. This has higher priority than B<$NEWSHOST>. The B<--nntp-server> command line option overrides B<$NNTPSERVER>. =item B<$NNTPPORT> The NNTP TCP port to connect news to. This variable only needs to be set if the TCP port is not 119 (the default). The B<--nntp-port> command line option overrides B<$NNTPPORT>. =back =head1 FILES =over 2 =item F The script itself. =item F<$XDG_CONFIG_HOME/yapfaqrc> F<$HOME/.config/yapfaqrc> F<$HOME/.yapfaqrc> Config file (on order of precedence). =item F/I.hdr> Headers for I. =item F/I.txt> Content (body) for I. =item F/I.cfg> Status data of I. =back =head1 BUGS Please report any bugs or feature requests to the author or use the bug tracker at L! =head1 SEE ALSO L will have the current version of this program. This program is maintained using the Git version control system at L. =head1 AUTHOR Thomas Hochstein Original author (up to version 0.5b, dating from 2003): Marc Brockschmidt =head1 COPYRIGHT AND LICENSE Copyright (c) 2003 Marc Brockschmidt Copyright (c) 2010-2017, 2026 Thomas Hochstein This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself. This program contains (modified) code from tinews.pl, copyright (c) 2002-2024 Urs Janssen and Marc Brockschmidt This program contains (modified) code from pgpverify.pl, written April 1996, (David C Lawrence), currently maintained by Russ Allbery =cut