email_parser_spec.rb 61 KB

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