#! /usr/bin/perl -W
#
-# yapfaq Version 0.6 by Thomas Hochstein
+# 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 <marc@marcbrockschmidt.de>
-# Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
+# Copyright (c) 2010-2017 Thomas Hochstein <thh@thh.name>
#
# It can be redistributed and/or modified under the same terms under
# which Perl itself is published.
-my $Version = "0.6.2";
+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';
-my @ValidConfVars = ('NNTPServer','NNTPUser','NNTPPass','Sender','ConfigFile',
- 'UsePGP','pgp','PGPVersion','PGPSigner','PGPPass',
- 'PathtoPGPPass','pgpbegin','pgpend','pgptmpf','pgpheader');
+# Valid configuration variables for use in a .rc file
+my @ValidConfVars = ('NNTPServer','NNTPUser','NNTPPass','Sender','ConfigFile','Program');
-################################### Defaults ##################################
-my %Config = (NNTPServer => "localhost",
+################################### 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",
- UsePGP => 0,
-
- ################################## PGP-Config #################################
- pgp => '/usr/bin/pgp', # path to pgp
- PGPVersion => '2', # Use 2 for 2.X, 5 for PGP > 2.X and GPG for GPG
- PGPSigner => '', # sign as who?
- PGPPass => '', # pgp2 only
- PathtoPGPPass => '', # pgp2, pgp5 and gpg
- pgpbegin => '-----BEGIN PGP SIGNATURE-----', # Begin of PGP-Signature
- pgpend => '-----END PGP SIGNATURE-----', # End of PGP-Signature
- pgptmpf => 'pgptmp', # temporary file for PGP.
- pgpheader => 'X-PGP-Sig');
-
-my @PGPSignHeaders = ('From', 'Newsgroups', 'Subject', 'Control',
- 'Supersedes', 'Followup-To', 'Date', 'Sender', 'Approved',
- 'Message-ID', 'Reply-To', 'Cancel-Lock', 'Cancel-Key',
- 'Also-Control', 'Distribution');
-
-my @PGPorderheaders = ('from', 'newsgroups', 'subject', 'control',
- 'supersedes', 'followup-To', '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');
-
-############################# End of Configuration #############################
+ 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 Fcntl ':flock'; # import LOCK_* constants
use Getopt::Std;
+$Getopt::Std::STANDARD_HELP_VERSION = 1;
my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
# read commandline options
getopts('Vhvpdt:f:c:s:', \%Options);
# -V: print version / copyright information
if ($Options{'V'}) {
- print "$0 v $Version\nCopyright (c) 2003 Marc Brockschmidt <marc\@marcbrockschmidt.de>\nCopyright (c) 2010 Thomas Hochstein <thh\@inter.net>\n";
+ print "$0 v $VERSION\nCopyright (c) 2003 Marc Brockschmidt <marc\@marcbrockschmidt.de>\nCopyright (c) 2010-2017 Thomas Hochstein <thh\@thh.name>\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);
}
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);
my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'});
my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'});
- my ($MIDF,$ReplyTo,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'extraheader'});
+ my ($MIDF,$ReplyTo,$Charset,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'charset'},$$_{'extraheader'});
my ($Supersede) =($$_{'supersede'});
# -f: loop if not FAQ to post
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,\$ExtHea,\$Config{'Sender'},\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
+ 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";
my ($File, $Config, $Faq) = @_;
my ($LastEntry, $Error, $i) = ('','',0);
- print "Reading configuration.\n" if($Options{'v'});
+ print "Reading configuration from $$File.\n" if($Options{'v'});
open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
while (<FH>) {
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{2,}\.\S{2,}>$/) {
+ 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-%d.%m.%y@'.hostfqdn.'>'; # set default if mid-format is invalid
+ $$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;
# It reads the data-file $File and then posts the article.
sub postfaq {
- my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
+ 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-%d.%m.%y@'.hostfqdn.'>' if !defined($MID); # set to default if unset
+ $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");
s/\r//;
push (@Body, $_), next if $InRealBody;
$InRealBody++ if /^$/;
- $LastModified = $1 if /^Last-modified: (\S+)$/i;
+ $LastModified = $1 if /^Last-modified:\s*(\S+)\s*$/i;
push @Body, $_;
}
close FH;
#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, "Sender: $$Sender\n" if $$Sender;
push @Header, "Mime-Version: 1.0\n";
push @Header, "Reply-To: $$ReplyTo\n" if $$ReplyTo;
- push @Header, "Content-Type: text/plain; charset=ISO-8859-15\n";
+ push @Header, "Content-Type: $ContentType\n";
push @Header, "Content-Transfer-Encoding: 8bit\n";
- push @Header, "User-Agent: yapfaq/$Version\n";
+ push @Header, "User-Agent: yapfaq/$VERSION\n";
if ($$ExtraHeaders) {
push @Header, "$_\n" for (split /\n/, $$ExtraHeaders);
}
- # sign article if $UsePGP is true
- my @Article = ($Config{'UsePGP'})?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
-
+ my @Article = (@Header, "\n", @Body);
+
# post article
print "$$ActName: Posting article ...\n" if($Options{'v'});
my $failure = post(\@Article);
return $failure;
}
-#-------- sub getpgpcommand
-# getpgpcommand generates the command to sign the message and returns it.
-#
-# Receives:
-# - $PGPVersion: A scalar holding the PGPVersion
-sub getpgpcommand {
- my ($PGPVersion) = @_;
- my $PGPCommand;
-
- if ($PGPVersion eq '2') {
- if ($Config{'PathtoPGPPass'} && !$Config{'PGPPass'}) {
- open (PGPPW, $Config{'PathtoPGPPass'}) or die "$0: E: Can't open $Config{'PathtoPGPPass'}: $!";
- Config{'$PGPPass'} = <PGPPW>;
- close PGPPW;
- }
-
- if (Config{'$PGPPass'}) {
- $PGPCommand = "PGPPASS=\"".$Config{'PGPPass'}."\" ".$Config{'pgp'}." -u \"".$Config{'PGPSigner'}."\" +verbose=0 language='en' -saft <".$Config{'pgptmpf'}.".txt >".$Config{'pgptmpf'}.".txt.asc";
- } else {
- die "$0: E: PGP-Passphrase is unknown!\n";
- }
- } elsif ($PGPVersion eq '5') {
- if ($Config{'PathtoPGPPass'}) {
- $PGPCommand = "PGPPASSFD=2 ".$Config{'pgp'}."s -u \"".$Config{'PGPSigner'}."\" -t --armor -o ".$Config{'pgptmpf'}.".txt.asc -z -f < ".$Config{'pgptmpf'}.".txt 2<".$Config{'PathtoPGPPass'};
- } else {
- die "$0: E: PGP-Passphrase is unknown!\n";
- }
- } elsif ($PGPVersion =~ m/GPG/io) {
- if (Config{'$PathtoPGPPass'}) {
- $PGPCommand = $Config{'pgp'}." --digest-algo MD5 -a -u \"".$Config{'PGPSigner'}."\" -o ".$Config{'pgptmpf'}.".txt.asc --no-tty --batch --passphrase-fd 2 2<".$Config{'PathtoPGPPass'}." --clearsign ".$Config{'pgptmpf'}.".txt";
- } else {
- die "$0: E: Passphrase is unknown!\n";
- }
- } else {
- die "$0: E: Unknown PGP-Version $PGPVersion!";
- }
- return $PGPCommand;
-}
-
-
-#-------- sub signarticle
-# signarticle signs an articel and returns a reference to an array
-# containing the whole signed Message.
-#
-# Receives:
-# - $HeaderAR: A reference to a array 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 signpgp {
- my ($HeaderAR, $BodyR) = @_;
- my (@pgphead, @pgpbody, $pgphead, $pgpbody, $header, $signheaders, @signheaders, $currentheader, $HeaderR, $line);
-
- foreach my $line (@$HeaderAR) {
- if ($line =~ /^(\S+):\s+(.*)$/s) {
- $currentheader = $1;
- $$HeaderR{lc($currentheader)} = "$1: $2";
- } else {
- $$HeaderR{lc($currentheader)} .= $line;
- }
- }
-
- foreach (@PGPSignHeaders) {
- if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
- push @signheaders, $_;
- }
- }
-
- $pgpbody = join ("", @$BodyR);
-
- # Delete and create the temporary pgp-Files
- unlink "$Config{'pgptmpf'}.txt";
- unlink "$Config{'pgptmpf'}.txt.asc";
- $signheaders = join(",", @signheaders);
-
- $pgphead = "X-Signed-Headers: $signheaders\n";
- foreach $header (@signheaders) {
- if ($$HeaderR{lc($header)} =~ m/^[^\s:]+: (.+?)\n?$/so) {
- $pgphead .= $header.": ".$1."\n";
- }
- }
-
- open(FH, ">" . $Config{'pgptmpf'} . ".txt") or die "$0: E: can't open $Config{'pgptmpf'}: $!\n";
- print FH $pgphead, "\n", $pgpbody;
- print FH "\n" if ($Config{'PGPVersion'} =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
- close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
-
- # Start PGP, then read the signature;
- my $PGPCommand = getpgpcommand($Config{'PGPVersion'});
- `$PGPCommand`;
-
- open (FH, "<" . $Config{'pgptmpf'} . ".txt.asc") or die "$0: E: can't open ".$Config{'pgptmpf'}.".txt.asc: $!\n";
- $/ = "$Config{'pgpbegin'}\n";
- $_ = <FH>;
- unless (m/\Q$Config{'pgpbegin'}\E$/o) {
-# unlink $Config{'pgptmpf'} . ".txt";
-# unlink $Config{'pgptmpf'} . ".txt.asc";
- die "$0: E: $Config{'pgpbegin'} not found in ".$Config{'pgptmpf'}.".txt.asc\n"
- }
- unlink($Config{'pgptmpf'} . ".txt") or warn "$0: W: Couldn't unlink $Config{'pgptmpf'}.txt: $!\n";
-
- $/ = "\n";
- $_ = <FH>;
- unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
- unlink $Config{'pgptmpf'} . ".txt";
- unlink $Config{'pgptmpf'} . ".txt.asc";
- die "$0: E: 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";
- unlink $Config{'pgptmpf'} . ".txt.asc";
- die "$0: E: unexpected data following $Config{'pgpend'}\n";
- }
- close(FH);
- unlink "$Config{'pgptmpf'}.txt.asc";
-
- my $tmppgpheader = $Config{'pgpheader'} . ": " . $$HeaderR{$Config{'pgpheader'}};
- delete $$HeaderR{$Config{'pgpheader'}};
-
- @pgphead = ();
- foreach $header (@PGPorderheaders) {
- if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
- push(@pgphead, "$$HeaderR{$header}");
- delete $$HeaderR{$header};
- }
- }
-
- foreach $header (keys %$HeaderR) {
- if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
- push(@pgphead, "$$HeaderR{$header}");
- delete $$HeaderR{$header};
- }
- }
-
- push @pgphead, ("X-PGP-Key: " . $Config{'PGPSigner'} . "\n"), $tmppgpheader;
- undef $tmppgpheader;
-
- @pgpbody = split /$/m, $pgpbody;
- my @pgpmessage = (@pgphead, "\n", @pgpbody);
- return \@pgpmessage;
-}
-
__END__
################################ Documentation #################################
=head1 SYNOPSIS
-B<yapfaq> [B<-hvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>] [B<-c> I<.rc file>]
+B<yapfaq> [B<-Vhvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>] [B<-c> I<.rc file>]
=head1 REQUIREMENTS
the Message-ID header of the message.
You may use the special strings C<%n> for the I<Name> of your project,
-C<%d> for the date the message is posted, C<%m> for the month and
-C<%y> for the year, respectively.
+C<%d> for the date the message is posted, C<%m> for the month, C<%y>
+for the year and C<%t> for a time stamp (number of seconds since the
+epoch), respectively.
-This setting is optional; the default is '<%n-%d.%m.%y@I<YOURHOST>>'
+This setting is optional; the default is '<%n-%y-%m-%d@I<YOURHOST>>'
where I<YOURHOST> is the fully qualified domain name (FQDN) of the
host B<yapfaq> is running on. Obviously that will only work if you
have defined a reasonable hostname that the hostfqdn() function of
Net::Domain can return.
+=item B<Charset> = I<encoding> (optional)
+
+The character encoding of your FAQ. This setting is optional, but
+should match the encoding of your FAQ B<File>. Default is set to
+I<UTF-8>.
+
+This setting is copied verbatim to the I<Content-Type> header.
+
=item B<Supersede> = I<yes> (optional)
Add Supersedes header to the message containing the Message-ID header
# Message-ID ("%n" is $Name)
# MID-Format = '<%n-%d.%m.%y@domain.invalid>'
+ # Character Encoding
+ # This setting is optional. Default: UTF-8
+ # Charset = ISO-8859-15
+
# Supersede last posting?
Supersede = yes
The configuration file defining the FAQ(s) to post. Must be set (or
omitted; the default is "yapfaq.cfg").
-=item B<UsePGP> = I<whether to add a digital signature> (optional)
+=item B<Program> = I<file name> (optional)
-Boolean value (0 or 1) controlling whether the FAQs will get digitally
-signed via an X-PGP-Sig header.
+A program the article is piped to instead of posting it to Usenet.
+See option "-f" below (which takes preference).
-This setting is optional; the default is 0.
-
-If you have set I<UsePGP> to 1, you must also supply the necessary
-information on your PGP oder GPG installation; please refer to the
-sample F<.yapfaqrc> file (see below) for more information on this
-topic.
+This setting is optional.
=back
NNTPPass = ''
Sender = ''
ConfigFile = 'yapfaq.cfg'
- UsePGP = 0
-
- ################################## PGP-Config #################################
- pgp = '/usr/bin/pgp' # path to pgp
- PGPVersion = '2' # Use 2 for 2.X 5 for PGP > 2.X and GPG for GPG
- PGPSigner = '' # sign as who?
- PGPPass = '' # pgp2 only
- PathtoPGPPass = '' # pgp2 pgp5 and gpg
- pgpbegin = '-----BEGIN PGP SIGNATURE-----' # Begin of PGP-Signature
- pgpend = '-----END PGP SIGNATURE-----' # End of PGP-Signature
- pgptmpf = 'pgptmp' # temporary file for PGP.
- pgpheader = 'X-PGP-Sig'
+ Program = ''
=head3 Using more than one runtime configuration
=item B<-t> I<newsgroup(s) | CONSOLE> (test)
Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
-newsgroups given after B<-t> as a comma-separated list or print the
-FAQs to STDOUT separated by lines of dashes if the special string
-C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
-do without embarassing yourself on Usenet. The status files are not
-updated when this option is given.
+(test) newsgroup(s) given after B<-t> as a comma-separated list or
+print the FAQs to STDOUT separated by lines of dashes if the special
+string C<CONSOLE> is given. This can be used to preview what
+B<yapfaq> would do without embarassing yourself on Usenet.
+
+The status files are not updated when this option is given.
+
+When this option is used to post to some other newsgroup(s), a(nother)
+timestamp is added to the Message-ID header and the Supersedes header
+is replaced by a special X-Supersedes header.
You may want to use this with the B<-f> option (see below).
I<program> on STDIN (which may post the article(s) then). A return
value of 0 will be considered success.
+For example, you may want to use the I<inews> utility from the INN package
+or the much more powerful replacement I<tinews.pl> from
+I<ftp://ftp.tin.org/tin/tools/tinews.pl> which is able to sign postings.
+
+If I<Program> is also defined in the runtime configuration file (by default
+F<.yapfaqrc>), B<-s> takes preference.
+
=item B<-c> I<.rc file>
Load another runtime configuration file (.rc file) than F<.yaofaq.rc>.
=back
+=head1 INSTALLATION
+
+Just copy the contents of the tarball in some directory and get started.
+
+You can post your first test with
+
+ yapfaq -c .yapfaqrc.sample
+
+or copy F<.yapfaqrc.sample> to F<.yapfaqrc> and F<yapfaq.cfg.sample>
+to F<yapfaq.cfg>, edit those files and get really started!
+
+=back
+
=head1 EXAMPLES
Post all FAQs that are due for posting:
yapfaq -t de.test -f myfaq
+Post all FAQs (that are due for posting) using inews from INN:
+
+ yapfaq -s inews
+
+Do a dry run using a runtime configuration from .alternaterc, showing
+which FAQs would be posted:
+
+ yapfaq -dvc .alternaterc
+
=head1 ENVIRONMENT
-There are no special environment variables used by B<yapfaq>.
+=over 4
+
+=item NNTPSERVER
+
+The default NNTP server to post to, used by the Net::NNTP module. You
+can also specify the server using the runtime configuration file (by
+default F<.yapfaqrc>).
+
+=back
=head1 FILES
=head1 BUGS
-Many, I'm sure.
+Please report any bugs or feature requests to the author or use the
+bug tracker at L<https://bugs.th-h.de/>!
=head1 SEE ALSO
-L<http://th-h.de/download/scripts.php> will have the current
+L<https://th-h.de/net/software/yapfaq/> will have the current
version of this program.
+This program is maintained using the Git version control system. You
+may clone L<git://code.th-h.de/usenet/yapfaq.git> to check out the
+current development tree or browse it on the web via
+L<https://code.th-h.de/?p=usenet/yapfaq.git>.
+
=head1 AUTHOR
-Thomas Hochstein <thh@inter.net>
+Thomas Hochstein <thh@thh.name>
Original author (up to version 0.5b, dating from 2003):
Marc Brockschmidt <marc@marcbrockschmidt.de>
Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
-Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
+Copyright (c) 2010-2017 Thomas Hochstein <thh@thh.name>
This program is free software; you may redistribute it and/or modify it
under the same terms as Perl itself.