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*$/m) {
246 push (@errors, 'WrongVoting') if ($config{votename} !~ /^\s*\Q$voting\E\s*$/);
248 push (@errors, 'NoVoting');
251 # personalized ballots?
252 if ($config{personal}) {
253 if ($$body =~ /$config{ballotidtext}\s+([a-z0-9]+)/) {
255 # Address registered? ($ids is set in UVconfig.pm)
256 if ($ids{$voter_addr}) {
257 push (@errors, 'WrongBallotID') if ($ids{$voter_addr} ne $ballot_id);
259 push (@errors, 'AddressNotRegistered');
262 push (@errors, 'NoBallotID');
266 # evaluate vote strings
267 for (my $n=0; $n<@groups; $n++) {
269 # counter starts at 1 in ballot
273 # a line looks like this: #1 [ VOTE ] Group
274 # matching only on number and vote, because of line breaks likely
275 # inserted by mail programs
278 if ($$body =~ /#$votenum\W*?\[\s*?(\w+)\s*?\].+?#$votenum\W*?\[\s*?(\w+)\s*?\]/s) {
279 push (@errors, "DuplicateVote") if ($1 ne $2);
282 # this matches on a single appearance:
283 if ($$body =~ /#$votenum\W*?\[(.+)\]/) {
284 # one or more vote strings were found
287 if ($votestring =~ /^\W*$config{ja_stimme}\W*$/i) {
289 } elsif ($votestring =~ /^\W*$config{nein_stimme}\W*$/i) {
291 } elsif ($votestring =~ /^\W*$config{enth_stimme}\W*$/i) {
293 } elsif ($votestring =~ /^\s*$/) {
294 # nothing has been entered between the [ ]
296 } elsif ($votestring =~ /^\W*$config{ann_stimme}\W*$/i) {
298 $onevote = 2; # Cancelled vote: set $onevote to 2
299 } elsif (!$votes[$n]) {
300 # vote not recognized
302 push (@errors, 'UnrecognizedVote #' . $votenum . "#$votestring");
304 push (@votes, $vote);
308 push (@errors, 'UnrecognizedVote #' . $votenum . '#(keine Stimmabgabe fuer "'
309 . $groups[$n] . '" gefunden)');
314 push (@errors, "NoVote") unless ($onevote);
315 } elsif ($onevote == 1) {
317 my $rule = UVrules::rule_check(\@votes);
318 push (@errors, "ViolatedRule #$rule") if ($rule);
320 # cancelled vote: replace all votes with an A
321 @votes = split(//, 'A' x scalar @votes);
324 # Evaluate Data Protection Law clause (not on cancelled votes)
325 if ($config{bdsg} && $onevote<2) {
327 # Text in ballot complete and clause accepted?
328 # Should read like this: #a [ STIMME ] Text
329 # (Text is configurable in usevote.cfg)
330 unless ($$body =~ /$bdsg_regexp/s &&
331 $$body =~ /#a\W*?\[\W*?$config{ja_stimme}\W*?\]\W*?$bdsg2_regexp/is) {
333 push (@errors, 'InvalidBDSG');
338 if ($$body =~ /($config{nametext}|$config{nametext2})( |\t)*(\S.+?)$/m) {
340 $voter_name =~ s/^\s+//; # strip leading spaces
341 $voter_name =~ s/\s+$//; # strip trailing spaces
346 push (@errors, 'InvalidName') unless ($voter_name =~ /$config{name_re}/);
349 push (@errors, 'NoName') unless ($voter_name);
352 # Errors encountered?
354 my $res = UVmenu::menu(\@votes, \@header, $body, \$voter_addr, \$voter_name,
355 \$ballot_id, \$voting, \@set, \@errors);
356 return 0 if ($res eq 'i'); # "Ignore": Ignore vote, don't save
360 # Check Ballot ID stuff
361 if ($config{personal}) {
363 if ($ids{$voter_addr}) {
364 if ($ids{$voter_addr} ne $ballot_id) {
365 $voteerror = UVmessage::get("VOTE_INVALID_BALLOTID");
366 $tpl = $config{tpl_wrong_ballotid};
369 $voteerror = UVmessage::get("VOTE_UNREGISTERED_ADDRESS");
370 $tpl = $config{tpl_addr_reg};
373 $voteerror = UVmessage::get("VOTE_MISSING_BALLOTID");
374 $tpl = $config{tpl_no_ballotid};
377 # generate error mail (if error occurred)
379 my $template = UVtemplate->new();
380 $template->setKey('head' => $entity->stringify_header);
381 $template->setKey('body' => $$body);
382 my $msg = $template->processTemplate($tpl);
383 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
388 # Check rules and send error mail unless rule violation was ignored in the use menu
389 # or another error was detected
390 if (grep(/ViolatedRule/, @errors) && !$voteerror && (my $rule = UVrules::rule_check(\@votes))) {
391 $voteerror = UVmessage::get("VOTE_VIOLATED_RULE", (RULE=>$rule));
392 my $template = UVtemplate->new();
393 $template->setKey('body' => $$body);
394 $template->setKey('rules' => UVrules::rule_print($rule-1));
395 my $msg = $template->processTemplate($config{tpl_rule_violated});
396 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
399 if (!$voteerror && @errors) {
401 # turn errors array into hash
404 foreach my $error (@errors) {
408 # Check uncorrected errors
409 if ($error{InvalidBDSG}) {
410 my $template = UVtemplate->new();
411 my $msg = $template->processTemplate($config{tpl_bdsg_error});
412 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
414 } elsif ($error{NoVoting} or $error{WrongVoting}) {
415 $voteerror = UVmessage::get("VOTE_WRONG_VOTING");
416 my $template = UVtemplate->new();
417 $template->setKey('body' => $$body);
418 my $msg = $template->processTemplate($config{tpl_wrong_voting});
419 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
420 } elsif ($error{NoVote}) {
421 $voteerror = UVmessage::get("VOTE_NO_VOTES");
422 my $template = UVtemplate->new();
423 $template->setKey('body' => $$body);
424 my $msg = $template->processTemplate($config{tpl_no_votes});
425 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
426 } elsif ($error{SuspiciousAccount}) {
427 $voteerror = UVmessage::get("VOTE_INVALID_ACCOUNT");
428 my $template = UVtemplate->new();
429 $template->setKey('head' => $entity->stringify_header);
430 $template->setKey('body' => $$body);
431 my $msg = $template->processTemplate($config{tpl_invalid_account});
432 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
433 } elsif ($error{InvalidAddress}) {
434 $voteerror = UVmessage::get("VOTE_INVALID_ADDRESS");
435 } elsif ($error{InvalidName}) {
436 $voteerror = UVmessage::get("VOTE_INVALID_REALNAME");
437 my $template = UVtemplate->new();
438 $template->setKey('head' => $entity->stringify_header);
439 $template->setKey('body' => $$body);
440 my $msg = $template->processTemplate($config{tpl_invalid_name});
441 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
442 } elsif ($error{DuplicateVote}) {
443 $voteerror = UVmessage::get("VOTE_DUPLICATES");
444 my $template = UVtemplate->new();
445 $template->setKey('head' => $entity->stringify_header);
446 $template->setKey('body' => $$body);
447 my $msg = $template->processTemplate($config{tpl_multiple_votes});
448 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
453 unless ($voter_name || $voteerror) {
454 $voteerror = UVmessage::get("VOTE_MISSING_NAME");
455 my $template = UVtemplate->new();
456 $template->setKey('head' => $entity->stringify_header);
457 $template->setKey('body' => $$body);
458 my $msg = $template->processTemplate($config{tpl_invalid_name});
459 UVsendmail::mail($voter_addr, "Fehler", $msg, $msgid) if ($config{voteack});
462 # set mark for cancelled vote
463 $onevote = 2 if ($votes[0] eq 'A');
465 # create comment line for result file
467 if ($config{personal}) {
468 # Personalized Ballots: insert ballot id
469 $comment = "($ballot_id)";
475 $comment .= ' '.UVmessage::get("VOTE_FILE_COMMENT", (FIELDS => join(', ', @set)));
479 print RESULT "A: $voter_addr\n";
480 print RESULT "N: $voter_name\n";
481 print RESULT "D: $h_date\n";
482 print RESULT "K: $comment\n";
486 print RESULT "S: ! $voteerror\n";
489 } elsif ($onevote == 2) {
490 print RESULT "S: * Annulliert\n";
492 if ($config{voteack}) {
493 # send cancellation acknowledge
494 my $template = UVtemplate->new();
495 my $msg = $template->processTemplate($config{tpl_cancelled});
496 UVsendmail::mail($voter_addr, "Bestaetigung", $msg, $msgid);
500 print RESULT "S: ", join ("", @votes), "\n";
502 # send acknowledge mail?
503 if ($config{voteack}) {
505 my $template = UVtemplate->new();
506 $template->setKey(ballotid => $ballot_id);
507 $template->setKey(address => $voter_addr);
508 $template->setKey(name => $voter_name);
510 for (my $n=0; $n<@groups; $n++) {
511 my $vote = $votes[$n];
513 $vote =~ s/^N$/NEIN/;
514 $vote =~ s/^E$/ENTHALTUNG/;
515 $template->addListItem('groups', pos=>$n+1, vote=>$vote, group=>$groups[$n]);
518 my $msg = $template->processTemplate($config{'tpl_ack_mail'});
519 UVsendmail::mail($voter_addr, "Bestaetigung", $msg, $msgid);
525 ##############################################################################
526 # Send out acknowledge mails and tidy up (we're called as "uvvote.pl clean") #
527 ##############################################################################
534 print UVmessage::get("INFO_TIDY_UP"), "\n";
536 # search unprocessed files
537 opendir (DIR, $config{tmpdir});
538 my @files = readdir DIR;
541 my @resultfiles = grep (/^ergebnis-/, @files);
542 my @votefiles = grep (/^stimmen-/, @files);
544 unless (@resultfiles) {
545 print wrap('', '', UVmessage::get("VOTE_NO_NEW_RESULTS")), "\n\n";
549 foreach my $thisresult (@resultfiles) {
550 chmod (0400, "$config{tmpdir}/$thisresult");
551 rename "$config{tmpdir}/$thisresult", "$config{archivedir}/$thisresult"
552 or die UVmessage::get("VOTE_MOVE_RESULTFILE", (FILE=>$thisresult)) . "$!\n\n";
555 foreach my $thisvotes (@votefiles) {
556 chmod (0400, "$config{tmpdir}/$thisvotes");
557 rename "$config{tmpdir}/$thisvotes", "$config{archivedir}/$thisvotes"
558 or die UVmessage::get("VOTE_MOVE_VOTEFILE", (FILE=>$thisvotes)) . "$!\n\n";
561 print UVmessage::get("VOTE_CREATING_RESULTS", (FILENAME=>$config{resultfile})), "\n";
563 # search all result files
564 opendir (DIR, "$config{archivedir}/");
565 @files = grep (/^ergebnis-/, readdir (DIR));
568 # Create complete result from all single result files.
569 # The resulting file (ergebnis.alle) is overwritten as there could have been
570 # made changes in the single result files
571 open(RESULT, ">$config{resultfile}");
572 foreach my $file (sort @files) {
573 open(THISRESULT, "<$config{archivedir}/$file");
574 print RESULT join('', <THISRESULT>);
584 ##############################################################################
585 # Print help text (options and syntax) on -h or --help #
586 ##############################################################################
590 Usage: uvvote.pl [-c config_file] [-t]
591 uvvote.pl [-c config_file] clean
594 Liest Mailboxen aus einer Datei oder per POP3 ein wertet die Mails
595 als Stimmzettel aus. Erst beim Aufruf mit der Option "clean" werden
596 die Ergebnisse endgueltig gespeichert und die Bestaetigungsmails
599 -c config_file liest die Konfiguration aus config_file
600 (usevote.cfg falls nicht angegeben)
602 -t, --test fuehrt einen Test der Konfiguration durch und
603 gibt das ermittelte Ergebnis aus.
605 -h, --help zeigt diesen Hilfetext an