email_parser_spec.rb 57 KB

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