diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 5a94194c..5cb2acba 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -246,6 +246,7 @@ const getNodeInfo = async ({ store }) => {
       store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
       store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
       store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
+      store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
 
       store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
       store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 7dc4b2a5..cd02711c 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -9,6 +9,7 @@ import UserProfile from 'components/user_profile/user_profile.vue'
 import Search from 'components/search/search.vue'
 import Settings from 'components/settings/settings.vue'
 import Registration from 'components/registration/registration.vue'
+import PasswordReset from 'components/password_reset/password_reset.vue'
 import UserSettings from 'components/user_settings/user_settings.vue'
 import FollowRequests from 'components/follow_requests/follow_requests.vue'
 import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
@@ -46,6 +47,7 @@ export default (store) => {
     { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
     { name: 'settings', path: '/settings', component: Settings },
     { name: 'registration', path: '/registration', component: Registration },
+    { name: 'password-reset', path: '/password-reset', component: PasswordReset },
     { name: 'registration-token', path: '/registration/:token', component: Registration },
     { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
     { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index 3ec7fe0c..b4fdcefb 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -33,6 +33,11 @@
               type="password"
             >
           </div>
+          <div class="form-group">
+            <router-link :to="{name: 'password-reset'}">
+              {{ $t('password_reset.forgot_password') }}
+            </router-link>
+          </div>
         </template>
 
         <div
diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js
new file mode 100644
index 00000000..fa71e07a
--- /dev/null
+++ b/src/components/password_reset/password_reset.js
@@ -0,0 +1,62 @@
+import { mapState } from 'vuex'
+import passwordResetApi from '../../services/new_api/password_reset.js'
+
+const passwordReset = {
+  data: () => ({
+    user: {
+      email: ''
+    },
+    isPending: false,
+    success: false,
+    throttled: false,
+    error: null
+  }),
+  computed: {
+    ...mapState({
+      signedIn: (state) => !!state.users.currentUser,
+      instance: state => state.instance
+    }),
+    mailerEnabled () {
+      return this.instance.mailerEnabled
+    }
+  },
+  created () {
+    if (this.signedIn) {
+      this.$router.push({ name: 'root' })
+    }
+  },
+  methods: {
+    dismissError () {
+      this.error = null
+    },
+    submit () {
+      this.isPending = true
+      const email = this.user.email
+      const instance = this.instance.server
+
+      passwordResetApi({ instance, email }).then(({ status }) => {
+        this.isPending = false
+        this.user.email = ''
+
+        if (status === 204) {
+          this.success = true
+          this.error = null
+        } else if (status === 404 || status === 400) {
+          this.error = this.$t('password_reset.not_found')
+          this.$nextTick(() => {
+            this.$refs.email.focus()
+          })
+        } else if (status === 429) {
+          this.throttled = true
+          this.error = this.$t('password_reset.too_many_requests')
+        }
+      }).catch(() => {
+        this.isPending = false
+        this.user.email = ''
+        this.error = this.$t('general.generic_error')
+      })
+    }
+  }
+}
+
+export default passwordReset
diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue
new file mode 100644
index 00000000..00474e95
--- /dev/null
+++ b/src/components/password_reset/password_reset.vue
@@ -0,0 +1,116 @@
+<template>
+  <div class="settings panel panel-default">
+    <div class="panel-heading">
+      {{ $t('password_reset.password_reset') }}
+    </div>
+    <div class="panel-body">
+      <form
+        class="password-reset-form"
+        @submit.prevent="submit"
+      >
+        <div class="container">
+          <div v-if="!mailerEnabled">
+            <p>
+              {{ $t('password_reset.password_reset_disabled') }}
+            </p>
+          </div>
+          <div v-else-if="success || throttled">
+            <p v-if="success">
+              {{ $t('password_reset.check_email') }}
+            </p>
+            <div class="form-group text-center">
+              <router-link :to="{name: 'root'}">
+                {{ $t('password_reset.return_home') }}
+              </router-link>
+            </div>
+          </div>
+          <div v-else>
+            <p>
+              {{ $t('password_reset.instruction') }}
+            </p>
+            <div class="form-group">
+              <input
+                ref="email"
+                v-model="user.email"
+                :disabled="isPending"
+                :placeholder="$t('password_reset.placeholder')"
+                class="form-control"
+                type="input"
+              >
+            </div>
+            <div class="form-group">
+              <button
+                :disabled="isPending"
+                type="submit"
+                class="btn btn-default btn-block"
+              >
+                {{ $t('general.submit') }}
+              </button>
+            </div>
+          </div>
+          <p
+            v-if="error"
+            class="alert error notice-dismissible"
+          >
+            <span>{{ error }}</span>
+            <a
+              class="button-icon dismiss"
+              @click.prevent="dismissError()"
+            >
+              <i class="icon-cancel" />
+            </a>
+          </p>
+        </div>
+      </form>
+    </div>
+  </div>
+</template>
+
+<script src="./password_reset.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.password-reset-form {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 0.6em;
+
+  .container {
+    display: flex;
+    flex: 1 0;
+    flex-direction: column;
+    margin-top: 0.6em;
+    max-width: 18rem;
+  }
+
+  .form-group {
+    display: flex;
+    flex-direction: column;
+    margin-bottom: 1em;
+    padding: 0.3em 0.0em 0.3em;
+    line-height: 24px;
+  }
+
+  .error {
+    text-align: center;
+    animation-name: shakeError;
+    animation-duration: 0.4s;
+    animation-timing-function: ease-in-out;
+  }
+
+  .alert {
+    padding: 0.5em;
+    margin: 0.3em 0.0em 1em;
+  }
+
+  .notice-dismissible {
+    padding-right: 2rem;
+  }
+
+  .icon-cancel {
+    cursor: pointer;
+  }
+}
+
+</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6a9af55c..ddde471a 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -608,5 +608,16 @@
     "person_talking": "{count} person talking",
     "people_talking": "{count} people talking",
     "no_results": "No results"
+  },
+  "password_reset": {
+    "forgot_password": "Forgot password?",
+    "password_reset": "Password reset",
+    "instruction": "Enter your email address or username. We will send you a link to reset your password.",
+    "placeholder": "Your email or username",
+    "check_email": "Check your email for a link to reset your password.",
+    "return_home": "Return to the home page",
+    "not_found": "We couldn't find that email or username.",
+    "too_many_requests": "You have reached the limit of attempts, try again later.",
+    "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator."
   }
 }
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 90ed6664..3af65f40 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -389,5 +389,16 @@
     "person_talking": "Популярно у {count} человека",
     "people_talking": "Популярно у {count} человек",
     "no_results": "Ничего не найдено"
+  },
+  "password_reset": {
+    "forgot_password": "Забыли пароль?",
+    "password_reset": "Сброс пароля",
+    "instruction": "Введите ваш email или имя пользователя, и мы отправим вам ссылку для сброса пароля.",
+    "placeholder": "Ваш email или имя пользователя",
+    "check_email": "Проверьте ваш email и перейдите по ссылке для сброса пароля.",
+    "return_home": "Вернуться на главную страницу",
+    "not_found": "Мы не смогли найти аккаунт с таким email-ом или именем пользователя.",
+    "too_many_requests": "Вы исчерпали допустимое количество попыток, попробуйте позже.",
+    "password_reset_disabled": "Сброс пароля отключен. Cвяжитесь с администратором вашего сервера."
   }
 }
diff --git a/src/services/new_api/password_reset.js b/src/services/new_api/password_reset.js
new file mode 100644
index 00000000..43199625
--- /dev/null
+++ b/src/services/new_api/password_reset.js
@@ -0,0 +1,18 @@
+import { reduce } from 'lodash'
+
+const MASTODON_PASSWORD_RESET_URL = `/auth/password`
+
+const resetPassword = ({ instance, email }) => {
+  const params = { email }
+  const query = reduce(params, (acc, v, k) => {
+    const encoded = `${k}=${encodeURIComponent(v)}`
+    return `${acc}&${encoded}`
+  }, '')
+  const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}`
+
+  return window.fetch(url, {
+    method: 'POST'
+  })
+}
+
+export default resetPassword