Skip to content

Commit

Permalink
Nicer emails
Browse files Browse the repository at this point in the history
Up until now we were using the standard default devise emails which
are very generic and very plain.

Prettify and rework the emails sent by devise, including signup
confirmation, password reset, and a few of others. Include some
branding and improved wording.

Uses the bootstrap-mailer gem to produce email compatible html
because that is very hard apparently. Uses haml instead of erb since
that's how we like to do it.

(Actually I'm not 100% certain all these emails are used but I've
updated them all anyhow. Should aim to review the devise config and
maybe test the account locking, the change email address, and the
change password mechanism soon to confirm it all works and to see
how the emails look in the real world.)

Closes #3
(...which is currently the oldest open issue, created in Jan 2021)
  • Loading branch information
simonbaird committed Jun 28, 2022
1 parent 6ea7d6e commit a82093f
Show file tree
Hide file tree
Showing 23 changed files with 240 additions and 31 deletions.
3 changes: 3 additions & 0 deletions rails/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ gem 'haml-rails'
# Support S3 for ActiveStorage
gem "aws-sdk-s3", require: false

# For nice emails
gem 'bootstrap-email'

# For payments
gem 'pay', '~> 3.0'
gem 'stripe', '>= 2.8', '< 6.0'
Expand Down
14 changes: 14 additions & 0 deletions rails/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ GEM
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.18)
bindex (0.8.1)
bootstrap-email (1.2.0)
htmlbeautifier (~> 1.3)
nokogiri (~> 1.6)
premailer (~> 1.7)
sassc (~> 2.1)
builder (3.2.4)
byebug (11.1.3)
capybara (3.37.1)
Expand All @@ -96,6 +101,8 @@ GEM
childprocess (4.1.0)
concurrent-ruby (1.1.10)
crass (1.0.6)
css_parser (1.11.0)
addressable
dalli (3.2.2)
devise (4.8.1)
bcrypt (~> 3.0)
Expand Down Expand Up @@ -125,6 +132,8 @@ GEM
haml (>= 4.0, < 6)
nokogiri (>= 1.6.0)
ruby_parser (~> 3.5)
htmlbeautifier (1.4.2)
htmlentities (4.3.4)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
jbuilder (2.11.5)
Expand Down Expand Up @@ -162,6 +171,10 @@ GEM
pay (3.0.24)
rails (>= 6.0.0)
pg (1.4.1)
premailer (1.16.0)
addressable
css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
public_suffix (4.0.7)
puma (5.6.4)
nio4r (~> 2.0)
Expand Down Expand Up @@ -269,6 +282,7 @@ PLATFORMS
DEPENDENCIES
acts-as-taggable-on (~> 7.0)
aws-sdk-s3
bootstrap-email
byebug
capybara (>= 3.26)
dalli
Expand Down
Binary file added rails/app/assets/images/email-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions rails/app/mailers/devise_bootstrap_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#
# See also test/mailers/previews/devise_bootstrap_mailer_preview
# Based on https://github.com/bootstrap-email/bootstrap-email/issues/41
#
class DeviseBootstrapMailer < Devise::Mailer

layout 'bootstrap-mailer'
default template_path: 'devise/mailer'

def devise_mail(record, action, opts = {}, &block)
initialize_from_record(record)

@email_title = email_title_for(action)

# Use bootstrap mail
make_bootstrap_mail(headers_for(action, opts.merge(to: record.pretty_email)), &block)
end

private

# See docker/bundle/ruby/3.1.0/gems/devise-4.8.1/lib/devise/mailers/helpers.rb
# (IIUC the more correct way to change the email subject wording would be to
# create an I18n locale file but let's save that for another day.)

# Save the method from the base class so we can use it below
alias_method :orig_subject_for, :subject_for

def email_title_for(action)
orig_subject_for(action).
sub(/Changed$/, "change notification").
sub(/^Confirmation/, "Signup confirmation")
end

def subject_for(action)
"Tiddlyhost #{email_title_for(action).downcase}"
end

end
8 changes: 8 additions & 0 deletions rails/app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ def username_or_email
username.presence || email
end

def short_name
name.split(/\s+/).first
end

