Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ jobs:
run: docker compose pull mariadb
env:
POSTAL_IMAGE: ghcr.io/modalsource/postal:ci-${{ github.sha }}
- name: Prepare database
run: docker compose run --rm postal sh -c 'bundle exec rails db:prepare'
env:
POSTAL_IMAGE: ghcr.io/modalsource/postal:ci-${{ github.sha }}
- run: docker compose run --rm postal sh -c 'bundle exec rspec'
env:
POSTAL_IMAGE: ghcr.io/modalsource/postal:ci-${{ github.sha }}
Expand Down
5 changes: 3 additions & 2 deletions app/lib/message_dequeuer/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def hold_if_server_development_mode
return unless queued_message.server.mode == "Development"

log "server is in development mode, holding"
create_delivery "Held", details: "Server is in development mode."
create_delivery "Held", details: "Server is in development mode.", ip_address_id: queued_message.ip_address_id
remove_from_queue
stop_processing
end
Expand All @@ -80,7 +80,8 @@ def log_sender_result
output: @result.output&.strip,
sent_with_ssl: @result.secure,
log_id: @result.log_id,
time: @result.time
time: @result.time,
ip_address_id: queued_message.ip_address_id
end

def handle_exception(exception)
Expand Down
16 changes: 9 additions & 7 deletions app/lib/message_dequeuer/outgoing_message_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def check_domain
return if queued_message.message.domain

log "message has no domain, hard failing"
create_delivery "HardFail", details: "Message's domain no longer exist"
create_delivery "HardFail", details: "Message's domain no longer exist", ip_address_id: queued_message.ip_address_id
remove_from_queue
stop_processing
end
Expand All @@ -44,7 +44,7 @@ def check_rcpt_to
return unless queued_message.message.rcpt_to.blank?

log "message has no 'to' address, hard failing"
create_delivery "HardFail", details: "Message doesn't have an RCPT to"
create_delivery "HardFail", details: "Message doesn't have an RCPT to", ip_address_id: queued_message.ip_address_id
remove_from_queue
stop_processing
end
Expand All @@ -63,7 +63,7 @@ def hold_if_credential_is_set_to_hold
return unless queued_message.message.credential.hold?

log "credential wants us to hold messages, holding"
create_delivery "Held", details: "Credential is configured to hold all messages authenticated by it."
create_delivery "Held", details: "Credential is configured to hold all messages authenticated by it.", ip_address_id: queued_message.ip_address_id
remove_from_queue
stop_processing
end
Expand All @@ -73,7 +73,7 @@ def hold_if_recipient_on_suppression_list
return unless sl = queued_message.server.message_db.suppression_list.get(:recipient, queued_message.message.rcpt_to)

log "recipient is on the suppression list, holding"
create_delivery "Held", details: "Recipient (#{queued_message.message.rcpt_to}) is on the suppression list (reason: #{sl['reason']})"
create_delivery "Held", details: "Recipient (#{queued_message.message.rcpt_to}) is on the suppression list (reason: #{sl['reason']})", ip_address_id: queued_message.ip_address_id
remove_from_queue
stop_processing
end
Expand Down Expand Up @@ -121,7 +121,8 @@ def inspect_message
log "added recipient to suppression list", recipient: queued_message.message.rcpt_to, reason: "Truemail validation failed"

create_delivery "HardFail",
details: "Email address validation failed: #{result.validation_message}"
details: "Email address validation failed: #{result.validation_message}",
ip_address_id: queued_message.ip_address_id
remove_from_queue
stop_processing
return
Expand All @@ -140,7 +141,8 @@ def fail_if_spam
log "message is spam (#{queued_message.message.spam_score}), hard failing", server_threshold: queued_message.server.outbound_spam_threshold
create_delivery "HardFail",
details: "Message is likely spam. Threshold is #{queued_message.server.outbound_spam_threshold} and " \
"the message scored #{queued_message.message.spam_score}."
"the message scored #{queued_message.message.spam_score}.",
ip_address_id: queued_message.ip_address_id
remove_from_queue
stop_processing
end
Expand All @@ -156,7 +158,7 @@ def check_send_limits
# If we're over the limit, we're going to be holding this message
log "server send limit has been exceeded, holding", send_limit: queued_message.server.send_limit
queued_message.server.update_columns(send_limit_exceeded_at: Time.now, send_limit_approaching_at: nil)
create_delivery "Held", details: "Message held because send limit (#{queued_message.server.send_limit}) has been reached."
create_delivery "Held", details: "Message held because send limit (#{queued_message.server.send_limit}) has been reached.", ip_address_id: queued_message.ip_address_id
remove_from_queue
stop_processing
elsif queued_message.server.send_limit_approaching?
Expand Down
10 changes: 10 additions & 0 deletions app/views/messages/_deliveries.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
.noData.noData--clean
%h2.noData__text No delivery attempts yet.
- else
- ip_address_ids = message.deliveries.map(&:ip_address_id).compact.uniq
- ip_addresses_by_id = IPAddress.where(id: ip_address_ids).index_by(&:id)
- for delivery in message.deliveries.reverse
%li.deliveryList__item
.deliveryList__top
Expand All @@ -38,6 +40,14 @@
- if delivery.sent_with_ssl
= image_tag 'icons/lock.svg', :class => 'deliveryList__secure'
%span.label.label--large{:class => "label--messageStatus-#{delivery.status.underscore}"}= delivery.status.underscore.humanize
- if delivery.ip_address_id
- ip_address = ip_addresses_by_id[delivery.ip_address_id]
- if ip_address
%p.deliveryList__info
%strong IP Address:
= ip_address.ipv4
- if ip_address.hostname
(#{ip_address.hostname})
- if delivery.details
%p.deliveryList__error= format_delivery_details(@server, delivery.details)
- if delivery.log_id || delivery.output
Expand Down
3 changes: 2 additions & 1 deletion lib/postal/message_db/delivery.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ def webhook_hash
output: output.to_s.dup.force_encoding("UTF-8").scrub.truncate(512),
sent_with_ssl: sent_with_ssl,
timestamp: @attributes["timestamp"],
time: time
time: time,
ip_address_id: ip_address_id
}
end

