Some checks failed
Lock Threads / action (push) Has been cancelled
Replace SMTP with LeadMail API service for sending system transactional emails (password resets, invitations, notifications). LeadMail provides built-in email verification via Verifalia and async queue-based sending. Configuration: - Set LEADMAIL_API_TOKEN and LEADMAIL_API_URL in .env - Falls back to SMTP if LeadMail token not present - Works via custom ActionMailer delivery method (stable across upstream merges) Includes: - LeadmailDelivery class with full spec coverage - Support for multipart messages, attachments, CC/BCC - Error handling and logging with message tracking - Documentation in docs/LEADMAIL_INTEGRATION.md
106 lines
2.7 KiB
Ruby
106 lines
2.7 KiB
Ruby
class LeadmailDelivery
|
|
attr_reader :settings
|
|
|
|
def initialize(settings)
|
|
@settings = settings
|
|
end
|
|
|
|
def deliver!(message)
|
|
payload = build_payload(message)
|
|
response = http_client.post('/emails/send', payload)
|
|
|
|
unless response.success?
|
|
raise LeadmailDeliveryError, "LeadMail API error: #{response.status} - #{response.body}"
|
|
end
|
|
|
|
response_data = JSON.parse(response.body)
|
|
message.header['X-LeadMail-Log-ID'] = response_data.dig('data', 'log_id')
|
|
Rails.logger.info("Email sent via LeadMail: log_id=#{response_data.dig('data', 'log_id')}, to=#{message.to.inspect}")
|
|
response
|
|
rescue StandardError => e
|
|
Rails.logger.error("LeadMail delivery failed: #{e.message}\nPayload: #{payload.inspect}")
|
|
raise
|
|
end
|
|
|
|
private
|
|
|
|
def build_payload(message)
|
|
{
|
|
from: extract_from(message),
|
|
to: extract_recipients(message.to),
|
|
cc: extract_recipients(message.cc),
|
|
bcc: extract_recipients(message.bcc),
|
|
reply_to: extract_reply_to(message),
|
|
subject: message.subject,
|
|
html_body: html_body(message),
|
|
text_body: text_body(message),
|
|
attachments: extract_attachments(message),
|
|
options: {
|
|
on_verification_failure: 'strip',
|
|
allow_disposable: false
|
|
}
|
|
}.compact
|
|
end
|
|
|
|
def extract_from(message)
|
|
from_address = message.from&.first
|
|
from_display = message[:from]&.display_names&.first
|
|
|
|
{
|
|
email: from_address,
|
|
name: from_display
|
|
}.compact
|
|
end
|
|
|
|
def extract_recipients(addresses)
|
|
return nil if addresses.blank?
|
|
|
|
Array(addresses).map { |email| { email: email } }
|
|
end
|
|
|
|
def extract_reply_to(message)
|
|
reply_to_address = message.reply_to&.first
|
|
return nil if reply_to_address.blank?
|
|
|
|
{ email: reply_to_address }
|
|
end
|
|
|
|
def html_body(message)
|
|
return message.html_part.body.to_s if message.html_part.present?
|
|
return message.body.to_s if message.content_type&.start_with?('text/html')
|
|
|
|
nil
|
|
end
|
|
|
|
def text_body(message)
|
|
return message.text_part.body.to_s if message.text_part.present?
|
|
return message.body.to_s if message.content_type&.start_with?('text/plain')
|
|
|
|
nil
|
|
end
|
|
|
|
def extract_attachments(message)
|
|
return nil if message.attachments.blank?
|
|
|
|
message.attachments.map do |attachment|
|
|
{
|
|
filename: attachment.filename,
|
|
mime_type: attachment.mime_type || 'application/octet-stream',
|
|
content: Base64.strict_encode64(attachment.body.to_s)
|
|
}
|
|
end
|
|
end
|
|
|
|
def http_client
|
|
@http_client ||= Faraday.new(
|
|
url: settings[:api_url],
|
|
headers: {
|
|
'Authorization' => "Bearer #{settings[:token]}",
|
|
'Content-Type' => 'application/json'
|
|
}
|
|
)
|
|
end
|
|
end
|
|
|
|
class LeadmailDeliveryError < StandardError; end
|