From 775c4f7d0683d41e5c41b99a9bf03e030ceb4aa0 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sun, 30 Aug 2015 20:16:29 +0200 Subject: [PATCH] Added bounce follow up feature (postmaster bounce emails will now issue an followup of origin ticket). --- app/models/channel/email_parser.rb | 87 +++++---- app/models/channel/filter/bounce_check.rb | 27 +++ app/models/channel/filter/follow_up_check.rb | 14 ++ app/models/ticket/article.rb | 15 +- .../20150830000001_update_message_id_md5.rb | 10 ++ ...33-undelivered-mail-returned-to-sender.box | 168 ++++++++++++++++++ test/unit/email_process_test.rb | 30 ++++ 7 files changed, 314 insertions(+), 37 deletions(-) create mode 100644 app/models/channel/filter/bounce_check.rb create mode 100644 app/models/channel/filter/follow_up_check.rb create mode 100644 db/migrate/20150830000001_update_message_id_md5.rb create mode 100644 test/fixtures/mail33-undelivered-mail-returned-to-sender.box diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb index f5de75dab..fb4199167 100644 --- a/app/models/channel/email_parser.rb +++ b/app/models/channel/email_parser.rb @@ -12,50 +12,50 @@ class Channel::EmailParser mail = parse( msg_as_string ) mail = { - :from => 'Some Name ', - :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 ', - :cc => 'Somebody ', - :subject => 'some message subject', - :body => 'some message body', - :attachments => [ + from: 'Some Name ', + 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 ', + cc: 'Somebody ', + subject: 'some message subject', + body: 'some message body', + attachments: [ { - :data => 'binary of attachment', - :filename => 'file_name_of_attachment.txt', - :preferences => { - :content-alternative => true, - :Mime-Type => 'text/plain', - :Charset => 'iso-8859-1', + data: 'binary of attachment', + filename: 'file_name_of_attachment.txt', + preferences: { + content-alternative: true, + Mime-Type: 'text/plain', + Charset: 'iso-8859-1', }, }, ], # ignore email header - :x-zammad-ignore => 'false', + x-zammad-ignore: 'false', # customer headers - :x-zammad-customer-login => '', - :x-zammad-customer-email => '', - :x-zammad-customer-firstname => '', - :x-zammad-customer-lastname => '', + x-zammad-customer-login: '', + x-zammad-customer-email: '', + x-zammad-customer-firstname: '', + x-zammad-customer-lastname: '', # ticket headers - :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', + 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', # article headers - :x-zammad-article-internal => false, - :x-zammad-article-type => 'agent', - :x-zammad-article-sender => 'customer', + x-zammad-article-internal: false, + x-zammad-article-type: 'agent', + x-zammad-article-sender: 'customer', # all other email headers - :some-header => 'some_value', + some-header: 'some_value', } =end @@ -243,6 +243,9 @@ class Channel::EmailParser data[:body].gsub!( /\r\n/, "\n" ) data[:body].gsub!( /\r/, "\n" ) + # remember original mail instance + data[:mail_instance] = mail + data end @@ -325,19 +328,32 @@ class Channel::EmailParser [attach] end +=begin + + parser = Channel::EmailParser.new + ticket, article, user = parser.process(channel, email_raw_string) + +retrns + + [ticket, article, user] + +=end + def process(channel, msg) mail = parse( msg ) # run postmaster pre filter filters = { '0010' => Channel::Filter::Trusted, + '0100' => Channel::Filter::FollowUpCheck, + '0900' => Channel::Filter::BounceCheck, '1000' => Channel::Filter::Database, } # filter( channel, mail ) filters.each {|_prio, backend| begin - backend.run( channel, mail ) + backend.run(channel, mail) rescue => e Rails.logger.error "can't run postmaster pre filter #{backend}" Rails.logger.error e.inspect @@ -393,8 +409,13 @@ class Channel::EmailParser # set current user UserInfo.current_user_id = user.id - # get ticket# from subject - ticket = Ticket::Number.check( mail[:subject] ) + # get ticket# based on email headers + 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 if ticket diff --git a/app/models/channel/filter/bounce_check.rb b/app/models/channel/filter/bounce_check.rb new file mode 100644 index 000000000..38ad39be6 --- /dev/null +++ b/app/models/channel/filter/bounce_check.rb @@ -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 diff --git a/app/models/channel/filter/follow_up_check.rb b/app/models/channel/filter/follow_up_check.rb new file mode 100644 index 000000000..1d22c7828 --- /dev/null +++ b/app/models/channel/filter/follow_up_check.rb @@ -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 diff --git a/app/models/ticket/article.rb b/app/models/ticket/article.rb index 4f1806977..162dde7eb 100644 --- a/app/models/ticket/article.rb +++ b/app/models/ticket/article.rb @@ -13,8 +13,9 @@ class Ticket::Article < ApplicationModel belongs_to :created_by, class_name: 'User' belongs_to :updated_by, class_name: 'User' store :preferences - before_create :check_subject - before_update :check_subject + before_create :check_subject, :check_message_id_md5 + before_update :check_subject, :check_message_id_md5 + notify_clients_support activity_stream_support ignore_attributes: { @@ -31,13 +32,19 @@ class Ticket::Article < ApplicationModel private + # strip not wanted chars def check_subject - return if !subject - subject.gsub!(/\s|\t|\r/, ' ') 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 end diff --git a/db/migrate/20150830000001_update_message_id_md5.rb b/db/migrate/20150830000001_update_message_id_md5.rb new file mode 100644 index 000000000..01adbe905 --- /dev/null +++ b/db/migrate/20150830000001_update_message_id_md5.rb @@ -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 diff --git a/test/fixtures/mail33-undelivered-mail-returned-to-sender.box b/test/fixtures/mail33-undelivered-mail-returned-to-sender.box new file mode 100644 index 000000000..f877ca01c --- /dev/null +++ b/test/fixtures/mail33-undelivered-mail-returned-to-sender.box @@ -0,0 +1,168 @@ +Return-Path: +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 + +: host arber.znuny.com[88.198.51.81] said: 550 5.1.1 + : 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 : Recipient address + rejected: User unknown + +--EF1A820C3E97.1440946566/mx1.zammad.com +Content-Description: Undelivered Message +Content-Type: message/rfc822 + +Return-Path: +Received: from appnode2.dc.zammad.com (appnode2.dc.zammad.com [144.1.1.1]) + by mx1.zammad.com (Postfix) with ESMTP id 4B9BB20C3E96 + for ; 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 +To: Martin Edenhofer +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 + + + + + + + +
Hello,

is somebody there?

Martin Edenhofer

--
Super Support - Waterford Business Park
5201 Blue Lagoon Drive - 8th Floor & 9th Floor - Miami, 33126 USA
--
+ + +----==_mimepart_55e319872c7db_39d5375468c9486e-- + +----==_mimepart_55e319872c5bd_39d5375468c94778-- + +----==_mimepart_55e319872c918_39d5375468c949db-- + +--EF1A820C3E97.1440946566/mx1.zammad.com-- diff --git a/test/unit/email_process_test.rb b/test/unit/email_process_test.rb index 1312b1597..982b86306 100644 --- a/test/unit/email_process_test.rb +++ b/test/unit/email_process_test.rb @@ -2023,6 +2023,36 @@ Some Text', process(files) 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 group1 = Group.create_if_not_exists( name: 'Test Group1',