Unsubscribe via email

This commit is contained in:
James Kiesel 2016-01-20 22:25:25 +13:00
parent 11ea16a91a
commit c7283751a3
13 changed files with 159 additions and 5 deletions

View file

@ -11,6 +11,7 @@ class EmailController < ApplicationController
def unsubscribe def unsubscribe
@user = DigestUnsubscribeKey.user_for_key(params[:key]) @user = DigestUnsubscribeKey.user_for_key(params[:key])
RateLimiter.new(@user, "unsubscribe_via_email", 3, 1.day).performed! unless @user && @user.staff?
# Don't allow the use of a key while logged in as a different user # Don't allow the use of a key while logged in as a different user
if current_user.present? && (@user != current_user) if current_user.present? && (@user != current_user)
@ -23,7 +24,12 @@ class EmailController < ApplicationController
return return
end end
@user.update_column(:email_digests, false) if params[:from_all]
@user.update_columns(email_digests: false, email_direct: false, email_private_messages: false, email_always: false)
else
@user.update_column(:email_digests, false)
end
@success = true @success = true
end end

View file

@ -0,0 +1,14 @@
require_dependency 'email/message_builder'
class SubscriptionMailer < ActionMailer::Base
include Email::BuildEmailHelper
def confirm_unsubscribe(user, opts={})
unsubscribe_key = DigestUnsubscribeKey.create_key_for(user)
build_email user.email,
template: "unsubscribe_mailer",
site_title: SiteSetting.title,
site_domain_name: Discourse.current_hostname,
confirm_unsubscribe_link: "#{Discourse.base_url}/unsubscribe/#{unsubscribe_key}?from_all=true"
end
end

View file

@ -307,6 +307,7 @@ class UserNotifications < ActionMailer::Base
context: context, context: context,
username: username, username: username,
add_unsubscribe_link: !user.staged, add_unsubscribe_link: !user.staged,
add_unsubscribe_via_email_link: user.mailing_list_mode,
unsubscribe_url: post.topic.unsubscribe_url, unsubscribe_url: post.topic.unsubscribe_url,
allow_reply_by_email: allow_reply_by_email, allow_reply_by_email: allow_reply_by_email,
use_site_subject: use_site_subject, use_site_subject: use_site_subject,

View file

@ -19,7 +19,7 @@
<hr> <hr>
<div class='footer'>%{respond_instructions}</div> <div class='footer'>%{respond_instructions}</div>
<div class='footer'>%{unsubscribe_link}</div> <div class='footer'>%{unsubscribe_link}%{unsubscribe_via_email_link}</div>
</div> </div>

View file

@ -1104,6 +1104,9 @@ en:
short_email_length: "Short email length in Bytes" short_email_length: "Short email length in Bytes"
display_name_on_email_from: "Display full names on email from fields" display_name_on_email_from: "Display full names on email from fields"
unsubscribe_via_email: "Allow users to unsubscribe from emails by sending an email with 'unsubscribe' in the subject or body"
unsubscribe_via_email_footer: "Attach an unsubscribe link to the footer of sent emails"
pop3_polling_enabled: "Poll via POP3 for email replies." pop3_polling_enabled: "Poll via POP3 for email replies."
pop3_polling_ssl: "Use SSL while connecting to the POP3 server. (Recommended)" pop3_polling_ssl: "Use SSL while connecting to the POP3 server. (Recommended)"
pop3_polling_period_mins: "The period in minutes between checking the POP3 account for email. NOTE: requires restart." pop3_polling_period_mins: "The period in minutes between checking the POP3 account for email. NOTE: requires restart."
@ -1379,6 +1382,17 @@ en:
blocked: "New registrations are not allowed from your IP address." blocked: "New registrations are not allowed from your IP address."
max_new_accounts_per_registration_ip: "New registrations are not allowed from your IP address (maximum limit reached). Contact a staff member." max_new_accounts_per_registration_ip: "New registrations are not allowed from your IP address (maximum limit reached). Contact a staff member."
unsubscribe_mailer:
subject_template: "Confirm you no longer want to receive email updates from %{site_title}"
text_body_template: |
Someone (possibly you?) requested to no longer send email updates from %{site_domain_name} to this address.
If you with to confirm this, please click this link:
%{confirm_unsubscribe_link}
I you want to continue receiving email updates, you may ignore this email.
invite_mailer: invite_mailer:
subject_template: "%{invitee_name} invited you to '%{topic_title}' on %{site_domain_name}" subject_template: "%{invitee_name} invited you to '%{topic_title}' on %{site_domain_name}"
text_body_template: | text_body_template: |
@ -1940,7 +1954,10 @@ en:
text_body_template: "The `download_remote_images_to_local` setting was disabled because the disk space limit at `download_remote_images_threshold` was reached." text_body_template: "The `download_remote_images_to_local` setting was disabled because the disk space limit at `download_remote_images_threshold` was reached."
unsubscribe_link: | unsubscribe_link: |
To stop receiving notifications for this particular topic, [click here](%{unsubscribe_url}). To unsubscribe from these emails, change your [user preferences](%{user_preferences_url}). To stop receiving notifications for this particular topic, [click here](%{unsubscribe_url}). To unsubscribe from these emails, change your [user preferences](%{user_preferences_url})
unsubscribe_via_email_link: |
or, [click here](mailto:reply@%{hostname}?subject=unsubscribe) to unsubscribe via email.
subject_re: "Re: " subject_re: "Re: "
subject_pm: "[PM] " subject_pm: "[PM] "

