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