email_parser_spec.rb 58 KB

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