From 3c20402b32f06a7a488e23df0b54811612d2d923 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 5 Sep 2014 19:57:33 -0400 Subject: [PATCH] Add a common base email template, translate the emails over to using jinja and add emails when e-mail addresses and passwords are changed. --- data/model/legacy.py | 4 +- emails/base.html | 46 ++++++++ emails/changeemail.html | 13 +++ emails/confirmemail.html | 13 +++ emails/emailchanged.html | 12 ++ emails/passwordchanged.html | 12 ++ emails/paymentfailure.html | 13 +++ emails/recovery.html | 18 +++ emails/repoauthorizeemail.html | 13 +++ endpoints/api/user.py | 3 +- endpoints/web.py | 6 +- util/useremails.py | 194 ++++++++++++++++++--------------- 12 files changed, 258 insertions(+), 89 deletions(-) create mode 100644 emails/base.html create mode 100644 emails/changeemail.html create mode 100644 emails/confirmemail.html create mode 100644 emails/emailchanged.html create mode 100644 emails/passwordchanged.html create mode 100644 emails/paymentfailure.html create mode 100644 emails/recovery.html create mode 100644 emails/repoauthorizeemail.html diff --git a/data/model/legacy.py b/data/model/legacy.py index 64bcdc860..bc49a585e 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -456,18 +456,20 @@ def confirm_user_email(code): user = code.user user.verified = True + old_email = None new_email = code.new_email if new_email: if find_user_by_email(new_email): raise DataModelException('E-mail address already used.') + old_email = user.email user.email = new_email user.save() code.delete_instance() - return user, new_email + return user, new_email, old_email def create_reset_password_email_code(email): diff --git a/emails/base.html b/emails/base.html new file mode 100644 index 000000000..286f83c98 --- /dev/null +++ b/emails/base.html @@ -0,0 +1,46 @@ + + + + + + {{ subject }} + + + + + + + +
+ +
+ + +
{{ app_title }}
+ +
+ + +
+ +
+ +
+ {% block content %}{% endblock %} +
+ +
+ + diff --git a/emails/changeemail.html b/emails/changeemail.html new file mode 100644 index 000000000..d247bdc2d --- /dev/null +++ b/emails/changeemail.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} + +

E-mail Address Change Requested

+ +This email address was recently asked to become the new e-mail address for user {{ username | user_reference }}. +
+
+To confirm this email address, please click the following link:
+{{ app_link('confirm?code=' + token) }} + +{% endblock %} diff --git a/emails/confirmemail.html b/emails/confirmemail.html new file mode 100644 index 000000000..de94372cd --- /dev/null +++ b/emails/confirmemail.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} + +

Please Confirm E-mail Address

+ +This email address was recently used to register user {{ username | user_reference }}. +
+
+To confirm this email address, please click the following link:
+{{ app_link('confirm?code=' + token) }} + +{% endblock %} diff --git a/emails/emailchanged.html b/emails/emailchanged.html new file mode 100644 index 000000000..ce6de5565 --- /dev/null +++ b/emails/emailchanged.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block content %} + +

Account E-mail Address Changed

+ +The email address for user {{ username | user_reference }} has been changed from this e-mail address to {{ new_email }}. +
+
+If this change was not expected, please immediately log into your {{ username | admin_reference }} and reset your email address. + +{% endblock %} diff --git a/emails/passwordchanged.html b/emails/passwordchanged.html new file mode 100644 index 000000000..e3fd554e4 --- /dev/null +++ b/emails/passwordchanged.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block content %} + +

Account Password Changed

+ +The password for user {{ username | user_reference }} has been updated. +
+
+If this change was not expected, please immediately log into your {{ username | admin_reference }} and reset your password. + +{% endblock %} diff --git a/emails/paymentfailure.html b/emails/paymentfailure.html new file mode 100644 index 000000000..790f590b4 --- /dev/null +++ b/emails/paymentfailure.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} + +

Subscription Payment Failure

+ +Your recent payment for account {{ username | user_reference }} failed, which usually results in our payments processor canceling +your subscription automatically. If you would like to continue to use {{ app_title }} without interruption, +please add a new card to {{ app_title }} and re-subscribe to your plan.
+
+You can find the card and subscription management features under your {{ username | admin_reference }}
+ +{% endblock %} diff --git a/emails/recovery.html b/emails/recovery.html new file mode 100644 index 000000000..6f0267e39 --- /dev/null +++ b/emails/recovery.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block content %} + +

Account recovery

+ +A user at {{ app_link() }} has attempted to recover their account +using this email address. +
+
+If you made this request, please click the following link to recover your account and +change your password: +{{ app_link('recovery?code=' + token) }} +

