diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/.yapfaqrc.sample b/.yapfaqrc.sample index 90b25eb..d8c9b8a 100644 --- a/.yapfaqrc.sample +++ b/.yapfaqrc.sample @@ -1,12 +1,6 @@ -# config options -# - datadir path to data directory -# - nntp-server NNTP server name -# - nntp-port NNTP port -# - nntp-user user name for AUTHINFO -# - nntp-pass password for AUTHINFO -# - force-auth force AUTHINFO -# - starttls 1 = use STARTTLS if possible, 0 = don't -# - verbose 1 = show status messages, 0 = don't -# - debug 1 = show debug output, 0 = don't -nntp-server = localhost -nntp-port = 119 +NNTPServer = 'localhost' +NNTPUser = '' +NNTPPass = '' +Sender = '' +ConfigFile = 'yapfaq.cfg.sample' +Program = '' diff --git a/ChangeLog b/ChangeLog index b66b4b0..bd61686 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,25 +1,25 @@ -yapfaq 1.0.0 (unreleased) -* Complete rewrite. -* Add POD. -* Fix file handling (UTF8 mode). -* Show next posting date if posting is not due. -* Add --simulation mode. -* Update examples in POD. - yapfaq 0.10 (unreleased) + * Add: Charset definition. Fixes #29. + * Mark yapfaq.pl executable. + * Change mail address. -yapfaq 0.9.1 (2010-11-01) + +Version 0.9.1 + * Fix: Test mode: Add X-Supersedes only if Supersedes would be set. Fixes #28. Thomas Hochstein Sun Oct 31 18:42:52 2010 +0100 -yapfaq 0.9 (2010-09-11) + +Version 0.9 + * Drop use of Fcntl (not needed). Thomas Hochstein Tue Jun 15 22:30:11 2010 +0200 + * Changed packaging. - Drop .yapfaqrc and yapfaq.cfg in favor of .yapfaqrc.sample and yapfaq.cfg.sample; rename test.txt to sample.txt. @@ -28,8 +28,10 @@ yapfaq 0.9 (2010-09-11) - Add "INSTALLATION" chapter to documentation. Fixes #7. Thomas Hochstein Sat May 15 19:16:40 2010 +0200 + * Change default Message-ID format. Thomas Hochstein Sat May 15 19:04:24 2010 +0200 + * Change: Modify headers for test posts. - Change MID so you can do multiple tests. - Replace Supersedes with X-Supersedes so you do not delete @@ -38,38 +40,51 @@ yapfaq 0.9 (2010-09-11) - Change documentation accordingly. Fixes #6. Thomas Hochstein Sat May 15 17:22:20 2010 +0200 + * Change: Drop %LM from subject if Last-Modified is not found. Thomas Hochstein Sat May 15 16:36:52 2010 +0200 -yapfaq 0.8.2 (2010-05-15) + +Version 0.8.2 + * Fix: Accept leading/trailing whitespace for Last-modified pseudo header. Fixes #5. Thomas Hochstein Sat May 15 16:32:58 2010 +0200 -yapfap 0.8.1 (2010-05-14) + +Version 0.8.1 + * Fix broken implementation of "Program" in .yapfaqrc. Fixes #4. Thomas Hochstein 2010-05-14 21:58:15 -yapfaq 0.8 (2010-05-13) + +Version 0.8 + * Documentation: Add Git repository and bug tracker. Thomas Hochstein Thu May 13 19:21:05 2010 +0200 + * Making use of Getopt::Std's --help and --version. Fixes #3. Thomas Hochstein Thu May 13 19:33:25 2010 +0200 + * New: Add "Program" to .yapfaqrc. Fixes #2. Thomas Hochstein Thu May 13 19:31:49 2010 +0200 + * Change: Drop PGP support. You may use tinews.pl from ftp://ftp.tin.org/tin/tools/tinews.pl instead. Fixes #1. Thomas Hochstein Thu May 13 19:24:44 2010 +0200 + * Fix: Consistency check for MID-Format fixed (regexp). Thomas Hochstein Wed Apr 14 23:17:16 2010 +0200 + * New: MID-Format may now contain %t for a Unix timestamp. %t will be replaced by the number of seconds since the epoch. Thomas Hochstein Wed Apr 14 23:18:04 2010 +0200 + * Documentation changes - Change sample yapfaq.cfg (mark optional settings). Optional settings are mostly commented out. @@ -79,8 +94,10 @@ yapfaq 0.8 (2010-05-13) Thomas Hochstein Wed Apr 14 10:02:48 2010 +0200 - Add comments pointing to .yapfaqrc to source. Thomas Hochstein Wed Apr 14 10:10:28 2010 +0200 + * readconfig(): Add file name to verbose output. Thomas Hochstein Wed Apr 14 09:38:23 2010 +0200 + * Change: Reset default for NNTPServer to "unset". Since yapfaq fill fall back to Perl's build-in defaults, that should be no problem; furthermore user may now @@ -88,13 +105,16 @@ yapfaq 0.8 (2010-05-13) the code. Thomas Hochstein Wed Apr 14 09:11:45 2010 +0200 -yapfaq 0.7 (2010-04-14) + +Version 0.7 + * Change: readconfig(): Make mid-format optional. Set defaults for expires and mid-format when they're invalid (defaults were already set in postfaq() if undefined). Change documentation accordingly; make it more clear if parameters are optional or mandatory. Thomas Hochstein Tue Apr 13 23:59:43 2010 +0200 + * Fix: Save status information only after successful posting. - New Function: updatestaus Move status information save to updatestatus. @@ -102,11 +122,13 @@ yapfaq 0.7 (2010-04-14) - postfaq() will update status information only when post() was successful. Thomas Hochstein Sat Apr 10 23:19:44 2010 +0200 + * New: Add option '-s': pipe article to script. Use an external program to post - or otherwise handle - the article. Amend documentation. Thomas Hochstein Sat Apr 10 02:14:59 2010 +0200 + * New: runtime configuration - Moved configuration to a hash (%Config). Thomas Hochstein Wed Apr 7 22:09:15 2010 +0200 @@ -122,40 +144,54 @@ yapfaq 0.7 (2010-04-14) Add the according sections to the POD documentation. Fix some wording. Thomas Hochstein Sat Apr 10 02:17:00 2010 +0200 + * Small changes. -t CONSOLE: Change delimiter. No leading \n is necessary. Add some more comments. Thomas Hochstein Sat Apr 10 01:43:19 2010 +0200 + * Add option '-V': print version and copyright information. Thomas Hochstein Thu Apr 8 07:36:11 2010 +0200 + * Change: -h: Replace version/usage information with man page. Feed script to perldoc when called with -h. Thomas Hochstein Thu Apr 8 06:21:05 2010 +0200 + * Change: Authenticate only if $NNTPUser is set. Thomas Hochstein Sat Apr 10 00:49:24 2010 +0200 + * Add check for MID-Format and fallback to FQDN. Uses hostfqdn from Net::Domain. Thomas Hochstein Thu Apr 8 08:33:01 2010 +0200 + * Add checks for mandatory content in configuration file. Enhance and optimize existing checks. Thomas Hochstein Thu Apr 8 08:30:21 2010 +0200 + * Code optimisation (verbose output). Thomas Hochstein Thu Apr 8 08:00:04 2010 +0200 -yapfaq 0.6.2 (2010-02-26) + +Version 0.6.2 + * Fix default for Expires. Bug introduced in v0.6.1. Thomas Hochstein Fri Feb 26 09:29:01 2010 +0100 -yapfaq 0.6.1 (2010-02-26) + +Version 0.6.1 + * Fix: Test mode must not update status information. Also fix runtime warning concerning expires. Thomas Hochstein Fri Feb 26 08:28:06 2010 +0100 + -yapfaq 0.6 (2010-02-25) +Version 0.6 + * Add documentation in POD format. Thomas Hochstein Thu Feb 25 17:00:07 2010 +0100 + * Add commandline options. - Using Getopt::Std. - Implement option '-h': @@ -176,6 +212,7 @@ yapfaq 0.6 (2010-02-25) 'test mode', post to (an)other newsgroup(s) given on the command line or to STDOUT ('console'). Thomas Hochstein Thu Feb 25 19:22:15 2010 +0100 + * Add variable expiry. - New Function: calcdelta Move date calculation for new posting date to @@ -184,20 +221,25 @@ yapfaq 0.6 (2010-02-25) Parse 'Expires'. Use calcdelta to calculate expiry. Thomas Hochstein Thu Feb 25 12:55:04 2010 +0100 + * Cleanup on yapfaq.cfg Reformat, translate to English language, add descriptions. Thomas Hochstein Thu Feb 25 16:16:49 2010 +0100 + * Change handling of warnings/errors. Don't output line number if .cfg file can't be opened. Inform user when writing to ERROR.dat. Add script name and Warning/Error to warn() and die() output. Thomas Hochstein Thu Feb 25 09:23:14 2010 +0100 + * Fix: Accept case-insensitive Last-modified pseudo header. Thomas Hochstein Sun Feb 21 18:39:05 2010 +0100 + * Change defaults Don't use PGP by default. Default $NNTPServer to 'localhost' Thomas Hochstein Thu Feb 25 15:15:57 2010 +0100 + * Update header/introduction, bump version/copyright information. Fix typo/language in header/introduction. Add new author / copyright information. diff --git a/README.md b/README.md index 10af39e..d39e4b6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # yapfaq -**yapfaq** can post FAQs from text files to Usenet at configurable intervals and will add/modify headers automatically. +**yapfaq** can post FAQs from text files to Usenet at configurable intervals and automatically adds the necessary headers. ## Description -With **yapfaq**, FAQs or other texts can be posted regularly (every x days, weeks, months or years) to one or more newsgroups. Posting frequency as well as headers can be defined in a separate file. `Expires:` and `Supersedes:` can be set automatically by the script. +With **yapfaq**, FAQs or other texts can be posted regularly (every x days, weeks, months or years) to one or more newsgroups. Posting frequency as well as the necessary headers - including the format of the `Message-ID:` headers that will be generated - can be defined in a configuration file. `Expires:` and `Supersedes:` will be set automatically by the script. -The text can either be posted via NNTP (even using STARTTLS) or piped to another program (like `inews` or `tinews.pl`). +The text can either be posted via NNTP or piped to another programme (like `inews` or `tinews.pl`). ## More information diff --git a/bin/yapfaq.pl b/bin/yapfaq.pl deleted file mode 100755 index 29a98bc..0000000 --- a/bin/yapfaq.pl +++ /dev/null @@ -1,1012 +0,0 @@ -#! /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 " -s only show which projects are due, implies -tv\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 from header.\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>. - -Can be combined with B<--project> to show if just one project is due. - -=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. If you add "-v", you'll -get a report mailed which FAQs have been posted and which were not -due. - -Pipe all FAQs that are due for posting to I from INN instead: - - yapfaq.pl -o | inews - -You may run this command daily from B, too. - -Show which FAQs are due for posting and the next due dates for those -that are not: - - yapfaq.pl -s - -Do a test run of your I text and and print it on STDOUT -(whether ist is due or not): - - yapfaq.pl -t -f -o -p example - (or yapfaq.pl -tfop example) - -The same, with debugging output (add "-d"): - - yapfaq.pl -tfdop example - -Force a test post of your I text to I, even if -the text is not due to be posted (same as before, just replace "-o" -by "-n alt-test"): - - yapfaq.pl -t -f -p example -n alt.test - -The same, with debugging output (add "-d"): - - yapfaq.pl -tfdp example -n alt.test - -=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 diff --git a/data/sample.hdr b/data/sample.hdr deleted file mode 100644 index 3572a3d..0000000 --- a/data/sample.hdr +++ /dev/null @@ -1,7 +0,0 @@ -From: John Doe -Subject: <%LM> test noreply ignore -Newsgroups: local.test -Message-ID: <%n-%y-%m-%d@domain.invalid> -Posting-frequency: 1d -Expires: 3m -Supersedes: yes diff --git a/data/sample.txt b/sample.txt similarity index 100% rename from data/sample.txt rename to sample.txt diff --git a/yapfaq.cfg.sample b/yapfaq.cfg.sample new file mode 100644 index 0000000..a25d50f --- /dev/null +++ b/yapfaq.cfg.sample @@ -0,0 +1,52 @@ +# name of your project +Name = 'sample' + +# file to post (complete body and pseudo-headers) +# ($File.cfg contains data on last posting and last MID) +File = 'sample.txt' + +# how often your project should be posted +# use (d)ay OR (w)eek OR (m)onth OR (y)ear +Posting-frequency = '1d' + +# time period after which the posting should expire +# use (d)ay OR (w)eek OR (m)onth OR (y)ear +# This setting is optional. Default: 3m +# Expires = '3m' + +# header "From:" +From = 'John Doe ' + +# header "Subject:" +# (may contain "%LM" which will be replaced by the contents of the +# Last-Modified pseudo header). +Subject = 'test noreply ignore' + +# comma-separated list of newsgroup(s) to post to +# (header "Newsgroups:") +NGs = 'de.test' + +# header "Followup-To:" +# This setting is optional. Default: unset +# Fup2 = 'poster' + +# Message-ID ("%n" is $Name) +# This setting is optional. Default: <%n-%y-%m-%d@YOURHOST> +# MID-Format = '<%n-%y-%m-%d@domain.invalid>' + +# Character Encoding +# This setting is optional. Default: UTF-8 +# Charset = ISO-8859-15 + +# Supersede last posting? +# This setting is optional. Default: unset +Supersede = yes + +# extra headers (appended verbatim) +# use this for custom headers like "Approved:" +# This setting is optional. Default: unset +ExtraHeader = 'Approved: moderator@domain.invalid +X-Header: Some text' + +# other projects may follow separated with "=====" +# ===== diff --git a/yapfaq.pl b/yapfaq.pl new file mode 100755 index 0000000..60d58d4 --- /dev/null +++ b/yapfaq.pl @@ -0,0 +1,877 @@ +#! /usr/bin/perl -W +# +# yapfaq Version 0.10 by Thomas Hochstein +# (Original author: Marc Brockschmidt) +# +# This script posts any project described in its config-file. Most people +# will use it in combination with cron(8). +# +# Copyright (C) 2003 Marc Brockschmidt +# Copyright (c) 2010-2017 Thomas Hochstein +# +# It can be redistributed and/or modified under the same terms under +# which Perl itself is published. + +our $VERSION = "0.10"; + +# Please do not change this setting! +# You may override the default .rc file (.yapfaqrc) by using "-c .rc file" +my $RCFile = '.yapfaqrc'; +# Valid configuration variables for use in a .rc file +my @ValidConfVars = ('NNTPServer','NNTPUser','NNTPPass','Sender','ConfigFile','Program'); + +################################### Defaults ################################### +# Please do not change anything in here! +# Use a runtime configuration file (.yapfaqrc by default) to override defaults. +my %Config = (NNTPServer => "", + NNTPUser => "", + NNTPPass => "", + Sender => "", + ConfigFile => "yapfaq.cfg", + Program => ""); + +################################# Main program ################################# + +use strict; +use Net::NNTP; +use Net::Domain qw(hostfqdn); +use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today); +use Getopt::Std; +$Getopt::Std::STANDARD_HELP_VERSION = 1; +my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date + +# read commandline options +my %Options; +getopts('Vhvpdt:f:c:s:', \%Options); +# -V: print version / copyright information +if ($Options{'V'}) { + print "$0 v $VERSION\nCopyright (c) 2003 Marc Brockschmidt \nCopyright (c) 2010-2017 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); +} +# -h: feed myself to perldoc +if ($Options{'h'}) { + exec ('perldoc', $0); + exit(0); +}; +# -f: set $Faq +my ($Faq) = $Options{'f'} if ($Options{'f'}); + +# read runtime configuration (configuration variables) +$RCFile = $Options{'c'} if ($Options{'c'}); +if (-f $RCFile) { + readrc (\$RCFile,\%Config); +} else { + warn "$0: W: .rc file $RCFile does not exist!\n"; +} + +$Options{'s'} = $Config{'Program'} if (defined($Config{'Program'}) && $Config{'Program'} && !defined($Options{'s'})); + +# read configuration (configured FAQs) +my @Config; +readconfig (\$Config{'ConfigFile'}, \@Config, \$Faq); + +# for each FAQ: +# - parse configuration +# - read status data +# - if FAQ is due: call postfaq() +foreach (@Config) { + my ($LPD,$LPM,$LPY) = (01, 01, 0001); #LP: Last posting-date + my ($NPY,$NPM,$NPD); #NP: Next posting-date + my $SupersedeMID; + + my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'}); + my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'}); + my ($MIDF,$ReplyTo,$Charset,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'charset'},$$_{'extraheader'}); + my ($Supersede) =($$_{'supersede'}); + + # -f: loop if not FAQ to post + next if (defined($Faq) && $ActName ne $Faq); + + # read status data + if (open (FH, "<$File.cfg")) { + while(){ + if (/##;; Lastpost:\s*(\d{1,2})\.(\d{1,2})\.(\d{2}(\d{2})?)/){ + ($LPD, $LPM, $LPY) = ($1, $2, $3); + } elsif (/^##;;\s*LastMID:\s*(<\S+@\S+>)\s*$/) { + $SupersedeMID = $1; + } + } + close FH; + } else { + warn "$0: W: Couldn't open $File.cfg: $!\n"; + } + + $SupersedeMID = "" unless $Supersede; + + ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq); + + # if FAQ is due: get it out + if (Delta_Days($NPY,$NPM,$NPD,$TDY,$TDM,$TDD) >= 0 or ($Options{'p'})) { + if($Options{'d'}) { + print "$ActName: Would be posted now (but running in simulation mode [$0 -d]).\n" if $Options{'v'}; + } else { + postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$Charset,\$ExtHea,\$Config{'Sender'},\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire); + } + } elsif($Options{'v'}) { + print "$ActName: Nothing to do.\n"; + } +} + +exit; + +#################################### readrc #################################### +# Takes a filename and the reference to an array which contains the valid options + +sub readrc{ + my ($File, $Config) = @_; + + print "Reading $$File.\n" if($Options{'v'}); + + open FH, "<$$File" or die "$0: Can't open $$File: $!"; + while () { + if (/^\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)/) { + if (grep(/$1/,@ValidConfVars)) { + $$Config{$1} = $2 if $2 ne ''; + } else { + warn "$0: W: $1 is not a valid configuration variable (reading from $$File)\n"; + } + } + } +} + +################################## readconfig ################################## +# Takes a filename, a reference to an array, which will hold hashes with +# the data from $File, and - optionally - the name of the (single) FAQ to post + +sub readconfig{ + my ($File, $Config, $Faq) = @_; + my ($LastEntry, $Error, $i) = ('','',0); + + print "Reading configuration from $$File.\n" if($Options{'v'}); + + open FH, "<$$File" or die "$0: E: Can't open $$File: $!"; + while () { + next if (defined($$Faq) && !/^\s*=====\s*$/ && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq ); + if (/^(\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)|^(.*?)'?\s*(#.*$|$))/ && not /^\s*$/) { + $LastEntry = lc($2) if $2; + $$Config[$i]{$LastEntry} .= $3 if $3; + $$Config[$i]{$LastEntry} .= "\n$5" if $5 && $5; + } + if (/^\s*=====\s*$/) { + $i++; + } + } + close FH; + + #Check saved values: + for $i (0..$i){ + next if (defined($$Faq) && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq ); + unless(defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} =~ /^\S+$/) { + $Error .= "E: The name of your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n" + } + unless(defined($$Config[$i]{'file'}) && -f $$Config[$i]{'file'}) { + $Error .= "E: The file to post for your project \"$$Config[$i]{'name'}\" is not defined or does not exist.\n" + } + unless(defined($$Config[$i]{'from'}) && $$Config[$i]{'from'} =~ /\S+\@(\S+\.)?\S{2,}\.\S{2,}/) { + $Error .= "E: The From header for your project \"$$Config[$i]{'name'}\" seems to be incorrect.\n" + } + unless(defined($$Config[$i]{'ngs'}) && $$Config[$i]{'ngs'} =~ /^\S+$/) { + $Error .= "E: The Newsgroups header for your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n" + } + unless(defined($$Config[$i]{'subject'})) { + $Error .= "E: The Subject header for your project \"$$Config[$i]{'name'}\" is not defined.\n" + } + unless(!$$Config[$i]{'fup2'} || $$Config[$i]{'fup2'} =~ /^\S+$/) { + $Error .= "E: The Followup-To header for your project \"$$Config[$i]{'name'}\" contains whitespaces.\n" + } + unless(defined($$Config[$i]{'posting-frequency'}) && $$Config[$i]{'posting-frequency'} =~ /^\s*\d+\s*[dwmy]\s*$/) { + $Error .= "E: The Posting-frequency for your project \"$$Config[$i]{'name'}\" is invalid.\n" + } + unless(!$$Config[$i]{'expires'} || $$Config[$i]{'expires'} =~ /^\s*\d+\s*[dwmy]\s*$/) { + warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" is invalid - set to 3 month.\n"; + $$Config[$i]{'expires'} = '3m'; # set default (3 month) if expires is unset or invalid + } + unless(!$$Config[$i]{'mid-format'} || $$Config[$i]{'mid-format'} =~ /^<\S+\@(\S+\.)?\S{2,}\.\S{2,}>/) { + warn "$0: W: The Message-ID format for your project \"$$Config[$i]{'name'}\" seems to be invalid - set to default.\n"; + $$Config[$i]{'mid-format'} = '<%n-%y-%m-%d@'.hostfqdn.'>'; # set default if mid-format is invalid + } + } + $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error; + die $Error if $Error; +} + +################################# calcdelta ################################# +# Takes a date (year, month and day) and a time period (1d, 1w, 1m, 1y, ...) +# and adds the latter to the former + +sub calcdelta { + my ($Year, $Month, $Day, $Period) = @_; + my ($NYear, $NMonth, $NDay); + + if ($Period =~ /(\d+)\s*([dw])/) { # Is counted in days or weeks: Use Add_Delta_Days. + ($NYear, $NMonth, $NDay) = Add_Delta_Days($Year, $Month, $Day, (($2 eq "w")?$1 * 7: $1 * 1)); + } elsif ($Period =~ /(\d+)\s*([my])/) { #Is counted in months or years: Use Add_Delta_YM + ($NYear, $NMonth, $NDay) = Add_Delta_YM($Year, $Month, $Day, (($2 eq "m")?(0,$1):($1,0))); + } + return ($NYear, $NMonth, $NDay); +} + +################################ updatestatus ############################### +# Takes a MID and a status file name +# and writes status information to disk + +sub updatestatus { + my ($ActName, $File, $date, $MID) = @_; + + print "$$ActName: Save status information.\n" if($Options{'v'}); + + open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!"; + print FH "##;; Lastpost: $date\n"; + print FH "##;; LastMID: $MID\n"; + close FH; +} + +################################## postfaq ################################## +# Takes a filename and many other vars. +# +# It reads the data-file $File and then posts the article. + +sub postfaq { + my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$Charset,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_; + my (@Header,@Body,$MID,$InRealBody,$LastModified); + + print "$$ActName: Preparing to post.\n" if($Options{'v'}); + + #Prepare MID: + $$TDM = ($$TDM < 10 && $$TDM !~ /^0/) ? "0" . $$TDM : $$TDM; + $$TDD = ($$TDD < 10 && $$TDD !~ /^0/) ? "0" . $$TDD : $$TDD; + my $Timestamp = time; + + $MID = $$MIDF; + $MID = '<%n-%y-%m-%d@'.hostfqdn.'>' if !defined($MID); # set to default if unset + $MID =~ s/\%n/$$ActName/g; + $MID =~ s/\%d/$$TDD/g; + $MID =~ s/\%m/$$TDM/g; + $MID =~ s/\%y/$$TDY/g; + $MID =~ s/\%t/$Timestamp/g; + + #Now get the body: + open (FH, "<$$File"); + while (){ + s/\r//; + push (@Body, $_), next if $InRealBody; + $InRealBody++ if /^$/; + $LastModified = $1 if /^Last-modified:\s*(\S+)\s*$/i; + push @Body, $_; + } + close FH; + push @Body, "\n" if ($Body[-1] ne "\n"); + + #Create Date- and Expires-Header: + 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; + 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 = (1900 + $time[5]); + my $tz = $time[8] ? " +0200" : " +0100"; + + $$Expire = '3m' if !$$Expire; # set default if unset: 3 month + + my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire); + my $expmonthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$expM-1]; + + my $date = "$day $monthN $year " . $hh . ":" . $mm . ":" . $ss . $tz; + my $expdate = "$expD $expmonthN $expY $hh:$mm:$ss$tz"; + + #Replace %LM by the content of the news.answer-pseudo-header Last-modified: + if ($LastModified) { + $$Subject =~ s/\%LM/$LastModified/; + } else { + $$Subject =~ s/[<\[{\(]?\%LM[>\]}\)]?//; + } + + # Set Charset + $$Charset = 'UTF-8' if !$$Charset; + my $ContentType = sprintf('text/plain; charset=%s',$$Charset); + + # Test mode? + if($Options{'t'} and $Options{'t'} !~ /console/i) { + $$NG = $Options{'t'}; + $MID =~ s/@/-$Timestamp-test@/g; + $$ExtraHeaders .= "\n" if $$ExtraHeaders; + $$ExtraHeaders .= "X-Supersedes: $$Supersedes\n" if $$Supersedes; + $$ExtraHeaders .= "X-yapfaq-Remark: This is only a test message."; + undef $$Supersedes; + } + + #Now create the complete Header: + push @Header, "From: $$From\n"; + push @Header, "Newsgroups: $$NG\n"; + push @Header, "Followup-To: $$Fup2\n" if $$Fup2; + push @Header, "Subject: $$Subject\n"; + push @Header, "Message-ID: $MID\n"; + push @Header, "Supersedes: $$Supersedes\n" if $$Supersedes; + push @Header, "Date: $date\n"; + push @Header, "Expires: $expdate\n"; + push @Header, "Sender: $$Sender\n" if $$Sender; + push @Header, "Mime-Version: 1.0\n"; + push @Header, "Reply-To: $$ReplyTo\n" if $$ReplyTo; + push @Header, "Content-Type: $ContentType\n"; + push @Header, "Content-Transfer-Encoding: 8bit\n"; + push @Header, "User-Agent: yapfaq/$VERSION\n"; + if ($$ExtraHeaders) { + push @Header, "$_\n" for (split /\n/, $$ExtraHeaders); + } + + my @Article = (@Header, "\n", @Body); + + # post article + print "$$ActName: Posting article ...\n" if($Options{'v'}); + my $failure = post(\@Article); + + if ($failure) { + print "$$ActName: Posting failed, ERROR.dat may have more information.\n" if($Options{'v'} && (!defined($Options{'t'}) || $Options{'t'} !~ /console/i)); + } else { + updatestatus($ActName, $File, "$day.$month.$year", $MID) if !defined($Options{'t'}); + } +} + +################################## post ################################## +# Takes a complete article (Header and Body). +# +# It opens a connection to $NNTPServer and posts the message. + +sub post { + my ($ArticleR) = @_; + my ($failure) = -1; + + # test mode - print article to console + if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) { + print "-----BEGIN--------------------------------------------------\n"; + print @$ArticleR; + print "------END---------------------------------------------------\n"; + # pipe article to script + } elsif(defined($Options{'s'})) { + open (POST, "| $Options{'s'}") or die "$0: E: Cannot fork $Options{'s'}: $!\n"; + print POST @$ArticleR; + close POST; + if ($? == 0) { + $failure = 0; + } else { + warn "$0: W: $Options{'s'} exited with status ", ($? >> 8), "\n"; + $failure = $?; + } + # post article + } else { + my $NewsConnection = Net::NNTP->new($Config{'NNTPServer'}, Reader => 1) or die "$0: E: Can't connect to news server '$Config{'NNTPServer'}'!\n"; + $NewsConnection->authinfo ($Config{'NNTPUser'}, $Config{'NNTPPass'}) if (defined($Config{'NNTPUser'})); + $NewsConnection->post(); + $NewsConnection->datasend (@$ArticleR); + $NewsConnection->dataend(); + + if ($NewsConnection->ok()) { + $failure = 0; + # Posting failed? Save to ERROR.dat + } else { + warn "$0: W: Posting failed!\n"; + open FH, ">>ERROR.dat"; + print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n"; + print FH $NewsConnection->code(); + print FH $NewsConnection->message(); + print FH "\n"; + print FH @$ArticleR; + print FH "-" x 80, "\n"; + close FH; + } + $NewsConnection->quit(); + } + return $failure; +} + +__END__ + +################################ Documentation ################################# + +=head1 NAME + +yapfaq - Post Usenet FAQs I<(yet another postfaq)> + +=head1 SYNOPSIS + +B [B<-Vhvpd>] [B<-t> I | CONSOLE] [B<-f> I] [B<-s> I] [B<-c> I<.rc file>] + +=head1 REQUIREMENTS + +=over 2 + +=item - + +Perl 5.8 or later + +=item - + +Net::NNTP + +=item - + +Date::Calc + +=item - + +Getopt::Std + +=back + +Furthermore you need access to a news server to actually post FAQs. + +=head1 DESCRIPTION + +B posts (one or more) FAQs to Usenet with a certain posting +frequency (every n days, weeks, months or years), adding all necessary +headers as defined in its config file (by default F). + +=head2 Configuration + +F consists of one or more blocks, separated by C<=====> on +a single line, each containing the configuration for one FAQ as a set +of definitions in the form of I. Everything after a "#" +sign is ignored so you may comment your configuration file. + +=over 4 + +=item B = I + +A name referring to your FAQ, also used for generation of a Message-ID. + +This value must be set. + +=item B = I + +A file containing the message body of your FAQ and all pseudo headers +(subheaders in the news.answers style). + +This value must be set. + +=item B = I