diff --git a/app/controllers/email_controller.rb b/app/controllers/email_controller.rb
index ca8255334..4e0ff301a 100644
--- a/app/controllers/email_controller.rb
+++ b/app/controllers/email_controller.rb
@@ -11,6 +11,7 @@ class EmailController < ApplicationController
def unsubscribe
@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
if current_user.present? && (@user != current_user)
@@ -23,7 +24,12 @@ class EmailController < ApplicationController
return
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
end
diff --git a/app/mailers/subscription_mailer.rb b/app/mailers/subscription_mailer.rb
new file mode 100644
index 000000000..c36228713
--- /dev/null
+++ b/app/mailers/subscription_mailer.rb
@@ -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
diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb
index acc2ff7ed..947735eaa 100644
--- a/app/mailers/user_notifications.rb
+++ b/app/mailers/user_notifications.rb
@@ -307,6 +307,7 @@ class UserNotifications < ActionMailer::Base
context: context,
username: username,
add_unsubscribe_link: !user.staged,
+ add_unsubscribe_via_email_link: user.mailing_list_mode,
unsubscribe_url: post.topic.unsubscribe_url,
allow_reply_by_email: allow_reply_by_email,
use_site_subject: use_site_subject,
diff --git a/app/views/email/notification.html.erb b/app/views/email/notification.html.erb
index 044de8352..681dc96af 100644
--- a/app/views/email/notification.html.erb
+++ b/app/views/email/notification.html.erb
@@ -19,7 +19,7 @@
-
+
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index b615f6936..3907166e1 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1104,6 +1104,9 @@ en:
short_email_length: "Short email length in Bytes"
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_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."
@@ -1379,6 +1382,17 @@ en:
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."
+ 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:
subject_template: "%{invitee_name} invited you to '%{topic_title}' on %{site_domain_name}"
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."
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_pm: "[PM] "
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 5f0687db9..d7e3ed876 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -535,6 +535,10 @@ email:
short_email_length: 2800
display_name_on_email_from:
default: true
+ unsubscribe_via_email:
+ default: true
+ unsubscribe_via_email_footer:
+ default: false
files:
max_image_size_kb: 3072
diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb
index b1cf2bd57..acb30c481 100644
--- a/lib/email/message_builder.rb
+++ b/lib/email/message_builder.rb
@@ -63,6 +63,13 @@ module Email
if @opts[:add_unsubscribe_link]
unsubscribe_link = PrettyText.cook(I18n.t('unsubscribe_link', template_args), sanitize: false).html_safe
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
html_override.gsub!("%{unsubscribe_link}", "")
end
@@ -103,6 +110,9 @@ module Email
if @opts[:add_unsubscribe_link]
body << "\n"
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
body
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index ad0da3e9f..736dcb173 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -59,7 +59,10 @@ module Email
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)
else
destination = destinations.first
@@ -226,6 +229,13 @@ module Email
@likes ||= Set.new ["+1", I18n.t('post_action_types.like.title').downcase]
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)
if likes.include?(body.strip.downcase)
PostActionType.types[:like]
diff --git a/spec/components/email/message_builder_spec.rb b/spec/components/email/message_builder_spec.rb
index 5f8a1d763..8ff2dc511 100644
--- a/spec/components/email/message_builder_spec.rb
+++ b/spec/components/email/message_builder_spec.rb
@@ -181,6 +181,23 @@ describe Email::MessageBuilder do
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
context "template_args" do
diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb
index b1ee7a7a8..4bffd967b 100644
--- a/spec/components/email/receiver_spec.rb
+++ b/spec/components/email/receiver_spec.rb
@@ -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 ;)")
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
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 wrote:\n\n> WAT November 28\n>\n> This is the previous post.\n\nAnd this is *my* reply :+1:")
diff --git a/spec/controllers/email_controller_spec.rb b/spec/controllers/email_controller_spec.rb
index 411d3ff14..f5c6e9180 100644
--- a/spec/controllers/email_controller_spec.rb
+++ b/spec/controllers/email_controller_spec.rb
@@ -39,9 +39,23 @@ describe EmailController 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) }
+ 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
before do
get :unsubscribe, key: key
diff --git a/spec/fixtures/emails/unsubscribe_body.eml b/spec/fixtures/emails/unsubscribe_body.eml
new file mode 100644
index 000000000..1ae876edb
--- /dev/null
+++ b/spec/fixtures/emails/unsubscribe_body.eml
@@ -0,0 +1,10 @@
+Return-Path:
+From: Foo Bar
+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
diff --git a/spec/fixtures/emails/unsubscribe_subject.eml b/spec/fixtures/emails/unsubscribe_subject.eml
new file mode 100644
index 000000000..84b89079b
--- /dev/null
+++ b/spec/fixtures/emails/unsubscribe_subject.eml
@@ -0,0 +1,11 @@
+Return-Path:
+From: Foo Bar
+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.