FEATURE: customization of html emails (#7934)
This feature adds the ability to customize the HTML part of all emails using a custom HTML template and optionally some CSS to style it. The CSS will be parsed and converted into inline styles because CSS is poorly supported by email clients. When writing the custom HTML and CSS, be aware of what email clients support. Keep customizations very simple. Customizations can be added and edited in Admin > Customize > Email Style. Since the summary email is already heavily styled, there is a setting to disable custom styles for summary emails called "apply custom styles to digest" found in Admin > Settings > Email. As part of this work, RTL locales are now rendered correctly for all emails.
This commit is contained in:
parent
340173eb12
commit
9656a21fdb
1
Gemfile
1
Gemfile
|
@ -78,6 +78,7 @@ gem 'discourse_image_optim', require: 'image_optim'
|
||||||
gem 'multi_json'
|
gem 'multi_json'
|
||||||
gem 'mustache'
|
gem 'mustache'
|
||||||
gem 'nokogiri'
|
gem 'nokogiri'
|
||||||
|
gem 'css_parser', require: false
|
||||||
|
|
||||||
gem 'omniauth'
|
gem 'omniauth'
|
||||||
gem 'omniauth-openid'
|
gem 'omniauth-openid'
|
||||||
|
|
|
@ -88,6 +88,8 @@ GEM
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
crass (1.0.4)
|
crass (1.0.4)
|
||||||
|
css_parser (1.7.0)
|
||||||
|
addressable
|
||||||
debug_inspector (0.0.3)
|
debug_inspector (0.0.3)
|
||||||
diff-lcs (1.3)
|
diff-lcs (1.3)
|
||||||
diffy (3.3.0)
|
diffy (3.3.0)
|
||||||
|
@ -438,6 +440,7 @@ DEPENDENCIES
|
||||||
certified
|
certified
|
||||||
colored2
|
colored2
|
||||||
cppjieba_rb
|
cppjieba_rb
|
||||||
|
css_parser
|
||||||
diffy
|
diffy
|
||||||
discourse-ember-source (~> 3.10.0)
|
discourse-ember-source (~> 3.10.0)
|
||||||
discourse_image_optim
|
discourse_image_optim
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
|
export default RestAdapter.extend({
|
||||||
|
pathFor() {
|
||||||
|
return "/admin/customize/email_style";
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
editorId: Ember.computed.reads("fieldName"),
|
||||||
|
|
||||||
|
@computed("fieldName", "styles.html", "styles.css")
|
||||||
|
resetDisabled(fieldName) {
|
||||||
|
return (
|
||||||
|
this.get(`styles.${fieldName}`) ===
|
||||||
|
this.get(`styles.default_${fieldName}`)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("styles", "fieldName")
|
||||||
|
editorContents: {
|
||||||
|
get(styles, fieldName) {
|
||||||
|
return styles[fieldName];
|
||||||
|
},
|
||||||
|
set(value, styles, fieldName) {
|
||||||
|
styles.setField(fieldName, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
reset() {
|
||||||
|
bootbox.confirm(
|
||||||
|
I18n.t("admin.customize.email_style.reset_confirm", {
|
||||||
|
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`)
|
||||||
|
}),
|
||||||
|
I18n.t("no_value"),
|
||||||
|
I18n.t("yes_value"),
|
||||||
|
result => {
|
||||||
|
if (result) {
|
||||||
|
this.styles.setField(
|
||||||
|
this.fieldName,
|
||||||
|
this.styles.get(`default_${this.fieldName}`)
|
||||||
|
);
|
||||||
|
this.notifyPropertyChange("editorContents");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
@computed("model.isSaving")
|
||||||
|
saveButtonText(isSaving) {
|
||||||
|
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("model.changed", "model.isSaving")
|
||||||
|
saveDisabled(changed, isSaving) {
|
||||||
|
return !changed || isSaving;
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
save() {
|
||||||
|
if (!this.model.saving) {
|
||||||
|
this.set("saving", true);
|
||||||
|
this.model
|
||||||
|
.update(this.model.getProperties("html", "css"))
|
||||||
|
.catch(e => {
|
||||||
|
const msg =
|
||||||
|
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
|
||||||
|
? I18n.t("admin.customize.email_style.save_error_with_reason", {
|
||||||
|
error: e.jqXHR.responseJSON.errors.join(". ")
|
||||||
|
})
|
||||||
|
: I18n.t("generic_error");
|
||||||
|
bootbox.alert(msg);
|
||||||
|
})
|
||||||
|
.finally(() => this.set("model.changed", false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
import RestModel from "discourse/models/rest";
|
||||||
|
|
||||||
|
export default RestModel.extend({
|
||||||
|
changed: false,
|
||||||
|
|
||||||
|
setField(fieldName, value) {
|
||||||
|
this.set(`${fieldName}`, value);
|
||||||
|
this.set("changed", true);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
export default Ember.Route.extend({
|
||||||
|
model(params) {
|
||||||
|
return {
|
||||||
|
model: this.modelFor("adminCustomizeEmailStyle"),
|
||||||
|
fieldName: params.field_name
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
controller.setProperties({
|
||||||
|
fieldName: model.fieldName,
|
||||||
|
model: model.model
|
||||||
|
});
|
||||||
|
this._shouldAlertUnsavedChanges = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
willTransition(transition) {
|
||||||
|
if (
|
||||||
|
this.get("controller.model.changed") &&
|
||||||
|
this._shouldAlertUnsavedChanges &&
|
||||||
|
transition.intent.name !== this.routeName
|
||||||
|
) {
|
||||||
|
transition.abort();
|
||||||
|
bootbox.confirm(
|
||||||
|
I18n.t("admin.customize.theme.unsaved_changes_alert"),
|
||||||
|
I18n.t("admin.customize.theme.discard"),
|
||||||
|
I18n.t("admin.customize.theme.stay"),
|
||||||
|
result => {
|
||||||
|
if (!result) {
|
||||||
|
this._shouldAlertUnsavedChanges = false;
|
||||||
|
transition.retry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default Ember.Route.extend({
|
||||||
|
model() {
|
||||||
|
return this.store.find("email-style");
|
||||||
|
},
|
||||||
|
|
||||||
|
redirect() {
|
||||||
|
this.transitionTo("adminCustomizeEmailStyle.edit", "html");
|
||||||
|
}
|
||||||
|
});
|
|
@ -90,6 +90,13 @@ export default function() {
|
||||||
path: "/robots",
|
path: "/robots",
|
||||||
resetNamespace: true
|
resetNamespace: true
|
||||||
});
|
});
|
||||||
|
this.route(
|
||||||
|
"adminCustomizeEmailStyle",
|
||||||
|
{ path: "/email_style", resetNamespace: true },
|
||||||
|
function() {
|
||||||
|
this.route("edit", { path: "/:field_name" });
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div class='row'>
|
||||||
|
<div class='admin-controls'>
|
||||||
|
<nav>
|
||||||
|
<ul class='nav nav-pills'>
|
||||||
|
<li>{{#link-to 'adminCustomizeEmailStyle.edit' 'html' replace=true}}{{i18n 'admin.customize.email_style.html'}}{{/link-to}}</li>
|
||||||
|
<li>{{#link-to 'adminCustomizeEmailStyle.edit' 'css' replace=true}}{{i18n 'admin.customize.email_style.css'}}{{/link-to}}</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ace-editor content=editorContents mode=fieldName editorId=editorId}}
|
||||||
|
|
||||||
|
<div class='admin-footer'>
|
||||||
|
<div class='buttons'>
|
||||||
|
{{#d-button action=(action "reset") disabled=resetDisabled class='btn-default'}}
|
||||||
|
{{i18n 'admin.customize.email_style.reset'}}
|
||||||
|
{{/d-button}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{email-styles-editor styles=model fieldName=fieldName}}
|
||||||
|
|
||||||
|
<div class='admin-footer'>
|
||||||
|
<div class='buttons'>
|
||||||
|
{{#d-button action=(action "save") disabled=saveDisabled class='btn-primary'}}
|
||||||
|
{{saveButtonText}}
|
||||||
|
{{/d-button}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div class='row'>
|
||||||
|
<h2>{{i18n 'admin.customize.email_style.heading'}}</h2>
|
||||||
|
|
||||||
|
<p>{{i18n 'admin.customize.email_style.instructions'}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{outlet}}
|
|
@ -3,6 +3,7 @@
|
||||||
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
||||||
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
||||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||||
|
{{nav-item route='adminCustomizeEmailStyle' label='admin.customize.email_style.title'}}
|
||||||
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
||||||
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
|
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
|
||||||
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
|
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
|
||||||
|
|
|
@ -790,3 +790,18 @@
|
||||||
height: 55vh;
|
height: 55vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-customize-email-style {
|
||||||
|
.ace-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
.ace_editor {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::EmailStylesController < Admin::AdminController
|
||||||
|
def show
|
||||||
|
render_serialized(EmailStyle.new, EmailStyleSerializer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
updater = EmailStyleUpdater.new(current_user)
|
||||||
|
if updater.update(params.require(:email_style).permit(:html, :css))
|
||||||
|
render_serialized(EmailStyle.new, EmailStyleSerializer)
|
||||||
|
else
|
||||||
|
render_json_error(updater.errors, status: 422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'erb'
|
||||||
|
|
||||||
module EmailHelper
|
module EmailHelper
|
||||||
|
|
||||||
def mailing_list_topic(topic, post_count)
|
def mailing_list_topic(topic, post_count)
|
||||||
|
@ -23,6 +25,14 @@ module EmailHelper
|
||||||
raw "<a href='#{Discourse.base_url}#{url}' style='color: ##{@anchor_color}'>#{title}</a>"
|
raw "<a href='#{Discourse.base_url}#{url}' style='color: ##{@anchor_color}'>#{title}</a>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email_html_template(binding_arg)
|
||||||
|
template = EmailStyle.new.html.sub(
|
||||||
|
'%{email_content}',
|
||||||
|
'<%= yield %><% if defined?(html_body) %><%= html_body %><% end %>'
|
||||||
|
)
|
||||||
|
ERB.new(template).result(binding_arg)
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def extract_details(topic)
|
def extract_details(topic)
|
||||||
|
|
|
@ -5,10 +5,7 @@ require_dependency 'email/message_builder'
|
||||||
class InviteMailer < ActionMailer::Base
|
class InviteMailer < ActionMailer::Base
|
||||||
include Email::BuildEmailHelper
|
include Email::BuildEmailHelper
|
||||||
|
|
||||||
class UserNotificationRenderer < ActionView::Base
|
layout 'email_template'
|
||||||
include UserNotificationsHelper
|
|
||||||
include EmailHelper
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_invite(invite)
|
def send_invite(invite)
|
||||||
# Find the first topic they were invited to
|
# Find the first topic they were invited to
|
||||||
|
|
|
@ -12,6 +12,7 @@ class UserNotifications < ActionMailer::Base
|
||||||
include ApplicationHelper
|
include ApplicationHelper
|
||||||
helper :application, :email
|
helper :application, :email
|
||||||
default charset: 'UTF-8'
|
default charset: 'UTF-8'
|
||||||
|
layout 'email_template'
|
||||||
|
|
||||||
include Email::BuildEmailHelper
|
include Email::BuildEmailHelper
|
||||||
|
|
||||||
|
@ -362,11 +363,6 @@ class UserNotifications < ActionMailer::Base
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
class UserNotificationRenderer < ActionView::Base
|
|
||||||
include UserNotificationsHelper
|
|
||||||
include EmailHelper
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.get_context_posts(post, topic_user, user)
|
def self.get_context_posts(post, topic_user, user)
|
||||||
if (user.user_option.email_previous_replies == UserOption.previous_replies_type[:never]) ||
|
if (user.user_option.email_previous_replies == UserOption.previous_replies_type[:never]) ||
|
||||||
SiteSetting.private_email?
|
SiteSetting.private_email?
|
||||||
|
@ -580,15 +576,7 @@ class UserNotifications < ActionMailer::Base
|
||||||
site_description: SiteSetting.site_description
|
site_description: SiteSetting.site_description
|
||||||
)
|
)
|
||||||
|
|
||||||
unless translation_override_exists
|
html = PrettyText.cook(message, sanitize: false).html_safe
|
||||||
html = UserNotificationRenderer.with_view_paths(Rails.configuration.paths["app/views"]).render(
|
|
||||||
template: 'email/invite',
|
|
||||||
format: :html,
|
|
||||||
locals: { message: PrettyText.cook(message, sanitize: false).html_safe,
|
|
||||||
classes: Rtl.new(user).css_class
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
reached_limit = SiteSetting.max_emails_per_day_per_user > 0
|
reached_limit = SiteSetting.max_emails_per_day_per_user > 0
|
||||||
reached_limit &&= (EmailLog.where(user_id: user.id)
|
reached_limit &&= (EmailLog.where(user_id: user.id)
|
||||||
|
@ -608,7 +596,6 @@ class UserNotifications < ActionMailer::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
unless translation_override_exists
|
unless translation_override_exists
|
||||||
|
|
||||||
html = UserNotificationRenderer.with_view_paths(Rails.configuration.paths["app/views"]).render(
|
html = UserNotificationRenderer.with_view_paths(Rails.configuration.paths["app/views"]).render(
|
||||||
template: 'email/notification',
|
template: 'email/notification',
|
||||||
format: :html,
|
format: :html,
|
||||||
|
@ -651,7 +638,6 @@ class UserNotifications < ActionMailer::Base
|
||||||
site_description: SiteSetting.site_description,
|
site_description: SiteSetting.site_description,
|
||||||
site_title: SiteSetting.title,
|
site_title: SiteSetting.title,
|
||||||
site_title_url_encoded: URI.encode(SiteSetting.title),
|
site_title_url_encoded: URI.encode(SiteSetting.title),
|
||||||
style: :notification,
|
|
||||||
locale: locale
|
locale: locale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -689,13 +675,6 @@ class UserNotifications < ActionMailer::Base
|
||||||
@anchor_color = ColorScheme.hex_for_name('tertiary')
|
@anchor_color = ColorScheme.hex_for_name('tertiary')
|
||||||
@markdown_linker = MarkdownLinker.new(@base_url)
|
@markdown_linker = MarkdownLinker.new(@base_url)
|
||||||
@unsubscribe_key = UnsubscribeKey.create_key_for(@user, "digest")
|
@unsubscribe_key = UnsubscribeKey.create_key_for(@user, "digest")
|
||||||
end
|
@disable_email_custom_styles = !SiteSetting.apply_custom_styles_to_digest
|
||||||
|
|
||||||
def apply_notification_styles(email)
|
|
||||||
email.html_part.body = Email::Styles.new(email.html_part.body.to_s).tap do |styles|
|
|
||||||
styles.format_basic
|
|
||||||
styles.format_notification
|
|
||||||
end.to_html
|
|
||||||
email
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EmailStyle
|
||||||
|
include ActiveModel::Serialization
|
||||||
|
|
||||||
|
attr_accessor :html, :css, :default_html, :default_css
|
||||||
|
|
||||||
|
def id
|
||||||
|
'email-style'
|
||||||
|
end
|
||||||
|
|
||||||
|
def html
|
||||||
|
SiteSetting.email_custom_template.presence || default_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def css
|
||||||
|
SiteSetting.email_custom_css || default_css
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_html
|
||||||
|
self.class.default_template
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_css
|
||||||
|
self.class.default_css
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_template
|
||||||
|
@_default_template ||= File.read(
|
||||||
|
File.join(Rails.root, 'app', 'views', 'email', 'default_template.html')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_css
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EmailStyleSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :html, :css, :default_html, :default_css
|
||||||
|
end
|
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EmailStyleUpdater
|
||||||
|
|
||||||
|
attr_reader :errors
|
||||||
|
|
||||||
|
def initialize(user)
|
||||||
|
@user = user
|
||||||
|
@errors = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(attrs)
|
||||||
|
if attrs.has_key?(:html)
|
||||||
|
if attrs[:html] == EmailStyle.default_template
|
||||||
|
SiteSetting.remove_override!(:email_custom_template)
|
||||||
|
else
|
||||||
|
if !attrs[:html].include?('%{email_content}')
|
||||||
|
@errors << I18n.t(
|
||||||
|
'email_style.html_missing_placeholder',
|
||||||
|
placeholder: '%{email_content}'
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SiteSetting.email_custom_template = attrs[:html]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if attrs.has_key?(:css)
|
||||||
|
if attrs[:css] == EmailStyle.default_css
|
||||||
|
SiteSetting.remove_override!(:email_custom_css)
|
||||||
|
else
|
||||||
|
SiteSetting.email_custom_css = attrs[:css]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@errors.empty?
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UserNotificationRenderer < ActionView::Base
|
||||||
|
include ApplicationHelper
|
||||||
|
include UserNotificationsHelper
|
||||||
|
include EmailHelper
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="%{html_lang}" xml:lang="%{html_lang}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-type" name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!--[if mso]>
|
||||||
|
<style type="text/css">
|
||||||
|
body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !important;}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
%{email_content}
|
||||||
|
|
||||||
|
<!-- prevent Gmail on iOS font size manipulation -->
|
||||||
|
<div style="display:none;white-space:nowrap;font:15px courier;line-height:0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,11 +0,0 @@
|
||||||
<div id='main' class=<%= classes %>>
|
|
||||||
|
|
||||||
<div class='header-instructions'>%{header_instructions}</div>
|
|
||||||
|
|
||||||
<% if message.present? %>
|
|
||||||
<div><%= message %></div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class='footer'>%{respond_instructions}</div>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<table cellspacing="0" cellpadding="0">
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;">
|
|
||||||
<a href="<%= Discourse.base_url %>">
|
|
||||||
<img src="<%= logo_url %>" style="max-width:100%"></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="background-color: #fff; padding: 10px 10px; font-family: Arial, Helvetica, sans-serif; font-size: 14px;">
|
|
||||||
<%= raw(html_body) %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<% if @disable_email_custom_styles %>
|
||||||
|
<%= yield %>
|
||||||
|
<% if defined?(html_body) %><%= html_body %><% end %>
|
||||||
|
<% else %>
|
||||||
|
<%= email_html_template(binding).html_safe %>
|
||||||
|
<% end %>
|
|
@ -1,19 +1,4 @@
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
<div class="summary-email">
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="<%= html_lang %>" xml:lang="<%= html_lang %>">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-type" name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body dir="<%= rtl? ? 'rtl' : 'ltr' %>" style="-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;text-align:<%= rtl? ? 'right' : 'left' %>;width:100%">
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso]>
|
|
||||||
<style type="text/css">
|
|
||||||
body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !important;}
|
|
||||||
</style>
|
|
||||||
<![endif]-->
|
|
||||||
|
|
||||||
<span class="preheader" style="display:none!important;color:#f3f3f3;font-size:1px;line-height:1px;max-height:0;max-width:0;mso-hide:all!important;opacity:0;overflow:hidden;visibility:hidden">
|
<span class="preheader" style="display:none!important;color:#f3f3f3;font-size:1px;line-height:1px;max-height:0;max-width:0;mso-hide:all!important;opacity:0;overflow:hidden;visibility:hidden">
|
||||||
<%= @preheader_text %>
|
<%= @preheader_text %>
|
||||||
|
@ -425,10 +410,4 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
|
||||||
|
|
||||||
<%= digest_custom_html("below_footer") %>
|
<%= digest_custom_html("below_footer") %>
|
||||||
|
|
||||||
<!-- prevent Gmail on iOS font size manipulation -->
|
</div>
|
||||||
<div style="display:none;white-space:nowrap;font:15px courier;line-height:0">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -3644,6 +3644,16 @@ en:
|
||||||
title: "Override your site's robots.txt file:"
|
title: "Override your site's robots.txt file:"
|
||||||
warning: "This will permanently override any related site settings."
|
warning: "This will permanently override any related site settings."
|
||||||
overridden: Your site's default robots.txt file is overridden.
|
overridden: Your site's default robots.txt file is overridden.
|
||||||
|
email_style:
|
||||||
|
title: "Email Style"
|
||||||
|
heading: "Customize Email Style"
|
||||||
|
html: "HTML Template"
|
||||||
|
css: "CSS"
|
||||||
|
reset: "Reset to default"
|
||||||
|
reset_confirm: "Are you sure you want to reset to the default %{fieldName} and lose all your changes?"
|
||||||
|
save_error_with_reason: "Your changes were not saved. %{error}"
|
||||||
|
instructions: "Customize the template in which all html emails are rendered, and style using CSS."
|
||||||
|
|
||||||
email:
|
email:
|
||||||
title: "Emails"
|
title: "Emails"
|
||||||
settings: "Settings"
|
settings: "Settings"
|
||||||
|
|
|
@ -1854,6 +1854,7 @@ en:
|
||||||
suppress_digest_email_after_days: "Suppress summary emails for users not seen on the site for more than (n) days."
|
suppress_digest_email_after_days: "Suppress summary emails for users not seen on the site for more than (n) days."
|
||||||
digest_suppress_categories: "Suppress these categories from summary emails."
|
digest_suppress_categories: "Suppress these categories from summary emails."
|
||||||
disable_digest_emails: "Disable summary emails for all users."
|
disable_digest_emails: "Disable summary emails for all users."
|
||||||
|
apply_custom_styles_to_digest: "Custom email template and css are applied to summary emails."
|
||||||
email_accent_bg_color: "The accent color to be used as the background of some elements in HTML emails. Enter a color name ('red') or hex value ('#FF0000')."
|
email_accent_bg_color: "The accent color to be used as the background of some elements in HTML emails. Enter a color name ('red') or hex value ('#FF0000')."
|
||||||
email_accent_fg_color: "The color of text rendered on the email bg color in HTML emails. Enter a color name ('white') or hex value ('#FFFFFF')."
|
email_accent_fg_color: "The color of text rendered on the email bg color in HTML emails. Enter a color name ('white') or hex value ('#FFFFFF')."
|
||||||
email_link_color: "The color of links in HTML emails. Enter a color name ('blue') or hex value ('#0000FF')."
|
email_link_color: "The color of links in HTML emails. Enter a color name ('blue') or hex value ('#0000FF')."
|
||||||
|
@ -4554,3 +4555,6 @@ en:
|
||||||
title: "Delete User"
|
title: "Delete User"
|
||||||
confirm: "Are you sure you want to delete that user? This will remove all of their posts and block their email and IP address."
|
confirm: "Are you sure you want to delete that user? This will remove all of their posts and block their email and IP address."
|
||||||
reason: "Deleted via review queue"
|
reason: "Deleted via review queue"
|
||||||
|
|
||||||
|
email_style:
|
||||||
|
html_missing_placeholder: "The html template must include %{placeholder}"
|
||||||
|
|
|
@ -244,6 +244,9 @@ Discourse::Application.routes.draw do
|
||||||
get 'robots' => 'robots_txt#show'
|
get 'robots' => 'robots_txt#show'
|
||||||
put 'robots.json' => 'robots_txt#update'
|
put 'robots.json' => 'robots_txt#update'
|
||||||
delete 'robots.json' => 'robots_txt#reset'
|
delete 'robots.json' => 'robots_txt#reset'
|
||||||
|
|
||||||
|
resource :email_style, only: [:show, :update]
|
||||||
|
get 'email_style/:field' => 'email_styles#show', constraints: { field: /html|css/ }
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :embeddable_hosts, constraints: AdminConstraint.new
|
resources :embeddable_hosts, constraints: AdminConstraint.new
|
||||||
|
|
|
@ -911,6 +911,7 @@ email:
|
||||||
disable_digest_emails:
|
disable_digest_emails:
|
||||||
default: false
|
default: false
|
||||||
client: true
|
client: true
|
||||||
|
apply_custom_styles_to_digest: true
|
||||||
email_accent_bg_color: "#2F70AC"
|
email_accent_bg_color: "#2F70AC"
|
||||||
email_accent_fg_color: "#FFFFFF"
|
email_accent_fg_color: "#FFFFFF"
|
||||||
email_link_color: "#006699"
|
email_link_color: "#006699"
|
||||||
|
@ -1024,6 +1025,12 @@ email:
|
||||||
enable_forwarded_emails: false
|
enable_forwarded_emails: false
|
||||||
always_show_trimmed_content: false
|
always_show_trimmed_content: false
|
||||||
private_email: false
|
private_email: false
|
||||||
|
email_custom_template:
|
||||||
|
default: ""
|
||||||
|
hidden: true
|
||||||
|
email_custom_css:
|
||||||
|
default: ""
|
||||||
|
hidden: true
|
||||||
email_total_attachment_size_limit_kb:
|
email_total_attachment_size_limit_kb:
|
||||||
default: 0
|
default: 0
|
||||||
max: 51200
|
max: 51200
|
||||||
|
|
|
@ -107,16 +107,17 @@ module Email
|
||||||
html_override.gsub!("%{respond_instructions}", "")
|
html_override.gsub!("%{respond_instructions}", "")
|
||||||
end
|
end
|
||||||
|
|
||||||
styled = Email::Styles.new(html_override, @opts)
|
html = UserNotificationRenderer.with_view_paths(
|
||||||
styled.format_basic
|
Rails.configuration.paths["app/views"]
|
||||||
|
).render(
|
||||||
if style = @opts[:style]
|
template: 'layouts/email_template',
|
||||||
styled.public_send("format_#{style}")
|
format: :html,
|
||||||
end
|
locals: { html_body: html_override.html_safe }
|
||||||
|
)
|
||||||
|
|
||||||
Mail::Part.new do
|
Mail::Part.new do
|
||||||
content_type 'text/html; charset=UTF-8'
|
content_type 'text/html; charset=UTF-8'
|
||||||
body styled.to_html
|
body html
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,15 +17,21 @@ module Email
|
||||||
end
|
end
|
||||||
|
|
||||||
def html
|
def html
|
||||||
if @message.html_part
|
style = if @message.html_part
|
||||||
style = Email::Styles.new(@message.html_part.body.to_s, @opts)
|
Email::Styles.new(@message.html_part.body.to_s, @opts)
|
||||||
style.format_basic
|
|
||||||
style.format_html
|
|
||||||
else
|
else
|
||||||
style = Email::Styles.new(PrettyText.cook(text), @opts)
|
unstyled = UserNotificationRenderer.with_view_paths(
|
||||||
style.format_basic
|
Rails.configuration.paths["app/views"]
|
||||||
|
).render(
|
||||||
|
template: 'layouts/email_template',
|
||||||
|
format: :html,
|
||||||
|
locals: { html_body: PrettyText.cook(text).html_safe }
|
||||||
|
)
|
||||||
|
Email::Styles.new(unstyled, @opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
style.format_basic
|
||||||
|
style.format_html
|
||||||
style.to_html
|
style.to_html
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ module Email
|
||||||
@html = html
|
@html = html
|
||||||
@opts = opts || {}
|
@opts = opts || {}
|
||||||
@fragment = Nokogiri::HTML.fragment(@html)
|
@fragment = Nokogiri::HTML.fragment(@html)
|
||||||
|
@custom_styles = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.register_plugin_style(&block)
|
def self.register_plugin_style(&block)
|
||||||
|
@ -32,6 +33,26 @@ module Email
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_styles
|
||||||
|
return @custom_styles unless @custom_styles.nil?
|
||||||
|
|
||||||
|
css = EmailStyle.new.css
|
||||||
|
@custom_styles = {}
|
||||||
|
|
||||||
|
if !css.blank?
|
||||||
|
require 'css_parser' unless defined?(CssParser)
|
||||||
|
|
||||||
|
parser = CssParser::Parser.new(import: false)
|
||||||
|
parser.load_string!(css)
|
||||||
|
parser.each_selector do |selector, value|
|
||||||
|
@custom_styles[selector] ||= +''
|
||||||
|
@custom_styles[selector] << value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@custom_styles
|
||||||
|
end
|
||||||
|
|
||||||
def format_basic
|
def format_basic
|
||||||
uri = URI(Discourse.base_url)
|
uri = URI(Discourse.base_url)
|
||||||
|
|
||||||
|
@ -83,29 +104,6 @@ module Email
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_notification
|
|
||||||
style('.previous-discussion', 'font-size: 17px; color: #444; margin-bottom:10px;')
|
|
||||||
style('.notification-date', "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px")
|
|
||||||
style('.username', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#{SiteSetting.email_link_color};text-decoration:none;font-weight:bold")
|
|
||||||
style('.user-title', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #999;")
|
|
||||||
style('.user-name', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #{SiteSetting.email_link_color};font-weight:normal;")
|
|
||||||
style('.post-wrapper', "margin-bottom:25px;")
|
|
||||||
style('.user-avatar', 'vertical-align:top;width:55px;')
|
|
||||||
style('.user-avatar img', nil, width: '45', height: '45')
|
|
||||||
style('hr', 'background-color: #ddd; height: 1px; border: 1px;')
|
|
||||||
style('.rtl', 'direction: rtl;')
|
|
||||||
style('div.body', 'padding-top:5px;')
|
|
||||||
style('.whisper div.body', 'font-style: italic; color: #9c9c9c;')
|
|
||||||
style('.lightbox-wrapper .meta', 'display: none')
|
|
||||||
correct_first_body_margin
|
|
||||||
correct_footer_style
|
|
||||||
style('div.undecorated-link-footer a', "font-weight: normal;")
|
|
||||||
correct_footer_style_hilight_first
|
|
||||||
reset_tables
|
|
||||||
onebox_styles
|
|
||||||
plugin_styles
|
|
||||||
end
|
|
||||||
|
|
||||||
def onebox_styles
|
def onebox_styles
|
||||||
# Links to other topics
|
# Links to other topics
|
||||||
style('aside.quote', 'padding: 12px 25px 2px 12px; margin-bottom: 10px;')
|
style('aside.quote', 'padding: 12px 25px 2px 12px; margin-bottom: 10px;')
|
||||||
|
@ -164,6 +162,16 @@ module Email
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_html
|
def format_html
|
||||||
|
html_lang = SiteSetting.default_locale.sub("_", "-")
|
||||||
|
style('html', nil, lang: html_lang, 'xml:lang' => html_lang)
|
||||||
|
style('body', "text-align:#{ Rtl.new(nil).enabled? ? 'right' : 'left' };")
|
||||||
|
style('body', nil, dir: Rtl.new(nil).enabled? ? 'rtl' : 'ltr')
|
||||||
|
|
||||||
|
style('.with-dir',
|
||||||
|
"text-align:#{ Rtl.new(nil).enabled? ? 'right' : 'left' };",
|
||||||
|
dir: Rtl.new(nil).enabled? ? 'rtl' : 'ltr'
|
||||||
|
)
|
||||||
|
|
||||||
style('.with-accent-colors', "background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};")
|
style('.with-accent-colors', "background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};")
|
||||||
style('h4', 'color: #222;')
|
style('h4', 'color: #222;')
|
||||||
style('h3', 'margin: 15px 0 20px 0;')
|
style('h3', 'margin: 15px 0 20px 0;')
|
||||||
|
@ -177,11 +185,39 @@ module Email
|
||||||
style('code', 'background-color: #f1f1ff; padding: 2px 5px;')
|
style('code', 'background-color: #f1f1ff; padding: 2px 5px;')
|
||||||
style('pre code', 'display: block; background-color: #f1f1ff; padding: 5px;')
|
style('pre code', 'display: block; background-color: #f1f1ff; padding: 5px;')
|
||||||
style('.featured-topic a', "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;")
|
style('.featured-topic a', "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;")
|
||||||
|
style('.summary-email', "-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%")
|
||||||
|
|
||||||
|
style('.previous-discussion', 'font-size: 17px; color: #444; margin-bottom:10px;')
|
||||||
|
style('.notification-date', "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px")
|
||||||
|
style('.username', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#{SiteSetting.email_link_color};text-decoration:none;font-weight:bold")
|
||||||
|
style('.user-title', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #999;")
|
||||||
|
style('.user-name', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #{SiteSetting.email_link_color};font-weight:normal;")
|
||||||
|
style('.post-wrapper', "margin-bottom:25px;")
|
||||||
|
style('.user-avatar', 'vertical-align:top;width:55px;')
|
||||||
|
style('.user-avatar img', nil, width: '45', height: '45')
|
||||||
|
style('hr', 'background-color: #ddd; height: 1px; border: 1px;')
|
||||||
|
style('.rtl', 'direction: rtl;')
|
||||||
|
style('div.body', 'padding-top:5px;')
|
||||||
|
style('.whisper div.body', 'font-style: italic; color: #9c9c9c;')
|
||||||
|
style('.lightbox-wrapper .meta', 'display: none')
|
||||||
|
correct_first_body_margin
|
||||||
|
correct_footer_style
|
||||||
|
style('div.undecorated-link-footer a', "font-weight: normal;")
|
||||||
|
correct_footer_style_hilight_first
|
||||||
|
reset_tables
|
||||||
|
|
||||||
onebox_styles
|
onebox_styles
|
||||||
plugin_styles
|
plugin_styles
|
||||||
|
|
||||||
style('.post-excerpt img', "max-width: 50%; max-height: 400px;")
|
style('.post-excerpt img', "max-width: 50%; max-height: 400px;")
|
||||||
|
|
||||||
|
format_custom
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_custom
|
||||||
|
custom_styles.each do |selector, value|
|
||||||
|
style(selector, value)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# this method is reserved for styles specific to plugin
|
# this method is reserved for styles specific to plugin
|
||||||
|
@ -240,7 +276,7 @@ module Email
|
||||||
end
|
end
|
||||||
|
|
||||||
def correct_first_body_margin
|
def correct_first_body_margin
|
||||||
@fragment.css('.body p').each do |element|
|
@fragment.css('div.body p').each do |element|
|
||||||
element['style'] = "margin-top:0; border: 0;"
|
element['style'] = "margin-top:0; border: 0;"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
describe EmailStyle do
|
||||||
|
before do
|
||||||
|
SiteSetting.email_custom_template = "<body><h1>FOR YOU</h1><div>%{email_content}</div></body>"
|
||||||
|
SiteSetting.email_custom_css = 'h1 { color: red; } div.body { color: #FAB; }'
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
SiteSetting.remove_override!(:email_custom_template)
|
||||||
|
SiteSetting.remove_override!(:email_custom_css)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invite' do
|
||||||
|
fab!(:invite) { Fabricate(:invite) }
|
||||||
|
let(:invite_mail) { InviteMailer.send_invite(invite) }
|
||||||
|
|
||||||
|
subject(:mail_html) { Email::Renderer.new(invite_mail).html }
|
||||||
|
|
||||||
|
it 'applies customizations' do
|
||||||
|
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||||
|
expect(mail_html).to match("#{Discourse.base_url}/invites/#{invite.invite_key}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can apply RTL attrs' do
|
||||||
|
SiteSetting.default_locale = 'he'
|
||||||
|
body_attrs = mail_html.match(/<body ([^>])+/)
|
||||||
|
expect(body_attrs[0]&.downcase).to match(/text-align:\s*right/)
|
||||||
|
expect(body_attrs[0]&.downcase).to include('dir="rtl"')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'user_replied' do
|
||||||
|
let(:response_by_user) { Fabricate(:user, name: "John Doe") }
|
||||||
|
let(:category) { Fabricate(:category, name: 'India') }
|
||||||
|
let(:topic) { Fabricate(:topic, category: category, title: "Super cool topic") }
|
||||||
|
let(:post) { Fabricate(:post, topic: topic, raw: 'This is My super duper cool topic') }
|
||||||
|
let(:response) { Fabricate(:basic_reply, topic: post.topic, user: response_by_user) }
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:notification) { Fabricate(:replied_notification, user: user, post: response) }
|
||||||
|
|
||||||
|
let(:mail) do
|
||||||
|
UserNotifications.user_replied(
|
||||||
|
user,
|
||||||
|
post: response,
|
||||||
|
notification_type: notification.notification_type,
|
||||||
|
notification_data_hash: notification.data_hash
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:mail_html) { Email::Renderer.new(mail).html }
|
||||||
|
|
||||||
|
it "customizations are applied to html part of emails" do
|
||||||
|
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||||
|
matches = mail_html.match(/<div style="([^"]+)">#{post.raw}/)
|
||||||
|
expect(matches[1]).to include('color: #FAB;') # custom
|
||||||
|
expect(matches[1]).to include('padding-top:5px;') # div.body
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: translation override
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'signup' do
|
||||||
|
let(:signup_mail) { UserNotifications.signup(Fabricate(:user)) }
|
||||||
|
subject(:mail_html) { Email::Renderer.new(signup_mail).html }
|
||||||
|
|
||||||
|
it "customizations are applied to html part of emails" do
|
||||||
|
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||||
|
expect(mail_html).to include('activate-account')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'translation override' do
|
||||||
|
before do
|
||||||
|
TranslationOverride.upsert!(
|
||||||
|
'en',
|
||||||
|
'user_notifications.signup.text_body_template',
|
||||||
|
"CLICK THAT LINK: %{base_url}/u/activate-account/%{email_token}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
TranslationOverride.revert!('en', ['user_notifications.signup.text_body_template'])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "applies customizations when translation override exists" do
|
||||||
|
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||||
|
expect(mail_html.scan('CLICK THAT LINK').count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with some bad css' do
|
||||||
|
before do
|
||||||
|
SiteSetting.email_custom_css = '@import "nope.css"; h1 {{{ size: really big; '
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can render the html" do
|
||||||
|
expect(mail_html.scan(/<h1\s*(?:style=""){0,1}>FOR YOU<\/h1>/).count).to eq(1)
|
||||||
|
expect(mail_html).to include('activate-account')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'digest' do
|
||||||
|
fab!(:popular_topic) { Fabricate(:topic, user: Fabricate(:coding_horror), created_at: 1.hour.ago) }
|
||||||
|
let(:summary_email) { UserNotifications.digest(Fabricate(:user)) }
|
||||||
|
subject(:mail_html) { Email::Renderer.new(summary_email).html }
|
||||||
|
|
||||||
|
it "customizations are applied to html part of emails" do
|
||||||
|
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||||
|
expect(mail_html).to include(popular_topic.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't apply customizations if apply_custom_styles_to_digest is disabled" do
|
||||||
|
SiteSetting.apply_custom_styles_to_digest = false
|
||||||
|
expect(mail_html).to_not include('<h1 style="color: red;">FOR YOU</h1>')
|
||||||
|
expect(mail_html).to_not include('FOR YOU')
|
||||||
|
expect(mail_html).to include(popular_topic.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -260,7 +260,7 @@ describe UserNotifications do
|
||||||
expect(mail.subject).to match(/Taggo/)
|
expect(mail.subject).to match(/Taggo/)
|
||||||
expect(mail.subject).to match(/Taggie/)
|
expect(mail.subject).to match(/Taggie/)
|
||||||
|
|
||||||
mail_html = mail.html_part.to_s
|
mail_html = mail.html_part.body.to_s
|
||||||
|
|
||||||
expect(mail_html.scan(/My super duper cool topic/).count).to eq(1)
|
expect(mail_html.scan(/My super duper cool topic/).count).to eq(1)
|
||||||
expect(mail_html.scan(/In Reply To/).count).to eq(1)
|
expect(mail_html.scan(/In Reply To/).count).to eq(1)
|
||||||
|
@ -287,7 +287,7 @@ describe UserNotifications do
|
||||||
notification_data_hash: notification.data_hash
|
notification_data_hash: notification.data_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(mail.html_part.to_s.scan(/In Reply To/).count).to eq(0)
|
expect(mail.html_part.body.to_s.scan(/In Reply To/).count).to eq(0)
|
||||||
|
|
||||||
SiteSetting.enable_names = true
|
SiteSetting.enable_names = true
|
||||||
SiteSetting.display_name_on_posts = true
|
SiteSetting.display_name_on_posts = true
|
||||||
|
@ -304,7 +304,7 @@ describe UserNotifications do
|
||||||
notification_data_hash: notification.data_hash
|
notification_data_hash: notification.data_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
mail_html = mail.html_part.to_s
|
mail_html = mail.html_part.body.to_s
|
||||||
expect(mail_html.scan(/>Bob Marley/).count).to eq(1)
|
expect(mail_html.scan(/>Bob Marley/).count).to eq(1)
|
||||||
expect(mail_html.scan(/>bobmarley/).count).to eq(0)
|
expect(mail_html.scan(/>bobmarley/).count).to eq(0)
|
||||||
|
|
||||||
|
@ -317,7 +317,7 @@ describe UserNotifications do
|
||||||
notification_data_hash: notification.data_hash
|
notification_data_hash: notification.data_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
mail_html = mail.html_part.to_s
|
mail_html = mail.html_part.body.to_s
|
||||||
expect(mail_html.scan(/>Bob Marley/).count).to eq(0)
|
expect(mail_html.scan(/>Bob Marley/).count).to eq(0)
|
||||||
expect(mail_html.scan(/>bobmarley/).count).to eq(1)
|
expect(mail_html.scan(/>bobmarley/).count).to eq(1)
|
||||||
end
|
end
|
||||||
|
@ -331,8 +331,8 @@ describe UserNotifications do
|
||||||
notification_data_hash: notification.data_hash
|
notification_data_hash: notification.data_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(mail.html_part.to_s).to_not include(response.raw)
|
expect(mail.html_part.body.to_s).to_not include(response.raw)
|
||||||
expect(mail.html_part.to_s).to_not include(topic.url)
|
expect(mail.html_part.body.to_s).to_not include(topic.url)
|
||||||
expect(mail.text_part.to_s).to_not include(response.raw)
|
expect(mail.text_part.to_s).to_not include(response.raw)
|
||||||
expect(mail.text_part.to_s).to_not include(topic.url)
|
expect(mail.text_part.to_s).to_not include(topic.url)
|
||||||
end
|
end
|
||||||
|
@ -365,10 +365,10 @@ describe UserNotifications do
|
||||||
expect(mail.subject).not_to match(/Uncategorized/)
|
expect(mail.subject).not_to match(/Uncategorized/)
|
||||||
|
|
||||||
# 1 respond to links as no context by default
|
# 1 respond to links as no context by default
|
||||||
expect(mail.html_part.to_s.scan(/to respond/).count).to eq(1)
|
expect(mail.html_part.body.to_s.scan(/to respond/).count).to eq(1)
|
||||||
|
|
||||||
# 1 unsubscribe link
|
# 1 unsubscribe link
|
||||||
expect(mail.html_part.to_s.scan(/To unsubscribe/).count).to eq(1)
|
expect(mail.html_part.body.to_s.scan(/To unsubscribe/).count).to eq(1)
|
||||||
|
|
||||||
# side effect, topic user is updated with post number
|
# side effect, topic user is updated with post number
|
||||||
tu = TopicUser.get(post.topic_id, user)
|
tu = TopicUser.get(post.topic_id, user)
|
||||||
|
@ -384,7 +384,7 @@ describe UserNotifications do
|
||||||
notification_data_hash: notification.data_hash
|
notification_data_hash: notification.data_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(mail.html_part.to_s).to_not include(response.raw)
|
expect(mail.html_part.body.to_s).to_not include(response.raw)
|
||||||
expect(mail.text_part.to_s).to_not include(response.raw)
|
expect(mail.text_part.to_s).to_not include(response.raw)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -451,13 +451,13 @@ describe UserNotifications do
|
||||||
expect(mail.subject).to include("[PM] ")
|
expect(mail.subject).to include("[PM] ")
|
||||||
|
|
||||||
# 1 "visit message" link
|
# 1 "visit message" link
|
||||||
expect(mail.html_part.to_s.scan(/Visit Message/).count).to eq(1)
|
expect(mail.html_part.body.to_s.scan(/Visit Message/).count).to eq(1)
|
||||||
|
|
||||||
# 1 respond to link
|
# 1 respond to link
|
||||||
expect(mail.html_part.to_s.scan(/to respond/).count).to eq(1)
|
expect(mail.html_part.body.to_s.scan(/to respond/).count).to eq(1)
|
||||||
|
|
||||||
# 1 unsubscribe link
|
# 1 unsubscribe link
|
||||||
expect(mail.html_part.to_s.scan(/To unsubscribe/).count).to eq(1)
|
expect(mail.html_part.body.to_s.scan(/To unsubscribe/).count).to eq(1)
|
||||||
|
|
||||||
# side effect, topic user is updated with post number
|
# side effect, topic user is updated with post number
|
||||||
tu = TopicUser.get(topic.id, user)
|
tu = TopicUser.get(topic.id, user)
|
||||||
|
@ -473,8 +473,8 @@ describe UserNotifications do
|
||||||
notification_data_hash: notification.data_hash
|
notification_data_hash: notification.data_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(mail.html_part.to_s).to_not include(response.raw)
|
expect(mail.html_part.body.to_s).to_not include(response.raw)
|
||||||
expect(mail.html_part.to_s).to_not include(topic.url)
|
expect(mail.html_part.body.to_s).to_not include(topic.url)
|
||||||
expect(mail.text_part.to_s).to_not include(response.raw)
|
expect(mail.text_part.to_s).to_not include(response.raw)
|
||||||
expect(mail.text_part.to_s).to_not include(topic.url)
|
expect(mail.text_part.to_s).to_not include(topic.url)
|
||||||
end
|
end
|
||||||
|
@ -635,7 +635,7 @@ describe UserNotifications do
|
||||||
|
|
||||||
# WARNING: you reached the limit of 100 email notifications per day. Further emails will be suppressed.
|
# WARNING: you reached the limit of 100 email notifications per day. Further emails will be suppressed.
|
||||||
# Consider watching less topics or disabling mailing list mode.
|
# Consider watching less topics or disabling mailing list mode.
|
||||||
expect(mail.html_part.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
|
expect(mail.html_part.body.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
|
||||||
expect(mail.body.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
|
expect(mail.body.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::EmailStylesController do
|
||||||
|
fab!(:admin) { Fabricate(:admin) }
|
||||||
|
let(:default_html) { File.read("#{Rails.root}/app/views/email/default_template.html") }
|
||||||
|
let(:default_css) { "" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
SiteSetting.remove_override!(:email_custom_template)
|
||||||
|
SiteSetting.remove_override!(:email_custom_css)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'show' do
|
||||||
|
it 'returns default values' do
|
||||||
|
get '/admin/customize/email_style.json'
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
json = ::JSON.parse(response.body)['email_style']
|
||||||
|
expect(json['html']).to eq(default_html)
|
||||||
|
expect(json['css']).to eq(default_css)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns customized values' do
|
||||||
|
SiteSetting.email_custom_template = "For you: %{email_content}"
|
||||||
|
SiteSetting.email_custom_css = ".user-name { font-size: 24px; }"
|
||||||
|
get '/admin/customize/email_style.json'
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
json = ::JSON.parse(response.body)['email_style']
|
||||||
|
expect(json['html']).to eq("For you: %{email_content}")
|
||||||
|
expect(json['css']).to eq(".user-name { font-size: 24px; }")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'update' do
|
||||||
|
let(:valid_params) do
|
||||||
|
{
|
||||||
|
html: 'For you: %{email_content}',
|
||||||
|
css: '.user-name { color: purple; }'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes the settings' do
|
||||||
|
SiteSetting.email_custom_css = ".user-name { font-size: 24px; }"
|
||||||
|
put '/admin/customize/email_style.json', params: { email_style: valid_params }
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(SiteSetting.email_custom_template).to eq(valid_params[:html])
|
||||||
|
expect(SiteSetting.email_custom_css).to eq(valid_params[:css])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reports errors' do
|
||||||
|
put '/admin/customize/email_style.json', params: {
|
||||||
|
email_style: valid_params.merge(html: 'No email content')
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
expect(json['errors']).to include(
|
||||||
|
I18n.t(
|
||||||
|
'email_style.html_missing_placeholder',
|
||||||
|
placeholder: '%{email_content}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,46 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe EmailStyleUpdater do
|
||||||
|
fab!(:admin) { Fabricate(:admin) }
|
||||||
|
let(:default_html) { File.read("#{Rails.root}/app/views/email/default_template.html") }
|
||||||
|
let(:updater) { EmailStyleUpdater.new(admin) }
|
||||||
|
|
||||||
|
describe 'update' do
|
||||||
|
it 'can change the settings' do
|
||||||
|
expect(
|
||||||
|
updater.update(
|
||||||
|
html: 'For you: %{email_content}',
|
||||||
|
css: 'h1 { color: blue; }'
|
||||||
|
)
|
||||||
|
).to eq(true)
|
||||||
|
expect(SiteSetting.email_custom_template).to eq('For you: %{email_content}')
|
||||||
|
expect(SiteSetting.email_custom_css).to eq('h1 { color: blue; }')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'will not store defaults' do
|
||||||
|
updater.update(html: default_html, css: '')
|
||||||
|
expect(SiteSetting.email_custom_template).to_not be_present
|
||||||
|
expect(SiteSetting.email_custom_css).to_not be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can clear settings if defaults given' do
|
||||||
|
SiteSetting.email_custom_template = 'For you: %{email_content}'
|
||||||
|
SiteSetting.email_custom_css = 'h1 { color: blue; }'
|
||||||
|
updater.update(html: default_html, css: '')
|
||||||
|
expect(SiteSetting.email_custom_template).to_not be_present
|
||||||
|
expect(SiteSetting.email_custom_css).to_not be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails if html is missing email_content' do
|
||||||
|
expect(updater.update(html: 'No email content', css: '')).to eq(false)
|
||||||
|
expect(updater.errors).to include(
|
||||||
|
I18n.t(
|
||||||
|
'email_style.html_missing_placeholder',
|
||||||
|
placeholder: '%{email_content}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue