OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
openproject/spec/models/mail_handler_spec.rb

699 lines
24 KiB

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
describe MailHandler, type: :model do
let(:anno_user) { User.anonymous }
let(:project) { FactoryBot.create(:valid_project, identifier: 'onlinestore', name: 'OnlineStore', public: false) }
let(:public_project) { FactoryBot.create(:valid_project, identifier: 'onlinestore', name: 'OnlineStore', public: true) }
let(:priority_low) { FactoryBot.create(:priority_low, is_default: true) }
before do
# we need both of these run first so the anonymous user is created and
# there is a default work package priority to save any work packages
priority_low
anno_user
end
after do
User.current = nil
allow(Setting).to receive(:default_language).and_return('en')
end
shared_context 'wp_on_given_project' do
let(:permissions) { %i[add_work_packages assign_versions] }
let!(:user) do
FactoryBot.create(:user,
mail: 'JSmith@somenet.foo',
firstname: 'John',
lastname: 'Smith',
member_in_project: project,
member_with_permissions: permissions)
end
let(:submit_options) { {} }
subject do
submit_email('wp_on_given_project.eml', **submit_options)
end
end
shared_context 'wp_on_given_project_case_insensitive' do
let(:permissions) { %i[add_work_packages assign_versions] }
let!(:user) do
FactoryBot.create(:user,
mail: 'JSmith@somenet.foo',
firstname: 'John',
lastname: 'Smith',
member_in_project: project,
member_with_permissions: permissions)
end
let(:submit_options) { { allow_override: 'version' } }
subject do
submit_email('wp_on_given_project_case_insensitive.eml', **submit_options)
end
end
shared_context 'wp_update_with_quoted_reply_above' do
let(:permissions) { %i[edit_work_packages view_work_packages] }
let!(:user) do
FactoryBot.create(:user,
mail: 'JSmith@somenet.foo',
member_in_project: project,
member_with_permissions: permissions)
end
let!(:work_package) do
FactoryBot.create(:work_package, id: 2, project: project)
end
subject do
submit_email('wp_update_with_quoted_reply_above.eml')
end
end
shared_context 'wp_update_with_multiple_quoted_reply_above' do
let(:permissions) { %i[edit_work_packages view_work_packages] }
let!(:user) do
FactoryBot.create(:user,
mail: 'JSmith@somenet.foo',
member_in_project: project,
member_with_permissions: permissions)
end
let!(:work_package) do
FactoryBot.create(:work_package, id: 2, project: project)
end
subject do
submit_email('wp_update_with_multiple_quoted_reply_above.eml')
end
end
shared_context 'wp create with cc' do
let(:permissions) { %i[add_work_packages view_work_packages add_work_package_watchers] }
let!(:user) do
FactoryBot.create(:user,
mail: 'JSmith@somenet.foo',
firstname: 'John',
lastname: 'Smith',
member_in_project: project,
member_with_permissions: permissions)
end
let!(:cc_user) do
FactoryBot.create(:user,
mail: 'dlopper@somenet.foo',
firstname: 'D',
lastname: 'Lopper',
member_in_project: project,
member_with_permissions: permissions)
end
let(:submit_options) { { issue: { project: project.identifier } } }
subject do
submit_email('ticket_with_cc.eml', **submit_options)
end
end
describe '#receive' do
shared_examples_for 'work package created' do
it 'creates the work package' do
expect(subject)
.to be_a(WorkPackage)
expect(subject)
.to be_persisted
end
end
shared_examples_for 'journal created' do
it 'creates the journal' do
expect(subject)
.to be_a(Journal)
expect(subject)
.to be_persisted
end
end
context 'create work package' do
context 'in a given project' do
let!(:status) { FactoryBot.create(:status, name: 'Resolved') }
let!(:version) { FactoryBot.create(:version, name: 'alpha', project: project) }
include_context 'wp_on_given_project' do
let(:submit_options) { { allow_override: 'version' } }
end
it_behaves_like 'work package created'
it 'sets the referenced project' do
expect(subject.project)
.to eql(project)
end
it 'sets the first type in the project' do
expect(subject.type)
.to eql(project.types.first)
end
it 'sets the subject' do
expect(subject.subject)
.to eql('New ticket on a given project')
end
it 'sets the sender as the author' do
expect(subject.author)
.to eql(user)
end
it 'set the description' do
expect(subject.description)
.to include('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
end
it 'sets the start date' do
expect(subject.start_date.to_s)
.to eql('2010-01-01')
end
it 'sets the due date' do
expect(subject.due_date.to_s)
.to eql('2010-12-31')
end
it 'sets the assignee' do
expect(subject.assigned_to)
.to eql(user)
end
it 'sets the status' do
expect(subject.status)
.to eql(status)
end
it 'sets the version' do
expect(subject.version)
.to eql(version)
end
it 'sets the estimated_hours' do
expect(subject.estimated_hours)
.to eql(2.5)
end
it 'sets the done_ratio' do
expect(subject.done_ratio)
.to eql(30)
end
it 'removes keywords' do
expect(subject.description)
.not_to match(/^Project:/i)
expect(subject.description)
.not_to match(/^Status:/i)
expect(subject.description)
.not_to match(/^Start Date:/i)
end
it 'sends notifications to watching users' do
# User gets all updates
user = FactoryBot.create(:user, member_in_project: project, member_with_permissions: %i(view_work_packages))
expect do
perform_enqueued_jobs do
subject
end
end.to change(Notification.where(recipient: user), :count).by(1)
end
end
context 'in given project with a default type' do
let(:default_type) do
FactoryBot.create(:type, is_default: true).tap do |t|
project.types << t
end
end
include_context 'wp_on_given_project' do
let(:submit_options) { { issue: { type: default_type.name } } }
end
it_behaves_like 'work package created'
it 'sets the default type' do
expect(subject.type.name)
.to eql(default_type.name)
end
end
context 'email by unknown user' do
it 'adds a work_package by create user on public project' do
Role.non_member.update_attribute :permissions, [:add_work_packages]
project.update_attribute :public, true
expect do
work_package = submit_email('ticket_by_unknown_user.eml', issue: { project: 'onlinestore' }, unknown_user: 'create')
work_package_created(work_package)
expect(work_package.author.active?).to be_truthy
expect(work_package.author.mail).to eq('john.doe@somenet.foo')
expect(work_package.author.firstname).to eq('John')
expect(work_package.author.lastname).to eq('Doe')
# account information
perform_enqueued_jobs
email = ActionMailer::Base.deliveries.first
expect(email).not_to be_nil
expect(email.subject).to eq(I18n.t('mail_subject_register', value: Setting.app_title))
login = email.body.encoded.match(/\* Username: (\S+)\s?$/)[1]
password = email.body.encoded.match(/\* Password: (\S+)\s?$/)[1]
# Can't log in here since randomly assigned password must be changed
found_user = User.find_by_login(login)
expect(work_package.author).to eq(found_user)
expect(found_user.check_password?(password)).to be_truthy
end.to change(User, :count).by(1)
end
it 'rejects if unknown_user=accept and permission check is present' do
expected =
'MailHandler: work_package could not be created by Anonymous due to ' \
'#["may not be accessed.", "Type was attempted to be written but is not writable.", ' \
'"Project was attempted to be written but is not writable.", ' \
'"Subject was attempted to be written but is not writable.", ' \
'"Description was attempted to be written but is not writable."]'
expect(Rails.logger)
.to receive(:error)
.with(expected)
result = submit_email 'ticket_by_unknown_user.eml',
issue: { project: project.identifier },
unknown_user: 'accept'
expect(result).to eq false
end
it 'accepts if unknown_user=accept and no_permission_check' do
work_package = submit_email 'ticket_by_unknown_user.eml',
issue: { project: project.identifier },
unknown_user: 'accept',
no_permission_check: 1
work_package_created(work_package)
expect(work_package.author).to eq(User.anonymous)
end
end
context 'email from emission address', with_settings: { mail_from: 'openproject@example.net' } do
before do
Role.non_member.add_permission!(:add_work_packages)
end
subject do
submit_email('ticket_from_emission_address.eml',
issue: { project: public_project.identifier },
unknown_user: 'create')
end
it 'returns false' do
expect(subject).to be_falsey
end
it 'does not create the user' do
expect { subject }
.not_to(change { User.count })
end
it 'does not create the work_package' do
expect { subject }
.not_to(change { WorkPackage.count })
end
end
context 'wp with status' do
let!(:status) { FactoryBot.create(:status, name: 'Resolved') }
# This email contains: 'Project: onlinestore' and 'Status: Resolved'
include_context 'wp_on_given_project'
it_behaves_like 'work package created'
it 'assigns the status to the created work package' do
expect(subject.status)
.to eql(status)
end
end
context 'wp with status case insensitive' do
let!(:status) { FactoryBot.create(:status, name: 'Resolved') }
let!(:priority_low) { FactoryBot.create(:priority_low, name: 'Low', is_default: true) }
let!(:version) { FactoryBot.create(:version, name: 'alpha', project: project) }
# This email contains: 'Project: onlinestore' and 'Status: resolved'
include_context 'wp_on_given_project_case_insensitive'
it_behaves_like 'work package created'
it 'assigns the status to the created work package' do
expect(subject.status).to eq(status)
expect(subject.version).to eq(version)
expect(subject.priority).to eq priority_low
end
end
context 'wp with cc' do
include_context 'wp create with cc'
it_behaves_like 'work package created'
it 'assigns cc and author as watcher' do
expect(subject.watcher_users)
.to match_array([user, cc_user])
end
end
end
describe 'update work package' do
let!(:mail_user) { FactoryBot.create :admin, mail: 'user@example.org' }
let!(:work_package) { FactoryBot.create :work_package, project: project }
before do
# Avoid trying to extract text
allow(OpenProject::Database).to receive(:allows_tsv?).and_return false
end
context 'with attachments to be added' do
it 'updates a work package with attachment' do
allow(WorkPackage).to receive(:find_by).with(id: 123).and_return(work_package)
# Mail with two attachemnts, one of which is skipped by signature.asc filename match
submit_email 'update_ticket_with_attachment_and_sig.eml', issue: { project: 'onlinestore' }
work_package.reload
# Expect comment
expect(work_package.journals.last.notes).to eq 'Reply to work package #123'
expect(work_package.journals.last.user).to eq mail_user
# Expect filename without signature to be saved
expect(work_package.attachments.count).to eq(1)
expect(work_package.attachments.first.filename).to eq('Photo25.jpg')
end
end
context 'with existing attachment' do
let!(:attachment) { FactoryBot.create(:attachment, container: work_package) }
it 'does not replace it (Regression #29722)' do
work_package.reload
allow(WorkPackage).to receive(:find_by).with(id: 123).and_return(work_package)
# Mail with two attachemnts, one of which is skipped by signature.asc filename match
submit_email 'update_ticket_with_attachment_and_sig.eml', issue: { project: 'onlinestore' }
expect(work_package.attachments.length).to eq 2
end
end
context 'with reply text' do
include_context 'wp_update_with_quoted_reply_above'
it_behaves_like 'journal created'
it 'sends notifications' do
assignee = FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i(view_work_packages),
notification_settings: [FactoryBot.build(:notification_setting, involved: true)])
work_package.update_column(:assigned_to_id, assignee.id)
# Sends notifications for the assignee and the author who is listening for all changes.
expect do
perform_enqueued_jobs do
subject
end
end.to change(Notification, :count).by(2)
end
end
context 'with a custom field' do
let(:work_package) { FactoryBot.create :work_package, project: project }
let(:type) { FactoryBot.create :type }
before do
type.custom_fields << custom_field
type.save!
allow_any_instance_of(WorkPackage).to receive(:available_custom_fields).and_return([custom_field])
expect(WorkPackage).to receive(:find_by).with(id: 42).and_return(work_package)
expect(User).to receive(:find_by_mail).with("h.wurst@openproject.com").and_return(mail_user)
end
context 'of type text' do
let(:custom_field) { FactoryBot.create :text_wp_custom_field, name: "Notes" }
before do
submit_email 'work_package_with_text_custom_field.eml', issue: { project: project.identifier }
work_package.reload
end
it "sets the value" do
value = work_package.custom_values.where(custom_field_id: custom_field.id).pluck(:value).first
expect(value).to eq "some text" # as given in .eml fixture
end
end
context 'of type list' do
let(:custom_field) { FactoryBot.create :list_wp_custom_field, name: "Letters", possible_values: %w(A B C) }
before do
submit_email 'work_package_with_list_custom_field.eml', issue: { project: project.identifier }
work_package.reload
end
it "sets the value" do
option = CustomOption.where(custom_field_id: custom_field.id, value: "B").first # as given in .eml fixture
value = work_package.custom_values.where(custom_field_id: custom_field.id).pluck(:value).first
expect(value).to eq option.id.to_s
end
end
end
end
context 'truncate emails based on the Setting' do
context 'with no setting', with_settings: { mail_handler_body_delimiters: '' } do
include_context 'wp_on_given_project'
it_behaves_like 'work package created'
it 'adds the entire email into the work_package' do
expect(subject.description)
.to include('---')
expect(subject.description)
.to include('This paragraph is after the delimiter')
end
end
context 'with a single string', with_settings: { mail_handler_body_delimiters: '---' } do
include_context 'wp_on_given_project'
it_behaves_like 'work package created'
it 'truncates the email at the delimiter for the work package' do
expect(subject.description)
.to include('This paragraph is before delimiters')
expect(subject.description)
.to include('--- This line starts with a delimiter')
expect(subject.description)
.not_to match(/^---$/)
expect(subject.description)
.not_to include('This paragraph is after the delimiter')
end
end
context 'with a single quoted reply (e.g. reply to a OpenProject email notification)',
with_settings: { mail_handler_body_delimiters: '--- Reply above. Do not remove this line. ---' } do
include_context 'wp_update_with_quoted_reply_above'
it_behaves_like 'journal created'
it 'truncates the email at the delimiter with the quoted reply symbols (>)' do
expect(subject.notes)
.to include('An update to the issue by the sender.')
expect(subject.notes)
.not_to match(Regexp.escape('--- Reply above. Do not remove this line. ---'))
expect(subject.notes)
.not_to include('Looks like the JSON api for projects was missed.')
end
end
context 'with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)',
with_settings: { mail_handler_body_delimiters: '--- Reply above. Do not remove this line. ---' } do
include_context 'wp_update_with_quoted_reply_above'
it_behaves_like 'journal created'
it 'truncates the email at the delimiter with the quoted reply symbols (>)' do
expect(subject.notes)
.to include('An update to the issue by the sender.')
expect(subject.notes)
.not_to match(Regexp.escape('--- Reply above. Do not remove this line. ---'))
expect(subject.notes)
.not_to include('Looks like the JSON api for projects was missed.')
end
end
context 'with multiple strings',
with_settings: { mail_handler_body_delimiters: "---\nBREAK" } do
include_context 'wp_on_given_project'
it_behaves_like 'work package created'
it 'truncates the email at the first delimiter found (BREAK)' do
expect(subject.description)
.to include('This paragraph is before delimiters')
expect(subject.description)
.not_to include('BREAK')
expect(subject.description)
.not_to include('This paragraph is between delimiters')
expect(subject.description)
.not_to match(/^---$/)
expect(subject.description)
.not_to include('This paragraph is after the delimiter')
end
end
end
describe 'category' do
let!(:category) { FactoryBot.create :category, project: project, name: 'Foobar' }
it 'should add a work_package with category' do
allow(Setting).to receive(:default_language).and_return('en')
Role.non_member.update_attribute :permissions, [:add_work_packages]
project.update_attribute :public, true
work_package = submit_email 'ticket_with_category.eml',
issue: { project: 'onlinestore' },
allow_override: ['category'],
unknown_user: 'create'
work_package_created(work_package)
expect(work_package.category).to eq(category)
end
end
end
describe '#cleanup_body' do
let(:input) do
"Subject:foo\nDescription:bar\n" \
">>> myserver.example.org 2016-01-27 15:56 >>>\n... (Email-Body) ..."
end
let(:handler) { MailHandler.send :new }
context 'with regex delimiter' do
before do
allow(Setting).to receive(:mail_handler_body_delimiter_regex).and_return('>>>.+?>>>.*')
allow(handler).to receive(:plain_text_body).and_return(input)
expect(handler).to receive(:cleaned_up_text_body).and_call_original
end
it 'removes the irrelevant lines' do
expect(handler.send(:cleaned_up_text_body)).to eq("Subject:foo\nDescription:bar")
end
end
end
describe '#dispatch_target_from_message_id' do
let!(:mail_user) { FactoryBot.create :admin, mail: 'user@example.org' }
let(:instance) do
mh = MailHandler.new
mh.options = {}
mh
end
subject { instance.receive mail }
context 'receiving reply from work package' do
let(:mail) { Mail.new(read_email('work_package_reply.eml')) }
it 'calls the work package reply' do
expect(instance).to receive(:receive_work_package_reply).with(34540)
subject
end
end
context 'receiving reply from message' do
let(:mail) { Mail.new(read_email('message_reply.eml')) }
it 'calls the work package reply' do
expect(instance).to receive(:receive_message_reply).with(12559)
subject
end
end
end
private
FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
def read_email(filename)
IO.read(File.join(FIXTURES_PATH, filename))
end
def submit_email(filename, options = {})
MailHandler.receive(read_email(filename), options)
end
def work_package_created(work_package)
expect(work_package.is_a?(WorkPackage)).to be_truthy
expect(work_package).not_to be_new_record
work_package.reload
end
end