diff --git a/reviewboard/accounts/forms/pages.py b/reviewboard/accounts/forms/pages.py
index 70923ae86e071479e93c7dd1da2df08692001711..370164fd358be0b54cf000d0ef45db041818f2a9 100644
--- a/reviewboard/accounts/forms/pages.py
+++ b/reviewboard/accounts/forms/pages.py
@@ -15,6 +15,7 @@ from djblets.configforms.forms import ConfigPageForm
 
 from reviewboard.accounts.backends import get_enabled_auth_backends
 from reviewboard.avatars import avatar_services
+from reviewboard.notifications.email import mail_password_changed
 from reviewboard.reviews.models import Group
 from reviewboard.site.urlresolvers import local_site_reverse
 
@@ -258,6 +259,11 @@ class ChangePasswordForm(AccountPageForm):
                                    'password. Please contact the '
                                    'administrator.'))
 
+        siteconfig = SiteConfiguration.objects.get_current()
+
+        if siteconfig.get('mail_send_password_changed_mail'):
+            mail_password_changed(self.user)
+
 
 class ProfileForm(AccountPageForm):
     """Form for the Profile page for an account."""
diff --git a/reviewboard/accounts/urls.py b/reviewboard/accounts/urls.py
index b391d3798e81675fc9ecf9f7f950bf412dac4129..c000a3e4bd7314cb0aed5012fcf42c95f4318a51 100644
--- a/reviewboard/accounts/urls.py
+++ b/reviewboard/accounts/urls.py
@@ -14,6 +14,9 @@ urlpatterns = patterns(
     url(r'^preferences/$',
         MyAccountView.as_view(),
         name="user-preferences"),
+    url(r'^preferences/preview-email/password-changed/$',
+        'preview_password_changed_email',
+        name='preview-password-change-email')
 )
 
 urlpatterns += patterns(
diff --git a/reviewboard/accounts/views.py b/reviewboard/accounts/views.py
index 96c42a275243fa027401fd4a6988286758d9481f..445a6114b7accf6739ad0110d04110c78843aa2f 100644
--- a/reviewboard/accounts/views.py
+++ b/reviewboard/accounts/views.py
@@ -1,8 +1,11 @@
 from __future__ import unicode_literals
 
 from django.contrib.auth.decorators import login_required
+from django.conf import settings
 from django.core.urlresolvers import reverse
-from django.http import HttpResponseRedirect
+from django.http import Http404, HttpResponse, HttpResponseRedirect
+from django.template.context import RequestContext
+from django.template.loader import render_to_string
 from django.utils.decorators import method_decorator
 from django.utils.functional import cached_property
 from django.utils.translation import ugettext_lazy as _
@@ -15,6 +18,8 @@ from djblets.util.decorators import augment_method_from
 from reviewboard.accounts.backends import get_enabled_auth_backends
 from reviewboard.accounts.forms.registration import RegistrationForm
 from reviewboard.accounts.pages import AccountPage
+from reviewboard.admin.server import build_server_url, get_server_url
+
 
 @csrf_protect
 def account_register(request, next_url='dashboard'):
@@ -81,3 +86,38 @@ class MyAccountView(ConfigPagesView):
     def ordered_user_local_sites(self):
         """Get the user's local sites, ordered by name."""
         return self.request.user.local_site.order_by('name')
+
+
+def preview_password_changed_email(
+    request,
+    text_template_name='notifications/password_changed.txt',
+    html_template_name='notifications/password_changed.html'):
+    if not settings.DEBUG:
+        raise Http404
+
+    format = request.GET.get('format', 'html')
+
+    if format == 'text':
+        template_name = text_template_name
+        mimetype = 'text/plain'
+    elif format == 'html':
+        template_name = html_template_name
+        mimetype = 'text/html'
+    else:
+        raise Http404
+
+    api_token_url = (
+        '%s#api-tokens'
+        % build_server_url(reverse('user-preferences'))
+    )
+
+    return HttpResponse(
+        render_to_string(
+            template_name,
+            RequestContext(request, {
+                'api_token_url': api_token_url,
+                'has_api_tokens': request.user.webapi_tokens.exists(),
+                'server_url': get_server_url(),
+                'user': request.user,
+            })),
+        content_type=mimetype)
diff --git a/reviewboard/admin/forms.py b/reviewboard/admin/forms.py
index 322ebaed34885fe3150ded3f49f37c2f3c18d591..56db8c68f74ce5d7b03d15b647dab39ff709b715 100644
--- a/reviewboard/admin/forms.py
+++ b/reviewboard/admin/forms.py
@@ -624,6 +624,9 @@ class EMailSettingsForm(SiteSettingsForm):
     mail_send_new_user_mail = forms.BooleanField(
         label=_("Send e-mails when new users register an account"),
         required=False)
+    mail_send_password_changed_mail = forms.BooleanField(
+        label=_('Send e-mails when a user changes their password'),
+        required=False)
     mail_enable_autogenerated_header = forms.BooleanField(
         label=_('Enable "Auto-Submitted: auto-generated" header'),
         help_text=_('Marks outgoing e-mails as "auto-generated" to avoid '
@@ -705,7 +708,8 @@ class EMailSettingsForm(SiteSettingsForm):
                 'title': _('E-Mail Notification Settings'),
                 'fields': ('mail_send_review_mail',
                            'mail_send_review_close_mail',
-                           'mail_send_new_user_mail'),
+                           'mail_send_new_user_mail',
+                           'mail_send_password_changed_mail'),
             },
             {
                 'classes': ('wide',),
diff --git a/reviewboard/admin/siteconfig.py b/reviewboard/admin/siteconfig.py
index e546d2f7a1d37098c2bd35789360c0e423479a05..91060becb4b129bad2dbd2d559d5fb701b4da4d7 100644
--- a/reviewboard/admin/siteconfig.py
+++ b/reviewboard/admin/siteconfig.py
@@ -151,6 +151,7 @@ defaults.update({
     'integration_gravatars': True,
     'mail_send_review_mail': False,
     'mail_send_new_user_mail': False,
+    'mail_send_password_changed_mail': False,
     'mail_enable_autogenerated_header': True,
     'search_enable': False,
     'send_support_usage_stats': True,
diff --git a/reviewboard/notifications/email.py b/reviewboard/notifications/email.py
index 69dc4b8a3536b676509d46b3d310520b9f91f4ef..8b4c23d924f5cf395d5c03bc9b59a72baa890c48 100644
--- a/reviewboard/notifications/email.py
+++ b/reviewboard/notifications/email.py
@@ -21,7 +21,7 @@ from djblets.siteconfig.models import SiteConfiguration
 from djblets.auth.signals import user_registered
 
 from reviewboard.accounts.models import ReviewRequestVisit
-from reviewboard.admin.server import get_server_url
+from reviewboard.admin.server import build_server_url, get_server_url
 from reviewboard.changedescs.models import ChangeDescription
 from reviewboard.reviews.models import Group, ReviewRequest, Review
 from reviewboard.reviews.signals import (review_request_published,
@@ -991,6 +991,47 @@ def mail_webapi_token(webapi_token, op):
                           subject, settings.SERVER_EMAIL, user_email, e)
 
 
+def mail_password_changed(user):
+    """Send an e-mail when a user's password changes.
+
+    Args:
+        user (django.contrib.auth.model.User):
+            The user whose password changed.
+    """
+    api_token_url = (
+        '%s#api-tokens'
+        % build_server_url(reverse('user-preferences'))
+    )
+    server_url = get_server_url()
+
+    context = {
+        'api_token_url': api_token_url,
+        'has_api_tokens': user.webapi_tokens.exists(),
+        'server_url': server_url,
+        'user': user,
+    }
+
+    user_email = build_email_address_for_user(user)
+    text_body = render_to_string('notifications/password_changed.txt', context)
+    html_body = render_to_string('notifications/password_changed.html',
+                                 context)
+
+    message = EmailMessage(
+        subject='Password changed for user "%s" on %s' % server_url,
+        text_body=text_body,
+        html_body=html_body,
+        from_email=settings.SERVER_EMAIL,
+        sender=settings.SERVER_EMAIL,
+        to=user_email,
+    )
+
+    try:
+        message.send()
+    except Exception as e:
+        logging.exception('Failed to send password changed email to %s: %s',
+                          user.username, e)
+
+
 def filter_email_recipients_from_hooks(to_field, cc_field, signal, **kwargs):
     """Filter the e-mail recipients through configured e-mail hooks.
 
diff --git a/reviewboard/templates/notifications/password_changed.html b/reviewboard/templates/notifications/password_changed.html
new file mode 100644
index 0000000000000000000000000000000000000000..06f64bee4df17b81ece29ca898bf412ee08ff0f4
--- /dev/null
+++ b/reviewboard/templates/notifications/password_changed.html
@@ -0,0 +1,27 @@
+{% load djblets_utils %}
+<html>
+ <body style="font-family: Verdana, Arial, Helvetica, Sans-Serif;">
+  <table bgcolor="#f9f3c9" width="100%" cellpadding="8" style="border: 1px #c9c399 solid;">
+   <tr>
+    <td>
+     This is an automatically generated e-mail.
+    </td>
+   </tr>
+  </table>
+
+  <p>Hi {{user|user_displayname}},</p>
+  <p>
+   Your password has been successfully changed on
+   <a href="{{server_url}}">{{server_url}}</a>.
+   If you did not change your password, please contact a server administrator
+   immediately.
+  </p>
+{% if has_api_tokens %}
+  <p>
+   You currently have API dokens. Changing your password does reset them. If
+   you wish to invalidate your API tokens, you must do that manually at
+   <a href="{{api_token_url}}">{{api_token_url}}</a>.
+  </p>
+{% endif %}
+ </body>
+</html>
diff --git a/reviewboard/templates/notifications/password_changed.txt b/reviewboard/templates/notifications/password_changed.txt
new file mode 100644
index 0000000000000000000000000000000000000000..db890bcebfecf713262095668b4dcf73554f1106
--- /dev/null
+++ b/reviewboard/templates/notifications/password_changed.txt
@@ -0,0 +1,17 @@
+{% autoescape off %}{% load djblets_email djblets_utils %}
+------------------------------------------
+This is an automatically generated e-mail.
+------------------------------------------
+
+Hi {{user|user_displayname}},
+
+Your password has been successfully changed on <{{server_url}}>.
+If you did not change your password, please contact a server administrator
+immediately.
+
+{% if has_api_tokens %}
+You currently have API tokens. Changing your password does not reset them. If
+you wish to invalidate your API tokens, you must do that manually at
+<{{api_token_url}}>.
+{% endif %}
+{% endautoescape %}
