Added bounce follow up feature (postmaster bounce emails will now issue an followup of origin ticket).

This commit is contained in:
Martin Edenhofer 2015-08-30 20:16:29 +02:00
parent 98cba4239c
commit 775c4f7d06
7 changed files with 314 additions and 37 deletions

View file

@ -12,50 +12,50 @@ class Channel::EmailParser
mail = parse( msg_as_string ) mail = parse( msg_as_string )
mail = { mail = {
:from => 'Some Name <some@example.com>', from: 'Some Name <some@example.com>',
:from_email => 'some@example.com', from_email: 'some@example.com',
:from_local => 'some', from_local: 'some',
:from_domain => 'example.com', from_domain: 'example.com',
:from_display_name => 'Some Name', from_display_name: 'Some Name',
:message_id => 'some_message_id@example.com', message_id: 'some_message_id@example.com',
:to => 'Some System <system@example.com>', to: 'Some System <system@example.com>',
:cc => 'Somebody <somebody@example.com>', cc: 'Somebody <somebody@example.com>',
:subject => 'some message subject', subject: 'some message subject',
:body => 'some message body', body: 'some message body',
:attachments => [ attachments: [
{ {
:data => 'binary of attachment', data: 'binary of attachment',
:filename => 'file_name_of_attachment.txt', filename: 'file_name_of_attachment.txt',
:preferences => { preferences: {
:content-alternative => true, content-alternative: true,
:Mime-Type => 'text/plain', Mime-Type: 'text/plain',
:Charset => 'iso-8859-1', Charset: 'iso-8859-1',
}, },
}, },
], ],
# ignore email header # ignore email header
:x-zammad-ignore => 'false', x-zammad-ignore: 'false',
# customer headers # customer headers
:x-zammad-customer-login => '', x-zammad-customer-login: '',
:x-zammad-customer-email => '', x-zammad-customer-email: '',
:x-zammad-customer-firstname => '', x-zammad-customer-firstname: '',
:x-zammad-customer-lastname => '', x-zammad-customer-lastname: '',
# ticket headers # ticket headers
:x-zammad-ticket-group => 'some_group', x-zammad-ticket-group: 'some_group',
:x-zammad-ticket-state => 'some_state', x-zammad-ticket-state: 'some_state',
:x-zammad-ticket-priority => 'some_priority', x-zammad-ticket-priority: 'some_priority',
:x-zammad-ticket-owner => 'some_owner_login', x-zammad-ticket-owner: 'some_owner_login',
# article headers # article headers
:x-zammad-article-internal => false, x-zammad-article-internal: false,
:x-zammad-article-type => 'agent', x-zammad-article-type: 'agent',
:x-zammad-article-sender => 'customer', x-zammad-article-sender: 'customer',
# all other email headers # all other email headers
:some-header => 'some_value', some-header: 'some_value',
} }
=end =end
@ -243,6 +243,9 @@ class Channel::EmailParser
data[:body].gsub!( /\r\n/, "\n" ) data[:body].gsub!( /\r\n/, "\n" )
data[:body].gsub!( /\r/, "\n" ) data[:body].gsub!( /\r/, "\n" )
# remember original mail instance
data[:mail_instance] = mail
data data
end end
@ -325,12 +328,25 @@ class Channel::EmailParser
[attach] [attach]
end end
=begin
parser = Channel::EmailParser.new
ticket, article, user = parser.process(channel, email_raw_string)
retrns
[ticket, article, user]
=end
def process(channel, msg) def process(channel, msg)
mail = parse( msg ) mail = parse( msg )
# run postmaster pre filter # run postmaster pre filter
filters = { filters = {
'0010' => Channel::Filter::Trusted, '0010' => Channel::Filter::Trusted,
'0100' => Channel::Filter::FollowUpCheck,
'0900' => Channel::Filter::BounceCheck,
'1000' => Channel::Filter::Database, '1000' => Channel::Filter::Database,
} }
@ -393,8 +409,13 @@ class Channel::EmailParser
# set current user # set current user
UserInfo.current_user_id = user.id UserInfo.current_user_id = user.id
# get ticket# from subject # get ticket# based on email headers
ticket = Ticket::Number.check( mail[:subject] ) if mail[ 'x-zammad-ticket-id'.to_sym ]
ticket = Ticket.find_by( id: mail[ 'x-zammad-ticket-id'.to_sym ] )
end
if mail[ 'x-zammad-ticket-number'.to_sym ]
ticket = Ticket.find_by( number: mail[ 'x-zammad-ticket-number'.to_sym ] )
end
# set ticket state to open if not new # set ticket state to open if not new
if ticket if ticket

View file

