email_parser_spec.rb 53 KB

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