email_parser_spec.rb 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120
  1. require 'rails_helper'
  2. RSpec.describe Channel::EmailParser, type: :model do
  3. describe '#parse' do
  4. # regression test for issue 2390 - Add a postmaster filter to not show emails with potential issue
  5. describe 'handling HTML links in message content' do
  6. context 'with under 5,000 links' do
  7. it 'parses message content as normal' do
  8. expect(described_class.new.parse(<<~RAW)[:body]).to start_with('<a href="https://zammad.com/"')
  9. From: nicole.braun@zammad.com
  10. Content-Type: text/html
  11. <html><body>
  12. #{Array.new(10) { '<a href="https://zammad.com/">Dummy Link</a>' }.join(' ')}
  13. </body></html>
  14. RAW
  15. end
  16. end
  17. context 'with 5,000+ links' do
  18. it 'replaces message content with error message' do
  19. expect(described_class.new.parse(<<~RAW)).to include('body' => Channel::EmailParser::EXCESSIVE_LINKS_MSG)
  20. From: nicole.braun@zammad.com
  21. Content-Type: text/html
  22. <html><body>
  23. #{Array.new(5001) { '<a href="https://zammad.com/">Dummy Link</a>' }.join(' ')}
  24. </body></html>
  25. RAW
  26. end
  27. end
  28. end
  29. end
  30. describe '#process' do
  31. let(:raw_mail) { File.read(mail_file) }
  32. before { Trigger.destroy_all } # triggers may cause additional articles to be created
  33. describe 'auto-creating new users' do
  34. context 'with one unrecognized email address' do
  35. it 'creates one new user' do
  36. expect { Channel::EmailParser.new.process({}, <<~RAW) }.to change(User, :count).by(1)
  37. From: #{Faker::Internet.unique.email}
  38. RAW
  39. end
  40. end
  41. context 'with a large number of unrecognized recipient addresses' do
  42. it 'never creates more than 40 users' do
  43. expect { Channel::EmailParser.new.process({}, <<~RAW) }.to change(User, :count).by(40)
  44. From: nicole.braun@zammad.org
  45. To: #{Array.new(20) { Faker::Internet.unique.email }.join(', ')}
  46. Cc: #{Array.new(21) { Faker::Internet.unique.email }.join(', ')}
  47. RAW
  48. end
  49. end
  50. end
  51. describe 'auto-updating existing users' do
  52. context 'with a previous email with no real name in the From: header' do
  53. let!(:customer) { Channel::EmailParser.new.process({}, previous_email).first.customer }
  54. let(:previous_email) { <<~RAW.chomp }
  55. From: customer@example.com
  56. To: myzammad@example.com
  57. Subject: test sender name update 1
  58. Some Text
  59. RAW
  60. context 'and a new email with a real name in the From: header' do
  61. let(:new_email) { <<~RAW.chomp }
  62. From: Max Smith <customer@example.com>
  63. To: myzammad@example.com
  64. Subject: test sender name update 2
  65. Some Text
  66. RAW
  67. it 'updates the customer’s #firstname and #lastname' do
  68. expect { Channel::EmailParser.new.process({}, new_email) }
  69. .to change { customer.reload.firstname }.from('').to('Max')
  70. .and change { customer.reload.lastname }.from('').to('Smith')
  71. end
  72. end
  73. end
  74. end
  75. describe 'creating new tickets' do
  76. context 'when subject contains no ticket reference' do
  77. let(:raw_mail) { <<~RAW.chomp }
  78. From: foo@bar.com
  79. To: baz@qux.net
  80. Subject: Foo
  81. Lorem ipsum dolor
  82. RAW
  83. it 'creates a ticket and article' do
  84. expect { Channel::EmailParser.new.process({}, raw_mail) }
  85. .to change(Ticket, :count).by(1)
  86. .and change(Ticket::Article, :count).by_at_least(1)
  87. end
  88. it 'sets #title to email subject' do
  89. Channel::EmailParser.new.process({}, raw_mail)
  90. expect(Ticket.last.title).to eq('Foo')
  91. end
  92. it 'sets #state to "new"' do
  93. Channel::EmailParser.new.process({}, raw_mail)
  94. expect(Ticket.last.state.name).to eq('new')
  95. end
  96. context 'when from address matches an existing agent' do
  97. let!(:agent) { create(:agent_user, email: 'foo@bar.com') }
  98. it 'sets article.sender to "Agent"' do
  99. Channel::EmailParser.new.process({}, raw_mail)
  100. expect(Ticket::Article.last.sender.name).to eq('Agent')
  101. end
  102. it 'sets ticket.state to "new"' do
  103. Channel::EmailParser.new.process({}, raw_mail)
  104. expect(Ticket.last.state.name).to eq('new')
  105. end
  106. end
  107. context 'when from address matches an existing customer' do
  108. let!(:customer) { create(:customer_user, email: 'foo@bar.com') }
  109. it 'sets article.sender to "Customer"' do
  110. Channel::EmailParser.new.process({}, raw_mail)
  111. expect(Ticket.last.articles.first.sender.name).to eq('Customer')
  112. end
  113. it 'sets ticket.state to "new"' do
  114. Channel::EmailParser.new.process({}, raw_mail)
  115. expect(Ticket.last.state.name).to eq('new')
  116. end
  117. end
  118. context 'when from address is unrecognized' do
  119. it 'sets article.sender to "Customer"' do
  120. Channel::EmailParser.new.process({}, raw_mail)
  121. expect(Ticket.last.articles.first.sender.name).to eq('Customer')
  122. end
  123. end
  124. end
  125. end
  126. describe 'associating emails to existing tickets' do
  127. let!(:ticket) { create(:ticket) }
  128. let(:ticket_ref) { Setting.get('ticket_hook') + Setting.get('ticket_hook_divider') + ticket.number }
  129. describe 'based on where a ticket reference appears in the message' do
  130. shared_context 'ticket reference in subject' do
  131. let(:raw_mail) { <<~RAW.chomp }
  132. From: me@example.com
  133. To: customer@example.com
  134. Subject: #{ticket_ref}
  135. Lorem ipsum dolor
  136. RAW
  137. end
  138. shared_context 'ticket reference in body' do
  139. let(:raw_mail) { <<~RAW.chomp }
  140. From: me@example.com
  141. To: customer@example.com
  142. Subject: no reference
  143. Lorem ipsum dolor #{ticket_ref}
  144. RAW
  145. end
  146. shared_context 'ticket reference in body (text/html)' do
  147. let(:raw_mail) { <<~RAW.chomp }
  148. From: me@example.com
  149. To: customer@example.com
  150. Subject: no reference
  151. Content-Transfer-Encoding: 7bit
  152. Content-Type: text/html;
  153. <b>Lorem ipsum dolor #{ticket_ref}</b>
  154. RAW
  155. end
  156. shared_context 'ticket reference in attachment' do
  157. let(:raw_mail) { <<~RAW.chomp }
  158. From: me@example.com
  159. Content-Type: multipart/mixed; boundary="Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2"
  160. Subject: no reference
  161. Date: Sun, 30 Aug 2015 23:20:54 +0200
  162. To: Martin Edenhofer <me@znuny.com>
  163. Mime-Version: 1.0 (Mac OS X Mail 8.2 \(2104\))
  164. X-Mailer: Apple Mail (2.2104)
  165. --Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
  166. Content-Transfer-Encoding: 7bit
  167. Content-Type: text/plain;
  168. charset=us-ascii
  169. no reference
  170. --Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
  171. Content-Disposition: attachment;
  172. filename=test1.txt
  173. Content-Type: text/plain;
  174. name="test.txt"
  175. Content-Transfer-Encoding: 7bit
  176. Some Text #{ticket_ref}
  177. --Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2--
  178. RAW
  179. end
  180. shared_context 'ticket reference in In-Reply-To header' do
  181. let(:raw_mail) { <<~RAW.chomp }
  182. From: me@example.com
  183. To: customer@example.com
  184. Subject: no reference
  185. In-Reply-To: #{article.message_id}
  186. Lorem ipsum dolor
  187. RAW
  188. let!(:article) { create(:ticket_article, ticket: ticket, message_id: '<20150830145601.30.608882@edenhofer.zammad.com>') }
  189. end
  190. shared_context 'ticket reference in References header' do
  191. let(:raw_mail) { <<~RAW.chomp }
  192. From: me@example.com
  193. To: customer@example.com
  194. Subject: no reference
  195. References: <DA918CD1-BE9A-4262-ACF6-5001E59291B6@znuny.com> #{article.message_id} <DA918CD1-BE9A-4262-ACF6-5001E59291XX@znuny.com>
  196. Lorem ipsum dolor
  197. RAW
  198. let!(:article) { create(:ticket_article, ticket: ticket, message_id: '<20150830145601.30.608882@edenhofer.zammad.com>') }
  199. end
  200. shared_examples 'adds message to ticket' do
  201. it 'adds message to ticket' do
  202. expect { described_class.new.process({}, raw_mail) }
  203. .to change { ticket.articles.length }.by(1)
  204. end
  205. end
  206. shared_examples 'creates a new ticket' do
  207. it 'creates a new ticket' do
  208. expect { described_class.new.process({}, raw_mail) }
  209. .to change(Ticket, :count).by(1)
  210. .and not_change { ticket.articles.length }
  211. end
  212. end
  213. context 'when not explicitly configured to search anywhere' do
  214. before { Setting.set('postmaster_follow_up_search_in', nil) }
  215. context 'when subject contains ticket reference' do
  216. include_context 'ticket reference in subject'
  217. include_examples 'adds message to ticket'
  218. context 'alongside other, invalid ticket references' do
  219. let(:raw_mail) { <<~RAW.chomp }
  220. From: me@example.com
  221. To: customer@example.com
  222. Subject: [#{Setting.get('ticket_hook') + Setting.get('ticket_hook_divider') + Ticket::Number.generate}] #{ticket_ref}
  223. Lorem ipsum dolor
  224. RAW
  225. include_examples 'adds message to ticket'
  226. end
  227. context 'and ticket is closed' do
  228. before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
  229. include_examples 'adds message to ticket'
  230. end
  231. context 'but ticket group’s #follow_up_possible attribute is "new_ticket"' do
  232. before { ticket.group.update(follow_up_possible: 'new_ticket') }
  233. context 'and ticket is open' do
  234. include_examples 'adds message to ticket'
  235. end
  236. context 'and ticket is closed' do
  237. before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
  238. include_examples 'creates a new ticket'
  239. end
  240. context 'and ticket is merged' do
  241. before { ticket.update(state: Ticket::State.find_by(name: 'merged')) }
  242. include_examples 'creates a new ticket'
  243. end
  244. context 'and ticket is removed' do
  245. before { ticket.update(state: Ticket::State.find_by(name: 'removed')) }
  246. include_examples 'creates a new ticket'
  247. end
  248. end
  249. context 'and "ticket_hook" setting is non-default value' do
  250. before { Setting.set('ticket_hook', 'VD-Ticket#') }
  251. include_examples 'adds message to ticket'
  252. end
  253. end
  254. context 'when body contains ticket reference' do
  255. include_context 'ticket reference in body'
  256. include_examples 'creates a new ticket'
  257. end
  258. context 'when attachment contains ticket reference' do
  259. include_context 'ticket reference in attachment'
  260. include_examples 'creates a new ticket'
  261. end
  262. context 'when In-Reply-To header contains article message-id' do
  263. include_context 'ticket reference in In-Reply-To header'
  264. include_examples 'creates a new ticket'
  265. context 'and subject matches article subject' do
  266. let(:raw_mail) { <<~RAW.chomp }
  267. From: customer@example.com
  268. To: me@example.com
  269. Subject: AW: RE: #{article.subject}
  270. In-Reply-To: #{article.message_id}
  271. Lorem ipsum dolor
  272. RAW
  273. include_examples 'adds message to ticket'
  274. end
  275. context 'and "ticket_hook_position" setting is "none"' do
  276. before { Setting.set('ticket_hook_position', 'none') }
  277. let(:raw_mail) { <<~RAW.chomp }
  278. From: customer@example.com
  279. To: me@example.com
  280. Subject: RE: Foo bar
  281. In-Reply-To: #{article.message_id}
  282. Lorem ipsum dolor
  283. RAW
  284. include_examples 'adds message to ticket'
  285. end
  286. end
  287. context 'when References header contains article message-id' do
  288. include_context 'ticket reference in References header'
  289. include_examples 'creates a new ticket'
  290. context 'and Auto-Submitted header reads "auto-replied"' do
  291. let(:raw_mail) { <<~RAW.chomp }
  292. From: me@example.com
  293. To: customer@example.com
  294. Subject: no reference
  295. References: #{article.message_id}
  296. Auto-Submitted: auto-replied
  297. Lorem ipsum dolor
  298. RAW
  299. include_examples 'adds message to ticket'
  300. end
  301. context 'and subject matches article subject' do
  302. let(:raw_mail) { <<~RAW.chomp }
  303. From: customer@example.com
  304. To: me@example.com
  305. Subject: AW: RE: #{article.subject}
  306. References: #{article.message_id}
  307. Lorem ipsum dolor
  308. RAW
  309. include_examples 'adds message to ticket'
  310. end
  311. context 'and "ticket_hook_position" setting is "none"' do
  312. before { Setting.set('ticket_hook_position', 'none') }
  313. let(:raw_mail) { <<~RAW.chomp }
  314. From: customer@example.com
  315. To: me@example.com
  316. Subject: RE: Foo bar
  317. References: #{article.message_id}
  318. Lorem ipsum dolor
  319. RAW
  320. include_examples 'adds message to ticket'
  321. end
  322. end
  323. end
  324. context 'when configured to search body' do
  325. before { Setting.set('postmaster_follow_up_search_in', 'body') }
  326. context 'when subject contains ticket reference' do
  327. include_context 'ticket reference in subject'
  328. include_examples 'adds message to ticket'
  329. end
  330. context 'when body contains ticket reference' do
  331. context 'in visible text' do
  332. include_context 'ticket reference in body'
  333. include_examples 'adds message to ticket'
  334. end
  335. context 'as part of a larger word' do
  336. let(:ticket_ref) { "Foo#{Setting.get('ticket_hook')}#{Setting.get('ticket_hook_divider')}#{ticket.number}bar" }
  337. include_context 'ticket reference in body'
  338. include_examples 'creates a new ticket'
  339. end
  340. context 'between html tags' do
  341. include_context 'ticket reference in body (text/html)'
  342. include_examples 'adds message to ticket'
  343. end
  344. context 'in html attributes' do
  345. let(:ticket_ref) { %(<table bgcolor="#{Setting.get('ticket_hook')}#{Setting.get('ticket_hook_divider')}#{ticket.number}"> </table>) }
  346. include_context 'ticket reference in body (text/html)'
  347. include_examples 'creates a new ticket'
  348. end
  349. end
  350. context 'when attachment contains ticket reference' do
  351. include_context 'ticket reference in attachment'
  352. include_examples 'creates a new ticket'
  353. end
  354. context 'when In-Reply-To header contains article message-id' do
  355. include_context 'ticket reference in In-Reply-To header'
  356. include_examples 'creates a new ticket'
  357. context 'and Auto-Submitted header reads "auto-replied"' do
  358. let(:raw_mail) { <<~RAW.chomp }
  359. From: me@example.com
  360. To: customer@example.com
  361. Subject: no reference
  362. References: #{article.message_id}
  363. Auto-Submitted: auto-replied
  364. Lorem ipsum dolor
  365. RAW
  366. include_examples 'adds message to ticket'
  367. end
  368. end
  369. context 'when References header contains article message-id' do
  370. include_context 'ticket reference in References header'
  371. include_examples 'creates a new ticket'
  372. end
  373. end
  374. context 'when configured to search attachments' do
  375. before { Setting.set('postmaster_follow_up_search_in', 'attachment') }
  376. context 'when subject contains ticket reference' do
  377. include_context 'ticket reference in subject'
  378. include_examples 'adds message to ticket'
  379. end
  380. context 'when body contains ticket reference' do
  381. include_context 'ticket reference in body'
  382. include_examples 'creates a new ticket'
  383. end
  384. context 'when attachment contains ticket reference' do
  385. include_context 'ticket reference in attachment'
  386. include_examples 'adds message to ticket'
  387. end
  388. context 'when In-Reply-To header contains article message-id' do
  389. include_context 'ticket reference in In-Reply-To header'
  390. include_examples 'creates a new ticket'
  391. end
  392. context 'when References header contains article message-id' do
  393. include_context 'ticket reference in References header'
  394. include_examples 'creates a new ticket'
  395. context 'and Auto-Submitted header reads "auto-replied"' do
  396. let(:raw_mail) { <<~RAW.chomp }
  397. From: me@example.com
  398. To: customer@example.com
  399. Subject: no reference
  400. References: #{article.message_id}
  401. Auto-Submitted: auto-replied
  402. Lorem ipsum dolor
  403. RAW
  404. include_examples 'adds message to ticket'
  405. end
  406. end
  407. end
  408. context 'when configured to search headers' do
  409. before { Setting.set('postmaster_follow_up_search_in', 'references') }
  410. context 'when subject contains ticket reference' do
  411. include_context 'ticket reference in subject'
  412. include_examples 'adds message to ticket'
  413. end
  414. context 'when body contains ticket reference' do
  415. include_context 'ticket reference in body'
  416. include_examples 'creates a new ticket'
  417. end
  418. context 'when attachment contains ticket reference' do
  419. include_context 'ticket reference in attachment'
  420. include_examples 'creates a new ticket'
  421. end
  422. context 'when In-Reply-To header contains article message-id' do
  423. include_context 'ticket reference in In-Reply-To header'
  424. include_examples 'adds message to ticket'
  425. end
  426. context 'when References header contains article message-id' do
  427. include_context 'ticket reference in References header'
  428. include_examples 'adds message to ticket'
  429. context 'that matches two separate tickets' do
  430. let!(:newer_ticket) { create(:ticket) }
  431. let!(:newer_article) { create(:ticket_article, ticket: newer_ticket, message_id: article.message_id) }
  432. it 'returns more recently created ticket' do
  433. expect(described_class.new.process({}, raw_mail).first).to eq(newer_ticket)
  434. end
  435. it 'adds message to more recently created ticket' do
  436. expect { described_class.new.process({}, raw_mail) }
  437. .to change { newer_ticket.articles.count }.by(1)
  438. .and not_change { ticket.articles.count }
  439. end
  440. end
  441. context 'and Auto-Submitted header reads "auto-replied"' do
  442. let(:raw_mail) { <<~RAW.chomp }
  443. From: me@example.com
  444. To: customer@example.com
  445. Subject: no reference
  446. References: #{article.message_id}
  447. Auto-Submitted: auto-replied
  448. Lorem ipsum dolor
  449. RAW
  450. include_examples 'adds message to ticket'
  451. end
  452. end
  453. end
  454. context 'when configured to search everything' do
  455. before { Setting.set('postmaster_follow_up_search_in', %w[body attachment references]) }
  456. context 'when subject contains ticket reference' do
  457. include_context 'ticket reference in subject'
  458. include_examples 'adds message to ticket'
  459. end
  460. context 'when body contains ticket reference' do
  461. include_context 'ticket reference in body'
  462. include_examples 'adds message to ticket'
  463. end
  464. context 'when attachment contains ticket reference' do
  465. include_context 'ticket reference in attachment'
  466. include_examples 'adds message to ticket'
  467. end
  468. context 'when In-Reply-To header contains article message-id' do
  469. include_context 'ticket reference in In-Reply-To header'
  470. include_examples 'adds message to ticket'
  471. end
  472. context 'when References header contains article message-id' do
  473. include_context 'ticket reference in References header'
  474. include_examples 'adds message to ticket'
  475. context 'and Auto-Submitted header reads "auto-replied"' do
  476. let(:raw_mail) { <<~RAW.chomp }
  477. From: me@example.com
  478. To: customer@example.com
  479. Subject: no reference
  480. References: #{article.message_id}
  481. Auto-Submitted: auto-replied
  482. Lorem ipsum dolor
  483. RAW
  484. include_examples 'adds message to ticket'
  485. end
  486. end
  487. end
  488. end
  489. context 'for a closed ticket' do
  490. let(:ticket) { create(:ticket, state_name: 'closed') }
  491. let(:raw_mail) { <<~RAW.chomp }
  492. From: me@example.com
  493. To: customer@example.com
  494. Subject: #{ticket_ref}
  495. Lorem ipsum dolor
  496. RAW
  497. it 'reopens it' do
  498. expect { described_class.new.process({}, raw_mail) }
  499. .to change { ticket.reload.state.name }.to('open')
  500. end
  501. end
  502. end
  503. describe 'assigning ticket.customer' do
  504. let(:agent) { create(:agent_user) }
  505. let(:customer) { create(:customer_user) }
  506. let(:raw_mail) { <<~RAW.chomp }
  507. From: #{agent.email}
  508. To: #{customer.email}
  509. Subject: Foo
  510. Lorem ipsum dolor
  511. RAW
  512. context 'when "postmaster_sender_is_agent_search_for_customer" setting is true (default)' do
  513. it 'sets ticket.customer to user with To: email' do
  514. expect { Channel::EmailParser.new.process({}, raw_mail) }
  515. .to change(Ticket, :count).by(1)
  516. expect(Ticket.last.customer).to eq(customer)
  517. end
  518. end
  519. context 'when "postmaster_sender_is_agent_search_for_customer" setting is false' do
  520. before { Setting.set('postmaster_sender_is_agent_search_for_customer', false) }
  521. it 'sets ticket.customer to user with To: email' do
  522. expect { Channel::EmailParser.new.process({}, raw_mail) }
  523. .to change(Ticket, :count).by(1)
  524. expect(Ticket.last.customer).to eq(agent)
  525. end
  526. end
  527. end
  528. describe 'formatting to/from addresses' do
  529. # see https://github.com/zammad/zammad/issues/2198
  530. context 'when sender address contains spaces (#2198)' do
  531. let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail071.box') }
  532. let(:sender_email) { 'powerquadrantsystem@example.com' }
  533. it 'removes them before creating a new user' do
  534. expect { described_class.new.process({}, raw_mail) }
  535. .to change { User.exists?(email: sender_email) }
  536. end
  537. it 'marks new user email as invalid' do
  538. described_class.new.process({}, raw_mail)
  539. expect(User.find_by(email: sender_email).preferences)
  540. .to include('mail_delivery_failed' => true)
  541. .and include('mail_delivery_failed_reason' => 'invalid email')
  542. .and include('mail_delivery_failed_data' => a_kind_of(ActiveSupport::TimeWithZone))
  543. end
  544. end
  545. # see https://github.com/zammad/zammad/issues/2254
  546. context 'when sender address contains > (#2254)' do
  547. let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail076.box') }
  548. let(:sender_email) { 'millionslotteryspaintransfer@example.com' }
  549. it 'removes them before creating a new user' do
  550. expect { described_class.new.process({}, raw_mail) }
  551. .to change { User.exists?(email: sender_email) }
  552. end
  553. it 'marks new user email as invalid' do
  554. described_class.new.process({}, raw_mail)
  555. expect(User.find_by(email: sender_email).preferences)
  556. .to include('mail_delivery_failed' => true)
  557. .and include('mail_delivery_failed_reason' => 'invalid email')
  558. .and include('mail_delivery_failed_data' => a_kind_of(ActiveSupport::TimeWithZone))
  559. end
  560. end
  561. end
  562. describe 'signature detection' do
  563. let(:raw_mail) { header + File.read(message_file) }
  564. let(:header) { <<~HEADER }
  565. From: Bob.Smith@music.com
  566. To: test@zammad.org
  567. Subject: test
  568. HEADER
  569. context 'for emails from an unrecognized email address' do
  570. let(:message_file) { Rails.root.join('test', 'data', 'email_signature_detection', 'client_a_1.txt') }
  571. it 'does not detect signatures' do
  572. described_class.new.process({}, raw_mail)
  573. expect { Scheduler.worker(true) }
  574. .to not_change { Ticket.last.customer.preferences[:signature_detection] }.from(nil)
  575. .and not_change { Ticket.last.articles.first.preferences[:signature_detection] }.from(nil)
  576. end
  577. end
  578. context 'for emails from a previously processed sender' do
  579. before do
  580. described_class.new.process({}, header + File.read(previous_message_file))
  581. end
  582. let(:previous_message_file) { Rails.root.join('test', 'data', 'email_signature_detection', 'client_a_1.txt') }
  583. let(:message_file) { Rails.root.join('test', 'data', 'email_signature_detection', 'client_a_2.txt') }
  584. it 'sets detected signature on user (in a background job)' do
  585. described_class.new.process({}, raw_mail)
  586. expect { Scheduler.worker(true) }
  587. .to change { Ticket.last.customer.preferences[:signature_detection] }
  588. end
  589. it 'sets line of detected signature on article (in a background job)' do
  590. described_class.new.process({}, raw_mail)
  591. expect { Scheduler.worker(true) }
  592. .to change { Ticket.last.articles.first.preferences[:signature_detection] }.to(20)
  593. end
  594. end
  595. end
  596. describe 'charset handling' do
  597. # see https://github.com/zammad/zammad/issues/2224
  598. context 'when header specifies Windows-1258 charset (#2224)' do
  599. let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail072.box') }
  600. it 'does not raise Encoding::ConverterNotFoundError' do
  601. expect { described_class.new.process({}, raw_mail) }
  602. .not_to raise_error
  603. end
  604. end
  605. end
  606. describe 'attachment handling' do
  607. context 'with header "Content-Transfer-Encoding: x-uuencode"' do
  608. let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail078-content_transfer_encoding_x_uuencode.box') }
  609. let(:article) { described_class.new.process({}, raw_mail).second }
  610. it 'does not raise RuntimeError' do
  611. expect { described_class.new.process({}, raw_mail) }
  612. .not_to raise_error
  613. end
  614. it 'parses the content correctly' do
  615. expect(article.attachments.first.filename).to eq('PGP_Cmts_on_12-14-01_Pkg.txt')
  616. expect(article.attachments.first.content).to eq('Hello Zammad')
  617. end
  618. end
  619. end
  620. describe 'inline image handling' do
  621. # see https://github.com/zammad/zammad/issues/2486
  622. context 'when image is large but not resizable' do
  623. let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail079.box') }
  624. let(:attachment) { article.attachments.to_a.find { |i| i.filename == 'a.jpg' } }
  625. let(:article) { described_class.new.process({}, raw_mail).second }
  626. it "doesn't set resizable preference" do
  627. expect(attachment.filename).to eq('a.jpg')
  628. expect(attachment.preferences).not_to include('resizable' => true)
  629. end
  630. end
  631. end
  632. describe 'XSS protection' do
  633. let(:article) { described_class.new.process({}, raw_mail).second }
  634. let(:raw_mail) { <<~RAW.chomp }
  635. From: ME Bob <me@example.com>
  636. To: customer@example.com
  637. Subject: some subject
  638. Content-Type: #{content_type}
  639. MIME-Version: 1.0
  640. no HTML <script type="text/javascript">alert(\'XSS\')</script>
  641. RAW
  642. context 'for Content-Type: text/html' do
  643. let(:content_type) { 'text/html' }
  644. it 'removes injected <script> tags from body' do
  645. expect(article.body).to eq("no HTML alert('XSS')")
  646. end
  647. end
  648. context 'for Content-Type: text/plain' do
  649. let(:content_type) { 'text/plain' }
  650. it 'leaves body as-is' do
  651. expect(article.body).to eq(<<~SANITIZED.chomp)
  652. no HTML <script type="text/javascript">alert(\'XSS\')</script>
  653. SANITIZED
  654. end
  655. end
  656. end
  657. context 'for “delivery failed” notifications (a.k.a. bounce messages)' do
  658. let(:ticket) { article.ticket }
  659. let(:article) { create(:ticket_article, sender_name: 'Agent', message_id: message_id) }
  660. let(:message_id) { raw_mail[/(?<=^(References|Message-ID): )\S*/] }
  661. context 'with future retries (delayed)' do
  662. let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail078.box') }
  663. context 'on a closed ticket' do
  664. before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
  665. it 'sets #preferences on resulting ticket to { "send-auto-responses" => false, "is-auto-reponse" => true }' do
  666. article = Channel::EmailParser.new.process({}, raw_mail).second
  667. expect(article.preferences)
  668. .to include('send-auto-response' => false, 'is-auto-response' => true)
  669. end
  670. it 'returns a Mail object with an x-zammad-out-of-office header' do
  671. output_mail = Channel::EmailParser.new.process({}, raw_mail).last
  672. expect(output_mail).to include('x-zammad-out-of-office': true)
  673. end
  674. it 'finds the article referenced in the bounce message headers, then adds the bounce message to its ticket' do
  675. expect { Channel::EmailParser.new.process({}, raw_mail) }
  676. .to change { ticket.articles.count }.by(1)
  677. end
  678. it 'does not re-open the ticket' do
  679. expect { Channel::EmailParser.new.process({}, raw_mail) }
  680. .not_to change { ticket.reload.state.name }.from('closed')
  681. end
  682. end
  683. end
  684. context 'with no future retries (undeliverable): sample input 1' do
  685. let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail033-undelivered-mail-returned-to-sender.box') }
  686. context 'for original message sent by Agent' do
  687. it 'sets #preferences on resulting ticket to { "send-auto-responses" => false, "is-auto-reponse" => true }' do
  688. article = Channel::EmailParser.new.process({}, raw_mail).second
  689. expect(article.preferences)
  690. .to include('send-auto-response' => false, 'is-auto-response' => true)
  691. end
  692. it 'finds the article referenced in the bounce message headers, then adds the bounce message to its ticket' do
  693. expect { Channel::EmailParser.new.process({}, raw_mail) }
  694. .to change { ticket.articles.count }.by(1)
  695. end
  696. it 'does not alter the ticket state' do
  697. expect { Channel::EmailParser.new.process({}, raw_mail) }
  698. .not_to change { ticket.reload.state.name }.from('open')
  699. end
  700. end
  701. context 'for original message sent by Customer' do
  702. let(:article) { create(:ticket_article, sender_name: 'Customer', message_id: message_id) }
  703. it 'sets #preferences on resulting ticket to { "send-auto-responses" => false, "is-auto-reponse" => true }' do
  704. article = Channel::EmailParser.new.process({}, raw_mail).second
  705. expect(article.preferences)
  706. .to include('send-auto-response' => false, 'is-auto-response' => true)
  707. end
  708. it 'finds the article referenced in the bounce message headers, then adds the bounce message to its ticket' do
  709. expect { Channel::EmailParser.new.process({}, raw_mail) }
  710. .to change { ticket.articles.count }.by(1)
  711. end
  712. it 'does not alter the ticket state' do
  713. expect { Channel::EmailParser.new.process({}, raw_mail) }
  714. .not_to change { ticket.reload.state.name }.from('new')
  715. end
  716. end
  717. end
  718. context 'with no future retries (undeliverable): sample input 2' do
  719. let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail055.box') }
  720. it 'finds the article referenced in the bounce message headers, then adds the bounce message to its ticket' do
  721. expect { Channel::EmailParser.new.process({}, raw_mail) }
  722. .to change { ticket.articles.count }.by(1)
  723. end
  724. it 'does not alter the ticket state' do
  725. expect { Channel::EmailParser.new.process({}, raw_mail) }
  726. .not_to change { ticket.reload.state.name }.from('open')
  727. end
  728. end
  729. end
  730. context 'for “out-of-office” notifications (a.k.a. auto-response messages)' do
  731. let(:raw_mail) { <<~RAW.chomp }
  732. From: me@example.com
  733. To: customer@example.com
  734. Subject: #{subject_line}
  735. Some Text
  736. RAW
  737. let(:subject_line) { 'Lorem ipsum dolor' }
  738. it 'applies the OutOfOfficeCheck filter to given message' do
  739. expect(Channel::Filter::OutOfOfficeCheck)
  740. .to receive(:run)
  741. .with(kind_of(Hash), hash_including(subject: subject_line))
  742. described_class.new.process({}, raw_mail)
  743. end
  744. context 'on an existing, closed ticket' do
  745. let(:ticket) { create(:ticket, state_name: 'closed') }
  746. let(:subject_line) { ticket.subject_build('Lorem ipsum dolor') }
  747. context 'when OutOfOfficeCheck filter applies x-zammad-out-of-office: false' do
  748. before do
  749. allow(Channel::Filter::OutOfOfficeCheck)
  750. .to receive(:run) { |_, mail_hash| mail_hash[:'x-zammad-out-of-office'] = false }
  751. end
  752. it 're-opens a closed ticket' do
  753. expect { described_class.new.process({}, raw_mail) }
  754. .to not_change(Ticket, :count)
  755. .and change { ticket.reload.state.name }.to('open')
  756. end
  757. end
  758. context 'when OutOfOfficeCheck filter applies x-zammad-out-of-office: true' do
  759. before do
  760. allow(Channel::Filter::OutOfOfficeCheck)
  761. .to receive(:run) { |_, mail_hash| mail_hash[:'x-zammad-out-of-office'] = true }
  762. end
  763. it 'does not re-open a closed ticket' do
  764. expect { described_class.new.process({}, raw_mail) }
  765. .to not_change(Ticket, :count)
  766. .and not_change { ticket.reload.state.name }
  767. end
  768. end
  769. end
  770. end
  771. describe 'suppressing normal Ticket::Article callbacks' do
  772. context 'from sender: "Agent"' do
  773. let(:agent) { create(:agent_user) }
  774. it 'does not dispatch an email on article creation' do
  775. expect(TicketArticleCommunicateEmailJob).not_to receive(:perform_later)
  776. described_class.new.process({}, <<~RAW.chomp)
  777. From: #{agent.email}
  778. To: customer@example.com
  779. Subject: some subject
  780. Some Text
  781. RAW
  782. end
  783. end
  784. end
  785. end
  786. describe '#compose_postmaster_reply' do
  787. let(:raw_incoming_mail) { File.read(Rails.root.join('test', 'data', 'mail', 'mail010.box')) }
  788. shared_examples 'postmaster reply' do
  789. it 'composes postmaster reply' do
  790. reply = Channel::EmailParser.new.send(:compose_postmaster_reply, raw_incoming_mail, locale)
  791. expect(reply[:to]).to eq('smith@example.com')
  792. expect(reply[:content_type]).to eq('text/plain')
  793. expect(reply[:subject]).to eq(expected_subject)
  794. expect(reply[:body]).to eq(expected_body)
  795. end
  796. end
  797. context 'for English locale (en)' do
  798. include_examples 'postmaster reply' do
  799. let(:locale) { 'en' }
  800. let(:expected_subject) { '[undeliverable] Message too large' }
  801. let(:expected_body) do
  802. body = <<~BODY
  803. Dear Smith Sepp,
  804. Unfortunately your email titled \"Gruß aus Oberalteich\" could not be delivered to one or more recipients.
  805. Your message was 0.01 MB but we only accept messages up to 10 MB.
  806. Please reduce the message size and try again. Thank you for your understanding.
  807. Regretfully,
  808. Postmaster of zammad.example.com
  809. BODY
  810. body.gsub(/\n/, "\r\n")
  811. end
  812. end
  813. end
  814. context 'for German locale (de)' do
  815. include_examples 'postmaster reply' do
  816. let(:locale) { 'de' }
  817. let(:expected_subject) { '[Unzustellbar] Nachricht zu groß' }
  818. let(:expected_body) do
  819. body = <<~BODY
  820. Hallo Smith Sepp,
  821. Ihre E-Mail mit dem Betreff \"Gruß aus Oberalteich\" konnte nicht an einen oder mehrere Empfänger zugestellt werden.
  822. Die Nachricht hatte eine Größe von 0.01 MB, wir akzeptieren jedoch nur E-Mails mit einer Größe von bis zu 10 MB.
  823. Bitte reduzieren Sie die Größe Ihrer Nachricht und versuchen Sie es erneut. Vielen Dank für Ihr Verständnis.
  824. Mit freundlichen Grüßen
  825. Postmaster von zammad.example.com
  826. BODY
  827. body.gsub(/\n/, "\r\n")
  828. end
  829. end
  830. end
  831. end
  832. describe '#compose_postmaster_reply' do
  833. let(:raw_incoming_mail) { File.read(Rails.root.join('test', 'data', 'mail', 'mail010.box')) }
  834. shared_examples 'postmaster reply' do
  835. it 'composes postmaster reply' do
  836. reply = Channel::EmailParser.new.send(:compose_postmaster_reply, raw_incoming_mail, locale)
  837. expect(reply[:to]).to eq('smith@example.com')
  838. expect(reply[:content_type]).to eq('text/plain')
  839. expect(reply[:subject]).to eq(expected_subject)
  840. expect(reply[:body]).to eq(expected_body)
  841. end
  842. end
  843. context 'for English locale (en)' do
  844. include_examples 'postmaster reply' do
  845. let(:locale) { 'en' }
  846. let(:expected_subject) { '[undeliverable] Message too large' }
  847. let(:expected_body) do
  848. body = <<~BODY
  849. Dear Smith Sepp,
  850. Unfortunately your email titled \"Gruß aus Oberalteich\" could not be delivered to one or more recipients.
  851. Your message was 0.01 MB but we only accept messages up to 10 MB.
  852. Please reduce the message size and try again. Thank you for your understanding.
  853. Regretfully,
  854. Postmaster of zammad.example.com
  855. BODY
  856. body.gsub(/\n/, "\r\n")
  857. end
  858. end
  859. end
  860. context 'for German locale (de)' do
  861. include_examples 'postmaster reply' do
  862. let(:locale) { 'de' }
  863. let(:expected_subject) { '[Unzustellbar] Nachricht zu groß' }
  864. let(:expected_body) do
  865. body = <<~BODY
  866. Hallo Smith Sepp,
  867. Ihre E-Mail mit dem Betreff \"Gruß aus Oberalteich\" konnte nicht an einen oder mehrere Empfänger zugestellt werden.
  868. Die Nachricht hatte eine Größe von 0.01 MB, wir akzeptieren jedoch nur E-Mails mit einer Größe von bis zu 10 MB.
  869. Bitte reduzieren Sie die Größe Ihrer Nachricht und versuchen Sie es erneut. Vielen Dank für Ihr Verständnis.
  870. Mit freundlichen Grüßen
  871. Postmaster von zammad.example.com
  872. BODY
  873. body.gsub(/\n/, "\r\n")
  874. end
  875. end
  876. end
  877. end
  878. end