مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-12-01 05:43:04 +00:00
refactor: refactor the SMTP sender
هذا الالتزام موجود في:
169
app/lib/smtp_client/endpoint.rb
Normal file
169
app/lib/smtp_client/endpoint.rb
Normal file
@@ -0,0 +1,169 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SMTPClient
|
||||
class Endpoint
|
||||
|
||||
class SMTPSessionNotStartedError < StandardError
|
||||
end
|
||||
|
||||
attr_reader :server
|
||||
attr_reader :ip_address
|
||||
attr_accessor :smtp_client
|
||||
|
||||
# @param server [Server] the server that this IP address is for
|
||||
# @param ip_address [String] the IP address
|
||||
def initialize(server, ip_address)
|
||||
@server = server
|
||||
@ip_address = ip_address
|
||||
end
|
||||
|
||||
# Return a description of this server with its IP address
|
||||
#
|
||||
# @return [String]
|
||||
def description
|
||||
"#{@ip_address}:#{@server.port} (#{@server.hostname})"
|
||||
end
|
||||
|
||||
# Return a string representation of this server
|
||||
#
|
||||
# @return [String]
|
||||
def to_s
|
||||
description
|
||||
end
|
||||
|
||||
# Return true if this is an IPv6 address
|
||||
#
|
||||
# @return [Boolean]
|
||||
def ipv6?
|
||||
@ip_address.include?(":")
|
||||
end
|
||||
|
||||
# Return true if this is an IPv4 address
|
||||
#
|
||||
# @return [Boolean]
|
||||
def ipv4?
|
||||
!ipv6?
|
||||
end
|
||||
|
||||
# Start a new SMTP session and store the client with this server for future use as needed
|
||||
#
|
||||
# @param source_ip_address [IPAddress] the IP address to use as the source address for the connection
|
||||
# @param allow_ssl [Boolean] whether to allow SSL for this connection, if false SSL mode is ignored
|
||||
#
|
||||
# @return [Net::SMTP]
|
||||
def start_smtp_session(source_ip_address: nil, allow_ssl: true)
|
||||
@smtp_client = Net::SMTP.new(@ip_address, @server.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 = @server.hostname
|
||||
|
||||
if source_ip_address
|
||||
@source_ip_address = source_ip_address
|
||||
end
|
||||
|
||||
if @source_ip_address
|
||||
@smtp_client.source_address = ipv6? ? @source_ip_address.ipv6 : @source_ip_address.ipv4
|
||||
end
|
||||
|
||||
if allow_ssl
|
||||
case @server.ssl_mode
|
||||
when SSLModes::AUTO
|
||||
@smtp_client.enable_starttls_auto(self.class.ssl_context_without_verify)
|
||||
when SSLModes::STARTTLS
|
||||
@smtp_client.enable_starttls(self.class.ssl_context_with_verify)
|
||||
when SSLModes::TLS
|
||||
@smtp_client.enable_tls(self.class.ssl_context_with_verify)
|
||||
else
|
||||
@smtp_client.disable_starttls
|
||||
@smtp_client.disable_tls
|
||||
end
|
||||
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)
|
||||
|
||||
@smtp_client
|
||||
end
|
||||
|
||||
# Send a message to the current SMTP session (or create one if there isn't one for this endpoint).
|
||||
# If sending messsage encouters some connection errors, retry again after re-establishing the SMTP
|
||||
# session.
|
||||
#
|
||||
# @param raw_message [String] the raw message to send
|
||||
# @param mail_from [String] the MAIL FROM address
|
||||
# @param rcpt_to [String] the RCPT TO address
|
||||
# @param retry_on_connection_error [Boolean] whether to retry the connection if there is a connection error
|
||||
#
|
||||
# @return [void]
|
||||
def send_message(raw_message, mail_from, rcpt_to, retry_on_connection_error: true)
|
||||
raise SMTPSessionNotStartedError if @smtp_client.nil? || (@smtp_client && !@smtp_client.started?)
|
||||
|
||||
@smtp_client.rset_errors
|
||||
@smtp_client.send_message(raw_message, mail_from, [rcpt_to])
|
||||
rescue Errno::ECONNRESET, Errno::EPIPE, OpenSSL::SSL::SSLError
|
||||
if retry_on_connection_error
|
||||
finish_smtp_session
|
||||
start_smtp_session
|
||||
return send_message(raw_message, mail_from, rcpt_to, retry_on_connection_error: false)
|
||||
end
|
||||
|
||||
raise
|
||||
end
|
||||
|
||||
# Reset the current SMTP session for this server if possible otherwise
|
||||
# finish the session
|
||||
#
|
||||
# @return [void]
|
||||
def reset_smtp_session
|
||||
@smtp_client&.rset
|
||||
rescue StandardError
|
||||
finish_smtp_session
|
||||
end
|
||||
|
||||
# Finish the current SMTP session for this server if possible.
|
||||
#
|
||||
# @return [void]
|
||||
def finish_smtp_session
|
||||
@smtp_client&.finish
|
||||
rescue StandardError
|
||||
nil
|
||||
ensure
|
||||
@smtp_client = nil
|
||||
end
|
||||
|
||||
class << self
|
||||
|
||||
# Return the default HELO hostname to present to SMTP servers that
|
||||
# we connect to
|
||||
#
|
||||
# @return [String]
|
||||
def default_helo_hostname
|
||||
Postal::Config.dns.helo_hostname ||
|
||||
Postal::Config.postal.smtp_hostname ||
|
||||
"localhost"
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
35
app/lib/smtp_client/server.rb
Normal file
35
app/lib/smtp_client/server.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SMTPClient
|
||||
class Server
|
||||
|
||||
attr_reader :hostname
|
||||
attr_reader :port
|
||||
attr_accessor :ssl_mode
|
||||
|
||||
def initialize(hostname, port: 25, ssl_mode: SSLModes::AUTO)
|
||||
@hostname = hostname
|
||||
@port = port
|
||||
@ssl_mode = ssl_mode
|
||||
end
|
||||
|
||||
# Return all IP addresses for this server by resolving its hostname.
|
||||
# IPv6 addresses will be returned first.
|
||||
#
|
||||
# @return [Array<SMTPClient::Endpoint>]
|
||||
def endpoints
|
||||
ips = []
|
||||
|
||||
DNSResolver.local.aaaa(@hostname).each do |ip|
|
||||
ips << Endpoint.new(self, ip)
|
||||
end
|
||||
|
||||
DNSResolver.local.a(@hostname).each do |ip|
|
||||
ips << Endpoint.new(self, ip)
|
||||
end
|
||||
|
||||
ips
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
12
app/lib/smtp_client/ssl_modes.rb
Normal file
12
app/lib/smtp_client/ssl_modes.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SMTPClient
|
||||
module SSLModes
|
||||
|
||||
AUTO = "Auto"
|
||||
STARTTLS = "STARTLS"
|
||||
TLS = "TLS"
|
||||
NONE = "None"
|
||||
|
||||
end
|
||||
end
|
||||
المرجع في مشكلة جديدة
حظر مستخدم