email_parser_spec.rb 62 KB

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