123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe Channel::Driver::Imap, integration: true, required_envs: %w[MAIL_SERVER MAIL_ADDRESS MAIL_PASS MAIL_ADDRESS_ASCII MAIL_PASS_ASCII] do
- # https://github.com/zammad/zammad/issues/2964
- context 'when connecting with a ASCII 8-Bit password' do
- it 'succeeds' do
- params = {
- host: ENV['MAIL_SERVER'],
- user: ENV['MAIL_ADDRESS_ASCII'],
- password: ENV['MAIL_PASS_ASCII'],
- ssl_verify: false,
- }
- result = described_class.new.fetch(params, nil, 'check')
- expect(result[:result]).to eq 'ok'
- end
- end
- describe '.parse_rfc822_headers' do
- it 'parses simple header' do
- expect(described_class.parse_rfc822_headers('Key: Value')).to have_key('Key').and(have_value('Value'))
- end
- it 'parses header with no white space' do
- expect(described_class.parse_rfc822_headers('Key:Value')).to have_key('Key').and(have_value('Value'))
- end
- it 'parses multiline header' do
- expect(described_class.parse_rfc822_headers("Key: Value\r\n2nd-key: 2nd-value"))
- .to have_key('Key').and(have_value('Value')).and(have_key('2nd-key')).and(have_value('2nd-value'))
- end
- it 'parses value with semicolons' do
- expect(described_class.parse_rfc822_headers('Key: Val:ue')).to have_key('Key').and(have_value('Val:ue'))
- end
- it 'parses key-only lines' do
- expect(described_class.parse_rfc822_headers('Key')).to have_key('Key')
- end
- it 'handles empty line' do
- expect { described_class.parse_rfc822_headers("Key: Value\r\n") }.not_to raise_error
- end
- it 'handles tabbed value' do
- expect(described_class.parse_rfc822_headers("Key: \r\n\tValue")).to have_key('Key').and(have_value('Value'))
- end
- end
- describe '.extract_rfc822_headers' do
- it 'extracts header' do
- object = Net::IMAP::FetchData.new :id, { 'RFC822.HEADER' => 'Key: Value' }
- expect(described_class.extract_rfc822_headers(object)).to have_key('Key').and(have_value('Value'))
- end
- it 'returns nil when header attribute is missing' do
- object = Net::IMAP::FetchData.new :id, { 'Another' => 'Key: Value' }
- expect(described_class.extract_rfc822_headers(object)).to be_nil
- end
- it 'does not raise error when given nil' do
- expect { described_class.extract_rfc822_headers(nil) }.not_to raise_error
- end
- end
- def expect_imap_fetch_check_results(result_params_to_compare = {})
- driver_call_result = {}
- expect { channel.fetch(true, driver_call_result) }.not_to change(Ticket::Article, :count)
- expect(driver_call_result).to include(result_params_to_compare)
- end
- describe '.fetch', :aggregate_failures do
- let(:folder) { "imap_spec-#{SecureRandom.uuid}" }
- let(:server_address) { ENV['MAIL_SERVER'] }
- let(:server_login) { ENV['MAIL_ADDRESS'] }
- let(:server_password) { ENV['MAIL_PASS'] }
- let(:email_address) { create(:email_address, name: 'Zammad Helpdesk', email: "some-zammad-#{ENV['MAIL_ADDRESS']}") }
- let(:group) { create(:group, email_address: email_address) }
- let(:inbound_options) do
- {
- adapter: 'imap',
- options: {
- host: ENV['MAIL_SERVER'],
- user: ENV['MAIL_ADDRESS'],
- password: server_password,
- ssl: true,
- ssl_verify: false,
- folder: folder,
- keep_on_server: false,
- }
- }
- end
- let(:outbound_options) do
- {
- adapter: 'smtp',
- options: {
- host: server_address,
- port: 25,
- start_tls: true,
- ssl_verify: false,
- user: server_login,
- password: server_password,
- email: email_address.email
- },
- }
- end
- let(:channel) do
- create(:email_channel, group: group, inbound: inbound_options, outbound: outbound_options).tap do |channel|
- email_address.channel = channel
- email_address.save!
- end
- end
- let(:imap) { Net::IMAP.new(server_address, port: 993, ssl: { verify_mode: OpenSSL::SSL::VERIFY_NONE }).tap { |imap| imap.login(server_login, server_password) } }
- let(:purge_inbox) do
- imap.select('inbox')
- imap.sort(['DATE'], ['ALL'], 'US-ASCII').each do |msg|
- imap.store(msg, '+FLAGS', [:Deleted])
- end
- imap.expunge
- end
- before do
- purge_inbox
- imap.create(folder)
- imap.select(folder)
- end
- after do
- imap.delete(folder)
- end
- context 'when checking for imap status' do
- let(:inbound_options) do
- {
- adapter: 'imap',
- options: {
- host: ENV['MAIL_SERVER'],
- user: ENV['MAIL_ADDRESS'],
- password: server_password,
- ssl: true,
- ssl_verify: false,
- folder: folder,
- keep_on_server: false,
- },
- args: ['check']
- }
- end
- let(:email_without_date) do
- <<~EMAIL.gsub(%r{\n}, "\r\n")
- Subject: hello1
- From: shugo@example.com
- To: shugo@example.com
- Message-ID: <some1@example_without_date>
- hello world
- EMAIL
- end
- let(:email_now_date) do
- <<~EMAIL.gsub(%r{\n}, "\r\n")
- Subject: hello1
- Date: #{Time.current.rfc2822}
- From: shugo@example.com
- To: shugo@example.com
- Message-ID: <some1@example_now_date>
- hello world
- EMAIL
- end
- let(:email_old_date) do
- <<~EMAIL.gsub(%r{\n}, "\r\n")
- Subject: hello1
- Date: Mon, 01 Jan 2000 03:00:00 +0000
- From: shugo@example.com
- To: shugo@example.com
- Message-ID: <some1@example_old_date>
- hello world
- EMAIL
- end
- context 'with support for imap sort by date' do
- it 'with dateless mail' do
- imap.append(folder, email_without_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: false, archive_possible_is_fallback: false })
- end
- it 'with now dated mail' do
- imap.append(folder, email_now_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: false, archive_possible_is_fallback: false })
- end
- it 'with old dated mail' do
- imap.append(folder, email_old_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: false })
- end
- end
- context 'without support for imap sort by date' do
- before do
- allow_any_instance_of(Net::IMAP).to receive(:sort).and_raise('this mail server does not support sorting by date')
- end
- it 'with dateless mail' do
- imap.append(folder, email_without_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: true })
- end
- it 'with now dated mail' do
- imap.append(folder, email_now_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: true })
- end
- it 'with old dated mail' do
- imap.append(folder, email_old_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: false })
- end
- end
- context 'without sort capability' do
- before do
- allow_any_instance_of(Net::IMAP).to receive(:capabilities).and_return(%w[ID IDLE IMAP4REV1 MOVE STARTTLS UIDPLUS UNSELECT])
- end
- it 'with dateless mail' do
- imap.append(folder, email_without_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: true })
- end
- it 'with now dated mail' do
- imap.append(folder, email_now_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: true })
- end
- it 'with old dated mail' do
- imap.append(folder, email_old_date, [], Time.zone.now)
- expect_imap_fetch_check_results({ archive_possible: true, archive_possible_is_fallback: false })
- end
- end
- end
- context 'when fetching regular emails' do
- let(:email1) do
- <<~EMAIL.gsub(%r{\n}, "\r\n")
- Subject: hello1
- From: shugo@example.com
- To: shugo@example.com
- Message-ID: <some1@example_keep_on_server>
- hello world
- EMAIL
- end
- let(:email2) do
- <<~EMAIL.gsub(%r{\n}, "\r\n")
- Subject: hello2
- From: shugo@example.com
- To: shugo@example.com
- Message-ID: <some2@example_keep_on_server>
- hello world
- EMAIL
- end
- context 'with keep_on_server flag' do
- let(:inbound_options) do
- {
- adapter: 'imap',
- options: {
- host: ENV['MAIL_SERVER'],
- user: ENV['MAIL_ADDRESS'],
- password: server_password,
- ssl: true,
- ssl_verify: false,
- folder: folder,
- keep_on_server: true,
- }
- }
- end
- it 'handles messages correctly' do # rubocop:disable RSpec/ExampleLength
- imap.append(folder, email1, [], Time.zone.now)
- # verify if message is still on server
- message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
- expect(message_ids.count).to be(1)
- message_meta = imap.fetch(1, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).not_to include(:Seen)
- # fetch messages - will import
- expect { channel.fetch(true) }.to change(Ticket::Article, :count)
- # verify if message is still on server
- message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
- expect(message_ids.count).to be(1)
- # message now has :seen flag
- message_meta = imap.fetch(1, ['RFC822.HEADER', 'FLAGS'])[0].attr
- expect(message_meta['FLAGS']).to include(:Seen)
- # fetch messages - will not import
- expect { channel.fetch(true) }.not_to change(Ticket::Article, :count)
- # verify if message is still on server
- message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
- expect(message_ids.count).to be(1)
- # put unseen message in it
- imap.append(folder, email2, [], Time.zone.now)
- message_meta = imap.fetch(1, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).to include(:Seen)
- message_meta = imap.fetch(2, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).not_to include(:Seen)
- # fetch messages - will import new
- expect { channel.fetch(true) }.to change(Ticket::Article, :count)
- # verify if message is still on server
- message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
- expect(message_ids.count).to be(2)
- message_meta = imap.fetch(1, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).to include(:Seen)
- message_meta = imap.fetch(2, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).to include(:Seen)
- # set messages to not seen
- imap.store(1, '-FLAGS', [:Seen])
- imap.store(2, '-FLAGS', [:Seen])
- # fetch messages - will still not import
- expect { channel.fetch(true) }.not_to change(Ticket::Article, :count)
- end
- end
- context 'without keep_on_server flag' do
- it 'handles messages correctly' do
- imap.append(folder, email1, [], Time.zone.now)
- # verify if message is still on server
- message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
- expect(message_ids.count).to be(1)
- message_meta = imap.fetch(1, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).not_to include(:Seen)
- # fetch messages - will import
- expect { channel.fetch(true) }.to change(Ticket::Article, :count)
- # verify if message is still on server
- message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
- expect(message_ids.count).to be(1)
- message_meta = imap.fetch(1, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).to include(:Seen, :Deleted)
- # put unseen message in it
- imap.append(folder, email2, [], Time.zone.now)
- # verify if message is still on server
- message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
- expect(message_ids.count).to be(1)
- message_meta = imap.fetch(1, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).not_to include(:Seen)
- # fetch messages - will import
- expect { channel.fetch(true) }.to change(Ticket::Article, :count)
- # verify if message is still on server
- message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
- expect(message_ids.count).to be(1)
- message_meta = imap.fetch(1, ['FLAGS'])[0].attr
- expect(message_meta['FLAGS']).to include(:Seen)
- end
- end
- end
- context 'when fetching oversized emails' do
- let(:sender_email_address) { ENV['MAIL_ADDRESS'] }
- let(:cid) { SecureRandom.uuid.tr('-', '.') }
- let(:oversized_email) do
- <<~OVERSIZED_EMAIL.gsub(%r{\n}, "\r\n")
- Subject: Oversized Email Message
- From: Max Mustermann <#{sender_email_address}>
- To: shugo@example.com
- Message-ID: <#{cid}@zammad.test.com>
- Oversized Email Message Body #{'#' * 120_000}
- OVERSIZED_EMAIL
- end
- let(:oversized_email_md5) { Digest::MD5.hexdigest(oversized_email) }
- let(:oversized_email_size) { format('%<MB>.2f', MB: oversized_email.size.to_f / 1024 / 1024) }
- let(:fetch_oversized_email) do
- imap.append(folder, oversized_email, [], Time.zone.now)
- channel.fetch(true)
- end
- context 'with email reply' do
- before do
- Setting.set('postmaster_max_size', 0.1)
- fetch_oversized_email
- end
- let(:oversized_email_reply) do
- imap.select('inbox')
- 5.times do |i|
- sleep i
- msg = imap.sort(['DATE'], ['ALL'], 'US-ASCII').first
- if msg
- return imap.fetch(msg, 'RFC822')[0].attr['RFC822']
- end
- end
- nil
- end
- let(:parsed_oversized_email_reply) do
- Channel::EmailParser.new.parse(oversized_email_reply)
- end
- it 'creates email reply correctly' do
- # verify that a postmaster response email has been sent to the sender
- expect(oversized_email_reply).to be_present
- # parse the reply mail and verify the various headers
- expect(parsed_oversized_email_reply).to include(
- {
- from_email: email_address.email,
- subject: '[undeliverable] Message too large',
- 'references' => "<#{cid}@zammad.test.com>",
- 'in-reply-to' => "<#{cid}@zammad.test.com>",
- }
- )
- # verify the reply mail body content
- 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)
- # check if original mail got removed
- imap.select(folder)
- expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII')).to be_empty
- end
- end
- context 'without email reply' do
- before do
- Setting.set('postmaster_max_size', 0.1)
- Setting.set('postmaster_send_reject_if_mail_too_large', false)
- fetch_oversized_email
- end
- it 'does not create email reply' do
- # verify that no postmaster response email has been sent
- imap.select('inbox')
- sleep 1
- expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII').count).to be_zero
- # check that original mail is still there
- imap.select(folder)
- expect(imap.sort(['DATE'], ['ALL'], 'US-ASCII').count).to be(1)
- end
- end
- end
- end
- describe '.fetch_message_body_key' do
- context 'with icloud mail server' do
- let(:host) { 'imap.mail.me.com' }
- it 'fetches mails with BODY field' do
- expect(described_class.new.fetch_message_body_key({ 'host' => host })).to eq('BODY[]')
- end
- end
- context 'with another mail server' do
- let(:host) { 'any.server.com' }
- it 'fetches mails with RFC822 field' do
- expect(described_class.new.fetch_message_body_key({ 'host' => host })).to eq('RFC822')
- end
- end
- end
- end
|