email_parser_spec.rb 64 KB

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