+If you did not make this request, your account has not been compromised and the user was +not given access. Please disregard this email. + +{% endblock %} diff --git a/emails/repoauthorizeemail.html b/emails/repoauthorizeemail.html new file mode 100644 index 000000000..7ae33975c --- /dev/null +++ b/emails/repoauthorizeemail.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} + +

Verify e-mail to receive repository notifications

+ +A request has been made to send notifications to this email address for repository {{ (namespace, repository) | repository_reference }} + +

+To verify this email address, please click the following link:
+{{ app_link('authrepoemail?code=' + token) }} + +{% endblock %} diff --git a/endpoints/api/user.py b/endpoints/api/user.py index ddf05aafa..4a5df20ee 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -19,7 +19,7 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository from auth.auth_context import get_authenticated_user from auth import scopes from util.gravatar import compute_hash -from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email) +from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email, send_password_changed) import features @@ -165,6 +165,7 @@ class User(ApiResource): logger.debug('Changing password for user: %s', user.username) log_action('account_change_password', user.username) model.change_password(user, user_data['password']) + send_password_changed(user.username, user.email) if 'invoice_email' in user_data: logger.debug('Changing invoice_email for user: %s', user.username) diff --git a/endpoints/web.py b/endpoints/web.py index 19f9bb7f1..c538e703d 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -18,6 +18,7 @@ from endpoints.common import common_login, render_page_template, route_show_if, from endpoints.csrf import csrf_protect, generate_csrf_token from util.names import parse_repository_name from util.gravatar import compute_hash +from util.useremails import send_email_changed from auth import scopes import features @@ -241,10 +242,13 @@ def confirm_email(): new_email = None try: - user, new_email = model.confirm_user_email(code) + user, new_email, old_email = model.confirm_user_email(code) except model.DataModelException as ex: return render_page_template('confirmerror.html', error_message=ex.message) + if new_email: + send_email_changed(user.username, old_email, new_email) + common_login(user) return redirect(url_for('web.user', tab='email') diff --git a/util/useremails.py b/util/useremails.py index 0a78da7e3..f280e276b 100644 --- a/util/useremails.py +++ b/util/useremails.py @@ -1,116 +1,143 @@ from flask.ext.mail import Message from app import mail, app, get_app_url +from jinja2 import Template, Environment, FileSystemLoader, contextfilter +from data import model +from util.gravatar import compute_hash + +def user_reference(username): + user = model.get_user(username) + if not user: + return username + + return """ + + + %s + """ % (compute_hash(user.email), username) -CONFIRM_MESSAGE = """ -This email address was recently used to register the username '%s' -at Quay.io.
-
-To confirm this email address, please click the following link:
-%s/confirm?code=%s -""" +def repository_reference(pair): + (namespace, repository) = pair + + owner = model.get_user(namespace) + if not owner: + return "%s/%s" % (namespace, repository) + + return """ + + + %s/%s + + """ % (compute_hash(owner.email), get_app_url(), namespace, repository, namespace, repository) -CHANGE_MESSAGE = """ -This email address was recently asked to become the new e-mail address for username '%s' -at Quay.io.
-
-To confirm this email address, please click the following link:
-%s/confirm?code=%s -""" +def admin_reference(username): + user = model.get_user(username) + if not user: + return 'account settings' + + if user.organization: + return """ + organization's admin setting + """ % (get_app_url(), username) + else: + return """ + account settings + """ % (get_app_url()) -RECOVERY_MESSAGE = """ -A user at Quay.io has attempted to recover their account -using this email address.
-
-If you made this request, please click the following link to recover your account and -change your password: -%s/recovery?code=%s
-
-If you did not make this request, your account has not been compromised and the user was -not given access. Please disregard this email.
-""" +template_loader = FileSystemLoader(searchpath="emails") +template_env = Environment(loader=template_loader) +template_env.filters['user_reference'] = user_reference +template_env.filters['admin_reference'] = admin_reference +template_env.filters['repository_reference'] = repository_reference -SUBSCRIPTION_CHANGE = """ -Change: {0}
-Customer id: {1}
-Customer email: {2}
-Quay user or org name: {3}
-""" +def send_email(recipient, subject, template_file, parameters): + app_title = app.config['REGISTRY_TITLE_SHORT'] + app_url = get_app_url() + + def app_link_handler(url=None, title=None): + real_url = app_url + '/' + url if url else app_url + if not title: + title = real_url if url else app_title + + return '%s' % (real_url, title) + + parameters.update({ + 'subject': subject, + 'app_logo': 'https://quay.io/static/img/quay-logo.png', # TODO: make this pull from config + 'app_url': app_url, + 'app_title': app_title, + 'app_link': app_link_handler + }) + + rendered_html = template_env.get_template(template_file + '.html').render(parameters) + + msg = Message('[%s] %s' % (app_title, subject), sender='support@quay.io', recipients=[recipient]) + msg.html = rendered_html + mail.send(msg) -PAYMENT_FAILED = """ -Hi {0},
-
-Your recent payment for Quay.io failed, which usually results in our payments processorcanceling -your subscription automatically. If you would like to continue to use Quay.io without interruption, -please add a new card to Quay.io and re-subscribe to your plan.
-
-You can find the card and subscription management features under your account settings.
-
-Thanks and have a great day!
-
--Quay.io Support
-""" - - -AUTH_FORREPO_MESSAGE = """ -A request has been made to send notifications to this email address for the -Quay.io repository %s/%s. -
-To confirm this email address, please click the following link:
-%s/authrepoemail?code=%s -""" - - -SUBSCRIPTION_CHANGE_TITLE = 'Subscription Change - {0} {1}' +def send_password_changed(username, email): + send_email(email, 'Account password changed', 'passwordchanged', { + 'username': username + }) +def send_email_changed(username, old_email, new_email): + send_email(old_email, 'Account e-mail address changed', 'emailchanged', { + 'username': username, + 'new_email': new_email + }) def send_change_email(username, email, token): - msg = Message('Quay.io email change. Please confirm your email.', - sender='support@quay.io', # Why do I need this? - recipients=[email]) - msg.html = CHANGE_MESSAGE % (username, get_app_url(), get_app_url(), token, get_app_url(), token) - mail.send(msg) - + send_email(email, 'E-mail address change requested', 'changeemail', { + 'username': username, + 'token': token + }) def send_confirmation_email(username, email, token): - msg = Message('Welcome to Quay.io! Please confirm your email.', - sender='support@quay.io', # Why do I need this? - recipients=[email]) - msg.html = CONFIRM_MESSAGE % (username, get_app_url(), get_app_url(), token, get_app_url(), token) - mail.send(msg) - + send_email(email, 'Please confirm your e-mail address', 'confirmemail', { + 'username': username, + 'token': token + }) def send_repo_authorization_email(namespace, repository, email, token): - msg = Message('Quay.io Notification: Please confirm your email.', - sender='support@quay.io', # Why do I need this? - recipients=[email]) - msg.html = AUTH_FORREPO_MESSAGE % (get_app_url(), get_app_url(), namespace, repository, namespace, - repository, get_app_url(), token, get_app_url(), token) - mail.send(msg) - + subject = 'Please verify your e-mail address for repository %s/%s' % (namespace, repository) + send_email(email, subject, 'repoauthorizeemail', { + 'namespace': namespace, + 'repository': repository, + 'token': token + }) def send_recovery_email(email, token): - msg = Message('Quay.io account recovery.', - sender='support@quay.io', # Why do I need this? - recipients=[email]) - msg.html = RECOVERY_MESSAGE % (get_app_url(), get_app_url(), token, get_app_url(), token) - mail.send(msg) + subject = 'Account recovery' + send_email(email, subject, 'recovery', { + 'email': email, + 'token': token + }) + +def send_payment_failed(email, username): + send_email(email, 'Subscription Payment Failure', 'paymentfailure', { + 'username': username + }) def send_invoice_email(email, contents): + # Note: This completely generates the contents of the email, so we don't use the + # normal template here. msg = Message('Quay.io payment received - Thank you!', - sender='support@quay.io', # Why do I need this? + sender='support@quay.io', recipients=[email]) msg.html = contents mail.send(msg) +# INTERNAL EMAILS BELOW + def send_subscription_change(change_description, customer_id, customer_email, quay_username): + SUBSCRIPTION_CHANGE_TITLE = 'Subscription Change - {0} {1}' title = SUBSCRIPTION_CHANGE_TITLE.format(quay_username, change_description) msg = Message(title, sender='support@quay.io', recipients=['stripe@quay.io']) msg.html = SUBSCRIPTION_CHANGE.format(change_description, customer_id, customer_email, @@ -118,8 +145,3 @@ def send_subscription_change(change_description, customer_id, customer_email, qu mail.send(msg) -def send_payment_failed(customer_email, quay_username): - msg = Message('Quay.io Subscription Payment Failure', sender='support@quay.io', - recipients=[customer_email]) - msg.html = PAYMENT_FAILED.format(quay_username) - mail.send(msg)