trabajo-afectivo/spec/models/channel/email_parser_spec.rb

1566 lines
56 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
RSpec.describe Channel::EmailParser, type: :model do
describe '#parse' do
# regression test for issue 2390 - Add a postmaster filter to not show emails with potential issue
describe 'handling HTML links in message content' do
context 'with under 5,000 links' do
it 'parses message content as normal' do
expect(described_class.new.parse(<<~RAW)[:body]).to start_with('<a href="https://zammad.com/"')
From: nicole.braun@zammad.com
Content-Type: text/html
<html><body>
#{Array.new(10) { '<a href="https://zammad.com/">Dummy Link</a>' }.join(' ')}
</body></html>
RAW
end
end
context 'with 5,000+ links' do
it 'replaces message content with error message' do
expect(described_class.new.parse(<<~RAW)).to include('body' => Channel::EmailParser::EXCESSIVE_LINKS_MSG)
From: nicole.braun@zammad.com
Content-Type: text/html
<html><body>
#{Array.new(5001) { '<a href="https://zammad.com/">Dummy Link</a>' }.join(' ')}
</body></html>
RAW
end
end
end
describe 'handling Japanese email in ISO-2022-JP encoding' do
let(:mail_file) { Rails.root.join('test/data/mail/mail091.box') }
let(:raw_mail) { File.read(mail_file) }
let(:parsed) { described_class.new.parse(raw_mail) }
it { expect(parsed['body']).to eq '<div>このアドレスへのメルマガを解除してください。</div>' }
it { expect(parsed['subject']).to eq 'メルマガ解除' }
end
end
describe '#process' do
let(:raw_mail) { File.read(mail_file) }
before { Trigger.destroy_all } # triggers may cause additional articles to be created
describe 'auto-creating new users' do
context 'with one unrecognized email address' do
it 'creates one new user' do
expect { described_class.new.process({}, <<~RAW) }.to change(User, :count).by(1)
From: #{Faker::Internet.unique.email}
RAW
end
end
context 'with a large number of unrecognized recipient addresses' do
it 'never creates more than 40 users' do
expect { described_class.new.process({}, <<~RAW) }.to change(User, :count).by(40)
From: nicole.braun@zammad.org
To: #{Array.new(20) { Faker::Internet.unique.email }.join(', ')}
Cc: #{Array.new(21) { Faker::Internet.unique.email }.join(', ')}
RAW
end
end
end
describe 'auto-updating existing users' do
context 'with a previous email with no real name in the From: header' do
let!(:customer) { described_class.new.process({}, previous_email).first.customer }
let(:previous_email) { <<~RAW.chomp }
From: customer@example.com
To: myzammad@example.com
Subject: test sender name update 1
Some Text
RAW
context 'and a new email with a real name in the From: header' do
let(:new_email) { <<~RAW.chomp }
From: Max Smith <customer@example.com>
To: myzammad@example.com
Subject: test sender name update 2
Some Text
RAW
it 'updates the customers #firstname and #lastname' do
expect { described_class.new.process({}, new_email) }
.to change { customer.reload.firstname }.from('').to('Max')
.and change { customer.reload.lastname }.from('').to('Smith')
end
end
end
end
describe 'creating new tickets' do
context 'when subject contains no ticket reference' do
let(:raw_mail) { <<~RAW.chomp }
From: foo@bar.com
To: baz@qux.net
Subject: Foo
Lorem ipsum dolor
RAW
it 'creates a ticket and article' do
expect { described_class.new.process({}, raw_mail) }
.to change(Ticket, :count).by(1)
.and change(Ticket::Article, :count).by_at_least(1)
end
it 'sets #title to email subject' do
described_class.new.process({}, raw_mail)
expect(Ticket.last.title).to eq('Foo')
end
it 'sets #state to "new"' do
described_class.new.process({}, raw_mail)
expect(Ticket.last.state.name).to eq('new')
end
context 'when no channel is given but a group with the :to address exists' do
let!(:email_address) { create(:email_address, email: 'baz@qux.net', channel: nil) }
let!(:group) { create(:group, name: 'baz headquarter', email_address: email_address) }
let!(:channel) do
channel = create(:email_channel, group: group)
email_address.update(channel: channel)
channel
end
it 'sets the group based on the :to field' do
described_class.new.process({}, raw_mail)
expect(Ticket.last.group.id).to eq(group.id)
end
end
context 'when from address matches an existing agent' do
let!(:agent) { create(:agent, email: 'foo@bar.com') }
it 'sets article.sender to "Agent"' do
described_class.new.process({}, raw_mail)
expect(Ticket::Article.last.sender.name).to eq('Agent')
end
it 'sets ticket.state to "new"' do
described_class.new.process({}, raw_mail)
expect(Ticket.last.state.name).to eq('new')
end
end
context 'when from address matches an existing agent customer' do
let!(:agent_customer) { create(:agent_and_customer, email: 'foo@bar.com') }
let!(:ticket) { create(:ticket, customer: agent_customer) }
let!(:raw_email) { <<~RAW.chomp }
From: foo@bar.com
To: myzammad@example.com
Subject: [#{Setting.get('ticket_hook') + Setting.get('ticket_hook_divider') + ticket.number}] test
Lorem ipsum dolor
RAW
it 'sets article.sender to "Customer"' do
described_class.new.process({}, raw_email)
expect(Ticket::Article.last.sender.name).to eq('Customer')
end
end
context 'when from address matches an existing customer' do
let!(:customer) { create(:customer, email: 'foo@bar.com') }
it 'sets article.sender to "Customer"' do
described_class.new.process({}, raw_mail)
expect(Ticket.last.articles.first.sender.name).to eq('Customer')
end
it 'sets ticket.state to "new"' do
described_class.new.process({}, raw_mail)
expect(Ticket.last.state.name).to eq('new')
end
end
context 'when from address is unrecognized' do
it 'sets article.sender to "Customer"' do
described_class.new.process({}, raw_mail)
expect(Ticket.last.articles.first.sender.name).to eq('Customer')
end
end
end
context 'when email contains x-headers' do
let(:raw_mail) { <<~RAW.chomp }
From: foo@bar.com
To: baz@qux.net
Subject: Foo
X-Zammad-Ticket-priority: 3 high
Lorem ipsum dolor
RAW
context 'when channel is not trusted' do
let(:channel) { create(:channel, options: { inbound: { trusted: false } }) }
it 'does not change the priority of the ticket (no channel)' do
described_class.new.process({}, raw_mail)
expect(Ticket.last.priority.name).to eq('2 normal')
end
it 'does not change the priority of the ticket (untrusted)' do
described_class.new.process(channel, raw_mail)
expect(Ticket.last.priority.name).to eq('2 normal')
end
end
context 'when channel is trusted' do
let(:channel) { create(:channel, options: { inbound: { trusted: true } }) }
it 'does not change the priority of the ticket' do
described_class.new.process(channel, raw_mail)
expect(Ticket.last.priority.name).to eq('3 high')
end
end
end
context 'Mentions:' do
let(:agent) { create(:agent) }
let(:raw_mail) { <<~RAW.chomp }
From: foo@bar.com
To: baz@qux.net
Subject: Foo
Lorem ipsum dolor <a data-mention-user-id=\"#{agent.id}\">agent</a>
RAW
it 'creates a ticket and article without mentions and no exception raised' do
expect { described_class.new.process({}, raw_mail) }
.to change(Ticket, :count).by(1)
.and change(Ticket::Article, :count).by_at_least(1)
.and not_change(Mention, :count)
end
end
end
describe 'associating emails to existing tickets' do
let!(:ticket) { create(:ticket) }
let(:ticket_ref) { Setting.get('ticket_hook') + Setting.get('ticket_hook_divider') + ticket.number }
describe 'based on where a ticket reference appears in the message' do
shared_context 'ticket reference in subject' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: #{ticket_ref}
Lorem ipsum dolor
RAW
end
shared_context 'ticket reference in body' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
Lorem ipsum dolor #{ticket_ref}
RAW
end
shared_context 'ticket reference in body (text/html)' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
Content-Transfer-Encoding: 7bit
Content-Type: text/html;
<b>Lorem ipsum dolor #{ticket_ref}</b>
RAW
end
shared_context 'ticket reference in text/plain attachment' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
Content-Type: multipart/mixed; boundary="Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2"
Subject: no reference
Date: Sun, 30 Aug 2015 23:20:54 +0200
To: Martin Edenhofer <me@znuny.com>
Mime-Version: 1.0 (Mac OS X Mail 8.2 \(2104\))
X-Mailer: Apple Mail (2.2104)
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=us-ascii
no reference
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
Content-Disposition: attachment;
filename=test1.txt
Content-Type: text/plain;
name="test.txt"
Content-Transfer-Encoding: 7bit
Some Text #{ticket_ref}
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2--
RAW
end
shared_context 'ticket reference in text/html (as content) attachment' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
Content-Type: multipart/mixed; boundary="Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2"
Subject: no reference
Date: Sun, 30 Aug 2015 23:20:54 +0200
To: Martin Edenhofer <me@znuny.com>
Mime-Version: 1.0 (Mac OS X Mail 8.2 \(2104\))
X-Mailer: Apple Mail (2.2104)
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=us-ascii
no reference
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
Content-Disposition: attachment;
filename=test1.txt
Content-Type: text/html;
name="test.txt"
Content-Transfer-Encoding: 7bit
<div>Some Text #{ticket_ref}</div>
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2--
RAW
end
shared_context 'ticket reference in text/html (attribute) attachment' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
Content-Type: multipart/mixed; boundary="Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2"
Subject: no reference
Date: Sun, 30 Aug 2015 23:20:54 +0200
To: Martin Edenhofer <me@znuny.com>
Mime-Version: 1.0 (Mac OS X Mail 8.2 \(2104\))
X-Mailer: Apple Mail (2.2104)
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=us-ascii
no reference
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
Content-Disposition: attachment;
filename=test1.txt
Content-Type: text/html;
name="test.txt"
Content-Transfer-Encoding: 7bit
<div>Some Text <b data-something="#{ticket_ref}">some text</b></div>
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2--
RAW
end
shared_context 'ticket reference in image/jpg attachment' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
Content-Type: multipart/mixed; boundary="Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2"
Subject: no reference
Date: Sun, 30 Aug 2015 23:20:54 +0200
To: Martin Edenhofer <me@znuny.com>
Mime-Version: 1.0 (Mac OS X Mail 8.2 \(2104\))
X-Mailer: Apple Mail (2.2104)
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=us-ascii
no reference
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2
Content-Disposition: attachment;
filename=test1.jpg
Content-Type: image/jpg;
name="test.jpg"
Content-Transfer-Encoding: 7bit
Some Text #{ticket_ref}
--Apple-Mail=_ED77AC8D-FB6F-40E5-8FBE-D41FF5E1BAF2--
RAW
end
shared_context 'ticket reference in In-Reply-To header' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
In-Reply-To: #{article.message_id}
Lorem ipsum dolor
RAW
let!(:article) { create(:ticket_article, ticket: ticket, message_id: '<20150830145601.30.608882@edenhofer.zammad.com>') }
end
shared_context 'ticket reference in References header' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
References: <DA918CD1-BE9A-4262-ACF6-5001E59291B6@znuny.com> #{article.message_id} <DA918CD1-BE9A-4262-ACF6-5001E59291XX@znuny.com>
Lorem ipsum dolor
RAW
let!(:article) { create(:ticket_article, ticket: ticket, message_id: '<20150830145601.30.608882@edenhofer.zammad.com>') }
end
shared_examples 'adds message to ticket' do
it 'adds message to ticket' do
expect { described_class.new.process({}, raw_mail) }
.to change { ticket.articles.length }.by(1)
end
end
shared_examples 'creates a new ticket' do
it 'creates a new ticket' do
expect { described_class.new.process({}, raw_mail) }
.to change(Ticket, :count).by(1)
.and not_change { ticket.articles.length }
end
end
context 'when not explicitly configured to search anywhere' do
before { Setting.set('postmaster_follow_up_search_in', nil) }
context 'when subject contains ticket reference' do
include_context 'ticket reference in subject'
include_examples 'adds message to ticket'
context 'alongside other, invalid ticket references' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: [#{Setting.get('ticket_hook') + Setting.get('ticket_hook_divider') + Ticket::Number.generate}] #{ticket_ref}
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
context 'and ticket is closed' do
before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
include_examples 'adds message to ticket'
end
context 'but ticket groups #follow_up_possible attribute is "new_ticket"' do
before { ticket.group.update(follow_up_possible: 'new_ticket') }
context 'and ticket is open' do
include_examples 'adds message to ticket'
end
context 'and ticket is closed' do
before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
include_examples 'creates a new ticket'
end
context 'and ticket is merged' do
before { ticket.update(state: Ticket::State.find_by(name: 'merged')) }
include_examples 'creates a new ticket'
end
context 'and ticket is removed' do
before { ticket.update(state: Ticket::State.find_by(name: 'removed')) }
include_examples 'creates a new ticket'
end
end
context 'and "ticket_hook" setting is non-default value' do
before { Setting.set('ticket_hook', 'VD-Ticket#') }
include_examples 'adds message to ticket'
end
end
context 'when body contains ticket reference' do
include_context 'ticket reference in body'
include_examples 'creates a new ticket'
end
context 'when text/plain attachment contains ticket reference' do
include_context 'ticket reference in text/plain attachment'
include_examples 'creates a new ticket'
end
context 'when text/html attachment (as content) contains ticket reference' do
include_context 'ticket reference in text/html (as content) attachment'
include_examples 'creates a new ticket'
end
context 'when text/html attachment (attribute) contains ticket reference' do
include_context 'ticket reference in text/html (attribute) attachment'
include_examples 'creates a new ticket'
end
context 'when image/jpg attachment contains ticket reference' do
include_context 'ticket reference in image/jpg attachment'
include_examples 'creates a new ticket'
end
context 'when In-Reply-To header contains article message-id' do
include_context 'ticket reference in In-Reply-To header'
include_examples 'creates a new ticket'
context 'and subject matches article subject' do
let(:raw_mail) { <<~RAW.chomp }
From: customer@example.com
To: me@example.com
Subject: AW: RE: #{article.subject}
In-Reply-To: #{article.message_id}
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
context 'and "ticket_hook_position" setting is "none"' do
before { Setting.set('ticket_hook_position', 'none') }
let(:raw_mail) { <<~RAW.chomp }
From: customer@example.com
To: me@example.com
Subject: RE: Foo bar
In-Reply-To: #{article.message_id}
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
end
context 'when References header contains article message-id' do
include_context 'ticket reference in References header'
include_examples 'creates a new ticket'
context 'and Auto-Submitted header reads "auto-replied"' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
References: #{article.message_id}
Auto-Submitted: auto-replied
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
context 'and subject matches article subject' do
let(:raw_mail) { <<~RAW.chomp }
From: customer@example.com
To: me@example.com
Subject: AW: RE: #{article.subject}
References: #{article.message_id}
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
context 'and "ticket_hook_position" setting is "none"' do
before { Setting.set('ticket_hook_position', 'none') }
let(:raw_mail) { <<~RAW.chomp }
From: customer@example.com
To: me@example.com
Subject: RE: Foo bar
References: #{article.message_id}
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
end
end
context 'when configured to search body' do
before { Setting.set('postmaster_follow_up_search_in', 'body') }
context 'when subject contains ticket reference' do
include_context 'ticket reference in subject'
include_examples 'adds message to ticket'
end
context 'when body contains ticket reference' do
context 'in visible text' do
include_context 'ticket reference in body'
include_examples 'adds message to ticket'
end
context 'in visible text with a linebreak' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
Lorem ipsum dolor #{ticket_ref}
consetetur sadipscing elitr
sed diam nonumy eirmod
RAW
include_examples 'adds message to ticket'
end
context 'as part of a larger word' do
let(:ticket_ref) { "Foo#{Setting.get('ticket_hook')}#{Setting.get('ticket_hook_divider')}#{ticket.number}bar" }
include_context 'ticket reference in body'
include_examples 'creates a new ticket'
end
context 'between html tags' do
include_context 'ticket reference in body (text/html)'
include_examples 'adds message to ticket'
end
context 'in html attributes' do
let(:ticket_ref) { %(<table bgcolor="#{Setting.get('ticket_hook')}#{Setting.get('ticket_hook_divider')}#{ticket.number}"> </table>) }
include_context 'ticket reference in body (text/html)'
include_examples 'creates a new ticket'
end
end
context 'when text/plain attachment contains ticket reference' do
include_context 'ticket reference in text/plain attachment'
include_examples 'creates a new ticket'
end
context 'when text/html attachment (as content) contains ticket reference' do
include_context 'ticket reference in text/html (as content) attachment'
include_examples 'creates a new ticket'
end
context 'when text/html attachment (attribute) contains ticket reference' do
include_context 'ticket reference in text/html (attribute) attachment'
include_examples 'creates a new ticket'
end
context 'when image/jpg attachment contains ticket reference' do
include_context 'ticket reference in image/jpg attachment'
include_examples 'creates a new ticket'
end
context 'when In-Reply-To header contains article message-id' do
include_context 'ticket reference in In-Reply-To header'
include_examples 'creates a new ticket'
context 'and Auto-Submitted header reads "auto-replied"' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
References: #{article.message_id}
Auto-Submitted: auto-replied
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
end
context 'when References header contains article message-id' do
include_context 'ticket reference in References header'
include_examples 'creates a new ticket'
end
end
context 'when configured to search attachments' do
before { Setting.set('postmaster_follow_up_search_in', 'attachment') }
context 'when subject contains ticket reference' do
include_context 'ticket reference in subject'
include_examples 'adds message to ticket'
end
context 'when body contains ticket reference' do
include_context 'ticket reference in body'
include_examples 'creates a new ticket'
end
context 'when text/plain attachment contains ticket reference' do
include_context 'ticket reference in text/plain attachment'
include_examples 'adds message to ticket'
end
context 'when text/html attachment (as content) contains ticket reference' do
include_context 'ticket reference in text/html (as content) attachment'
include_examples 'adds message to ticket'
end
context 'when text/html attachment (attribute) contains ticket reference' do
include_context 'ticket reference in text/html (attribute) attachment'
include_examples 'creates a new ticket'
end
context 'when image/jpg attachment contains ticket reference' do
include_context 'ticket reference in image/jpg attachment'
include_examples 'creates a new ticket'
end
context 'when In-Reply-To header contains article message-id' do
include_context 'ticket reference in In-Reply-To header'
include_examples 'creates a new ticket'
end
context 'when References header contains article message-id' do
include_context 'ticket reference in References header'
include_examples 'creates a new ticket'
context 'and Auto-Submitted header reads "auto-replied"' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
References: #{article.message_id}
Auto-Submitted: auto-replied
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
end
end
context 'when configured to search headers' do
before { Setting.set('postmaster_follow_up_search_in', 'references') }
context 'when subject contains ticket reference' do
include_context 'ticket reference in subject'
include_examples 'adds message to ticket'
end
context 'when body contains ticket reference' do
include_context 'ticket reference in body'
include_examples 'creates a new ticket'
end
context 'when text/plain attachment contains ticket reference' do
include_context 'ticket reference in text/plain attachment'
include_examples 'creates a new ticket'
end
context 'when text/html attachment (as content) contains ticket reference' do
include_context 'ticket reference in text/html (as content) attachment'
include_examples 'creates a new ticket'
end
context 'when text/html attachment (attribute) contains ticket reference' do
include_context 'ticket reference in text/html (attribute) attachment'
include_examples 'creates a new ticket'
end
context 'when image/jpg attachment contains ticket reference' do
include_context 'ticket reference in image/jpg attachment'
include_examples 'creates a new ticket'
end
context 'when In-Reply-To header contains article message-id' do
include_context 'ticket reference in In-Reply-To header'
include_examples 'adds message to ticket'
end
context 'when References header contains article message-id' do
include_context 'ticket reference in References header'
include_examples 'adds message to ticket'
context 'that matches two separate tickets' do
let!(:newer_ticket) { create(:ticket) }
let!(:newer_article) { create(:ticket_article, ticket: newer_ticket, message_id: article.message_id) }
it 'returns more recently created ticket' do
expect(described_class.new.process({}, raw_mail).first).to eq(newer_ticket)
end
it 'adds message to more recently created ticket' do
expect { described_class.new.process({}, raw_mail) }
.to change { newer_ticket.articles.count }.by(1)
.and not_change { ticket.articles.count }
end
end
context 'and Auto-Submitted header reads "auto-replied"' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
References: #{article.message_id}
Auto-Submitted: auto-replied
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
end
end
context 'when configured to search everything' do
before { Setting.set('postmaster_follow_up_search_in', %w[body attachment references]) }
context 'when subject contains ticket reference' do
include_context 'ticket reference in subject'
include_examples 'adds message to ticket'
end
context 'when body contains ticket reference' do
include_context 'ticket reference in body'
include_examples 'adds message to ticket'
end
context 'when text/plain attachment contains ticket reference' do
include_context 'ticket reference in text/plain attachment'
include_examples 'adds message to ticket'
end
context 'when text/html attachment (as content) contains ticket reference' do
include_context 'ticket reference in text/html (as content) attachment'
include_examples 'adds message to ticket'
end
context 'when text/html attachment (attribute) contains ticket reference' do
include_context 'ticket reference in text/html (attribute) attachment'
include_examples 'creates a new ticket'
end
context 'when image/jpg attachment contains ticket reference' do
include_context 'ticket reference in image/jpg attachment'
include_examples 'creates a new ticket'
end
context 'when In-Reply-To header contains article message-id' do
include_context 'ticket reference in In-Reply-To header'
include_examples 'adds message to ticket'
end
context 'when References header contains article message-id' do
include_context 'ticket reference in References header'
include_examples 'adds message to ticket'
context 'and Auto-Submitted header reads "auto-replied"' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: no reference
References: #{article.message_id}
Auto-Submitted: auto-replied
Lorem ipsum dolor
RAW
include_examples 'adds message to ticket'
end
end
end
end
context 'for a closed ticket' do
let(:ticket) { create(:ticket, state_name: 'closed') }
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: #{ticket_ref}
Lorem ipsum dolor
RAW
it 'reopens it' do
expect { described_class.new.process({}, raw_mail) }
.to change { ticket.reload.state.name }.to('open')
end
context 'when group has follow_up_assignment true' do
let(:group) { create(:group, follow_up_assignment: true) }
let(:agent) { create(:agent, groups: [group]) }
let(:ticket) { create(:ticket, state_name: 'closed', owner: agent, group: group) }
it 'does not change the owner' do
expect { described_class.new.process({}, raw_mail) }
.not_to change { ticket.reload.owner.login }
end
end
context 'when group has follow_up_assignment false' do
let(:group) { create(:group, follow_up_assignment: false) }
let(:agent) { create(:agent, groups: [group]) }
let(:ticket) { create(:ticket, state_name: 'closed', owner: agent, group: group) }
it 'does change the owner' do
expect { described_class.new.process({}, raw_mail) }
.to change { ticket.reload.owner.login }.to eq(User.find(1).login)
end
end
end
end
describe 'assigning ticket.customer' do
let(:agent) { create(:agent) }
let(:customer) { create(:customer) }
let(:raw_mail) { <<~RAW.chomp }
From: #{agent.email}
To: #{customer.email}
Subject: Foo
Lorem ipsum dolor
RAW
context 'when "postmaster_sender_is_agent_search_for_customer" setting is true (default)' do
it 'sets ticket.customer to user with To: email' do
expect { described_class.new.process({}, raw_mail) }
.to change(Ticket, :count).by(1)
expect(Ticket.last.customer).to eq(customer)
end
end
context 'when "postmaster_sender_is_agent_search_for_customer" setting is false' do
before { Setting.set('postmaster_sender_is_agent_search_for_customer', false) }
it 'sets ticket.customer to user with To: email' do
expect { described_class.new.process({}, raw_mail) }
.to change(Ticket, :count).by(1)
expect(Ticket.last.customer).to eq(agent)
end
end
end
describe 'formatting to/from addresses' do
# see https://github.com/zammad/zammad/issues/2198
context 'when sender address contains spaces (#2198)' do
let(:mail_file) { Rails.root.join('test/data/mail/mail071.box') }
let(:sender_email) { 'powerquadrantsystem@example.com' }
it 'removes them before creating a new user' do
expect { described_class.new.process({}, raw_mail) }
.to change { User.exists?(email: sender_email) }
end
it 'marks new user email as invalid' do
described_class.new.process({}, raw_mail)
expect(User.find_by(email: sender_email).preferences)
.to include('mail_delivery_failed' => true)
.and include('mail_delivery_failed_reason' => 'invalid email')
.and include('mail_delivery_failed_data' => a_kind_of(ActiveSupport::TimeWithZone))
end
end
# see https://github.com/zammad/zammad/issues/2254
context 'when sender address contains > (#2254)' do
let(:mail_file) { Rails.root.join('test/data/mail/mail076.box') }
let(:sender_email) { 'millionslotteryspaintransfer@example.com' }
it 'removes them before creating a new user' do
expect { described_class.new.process({}, raw_mail) }
.to change { User.exists?(email: sender_email) }
end
it 'marks new user email as invalid' do
described_class.new.process({}, raw_mail)
expect(User.find_by(email: sender_email).preferences)
.to include('mail_delivery_failed' => true)
.and include('mail_delivery_failed_reason' => 'invalid email')
.and include('mail_delivery_failed_data' => a_kind_of(ActiveSupport::TimeWithZone))
end
end
end
describe 'signature detection' do
let(:raw_mail) { header + File.read(message_file) }
let(:header) { <<~HEADER }
From: Bob.Smith@music.com
To: test@zammad.org
Subject: test
HEADER
context 'for emails from an unrecognized email address' do
let(:message_file) { Rails.root.join('test/data/email_signature_detection/client_a_1.txt') }
it 'does not detect signatures' do
described_class.new.process({}, raw_mail)
expect { Scheduler.worker(true) }
.to not_change { Ticket.last.customer.preferences[:signature_detection] }.from(nil)
.and not_change { Ticket.last.articles.first.preferences[:signature_detection] }.from(nil)
end
end
context 'for emails from a previously processed sender' do
before do
described_class.new.process({}, header + File.read(previous_message_file))
end
let(:previous_message_file) { Rails.root.join('test/data/email_signature_detection/client_a_1.txt') }
let(:message_file) { Rails.root.join('test/data/email_signature_detection/client_a_2.txt') }
it 'sets detected signature on user (in a background job)' do
described_class.new.process({}, raw_mail)
expect { Scheduler.worker(true) }
.to change { Ticket.last.customer.preferences[:signature_detection] }
end
it 'sets line of detected signature on article (in a background job)' do
described_class.new.process({}, raw_mail)
expect { Scheduler.worker(true) }
.to change { Ticket.last.articles.first.preferences[:signature_detection] }.to(20)
end
end
end
describe 'charset handling' do
# see https://github.com/zammad/zammad/issues/2224
context 'when header specifies Windows-1258 charset (#2224)' do
let(:mail_file) { Rails.root.join('test/data/mail/mail072.box') }
it 'does not raise Encoding::ConverterNotFoundError' do
expect { described_class.new.process({}, raw_mail) }
.not_to raise_error
end
end
context 'when attachment for follow up check contains invalid charsets (#2808)' do
let(:mail_file) { Rails.root.join('test/data/mail/mail085.box') }
before { Setting.set('postmaster_follow_up_search_in', %w[attachment body]) }
it 'does not raise Encoding::CompatibilityError:' do
expect { described_class.new.process({}, raw_mail) }
.not_to raise_error
end
end
end
describe 'attachment handling' do
context 'with header "Content-Transfer-Encoding: x-uuencode"' do
let(:mail_file) { Rails.root.join('test/data/mail/mail078-content_transfer_encoding_x_uuencode.box') }
let(:article) { described_class.new.process({}, raw_mail).second }
it 'does not raise RuntimeError' do
expect { described_class.new.process({}, raw_mail) }
.not_to raise_error
end
it 'parses the content correctly' do
expect(article.attachments.first.filename).to eq('PGP_Cmts_on_12-14-01_Pkg.txt')
expect(article.attachments.first.content).to eq('Hello Zammad')
end
end
# https://github.com/zammad/zammad/issues/3529
context 'Attachments sent by Zammad not shown in Outlook' do
subject(:mail) do
Channel::EmailBuild.build(
from: 'sender@example.com',
to: 'recipient@example.com',
body: body,
content_type: 'text/html',
attachments: Store.where(filename: 'super-seven.jpg')
)
end
let(:mail_file) { Rails.root.join('test/data/mail/mail101.box') }
before do
described_class.new.process({}, raw_mail)
end
context 'when no reference in body' do
let(:body) { 'no reference here' }
it 'does not have content disposition inline' do
expect(mail.to_s).to include('Content-Disposition: attachment').and not_include('Content-Disposition: inline')
end
end
context 'when reference in body' do
let(:body) { %(somebody with some text <img src="cid:#{Store.find_by(filename: 'super-seven.jpg').preferences['Content-ID']}">) }
it 'does have content disposition inline' do
expect(mail.to_s).to include('Content-Disposition: inline').and not_include('Content-Disposition: attachment')
end
context 'when encoded as ISO-8859-1' do
let(:body) { super().encode('ISO-8859-1') }
it 'does not raise exception' do
expect { mail.to_s }.not_to raise_error
end
end
end
end
end
describe 'inline image handling' do
# see https://github.com/zammad/zammad/issues/2486
context 'when image is large but not resizable' do
let(:mail_file) { Rails.root.join('test/data/mail/mail079.box') }
let(:attachment) { article.attachments.to_a.find { |i| i.filename == 'a.jpg' } }
let(:article) { described_class.new.process({}, raw_mail).second }
it "doesn't set resizable preference" do
expect(attachment.filename).to eq('a.jpg')
expect(attachment.preferences).not_to include('resizable' => true)
end
end
end
describe 'ServiceNow handling' do
context 'new Ticket' do
let(:mail_file) { Rails.root.join('test/data/mail/mail089.box') }
it 'creates an ExternalSync reference' do
described_class.new.process({}, raw_mail)
expect(ExternalSync.last).to have_attributes(
source: 'ServiceNow-example@service-now.com',
source_id: 'INC678439',
object: 'Ticket',
o_id: Ticket.last.id,
)
end
end
context 'follow up' do
let(:mail_file) { Rails.root.join('test/data/mail/mail090.box') }
let(:ticket) { create(:ticket) }
let!(:external_sync) do
create(:external_sync,
source: 'ServiceNow-example@service-now.com',
source_id: 'INC678439',
object: 'Ticket',
o_id: ticket.id,)
end
it 'adds Article to existing Ticket' do
expect { described_class.new.process({}, raw_mail) }.to change { ticket.reload.articles.count }
end
context 'key insensitive sender address' do
let(:raw_mail) { super().gsub('example@service-now.com', 'Example@Service-Now.com') }
it 'adds Article to existing Ticket' do
expect { described_class.new.process({}, raw_mail) }.to change { ticket.reload.articles.count }
end
end
end
end
describe 'Jira handling' do
context 'new Ticket' do
let(:mail_file) { Rails.root.join('test/data/mail/mail103.box') }
it 'creates an ExternalSync reference' do
described_class.new.process({}, raw_mail)
expect(ExternalSync.last).to have_attributes(
source: 'Jira-example@jira.com',
source_id: 'SYS-422',
object: 'Ticket',
o_id: Ticket.last.id,
)
end
end
context 'follow up' do
let(:mail_file) { Rails.root.join('test/data/mail/mail104.box') }
let(:ticket) { create(:ticket) }
let!(:external_sync) do
create(:external_sync,
source: 'Jira-example@jira.com',
source_id: 'SYS-422',
object: 'Ticket',
o_id: ticket.id,)
end
it 'adds Article to existing Ticket' do
expect { described_class.new.process({}, raw_mail) }.to change { ticket.reload.articles.count }
end
context 'key insensitive sender address' do
let(:raw_mail) { super().gsub('example@service-now.com', 'Example@Service-Now.com') }
it 'adds Article to existing Ticket' do
expect { described_class.new.process({}, raw_mail) }.to change { ticket.reload.articles.count }
end
end
end
end
describe 'XSS protection' do
before do
# XSS processing may run into a timeout on slow CI systems, so turn the timeout off for the test.
stub_const("#{HtmlSanitizer}::PROCESSING_TIMEOUT", nil)
end
let(:article) { described_class.new.process({}, raw_mail).second }
let(:raw_mail) { <<~RAW.chomp }
From: ME Bob <me@example.com>
To: customer@example.com
Subject: some subject
Content-Type: #{content_type}
MIME-Version: 1.0
no HTML <script type="text/javascript">alert(\'XSS\')</script>
RAW
context 'for Content-Type: text/html' do
let(:content_type) { 'text/html' }
it 'removes injected <script> tags from body' do
expect(article.body).to eq('no HTML')
end
end
context 'for Content-Type: text/plain' do
let(:content_type) { 'text/plain' }
it 'leaves body as-is' do
expect(article.body).to eq(<<~SANITIZED.chomp)
no HTML <script type="text/javascript">alert(\'XSS\')</script>
SANITIZED
end
end
end
context 'for delivery failed notifications (a.k.a. bounce messages)' do
let(:ticket) { article.ticket }
let(:article) { create(:ticket_article, sender_name: 'Agent', message_id: message_id) }
let(:message_id) { raw_mail[%r{(?<=^(References|Message-ID): )\S*}] }
context 'with future retries (delayed)' do
let(:mail_file) { Rails.root.join('test/data/mail/mail078.box') }
context 'on a closed ticket' do
before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
it 'sets #preferences on resulting ticket to { "send-auto-responses" => false, "is-auto-reponse" => true }' do
article = described_class.new.process({}, raw_mail).second
expect(article.preferences)
.to include('send-auto-response' => false, 'is-auto-response' => true)
end
it 'returns a Mail object with an x-zammad-out-of-office header' do
output_mail = described_class.new.process({}, raw_mail).last
expect(output_mail).to include('x-zammad-out-of-office': true)
end
it 'finds the article referenced in the bounce message headers, then adds the bounce message to its ticket' do
expect { described_class.new.process({}, raw_mail) }
.to change { ticket.articles.count }.by(1)
end
it 'does not re-open the ticket' do
expect { described_class.new.process({}, raw_mail) }
.not_to change { ticket.reload.state.name }.from('closed')
end
end
end
context 'with no future retries (undeliverable): sample input 1' do
let(:mail_file) { Rails.root.join('test/data/mail/mail033-undelivered-mail-returned-to-sender.box') }
context 'for original message sent by Agent' do
it 'sets #preferences on resulting ticket to { "send-auto-responses" => false, "is-auto-reponse" => true }' do
article = described_class.new.process({}, raw_mail).second
expect(article.preferences)
.to include('send-auto-response' => false, 'is-auto-response' => true)
end
it 'finds the article referenced in the bounce message headers, then adds the bounce message to its ticket' do
expect { described_class.new.process({}, raw_mail) }
.to change { ticket.articles.count }.by(1)
end
it 'does not alter the ticket state' do
expect { described_class.new.process({}, raw_mail) }
.not_to change { ticket.reload.state.name }.from('open')
end
end
context 'for original message sent by Customer' do
let(:article) { create(:ticket_article, sender_name: 'Customer', message_id: message_id) }
it 'sets #preferences on resulting ticket to { "send-auto-responses" => false, "is-auto-reponse" => true }' do
article = described_class.new.process({}, raw_mail).second
expect(article.preferences)
.to include('send-auto-response' => false, 'is-auto-response' => true)
end
it 'finds the article referenced in the bounce message headers, then adds the bounce message to its ticket' do
expect { described_class.new.process({}, raw_mail) }
.to change { ticket.articles.count }.by(1)
end
it 'does not alter the ticket state' do
expect { described_class.new.process({}, raw_mail) }
.not_to change { ticket.reload.state.name }.from('new')
end
end
end
context 'with no future retries (undeliverable): sample input 2' do
let(:mail_file) { Rails.root.join('test/data/mail/mail055.box') }
it 'finds the article referenced in the bounce message headers, then adds the bounce message to its ticket' do
expect { described_class.new.process({}, raw_mail) }
.to change { ticket.articles.count }.by(1)
end
it 'does not alter the ticket state' do
expect { described_class.new.process({}, raw_mail) }
.not_to change { ticket.reload.state.name }.from('open')
end
end
end
context 'for “out-of-office” notifications (a.k.a. auto-response messages)' do
let(:raw_mail) { <<~RAW.chomp }
From: me@example.com
To: customer@example.com
Subject: #{subject_line}
Some Text
RAW
let(:subject_line) { 'Lorem ipsum dolor' }
it 'applies the OutOfOfficeCheck filter to given message' do
expect(Channel::Filter::OutOfOfficeCheck)
.to receive(:run)
.with(kind_of(Hash), hash_including(subject: subject_line), kind_of(Hash))
described_class.new.process({}, raw_mail)
end
context 'on an existing, closed ticket' do
let(:ticket) { create(:ticket, state_name: 'closed') }
let(:subject_line) { ticket.subject_build('Lorem ipsum dolor') }
context 'when OutOfOfficeCheck filter applies x-zammad-out-of-office: false' do
before do
allow(Channel::Filter::OutOfOfficeCheck)
.to receive(:run) { |_, mail_hash| mail_hash[:'x-zammad-out-of-office'] = false }
end
it 're-opens a closed ticket' do
expect { described_class.new.process({}, raw_mail) }
.to not_change(Ticket, :count)
.and change { ticket.reload.state.name }.to('open')
end
end
context 'when OutOfOfficeCheck filter applies x-zammad-out-of-office: true' do
before do
allow(Channel::Filter::OutOfOfficeCheck)
.to receive(:run) { |_, mail_hash| mail_hash[:'x-zammad-out-of-office'] = true }
end
it 'does not re-open a closed ticket' do
expect { described_class.new.process({}, raw_mail) }
.to not_change(Ticket, :count)
.and not_change { ticket.reload.state.name }
end
end
end
end
describe 'suppressing normal Ticket::Article callbacks' do
context 'from sender: "Agent"' do
let(:agent) { create(:agent) }
it 'does not dispatch an email on article creation' do
expect(TicketArticleCommunicateEmailJob).not_to receive(:perform_later)
described_class.new.process({}, <<~RAW.chomp)
From: #{agent.email}
To: customer@example.com
Subject: some subject
Some Text
RAW
end
end
end
end
describe '#compose_postmaster_reply' do
let(:raw_incoming_mail) { File.read(Rails.root.join('test/data/mail/mail010.box')) }
shared_examples 'postmaster reply' do
it 'composes postmaster reply' do
reply = described_class.new.send(:compose_postmaster_reply, raw_incoming_mail, locale)
expect(reply[:to]).to eq('smith@example.com')
expect(reply[:content_type]).to eq('text/plain')
expect(reply[:subject]).to eq(expected_subject)
expect(reply[:body]).to eq(expected_body)
end
end
context 'for English locale (en)' do
include_examples 'postmaster reply' do
let(:locale) { 'en' }
let(:expected_subject) { '[undeliverable] Message too large' }
let(:expected_body) do
body = <<~BODY
Dear Smith Sepp,
Unfortunately your email titled \"Gruß aus Oberalteich\" could not be delivered to one or more recipients.
Your message was 0.01 MB but we only accept messages up to 10 MB.
Please reduce the message size and try again. Thank you for your understanding.
Regretfully,
Postmaster of zammad.example.com
BODY
body.gsub(%r{\n}, "\r\n")
end
end
end
context 'for German locale (de)' do
include_examples 'postmaster reply' do
let(:locale) { 'de' }
let(:expected_subject) { '[Unzustellbar] Nachricht zu groß' }
let(:expected_body) do
body = <<~BODY
Hallo Smith Sepp,
Ihre E-Mail mit dem Betreff \"Gruß aus Oberalteich\" konnte nicht an einen oder mehrere Empfänger zugestellt werden.
Die Nachricht hatte eine Größe von 0.01 MB, wir akzeptieren jedoch nur E-Mails mit einer Größe von bis zu 10 MB.
Bitte reduzieren Sie die Größe Ihrer Nachricht und versuchen Sie es erneut. Vielen Dank für Ihr Verständnis.
Mit freundlichen Grüßen
Postmaster von zammad.example.com
BODY
body.gsub(%r{\n}, "\r\n")
end
end
end
end
describe '#mail_to_group' do
context 'when EmailAddress exists' do
context 'when gives address matches exactly' do
let(:group) { create(:group) }
let(:channel) { create(:email_channel, group: group) }
let!(:email_address) { create(:email_address, channel: channel) }
it 'returns the Channel Group' do
expect(described_class.mail_to_group(email_address.email)).to eq(group)
end
end
context 'when gives address matches key insensitive' do
let(:group) { create(:group) }
let(:channel) { create(:email_channel, group: group) }
let(:address) { 'KeyInsensitive@example.COM' }
let!(:email_address) { create(:email_address, email: address, channel: channel) }
it 'returns the Channel Group' do
expect(described_class.mail_to_group(address)).to eq(group)
end
end
context 'when no Channel is assigned' do
let!(:email_address) { create(:email_address, channel: nil) }
it 'returns nil' do
expect(described_class.mail_to_group(email_address.email)).to be_nil
end
end
context 'when Channel has no Group assigned' do
let(:channel) { create(:email_channel, group: nil) }
let!(:email_address) { create(:email_address, channel: channel) }
it 'returns nil' do
expect(described_class.mail_to_group(email_address.email)).to be_nil
end
end
end
context 'when given address is not parse-able' do
let(:address) { 'this_is_not_a_valid_email_address' }
it 'returns nil' do
expect(described_class.mail_to_group(address)).to be_nil
end
end
end
end