Skip to content

Commit 79bd521

Browse files
committed
Merge branch 'main' into release/20.10
2 parents 0332a95 + 46400e9 commit 79bd521

File tree

31 files changed

+361
-32
lines changed

31 files changed

+361
-32
lines changed

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ Rails/OutputSafety:
8686
Style/ClassAndModuleChildren:
8787
Enabled: false
8888

89+
# I think this makes code significantly harder to read, personally
90+
Style/ConditionalAssignment:
91+
Enabled: false
92+
8993
# This test is for a feature that parses a list of IP addresses
9094
Style/IpAddresses:
9195
Exclude:
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
# ShinyCMS ~ https://shinycms.org
4+
#
5+
# Copyright 2009-2020 Denny de la Haye ~ https://denny.me
6+
#
7+
# ShinyCMS is free software; you can redistribute it and/or modify it under the terms of the GPL (version 2 or later)
8+
9+
# Controller for main site email-recipient features in ShinyCMS
10+
class EmailRecipientsController < MainController
11+
# Confirm that the person has access to the email account - AKA double opt-in
12+
def confirm
13+
recipient = EmailRecipient.find_by( token: params[ :token ] )
14+
15+
if recipient&.confirm
16+
flash[ :notice ] = t( '.success' )
17+
else
18+
flash[ :alert ] = t( confirm_failure_message( recipient ) )
19+
end
20+
21+
redirect_back fallback_location: root_path
22+
end
23+
24+
private
25+
26+
def confirm_failure_message( recipient = nil )
27+
return '.token_not_found' if recipient.blank?
28+
return '.token_expired' if recipient.confirm_expired?
29+
30+
# '.failure'
31+
end
32+
end

app/mailers/discussion_mailer.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def parent_comment_notification( comment )
1717

1818
@user = notified_user( @parent.notification_email, @parent.author_name_any )
1919

20-
return if DoNotContact.include? @user.email # TODO: make this happen without explicit call
20+
return if @user.do_not_email? # TODO: make this happen without explicit call
2121

2222
mail to: @user.email_to, subject: parent_comment_notification_subject do |format|
2323
format.html
@@ -30,7 +30,7 @@ def discussion_notification( comment )
3030

3131
@comment, @resource, @user = comment_and_resource_and_user( comment )
3232

33-
return if DoNotContact.include? @user.email # TODO: make this happen without explicit call
33+
return if @user.do_not_email? # TODO: make this happen without explicit call
3434

3535
mail to: @user.email_to, subject: discussion_notification_subject do |format|
3636
format.html

app/mailers/email_recipient_mailer.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
# ShinyCMS ~ https://shinycms.org
4+
#
5+
# Copyright 2009-2020 Denny de la Haye ~ https://denny.me
6+
#
7+
# ShinyCMS is free software; you can redistribute it and/or modify it under the terms of the GPL (version 2 or later)
8+
9+
# Mailer for EmailRecipients (non-authenticated site users that we want to send email to)
10+
class EmailRecipientMailer < ApplicationMailer
11+
# Don't store URLs that might have security tokens in them in email stats data
12+
track click: false
13+
14+
# Email a link that the user must click to prove they have access to this email address
15+
def confirm( recipient )
16+
@user = @recipient = recipient
17+
@confirm_token = recipient.confirm_token
18+
19+
return if DoNotContact.include? recipient.email # TODO: make this happen without explicit call
20+
21+
mail to: recipient.email_to, subject: t( '.subject', site_name: site_name ) do |format|
22+
format.html
23+
format.text
24+
end
25+
end
26+
end

app/models/concerns/shiny_email.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ def generate_canonical_email
2323
self.canonical_email = EmailAddress.canonical( email )
2424
end
2525

26+
def do_not_email?
27+
!confirmed? || DoNotContact.include?( email )
28+
end
29+
2630
def obfuscated_email
2731
EmailAddress.munge( email )
2832
end

app/models/email_recipient.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,46 @@ class EmailRecipient < ApplicationRecord
1717

