email_parser_spec.rb 54 KB

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