email_build.rb 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. module Channel::EmailBuild
  3. =begin
  4. generate email
  5. mail = Channel::EmailBuild.build(
  6. from: 'sender@example.com',
  7. to: 'recipient@example.com',
  8. body: 'somebody with some text',
  9. content_type: 'text/plain',
  10. )
  11. generate email with S/MIME
  12. mail = Channel::EmailBuild.build(
  13. from: 'sender@example.com',
  14. to: 'recipient@example.com',
  15. body: 'somebody with some text',
  16. content_type: 'text/plain',
  17. security: {
  18. type: 'S/MIME',
  19. encryption: {
  20. success: true,
  21. },
  22. sign: {
  23. success: true,
  24. },
  25. }
  26. )
  27. =end
  28. def self.build(attr, notification = false)
  29. mail = Mail.new
  30. # set headers
  31. attr.each do |key, value|
  32. next if key.to_s == 'attachments'
  33. next if key.to_s == 'body'
  34. next if key.to_s == 'content_type'
  35. next if key.to_s == 'security'
  36. mail[key.to_s] = if value.present? && value.class != Array
  37. value.to_s
  38. else
  39. value
  40. end
  41. end
  42. # add html part
  43. if attr[:content_type] && attr[:content_type] == 'text/html'
  44. html_alternative = Mail::Part.new do
  45. content_type 'text/html; charset=UTF-8'
  46. # complete check
  47. html_document = Channel::EmailBuild.html_complete_check(attr[:body])
  48. body html_document
  49. end
  50. # generate plain part
  51. attr[:body] = attr[:body].html2text
  52. end
  53. # add plain text part
  54. text_alternative = Mail::Part.new do
  55. content_type 'text/plain; charset=UTF-8'
  56. body attr[:body]
  57. end
  58. # build email without any attachments
  59. if !html_alternative && attr[:attachments].blank?
  60. mail.content_type 'text/plain; charset=UTF-8'
  61. mail.body attr[:body]
  62. SecureMailing.outgoing(mail, attr[:security])
  63. return mail
  64. end
  65. # build email with attachments
  66. alternative_bodies = Mail::Part.new { content_type 'multipart/alternative' }
  67. alternative_bodies.add_part text_alternative
  68. found_content_ids = {}
  69. if html_alternative
  70. # find all inline attachments used in body
  71. begin
  72. scrubber = Loofah::Scrubber.new do |node|
  73. next if node.name != 'img'
  74. next if node['src'].blank?
  75. next if node['src'] !~ %r{^cid:\s{0,2}(.+?)\s{0,2}$}
  76. found_content_ids[$1] = true
  77. end
  78. Loofah.fragment(html_alternative.body.to_s).scrub!(scrubber)
  79. rescue => e
  80. logger.error e
  81. end
  82. html_alternative.body = HtmlSanitizer.adjust_inline_image_size(html_alternative.body.to_s) if found_content_ids.present?
  83. html_container = Mail::Part.new { content_type 'multipart/related' }
  84. html_container.add_part html_alternative
  85. # place to add inline attachments related to html alternative
  86. attr[:attachments]&.each do |attachment|
  87. next if attachment.instance_of?(Hash)
  88. next if attachment.preferences['Content-ID'].blank?
  89. next if !found_content_ids[ attachment.preferences['Content-ID'] ]
  90. attachment = Mail::Part.new do
  91. content_type attachment.preferences['Content-Type']
  92. content_id "<#{attachment.preferences['Content-ID']}>"
  93. content_disposition attachment.preferences['Content-Disposition'] || 'inline'
  94. content_transfer_encoding 'binary'
  95. body attachment.content.force_encoding('BINARY')
  96. end
  97. html_container.add_part attachment
  98. end
  99. alternative_bodies.add_part html_container
  100. end
  101. mail.add_part alternative_bodies
  102. # add attachments
  103. attr[:attachments]&.each do |attachment|
  104. if attachment.instance_of?(Hash)
  105. attachment['content-id'] = nil
  106. mail.attachments[attachment[:filename]] = attachment
  107. else
  108. next if attachment.preferences['Content-ID'].present? && found_content_ids[ attachment.preferences['Content-ID'] ]
  109. filename = attachment.filename
  110. encoded_filename = Mail::Encodings.decode_encode filename, :encode
  111. disposition = attachment.preferences['Content-Disposition'] || 'attachment'
  112. content_type = attachment.preferences['Content-Type'] || attachment.preferences['Mime-Type'] || 'application/octet-stream'
  113. mail.attachments[attachment.filename] = {
  114. content_disposition: "#{disposition}; filename=\"#{encoded_filename}\"",
  115. content_type: "#{content_type}; filename=\"#{encoded_filename}\"",
  116. content: attachment.content
  117. }
  118. end
  119. end
  120. SecureMailing.outgoing(mail, attr[:security])
  121. # set organization
  122. organization = Setting.get('organization')
  123. if organization.present?
  124. mail['Organization'] = organization.to_s
  125. end
  126. if notification
  127. mail['X-Loop'] = 'yes'
  128. mail['Precedence'] = 'bulk'
  129. mail['Auto-Submitted'] = 'auto-generated'
  130. mail['X-Auto-Response-Suppress'] = 'All'
  131. end
  132. # rubocop:disable Zammad/DetectTranslatableString
  133. mail['X-Powered-By'] = 'Zammad - Helpdesk/Support (https://zammad.org/)'
  134. mail['X-Mailer'] = 'Zammad Mail Service'
  135. # rubocop:enable Zammad/DetectTranslatableString
  136. mail
  137. end
  138. =begin
  139. quoted_in_one_line = Channel::EmailBuild.recipient_line('Somebody @ "Company"', 'some.body@example.com')
  140. returns
  141. '"Somebody @ \"Company\"" <some.body@example.com>'
  142. =end
  143. def self.recipient_line(realname, email)
  144. Mail::Address.new.tap do |address|
  145. address.display_name = realname
  146. address.address = email
  147. end.format
  148. end
  149. =begin
  150. Check if string is a complete html document. If not, add head and css styles.
  151. full_html_document_string = Channel::EmailBuild.html_complete_check(html_string)
  152. =end
  153. def self.html_complete_check(html)
  154. # apply mail client fixes
  155. html = Channel::EmailBuild.html_mail_client_fixes(html)
  156. return html if html.match?(%r{<html>}i)
  157. html_email_body = File.read(Rails.root.join('app/views/mailer/application_wrapper.html.erb').to_s)
  158. html_email_body.gsub!('###html_email_css_font###', Setting.get('html_email_css_font'))
  159. # use block form because variable html could contain backslashes and e. g. '\1' that
  160. # must not be handled as back-references for regular expressions
  161. html_email_body.sub('###html###') { html }
  162. end
  163. =begin
  164. Add/change markup to display html in any mail client nice.
  165. html_string_with_fixes = Channel::EmailBuild.html_mail_client_fixes(html_string)
  166. =end
  167. def self.html_mail_client_fixes(html)
  168. # https://github.com/martini/zammad/issues/165
  169. new_html = html.gsub('<blockquote type="cite">', '<blockquote type="cite" style="border-left: 2px solid blue; margin: 0 0 16px; padding: 8px 12px 8px 12px;">')
  170. new_html.gsub!(%r{<p>}mxi, '<p style="margin: 0;">')
  171. new_html.gsub!(%r{</?hr>}mxi, '<hr style="margin-top: 6px; margin-bottom: 6px; border: 0; border-top: 1px solid #dfdfdf;">')
  172. new_html
  173. end
  174. end