1818
# Email stats (powered by Ahoy)
1919
has_many :messages, as: :user, dependent: :nullify, class_name: 'Ahoy::Message'
20+
21+
# Scopes
22+
23+
scope :confirmed, -> { where.not( confirmed_at: nil ) }
24+
25+
# Instance methods
26+
27+
after_create :send_confirm_email
28+
29+
def set_confirm_token
30+
update!(
31+
confirm_token: SecureRandom.uuid,
32+
confirm_sent_at: Time.zone.now,
33+
confirmed_at: nil
34+
)
35+
end
36+
37+
def send_confirm_email
38+
set_confirm_token
39+
EmailRecipientMailer.confirm( self )
40+
end
41+
42+
def confirm
43+
return false if confirm_expired?
44+
45+
update!( confirmed_at: Time.zone.now )
46+
end
47+
48+
def confirmed?
49+
confirmed_at.present?
50+
end
51+
52+
def confirm_expired?
53+
confirm_sent_at < self.class.confirm_token_valid_for.ago
54+
end
55+
56+
# Class methods
57+
58+
def self.confirm_token_valid_for
59+
# TODO: make this configurable
60+
7.days
61+
end
2062
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<mj-body background-color="#f8f8f8">
2+
<mj-section background-color="#ffffff" background-repeat="repeat" padding-bottom="0px" padding-left="0px" padding-right="0px" padding-top="0px" padding="20px 0" text-align="center">
3+
<mj-column>
4+
<mj-divider border-color="#60b4cc" border-style="solid" border-width="7px" padding-bottom="40px" padding-left="0px" padding-right="0px" padding-top="0px" padding="10px 25px" width="100%">
5+
</mj-divider>
6+
<mj-image align="center" alt="" border="none" href="" padding-bottom="0px" padding-top="0px" padding="10px 25px" src="https://shinycms.org/images/spiral.png" target="_blank" title="" height="auto" width="110px">
7+
</mj-image>
8+
</mj-column>
9+
</mj-section>
10+
<mj-section background-color="#ffffff" background-repeat="repeat" background-size="auto" padding-bottom="0px" padding-top="0px" padding="20px 0" text-align="center">
11+
<mj-column>
12+
<mj-image align="center" alt="" border="none" height="auto" href="" padding-bottom="0px" padding-left="50px" padding-right="50px" padding-top="40px" padding="10px 25px" src="http://9pl9.mjt.lu/tplimg/9pl9/b/yg0q/t65sy.png" target="_blank" title="" width="300px">
13+
</mj-image>
14+
</mj-column>
15+
</mj-section>
16+
<mj-section background-color="#ffffff" background-repeat="repeat" background-size="auto" padding-bottom="70px" padding-top="30px" padding="20px 0px 20px 0px" text-align="center">
17+
<mj-column>
18+
<mj-text align="left" color="#797e82" font-family="Open Sans, Helvetica, Arial, sans-serif" font-size="13px" line-height="22px" padding-bottom="0px" padding-left="50px" padding-right="50px" padding-top="0px" padding="0px 25px 0px 25px">
19+
<h2 style="text-align:center; color: #000000; line-height:32px">
20+
<%= t( 'user_mailer.hello', name: ( @recipient.name || @recipient.email ) ) %>
21+
</h2>
22+
</mj-text>
23+
<mj-text align="left" color="#797e82" font-family="Open Sans, Helvetica, Arial, sans-serif" font-size="13px" line-height="22px" padding-bottom="0px" padding-left="50px" padding-right="50px" padding-top="0px" padding="0px 25px 0px 25px">
24+
<p style="margin: 10px 0; text-align: center;">
25+
Before we can send you any emails from <%= site_name %>, we need you to click on the button below:
26+
</p>
27+
</mj-text>
28+
<mj-button href="<%= confirm_email_url( @confirm_token ) %>" align="center" background-color="#60b4cc" border-radius="100px" border="none" color="#ffffff" font-family="Open Sans, Helvetica, Arial, sans-serif" font-size="13px" font-weight="normal" inner-padding="15px 25px 15px 25px" padding-bottom="20px" padding-top="20px" padding="10px 25px" text-decoration="none" text-transform="none" vertical-align="middle">
29+
<b style="font-weight:700">Verify my email address</b>
30+
</mj-button>
31+
<mj-text align="left" color="#797e82" font-family="Open Sans, Helvetica, Arial, sans-serif" font-size="13px" line-height="22px" padding-bottom="0px" padding-left="50px" padding-right="50px" padding-top="0px" padding="0px 25px 0px 25px">
32+
<p style="margin: 10px 0; text-align: center;">
33+
If the button doesn&rsquo;t work, copy this URL into your browser:
34+
<br>
35+
<%= confirm_email_url( @confirm_token ) %>
36+
</p>
37+
</mj-text>
38+
</mj-column>
39+
</mj-section>
40+
</mj-body>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Hi <%= @recipient.name || @recipient.email %>,
2+
3+
Before we can send you any emails from <%= @site_name %>, we need you to click on the link below:
4+
5+
<%= confirm_email_url( @confirm_token ) %>
6+
7+
Thank you!

