Moved Email components into a module

This commit is contained in:
Robin Ward 2013-06-10 15:33:37 -04:00
parent 78000fe870
commit 93bbe190c0
22 changed files with 203 additions and 189 deletions

View file

@ -1,4 +1,4 @@
require_dependency 'email_renderer' require_dependency 'email/renderer'
class Admin::EmailController < Admin::AdminController class Admin::EmailController < Admin::AdminController
@ -30,7 +30,7 @@ class Admin::EmailController < Admin::AdminController
def preview_digest def preview_digest
params.require(:last_seen_at) params.require(:last_seen_at)
renderer = EmailRenderer.new(UserNotifications.digest(current_user, since: params[:last_seen_at]), html_template: true) renderer = Email::Renderer.new(UserNotifications.digest(current_user, since: params[:last_seen_at]), html_template: true)
render json: MultiJson.dump(html_content: renderer.html, text_content: renderer.text) render json: MultiJson.dump(html_content: renderer.html, text_content: renderer.text)
end end

View file

@ -1,8 +1,8 @@
require_dependency 'email_builder' require_dependency 'email/builder'
class InviteMailer < ActionMailer::Base class InviteMailer < ActionMailer::Base
default charset: 'UTF-8' default charset: 'UTF-8'
include EmailBuilder include Email::Builder
def send_invite(invite) def send_invite(invite)

View file

@ -1,8 +1,8 @@
require_dependency 'email_builder' require_dependency 'email/builder'
class TestMailer < ActionMailer::Base class TestMailer < ActionMailer::Base
default charset: 'UTF-8' default charset: 'UTF-8'
include EmailBuilder include Email::Builder
def send_test(to_address) def send_test(to_address)
build_email to_address, 'test_mailer' build_email to_address, 'test_mailer'

View file

@ -1,10 +1,10 @@
require_dependency 'markdown_linker' require_dependency 'markdown_linker'
require_dependency 'email_builder' require_dependency 'email/builder'
class UserNotifications < ActionMailer::Base class UserNotifications < ActionMailer::Base
default charset: 'UTF-8' default charset: 'UTF-8'
include EmailBuilder include Email::Builder
def signup(user, opts={}) def signup(user, opts={})
build_email(user.email, "user_notifications.signup", email_token: opts[:email_token]) build_email(user.email, "user_notifications.signup", email_token: opts[:email_token])

View file

@ -1,4 +1,8 @@
require 'mail' require 'mail'
require_dependency 'email/builder'
require_dependency 'email/renderer'
require_dependency 'email/sender'
require_dependency 'email/styles'
module Email module Email

32
lib/email/builder.rb Normal file
View file

@ -0,0 +1,32 @@
# Help us build an email
module Email
module Builder
def build_email(to, email_key, params={})
params[:site_name] = SiteSetting.title
params[:base_url] = Discourse.base_url
params[:user_preferences_url] = "#{Discourse.base_url}/user_preferences"
body = I18n.t("#{email_key}.text_body_template", params)
# Are we appending an unsubscribe link?
if params[:add_unsubscribe_link]
body << "\n"
body << I18n.t("unsubscribe_link", params)
headers 'List-Unsubscribe' => "<#{params[:user_preferences_url]}>"
end
mail_args = {
to: to,
subject: I18n.t("#{email_key}.subject_template", params),
body: body
}
mail_args[:from] = params[:from] || SiteSetting.notification_email
mail_args[:charset] = 'UTF-8'
mail(mail_args)
end
end
end

38
lib/email/renderer.rb Normal file
View file

@ -0,0 +1,38 @@
require_dependency 'email/styles'
module Email
class Renderer
def initialize(message, opts=nil)
@message = message
@opts = opts || {}
end
def text
@text ||= @message.body.to_s.force_encoding('UTF-8')
end
def logo_url
logo_url = SiteSetting.logo_url
if logo_url !~ /http(s)?\:\/\//
logo_url = "#{Discourse.base_url}#{logo_url}"
end
logo_url
end
def html
cooked = PrettyText.cook(text, environment: 'email')
if @opts[:html_template]
ActionView::Base.new(Rails.configuration.paths["app/views"]).render(
template: 'email/template',
format: :html,
locals: { html_body: Email::Styles.new(cooked).format, logo_url: logo_url }
)
else
cooked
end
end
end
end

