Browse Source

Fixes #4691 - Size of inline images in mails from Zammad are not shown correctly in Microsoft mail clients.

Florian Liebe 9 months ago
parent
commit
92a3befd35

+ 13 - 0
app/models/channel/email_build.rb

@@ -100,6 +100,8 @@ generate email with S/MIME
         logger.error e
       end
 
+      html_alternative.body = Channel::EmailBuild.adjust_inline_image_size(html_alternative.body.to_s) if found_content_ids.present?
+
       html_container = Mail::Part.new { content_type 'multipart/related' }
       html_container.add_part html_alternative
 
@@ -224,4 +226,15 @@ Add/change markup to display html in any mail client nice.
     new_html
   end
 
+=begin
+
+Adjust image size in html email for MS Outlook to always contain `width` and `height` as tags, not only as part of the `style`.
+
+  html_string_with_adjustments = Channel::EmailBuild.adjust_inline_image_size(html_string)
+
+=end
+
+  def self.adjust_inline_image_size(html)
+    Loofah.fragment(html).scrub!(HtmlSanitizer::Scrubber::Outgoing::ImageSize.new).to_html
+  end
 end

+ 12 - 0
lib/html_sanitizer.rb

@@ -55,4 +55,16 @@ sanitize style of img tags
     HtmlSanitizer::DynamicImageSize.new.sanitize(string)
   end
 
+=begin
+
+Adjust height + width of img tags
+
+  string = HtmlSanitizer.adjust_inline_image_size(article.body)
+
+=end
+
+  def self.adjust_inline_image_size(string)
+    HtmlSanitizer::AdjustInlineImageSize.new.sanitize(string)
+  end
+
 end

+ 12 - 0
lib/html_sanitizer/adjust_inline_image_size.rb

@@ -0,0 +1,12 @@
+# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+class HtmlSanitizer
+  class AdjustInlineImageSize
+    def sanitize(string)
+      Loofah
+        .fragment(string)
+        .scrub!(HtmlSanitizer::Scrubber::Outgoing::ImageSize.new)
+        .to_html
+    end
+  end
+end

+ 40 - 0
lib/html_sanitizer/scrubber/outgoing/image_size.rb

@@ -0,0 +1,40 @@
+# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+class HtmlSanitizer
+  module Scrubber
+    module Outgoing
+      class ImageSize < Base
+        def scrub(node)
+          return CONTINUE if node.name != 'img'
+          return CONTINUE if node['style'].blank?
+
+          adjust(node, 'height')
+          adjust(node, 'width')
+
+          STOP
+        end
+
+        private
+
+        def split_style(node)
+          node['style'].downcase.gsub(%r{\t|\n|\r}, '').split(';')
+        end
+
+        def adjust(node, key)
+          return if node[key].present?
+
+          split_style(node).each do |elem|
+            attr, value = elem.split(':')
+
+            attr.strip!
+            value.strip!
+
+            next if attr != key
+
+            node[key] = value.include?('.') ? value.to_f : value.to_i
+          end
+        end
+      end
+    end
+  end
+end

+ 1 - 0
lib/notification_factory/mailer.rb

@@ -219,6 +219,7 @@ returns
 
     # prepare scaling of images
     if result[:body]
+      result[:body] = HtmlSanitizer.adjust_inline_image_size(result[:body])
       result[:body] = HtmlSanitizer.dynamic_image_size(result[:body])
     end
 

+ 83 - 0
spec/lib/html_sanitizer/scrubber/outgoing/image_size_spec.rb

