مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-12-01 05:43:04 +00:00
refactor: move senders in to app/senders/
هذا الالتزام موجود في:
@@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "resolv"
|
||||||
|
|
||||||
class DNSResolver
|
class DNSResolver
|
||||||
|
|
||||||
attr_reader :nameservers
|
attr_reader :nameservers
|
||||||
|
|||||||
@@ -163,11 +163,11 @@ module MessageDequeuer
|
|||||||
|
|
||||||
case queued_message.message.endpoint
|
case queued_message.message.endpoint
|
||||||
when SMTPEndpoint
|
when SMTPEndpoint
|
||||||
sender = @state.sender_for(Postal::SMTPSender, queued_message.message.recipient_domain, nil, servers: [queued_message.message.endpoint])
|
sender = @state.sender_for(SMTPSender, queued_message.message.recipient_domain, nil, servers: [queued_message.message.endpoint])
|
||||||
when HTTPEndpoint
|
when HTTPEndpoint
|
||||||
sender = @state.sender_for(Postal::HTTPSender, queued_message.message.endpoint)
|
sender = @state.sender_for(HTTPSender, queued_message.message.endpoint)
|
||||||
when AddressEndpoint
|
when AddressEndpoint
|
||||||
sender = @state.sender_for(Postal::SMTPSender, queued_message.message.endpoint.domain, nil, force_rcpt_to: queued_message.message.endpoint.address)
|
sender = @state.sender_for(SMTPSender, queued_message.message.endpoint.domain, nil, force_rcpt_to: queued_message.message.endpoint.address)
|
||||||
else
|
else
|
||||||
log "invalid endpoint for route (#{queued_message.message.endpoint_type})"
|
log "invalid endpoint for route (#{queued_message.message.endpoint_type})"
|
||||||
create_delivery "HardFail", details: "Invalid endpoint for route."
|
create_delivery "HardFail", details: "Invalid endpoint for route."
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ module MessageDequeuer
|
|||||||
@result = @state.send_result
|
@result = @state.send_result
|
||||||
return if @result
|
return if @result
|
||||||
|
|
||||||
sender = @state.sender_for(Postal::SMTPSender,
|
sender = @state.sender_for(SMTPSender,
|
||||||
queued_message.message.recipient_domain,
|
queued_message.message.recipient_domain,
|
||||||
queued_message.ip_address)
|
queued_message.ip_address)
|
||||||
|
|
||||||
|
|||||||
14
app/senders/base_sender.rb
Normal file
14
app/senders/base_sender.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BaseSender
|
||||||
|
|
||||||
|
def start
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_message(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
134
app/senders/http_sender.rb
Normal file
134
app/senders/http_sender.rb
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class HTTPSender < BaseSender
|
||||||
|
|
||||||
|
def initialize(endpoint, options = {})
|
||||||
|
super()
|
||||||
|
@endpoint = endpoint
|
||||||
|
@options = options
|
||||||
|
@log_id = Nifty::Utils::RandomString.generate(length: 8).upcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_message(message)
|
||||||
|
start_time = Time.now
|
||||||
|
result = SendResult.new
|
||||||
|
result.log_id = @log_id
|
||||||
|
|
||||||
|
request_options = {}
|
||||||
|
request_options[:sign] = true
|
||||||
|
request_options[:timeout] = @endpoint.timeout || 5
|
||||||
|
case @endpoint.encoding
|
||||||
|
when "BodyAsJSON"
|
||||||
|
request_options[:json] = parameters(message, flat: false).to_json
|
||||||
|
when "FormData"
|
||||||
|
request_options[:params] = parameters(message, flat: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
log "Sending request to #{@endpoint.url}"
|
||||||
|
response = Postal::HTTP.post(@endpoint.url, request_options)
|
||||||
|
result.secure = !!response[:secure] # rubocop:disable Style/DoubleNegation
|
||||||
|
result.details = "Received a #{response[:code]} from #{@endpoint.url}"
|
||||||
|
log " -> Received: #{response[:code]}"
|
||||||
|
if response[:body]
|
||||||
|
log " -> Body: #{response[:body][0, 255]}"
|
||||||
|
result.output = response[:body].to_s[0, 500].strip
|
||||||
|
end
|
||||||
|
if response[:code] >= 200 && response[:code] < 300
|
||||||
|
# This is considered a success
|
||||||
|
result.type = "Sent"
|
||||||
|
elsif response[:code] >= 500 && response[:code] < 600
|
||||||
|
# This is temporary. They might fix their server so it should soft fail.
|
||||||
|
result.type = "SoftFail"
|
||||||
|
result.retry = true
|
||||||
|
elsif response[:code].negative?
|
||||||
|
# Connection/SSL etc... errors
|
||||||
|
result.type = "SoftFail"
|
||||||
|
result.retry = true
|
||||||
|
result.connect_error = true
|
||||||
|
elsif response[:code] == 429
|
||||||
|
# Rate limit exceeded, treat as a hard fail and don't send bounces
|
||||||
|
result.type = "HardFail"
|
||||||
|
result.suppress_bounce = true
|
||||||
|
else
|
||||||
|
# This is permanent. Any other error isn't cool with us.
|
||||||
|
result.type = "HardFail"
|
||||||
|
end
|
||||||
|
result.time = (Time.now - start_time).to_f.round(2)
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def log(text)
|
||||||
|
Postal.logger.info text, id: @log_id, component: "http-sender"
|
||||||
|
end
|
||||||
|
|
||||||
|
def parameters(message, options = {})
|
||||||
|
case @endpoint.format
|
||||||
|
when "Hash"
|
||||||
|
hash = {
|
||||||
|
id: message.id,
|
||||||
|
rcpt_to: message.rcpt_to,
|
||||||
|
mail_from: message.mail_from,
|
||||||
|
token: message.token,
|
||||||
|
subject: message.subject,
|
||||||
|
message_id: message.message_id,
|
||||||
|
timestamp: message.timestamp.to_f,
|
||||||
|
size: message.size,
|
||||||
|
spam_status: message.spam_status,
|
||||||
|
bounce: message.bounce,
|
||||||
|
received_with_ssl: message.received_with_ssl,
|
||||||
|
to: message.headers["to"]&.last,
|
||||||
|
cc: message.headers["cc"]&.last,
|
||||||
|
from: message.headers["from"]&.last,
|
||||||
|
date: message.headers["date"]&.last,
|
||||||
|
in_reply_to: message.headers["in-reply-to"]&.last,
|
||||||
|
references: message.headers["references"]&.last,
|
||||||
|
html_body: message.html_body,
|
||||||
|
attachment_quantity: message.attachments.size,
|
||||||
|
auto_submitted: message.headers["auto-submitted"]&.last,
|
||||||
|
reply_to: message.headers["reply-to"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if @endpoint.strip_replies
|
||||||
|
hash[:plain_body], hash[:replies_from_plain_body] = Postal::ReplySeparator.separate(message.plain_body)
|
||||||
|
else
|
||||||
|
hash[:plain_body] = message.plain_body
|
||||||
|
end
|
||||||
|
|
||||||
|
if @endpoint.include_attachments?
|
||||||
|
if options[:flat]
|
||||||
|
message.attachments.each_with_index do |a, i|
|
||||||
|
hash["attachments[#{i}][filename]"] = a.filename
|
||||||
|
hash["attachments[#{i}][content_type]"] = a.content_type
|
||||||
|
hash["attachments[#{i}][size]"] = a.body.to_s.bytesize.to_s
|
||||||
|
hash["attachments[#{i}][data]"] = Base64.encode64(a.body.to_s)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
hash[:attachments] = message.attachments.map do |a|
|
||||||
|
{
|
||||||
|
filename: a.filename,
|
||||||
|
content_type: a.mime_type,
|
||||||
|
size: a.body.to_s.bytesize,
|
||||||
|
data: Base64.encode64(a.body.to_s)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
hash
|
||||||
|
when "RawMessage"
|
||||||
|
{
|
||||||
|
id: message.id,
|
||||||
|
rcpt_to: message.rcpt_to,
|
||||||
|
mail_from: message.mail_from,
|
||||||
|
message: Base64.encode64(message.raw_message),
|
||||||
|
base64: true,
|
||||||
|
size: message.size.to_i
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
20
app/senders/send_result.rb
Normal file
20
app/senders/send_result.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendResult
|
||||||
|
|
||||||
|
attr_accessor :type
|
||||||
|
attr_accessor :details
|
||||||
|
attr_accessor :retry
|
||||||
|
attr_accessor :output
|
||||||
|
attr_accessor :secure
|
||||||
|
attr_accessor :connect_error
|
||||||
|
attr_accessor :log_id
|
||||||
|
attr_accessor :time
|
||||||
|
attr_accessor :suppress_bounce
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@details = ""
|
||||||
|
yield self if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
291
app/senders/smtp_sender.rb
Normal file
291
app/senders/smtp_sender.rb
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SMTPSender < BaseSender
|
||||||
|
|
||||||
|
def initialize(domain, source_ip_address, options = {})
|
||||||
|
super()
|
||||||
|
@domain = domain
|
||||||
|
@source_ip_address = source_ip_address
|
||||||
|
@options = options
|
||||||
|
@smtp_client = nil
|
||||||
|
@connection_errors = []
|
||||||
|
@hostnames = []
|
||||||
|
@log_id = Nifty::Utils::RandomString.generate(length: 8).upcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
servers.each do |server|
|
||||||
|
if server.is_a?(SMTPEndpoint)
|
||||||
|
hostname = server.hostname
|
||||||
|
port = server.port || 25
|
||||||
|
ssl_mode = server.ssl_mode
|
||||||
|
elsif server.is_a?(Hash)
|
||||||
|
hostname = server[:hostname]
|
||||||
|
port = server[:port] || 25
|
||||||
|
ssl_mode = server[:ssl_mode] || "Auto"
|
||||||
|
else
|
||||||
|
hostname = server
|
||||||
|
port = 25
|
||||||
|
ssl_mode = "Auto"
|
||||||
|
end
|
||||||
|
|
||||||
|
@hostnames << hostname
|
||||||
|
[:aaaa, :a].each do |ip_type|
|
||||||
|
if @source_ip_address && @source_ip_address.ipv6.blank? && ip_type == :aaaa
|
||||||
|
# Don't try to use IPv6 if the IP address we're sending from doesn't support it.
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
@remote_ip = lookup_ip_address(ip_type, hostname)
|
||||||
|
if @remote_ip.nil?
|
||||||
|
if ip_type == :a
|
||||||
|
# As we can't resolve the last IP, we'll put this
|
||||||
|
@connection_errors << "Could not resolve #{hostname}"
|
||||||
|
end
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
smtp_client = Net::SMTP.new(@remote_ip, port)
|
||||||
|
smtp_client.open_timeout = Postal.config.smtp_client.open_timeout
|
||||||
|
smtp_client.read_timeout = Postal.config.smtp_client.read_timeout
|
||||||
|
smtp_client.tls_hostname = hostname
|
||||||
|
|
||||||
|
if @source_ip_address
|
||||||
|
# Set the source IP as appropriate
|
||||||
|
smtp_client.source_address = ip_type == :aaaa ? @source_ip_address.ipv6 : @source_ip_address.ipv4
|
||||||
|
end
|
||||||
|
|
||||||
|
case ssl_mode
|
||||||
|
when "Auto"
|
||||||
|
smtp_client.enable_starttls_auto(self.class.ssl_context_without_verify)
|
||||||
|
when "STARTTLS"
|
||||||
|
smtp_client.enable_starttls(self.class.ssl_context_with_verify)
|
||||||
|
when "TLS"
|
||||||
|
smtp_client.enable_tls(self.class.ssl_context_with_verify)
|
||||||
|
else
|
||||||
|
smtp_client.disable_starttls
|
||||||
|
smtp_client.disable_tls
|
||||||
|
end
|
||||||
|
|
||||||
|
smtp_client.start(@source_ip_address ? @source_ip_address.hostname : self.class.default_helo_hostname)
|
||||||
|
log "Connected to #{@remote_ip}:#{port} (#{hostname})"
|
||||||
|
rescue StandardError => e
|
||||||
|
if e.is_a?(OpenSSL::SSL::SSLError) && ssl_mode == "Auto"
|
||||||
|
log "SSL error (#{e.message}), retrying without SSL"
|
||||||
|
ssl_mode = nil
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
log "Cannot connect to #{@remote_ip}:#{port} (#{hostname}) (#{e.class}: #{e.message})"
|
||||||
|
@connection_errors << e.message unless @connection_errors.include?(e.message)
|
||||||
|
begin
|
||||||
|
smtp_client.finish
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
smtp_client = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if smtp_client
|
||||||
|
@smtp_client = smtp_client
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@connection_errors
|
||||||
|
end
|
||||||
|
|
||||||
|
def reconnect
|
||||||
|
log "Reconnecting"
|
||||||
|
begin
|
||||||
|
@smtp_client&.finish
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
start
|
||||||
|
end
|
||||||
|
|
||||||
|
def safe_rset
|
||||||
|
# Something went wrong sending the last email. Reset the connection if possible, else disconnect.
|
||||||
|
|
||||||
|
@smtp_client.rset
|
||||||
|
rescue StandardError
|
||||||
|
# Don't reconnect, this would be rather rude if we don't have any more emails to send.
|
||||||
|
begin
|
||||||
|
@smtp_client.finish
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_message(message, force_rcpt_to = nil)
|
||||||
|
start_time = Time.now
|
||||||
|
result = SendResult.new
|
||||||
|
result.log_id = @log_id
|
||||||
|
if @smtp_client && !@smtp_client.started?
|
||||||
|
# For some reason we had an SMTP connection but it's no longer connected.
|
||||||
|
# Make a new one.
|
||||||
|
start
|
||||||
|
end
|
||||||
|
|
||||||
|
if @smtp_client
|
||||||
|
result.secure = @smtp_client.secure_socket?
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
if message.bounce
|
||||||
|
mail_from = ""
|
||||||
|
elsif message.domain.return_path_status == "OK"
|
||||||
|
mail_from = "#{message.server.token}@#{message.domain.return_path_domain}"
|
||||||
|
else
|
||||||
|
mail_from = "#{message.server.token}@#{Postal.config.dns.return_path}"
|
||||||
|
end
|
||||||
|
if Postal.config.general.use_resent_sender_header
|
||||||
|
raw_message = "Resent-Sender: #{mail_from}\r\n" + message.raw_message
|
||||||
|
else
|
||||||
|
raw_message = message.raw_message
|
||||||
|
end
|
||||||
|
tries = 0
|
||||||
|
begin
|
||||||
|
if @smtp_client.nil?
|
||||||
|
log "-> No SMTP server available for #{@domain}"
|
||||||
|
log "-> Hostnames: #{@hostnames.inspect}"
|
||||||
|
log "-> Errors: #{@connection_errors.inspect}"
|
||||||
|
result.type = "SoftFail"
|
||||||
|
result.retry = true
|
||||||
|
result.details = "No SMTP servers were available for #{@domain}. Tried #{@hostnames.to_sentence}"
|
||||||
|
result.output = @connection_errors.join(", ")
|
||||||
|
result.connect_error = true
|
||||||
|
return result
|
||||||
|
else
|
||||||
|
@smtp_client.rset_errors
|
||||||
|
rcpt_to = force_rcpt_to || @options[:force_rcpt_to] || message.rcpt_to
|
||||||
|
log "Sending message #{message.server.id}::#{message.id} to #{rcpt_to}"
|
||||||
|
smtp_result = @smtp_client.send_message(raw_message, mail_from, [rcpt_to])
|
||||||
|
end
|
||||||
|
rescue Errno::ECONNRESET, Errno::EPIPE, OpenSSL::SSL::SSLError
|
||||||
|
raise unless (tries += 1) < 2
|
||||||
|
|
||||||
|
reconnect
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
result.type = "Sent"
|
||||||
|
result.details = "Message for #{rcpt_to} accepted by #{destination_host_description}"
|
||||||
|
if @smtp_client.source_address
|
||||||
|
result.details += " (from #{@smtp_client.source_address})"
|
||||||
|
end
|
||||||
|
result.output = smtp_result.string
|
||||||
|
log "Message sent ##{message.id} to #{destination_host_description} for #{rcpt_to}"
|
||||||
|
rescue Net::SMTPServerBusy, Net::SMTPAuthenticationError, Net::SMTPSyntaxError, Net::SMTPUnknownError, Net::ReadTimeout => e
|
||||||
|
log "#{e.class}: #{e.message}"
|
||||||
|
result.type = "SoftFail"
|
||||||
|
result.retry = true
|
||||||
|
result.details = "Temporary SMTP delivery error when sending to #{destination_host_description}"
|
||||||
|
result.output = e.message
|
||||||
|
if e.to_s =~ /(\d+) seconds/
|
||||||
|
result.retry = ::Regexp.last_match(1).to_i + 10
|
||||||
|
elsif e.to_s =~ /(\d+) minutes/
|
||||||
|
result.retry = (::Regexp.last_match(1).to_i * 60) + 10
|
||||||
|
end
|
||||||
|
|
||||||
|
safe_rset
|
||||||
|
rescue Net::SMTPFatalError => e
|
||||||
|
log "#{e.class}: #{e.message}"
|
||||||
|
result.type = "HardFail"
|
||||||
|
result.details = "Permanent SMTP delivery error when sending to #{destination_host_description}"
|
||||||
|
result.output = e.message
|
||||||
|
safe_rset
|
||||||
|
rescue StandardError => e
|
||||||
|
log "#{e.class}: #{e.message}"
|
||||||
|
if defined?(Sentry)
|
||||||
|
Sentry.capture_exception(e, extra: { log_id: @log_id, server_id: message.server.id, message_id: message.id })
|
||||||
|
end
|
||||||
|
result.type = "SoftFail"
|
||||||
|
result.retry = true
|
||||||
|
result.details = "An error occurred while sending the message to #{destination_host_description}"
|
||||||
|
result.output = e.message
|
||||||
|
safe_rset
|
||||||
|
end
|
||||||
|
|
||||||
|
result.time = (Time.now - start_time).to_f.round(2)
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish
|
||||||
|
log "Finishing up"
|
||||||
|
@smtp_client&.finish
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def servers
|
||||||
|
@options[:servers] || self.class.relay_hosts || @servers ||= begin
|
||||||
|
mx_servers = DNSResolver.local.mx(@domain).map(&:last)
|
||||||
|
if mx_servers.empty?
|
||||||
|
mx_servers = [@domain] # This will be resolved to an A or AAAA record later
|
||||||
|
end
|
||||||
|
mx_servers
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def log(text)
|
||||||
|
Postal.logger.info text, id: @log_id, component: "smtp-sender"
|
||||||
|
end
|
||||||
|
|
||||||
|
def destination_host_description
|
||||||
|
"#{@hostnames.last} (#{@remote_ip})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def lookup_ip_address(type, hostname)
|
||||||
|
records = []
|
||||||
|
case type
|
||||||
|
when :a
|
||||||
|
records = DNSResolver.local.a(hostname)
|
||||||
|
when :aaaa
|
||||||
|
records = DNSResolver.local.aaaa(hostname)
|
||||||
|
end
|
||||||
|
records.first&.to_s&.downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
|
||||||
|
def ssl_context_with_verify
|
||||||
|
@ssl_context_with_verify ||= begin
|
||||||
|
c = OpenSSL::SSL::SSLContext.new
|
||||||
|
c.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||||||
|
c.cert_store = OpenSSL::X509::Store.new
|
||||||
|
c.cert_store.set_default_paths
|
||||||
|
c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ssl_context_without_verify
|
||||||
|
@ssl_context_without_verify ||= begin
|
||||||
|
c = OpenSSL::SSL::SSLContext.new
|
||||||
|
c.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||||
|
c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_helo_hostname
|
||||||
|
Postal.config.dns.helo_hostname || Postal.config.dns.smtp_server_hostname || "localhost"
|
||||||
|
end
|
||||||
|
|
||||||
|
def relay_hosts
|
||||||
|
hosts = Postal.config.smtp_relays.map do |relay|
|
||||||
|
next unless relay.hostname.present?
|
||||||
|
|
||||||
|
{
|
||||||
|
hostname: relay.hostname,
|
||||||
|
port: relay.port,
|
||||||
|
ssl_mode: relay.ssl_mode
|
||||||
|
}
|
||||||
|
end.compact
|
||||||
|
hosts.empty? ? nil : hosts
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Postal
|
|
||||||
class HTTPSender < Sender
|
|
||||||
|
|
||||||
def initialize(endpoint, options = {})
|
|
||||||
super()
|
|
||||||
@endpoint = endpoint
|
|
||||||
@options = options
|
|
||||||
@log_id = Nifty::Utils::RandomString.generate(length: 8).upcase
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_message(message)
|
|
||||||
start_time = Time.now
|
|
||||||
result = SendResult.new
|
|
||||||
result.log_id = @log_id
|
|
||||||
|
|
||||||
request_options = {}
|
|
||||||
request_options[:sign] = true
|
|
||||||
request_options[:timeout] = @endpoint.timeout || 5
|
|
||||||
case @endpoint.encoding
|
|
||||||
when "BodyAsJSON"
|
|
||||||
request_options[:json] = parameters(message, flat: false).to_json
|
|
||||||
when "FormData"
|
|
||||||
request_options[:params] = parameters(message, flat: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
log "Sending request to #{@endpoint.url}"
|
|
||||||
response = Postal::HTTP.post(@endpoint.url, request_options)
|
|
||||||
result.secure = !!response[:secure] # rubocop:disable Style/DoubleNegation
|
|
||||||
result.details = "Received a #{response[:code]} from #{@endpoint.url}"
|
|
||||||
log " -> Received: #{response[:code]}"
|
|
||||||
if response[:body]
|
|
||||||
log " -> Body: #{response[:body][0, 255]}"
|
|
||||||
result.output = response[:body].to_s[0, 500].strip
|
|
||||||
end
|
|
||||||
if response[:code] >= 200 && response[:code] < 300
|
|
||||||
# This is considered a success
|
|
||||||
result.type = "Sent"
|
|
||||||
elsif response[:code] >= 500 && response[:code] < 600
|
|
||||||
# This is temporary. They might fix their server so it should soft fail.
|
|
||||||
result.type = "SoftFail"
|
|
||||||
result.retry = true
|
|
||||||
elsif response[:code].negative?
|
|
||||||
# Connection/SSL etc... errors
|
|
||||||
result.type = "SoftFail"
|
|
||||||
result.retry = true
|
|
||||||
result.connect_error = true
|
|
||||||
elsif response[:code] == 429
|
|
||||||
# Rate limit exceeded, treat as a hard fail and don't send bounces
|
|
||||||
result.type = "HardFail"
|
|
||||||
result.suppress_bounce = true
|
|
||||||
else
|
|
||||||
# This is permanent. Any other error isn't cool with us.
|
|
||||||
result.type = "HardFail"
|
|
||||||
end
|
|
||||||
result.time = (Time.now - start_time).to_f.round(2)
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def log(text)
|
|
||||||
Postal.logger.info text, id: @log_id, component: "http-sender"
|
|
||||||
end
|
|
||||||
|
|
||||||
def parameters(message, options = {})
|
|
||||||
case @endpoint.format
|
|
||||||
when "Hash"
|
|
||||||
hash = {
|
|
||||||
id: message.id,
|
|
||||||
rcpt_to: message.rcpt_to,
|
|
||||||
mail_from: message.mail_from,
|
|
||||||
token: message.token,
|
|
||||||
subject: message.subject,
|
|
||||||
message_id: message.message_id,
|
|
||||||
timestamp: message.timestamp.to_f,
|
|
||||||
size: message.size,
|
|
||||||
spam_status: message.spam_status,
|
|
||||||
bounce: message.bounce,
|
|
||||||
received_with_ssl: message.received_with_ssl,
|
|
||||||
to: message.headers["to"]&.last,
|
|
||||||
cc: message.headers["cc"]&.last,
|
|
||||||
from: message.headers["from"]&.last,
|
|
||||||
date: message.headers["date"]&.last,
|
|
||||||
in_reply_to: message.headers["in-reply-to"]&.last,
|
|
||||||
references: message.headers["references"]&.last,
|
|
||||||
html_body: message.html_body,
|
|
||||||
attachment_quantity: message.attachments.size,
|
|
||||||
auto_submitted: message.headers["auto-submitted"]&.last,
|
|
||||||
reply_to: message.headers["reply-to"]
|
|
||||||
}
|
|
||||||
|
|
||||||
if @endpoint.strip_replies
|
|
||||||
hash[:plain_body], hash[:replies_from_plain_body] = Postal::ReplySeparator.separate(message.plain_body)
|
|
||||||
else
|
|
||||||
hash[:plain_body] = message.plain_body
|
|
||||||
end
|
|
||||||
|
|
||||||
if @endpoint.include_attachments?
|
|
||||||
if options[:flat]
|
|
||||||
message.attachments.each_with_index do |a, i|
|
|
||||||
hash["attachments[#{i}][filename]"] = a.filename
|
|
||||||
hash["attachments[#{i}][content_type]"] = a.content_type
|
|
||||||
hash["attachments[#{i}][size]"] = a.body.to_s.bytesize.to_s
|
|
||||||
hash["attachments[#{i}][data]"] = Base64.encode64(a.body.to_s)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
hash[:attachments] = message.attachments.map do |a|
|
|
||||||
{
|
|
||||||
filename: a.filename,
|
|
||||||
content_type: a.mime_type,
|
|
||||||
size: a.body.to_s.bytesize,
|
|
||||||
data: Base64.encode64(a.body.to_s)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
hash
|
|
||||||
when "RawMessage"
|
|
||||||
{
|
|
||||||
id: message.id,
|
|
||||||
rcpt_to: message.rcpt_to,
|
|
||||||
mail_from: message.mail_from,
|
|
||||||
message: Base64.encode64(message.raw_message),
|
|
||||||
base64: true,
|
|
||||||
size: message.size.to_i
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Postal
|
|
||||||
class SendResult
|
|
||||||
|
|
||||||
attr_accessor :type
|
|
||||||
attr_accessor :details
|
|
||||||
attr_accessor :retry
|
|
||||||
attr_accessor :output
|
|
||||||
attr_accessor :secure
|
|
||||||
attr_accessor :connect_error
|
|
||||||
attr_accessor :log_id
|
|
||||||
attr_accessor :time
|
|
||||||
attr_accessor :suppress_bounce
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@details = ""
|
|
||||||
yield self if block_given?
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Postal
|
|
||||||
class Sender
|
|
||||||
|
|
||||||
def start
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_message(message)
|
|
||||||
end
|
|
||||||
|
|
||||||
def finish
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "resolv"
|
|
||||||
|
|
||||||
module Postal
|
|
||||||
class SMTPSender < Sender
|
|
||||||
|
|
||||||
def initialize(domain, source_ip_address, options = {})
|
|
||||||
super()
|
|
||||||
@domain = domain
|
|
||||||
@source_ip_address = source_ip_address
|
|
||||||
@options = options
|
|
||||||
@smtp_client = nil
|
|
||||||
@connection_errors = []
|
|
||||||
@hostnames = []
|
|
||||||
@log_id = Nifty::Utils::RandomString.generate(length: 8).upcase
|
|
||||||
end
|
|
||||||
|
|
||||||
def start
|
|
||||||
servers.each do |server|
|
|
||||||
if server.is_a?(SMTPEndpoint)
|
|
||||||
hostname = server.hostname
|
|
||||||
port = server.port || 25
|
|
||||||
ssl_mode = server.ssl_mode
|
|
||||||
elsif server.is_a?(Hash)
|
|
||||||
hostname = server[:hostname]
|
|
||||||
port = server[:port] || 25
|
|
||||||
ssl_mode = server[:ssl_mode] || "Auto"
|
|
||||||
else
|
|
||||||
hostname = server
|
|
||||||
port = 25
|
|
||||||
ssl_mode = "Auto"
|
|
||||||
end
|
|
||||||
|
|
||||||
@hostnames << hostname
|
|
||||||
[:aaaa, :a].each do |ip_type|
|
|
||||||
if @source_ip_address && @source_ip_address.ipv6.blank? && ip_type == :aaaa
|
|
||||||
# Don't try to use IPv6 if the IP address we're sending from doesn't support it.
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
@remote_ip = lookup_ip_address(ip_type, hostname)
|
|
||||||
if @remote_ip.nil?
|
|
||||||
if ip_type == :a
|
|
||||||
# As we can't resolve the last IP, we'll put this
|
|
||||||
@connection_errors << "Could not resolve #{hostname}"
|
|
||||||
end
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
smtp_client = Net::SMTP.new(@remote_ip, port)
|
|
||||||
smtp_client.open_timeout = Postal.config.smtp_client.open_timeout
|
|
||||||
smtp_client.read_timeout = Postal.config.smtp_client.read_timeout
|
|
||||||
smtp_client.tls_hostname = hostname
|
|
||||||
|
|
||||||
if @source_ip_address
|
|
||||||
# Set the source IP as appropriate
|
|
||||||
smtp_client.source_address = ip_type == :aaaa ? @source_ip_address.ipv6 : @source_ip_address.ipv4
|
|
||||||
end
|
|
||||||
|
|
||||||
case ssl_mode
|
|
||||||
when "Auto"
|
|
||||||
smtp_client.enable_starttls_auto(self.class.ssl_context_without_verify)
|
|
||||||
when "STARTTLS"
|
|
||||||
smtp_client.enable_starttls(self.class.ssl_context_with_verify)
|
|
||||||
when "TLS"
|
|
||||||
smtp_client.enable_tls(self.class.ssl_context_with_verify)
|
|
||||||
else
|
|
||||||
smtp_client.disable_starttls
|
|
||||||
smtp_client.disable_tls
|
|
||||||
end
|
|
||||||
|
|
||||||
smtp_client.start(@source_ip_address ? @source_ip_address.hostname : self.class.default_helo_hostname)
|
|
||||||
log "Connected to #{@remote_ip}:#{port} (#{hostname})"
|
|
||||||
rescue StandardError => e
|
|
||||||
if e.is_a?(OpenSSL::SSL::SSLError) && ssl_mode == "Auto"
|
|
||||||
log "SSL error (#{e.message}), retrying without SSL"
|
|
||||||
ssl_mode = nil
|
|
||||||
retry
|
|
||||||
end
|
|
||||||
|
|
||||||
log "Cannot connect to #{@remote_ip}:#{port} (#{hostname}) (#{e.class}: #{e.message})"
|
|
||||||
@connection_errors << e.message unless @connection_errors.include?(e.message)
|
|
||||||
begin
|
|
||||||
smtp_client.finish
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
smtp_client = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
if smtp_client
|
|
||||||
@smtp_client = smtp_client
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@connection_errors
|
|
||||||
end
|
|
||||||
|
|
||||||
def reconnect
|
|
||||||
log "Reconnecting"
|
|
||||||
begin
|
|
||||||
@smtp_client&.finish
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
start
|
|
||||||
end
|
|
||||||
|
|
||||||
def safe_rset
|
|
||||||
# Something went wrong sending the last email. Reset the connection if possible, else disconnect.
|
|
||||||
|
|
||||||
@smtp_client.rset
|
|
||||||
rescue StandardError
|
|
||||||
# Don't reconnect, this would be rather rude if we don't have any more emails to send.
|
|
||||||
begin
|
|
||||||
@smtp_client.finish
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_message(message, force_rcpt_to = nil)
|
|
||||||
start_time = Time.now
|
|
||||||
result = SendResult.new
|
|
||||||
result.log_id = @log_id
|
|
||||||
if @smtp_client && !@smtp_client.started?
|
|
||||||
# For some reason we had an SMTP connection but it's no longer connected.
|
|
||||||
# Make a new one.
|
|
||||||
start
|
|
||||||
end
|
|
||||||
|
|
||||||
if @smtp_client
|
|
||||||
result.secure = @smtp_client.secure_socket?
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
if message.bounce
|
|
||||||
mail_from = ""
|
|
||||||
elsif message.domain.return_path_status == "OK"
|
|
||||||
mail_from = "#{message.server.token}@#{message.domain.return_path_domain}"
|
|
||||||
else
|
|
||||||
mail_from = "#{message.server.token}@#{Postal.config.dns.return_path}"
|
|
||||||
end
|
|
||||||
if Postal.config.general.use_resent_sender_header
|
|
||||||
raw_message = "Resent-Sender: #{mail_from}\r\n" + message.raw_message
|
|
||||||
else
|
|
||||||
raw_message = message.raw_message
|
|
||||||
end
|
|
||||||
tries = 0
|
|
||||||
begin
|
|
||||||
if @smtp_client.nil?
|
|
||||||
log "-> No SMTP server available for #{@domain}"
|
|
||||||
log "-> Hostnames: #{@hostnames.inspect}"
|
|
||||||
log "-> Errors: #{@connection_errors.inspect}"
|
|
||||||
result.type = "SoftFail"
|
|
||||||
result.retry = true
|
|
||||||
result.details = "No SMTP servers were available for #{@domain}. Tried #{@hostnames.to_sentence}"
|
|
||||||
result.output = @connection_errors.join(", ")
|
|
||||||
result.connect_error = true
|
|
||||||
return result
|
|
||||||
else
|
|
||||||
@smtp_client.rset_errors
|
|
||||||
rcpt_to = force_rcpt_to || @options[:force_rcpt_to] || message.rcpt_to
|
|
||||||
log "Sending message #{message.server.id}::#{message.id} to #{rcpt_to}"
|
|
||||||
smtp_result = @smtp_client.send_message(raw_message, mail_from, [rcpt_to])
|
|
||||||
end
|
|
||||||
rescue Errno::ECONNRESET, Errno::EPIPE, OpenSSL::SSL::SSLError
|
|
||||||
raise unless (tries += 1) < 2
|
|
||||||
|
|
||||||
reconnect
|
|
||||||
retry
|
|
||||||
end
|
|
||||||
result.type = "Sent"
|
|
||||||
result.details = "Message for #{rcpt_to} accepted by #{destination_host_description}"
|
|
||||||
if @smtp_client.source_address
|
|
||||||
result.details += " (from #{@smtp_client.source_address})"
|
|
||||||
end
|
|
||||||
result.output = smtp_result.string
|
|
||||||
log "Message sent ##{message.id} to #{destination_host_description} for #{rcpt_to}"
|
|
||||||
rescue Net::SMTPServerBusy, Net::SMTPAuthenticationError, Net::SMTPSyntaxError, Net::SMTPUnknownError, Net::ReadTimeout => e
|
|
||||||
log "#{e.class}: #{e.message}"
|
|
||||||
result.type = "SoftFail"
|
|
||||||
result.retry = true
|
|
||||||
result.details = "Temporary SMTP delivery error when sending to #{destination_host_description}"
|
|
||||||
result.output = e.message
|
|
||||||
if e.to_s =~ /(\d+) seconds/
|
|
||||||
result.retry = ::Regexp.last_match(1).to_i + 10
|
|
||||||
elsif e.to_s =~ /(\d+) minutes/
|
|
||||||
result.retry = (::Regexp.last_match(1).to_i * 60) + 10
|
|
||||||
end
|
|
||||||
|
|
||||||
safe_rset
|
|
||||||
rescue Net::SMTPFatalError => e
|
|
||||||
log "#{e.class}: #{e.message}"
|
|
||||||
result.type = "HardFail"
|
|
||||||
result.details = "Permanent SMTP delivery error when sending to #{destination_host_description}"
|
|
||||||
result.output = e.message
|
|
||||||
safe_rset
|
|
||||||
rescue StandardError => e
|
|
||||||
log "#{e.class}: #{e.message}"
|
|
||||||
if defined?(Sentry)
|
|
||||||
Sentry.capture_exception(e, extra: { log_id: @log_id, server_id: message.server.id, message_id: message.id })
|
|
||||||
end
|
|
||||||
result.type = "SoftFail"
|
|
||||||
result.retry = true
|
|
||||||
result.details = "An error occurred while sending the message to #{destination_host_description}"
|
|
||||||
result.output = e.message
|
|
||||||
safe_rset
|
|
||||||
end
|
|
||||||
|
|
||||||
result.time = (Time.now - start_time).to_f.round(2)
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def finish
|
|
||||||
log "Finishing up"
|
|
||||||
@smtp_client&.finish
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def servers
|
|
||||||
@options[:servers] || self.class.relay_hosts || @servers ||= begin
|
|
||||||
mx_servers = DNSResolver.local.mx(@domain).map(&:last)
|
|
||||||
if mx_servers.empty?
|
|
||||||
mx_servers = [@domain] # This will be resolved to an A or AAAA record later
|
|
||||||
end
|
|
||||||
mx_servers
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def log(text)
|
|
||||||
Postal.logger.info text, id: @log_id, component: "smtp-sender"
|
|
||||||
end
|
|
||||||
|
|
||||||
def destination_host_description
|
|
||||||
"#{@hostnames.last} (#{@remote_ip})"
|
|
||||||
end
|
|
||||||
|
|
||||||
def lookup_ip_address(type, hostname)
|
|
||||||
records = []
|
|
||||||
case type
|
|
||||||
when :a
|
|
||||||
records = DNSResolver.local.a(hostname)
|
|
||||||
when :aaaa
|
|
||||||
records = DNSResolver.local.aaaa(hostname)
|
|
||||||
end
|
|
||||||
records.first&.to_s&.downcase
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
|
||||||
|
|
||||||
def ssl_context_with_verify
|
|
||||||
@ssl_context_with_verify ||= begin
|
|
||||||
c = OpenSSL::SSL::SSLContext.new
|
|
||||||
c.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
||||||
c.cert_store = OpenSSL::X509::Store.new
|
|
||||||
c.cert_store.set_default_paths
|
|
||||||
c
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def ssl_context_without_verify
|
|
||||||
@ssl_context_without_verify ||= begin
|
|
||||||
c = OpenSSL::SSL::SSLContext.new
|
|
||||||
c.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
||||||
c
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def default_helo_hostname
|
|
||||||
Postal.config.dns.helo_hostname || Postal.config.dns.smtp_server_hostname || "localhost"
|
|
||||||
end
|
|
||||||
|
|
||||||
def relay_hosts
|
|
||||||
hosts = Postal.config.smtp_relays.map do |relay|
|
|
||||||
next unless relay.hostname.present?
|
|
||||||
|
|
||||||
{
|
|
||||||
hostname: relay.hostname,
|
|
||||||
port: relay.port,
|
|
||||||
ssl_mode: relay.ssl_mode
|
|
||||||
}
|
|
||||||
end.compact
|
|
||||||
hosts.empty? ? nil : hosts
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -409,8 +409,8 @@ module MessageDequeuer
|
|||||||
|
|
||||||
it "gets a sender from the state and sends the message to it" do
|
it "gets a sender from the state and sends the message to it" do
|
||||||
http_sender_double = double("HTTPSender")
|
http_sender_double = double("HTTPSender")
|
||||||
expect(http_sender_double).to receive(:send_message).with(queued_message.message).and_return(Postal::SendResult.new)
|
expect(http_sender_double).to receive(:send_message).with(queued_message.message).and_return(SendResult.new)
|
||||||
expect(state).to receive(:sender_for).with(Postal::HTTPSender, endpoint).and_return(http_sender_double)
|
expect(state).to receive(:sender_for).with(HTTPSender, endpoint).and_return(http_sender_double)
|
||||||
processor.process
|
processor.process
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -421,8 +421,8 @@ module MessageDequeuer
|
|||||||
|
|
||||||
it "gets a sender from the state and sends the message to it" do
|
it "gets a sender from the state and sends the message to it" do
|
||||||
smtp_sender_double = double("SMTPSender")
|
smtp_sender_double = double("SMTPSender")
|
||||||
expect(smtp_sender_double).to receive(:send_message).with(queued_message.message).and_return(Postal::SendResult.new)
|
expect(smtp_sender_double).to receive(:send_message).with(queued_message.message).and_return(SendResult.new)
|
||||||
expect(state).to receive(:sender_for).with(Postal::SMTPSender, message.recipient_domain, nil, { servers: [endpoint] }).and_return(smtp_sender_double)
|
expect(state).to receive(:sender_for).with(SMTPSender, message.recipient_domain, nil, { servers: [endpoint] }).and_return(smtp_sender_double)
|
||||||
processor.process
|
processor.process
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -433,8 +433,8 @@ module MessageDequeuer
|
|||||||
|
|
||||||
it "gets a sender from the state and sends the message to it" do
|
it "gets a sender from the state and sends the message to it" do
|
||||||
smtp_sender_double = double("SMTPSender")
|
smtp_sender_double = double("SMTPSender")
|
||||||
expect(smtp_sender_double).to receive(:send_message).with(queued_message.message).and_return(Postal::SendResult.new)
|
expect(smtp_sender_double).to receive(:send_message).with(queued_message.message).and_return(SendResult.new)
|
||||||
expect(state).to receive(:sender_for).with(Postal::SMTPSender, endpoint.domain, nil, { force_rcpt_to: endpoint.address }).and_return(smtp_sender_double)
|
expect(state).to receive(:sender_for).with(SMTPSender, endpoint.domain, nil, { force_rcpt_to: endpoint.address }).and_return(smtp_sender_double)
|
||||||
processor.process
|
processor.process
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -469,7 +469,7 @@ module MessageDequeuer
|
|||||||
let(:route) { create(:route, server: server, mode: "Endpoint", endpoint: endpoint) }
|
let(:route) { create(:route, server: server, mode: "Endpoint", endpoint: endpoint) }
|
||||||
|
|
||||||
let(:send_result) do
|
let(:send_result) do
|
||||||
Postal::SendResult.new do |result|
|
SendResult.new do |result|
|
||||||
result.type = "Sent"
|
result.type = "Sent"
|
||||||
result.details = "Sent successfully"
|
result.details = "Sent successfully"
|
||||||
end
|
end
|
||||||
@@ -477,7 +477,7 @@ module MessageDequeuer
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
smtp_sender_mock = double("SMTPSender")
|
smtp_sender_mock = double("SMTPSender")
|
||||||
allow(Postal::SMTPSender).to receive(:new).and_return(smtp_sender_mock)
|
allow(SMTPSender).to receive(:new).and_return(smtp_sender_mock)
|
||||||
allow(smtp_sender_mock).to receive(:start)
|
allow(smtp_sender_mock).to receive(:start)
|
||||||
allow(smtp_sender_mock).to receive(:finish)
|
allow(smtp_sender_mock).to receive(:finish)
|
||||||
allow(smtp_sender_mock).to receive(:send_message).and_return(send_result)
|
allow(smtp_sender_mock).to receive(:send_message).and_return(send_result)
|
||||||
@@ -611,7 +611,7 @@ module MessageDequeuer
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
smtp_sender_mock = double("SMTPSender")
|
smtp_sender_mock = double("SMTPSender")
|
||||||
allow(Postal::SMTPSender).to receive(:new).and_return(smtp_sender_mock)
|
allow(SMTPSender).to receive(:new).and_return(smtp_sender_mock)
|
||||||
allow(smtp_sender_mock).to receive(:start)
|
allow(smtp_sender_mock).to receive(:start)
|
||||||
allow(smtp_sender_mock).to receive(:finish)
|
allow(smtp_sender_mock).to receive(:finish)
|
||||||
allow(smtp_sender_mock).to receive(:send_message) do
|
allow(smtp_sender_mock).to receive(:send_message) do
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ module MessageDequeuer
|
|||||||
|
|
||||||
context "when there are no other impediments" do
|
context "when there are no other impediments" do
|
||||||
let(:send_result) do
|
let(:send_result) do
|
||||||
Postal::SendResult.new do |r|
|
SendResult.new do |r|
|
||||||
r.type = "Sent"
|
r.type = "Sent"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -383,7 +383,7 @@ module MessageDequeuer
|
|||||||
it "gets a sender from the state and sends the message to it" do
|
it "gets a sender from the state and sends the message to it" do
|
||||||
mocked_sender = double("SMTPSender")
|
mocked_sender = double("SMTPSender")
|
||||||
expect(mocked_sender).to receive(:send_message).with(queued_message.message).and_return(send_result)
|
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)
|
expect(state).to receive(:sender_for).with(SMTPSender, message.recipient_domain, ip).and_return(mocked_sender)
|
||||||
|
|
||||||
processor.process
|
processor.process
|
||||||
end
|
end
|
||||||
@@ -393,7 +393,7 @@ module MessageDequeuer
|
|||||||
it "gets a sender from the state and sends the message to it" do
|
it "gets a sender from the state and sends the message to it" do
|
||||||
mocked_sender = double("SMTPSender")
|
mocked_sender = double("SMTPSender")
|
||||||
expect(mocked_sender).to receive(:send_message).with(queued_message.message).and_return(send_result)
|
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)
|
expect(state).to receive(:sender_for).with(SMTPSender, message.recipient_domain, nil).and_return(mocked_sender)
|
||||||
|
|
||||||
processor.process
|
processor.process
|
||||||
end
|
end
|
||||||
@@ -514,7 +514,7 @@ module MessageDequeuer
|
|||||||
context "when an exception occurrs during processing" do
|
context "when an exception occurrs during processing" do
|
||||||
before do
|
before do
|
||||||
smtp_sender_mock = double("SMTPSender")
|
smtp_sender_mock = double("SMTPSender")
|
||||||
allow(Postal::SMTPSender).to receive(:new).and_return(smtp_sender_mock)
|
allow(SMTPSender).to receive(:new).and_return(smtp_sender_mock)
|
||||||
allow(smtp_sender_mock).to receive(:start)
|
allow(smtp_sender_mock).to receive(:start)
|
||||||
allow(smtp_sender_mock).to receive(:send_message) do
|
allow(smtp_sender_mock).to receive(:send_message) do
|
||||||
1 / 0
|
1 / 0
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module MessageDequeuer
|
|||||||
|
|
||||||
describe "#send_result" do
|
describe "#send_result" do
|
||||||
it "can be get and set" do
|
it "can be get and set" do
|
||||||
result = instance_double(Postal::SendResult)
|
result = instance_double(SendResult)
|
||||||
state.send_result = result
|
state.send_result = result
|
||||||
expect(state.send_result).to be result
|
expect(state.send_result).to be result
|
||||||
end
|
end
|
||||||
@@ -17,20 +17,20 @@ module MessageDequeuer
|
|||||||
|
|
||||||
describe "#sender_for" do
|
describe "#sender_for" do
|
||||||
it "returns a instance of the given sender initialized with the args" do
|
it "returns a instance of the given sender initialized with the args" do
|
||||||
sender = state.sender_for(Postal::HTTPSender, "1234")
|
sender = state.sender_for(HTTPSender, "1234")
|
||||||
expect(sender).to be_a Postal::HTTPSender
|
expect(sender).to be_a HTTPSender
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a cached sender on subsequent calls" do
|
it "returns a cached sender on subsequent calls" do
|
||||||
sender = state.sender_for(Postal::HTTPSender, "1234")
|
sender = state.sender_for(HTTPSender, "1234")
|
||||||
expect(state.sender_for(Postal::HTTPSender, "1234")).to be sender
|
expect(state.sender_for(HTTPSender, "1234")).to be sender
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#finished" do
|
describe "#finished" do
|
||||||
it "calls finish on all cached senders" do
|
it "calls finish on all cached senders" do
|
||||||
sender1 = state.sender_for(Postal::HTTPSender, "1234")
|
sender1 = state.sender_for(HTTPSender, "1234")
|
||||||
sender2 = state.sender_for(Postal::HTTPSender, "4444")
|
sender2 = state.sender_for(HTTPSender, "4444")
|
||||||
expect(sender1).to receive(:finish)
|
expect(sender1).to receive(:finish)
|
||||||
expect(sender2).to receive(:finish)
|
expect(sender2).to receive(:finish)
|
||||||
|
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم