email_parser_spec.rb 64 KB

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