email_build_spec.rb 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe Channel::EmailBuild, type: :model do
  4. describe '#build' do
  5. let(:html_body) do
  6. <<~MSG_HTML.chomp
  7. <!DOCTYPE html>
  8. <html>
  9. <head>
  10. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  11. </head>
  12. <body style="font-family:Geneva,Helvetica,Arial,sans-serif; font-size: 12px;">
  13. <div>&gt; Welcome!</div><div>&gt;</div><div>&gt; Thank you for installing Zammad. äöüß</div><div>&gt;</div>
  14. </body>
  15. </html>
  16. MSG_HTML
  17. end
  18. let(:plain_text_body) do
  19. <<~MSG_TEXT.chomp
  20. > Welcome!
  21. >
  22. > Thank you for installing Zammad. äöüß
  23. >
  24. MSG_TEXT
  25. end
  26. let(:parser) { Channel::EmailParser.new }
  27. let(:parsed_data) { parser.parse(mail.to_s) }
  28. let(:html_part_attachment) do
  29. parsed_data[:attachments]
  30. .find { |attachment| attachment[:filename] == 'message.html' }
  31. end
  32. let(:file_attachment) do
  33. parsed_data[:attachments]
  34. .find { |attachment| attachment[:filename] == filename }
  35. end
  36. shared_examples 'adding the email html part as an attachment' do
  37. it 'adds the html part as an attachment' do
  38. expect(html_part_attachment).to be_a Hash
  39. end
  40. it 'adds the html part as an attachment' do
  41. expect(html_part_attachment).to include(
  42. 'filename' => 'message.html',
  43. 'preferences' => include('content-alternative' => true, 'Charset' => 'UTF-8',
  44. 'Mime-Type' => 'text/html', 'original-format' => true)
  45. )
  46. end
  47. it 'does not include content-id property in attachment preferences' do
  48. expect(html_part_attachment).not_to include(
  49. 'preferences' => include('Content-ID')
  50. )
  51. end
  52. end
  53. shared_examples 'adding a text file as an attachment' do
  54. it 'adds the text file as an attachment' do
  55. expect(file_attachment).to include(
  56. 'filename' => filename,
  57. 'preferences' => include('Charset' => 'UTF-8', 'Mime-Type' => mime_type,
  58. 'Content-Type' => "text/plain; charset=UTF-8; filename=#{filename}")
  59. )
  60. end
  61. it 'does not include content* properties in attachment preferences' do
  62. expect(file_attachment).not_to include(
  63. 'preferences' => include('Content-ID', 'content-alternative')
  64. )
  65. end
  66. end
  67. shared_examples 'adding a file as an attachment' do |file_type|
  68. it "adds a #{file_type} as an attachment'" do
  69. expect(file_attachment).to include(
  70. 'data' => content, 'filename' => filename,
  71. 'preferences' => include('Charset' => 'UTF-8', 'Mime-Type' => mime_type,
  72. 'Content-Type' => preferences_content_type)
  73. )
  74. end
  75. it 'does not include content* properties in attachment preferences' do
  76. expect(file_attachment).not_to include(
  77. 'preferences' => include('content-alternative', 'Content-ID')
  78. )
  79. end
  80. end
  81. shared_examples 'not adding email content as attachment' do
  82. it 'does not add email content as an attachment' do
  83. expect(html_part_attachment).to be_nil
  84. end
  85. end
  86. context 'with email only' do
  87. let(:mail) do
  88. described_class.build(
  89. from: 'sender@example.com',
  90. to: 'recipient@example.com',
  91. body: mail_body,
  92. content_type: content_type
  93. )
  94. end
  95. let(:expected_text) do
  96. <<~MSG_TEXT.chomp
  97. > Welcome!\r
  98. >\r
  99. > Thank you for installing Zammad. äöüß\r
  100. >\r
  101. MSG_TEXT
  102. end
  103. context 'when email contains only html' do
  104. let(:mail_body) { html_body }
  105. let(:content_type) { 'text/html' }
  106. let(:expected_html) do
  107. <<~MSG_HTML.chomp
  108. <!DOCTYPE html>\r
  109. <html>\r
  110. <head>\r
  111. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\r
  112. </head>\r
  113. <body style="font-family:Geneva,Helvetica,Arial,sans-serif; font-size: 12px;">\r
  114. <div>&gt; Welcome!</div><div>&gt;</div><div>&gt; Thank you for installing Zammad. äöüß</div><div>&gt;</div>\r
  115. </body>\r
  116. </html>
  117. MSG_HTML
  118. end
  119. let(:expected_body) { '<div>&gt; Welcome!</div><div>&gt;</div><div>&gt; Thank you for installing Zammad. äöüß</div><div>&gt;</div>' }
  120. it 'builds a mail with a text part' do
  121. expect(mail.text_part.body.to_s).to eq expected_text
  122. end
  123. it 'builds a mail with a html part' do
  124. expect(mail.html_part.body.to_s).to eq expected_html
  125. end
  126. it 'builds a mail that is parsed correctly' do
  127. expect(parsed_data).to include(body: expected_body, content_type: 'text/html')
  128. end
  129. it_behaves_like 'adding the email html part as an attachment'
  130. end
  131. context 'when email contains only plain text' do
  132. let(:mail_body) { plain_text_body }
  133. let(:content_type) { 'text/plain' }
  134. it 'builds a mail with a text part' do
  135. expect(mail.body.to_s).to eq expected_text
  136. end
  137. it 'does not build a html part' do
  138. expect(mail.html_part).to be_nil
  139. end
  140. it 'builds a mail that is parsed correctly' do
  141. expect(parsed_data).to include(body: plain_text_body, content_type: 'text/plain')
  142. end
  143. it 'does not have an attachment' do
  144. expect(parsed_data[:attachments].first).to be_nil
  145. end
  146. it_behaves_like 'not adding email content as attachment'
  147. end
  148. end
  149. context 'with email and attachment' do
  150. let(:mail) do
  151. described_class.build(
  152. from: 'sender@example.com',
  153. to: 'recipient@example.com',
  154. body: mail_body,
  155. content_type: content_type,
  156. attachments: attachments
  157. )
  158. end
  159. let(:filename) { 'somename.txt' }
  160. let(:mime_type) { 'text/plain' }
  161. let(:content) { 'Some text' }
  162. let(:direct_attachment) do
  163. [{
  164. 'Mime-Type' => mime_type,
  165. :content => content,
  166. :filename => filename
  167. }]
  168. end
  169. let(:ticket) { create(:ticket, title: 'some article text attachment test', group: group) }
  170. let(:group) { Group.lookup(name: 'Users') }
  171. let(:article) do
  172. create(:ticket_article,
  173. ticket: ticket,
  174. body: 'some message article helper test1 <div><img style="width: 85.5px; height: 49.5px" src="cid:15.274327094.140938@zammad.example.com">asdasd<img src="cid:15.274327094.140939@zammad.example.com"><br>')
  175. end
  176. let(:store_attributes) do
  177. {
  178. object: 'Ticket::Article',
  179. o_id: article.id,
  180. data: content,
  181. filename: filename,
  182. preferences: {
  183. 'Mime-Type' => mime_type
  184. }
  185. }
  186. end
  187. let(:store) { create(:store, **store_attributes) }
  188. shared_context 'with attachment checks' do
  189. context 'when attachment is a text file' do
  190. it_behaves_like 'adding a text file as an attachment'
  191. end
  192. context 'when attachment is a image file' do
  193. let(:filename) { 'somename.png' }
  194. let(:mime_type) { 'image/png' }
  195. let(:preferences_content_type) { "#{mime_type}; filename=#{filename}" }
  196. let(:content) { 'xxxxxxx' }
  197. it_behaves_like 'adding a file as an attachment', 'image'
  198. end
  199. context 'when attachment is a calendar file' do
  200. let(:filename) { 'schedule.ics' }
  201. let(:mime_type) { 'text/calendar' }
  202. let(:preferences_content_type) { "#{mime_type}; charset=UTF-8; filename=#{filename}" }
  203. let(:content) { 'xxxxxxx' }
  204. it_behaves_like 'adding a file as an attachment', 'calendar'
  205. end
  206. end
  207. context 'with html email' do
  208. let(:mail_body) { html_body }
  209. let(:content_type) { 'text/html' }
  210. context 'with direct attachment' do
  211. let(:attachments) { direct_attachment }
  212. it 'has two attachments' do
  213. expect(parsed_data[:attachments].size).to eq 2
  214. end
  215. it_behaves_like 'adding the email html part as an attachment'
  216. include_context 'with attachment checks'
  217. end
  218. context 'with attachement from store' do
  219. let(:attachments) { [ store ] }
  220. let(:filename) { 'text_file.txt' }
  221. let(:mime_type) { 'text/plain' }
  222. it 'has two attachments' do
  223. expect(parsed_data[:attachments].size).to eq 2
  224. end
  225. it_behaves_like 'adding the email html part as an attachment'
  226. include_context 'with attachment checks'
  227. end
  228. end
  229. context 'with plain text email' do
  230. let(:mail_body) { plain_text_body }
  231. let(:content_type) { 'text/plain' }
  232. context 'with direct attachment' do
  233. let(:attachments) { direct_attachment }
  234. let(:filename) { 'somename.txt' }
  235. let(:mime_type) { 'text/plain' }
  236. it 'has only one attachment' do
  237. expect(parsed_data[:attachments].size).to eq 1
  238. end
  239. it_behaves_like 'not adding email content as attachment'
  240. include_context 'with attachment checks'
  241. end
  242. context 'with attachement from store' do
  243. let(:attachments) { [ store ] }
  244. let(:filename) { 'text_file.txt' }
  245. let(:mime_type) { 'text/plain' }
  246. let(:mail_body) do
  247. <<~MSG_TEXT.chomp
  248. > Welcome!
  249. >
  250. > Email Content
  251. MSG_TEXT
  252. end
  253. let(:content) { 'Text Content' }
  254. it 'has only one attachment' do
  255. expect(parsed_data[:attachments].size).to eq 1
  256. end
  257. # #2362 - Attached text files get prepended on e-mail reply instead of appended
  258. it 'Email Content should appear before the Text Content within the raw email' do
  259. expect(mail.to_s).to match(%r{Email Content[\s\S]*Text Content})
  260. end
  261. it_behaves_like 'not adding email content as attachment'
  262. include_context 'with attachment checks'
  263. end
  264. end
  265. end
  266. end
  267. describe '#recipient_line' do
  268. let(:email) { 'some.body@example.com' }
  269. let(:generated_recipient_line) { described_class.recipient_line(realname, email) }
  270. context 'with quote in the realname' do
  271. let(:realname) { 'Somebody @ "Company"' }
  272. it 'escapes the quotes' do
  273. expected_recipient_line = '"Somebody @ \"Company\"" <some.body@example.com>'
  274. expect(generated_recipient_line).to eq expected_recipient_line
  275. end
  276. end
  277. context 'with a simple realname with no special characters' do
  278. let(:realname) { 'Somebody' }
  279. it 'wraps the realname with quotes and wraps the email with <>' do
  280. expected_recipient_line = 'Somebody <some.body@example.com>'
  281. expect(generated_recipient_line).to eq expected_recipient_line
  282. end
  283. end
  284. context 'with special characters (|) in the realname' do
  285. let(:realname) { 'Somebody | Some Org' }
  286. it 'wraps the email with <>' do
  287. expected_recipient_line = 'Somebody | Some Org <some.body@example.com>'
  288. expect(generated_recipient_line).to eq expected_recipient_line
  289. end
  290. end
  291. context 'with special characters (spaces) in the realname' do
  292. let(:realname) { 'Test Admin Agent via Support' }
  293. it 'wraps the email with <>' do
  294. expected_recipient_line = 'Test Admin Agent via Support <some.body@example.com>'
  295. expect(generated_recipient_line).to eq expected_recipient_line
  296. end
  297. end
  298. end
  299. # https://github.com/zammad/zammad/issues/165
  300. describe '#html_mail_client_fixes' do
  301. let(:generated_html) { described_class.html_mail_client_fixes(html) }
  302. shared_examples 'adding styles to the element' do
  303. it 'adds style to the element' do
  304. expect(generated_html).to eq expected_html
  305. end
  306. it { expect(generated_html).not_to eq html }
  307. end
  308. context 'when html element is a blockquote' do
  309. let(:html) do
  310. <<~HTML.chomp
  311. <blockquote type="cite">some
  312. text
  313. </blockquote>
  314. 123
  315. <blockquote type="cite">some
  316. text
  317. </blockquote>
  318. HTML
  319. end
  320. let(:expected_html) do
  321. <<~HTML.chomp
  322. <blockquote type="cite" style="border-left: 2px solid blue; margin: 0 0 16px; padding: 8px 12px 8px 12px;">some
  323. text
  324. </blockquote>
  325. 123
  326. <blockquote type="cite" style="border-left: 2px solid blue; margin: 0 0 16px; padding: 8px 12px 8px 12px;">some
  327. text
  328. </blockquote>
  329. HTML
  330. end
  331. it_behaves_like 'adding styles to the element'
  332. end
  333. context 'when html element is a p' do
  334. let(:html) do
  335. <<~HTML.chomp
  336. <p>some
  337. text
  338. </p>
  339. <p>123</p>
  340. HTML
  341. end
  342. let(:expected_html) do
  343. <<~HTML.chomp
  344. <p style="margin: 0;">some
  345. text
  346. </p>
  347. <p style="margin: 0;">123</p>
  348. HTML
  349. end
  350. it_behaves_like 'adding styles to the element'
  351. end
  352. context 'when html element is a hr' do
  353. let(:html) do
  354. <<~HTML.chomp
  355. <p>sometext</p><hr><p>123</p>
  356. HTML
  357. end
  358. let(:expected_html) do
  359. <<~HTML.chomp
  360. <p style="margin: 0;">sometext</p><hr style="margin-top: 6px; margin-bottom: 6px; border: 0; border-top: 1px solid #dfdfdf;"><p style="margin: 0;">123</p>
  361. HTML
  362. end
  363. it_behaves_like 'adding styles to the element'
  364. context 'when hr is a closing tag' do
  365. let(:html) do
  366. <<~HTML.chomp
  367. <p>sometext</p></hr>
  368. HTML
  369. end
  370. let(:expected_html) do
  371. <<~HTML.chomp
  372. <p style="margin: 0;">sometext</p><hr style="margin-top: 6px; margin-bottom: 6px; border: 0; border-top: 1px solid #dfdfdf;">
  373. HTML
  374. end
  375. it_behaves_like 'adding styles to the element'
  376. end
  377. end
  378. context 'when html element does not contian p, hr or blockquote' do
  379. let(:html) do
  380. <<~HTML.chomp
  381. <div>
  382. <h2>Testing</h2>
  383. <ul>
  384. <li><a href="#"><b>Test</b> <span>1</span></a></li>
  385. <li><a href="#"><b>Test</b> <span>2</span></a></li>
  386. <li><a href="#"><b>Test</b> <span>3</span></a></li>
  387. </ul>
  388. </div>
  389. HTML
  390. end
  391. it 'does not add style to the element' do
  392. expect(generated_html).to eq html
  393. end
  394. end
  395. end
  396. describe '#html_complete_check' do
  397. let(:generated_html) { described_class.html_complete_check(html) }
  398. context 'when html element includes an html tag' do
  399. let(:html) { '<!DOCTYPE html><html><b>test</b></html>' }
  400. it 'returns the html as it is' do
  401. expect(generated_html).to eq html
  402. end
  403. end
  404. context 'when html does not include an html tag' do
  405. let(:html) { '<b>test</b>' }
  406. it 'adds DOCTYPE tag to the element' do
  407. expect(generated_html).to start_with '<!DOCTYPE'
  408. end
  409. it 'adds an html tag' do
  410. expect(generated_html).to match '<html dir="auto">'
  411. end
  412. it 'adds the original element' do
  413. expect(generated_html).to match html
  414. end
  415. end
  416. # Issue #1230, missing backslashes
  417. # 'Test URL: \\storage\project\100242-Inc'
  418. context 'when html includes a backslash' do
  419. let(:html) { '<b>Test URL</b>: \\\\storage\\project\\100242-Inc' }
  420. it 'keeps the backslashes' do
  421. expect(generated_html).to include html
  422. end
  423. end
  424. context 'with a configured html_email_css_font setting' do
  425. let(:html) { '<b>test</b>' }
  426. let(:css_font) { "font-family:'Helvetica Neue', sans-serif; font-size: 12px;" }
  427. before { Setting.set('html_email_css_font', css_font) }
  428. it 'includes the configured css font' do
  429. expect(generated_html).to include("#{css_font}\n")
  430. end
  431. end
  432. end
  433. end