49
lib/email/sender.rb Normal file
View file

@ -0,0 +1,49 @@
#
# A helper class to send an email. It will also handle a nil message, which it considers
# to be "do nothing". This is because some Mailers will decide not to do work for some
# reason. For example, emailing a user too frequently. A nil to address is also considered
# "do nothing"
#
# It also adds an HTML part for the plain text body
#
require_dependency 'email/renderer'
module Email
class Sender
def initialize(message, email_type, user=nil)
@message = message
@email_type = email_type
@user = user
end
def send
return if @message.blank?
return if @message.to.blank?
return if @message.body.blank?
@message.charset = 'UTF-8'
opts = {}
# Only use the html template on digest emails
opts[:html_template] = true if (@email_type == 'digest')
renderer = Email::Renderer.new(@message, opts)
@message.html_part = Mail::Part.new do
content_type 'text/html; charset=UTF-8'
body renderer.html
end
@message.text_part.content_type = 'text/plain; charset=UTF-8'
@message.deliver
to_address = @message.to
to_address = to_address.first if to_address.is_a?(Array)
EmailLog.create!(email_type: @email_type, to_address: to_address, user_id: @user.try(:id))
end
end
end

44
lib/email/styles.rb Normal file
View file

@ -0,0 +1,44 @@
#
# HTML emails don't support CSS, so we can use nokogiri to inline attributes based on
# matchers.
#
module Email
class Styles
def initialize(html)
@html = html
end
def format
fragment = Nokogiri::HTML.fragment(@html)
fragment.css('h3').each do |h3|
h3['style'] = 'margin: 15px 0 20px 0; border-bottom: 1px solid #ddd;'
end
fragment.css('hr').each do |hr|
hr['style'] = 'background-color: #ddd; height: 1px; border: 1px;'
end
fragment.css('a').each do |a|
a['style'] = 'text-decoration: none; font-weight: bold; font-size: 15px; color: #006699;'
end
fragment.css('ul').each do |ul|
ul['style'] = 'margin: 0 0 0 10px; padding: 0 0 0 20px;'
end
fragment.css('li').each do |li|
li['style'] = 'padding-bottom: 10px'
end
fragment.css('pre').each do |pre|
pre.replace(pre.text)
end
fragment.to_html
end
end
end

View file

@ -1,28 +0,0 @@
# Help us build an email
module EmailBuilder
def build_email(to, email_key, params={})
params[:site_name] = SiteSetting.title
params[:base_url] = Discourse.base_url
params[:user_preferences_url] = "#{Discourse.base_url}/user_preferences"
body = I18n.t("#{email_key}.text_body_template", params)
# Are we appending an unsubscribe link?
if params[:add_unsubscribe_link]
body << "\n"
body << I18n.t("unsubscribe_link", params)
headers 'List-Unsubscribe' => "<#{params[:user_preferences_url]}>"
end
mail_args = {
to: to,
subject: I18n.t("#{email_key}.subject_template", params),
body: body
}
mail_args[:from] = params[:from] || SiteSetting.notification_email
mail_args[:charset] = 'UTF-8'
mail(mail_args)
end
end

View file

@ -1,36 +0,0 @@
require_dependency 'email_styles'
class EmailRenderer
def initialize(message, opts=nil)
@message = message
@opts = opts || {}
end
def text
@text ||= @message.body.to_s.force_encoding('UTF-8')
end
def logo_url
logo_url = SiteSetting.logo_url
if logo_url !~ /http(s)?\:\/\//
logo_url = "#{Discourse.base_url}#{logo_url}"
end
logo_url
end
def html
cooked = PrettyText.cook(text, environment: 'email')
if @opts[:html_template]
ActionView::Base.new(Rails.configuration.paths["app/views"]).render(
template: 'email/template',
format: :html,
locals: { html_body: EmailStyles.new(cooked).format, logo_url: logo_url }
)
else
cooked
end
end
end

View file

