imap_spec.rb 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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. end
  193. context 'when fetching regular emails' do
  194. let(:email1) do
  195. <<~EMAIL.gsub(%r{\n}, "\r\n")
  196. Subject: hello1
  197. From: shugo@example.com
  198. To: shugo@example.com
  199. Message-ID: <some1@example_keep_on_server>
  200. hello world
  201. EMAIL
  202. end
  203. let(:email2) do
  204. <<~EMAIL.gsub(%r{\n}, "\r\n")
  205. Subject: hello2
  206. From: shugo@example.com
  207. To: shugo@example.com
  208. Message-ID: <some2@example_keep_on_server>
  209. hello world
  210. EMAIL
  211. end
  212. context 'with keep_on_server flag' do
  213. let(:inbound_options) do
  214. {
  215. adapter: 'imap',
  216. options: {
  217. host: ENV['MAIL_SERVER'],
  218. user: ENV['MAIL_ADDRESS'],
  219. password: server_password,
  220. ssl: true,
  221. ssl_verify: false,
  222. folder: folder,
  223. keep_on_server: true,
  224. }
  225. }
  226. end
  227. it 'handles messages correctly' do # rubocop:disable RSpec/ExampleLength
  228. imap.append(folder, email1, [], Time.zone.now)
  229. # verify if message is still on server
  230. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  231. expect(message_ids.count).to be(1)
  232. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  233. expect(message_meta['FLAGS']).not_to include(:Seen)
  234. # fetch messages - will import
  235. expect { channel.fetch(true) }.to change(Ticket::Article, :count)
  236. # verify if message is still on server
  237. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  238. expect(message_ids.count).to be(1)
  239. # message now has :seen flag
  240. message_meta = imap.fetch(1, ['RFC822.HEADER', 'FLAGS'])[0].attr
  241. expect(message_meta['FLAGS']).to include(:Seen)
  242. # fetch messages - will not import
  243. expect { channel.fetch(true) }.not_to change(Ticket::Article, :count)
  244. # verify if message is still on server
  245. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  246. expect(message_ids.count).to be(1)
  247. # put unseen message in it
  248. imap.append(folder, email2, [], Time.zone.now)
  249. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  250. expect(message_meta['FLAGS']).to include(:Seen)
  251. message_meta = imap.fetch(2, ['FLAGS'])[0].attr
  252. expect(message_meta['FLAGS']).not_to include(:Seen)
  253. # fetch messages - will import new
  254. expect { channel.fetch(true) }.to change(Ticket::Article, :count)
  255. # verify if message is still on server
  256. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  257. expect(message_ids.count).to be(2)
  258. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  259. expect(message_meta['FLAGS']).to include(:Seen)
  260. message_meta = imap.fetch(2, ['FLAGS'])[0].attr
  261. expect(message_meta['FLAGS']).to include(:Seen)
  262. # set messages to not seen
  263. imap.store(1, '-FLAGS', [:Seen])
  264. imap.store(2, '-FLAGS', [:Seen])
  265. # fetch messages - will still not import
  266. expect { channel.fetch(true) }.not_to change(Ticket::Article, :count)
  267. end
  268. end
  269. context 'without keep_on_server flag' do
  270. it 'handles messages correctly' do
  271. imap.append(folder, email1, [], Time.zone.now)
  272. # verify if message is still on server
  273. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  274. expect(message_ids.count).to be(1)
  275. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  276. expect(message_meta['FLAGS']).not_to include(:Seen)
  277. # fetch messages - will import
  278. expect { channel.fetch(true) }.to change(Ticket::Article, :count)
  279. # verify if message is still on server
  280. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  281. expect(message_ids.count).to be(1)
  282. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  283. expect(message_meta['FLAGS']).to include(:Seen, :Deleted)
  284. # put unseen message in it
  285. imap.append(folder, email2, [], Time.zone.now)
  286. # verify if message is still on server
  287. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  288. expect(message_ids.count).to be(1)
  289. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  290. expect(message_meta['FLAGS']).not_to include(:Seen)
  291. # fetch messages - will import
  292. expect { channel.fetch(true) }.to change(Ticket::Article, :count)
  293. # verify if message is still on server
  294. message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
  295. expect(message_ids.count).to be(1)
  296. message_meta = imap.fetch(1, ['FLAGS'])[0].attr
  297. expect(message_meta['FLAGS']).to include(:Seen)
  298. end
  299. end
  300. end
  301. context 'when fetching oversized emails' do
  302. let(:sender_email_address) { ENV['MAIL_ADDRESS'] }
  303. let(:cid) { SecureRandom.uuid.tr('-', '.') }
  304. let(:oversized_email) do
  305. <<~OVERSIZED_EMAIL.gsub(%r{\n}, "\r\n")
  306. Subject: Oversized Email Message
  307. From: Max Mustermann <#{sender_email_address}>
  308. To: shugo@example.com
  309. Message-ID: <#{cid}@zammad.test.com>
  310. Oversized Email Message Body #{'#' * 120_000}
  311. OVERSIZED_EMAIL
  312. end
  313. let(:oversized_email_md5) { Digest::MD5.hexdigest(oversized_email) }
  314. let(:oversized_email_size) { format('%<MB>.2f', MB: oversized_email.size.to_f / 1024 / 1024) }
  315. let(:fetch_oversized_email) do
  316. imap.append(folder, oversized_email, [], Time.zone.now)
  317. channel.fetch(true)
  318. end
  319. context 'with email reply' do
  320. before do
  321. Setting.set('postmaster_max_size', 0.1)
  322. fetch_oversized_email
  323. end
  324. let(:oversized_email_reply) do
  325. imap.select('inbox')
  326. 5.times do |i|
  327. sleep i
  328. msg = imap.sort(['DATE'], ['ALL'], 'US-ASCII').first
  329. if msg
  330. return imap.fetch(msg, 'RFC822')[0].attr['RFC822']
  331. end
  332. end
  333. nil
  334. end
  335. let(:parsed_oversized_email_reply) do
  336. Channel::EmailParser.new.parse(oversized_email_reply)
  337. end
  338. it 'creates email reply correctly' do
  339. # verify that a postmaster response email has been sent to the sender
  340. expect(oversized_email_reply).to be_present
  341. # parse the reply mail and verify the various headers
  342. expect(parsed_oversized_email_reply).to include(
  343. {
  344. from_email: email_address.email,
  345. subject: '[undeliverable] Message too large',
  346. 'references' => "<#{cid}@zammad.test.com>",
  347. 'in-reply-to' => "<#{cid}@zammad.test.com>",
  348. }
  349. )
  350. # verify the reply mail body content
  351. 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)
  352. # check if original mail got removed
  353. imap.select(folder)
  354. expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII')).to be_empty
  355. end
  356. end
  357. context 'without email reply' do
  358. before do
  359. Setting.set('postmaster_max_size', 0.1)
  360. Setting.set('postmaster_send_reject_if_mail_too_large', false)
  361. fetch_oversized_email
  362. end
  363. it 'does not create email reply' do
  364. # verify that no postmaster response email has been sent
  365. imap.select('inbox')
  366. sleep 1
  367. expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII').count).to be_zero
  368. # check that original mail is still there
  369. imap.select(folder)
  370. expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII').count).to be(1)
  371. end
  372. end
  373. end
  374. end
  375. describe '.fetch_message_body_key' do
  376. context 'with icloud mail server' do
  377. let(:host) { 'imap.mail.me.com' }
  378. it 'fetches mails with BODY field' do
  379. expect(described_class.new.fetch_message_body_key({ 'host' => host })).to eq('BODY[]')
  380. end
  381. end
  382. context 'with another mail server' do
  383. let(:host) { 'any.server.com' }
  384. it 'fetches mails with RFC822 field' do
  385. expect(described_class.new.fetch_message_body_key({ 'host' => host })).to eq('RFC822')
  386. end
  387. end
  388. end
  389. end