email_parser_spec.rb 47 KB

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