@ -1,47 +0,0 @@
#
# A helper class to send an email. It will also handle a nil message, which it considers
# to be "do nothing". This is because some Mailers will decide not to do work for some
# reason. For example, emailing a user too frequently. A nil to address is also considered
# "do nothing"
#
# It also adds an HTML part for the plain text body
#
require_dependency 'email_renderer'
class EmailSender
def initialize(message, email_type, user=nil)
@message = message
@email_type = email_type
@user = user
end
def send
return if @message.blank?
return if @message.to.blank?
return if @message.body.blank?
@message.charset = 'UTF-8'
opts = {}
# Only use the html template on digest emails
opts[:html_template] = true if (@email_type == 'digest')
renderer = EmailRenderer.new(@message, opts)
@message.html_part = Mail::Part.new do
content_type 'text/html; charset=UTF-8'
body renderer.html
end
@message.text_part.content_type = 'text/plain; charset=UTF-8'
@message.deliver
to_address = @message.to
to_address = to_address.first if to_address.is_a?(Array)
EmailLog.create!(email_type: @email_type, to_address: to_address, user_id: @user.try(:id))
end
end

View file

@ -1,42 +0,0 @@
#
# HTML emails don't support CSS, so we can use nokogiri to inline attributes based on
# matchers.
#
class EmailStyles
def initialize(html)
@html = html
end
def format
fragment = Nokogiri::HTML.fragment(@html)
fragment.css('h3').each do |h3|
h3['style'] = 'margin: 15px 0 20px 0; border-bottom: 1px solid #ddd;'
end
fragment.css('hr').each do |hr|
hr['style'] = 'background-color: #ddd; height: 1px; border: 1px;'
end
fragment.css('a').each do |a|
a['style'] = 'text-decoration: none; font-weight: bold; font-size: 15px; color: #006699;'
end
fragment.css('ul').each do |ul|
ul['style'] = 'margin: 0 0 0 10px; padding: 0 0 0 20px;'
end
fragment.css('li').each do |li|
li['style'] = 'padding-bottom: 10px'
end
fragment.css('pre').each do |pre|
pre.replace(pre.text)
end
fragment.to_html
end
end

View file

@ -1,4 +1,4 @@
require_dependency 'email_sender' require_dependency 'email/sender'
module Jobs module Jobs
@ -10,7 +10,7 @@ module Jobs
invite = Invite.where(id: args[:invite_id]).first invite = Invite.where(id: args[:invite_id]).first
message = InviteMailer.send_invite(invite) message = InviteMailer.send_invite(invite)
EmailSender.new(message, :invite).send Email::Sender.new(message, :invite).send
end end
end end

View file

@ -1,4 +1,4 @@
require_dependency 'email_sender' require_dependency 'email/sender'
module Jobs module Jobs
@ -10,7 +10,7 @@ module Jobs
raise Discourse::InvalidParameters.new(:to_address) unless args[:to_address].present? raise Discourse::InvalidParameters.new(:to_address) unless args[:to_address].present?
message = TestMailer.send_test(args[:to_address]) message = TestMailer.send_test(args[:to_address])
EmailSender.new(message, :test_message).send Email::Sender.new(message, :test_message).send
end end
end end

View file

@ -1,4 +1,4 @@
require_dependency 'email_sender' require_dependency 'email/sender'
module Jobs module Jobs
@ -69,7 +69,7 @@ module Jobs
message.to = [args[:to_address]] message.to = [args[:to_address]]
end end
EmailSender.new(message, args[:type], user).send Email::Sender.new(message, args[:type], user).send
end end

View file

@ -1,23 +1,23 @@
require 'spec_helper' require 'spec_helper'
require 'email_sender' require 'email/sender'
describe EmailSender do describe Email::Sender do
it "doesn't deliver mail when the message is nil" do it "doesn't deliver mail when the message is nil" do
Mail::Message.any_instance.expects(:deliver).never Mail::Message.any_instance.expects(:deliver).never
EmailSender.new(nil, :hello).send Email::Sender.new(nil, :hello).send
end end
it "doesn't deliver when the to address is nil" do it "doesn't deliver when the to address is nil" do
message = Mail::Message.new(body: 'hello') message = Mail::Message.new(body: 'hello')
message.expects(:deliver).never message.expects(:deliver).never
EmailSender.new(message, :hello).send Email::Sender.new(message, :hello).send
end end
it "doesn't deliver when the body is nil" do it "doesn't deliver when the body is nil" do
message = Mail::Message.new(to: 'eviltrout@test.domain') message = Mail::Message.new(to: 'eviltrout@test.domain')
message.expects(:deliver).never message.expects(:deliver).never
EmailSender.new(message, :hello).send Email::Sender.new(message, :hello).send
end end
context 'with a valid message' do context 'with a valid message' do
@ -29,7 +29,7 @@ describe EmailSender do
message message
end end
let(:email_sender) { EmailSender.new(message, :valid_type) } let(:email_sender) { Email::Sender.new(message, :valid_type) }
it 'calls deliver' do it 'calls deliver' do
message.expects(:deliver).once message.expects(:deliver).once
@ -91,7 +91,7 @@ describe EmailSender do
end end
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:email_sender) { EmailSender.new(message, :valid_type, user) } let(:email_sender) { Email::Sender.new(message, :valid_type, user) }
before do before do
email_sender.send email_sender.send

