1
0
مراية لـ https://github.com/postalserver/postal.git تم المزامنة 2025-12-01 05:43:04 +00:00

refactor: refactors message dequeueing (#2810)

هذا الالتزام موجود في:
Adam Cooke
2024-02-22 22:26:27 +00:00
ملتزم من قبل Adam Cooke
الأصل 07eb15246f
التزام a44e1f9081
20 ملفات معدلة مع 1808 إضافات و1537 حذوفات

عرض الملف

@@ -0,0 +1,38 @@
# frozen_string_literal: true
require "rails_helper"
module MessageDequeuer
RSpec.describe Base do
describe ".new" do
context "when given state" do
it "uses that state" do
base = described_class.new(nil, logger: nil, state: 1234)
expect(base.state).to eq 1234
end
end
context "when not given state" do
it "creates a new state" do
base = described_class.new(nil, logger: nil)
expect(base.state).to be_a State
end
end
end
describe ".process" do
it "creates a new instances of the class and calls process" do
message = create(:queued_message)
logger = TestLogger.new
mock = double("Base")
expect(mock).to receive(:process).once
expect(described_class).to receive(:new).with(message, logger: logger).and_return(mock)
described_class.process(message, logger: logger)
end
end
end
end

عرض الملف

@@ -0,0 +1,640 @@
# frozen_string_literal: true
require "rails_helper"
module MessageDequeuer
RSpec.describe IncomingMessageProcessor do
let(:server) { create(:server) }
let(:state) { State.new }
let(:logger) { TestLogger.new }
let(:route) { create(:route, server: server) }
let(:message) { MessageFactory.incoming(server, route: route) }
let(:queued_message) { create(:queued_message, :locked, message: message) }
subject(:processor) { described_class.new(queued_message, logger: logger, state: state) }
context "when the message was a bounce but there's no return path for it" do
let(:message) do
MessageFactory.incoming(server) do |msg|
msg.bounce = true
end
end
it "logs" do
processor.process
expect(logger).to have_logged(/no source messages found, hard failing/)
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /was a bounce but we couldn't link it with any outgoing message/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the message is a bounce for an existing message" do
let(:existing_message) { MessageFactory.outgoing(server) }
let(:message) do
MessageFactory.incoming(server) do |msg, mail|
msg.bounce = true
mail["X-Postal-MsgID"] = existing_message.token
end
end
it "logs" do
processor.process
expect(logger).to have_logged(/message is a bounce/)
end
it "adds the original message as the bounce ID for the received message" do
processor.process
expect(message.reload.bounce_for_id).to eq existing_message.id
end
it "sets the received message status to Processed" do
processor.process
expect(message.reload.status).to eq "Processed"
end
it "creates a Processed delivery on the received message" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Processed", details: /This has been detected as a bounce message for <msg:#{existing_message.id}>/i)
end
it "sets the existing message status to Bounced" do
processor.process
expect(existing_message.reload.status).to eq "Bounced"
end
it "creates a Bounced delivery on the original message" do
processor.process
delivery = existing_message.deliveries.last
expect(delivery).to have_attributes(status: "Bounced", details: /received a bounce message for this e-mail. See <msg:#{message.id}> for/i)
end
it "triggers a MessageBounced webhook event" do
expect(WebhookRequest).to receive(:trigger).with(server, "MessageBounced", {
original_message: kind_of(Hash),
bounce: kind_of(Hash)
})
processor.process
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the message is not a bounce" do
it "increments the stats for the server" do
expect { processor.process }.to change { server.message_db.live_stats.total(5) }.by(1)
end
it "inspects the message and adds headers" do
expect { processor.process }.to change { message.reload.inspected }.from(false).to(true)
new_message = message.reload
expect(new_message.headers).to match hash_including(
"x-postal-spam" => ["no"],
"x-postal-spam-threshold" => ["5.0"],
"x-postal-threat" => ["no"]
)
end
it "marks the message as spam if the spam score is higher than the server threshold" do
inspection_result = double("Result", spam_score: server.spam_threshold + 1, threat: false, threat_message: nil, spam_checks: [])
allow(Postal::MessageInspection).to receive(:scan).and_return(inspection_result)
processor.process
expect(message.reload.spam).to be true
end
end
context "when the message has a spam score greater than the server's spam failure threshold" do
before do
inspection_result = double("Result", spam_score: 100, threat: false, threat_message: nil, spam_checks: [])
allow(Postal::MessageInspection).to receive(:scan).and_return(inspection_result)
end
it "logs" do
processor.process
expect(logger).to have_logged(/message has a spam score higher than the server's maxmimum/)
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /spam score is higher than the failure threshold for this server/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the server mode is Development and the message was not manually queued" do
before do
server.update!(mode: "Development")
end
after do
server.update!(mode: "Live")
end
it "logs" do
processor.process
expect(logger).to have_logged(/server is in development mode/)
end
it "sets the message status to Held" do
processor.process
expect(message.reload.status).to eq "Held"
end
it "creates a Held delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Held", details: /server is in development mode/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when there is no route for the incoming message" do
let(:route) { nil }
it "logs" do
processor.process
expect(logger).to have_logged(/no route and\/or endpoint available for processing/i)
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /does not have a route and\/or endpoint available/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the route's spam mode is Quarantine, the message is spam and not manually queued" do
let(:route) { create(:route, server: server, spam_mode: "Quarantine") }
before do
inspection_result = double("Result", spam_score: server.spam_threshold + 1, threat: false, threat_message: nil, spam_checks: [])
allow(Postal::MessageInspection).to receive(:scan).and_return(inspection_result)
end
it "logs" do
processor.process
expect(logger).to have_logged(/message is spam and route says to quarantine spam message/i)
end
it "sets the message status to Held" do
processor.process
expect(message.reload.status).to eq "Held"
end
it "creates a Held delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Held", details: /message placed into quarantine/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the route's spam mode is Fail, the message is spam and not manually queued" do
let(:route) { create(:route, server: server, spam_mode: "Fail") }
before do
inspection_result = double("Result", spam_score: server.spam_threshold + 1, threat: false, threat_message: nil, spam_checks: [])
allow(Postal::MessageInspection).to receive(:scan).and_return(inspection_result)
end
it "logs" do
processor.process
expect(logger).to have_logged(/message is spam and route says to fail spam message/i)
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /message is spam and the route specifies it should be failed/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the route's mode is Accept" do
it "logs" do
processor.process
expect(logger).to have_logged(/route says to accept without endpoint/i)
end
it "sets the message status to Processed" do
processor.process
expect(message.reload.status).to eq "Processed"
end
it "creates a Processed delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Processed", details: /message has been accepted but not sent to any endpoints/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the route's mode is Hold" do
let(:route) { create(:route, server: server, mode: "Hold") }
context "when the message was queued manually" do
let(:queued_message) { create(:queued_message, :locked, server: server, message: message, manual: true) }
it "logs" do
processor.process
expect(logger).to have_logged(/route says to hold and message was queued manually/i)
end
it "sets the message status to Processed" do
processor.process
expect(message.reload.status).to eq "Processed"
end
it "creates a Processed delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Processed", details: /message has been processed/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the message was not queued manually" do
let(:queued_message) { create(:queued_message, :locked, server: server, message: message, manual: false) }
it "logs" do
processor.process
expect(logger).to have_logged(/route says to hold, marking as held/i)
end
it "sets the message status to Held" do
processor.process
expect(message.reload.status).to eq "Held"
end
it "creates a Held delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Held", details: /message has been accepted but not sent to any endpoints/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context "when the route's mode is Bounce" do
let(:route) { create(:route, server: server, mode: "Bounce") }
it "logs" do
processor.process
expect(logger).to have_logged(/route says to bounce/i)
end
it "sends a bounce" do
expect(BounceMessage).to receive(:new).with(server, queued_message.message)
processor.process
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /message has been bounced because/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the route's mode is Reject" do
let(:route) { create(:route, server: server, mode: "Reject") }
it "logs" do
processor.process
expect(logger).to have_logged(/route says to bounce/i)
end
it "sends a bounce" do
expect(BounceMessage).to receive(:new).with(server, queued_message.message)
processor.process
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /message has been bounced because/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the route's endpoint is an HTTP endpoint" do
let(:endpoint) { create(:http_endpoint, server: server) }
let(:route) { create(:route, server: server, mode: "Endpoint", endpoint: endpoint) }
it "gets a sender from the state and sends the message to it" do
http_sender_double = double("HTTPSender")
expect(http_sender_double).to receive(:send_message).with(queued_message.message).and_return(Postal::SendResult.new)
expect(state).to receive(:sender_for).with(Postal::HTTPSender, endpoint).and_return(http_sender_double)
processor.process
end
end
context "when the route's endpoint is an SMTP endpoint" do
let(:endpoint) { create(:smtp_endpoint, server: server) }
let(:route) { create(:route, server: server, mode: "Endpoint", endpoint: endpoint) }
it "gets a sender from the state and sends the message to it" do
smtp_sender_double = double("SMTPSender")
expect(smtp_sender_double).to receive(:send_message).with(queued_message.message).and_return(Postal::SendResult.new)
expect(state).to receive(:sender_for).with(Postal::SMTPSender, message.recipient_domain, nil, { servers: [endpoint] }).and_return(smtp_sender_double)
processor.process
end
end
context "when the route's endpoint is an Address endpoint" do
let(:endpoint) { create(:address_endpoint, server: server) }
let(:route) { create(:route, server: server, mode: "Endpoint", endpoint: endpoint) }
it "gets a sender from the state and sends the message to it" do
smtp_sender_double = double("SMTPSender")
expect(smtp_sender_double).to receive(:send_message).with(queued_message.message).and_return(Postal::SendResult.new)
expect(state).to receive(:sender_for).with(Postal::SMTPSender, endpoint.domain, nil, { force_rcpt_to: endpoint.address }).and_return(smtp_sender_double)
processor.process
end
end
context "when the route's endpoint is an unknown endpoint" do
let(:route) { create(:route, server: server, mode: "Endpoint", endpoint: create(:webhook, server: server)) }
it "logs" do
processor.process
expect(logger).to have_logged(/invalid endpoint for route/i)
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /invalid endpoint for route/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the message has been sent to a sender" do
let(:endpoint) { create(:smtp_endpoint, server: server) }
let(:route) { create(:route, server: server, mode: "Endpoint", endpoint: endpoint) }
let(:send_result) do
Postal::SendResult.new do |result|
result.type = "Sent"
result.details = "Sent successfully"
end
end
before do
smtp_sender_mock = double("SMTPSender")
allow(Postal::SMTPSender).to receive(:new).and_return(smtp_sender_mock)
allow(smtp_sender_mock).to receive(:start)
allow(smtp_sender_mock).to receive(:finish)
allow(smtp_sender_mock).to receive(:send_message).and_return(send_result)
end
context "when the sender returns a HardFail and bounces are suppressed" do
before do
send_result.type = "HardFail"
send_result.suppress_bounce = true
end
it "logs" do
processor.process
expect(logger).to have_logged(/suppressing bounce message after hard fail/)
end
it "does not send a bounce" do
allow(BounceMessage).to receive(:new)
processor.process
expect(BounceMessage).to_not have_received(:new)
end
end
context "when the sender returns a HardFail and bounces should be sent" do
before do
send_result.type = "HardFail"
send_result.details = "Failed to send message"
end
it "logs" do
processor.process
expect(logger).to have_logged(/sending a bounce because message hard failed/)
end
it "sends a bounce" do
expect(BounceMessage).to receive(:new).with(server, queued_message.message)
processor.process
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a delivery with the details and a suffix about the bounce message" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /Failed to send message. Sent bounce message to sender \(see message <msg:\d+>\)/i)
end
end
it "creates a delivery with the result from the sender" do
send_result.output = "some output here"
send_result.secure = true
send_result.log_id = "12345"
send_result.time = 2.32
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Sent",
details: "Sent successfully",
output: "some output here",
sent_with_ssl: true,
log_id: "12345",
time: 2.32)
end
context "when the sender wants to retry" do
before do
send_result.type = "SoftFail"
send_result.retry = true
end
it "logs" do
processor.process
expect(logger).to have_logged(/message requeued for trying later, at/i)
end
it "sets the message status to SoftFail" do
processor.process
expect(message.reload.status).to eq "SoftFail"
end
it "updates the queued message with a new retry time" do
Timecop.freeze do
retry_time = 5.minutes.from_now.change(usec: 0)
processor.process
expect(queued_message.reload.retry_after).to eq retry_time
end
end
it "allocates a new IP address to send the message from and updates the queued message" do
expect(queued_message).to receive(:allocate_ip_address)
processor.process
end
it "does not remove the queued message" do
processor.process
expect(queued_message.reload).to be_present
end
end
context "when the sender does not want a retry" do
it "logs" do
processor.process
expect(logger).to have_logged(/message processing completed/i)
end
it "sets the message status to Sent" do
processor.process
expect(message.reload.status).to eq "Sent"
end
it "marks the endpoint as used" do
route.endpoint.update!(last_used_at: nil)
Timecop.freeze do
expect { processor.process }.to change { route.endpoint.reload.last_used_at.to_i }.from(0).to(Time.now.to_i)
end
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context "when an exception occurrs during processing" do
let(:endpoint) { create(:smtp_endpoint, server: server) }
let(:route) { create(:route, server: server, mode: "Endpoint", endpoint: endpoint) }
before do
smtp_sender_mock = double("SMTPSender")
allow(Postal::SMTPSender).to receive(:new).and_return(smtp_sender_mock)
allow(smtp_sender_mock).to receive(:start)
allow(smtp_sender_mock).to receive(:finish)
allow(smtp_sender_mock).to receive(:send_message) do
1 / 0
end
end
it "logs" do
processor.process
expect(logger).to have_logged(/internal error: ZeroDivisionError/i)
end
it "creates an Error delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Error", details: /internal error/i)
end
it "marks the message for retrying later" do
processor.process
expect(queued_message.reload.retry_after).to be_present
end
end
end
end

عرض الملف

@@ -0,0 +1,94 @@
# frozen_string_literal: true
require "rails_helper"
module MessageDequeuer
RSpec.describe InitialProcessor do
let(:server) { create(:server) }
let(:logger) { TestLogger.new }
let(:route) { create(:route, server: server) }
let(:message) { MessageFactory.incoming(server, route: route) }
let(:queued_message) { create(:queued_message, :locked, message: message) }
subject(:processor) { described_class.new(queued_message, logger: logger) }
it "has state when not given any" do
expect(processor.state).to be_a State
end
context "when associated message does not exist" do
let(:queued_message) { create(:queued_message, :locked, message_id: 12_345) }
it "logs" do
processor.process
expect(logger).to have_logged(/unqueue because backend message has been removed/)
end
it "removes from queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the queued message is not ready for processing" do
let(:queued_message) { create(:queued_message, :locked, message: message, retry_after: 1.hour.from_now) }
it "logs" do
processor.process
expect(logger).to have_logged(/skipping because message isn't ready for processing/)
end
it "unlocks and keeps the queued message" do
processor.process
expect(queued_message.reload).to_not be_locked
end
end
context "when there are no other batchable messages" do
it "calls the single message processor for the initial message" do
expect(SingleMessageProcessor).to receive(:process).with(queued_message,
logger: logger,
state: processor.state)
processor.process
end
end
context "when there are batchable messages" do
before do
@message2 = MessageFactory.incoming(server, route: route)
@queued_message2 = create(:queued_message, message: @message2)
@message3 = MessageFactory.incoming(server, route: route)
@queued_message3 = create(:queued_message, message: @message3)
end
it "calls the single message process for the initial message and all batchable messages" do
[queued_message, @queued_message2, @queued_message3].each do |msg|
expect(SingleMessageProcessor).to receive(:process).with(msg,
logger: logger,
state: processor.state)
end
processor.process
end
end
context "when an error occurs while finding batchable messages" do
before do
allow(queued_message).to receive(:batchable_messages) { 1 / 0 }
end
it "unlocks the queued message and raises the error" do
expect { processor.process }.to raise_error(ZeroDivisionError)
expect(queued_message.reload).to_not be_locked
end
end
context "when finished" do
it "notifies the state that processing is complete" do
expect(processor.state).to receive(:finished)
processor.process
end
end
end
end

عرض الملف

@@ -0,0 +1,542 @@
# frozen_string_literal: true
require "rails_helper"
module MessageDequeuer
RSpec.describe OutgoingMessageProcessor do
let(:server) { create(:server) }
let(:state) { State.new }
let(:logger) { TestLogger.new }
let(:domain) { create(:domain, server: server) }
let(:credential) { create(:credential, server: server) }
let(:message) { MessageFactory.outgoing(server, domain: domain, credential: credential) }
let(:queued_message) { create(:queued_message, :locked, message: message) }
subject(:processor) { described_class.new(queued_message, logger: logger, state: state) }
context "when the domain belonging to the message no longer exists" do
let(:message) { MessageFactory.outgoing(server, domain: nil, credential: credential) }
it "logs" do
processor.process
expect(logger).to have_logged(/message has no domain/)
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /Message's domain no longer exist/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the message has no rcpt to address" do
before do
message.update(rcpt_to: "")
end
it "logs" do
processor.process
expect(logger).to have_logged(/message has no 'to' address/)
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /Message doesn't have an RCPT to/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the message has a x-postal-tag header" do
let(:message) do
MessageFactory.outgoing(server, domain: domain) do |_msg, mail|
mail["x-postal-tag"] = "example-tag"
end
end
it "logs" do
processor.process
expect(logger).to have_logged(/added tag: example-tag/)
end
it "adds the tag to the message object" do
processor.process
expect(message.reload.tag).to eq("example-tag")
end
end
context "when the credential says to hold the message" do
let(:credential) { create(:credential, hold: true) }
context "when the message was queued manually" do
let(:queued_message) { create(:queued_message, :locked, message: message, manual: true) }
it "does not hold the message" do
processor.process
deliveries = message.deliveries.find { |d| d.status == "Held" }
expect(deliveries).to be_nil
end
end
context "when the message was not queued manually" do
it "logs" do
processor.process
expect(logger).to have_logged(/credential wants us to hold messages/)
end
it "sets the message status to Held" do
processor.process
expect(message.reload.status).to eq "Held"
end
it "creates a Held delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Held", details: /Credential is configured to hold all messages authenticated/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context "when the rcpt address is on the suppression list" do
before do
server.message_db.suppression_list.add(:recipient, message.rcpt_to, reason: "testing")
end
context "when the message was queued manually" do
let(:queued_message) { create(:queued_message, :locked, message: message, manual: true) }
it "does not hold the message" do
processor.process
deliveries = message.deliveries.find { |d| d.status == "Held" }
expect(deliveries).to be_nil
end
end
context "when the message was not queued manually" do
it "logs" do
processor.process
expect(logger).to have_logged(/recipient is on the suppression list/)
end
it "sets the message status to Held" do
processor.process
expect(message.reload.status).to eq "Held"
end
it "creates a Held delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Held", details: /Recipient \(#{message.rcpt_to}\) is on the suppression list/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context "when the message content has not been parsed" do
it "parses the content" do
mocked_parser = double("Result")
allow(mocked_parser).to receive(:actioned?).and_return(false)
allow(mocked_parser).to receive(:tracked_links).and_return(0)
allow(mocked_parser).to receive(:tracked_images).and_return(0)
expect(Postal::MessageParser).to receive(:new).with(kind_of(Postal::MessageDB::Message)).and_return(mocked_parser)
processor.process
reloaded_message = message.reload
expect(reloaded_message.parsed).to eq 1
expect(reloaded_message.tracked_links).to eq 0
expect(reloaded_message.tracked_images).to eq 0
end
end
context "when the server has an outbound spam threshold configured" do
let(:server) { create(:server, outbound_spam_threshold: 5.0) }
it "logs" do
processor.process
expect(logger).to have_logged(/inspecting message/)
expect(logger).to have_logged(/message inspected successfully/)
end
it "inspects the message" do
inspection_result = double("Result", spam_score: 1.0, threat: false, threat_message: nil, spam_checks: [])
expect(Postal::MessageInspection).to receive(:scan).and_return(inspection_result)
processor.process
end
context "when the message spam score is higher than the threshold" do
before do
inspection_result = double("Result", spam_score: 6.0, threat: false, threat_message: nil, spam_checks: [])
allow(Postal::MessageInspection).to receive(:scan).and_return(inspection_result)
end
it "logs" do
processor.process
expect(logger).to have_logged(/message is spam/)
end
it "sets the spam boolean on the message" do
processor.process
expect(message.reload.spam).to be true
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /Message is likely spam. Threshold is 5.0 and the message scored 6.0/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context "when the server does not have a outbound spam threshold configured" do
it "does not inspect the message" do
expect(Postal::MessageInspection).to_not receive(:scan)
processor.process
end
end
context "when the message already has an x-postal-msgid header" do
let(:message) do
MessageFactory.outgoing(server, domain: domain, credential: credential) do |_, mail|
mail["x-postal-msgid"] = "existing-id"
end
end
it "does not another one" do
processor.process
expect(message.reload.headers["x-postal-msgid"]).to eq ["existing-id"]
end
it "does not add dkim headers" do
processor.process
expect(message.reload.headers["dkim-signature"]).to be_nil
end
end
context "when the message does not have a x-postal-msgid header" do
it "adds it" do
processor.process
expect(message.reload.headers["x-postal-msgid"]).to match [match(/[a-zA-Z0-9]{12}/)]
end
it "adds a dkim header" do
processor.process
expect(message.reload.headers["dkim-signature"]).to match [match(/\Av=1; a=rsa-sha256/)]
end
end
context "when the server has exceeded its send limit" do
let(:server) { create(:server, send_limit: 5) }
before do
5.times { server.message_db.live_stats.increment("outgoing") }
end
it "updates the time the limit was exceeded" do
expect { processor.process }.to change { server.reload.send_limit_exceeded_at }.from(nil).to(kind_of(Time))
end
it "logs" do
processor.process
expect(logger).to have_logged(/server send limit has been exceeded/)
end
it "sets the message status to Held" do
processor.process
expect(message.reload.status).to eq "Held"
end
it "creates a Held delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Held", details: /Message held because send limit \(5\) has been reached/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the server is approaching its send limit" do
let(:server) { create(:server, send_limit: 10) }
before do
9.times { server.message_db.live_stats.increment("outgoing") }
end
it "updates the time the limit was being approached" do
expect { processor.process }.to change { server.reload.send_limit_approaching_at }.from(nil).to(kind_of(Time))
end
it "does not set the exceeded time" do
expect { processor.process }.to_not change { server.reload.send_limit_exceeded_at } # rubocop:disable Lint/AmbiguousBlockAssociation
end
end
context "when the server is not exceeded or approaching its limit" do
let(:server) { create(:server, :exceeded_send_limit, send_limit: 10) }
it "clears the approaching and exceeded limits" do
processor.process
server.reload
expect(server.send_limit_approaching_at).to be_nil
expect(server.send_limit_exceeded_at).to be_nil
end
end
context "when the server is in development mode" do
let(:server) { create(:server, mode: "Development") }
context "when the message was queued manually" do
let(:queued_message) { create(:queued_message, :locked, message: message, manual: true) }
it "does not hold the message" do
processor.process
deliveries = message.deliveries.find { |d| d.status == "Held" }
expect(deliveries).to be_nil
end
end
context "when the message was not queued manually" do
it "logs" do
processor.process
expect(logger).to have_logged(/server is in development mode/)
end
it "sets the message status to Held" do
processor.process
expect(message.reload.status).to eq "Held"
end
it "creates a Held delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Held", details: /Server is in development mode/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context "when there are no other impediments" do
let(:send_result) do
Postal::SendResult.new do |r|
r.type = "Sent"
end
end
before do
mocked_sender = double("SMTPSender")
allow(mocked_sender).to receive(:send_message).and_return(send_result)
allow(state).to receive(:sender_for).and_return(mocked_sender)
end
it "increments the live stats" do
expect { processor.process }.to change { server.message_db.live_stats.total(60) }.from(0).to(1)
end
context "when there is an IP address assigned to the queued message" do
let(:ip) { create(:ip_address) }
let(:queued_message) { create(:queued_message, :locked, message: message, ip_address: ip) }
it "gets a sender from the state and sends the message to it" do
mocked_sender = double("SMTPSender")
expect(mocked_sender).to receive(:send_message).with(queued_message.message).and_return(send_result)
expect(state).to receive(:sender_for).with(Postal::SMTPSender, message.recipient_domain, ip).and_return(mocked_sender)
processor.process
end
end
context "when there is no IP address assigned to the queued message" do
it "gets a sender from the state and sends the message to it" do
mocked_sender = double("SMTPSender")
expect(mocked_sender).to receive(:send_message).with(queued_message.message).and_return(send_result)
expect(state).to receive(:sender_for).with(Postal::SMTPSender, message.recipient_domain, nil).and_return(mocked_sender)
processor.process
end
end
context "when the message hard fails" do
before do
send_result.type = "HardFail"
end
context "when the recipient has got no hard fails in the last 24 hours" do
it "does not add to the suppression list" do
processor.process
expect(server.message_db.suppression_list.all_with_pagination(1)[:total]).to eq 0
end
end
context "when the recipient has more than one hard fail in the last 24 hours" do
before do
2.times do
MessageFactory.outgoing(server, domain: domain, credential: credential) do |msg|
msg.status = "HardFail"
end
end
end
it "logs" do
processor.process
expect(logger).to have_logged(/added #{message.rcpt_to} to suppression list because 2 hard fails in 24 hours/i)
end
it "adds the recipient to the suppression list" do
processor.process
entry = server.message_db.suppression_list.get(:recipient, message.rcpt_to)
expect(entry).to match hash_including(
"address" => message.rcpt_to,
"type" => "recipient",
"reason" => "too many hard fails"
)
end
end
end
context "when the message is sent manually and the recipient is on the suppression list" do
let(:queued_message) { create(:queued_message, :locked, message: message, manual: true) }
before do
server.message_db.suppression_list.add(:recipient, message.rcpt_to, reason: "testing")
end
it "logs" do
processor.process
expect(logger).to have_logged(/removed #{message.rcpt_to} from suppression list/)
end
it "removes them from the suppression list" do
processor.process
expect(server.message_db.suppression_list.get(:recipient, message.rcpt_to)).to be_nil
end
it "adds the details to the delivery details" do
processor.process
delivery = message.deliveries.last
expect(delivery.details).to include("Recipient removed from suppression list")
end
end
it "creates a delivery with the appropriate details" do
send_result.details = "Sent successfully to mx.example.com"
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Sent", details: "Sent successfully to mx.example.com")
end
context "if the message should be retried" do
before do
send_result.type = "SoftFail"
send_result.retry = true
end
it "logs" do
processor.process
expect(logger).to have_logged(/message requeued for trying later/)
end
it "sets the message status to SoftFail" do
processor.process
expect(message.reload.status).to eq "SoftFail"
end
it "updates the retry time on the queued message" do
Timecop.freeze do
retry_time = 5.minutes.from_now.change(usec: 0)
processor.process
expect(queued_message.reload.retry_after).to eq retry_time
end
end
end
context "if the message should not be retried" do
it "logs" do
processor.process
expect(logger).to have_logged(/message processing complete/)
end
it "sets the message status to Sent" do
processor.process
expect(message.reload.status).to eq "Sent"
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context "when an exception occurrs during processing" do
before do
smtp_sender_mock = double("SMTPSender")
allow(Postal::SMTPSender).to receive(:new).and_return(smtp_sender_mock)
allow(smtp_sender_mock).to receive(:start)
allow(smtp_sender_mock).to receive(:send_message) do
1 / 0
end
end
it "logs" do
processor.process
expect(logger).to have_logged(/internal error: ZeroDivisionError/i)
end
it "creates an Error delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Error", details: /internal error/i)
end
it "marks the message for retrying later" do
processor.process
expect(queued_message.reload.retry_after).to be_present
end
end
end
end

عرض الملف

@@ -0,0 +1,134 @@
# frozen_string_literal: true
require "rails_helper"
module MessageDequeuer
RSpec.describe SingleMessageProcessor do
let(:server) { create(:server) }
let(:state) { State.new }
let(:logger) { TestLogger.new }
let(:route) { create(:route, server: server) }
let(:message) { MessageFactory.incoming(server, route: route) }
let(:queued_message) { create(:queued_message, :locked, message: message) }
subject(:processor) { described_class.new(queued_message, logger: logger, state: state) }
context "when the server is suspended" do
before do
allow(queued_message.server).to receive(:suspended?).and_return(true)
end
it "logs" do
processor.process
expect(logger).to have_logged(/server is suspended/)
end
it "sets the message status to Held" do
processor.process
expect(message.reload.status).to eq "Held"
end
it "creates a Held delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "Held", details: /server has been suspended/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the number of attempts is more than the maximum" do
let(:queued_message) { create(:queued_message, :locked, message: message, attempts: Postal.config.general.maximum_delivery_attempts + 1) }
it "logs" do
processor.process
expect(logger).to have_logged(/message has reached maximum number of attempts/)
end
it "sends a bounce to the sender" do
expect(BounceMessage).to receive(:new).with(server, queued_message.message)
processor.process
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /maximum number of delivery attempts.*bounce sent to sender/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the message raw data has been removed" do
before do
message.raw_table = nil
message.save
end
it "logs" do
processor.process
expect(logger).to have_logged(/raw message has been removed/)
end
it "sets the message status to HardFail" do
processor.process
expect(message.reload.status).to eq "HardFail"
end
it "creates a HardFail delivery" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(status: "HardFail", details: /Raw message has been removed/i)
end
it "removes the queued message" do
processor.process
expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when the message is incoming" do
it "calls the incoming message processor" do
expect(IncomingMessageProcessor).to receive(:new).with(queued_message,
logger: logger,
state: processor.state)
processor.process
end
it "does not call the outgoing message processor" do
expect(OutgoingMessageProcessor).to_not receive(:process)
processor.process
end
end
context "when the message is outgoing" do
let(:message) { MessageFactory.outgoing(server) }
it "calls the outgoing message processor" do
expect(OutgoingMessageProcessor).to receive(:process).with(queued_message,
logger: logger,
state: processor.state)
processor.process
end
it "does not call the incoming message processor" do
expect(IncomingMessageProcessor).to_not receive(:process)
processor.process
end
end
end
end

عرض الملف

@@ -0,0 +1,42 @@
# frozen_string_literal: true
require "rails_helper"
module MessageDequeuer
RSpec.describe State do
subject(:state) { described_class.new }
describe "#send_result" do
it "can be get and set" do
result = instance_double(Postal::SendResult)
state.send_result = result
expect(state.send_result).to be result
end
end
describe "#sender_for" do
it "returns a instance of the given sender initialized with the args" do
sender = state.sender_for(Postal::HTTPSender, "1234")
expect(sender).to be_a Postal::HTTPSender
end
it "returns a cached sender on subsequent calls" do
sender = state.sender_for(Postal::HTTPSender, "1234")
expect(state.sender_for(Postal::HTTPSender, "1234")).to be sender
end
end
describe "#finished" do
it "calls finish on all cached senders" do
sender1 = state.sender_for(Postal::HTTPSender, "1234")
sender2 = state.sender_for(Postal::HTTPSender, "4444")
expect(sender1).to receive(:finish)
expect(sender2).to receive(:finish)
state.finished
end
end
end
end