diff --git a/src/PasswordReset.js b/src/PasswordReset.js
new file mode 100644
index 0000000000000000000000000000000000000000..1029b07b709a0214788abb971be9b366e3822fce
--- /dev/null
+++ b/src/PasswordReset.js
@@ -0,0 +1,104 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+var Matrix = require("matrix-js-sdk");
+
+/**
+ * Allows a user to reset their password on a homeserver.
+ *
+ * This involves getting an email token from the identity server to "prove" that
+ * the client owns the given email address, which is then passed to the password
+ * API on the homeserver in question with the new password.
+ */
+class PasswordReset {
+
+    /**
+     * Configure the endpoints for password resetting.
+     * @param {string} homeserverUrl The URL to the HS which has the account to reset.
+     * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
+     */
+    constructor(homeserverUrl, identityUrl) {
+        this.client = Matrix.createClient({
+            baseUrl: homeserverUrl,
+            idBaseUrl: identityUrl
+        });
+        this.clientSecret = generateClientSecret();
+        this.identityServerDomain = identityUrl.split("://")[1];
+    }
+
+    /**
+     * Attempt to reset the user's password. This will trigger a side-effect of
+     * sending an email to the provided email address.
+     * @param {string} emailAddress The email address
+     * @param {string} newPassword The new password for the account.
+     * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
+     */
+    resetPassword(emailAddress, newPassword) {
+        this.password = newPassword;
+        return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
+            this.sessionId = res.sid;
+            return res;
+        }, function(err) {
+            if (err.httpStatus) {
+                err.message = err.message + ` (Status ${err.httpStatus})`;
+            }
+            throw err;
+        });
+    }
+
+    /**
+     * Checks if the email link has been clicked by attempting to change the password
+     * for the mxid linked to the email.
+     * @return {Promise} Resolves if the password was reset. Rejects with an object
+     * with a "message" property which contains a human-readable message detailing why
+     * the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
+     */
+    checkEmailLinkClicked() {
+        return this.client.setPassword({
+            type: "m.login.email.identity",
+            threepid_creds: {
+                sid: this.sessionId,
+                client_secret: this.clientSecret,
+                id_server: this.identityServerDomain
+            }
+        }, this.password).catch(function(err) {
+            if (err.httpStatus === 401) {
+                err.message = "Failed to verify email address: make sure you clicked the link in the email";
+            }
+            else if (err.httpStatus === 404) {
+                err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver.";
+            }
+            else if (err.httpStatus) {
+                err.message += ` (Status ${err.httpStatus})`;
+            }
+            throw err;
+        });
+    }
+}
+
+// from Angular SDK
+function generateClientSecret() {
+    var ret = "";
+    var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+    for (var i = 0; i < 32; i++) {
+        ret += chars.charAt(Math.floor(Math.random() * chars.length));
+    }
+
+    return ret;
+}
+
+module.exports = PasswordReset;
diff --git a/src/component-index.js b/src/component-index.js
index 0c08d70b7383c36d83cc7fa1b9d147ed7875021a..ac9a83346ae5d4a9ce77b751051f393736994390 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -23,14 +23,15 @@ limitations under the License.
 
 module.exports.components = {};
 module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
+module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword');
+module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
+module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
+module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
 module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
 module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
 module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
 module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
 module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
-module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
-module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
-module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
 module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
 module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar');
 module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
@@ -51,10 +52,10 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie
 module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin');
 module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm');
 module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig');
+module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
 module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody');
 module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
 module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody');
-module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
 module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
 module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
 module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 320dad09b3df8c8db194ac1489a62909594099b7..ede2ab8f46aaafa46029d55946288d1963744eec 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -233,6 +233,13 @@ module.exports = React.createClass({
                 });
                 this.notifyNewScreen('register');
                 break;
+            case 'start_password_recovery':
+                if (this.state.logged_in) return;
+                this.replaceState({
+                    screen: 'forgot_password'
+                });
+                this.notifyNewScreen('forgot_password');
+                break;
             case 'token_login':
                 if (this.state.logged_in) return;
 
@@ -559,6 +566,11 @@ module.exports = React.createClass({
                 action: 'token_login',
                 params: params
             });
