imap_spec.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe Channel::Driver::Imap, integration: true, required_envs: %w[MAIL_SERVER MAIL_ADDRESS MAIL_PASS MAIL_ADDRESS_ASCII MAIL_PASS_ASCII] do
  4. # https://github.com/zammad/zammad/issues/2964
  5. context 'when connecting with a ASCII 8-Bit password' do
  6. it 'succeeds' do
  7. params = {
  8. host: ENV['MAIL_SERVER'],
  9. user: ENV['MAIL_ADDRESS_ASCII'],
  10. password: ENV['MAIL_PASS_ASCII'],
  11. ssl_verify: false,
  12. }
  13. result = described_class.new.fetch(params, nil, 'check')
  14. expect(result[:result]).to eq 'ok'
  15. end
  16. end
  17. describe '.parse_rfc822_headers' do
  18. it 'parses simple header' do
  19. expect(described_class.parse_rfc822_headers('Key: Value')).to have_key('Key').and(have_value('Value'))
  20. end
  21. it 'parses header with no white space' do
  22. expect(described_class.parse_rfc822_headers('Key:Value')).to have_key('Key').and(have_value('Value'))
  23. end
  24. it 'parses multiline header' do
  25. expect(described_class.parse_rfc822_headers("Key: Value\r\n2nd-key: 2nd-value"))
  26. .to have_key('Key').and(have_value('Value')).and(have_key('2nd-key')).and(have_value('2nd-value'))
  27. end
  28. it 'parses value with semicolons' do
  29. expect(described_class.parse_rfc822_headers('Key: Val:ue')).to have_key('Key').and(have_value('Val:ue'))
  30. end
  31. it 'parses key-only lines' do
  32. expect(described_class.parse_rfc822_headers('Key')).to have_key('Key')
  33. end
  34. it 'handles empty line' do
  35. expect { described_class.parse_rfc822_headers("Key: Value\r\n") }.not_to raise_error
  36. end
  37. it 'handles tabbed value' do
  38. expect(described_class.parse_rfc822_headers("Key: \r\n\tValue")).to have_key('Key').and(have_value('Value'))
  39. end
  40. end
  41. describe '.extract_rfc822_headers' do
  42. it 'extracts header' do
  43. object = Net::IMAP::FetchData.new :id, { 'RFC822.HEADER' => 'Key: Value' }
  44. expect(described_class.extract_rfc822_headers(object)).to have_key('Key').and(have_value('Value'))
  45. end
  46. it 'returns nil when header attribute is missing' do
  47. object = Net::IMAP::FetchData.new :id, { 'Another' => 'Key: Value' }
  48. expect(described_class.extract_rfc822_headers(object)).to be_nil
  49. end
  50. it 'does not raise error when given nil' do
  51. expect { described_class.extract_rfc822_headers(nil) }.not_to raise_error
  52. end
  53. end
  54. def expect_imap_fetch_check_results(result_params_to_compare = {})
  55. driver_call_result = {}
  56. expect { channel.fetch(true, driver_call_result) }.not_to change(Ticket::Article, :count)
  57. expect(driver_call_result).to include(result_params_to_compare)
  58. end
  59. describe '.fetch', :aggregate_failures do
  60. let(:folder) { "imap_spec-#{SecureRandom.uuid}" }
  61. let(:server_address) { ENV['MAIL_SERVER'] }
  62. let(:server_login) { ENV['MAIL_ADDRESS'] }
  63. let(:server_password) { ENV['MAIL_PASS'] }
  64. let(:email_address) { create(:email_address, name: 'Zammad Helpdesk', email: "some-zammad-#{ENV['MAIL_ADDRESS']}") }
  65. let(:group) { create(:group, email_address: email_address) }
  66. let(:inbound_options) do
  67. {
  68. adapter: 'imap',
  69. options: {
  70. host: ENV['MAIL_SERVER'],
  71. user: ENV['MAIL_ADDRESS'],
  72. password: server_password,
  73. ssl: true,
  74. ssl_verify: false,
  75. folder: folder,
  76. keep_on_server: false,
  77. }
  78. }
  79. end
  80. let(:outbound_options) do
  81. {
  82. adapter: 'smtp',
  83. options: {
  84. host: server_address,
  85. port: 25,
  86. start_tls: true,
  87. ssl_verify: false,
  88. user: server_login,
  89. password: server_password,
  90. email: email_address.email
  91. },
  92. }
  93. end
  94. let(:channel) do
  95. create(:email_channel, group: group, inbound: inbound_options, outbound: outbound_options).tap do |channel|
  96. email_address.channel = channel
  97. email_address.save!
  98. end
  99. end
  100. let(:imap) { Net::IMAP.new(server_address, port: 993, ssl: { verify_mode: OpenSSL::SSL::VERIFY_NONE }).tap { |imap| imap.login(server_login, server_password) } }
  101. let(:purge_inbox) do
  102. imap.select('inbox')
  103. imap.sort(['DATE'], ['ALL'], 'US-ASCII').each do |msg|
  104. imap.store(msg, '+FLAGS', [:Deleted])
  105. end
  106. imap.expunge
  107. end
  108. before do
  109. purge_inbox
  110. imap.create(folder)
  111. imap.select(folder)
  112. end
  113. after do
  114. imap.delete(folder)
  115. end
  116. context 'when checking for imap status' do
  117. let(:inbound_options) do
  118. {
  119. adapter: 'imap',
  120. options: {
  121. host: ENV['MAIL_SERVER'],
  122. user: ENV['MAIL_ADDRESS'],
  123. password: server_password,
  124. ssl: true,
  125. ssl_verify: false,
  126. folder: folder,
  127. keep_on_server: false,
  128. },
  129. args: ['check']
  130. }
  131. end
  132. let(:email_without_date) do
  133. <<~EMAIL.gsub(%r{\n}, "\r\n")
  134. Subject: hello1
  135. From: shugo@example.com
  136. To: shugo@example.com
  137. Message-ID: <some1@example_without_date>
  138. hello world
  139. EMAIL
  140. end
  141. let(:email_now_date) do
  142. <<~EMAIL.gsub(%r{\n}, "\r\n")
  143. Subject: hello1
  144. Date: #{Time.current.rfc2822}
  145. From: shugo@example.com
  146. To: shugo@example.com
  147. Message-ID: <some1@example_now_date>
  148. hello world
  149. EMAIL
  150. end
  151. let(:email_old_date) do
  152. <<~EMAIL.gsub(%r{\n}, "\r\n")
  153. Subject: hello1
  154. Date: Mon, 01 Jan 2000 03:00:00 +0000
  155. From: shugo@example.com
  156. To: shugo@example.com
  157. Message-ID: <some1@example_old_date>
  158. hello world
  159. EMAIL
  160. end
  161. context 'with support for imap sort by date' do
  162. it 'with dateless mail' do
  163. imap.append(folder, email_without_date, [], Time.zone.now)
  164. expect_imap_fetch_check_results({ archive_possible: false, archive_possible_is_fallback: false })
  165. end
  166. it 'with now dated mail' do
  167. imap.append(folder, email_now_date, [], Time.zone.now)
  168. expect_imap_fetch_check_results({ archive_possible: false, archive_possible_is_fallback: false })
  169. end
  170. it 'with old dated mail' do
  171. imap.append(folder, email_old_date, [], Time.zone.now)
  172. expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: false })
  173. end
  174. end
  175. context 'without support for imap sort by date' do
  176. before do
  177. allow_any_instance_of(Net::IMAP).to receive(:sort).and_raise('this mail server does not support sorting by date')
  178. end
  179. it 'with dateless mail' do
  180. imap.append(folder, email_without_date, [], Time.zone.now)
  181. expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: true })
  182. end
  183. it 'with now dated mail' do
  184. imap.append(folder, email_now_date, [], Time.zone.now)
  185. expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: true })
  186. end
  187. it 'with old dated mail' do
  188. imap.append(folder, email_old_date, [], Time.zone.now)
  189. expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: false })
  190. end
  191. end
  192. context 'without sort capability' do
  193. before do
  194. allow_any_instance_of(Net::IMAP).to receive(:capabilities).and_return(%w[ID IDLE IMAP4REV1 MOVE STARTTLS UIDPLUS UNSELECT])
  195. end
  196. it 'with dateless mail' do
  197. imap.append(folder, email_without_date, [], Time.zone.now)
  198. expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: true })
  199. end
  200. it 'with now dated mail' do
  201. imap.append(folder, email_now_date, [], Time.zone.now)
  202. expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: true })
  203. end
  204. it 'with old dated mail' do
  205. imap.append(folder, email_old_date, [], Time.zone.now)
  206. expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: false })
  207. end
  208. end
  209. end
  210. context 'when fetching regular emails' do
  211. let(:email1) do
  212. <<~EMAIL.gsub(%r{\n}, "\r\n")
  213. Subject: hello1
  214. From: shugo@example.com
  215. To: shugo@example.com
  216. Message-ID: <some1@example_keep_on_server>
  217. hello world
  218. EMAIL
  219. end
  220. let(:email2) do
  221. <<~EMAIL.gsub(%r{\n}, "\r\n")
  222. Subject: hello2
  223. From: shugo@example.com
  224. To: shugo@example.com
  225. Message-ID: <some2@example_keep_on_server>
  226. hello world
  227. EMAIL
  228. end
  229. context 'with keep_on_server flag' do
  230. let(:inbound_options) do
  231. {
  232. adapter: 'imap',
  233. options: {
  234. host: ENV['MAIL_SERVER'],
  235. user: ENV['MAIL_ADDRESS'],
  236. password: server_password,
  237. ssl: true,
  238. ssl_verify: false,
  239. folder: folder,
  240. keep_on_server: true,
  241. }
  242. }
  243. end
  244. it 'handles messages correctly' do # rubocop:disable RSpec/ExampleLength
  245. imap.append(folder, email1, [], Time.zone.now)
  246. # verify if message is still on server
  247. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  248. expect(message_ids.count).to be(1)
  249. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  250. expect(message_meta['FLAGS']).not_to include(:Seen)
  251. # fetch messages - will import
  252. expect { channel.fetch(true) }.to change(Ticket::Article, :count)
  253. # verify if message is still on server
  254. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  255. expect(message_ids.count).to be(1)
  256. # message now has :seen flag
  257. message_meta = imap.fetch(1, ['RFC822.HEADER', 'FLAGS'])[0].attr
  258. expect(message_meta['FLAGS']).to include(:Seen)
  259. # fetch messages - will not import
  260. expect { channel.fetch(true) }.not_to change(Ticket::Article, :count)
  261. # verify if message is still on server
  262. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  263. expect(message_ids.count).to be(1)
  264. # put unseen message in it
  265. imap.append(folder, email2, [], Time.zone.now)
  266. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  267. expect(message_meta['FLAGS']).to include(:Seen)
  268. message_meta = imap.fetch(2, ['FLAGS'])[0].attr
  269. expect(message_meta['FLAGS']).not_to include(:Seen)
  270. # fetch messages - will import new
  271. expect { channel.fetch(true) }.to change(Ticket::Article, :count)
  272. # verify if message is still on server
  273. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  274. expect(message_ids.count).to be(2)
  275. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  276. expect(message_meta['FLAGS']).to include(:Seen)
  277. message_meta = imap.fetch(2, ['FLAGS'])[0].attr
  278. expect(message_meta['FLAGS']).to include(:Seen)
  279. # set messages to not seen
  280. imap.store(1, '-FLAGS', [:Seen])
  281. imap.store(2, '-FLAGS', [:Seen])
  282. # fetch messages - will still not import
  283. expect { channel.fetch(true) }.not_to change(Ticket::Article, :count)
  284. end
  285. end
  286. context 'without keep_on_server flag' do
  287. it 'handles messages correctly' do
  288. imap.append(folder, email1, [], Time.zone.now)
  289. # verify if message is still on server
  290. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  291. expect(message_ids.count).to be(1)
  292. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  293. expect(message_meta['FLAGS']).not_to include(:Seen)
  294. # fetch messages - will import
  295. expect { channel.fetch(true) }.to change(Ticket::Article, :count)
  296. # verify if message is still on server
  297. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  298. expect(message_ids.count).to be(1)
  299. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  300. expect(message_meta['FLAGS']).to include(:Seen, :Deleted)
  301. # put unseen message in it
  302. imap.append(folder, email2, [], Time.zone.now)
  303. # verify if message is still on server
  304. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  305. expect(message_ids.count).to be(1)
  306. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  307. expect(message_meta['FLAGS']).not_to include(:Seen)
  308. # fetch messages - will import
  309. expect { channel.fetch(true) }.to change(Ticket::Article, :count)
  310. # verify if message is still on server
  311. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  312. expect(message_ids.count).to be(1)
  313. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  314. expect(message_meta['FLAGS']).to include(:Seen)
  315. end
  316. end
  317. end
  318. context 'when fetching oversized emails' do
  319. let(:sender_email_address) { ENV['MAIL_ADDRESS'] }
  320. let(:cid) { SecureRandom.uuid.tr('-', '.') }
  321. let(:oversized_email) do
  322. <<~OVERSIZED_EMAIL.gsub(%r{\n}, "\r\n")
  323. Subject: Oversized Email Message
  324. From: Max Mustermann <#{sender_email_address}>
  325. To: shugo@example.com
  326. Message-ID: <#{cid}@zammad.test.com>
  327. Oversized Email Message Body #{'#' * 120_000}
  328. OVERSIZED_EMAIL
  329. end
  330. let(:oversized_email_md5) { Digest::MD5.hexdigest(oversized_email) }
  331. let(:oversized_email_size) { format('%<MB>.2f', MB: oversized_email.size.to_f / 1024 / 1024) }
  332. let(:fetch_oversized_email) do
  333. imap.append(folder, oversized_email, [], Time.zone.now)
  334. channel.fetch(true)
  335. end
  336. context 'with email reply' do
  337. before do
  338. Setting.set('postmaster_max_size', 0.1)
  339. fetch_oversized_email
  340. end
  341. let(:oversized_email_reply) do
  342. imap.select('inbox')
  343. 5.times do |i|
  344. sleep i
  345. msg = imap.sort(['DATE'], ['ALL'], 'US-ASCII').first
  346. if msg
  347. return imap.fetch(msg, 'RFC822')[0].attr['RFC822']
  348. end
  349. end
  350. nil
  351. end
  352. let(:parsed_oversized_email_reply) do
  353. Channel::EmailParser.new.parse(oversized_email_reply)
  354. end
  355. it 'creates email reply correctly' do
  356. # verify that a postmaster response email has been sent to the sender
  357. expect(oversized_email_reply).to be_present
  358. # parse the reply mail and verify the various headers
  359. expect(parsed_oversized_email_reply).to include(
  360. {
  361. from_email: email_address.email,
  362. subject: '[undeliverable] Message too large',
  363. 'references' => "<#{cid}@zammad.test.com>",
  364. 'in-reply-to' => "<#{cid}@zammad.test.com>",
  365. }
  366. )
  367. # verify the reply mail body content
  368. expect(parsed_oversized_email_reply[:body]).to match(%r{^Dear Max Mustermann.*Oversized Email Message.*#{oversized_email_size} MB.*0.1 MB.*#{Setting.get('fqdn')}}sm)
  369. # check if original mail got removed
  370. imap.select(folder)
  371. expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII')).to be_empty
  372. end
  373. end
  374. context 'without email reply' do
  375. before do
  376. Setting.set('postmaster_max_size', 0.1)
  377. Setting.set('postmaster_send_reject_if_mail_too_large', false)
  378. fetch_oversized_email
  379. end
  380. it 'does not create email reply' do
  381. # verify that no postmaster response email has been sent
  382. imap.select('inbox')
  383. sleep 1
  384. expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII').count).to be_zero
  385. # check that original mail is still there
  386. imap.select(folder)
  387. expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII').count).to be(1)
  388. end
  389. end
  390. end
  391. end
  392. describe '.fetch_message_body_key' do
  393. context 'with icloud mail server' do
  394. let(:host) { 'imap.mail.me.com' }
  395. it 'fetches mails with BODY field' do
  396. expect(described_class.new.fetch_message_body_key({ 'host' => host })).to eq('BODY[]')
  397. end
  398. end
  399. context 'with another mail server' do
  400. let(:host) { 'any.server.com' }
  401. it 'fetches mails with RFC822 field' do
  402. expect(described_class.new.fetch_message_body_key({ 'host' => host })).to eq('RFC822')
  403. end
  404. end
  405. end
  406. end