diff --git a/helios/models.py b/helios/models.py
index db27f5a1ca37c569a8252812ec9177e690809f5d..f5e384e436f7eee9827b3d930d3021f410c9e166 100644
--- a/helios/models.py
+++ b/helios/models.py
@@ -16,6 +16,7 @@ import bleach
 import unicodecsv
 from django.conf import settings
 from django.db import models, transaction
+from validate_email import validate_email
 
 from helios import datatypes
 from helios import utils
@@ -765,64 +766,74 @@ class VoterFile(models.Model):
 
     for voter_fields in reader:
       # bad line
-      if len(voter_fields) < 1:
+      if len(voter_fields) < 2:
         continue
-    
-      return_dict = {'voter_id': voter_fields[0].strip()}
 
-      if len(voter_fields) > 1:
-        return_dict['email'] = voter_fields[1].strip()
-      else:
-        # assume single field means the email is the same field
-        return_dict['email'] = voter_fields[0].strip()
+      voter_type = voter_fields[0].strip()
+      voter_id = voter_fields[1].strip()
 
+      if not voter_type in AUTH_SYSTEMS:
+        raise Exception("invalid voter type '%s' for voter id '%s'" % (voter_type, voter_id))
+
+      # default to having email be the same as voter_id
+      voter_email = voter_id
       if len(voter_fields) > 2:
-        return_dict['name'] = voter_fields[2].strip()
-      else:
-        return_dict['name'] = return_dict['email']
+        # but if it's supplied, it will be the 3rd field.
+        voter_email = voter_fields[2].strip()
+      if voter_type == "password" and not validate_email(voter_email):
+        raise Exception("invalid voter email '%s' for voter id '%s'" % (voter_email, voter_id))
+
+      # same thing for voter display name.
+      voter_name = voter_email
+      if len(voter_fields) > 3:
+        # which is supplied as the 4th field if known.
+        voter_name = voter_fields[3].strip()
+
+      yield {
+        'voter_type': voter_type,
+        'voter_id': voter_id,
+        'email': voter_email,
+        'name': voter_name,
+      }
 
-      yield return_dict
     if close:
       voter_stream.close()
-    
+
   def process(self):
     self.processing_started_at = datetime.datetime.utcnow()
     self.save()
 
-    election = self.election    
-    last_alias_num = election.last_alias_num
-
-    num_voters = 0
-    new_voters = []
-    for voter in self.itervoters():
-      num_voters += 1
-    
-      # does voter for this user already exist
-      existing_voter = Voter.get_by_election_and_voter_id(election, voter['voter_id'])
-    
-      # create the voter
-      if not existing_voter:
-        voter_uuid = str(uuid.uuid4())
-        existing_voter = Voter(uuid= voter_uuid, user = None, voter_login_id = voter['voter_id'],
-                      voter_name = voter['name'], voter_email = voter['email'], election = election)
-        existing_voter.generate_password()
-        new_voters.append(existing_voter)
-        existing_voter.save()
-
-    if election.use_voter_aliases:
-      voter_alias_integers = list(range(last_alias_num+1, last_alias_num+1+num_voters))
-      random.shuffle(voter_alias_integers)
-      for i, voter in enumerate(new_voters):
-        voter.alias = 'V%s' % voter_alias_integers[i]
-        voter.save()
+    voters = list(self.itervoters())
+    self.num_voters = len(voters)
+    random.shuffle(voters)
+
+    for voter in voters:
+      if voter['voter_type'] == 'password':
+          # does voter for this user already exist
+          existing_voter = Voter.get_by_election_and_voter_id(self.election, voter['voter_id'])
+          if existing_voter:
+              continue
+          # create the voter
+          voter_uuid = str(uuid.uuid4())
+          new_voter = Voter(uuid=voter_uuid, user = None, voter_login_id = voter['voter_id'],
+              voter_name = voter['name'], voter_email = voter['email'], election = self.election)
+          new_voter.generate_password()
+          if election.use_voter_aliases:
+              utils.lock_row(Election, election.id)
+              alias_num = election.last_alias_num + 1
+              new_voter.alias = "V%s" % alias_num
+          new_voter.save()
+      else:
+          user, _ = User.objects.get_or_create(user_type=voter['voter_type'], user_id=voter['voter_id'], defaults = {'name': voter['voter_id'], 'info': {}, 'token': None})
+          existing_voter = Voter.get_by_election_and_user(self.election, user)
+          if not existing_voter:
+              Voter.register_user_in_election(user, self.election)
 
-    self.num_voters = num_voters
     self.processing_finished_at = datetime.datetime.utcnow()
     self.save()
 
