From 3f09ec2aca363e369f583cf6da38c8c8cf18b5f0 Mon Sep 17 00:00:00 2001 From: scossar Date: Tue, 19 Jan 2016 16:42:56 -0800 Subject: [PATCH 1/4] add layout for notifications --- app/mailers/user_notifications.rb | 2 + app/views/layouts/user_notifications.html.erb | 42 +++++++++ lib/email/styles.rb | 52 +++++----- spec/components/email/styles_spec.rb | 94 +++++++++---------- 4 files changed, 122 insertions(+), 68 deletions(-) create mode 100644 app/views/layouts/user_notifications.html.erb diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index d2ff9cc51..3af7f5d6b 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -5,6 +5,7 @@ require_dependency 'age_words' class UserNotifications < ActionMailer::Base helper :application default charset: 'UTF-8' + layout 'user_notifications' include Email::BuildEmailHelper @@ -290,6 +291,7 @@ class UserNotifications < ActionMailer::Base else html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render( template: 'email/notification', + layout: 'layouts/user_notifications', format: :html, locals: { context_posts: context_posts, post: post, diff --git a/app/views/layouts/user_notifications.html.erb b/app/views/layouts/user_notifications.html.erb new file mode 100644 index 000000000..67e2802b7 --- /dev/null +++ b/app/views/layouts/user_notifications.html.erb @@ -0,0 +1,42 @@ + + + + + + + + + + + + +
+
+ + + + + +
+ + <%= yield %> + +
+ + +
+
+ + \ No newline at end of file diff --git a/lib/email/styles.rb b/lib/email/styles.rb index 67447a10a..0c7bcddc3 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -9,7 +9,7 @@ module Email def initialize(html, opts=nil) @html = html @opts = opts || {} - @fragment = Nokogiri::HTML.fragment(@html) + @doc = Nokogiri::HTML(@html) end def self.register_plugin_style(&block) @@ -30,7 +30,7 @@ module Email uri = URI(Discourse.base_url) # images - @fragment.css('img').each do |img| + @doc.css('img').each do |img| next if img['class'] == 'site-logo' if img['class'] == "emoji" || img['src'] =~ /plugins\/emoji/ @@ -56,15 +56,16 @@ module Email end # add max-width to big images - big_images = @fragment.css('img[width="auto"][height="auto"]') - - @fragment.css('aside.onebox img') - - @fragment.css('img.site-logo, img.emoji') + big_images = @doc.css('img[width="auto"][height="auto"]') - + @doc.css('aside.onebox img') - + @doc.css('img.site-logo, img.emoji') big_images.each do |img| - add_styles(img, 'max-width: 100%;') if img['style'] !~ /max-width/ + add_styles(img, 'width: 100%; height: auto; max-width: 600px;') if img['style'] !~ /max-width/ + add_attributes(img, width: '600') end # attachments - @fragment.css('a.attachment').each do |a| + @doc.css('a.attachment').each do |a| # ensure all urls are absolute if a['href'] =~ /^\/[^\/]/ a['href'] = "#{Discourse.base_url}#{a['href']}" @@ -117,12 +118,12 @@ module Email style('aside.clearfix', "clear: both") # Finally, convert all `aside` tags to `div`s - @fragment.css('aside, article, header').each do |n| + @doc.css('aside, article, header').each do |n| n.name = "div" end # iframes can't go in emails, so replace them with clickable links - @fragment.css('iframe').each do |i| + @doc.css('iframe').each do |i| begin src_uri = URI(i['src']) @@ -157,20 +158,21 @@ module Email # this method is reserved for styles specific to plugin def plugin_styles - @@plugin_callbacks.each { |block| block.call(@fragment, @opts) } + @@plugin_callbacks.each { |block| block.call(@doc, @opts) } end def to_html + return "" unless has_body? strip_classes_and_ids replace_relative_urls - @fragment.to_html.tap do |result| + @doc.to_html.tap do |result| result.gsub!(/\[email-indent\]/, "
") result.gsub!(/\[\/email-indent\]/, "
") end end def strip_avatars_and_emojis - @fragment.search('img').each do |img| + @doc.search('img').each do |img| if img['src'] =~ /_avatar/ img.parent['style'] = "vertical-align: top;" if img.parent.name == 'td' img.remove @@ -182,7 +184,7 @@ module Email end end - @fragment.to_s + @doc.to_s end private @@ -192,7 +194,7 @@ module Email host = forum_uri.host scheme = forum_uri.scheme - @fragment.css('[href]').each do |element| + @doc.css('[href]').each do |element| href = element['href'] if href =~ /^\/\/#{host}/ element['href'] = "#{scheme}:#{href}" @@ -201,14 +203,14 @@ module Email end def correct_first_body_margin - @fragment.css('.body p').each do |element| + @doc.css('.body p').each do |element| element['style'] = "margin-top:0; border: 0;" end end def correct_footer_style footernum = 0 - @fragment.css('.footer').each do |element| + @doc.css('.footer').each do |element| element['style'] = "color:#666;" linknum = 0 element.css('a').each do |inner| @@ -225,7 +227,7 @@ module Email end def strip_classes_and_ids - @fragment.css('*').each do |element| + @doc.css('*').each do |element| element.delete('class') element.delete('id') end @@ -235,12 +237,20 @@ module Email style('table', nil, cellspacing: '0', cellpadding: '0', border: '0') end + def add_attributes(element, attribs) + attribs.each do |k, v| + element[k] = v + end + end + + def has_body? + @doc.at_css('body') + end + def style(selector, style, attribs = {}) - @fragment.css(selector).each do |element| + @doc.css(selector).each do |element| add_styles(element, style) if style - attribs.each do |k,v| - element[k] = v - end + add_attributes(element, attribs) end end end diff --git a/spec/components/email/styles_spec.rb b/spec/components/email/styles_spec.rb index 1f8bb953b..6b4af50b8 100644 --- a/spec/components/email/styles_spec.rb +++ b/spec/components/email/styles_spec.rb @@ -3,17 +3,17 @@ require 'email' describe Email::Styles do - def basic_fragment(html) + def basic_doc(html) styler = Email::Styles.new(html) styler.format_basic - Nokogiri::HTML.fragment(styler.to_html) + Nokogiri::HTML(styler.to_html) end - def html_fragment(html) + def html_doc(html) styler = Email::Styles.new(html) styler.format_basic styler.format_html - Nokogiri::HTML.fragment(styler.to_html) + Nokogiri::HTML(styler.to_html) end context "basic formatter" do @@ -26,24 +26,24 @@ describe Email::Styles do # Pending due to email effort @coding-horror made in d2fb2bc4c skip "adds a max-width to images" do - frag = basic_fragment("") - expect(frag.at("img")["style"]).to match("max-width") + doc = basic_doc("") + expect(doc.at("img")["style"]).to match("max-width") end it "adds a width and height to images with an emoji path" do - frag = basic_fragment("") - expect(frag.at("img")["width"]).to eq("20") - expect(frag.at("img")["height"]).to eq("20") + doc = basic_doc("") + expect(doc.at("img")["width"]).to eq("20") + expect(doc.at("img")["height"]).to eq("20") end it "converts relative paths to absolute paths" do - frag = basic_fragment("") - expect(frag.at("img")["src"]).to eq("#{Discourse.base_url}/some-image.png") + doc = basic_doc("") + expect(doc.at("img")["src"]).to eq("#{Discourse.base_url}/some-image.png") end it "strips classes and ids" do - frag = basic_fragment("
") - expect(frag.to_html).to eq("
") + doc = basic_doc("
") + expect(doc.to_html).to match(/
<\/div><\/div>/) end end @@ -56,51 +56,51 @@ describe Email::Styles do end it "attaches a style to h3 tags" do - frag = html_fragment("

hello

") - expect(frag.at('h3')['style']).to be_present + doc = html_doc("

hello

") + expect(doc.at('h3')['style']).to be_present end it "attaches a style to hr tags" do - frag = html_fragment("hello
") - expect(frag.at('hr')['style']).to be_present + doc = html_doc("hello
") + expect(doc.at('hr')['style']).to be_present end it "attaches a style to a tags" do - frag = html_fragment("wat") - expect(frag.at('a')['style']).to be_present + doc = html_doc("wat") + expect(doc.at('a')['style']).to be_present end it "attaches a style to a tags" do - frag = html_fragment("wat") - expect(frag.at('a')['style']).to be_present + doc = html_doc("wat") + expect(doc.at('a')['style']).to be_present end it "attaches a style to ul and li tags" do - frag = html_fragment("
  • hello
") - expect(frag.at('ul')['style']).to be_present - expect(frag.at('li')['style']).to be_present + doc = html_doc("
  • hello
") + expect(doc.at('ul')['style']).to be_present + expect(doc.at('li')['style']).to be_present end it "converts iframes to links" do iframe_url = "http://www.youtube.com/embed/7twifrxOTQY?feature=oembed&wmode=opaque" - frag = html_fragment("") - expect(frag.at('iframe')).to be_blank - expect(frag.at('a')).to be_present - expect(frag.at('a')['href']).to eq(iframe_url) + doc = html_doc("") + expect(doc.at('iframe')).to be_blank + expect(doc.at('a')).to be_present + expect(doc.at('a')['href']).to eq(iframe_url) end it "won't allow non URLs in iframe src, strips them with no link" do iframe_url = "alert('xss hole')" - frag = html_fragment("") - expect(frag.at('iframe')).to be_blank - expect(frag.at('a')).to be_blank + doc = html_doc("") + expect(doc.at('iframe')).to be_blank + expect(doc.at('a')).to be_blank end end context "rewriting protocol relative URLs to the forum" do it "doesn't rewrite a url to another site" do - frag = html_fragment('hello') - expect(frag.at('a')['href']).to eq("//youtube.com/discourse") + doc = html_doc('hello') + expect(doc.at('a')['href']).to eq("//youtube.com/discourse") end context "without https" do @@ -109,18 +109,18 @@ describe Email::Styles do end it "rewrites the href to have http" do - frag = html_fragment('hello') - expect(frag.at('a')['href']).to eq("http://test.localhost/discourse") + doc = html_doc('hello') + expect(doc.at('a')['href']).to eq("http://test.localhost/discourse") end it "rewrites the href for attachment files to have http" do - frag = html_fragment('attachment_file.txt') - expect(frag.at('a')['href']).to eq("http://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt") + doc = html_doc('attachment_file.txt') + expect(doc.at('a')['href']).to eq("http://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt") end it "rewrites the src to have http" do - frag = html_fragment('') - expect(frag.at('img')['src']).to eq("http://test.localhost/blah.jpg") + doc = html_doc('') + expect(doc.at('img')['src']).to eq("http://test.localhost/blah.jpg") end end @@ -130,18 +130,18 @@ describe Email::Styles do end it "rewrites the forum URL to have https" do - frag = html_fragment('hello') - expect(frag.at('a')['href']).to eq("https://test.localhost/discourse") + doc = html_doc('hello') + expect(doc.at('a')['href']).to eq("https://test.localhost/discourse") end it "rewrites the href for attachment files to have https" do - frag = html_fragment('attachment_file.txt') - expect(frag.at('a')['href']).to eq("https://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt") + doc = html_doc('attachment_file.txt') + expect(doc.at('a')['href']).to eq("https://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt") end it "rewrites the src to have https" do - frag = html_fragment('') - expect(frag.at('img')['src']).to eq("https://test.localhost/blah.jpg") + doc = html_doc('') + expect(doc.at('img')['src']).to eq("https://test.localhost/blah.jpg") end end @@ -152,14 +152,14 @@ describe Email::Styles do emoji = "" style = Email::Styles.new(emoji) style.strip_avatars_and_emojis - expect(style.to_html).to match_html(emoji) + expect(style.to_html).to match_html("#{emoji}") end it "works for lonesome emoji with title" do emoji = "" style = Email::Styles.new(emoji) style.strip_avatars_and_emojis - expect(style.to_html).to match_html("cry_cry") + expect(style.to_html).to match_html("cry_cry") end end From 8d10130c105191a7524420c71804bfde600e2dcf Mon Sep 17 00:00:00 2001 From: scossar Date: Wed, 27 Jan 2016 15:44:49 -0800 Subject: [PATCH 2/4] test format_notifications --- spec/components/email/styles_spec.rb | 32 ++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/spec/components/email/styles_spec.rb b/spec/components/email/styles_spec.rb index 6b4af50b8..0e6bfcd36 100644 --- a/spec/components/email/styles_spec.rb +++ b/spec/components/email/styles_spec.rb @@ -16,6 +16,14 @@ describe Email::Styles do Nokogiri::HTML(styler.to_html) end + def notification_doc(html) + styler = Email::Styles.new(html) + styler.format_basic + styler.format_notification + styler.format_html + Nokogiri::HTML(styler.to_html) + end + context "basic formatter" do it "works with an empty string" do @@ -24,12 +32,16 @@ describe Email::Styles do expect(style.to_html).to be_blank end - # Pending due to email effort @coding-horror made in d2fb2bc4c - skip "adds a max-width to images" do - doc = basic_doc("") + it "adds a max-width to big images" do + doc = basic_doc("") expect(doc.at("img")["style"]).to match("max-width") end + it "doesn't add a maz-width to small images" do + doc = basic_doc("") + expect(doc.at("img")["style"]).not_to match("max-width") + end + it "adds a width and height to images with an emoji path" do doc = basic_doc("") expect(doc.at("img")["width"]).to eq("20") @@ -97,6 +109,19 @@ describe Email::Styles do end end + context "format notifications" do + it "adds both styles and attributes" do + doc = notification_doc("
hello
") + expect(doc.at('td')['style']).to eq('padding-top:5px;') + expect(doc.at('td')['colspan']).to eq('2') + end + + it "adds attributes when no styles are present" do + doc = notification_doc("
") + expect(doc.at('img')['width']).to eq('45') + end + end + context "rewriting protocol relative URLs to the forum" do it "doesn't rewrite a url to another site" do doc = html_doc('hello') @@ -163,5 +188,4 @@ describe Email::Styles do end end - end From 017820e445c591747f5f6b68a9787a2d7d66c15e Mon Sep 17 00:00:00 2001 From: scossar Date: Wed, 27 Jan 2016 18:39:20 -0800 Subject: [PATCH 3/4] check for template --- spec/mailers/user_notifications_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index 4c250f18e..7b56e2bb7 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -35,6 +35,10 @@ describe UserNotifications do expect(subject.body).to be_present end + it "does not use the layout" do + expect(subject.html_part.to_s.scan(//).count).to eq(0) + end + end describe ".forgot_password" do @@ -48,6 +52,10 @@ describe UserNotifications do expect(subject.body).to be_present end + it "does not use the layout" do + expect(subject.html_part.to_s.scan(//).count).to eq(0) + end + end describe '.digest' do @@ -77,6 +85,10 @@ describe UserNotifications do expect(subject.text_part.body.to_s).to be_present end + it "uses the layout" do + expect(subject.html_part.to_s.scan(//).count).to eq(1) + end + it "includes email_prefix in email subject instead of site title" do SiteSetting.email_prefix = "Try Discourse" SiteSetting.title = "Discourse Meta" @@ -104,6 +116,8 @@ describe UserNotifications do notification_type: notification.notification_type, notification_data_hash: notification.data_hash ) + # meta tag should be present from the layout + expect(mail.html_part.to_s.scan(//).count).to eq(1) # from should include full user name expect(mail[:from].display_names).to eql(['John Doe']) @@ -156,6 +170,9 @@ describe UserNotifications do notification_data_hash: notification.data_hash ) + # meta tag should be present from the layout + expect(mail.html_part.to_s.scan(//).count).to eq(1) + # from should not include full user name if "show user full names" is disabled expect(mail[:from].display_names).to_not eql(['John Doe']) @@ -193,6 +210,9 @@ describe UserNotifications do notification_data_hash: notification.data_hash ) + # meta tag should be present from the layout + expect(mail.html_part.to_s.scan(//).count).to eq(1) + # from should include username if full user name is not provided expect(mail[:from].display_names).to eql(['john']) From 77167f12ad0e0b4326993d51b4ec51ab582778ac Mon Sep 17 00:00:00 2001 From: scossar Date: Wed, 27 Jan 2016 19:07:21 -0800 Subject: [PATCH 4/4] move styles to Styles --- app/views/layouts/user_notifications.html.erb | 12 ++++++------ lib/email/styles.rb | 5 +++++ spec/components/email/styles_spec.rb | 11 +++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/views/layouts/user_notifications.html.erb b/app/views/layouts/user_notifications.html.erb index 67e2802b7..9202d99d6 100644 --- a/app/views/layouts/user_notifications.html.erb +++ b/app/views/layouts/user_notifications.html.erb @@ -13,17 +13,17 @@ - -
-
+ +
+
- +
-
+ <%= yield %> diff --git a/lib/email/styles.rb b/lib/email/styles.rb index 0c7bcddc3..25d6a6414 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -91,6 +91,10 @@ module Email style('.rtl', 'direction: rtl;') style('td.body', 'padding-top:5px;', colspan: "2") style('.whisper td.body', 'font-style: italic; color: #9c9c9c;') + style('.wrapper', 'width:100%;table-layout:fixed;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;') + style('.webkit', 'max-width:600px;') + style('.outer', 'Margin: 0 auto; width: 100%; max-width: 600px;') + style('.container', 'padding: 10px;') correct_first_body_margin correct_footer_style reset_tables @@ -138,6 +142,7 @@ module Email end def format_html + style('body', 'Margin: 0;padding:0;min-width:100%;background-color:#ffffff;') style('h3', 'margin: 15px 0 20px 0;') style('hr', 'background-color: #ddd; height: 1px; border: 1px;') style('a', 'text-decoration: none; font-weight: bold; color: #006699;') diff --git a/spec/components/email/styles_spec.rb b/spec/components/email/styles_spec.rb index 0e6bfcd36..e471c310f 100644 --- a/spec/components/email/styles_spec.rb +++ b/spec/components/email/styles_spec.rb @@ -120,6 +120,17 @@ describe Email::Styles do doc = notification_doc("
") expect(doc.at('img')['width']).to eq('45') end + + it "adds correct styles to the wrapper" do + doc = notification_doc('
') + expect(doc.at('center')['style']).to eq('width:100%;table-layout:fixed;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;') + end + + it "doesn't override inline attributes" do + doc = notification_doc('
test
') + expect(doc.at('table')['align']).to eq('center') + expect(doc.at('table')['width']).to eq('600') + end end context "rewriting protocol relative URLs to the forum" do