def pretty_email
%{"#{name}" <#{email}>}
end

scope :with_plan, ->(*plan_names) { where( plan_id: Plan.where(name: plan_names.map(&:to_s)).pluck(:id)) }
scope :without_plan, ->(*plan_names) { where.not(plan_id: Plan.where(name: plan_names.map(&:to_s)).pluck(:id)) }

Expand Down
2 changes: 2 additions & 0 deletions rails/app/views/devise/mailer/_button_link.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
%p.pt-2
=link_to link_text, link_url, class: "btn btn-success", style: "font-weight: bold;"
4 changes: 4 additions & 0 deletions rails/app/views/devise/mailer/_salutation.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
%p
Hi #{@resource.short_name},


3 changes: 3 additions & 0 deletions rails/app/views/devise/mailer/_show_email.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
%p.pt-4
%small
Your Tiddlyhost account email address is <b>#{@resource.email}</b>.

This file was deleted.

18 changes: 18 additions & 0 deletions rails/app/views/devise/mailer/confirmation_instructions.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
=render 'salutation'

%p
Welcome to Tiddlyhost!

%p
To complete the signup process please confirm your account
email address by clicking 'Confirm account' below.

=render 'button_link', link_text: 'Confirm account', link_url: confirmation_url(@resource, confirmation_token: @token)

=render 'show_email'

%p.pt-5
%small.text-gray-600
You received this email because you or someone submitted the
=link_to 'Tiddlyhost sign up', "#{Settings.main_site_url}/users/sign_up", target: "_blank"
form using this email address.
7 changes: 0 additions & 7 deletions rails/app/views/devise/mailer/email_changed.html.erb

This file was deleted.

12 changes: 12 additions & 0 deletions rails/app/views/devise/mailer/email_changed.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-if @resource&.unconfirmed_email?
%p
This is a notification to inform you that your Tiddlyhost account email address
is being changed from <b>#{@resource.email}</b> to <b>#{@resource.unconfirmed_email}</b>.

%p
A confirmation from the new email address is required to verify the change.

-else
%p
This is a notification to inform you that that your Tiddlyhost account email address
has been successfully changed to <b>#{@resource.email}</b>.
3 changes: 0 additions & 3 deletions rails/app/views/devise/mailer/password_change.html.erb

This file was deleted.

3 changes: 3 additions & 0 deletions rails/app/views/devise/mailer/password_change.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
%p
This is a notification to inform you that that your Tiddlyhost account
password has been successfully changed.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
=render 'salutation'

%p
You can set a new Tiddlyhost account password by clicking 'Change password' below.

=render 'button_link', link_text: 'Change password', link_url: edit_password_url(@resource, reset_password_token: @token)

=render 'show_email'

%p.pt-4
%small
If you didn't request a password reset please ignore this email.

%p.pt-5
%small.text-gray-600
You received this email because you or someone submitted the
=link_to 'Forgot password?', "#{Settings.main_site_url}/users/password/new", target: "_blank"
form using this email address.
7 changes: 0 additions & 7 deletions rails/app/views/devise/mailer/unlock_instructions.html.erb

This file was deleted.

11 changes: 11 additions & 0 deletions rails/app/views/devise/mailer/unlock_instructions.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
=render 'salutation'

%p
Your Tiddlyhost account has been locked due to an excessive number of unsuccessful login attempts.

%p
To unlock your account click the 'Unlock account' link below.

=render 'button_link', link_text: 'Unlock account', link_url: unlock_url(@resource, unlock_token: @token)

=render 'show_email'
40 changes: 40 additions & 0 deletions rails/app/views/layouts/bootstrap-mailer.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<%#
#
# This will be converted into email friendly html with tables and inlined styles.
# The email content is defined under app/views/devise/mailer.
#
# See also:
# - https://bootstrapemail.com/
# - app/mailers/devise_bootstrap_mailer.rb
# - test/mailers/previews/devise_bootstrap_mailer_preview.rb
# - http://tiddlyhost.local:3333/rails/mailers/devise_bootstrap_mailer
# - or https://tiddlyhost.local/rails/mailers/devise_bootstrap_mailer
#
-%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
</head>
<body class="bg-light">
<div class="container">
<div class="card my-10">
<div class="card-header" style="background-color: #3c60b9; color: white; height: 2.5em;" class="navbar nav bg-gradient">
<%= link_to Settings.main_site_url, target: '_blank' do %>
<%= image_tag("email-banner", style: "display: inline; height: 100%;") %>
<% end %>
</div>
<div class="card-body">
<% if @email_title.present? %><h1 class="h3 mb-2 mt-2"><%= @email_title %></h1><% end %>
<div class="space-y-3 p-2 pt-5">
<%= yield %>
</div>
</div>
</div>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions rails/config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

# Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer'
config.mailer = 'DeviseBootstrapMailer'

# Configure the parent class responsible to send e-mails.
# config.parent_mailer = 'ActionMailer::Base'
Expand Down
2 changes: 1 addition & 1 deletion rails/test/integration/user_signup_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class UserSignupTest < CapybaraIntegrationTest
assert_equal [email], confirmation_email.to

# Extract the confirmation link from the email and click it
confirmation_link = confirmation_email.body.match(/href="([^"]+)"/)[1]
confirmation_link = confirmation_email.body.encoded.match(%r{href="([^"]+)">Confirm account</a>})[1]
visit confirmation_link

# Login
Expand Down
30 changes: 30 additions & 0 deletions rails/test/mailers/devise_bootstrap_mailer_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "test_helper"

class DeviseBootstrapMailerTest < ActionMailer::TestCase

def setup
@user = users(:bobby)
@token = 'abc123'
end

test 'smoke test' do
{
confirmation_instructions: [@user, @token],
reset_password_instructions: [@user, @token],
unlock_instructions: [@user, @token],
email_changed: [@user],
password_change: [@user],

}.each do |email_type, params|
email = DeviseBootstrapMailer.send(email_type, *params)
assert_emails 1 do
email.deliver_later
end

assert_equal [@user.email], email.to
assert_match /Tiddlyhost /, email.subject
assert_match '<div class="card-header" style=', email.body.encoded
end
end

end
34 changes: 34 additions & 0 deletions rails/test/mailers/previews/devise_bootstrap_mailer_preview.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#
# To preview emails:
# - http://tiddlyhost.local:3333/rails/mailers/devise_bootstrap_mailer
# - or https://tiddlyhost.local/rails/mailers/devise_bootstrap_mailer
#
class DeviseBootstrapMailerPreview < ActionMailer::Preview

def initialize(params = {})
@user = User.first
@token = 'abc123'
super
end

def confirmation_instructions
DeviseBootstrapMailer.confirmation_instructions(@user, @token)
end

def reset_password_instructions
DeviseBootstrapMailer.reset_password_instructions(@user, @token)
end

def unlock_instructions
DeviseBootstrapMailer.unlock_instructions(@user, @token)
end

def email_changed
DeviseBootstrapMailer.email_changed(@user)
end

def password_change
DeviseBootstrapMailer.password_change(@user)
end

end

0 comments on commit a82093f

Please sign in to comment.