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
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 )
2012-05-04 11:33:05 +00:00
data = { }
2015-12-14 09:23:14 +00:00
mail = Mail . new ( msg )
2012-05-04 11:33:05 +00:00
2012-05-05 09:24:05 +00:00
# set all headers
2017-10-01 12:25:52 +00:00
mail . header . fields . each do | field |
2017-03-14 11:00:38 +00:00
2015-05-07 09:04:40 +00:00
# full line, encode, ready for storage
2017-03-14 11:00:38 +00:00
begin
2017-03-15 15:13:48 +00:00
value = Encode . conv ( 'utf8' , field . to_s )
if value . blank?
value = field . raw_value
end
data [ field . name . to_s . downcase . to_sym ] = value
2017-03-14 11:00:38 +00:00
rescue = > e
2017-03-15 15:13:48 +00:00
data [ field . name . to_s . downcase . to_sym ] = field . raw_value
2017-03-14 11:00:38 +00:00
end
2015-05-07 09:04:40 +00:00
# if we need to access the lines by objects later again
2017-01-30 14:47:12 +00:00
data [ " raw- #{ field . name . downcase } " . to_sym ] = field
2017-10-01 12:25:52 +00:00
end
2012-05-05 09:24:05 +00:00
2017-03-15 15:13:48 +00:00
# verify content, ignore recipients with non email address
2017-10-01 12:25:52 +00:00
[ 'to' , 'cc' , 'delivered-to' , 'x-original-to' , 'envelope-to' ] . each do | field |
2017-03-15 15:13:48 +00:00
next if data [ field . to_sym ] . blank?
2017-11-23 08:09:44 +00:00
next if data [ field . to_sym ] . match? ( / @ / )
2017-03-15 15:13:48 +00:00
data [ field . to_sym ] = ''
2017-10-01 12:25:52 +00:00
end
2017-03-15 15:13:48 +00:00
2017-08-15 07:46:21 +00:00
# get sender with @ / email address
2013-10-31 22:56:08 +00:00
from = nil
2017-10-01 12:25:52 +00:00
[ 'from' , 'reply-to' , 'return-path' ] . each do | item |
2017-03-15 15:13:48 +00:00
next if data [ item . to_sym ] . blank?
2017-08-15 07:46:21 +00:00
next if data [ item . to_sym ] !~ / @ /
2017-03-15 15:13:48 +00:00
from = data [ item . to_sym ]
2015-05-07 09:04:40 +00:00
break if from
2017-10-01 12:25:52 +00:00
end
2013-10-31 22:56:08 +00:00
2017-08-15 07:46:21 +00:00
# in case of no sender with email address - get sender
if ! from
2017-10-01 12:25:52 +00:00
[ 'from' , 'reply-to' , 'return-path' ] . each do | item |
2017-08-15 07:46:21 +00:00
next if data [ item . to_sym ] . blank?
from = data [ item . to_sym ]
break if from
2017-10-01 12:25:52 +00:00
end
2017-08-15 07:46:21 +00:00
end
2014-06-08 15:14:51 +00:00
# set x-any-recipient
data [ 'x-any-recipient' . to_sym ] = ''
2017-10-01 12:25:52 +00:00
[ 'to' , 'cc' , 'delivered-to' , 'x-original-to' , 'envelope-to' ] . each do | item |
2017-03-15 15:13:48 +00:00
next if data [ item . to_sym ] . blank?
2015-05-07 09:04:40 +00:00
if data [ 'x-any-recipient' . to_sym ] != ''
data [ 'x-any-recipient' . to_sym ] += ', '
2014-06-08 15:14:51 +00:00
end
2015-05-07 09:04:40 +00:00
data [ 'x-any-recipient' . to_sym ] += mail [ item . to_sym ] . to_s
2017-10-01 12:25:52 +00:00
end
2014-06-08 15:14:51 +00:00
2012-05-05 09:24:05 +00:00
# set extra headers
2017-05-26 13:34:32 +00:00
data = data . merge ( Channel :: EmailParser . sender_properties ( from ) )
2012-05-05 09:24:05 +00:00
2017-05-05 11:13:18 +00:00
# do extra encoding (see issue#1045)
if data [ :subject ] . present?
data [ :subject ] . sub! ( / ^= \ ?us-ascii \ ?Q \ ?(.+) \ ?=$ / , '\1' )
end
2012-05-05 09:24:05 +00:00
# compat headers
data [ :message_id ] = data [ 'message-id' . to_sym ]
2012-05-04 11:33:05 +00:00
# body
2013-06-12 15:59:58 +00:00
# plain_part = mail.multipart? ? (mail.text_part ? mail.text_part.body.decoded : nil) : mail.body.decoded
# html_part = message.html_part ? message.html_part.body.decoded : nil
2012-05-07 22:37:07 +00:00
data [ :attachments ] = [ ]
2012-10-05 06:42:12 +00:00
2012-07-02 18:52:27 +00:00
# multi part email
2012-05-04 19:30:22 +00:00
if mail . multipart?
2012-10-05 06:42:12 +00:00
2016-06-21 15:14:15 +00:00
# html attachment/body may exists and will be converted to strict html
2017-11-23 08:09:44 +00:00
if mail . html_part & . body
2016-06-21 15:14:15 +00:00
data [ :body ] = mail . html_part . body . to_s
data [ :body ] = Encode . conv ( mail . html_part . charset . to_s , data [ :body ] )
data [ :body ] = data [ :body ] . html2html_strict . to_s . force_encoding ( 'utf-8' )
if ! data [ :body ] . force_encoding ( 'UTF-8' ) . valid_encoding?
data [ :body ] = data [ :body ] . encode ( 'utf-8' , 'binary' , invalid : :replace , undef : :replace , replace : '?' )
end
data [ :content_type ] = 'text/html'
end
2012-07-02 18:52:27 +00:00
# text attachment/body exists
2017-11-06 03:26:00 +00:00
if data [ :body ] . blank? && mail . text_part
2012-10-04 06:54:21 +00:00
data [ :body ] = mail . text_part . body . decoded
2015-12-14 09:23:14 +00:00
data [ :body ] = Encode . conv ( mail . text_part . charset , data [ :body ] )
2017-06-23 11:57:24 +00:00
data [ :body ] = data [ :body ] . to_s . force_encoding ( 'utf-8' )
2012-10-05 06:42:12 +00:00
2014-08-06 11:41:10 +00:00
if ! data [ :body ] . valid_encoding?
2015-04-27 13:42:53 +00:00
data [ :body ] = data [ :body ] . encode ( 'utf-8' , 'binary' , invalid : :replace , undef : :replace , replace : '?' )
2014-08-06 11:41:10 +00:00
end
2016-06-21 15:14:15 +00:00
data [ :content_type ] = 'text/plain'
2015-11-26 08:29:58 +00:00
end
2014-08-06 11:41:10 +00:00
2016-06-21 15:14:15 +00:00
# any other attachments
2017-11-06 03:26:00 +00:00
if data [ :body ] . blank?
2016-06-21 15:14:15 +00:00
data [ :body ] = 'no visible content'
data [ :content_type ] = 'text/plain'
2012-07-02 18:52:27 +00:00
end
2012-06-15 11:10:23 +00:00
2012-07-02 18:52:27 +00:00
# add html attachment/body as real attachment
if mail . html_part
filename = 'message.html'
headers_store = {
'content-alternative' = > true ,
2018-03-06 12:52:02 +00:00
'original-format' = > true ,
2012-07-02 18:52:27 +00:00
}
2012-06-15 11:10:23 +00:00
if mail . mime_type
headers_store [ 'Mime-Type' ] = mail . html_part . mime_type
end
if mail . charset
headers_store [ 'Charset' ] = mail . html_part . charset
end
attachment = {
2015-04-27 13:42:53 +00:00
data : mail . html_part . body . to_s ,
filename : mail . html_part . filename || filename ,
preferences : headers_store
2012-06-15 11:10:23 +00:00
}
data [ :attachments ] . push attachment
end
2012-10-04 06:54:21 +00:00
2012-07-02 18:52:27 +00:00
# get attachments
2017-11-23 08:09:44 +00:00
mail . parts & . each do | part |
# protect process to work fine with spam emails, see test/fixtures/mail15.box
begin
attachs = _get_attachment ( part , data [ :attachments ] , mail )
data [ :attachments ] . concat ( attachs )
rescue
attachs = _get_attachment ( part , data [ :attachments ] , mail )
data [ :attachments ] . concat ( attachs )
2017-10-01 12:25:52 +00:00
end
2012-07-02 18:52:27 +00:00
end
2014-12-28 23:11:37 +00:00
# not multipart email
2012-05-07 22:37:07 +00:00
2016-06-21 15:14:15 +00:00
# html part only, convert to text and add it as attachment
elsif mail . mime_type && mail . mime_type . to_s . casecmp ( 'text/html' ) . zero?
filename = 'message.html'
data [ :body ] = mail . body . decoded
data [ :body ] = Encode . conv ( mail . charset , data [ :body ] )
data [ :body ] = data [ :body ] . html2html_strict . to_s . force_encoding ( 'utf-8' )
if ! data [ :body ] . valid_encoding?
data [ :body ] = data [ :body ] . encode ( 'utf-8' , 'binary' , invalid : :replace , undef : :replace , replace : '?' )
end
data [ :content_type ] = 'text/html'
# add body as attachment
headers_store = {
'content-alternative' = > true ,
2018-03-06 12:52:02 +00:00
'original-format' = > true ,
2016-06-21 15:14:15 +00:00
}
if mail . mime_type
headers_store [ 'Mime-Type' ] = mail . mime_type
end
if mail . charset
headers_store [ 'Charset' ] = mail . charset
end
attachment = {
data : mail . body . decoded ,
filename : mail . filename || filename ,
preferences : headers_store
}
data [ :attachments ] . push attachment
2016-01-15 17:22:57 +00:00
# text part only
2016-01-16 10:05:04 +00:00
elsif ! mail . mime_type || mail . mime_type . to_s == '' || mail . mime_type . to_s . casecmp ( 'text/plain' ) . zero?
2016-01-15 17:22:57 +00:00
data [ :body ] = mail . body . decoded
data [ :body ] = Encode . conv ( mail . charset , data [ :body ] )
if ! data [ :body ] . force_encoding ( 'UTF-8' ) . valid_encoding?
data [ :body ] = data [ :body ] . encode ( 'utf-8' , 'binary' , invalid : :replace , undef : :replace , replace : '?' )
end
2016-06-21 15:14:15 +00:00
data [ :content_type ] = 'text/plain'
2016-01-15 17:22:57 +00:00
else
filename = '-no name-'
2016-06-21 15:14:15 +00:00
data [ :body ] = 'no visible content'
2016-07-19 13:19:22 +00:00
data [ :content_type ] = 'text/plain'
2012-05-07 22:37:07 +00:00
2016-01-15 17:22:57 +00:00
# add body as attachment
headers_store = {
'content-alternative' = > true ,
}
if mail . mime_type
headers_store [ 'Mime-Type' ] = mail . mime_type
end
if mail . charset
headers_store [ 'Charset' ] = mail . charset
2012-05-07 22:37:07 +00:00
end
2016-01-15 17:22:57 +00:00
attachment = {
data : mail . body . decoded ,
filename : mail . filename || filename ,
preferences : headers_store
}
data [ :attachments ] . push attachment
2012-05-04 19:30:22 +00:00
end
2012-05-04 11:33:05 +00:00
2012-07-02 18:52:27 +00:00
# strip not wanted chars
2015-12-14 09:23:14 +00:00
data [ :body ] . gsub! ( / \ n \ r / , " \n " )
data [ :body ] . gsub! ( / \ r \ n / , " \n " )
2016-01-15 17:22:57 +00:00
data [ :body ] . tr! ( " \r " , " \n " )
2012-07-02 18:52:27 +00:00
2016-06-02 18:58:46 +00:00
# get mail date
begin
if mail . date
data [ :date ] = Time . zone . parse ( mail . date . to_s )
end
rescue
data [ :date ] = nil
end
2015-08-30 18:16:29 +00:00
# remember original mail instance
data [ :mail_instance ] = mail
2014-12-29 10:27:59 +00:00
data
2012-05-04 11:33:05 +00:00
end
2015-12-14 09:23:14 +00:00
def _get_attachment ( file , attachments , mail )
2013-01-23 13:47:57 +00:00
# check if sub parts are available
2017-11-06 03:26:00 +00:00
if file . parts . present?
list = [ ]
2017-10-01 12:25:52 +00:00
file . parts . each do | p |
2015-12-14 09:23:14 +00:00
attachment = _get_attachment ( p , attachments , mail )
2017-11-06 03:26:00 +00:00
list . concat ( attachment )
2017-10-01 12:25:52 +00:00
end
2017-11-06 03:26:00 +00:00
return list
2013-01-23 13:47:57 +00:00
end
2014-12-28 23:11:37 +00:00
# ignore text/plain attachments - already shown in view
2017-11-23 08:09:44 +00:00
return [ ] if mail . text_part & . body . to_s == file . body . to_s
2014-12-28 23:11:37 +00:00
# ignore text/html - html part, already shown in view
2017-11-23 08:09:44 +00:00
return [ ] if mail . html_part & . body . to_s == file . body . to_s
2014-12-28 23:11:37 +00:00
2013-01-23 13:47:57 +00:00
# get file preferences
headers_store = { }
2017-10-01 12:25:52 +00:00
file . header . fields . each do | field |
2017-06-06 18:20:37 +00:00
# full line, encode, ready for storage
begin
value = Encode . conv ( 'utf8' , field . to_s )
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
2017-10-01 12:25:52 +00:00
end
2013-01-23 13:47:57 +00:00
2017-11-06 03:26:00 +00:00
# cleanup content id, <> will be added automatically later
if headers_store [ 'Content-ID' ]
headers_store [ 'Content-ID' ] . gsub! ( / ^< / , '' )
headers_store [ 'Content-ID' ] . gsub! ( / >$ / , '' )
end
2013-01-23 13:47:57 +00:00
# get filename from content-disposition
filename = nil
# workaround for: NoMethodError: undefined method `filename' for #<Mail::UnstructuredField:0x007ff109e80678>
begin
filename = file . header [ :content_disposition ] . filename
rescue
2016-01-28 18:55:15 +00:00
begin
2017-06-02 15:41:02 +00:00
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
2016-01-28 18:55:15 +00:00
end
2016-01-28 19:08:13 +00:00
rescue
2018-03-20 17:47:49 +00:00
Rails . logger . debug { 'Unable to get filename' }
2013-01-23 13:47:57 +00:00
end
end
2017-06-06 18:20:37 +00:00
# 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
2013-01-23 13:47:57 +00:00
# for some broken sm mail clients (X-MimeOLE: Produced By Microsoft Exchange V6.5)
2016-12-02 11:24:00 +00:00
filename || = file . header [ :content_location ] . to_s
2013-01-23 13:47:57 +00:00
2017-11-06 03:26:00 +00:00
# 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?
2017-11-23 08:09:44 +00:00
if headers_store [ 'Content-Type' ] . match? ( %r{ ^message/rfc822 }i )
2017-11-06 03:26:00 +00:00
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?
2018-01-02 13:28:34 +00:00
" #{ headers_store [ 'Content-Description' ] } .eml " . to_s . force_encoding ( 'utf-8' )
2017-11-06 03:26:00 +00:00
else
'Mail.eml'
end
rescue
filename = 'Mail.eml'
end
end
2017-11-06 16:43:06 +00:00
# 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' ] ,
2017-11-23 08:09:44 +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 ] ,
2017-11-06 16:43:06 +00:00
}
map . each do | type , ext |
next if headers_store [ 'Content-Type' ] !~ / ^ #{ Regexp . quote ( type ) } /i
filename = if headers_store [ 'Content-Description' ] . present?
2018-01-02 13:28:34 +00:00
" #{ headers_store [ 'Content-Description' ] } . #{ ext [ 0 ] } " . to_s . force_encoding ( 'utf-8' )
2017-11-06 16:43:06 +00:00
else
" #{ ext [ 1 ] } . #{ ext [ 0 ] } "
end
break
end
end
2017-11-06 03:26:00 +00:00
end
2016-12-02 11:24:00 +00:00
if filename . blank?
2017-11-06 16:43:06 +00:00
filename = 'file'
end
attachment_count = 0
local_filename = ''
local_extention = ''
if filename =~ / ^(.*?) \ .(.+?)$ /
local_filename = $1
local_extention = $2
end
( 1 .. 1000 ) . each do | count |
filename_exists = false
attachments . each do | attachment |
if attachment [ :filename ] == filename
filename_exists = true
2017-10-01 12:25:52 +00:00
end
end
2017-11-06 16:43:06 +00:00
break if filename_exists == false
filename = if local_extention . present?
" #{ local_filename } #{ count } . #{ local_extention } "
else
" #{ local_filename } #{ count } "
end
2013-01-23 13:47:57 +00:00
end
# get mime type
2017-11-23 08:09:44 +00:00
if file . header [ :content_type ] & . string
2013-01-23 13:47:57 +00:00
headers_store [ 'Mime-Type' ] = file . header [ :content_type ] . string
end
# get charset
2017-11-23 08:09:44 +00:00
if file . header & . charset
2013-01-23 13:47:57 +00:00
headers_store [ 'Charset' ] = file . header . charset
end
# remove not needed header
headers_store . delete ( 'Content-Transfer-Encoding' )
headers_store . delete ( 'Content-Disposition' )
2017-04-04 21:17:55 +00:00
# workaround for mail gem
# https://github.com/zammad/zammad/issues/928
filename = Mail :: Encodings . value_decode ( filename )
2013-01-23 13:47:57 +00:00
attach = {
2015-04-27 13:42:53 +00:00
data : file . body . to_s ,
filename : filename ,
preferences : headers_store ,
2013-01-23 13:47:57 +00:00
}
2014-12-28 23:11:37 +00:00
[ attach ]
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 |
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
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!
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?
filename = filename . encode ( 'utf-8' , 'binary' , invalid : :replace , undef : :replace , replace : '?' )
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
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
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
2017-05-26 13:34:32 +00:00
def self . sender_properties ( from )
data = { }
2017-09-23 06:25:55 +00:00
return data if from . blank?
2017-05-26 13:34:32 +00:00
begin
2017-06-19 20:24:22 +00:00
list = Mail :: AddressList . new ( from )
2017-10-01 12:25:52 +00:00
list . addresses . each do | address |
2017-06-19 20:24:22 +00:00
data [ :from_email ] = address . address
data [ :from_local ] = address . local
data [ :from_domain ] = address . domain
data [ :from_display_name ] = address . display_name ||
( address . comments && address . comments [ 0 ] )
break if data [ :from_email ] . present? && data [ :from_email ] =~ / @ /
2017-10-01 12:25:52 +00:00
end
2017-06-19 20:24:22 +00:00
rescue = > e
if from =~ / <> / && from =~ / <.+?> /
data = sender_properties ( from . gsub ( / <> / , '' ) )
end
end
2017-11-06 03:26:00 +00:00
if data . blank? || data [ :from_email ] . blank?
2017-05-26 13:34:32 +00:00
from . strip!
if from =~ / ^(.+?)<(.+?)@(.+?)>$ /
data [ :from_email ] = " #{ $2 } @ #{ $3 } "
data [ :from_local ] = $2
data [ :from_domain ] = $3
data [ :from_display_name ] = $1
else
data [ :from_email ] = from
data [ :from_local ] = from
data [ :from_domain ] = from
end
end
# do extra decoding because we needed to use field.value
2018-01-18 10:13:10 +00:00
data [ :from_display_name ] = Mail :: Field . new ( 'X-From' , Encode . conv ( 'utf8' , data [ :from_display_name ] ) ) . to_s
2017-05-26 13:34:32 +00:00
data [ :from_display_name ] . delete! ( '"' )
data [ :from_display_name ] . strip!
data [ :from_display_name ] . gsub! ( / ^' / , '' )
data [ :from_display_name ] . gsub! ( / '$ / , '' )
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
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
value = Encode . conv ( 'utf8' , @raw_value )
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
2016-11-03 22:18:10 +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
2015-04-27 13:20:16 +00:00
return str unless 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
2016-11-03 22:18:10 +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