]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Remaining pieces of Bug 23067 from yesterday... no idea why the first commit didn...
authorjustdave%syndicomm.com <>
Tue, 2 Apr 2002 06:52:37 +0000 (06:52 +0000)
committerjustdave%syndicomm.com <>
Tue, 2 Apr 2002 06:52:37 +0000 (06:52 +0000)
Bugzilla/Token.pm
CGI.pl
Token.pm
createaccount.cgi
defparams.pl
editusers.cgi
globals.pl
token.cgi
userprefs.cgi

index 4f7f61882ef341ce7bec31fbd1568721a4840288..9c136184bec73b50288a9dc8efe0eaa956dceddf 100644 (file)
@@ -37,6 +37,62 @@ package Token;
 # Functions
 ################################################################################
 
+sub IssueEmailChangeToken {
+    my ($userid, $old_email, $new_email) = @_;
+
+    # Generate a unique token and insert it into the tokens table.
+    # We have to lock the tokens table before generating the token, 
+    # since the database must be queried for token uniqueness.
+    &::SendSQL("LOCK TABLES tokens WRITE");
+    my $token = GenerateUniqueToken();
+    my $quotedtoken = &::SqlQuote($token);
+    my $quoted_emails = &::SqlQuote($old_email . ":" . $new_email);
+    &::SendSQL("INSERT INTO tokens ( userid , issuedate , token , 
+                                     tokentype , eventdata )
+                VALUES             ( $userid , NOW() , $quotedtoken , 
+                                     'emailold' , $quoted_emails )");
+    my $newtoken = GenerateUniqueToken();
+    $quotedtoken = &::SqlQuote($newtoken);
+    &::SendSQL("INSERT INTO tokens ( userid , issuedate , token , 
+                                     tokentype , eventdata )
+                VALUES             ( $userid , NOW() , $quotedtoken , 
+                                     'emailnew' , $quoted_emails )");
+    &::SendSQL("UNLOCK TABLES");
+
+    # Mail the user the token along with instructions for using it.
+
+    my $template = $::template;
+    my $vars = $::vars;
+
+    $vars->{'oldemailaddress'} = $old_email . &::Param('emailsuffix');
+    $vars->{'newemailaddress'} = $new_email . &::Param('emailsuffix');
+
+    $vars->{'token'} = &::url_quote($token);
+    $vars->{'emailaddress'} = $old_email . &::Param('emailsuffix');
+
+    my $message;
+    $template->process("token/emailchangeold.txt.tmpl", $vars, \$message)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+
+    open SENDMAIL, "|/usr/lib/sendmail -t -i";
+    print SENDMAIL $message;
+    close SENDMAIL;
+
+    $vars->{'token'} = &::url_quote($newtoken);
+    $vars->{'emailaddress'} = $new_email . &::Param('emailsuffix');
+
+    $message = "";
+    $template->process("token/emailchangenew.txt.tmpl", $vars, \$message)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+
+    open SENDMAIL, "|/usr/lib/sendmail -t -i";
+    print SENDMAIL $message;
+    close SENDMAIL;
+
+}
+
 sub IssuePasswordToken {
     # Generates a random token, adds it to the tokens table, and sends it
     # to the user with instructions for using it to change their password.
@@ -65,6 +121,14 @@ sub IssuePasswordToken {
 }
 
 
+sub CleanTokenTable {
+    &::SendSQL("LOCK TABLES tokens WRITE");
+    &::SendSQL("DELETE FROM tokens 
+                WHERE TO_DAYS(NOW()) - TO_DAYS(issuedate) >= 3");
+    &::SendSQL("UNLOCK TABLES");
+}
+
+
 sub GenerateUniqueToken {
     # Generates a unique random token.  Uses &GenerateRandomPassword 
     # for the tokens themselves and checks uniqueness by searching for
@@ -143,25 +207,27 @@ sub Cancel {
     # Format the user's real name and email address into a single string.
     my $username = $realname ? $realname . " <" . $loginname . ">" : $loginname;
 
-    # Notify the user via email about the cancellation.
-    open SENDMAIL, "|/usr/lib/sendmail -t -i";
-    print SENDMAIL qq|From: bugzilla-daemon
-To: $username
-Subject: "$tokentype" token cancelled
+    my $template = $::template;
+    my $vars = $::vars;
 
-A token was cancelled from $::ENV{'REMOTE_ADDR'}.  This is either 
-an honest mistake or the result of a malicious hack attempt.  
-Take a look at the information below and forward this email 
-to $maintainer if you suspect foul play.
+    $vars->{'emailaddress'} = $username;
+    $vars->{'maintainer'} = $maintainer;
+    $vars->{'remoteaddress'} = $::ENV{'REMOTE_ADDR'};
+    $vars->{'token'} = &::url_quote($token);
+    $vars->{'tokentype'} = $tokentype;
+    $vars->{'issuedate'} = $issuedate;
+    $vars->{'eventdata'} = $eventdata;
+    $vars->{'cancelaction'} = $cancelaction;
 
-            Token: $token
-       Token Type: $tokentype
-             User: $username
-       Issue Date: $issuedate
-       Event Data: $eventdata
+    # Notify the user via email about the cancellation.
 
-Cancelled Because: $cancelaction
-|;
+    my $message;
+    $template->process("token/tokencancel.txt.tmpl", $vars, \$message)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+
+    open SENDMAIL, "|/usr/lib/sendmail -t -i";
+    print SENDMAIL $message;
     close SENDMAIL;
 
     # Delete the token from the database.
@@ -171,14 +237,30 @@ Cancelled Because: $cancelaction
 }
 
 sub HasPasswordToken {
-    # Returns a password token if the user has one.  Otherwise returns 0 (false).
+    # Returns a password token if the user has one.
     
     my ($userid) = @_;
     
-    &::SendSQL("SELECT token FROM tokens WHERE userid = $userid LIMIT 1");
+    &::SendSQL("SELECT token FROM tokens 
+                WHERE userid = $userid AND tokentype = 'password' LIMIT 1");
     my ($token) = &::FetchSQLData();
     
     return $token;
 }
 
+sub HasEmailChangeToken {
+    # Returns an email change token if the user has one. 
+    
+    my ($userid) = @_;
+    
+    &::SendSQL("SELECT token FROM tokens 
+                 WHERE userid = $userid 
+                   AND tokentype = 'emailnew' 
+                    OR tokentype = 'emailold' LIMIT 1");
+    my ($token) = &::FetchSQLData();
+    
+    return $token;
+}
+
+
 1;
diff --git a/CGI.pl b/CGI.pl
index 2b8f5d048c9851698e620ba002a0664f43bd575b..be6c95270a3e128dde182a978cde0b18c526afa8 100644 (file)
--- a/CGI.pl
+++ b/CGI.pl
@@ -814,6 +814,12 @@ sub confirm_login {
         # If this is a new user, generate a password, insert a record
         # into the database, and email their password to them.
         if ( defined $::FORM{"PleaseMailAPassword"} && !$userid ) {
+            # Ensure the new login is valid
+            if(!ValidateNewUser($enteredlogin)) {
+                DisplayError("Account Exists");
+                exit;
+            }
+
             my $password = InsertNewUser($enteredlogin, "");
             # There's a template for this - account_created.tmpl - but
             # it's easier to wait to use it until templatisation has progressed
index 4f7f61882ef341ce7bec31fbd1568721a4840288..9c136184bec73b50288a9dc8efe0eaa956dceddf 100644 (file)
--- a/Token.pm
+++ b/Token.pm
@@ -37,6 +37,62 @@ package Token;
 # Functions
 ################################################################################
 
+sub IssueEmailChangeToken {
+    my ($userid, $old_email, $new_email) = @_;
+
+    # Generate a unique token and insert it into the tokens table.
+    # We have to lock the tokens table before generating the token, 
+    # since the database must be queried for token uniqueness.
+    &::SendSQL("LOCK TABLES tokens WRITE");
+    my $token = GenerateUniqueToken();
+    my $quotedtoken = &::SqlQuote($token);
+    my $quoted_emails = &::SqlQuote($old_email . ":" . $new_email);
+    &::SendSQL("INSERT INTO tokens ( userid , issuedate , token , 
+                                     tokentype , eventdata )
+                VALUES             ( $userid , NOW() , $quotedtoken , 
+                                     'emailold' , $quoted_emails )");
+    my $newtoken = GenerateUniqueToken();
+    $quotedtoken = &::SqlQuote($newtoken);
+    &::SendSQL("INSERT INTO tokens ( userid , issuedate , token , 
+                                     tokentype , eventdata )
+                VALUES             ( $userid , NOW() , $quotedtoken , 
+                                     'emailnew' , $quoted_emails )");
+    &::SendSQL("UNLOCK TABLES");
+
+    # Mail the user the token along with instructions for using it.
+
+    my $template = $::template;
+    my $vars = $::vars;
+
+    $vars->{'oldemailaddress'} = $old_email . &::Param('emailsuffix');
+    $vars->{'newemailaddress'} = $new_email . &::Param('emailsuffix');
+
+    $vars->{'token'} = &::url_quote($token);
+    $vars->{'emailaddress'} = $old_email . &::Param('emailsuffix');
+
+    my $message;
+    $template->process("token/emailchangeold.txt.tmpl", $vars, \$message)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+
+    open SENDMAIL, "|/usr/lib/sendmail -t -i";
+    print SENDMAIL $message;
+    close SENDMAIL;
+
+    $vars->{'token'} = &::url_quote($newtoken);
+    $vars->{'emailaddress'} = $new_email . &::Param('emailsuffix');
+
+    $message = "";
+    $template->process("token/emailchangenew.txt.tmpl", $vars, \$message)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+
+    open SENDMAIL, "|/usr/lib/sendmail -t -i";
+    print SENDMAIL $message;
+    close SENDMAIL;
+
+}
+
 sub IssuePasswordToken {
     # Generates a random token, adds it to the tokens table, and sends it
     # to the user with instructions for using it to change their password.
@@ -65,6 +121,14 @@ sub IssuePasswordToken {
 }
 
 
+sub CleanTokenTable {
+    &::SendSQL("LOCK TABLES tokens WRITE");
+    &::SendSQL("DELETE FROM tokens 
+                WHERE TO_DAYS(NOW()) - TO_DAYS(issuedate) >= 3");
+    &::SendSQL("UNLOCK TABLES");
+}
+
+
 sub GenerateUniqueToken {
     # Generates a unique random token.  Uses &GenerateRandomPassword 
     # for the tokens themselves and checks uniqueness by searching for
@@ -143,25 +207,27 @@ sub Cancel {
     # Format the user's real name and email address into a single string.
     my $username = $realname ? $realname . " <" . $loginname . ">" : $loginname;
 
-    # Notify the user via email about the cancellation.
-    open SENDMAIL, "|/usr/lib/sendmail -t -i";
-    print SENDMAIL qq|From: bugzilla-daemon
-To: $username
-Subject: "$tokentype" token cancelled
+    my $template = $::template;
+    my $vars = $::vars;
 
-A token was cancelled from $::ENV{'REMOTE_ADDR'}.  This is either 
-an honest mistake or the result of a malicious hack attempt.  
-Take a look at the information below and forward this email 
-to $maintainer if you suspect foul play.
+    $vars->{'emailaddress'} = $username;
+    $vars->{'maintainer'} = $maintainer;
+    $vars->{'remoteaddress'} = $::ENV{'REMOTE_ADDR'};
+    $vars->{'token'} = &::url_quote($token);
+    $vars->{'tokentype'} = $tokentype;
+    $vars->{'issuedate'} = $issuedate;
+    $vars->{'eventdata'} = $eventdata;
+    $vars->{'cancelaction'} = $cancelaction;
 
-            Token: $token
-       Token Type: $tokentype
-             User: $username
-       Issue Date: $issuedate
-       Event Data: $eventdata
+    # Notify the user via email about the cancellation.
 
-Cancelled Because: $cancelaction
-|;
+    my $message;
+    $template->process("token/tokencancel.txt.tmpl", $vars, \$message)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+
+    open SENDMAIL, "|/usr/lib/sendmail -t -i";
+    print SENDMAIL $message;
     close SENDMAIL;
 
     # Delete the token from the database.
@@ -171,14 +237,30 @@ Cancelled Because: $cancelaction
 }
 
 sub HasPasswordToken {
-    # Returns a password token if the user has one.  Otherwise returns 0 (false).
+    # Returns a password token if the user has one.
     
     my ($userid) = @_;
     
-    &::SendSQL("SELECT token FROM tokens WHERE userid = $userid LIMIT 1");
+    &::SendSQL("SELECT token FROM tokens 
+                WHERE userid = $userid AND tokentype = 'password' LIMIT 1");
     my ($token) = &::FetchSQLData();
     
     return $token;
 }
 
+sub HasEmailChangeToken {
+    # Returns an email change token if the user has one. 
+    
+    my ($userid) = @_;
+    
+    &::SendSQL("SELECT token FROM tokens 
+                 WHERE userid = $userid 
+                   AND tokentype = 'emailnew' 
+                    OR tokentype = 'emailold' LIMIT 1");
+    my ($token) = &::FetchSQLData();
+    
+    return $token;
+}
+
+
 1;
index ecf3a68f67b1bd27273bd41f138375aff35ac0c4..aaec3b679673b66e0138bfaa0ddc0464fc275058 100755 (executable)
@@ -65,7 +65,7 @@ if (defined($login)) {
     CheckEmailSyntax($login);
     $vars->{'login'} = $login;
     
-    if (DBname_to_id($login) != 0) {
+    if (!ValidateNewUser($login)) {
         # Account already exists        
         $template->process("admin/account_exists.tmpl", $vars)
           || DisplayError("Template process failed: " . $template->error());
index 7fc73f51b70590ba3cf20a0270914e802cbf9ee5..f5b7ba098c47f322df3171b8a3c1c89916dfa317 100644 (file)
@@ -625,6 +625,12 @@ DefParam("allowbugdeletion",
          0);
 
 
+DefParam("allowemailchange",
+         q{Users can change their own email address through the preferences.  Note that the change is validated by emailing both addresses, so switching this option on will not let users use an invalid address.},
+         "b",
+         0);
+
+
 DefParam("allowuserdeletion",
          q{The pages to edit users can also let you delete a user.  But there is no code that goes and cleans up any references to that user in other tables, so such deletions are kinda scary.  So, you have to turn on this option before any such deletions will ever happen.},
          "b",
index bc864bcf05c2d392403bab2f570518c811bfbd58..3c24091554ea4aaad96912df26ab639f2966c6fb 100755 (executable)
@@ -451,7 +451,7 @@ if ($action eq 'new') {
         PutTrailer($localtrailer);
         exit;
     }
-    if (TestUser($user)) {
+    if (!ValidateNewUser($user)) {
         print "The user '$user' does already exist. Please press\n";
         print "<b>Back</b> and try again.\n";
         PutTrailer($localtrailer);
index 562237a6885553268688d34afde71d049696a96e..cb3612671c5e63f7243133133f843e75d896c5bb 100644 (file)
@@ -671,6 +671,8 @@ sub GetVersionTable {
         $mtime = 0;
     }
     if (time() - $mtime > 3600) {
+        use Token;
+        Token::CleanTokenTable();
         GenerateVersionTable();
     }
     require 'data/versioncache';
@@ -686,6 +688,31 @@ sub GetVersionTable {
 }
 
 
+# Validates a given username as a new username
+# returns 1 if valid, 0 if invalid
+sub ValidateNewUser {
+    my ($username, $old_username) = @_;
+
+    if(DBname_to_id($username) != 0) {
+        return 0;
+    }
+
+    # Reject if the new login is part of an email change which is 
+    # still in progress
+    SendSQL("SELECT eventdata FROM tokens WHERE tokentype = 'emailold' 
+                AND eventdata like '%:$username' 
+                 OR eventdata like '$username:%'");
+    if (my ($eventdata) = FetchSQLData()) {
+        # Allow thru owner of token
+        if($old_username && ($eventdata eq "$old_username:$username")) {
+            return 1;
+        }
+        return 0;
+    }
+
+    return 1;
+}
+
 sub InsertNewUser {
     my ($username, $realname) = (@_);
 
@@ -963,10 +990,12 @@ sub DBNameToIdAndCheck {
         return $result;
     }
     if ($forceok) {
-        InsertNewUser($name, "");
-        $result = DBname_to_id($name);
-        if ($result > 0) {
-            return $result;
+        if(ValidateNewUser($name)) {
+            InsertNewUser($name, "");
+            $result = DBname_to_id($name);
+            if ($result > 0) {
+                return $result;
+            }
         }
         print "Yikes; couldn't create user $name.  Please report problem to " .
             Param("maintainer") ."\n";
index d0de17baa2fe8498b144b7af2050b651eac1939f..e8fb3f90f95c8b986b476047deb3c12b2da3ed14 100755 (executable)
--- a/token.cgi
+++ b/token.cgi
@@ -69,10 +69,13 @@ if ($::FORM{'t'}) {
       exit;
   }
 
+
+  Token::CleanTokenTable();
+
   # Make sure the token exists in the database.
   SendSQL( "SELECT tokentype FROM tokens WHERE token = $::quotedtoken" );
   (my $tokentype = FetchSQLData())
-    || DisplayError("The token you submitted does not exist.")
+    || DisplayError("The token you submitted does not exist, has expired, or has been cancelled.")
     && exit;
 
   # Make sure the token is the correct type for the action being taken.
@@ -81,6 +84,20 @@ if ($::FORM{'t'}) {
     Token::Cancel($::token, "user tried to use token to change password");
     exit;
   }
+  if ( ($::action eq 'cxlem') 
+      && (($tokentype ne 'emailold') && ($tokentype ne 'emailnew')) ) {
+    DisplayError("That token cannot be used to cancel an email address change.");
+    Token::Cancel($::token, 
+                  "user tried to use token to cancel email address change");
+    exit;
+  }
+  if ( grep($::action eq $_ , qw(cfmem chgem)) 
+      && ($tokentype ne 'emailnew') ) {
+    DisplayError("That token cannot be used to change your email address.");
+    Token::Cancel($::token, 
+                  "user tried to use token to confirm email address change");
+    exit;
+  }
 }
 
 # If the user is requesting a password change, make sure they submitted
@@ -132,6 +149,12 @@ if ($::action eq 'reqpw') {
     cancelChangePassword(); 
 } elsif ($::action eq 'chgpw') { 
     changePassword(); 
+} elsif ($::action eq 'cfmem') {
+    confirmChangeEmail();
+} elsif ($::action eq 'cxlem') {
+    cancelChangeEmail();
+} elsif ($::action eq 'chgem') {
+    changeEmail();
 } else { 
     # If the action that the user wants to take (specified in the "a" form field)
     # is none of the above listed actions, display an error telling the user 
@@ -210,6 +233,110 @@ sub changePassword {
       && exit;
 }
 
+sub confirmChangeEmail {
+    # Return HTTP response headers.
+    print "Content-Type: text/html\n\n";
+
+    $vars->{'title'} = "Confirm Change Email";
+    $vars->{'token'} = $::token;
+
+    $template->process("token/confirmemail.html.tmpl", $vars)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+}
+
+sub changeEmail {
+
+    # Get the user's ID from the tokens table.
+    SendSQL("SELECT userid, eventdata FROM tokens 
+              WHERE token = $::quotedtoken");
+    my ($userid, $eventdata) = FetchSQLData();
+    my ($old_email, $new_email) = split(/:/,$eventdata);
+    my $quotednewemail = SqlQuote($new_email);
+
+    # Check the user entered the correct old email address
+    if($::FORM{'email'} ne $old_email) {
+        DisplayError("Email Address confirmation failed");
+        exit;
+    }
+    # The new email address should be available as this was 
+    # confirmed initially so cancel token if it is not still available
+    if (! ValidateNewUser($new_email,$old_email)) {
+        DisplayError("Account $new_email already exists.");
+        Token::Cancel($::token,"Account $new_email already exists.");
+        exit;
+    } 
+
+    # Update the user's login name in the profiles table and delete the token
+    # from the tokens table.
+    SendSQL("LOCK TABLES profiles WRITE , tokens WRITE");
+    SendSQL("UPDATE   profiles
+         SET      login_name = $quotednewemail
+         WHERE    userid = $userid");
+    SendSQL("DELETE FROM tokens WHERE token = $::quotedtoken");
+    SendSQL("DELETE FROM tokens WHERE userid = $userid 
+                                  AND tokentype = 'emailnew'");
+    SendSQL("UNLOCK TABLES");
+
+    # Return HTTP response headers.
+    print "Content-Type: text/html\n\n";
+
+    # Let the user know their email address has been changed.
+
+    $vars->{'title'} = "Bugzilla Login Changed";
+    $vars->{'message'} = "Your Bugzilla login has been changed.";
+
+    $template->process("global/message.html.tmpl", $vars)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+}
+
+sub cancelChangeEmail {
+    # Get the user's ID from the tokens table.
+    SendSQL("SELECT userid, tokentype, eventdata FROM tokens 
+             WHERE token = $::quotedtoken");
+    my ($userid, $tokentype, $eventdata) = FetchSQLData();
+    my ($old_email, $new_email) = split(/:/,$eventdata);
+
+    if($tokentype eq "emailold") {
+        $vars->{'message'} = "The request to change the email address " .
+            "for your account to $new_email has been cancelled.";
+
+        SendSQL("SELECT login_name FROM profiles WHERE userid = $userid");
+        my $actualemail = FetchSQLData();
+        
+        # check to see if it has been altered
+        if($actualemail ne $old_email) {
+            my $quotedoldemail = SqlQuote($old_email);
+
+            SendSQL("LOCK TABLES profiles WRITE");
+            SendSQL("UPDATE   profiles
+                 SET      login_name = $quotedoldemail
+                 WHERE    userid = $userid");
+            SendSQL("UNLOCK TABLES");
+            $vars->{'message'} .= 
+                "  Your old account settings have been reinstated.";
+        } 
+    } 
+    else {
+        $vars->{'message'} = "The request to change the email address " .
+            "for the $old_email account to $new_email has been cancelled.";
+    }
+    Token::Cancel($::token, $vars->{'message'});
+
+    SendSQL("LOCK TABLES tokens WRITE");
+    SendSQL("DELETE FROM tokens 
+             WHERE userid = $userid 
+             AND tokentype = 'emailold' OR tokentype = 'emailnew'");
+    SendSQL("UNLOCK TABLES");
 
+    # Return HTTP response headers.
+    print "Content-Type: text/html\n\n";
 
+    $vars->{'title'} = "Cancel Request to Change Email Address";
+
+    $template->process("global/message.html.tmpl", $vars)
+      || &::DisplayError("Template process failed: " . $template->error())
+      && exit;
+}
 
index e38cd40950e70533f06545d853b7c01b2ddd47f7..ddeb0f1f7f33f29fe806c70bc9abfba9422e45ec 100755 (executable)
@@ -65,16 +65,33 @@ chop $defaultflagstring;
 sub DoAccount {
     SendSQL("SELECT realname FROM profiles WHERE userid = $userid");
     $vars->{'realname'} = FetchSQLData();
+
+    if(Param('allowemailchange')) {
+        SendSQL("SELECT tokentype, issuedate + INTERVAL 3 DAY, eventdata 
+                    FROM tokens
+                    WHERE userid = $userid
+                    AND tokentype LIKE 'email%' 
+                    ORDER BY tokentype ASC LIMIT 1");
+        if(MoreSQLData()) {
+            my ($tokentype, $change_date, $eventdata) = &::FetchSQLData();
+            $vars->{'login_change_date'} = $change_date;
+
+            if($tokentype eq 'emailnew') {
+                my ($oldemail,$newemail) = split(/:/,$eventdata);
+                $vars->{'new_login_name'} = $newemail;
+            }
+        }
+    }
 }
 
 sub SaveAccount {
+    my $pwd1 = $::FORM{'new_password1'};
+    my $pwd2 = $::FORM{'new_password2'};
+
     if ($::FORM{'Bugzilla_password'} ne "" || 
-        $::FORM{'new_password1'} ne "" || 
-        $::FORM{'new_password2'} ne "") 
+        $pwd1 ne "" || $pwd2 ne "") 
     {
         my $old = SqlQuote($::FORM{'Bugzilla_password'});
-        my $pwd1 = SqlQuote($::FORM{'new_password1'});
-        my $pwd2 = SqlQuote($::FORM{'new_password2'});
         SendSQL("SELECT cryptpassword FROM profiles WHERE userid = $userid");
         my $oldcryptedpwd = FetchOneColumn();
         if (!$oldcryptedpwd) {
@@ -87,23 +104,63 @@ sub SaveAccount {
             DisplayError("You did not enter your old password correctly.");
             exit;
         }
-        if ($pwd1 ne $pwd2) {
-            DisplayError("The two passwords you entered did not match.");
-            exit;
+
+        if ($pwd1 ne "" || $pwd2 ne "")
+        {
+            if ($pwd1 ne $pwd2) {
+                DisplayError("The two passwords you entered did not match.");
+                exit;
+            }
+            if ($::FORM{'new_password1'} eq '') {
+                DisplayError("You must enter a new password.");
+                exit;
+            }
+            my $passworderror = ValidatePassword($pwd1);
+            (DisplayError($passworderror) && exit) if $passworderror;
+        
+            my $cryptedpassword = SqlQuote(Crypt($pwd1));
+            SendSQL("UPDATE profiles 
+                     SET    cryptpassword = $cryptedpassword 
+                     WHERE  userid = $userid");
+            # Invalidate all logins except for the current one
+            InvalidateLogins($userid, $::COOKIE{"Bugzilla_logincookie"});
         }
-        if ($::FORM{'new_password1'} eq '') {
-            DisplayError("You must enter a new password.");
-            exit;
+    }
+
+    if(Param("allowemailchange") && $::FORM{'new_login_name'}) {
+        my $old_login_name = $::FORM{'Bugzilla_login'};
+        my $new_login_name = trim($::FORM{'new_login_name'});
+
+        if($old_login_name ne $new_login_name) {
+            if( $::FORM{'Bugzilla_password'} eq "") {
+                DisplayError("You must enter your old password to 
+                                             change email address.");
+                exit;
+            }
+
+            use Token;
+            # Block multiple email changes for the same user.
+            if (Token::HasEmailChangeToken($userid)) {
+                DisplayError("Email change already in progress; 
+                                          please check your email.");
+                exit;
+            }
+
+            # Before changing an email address, confirm one does not exist.
+            CheckEmailSyntax($new_login_name);
+            trick_taint($new_login_name);
+            if (!ValidateNewUser($new_login_name)) {
+                DisplayError("Account $new_login_name already exists");
+                exit;
+            }
+
+            Token::IssueEmailChangeToken($userid,$old_login_name,
+                                                 $new_login_name);
+
+            $vars->{'changes_saved'} = 
+                "An email has been sent to both old and new email 
+                 addresses to confirm the change of email address.";
         }
-        my $passworderror = ValidatePassword($::FORM{'new_password1'});
-        (DisplayError($passworderror) && exit) if $passworderror;
-        
-        my $cryptedpassword = SqlQuote(Crypt($::FORM{'new_password1'}));
-        SendSQL("UPDATE profiles 
-                 SET    cryptpassword = $cryptedpassword 
-                 WHERE  userid = $userid");
-        # Invalidate all logins except for the current one
-        InvalidateLogins($userid, $::COOKIE{"Bugzilla_logincookie"});
     }
 
     SendSQL("UPDATE profiles SET " .