email_parser_spec.rb 47 KB

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