@ -0,0 +1,27 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
module Channel::Filter::BounceCheck
def self.run( _channel, mail )
return if !mail[:mail_instance]
return if !mail[:mail_instance].bounced?
return if !mail[:attachments]
return if mail[ 'x-zammad-ticket-id'.to_sym ]
mail[:attachments].each {|attachment|
next if !attachment[:preferences]
next if attachment[:preferences]['Mime-Type'] != 'message/rfc822'
next if !attachment[:data]
result = Channel::EmailParser.new.parse(attachment[:data])
next if !result[:message_id]
message_id_md5 = Digest::MD5.hexdigest(result[:message_id])
article = Ticket::Article.where(message_id_md5: message_id_md5).order('id DESC').limit(1).first
if article
mail[ 'x-zammad-ticket-id'.to_sym ] = article.ticket_id
break
end
}
end
end

View file

@ -0,0 +1,14 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
module Channel::Filter::FollowUpCheck
def self.run( _channel, mail )
return if mail[ 'x-zammad-ticket-id'.to_sym ]
# get ticket# from subject
ticket = Ticket::Number.check( mail[:subject] )
return if !ticket
mail[ 'x-zammad-ticket-id'.to_sym ] = ticket.id
end
end

View file

@ -13,8 +13,9 @@ class Ticket::Article < ApplicationModel
belongs_to :created_by, class_name: 'User' belongs_to :created_by, class_name: 'User'
belongs_to :updated_by, class_name: 'User' belongs_to :updated_by, class_name: 'User'
store :preferences store :preferences
before_create :check_subject before_create :check_subject, :check_message_id_md5
before_update :check_subject before_update :check_subject, :check_message_id_md5
notify_clients_support notify_clients_support
activity_stream_support ignore_attributes: { activity_stream_support ignore_attributes: {
@ -31,13 +32,19 @@ class Ticket::Article < ApplicationModel
private private
# strip not wanted chars
def check_subject def check_subject
return if !subject return if !subject
subject.gsub!(/\s|\t|\r/, ' ') subject.gsub!(/\s|\t|\r/, ' ')
end end
# fillup md5 of message id to search easier on very long message ids
def check_message_id_md5
return if !message_id
return if message_id_md5
self.message_id_md5 = Digest::MD5.hexdigest(message_id)
end
class Flag < ApplicationModel class Flag < ApplicationModel
end end

View file

@ -0,0 +1,10 @@
class UpdateMessageIdMd5 < ActiveRecord::Migration
def up
Ticket::Article.all.each {|article|
next if !article.message_id
next if article.message_id_md5
message_id_md5 = Digest::MD5.hexdigest(article.message_id)
article.update_columns({ message_id_md5: message_id_md5 })
}
end
end

View file

@ -0,0 +1,168 @@
Return-Path: <MAILER-DAEMON>
Delivered-To: edenhofer@zammad.example
Received: by mx1.zammad.com (Postfix)
id 9246B20C3E96; Sun, 30 Aug 2015 16:56:06 +0200 (CEST)
Date: Sun, 30 Aug 2015 16:56:06 +0200 (CEST)
From: MAILER-DAEMON@mx1.zammad.com (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: edenhofer@zammad.example
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
boundary="EF1A820C3E97.1440946566/mx1.zammad.com"
Message-Id: <20150830145606.9246B20C3E96@mx1.zammad.com>
This is a MIME-encapsulated message.
--EF1A820C3E97.1440946566/mx1.zammad.com
Content-Description: Notification
Content-Type: text/plain; charset=us-ascii
This is the mail system at host mx1.zammad.com.
I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.
For further assistance, please send mail to postmaster.
If you do so, please include this problem report. You can
delete your own text from the attached returned message.
The mail system
<not_existing@znuny.com>: host arber.znuny.com[88.198.51.81] said: 550 5.1.1
<not_existing@znuny.com>: Recipient address rejected: User unknown (in
reply to RCPT TO command)
--EF1A820C3E97.1440946566/mx1.zammad.com
Content-Description: Delivery report
Content-Type: message/delivery-status
Reporting-MTA: dns; mx1.zammad.com
X-Postfix-Queue-ID: EF1A820C3E97
X-Postfix-Sender: rfc822; edenhofer@zammad.example
Arrival-Date: Sun, 30 Aug 2015 16:56:02 +0200 (CEST)
Final-Recipient: rfc822; not_existing@znuny.com
Original-Recipient: rfc822;not_existing@znuny.com
Action: failed
Status: 5.1.1
Remote-MTA: dns; arber.znuny.com
Diagnostic-Code: smtp; 550 5.1.1 <not_existing@znuny.com>: Recipient address
rejected: User unknown
--EF1A820C3E97.1440946566/mx1.zammad.com
Content-Description: Undelivered Message
Content-Type: message/rfc822
Return-Path: <edenhofer@zammad.example>
Received: from appnode2.dc.zammad.com (appnode2.dc.zammad.com [144.1.1.1])
by mx1.zammad.com (Postfix) with ESMTP id 4B9BB20C3E96
for <not_existing@znuny.com>; Sun, 30 Aug 2015 16:56:02 +0200 (CEST)
Received: by appnode2.dc.zammad.com (Postfix, from userid 1001)
id 36680141408; Sun, 30 Aug 2015 16:56:07 +0200 (CEST)
Date: Sun, 30 Aug 2015 16:56:07 +0200
From: Martin Edenhofer via Zammad Helpdesk <edenhofer@zammad.example>
To: Martin Edenhofer <not_existing@znuny.com>
Message-ID: <20150830145601.30.608881@edenhofer.zammad.com>
In-Reply-To:
Subject: test [Ticket#10010]
Mime-Version: 1.0
Content-Type: multipart/mixed;
boundary="--==_mimepart_55e319872c918_39d5375468c949db";
charset=UTF-8
Content-Transfer-Encoding: 7bit
Organization:
X-Mailer: Zammad Mail Service (1.x)
X-znuny-MailScanner-Information: Please contact the ISP for more information
X-znuny-MailScanner-ID: 4B9BB20C3E96.A3EE2
X-znuny-MailScanner: Found to be clean
X-znuny-MailScanner-From: edenhofer@zammad.example
X-Spam-Status: No
----==_mimepart_55e319872c918_39d5375468c949db
Content-Type: multipart/alternative;
boundary="--==_mimepart_55e319872c5bd_39d5375468c94778";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_55e319872c5bd_39d5375468c94778
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
Hello,
is somebody there?
Martin Edenhofer
--
Super Support - Waterford Business Park
5201 Blue Lagoon Drive - 8th Floor & 9th Floor - Miami, 33126 USA
Email: [1] hot@example.com - Web: [2] http://www.example.com/
--
[1] mailto:hot@example.com
[2] http://www.example.com/
----==_mimepart_55e319872c5bd_39d5375468c94778
Content-Type: multipart/related;
boundary="--==_mimepart_55e319872c7db_39d5375468c9486e";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_55e319872c7db_39d5375468c9486e
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style type="text/css">
body {
width:90% !important;
-webkit-text-size-adjust:90%;
-ms-text-size-adjust:90%;
font-family:'Helvetica Neue', Helvetica, Arial, Geneva, sans-serif; font-size: 12px;;
}
img {
outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;
}
a img {
border:none;
}
table td {
border-collapse: collapse;
}
table {
border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;
}
p, table, div, td {
max-width: 600px;
}
p {
margin: 0;
}
blockquote, pre {
margin: 0px;
padding: 8px 12px 8px 12px;
}
</style>
<head>
<body style="font-family:'Helvetica Neue', Helvetica, Arial, Geneva, sans-serif; font-size: 12px;"><div>Hello,</div><div><br></div><div>is somebody there?</div><br><div data-signature="true" data-signature-id="1"><div>Martin Edenhofer</div><div><br></div><div>--</div><div> Super Support - Waterford Business Park</div><div> 5201 Blue Lagoon Drive - 8th Floor &amp; 9th Floor - Miami, 33126 USA</div><div> Email: <a href="mailto:hot@example.com" title="mailto:hot@example.com" target="_blank">hot@example.com</a> - Web: <a href="http://www.example.com/" title="http://www.example.com/" target="_blank">http://www.example.com/</a></div><div>--</div></div></body>
</html>
----==_mimepart_55e319872c7db_39d5375468c9486e--
----==_mimepart_55e319872c5bd_39d5375468c94778--
----==_mimepart_55e319872c918_39d5375468c949db--
--EF1A820C3E97.1440946566/mx1.zammad.com--

View file

@ -2023,6 +2023,36 @@ Some Text',
process(files) process(files)
end end
test 'process with bounce check' do
ticket = Ticket.create(
title: 'bounce check',
group: Group.lookup( name: 'Users'),
customer_id: 2,
state: Ticket::State.lookup( name: 'new' ),
priority: Ticket::Priority.lookup( name: '2 normal' ),
updated_by_id: 1,
created_by_id: 1,
)
article = Ticket::Article.create(
ticket_id: ticket.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'some subject',
message_id: '<20150830145601.30.608881@edenhofer.zammad.com>',
body: 'some message article',
internal: false,
sender: Ticket::Article::Sender.where(name: 'Agent').first,
type: Ticket::Article::Type.where(name: 'email').first,
updated_by_id: 1,
created_by_id: 1,
)
email_raw_string = IO.read('test/fixtures/mail33-undelivered-mail-returned-to-sender.box')
ticket_p, article_p, user_p = Channel::EmailParser.new.process( {}, email_raw_string)
assert_equal(ticket_p.id, ticket.id)
end
test 'process with postmaster filter' do test 'process with postmaster filter' do
group1 = Group.create_if_not_exists( group1 = Group.create_if_not_exists(
name: 'Test Group1', name: 'Test Group1',