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