Expand Down
4 changes: 3 additions & 1 deletion lib/postal/message_db/migrations/03_create_deliveries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ def up
details: "varchar(512) DEFAULT NULL",
sent_with_ssl: "tinyint(1) DEFAULT 0",
log_id: "varchar(100) DEFAULT NULL",
timestamp: "decimal(18,6) DEFAULT NULL"
timestamp: "decimal(18,6) DEFAULT NULL",
time: "decimal(8,2) DEFAULT NULL",
ip_address_id: "int(11) DEFAULT NULL"
},
indexes: {
on_message_id: "`message_id`"
Expand Down
4 changes: 4 additions & 0 deletions lib/postal/message_db/migrations/11_add_time_to_deliveries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module Migrations
class AddTimeToDeliveries < Postal::MessageDB::Migration

def up
# Check if column already exists (for fresh installs that include it in CREATE TABLE)
result = @database.query("SELECT COUNT(*) as count FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '#{@database.database_name}' AND TABLE_NAME = 'deliveries' AND COLUMN_NAME = 'time'").first
return if result["count"] > 0

@database.query("ALTER TABLE `#{@database.database_name}`.`deliveries` ADD COLUMN `time` decimal(8,2)")
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Postal
module MessageDB
module Migrations
class AddIPAddressIdToDeliveries < Postal::MessageDB::Migration

def up
# Check if column already exists (for fresh installs that include it in CREATE TABLE)
result = @database.query("SELECT COUNT(*) as count FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '#{@database.database_name}' AND TABLE_NAME = 'deliveries' AND COLUMN_NAME = 'ip_address_id'").first
return if result["count"] > 0

@database.query("ALTER TABLE `#{@database.database_name}`.`deliveries` ADD COLUMN `ip_address_id` int(11) NULL")
end

def down
@database.query("ALTER TABLE `#{@database.database_name}`.`deliveries` DROP COLUMN `ip_address_id`")
end

end
end
end
end
78 changes: 78 additions & 0 deletions spec/lib/message_dequeuer/outgoing_message_processor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,84 @@ module MessageDequeuer
end
end

context "when ip_address_id is set on the queued message" do
let(:organization) { create(:organization) }
let(:ip_pool) { create(:ip_pool, :with_ip_address) }
let(:ip_address) { ip_pool.ip_addresses.first }
let(:server) { create(:server, ip_pool: ip_pool, organization: organization) }
let(:queued_message) { create(:queued_message, :locked, message: message, ip_address: ip_address) }
let(:send_result) do
SendResult.new do |r|
r.type = "Sent"
end
end

before do
organization.ip_pools << ip_pool
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 "stores ip_address_id in the delivery for Sent status" do
processor.process
delivery = message.deliveries.last
expect(delivery.ip_address_id).to eq(ip_address.id)
end

context "when message has no domain and fails" do
let(:message) { MessageFactory.outgoing(server, domain: nil, credential: credential) }

it "stores ip_address_id in the delivery for HardFail status" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(
status: "HardFail",
ip_address_id: ip_address.id
)
end
end

context "when message has no rcpt_to and fails" do
before do
message.update(rcpt_to: "")
end

it "stores ip_address_id in the delivery for HardFail status" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(
status: "HardFail",
ip_address_id: ip_address.id
)
end
end

context "when message is spam and fails" do
before do
server.update(outbound_spam_threshold: 5.0)
inspection_result = double("Result",
spam_score: 6.0,
threat: false,
threat_message: nil,
spam_checks: [],
validation_failed: false,
validation_message: nil
)
allow(Postal::MessageInspection).to receive(:scan).and_return(inspection_result)
end

it "stores ip_address_id in the delivery for HardFail status" do
processor.process
delivery = message.deliveries.last
expect(delivery).to have_attributes(
status: "HardFail",
ip_address_id: ip_address.id
)
end
end
end

context "when an exception occurrs during processing" do
before do
smtp_sender_mock = double("SMTPSender")
Expand Down