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
2018-06-05 11:08:52 +00:00
EMAIL_REGEX = / .+@.+ /
RECIPIENT_FIELDS = %w[ to cc delivered-to x-original-to envelope-to ] . freeze
SENDER_FIELDS = %w[ from reply-to return-path ] . 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 )
2018-08-22 10:05:14 +00:00
mail = Mail . new ( msg . force_encoding ( 'binary' ) )
2012-05-04 11:33:05 +00:00
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 ) ,
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
_process ( channel , msg )
rescue = > e
# store unprocessable email for bug reporting
2017-11-23 08:09:44 +00:00
path = Rails . root . join ( 'tmp' , 'unprocessable_mail' )
2016-05-30 08:33:27 +00:00
FileUtils . mkpath path
md5 = Digest :: MD5 . hexdigest ( msg )
filename = " #{ path } / #{ md5 } .eml "
2016-11-03 22:18:10 +00:00
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
2017-04-19 10:09:54 +00:00
Rails . logger . error e
2017-10-01 12:25:52 +00:00
File . open ( filename , 'wb' ) do | file |
2016-05-30 08:33:27 +00:00
file . write msg
2017-10-01 12:25:52 +00:00
end
2016-11-07 22:20:58 +00:00
return false if exception == false
2018-03-02 19:27:13 +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
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
2015-08-30 18:16:29 +00:00
backend . run ( channel , mail )
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
2017-01-30 14:47:12 +00:00
if mail [ 'x-zammad-ignore' . to_sym ] == 'true' || mail [ 'x-zammad-ignore' . to_sym ] == 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
# set interface handle
original_interface_handle = ApplicationHandleInfo . current
2012-05-06 20:48:23 +00:00
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
2016-09-06 05:51:12 +00:00
Transaction . execute ( interface_handle : " #{ original_interface_handle } .postmaster " ) do
2012-04-13 16:42:25 +00:00
2016-08-23 09:49:12 +00:00
# get sender user
2017-01-30 14:47:12 +00:00
session_user_id = mail [ 'x-zammad-session-user-id' . to_sym ]
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
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
2017-01-30 14:47:12 +00:00
if mail [ 'x-zammad-ticket-id' . to_sym ]
ticket = Ticket . find_by ( id : mail [ 'x-zammad-ticket-id' . to_sym ] )
2015-08-30 18:16:29 +00:00
end
2017-01-30 14:47:12 +00:00
if mail [ 'x-zammad-ticket-number' . to_sym ]
ticket = Ticket . find_by ( number : mail [ 'x-zammad-ticket-number' . to_sym ] )
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
2015-12-14 09:23:14 +00:00
state = Ticket :: State . find ( ticket . state_id )
state_type = Ticket :: StateType . find ( state . state_type_id )
2012-08-28 05:21:45 +00:00
2017-02-12 17:21:03 +00:00
# set ticket to open again or keep create state
2017-01-30 14:47:12 +00:00
if ! mail [ 'x-zammad-ticket-followup-state' . to_sym ] && ! mail [ 'x-zammad-ticket-followup-state_id' . to_sym ]
2017-02-12 17:21:03 +00:00
new_state = Ticket :: State . find_by ( default_create : true )
if ticket . state_id != new_state . id && ! mail [ 'x-zammad-out-of-office' . to_sym ]
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 ] )
end
2017-11-21 14:25:04 +00:00
if group . blank? || group . active == false
2016-05-23 17:23:06 +00:00
group = Group . where ( active : true ) . order ( 'id ASC' ) . first
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 (
2016-05-23 17:23:06 +00:00
group_id : group . id ,
2017-02-13 14:05:28 +00:00
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
if mail [ 'x-zammad-ticket-tags' . to_sym ] . present?
mail [ 'x-zammad-ticket-tags' . to_sym ] . each do | tag |
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 (
ticket_id : ticket . id ,
type_id : Ticket :: Article :: Type . find_by ( name : 'email' ) . id ,
sender_id : Ticket :: Article :: Sender . find_by ( name : 'Customer' ) . id ,
content_type : mail [ :content_type ] ,
body : mail [ :body ] ,
from : mail [ :from ] ,
2017-04-26 14:50:57 +00:00
reply_to : mail [ :" reply-to " ] ,
2016-09-08 19:18:26 +00:00
to : mail [ :to ] ,
cc : mail [ :cc ] ,
subject : mail [ :subject ] ,
message_id : mail [ :message_id ] ,
internal : false ,
)
# 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 (
object : 'Ticket::Article' ,
o_id : article . id ,
data : attachment [ :data ] ,
2018-01-02 13:28:34 +00:00
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 |
2016-04-12 07:25:20 +00:00
filters [ setting . name ] = Kernel . const_get ( Setting . get ( setting . name ) )
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
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'
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
if class_name . downcase == 'article'
class_name = 'Ticket::Article'
end
return true if ! attribute
key_short = attribute [ attribute . length - 3 , attribute . length ]
return true if key_short != '_id'
class_object = Object . const_get ( class_name . to_classname )
return if ! class_object
class_instance = class_object . new
2017-01-31 17:13:45 +00:00
return false if ! class_instance . association_id_validation ( attribute , value )
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
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
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
2014-06-08 03:11:21 +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
2015-05-07 09:04:40 +00:00
if item . respond_to? ( :name )
2017-10-02 10:31:59 +00:00
assoc_object = item . lookup ( name : mail [ header . to_sym ] )
end
if ! assoc_object && item . respond_to? ( :login )
assoc_object = item . lookup ( login : mail [ header . to_sym ] )
2014-06-08 03:11:21 +00:00
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 = { } )
path = Rails . root . join ( 'tmp' , 'unprocessable_mail' )
files = [ ]
Dir . glob ( " #{ path } /*.eml " ) do | entry |
ticket , article , user , mail = Channel :: EmailParser . new . process ( params , IO . binread ( entry ) )
next if ticket . blank?
files . push entry
File . delete ( entry )
end
files
end
2018-06-05 11:08:52 +00:00
private
def message_header_hash ( mail )
imported_fields = mail . header . fields . map do | f |
2018-08-22 10:05:14 +00:00
begin
value = f . to_utf8
if value . blank?
value = f . raw_value . to_utf8
end
rescue
value = f . raw_value . to_utf8 ( fallback : :read_as_sanitized_binary )
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
raw_fields = mail . header . fields . map { | f | [ " raw- #{ f . name . downcase } " , f ] } . to_h
custom_fields = { } . tap do | h |
validated_recipients = imported_fields . slice ( * RECIPIENT_FIELDS )
. transform_values { | v | v . match? ( EMAIL_REGEX ) ? v : '' }
h . merge! ( validated_recipients )
h [ 'date' ] = Time . zone . parse ( mail . date . to_s ) || imported_fields [ 'date' ]
h [ 'message_id' ] = imported_fields [ 'message-id' ]
h [ 'subject' ] = imported_fields [ 'subject' ] & . sub ( / ^= \ ?us-ascii \ ?Q \ ?(.+) \ ?=$ / , '\1' )
h [ 'x-any-recipient' ] = validated_recipients . values . select ( & :present? ) . join ( ', ' )
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
message . body . to_s
rescue Mail :: UnknownEncodingType # see test/data/mail/mail043.box / issue #348
message . body . raw_source
end
body_text = body_text . utf8_encode ( from : message . charset , fallback : :read_as_sanitized_binary )
body_text = Mail :: Utilities . to_lf ( body_text )
return body_text . html2html_strict if options [ :strict_html ]
body_text
end
def collect_attachments ( mail )
attachments = [ ]
# Add non-plaintext body as an attachment
if mail . html_part & . body . present? ||
( ! mail . multipart? && mail . mime_type . present? && mail . mime_type != 'text/plain' )
message = mail . html_part || mail
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? }
attachments . push ( { data : body_text ( message ) ,
filename : filename ,
preferences : headers_store } )
end
mail . parts . each do | part |
begin
new_attachments = get_attachments ( part , attachments , mail ) . flatten . compact
attachments . push ( * new_attachments )
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
end
end
attachments
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
begin
value = field . to_utf8
if value . blank?
value = field . raw_value
end
headers_store [ field . name . to_s ] = value
rescue = > e
headers_store [ field . name . to_s ] = field . raw_value
end
end
# cleanup content id, <> will be added automatically later
if headers_store [ 'Content-ID' ]
headers_store [ 'Content-ID' ] . gsub! ( / ^< / , '' )
headers_store [ 'Content-ID' ] . gsub! ( / >$ / , '' )
end
# get filename from content-disposition
# workaround for: NoMethodError: undefined method `filename' for #<Mail::UnstructuredField:0x007ff109e80678>
filename = file . header [ :content_disposition ] . try ( :filename )
begin
if file . header [ :content_disposition ] . to_s =~ / filename="(.+?)" /i
filename = $1
elsif file . header [ :content_disposition ] . to_s =~ / filename='(.+?)' /i
filename = $1
elsif file . header [ :content_disposition ] . to_s =~ / filename=(.+?); /i
filename = $1
end
rescue
Rails . logger . debug { 'Unable to get filename' }
end
# as fallback, use raw values
if filename . blank?
if headers_store [ 'Content-Disposition' ] . to_s =~ / filename="(.+?)" /i
filename = $1
elsif headers_store [ 'Content-Disposition' ] . to_s =~ / filename='(.+?)' /i
filename = $1
elsif headers_store [ 'Content-Disposition' ] . to_s =~ / filename=(.+?); /i
filename = $1
end
end
# for some broken sm mail clients (X-MimeOLE: Produced By Microsoft Exchange V6.5)
filename || = file . header [ :content_location ] . to_s
# generate file name based on content-id
if filename . blank? && headers_store [ 'Content-ID' ] . present?
if headers_store [ 'Content-ID' ] =~ / (.+?)@.+? /i
filename = $1
end
end
# generate file name based on content type
if filename . blank? && headers_store [ 'Content-Type' ] . present?
if headers_store [ 'Content-Type' ] . match? ( %r{ ^message/rfc822 }i )
begin
parser = Channel :: EmailParser . new
mail_local = parser . parse ( file . body . to_s )
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'
end
end
# e. g. Content-Type: video/quicktime; name="Video.MOV";
if filename . blank?
[ 'name="(.+?)"(;|$)' , " name='(.+?)'(;|$) " , 'name=(.+?)(;|$)' ] . each do | regexp |
if headers_store [ 'Content-Type' ] =~ / #{ regexp } /i
filename = $1
break
end
end
end
# e. g. Content-Type: video/quicktime
if filename . blank?
map = {
'message/delivery-status' : [ 'txt' , 'delivery-status' ] ,
'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 |
next if headers_store [ 'Content-Type' ] !~ / ^ #{ Regexp . quote ( type ) } /i
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
end
end
end
if filename . blank?
filename = 'file'
end
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
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' )
# workaround for mail gem
# https://github.com/zammad/zammad/issues/928
filename = Mail :: Encodings . value_decode ( filename )
attach = {
data : file . body . to_s ,
filename : filename ,
preferences : headers_store ,
}
[ attach ]
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?
2017-03-15 15:13:48 +00:00
value . sub ( / ^.+?:( \ s|) / , '' )
end
end
2018-06-05 11:08:52 +00:00
# workaround to parse subjects with 2 different encodings correctly (e. g. quoted-printable see test/fixtures/mail9.box)
2012-07-25 13:58:08 +00:00
module Encodings
2015-05-05 11:01:16 +00:00
def self . value_decode ( str )
2012-07-25 13:58:08 +00:00
# Optimization: If there's no encoded-words in the string, just return it
2018-05-04 14:05:10 +00:00
return str if ! str . index ( '=?' )
2012-07-25 13:58:08 +00:00
str = str . gsub ( / \ ?=( \ s*)= \ ? / , '?==?' ) # Remove whitespaces between 'encoded-word's
# Split on white-space boundaries with capture, so we capture the white-space as well
str . split ( / ([ \ t]) / ) . map do | text |
if text . index ( '=?' ) . nil?
text
else
# Join QP encoded-words that are adjacent to avoid decoding partial chars
2013-06-12 15:59:58 +00:00
# text.gsub!(/\?\=\=\?.+?\?[Qq]\?/m, '') if text =~ /\?==\?/
2012-07-25 13:58:08 +00:00
# Search for occurences of quoted strings or plain strings
text . scan ( / ( # Group around entire regex to include it in matches
2013-06-13 07:01:06 +00:00
\ = \ ?[ ^ ?] + \ ?( [ QB ] ) \ ?[ ^ ?] + ?\ ?\ = # Quoted String with subgroup for encoding method
| # or
. + ?( ?= \ = \ ?| $ ) # Plain String
2013-06-12 15:59:58 +00:00
) / xmi ) . map do | matches |
2012-07-25 13:58:08 +00:00
string , method = * matches
2017-11-23 08:09:44 +00:00
if method == 'b' || method == 'B' # rubocop:disable Style/MultipleComparison
2012-07-25 13:58:08 +00:00
b_value_decode ( string )
2017-11-23 08:09:44 +00:00
elsif method == 'q' || method == 'Q' # rubocop:disable Style/MultipleComparison
2012-07-25 13:58:08 +00:00
q_value_decode ( string )
else
string
end
end
end
2015-04-27 13:20:16 +00:00
end . join ( '' )
2012-07-25 13:58:08 +00:00
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
if ! Encodings . defined? ( encoding )
#raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
Rails . logger . info " UnknownEncodingType: Don't know how to decode #{ encoding } ! "
raw_source
else
Encodings . get_encoding ( encoding ) . decode ( raw_source )
end
end
end
2015-04-27 14:15:29 +00:00
end