3 ###############################################################################
4 # UseVoteGer 4.09 Wahldurchfuehrung
5 # (c) 2001-2005 Marc Langer <uv@marclanger.de>
7 # This script package is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Public License as published by the
9 # Free Software Foundation.
11 # The script reads usenet vote ballots from mailbox files. The format
12 # can be set by changing the option "mailstart".
15 # - Ron Dippold (Usevote 3.0, 1993/94)
16 # - Frederik Ramm (German translation, 1994)
17 # - Wolfgang Behrens (UseVoteGer 3.1, based on Frederik's translation, 1998/99)
18 # - Cornell Binder for some good advice and code fragments
20 # This is a complete rewrite of UseVoteGer 3.1 in Perl (former versions were
21 # written in C). Not all functions of Usevote/UseVoteGer 3.x are implemented!
22 ###############################################################################
26 use Text::Wrap qw(wrap $columns);
40 print "\n$usevote_version Wahldurchfuehrung - (c) 2001-2005 Marc Langer\n\n";
42 # unknown parameters remain in @ARGV (for "help")
43 Getopt::Long::Configure(qw(pass_through bundling));
45 # Put known parameters in %opt_ctl
46 GetOptions(\%opt_ctl, qw(test t config-file=s c=s));
48 # Get name auf config file (default: usevote.cfg) and read it
49 my $cfgfile = $opt_ctl{'config-file'} || $opt_ctl{c} || "usevote.cfg";
51 # test mode? (default: no)
52 my $test_only = $opt_ctl{test} || $opt_ctl{t} || 0;
55 # additional parameters passed
57 if ($ARGV[0] eq "clean") {
60 # print help and exit program
65 UVconfig::read_config($cfgfile, 1); # read config file, redirect errors to log
66 UVrules::read_rulefile(); # read rules from file
68 # read list of suspicious mail addresses from file
69 my @bad_addr = UVconfig::read_badaddr();
73 UVconfig::test_config();
78 if (-e $config{lockfile}) {
79 my $lockfile = $config{lockfile};
81 # don't delete lockfile in END block ;-)
82 $config{lockfile} = '';
85 die UVmessage::get("ERR_LOCK", (FILE=>$lockfile)) . "\n\n";
88 # safe exit (delete lockfile)
89 $SIG{QUIT} = 'sighandler';
90 $SIG{INT} = 'sighandler';
91 $SIG{KILL} = 'sighandler';
92 $SIG{TERM} = 'sighandler';
93 $SIG{HUP} = 'sighandler';
96 open (LOCKFILE, ">$config{lockfile}");
99 # Set columns for Text::Wrap
100 $columns = $config{rightmargin};
102 # check for tmp and archive directory
103 unless (-d $config{archivedir}) {
104 mkdir ($config{archivedir}, 0700)
105 or die UVmessage::get("ERR_MKDIR", (DIR=>$config{archivedir})) . "$!\n\n";
108 unless (-d $config{tmpdir}) {
109 mkdir ($config{tmpdir}, 0700)
110 or die UVmessage::get("ERR_MKDIR", (DIR=>$config{tmpdir})) . "$!\n\n";
114 # Program has been startet with "clean" option:
115 # save votes and send out acknowledge mails
121 # generate file names for result file
122 # normally unixtime is sufficient, if it is not unique append our PID
125 opendir (DIR, $config{tmpdir});
126 my @tmpfiles = readdir (DIR);
128 opendir (FERTIG, $config{archivedir});
129 my @fertigfiles = readdir (FERTIG);
132 # append PID if necessary
133 $ext .= "-$$" if (grep (/$ext/, @tmpfiles) || grep (/$ext/, @fertigfiles));
135 my $thisresult = "ergebnis-" . $ext;
136 my $thisvotes = "stimmen-" . $ext;
138 # POP3 not activated: rename votes file
139 unless ($config{pop3}) {
140 print UVmessage::get("VOTE_RENAMING_MAILBOX"), "\n";
141 rename ($config{votefile}, "$config{tmpdir}/$thisvotes")
142 or die UVmessage::get("ERR_RENAME_MAILFILE") . "$!\n\n";
144 # wait, so that current mail deliveries can finalize
149 open (RESULT, ">>$config{tmpdir}/$thisresult")
150 or die UVmessage::get("VOTE_WRITE_RESULTS", (FILE=>$thisresult)) . "\n\n";
152 # read votes and process them
153 # for each mail pass a reference to the sub to be called
154 my $count = UVreadmail::process("$config{tmpdir}/$thisvotes", \&process_vote, 0);
157 or print STDERR UVmessage::get("VOTE_CLOSE_RESULTS", (FILE=>$thisresult)) . "\n";
159 # no mails: exit here
161 print UVmessage::get("VOTE_NO_VOTEMAILS") . "\n\n";
165 if ($config{onestep}) {
166 # everything should be done in one step
167 print "\n" . UVmessage::get("VOTE_NUM_VOTES", (COUNT=>$count)) . "\n";
172 print "\n", UVmessage::get("VOTE_NOT_SAVED", (COUNT=>$count)), "\n",
173 wrap('', '', UVmessage::get("VOTE_FIRSTRUN")), "\n\n";
183 unlink $config{lockfile} if ($config{lockfile});
185 if (-s $config{errorfile}) {
187 print '*' x $config{rightmargin}, "\n",
188 UVmessage::get("VOTE_ERRORS",(FILE => $config{errorfile})), "\n",
189 '*' x $config{rightmargin}, "\n\n";
190 open (ERRFILE, "<$config{errorfile}");
195 unlink ($config{errorfile});
202 die "\n\nSIG$sig: deleting lockfile and exiting\n\n";
206 ##############################################################################
207 # Evaluation of a vote mail #
208 # Called from UVreadmail::process() for each mail. #
209 # Parameters: voter address and name, date header of the vote mail (strings) #
210 # complete header (reference to array), body (ref. to strings) #
211 ##############################################################################
214 my ($voter_addr, $voter_name, $h_date, $entity, $body) = @_;
216 my @header = split(/\n/, $entity->stringify_header);
217 my $head = $entity->head;
218 my $msgid = $head->get('Message-ID');
219 chomp($msgid) if ($msgid);
221 my @votes = (); # the votes
222 my @set; # interactively changed fields
223 my @errors = (); # recognized errors (show menu for manual action)
224 my $onevote = 0; # 0=no votes, 1=everything OK, 2=vote cancelled
225 my $voteerror = ""; # error message in case of invalid vote
226 my $ballot_id = ""; # ballot id (German: Wahlscheinkennung)
227 my $voting = ""; # voting (should be votename)
231 # search for suspicious addresses
232 foreach my $element (@bad_addr) {
233 if ($voter_addr =~ /^$element/) {
234 push (@errors, 'SuspiciousAccount');
239 # found no address in mail (perhaps violates RFC?)
240 push (@errors, 'InvalidAddress');
244 if ($$body =~ /\Q$config{ballotintro}\E\s+(.+?)\s*\n(.*?[\t ]+(\S+.+)\s*$)?/m) {
246 $voting .= " $3" if defined($3);
247 push (@errors, 'WrongVoting') if ($config{votename} !~ /^\s*\Q$voting\E\s*$/);
249 push (@errors, 'NoVoting');
252 # personalized ballots?
253 if ($config{personal}) {
254 if ($$body =~ /$config{ballotidtext}\s+([a-z0-9]+)/) {
256 # Address registered? ($ids is set in UVconfig.pm)
257 if ($ids{$voter_addr}) {
258 push (@errors, 'WrongBallotID') if ($ids{$voter_addr} ne $ballot_id);
260 push (@errors, 'AddressNotRegistered');
263 push (@errors, 'NoBallotID');
267 # evaluate vote strings
268 for (my $n=0; $n<@groups; $n++) {
270 # counter starts at 1 in ballot
274 # a line looks like this: #1 [ VOTE ] Group
275 # matching only on number and vote, because of line breaks likely
276 # inserted by mail programs
279 if ($$body =~ /#$votenum\W*?\[\s*?(\w+)\s*?\].+?#$votenum\W*?\[\s*?(\w+)\s*?\]/s) {
280 push (@errors, "DuplicateVote") if ($1 ne $2);
283 # this matches on a single appearance:
284 if ($$body =~ /#$votenum\W*?\[(.+)\]/) {
285 # one or more vote strings were found
288 if ($votestring =~ /^\W*$config{ja_stimme}\W*$/i) {
290 } elsif ($votestring =~ /^\W*$config{nein_stimme}\W*$/i) {
292 } elsif ($votestring =~ /^\W*$config{enth_stimme}\W*$/i) {
294 } elsif ($votestring =~ /^\s*$/) {
295 # nothing has been entered between the [ ]
297 } elsif ($votestring =~ /^\W*$config{ann_stimme}\W*$/i) {
299 $onevote = 2; # Cancelled vote: set $onevote to 2
300 } elsif (!$votes[$n]) {
301 # vote not recognized
303 push (@errors, 'UnrecognizedVote #' . $votenum . "#$votestring");
305 push (@votes, $vote);
309 push (@errors, 'UnrecognizedVote #' . $votenum . '#(keine Stimmabgabe fuer "'
310 . $groups[$n] . '" gefunden)');
315 push (@errors, "NoVote") unless ($onevote);
316 } elsif ($onevote == 1) {
318 my $rule = UVrules::rule_check(\@votes);
319 push (@errors, "ViolatedRule #$rule") if ($rule);
321 # cancelled vote: replace all votes with an A
322 @votes = split(//, 'A' x scalar @votes);
325 # Evaluate Data Protection Law clause (not on cancelled votes)
326 if ($config{bdsg} && $onevote<2) {
328 # Text in ballot complete and clause accepted?
329 # Should read like this: #a [ STIMME ] Text
330 # (Text is configurable in usevote.cfg)
331 unless ($$body =~ /$bdsg_regexp/s &&
332 $$body =~ /#a\W*?\[\W*?$config{ja_stimme}\W*?\]\W*?$bdsg2_regexp/is) {
334 push (@errors, 'InvalidBDSG');
339 if ($$body =~ /($config{nametext}|$config{nametext2})( |\t)*(\S.+?)$/m) {
341 $voter_name =~ s/^\s+//; # strip leading spaces
342 $voter_name =~ s/\s+$//; # strip trailing spaces
347 push (@errors, 'InvalidName') unless ($voter_name =~ /$config{name_re}/);
350 push (@errors, 'NoName') unless ($voter_name);
353 # Errors encountered?
355 my $res = UVmenu::menu(\@votes, \@header, $body, \$voter_addr, \$voter_name,
356 \$ballot_id, \$voting, \@set, \@errors);
357 return 0 if ($res eq 'i'); # "Ignore": Ignore vote, don't save
361 # Check Ballot ID stuff
362 if ($config{personal}) {
364 if ($ids{$voter_addr}) {
365 if ($ids{$voter_addr} ne $ballot_id) {
366 $voteerror = UVmessage::get("VOTE_INVALID_BALLOTID");
367 $tpl = $config{tpl_wrong_ballotid};
370 $voteerror = UVmessage::get("VOTE_UNREGISTERED_ADDRESS");
371 $tpl = $config{tpl_addr_reg};
374 $voteerror = UVmessage::get("VOTE_MISSING_BALLOTID");
375 $tpl = $config{tpl_no_ballotid};
378 # generate error mail (if error occurred)
380 my $template = UVtemplate->new();
381 $template->setKey('head' => $entity->stringify_header);
382 $template->setKey('body' => $$body);
383 my $msg = $template->processTemplate($tpl);
384 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
389 # Check rules and send error mail unless rule violation was ignored in the use menu
390 # or another error was detected
391 if (grep(/ViolatedRule/, @errors) && !$voteerror && (my $rule = UVrules::rule_check(\@votes))) {
392 $voteerror = UVmessage::get("VOTE_VIOLATED_RULE", (RULE=>$rule));
393 my $template = UVtemplate->new();
394 $template->setKey('body' => $$body);
395 $template->setKey('rules' => UVrules::rule_print($rule-1));
396 my $msg = $template->processTemplate($config{tpl_rule_violated});
397 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
400 if (!$voteerror && @errors) {
402 # turn errors array into hash
405 foreach my $error (@errors) {
409 # Check uncorrected errors
410 if ($error{InvalidBDSG}) {
411 my $template = UVtemplate->new();
412 my $msg = $template->processTemplate($config{tpl_bdsg_error});
413 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
415 } elsif ($error{NoVoting} or $error{WrongVoting}) {
416 $voteerror = UVmessage::get("VOTE_WRONG_VOTING");
417 my $template = UVtemplate->new();
418 $template->setKey('body' => $$body);
419 my $msg = $template->processTemplate($config{tpl_wrong_voting});
420 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
421 } elsif ($error{NoVote}) {
422 $voteerror = UVmessage::get("VOTE_NO_VOTES");
423 my $template = UVtemplate->new();
424 $template->setKey('body' => $$body);
425 my $msg = $template->processTemplate($config{tpl_no_votes});
426 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
427 } elsif ($error{SuspiciousAccount}) {
428 $voteerror = UVmessage::get("VOTE_INVALID_ACCOUNT");
429 my $template = UVtemplate->new();
430 $template->setKey('head' => $entity->stringify_header);
431 $template->setKey('body' => $$body);
432 my $msg = $template->processTemplate($config{tpl_invalid_account});
433 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
434 } elsif ($error{InvalidAddress}) {
435 $voteerror = UVmessage::get("VOTE_INVALID_ADDRESS");
436 } elsif ($error{InvalidName}) {
437 $voteerror = UVmessage::get("VOTE_INVALID_REALNAME");
438 my $template = UVtemplate->new();
439 $template->setKey('head' => $entity->stringify_header);
440 $template->setKey('body' => $$body);
441 my $msg = $template->processTemplate($config{tpl_invalid_name});
442 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
443 } elsif ($error{DuplicateVote}) {
444 $voteerror = UVmessage::get("VOTE_DUPLICATES");
445 my $template = UVtemplate->new();
446 $template->setKey('head' => $entity->stringify_header);
447 $template->setKey('body' => $$body);
448 my $msg = $template->processTemplate($config{tpl_multiple_votes});
449 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
454 unless ($voter_name || $voteerror) {
455 $voteerror = UVmessage::get("VOTE_MISSING_NAME");
456 my $template = UVtemplate->new();
457 $template->setKey('head' => $entity->stringify_header);
458 $template->setKey('body' => $$body);
459 my $msg = $template->processTemplate($config{tpl_invalid_name});
460 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
463 # set mark for cancelled vote
464 $onevote = 2 if ($votes[0] eq 'A');
466 # create comment line for result file
468 if ($config{personal}) {
469 # Personalized Ballots: insert ballot id
470 $comment = "($ballot_id)";
476 $comment .= ' '.UVmessage::get("VOTE_FILE_COMMENT", (FIELDS => join(', ', @set)));
480 print RESULT "A: $voter_addr\n";
481 print RESULT "N: $voter_name\n";
482 print RESULT "D: $h_date\n";
483 print RESULT "K: $comment\n";
487 print RESULT "S: ! $voteerror\n";
490 } elsif ($onevote == 2) {
491 print RESULT "S: * Annulliert\n";
493 if ($config{voteack}) {
494 # send cancellation acknowledge
495 my $template = UVtemplate->new();
496 my $msg = $template->processTemplate($config{tpl_cancelled});
497 UVsendmail::mail($voter_addr, "Bestaetigung", $msg, $msgid);
501 print RESULT "S: ", join ("", @votes), "\n";
503 # send acknowledge mail?
504 if ($config{voteack}) {
506 my $template = UVtemplate->new();
507 $template->setKey(ballotid => $ballot_id);
508 $template->setKey(address => $voter_addr);
509 $template->setKey(name => $voter_name);
511 for (my $n=0; $n<@groups; $n++) {
512 my $vote = $votes[$n];
514 $vote =~ s/^N$/NEIN/;
515 $vote =~ s/^E$/ENTHALTUNG/;
516 $template->addListItem('groups', pos=>$n+1, vote=>$vote, group=>$groups[$n]);
519 my $msg = $template->processTemplate($config{'tpl_ack_mail'});
520 UVsendmail::mail($voter_addr, "Bestaetigung", $msg, $msgid);
526 ##############################################################################
527 # Send out acknowledge mails and tidy up (we're called as "uvvote.pl clean") #
528 ##############################################################################
535 print UVmessage::get("INFO_TIDY_UP"), "\n";
537 # search unprocessed files
538 opendir (DIR, $config{tmpdir});
539 my @files = readdir DIR;
542 my @resultfiles = grep (/^ergebnis-/, @files);
543 my @votefiles = grep (/^stimmen-/, @files);
545 unless (@resultfiles) {
546 print wrap('', '', UVmessage::get("VOTE_NO_NEW_RESULTS")), "\n\n";
550 foreach my $thisresult (@resultfiles) {
551 chmod (0400, "$config{tmpdir}/$thisresult");
552 rename "$config{tmpdir}/$thisresult", "$config{archivedir}/$thisresult"
553 or die UVmessage::get("VOTE_MOVE_RESULTFILE", (FILE=>$thisresult)) . "$!\n\n";
556 foreach my $thisvotes (@votefiles) {
557 chmod (0400, "$config{tmpdir}/$thisvotes");
558 rename "$config{tmpdir}/$thisvotes", "$config{archivedir}/$thisvotes"
559 or die UVmessage::get("VOTE_MOVE_VOTEFILE", (FILE=>$thisvotes)) . "$!\n\n";
562 print UVmessage::get("VOTE_CREATING_RESULTS", (FILENAME=>$config{resultfile})), "\n";
564 # search all result files
565 opendir (DIR, "$config{archivedir}/");
566 @files = grep (/^ergebnis-/, readdir (DIR));
569 # Create complete result from all single result files.
570 # The resulting file (ergebnis.alle) is overwritten as there could have been
571 # made changes in the single result files
572 open(RESULT, ">$config{resultfile}");
573 foreach my $file (sort @files) {
574 open(THISRESULT, "<$config{archivedir}/$file");
575 print RESULT join('', <THISRESULT>);
585 ##############################################################################
586 # Print help text (options and syntax) on -h or --help #
587 ##############################################################################
591 Usage: uvvote.pl [-c config_file] [-t]
592 uvvote.pl [-c config_file] clean
595 Liest Mailboxen aus einer Datei oder per POP3 ein wertet die Mails
596 als Stimmzettel aus. Erst beim Aufruf mit der Option "clean" werden
597 die Ergebnisse endgueltig gespeichert und die Bestaetigungsmails
600 -c config_file liest die Konfiguration aus config_file
601 (usevote.cfg falls nicht angegeben)
603 -t, --test fuehrt einen Test der Konfiguration durch und
604 gibt das ermittelte Ergebnis aus.
606 -h, --help zeigt diesen Hilfetext an