config/locales/en.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ en:
102102
failure: Failed to add email address to Do Not Contact list; please try again
103103
duplicate: Your email address is already on our Do Not Contact list
104104

105+
email_recipients:
106+
confirm:
107+
success: Thank you for verifying your email address
108+
token_not_found: Failed to find email address; please check URL and try again
109+
token_expired: Your confirmation link has expired; please request a new email and try again
110+
# failure: Failed to confirm email address; please try again, or alert site admins
111+
105112
errors:
106113
messages:
107114
slug_not_safe_at_top_level: cannot be used as a top-level slug
@@ -154,6 +161,10 @@ en:
154161
overview_notification:
155162
subject: '%{comment_author_name} commented on %{site_name}'
156163

164+
email_recipient_mailer:
165+
confirm:
166+
subject: Confirm your email address for %{site_name}
167+
157168
user_mailer:
158169
welcome: Welcome, %{name}!
159170
hello: Hello %{name},

config/routes.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
get 'discussion/:id/:number', to: 'discussions#show_thread', as: :comment
2424
post 'discussion/:id/:number', to: 'discussions#add_reply'
2525

26-
get 'do-not-contact', to: 'do_not_contact#new'
27-
post 'do-not-contact', to: 'do_not_contact#create'
26+
get 'email/confirm/:token', to: 'email_recipients#confirm', as: :confirm_email
27+
28+
get 'email/do-not-contact', to: 'do_not_contact#new', as: :do_not_contact
29+
post 'email/do-not-contact', to: 'do_not_contact#create'
2830

2931
get 'site-settings', to: 'site_settings#index'
3032
put 'site-settings', to: 'site_settings#update'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
# ShinyCMS ~ https://shinycms.org
4+
#
5+
# Copyright 2009-2020 Denny de la Haye ~ https://denny.me
6+
#
7+
# ShinyCMS is free software; you can redistribute it and/or modify it under the terms of the GPL (version 2 or later)
8+
9+
class AddConfirmToEmailRecipient < ActiveRecord::Migration[6.0]
10+
def change
11+
add_column :email_recipients, :confirm_token, :uuid
12+
add_column :email_recipients, :confirm_sent_at, :timestamp, precision: 6
13+
add_column :email_recipients, :confirmed_at, :timestamp, precision: 6
14+
end
15+
end

db/schema.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# migrations from scratch. Old migrations may fail to apply correctly if those
1313
# migrations use external dependencies or application code.
1414

15-
ActiveRecord::Schema.define(version: 2020_09_15_225123) do
15+
ActiveRecord::Schema.define(version: 2020_10_01_180612) do
1616

1717
# These are extensions that must be enabled in order to support this database
1818
enable_extension "pg_stat_statements"
@@ -217,6 +217,9 @@
217217
t.string "email", null: false
218218
t.string "canonical_email", null: false
219219
t.uuid "token", default: -> { "gen_random_uuid()" }, null: false
220+
t.uuid "confirm_token"
221+
t.datetime "confirm_sent_at", precision: 6
222+
t.datetime "confirmed_at", precision: 6
220223
t.datetime "created_at", precision: 6, null: false
221224
t.datetime "updated_at", precision: 6, null: false
222225
t.index ["email"], name: "index_email_recipients_on_email", unique: true

docs/Developers/TODO.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
## Fixes and refactoring of code already written - to do next/soon
44

5-
* Double opt-in journey
6-
* Email recipients
7-
* List subscriptions
8-
* Comment notifications
5+
* Add subscription-management links to list emails
96

107
* Move pages, newsletters, and forms test templates into each plugin's spec/fixtures
118

@@ -78,6 +75,8 @@
7875
* 2FA
7976
* https://github.com/tinfoil/devise-two-factor
8077

78+
* Allow an EmailRecipient to reset their token (in case they forward an email containing it to somebody else)
79+
8180
* Configurable (per-site and per-user) menu order in admin area
8281

8382
* Better tooling for loading (and ideally, for creating/updating) the demo data

plugins/ShinyBlog/spec/factories/shiny_blog/blog_posts.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module ShinyBlog
1414
body { Faker::Lorem.paragraph }
1515
posted_at { Time.zone.now.iso8601 }
1616

