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.