email_parser_spec.rb 58 KB

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