View file

@ -1,16 +1,16 @@
require 'spec_helper' require 'spec_helper'
require 'email' require 'email'
describe EmailStyles do describe Email::Styles do
def style_exists(html, css_rule) def style_exists(html, css_rule)
fragment = Nokogiri::HTML.fragment(EmailStyles.new(html).format) fragment = Nokogiri::HTML.fragment(Email::Styles.new(html).format)
element = fragment.at(css_rule) element = fragment.at(css_rule)
expect(element["style"]).not_to be_blank expect(element["style"]).not_to be_blank
end end
it "returns blank from an empty string" do it "returns blank from an empty string" do
EmailStyles.new("").format.should be_blank Email::Styles.new("").format.should be_blank
end end
it "attaches a style to h3 tags" do it "attaches a style to h3 tags" do
@ -34,7 +34,7 @@ describe EmailStyles do
end end
it "removes pre tags but keeps their contents" do it "removes pre tags but keeps their contents" do
expect(EmailStyles.new("<pre>hello</pre>").format).to eq("hello") expect(Email::Styles.new("<pre>hello</pre>").format).to eq("hello")
end end
end end

View file

@ -15,7 +15,7 @@ describe Jobs::InviteEmail do
let (:invite) { Fabricate(:invite) } let (:invite) { Fabricate(:invite) }
it 'delegates to the test mailer' do it 'delegates to the test mailer' do
EmailSender.any_instance.expects(:send) Email::Sender.any_instance.expects(:send)
InviteMailer.expects(:send_invite).with(invite).returns(mailer) InviteMailer.expects(:send_invite).with(invite).returns(mailer)
Jobs::InviteEmail.new.execute(invite_id: invite.id) Jobs::InviteEmail.new.execute(invite_id: invite.id)
end end

View file

@ -13,7 +13,7 @@ describe Jobs::TestEmail do
let (:mailer) { Mail::Message.new(to: 'eviltrout@test.domain') } let (:mailer) { Mail::Message.new(to: 'eviltrout@test.domain') }
it 'delegates to the test mailer' do it 'delegates to the test mailer' do
EmailSender.any_instance.expects(:send) Email::Sender.any_instance.expects(:send)
TestMailer.expects(:send_test).with('eviltrout@test.domain').returns(mailer) TestMailer.expects(:send_test).with('eviltrout@test.domain').returns(mailer)
Jobs::TestEmail.new.execute(to_address: 'eviltrout@test.domain') Jobs::TestEmail.new.execute(to_address: 'eviltrout@test.domain')
end end

View file

