mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-04-27 03:00:12 -04:00
Orangered emails: one-click unsubscribe
Even if it's opt-in, we want people to be able to easily unsubscribe from notification emails. Using an HMAC instead of a generated token means we don't have to store anything extra, but just perform a calculation on email send and in the unsubscribe responder.
This commit is contained in:
@@ -25,6 +25,8 @@ media_embed = YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5
|
||||
comment_embed = YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5
|
||||
# secret for authenticating controller#action name
|
||||
action_name = YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5
|
||||
# secret for email notification one-click unsubscribe links
|
||||
email_notifications = YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5
|
||||
# secrets for communicating with Stripe (optional payment processor)
|
||||
stripe_webhook =
|
||||
stripe_public_key =
|
||||
|
||||
@@ -217,6 +217,8 @@ def make_map():
|
||||
|
||||
mc('/mail/optout', controller='forms', action='optout')
|
||||
mc('/mail/optin', controller='forms', action='optin')
|
||||
mc('/mail/unsubscribe/:user/:key', controller='forms',
|
||||
action='unsubscribe_emails')
|
||||
mc('/stylesheet', controller='front', action='stylesheet')
|
||||
mc('/frame', controller='front', action='frame')
|
||||
mc('/framebuster/:blah', controller='front', action='framebuster')
|
||||
|
||||
@@ -36,7 +36,7 @@ from r2 import config
|
||||
from r2.models import *
|
||||
from r2.models.recommend import ExploreSettings
|
||||
from r2.config.extensions import is_api
|
||||
from r2.lib import recommender, embeds
|
||||
from r2.lib import hooks, recommender, embeds
|
||||
from r2.lib.pages import *
|
||||
from r2.lib.pages.things import hot_links_by_url_listing
|
||||
from r2.lib.pages import trafficpages
|
||||
@@ -47,7 +47,7 @@ from r2.lib.utils import to36, sanitize_url, title_to_url
|
||||
from r2.lib.utils import query_string, UrlParser, url_links_builder
|
||||
from r2.lib.template_helpers import get_domain
|
||||
from r2.lib.filters import unsafe, _force_unicode, _force_utf8
|
||||
from r2.lib.emailer import Email
|
||||
from r2.lib.emailer import Email, generate_notification_email_unsubscribe_token
|
||||
from r2.lib.db.operators import desc
|
||||
from r2.lib.db import queries
|
||||
from r2.lib.db.tdb_cassandra import MultiColumnQuery
|
||||
@@ -1355,6 +1355,28 @@ class FormsController(RedditController):
|
||||
)
|
||||
).render()
|
||||
|
||||
@validate(
|
||||
user_id36=nop('user'),
|
||||
provided_mac=nop('key')
|
||||
)
|
||||
def GET_unsubscribe_emails(self, user_id36, provided_mac):
|
||||
from r2.lib.utils import constant_time_compare
|
||||
|
||||
expected_mac = generate_notification_email_unsubscribe_token(user_id36)
|
||||
if not constant_time_compare(provided_mac or '', expected_mac):
|
||||
error_page = pages.RedditError(
|
||||
title=_('incorrect message token'),
|
||||
message='',
|
||||
)
|
||||
request.environ["usable_error_content"] = error_page.render()
|
||||
self.abort404()
|
||||
user = Account._byID36(user_id36, data=True)
|
||||
user.pref_email_messages = False
|
||||
user._commit()
|
||||
|
||||
return BoringPage(_('emails unsubscribed'),
|
||||
content=MessageNotificationEmailsUnsubscribe()).render()
|
||||
|
||||
@disable_subreddit_css()
|
||||
@validate(VUser(),
|
||||
location=nop("location"),
|
||||
|
||||
@@ -142,9 +142,16 @@ def message_notification_email(data):
|
||||
raise Exception(
|
||||
'Message notification emails: safety limit exceeded!')
|
||||
|
||||
mac = generate_notification_email_unsubscribe_token(
|
||||
datum['to'], user_email=user.email,
|
||||
user_password_hash=user.password)
|
||||
base = g.https_endpoint or g.origin
|
||||
unsubscribe_link = base + '/mail/unsubscribe/%s/%s' % (datum['to'], mac)
|
||||
|
||||
templateData = {
|
||||
'comment': comment,
|
||||
'permalink': datum['permalink'],
|
||||
'unsubscribe_link': unsubscribe_link,
|
||||
}
|
||||
_system_email(user.email,
|
||||
MessageNotificationEmail(**templateData).render(style='email'),
|
||||
@@ -154,6 +161,31 @@ def message_notification_email(data):
|
||||
g.stats.simple_event('email.message_notification.queued')
|
||||
g.cache.incr(MESSAGE_THROTTLE_KEY)
|
||||
|
||||
def generate_notification_email_unsubscribe_token(user_id36, user_email=None,
|
||||
user_password_hash=None):
|
||||
"""Generate a token used for one-click unsubscribe links for notification
|
||||
emails.
|
||||
|
||||
user_id36: A base36-encoded user id.
|
||||
user_email: The user's email. Looked up if not provided.
|
||||
user_password_hash: The hash of the user's password. Looked up if not
|
||||
provided.
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
if (not user_email) or (not user_password_hash):
|
||||
user = Account._byID36(user_id36, data=True)
|
||||
if not user_email:
|
||||
user_email = user.email
|
||||
if not user_password_hash:
|
||||
user_password_hash = user.password
|
||||
|
||||
return hmac.new(
|
||||
g.secrets['email_notifications'],
|
||||
user_id36 + user_email + user_password_hash,
|
||||
hashlib.sha256).hexdigest()
|
||||
|
||||
def password_change_email(user):
|
||||
"""Queues a system email for a password change notification."""
|
||||
from r2.lib.pages import PasswordChangeEmail
|
||||
|
||||
@@ -2781,6 +2781,11 @@ class MessageNotificationEmail(Templated):
|
||||
"""Notification e-mail that a user has received a new message."""
|
||||
pass
|
||||
|
||||
class MessageNotificationEmailsUnsubscribe(Templated):
|
||||
"""The page we show users when they unsubscribe from notification
|
||||
emails."""
|
||||
pass
|
||||
|
||||
class PasswordChangeEmail(Templated):
|
||||
"""Notification e-mail that a user's password has changed."""
|
||||
pass
|
||||
|
||||
@@ -1279,6 +1279,9 @@ def constant_time_compare(actual, expected):
|
||||
|
||||
The time taken is dependent on the number of characters provided
|
||||
instead of the number of characters that match.
|
||||
|
||||
When we upgrade to Python 2.7.7 or newer, we should use hmac.compare_digest
|
||||
instead.
|
||||
"""
|
||||
actual_len = len(actual)
|
||||
expected_len = len(expected)
|
||||
|
||||
@@ -29,3 +29,5 @@ ${_("You've received a new message!")}
|
||||
${unsafe(thing.comment.body)}
|
||||
|
||||
${_("View on the web")}: ${thing.permalink}
|
||||
|
||||
${_("Unsubscribe")}: ${thing.unsubscribe_link}
|
||||
|
||||
25
r2/r2/templates/messagenotificationemailsunsubscribe.html
Normal file
25
r2/r2/templates/messagenotificationemailsunsubscribe.html
Normal file
@@ -0,0 +1,25 @@
|
||||
## The contents of this file are subject to the Common Public Attribution
|
||||
## License Version 1.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://code.reddit.com/LICENSE. The License is based on the Mozilla Public
|
||||
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
|
||||
## software over a computer network and provide for limited attribution for the
|
||||
## Original Developer. In addition, Exhibit A has been modified to be
|
||||
## consistent with Exhibit B.
|
||||
##
|
||||
## 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 reddit.
|
||||
##
|
||||
## The Original Developer is the Initial Developer. The Initial Developer of
|
||||
## the Original Code is reddit Inc.
|
||||
##
|
||||
## All portions of the code written by reddit are Copyright (c) 2006-2015
|
||||
## reddit Inc. All Rights Reserved.
|
||||
###############################################################################
|
||||
|
||||
<div style="text-align: center">
|
||||
<p>${_("Thanks! You're unsubscribed from all message notification emails.")}</p>
|
||||
</div>
|
||||
Reference in New Issue
Block a user