diff --git a/resources/static/dialog/qunit.html b/resources/static/dialog/qunit.html
index 8a9f00c078ad7564ce39f6dd6c55ead608502bc6..0236807217744451277741940817a7f083dcc48c 100644
--- a/resources/static/dialog/qunit.html
+++ b/resources/static/dialog/qunit.html
@@ -49,6 +49,11 @@
       This is a long tooltip.  This should remain on the screen for about 5 seconds.
     </div>
 
+    <ul class="notifications">
+      <li class="notification emailsent">Email Sent</li> 
+      <li class="notification doh">doh</li> 
+    </ul>
+
     <script type="text/html" id="templateTooltip">
       <div class="tooltip">
         {{ contents }}
diff --git a/resources/static/dialog/resources/user.js b/resources/static/dialog/resources/user.js
index f78fadefde32261d645b0b1d25c5653880e7910b..64d4ae72fc52a75b70d4053e138d9fa70141c2c8 100644
--- a/resources/static/dialog/resources/user.js
+++ b/resources/static/dialog/resources/user.js
@@ -283,36 +283,54 @@ BrowserID.User = (function() {
      * Set the password of the current user.
      * @method setPassword
      * @param {string} password - password to set
-     * @param {function} [onSuccess] - Called on successful completion. 
+     * @param {function} [onComplete] - Called on successful completion. 
      * @param {function} [onFailure] - Called on error.
      */
-    setPassword: function(password, onSuccess, onFailure) {
-      network.setPassword(password, onSuccess, onFailure);
+    setPassword: function(password, onComplete, onFailure) {
+      network.setPassword(password, onComplete, onFailure);
     },
 
     /**
      * Request a password reset for the given email address.
      * @method requestPasswordReset
      * @param {string} email - email address to reset password for.
-     * @param {function} [onSuccess] - Callback to call when complete.
+     * @param {function} [onComplete] - Callback to call when complete, called 
+     * with a single object, info.
+     *    info.status {boolean} - true or false whether request was successful.
+     *    info.reason {string} - if status false, reason of failure.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    requestPasswordReset: function(email, onSuccess, onFailure) {
-      network.requestPasswordReset(email, origin, onSuccess, onFailure);
+    requestPasswordReset: function(email, onComplete, onFailure) {
+      this.isEmailRegistered(email, function(registered) {
+        if (registered) {
+          network.requestPasswordReset(email, origin, function(reset) {
+            var status = {
+              success: reset
+            };
+
+            if(!reset) status.reason = "throttle";
+
+            if (onComplete) onComplete(status);
+          }, onFailure);
+        }
+        else if (onComplete) {
+          onComplete({ success: false, reason: "invalid_user" });
+        }
+      }, onFailure);
     },
 
     /**
      * Cancel the current user's account.  Remove last traces of their 
      * identity.
      * @method cancelUser
-     * @param {function} [onSuccess] - Called whenever complete.
+     * @param {function} [onComplete] - Called whenever complete.
      * @param {function} [onFailure] - called on error.
      */
-    cancelUser: function(onSuccess, onFailure) {
+    cancelUser: function(onComplete, onFailure) {
       network.cancelUser(function() {
         setAuthenticationStatus(false);
-        if (onSuccess) {
-          onSuccess();
+        if (onComplete) {
+          onComplete();
         }
       }, onFailure);
 
diff --git a/resources/static/dialog/test/qunit/pages/forgot_unit_test.js b/resources/static/dialog/test/qunit/pages/forgot_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..d9afb17b14c42cb90173d963237cb5514646d51b
--- /dev/null
+++ b/resources/static/dialog/test/qunit/pages/forgot_unit_test.js
@@ -0,0 +1,135 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery").then("/dialog/resources/network", "/dialog/resources/user", "/js/pages/forgot", function() {
+  "use strict";
+
+  var bid = BrowserID,
+      network = bid.Network,
+      user = bid.User,
+      xhrError = false,
+      CHECK_DELAY = 500;
+
+  var netMock = {
+    requestPasswordReset: function(email, origin, onComplete, onFailure) {
+      xhrError ? onFailure() : onComplete(email === "registered@testuser.com");
+    },
+
+    emailRegistered: function(email, onComplete, onFailure) {
+      xhrError ? onFailure() : onComplete(email === "registered@testuser.com");
+    }
+  };
+
+  module("pages/forgot", {
+    setup: function() {
+      user.setNetwork(netMock);
+      $(".error").stop().hide();
+      xhrError = false;
+      bid.forgot();
+    },
+    teardown: function() {
+      user.setNetwork(network);  
+      $(".error").stop().hide();
+      $(".website").text("");
+      bid.forgot.reset();
+    }
+  });
+
+  test("requestPasswordReset with invalid email", function() {
+    $("#email").val("invalid");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      equal($(".emailsent").is(":visible"), false, "email not sent");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+  });
+
+  test("requestPasswordReset with known email", function() {
+    $("#email").val("registered@testuser.com");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      ok($(".emailsent").is(":visible"), "email sent successfully");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+  });
+
+  test("requestPasswordReset with unknown email", function() {
+    $("#email").val("unregistered@testuser.com");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      equal($(".emailsent").is(":visible"), false, "email not sent");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+  });
+
+  test("requestPasswordReset with throttling", function() {
+    $("#email").val("throttled@testuser.com");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      equal($(".emailsent").is(":visible"), false, "email not sent");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+  });
+
+  test("requestPasswordReset with XHR Error", function() {
+    xhrError = true;
+
+    $("#email").val("testuser@testuser.com");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      equal($(".emailsent").is(":visible"), false, "email not sent");
+      equal($(".doh").is(":visible"), true, "XHR error message is displayed");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+
+  });
+
+});
diff --git a/resources/static/dialog/test/qunit/qunit.js b/resources/static/dialog/test/qunit/qunit.js
index 093568db7df89e758c8ecb875c6cf0b872acd0d4..a9c3269bcc65824a35015099d0f540069a3efef3 100644
--- a/resources/static/dialog/test/qunit/qunit.js
+++ b/resources/static/dialog/test/qunit/qunit.js
@@ -22,6 +22,7 @@ steal("/dialog/resources/browserid.js",
   .then("include_unit_test")
   .then("relay/relay_unit_test")
   .then("pages/add_email_address_test")
+  .then("pages/forgot_unit_test")
   .then("resources/tooltip_unit_test")
   .then("resources/channel_unit_test")
   .then("resources/browser-support_unit_test")
diff --git a/resources/static/dialog/test/qunit/resources/user_unit_test.js b/resources/static/dialog/test/qunit/resources/user_unit_test.js
index 7ef2dfd486e92752218cea80e5911dbc93705840..568d7d6aa73f93d9a23912d3a754d07c0970d293 100644
--- a/resources/static/dialog/test/qunit/resources/user_unit_test.js
+++ b/resources/static/dialog/test/qunit/resources/user_unit_test.js
@@ -57,6 +57,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       emailCheckCount = 0,
       registrationResponse,
       xhrFailure = false,
+      throttle = false,
       validToken = true; 
 
   var netStub = {
@@ -67,6 +68,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       userEmails = {"testuser@testuser.com": {}};
       registrationResponse = "complete";
       xhrFailure = false;
+      throttle = false;
     },
 
     checkUserRegistration: function(email, onSuccess, onFailure) {
@@ -153,7 +155,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     },
 
     requestPasswordReset: function(email, origin, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(true);
+      xhrFailure ? onFailure() : onSuccess(!throttle);
     },
 
     cancelUser: function(onSuccess, onFailure) {
@@ -431,10 +433,52 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
-  test("requestPasswordReset", function() {
-    lib.requestPasswordReset("address", function(reset) {
-      // XXX fill this in.
-      ok(true);
+  test("requestPasswordReset with known email", function() {
+    lib.requestPasswordReset("registered", function(status) {
+      equal(status.success, true, "password reset for known user");
+      start();
+    }, function() {
+      ok(false, "onFailure should not be called"); 
+      start();
+    });
+
+    stop();
+  });
+
+  test("requestPasswordReset with unknown email", function() {
+    lib.requestPasswordReset("unregistered", function(status) {
+      equal(status.success, false, "password not reset for unknown user");
+      equal(status.reason, "invalid_user", "invalid_user is the reason");
+      start();
+    }, function() {
+      ok(false, "onFailure should not be called"); 
+      start();
+    });
+
+    stop();
+  });
+
+  test("requestPasswordReset with throttle", function() {
+    throttle = true;
+    lib.requestPasswordReset("registered", function(status) {
+      equal(status.success, false, "password not reset for throttle");
+      equal(status.reason, "throttle", "password reset was throttled");
+      start();
+    }, function() {
+      ok(false, "onFailure should not be called"); 
+      start();
+    });
+
+    stop();
+  });
+
+  test("requestPasswordReset with XHR failure", function() {
+    xhrFailure = true;
+    lib.requestPasswordReset("address", function(status) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
       start();
     });
 
diff --git a/resources/static/js/pages/forgot.js b/resources/static/js/pages/forgot.js
index b329a1da7e10f9cf156e5d88c8b702cd0f64adea..32004aef4ed8602904b56fd38a62f18ff2abf114 100644
--- a/resources/static/js/pages/forgot.js
+++ b/resources/static/js/pages/forgot.js
@@ -38,38 +38,55 @@ BrowserID.forgot = (function() {
   "use strict";
 
   var bid = BrowserID,
-      pageHelpers = bid.PageHelpers;
+      user = bid.User,
+      pageHelpers = bid.PageHelpers,
+      tooltip = bid.Tooltip;
 
-  return function() {
-    $("form input[autofocus]").focus();
-
-    pageHelpers.setupEmail();
+  function submit(event) {
+    if (event) event.preventDefault();
 
-    $("#signUpForm").bind("submit", function(event) {
-      event.preventDefault();
-      $(".notifications .notification").hide();
+    // GET RID OF THIS HIDE CRAP AND USE CSS!
+    $(".notifications .notification").hide();
 
-      var email = $("#email").val(),
-          password = $("#password").val(),
-          vpassword = $("#vpassword").val();
+    var email = $("#email").val(),
+        valid = bid.Validation.email(email);
 
-      if (password != vpassword) {
-        $(".notifications .notification.mismatchpassword").fadeIn();
-        return false;
-      }
-
-      pageHelpers.clearStoredEmail();
-      bid.User.createUser(email, function onSuccess(keypair) {
-        $('#sent_to_email').html(email);
-        $('#forminputs').fadeOut();
-        $(".notifications .notification.emailsent").fadeIn();
+    if (valid) {
+      user.requestPasswordReset(email, function onSuccess(info) {
+        if (info.success) {
+          pageHelpers.clearStoredEmail();
+          $('#sent_to_email').html(email);
+          $('#forminputs').fadeOut();
+          $(".notifications .notification.emailsent").fadeIn();
+        }
+        else {
+          var tooltipEl = info.reason === "throttle" ? "#could_not_add" : "#not_registered";
+          tooltip.showTooltip(tooltipEl);
+        }
       }, function onFailure() {
-        // bad authentication
         $(".notifications .notification.doh").fadeIn();
       });
-    });
+    }
   };
 
+  function init() {
+    $("form input[autofocus]").focus();
+
+    pageHelpers.setupEmail();
+
+    $("#signUpForm").bind("submit", submit);
+  }
+
+  function reset() {
+    $("#signUpForm").unbind("submit", submit);
+  }
+
+
+  var forgot = init;
+  forgot.submit = submit; 
+  forgot.reset = reset;
+
+  return forgot;
 
 }());
 
diff --git a/resources/static/js/pages/verify_email_address.js b/resources/static/js/pages/verify_email_address.js
index 07642d077dc1639d25939d9e7510d3f6abc8a578..6c97f534739ff52494eaab9f051991526c3891ac 100644
--- a/resources/static/js/pages/verify_email_address.js
+++ b/resources/static/js/pages/verify_email_address.js
@@ -37,8 +37,7 @@
 (function() {
   "use strict";
 
-  var bid = BrowserID,
-      tooltip = bid.Tooltip;
+  var bid = BrowserID;
 
   function showError(el) {
     $(el).fadeIn(250);
diff --git a/resources/views/forgot.ejs b/resources/views/forgot.ejs
index 3e36ada4c89b785dcbbffb198fa1f9c8065de9e7..763420c42f8d2d04d8ee660550bb85a42fd0a2ab 100644
--- a/resources/views/forgot.ejs
+++ b/resources/views/forgot.ejs
@@ -1,7 +1,7 @@
 <div id="vAlign" class="display_always">   
     <div id="signUpFormWrap">
         <!-- XXX this form submits to nowhere -->
-        <form id="signUpForm" class="cf authform">
+        <form id="signUpForm" class="cf authform" novalidate>
             <h1 class="serif">Forgot Password</h1>
             <div class="notifications">
                 <div class="notification error doh">Doh! Something went wrong :-( </div>
@@ -13,7 +13,25 @@
                 <li>
                     <label class="serif" for="email">Email Address</label>
                     <input class="sans" id="email" autofocus required placeholder="Your Email" type="email" autocapitalize="off" autocorrect="off" maxlength="254" />
+
+                    <div id="email_format" class="tooltip" for="email">
+                      This field must be an email address.
+                    </div>
+
+                    <div id="email_required" class="tooltip" for="email">
+                      The email field is required.
+                    </div>
+
+                    <div id="could_not_add" class="tooltip" for="email">
+                      We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+                    </div>
+
+                    <div id="not_registered" class="tooltip" for="email">
+                      Non existent user!
+                    </div>
                 </li>
+
+
             </ul>
             <div class="submit cf">
                 <div class="remember cf">