Fixed #2399 - Attached images are broken on trigger reply with #{article.body_as_html}
This commit is contained in:
parent
f7a59c6bbd
commit
487f36a5a5
6 changed files with 264 additions and 35 deletions
|
@ -6,41 +6,7 @@ module ClonesTicketArticleAttachments
|
||||||
def article_attachments_clone(article)
|
def article_attachments_clone(article)
|
||||||
raise Exceptions::UnprocessableEntity, 'Need form_id to attach attachments to new form.' if params[:form_id].blank?
|
raise Exceptions::UnprocessableEntity, 'Need form_id to attach attachments to new form.' if params[:form_id].blank?
|
||||||
|
|
||||||
existing_attachments = Store.list(
|
article.clone_attachments('UploadCache', params[:form_id], only_attached_attachments: true)
|
||||||
object: 'UploadCache',
|
|
||||||
o_id: params[:form_id],
|
|
||||||
)
|
|
||||||
attachments = []
|
|
||||||
article.attachments.each do |new_attachment|
|
|
||||||
next if new_attachment.preferences['content-alternative'] == true
|
|
||||||
|
|
||||||
if article.content_type.present? && article.content_type =~ %r{text/html}i
|
|
||||||
next if new_attachment.preferences['content_disposition'].present? && new_attachment.preferences['content_disposition'] !~ /inline/
|
|
||||||
|
|
||||||
if new_attachment.preferences['Content-ID'].present? && article.body.present?
|
|
||||||
next if article.body.match?(/#{Regexp.quote(new_attachment.preferences['Content-ID'])}/i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
already_added = false
|
|
||||||
existing_attachments.each do |existing_attachment|
|
|
||||||
next if existing_attachment.filename != new_attachment.filename || existing_attachment.size != new_attachment.size
|
|
||||||
|
|
||||||
already_added = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
next if already_added == true
|
|
||||||
|
|
||||||
file = Store.add(
|
|
||||||
object: 'UploadCache',
|
|
||||||
o_id: params[:form_id],
|
|
||||||
data: new_attachment.content,
|
|
||||||
filename: new_attachment.filename,
|
|
||||||
preferences: new_attachment.preferences,
|
|
||||||
)
|
|
||||||
attachments.push file
|
|
||||||
end
|
|
||||||
|
|
||||||
attachments
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1486,6 +1486,12 @@ result
|
||||||
preferences: attachment[:preferences],
|
preferences: attachment[:preferences],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
original_article = objects[:article]
|
||||||
|
if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
|
||||||
|
original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
|
||||||
|
original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def sms_recipients_by_type(recipient_type, article)
|
def sms_recipients_by_type(recipient_type, article)
|
||||||
|
|
|
@ -44,6 +44,9 @@ class Ticket::Article < ApplicationModel
|
||||||
:to,
|
:to,
|
||||||
:cc
|
:cc
|
||||||
|
|
||||||
|
attr_accessor :should_clone_inline_attachments
|
||||||
|
alias should_clone_inline_attachments? should_clone_inline_attachments
|
||||||
|
|
||||||
# fillup md5 of message id to search easier on very long message ids
|
# fillup md5 of message id to search easier on very long message ids
|
||||||
def check_message_id_md5
|
def check_message_id_md5
|
||||||
return true if message_id.blank?
|
return true if message_id.blank?
|
||||||
|
@ -130,6 +133,77 @@ returns
|
||||||
new_attachments
|
new_attachments
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
clone existing attachments of article to the target object
|
||||||
|
|
||||||
|
article_parent = Ticket::Article.find(123)
|
||||||
|
article_new = Ticket::Article.find(456)
|
||||||
|
|
||||||
|
attached_attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_attached_attachments: true)
|
||||||
|
|
||||||
|
inline_attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_inline_attachments: true)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
[attachment1, attachment2, ...]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def clone_attachments(object_type, object_id, options = {})
|
||||||
|
existing_attachments = Store.list(
|
||||||
|
object: object_type,
|
||||||
|
o_id: object_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_html_content = false
|
||||||
|
if content_type.present? && content_type =~ %r{text/html}i
|
||||||
|
is_html_content = true
|
||||||
|
end
|
||||||
|
|
||||||
|
new_attachments = []
|
||||||
|
attachments.each do |new_attachment|
|
||||||
|
next if new_attachment.preferences['content-alternative'] == true
|
||||||
|
|
||||||
|
# only_attached_attachments mode is used by apply attached attachments to forwared article
|
||||||
|
if options[:only_attached_attachments] == true
|
||||||
|
if is_html_content
|
||||||
|
next if new_attachment.preferences['content_disposition'].present? && new_attachment.preferences['content_disposition'] =~ /inline/
|
||||||
|
next if new_attachment.preferences['Content-ID'].blank?
|
||||||
|
next if body.present? && body.match?(/#{Regexp.quote(new_attachment.preferences['Content-ID'])}/i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# only_inline_attachments mode is used when quoting HTML mail with #{article.body_as_html}
|
||||||
|
if options[:only_inline_attachments] == true
|
||||||
|
next if is_html_content == false
|
||||||
|
next if body.blank?
|
||||||
|
next if new_attachment.preferences['content_disposition'].present? && new_attachment.preferences['content_disposition'] !~ /inline/
|
||||||
|
next if new_attachment.preferences['Content-ID'].present? && !body.match?(/#{Regexp.quote(new_attachment.preferences['Content-ID'])}/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
already_added = false
|
||||||
|
existing_attachments.each do |existing_attachment|
|
||||||
|
next if existing_attachment.filename != new_attachment.filename || existing_attachment.size != new_attachment.size
|
||||||
|
|
||||||
|
already_added = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
next if already_added == true
|
||||||
|
|
||||||
|
file = Store.add(
|
||||||
|
object: object_type,
|
||||||
|
o_id: object_id,
|
||||||
|
data: new_attachment.content,
|
||||||
|
filename: new_attachment.filename,
|
||||||
|
preferences: new_attachment.preferences,
|
||||||
|
)
|
||||||
|
new_attachments.push file
|
||||||
|
end
|
||||||
|
|
||||||
|
new_attachments
|
||||||
|
end
|
||||||
|
|
||||||
def self.last_customer_agent_article(ticket_id)
|
def self.last_customer_agent_article(ticket_id)
|
||||||
sender = Ticket::Article::Sender.lookup(name: 'System')
|
sender = Ticket::Article::Sender.lookup(name: 'System')
|
||||||
Ticket::Article.where('ticket_id = ? AND sender_id NOT IN (?)', ticket_id, sender.id).order('created_at DESC').first
|
Ticket::Article.where('ticket_id = ? AND sender_id NOT IN (?)', ticket_id, sender.id).order('created_at DESC').first
|
||||||
|
|
|
@ -126,6 +126,11 @@ examples how to use
|
||||||
begin
|
begin
|
||||||
previous_object_refs = object_refs
|
previous_object_refs = object_refs
|
||||||
object_refs = object_refs.send(method.to_sym, *arguments)
|
object_refs = object_refs.send(method.to_sym, *arguments)
|
||||||
|
|
||||||
|
# body_as_html should trigger the cloning of all inline attachments from the parent article (issue #2399)
|
||||||
|
if method.to_sym == :body_as_html && previous_object_refs.respond_to?(:should_clone_inline_attachments)
|
||||||
|
previous_object_refs.should_clone_inline_attachments = true
|
||||||
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
value = "\#{#{object_name}.#{object_methods_s} / #{e.message}}"
|
value = "\#{#{object_name}.#{object_methods_s} / #{e.message}}"
|
||||||
break
|
break
|
||||||
|
|
|
@ -124,4 +124,95 @@ RSpec.describe Ticket::Article, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'clone attachments' do
|
||||||
|
context 'of forwarded article' do
|
||||||
|
context 'via email' do
|
||||||
|
|
||||||
|
it 'only need to clone attached attachments' do
|
||||||
|
article_parent = create(:ticket_article,
|
||||||
|
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||||
|
content_type: 'text/html',
|
||||||
|
body: '<img src="cid:15.274327094.140938@zammad.example.com"> some text',)
|
||||||
|
Store.add(
|
||||||
|
object: 'Ticket::Article',
|
||||||
|
o_id: article_parent.id,
|
||||||
|
data: 'content_file1_normally_should_be_an_image',
|
||||||
|
filename: 'some_file1.jpg',
|
||||||
|
preferences: {
|
||||||
|
'Content-Type' => 'image/jpeg',
|
||||||
|
'Mime-Type' => 'image/jpeg',
|
||||||
|
'Content-ID' => '15.274327094.140938@zammad.example.com',
|
||||||
|
'Content-Disposition' => 'inline',
|
||||||
|
},
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
Store.add(
|
||||||
|
object: 'Ticket::Article',
|
||||||
|
o_id: article_parent.id,
|
||||||
|
data: 'content_file2_normally_should_be_an_image',
|
||||||
|
filename: 'some_file2.jpg',
|
||||||
|
preferences: {
|
||||||
|
'Content-Type' => 'image/jpeg',
|
||||||
|
'Mime-Type' => 'image/jpeg',
|
||||||
|
'Content-ID' => '15.274327094.140938_not_reffered@zammad.example.com',
|
||||||
|
'Content-Disposition' => 'inline',
|
||||||
|
},
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
article_new = create(:ticket_article)
|
||||||
|
UserInfo.current_user_id = 1
|
||||||
|
|
||||||
|
attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_attached_attachments: true)
|
||||||
|
|
||||||
|
expect(attachments.count).to eq(1)
|
||||||
|
expect(attachments[0].filename).to eq('some_file2.jpg')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'of trigger' do
|
||||||
|
context 'via email notifications' do
|
||||||
|
it 'only need to clone inline attachments used in body' do
|
||||||
|
article_parent = create(:ticket_article,
|
||||||
|
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||||
|
content_type: 'text/html',
|
||||||
|
body: '<img src="cid:15.274327094.140938@zammad.example.com"> some text',)
|
||||||
|
Store.add(
|
||||||
|
object: 'Ticket::Article',
|
||||||
|
o_id: article_parent.id,
|
||||||
|
data: 'content_file1_normally_should_be_an_image',
|
||||||
|
filename: 'some_file1.jpg',
|
||||||
|
preferences: {
|
||||||
|
'Content-Type' => 'image/jpeg',
|
||||||
|
'Mime-Type' => 'image/jpeg',
|
||||||
|
'Content-ID' => '15.274327094.140938@zammad.example.com',
|
||||||
|
'Content-Disposition' => 'inline',
|
||||||
|
},
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
Store.add(
|
||||||
|
object: 'Ticket::Article',
|
||||||
|
o_id: article_parent.id,
|
||||||
|
data: 'content_file2_normally_should_be_an_image',
|
||||||
|
filename: 'some_file2.jpg',
|
||||||
|
preferences: {
|
||||||
|
'Content-Type' => 'image/jpeg',
|
||||||
|
'Mime-Type' => 'image/jpeg',
|
||||||
|
'Content-ID' => '15.274327094.140938_not_reffered@zammad.example.com',
|
||||||
|
'Content-Disposition' => 'inline',
|
||||||
|
},
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
article_new = create(:ticket_article)
|
||||||
|
UserInfo.current_user_id = 1
|
||||||
|
|
||||||
|
attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_inline_attachments: true )
|
||||||
|
|
||||||
|
expect(attachments.count).to eq(1)
|
||||||
|
expect(attachments[0].filename).to eq('some_file1.jpg')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4596,4 +4596,91 @@ class TicketTriggerTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#2399 - Attached images are broken on trigger reply with #{article.body_as_html}
|
||||||
|
test 'make sure auto reply using #{article.body_as_html} copies all articles image attachments as well' do
|
||||||
|
# make sure that this auto reply trigger only reacts to this particular test in order not to interfer with other auto reply tests
|
||||||
|
trigger1 = Trigger.create!(
|
||||||
|
name: 'auto reply with HTML quote',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'create',
|
||||||
|
},
|
||||||
|
'ticket.state_id' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => Ticket::State.lookup(name: 'new').id.to_s,
|
||||||
|
},
|
||||||
|
'ticket.title' => {
|
||||||
|
'operator' => 'contains',
|
||||||
|
'value' => 'AW: OTRS / Anfrage OTRS Einführung/Präsentation [Ticket#11545]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => '#{article.body_as_html}',
|
||||||
|
'recipient' => 'article_last_sender',
|
||||||
|
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket1, article1, user, mail = Channel::EmailParser.new.process({}, File.read(Rails.root.join('test', 'data', 'mail', 'mail048.box')))
|
||||||
|
|
||||||
|
assert_equal('AW: OTRS / Anfrage OTRS Einführung/Präsentation [Ticket#11545]', ticket1.title, 'ticket1.title verify')
|
||||||
|
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
|
||||||
|
assert_equal(2, ticket1.articles.first.attachments.count)
|
||||||
|
|
||||||
|
article1 = ticket1.articles.last
|
||||||
|
assert_match('Thanks for your inquiry (AW: OTRS / Anfrage OTRS Einführung/Präsentation [Ticket#11545])!', article1.subject)
|
||||||
|
assert_equal(1, article1.attachments.count)
|
||||||
|
assert_equal('50606', article1.attachments[0].size)
|
||||||
|
assert_equal('CPG-Reklamationsmitteilung bezügl.01234567895 an Voda-28.03.2017.jpg', article1.attachments[0].filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
#2399 - Attached images are broken on trigger reply with #{article.body_as_html}
|
||||||
|
test 'make sure auto reply using #{article.body_as_html} does not copy any non-image attachments' do
|
||||||
|
# make sure that this auto reply trigger only reacts to this particular test in order not to interfer with other auto reply tests
|
||||||
|
trigger1 = Trigger.create!(
|
||||||
|
name: 'auto reply with HTML quote',
|
||||||
|
condition: {
|
||||||
|
'ticket.action' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => 'create',
|
||||||
|
},
|
||||||
|
'ticket.state_id' => {
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => Ticket::State.lookup(name: 'new').id.to_s,
|
||||||
|
},
|
||||||
|
'ticket.title' => {
|
||||||
|
'operator' => 'contains',
|
||||||
|
'value' => 'Online-apotheke. Günstigster Preis. Ohne Rezepte',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perform: {
|
||||||
|
'notification.email' => {
|
||||||
|
'body' => '#{article.body_as_html}',
|
||||||
|
'recipient' => 'article_last_sender',
|
||||||
|
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disable_notification: true,
|
||||||
|
active: true,
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket1, article1, user, mail = Channel::EmailParser.new.process({}, File.read(Rails.root.join('test', 'data', 'mail', 'mail069.box')))
|
||||||
|
|
||||||
|
assert_equal('Online-apotheke. Günstigster Preis. Ohne Rezepte', ticket1.title, 'ticket1.title verify')
|
||||||
|
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
|
||||||
|
assert_equal(1, ticket1.articles.first.attachments.count)
|
||||||
|
|
||||||
|
article1 = ticket1.articles.last
|
||||||
|
assert_match('Thanks for your inquiry (Online-apotheke. Günstigster Preis. Ohne Rezepte)!', article1.subject)
|
||||||
|
assert_equal(0, article1.attachments.count)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue