Skip to content
Permalink
Browse files

Implemented issue #2092 - Send postmaster email to sender if email is…

… too big
  • Loading branch information...
SummerStorm authored and zammad-sync committed Aug 6, 2019
1 parent bc38f1c commit fae194918eb8a07036060a1b41f1b33c45e03cad
@@ -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
@@ -206,6 +206,7 @@ def fetch(options, channel, check_type = '', verify_string = '')
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 @@ def fetch(options, channel, check_type = '', verify_string = '')
# 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 @@ def fetch(options, channel, check_type = '', verify_string = '')
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 @@ def fetch(options, channel, check_type = '', verify_string = '')
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,22 +436,21 @@ def deleted?(message_meta, count, count_all)
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
true|false
=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

@@ -136,6 +136,7 @@ def fetch(options, channel, check_type = '', verify_string = '')
count_all = mails.size
count = 0
count_fetched = 0
too_large_messages = []
active_check_interval = 20
notice = ''
mails.first(2000).each do |m|
@@ -165,25 +166,40 @@ def fetch(options, channel, check_type = '', verify_string = '')
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
disconnect
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',
@@ -116,18 +116,15 @@ def process(channel, msg, exception = true)
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 @@ def self.process_unprocessable_mails(params = {})
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 @@ def get_attachments(file, attachments, mail)

[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
@@ -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}
@@ -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}
@@ -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
@@ -2582,6 +2582,33 @@
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',
@@ -29,17 +29,26 @@ module NotificationFactory
=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

0 comments on commit fae1949

Please sign in to comment.
You can’t perform that action at this time.