email_parser_spec.rb 63 KB

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