Improved email loop protection. Remember permanent delivery failed on user to prevent email loops.
This commit is contained in:
parent
2fe0b19a3d
commit
48e3da57ee
9 changed files with 841 additions and 42 deletions
|
@ -0,0 +1,73 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
module Channel::Filter::BounceDeliveryPermanentFailed
|
||||||
|
|
||||||
|
def self.run(_channel, mail)
|
||||||
|
|
||||||
|
return if !mail[:mail_instance]
|
||||||
|
return if !mail[:mail_instance].bounced?
|
||||||
|
return if !mail[:attachments]
|
||||||
|
|
||||||
|
# remember, do not send notifications to certain recipients again if failed permanent
|
||||||
|
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('created_at DESC, id DESC').limit(1).first
|
||||||
|
next if !article
|
||||||
|
|
||||||
|
# check user preferences
|
||||||
|
next if mail[:mail_instance].action != 'failed'
|
||||||
|
next if mail[:mail_instance].retryable? != false
|
||||||
|
next if mail[:mail_instance].error_status != '5.1.1'
|
||||||
|
|
||||||
|
# get recipient of origin article, if only one - mark this user to not sent notifications anymore
|
||||||
|
recipients = []
|
||||||
|
if article.sender.name == 'System' || article.sender.name == 'Agent'
|
||||||
|
%w(to cc).each { |line|
|
||||||
|
next if article[line].blank?
|
||||||
|
recipients = []
|
||||||
|
begin
|
||||||
|
list = Mail::AddressList.new(article[line])
|
||||||
|
list.addresses.each { |address|
|
||||||
|
next if address.address.blank?
|
||||||
|
recipients.push address.address.downcase
|
||||||
|
}
|
||||||
|
rescue
|
||||||
|
Rails.logger.info "Unable to parse email address in '#{article[line]}'"
|
||||||
|
end
|
||||||
|
}
|
||||||
|
if recipients.count > 1
|
||||||
|
recipients = []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# get recipient bounce mail, mark this user to not sent notifications anymore
|
||||||
|
final_recipient = mail[:mail_instance].final_recipient
|
||||||
|
if final_recipient.present?
|
||||||
|
final_recipient.sub!(/rfc822;\s{0,10}/, '')
|
||||||
|
if final_recipient.present?
|
||||||
|
recipients.push final_recipient.downcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# set user preferences
|
||||||
|
recipients.each { |recipient|
|
||||||
|
users = User.where(email: recipient)
|
||||||
|
users.each { |user|
|
||||||
|
next if !user
|
||||||
|
user.preferences[:mail_delivery_failed] = true
|
||||||
|
user.preferences[:mail_delivery_failed_data] = Time.zone.now
|
||||||
|
user.save!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,6 @@
|
||||||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
module Channel::Filter::BounceCheck
|
module Channel::Filter::BounceFollowUpCheck
|
||||||
|
|
||||||
def self.run(_channel, mail)
|
def self.run(_channel, mail)
|
||||||
|
|
||||||
|
@ -13,13 +13,17 @@ module Channel::Filter::BounceCheck
|
||||||
next if !attachment[:preferences]
|
next if !attachment[:preferences]
|
||||||
next if attachment[:preferences]['Mime-Type'] != 'message/rfc822'
|
next if attachment[:preferences]['Mime-Type'] != 'message/rfc822'
|
||||||
next if !attachment[:data]
|
next if !attachment[:data]
|
||||||
|
|
||||||
result = Channel::EmailParser.new.parse(attachment[:data])
|
result = Channel::EmailParser.new.parse(attachment[:data])
|
||||||
next if !result[:message_id]
|
next if !result[:message_id]
|
||||||
message_id_md5 = Digest::MD5.hexdigest(result[:message_id])
|
message_id_md5 = Digest::MD5.hexdigest(result[:message_id])
|
||||||
article = Ticket::Article.where(message_id_md5: message_id_md5).order('created_at DESC, id DESC').limit(1).first
|
article = Ticket::Article.where(message_id_md5: message_id_md5).order('created_at DESC, id DESC').limit(1).first
|
||||||
next if !article
|
next if !article
|
||||||
|
|
||||||
Rails.logger.debug "Follow up for '##{article.ticket.number}' in bounce email."
|
Rails.logger.debug "Follow up for '##{article.ticket.number}' in bounce email."
|
||||||
mail[ 'x-zammad-ticket-id'.to_sym ] = article.ticket_id
|
mail[ 'x-zammad-ticket-id'.to_sym ] = article.ticket_id
|
||||||
|
mail[ 'x-zammad-is-auto-response'.to_sym ] = true
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -813,6 +813,19 @@ perform changes on ticket
|
||||||
recipients_checked = []
|
recipients_checked = []
|
||||||
recipients_raw.each { |recipient_email|
|
recipients_raw.each { |recipient_email|
|
||||||
|
|
||||||
|
skip_user = false
|
||||||
|
users = User.where(email: recipient_email)
|
||||||
|
users.each { |user|
|
||||||
|
next if user.preferences[:mail_delivery_failed] != true
|
||||||
|
next if !user.preferences[:mail_delivery_failed_data]
|
||||||
|
till_blocked = ((user.preferences[:mail_delivery_failed_data] - Time.zone.now - 60.days) / 60 / 60 / 24).round
|
||||||
|
next if till_blocked.positive?
|
||||||
|
logger.info "Send no trigger based notification to #{recipient_email} because email is marked as mail_delivery_failed for #{till_blocked} days"
|
||||||
|
skip_user = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next if skip_user
|
||||||
|
|
||||||
# send notifications only to email adresses
|
# send notifications only to email adresses
|
||||||
next if !recipient_email
|
next if !recipient_email
|
||||||
next if recipient_email !~ /@/
|
next if recipient_email !~ /@/
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
class SettingDeliveryPermanentFailed < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.find_by(name: 'system_init_done')
|
||||||
|
|
||||||
|
setting = Setting.find_by(name: '0900_postmaster_filter_bounce_check')
|
||||||
|
if setting
|
||||||
|
setting.name = '0900_postmaster_filter_bounce_follow_up_check'
|
||||||
|
setting.state = 'Channel::Filter::BounceFollowUpCheck'
|
||||||
|
setting.save!
|
||||||
|
else
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Defines postmaster filter.',
|
||||||
|
name: '0900_postmaster_filter_bounce_follow_up_check',
|
||||||
|
area: 'Postmaster::PreFilter',
|
||||||
|
description: 'Defines postmaster filter to identify postmaster bounced - to handle it as follow-up of the original ticket.',
|
||||||
|
options: {},
|
||||||
|
state: 'Channel::Filter::BounceFollowUpCheck',
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Defines postmaster filter.',
|
||||||
|
name: '0950_postmaster_filter_bounce_delivery_permanent_failed',
|
||||||
|
area: 'Postmaster::PreFilter',
|
||||||
|
description: 'Defines postmaster filter to identify postmaster bounced - disable sending notification on permanent deleivery failed.',
|
||||||
|
options: {},
|
||||||
|
state: 'Channel::Filter::BounceDeliveryPermanentFailed',
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -2327,11 +2327,20 @@ Setting.create_if_not_exists(
|
||||||
)
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Defines postmaster filter.',
|
title: 'Defines postmaster filter.',
|
||||||
name: '0900_postmaster_filter_bounce_check',
|
name: '0900_postmaster_filter_bounce_follow_up_check',
|
||||||
area: 'Postmaster::PreFilter',
|
area: 'Postmaster::PreFilter',
|
||||||
description: 'Defines postmaster filter to identify postmaster bounced - to handle it as follow-up of the original ticket.',
|
description: 'Defines postmaster filter to identify postmaster bounced - to handle it as follow-up of the original ticket.',
|
||||||
options: {},
|
options: {},
|
||||||
state: 'Channel::Filter::BounceCheck',
|
state: 'Channel::Filter::BounceFollowUpCheck',
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Defines postmaster filter.',
|
||||||
|
name: '0950_postmaster_filter_bounce_delivery_permanent_failed',
|
||||||
|
area: 'Postmaster::PreFilter',
|
||||||
|
description: 'Defines postmaster filter to identify postmaster bounced - disable sending notification on permanent deleivery failed.',
|
||||||
|
options: {},
|
||||||
|
state: 'Channel::Filter::BounceDeliveryPermanentFailed',
|
||||||
frontend: false
|
frontend: false
|
||||||
)
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
|
|
230
test/fixtures/mail55.box
vendored
Normal file
230
test/fixtures/mail55.box
vendored
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
Return-Path: <MAILER-DAEMON>
|
||||||
|
Delivered-To: example@zammad.com
|
||||||
|
Received: by mx1.zammad.loc (Postfix)
|
||||||
|
id 738F920A13B2; Fri, 26 May 2017 17:01:45 +0200 (CEST)
|
||||||
|
Date: Fri, 26 May 2017 17:01:45 +0200 (CEST)
|
||||||
|
From: MAILER-DAEMON@mx1.zammad.loc (Mail Delivery System)
|
||||||
|
Subject: Undelivered Mail Returned to Sender
|
||||||
|
To: example@zammad.com
|
||||||
|
Auto-Submitted: auto-replied
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/report; report-type=delivery-status;
|
||||||
|
boundary="207FF20398ED.1495810905/mx1.zammad.loc"
|
||||||
|
Message-Id: <20170526150145.738F920A13B2@mx1.zammad.loc>
|
||||||
|
|
||||||
|
This is a MIME-encapsulated message.
|
||||||
|
|
||||||
|
--207FF20398ED.1495810905/mx1.zammad.loc
|
||||||
|
Content-Description: Notification
|
||||||
|
Content-Type: text/plain; charset=us-ascii
|
||||||
|
|
||||||
|
This is the mail system at host mx1.zammad.loc.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
<ticket-bounce-trigger2@example.com>: host aspmx.l.example.com[108.177.96.26] said:
|
||||||
|
550-5.1.1 The email account that you tried to reach does not exist. Please
|
||||||
|
try 550-5.1.1 double-checking the recipient's email address for typos or
|
||||||
|
550-5.1.1 unnecessary spaces. Learn more at 550 5.1.1
|
||||||
|
https://support.example.com/mail/?p=NoSuchUser l59si1635011edl.281 - gsmtp
|
||||||
|
(in reply to RCPT TO command)
|
||||||
|
|
||||||
|
--207FF20398ED.1495810905/mx1.zammad.loc
|
||||||
|
Content-Description: Delivery report
|
||||||
|
Content-Type: message/delivery-status
|
||||||
|
|
||||||
|
Reporting-MTA: dns; mx1.zammad.loc
|
||||||
|
X-Postfix-Queue-ID: 207FF20398ED
|
||||||
|
X-Postfix-Sender: rfc822; example@zammad.com
|
||||||
|
Arrival-Date: Fri, 26 May 2017 17:01:45 +0200 (CEST)
|
||||||
|
|
||||||
|
Final-Recipient: rfc822; ticket-bounce-trigger2@example.com
|
||||||
|
Original-Recipient: rfc822;ticket-bounce-trigger2@example.com
|
||||||
|
Action: failed
|
||||||
|
Status: 5.1.1
|
||||||
|
Remote-MTA: dns; aspmx.l.example.com
|
||||||
|
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does
|
||||||
|
not exist. Please try 550-5.1.1 double-checking the recipient's email
|
||||||
|
address for typos or 550-5.1.1 unnecessary spaces. Learn more at 550 5.1.1
|
||||||
|
https://support.example.com/mail/?p=NoSuchUser l59si1635011edl.281 - gsmtp
|
||||||
|
|
||||||
|
--207FF20398ED.1495810905/mx1.zammad.loc
|
||||||
|
Content-Description: Undelivered Message
|
||||||
|
Content-Type: message/rfc822
|
||||||
|
|
||||||
|
Return-Path: <example@zammad.com>
|
||||||
|
Received: from apn0000.dc.zammad.com (apn0000.dc.zammad.com [88.0.0.0])
|
||||||
|
by mx1.zammad.loc (Postfix) with ESMTP id 207FF20398ED
|
||||||
|
for <ticket-bounce-trigger2@example.com>; Fri, 26 May 2017 17:01:45 +0200 (CEST)
|
||||||
|
Received: by apn0000.dc.zammad.com (Postfix, from userid 1050)
|
||||||
|
id 08443420973; Fri, 26 May 2017 17:01:45 +0200 (CEST)
|
||||||
|
Date: Fri, 26 May 2017 17:01:45 +0200
|
||||||
|
From: Twelve SaaS GmbH Helpdesk <example@zammad.com>
|
||||||
|
To: ticket-bounce-trigger2@example.com
|
||||||
|
Message-ID: <20170526150141.232.13312@example.zammad.loc>
|
||||||
|
In-Reply-To:
|
||||||
|
References: <20170526150142.232.819805@example.zammad.loc>
|
||||||
|
<20170526150119.6C5E520A13B2@mx1.zammad.loc>
|
||||||
|
<20170526150141.232.799457@example.zammad.loc>
|
||||||
|
<20170526150117.0560820A13B3@mx1.zammad.loc>
|
||||||
|
<20170526150115.232.175460@example.zammad.loc>
|
||||||
|
<20170526150108.232.482766@example.zammad.loc>
|
||||||
|
<20170526150041.F3D2C20A13B3@mx1.zammad.loc>
|
||||||
|
<20170526150036.232.513248@example.zammad.loc>
|
||||||
|
<20170526150008.6AE8A20A13B8@mx1.zammad.loc>
|
||||||
|
<20170526150004.232.103372@example.zammad.loc>
|
||||||
|
<20170526145940.D799220A13B3@mx1.zammad.loc>
|
||||||
|
<20170526145932.232.91897@example.zammad.loc>
|
||||||
|
<20170526145906.8FCA520A13B2@mx1.zammad.loc>
|
||||||
|
<20170526145901.232.269971@example.zammad.loc>
|
||||||
|
<lyHYjBDWwaU5KGbDrCyOfA@notifications.example.com>
|
||||||
|
Subject: =?UTF-8?Q?[Ticket#1705265400361]_RE:_Thanks_for_your_follow_up?=
|
||||||
|
=?UTF-8?Q?_=28G_Suite:_Benachrichtigung_=C3=BCber_Verl=C3=A4ngerung_in_30?=
|
||||||
|
=?UTF-8?Q?_Tagen=29?=
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed;
|
||||||
|
boundary="--==_mimepart_5928435950b1_22d42086504355bc";
|
||||||
|
charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
Organization: Twelve SaaS GmbH
|
||||||
|
X-Loop: yes
|
||||||
|
Precedence: bulk
|
||||||
|
Auto-Submitted: auto-generated
|
||||||
|
X-Auto-Response-Suppress: All
|
||||||
|
X-Powered-By: Zammad - Helpdesk/Support (https://zammad.org/)
|
||||||
|
X-Mailer: Zammad Mail Service
|
||||||
|
|
||||||
|
|
||||||
|
----==_mimepart_5928435950b1_22d42086504355bc
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="--==_mimepart_592843594d35_22d4208650435394";
|
||||||
|
charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
|
||||||
|
----==_mimepart_592843594d35_22d4208650435394
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Your follow up for (Ticket#1705265400361) has been received and will be reviewed by our support staff.
|
||||||
|
|
||||||
|
To provide additional information, please reply to this email or click on the following link:[1] https://example.zammad.loc/#ticket/zoom/232
|
||||||
|
|
||||||
|
Your Twelve SaaS Helpdesk Team
|
||||||
|
|
||||||
|
[2] Zammad, your customer support system
|
||||||
|
|
||||||
|
[1] https://example.zammad.loc/#ticket/zoom/232
|
||||||
|
[2] https://zammad.com
|
||||||
|
----==_mimepart_592843594d35_22d4208650435394
|
||||||
|
Content-Type: multipart/related;
|
||||||
|
boundary="--==_mimepart_592843594e47_22d4208650435484";
|
||||||
|
charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
|
||||||
|
----==_mimepart_592843594e47_22d4208650435484
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
border: none;
|
||||||
|
table-layout: auto;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
table,
|
||||||
|
pre,
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 1px solid hsl(0,0%,87%);
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
tbody tr:nth-child(even) {
|
||||||
|
background: hsl(0,0%,97%);
|
||||||
|
}
|
||||||
|
col {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
border: none;
|
||||||
|
background: hsl(0,0%,97%);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-left: 5px solid #eee;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
background: hsl(0,0%,97%);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="font-family:'Helvetica Neue', Helvetica, Arial, Geneva, sans-serif; font-size: 12px;"><div>Your follow up for <b>(Ticket#1705265400361)</b> has been received and will be reviewed by our support staff.</div>
|
||||||
|
<br>
|
||||||
|
<div>To provide additional information, please reply to this email or click on the following link:
|
||||||
|
<a href="https://example.zammad.loc/#ticket/zoom/232" rel="nofollow noreferrer noopener" target="_blank">https://example.zammad.loc/#ticket/zoom/232</a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div>Your Twelve SaaS Helpdesk Team</div>
|
||||||
|
<br>
|
||||||
|
<div><i><a href="https://zammad.com" rel="nofollow noreferrer noopener" target="_blank">Zammad</a>, your customer support system</i></div></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
----==_mimepart_592843594e47_22d4208650435484--
|
||||||
|
|
||||||
|
----==_mimepart_592843594d35_22d4208650435394--
|
||||||
|
|
||||||
|
----==_mimepart_5928435950b1_22d42086504355bc--
|
||||||
|
|
||||||
|
--207FF20398ED.1495810905/mx1.zammad.loc--
|
220
test/unit/email_process_bounce_delivery_permanent_failed_test.rb
Normal file
220
test/unit/email_process_bounce_delivery_permanent_failed_test.rb
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class EmailProcessBounceDeliveryPermanentFailedTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
|
test 'process with bounce trigger email loop check - article based blocker' do
|
||||||
|
roles = Role.where(name: %w(Customer))
|
||||||
|
customer1 = User.create_or_update(
|
||||||
|
login: 'ticket-bounce-trigger1@example.com',
|
||||||
|
firstname: 'Notification',
|
||||||
|
lastname: 'Customer1',
|
||||||
|
email: 'ticket-bounce-trigger1@example.com',
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
preferences: {},
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
Trigger.create_or_update(
|
||||||
|
name: 'auto reply new ticket',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
|
||||||
|
'recipient' => 'ticket_customer',
|
||||||
|
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
Trigger.create_or_update(
|
||||||
|
name: 'auto reply followup',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
|
||||||
|
'recipient' => 'ticket_customer',
|
||||||
|
'subject' => 'Thanks for your follow up (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket = Ticket.create(
|
||||||
|
title: 'bounce check',
|
||||||
|
group: Group.lookup(name: 'Users'),
|
||||||
|
customer: customer1,
|
||||||
|
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: 'bounce check',
|
||||||
|
message_id: '<20150830145601.30.6088xx@edenhofer.zammad.com>',
|
||||||
|
body: 'some message bounce check',
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
assert_equal('new', ticket.state.name)
|
||||||
|
assert_equal(2, ticket.articles.count)
|
||||||
|
|
||||||
|
article = Ticket::Article.create(
|
||||||
|
ticket_id: ticket.id,
|
||||||
|
from: 'some_sender@example.com',
|
||||||
|
to: customer1.email,
|
||||||
|
subject: 'bounce check 2',
|
||||||
|
message_id: '<20150830145601.30.608881@edenhofer.zammad.com>',
|
||||||
|
body: 'some message bounce check 2',
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
assert_equal(4, ticket.articles.count)
|
||||||
|
|
||||||
|
travel 1.second
|
||||||
|
email_raw_string = IO.binread('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.id, ticket_p.id)
|
||||||
|
assert_equal('open', ticket_p.state.name)
|
||||||
|
assert_equal(5, ticket_p.articles.count)
|
||||||
|
travel_back
|
||||||
|
ticket.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'process with bounce trigger email loop check - bounce based blocker' do
|
||||||
|
roles = Role.where(name: %w(Customer))
|
||||||
|
customer2 = User.create_or_update(
|
||||||
|
login: 'ticket-bounce-trigger2@example.com',
|
||||||
|
firstname: 'Notification',
|
||||||
|
lastname: 'Customer2',
|
||||||
|
email: 'ticket-bounce-trigger2@example.com',
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
preferences: {},
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
Trigger.create_or_update(
|
||||||
|
name: 'auto reply new ticket',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
|
||||||
|
'recipient' => 'ticket_customer',
|
||||||
|
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
Trigger.create_or_update(
|
||||||
|
name: 'auto reply followup',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
|
||||||
|
'recipient' => 'ticket_customer',
|
||||||
|
'subject' => 'Thanks for your follow up (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket = Ticket.create(
|
||||||
|
title: 'bounce check',
|
||||||
|
group: Group.lookup(name: 'Users'),
|
||||||
|
customer: customer2,
|
||||||
|
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: 'bounce check',
|
||||||
|
message_id: '<20150830145601.30.6088xx@edenhofer.zammad.com>',
|
||||||
|
body: 'some message bounce check',
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
assert_equal('new', ticket.state.name)
|
||||||
|
assert_equal(2, ticket.articles.count)
|
||||||
|
|
||||||
|
article = Ticket::Article.create(
|
||||||
|
ticket_id: ticket.id,
|
||||||
|
from: 'some_sender@example.com',
|
||||||
|
to: 'some_recipient@example.com',
|
||||||
|
subject: 'bounce check 2',
|
||||||
|
message_id: '<20170526150141.232.13312@example.zammad.loc>',
|
||||||
|
body: 'some message bounce check 2',
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
assert_equal(4, ticket.articles.count)
|
||||||
|
|
||||||
|
travel 1.second
|
||||||
|
email_raw_string = IO.binread('test/fixtures/mail55.box')
|
||||||
|
ticket_p, article_p, user_p = Channel::EmailParser.new.process({}, email_raw_string)
|
||||||
|
assert_equal(ticket.id, ticket_p.id)
|
||||||
|
assert_equal('open', ticket_p.state.name)
|
||||||
|
assert_equal(5, ticket_p.articles.count)
|
||||||
|
travel_back
|
||||||
|
ticket.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
254
test/unit/email_process_bounce_follow_test.rb
Normal file
254
test/unit/email_process_bounce_follow_test.rb
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class EmailProcessBounceFollowUpTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
|
test 'process with bounce follow up 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: 'bounce check',
|
||||||
|
message_id: '<20150830145601.30.608881@edenhofer.zammad.com>',
|
||||||
|
body: 'some message bounce check',
|
||||||
|
internal: false,
|
||||||
|
sender: Ticket::Article::Sender.where(name: 'Customer').first,
|
||||||
|
type: Ticket::Article::Type.where(name: 'email').first,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
travel 1.second
|
||||||
|
email_raw_string = IO.binread('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.id, ticket_p.id)
|
||||||
|
assert_equal('new', ticket_p.state.name)
|
||||||
|
travel_back
|
||||||
|
ticket.destroy
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'process with bounce trigger email loop check - article based blocker' do
|
||||||
|
roles = Role.where(name: %w(Customer))
|
||||||
|
customer1 = User.create_or_update(
|
||||||
|
login: 'ticket-bounce-trigger1@example.com',
|
||||||
|
firstname: 'Notification',
|
||||||
|
lastname: 'Customer1',
|
||||||
|
email: 'ticket-bounce-trigger1@example.com',
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
preferences: {},
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
Trigger.create_or_update(
|
||||||
|
name: 'auto reply new ticket',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
|
||||||
|
'recipient' => 'ticket_customer',
|
||||||
|
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
Trigger.create_or_update(
|
||||||
|
name: 'auto reply followup',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
|
||||||
|
'recipient' => 'ticket_customer',
|
||||||
|
'subject' => 'Thanks for your follow up (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket = Ticket.create(
|
||||||
|
title: 'bounce check',
|
||||||
|
group: Group.lookup(name: 'Users'),
|
||||||
|
customer: customer1,
|
||||||
|
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: 'bounce check',
|
||||||
|
message_id: '<20150830145601.30.6088xx@edenhofer.zammad.com>',
|
||||||
|
body: 'some message bounce check',
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
assert_equal('new', ticket.state.name)
|
||||||
|
assert_equal(2, ticket.articles.count)
|
||||||
|
|
||||||
|
article = Ticket::Article.create(
|
||||||
|
ticket_id: ticket.id,
|
||||||
|
from: 'some_sender@example.com',
|
||||||
|
to: customer1.email,
|
||||||
|
subject: 'bounce check 2',
|
||||||
|
message_id: '<20150830145601.30.608881@edenhofer.zammad.com>',
|
||||||
|
body: 'some message bounce check 2',
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
assert_equal(4, ticket.articles.count)
|
||||||
|
|
||||||
|
travel 1.second
|
||||||
|
email_raw_string = IO.binread('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.id, ticket_p.id)
|
||||||
|
assert_equal('open', ticket_p.state.name)
|
||||||
|
assert_equal(5, ticket_p.articles.count)
|
||||||
|
travel_back
|
||||||
|
ticket.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'process with bounce trigger email loop check - bounce based blocker' do
|
||||||
|
roles = Role.where(name: %w(Customer))
|
||||||
|
customer2 = User.create_or_update(
|
||||||
|
login: 'ticket-bounce-trigger2@example.com',
|
||||||
|
firstname: 'Notification',
|
||||||
|
lastname: 'Customer2',
|
||||||
|
email: 'ticket-bounce-trigger2@example.com',
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
preferences: {},
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
Trigger.create_or_update(
|
||||||
|
name: 'auto reply new ticket',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
|
||||||
|
'recipient' => 'ticket_customer',
|
||||||
|
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
Trigger.create_or_update(
|
||||||
|
name: 'auto reply followup',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => 'some text<br>#{ticket.customer.lastname}<br>#{ticket.title}<br>#{article.body}',
|
||||||
|
'recipient' => 'ticket_customer',
|
||||||
|
'subject' => 'Thanks for your follow up (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket = Ticket.create(
|
||||||
|
title: 'bounce check',
|
||||||
|
group: Group.lookup(name: 'Users'),
|
||||||
|
customer: customer2,
|
||||||
|
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: 'bounce check',
|
||||||
|
message_id: '<20150830145601.30.6088xx@edenhofer.zammad.com>',
|
||||||
|
body: 'some message bounce check',
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
assert_equal('new', ticket.state.name)
|
||||||
|
assert_equal(2, ticket.articles.count)
|
||||||
|
|
||||||
|
article = Ticket::Article.create(
|
||||||
|
ticket_id: ticket.id,
|
||||||
|
from: 'some_sender@example.com',
|
||||||
|
to: 'some_recipient@example.com',
|
||||||
|
subject: 'bounce check 2',
|
||||||
|
message_id: '<20170526150141.232.13312@example.zammad.loc>',
|
||||||
|
body: 'some message bounce check 2',
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
assert_equal(4, ticket.articles.count)
|
||||||
|
|
||||||
|
travel 1.second
|
||||||
|
email_raw_string = IO.binread('test/fixtures/mail55.box')
|
||||||
|
ticket_p, article_p, user_p = Channel::EmailParser.new.process({}, email_raw_string)
|
||||||
|
assert_equal(ticket.id, ticket_p.id)
|
||||||
|
assert_equal('open', ticket_p.state.name)
|
||||||
|
assert_equal(5, ticket_p.articles.count)
|
||||||
|
travel_back
|
||||||
|
ticket.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1,39 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
require 'test_helper'
|
|
||||||
|
|
||||||
class EmailProcessBounceTest < ActiveSupport::TestCase
|
|
||||||
|
|
||||||
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: 'bounce check',
|
|
||||||
message_id: '<20150830145601.30.608881@edenhofer.zammad.com>',
|
|
||||||
body: 'some message bounce check',
|
|
||||||
internal: false,
|
|
||||||
sender: Ticket::Article::Sender.where(name: 'Customer').first,
|
|
||||||
type: Ticket::Article::Type.where(name: 'email').first,
|
|
||||||
updated_by_id: 1,
|
|
||||||
created_by_id: 1,
|
|
||||||
)
|
|
||||||
travel 1.second
|
|
||||||
email_raw_string = IO.binread('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.id, ticket_p.id)
|
|
||||||
assert_equal('new', ticket_p.state.name)
|
|
||||||
travel_back
|
|
||||||
ticket.destroy
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
Loading…
Reference in a new issue