Add comments pointing to .yapfaqrc to source.
[usenet/yapfaq.git] / yapfaq.pl
CommitLineData
dc88d139
TH
1#! /usr/bin/perl -W
2#
ace701c4 3# yapfaq Version 0.7 by Thomas Hochstein
7aaba0e0 4# (Original author: Marc Brockschmidt)
dc88d139 5#
7aaba0e0 6# This script posts any project described in its config-file. Most people
dc88d139
TH
7# will use it in combination with cron(8).
8#
9# Copyright (C) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
7aaba0e0 10# Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
dc88d139
TH
11#
12# It can be redistributed and/or modified under the same terms under
13# which Perl itself is published.
14
647af2ea 15my $Version = "0.8-prelease";
dc88d139 16
605916ef
TH
17# Please do not change this setting!
18# You may override the default .rc file (.yapfaqrc) by using "-c .rc file"
d60c2d5f 19my $RCFile = '.yapfaqrc';
605916ef 20# Valid configuration variables for use in a .rc file
d60c2d5f
TH
21my @ValidConfVars = ('NNTPServer','NNTPUser','NNTPPass','Sender','ConfigFile',
22 'UsePGP','pgp','PGPVersion','PGPSigner','PGPPass',
23 'PathtoPGPPass','pgpbegin','pgpend','pgptmpf','pgpheader');
24
605916ef
TH
25################################### Defaults ###################################
26# Please do not change anything in here!
27# Use a runtime configuration file (.yapfaqrc by default) to override defaults.
7ef63844 28my %Config = (NNTPServer => "",
2507947f
TH
29 NNTPUser => "",
30 NNTPPass => "",
31 Sender => "",
32 ConfigFile => "yapfaq.cfg",
33 UsePGP => 0,
34
35 ################################## PGP-Config #################################
36 pgp => '/usr/bin/pgp', # path to pgp
37 PGPVersion => '2', # Use 2 for 2.X, 5 for PGP > 2.X and GPG for GPG
38 PGPSigner => '', # sign as who?
39 PGPPass => '', # pgp2 only
40 PathtoPGPPass => '', # pgp2, pgp5 and gpg
41 pgpbegin => '-----BEGIN PGP SIGNATURE-----', # Begin of PGP-Signature
42 pgpend => '-----END PGP SIGNATURE-----', # End of PGP-Signature
43 pgptmpf => 'pgptmp', # temporary file for PGP.
44 pgpheader => 'X-PGP-Sig');
dc88d139
TH
45
46my @PGPSignHeaders = ('From', 'Newsgroups', 'Subject', 'Control',
47 'Supersedes', 'Followup-To', 'Date', 'Sender', 'Approved',
48 'Message-ID', 'Reply-To', 'Cancel-Lock', 'Cancel-Key',
49 'Also-Control', 'Distribution');
50
51my @PGPorderheaders = ('from', 'newsgroups', 'subject', 'control',
52 'supersedes', 'followup-To', 'date', 'organization', 'lines',
53 'sender', 'approved', 'distribution', 'message-id',
54 'references', 'reply-to', 'mime-version', 'content-type',
55 'content-transfer-encoding', 'summary', 'keywords', 'cancel-lock',
56 'cancel-key', 'also-control', 'x-pgp', 'user-agent');
57
605916ef 58################################# Main program #################################
dc88d139
TH
59
60use strict;
61use Net::NNTP;
40847f71 62use Net::Domain qw(hostfqdn);
dc88d139
TH
63use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today);
64use Fcntl ':flock'; # import LOCK_* constants
4251e545 65use Getopt::Std;
dc88d139
TH
66my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
67
b9550622 68# read commandline options
4251e545 69my %Options;
86c0a100 70getopts('Vhvpdt:f:c:s:', \%Options);
b9550622 71# -V: print version / copyright information
a052296f
TH
72if ($Options{'V'}) {
73 print "$0 v $Version\nCopyright (c) 2003 Marc Brockschmidt <marc\@marcbrockschmidt.de>\nCopyright (c) 2010 Thomas Hochstein <thh\@inter.net>\n";
74 print "This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.\n";
75 exit(0);
76}
b9550622 77# -h: feed myself to perldoc
4251e545 78if ($Options{'h'}) {
ae3b1b79 79 exec ('perldoc', $0);
4251e545
TH
80 exit(0);
81};
b9550622 82# -f: set $Faq
4251e545
TH
83my ($Faq) = $Options{'f'} if ($Options{'f'});
84
d60c2d5f 85# read runtime configuration (configuration variables)
86c0a100
TH
86$RCFile = $Options{'c'} if ($Options{'c'});
87if (-f $RCFile) {
88 readrc (\$RCFile,\%Config);
89} else {
90 warn "$0: W: .rc file $RCFile does not exist!\n";
91}
d60c2d5f 92
b9550622 93# read configuration (configured FAQs)
dc88d139 94my @Config;
2507947f 95readconfig (\$Config{'ConfigFile'}, \@Config, \$Faq);
dc88d139 96
b9550622
TH
97# for each FAQ:
98# - parse configuration
99# - read status data
100# - if FAQ is due: call postfaq()
dc88d139
TH
101foreach (@Config) {
102 my ($LPD,$LPM,$LPY) = (01, 01, 0001); #LP: Last posting-date
103 my ($NPY,$NPM,$NPD); #NP: Next posting-date
104 my $SupersedeMID;
105
0c6ebe78 106 my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'});
dc88d139
TH
107 my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'});
108 my ($MIDF,$ReplyTo,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'extraheader'});
109 my ($Supersede) =($$_{'supersede'});
4251e545 110
b9550622 111 # -f: loop if not FAQ to post
4251e545
TH
112 next if (defined($Faq) && $ActName ne $Faq);
113
b9550622 114 # read status data
dc88d139
TH
115 if (open (FH, "<$File.cfg")) {
116 while(<FH>){
117 if (/##;; Lastpost:\s*(\d{1,2})\.(\d{1,2})\.(\d{2}(\d{2})?)/){
118 ($LPD, $LPM, $LPY) = ($1, $2, $3);
119 } elsif (/^##;;\s*LastMID:\s*(<\S+@\S+>)\s*$/) {
120 $SupersedeMID = $1;
121 }
122 }
123 close FH;
124 } else {
74407146 125 warn "$0: W: Couldn't open $File.cfg: $!\n";
dc88d139
TH
126 }
127
128 $SupersedeMID = "" unless $Supersede;
129
0c6ebe78
TH
130 ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq);
131
b9550622 132 # if FAQ is due: get it out
4251e545
TH
133 if (Delta_Days($NPY,$NPM,$NPD,$TDY,$TDM,$TDD) >= 0 or ($Options{'p'})) {
134 if($Options{'d'}) {
135 print "$ActName: Would be posted now (but running in simulation mode [$0 -d]).\n" if $Options{'v'};
136 } else {
2507947f 137 postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$ExtHea,\$Config{'Sender'},\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
4251e545
TH
138 }
139 } elsif($Options{'v'}) {
140 print "$ActName: Nothing to do.\n";
dc88d139
TH
141 }
142}
143
144exit;
145
d60c2d5f
TH
146#################################### readrc ####################################
147# Takes a filename and the reference to an array which contains the valid options
148
149sub readrc{
150 my ($File, $Config) = @_;
151
152 print "Reading $$File.\n" if($Options{'v'});
153
154 open FH, "<$$File" or die "$0: Can't open $$File: $!";
155 while (<FH>) {
156 if (/^\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)/) {
157 if (grep(/$1/,@ValidConfVars)) {
158 $$Config{$1} = $2 if $2 ne '';
159 } else {
160 warn "$0: W: $1 is not a valid configuration variable (reading from $$File)\n";
161 }
162 }
163 }
164}
165
dc88d139 166################################## readconfig ##################################
4251e545
TH
167# Takes a filename, a reference to an array, which will hold hashes with
168# the data from $File, and - optionally - the name of the (single) FAQ to post
dc88d139
TH
169
170sub readconfig{
4251e545 171 my ($File, $Config, $Faq) = @_;
dc88d139
TH
172 my ($LastEntry, $Error, $i) = ('','',0);
173
b802358a 174 print "Reading configuration from $$File.\n" if($Options{'v'});
4251e545 175
74407146 176 open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
dc88d139 177 while (<FH>) {
4251e545 178 next if (defined($$Faq) && !/^\s*=====\s*$/ && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
dc88d139
TH
179 if (/^(\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)|^(.*?)'?\s*(#.*$|$))/ && not /^\s*$/) {
180 $LastEntry = lc($2) if $2;
181 $$Config[$i]{$LastEntry} .= $3 if $3;
182 $$Config[$i]{$LastEntry} .= "\n$5" if $5 && $5;
183 }
184 if (/^\s*=====\s*$/) {
185 $i++;
186 }
187 }
188 close FH;
189
190 #Check saved values:
191 for $i (0..$i){
4251e545 192 next if (defined($$Faq) && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
dbca4ad8
TH
193 unless(defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} =~ /^\S+$/) {
194 $Error .= "E: The name of your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
dc88d139 195 }
dbca4ad8
TH
196 unless(defined($$Config[$i]{'file'}) && -f $$Config[$i]{'file'}) {
197 $Error .= "E: The file to post for your project \"$$Config[$i]{'name'}\" is not defined or does not exist.\n"
198 }
199 unless(defined($$Config[$i]{'from'}) && $$Config[$i]{'from'} =~ /\S+\@(\S+\.)?\S{2,}\.\S{2,}/) {
200 $Error .= "E: The From header for your project \"$$Config[$i]{'name'}\" seems to be incorrect.\n"
201 }
202 unless(defined($$Config[$i]{'ngs'}) && $$Config[$i]{'ngs'} =~ /^\S+$/) {
203 $Error .= "E: The Newsgroups header for your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
204 }
205 unless(defined($$Config[$i]{'subject'})) {
206 $Error .= "E: The Subject header for your project \"$$Config[$i]{'name'}\" is not defined.\n"
dc88d139
TH
207 }
208 unless(!$$Config[$i]{'fup2'} || $$Config[$i]{'fup2'} =~ /^\S+$/) {
dbca4ad8 209 $Error .= "E: The Followup-To header for your project \"$$Config[$i]{'name'}\" contains whitespaces.\n"
dc88d139 210 }
dbca4ad8 211 unless(defined($$Config[$i]{'posting-frequency'}) && $$Config[$i]{'posting-frequency'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
74407146 212 $Error .= "E: The Posting-frequency for your project \"$$Config[$i]{'name'}\" is invalid.\n"
dc88d139 213 }
5ddba442 214 unless(!$$Config[$i]{'expires'} || $$Config[$i]{'expires'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
ac69c3ee
TH
215 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" is invalid - set to 3 month.\n";
216 $$Config[$i]{'expires'} = '3m'; # set default (3 month) if expires is unset or invalid
0c6ebe78 217 }
ac69c3ee
TH
218 unless(!$$Config[$i]{'mid-format'} || $$Config[$i]{'mid-format'} =~ /^<\S+\@\S{2,}\.\S{2,}>$/) {
219 warn "$0: W: The Message-ID format for your project \"$$Config[$i]{'name'}\" seems to be invalid - set to default.\n";
220 $$Config[$i]{'mid-format'} = '<%n-%d.%m.%y@'.hostfqdn.'>'; # set default if mid-format is invalid
40847f71 221 }
dc88d139 222 }
dbca4ad8 223 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
dc88d139
TH
224 die $Error if $Error;
225}
226
0c6ebe78
TH
227################################# calcdelta #################################
228# Takes a date (year, month and day) and a time period (1d, 1w, 1m, 1y, ...)
229# and adds the latter to the former
230
231sub calcdelta {
232 my ($Year, $Month, $Day, $Period) = @_;
233 my ($NYear, $NMonth, $NDay);
234
235 if ($Period =~ /(\d+)\s*([dw])/) { # Is counted in days or weeks: Use Add_Delta_Days.
236 ($NYear, $NMonth, $NDay) = Add_Delta_Days($Year, $Month, $Day, (($2 eq "w")?$1 * 7: $1 * 1));
237 } elsif ($Period =~ /(\d+)\s*([my])/) { #Is counted in months or years: Use Add_Delta_YM
238 ($NYear, $NMonth, $NDay) = Add_Delta_YM($Year, $Month, $Day, (($2 eq "m")?(0,$1):($1,0)));
239 }
240 return ($NYear, $NMonth, $NDay);
241}
5a6670c7
TH
242
243################################ updatestatus ###############################
244# Takes a MID and a status file name
245# and writes status information to disk
246
247sub updatestatus {
248 my ($ActName, $File, $date, $MID) = @_;
249
250 print "$$ActName: Save status information.\n" if($Options{'v'});
251
252 open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!";
253 print FH "##;; Lastpost: $date\n";
254 print FH "##;; LastMID: $MID\n";
255 close FH;
256}
0c6ebe78 257
dc88d139
TH
258################################## postfaq ##################################
259# Takes a filename and many other vars.
260#
261# It reads the data-file $File and then posts the article.
262
263sub postfaq {
0c6ebe78 264 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
dc88d139
TH
265 my (@Header,@Body,$MID,$InRealBody,$LastModified);
266
366322b2 267 print "$$ActName: Preparing to post.\n" if($Options{'v'});
4251e545 268
dc88d139
TH
269 #Prepare MID:
270 $$TDM = ($$TDM < 10 && $$TDM !~ /^0/) ? "0" . $$TDM : $$TDM;
271 $$TDD = ($$TDD < 10 && $$TDD !~ /^0/) ? "0" . $$TDD : $$TDD;
272
273 $MID = $$MIDF;
ac69c3ee 274 $MID = '<%n-%d.%m.%y@'.hostfqdn.'>' if !defined($MID); # set to default if unset
dc88d139
TH
275 $MID =~ s/\%n/$$ActName/g;
276 $MID =~ s/\%d/$$TDD/g;
277 $MID =~ s/\%m/$$TDM/g;
278 $MID =~ s/\%y/$$TDY/g;
279
dc88d139
TH
280 #Now get the body:
281 open (FH, "<$$File");
282 while (<FH>){
283 s/\r//;
284 push (@Body, $_), next if $InRealBody;
285 $InRealBody++ if /^$/;
8e1cb154 286 $LastModified = $1 if /^Last-modified: (\S+)$/i;
dc88d139
TH
287 push @Body, $_;
288 }
289 close FH;
290 push @Body, "\n" if ($Body[-1] ne "\n");
291
292 #Create Date- and Expires-Header:
293 my @time = localtime;
294 my $ss = ($time[0]<10) ? "0" . $time[0] : $time[0];
295 my $mm = ($time[1]<10) ? "0" . $time[1] : $time[1];
296 my $hh = ($time[2]<10) ? "0" . $time[2] : $time[2];
297 my $day = $time[3];
298 my $month = ($time[4]+1<10) ? "0" . ($time[4]+1) : $time[4]+1;
299 my $monthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$time[4]];
300 my $wday = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$time[6]];
301 my $year = (1900 + $time[5]);
302 my $tz = $time[8] ? " +0200" : " +0100";
7823ece9
TH
303
304 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
305
0c6ebe78 306 my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire);
dc88d139
TH
307 my $expmonthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$expM-1];
308
309 my $date = "$day $monthN $year " . $hh . ":" . $mm . ":" . $ss . $tz;
310 my $expdate = "$expD $expmonthN $expY $hh:$mm:$ss$tz";
0c6ebe78 311
dc88d139
TH
312 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
313 if ($LastModified) {
314 $$Subject =~ s/\%LM/$LastModified/;
315 }
316
4251e545
TH
317 # Test mode?
318 if($Options{'t'} and $Options{'t'} !~ /console/i) {
319 $$NG = $Options{'t'};
320 }
321
dc88d139
TH
322 #Now create the complete Header:
323 push @Header, "From: $$From\n";
324 push @Header, "Newsgroups: $$NG\n";
325 push @Header, "Followup-To: $$Fup2\n" if $$Fup2;
326 push @Header, "Subject: $$Subject\n";
327 push @Header, "Message-ID: $MID\n";
328 push @Header, "Supersedes: $$Supersedes\n" if $$Supersedes;
329 push @Header, "Date: $date\n";
330 push @Header, "Expires: $expdate\n";
331 push @Header, "Sender: $$Sender\n" if $$Sender;
332 push @Header, "Mime-Version: 1.0\n";
333 push @Header, "Reply-To: $$ReplyTo\n" if $$ReplyTo;
334 push @Header, "Content-Type: text/plain; charset=ISO-8859-15\n";
335 push @Header, "Content-Transfer-Encoding: 8bit\n";
336 push @Header, "User-Agent: yapfaq/$Version\n";
337 if ($$ExtraHeaders) {
338 push @Header, "$_\n" for (split /\n/, $$ExtraHeaders);
339 }
340
b9550622 341 # sign article if $UsePGP is true
2507947f 342 my @Article = ($Config{'UsePGP'})?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
dc88d139 343
b9550622 344 # post article
366322b2 345 print "$$ActName: Posting article ...\n" if($Options{'v'});
5a6670c7
TH
346 my $failure = post(\@Article);
347
348 if ($failure) {
349 print "$$ActName: Posting failed, ERROR.dat may have more information.\n" if($Options{'v'} && (!defined($Options{'t'}) || $Options{'t'} !~ /console/i));
350 } else {
351 updatestatus($ActName, $File, "$day.$month.$year", $MID) if !defined($Options{'t'});
352 }
dc88d139
TH
353}
354
355################################## post ##################################
356# Takes a complete article (Header and Body).
357#
358# It opens a connection to $NNTPServer and posts the message.
359
360sub post {
361 my ($ArticleR) = @_;
5a6670c7 362 my ($failure) = -1;
dc88d139 363
5a6670c7 364 # test mode - print article to console
4251e545 365 if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) {
55bfbd3c 366 print "-----BEGIN--------------------------------------------------\n";
5a6670c7 367 print @$ArticleR;
55bfbd3c 368 print "------END---------------------------------------------------\n";
5a6670c7
TH
369 # pipe article to script
370 } elsif(defined($Options{'s'})) {
b855559e
TH
371 open (POST, "| $Options{'s'}") or die "$0: E: Cannot fork $Options{'s'}: $!\n";
372 print POST @$ArticleR;
373 close POST;
5a6670c7
TH
374 if ($? == 0) {
375 $failure = 0;
376 } else {
377 warn "$0: W: $Options{'s'} exited with status ", ($? >> 8), "\n";
378 $failure = $?;
379 }
380 # post article
381 } else {
382 my $NewsConnection = Net::NNTP->new($Config{'NNTPServer'}, Reader => 1) or die "$0: E: Can't connect to news server '$Config{'NNTPServer'}'!\n";
383 $NewsConnection->authinfo ($Config{'NNTPUser'}, $Config{'NNTPPass'}) if (defined($Config{'NNTPUser'}));
384 $NewsConnection->post();
385 $NewsConnection->datasend (@$ArticleR);
386 $NewsConnection->dataend();
387
388 if ($NewsConnection->ok()) {
389 $failure = 0;
390 # Posting failed? Save to ERROR.dat
391 } else {
392 warn "$0: W: Posting failed!\n";
393 open FH, ">>ERROR.dat";
394 print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n";
395 print FH $NewsConnection->code();
396 print FH $NewsConnection->message();
397 print FH "\n";
398 print FH @$ArticleR;
399 print FH "-" x 80, "\n";
400 close FH;
401 }
402 $NewsConnection->quit();
dc88d139 403 }
5a6670c7 404 return $failure;
dc88d139
TH
405}
406
407#-------- sub getpgpcommand
408# getpgpcommand generates the command to sign the message and returns it.
409#
410# Receives:
411# - $PGPVersion: A scalar holding the PGPVersion
412sub getpgpcommand {
413 my ($PGPVersion) = @_;
414 my $PGPCommand;
415
416 if ($PGPVersion eq '2') {
2507947f
TH
417 if ($Config{'PathtoPGPPass'} && !$Config{'PGPPass'}) {
418 open (PGPPW, $Config{'PathtoPGPPass'}) or die "$0: E: Can't open $Config{'PathtoPGPPass'}: $!";
419 Config{'$PGPPass'} = <PGPPW>;
dc88d139
TH
420 close PGPPW;
421 }
422
2507947f
TH
423 if (Config{'$PGPPass'}) {
424 $PGPCommand = "PGPPASS=\"".$Config{'PGPPass'}."\" ".$Config{'pgp'}." -u \"".$Config{'PGPSigner'}."\" +verbose=0 language='en' -saft <".$Config{'pgptmpf'}.".txt >".$Config{'pgptmpf'}.".txt.asc";
dc88d139 425 } else {
74407146 426 die "$0: E: PGP-Passphrase is unknown!\n";
dc88d139
TH
427 }
428 } elsif ($PGPVersion eq '5') {
2507947f
TH
429 if ($Config{'PathtoPGPPass'}) {
430 $PGPCommand = "PGPPASSFD=2 ".$Config{'pgp'}."s -u \"".$Config{'PGPSigner'}."\" -t --armor -o ".$Config{'pgptmpf'}.".txt.asc -z -f < ".$Config{'pgptmpf'}.".txt 2<".$Config{'PathtoPGPPass'};
dc88d139 431 } else {
74407146 432 die "$0: E: PGP-Passphrase is unknown!\n";
dc88d139
TH
433 }
434 } elsif ($PGPVersion =~ m/GPG/io) {
2507947f
TH
435 if (Config{'$PathtoPGPPass'}) {
436 $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";
dc88d139 437 } else {
74407146 438 die "$0: E: Passphrase is unknown!\n";
dc88d139
TH
439 }
440 } else {
74407146 441 die "$0: E: Unknown PGP-Version $PGPVersion!";
dc88d139
TH
442 }
443 return $PGPCommand;
444}
445
446
447#-------- sub signarticle
448# signarticle signs an articel and returns a reference to an array
449# containing the whole signed Message.
450#
451# Receives:
452# - $HeaderAR: A reference to a array containing the articles headers.
453# - $BodyR: A reference to an array containing the body.
454#
455# Returns:
456# - $MessageRef: A reference to an array containing the whole message.
457sub signpgp {
458 my ($HeaderAR, $BodyR) = @_;
459 my (@pgphead, @pgpbody, $pgphead, $pgpbody, $header, $signheaders, @signheaders, $currentheader, $HeaderR, $line);
460
461 foreach my $line (@$HeaderAR) {
462 if ($line =~ /^(\S+):\s+(.*)$/s) {
463 $currentheader = $1;
464 $$HeaderR{lc($currentheader)} = "$1: $2";
465 } else {
466 $$HeaderR{lc($currentheader)} .= $line;
467 }
468 }
469
470 foreach (@PGPSignHeaders) {
471 if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
472 push @signheaders, $_;
473 }
474 }
475
476 $pgpbody = join ("", @$BodyR);
477
478 # Delete and create the temporary pgp-Files
2507947f
TH
479 unlink "$Config{'pgptmpf'}.txt";
480 unlink "$Config{'pgptmpf'}.txt.asc";
dc88d139
TH
481 $signheaders = join(",", @signheaders);
482
483 $pgphead = "X-Signed-Headers: $signheaders\n";
484 foreach $header (@signheaders) {
485 if ($$HeaderR{lc($header)} =~ m/^[^\s:]+: (.+?)\n?$/so) {
486 $pgphead .= $header.": ".$1."\n";
487 }
488 }
489
2507947f 490 open(FH, ">" . $Config{'pgptmpf'} . ".txt") or die "$0: E: can't open $Config{'pgptmpf'}: $!\n";
dc88d139 491 print FH $pgphead, "\n", $pgpbody;
2507947f 492 print FH "\n" if ($Config{'PGPVersion'} =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
74407146 493 close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
dc88d139
TH
494
495 # Start PGP, then read the signature;
2507947f 496 my $PGPCommand = getpgpcommand($Config{'PGPVersion'});
dc88d139
TH
497 `$PGPCommand`;
498
2507947f
TH
499 open (FH, "<" . $Config{'pgptmpf'} . ".txt.asc") or die "$0: E: can't open ".$Config{'pgptmpf'}.".txt.asc: $!\n";
500 $/ = "$Config{'pgpbegin'}\n";
dc88d139 501 $_ = <FH>;
2507947f
TH
502 unless (m/\Q$Config{'pgpbegin'}\E$/o) {
503# unlink $Config{'pgptmpf'} . ".txt";
504# unlink $Config{'pgptmpf'} . ".txt.asc";
505 die "$0: E: $Config{'pgpbegin'} not found in ".$Config{'pgptmpf'}.".txt.asc\n"
dc88d139 506 }
2507947f 507 unlink($Config{'pgptmpf'} . ".txt") or warn "$0: W: Couldn't unlink $Config{'pgptmpf'}.txt: $!\n";
dc88d139
TH
508
509 $/ = "\n";
510 $_ = <FH>;
511 unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
2507947f
TH
512 unlink $Config{'pgptmpf'} . ".txt";
513 unlink $Config{'pgptmpf'} . ".txt.asc";
74407146 514 die "$0: E: didn't find PGP Version line where expected.\n";
dc88d139
TH
515 }
516
517 if (defined($2)) {
2507947f 518 $$HeaderR{$Config{'pgpheader'}} = $1."-".$2." ".$signheaders;
dc88d139 519 } else {
2507947f 520 $$HeaderR{$Config{'pgpheader'}} = $1." ".$signheaders;
dc88d139
TH
521 }
522
523 do { # skip other pgp headers like
524 $_ = <FH>; # "charset:"||"comment:" until empty line
525 } while ! /^$/;
526
527 while (<FH>) {
528 chomp;
2507947f
TH
529 last if /^\Q$Config{'pgpend'}\E$/;
530 $$HeaderR{$Config{'pgpheader'}} .= "\n\t$_";
dc88d139
TH
531 }
532
2507947f 533 $$HeaderR{$Config{'pgpheader'}} .= "\n" unless ($$HeaderR{$Config{'pgpheader'}} =~ /\n$/s);
dc88d139
TH
534
535 $_ = <FH>;
536 unless (eof(FH)) {
2507947f
TH
537 unlink $Config{'pgptmpf'} . ".txt";
538 unlink $Config{'pgptmpf'} . ".txt.asc";
539 die "$0: E: unexpected data following $Config{'pgpend'}\n";
dc88d139
TH
540 }
541 close(FH);
2507947f 542 unlink "$Config{'pgptmpf'}.txt.asc";
dc88d139 543
2507947f
TH
544 my $tmppgpheader = $Config{'pgpheader'} . ": " . $$HeaderR{$Config{'pgpheader'}};
545 delete $$HeaderR{$Config{'pgpheader'}};
dc88d139
TH
546
547 @pgphead = ();
548 foreach $header (@PGPorderheaders) {
549 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
550 push(@pgphead, "$$HeaderR{$header}");
551 delete $$HeaderR{$header};
552 }
553 }
554
555 foreach $header (keys %$HeaderR) {
556 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
557 push(@pgphead, "$$HeaderR{$header}");
558 delete $$HeaderR{$header};
559 }
560 }
561
2507947f 562 push @pgphead, ("X-PGP-Key: " . $Config{'PGPSigner'} . "\n"), $tmppgpheader;
dc88d139
TH
563 undef $tmppgpheader;
564
565 @pgpbody = split /$/m, $pgpbody;
566 my @pgpmessage = (@pgphead, "\n", @pgpbody);
567 return \@pgpmessage;
568}
272b0243
TH
569
570__END__
571
572################################ Documentation #################################
573
574=head1 NAME
575
576yapfaq - Post Usenet FAQs I<(yet another postfaq)>
577
578=head1 SYNOPSIS
579
9817f98a 580B<yapfaq> [B<-Vhvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>] [B<-c> I<.rc file>]
272b0243
TH
581
582=head1 REQUIREMENTS
583
584=over 2
585
586=item -
587
588Perl 5.8 or later
589
590=item -
591
592Net::NNTP
593
594=item -
595
596Date::Calc
597
598=item -
599
600Getopt::Std
601
602=back
603
604Furthermore you need access to a news server to actually post FAQs.
605
606=head1 DESCRIPTION
607
608B<yapfaq> posts (one or more) FAQs to Usenet with a certain posting
609frequency (every n days, weeks, months or years), adding all necessary
610headers as defined in its config file (by default F<yapfaq.cfg>).
611
612=head2 Configuration
613
614F<yapfaq.cfg> consists of one or more blocks, separated by C<=====> on
615a single line, each containing the configuration for one FAQ as a set
227afd47
TH
616of definitions in the form of I<param = value>. Everything after a "#"
617sign is ignored so you may comment your configuration file.
272b0243
TH
618
619=over 4
620
621=item B<Name> = I<project name>
622
623A name referring to your FAQ, also used for generation of a Message-ID.
624
625This value must be set.
626
627=item B<File> = I<file name>
628
629A file containing the message body of your FAQ and all pseudo headers
630(subheaders in the news.answers style).
631
632This value must be set.
633
634=item B<Posting-frequency> = I<time period>
635
636The posting frequency defines how often your FAQ will be posted.
637B<yapfaq> will only post your FAQ if this period of time has passed
638since the last posting.
639
640You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
641or I<B<m>onths> or I<B<y>ears>.
642
643This value must be set.
644
ac69c3ee 645=item B<Expires> = I<time period> (optional)
272b0243
TH
646
647The period of time after which your message will expire. An Expires
648header will be calculated adding this time period to today's date.
649
650You can declare this time period either in I<B<d>ays> or I<B<w>weeks>
651or I<B<m>onths> or I<B<y>ears>.
652
ac69c3ee 653This setting is optional; the default is 3 months.
272b0243
TH
654
655=item B<From> = I<author>
656
657The author of your FAQ as it will appear in the From header of the
658message.
659
660This value must be set.
661
662=item B<Subject> = I<subject>
663
664The title of your FAQ as it will appear in the Subject header of the
665message.
666
667You may use the special string C<%LM> which will be replaced with
668the contents of the Last-Modified subheader in your I<File>.
669
670This value must be set.
671
672=item B<NGs> = I<newsgroups>
673
674A comma-separated list of newsgroup(s) to post your FAQ to as it will
675appear in the Newsgroups header of the message.
676
677This value must be set.
678
ac69c3ee 679=item B<Fup2> = I<newsgroup | poster> (optional)
272b0243
TH
680
681A comma-separated list of newsgroup(s) or the special string I<poster>
682as it will appear in the Followup-To header of the message.
683
684This setting is optional.
685
ac69c3ee 686=item B<MID-Format> = I<pattern> (optional)
272b0243
TH
687
688A pattern from which the message ID is generated as it will appear in
689the Message-ID header of the message.
690
691You may use the special strings C<%n> for the I<Name> of your project,
692C<%d> for the date the message is posted, C<%m> for the month and
693C<%y> for the year, respectively.
694
ac69c3ee
TH
695This setting is optional; the default is '<%n-%d.%m.%y@I<YOURHOST>>'
696where I<YOURHOST> is the fully qualified domain name (FQDN) of the
697host B<yapfaq> is running on. Obviously that will only work if you
698have defined a reasonable hostname that the hostfqdn() function of
699Net::Domain can return.
272b0243 700
ac69c3ee 701=item B<Supersede> = I<yes> (optional)
272b0243
TH
702
703Add Supersedes header to the message containing the Message-ID header
704of the last posting.
705
706This setting is optional; you should set it to yes or leave it out.
707
ac69c3ee 708=item B<ExtraHeader> = I<additional headers> (optional)
272b0243
TH
709
710The contents of I<ExtraHeader> is added verbatim to the headers of
711your message so you can add custom headers like Approved.
712
713This setting is optional.
714
715=back
716
227afd47 717=head3 Example configuration file
272b0243
TH
718
719 # name of your project
720 Name = 'testpost'
721
722 # file to post (complete body and pseudo-headers)
723 # ($File.cfg contains data on last posting and last MID)
724 File = 'test.txt'
725
726 # how often your project should be posted
727 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
728 Posting-frequency = '1d'
729
730 # time period after which the posting should expire
731 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
ac69c3ee 732 # Expires = '3m'
272b0243
TH
733
734 # header "From:"
735 From = 'test@domain.invalid'
736
737 # header "Subject:"
738 # (may contain "%LM" which will be replaced by the contents of the
739 # Last-Modified pseudo header).
740 Subject = 'test noreply ignore'
741
742 # comma-separated list of newsgroup(s) to post to
743 # (header "Newsgroups:")
744 NGs = 'de.test'
745
746 # header "Followup-To:"
ac69c3ee 747 # Fup2 = 'poster'
272b0243
TH
748
749 # Message-ID ("%n" is $Name)
ac69c3ee 750 # MID-Format = '<%n-%d.%m.%y@domain.invalid>'
272b0243
TH
751
752 # Supersede last posting?
753 Supersede = yes
754
755 # extra headers (appended verbatim)
756 # use this for custom headers like "Approved:"
757 ExtraHeader = 'Approved: moderator@domain.invalid
758 X-Header: Some text'
759
760 # other projects may follow separated with "====="
761 =====
762
763 Name = 'othertest'
764 File = 'test.txt'
765 Posting-frequency = '2m'
766 From = 'My Name <my.name@domain.invalid>'
767 Subject = 'Test of yapfag <%LM>'
768 NGs = 'de.test,de.alt.test'
769 Fup2 = 'de.test'
770 MID-Format = '<%n-%m.%y@domain.invalid>'
771 Supersede = yes
772
227afd47
TH
773=head3 Status Information
774
272b0243
TH
775Information about the last post and about how to form message IDs for
776posts is stored in a file named F<I<project name>.cfg> which will be
777generated if it does not exist. Each of those status files will
778contain two lines, the first being the date of the last time the FAQ
779was posted and the second being the message ID of that incarnation.
780
227afd47
TH
781=head2 Runtime Configuration
782
783Apart from configuring which FAQ(s) to post you may (re)set some
784runtime configuration variables via the .rcfile (by default
785F<.yapfaqrc>). F<.yapfaqrc> must contain one definition in the form of
786I<param = value> on each line; everything after a "#" sign is ignored.
787
788If you omit some settings they will be set to default values hardcoded
789in F<yapfaq.pl>.
790
791B<Please note that all parameter names are case-sensitive!>
792
793=over 4
794
795=item B<NNTPServer> = I<NNTP server> (mandatory)
796
797Host name of the NNTP server to post to. Must be set (or omitted; the
798default is "localhost"); if set to en empty string, B<yapfaq> falls
799back to Perl's build-in defaults (contents of environment variables
800NNTPSERVER and NEWSHOST; if not set, default from Net::Config; if not
801set, "news" is used).
802
803=item B<NNTPUser> = I<user name> (optional)
804
805User name used for authentication with the NNTP server (I<AUTHINFO
806USER>).
807
808This setting is optional; if it is not set, I<NNTPPass> is ignored and
809no authentication is tried.
810
811=item B<NNTPPass> = I<password> (optional)
812
813Password used for authentication with the NNTP server (I<AUTHINFO
814PASS>).
815
816This setting is optional; it must be set if I<NNTPUser> is present.
817
818=item B<Sender> = I<Sender header> (optional)
819
820The Sender header that will be added to every posted message.
821
822This setting is optional.
823
824=item B<ConfigFile> = I<configuration file> (mandatory)
825
826The configuration file defining the FAQ(s) to post. Must be set (or
827omitted; the default is "yapfaq.cfg").
828
829=item B<UsePGP> = I<whether to add a digital signature> (optional)
830
831Boolean value (0 or 1) controlling whether the FAQs will get digitally
832signed via an X-PGP-Sig header.
833
834This setting is optional; the default is 0.
835
836If you have set I<UsePGP> to 1, you must also supply the necessary
837information on your PGP oder GPG installation; please refer to the
838sample F<.yapfaqrc> file (see below) for more information on this
839topic.
840
841=back
842
843=head3 Example runtime configuration file
844
845 NNTPServer = 'localhost'
846 NNTPUser = ''
847 NNTPPass = ''
848 Sender = ''
849 ConfigFile = 'yapfaq.cfg'
850 UsePGP = 0
851
852 ################################## PGP-Config #################################
853 pgp = '/usr/bin/pgp' # path to pgp
854 PGPVersion = '2' # Use 2 for 2.X 5 for PGP > 2.X and GPG for GPG
855 PGPSigner = '' # sign as who?
856 PGPPass = '' # pgp2 only
857 PathtoPGPPass = '' # pgp2 pgp5 and gpg
858 pgpbegin = '-----BEGIN PGP SIGNATURE-----' # Begin of PGP-Signature
859 pgpend = '-----END PGP SIGNATURE-----' # End of PGP-Signature
860 pgptmpf = 'pgptmp' # temporary file for PGP.
861 pgpheader = 'X-PGP-Sig'
862
863=head3 Using more than one runtime configuration
864
865You may use more than one runtime configuration file with the B<-c>
866option (see below).
867
272b0243
TH
868=head1 OPTIONS
869
870=over 3
871
a052296f
TH
872=item B<-V> (version)
873
874Print out version and copyright information on B<yapfaq> and exit.
875
272b0243
TH
876=item B<-h> (help)
877
ae3b1b79 878Print this man page and exit.
272b0243
TH
879
880=item B<-v> (verbose)
881
882Print out status information while running to STDOUT.
883
884=item B<-p> (post unconditionally)
885
886Post (all) FAQs unconditionally ignoring the posting frequency setting.
887
888You may want to use this with the B<-f> option (see below).
889
890=item B<-d> (dry run)
891
892Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
893update any status information.
894
895=item B<-t> I<newsgroup(s) | CONSOLE> (test)
896
897Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
898newsgroups given after B<-t> as a comma-separated list or print the
899FAQs to STDOUT separated by lines of dashes if the special string
900C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
901do without embarassing yourself on Usenet. The status files are not
902updated when this option is given.
903
904You may want to use this with the B<-f> option (see below).
905
906=item B<-f> I<project name>
907
908Just deal with one FAQ only.
909
910By default B<yapfaq> will work on all FAQs that are defined in
911F<yapfaq.cfg>, check whether they are due for posting and - if they
912are - post them. Consequently when the B<-p> option is set all FAQs
913will be posted unconditionally. That may not be what you want to
914achieve, so you can limit the operation of B<yapfaq> to the named FAQ
915only.
916
b855559e
TH
917=item B<-s> I<program> (pipe to script)
918
919Instead of posting the article(s) to Usenet pipe them to the external
920I<program> on STDIN (which may post the article(s) then). A return
921value of 0 will be considered success.
922
227afd47
TH
923=item B<-c> I<.rc file>
924
925Load another runtime configuration file (.rc file) than F<.yaofaq.rc>.
926
927You may for example define another usenet server to post your FAQ(s)
928to or load another configuration file defining (an)other FAQ(s).
929
272b0243
TH
930=back
931
932=head1 EXAMPLES
933
934Post all FAQs that are due for posting:
935
936 yapfaq
937
938Do a dry run, showing which FAQs would be posted:
939
940 yapfaq -dv
941
942Do a test run and print on STDOUT what the FAQ I<myfaq> would look
943like when posted, regardless whether it is due for posting or not:
944
945 yapfaq -pt CONSOLE -f myfaq
946
947Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
948if it is due:
949
950 yapfaq -t de.test -f myfaq
951
9817f98a
TH
952Post all FAQs (that are due for posting) using inews from INN:
953
954 yapfaq -s inews
955
956Do a dry run using a runtime configuration from .alternaterc, showing
957which FAQs would be posted:
958
959 yapfaq -dvc .alternaterc
960
272b0243
TH
961=head1 ENVIRONMENT
962
9817f98a
TH
963=over 4
964
965=item NNTPSERVER
966
967The default NNTP server to post to, used by the Net::NNTP module. You
968can also specify the server using the runtime configuration file (by
969default F<.yapfaqrc>).
970
971=back
272b0243
TH
972
973=head1 FILES
974
975=over 4
976
977=item F<yapfaq.pl>
978
979The script itself.
980
227afd47
TH
981=item F<.yapfaqrc>
982
983Runtime configuration file for B<yapfaq>.
984
272b0243
TH
985=item F<yapfaq.cfg>
986
987Configuration file for B<yapfaq>.
988
989=item F<*.cfg>
990
991Status data on FAQs.
992
993The status files will be created on successful posting if they don't
994already exist. The first line of the file will be the date of the last
995time the FAQ was posted and the second line will be the message ID of
996the last post of that FAQ.
997
998=back
999
1000=head1 BUGS
1001
1002Many, I'm sure.
1003
1004=head1 SEE ALSO
1005
1006L<http://th-h.de/download/scripts.php> will have the current
1007version of this program.
1008
1009=head1 AUTHOR
1010
1011Thomas Hochstein <thh@inter.net>
1012
227afd47 1013Original author (up to version 0.5b, dating from 2003):
272b0243
TH
1014Marc Brockschmidt <marc@marcbrockschmidt.de>
1015
272b0243
TH
1016=head1 COPYRIGHT AND LICENSE
1017
1018Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
1019
1020Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
1021
1022This program is free software; you may redistribute it and/or modify it
1023under the same terms as Perl itself.
1024
1025=cut
This page took 0.077152 seconds and 4 git commands to generate.