email_parser_spec.rb 65 KB

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