-    return num_voters
+    return self.num_voters
 
-    
 class Voter(HeliosModel):
   election = models.ForeignKey(Election, on_delete=models.CASCADE)
   
diff --git a/helios/templates/voters_upload.html b/helios/templates/voters_upload.html
index 908c147f124f4c342b034b6845bd406f3e5b8048..116ed7ec5392951a13930e99d23ecc61460a0bed 100644
--- a/helios/templates/voters_upload.html
+++ b/helios/templates/voters_upload.html
@@ -4,22 +4,28 @@
   <h2 class="title">{{election.name}} &mdash; Bulk Upload Voters <span style="font-size:0.7em;">[<a href="{% url "election@view" election.uuid %}">back to election</a>]</span></h2>
 
 <form method="post" action="" id="upload_form" enctype="multipart/form-data">
-  <p>
+<p>
     If you would like to specify your list of voters by name and email address,<br />
     you can bulk upload a list of such voters here.<br /><br />
 
     Please prepare a text file of comma-separated values with the fields:
 </p>
 <pre>
-   &lt;unique_id&gt;,&lt;email&gt,&lt;full name&gt;
+   password,&lt;unique_id&gt;,&lt;email&gt,&lt;full name&gt;
+</pre>
+<p>
+or
+</p>
+<pre>
+   github,&lt;username&gt;
 </pre>
 
 <p>
 For example:
   </p>
   <pre>
-      benadida,ben@adida.net,Ben Adida
-      bobsmith,bob@acme.org,Bob Smith
+      password,bobsmith,bob@acme.org,Bob Smith
+      github,benadida
       ...
   </pre> 
 
diff --git a/helios/templates/voters_upload_confirm.html b/helios/templates/voters_upload_confirm.html
index 911cb259d9d321e6bd0cc69ddc9cde95acb7320c..978aecee2bc327efd16c2c2b6d96c93a11b6164a 100644
--- a/helios/templates/voters_upload_confirm.html
+++ b/helios/templates/voters_upload_confirm.html
@@ -8,9 +8,9 @@ You have uploaded a file of voters. The first few rows of this file are:
 </p>
 
 <table>
-<tr><th>Voter Login</th><th>Email Address</th><th>Name</th></tr>
+<tr><th>Voter Type</th><th>Voter Login</th><th>Email Address</th><th>Name</th></tr>
 {% for v in voters %}
-<tr><td>{{v.voter_id}}</td><td>{{v.email}}</td><td>{{v.name}}</td></tr>
+<tr><td>{{v.voter_type}}</td><td>{{v.voter_id}}</td><td>{{v.email}}</td><td>{{v.name}}</td></tr>
 {% endfor %}
 </table>
 
diff --git a/helios/views.py b/helios/views.py
index 3b19d8ca680f2e5cba9082758d25784472de2770..9be3a316677d9459ed5f953ad579e950b82ec514 100644
--- a/helios/views.py
+++ b/helios/views.py
@@ -17,7 +17,6 @@ from django.core.paginator import Paginator
 from django.db import transaction, IntegrityError
 from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseForbidden
 from django.urls import reverse
-from validate_email import validate_email
 
 import helios_auth.url_names as helios_auth_urls
 from helios import utils, VOTERS_EMAIL, VOTERS_UPLOAD, url_names
@@ -1268,10 +1267,8 @@ def voters_eligibility(request, election):
 @election_admin()
 def voters_upload(request, election):
   """
-  Upload a CSV of password-based voters with
-  voter_id, email, name
-  
-  name and email are needed only if voter_type is static
+  Upload a CSV of voters with
+  voter_type, voter_id, optional_additional_params (e.g. email, name)
   """
 
   ## TRYING this: allowing voters upload by admin when election is frozen
@@ -1301,13 +1298,11 @@ def voters_upload(request, election):
         # import the first few lines to check
         try:
           voters = [v for v in voter_file_obj.itervoters()][:5]
-        except:
+          if len(voters) == 0:
+            raise Exception("no valid lines found in voter file")
+        except Exception as e:
           voters = []
-          problems.append("your CSV file could not be processed. Please check that it is a proper CSV file.")
-
-        # check if voter emails look like emails
-        if False in [validate_email(v['email']) for v in voters]:
-          problems.append("those don't look like correct email addresses. Are you sure you uploaded a file with email address as second field?")
+          problems.append("your CSV file could not be processed because %s" % str(e))
 
         return render_template(request, 'voters_upload_confirm', {'election': election, 'voters': voters, 'problems': problems})
       else: