1a0fa394868048c431e7446433e7b04ffc209e11
[usenet/newsstats.git] / bin / parsedb.pl
1 #! /usr/bin/perl
2 #
3 # parsedb.pl
4 #
5 # This script will parse a database with raw header information
6 # from a INN feed to a structured database.
7 #
8 # It is part of the NewsStats package.
9 #
10 # Copyright (c) 2013 Thomas Hochstein <thh@inter.net>
11 #
12 # It can be redistributed and/or modified under the same terms under
13 # which Perl itself is published.
14
15 BEGIN {
16   our $VERSION = "0.01";
17   use File::Basename;
18   # we're in .../bin, so our module is in ../lib
19   push(@INC, dirname($0).'/../lib');
20 }
21 use strict;
22 use warnings;
23
24 use NewsStats qw(:DEFAULT :TimePeriods :SQLHelper);
25
26 use DBI;
27 use Getopt::Long qw(GetOptions);
28 Getopt::Long::config ('bundling');
29
30 use Encode qw/decode/;
31 use Mail::Address;
32
33 ################################# Definitions ##################################
34
35 # define header names with separate database fields
36 my %DBFields = ('date'                      => 'date',
37                 'references'                => 'refs',
38                 'followup-to'               => 'fupto',
39                 'from'                      => 'from_',
40                 'sender'                    => 'sender',
41                 'reply-to'                  => 'replyto',
42                 'subject'                   => 'subject',
43                 'organization'              => 'organization',
44                 'lines'                     => 'linecount',
45                 'approved'                  => 'approved',
46                 'supersedes'                => 'supersedes',
47                 'expires'                   => 'expires',
48                 'user-agent'                => 'useragent',
49                 'x-newsreader'              => 'xnewsreader',
50                 'x-mailer'                  => 'xmailer',
51                 'x-no-archive'              => 'xnoarchive',
52                 'content-type'              => 'contenttype',
53                 'content-transfer-encoding' => 'contentencoding',
54                 'cancel-lock'               => 'cancellock',
55                 'injection-info'            => 'injectioninfo',
56                 'x-trace'                   => 'xtrace',
57                 'nntp-posting-host'         => 'postinghost');
58
59 # define field list for database
60 my @DBFields = qw/day mid refs date path newsgroups fupto from_ from_parsed
61                  from_name from_address sender sender_parsed sender_name
62                  sender_address replyto replyto_parsed replyto_name
63                  replyto_address subject subject_parsed organization linecount
64                  approved supersedes expires useragent xnewsreader xmailer
65                  xnoarchive contenttype contentencoding cancellock injectioninfo
66                  xtrace postinghost headers disregard/;
67
68 ################################# Main program #################################
69
70 ### read commandline options
71 my ($OptDay,$OptDebug,$OptParseDB,$OptRawDB,$OptTest,$OptConfFile);
72 GetOptions ('d|day=s'         => \$OptDay,
73             'debug!'          => \$OptDebug,
74             'parsedb=s'       => \$OptParseDB,
75             'rawdb=s'         => \$OptRawDB,
76             't|test!'         => \$OptTest,
77             'conffile=s'      => \$OptConfFile,
78             'h|help'          => \&ShowPOD,
79             'V|version'       => \&ShowVersion) or exit 1;
80
81 ### read configuration
82 my %Conf = %{ReadConfig($OptConfFile)};
83
84 ### override configuration via commandline options
85 my %ConfOverride;
86 $ConfOverride{'DBTableRaw'}   = $OptRawDB if $OptRawDB;
87 $ConfOverride{'DBTableParse'} = $OptParseDB if $OptParseDB;
88 &OverrideConfig(\%Conf,\%ConfOverride);
89
90 ### get time period
91 ### and set $Period for output and expression for SQL 'WHERE' clause
92 my ($Period,$SQLWherePeriod) = &GetTimePeriod($OptDay,'day');
93 # bail out if --month is invalid or "all"
94 &Bleat(2,"--day option has an invalid format - please use 'YYYY-MM-DD' or ".
95          "'YYYY-MM-DD:YYYY-MM-DD'!") if (!$Period or $Period eq 'all time');
96
97 ### init database
98 my $DBHandle = InitDB(\%Conf,1);
99
100 ### get & write data
101 &Bleat(1,'Test mode. Database is not updated.') if $OptTest;
102
103 # create $SQLWhereClause
104 my $SQLWhereClause = SQLBuildClause('where',$SQLWherePeriod,'NOT disregard');
105
106 # delete old data for current period
107 if (!$OptTest) {
108   print "----------- Deleting old data ... -----------\n" if $OptDebug;
109   my $DBQuery = $DBHandle->do(sprintf("DELETE FROM %s.%s %s",
110                                      $Conf{'DBDatabase'},$Conf{'DBTableParse'},
111                                      $SQLWhereClause))
112       or &Bleat(2,sprintf("Can't delete old parsed data for %s from %s.%s: ".
113                           "$DBI::errstr\n",$Period,
114                           $Conf{'DBDatabase'},$Conf{'DBTableParse'}));
115 };
116
117 # read from DBTableRaw
118 print "-------------- Reading data ... -------------\n" if $OptDebug;
119 my $DBQuery = $DBHandle->prepare(sprintf("SELECT id, day, mid, peer, path, ".
120                                          "newsgroups, headers, disregard ".
121                                          "FROM %s.%s %s", $Conf{'DBDatabase'},
122                                          $Conf{'DBTableRaw'}, $SQLWhereClause));
123 $DBQuery->execute()
124   or &Bleat(2,sprintf("Can't get data for %s from %s.%s: ".
125                       "$DBI::errstr\n",$Period,
126                       $Conf{'DBDatabase'},$Conf{'DBTableRaw'}));
127
128 # set output and database connection to UTF-8
129 # as we're going to write decoded header contents containing UTF-8 chars
130 binmode(STDOUT, ":utf8");
131 $DBHandle->do("SET NAMES 'utf8'");
132
133 # parse data in a loop and write it out
134 print "-------------- Parsing data ... -------------\n" if $OptDebug;
135 while (my $HeadersR = $DBQuery->fetchrow_hashref) {
136   my %Headers = %{$HeadersR};
137
138   # parse $Headers{'headers'} ('headers' from DBTableRaw)
139   # merge continuation lines
140   # from Perl Cookbook, 1st German ed. 1999, pg. 91
141   $Headers{'headers'} =~ s/\n\s+/ /g;
142   # split headers in single lines
143   my $OtherHeaders;
144   for (split(/\n/,$Headers{'headers'})) {
145     # split header lines in header name and header content
146     my ($key,$value) = split(/:/,$_,2);
147     $key =~ s/\s*//;
148     $value =~ s/^\s*(.+)\s*$/$1/;
149     # save each header, separate database fields in %Headers,
150     # the rest in $OtherHeaders (but not Message-ID, Path, Peer
151     # and Newsgroups as those do already exist)
152     if (defined($DBFields{lc($key)})) {
153       $Headers{$DBFields{lc($key)}} = $value;
154     } else {
155       $OtherHeaders .= sprintf("%s: %s\n",$key,$value)
156         if lc($key) !~ /^(message-id|path|peer|newsgroups)$/;
157     }
158   }
159   # replace old (now parsed) $Headers{'headers'} with remanining $OtherHeaders
160   chomp($OtherHeaders);
161   $Headers{'headers'} = $OtherHeaders;
162
163   foreach ('from_','sender', 'replyto', 'subject') {
164     if ($Headers{$_}) {
165       my $HeaderName = $_;
166       $HeaderName  =~ s/_$//;
167       # decode From: / Sender: / Reply-To: / Subject:
168       if ($Headers{$_} =~ /\?(B|Q)\?/) {
169         $Headers{$HeaderName.'_parsed'} = decode('MIME-Header',$Headers{$_});
170       }
171       # extract name(s) and mail(s) from From: / Sender: / Reply-To:
172       # in parsed form, if available
173       if ($_ ne 'subject') {
174         my @Address;
175         # start parser on header or parsed header
176         # @Address will have an array of Mail::Address objects, one for
177         # each name/mail (you can have more than one person in From:!)
178         if (defined($Headers{$HeaderName.'_parsed'})) {
179           @Address = Mail::Address->parse($Headers{$HeaderName.'_parsed'});
180         } else {
181           @Address = Mail::Address->parse($Headers{$_});
182         }
183         # split each Mail::Address object to @Names and @Adresses
184         my (@Names,@Adresses);
185         foreach (@Address) {
186           # take address part in @Addresses
187           push (@Adresses, $_->address());
188           # take name part form "phrase", if there is one:
189           # From: My Name <addr@ess> (Comment)
190           # otherwise, take it from "comment":
191           # From: addr@ess (Comment)
192           # and push it in @Names
193           my ($Name);
194           $Name = $_->comment() unless $Name = $_->phrase;
195           $Name =~ s/^\((.+)\)$/$1/;
196           push (@Names, $Name);
197         }
198         # put all @Adresses and all @Names in %Headers as comma separated lists
199         $Headers{$HeaderName.'_address'} = join(', ',@Adresses);
200         $Headers{$HeaderName.'_name'}    = join(', ',@Names);
201       }
202     }
203   }
204
205   # order output for database entry: fill @SQLBindVars
206   print "-------------- Next entry:\n" if $OptDebug;
207   my @SQLBindVars;
208   foreach (@DBFields) {
209     if (defined($Headers{$_}) and $Headers{$_} ne '') {
210       push (@SQLBindVars,$Headers{$_});
211       printf ("FOUND: %s -> %s\n",$_,$Headers{$_}) if $OptDebug;
212     } else {
213       push (@SQLBindVars,undef);
214     }
215   }
216
217   # write data to DBTableParse
218   if (!$OptTest) {
219     print "-------------- Writing data ... -------------\n" if $OptDebug;
220     my $DBWrite =
221        $DBHandle->prepare(sprintf("INSERT INTO %s.%s (%s) VALUES (%s)",
222                                   $Conf{'DBDatabase'},
223                                   $Conf{'DBTableParse'},
224                                   # get field names from @DBFields
225                                   join(', ',@DBFields),
226                                   # create a list of '?' for each DBField
227                                   join(', ',
228                                        split(/ /,'? ' x scalar(@DBFields)))
229                                 ));
230   $DBWrite->execute(@SQLBindVars)
231       or &Bleat(2,sprintf("Can't write parsed data for %s to %s.%s: ".
232                           "$DBI::errstr\n",$Period,
233                           $Conf{'DBDatabase'},$Conf{'DBTableParse'}));
234     $DBWrite->finish;
235   }
236 };
237 $DBQuery->finish;
238
239 ### close handles
240 $DBHandle->disconnect;
241
242 print "------------------- DONE! -------------------\n" if $OptDebug;
243 __END__
244
245 ################################ Documentation #################################
246
247 =head1 NAME
248
249 parsedb - parse raw data and save it to a database
250
251 =head1 SYNOPSIS
252
253 B<parsedb> [B<-Vht>] [B<--day> I<YYYY-MM-DD> | I<YYYY-MM-DD:YYYY-MM-DD>] [B<--rawdb> I<database table>] [B<--parsedb> I<database table>] [B<--conffile> I<filename>] [B<--debug>]
254
255 =head1 REQUIREMENTS
256
257 See L<doc/README>.
258
259 =head1 DESCRIPTION
260
261 ...
262
263 =head2 Configuration
264
265 ...
266
267 =head1 OPTIONS
268
269 =over 3
270
271 =item B<-V>, B<--version>
272
273 Print out version and copyright information and exit.
274
275 =item B<-h>, B<--help>
276
277 Print this man page and exit.
278
279 =item B<--debug>
280
281 Output (rather much) debugging information to STDOUT while processing.
282
283 =item B<-t>, B<--test>
284
285 Do not write results to database. You should use B<--debug> in
286 conjunction with B<--test> ... everything else seems a bit pointless.
287
288 =item B<-d>, B<--day> I<YYYY-MM-DD[:YYYY-MM-DD]>
289
290 Set processing period to a single day in YYYY-MM-DD format or to a time
291 period between two days in YYYY-MM-DD:YYYY-MM-DD format (two days, separated
292 by a colon).
293
294 Defaults to yesterday.
295
296 =item B<--rawdb> I<table> (raw data table)
297
298 Override I<DBTableRaw> from F<newsstats.conf>.
299
300 =item B<--parsedb> I<table> (parsed data table)
301
302 Override I<DBTableParse> from F<newsstats.conf>.
303
304 =item B<--conffile> I<filename>
305
306 Load configuration from I<filename> instead of F<newsstats.conf>.
307
308 =back
309
310 =head1 INSTALLATION
311
312 See L<doc/INSTALL>.
313
314 =head1 EXAMPLES
315
316 ...
317
318 =head1 FILES
319
320 =over 4
321
322 =item F<bin/parsedb.pl>
323
324 The script itself.
325
326 =item F<lib/NewsStats.pm>
327
328 Library functions for the NewsStats package.
329
330 =item F<etc/newsstats.conf>
331
332 Runtime configuration file.
333
334 =back
335
336 =head1 BUGS
337
338 Please report any bugs or feature requests to the author or use the
339 bug tracker at L<http://bugs.th-h.de/>!
340
341 =head1 SEE ALSO
342
343 =over 2
344
345 =item -
346
347 L<doc/README>
348
349 =item -
350
351 L<doc/INSTALL>
352
353 =back
354
355 This script is part of the B<NewsStats> package.
356
357 =head1 AUTHOR
358
359 Thomas Hochstein <thh@inter.net>
360
361 =head1 COPYRIGHT AND LICENSE
362
363 Copyright (c) 2013 Thomas Hochstein <thh@inter.net>
364
365 This program is free software; you may redistribute it and/or modify it
366 under the same terms as Perl itself.
367
368 =cut
This page took 0.017906 seconds and 2 git commands to generate.