17-
association :user
17+
association :user, factory: :blog_admin
1818
end
1919
end
2020
end

plugins/ShinyLists/app/controllers/shiny_lists/subscriptions_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ def index
1414

1515
if subscriber
1616
@subscriptions = subscriber.subscriptions
17-
@subscriber = subscriber
17+
18+
flash.now[:alert] = t( '.email_not_confirmed' ) unless subscriber.confirmed?
1819
else
1920
flash.now[:alert] = t( '.subscriber_not_found' )
2021
end

plugins/ShinyLists/app/views/shiny_lists/subscriptions/index.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<% if @subscriptions.present? %>
1111
<p>
12-
You are managing subscriptions for <strong><%= @subscriber.obfuscated_email %></strong>.
12+
You are managing subscriptions for <strong><%= @subscriptions.first.subscriber.obfuscated_email %></strong>.
1313
If this is not you, please <% if user_signed_in? %>log out<% else %>check the link you used<% end %>!
1414
</p>
1515

plugins/ShinyLists/config/locales/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ en:
3030
title: Your mailing list subscriptions
3131
unsubscribe: Unsubscribe
3232
subscriber_not_found: Subscriber not found - please check link and try again
33+
email_not_confirmed: You will not receive any email until your email address has been confirmed
3334
subscribe:
3435
success: You have been subscribed
3536
already_subscribed: You are already subscribed to this mailing list

plugins/ShinyLists/spec/factories/shiny_lists/subscriptions.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module ShinyLists
1111
FactoryBot.define do
1212
factory :mailing_list_subscription, class: 'ShinyLists::Subscription' do
1313
association :list, factory: :mailing_list
14-
association :subscriber, factory: :email_recipient
14+
association :subscriber, factory: %i[ email_recipient confirmed ]
1515

1616
association :consent_version
1717
end

plugins/ShinyLists/spec/requests/subscriptions_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
describe 'GET /subscriptions/:token' do
4141
it "displays a token-identified user's subscriptions" do
42-
subscriber1 = create :email_recipient
42+
subscriber1 = create :email_recipient, :confirmed
4343

4444
get shiny_lists.token_list_subscriptions_path( subscriber1.token )
4545

@@ -225,7 +225,7 @@
225225
describe 'PUT /list/:slug/unsubscribe/:token' do
226226
it 'unsubscribes a token-authenticated user' do
227227
list1 = create :mailing_list
228-
subscriber1 = create :email_recipient
228+
subscriber1 = create :email_recipient, :confirmed
229229
create :mailing_list_subscription, list: list1, subscriber: subscriber1
230230

231231
put shiny_lists.token_list_unsubscribe_path( list1.slug, subscriber1.token )

plugins/ShinyNewsletters/app/controllers/shiny_newsletters/newsletters_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def show
2929
private
3030

3131
def subscriber
32-
current_user || EmailRecipient.find_by( token: params[:token] )
32+
current_user || EmailRecipient.confirmed.find_by( token: params[:token] )
3333
end
3434

3535
def newsletters_sent_to_subscribed_lists

plugins/ShinyNewsletters/app/jobs/shiny_newsletters/send_to_list_job.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ def perform( send )
1717
send.update!( started_sending_at: Time.zone.now )
1818

1919
send.list.subscriptions.each do |subscription|
20+
next if subscription.subscriber.do_not_email?
21+
2022
SendToSubscriberJob.perform_later( send, subscription.subscriber )
2123
end
2224

plugins/ShinyNewsletters/app/jobs/shiny_newsletters/send_to_subscriber_job.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module ShinyNewsletters
1010
# Send an edition of a newsletter to an individual subscriber on a list
1111
class SendToSubscriberJob < ApplicationJob
1212
def perform( send, subscriber )
13+
return if subscriber.do_not_email?
1314
return if send.sent?
1415

1516
return unless send.list.subscribed? subscriber.email

plugins/ShinyNewsletters/app/mailers/shiny_newsletters/newsletter_mailer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def send_email( edition, recipient )
1515
stash_content( edition )
1616
stash_user( recipient )
1717

18-
return if DoNotContact.include? recipient.email # TODO: make this happen without explicit call
18+
return if @user.do_not_email? # TODO: make this happen without explicit call
1919

2020
mail to: @user.email_to, subject: @edition.subject, template_name: @edition.template.filename do |format|
2121
format.html

0 commit comments

Comments
 (0)