+        } else if (screen == 'forgot_password') {
+            dis.dispatch({
+                action: 'start_password_recovery',
+                params: params
+            });
         } else if (screen == 'new') {
             dis.dispatch({
                 action: 'view_create_room',
@@ -668,6 +680,10 @@ module.exports = React.createClass({
         this.showScreen("login");
     },
 
+    onForgotPasswordClick: function() {
+        this.showScreen("forgot_password");
+    },
+
     onRegistered: function(credentials) {
         this.onLoggedIn(credentials);
         // do post-registration stuff
@@ -706,6 +722,7 @@ module.exports = React.createClass({
         var CreateRoom = sdk.getComponent('structures.CreateRoom');
         var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
         var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
+        var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
 
         // needs to be before normal PageTypes as you are logged in technically
         if (this.state.screen == 'post_registration') {
@@ -801,13 +818,21 @@ module.exports = React.createClass({
                     onLoggedIn={this.onRegistered}
                     onLoginClick={this.onLoginClick} />
             );
+        } else if (this.state.screen == 'forgot_password') {
+            return (
+                <ForgotPassword
+                    homeserverUrl={this.props.config.default_hs_url}
+                    identityServerUrl={this.props.config.default_is_url}
+                    onComplete={this.onLoginClick} />
+            );
         } else {
             return (
                 <Login
                     onLoggedIn={this.onLoggedIn}
                     onRegisterClick={this.onRegisterClick}
                     homeserverUrl={this.props.config.default_hs_url}
-                    identityServerUrl={this.props.config.default_is_url} />
+                    identityServerUrl={this.props.config.default_is_url}
+                    onForgotPasswordClick={this.onForgotPasswordClick} />
             );
         }
     }
diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js
new file mode 100644
index 0000000000000000000000000000000000000000..dcf6a7c28e00ca747d59e8bfe1c73d402f2793d5
--- /dev/null
+++ b/src/components/structures/login/ForgotPassword.js
@@ -0,0 +1,199 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+var React = require('react');
+var sdk = require('../../../index');
+var Modal = require("../../../Modal");
+var MatrixClientPeg = require('../../../MatrixClientPeg');
+
+var PasswordReset = require("../../../PasswordReset");
+
+module.exports = React.createClass({
+    displayName: 'ForgotPassword',
+
+    propTypes: {
+        homeserverUrl: React.PropTypes.string,
+        identityServerUrl: React.PropTypes.string,
+        onComplete: React.PropTypes.func.isRequired
+    },
+
+    getInitialState: function() {
+        return {
+            enteredHomeserverUrl: this.props.homeserverUrl,
+            enteredIdentityServerUrl: this.props.identityServerUrl,
+            progress: null
+        };
+    },
+
+    submitPasswordReset: function(hsUrl, identityUrl, email, password) {
+        this.setState({
+            progress: "sending_email"
+        });
+        this.reset = new PasswordReset(hsUrl, identityUrl);
+        this.reset.resetPassword(email, password).done(() => {
+            this.setState({
+                progress: "sent_email"
+            });
+        }, (err) => {
+            this.showErrorDialog("Failed to send email: " + err.message);
+            this.setState({
+                progress: null
+            });
+        })
+    },
+
+    onVerify: function(ev) {
+        ev.preventDefault();
+        if (!this.reset) {
+            console.error("onVerify called before submitPasswordReset!");
+            return;
+        }
+        this.reset.checkEmailLinkClicked().done((res) => {
+            this.setState({ progress: "complete" });
+        }, (err) => {
+            this.showErrorDialog(err.message);
+        })
+    },
+
+    onSubmitForm: function(ev) {
+        ev.preventDefault();
+
+        if (!this.state.email) {
+            this.showErrorDialog("The email address linked to your account must be entered.");
+        }
+        else if (!this.state.password || !this.state.password2) {
+            this.showErrorDialog("A new password must be entered.");
+        }
+        else if (this.state.password !== this.state.password2) {
+            this.showErrorDialog("New passwords must match each other.");
+        }
+        else {
+            this.submitPasswordReset(
+                this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
+                this.state.email, this.state.password
+            );
+        }
+    },
+
+    onInputChanged: function(stateKey, ev) {
+        this.setState({
+            [stateKey]: ev.target.value
+        });
+    },
+
+    onHsUrlChanged: function(newHsUrl) {
+        this.setState({
+            enteredHomeserverUrl: newHsUrl
+        });
+    },
+
+    onIsUrlChanged: function(newIsUrl) {
+        this.setState({
+            enteredIdentityServerUrl: newIsUrl
+        });
+    },
+
+    showErrorDialog: function(body, title) {
+        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+        Modal.createDialog(ErrorDialog, {
+            title: title,
+            description: body
+        });
+    },
+
+    render: function() {
+        var LoginHeader = sdk.getComponent("login.LoginHeader");
+        var LoginFooter = sdk.getComponent("login.LoginFooter");
+        var ServerConfig = sdk.getComponent("login.ServerConfig");
+        var Spinner = sdk.getComponent("elements.Spinner");
+
+        var resetPasswordJsx;
+
+        if (this.state.progress === "sending_email") {
+            resetPasswordJsx = <Spinner />
+        }
+        else if (this.state.progress === "sent_email") {
+            resetPasswordJsx = (
+                <div>
+                    An email has been sent to {this.state.email}. Once you&#39;ve followed
+                    the link it contains, click below.
+                    <br />
+                    <input className="mx_Login_submit" type="button" onClick={this.onVerify}
+                        value="I have verified my email address" />
+                </div>
+            );
+        }
+        else if (this.state.progress === "complete") {
+            resetPasswordJsx = (
+                <div>
+                    <p>Your password has been reset.</p>
+                    <p>You have been logged out of all devices and will no longer receive push notifications.
+                    To re-enable notifications, re-log in on each device.</p>
+                    <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
+                        value="Return to login screen" />
+                </div>
+            );
+        }
+        else {
+            resetPasswordJsx = (
+            <div>
+                To reset your password, enter the email address linked to your account:
+                <br />
+                <div>
+                    <form onSubmit={this.onSubmitForm}>
+                        <input className="mx_Login_field" ref="user" type="text"
+                            value={this.state.email}
+                            onChange={this.onInputChanged.bind(this, "email")}
+                            placeholder="Email address" autoFocus />
+                        <br />
+                        <input className="mx_Login_field" ref="pass" type="password"
+                            value={this.state.password}
+                            onChange={this.onInputChanged.bind(this, "password")}
+                            placeholder="New password" />
+                        <br />
+                        <input className="mx_Login_field" ref="pass" type="password"
+                            value={this.state.password2}
+                            onChange={this.onInputChanged.bind(this, "password2")}
+                            placeholder="Confirm your new password" />
+                        <br />
+                        <input className="mx_Login_submit" type="submit" value="Send Reset Email" />
+                    </form>
+                    <ServerConfig ref="serverConfig"
+                        withToggleButton={true}
+                        defaultHsUrl={this.props.homeserverUrl}
+                        defaultIsUrl={this.props.identityServerUrl}
+                        onHsUrlChanged={this.onHsUrlChanged}
+                        onIsUrlChanged={this.onIsUrlChanged}
+                        delayTimeMs={0}/>
+                    <LoginFooter />
+                </div>
+            </div>
+            );
+        }
+
+
+        return (
+            <div className="mx_Login">
+                <div className="mx_Login_box">
+                    <LoginHeader />
+                    {resetPasswordJsx}
+                </div>
+            </div>
+        );
+    }
+});
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index b7d2d762a472913021681c6fe5fad52ca932f514..b853b8fd95cc8da920b07a18626e530814221e4b 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -33,7 +33,9 @@ module.exports = React.createClass({displayName: 'Login',
         homeserverUrl: React.PropTypes.string,
         identityServerUrl: React.PropTypes.string,
         // login shouldn't know or care how registration is done.
-        onRegisterClick: React.PropTypes.func.isRequired
+        onRegisterClick: React.PropTypes.func.isRequired,
+        // login shouldn't care how password recovery is done.
+        onForgotPasswordClick: React.PropTypes.func
     },
 
     getDefaultProps: function() {
@@ -138,7 +140,9 @@ module.exports = React.createClass({displayName: 'Login',
         switch (step) {
             case 'm.login.password':
                 return (
-                    <PasswordLogin onSubmit={this.onPasswordLogin} />
+                    <PasswordLogin
+                        onSubmit={this.onPasswordLogin}
+                        onForgotPasswordClick={this.props.onForgotPasswordClick} />
                 );
             case 'm.login.cas':
                 return (
diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js
index 3367ac32573e6258a0689042159f8e6ae2c4c901..a8751da1a78b94330fd1006797dd919039bdc062 100644
--- a/src/components/views/login/PasswordLogin.js
+++ b/src/components/views/login/PasswordLogin.js
@@ -22,7 +22,8 @@ var ReactDOM = require('react-dom');
  */
 module.exports = React.createClass({displayName: 'PasswordLogin',
     propTypes: {
-        onSubmit: React.PropTypes.func.isRequired // fn(username, password)
+        onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
+        onForgotPasswordClick: React.PropTypes.func // fn()
     },
 
     getInitialState: function() {
@@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
     },
 
     render: function() {
+        var forgotPasswordJsx;
+
+        if (this.props.onForgotPasswordClick) {
+            forgotPasswordJsx = (
+                <a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
+                    Forgot your password?
+                </a>
+            );
+        }
+
         return (
             <div>
                 <form onSubmit={this.onSubmitForm}>
@@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
                     value={this.state.password} onChange={this.onPasswordChanged}
                     placeholder="Password" />
                 <br />
+                {forgotPasswordJsx}
                 <input className="mx_Login_submit" type="submit" value="Log in" />
                 </form>
             </div>