From fae194918eb8a07036060a1b41f1b33c45e03cad Mon Sep 17 00:00:00 2001 From: Billy Zhou Date: Tue, 6 Aug 2019 17:26:29 +0200 Subject: [PATCH] Implemented issue #2092 - Send postmaster email to sender if email is too big --- .gitlab-ci.yml | 1 + app/models/channel/driver/imap.rb | 41 ++-- app/models/channel/driver/pop3.rb | 30 ++- app/models/channel/email_parser.rb | 88 +++++++- app/views/mailer/email_oversized/de.txt.erb | 12 + app/views/mailer/email_oversized/en.txt.erb | 12 + ...01_setting_postmaster_send_reject_email.rb | 34 +++ db/seeds/settings.rb | 27 +++ lib/notification_factory.rb | 15 +- lib/notification_factory/mailer.rb | 6 +- spec/lib/notification_factory/mailer_spec.rb | 93 ++++++++ spec/models/channel/email_parser_spec.rb | 120 ++++++++++ .../integration/email_postmaster_to_sender.rb | 213 ++++++++++++++++++ 13 files changed, 661 insertions(+), 31 deletions(-) create mode 100644 app/views/mailer/email_oversized/de.txt.erb create mode 100644 app/views/mailer/email_oversized/en.txt.erb create mode 100644 db/migrate/20190806000001_setting_postmaster_send_reject_email.rb create mode 100644 spec/lib/notification_factory/mailer_spec.rb create mode 100644 test/integration/email_postmaster_to_sender.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3ccef350..e2977302d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -172,6 +172,7 @@ test:integration:email_helper_deliver: - bundle exec rails test test/integration/email_helper_test.rb - bundle exec rails test test/integration/email_deliver_test.rb - bundle exec rails test test/integration/email_keep_on_server_test.rb + - bundle exec rails test test/integration/email_postmaster_to_sender.rb test:integration:facebook: <<: *test_integration_definition diff --git a/app/models/channel/driver/imap.rb b/app/models/channel/driver/imap.rb index 7004dcff5..6f587ed4d 100644 --- a/app/models/channel/driver/imap.rb +++ b/app/models/channel/driver/imap.rb @@ -206,6 +206,7 @@ example count = 0 count_fetched = 0 count_max = 5000 + too_large_messages = [] active_check_interval = 20 notice = '' message_ids.each do |message_id| @@ -226,13 +227,6 @@ example # ignore verify messages next if !messages_is_too_old_verify?(message_meta, count, count_all) - # ignore to big messages - info = too_big?(message_meta, count, count_all) - if info - notice += "#{info}\n" - next - end - # ignore deleted messages next if deleted?(message_meta, count, count_all) @@ -251,7 +245,24 @@ example end next if !msg - process(channel, msg, false) + # do not process too big messages, instead download & send postmaster reply + too_large_info = too_large?(message_meta) + if too_large_info + if Setting.get('postmaster_send_reject_if_mail_too_large') == true + info = " - download message #{count}/#{count_all} - ignore message because it's too large (is:#{too_large_info[0]} MB/max:#{too_large_info[1]} MB)" + Rails.logger.info info + notice += "#{info}\n" + process_oversized_mail(channel, msg) + else + info = " - ignore message #{count}/#{count_all} - because message is too large (is:#{too_large_info[0]} MB/max:#{too_large_info[1]} MB)" + Rails.logger.info info + notice += "#{info}\n" + too_large_messages.push info + next + end + else + process(channel, msg, false) + end begin timeout(FETCH_MSG_TIMEOUT) do @@ -282,6 +293,11 @@ example if count.zero? Rails.logger.info ' - no message' end + + if too_large_messages.present? + raise too_large_messages.join("\n") + end + Rails.logger.info 'done' { result: 'ok', @@ -420,7 +436,7 @@ returns check if email is to big - Channel::Driver::IMAP.too_big?(message_meta, count, count_all) + Channel::Driver::IMAP.too_large?(message_meta, count, count_all) returns @@ -428,14 +444,13 @@ returns =end - def too_big?(message_meta, count, count_all) + def too_large?(message_meta) max_message_size = Setting.get('postmaster_max_size').to_f real_message_size = message_meta.attr['RFC822.SIZE'].to_f / 1024 / 1024 if real_message_size > max_message_size - info = " - ignore message #{count}/#{count_all} - because message is too big (is:#{real_message_size} MB/max:#{max_message_size} MB)" - Rails.logger.info info - return info + return [real_message_size, max_message_size] end + false end diff --git a/app/models/channel/driver/pop3.rb b/app/models/channel/driver/pop3.rb index 84596ca88..227b0a0be 100644 --- a/app/models/channel/driver/pop3.rb +++ b/app/models/channel/driver/pop3.rb @@ -136,6 +136,7 @@ returns count_all = mails.size count = 0 count_fetched = 0 + too_large_messages = [] active_check_interval = 20 notice = '' mails.first(2000).each do |m| @@ -165,18 +166,28 @@ returns end end - # ignore to big messages + # do not process too large messages, instead download and send postmaster reply max_message_size = Setting.get('postmaster_max_size').to_f real_message_size = mail.size.to_f / 1024 / 1024 if real_message_size > max_message_size - info = " - ignore message #{count}/#{count_all} - because message is too big (is:#{real_message_size} MB/max:#{max_message_size} MB)" - Rails.logger.info info - notice += "#{info}\n" - next - end + if Setting.get('postmaster_send_reject_if_mail_too_large') == true + info = " - download message #{count}/#{count_all} - ignore message because it's too large (is:#{real_message_size} MB/max:#{max_message_size} MB)" + Rails.logger.info info + notice += "#{info}\n" + process_oversized_mail(channel, mail) + else + info = " - ignore message #{count}/#{count_all} - because message is too large (is:#{real_message_size} MB/max:#{max_message_size} MB)" + Rails.logger.info info + notice += "#{info}\n" + too_large_messages.push info + next + end # delete email from server after article was created - process(channel, m.pop, false) + else + process(channel, m.pop, false) + end + m.delete count_fetched += 1 end @@ -184,6 +195,11 @@ returns if count.zero? Rails.logger.info ' - no message' end + + if too_large_messages.present? + raise too_large_messages.join("\n") + end + Rails.logger.info 'done' { result: 'ok', diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb index 9209bdc92..f8c82c23d 100644 --- a/app/models/channel/email_parser.rb +++ b/app/models/channel/email_parser.rb @@ -116,18 +116,15 @@ returns end rescue => e # store unprocessable email for bug reporting - path = Rails.root.join('tmp', 'unprocessable_mail') - FileUtils.mkpath path - md5 = Digest::MD5.hexdigest(msg) - filename = "#{path}/#{md5}.eml" + filename = archive_mail('unprocessable_mail', msg) + message = "ERROR: Can't process email, you will find it for bug reporting under #{filename}, please create an issue at https://github.com/zammad/zammad/issues" + p message # rubocop:disable Rails/Output p 'ERROR: ' + e.inspect # rubocop:disable Rails/Output Rails.logger.error message Rails.logger.error e - File.open(filename, 'wb') do |file| - file.write msg - end + return false if exception == false raise e.inspect + "\n" + e.backtrace.join("\n") @@ -486,6 +483,19 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again files end +=begin + + process oversized emails by: + 1. Archiving the oversized mail as tmp/oversized_mail/timestamp_md5.eml + 2. Reply with a postmaster message to inform the sender + +=end + + def process_oversized_mail(channel, msg) + archive_mail('oversized_mail', msg) + postmaster_response(channel, msg) + end + private def message_header_hash(mail) @@ -783,6 +793,70 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again [attach] end + + # Archive the given message as tmp/folder/timestamp_md5.eml + def archive_mail(folder, msg) + path = Rails.root.join('tmp', folder) + FileUtils.mkpath path + + # MD5 hash the msg and save it as "timestamp_md5.eml" + md5 = Digest::MD5.hexdigest(msg) + filename = "#{Time.zone.now.iso8601}_#{md5}.eml" + file_path = Rails.root.join('tmp', folder, filename) + + File.open(file_path, 'wb') do |file| + file.write msg + end + + file_path + end + + # Auto reply as the postmaster to oversized emails with: + # [ALERT] Message too large + def postmaster_response(channel, msg) + begin + reply_mail = compose_postmaster_reply(msg) + rescue NotificationFactory::FileNotFoundError => e + Rails.logger.error 'No valid postmaster email_oversized template found. Skipping postmaster reply. ' + e.inspect + return + end + + Rails.logger.error "Send mail too large postmaster message to: #{reply_mail[:to]}" + reply_mail[:from] = EmailAddress.find_by(channel: channel).email + channel.deliver(reply_mail) + rescue => e + Rails.logger.error "Error during sending of postmaster oversized email auto-reply: #{e.inspect}\n#{e.backtrace}" + end + + # Compose a "Message too large" reply to the given message + def compose_postmaster_reply(raw_incoming_mail, locale = nil) + parsed_incoming_mail = Channel::EmailParser.new.parse(raw_incoming_mail) + + # construct a dummy mail object + mail = OpenStruct.new + mail.from_display_name = parsed_incoming_mail[:from_display_name] + mail.subject = parsed_incoming_mail[:subject] + mail.msg_size = format('%.2f', raw_incoming_mail.size.to_f / 1024 / 1024) + + reply = NotificationFactory::Mailer.template( + template: 'email_oversized', + locale: locale, + format: 'txt', + objects: { + mail: mail, + }, + raw: true, # will not add application template + standalone: true, # default: false - will send header & footer + ) + + reply.merge( + to: parsed_incoming_mail[:from_email], + body: reply[:body].gsub(/\n/, "\r\n"), + content_type: 'text/plain', + References: parsed_incoming_mail[:message_id], + 'In-Reply-To': parsed_incoming_mail[:message_id], + ) + end end module Mail diff --git a/app/views/mailer/email_oversized/de.txt.erb b/app/views/mailer/email_oversized/de.txt.erb new file mode 100644 index 000000000..6a6030cb6 --- /dev/null +++ b/app/views/mailer/email_oversized/de.txt.erb @@ -0,0 +1,12 @@ +[Unzustellbar] Nachricht zu groß +Hallo #{mail.from_display_name}, + +Ihre E-Mail mit dem Betreff "#{mail.subject}" konnte nicht an einen oder mehrere Empfänger zugestellt werden. + +Die Nachricht hatte eine Größe von #{mail.msg_size} MB, wir akzeptieren jedoch nur E-Mails mit einer Größe von bis zu #{config.postmaster_max_size} MB. + +Bitte reduzieren Sie die Größe Ihrer Nachricht und versuchen Sie es erneut. Vielen Dank für Ihr Verständnis. + +Mit freundlichen Grüßen + +Postmaster von #{config.fqdn} diff --git a/app/views/mailer/email_oversized/en.txt.erb b/app/views/mailer/email_oversized/en.txt.erb new file mode 100644 index 000000000..24eea2f21 --- /dev/null +++ b/app/views/mailer/email_oversized/en.txt.erb @@ -0,0 +1,12 @@ +[ALERT] Message too large +Dear #{mail.from_display_name}, + +Unfortunately your email titled "#{mail.subject}" could not be delivered to one or more recipients. + +Your message was #{mail.msg_size} MB but we only accept messages up to #{config.postmaster_max_size} MB. + +Please reduce the message size and try again. Thank you for your understanding. + +Regretfully, + +Postmaster of #{config.fqdn} diff --git a/db/migrate/20190806000001_setting_postmaster_send_reject_email.rb b/db/migrate/20190806000001_setting_postmaster_send_reject_email.rb new file mode 100644 index 000000000..6c1ea1975 --- /dev/null +++ b/db/migrate/20190806000001_setting_postmaster_send_reject_email.rb @@ -0,0 +1,34 @@ +class SettingPostmasterSendRejectEmail < ActiveRecord::Migration[5.2] + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'Send postmaster mail if mail too large', + name: 'postmaster_send_reject_if_mail_too_large', + area: 'Email::Base', + description: 'Send postmaster reject mail to sender of mail if mail is too large.', + options: { + form: [ + { + display: '', + null: true, + name: 'postmaster_send_reject_if_mail_too_large', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { + online_service_disable: true, + permission: ['admin.channel_email'], + }, + frontend: false + ) + end +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 1a0ebcb0e..3b4d6aff2 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -2582,6 +2582,33 @@ Setting.create_if_not_exists( frontend: false ) +Setting.create_if_not_exists( + title: 'Send postmaster mail if mail too large', + name: 'postmaster_send_reject_if_mail_too_large', + area: 'Email::Base', + description: 'Send postmaster reject mail to sender of mail if mail is too large.', + options: { + form: [ + { + display: '', + null: true, + name: 'postmaster_send_reject_if_mail_too_large', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { + online_service_disable: true, + permission: ['admin.channel_email'], + }, + frontend: false +) + Setting.create_if_not_exists( title: 'Notification Sender', name: 'notification_sender', diff --git a/lib/notification_factory.rb b/lib/notification_factory.rb index b7d92fcff..931799516 100644 --- a/lib/notification_factory.rb +++ b/lib/notification_factory.rb @@ -29,17 +29,26 @@ returns =end + class FileNotFoundError < StandardError; end + def self.template_read(data) - template = File.readlines(template_path(data)) + template_path = template_path(data) + + template = File.readlines(template_path) { subject: template.shift, body: template.join } end def self.template_path(data) - template_filenames(data) + candidates = template_filenames(data) .map { |filename| data.merge(filename: filename) } .map { |data_hash| TEMPLATE_PATH_STRING % data_hash } - .find(&File.method(:exist?)) + + found = candidates.find(&File.method(:exist?)) + + raise FileNotFoundError, "Missing template files #{candidates}!" if !found + + found end private_class_method :template_path diff --git a/lib/notification_factory/mailer.rb b/lib/notification_factory/mailer.rb index 463e3bd87..d4358abb6 100644 --- a/lib/notification_factory/mailer.rb +++ b/lib/notification_factory/mailer.rb @@ -281,7 +281,7 @@ returns template = NotificationFactory.template_read( locale: data[:locale] || Setting.get('locale_default') || 'en-us', template: data[:template], - format: 'html', + format: data[:format] || 'html', type: 'mailer', ) @@ -292,6 +292,10 @@ returns template: template[:subject], escape: false ).render + + # strip off the extra newline at the end for the subjects of plaintext templates + message_subject.chomp! if data[:format] == 'txt' + message_body = NotificationFactory::Renderer.new( objects: data[:objects], locale: data[:locale], diff --git a/spec/lib/notification_factory/mailer_spec.rb b/spec/lib/notification_factory/mailer_spec.rb new file mode 100644 index 000000000..5cc98a22a --- /dev/null +++ b/spec/lib/notification_factory/mailer_spec.rb @@ -0,0 +1,93 @@ +require 'rails_helper' +require 'ostruct' + +RSpec.describe NotificationFactory::Mailer do + describe '#template' do + context 'for postmaster oversized mail' do + let(:raw_incoming_mail) { File.read(Rails.root.join('test', 'data', 'mail', 'mail010.box')) } + + let(:parsed_incoming_mail) { Channel::EmailParser.new.parse raw_incoming_mail } + + let(:incoming_mail) do + mail = OpenStruct.new + mail.from_display_name = parsed_incoming_mail[:from_display_name] + mail.subject = parsed_incoming_mail[:subject] + mail.msg_size = format('%.2f', raw_incoming_mail.size.to_f / 1024 / 1024) + mail + end + + let(:en_expected_subject) { '[ALERT] Message too large' } + + let(:en_expected_body) do + <<~BODY + Dear Smith Sepp, + + Unfortunately your email titled \"Gruß aus Oberalteich\" could not be delivered to one or more recipients. + + Your message was 0.01 MB but we only accept messages up to 10 MB. + + Please reduce the message size and try again. Thank you for your understanding. + + Regretfully, + + Postmaster of zammad.example.com + BODY + end + + shared_examples 'plaintext mail templating' do + it 'templates correctly' do + result = described_class.template( + template: 'email_oversized', + locale: locale, + format: 'txt', + objects: { + mail: incoming_mail, + }, + raw: true, # will not add application template + standalone: true, # default: false - will send header & footer + ) + expect(result[:subject]).to eq(expected_subject) + expect(result[:body]).to eq(expected_body) + end + end + + context 'English locale (en)' do + include_examples 'plaintext mail templating' do + let(:locale) { 'en' } + let(:expected_subject) { en_expected_subject } + let(:expected_body) { en_expected_body } + end + end + + context 'German locale (de)' do + include_examples 'plaintext mail templating' do + let(:locale) { 'de' } + let(:expected_subject) { '[Unzustellbar] Nachricht zu groß' } + let(:expected_body) do + <<~BODY + Hallo Smith Sepp, + + Ihre E-Mail mit dem Betreff \"Gruß aus Oberalteich\" konnte nicht an einen oder mehrere Empfänger zugestellt werden. + + 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. + + Bitte reduzieren Sie die Größe Ihrer Nachricht und versuchen Sie es erneut. Vielen Dank für Ihr Verständnis. + + Mit freundlichen Grüßen + + Postmaster von zammad.example.com + BODY + end + end + end + + context 'unsupported locale, which defaults back to English locale (en)' do + include_examples 'plaintext mail templating' do + let(:locale) { 'UNSUPPORTED_LOCALE' } + let(:expected_subject) { en_expected_subject } + let(:expected_body) { en_expected_body } + end + end + end + end +end diff --git a/spec/models/channel/email_parser_spec.rb b/spec/models/channel/email_parser_spec.rb index aae355be2..c15e6a021 100644 --- a/spec/models/channel/email_parser_spec.rb +++ b/spec/models/channel/email_parser_spec.rb @@ -997,4 +997,124 @@ RSpec.describe Channel::EmailParser, type: :model do end end end + + describe '#compose_postmaster_reply' do + let(:raw_incoming_mail) { File.read(Rails.root.join('test', 'data', 'mail', 'mail010.box')) } + + shared_examples 'postmaster reply' do + it 'composes postmaster reply' do + reply = Channel::EmailParser.new.send(:compose_postmaster_reply, raw_incoming_mail, locale) + expect(reply[:to]).to eq('smith@example.com') + expect(reply[:content_type]).to eq('text/plain') + expect(reply[:subject]).to eq(expected_subject) + expect(reply[:body]).to eq(expected_body) + end + end + + context 'for English locale (en)' do + include_examples 'postmaster reply' do + let(:locale) { 'en' } + let(:expected_subject) { '[ALERT] Message too large' } + let(:expected_body) do + body = <<~BODY + Dear Smith Sepp, + + Unfortunately your email titled \"Gruß aus Oberalteich\" could not be delivered to one or more recipients. + + Your message was 0.01 MB but we only accept messages up to 10 MB. + + Please reduce the message size and try again. Thank you for your understanding. + + Regretfully, + + Postmaster of zammad.example.com + BODY + body.gsub(/\n/, "\r\n") + end + end + end + + context 'for German locale (de)' do + include_examples 'postmaster reply' do + let(:locale) { 'de' } + let(:expected_subject) { '[Unzustellbar] Nachricht zu groß' } + let(:expected_body) do + body = <<~BODY + Hallo Smith Sepp, + + Ihre E-Mail mit dem Betreff \"Gruß aus Oberalteich\" konnte nicht an einen oder mehrere Empfänger zugestellt werden. + + 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. + + Bitte reduzieren Sie die Größe Ihrer Nachricht und versuchen Sie es erneut. Vielen Dank für Ihr Verständnis. + + Mit freundlichen Grüßen + + Postmaster von zammad.example.com + BODY + body.gsub(/\n/, "\r\n") + end + end + end + end + + describe '#compose_postmaster_reply' do + let(:raw_incoming_mail) { File.read(Rails.root.join('test', 'data', 'mail', 'mail010.box')) } + + shared_examples 'postmaster reply' do + it 'composes postmaster reply' do + reply = Channel::EmailParser.new.send(:compose_postmaster_reply, raw_incoming_mail, locale) + expect(reply[:to]).to eq('smith@example.com') + expect(reply[:content_type]).to eq('text/plain') + expect(reply[:subject]).to eq(expected_subject) + expect(reply[:body]).to eq(expected_body) + end + end + + context 'for English locale (en)' do + include_examples 'postmaster reply' do + let(:locale) { 'en' } + let(:expected_subject) { '[ALERT] Message too large' } + let(:expected_body) do + body = <<~BODY + Dear Smith Sepp, + + Unfortunately your email titled \"Gruß aus Oberalteich\" could not be delivered to one or more recipients. + + Your message was 0.01 MB but we only accept messages up to 10 MB. + + Please reduce the message size and try again. Thank you for your understanding. + + Regretfully, + + Postmaster of zammad.example.com + BODY + body.gsub(/\n/, "\r\n") + end + end + end + + context 'for German locale (de)' do + include_examples 'postmaster reply' do + let(:locale) { 'de' } + let(:expected_subject) { '[Unzustellbar] Nachricht zu groß' } + let(:expected_body) do + body = <<~BODY + Hallo Smith Sepp, + + Ihre E-Mail mit dem Betreff \"Gruß aus Oberalteich\" konnte nicht an einen oder mehrere Empfänger zugestellt werden. + + 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. + + Bitte reduzieren Sie die Größe Ihrer Nachricht und versuchen Sie es erneut. Vielen Dank für Ihr Verständnis. + + Mit freundlichen Grüßen + + Postmaster von zammad.example.com + BODY + body.gsub(/\n/, "\r\n") + end + end + end + end end diff --git a/test/integration/email_postmaster_to_sender.rb b/test/integration/email_postmaster_to_sender.rb new file mode 100644 index 000000000..9b68c3f72 --- /dev/null +++ b/test/integration/email_postmaster_to_sender.rb @@ -0,0 +1,213 @@ +require 'test_helper' +require 'net/imap' + +class EmailPostmasterToSender < ActiveSupport::TestCase + + setup do + Setting.set('postmaster_max_size', 0.1) + + @test_id = rand(999_999_999) + + # setup the IMAP account info for Zammad + if ENV['MAIL_SERVER'].blank? + raise "Need MAIL_SERVER as ENV variable like export MAIL_SERVER='mx.example.com'" + end + if ENV['MAIL_SERVER_ACCOUNT'].blank? + raise "Need MAIL_SERVER_ACCOUNT as ENV variable like export MAIL_SERVER_ACCOUNT='user:somepass'" + end + + @server_address = ENV['MAIL_SERVER'] + @server_login = ENV['MAIL_SERVER_ACCOUNT'].split(':')[0] + @server_password = ENV['MAIL_SERVER_ACCOUNT'].split(':')[1] + + @folder = "postmaster_to_sender_#{@test_id}" + + if ENV['MAIL_SERVER_EMAIL'].blank? + raise "Need MAIL_SERVER_EMAIL as ENV variable like export MAIL_SERVER_EMAIL='master@example.com'" + end + + @sender_email_address = ENV['MAIL_SERVER_EMAIL'] + + @email_address = EmailAddress.create!( + realname: 'me Helpdesk', + email: "me#{@test_id}@example.com", + updated_by_id: 1, + created_by_id: 1, + ) + + group = Group.create_or_update( + name: 'PostmasterToSenderTest', + email_address_id: @email_address.id, + updated_by_id: 1, + created_by_id: 1, + ) + + @channel = Channel.create!( + area: 'Email::Account', + group_id: group.id, + options: { + inbound: { + adapter: 'imap', + options: { + host: @server_address, + user: @server_login, + password: @server_password, + ssl: true, + folder: @folder, + keep_on_server: false, + } + }, + outbound: { + adapter: 'smtp', + options: { + host: @server_address, + port: 25, + start_tls: true, + user: @server_login, + password: @server_password, + email: @email_address.email + }, + }, + }, + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + @email_address.channel_id = @channel.id + @email_address.save! + end + + test 'postmaster reply with email on oversized incoming emails' do + imap = Net::IMAP.new(@server_address, 993, true, nil, false) + imap.login(@server_login, @server_password) + imap.create(@folder) + imap.select(@folder) + + # put a very large message in it + large_message = "Subject: Oversized Email Message +From: Max Mustermann <#{@sender_email_address}> +To: shugo@example.com +Message-ID: <#{@test_id}@zammad.test.com> + +Oversized Email Message Body #{'#' * 120_000} +".gsub(/\n/, "\r\n") + + large_message_md5 = Digest::MD5.hexdigest(large_message) + large_message_size = format('%.2f', large_message.size.to_f / 1024 / 1024) + + imap.append(@folder, large_message, [], Time.zone.now) + + @channel.fetch(true) + + # 1. verify that the oversized email has been saved locally to: + # /tmp/oversized_mail/yyyy-mm-ddThh:mm:ss-:md5.eml + path = Rails.root.join('tmp', 'oversized_mail') + target_files = Dir.entries(path).select do |filename| + filename =~ /^.+_#{large_message_md5}\.eml$/ + end + assert(target_files.present?, 'Large message .eml log file must be present.') + + # pick the latest file that matches the criteria + target_file = target_files.max + + # verify that the file is byte for byte identical to the sent message + file_path = Rails.root.join('tmp', 'oversized_mail', target_file) + eml_data = File.read(file_path) + assert_equal(large_message, eml_data) + + # 2. verify that a postmaster response email has been sent to the sender + imap.select('inbox') + message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII') + assert(message_ids.count.positive?, 'Must have received a reply from the postmaster') + imap_message_id = message_ids.last + msg = imap.fetch(imap_message_id, 'RFC822')[0].attr['RFC822'] + assert(msg.present?, 'Must have received a reply from the postmaster') + imap.store(imap_message_id, '+FLAGS', [:Deleted]) + imap.expunge() + + # parse the reply mail and verify the various headers + parser = Channel::EmailParser.new + mail = parser.parse(msg) + assert_equal(mail[:from_email], @email_address.email) + assert_equal(mail[:subject], '[ALERT] Message too large') + assert_equal("<#{@test_id}@zammad.test.com>", + mail['references'], + 'Reply\'s Referecnes header must match the send message ID') + assert_equal("<#{@test_id}@zammad.test.com>", + mail['in-reply-to'], + 'Reply\'s In-Reply-To header must match the send message ID') + + # verify the reply mail body content + body = mail[:body] + assert(body.start_with?('Dear Max Mustermann'), 'Body must contain sender name') + assert(body.include?('Oversized Email Message'), 'Body must contain original subject') + assert(body.include?('0.1 MB'), 'Body must contain max allowed message size') + assert(body.include?("#{large_message_size} MB"), 'Body must contain the original message size') + assert(body.include?(Setting.get('fqdn')), 'Body must contain the Zammad instance name') + + # 3. check if original mail got removed + imap.select(@folder) + message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII') + assert_equal(message_ids.count, 0, 'Original customer mail must be deleted.') + + # final clean up + imap.delete(@folder) + @channel.destroy! + end + + test 'postmaster reply with no email on oversized incoming emails' do + Setting.set('postmaster_send_reject_if_mail_too_large', false) + imap = Net::IMAP.new(@server_address, 993, true, nil, false) + imap.login(@server_login, @server_password) + + imap.select('inbox') + message_count = imap.sort(['DATE'], ['ALL'], 'US-ASCII').count + + imap.create(@folder) + imap.select(@folder) + + # put a very large message in it + large_message = "Subject: Oversized Email Message +From: Max Mustermann <#{@sender_email_address}> +To: shugo@example.com +Message-ID: <#{@test_id}@zammad.test.com> + +Oversized Email Message Body #{'#' * 120_000} +".gsub(/\n/, "\r\n") + + imap.append(@folder, large_message, [], Time.zone.now) + + @channel.fetch(true) + + # 1. verify that the oversized email has been saved locally to: + # /tmp/oversized_mail/yyyy-mm-ddThh:mm:ss-:md5.eml + path = Rails.root.join('tmp', 'oversized_mail') + target_files = Dir.entries(path).select do |filename| + filename =~ /^.+?\.eml$/ + end + assert_not(target_files.blank?, 'Large message .eml log file must be blank.') + + # 2. verify that a postmaster response email has been sent to the sender + imap.select('inbox') + message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII') + assert_equal(message_ids.count, message_count, 'Must not have received a reply from the postmaster') + + # 3. check if original mail got removed + imap.select(@folder) + message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII') + imap_message_id = message_ids.last + msg = imap.fetch(imap_message_id, 'RFC822')[0].attr['RFC822'] + imap.store(imap_message_id, '+FLAGS', [:Deleted]) + imap.expunge() + assert(msg.present?, 'Oversized Email Message') + assert_equal(message_ids.count, 1, 'Original customer mail must be deleted.') + + # final clean up + imap.delete(@folder) + @channel.destroy! + end + + teardown do + Setting.set('postmaster_max_size', 10) + end +end