2016-10-19 03:11:36 +00:00
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
2013-06-12 15:59:58 +00:00
2012-06-15 11:10:23 +00:00
# encoding: utf-8
2012-12-05 01:27:56 +00:00
class Channel :: EmailParser
2019-01-29 20:35:22 +00:00
PROZESS_TIME_MAX = 180
2018-12-19 17:31:51 +00:00
EMAIL_REGEX = / .+@.+ / . freeze
2018-06-05 11:08:52 +00:00
RECIPIENT_FIELDS = %w[ to cc delivered-to x-original-to envelope-to ] . freeze
2018-12-11 08:36:16 +00:00
SENDER_FIELDS = %w[ from reply-to return-path sender ] . freeze
2018-12-19 02:54:03 +00:00
EXCESSIVE_LINKS_MSG = 'This message cannot be displayed because it contains over 5,000 links. Download the raw message below and open it via an Email client if you still wish to view it.' . freeze
2012-10-04 06:54:21 +00:00
= begin
2016-05-03 11:03:10 +00:00
parser = Channel :: EmailParser . new
mail = parser . parse ( msg_as_string )
2012-10-04 06:54:21 +00:00
mail = {
2015-08-30 18:16:29 +00:00
from : 'Some Name <some@example.com>' ,
from_email : 'some@example.com' ,
from_local : 'some' ,
from_domain : 'example.com' ,
from_display_name : 'Some Name' ,
message_id : 'some_message_id@example.com' ,
to : 'Some System <system@example.com>' ,
cc : 'Somebody <somebody@example.com>' ,
subject : 'some message subject' ,
body : 'some message body' ,
2016-06-21 15:14:15 +00:00
content_type : 'text/html' , # text/plain
2016-06-03 13:25:35 +00:00
date : Time . zone . now ,
2015-08-30 18:16:29 +00:00
attachments : [
2012-10-04 07:09:27 +00:00
{
2015-08-30 18:16:29 +00:00
data : 'binary of attachment' ,
filename : 'file_name_of_attachment.txt' ,
preferences : {
2017-03-06 11:18:04 +00:00
'content-alternative' = > true ,
'Mime-Type' = > 'text/plain' ,
'Charset: => ' iso - 8859 - 1 ' ,
2012-10-04 07:09:27 +00:00
} ,
} ,
2012-10-04 06:54:21 +00:00
] ,
2012-10-04 07:09:27 +00:00
# ignore email header
2015-08-30 18:16:29 +00:00
x - zammad - ignore : 'false' ,
2012-10-04 07:09:27 +00:00
# customer headers
2015-08-30 18:16:29 +00:00
x - zammad - customer - login : '' ,
x - zammad - customer - email : '' ,
x - zammad - customer - firstname : '' ,
x - zammad - customer - lastname : '' ,
2012-10-04 07:09:27 +00:00
2016-04-12 11:44:28 +00:00
# ticket headers (for new tickets)
2015-08-30 18:16:29 +00:00
x - zammad - ticket - group : 'some_group' ,
x - zammad - ticket - state : 'some_state' ,
x - zammad - ticket - priority : 'some_priority' ,
x - zammad - ticket - owner : 'some_owner_login' ,
2012-10-04 07:09:27 +00:00
2016-04-12 11:44:28 +00:00
# ticket headers (for existing tickets)
x - zammad - ticket - followup - group : 'some_group' ,
x - zammad - ticket - followup - state : 'some_state' ,
x - zammad - ticket - followup - priority : 'some_priority' ,
x - zammad - ticket - followup - owner : 'some_owner_login' ,
2012-10-04 07:09:27 +00:00
# article headers
2015-08-30 18:16:29 +00:00
x - zammad - article - internal : false ,
x - zammad - article - type : 'agent' ,
x - zammad - article - sender : 'customer' ,
2012-10-04 07:09:27 +00:00
# all other email headers
2015-08-30 18:16:29 +00:00
some - header : 'some_value' ,
2012-10-04 06:54:21 +00:00
}
= end
2015-12-14 09:23:14 +00:00
def parse ( msg )
2019-03-27 12:02:56 +00:00
msg = msg . force_encoding ( 'binary' )
# mail 2.6 and earlier accepted non-conforming mails that lacked the correct CRLF seperators,
# mail 2.7 and above require CRLF so we force it on using binary_unsafe_to_crlf
msg = Mail :: Utilities . binary_unsafe_to_crlf ( msg )
mail = Mail . new ( msg )
2012-05-04 11:33:05 +00:00
2020-12-18 15:47:28 +00:00
message_ensure_message_id ( msg , mail )
2020-06-02 09:30:19 +00:00
force_parts_encoding_if_needed ( mail )
2018-08-22 10:05:14 +00:00
headers = message_header_hash ( mail )
body = message_body_hash ( mail )
2018-06-05 11:08:52 +00:00
message_attributes = [
{ mail_instance : mail } ,
2018-08-22 10:05:14 +00:00
headers ,
body ,
self . class . sender_attributes ( headers ) ,
2020-07-13 12:46:08 +00:00
{ raw : msg } ,
2018-06-05 11:08:52 +00:00
]
message_attributes . reduce ( { } . with_indifferent_access , & :merge )
2013-01-23 13:47:57 +00:00
end
2015-08-30 18:16:29 +00:00
= begin
parser = Channel :: EmailParser . new
2017-04-26 14:50:57 +00:00
ticket , article , user , mail = parser . process ( channel , email_raw_string )
2015-08-30 18:16:29 +00:00
2016-11-07 22:20:58 +00:00
returns
2015-08-30 18:16:29 +00:00
2017-04-26 14:50:57 +00:00
[ ticket , article , user , mail ]
2015-08-30 18:16:29 +00:00
2016-11-07 22:20:58 +00:00
do not raise an exception - e . g . if used by scheduler
parser = Channel :: EmailParser . new
2017-04-26 14:50:57 +00:00
ticket , article , user , mail = parser . process ( channel , email_raw_string , false )
2016-11-07 22:20:58 +00:00
returns
2017-04-26 14:50:57 +00:00
[ ticket , article , user , mail ] || false
2016-11-07 22:20:58 +00:00
2015-08-30 18:16:29 +00:00
= end
2016-11-07 22:20:58 +00:00
def process ( channel , msg , exception = true )
2016-05-30 08:33:27 +00:00
2019-01-29 20:35:22 +00:00
Timeout . timeout ( PROZESS_TIME_MAX ) do
_process ( channel , msg )
end
2016-05-30 08:33:27 +00:00
rescue = > e
# store unprocessable email for bug reporting
2019-08-06 15:26:29 +00:00
filename = archive_mail ( 'unprocessable_mail' , msg )
2020-03-12 08:23:19 +00:00
message = " Can't process email, you will find it for bug reporting under #{ filename } , please create an issue at https://github.com/zammad/zammad/issues "
2019-08-06 15:26:29 +00:00
2020-09-30 09:07:01 +00:00
p " ERROR: #{ message } " # rubocop:disable Rails/Output
p " ERROR: #{ e . inspect } " # rubocop:disable Rails/Output
2016-11-03 22:18:10 +00:00
Rails . logger . error message
2017-04-19 10:09:54 +00:00
Rails . logger . error e
2019-08-06 15:26:29 +00:00
2016-11-07 22:20:58 +00:00
return false if exception == false
2018-10-09 06:17:41 +00:00
2020-09-30 09:07:01 +00:00
raise %( #{ e . inspect } \n #{ e . backtrace . join ( " \n " ) } )
2016-05-30 08:33:27 +00:00
end
def _process ( channel , msg )
2016-08-20 19:29:22 +00:00
# parse email
2015-12-14 09:23:14 +00:00
mail = parse ( msg )
2012-04-13 16:42:25 +00:00
2018-04-06 08:58:00 +00:00
Rails . logger . info " Process email with msgid ' #{ mail [ :message_id ] } ' "
2012-10-05 06:42:12 +00:00
# run postmaster pre filter
2016-04-12 15:21:48 +00:00
UserInfo . current_user_id = 1
2020-10-29 14:58:36 +00:00
# set interface handle
original_interface_handle = ApplicationHandleInfo . current
transaction_params = { interface_handle : " #{ original_interface_handle } .postmaster " , disable : [ ] }
2016-04-12 07:25:20 +00:00
filters = { }
2017-10-01 12:25:52 +00:00
Setting . where ( area : 'Postmaster::PreFilter' ) . order ( :name ) . each do | setting |
2018-06-19 03:53:00 +00:00
filters [ setting . name ] = Setting . get ( setting . name ) . constantize
2017-10-01 12:25:52 +00:00
end
2018-02-15 11:25:47 +00:00
filters . each do | key , backend |
2018-03-20 17:47:49 +00:00
Rails . logger . debug { " run postmaster pre filter #{ key } : #{ backend } " }
2012-10-05 06:42:12 +00:00
begin
2020-10-29 14:58:36 +00:00
backend . run ( channel , mail , transaction_params )
2015-05-08 14:09:24 +00:00
rescue = > e
2018-02-15 11:25:47 +00:00
Rails . logger . error " can't run postmaster pre filter #{ key } : #{ backend } "
2015-05-04 18:58:28 +00:00
Rails . logger . error e . inspect
2015-08-31 09:27:12 +00:00
raise e
2012-10-05 06:42:12 +00:00
end
2017-10-01 12:25:52 +00:00
end
2012-10-04 07:09:27 +00:00
2012-05-06 20:48:23 +00:00
# check ignore header
2020-08-21 08:18:31 +00:00
if mail [ :'x-zammad-ignore' ] == 'true' || mail [ :'x-zammad-ignore' ] == true
2016-08-20 19:29:22 +00:00
Rails . logger . info " ignored email with msgid ' #{ mail [ :message_id ] } ' from ' #{ mail [ :from ] } ' because of x-zammad-ignore header "
2017-09-23 06:25:55 +00:00
return
2016-08-20 19:29:22 +00:00
end
2016-08-23 10:54:29 +00:00
ticket = nil
article = nil
session_user = nil
2012-05-06 20:48:23 +00:00
2012-04-13 16:42:25 +00:00
# use transaction
2020-10-29 14:58:36 +00:00
Transaction . execute ( transaction_params ) do
2012-04-13 16:42:25 +00:00
2016-08-23 09:49:12 +00:00
# get sender user
2020-08-21 08:18:31 +00:00
session_user_id = mail [ :'x-zammad-session-user-id' ]
2016-08-23 09:49:12 +00:00
if ! session_user_id
raise 'No x-zammad-session-user-id, no sender set!'
2012-05-06 20:48:23 +00:00
end
2018-10-09 06:17:41 +00:00
2016-08-23 09:49:12 +00:00
session_user = User . lookup ( id : session_user_id )
if ! session_user
raise " No user found for x-zammad-session-user-id: #{ session_user_id } ! "
2012-04-13 16:42:25 +00:00
end
2012-10-04 07:09:27 +00:00
2012-04-13 16:42:25 +00:00
# set current user
2016-08-23 09:49:12 +00:00
UserInfo . current_user_id = session_user . id
2012-10-04 07:09:27 +00:00
2015-08-30 18:16:29 +00:00
# get ticket# based on email headers
2020-08-21 08:18:31 +00:00
if mail [ :'x-zammad-ticket-id' ]
ticket = Ticket . find_by ( id : mail [ :'x-zammad-ticket-id' ] )
2015-08-30 18:16:29 +00:00
end
2020-08-21 08:18:31 +00:00
if mail [ :'x-zammad-ticket-number' ]
ticket = Ticket . find_by ( number : mail [ :'x-zammad-ticket-number' ] )
2015-08-30 18:16:29 +00:00
end
2012-05-04 11:33:05 +00:00
2012-04-13 16:42:25 +00:00
# set ticket state to open if not new
if ticket
2016-04-12 11:44:28 +00:00
set_attributes_by_x_headers ( ticket , 'ticket' , mail , 'followup' )
2016-04-12 15:08:26 +00:00
# save changes set by x-zammad-ticket-followup-* headers
2017-09-23 06:25:55 +00:00
ticket . save! if ticket . has_changes_to_save?
2016-04-12 15:08:26 +00:00
2017-02-12 17:21:03 +00:00
# set ticket to open again or keep create state
2020-08-21 08:18:31 +00:00
if ! mail [ :'x-zammad-ticket-followup-state' ] && ! mail [ :'x-zammad-ticket-followup-state_id' ]
2017-02-12 17:21:03 +00:00
new_state = Ticket :: State . find_by ( default_create : true )
2020-08-21 08:18:31 +00:00
if ticket . state_id != new_state . id && ! mail [ :'x-zammad-out-of-office' ]
2017-02-12 17:21:03 +00:00
ticket . state = Ticket :: State . find_by ( default_follow_up : true )
2016-08-23 09:49:12 +00:00
ticket . save!
2016-04-12 11:44:28 +00:00
end
2012-04-13 16:42:25 +00:00
end
end
# create new ticket
2012-05-06 20:48:23 +00:00
if ! ticket
2015-12-14 09:23:14 +00:00
preferences = { }
if channel [ :id ]
preferences = {
channel_id : channel [ :id ]
}
end
2016-05-23 17:23:06 +00:00
# get default group where ticket is created
group = nil
if channel [ :group_id ]
group = Group . lookup ( id : channel [ :group_id ] )
2020-10-27 09:30:05 +00:00
else
mail_to_group = self . class . mail_to_group ( mail [ :to ] )
if mail_to_group . present?
group = mail_to_group
end
2016-05-23 17:23:06 +00:00
end
2017-11-21 14:25:04 +00:00
if group . blank? || group . active == false
2019-04-07 15:23:03 +00:00
group = Group . where ( active : true ) . order ( id : :asc ) . first
2016-05-23 17:23:06 +00:00
end
2017-11-21 14:25:04 +00:00
if group . blank?
2016-05-23 17:23:06 +00:00
group = Group . first
end
2017-02-13 14:05:28 +00:00
title = mail [ :subject ]
if title . blank?
title = '-'
end
2014-06-08 03:11:21 +00:00
ticket = Ticket . new (
2018-12-19 17:31:51 +00:00
group_id : group . id ,
title : title ,
2015-12-14 09:23:14 +00:00
preferences : preferences ,
2014-06-08 03:11:21 +00:00
)
2015-12-14 09:23:14 +00:00
set_attributes_by_x_headers ( ticket , 'ticket' , mail )
2012-05-06 20:48:23 +00:00
# create ticket
2016-08-20 19:29:22 +00:00
ticket . save!
2018-06-20 12:13:35 +00:00
end
# apply tags to ticket
2020-08-21 08:18:31 +00:00
if mail [ :'x-zammad-ticket-tags' ] . present?
mail [ :'x-zammad-ticket-tags' ] . each do | tag |
2018-06-20 12:13:35 +00:00
ticket . tag_add ( tag )
end
2012-04-13 16:42:25 +00:00
end
2012-10-04 06:54:21 +00:00
2012-05-06 20:48:23 +00:00
# set attributes
2016-09-08 19:18:26 +00:00
ticket . with_lock do
article = Ticket :: Article . new (
2018-12-19 17:31:51 +00:00
ticket_id : ticket . id ,
type_id : Ticket :: Article :: Type . find_by ( name : 'email' ) . id ,
sender_id : Ticket :: Article :: Sender . find_by ( name : 'Customer' ) . id ,
2016-09-08 19:18:26 +00:00
content_type : mail [ :content_type ] ,
2018-12-19 17:31:51 +00:00
body : mail [ :body ] ,
from : mail [ :from ] ,
reply_to : mail [ :" reply-to " ] ,
to : mail [ :to ] ,
cc : mail [ :cc ] ,
subject : mail [ :subject ] ,
message_id : mail [ :message_id ] ,
internal : false ,
2016-09-08 19:18:26 +00:00
)
# x-headers lookup
set_attributes_by_x_headers ( article , 'article' , mail )
# create article
article . save!
# store mail plain
2016-12-20 23:07:47 +00:00
article . save_as_raw ( msg )
2016-09-08 19:18:26 +00:00
# store attachments
2017-11-23 08:09:44 +00:00
mail [ :attachments ] & . each do | attachment |
2018-01-02 13:28:34 +00:00
filename = attachment [ :filename ] . force_encoding ( 'utf-8' )
if ! filename . force_encoding ( 'UTF-8' ) . valid_encoding?
2018-06-01 11:32:59 +00:00
filename = filename . utf8_encode ( fallback : :read_as_sanitized_binary )
2018-01-02 13:28:34 +00:00
end
2017-11-23 08:09:44 +00:00
Store . add (
2018-12-19 17:31:51 +00:00
object : 'Ticket::Article' ,
o_id : article . id ,
data : attachment [ :data ] ,
filename : filename ,
2017-11-23 08:09:44 +00:00
preferences : attachment [ :preferences ]
)
2012-04-13 16:42:25 +00:00
end
end
end
2018-05-29 15:42:14 +00:00
ticket . reload
article . reload
session_user . reload
2012-10-05 06:42:12 +00:00
# run postmaster post filter
2016-04-21 18:49:30 +00:00
filters = { }
2017-10-01 12:25:52 +00:00
Setting . where ( area : 'Postmaster::PostFilter' ) . order ( :name ) . each do | setting |
2019-01-06 18:41:29 +00:00
filters [ setting . name ] = Setting . get ( setting . name ) . constantize
2017-10-01 12:25:52 +00:00
end
2017-11-23 08:09:44 +00:00
filters . each_value do | backend |
2018-03-20 17:47:49 +00:00
Rails . logger . debug { " run postmaster post filter #{ backend } " }
2012-10-05 06:42:12 +00:00
begin
2016-08-23 10:54:29 +00:00
backend . run ( channel , mail , ticket , article , session_user )
2015-05-08 14:09:24 +00:00
rescue = > e
2015-05-04 18:58:28 +00:00
Rails . logger . error " can't run postmaster post filter #{ backend } "
Rails . logger . error e . inspect
2012-10-05 06:42:12 +00:00
end
2017-10-01 12:25:52 +00:00
end
2012-10-05 06:42:12 +00:00
2012-05-06 20:48:23 +00:00
# return new objects
2016-08-23 10:54:29 +00:00
[ ticket , article , session_user , mail ]
2012-04-13 16:42:25 +00:00
end
2012-11-07 16:38:09 +00:00
2020-10-27 09:30:05 +00:00
def self . mail_to_group ( to )
begin
to = Mail :: AddressList . new ( to ) & . addresses & . first & . address
rescue
Rails . logger . error 'Can not parse :to field for group destination!'
end
return if to . blank?
2020-11-23 15:16:11 +00:00
email = EmailAddress . find_by ( email : to . downcase )
2020-10-27 09:30:05 +00:00
return if email & . channel . blank?
email . channel & . group
end
2016-12-20 23:07:47 +00:00
def self . check_attributes_by_x_headers ( header_name , value )
class_name = nil
attribute = nil
2018-06-20 12:13:35 +00:00
# skip check attributes if it is tags
return true if header_name == 'x-zammad-ticket-tags'
2018-10-09 06:17:41 +00:00
2016-12-20 23:07:47 +00:00
if header_name =~ / ^x-zammad-(.+?)-(followup-|)(.*)$ /i
class_name = $1
attribute = $3
end
return true if ! class_name
2018-10-09 06:17:41 +00:00
2019-09-05 13:51:13 +00:00
if class_name . casecmp ( 'article' ) . zero?
2016-12-20 23:07:47 +00:00
class_name = 'Ticket::Article'
end
return true if ! attribute
2018-10-09 06:17:41 +00:00
2016-12-20 23:07:47 +00:00
key_short = attribute [ attribute . length - 3 , attribute . length ]
return true if key_short != '_id'
2019-01-06 18:41:29 +00:00
class_object = class_name . to_classname . constantize
2016-12-20 23:07:47 +00:00
return if ! class_object
2018-10-09 06:17:41 +00:00
2016-12-20 23:07:47 +00:00
class_instance = class_object . new
2017-01-31 17:13:45 +00:00
return false if ! class_instance . association_id_validation ( attribute , value )
2018-10-09 06:17:41 +00:00
2016-12-20 23:07:47 +00:00
true
end
2018-06-05 11:08:52 +00:00
def self . sender_attributes ( from )
2018-08-22 10:05:14 +00:00
if from . is_a? ( HashWithIndifferentAccess )
from = SENDER_FIELDS . map { | f | from [ f ] } . compact
2018-06-05 11:08:52 +00:00
. map ( & :to_utf8 ) . reject ( & :blank? )
. partition { | address | address . match? ( EMAIL_REGEX ) }
. flatten . first
2017-06-19 20:24:22 +00:00
end
2018-06-05 11:08:52 +00:00
data = { } . with_indifferent_access
return data if from . blank?
from = from . gsub ( '<>' , '' ) . strip
mail_address = begin
2020-09-30 09:07:01 +00:00
Mail :: AddressList . new ( from ) . addresses
. select { | a | a . address . present? }
. partition { | a | a . address . match? ( EMAIL_REGEX ) }
. flatten . first
rescue Mail :: Field :: ParseError = > e
$stdout . puts e
end
2018-06-05 11:08:52 +00:00
if mail_address & . address . present?
data [ :from_email ] = mail_address . address
data [ :from_local ] = mail_address . local
data [ :from_domain ] = mail_address . domain
data [ :from_display_name ] = mail_address . display_name || mail_address . comments & . first
2018-07-16 15:07:23 +00:00
elsif from =~ / ^(.+?)<((.+?)@(.+?))> /
2018-06-05 11:08:52 +00:00
data [ :from_email ] = $2
data [ :from_local ] = $3
data [ :from_domain ] = $4
data [ :from_display_name ] = $1
else
data [ :from_email ] = from
data [ :from_local ] = from
data [ :from_domain ] = from
data [ :from_display_name ] = from
2017-05-26 13:34:32 +00:00
end
# do extra decoding because we needed to use field.value
2018-06-05 11:08:52 +00:00
data [ :from_display_name ] =
Mail :: Field . new ( 'X-From' , data [ :from_display_name ] . to_utf8 )
. to_s
. delete ( '"' )
. strip
. gsub ( / (^'|'$) / , '' )
2017-05-26 13:34:32 +00:00
data
end
2016-04-12 11:44:28 +00:00
def set_attributes_by_x_headers ( item_object , header_name , mail , suffix = false )
2014-06-08 03:11:21 +00:00
2017-10-02 10:31:59 +00:00
# loop all x-zammad-header-* headers
2017-11-23 08:09:44 +00:00
item_object . attributes . each_key do | key |
2014-06-08 03:11:21 +00:00
# ignore read only attributes
next if key == 'updated_by_id'
next if key == 'created_by_id'
# check if id exists
2015-04-27 14:42:53 +00:00
key_short = key [ key . length - 3 , key . length ]
2014-06-08 03:11:21 +00:00
if key_short == '_id'
2015-04-27 14:41:03 +00:00
key_short = key [ 0 , key . length - 3 ]
2014-06-08 03:11:21 +00:00
header = " x-zammad- #{ header_name } - #{ key_short } "
2016-04-12 11:44:28 +00:00
if suffix
header = " x-zammad- #{ header_name } - #{ suffix } - #{ key_short } "
end
2016-12-20 23:07:47 +00:00
# only set value on _id if value/reference lookup exists
2019-08-26 20:14:33 +00:00
if mail [ header . to_sym ]
2017-10-02 13:07:22 +00:00
2017-10-02 10:31:59 +00:00
Rails . logger . info " set_attributes_by_x_headers header #{ header } found #{ mail [ header . to_sym ] } "
2017-10-01 12:25:52 +00:00
item_object . class . reflect_on_all_associations . map do | assoc |
2015-05-07 09:04:40 +00:00
next if assoc . name . to_s != key_short
2017-10-02 10:31:59 +00:00
Rails . logger . info " set_attributes_by_x_headers found #{ assoc . class_name } lookup for ' #{ mail [ header . to_sym ] } ' "
2015-05-07 09:04:40 +00:00
item = assoc . class_name . constantize
2016-12-20 23:07:47 +00:00
assoc_object = nil
2019-08-26 20:14:33 +00:00
if item . new . respond_to? ( :name )
2017-10-02 10:31:59 +00:00
assoc_object = item . lookup ( name : mail [ header . to_sym ] )
end
2019-08-26 20:14:33 +00:00
if ! assoc_object && item . new . respond_to? ( :login )
2017-10-02 10:31:59 +00:00
assoc_object = item . lookup ( login : mail [ header . to_sym ] )
2014-06-08 03:11:21 +00:00
end
2019-08-26 20:14:33 +00:00
if ! assoc_object && item . new . respond_to? ( :email )
assoc_object = item . lookup ( email : mail [ header . to_sym ] )
end
2016-12-20 23:07:47 +00:00
2017-10-02 10:31:59 +00:00
if assoc_object . blank?
2016-12-20 23:07:47 +00:00
2017-10-02 10:31:59 +00:00
# no assoc exists, remove header
mail . delete ( header . to_sym )
2016-12-20 23:07:47 +00:00
next
end
2017-10-02 10:31:59 +00:00
Rails . logger . info " set_attributes_by_x_headers assign #{ item_object . class } #{ key } = #{ assoc_object . id } "
item_object [ key ] = assoc_object . id
2017-10-01 12:25:52 +00:00
end
2012-10-05 06:42:12 +00:00
end
end
2014-06-08 03:11:21 +00:00
# check if attribute exists
header = " x-zammad- #{ header_name } - #{ key } "
2016-05-13 09:32:35 +00:00
if suffix
header = " x-zammad- #{ header_name } - #{ suffix } - #{ key } "
end
2017-01-30 14:47:12 +00:00
if mail [ header . to_sym ]
2017-10-02 10:31:59 +00:00
Rails . logger . info " set_attributes_by_x_headers header #{ header } found. Assign #{ key } = #{ mail [ header . to_sym ] } "
2017-01-30 14:47:12 +00:00
item_object [ key ] = mail [ header . to_sym ]
2014-06-08 03:11:21 +00:00
end
2017-10-01 12:25:52 +00:00
end
2012-10-05 06:42:12 +00:00
end
2016-08-20 19:29:22 +00:00
2018-01-30 14:05:52 +00:00
= begin
process unprocessable_mails ( tmp / unprocessable_mail / * . eml ) again
Channel :: EmailParser . process_unprocessable_mails
= end
def self . process_unprocessable_mails ( params = { } )
2020-02-18 19:51:31 +00:00
path = Rails . root . join ( 'tmp/unprocessable_mail' )
2018-01-30 14:05:52 +00:00
files = [ ]
Dir . glob ( " #{ path } /*.eml " ) do | entry |
2019-06-28 11:38:49 +00:00
ticket , _article , _user , _mail = Channel :: EmailParser . new . process ( params , IO . binread ( entry ) )
2018-01-30 14:05:52 +00:00
next if ticket . blank?
2018-10-09 06:17:41 +00:00
2018-01-30 14:05:52 +00:00
files . push entry
File . delete ( entry )
end
files
end
2019-08-06 15:26:29 +00:00
= begin
process oversized emails by :
2019-10-08 07:19:04 +00:00
1 . Archiving the oversized mail as tmp / oversized_mail / md5 . eml
2019-08-06 15:26:29 +00:00
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
2018-06-05 11:08:52 +00:00
private
2020-06-02 09:30:19 +00:00
# https://github.com/zammad/zammad/issues/2922
def force_parts_encoding_if_needed ( mail )
2020-12-30 13:55:08 +00:00
# enforce encoding on both multipart parts and main body
( [ mail ] + mail . parts ) . each { | elem | force_single_part_encoding_if_needed ( elem ) }
2020-06-02 09:30:19 +00:00
end
# https://github.com/zammad/zammad/issues/2922
def force_single_part_encoding_if_needed ( part )
2020-12-30 13:55:08 +00:00
return if part . charset & . downcase != 'iso-2022-jp'
2020-06-02 09:30:19 +00:00
2020-12-30 13:55:08 +00:00
part . body = force_japanese_encoding part . body . encoded . unpack1 ( 'M' )
2020-10-21 16:28:06 +00:00
end
ISO2022JP_REGEXP = / = \ ?ISO-2022-JP \ ?B \ ?(.+?) \ ?= / . freeze
# https://github.com/zammad/zammad/issues/3115
def header_field_unpack_japanese ( field )
field . value . gsub ISO2022JP_REGEXP do
2020-12-30 13:55:08 +00:00
force_japanese_encoding Base64 . decode64 ( $1 )
2020-10-21 16:28:06 +00:00
end
2020-06-02 09:30:19 +00:00
end
2020-12-18 15:47:28 +00:00
# generate Message ID on the fly if it was missing
# yes, Mail gem generates one in some cases
# but it is 100% random so duplicate messages would not be detected
def message_ensure_message_id ( raw , parsed )
field = parsed . header . fields . find { | elem | elem . name == 'Message-ID' }
return true if field & . unparsed_value . present?
parsed . message_id = generate_message_id ( raw , parsed . from )
end
2018-06-05 11:08:52 +00:00
def message_header_hash ( mail )
imported_fields = mail . header . fields . map do | f |
2018-08-22 10:05:14 +00:00
begin
2020-10-21 16:28:06 +00:00
value = if f . value . match? ( ISO2022JP_REGEXP )
header_field_unpack_japanese ( f )
else
f . to_utf8
end
2018-08-22 10:05:14 +00:00
if value . blank?
2019-03-27 12:02:56 +00:00
value = f . decoded . to_utf8
2018-08-22 10:05:14 +00:00
end
2019-03-27 12:02:56 +00:00
# fields that cannot be cleanly parsed fallback to the empty string
rescue Mail :: Field :: IncompleteParseError
value = ''
2018-08-22 10:05:14 +00:00
rescue
2019-03-27 12:02:56 +00:00
value = f . decoded . to_utf8 ( fallback : :read_as_sanitized_binary )
2018-08-22 10:05:14 +00:00
end
2018-06-05 11:08:52 +00:00
[ f . name . downcase , value ]
end . to_h
# imported_fields = mail.header.fields.map { |f| [f.name.downcase, f.to_utf8] }.to_h
2020-04-14 08:14:14 +00:00
raw_fields = mail . header . fields . index_by { | f | " raw- #{ f . name . downcase } " }
2018-06-05 11:08:52 +00:00
custom_fields = { } . tap do | h |
2018-08-10 07:19:37 +00:00
h . replace ( imported_fields . slice ( * RECIPIENT_FIELDS )
. transform_values { | v | v . match? ( EMAIL_REGEX ) ? v : '' } )
2018-06-05 11:08:52 +00:00
2018-08-10 07:19:37 +00:00
h [ 'x-any-recipient' ] = h . values . select ( & :present? ) . join ( ', ' )
2018-06-05 11:08:52 +00:00
h [ 'message_id' ] = imported_fields [ 'message-id' ]
2019-01-30 15:58:48 +00:00
h [ 'subject' ] = imported_fields [ 'subject' ]
2018-09-20 16:54:54 +00:00
begin
h [ 'date' ] = Time . zone . parse ( mail . date . to_s ) || imported_fields [ 'date' ]
rescue
h [ 'date' ] = nil
end
2018-06-05 11:08:52 +00:00
end
[ imported_fields , raw_fields , custom_fields ] . reduce ( { } . with_indifferent_access , & :merge )
end
def message_body_hash ( mail )
message = [ mail . html_part , mail . text_part , mail ] . find { | m | m & . body . present? }
2018-07-04 11:37:56 +00:00
if message . present? && ( message . mime_type . nil? || message . mime_type . match? ( %r{ ^text/(plain|html)$ } ) )
2018-06-05 11:08:52 +00:00
content_type = message . mime_type || 'text/plain'
body = body_text ( message , strict_html : content_type . eql? ( 'text/html' ) )
end
content_type = 'text/plain' if body . blank?
{
attachments : collect_attachments ( mail ) ,
content_type : content_type || 'text/plain' ,
body : body . presence || 'no visible content'
} . with_indifferent_access
end
def body_text ( message , ** options )
body_text = begin
2020-09-30 09:07:01 +00:00
message . body . to_s
rescue Mail :: UnknownEncodingType # see test/data/mail/mail043.box / issue #348
message . body . raw_source
end
2018-06-05 11:08:52 +00:00
body_text = body_text . utf8_encode ( from : message . charset , fallback : :read_as_sanitized_binary )
body_text = Mail :: Utilities . to_lf ( body_text )
2018-12-19 02:54:03 +00:00
# plaintext body requires no processing
return body_text if ! options [ :strict_html ]
2018-10-09 06:17:41 +00:00
2018-12-19 02:54:03 +00:00
# Issue #2390 - emails with >5k HTML links should be rejected
return EXCESSIVE_LINKS_MSG if body_text . scan ( / <a[[:space:]] /i ) . count > = 5_000
body_text . html2html_strict
2018-06-05 11:08:52 +00:00
end
def collect_attachments ( mail )
attachments = [ ]
2020-01-28 09:42:40 +00:00
attachments . push ( * get_nonplaintext_body_as_attachment ( mail ) )
2018-06-05 11:08:52 +00:00
mail . parts . each do | part |
2020-01-28 09:42:40 +00:00
attachments . push ( * gracefully_get_attachments ( part , attachments , mail ) )
end
2018-10-09 06:17:41 +00:00
2020-01-28 09:42:40 +00:00
attachments
end
def get_nonplaintext_body_as_attachment ( mail )
if ! ( mail . html_part & . body . present? || ( ! mail . multipart? && mail . mime_type . present? && mail . mime_type != 'text/plain' ) )
return
end
2019-06-27 18:26:28 +00:00
2020-01-28 09:42:40 +00:00
message = mail . html_part || mail
2019-06-27 18:26:28 +00:00
2020-01-28 09:42:40 +00:00
if ! mail . mime_type . starts_with? ( 'text/' ) && mail . html_part . blank?
return gracefully_get_attachments ( message , [ ] , mail )
2018-06-05 11:08:52 +00:00
end
2020-01-28 09:42:40 +00:00
filename = message . filename . presence || ( message . mime_type . eql? ( 'text/html' ) ? 'message.html' : '-no name-' )
headers_store = {
'content-alternative' = > true ,
'original-format' = > message . mime_type . eql? ( 'text/html' ) ,
'Mime-Type' = > message . mime_type ,
'Charset' = > message . charset ,
} . reject { | _ , v | v . blank? }
[ {
data : body_text ( message ) ,
filename : filename ,
preferences : headers_store
} ]
end
def gracefully_get_attachments ( part , attachments , mail )
get_attachments ( part , attachments , mail ) . flatten . compact
rescue = > e # Protect process to work with spam emails (see test/fixtures/mail15.box)
raise e if ( fail_count || = 0 ) . positive?
( fail_count += 1 ) && retry
2018-06-05 11:08:52 +00:00
end
def get_attachments ( file , attachments , mail )
return file . parts . map { | p | get_attachments ( p , attachments , mail ) } if file . parts . any?
2018-08-22 10:05:14 +00:00
return [ ] if [ mail . text_part & . body & . encoded , mail . html_part & . body & . encoded ] . include? ( file . body . encoded )
2018-06-05 11:08:52 +00:00
# get file preferences
headers_store = { }
file . header . fields . each do | field |
# full line, encode, ready for storage
2019-06-27 18:26:28 +00:00
value = field . to_utf8
if value . blank?
value = field . raw_value
2018-06-05 11:08:52 +00:00
end
2019-06-27 18:26:28 +00:00
headers_store [ field . name . to_s ] = value
2019-06-28 11:38:49 +00:00
rescue
2019-06-27 18:26:28 +00:00
headers_store [ field . name . to_s ] = field . raw_value
2018-06-05 11:08:52 +00:00
end
# cleanup content id, <> will be added automatically later
if headers_store [ 'Content-ID' ]
2020-05-25 07:05:17 +00:00
headers_store [ 'Content-ID' ] . delete_prefix! ( '<' )
headers_store [ 'Content-ID' ] . delete_suffix! ( '>' )
2018-06-05 11:08:52 +00:00
end
# get filename from content-disposition
# workaround for: NoMethodError: undefined method `filename' for #<Mail::UnstructuredField:0x007ff109e80678>
2018-09-07 12:20:11 +00:00
begin
filename = file . header [ :content_disposition ] . try ( :filename )
rescue
begin
2020-10-22 13:57:01 +00:00
case file . header [ :content_disposition ] . to_s
2020-11-12 11:42:44 +00:00
when / (filename|name)( \ *{0,1})="(.+?)" /i , / (filename|name)( \ *{0,1})='(.+?)' /i , / (filename|name)( \ *{0,1})=(.+?); /i
2018-09-07 12:20:11 +00:00
filename = $3
end
rescue
Rails . logger . debug { 'Unable to get filename' }
end
end
2018-06-05 11:08:52 +00:00
begin
2020-10-22 13:57:01 +00:00
case file . header [ :content_disposition ] . to_s
2020-11-12 11:42:44 +00:00
when / (filename|name)( \ *{0,1})="(.+?)" /i , / (filename|name)( \ *{0,1})='(.+?)' /i , / (filename|name)( \ *{0,1})=(.+?); /i
2018-09-07 12:20:11 +00:00
filename = $3
2018-06-05 11:08:52 +00:00
end
rescue
Rails . logger . debug { 'Unable to get filename' }
end
# as fallback, use raw values
if filename . blank?
2020-10-22 13:57:01 +00:00
case headers_store [ 'Content-Disposition' ] . to_s
2020-11-12 11:42:44 +00:00
when / (filename|name)( \ *{0,1})="(.+?)" /i , / (filename|name)( \ *{0,1})='(.+?)' /i , / (filename|name)( \ *{0,1})=(.+?); /i
2018-09-07 12:20:11 +00:00
filename = $3
2018-06-05 11:08:52 +00:00
end
end
# for some broken sm mail clients (X-MimeOLE: Produced By Microsoft Exchange V6.5)
2019-02-24 14:53:58 +00:00
filename || = file . header [ :content_location ] . to_s . force_encoding ( 'utf-8' )
2018-06-05 11:08:52 +00:00
# generate file name based on content-id
2020-09-30 09:07:01 +00:00
if filename . blank? && headers_store [ 'Content-ID' ] . present? && headers_store [ 'Content-ID' ] =~ / (.+?)@.+? /i
filename = $1
2018-06-05 11:08:52 +00:00
end
2020-08-04 08:08:28 +00:00
file_body = String . new ( file . body . to_s )
2018-06-05 11:08:52 +00:00
# generate file name based on content type
2019-02-24 14:53:58 +00:00
if filename . blank? && headers_store [ 'Content-Type' ] . present? && headers_store [ 'Content-Type' ] . match? ( %r{ ^message/rfc822 }i )
begin
parser = Channel :: EmailParser . new
2020-08-04 08:08:28 +00:00
mail_local = parser . parse ( file_body )
2019-02-24 14:53:58 +00:00
filename = if mail_local [ :subject ] . present?
" #{ mail_local [ :subject ] } .eml "
elsif headers_store [ 'Content-Description' ] . present?
" #{ headers_store [ 'Content-Description' ] } .eml " . to_s . force_encoding ( 'utf-8' )
else
'Mail.eml'
end
rescue
filename = 'Mail.eml'
2018-06-05 11:08:52 +00:00
end
2019-02-24 14:53:58 +00:00
end
2018-06-05 11:08:52 +00:00
2019-02-24 14:53:58 +00:00
# e. g. Content-Type: video/quicktime; name="Video.MOV";
if filename . blank?
[ '(filename|name)(\*{0,1})="(.+?)"(;|$)' , '(filename|name)(\*{0,1})=\'(.+?)\'(;|$)' , '(filename|name)(\*{0,1})=(.+?)(;|$)' ] . each do | regexp |
if headers_store [ 'Content-Type' ] =~ / #{ regexp } /i
filename = $3
break
2018-06-05 11:08:52 +00:00
end
end
2019-02-24 14:53:58 +00:00
end
2018-06-05 11:08:52 +00:00
2019-02-24 14:53:58 +00:00
# workaround for mail gem - decode filenames
# https://github.com/zammad/zammad/issues/928
if filename . present?
filename = Mail :: Encodings . value_decode ( filename )
end
if ! filename . force_encoding ( 'UTF-8' ) . valid_encoding?
filename = filename . utf8_encode ( fallback : :read_as_sanitized_binary )
end
# e. g. Content-Type: video/quicktime
2020-01-27 09:07:55 +00:00
if filename . blank? && ( content_type = headers_store [ 'Content-Type' ] )
2019-02-24 14:53:58 +00:00
map = {
2019-09-02 11:52:11 +00:00
'message/delivery-status' : %w[ txt delivery-status ] ,
2019-02-24 14:53:58 +00:00
'text/plain' : %w[ txt document ] ,
'text/html' : %w[ html document ] ,
'video/quicktime' : %w[ mov video ] ,
'image/jpeg' : %w[ jpg image ] ,
'image/jpg' : %w[ jpg image ] ,
'image/png' : %w[ png image ] ,
'image/gif' : %w[ gif image ] ,
}
map . each do | type , ext |
2020-01-27 09:07:55 +00:00
next if ! content_type . match? ( / ^ #{ Regexp . quote ( type ) } /i )
2019-02-24 14:53:58 +00:00
filename = if headers_store [ 'Content-Description' ] . present?
" #{ headers_store [ 'Content-Description' ] } . #{ ext [ 0 ] } " . to_s . force_encoding ( 'utf-8' )
else
" #{ ext [ 1 ] } . #{ ext [ 0 ] } "
end
break
2018-06-05 11:08:52 +00:00
end
end
2019-02-24 14:53:58 +00:00
# set fallback filename
2018-06-05 11:08:52 +00:00
if filename . blank?
filename = 'file'
end
2019-02-24 14:53:58 +00:00
# create uniq filename
2018-06-05 11:08:52 +00:00
local_filename = ''
local_extention = ''
if filename =~ / ^(.*?) \ .(.+?)$ /
local_filename = $1
local_extention = $2
end
1 . upto ( 1000 ) do | i |
filename_exists = false
attachments . each do | attachment |
if attachment [ :filename ] == filename
filename_exists = true
end
end
break if filename_exists == false
2018-10-09 06:17:41 +00:00
2018-06-05 11:08:52 +00:00
filename = if local_extention . present?
" #{ local_filename } #{ i } . #{ local_extention } "
else
" #{ local_filename } #{ i } "
end
end
# get mime type
if file . header [ :content_type ] & . string
headers_store [ 'Mime-Type' ] = file . header [ :content_type ] . string
end
# get charset
if file . header & . charset
headers_store [ 'Charset' ] = file . header . charset
end
# remove not needed header
headers_store . delete ( 'Content-Transfer-Encoding' )
headers_store . delete ( 'Content-Disposition' )
attach = {
2020-08-04 08:08:28 +00:00
data : file_body ,
2018-12-19 17:31:51 +00:00
filename : filename ,
2018-06-05 11:08:52 +00:00
preferences : headers_store ,
}
[ attach ]
end
2019-08-06 15:26:29 +00:00
2019-10-08 07:19:04 +00:00
# Archive the given message as tmp/folder/md5.eml
2019-08-06 15:26:29 +00:00
def archive_mail ( folder , msg )
path = Rails . root . join ( 'tmp' , folder )
FileUtils . mkpath path
2019-10-08 07:19:04 +00:00
# MD5 hash the msg and save it as "md5.eml"
2019-08-06 15:26:29 +00:00
md5 = Digest :: MD5 . hexdigest ( msg )
2019-10-08 07:19:04 +00:00
file_path = Rails . root . join ( 'tmp' , folder , " #{ md5 } .eml " )
2019-08-06 15:26:29 +00:00
File . open ( file_path , 'wb' ) do | file |
file . write msg
end
file_path
end
# Auto reply as the postmaster to oversized emails with:
2019-08-09 10:47:21 +00:00
# [undeliverable] Message too large
2019-08-06 15:26:29 +00:00
def postmaster_response ( channel , msg )
begin
reply_mail = compose_postmaster_reply ( msg )
rescue NotificationFactory :: FileNotFoundError = > e
2020-09-30 09:07:01 +00:00
Rails . logger . error " No valid postmaster email_oversized template found. Skipping postmaster reply. #{ e . inspect } "
2019-08-06 15:26:29 +00:00
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 ]
2020-02-18 19:51:31 +00:00
mail . msg_size = format ( '%<MB>.2f' , MB : raw_incoming_mail . size . to_f / 1024 / 1024 )
2019-08-06 15:26:29 +00:00
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
2020-12-18 15:47:28 +00:00
def guess_email_fqdn ( from )
Mail :: Address . new ( from ) . domain . strip
rescue
nil
end
def generate_message_id ( raw_message , from )
fqdn = guess_email_fqdn ( from ) || 'zammad_generated'
" <gen- #{ Digest :: MD5 . hexdigest ( raw_message ) } @ #{ fqdn } > "
end
2020-12-30 13:55:08 +00:00
# https://github.com/zammad/zammad/issues/3096
# specific email needs to be forced to ISO-2022-JP
# but that breaks other emails that can be forced to SJIS only
# thus force to ISO-2022-JP but fallback to SJIS
2021-01-25 10:54:37 +00:00
#
# https://github.com/zammad/zammad/issues/3368
# some characters are not included in the official ISO-2022-JP
# ISO-2022-JP-KDDI superset provides support for more characters
2020-12-30 13:55:08 +00:00
def force_japanese_encoding ( input )
2021-01-25 10:54:37 +00:00
%w[ ISO-2022-JP ISO-2022-JP-KDDI SJIS ]
. lazy
. map { | encoding | try_encoding ( input , encoding ) }
. detect ( & :present? )
end
def try_encoding ( input , encoding )
input . force_encoding ( encoding ) . encode ( 'UTF-8' )
2020-12-30 13:55:08 +00:00
rescue
2021-01-25 10:54:37 +00:00
nil
2020-12-30 13:55:08 +00:00
end
2012-07-25 13:58:08 +00:00
end
module Mail
2016-11-03 22:18:10 +00:00
2017-03-15 15:13:48 +00:00
# workaround to get content of no parseable headers - in most cases with non 7 bit ascii signs
class Field
def raw_value
2018-08-22 10:05:14 +00:00
begin
value = @raw_value . try ( :utf8_encode )
rescue
value = @raw_value . utf8_encode ( fallback : :read_as_sanitized_binary )
end
2017-05-16 12:03:42 +00:00
return value if value . blank?
2018-10-09 06:17:41 +00:00
2017-03-15 15:13:48 +00:00
value . sub ( / ^.+?:( \ s|) / , '' )
end
end
2018-08-22 10:05:14 +00:00
# issue#348 - IMAP mail fetching stops because of broken spam email (e. g. broken Content-Transfer-Encoding value see test/fixtures/mail43.box)
# https://github.com/zammad/zammad/issues/348
class Body
def decoded
2020-11-05 16:31:00 +00:00
if Encodings . defined? ( encoding )
Encodings . get_encoding ( encoding ) . decode ( raw_source )
else
2018-08-22 10:05:14 +00:00
Rails . logger . info " UnknownEncodingType: Don't know how to decode #{ encoding } ! "
raw_source
end
end
end
2015-04-27 14:15:29 +00:00
end