View file

@ -535,6 +535,10 @@ email:
short_email_length: 2800 short_email_length: 2800
display_name_on_email_from: display_name_on_email_from:
default: true default: true
unsubscribe_via_email:
default: true
unsubscribe_via_email_footer:
default: false
files: files:
max_image_size_kb: 3072 max_image_size_kb: 3072

View file

@ -63,6 +63,13 @@ module Email
if @opts[:add_unsubscribe_link] if @opts[:add_unsubscribe_link]
unsubscribe_link = PrettyText.cook(I18n.t('unsubscribe_link', template_args), sanitize: false).html_safe unsubscribe_link = PrettyText.cook(I18n.t('unsubscribe_link', template_args), sanitize: false).html_safe
html_override.gsub!("%{unsubscribe_link}", unsubscribe_link) html_override.gsub!("%{unsubscribe_link}", unsubscribe_link)
if SiteSetting.unsubscribe_via_email_footer && @opts[:add_unsubscribe_via_email_link]
unsubscribe_via_email_link = PrettyText.cook(I18n.t('unsubscribe_via_email_link', hostname: Discourse.current_hostname), sanitize: false).html_safe
html_override.gsub!("%{unsubscribe_via_email_link}", unsubscribe_via_email_link)
else
html_override.gsub!("%{unsubscribe_via_email_link}", "")
end
else else
html_override.gsub!("%{unsubscribe_link}", "") html_override.gsub!("%{unsubscribe_link}", "")
end end
@ -103,6 +110,9 @@ module Email
if @opts[:add_unsubscribe_link] if @opts[:add_unsubscribe_link]
body << "\n" body << "\n"
body << I18n.t('unsubscribe_link', template_args) body << I18n.t('unsubscribe_link', template_args)
if SiteSetting.unsubscribe_via_email_footer && @opts[:add_unsubscribe_via_email_link]
body << I18n.t('unsubscribe_via_email_link', hostname: Discourse.current_hostname)
end
end end
body body

View file

@ -59,7 +59,10 @@ module Email
raise InactiveUserError if !user.active && !user.staged raise InactiveUserError if !user.active && !user.staged
if post = find_related_post if action = subscription_action_for(body, @mail.subject)
message = SubscriptionMailer.send(action, user)
Email::Sender.new(message, :subscription).send
elsif post = find_related_post
create_reply(user: user, raw: body, post: post, topic: post.topic) create_reply(user: user, raw: body, post: post, topic: post.topic)
else else
destination = destinations.first destination = destinations.first
@ -226,6 +229,13 @@ module Email
@likes ||= Set.new ["+1", I18n.t('post_action_types.like.title').downcase] @likes ||= Set.new ["+1", I18n.t('post_action_types.like.title').downcase]
end end
def subscription_action_for(body, subject)
return unless SiteSetting.unsubscribe_via_email
if ([subject, body].compact.map(&:to_s).map(&:downcase) & ['unsubscribe']).any?
:confirm_unsubscribe
end
end
def post_action_for(body) def post_action_for(body)
if likes.include?(body.strip.downcase) if likes.include?(body.strip.downcase)
PostActionType.types[:like] PostActionType.types[:like]

View file