@@ -0,0 +1,83 @@
+# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe HtmlSanitizer::Scrubber::Outgoing::ImageSize do
+  let(:scrubber) { described_class.new }
+
+  describe('#scrubber') do
+    subject(:actual) { fragment.scrub!(scrubber).to_html }
+
+    let(:fragment) { Loofah.fragment(input) }
+
+    context 'when no img tag is used' do
+      let(:input)  { '<script src="..."></script>' }
+      let(:target) { '<script src="..."></script>' }
+
+      it { is_expected.to eq target }
+    end
+
+    context 'when no style tag is present' do
+      let(:input)  { '<img>' }
+      let(:target) { '<img>' }
+
+      it { is_expected.to eq target }
+    end
+
+    context 'when width is already present' do
+      let(:input)  { '<img width="100">' }
+      let(:target) { '<img width="100">' }
+
+      it { is_expected.to eq target }
+    end
+
+    context 'when height is already present' do
+      let(:input)  { '<img height="100">' }
+      let(:target) { '<img height="100">' }
+
+      it { is_expected.to eq target }
+    end
+
+    context 'when height and width is already present' do
+      let(:input)  { '<img height="25" width="50">' }
+      let(:target) { '<img height="25" width="50">' }
+
+      it { is_expected.to eq target }
+    end
+
+    context 'when width is present in style' do
+      let(:input)  { '<img style="width: 100px">' }
+      let(:target) { '<img style="width: 100px" width="100">' }
+
+      it { is_expected.to eq target }
+
+      context 'when width is not a whole number' do
+        let(:input)  { '<img style="width: 306.578125px">' }
+        let(:target) { '<img style="width: 306.578125px" width="306.578125">' }
+
+        it { is_expected.to eq target }
+      end
+    end
+
+    context 'when height is present in style' do
+      let(:input)  { '<img style="height: 100px">' }
+      let(:target) { '<img style="height: 100px" height="100">' }
+
+      it { is_expected.to eq target }
+    end
+
+    context 'when height and width are present in style' do
+      let(:input)  { '<img style="height: 25px; width: 50px">' }
+      let(:target) { '<img style="height: 25px; width: 50px" height="25" width="50">' }
+
+      it { is_expected.to eq target }
+    end
+
+    context 'when height and width are present in style and are also set as tags' do
+      let(:input)  { '<img style="height: 25px; width: 50px" height="25" width="50">' }
+      let(:target) { '<img style="height: 25px; width: 50px" height="25" width="50">' }
+
+      it { is_expected.to eq target }
+    end
+  end
+end

+ 43 - 0
spec/models/channel/email_build/inline_image_adjustments_spec.rb

@@ -0,0 +1,43 @@
+# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe 'Channel::EmailBuild > Inline Images Adjustments', aggregate_failures: true, type: :model do
+  let(:html_body) do
+    <<~HTML.chomp
+      <!DOCTYPE html>
+      <html>
+        <head>
+          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+        </head>
+        <body style="font-family:Geneva,Helvetica,Arial,sans-serif; font-size: 12px;">
+          <img style="width: 125px; max-width: 100%; height: 187.5px;" src="cid:1.e83460e9-7e36-48f7-97db-dc7f0ba7c51f@zammad.example.com">
+          <br><br>
+          <div data-signature="true" data-signature-id="1">
+          Test Admin Agent<br><br>
+          --<br>
+          Super Support - Waterford Business Park<br>
+          5201 Blue Lagoon Drive - 8th Floor &amp; 9th Floor - Miami, 33126 USA<br>
+          Email: hot@example.com - Web: <a href="http://www.example.com/" rel="nofollow noreferrer noopener" target="_blank">http://www.example.com/</a><br>
+          --
+          </div>
+        </body>
+      </html>
+    HTML
+  end
+
+  let(:mail) do
+    Channel::EmailBuild.build(
+      from:         'sender@example.com',
+      to:           'recipient@example.com',
+      body:         html_body,
+      content_type: 'text/html',
+    )
+  end
+
+  context 'when an email is built with inline images' do
+    it 'adjusts the inline images width and height' do
+      expect(mail.html_part.body.to_s).to include('<img style="width: 125px; max-width: 100%; height: 187.5px;" src="cid:1.e83460e9-7e36-48f7-97db-dc7f0ba7c51f@zammad.example.com" height="187.5" width="125">')
+    end
+  end
+end