changeset 0:ba44dc878db8

Initial version
author "German Poo-Caaman~o <gpoo@gnome.org>"
date Thu, 07 Dec 2006 10:33:46 -0300
parents
children 910b2909e680
files .hgignore list-addresses.py mail-ballots.pl vote-counter.py
diffstat 4 files changed, 398 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Thu Dec 07 10:33:46 2006 -0300
@@ -0,0 +1,8 @@
+# use glob syntax.
+syntax: glob
+*~
+.*.swp
+*.pyc
+
+syntax: regexp
+.*\#.*\#$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/list-addresses.py	Thu Dec 07 10:33:46 2006 -0300
@@ -0,0 +1,42 @@
+#! /usr/bin/python
+
+import re
+import sys
+import string
+
+nospam_re = re.compile ("no_spam\.?")
+
+def unmunge_email (addr):
+    unmunged = nospam_re.sub ("", addr)
+    return unmunged
+
+
+comment_re = re.compile ("^#.*")
+entry_re = re.compile (" *(.*?)(<.*?>) *\((.*?)\) *")
+
+filename = sys.argv[1]
+
+handle = open (filename)
+
+lines = handle.readlines ()
+
+count = 0
+
+for line in lines:
+    line = comment_re.sub ("", line)
+    string.strip (line)
+    if line == "" or line == "\n":
+        continue
+
+    match = entry_re.search (line)
+    if match:
+        name = string.strip (match.group (1))
+        email = unmunge_email (string.strip (match.group (2)))
+        contribution = string.strip (match.group (3))
+        count = count + 1
+        print email
+    else:
+        print "No match: " + line
+
+handle.close ()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail-ballots.pl	Thu Dec 07 10:33:46 2006 -0300
@@ -0,0 +1,108 @@
+#!/usr/bin/perl
+
+# Script to send ballots to all voters.
+#
+# How to use this script
+# ======================
+#
+# You probably want to first update the subject of the e-mail that will be
+# sent. Look for "Subject:" in this file and update the subject.
+#
+# You should know the secret string used to build the validation token for the
+# current elections/referendum. Let's suppose it's "secretstring".
+#
+# Let's also suppose that the ballot is in ballot.txt and that you made a list
+# of voters in voters.txt (the format of this file should be the same as the
+# one used in membership_new.txt).
+#
+# You should use this script like this:
+# $ ./mail-ballots.pl voters.txt ballot.txt "secretstring"
+#
+# This script needs a MTA to send the e-mails. If you don't have one or if
+# you're not sure that your ISP allows you to directly send mails, it's
+# probably better and safer to run the script from a gnome.org server.
+#
+# You may want to look at your mail server logs (and maybe keep them) to
+# know if the mail was delivered. There are usually 10-15 errors. In case of
+# such errors, you can try to look for the new e-mail addresses of the voters
+# to ask them if they want to update their registered e-mail address and
+# receive their ballot.
+
+use Digest::MD5 qw (md5_hex);
+use MD5;
+use Mail::Internet;
+
+die "Usage: mail-ballots.pl <recipient list> <ballot template> <secret string>\n" 
+	unless $#ARGV == 2;
+
+my $SECRET_STRING = $ARGV[2];
+
+open BALLOT, "<$ARGV[1]" || die "Cannot open ballot file $ARGV: $!";
+my @ballot = <BALLOT>;
+close BALLOT;
+
+for (my $i = 0; $i <= $#ballot; $i++) {
+    push @dear_indexes,     $i if $ballot[$i] =~ /^Estimado\(a\) <member>,/;
+    push @identity_indexes, $i if $ballot[$i] =~ /^\s*Member:/;
+    push @addr_indexes,     $i if $ballot[$i] =~ /^\s*Member Address:/;
+    push @token_indexes,    $i if $ballot[$i] =~ /^\s*Validation Token:/;
+}
+
+my $head = Mail::Header->new ( 
+	[ "From: Eleccion de sede Encuentro Linux <voto\@encuentrolinux.cl>",
+	  "Subject: Balota oficial de seleccion de sede Encuentro Linux 2008",
+	  "Reply-To: voto\@encuentrolinux.cl",
+	  "Sender: German Poo Caaman~o <gpoo\@ubiobio.cl>"
+	]);
+
+open RECIPS, "<$ARGV[0]" || die "Cannot open file $ARGV: $!";
+
+my $sent = 0;
+my $errors = 0;
+
+while (<RECIPS>) {
+    chomp; 
+    next if (/^\#/ || /^$/);
+
+    #if (!(/^ *(.*)\/(.*)\@no_spam\.(.*)\/.*\/.* *$/)) {
+    if (!(/^ *(.*) (<.*)\@no_spam\.(.*>).*$/)) {
+        print "Error for line: $_\n";
+		exit;
+        next;
+	}
+    my $identity = $1;
+    my $addr = "$2\@$3";
+    #my $hash = MD5->hexhash ("$identity $addr", $SECRET_STRING);
+	#print "$identity $addr-$SECRET_STRING: $hash\n";
+    my $hash = MD5->hexhash ("$addr", $SECRET_STRING);
+	print "$addr-$SECRET_STRING: $hash\n";
+
+    foreach $index (@dear_indexes) {
+        $ballot[$index] = "Estimado(a) $identity,\n";
+    }
+
+    foreach $index (@identity_indexes) {
+        $ballot[$index] = "Member: $identity\n";
+    }
+
+    foreach $index (@addr_indexes) {
+        $ballot[$index] = "Member Address: $addr\n";
+    }
+
+    foreach $index (@token_indexes) {
+        $ballot[$index] = "Validation Token: $hash\n";
+    }
+
+    $head->replace ("To", $addr);
+    my $mail = Mail::Internet->new (Header => $head, Body => \@ballot);
+    unless ($mail->smtpsend (MailFrom => "voto-owner\@encuentrolinux.cl")) {
+        print "Error: Could not send to $addr ($identity)!\n";
+        $errors++;
+    } else {
+        $sent++;
+    }
+}
+
+close RECIPS;
+
+print "Mailed $sent ballots; $errors could not be mailed.\n";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vote-counter.py	Thu Dec 07 10:33:46 2006 -0300
@@ -0,0 +1,240 @@
+#! /usr/bin/python
+
+import re
+import sys
+import string
+import md5
+
+MAX_CANDIDATES = 1
+
+class Ballot:
+    def __init__ (self):
+        self.email = 0
+        self.member_name = 0
+        self.member = 0
+        self.token = 0
+        self.votes = []
+
+    def add_vote (self, name, id):
+        self.votes.append ((name, id))
+
+class Candidate:
+    def __init__ (self, name, id):
+        self.name = name
+        self.id = id
+        self.count = 0
+	self.voters = []
+        
+candidates = {}
+
+candidate_tuples = [ \
+    ("UNIVERSIDAD TECNICA FEDERICO SANTA MARIA", 1), \
+    ("UNIVERSIDAD DE CONCEPCION", 2) ]
+
+for c in candidate_tuples:
+    cand = Candidate (c[0], c[1])
+    candidates[cand.id] = cand
+
+from_line_re = re.compile ("^From: *(.*)")
+member_name_re = re.compile (">? *Member: *(.*)")
+member_address_re = re.compile (">? *Member Address: *([^ ]*)")
+auth_token_re = re.compile (">? *Validation Token: *(.*)")
+vote_re = re.compile (">? *([A-Z ]+) *\(ID# *([0-9]+)\)")
+
+ballots = []
+current_ballot = 0
+
+filename = sys.argv[1]      # mail archive file 
+secret_cookie = sys.argv[2] # secret cookie
+voter_list = sys.argv[3]    # list of valid voter addresses
+
+# hash from valid addresses to whether they have sent in a ballot yet
+valid_addresses = {}
+
+voter_handle = open (voter_list)
+for voter_addr in voter_handle.readlines ():
+    valid_addresses[string.strip (voter_addr)] = 0
+
+handle = open (filename)
+lines = handle.readlines ()
+for line in lines:
+
+    match = from_line_re.match (line)
+    if match:
+        email = string.strip (match.group (1))
+        if current_ballot:
+            ballots.append (current_ballot)
+        current_ballot = Ballot ()
+        current_ballot.email = email
+
+        continue
+
+    match = member_name_re.match (line)
+    if match:
+        member = string.strip (match.group (1))
+        if (current_ballot.member_name):
+            print "Duplicate member name in ballot from '%s' - duplicates ''%s', '%s'" \
+                  % (current_ballot.email, current_ballot.member_name, member)
+        else:        
+            current_ballot.member_name = member
+
+        continue
+            
+    match = member_address_re.match (line)
+    if match:
+        member = string.strip (match.group (1))
+        if (current_ballot.member):
+            print "Duplicate member address in ballot from '%s' - duplicates ''%s', '%s'" \
+                  % (current_ballot.email, current_ballot.member, member)
+        else:        
+            current_ballot.member = member
+
+        continue
+            
+    match = auth_token_re.match (line)
+    if match:
+        token = string.strip (match.group (1))
+        if (current_ballot.token):
+            print "Duplicate auth token in ballot from '%s' - duplicates '%s', '%s'" \
+                  % (current_ballot.email, current_ballot.token, token)
+        else:
+            current_ballot.token = token
+
+        continue
+
+    match = vote_re.match (line)
+    if match:
+        name = string.strip (match.group (1))
+        id = string.strip (match.group (2))
+
+        id = int(id)
+
+        if not candidates.has_key (id):
+            print "Unknown candidate '%s' ID %d in ballot from '%s'" % (name, id, current_ballot.email)
+        elif not candidates[id].name == name:
+            print "Candidate name '%s' for ID '%s' doesn't match, expected '%s'" % (name, id, candidates[id].name)    
+        else:
+            current_ballot.add_vote (name, id)
+
+        continue
+    
+if current_ballot:
+    ballots.append (current_ballot)    
+        
+handle.close ()
+
+def contains_dups (b):
+    dups = {}
+    for v in b.votes:
+        id = v[1]
+        if dups.has_key (id):
+            return 1
+        dups[id] = 1
+    return 0
+
+dup_tokens = {}
+def md5_is_bad (b):
+    #key = b.member + secret_cookie
+    key = "%s %s%s" % (b.member_name, b.member, secret_cookie)
+    m = md5.new (key)
+    digest = m.digest ()
+    token = m.hexdigest()
+    if token == b.token:
+        if dup_tokens.has_key (token):
+            print "Auth token occurs twice, someone voted more than once"
+            return 1
+        else:
+            dup_tokens[token] = 1
+        return 0
+    else:
+        print "Bad auth token is %s hashed from '%s'" % (token, key)
+        return 1
+
+def valid_voter (addr):
+    return valid_addresses.has_key (addr)
+
+valid_ballots = {}
+
+i = 0
+for b in ballots:
+    error = 0
+    if not b.member:
+        error = "missing member address"
+    elif not b.token:
+        error = "missing auth token"
+    elif len (b.votes) > MAX_CANDIDATES:
+        error = "too many votes (%d votes)" % len (b.votes)
+    elif len (b.votes) == 0:
+        error = "didn't list any candidates"
+    elif contains_dups (b):
+        error = "contains duplicate votes for the same candidate"
+    elif md5_is_bad (b):
+        error = "bad authentication token"
+    elif not valid_voter (b.member):
+        error = "ballot from someone not on the list of valid voters"
+    else:
+        if valid_ballots.has_key (b.token):
+            old = valid_ballots[b.token]
+            print "Overriding previous valid ballot %d from %s with new ballot %d" %\
+                  (old[1], old[0].email, i)
+        valid_ballots[b.token] = (b, i)
+
+    if error:
+        print "Ignoring ballot %d from '%s' due to: %s" % (i, b.email, error)
+        
+    i = i + 1
+
+def tupcmp (a, b):
+    return cmp (a[1], b[1])
+
+## Print results only after all errors have been printed, so
+## we don't lose any errors.
+valids = valid_ballots.values ()
+valids.sort (tupcmp)
+for (b, i) in valids:
+    print "Ballot %d:" % i
+
+    print "  From:   " + b.email
+    print "  Member: " + b.member_name
+    print "  Member Address: " + b.member
+    print "  Token:  " + b.token
+    print "  Voted for %d candidates:" % len (b.votes)
+
+    voted_for = []
+
+    valid_addresses[b.member] = 1
+
+    for v in b.votes:
+        id = v[1]
+        candidates[id].count = candidates[id].count + 1
+	candidates[id].voters.append (b.member)
+        voted_for.append (candidates[id].name)
+
+    for v in voted_for:
+        print "   " + v
+
+print "The following members did not vote:"
+for addr in valid_addresses.keys ():
+    if not valid_addresses[addr]:
+        print addr
+
+def cmpcand (a, b):
+    return cmp (a.count, b.count)
+
+cand_list = candidates.values ()
+cand_list.sort (cmpcand)
+
+print ""
+print ""
+print "ELECTION RESULTS:"
+
+print " %d of %d members cast a valid ballot" \
+      % (len (valids), len (valid_addresses.keys()))
+
+for c in cand_list:
+    print "  %s (%d votes)" % (c.name, c.count)
+
+
+
+
+