@ -181,6 +181,23 @@ describe Email::MessageBuilder do
end end
context "with unsubscribe_via_email_link true" do
let(:message_with_unsubscribe_via_email) { Email::MessageBuilder.new(to_address,
body: 'hello world',
add_unsubscribe_link: true,
add_unsubscribe_via_email_link: true,
unsubscribe_url: "/t/1234/unsubscribe") }
it "can add an unsubscribe via email link" do
SiteSetting.stubs(:unsubscribe_via_email_footer).returns(true)
expect(message_with_unsubscribe_via_email.body).to match(/mailto:reply@#{Discourse.current_hostname}\?subject=unsubscribe/)
end
it "does not add unsubscribe via email link without site setting set" do
expect(message_with_unsubscribe_via_email.body).to_not match(/mailto:reply@#{Discourse.current_hostname}\?subject=unsubscribe/)
end
end
end end
context "template_args" do context "template_args" do

View file

@ -130,6 +130,46 @@ describe Email::Receiver do
expect(topic.posts.last.raw).to eq("Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. Anf if\nyou can mix it up with some anise, then I'm in heaven ;)") expect(topic.posts.last.raw).to eq("Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. Anf if\nyou can mix it up with some anise, then I'm in heaven ;)")
end end
describe 'Unsubscribing via email' do
let(:last_email) { ActionMailer::Base.deliveries.last }
describe 'unsubscribe_subject.eml' do
it 'sends an email asking the user to confirm the unsubscription' do
expect { process("unsubscribe_subject") }.to change { ActionMailer::Base.deliveries.count }.by(1)
expect(last_email.to.length).to eq 1
expect(last_email.from.length).to eq 1
expect(last_email.from).to include "noreply@#{Discourse.current_hostname}"
expect(last_email.to).to include "discourse@bar.com"
expect(last_email.subject).to eq I18n.t(:"unsubscribe_mailer.subject_template").gsub("%{site_title}", SiteSetting.title)
end
it 'does nothing unless unsubscribe_via_email is turned on' do
SiteSetting.stubs("unsubscribe_via_email").returns(false)
before_deliveries = ActionMailer::Base.deliveries.count
expect { process("unsubscribe_subject") }.to raise_error { Email::Receiver::BadDestinationAddress }
expect(before_deliveries).to eq ActionMailer::Base.deliveries.count
end
end
describe 'unsubscribe_body.eml' do
it 'sends an email asking the user to confirm the unsubscription' do
expect { process("unsubscribe_body") }.to change { ActionMailer::Base.deliveries.count }.by(1)
expect(last_email.to.length).to eq 1
expect(last_email.from.length).to eq 1
expect(last_email.from).to include "noreply@#{Discourse.current_hostname}"
expect(last_email.to).to include "discourse@bar.com"
expect(last_email.subject).to eq I18n.t(:"unsubscribe_mailer.subject_template").gsub("%{site_title}", SiteSetting.title)
end
it 'does nothing unless unsubscribe_via_email is turned on' do
SiteSetting.stubs(:unsubscribe_via_email).returns(false)
before_deliveries = ActionMailer::Base.deliveries.count
expect { process("unsubscribe_body") }.to raise_error { Email::Receiver::InvalidPost }
expect(before_deliveries).to eq ActionMailer::Base.deliveries.count
end
end
end
it "handles inline reply" do it "handles inline reply" do
expect { process(:inline_reply) }.to change { topic.posts.count } expect { process(:inline_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("On Tue, Jan 15, 2016 at 11:12 AM, Bar Foo <info@unconfigured.discourse.org> wrote:\n\n> WAT <https://bar.com/users/wat> November 28\n>\n> This is the previous post.\n\nAnd this is *my* reply :+1:") expect(topic.posts.last.raw).to eq("On Tue, Jan 15, 2016 at 11:12 AM, Bar Foo <info@unconfigured.discourse.org> wrote:\n\n> WAT <https://bar.com/users/wat> November 28\n>\n> This is the previous post.\n\nAnd this is *my* reply :+1:")

View file

@ -39,9 +39,23 @@ describe EmailController do
context '.unsubscribe' do context '.unsubscribe' do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user, email_digests: true, email_direct: true, email_private_messages: true, email_always: true) }
let(:key) { DigestUnsubscribeKey.create_key_for(user) } let(:key) { DigestUnsubscribeKey.create_key_for(user) }
context 'from confirm unsubscribe email' do
before do
get :unsubscribe, key: key, from_all: true
user.reload
end
it 'unsubscribes from all emails' do
expect(user.email_digests).to eq false
expect(user.email_direct).to eq false
expect(user.email_private_messages).to eq false
expect(user.email_always).to eq false
end
end
context 'with a valid key' do context 'with a valid key' do
before do before do
get :unsubscribe, key: key get :unsubscribe, key: key

View file

@ -0,0 +1,10 @@
Return-Path: <discourse@bar.com>
From: Foo Bar <discourse@bar.com>
To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com
Date: Thu, 13 Jun 2013 17:03:48 -0400
Message-ID: <55@foo.bar.mail>
Mime-Version: 1.0
Content-Type: text/plain;
Content-Transfer-Encoding: 7bit
UNSUBSCRIBE

View file

@ -0,0 +1,11 @@
Return-Path: <discourse@bar.com>
From: Foo Bar <discourse@bar.com>
To: reply@bar.com
Date: Thu, 13 Jun 2013 17:03:48 -0400
Message-ID: <56@foo.bar.mail>
Subject: UnSuBScRiBe
Mime-Version: 1.0
Content-Type: text/plain;
Content-Transfer-Encoding: 7bit
I've basically had enough of your mailing list and would very much like it if you went away.