#-- 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 'with a reply to a wp mention with quotes 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).tap do |wp| wp.journals.last.update_column(:id, 891223) end end subject do submit_email('wp_reply_with_quoted_reply_above.eml') end end shared_context 'with 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 shared_context 'with a reply to a wp mention' do let(:permissions) { %i[add_work_package_notes view_work_packages] } let!(:user) do FactoryBot.create(:user, mail: 'j.doe@openproject.org', member_in_project: project, member_with_permissions: permissions) end let!(:work_package) do FactoryBot.create(:work_package, subject: 'Some subject of the bug', id: 39733, project: project).tap do |wp| wp.journals.last.update_column(:id, 99999999) end end subject do submit_email('wp_mention_reply.eml') end end shared_context 'with a reply to a wp mention with attributes' do let(:permissions) { %i[add_work_package_notes view_work_packages edit_work_packages] } let(:role) do FactoryBot.create(:role, permissions: permissions) end let!(:user) do FactoryBot.create(:user, mail: 'j.doe@openproject.org', member_in_project: project, member_through_role: role) end let!(:work_package) do FactoryBot.create(:work_package, subject: 'Some subject of the bug', id: 39733, project: project, status: original_status).tap do |wp| wp.journals.last.update_column(:id, 99999999) end end let!(:original_status) do FactoryBot.create(:default_status) end let!(:resolved_status) do FactoryBot.create(:status, name: 'Resolved').tap do |status| FactoryBot.create(:workflow, old_status: original_status, new_status: status, role: role, type: work_package.type) end end let!(:other_user) do FactoryBot.create(:user, mail: 'jsmith@somenet.foo', member_in_project: project, member_through_role: role) end let!(:float_cf) do FactoryBot.create(:float_wp_custom_field, name: 'float field').tap do |cf| project.work_package_custom_fields << cf work_package.type.custom_fields << cf end end subject do submit_email('wp_mention_reply_with_attributes.eml') end end shared_context 'with a reply to a message' do let(:permissions) { %i[view_messages add_messages] } let!(:user) do FactoryBot.create(:user, mail: 'j.doe@openproject.org', member_in_project: project, member_with_permissions: permissions) end let!(:message) do FactoryBot.create(:message, id: 70917, forum: FactoryBot.create(:forum, project: project)).tap do |wp| wp.journals.last.update_column(:id, 99999999) end end subject do submit_email('message_reply.eml') 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 'when sending a mail not as a reply' 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."]' allow(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 expect(Rails.logger) .to have_received(:error) .with(expected) 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 'with 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 context 'when sending a reply to work package mail' 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 attachments, 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 'with a reply to a wp mention with quotes 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 'when replying to mention mail with only text' do include_context 'with a reply to a wp mention' it_behaves_like 'journal created' it 'adds the content to the last journal' do subject expect(work_package.journals.reload.last.notes) .to include 'The text of the reply.' end it 'does not alter any attributes' do subject expect(work_package.journals.reload.last.details) .to be_empty end it 'performs the changes in the name of the sender' do subject expect(work_package.journals.reload.last.user) .to eql user end end context 'when replying to mention mail with text and attributes' do include_context 'with a reply to a wp mention with attributes' it_behaves_like 'journal created' it 'adds the content to the last journal' do subject expect(work_package.journals.reload.last.notes) .to include 'The text of the reply.' end it 'alters the attributes' do subject expect(work_package.journals.reload.last.details) .to eql( "due_date" => [nil, Date.parse("Fri, 31 Dec 2010")], "status_id" => [original_status.id, resolved_status.id], "assigned_to_id" => [nil, other_user.id], "start_date" => [nil, Date.parse("Fri, 01 Jan 2010")], "custom_fields_#{float_cf.id}" => [nil, "52.6"] ) end it 'performs the changes in the name of the sender' do subject expect(work_package.journals.reload.last.user) .to eql user 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]) allow(WorkPackage).to receive(:find_by).with(id: 42).and_return(work_package) allow(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 'wp_reply_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 'wp_reply_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 'when sending a reply to a message mail' do include_context 'with a reply to a message' it 'creates a new message in the name of the sender', aggregate_failures: true do expect(subject) .to be_a Message expect(subject.subject) .to eql('Response to the original message') expect(subject.content) .to include('Test message') expect(subject.author) .to eql user expect(subject.forum) .to eql message.forum expect(subject.parent) .to eql message 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 'with a reply to a wp mention with quotes 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 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