@ -31,7 +31,7 @@ describe Jobs::UserEmail do
context 'to_address' do context 'to_address' do
it 'overwrites a to_address when present' do it 'overwrites a to_address when present' do
UserNotifications.expects(:authorize_email).returns(mailer) UserNotifications.expects(:authorize_email).returns(mailer)
EmailSender.any_instance.expects(:send) Email::Sender.any_instance.expects(:send)
Jobs::UserEmail.new.execute(type: :authorize_email, user_id: user.id, to_address: 'jake@adventuretime.ooo') Jobs::UserEmail.new.execute(type: :authorize_email, user_id: user.id, to_address: 'jake@adventuretime.ooo')
mailer.to.should == ['jake@adventuretime.ooo'] mailer.to.should == ['jake@adventuretime.ooo']
end end
@ -42,7 +42,7 @@ describe Jobs::UserEmail do
it "doesn't send an email to a user that's been recently seen" do it "doesn't send an email to a user that's been recently seen" do
user.update_column(:last_seen_at, 9.minutes.ago) user.update_column(:last_seen_at, 9.minutes.ago)
EmailSender.any_instance.expects(:send).never Email::Sender.any_instance.expects(:send).never
Jobs::UserEmail.new.execute(type: :private_message, user_id: user.id, post_id: post.id) Jobs::UserEmail.new.execute(type: :private_message, user_id: user.id, post_id: post.id)
end end
end end
@ -51,7 +51,7 @@ describe Jobs::UserEmail do
it 'passes a token as an argument when a token is present' do it 'passes a token as an argument when a token is present' do
UserNotifications.expects(:forgot_password).with(user, {email_token: 'asdfasdf'}).returns(mailer) UserNotifications.expects(:forgot_password).with(user, {email_token: 'asdfasdf'}).returns(mailer)
EmailSender.any_instance.expects(:send) Email::Sender.any_instance.expects(:send)
Jobs::UserEmail.new.execute(type: :forgot_password, user_id: user.id, email_token: 'asdfasdf') Jobs::UserEmail.new.execute(type: :forgot_password, user_id: user.id, email_token: 'asdfasdf')
end end
@ -60,18 +60,18 @@ describe Jobs::UserEmail do
it 'passes a post as an argument when a post_id is present' do it 'passes a post as an argument when a post_id is present' do
UserNotifications.expects(:private_message).with(user, {post: post}).returns(mailer) UserNotifications.expects(:private_message).with(user, {post: post}).returns(mailer)
EmailSender.any_instance.expects(:send) Email::Sender.any_instance.expects(:send)
Jobs::UserEmail.new.execute(type: :private_message, user_id: user.id, post_id: post.id) Jobs::UserEmail.new.execute(type: :private_message, user_id: user.id, post_id: post.id)
end end
it "doesn't send the email if you've seen the post" do it "doesn't send the email if you've seen the post" do
EmailSender.any_instance.expects(:send).never Email::Sender.any_instance.expects(:send).never
PostTiming.record_timing(topic_id: post.topic_id, user_id: user.id, post_number: post.post_number, msecs: 6666) PostTiming.record_timing(topic_id: post.topic_id, user_id: user.id, post_number: post.post_number, msecs: 6666)
Jobs::UserEmail.new.execute(type: :private_message, user_id: user.id, post_id: post.id) Jobs::UserEmail.new.execute(type: :private_message, user_id: user.id, post_id: post.id)
end end
it "doesn't send the email if the user deleted the post" do it "doesn't send the email if the user deleted the post" do
EmailSender.any_instance.expects(:send).never Email::Sender.any_instance.expects(:send).never
post.update_column(:user_deleted, true) post.update_column(:user_deleted, true)
Jobs::UserEmail.new.execute(type: :private_message, user_id: user.id, post_id: post.id) Jobs::UserEmail.new.execute(type: :private_message, user_id: user.id, post_id: post.id)
end end
@ -84,19 +84,19 @@ describe Jobs::UserEmail do
let!(:notification) { Fabricate(:notification, user: user, topic: post.topic, post_number: post.post_number)} let!(:notification) { Fabricate(:notification, user: user, topic: post.topic, post_number: post.post_number)}
it 'passes a notification as an argument when a notification_id is present' do it 'passes a notification as an argument when a notification_id is present' do
EmailSender.any_instance.expects(:send) Email::Sender.any_instance.expects(:send)
UserNotifications.expects(:user_mentioned).with(user, notification: notification, post: post).returns(mailer) UserNotifications.expects(:user_mentioned).with(user, notification: notification, post: post).returns(mailer)
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id) Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id)
end end
it "doesn't send the email if the notification has been seen" do it "doesn't send the email if the notification has been seen" do
EmailSender.any_instance.expects(:send).never Email::Sender.any_instance.expects(:send).never
notification.update_column(:read, true) notification.update_column(:read, true)
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id) Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id)
end end
it "doesn't send the email if the post has been user deleted" do it "doesn't send the email if the post has been user deleted" do
EmailSender.any_instance.expects(:send).never Email::Sender.any_instance.expects(:send).never
post.update_column(:user_deleted, true) post.update_column(:user_deleted, true)
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id) Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id)
end end