mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Nest Action Mailbox classes in the API docs
This commit is contained in:
parent
11a8ba1272
commit
6c168aaffb
19 changed files with 559 additions and 521 deletions
|
@ -1,36 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# The base class for all Active Mailbox ingress controllers.
|
||||
class ActionMailbox::BaseController < ActionController::Base
|
||||
skip_forgery_protection
|
||||
module ActionMailbox
|
||||
# The base class for all Active Mailbox ingress controllers.
|
||||
class BaseController < ActionController::Base
|
||||
skip_forgery_protection
|
||||
|
||||
before_action :ensure_configured
|
||||
before_action :ensure_configured
|
||||
|
||||
def self.prepare
|
||||
# Override in concrete controllers to run code on load.
|
||||
def self.prepare
|
||||
# Override in concrete controllers to run code on load.
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_configured
|
||||
unless ActionMailbox.ingress == ingress_name
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def ingress_name
|
||||
self.class.name.remove(/\AActionMailbox::Ingresses::/, /::InboundEmailsController\z/).underscore.to_sym
|
||||
end
|
||||
|
||||
|
||||
def authenticate_by_password
|
||||
if password.present?
|
||||
http_basic_authenticate_or_request_with name: "actionmailbox", password: password, realm: "Action Mailbox"
|
||||
else
|
||||
raise ArgumentError, "Missing required ingress credentials"
|
||||
end
|
||||
end
|
||||
|
||||
def password
|
||||
Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_configured
|
||||
unless ActionMailbox.ingress == ingress_name
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def ingress_name
|
||||
self.class.name.remove(/\AActionMailbox::Ingresses::/, /::InboundEmailsController\z/).underscore.to_sym
|
||||
end
|
||||
|
||||
|
||||
def authenticate_by_password
|
||||
if password.present?
|
||||
http_basic_authenticate_or_request_with name: "actionmailbox", password: password, realm: "Action Mailbox"
|
||||
else
|
||||
raise ArgumentError, "Missing required ingress credentials"
|
||||
end
|
||||
end
|
||||
|
||||
def password
|
||||
Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,52 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Ingests inbound emails from Amazon's Simple Email Service (SES).
|
||||
#
|
||||
# Requires the full RFC 822 message in the +content+ parameter. Authenticates requests by validating their signatures.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SES
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +content+ parameter
|
||||
# - <tt>500 Server Error</tt> if one of the Active Record database, the Active Storage service, or
|
||||
# the Active Job backend is misconfigured or unavailable
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# 1. Install the {aws-sdk-sns}[https://rubygems.org/gems/aws-sdk-sns] gem:
|
||||
#
|
||||
# # Gemfile
|
||||
# gem "aws-sdk-sns", ">= 1.9.0", require: false
|
||||
#
|
||||
# 2. Tell Action Mailbox to accept emails from SES:
|
||||
#
|
||||
# # config/environments/production.rb
|
||||
# config.action_mailbox.ingress = :amazon
|
||||
#
|
||||
# 3. {Configure SES}[https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html]
|
||||
# to deliver emails to your application via POST requests to +/rails/action_mailbox/amazon/inbound_emails+.
|
||||
# If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
||||
# <tt>https://example.com/rails/action_mailbox/amazon/inbound_emails</tt>.
|
||||
class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate
|
||||
module ActionMailbox
|
||||
# Ingests inbound emails from Amazon's Simple Email Service (SES).
|
||||
#
|
||||
# Requires the full RFC 822 message in the +content+ parameter. Authenticates requests by validating their signatures.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SES
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +content+ parameter
|
||||
# - <tt>500 Server Error</tt> if one of the Active Record database, the Active Storage service, or
|
||||
# the Active Job backend is misconfigured or unavailable
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# 1. Install the {aws-sdk-sns}[https://rubygems.org/gems/aws-sdk-sns] gem:
|
||||
#
|
||||
# # Gemfile
|
||||
# gem "aws-sdk-sns", ">= 1.9.0", require: false
|
||||
#
|
||||
# 2. Tell Action Mailbox to accept emails from SES:
|
||||
#
|
||||
# # config/environments/production.rb
|
||||
# config.action_mailbox.ingress = :amazon
|
||||
#
|
||||
# 3. {Configure SES}[https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html]
|
||||
# to deliver emails to your application via POST requests to +/rails/action_mailbox/amazon/inbound_emails+.
|
||||
# If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
||||
# <tt>https://example.com/rails/action_mailbox/amazon/inbound_emails</tt>.
|
||||
class Ingresses::Amazon::InboundEmailsController < BaseController
|
||||
before_action :authenticate
|
||||
|
||||
cattr_accessor :verifier
|
||||
cattr_accessor :verifier
|
||||
|
||||
def self.prepare
|
||||
self.verifier ||= begin
|
||||
require "aws-sdk-sns/message_verifier"
|
||||
Aws::SNS::MessageVerifier.new
|
||||
def self.prepare
|
||||
self.verifier ||= begin
|
||||
require "aws-sdk-sns/message_verifier"
|
||||
Aws::SNS::MessageVerifier.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content)
|
||||
end
|
||||
|
||||
private
|
||||
def authenticate
|
||||
head :unauthorized unless verifier.authentic?(request.body)
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content)
|
||||
end
|
||||
|
||||
private
|
||||
def authenticate
|
||||
head :unauthorized unless verifier.authentic?(request.body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,101 +1,103 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Ingests inbound emails from Mailgun. Requires the following parameters:
|
||||
#
|
||||
# - +body-mime+: The full RFC 822 message
|
||||
# - +timestamp+: The current time according to Mailgun as the number of seconds passed since the UNIX epoch
|
||||
# - +token+: A randomly-generated, 50-character string
|
||||
# - +signature+: A hexadecimal HMAC-SHA256 of the timestamp concatenated with the token, generated using the Mailgun API key
|
||||
#
|
||||
# Authenticates requests by validating their signatures.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated, or if its timestamp is more than 2 minutes old
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mailgun
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
||||
# - <tt>500 Server Error</tt> if the Mailgun API key is missing, or one of the Active Record database,
|
||||
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# 1. Give Action Mailbox your {Mailgun API key}[https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials-]
|
||||
# so it can authenticate requests to the Mailgun ingress.
|
||||
#
|
||||
# Use <tt>rails credentials:edit</tt> to add your API key to your application's encrypted credentials under
|
||||
# +action_mailbox.mailgun_api_key+, where Action Mailbox will automatically find it:
|
||||
#
|
||||
# action_mailbox:
|
||||
# mailgun_api_key: ...
|
||||
#
|
||||
# Alternatively, provide your API key in the +MAILGUN_INGRESS_API_KEY+ environment variable.
|
||||
#
|
||||
# 2. Tell Action Mailbox to accept emails from Mailgun:
|
||||
#
|
||||
# # config/environments/production.rb
|
||||
# config.action_mailbox.ingress = :mailgun
|
||||
#
|
||||
# 3. {Configure Mailgun}[https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages]
|
||||
# to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`.
|
||||
#
|
||||
# If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
||||
# <tt>https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime</tt>.
|
||||
class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate
|
||||
module ActionMailbox
|
||||
# Ingests inbound emails from Mailgun. Requires the following parameters:
|
||||
#
|
||||
# - +body-mime+: The full RFC 822 message
|
||||
# - +timestamp+: The current time according to Mailgun as the number of seconds passed since the UNIX epoch
|
||||
# - +token+: A randomly-generated, 50-character string
|
||||
# - +signature+: A hexadecimal HMAC-SHA256 of the timestamp concatenated with the token, generated using the Mailgun API key
|
||||
#
|
||||
# Authenticates requests by validating their signatures.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated, or if its timestamp is more than 2 minutes old
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mailgun
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
||||
# - <tt>500 Server Error</tt> if the Mailgun API key is missing, or one of the Active Record database,
|
||||
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# 1. Give Action Mailbox your {Mailgun API key}[https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials-]
|
||||
# so it can authenticate requests to the Mailgun ingress.
|
||||
#
|
||||
# Use <tt>rails credentials:edit</tt> to add your API key to your application's encrypted credentials under
|
||||
# +action_mailbox.mailgun_api_key+, where Action Mailbox will automatically find it:
|
||||
#
|
||||
# action_mailbox:
|
||||
# mailgun_api_key: ...
|
||||
#
|
||||
# Alternatively, provide your API key in the +MAILGUN_INGRESS_API_KEY+ environment variable.
|
||||
#
|
||||
# 2. Tell Action Mailbox to accept emails from Mailgun:
|
||||
#
|
||||
# # config/environments/production.rb
|
||||
# config.action_mailbox.ingress = :mailgun
|
||||
#
|
||||
# 3. {Configure Mailgun}[https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages]
|
||||
# to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`.
|
||||
#
|
||||
# If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
||||
# <tt>https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime</tt>.
|
||||
class Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate
|
||||
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime")
|
||||
end
|
||||
|
||||
private
|
||||
def authenticate
|
||||
head :unauthorized unless authenticated?
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime")
|
||||
end
|
||||
|
||||
def authenticated?
|
||||
if key.present?
|
||||
Authenticator.new(
|
||||
key: key,
|
||||
timestamp: params.require(:timestamp),
|
||||
token: params.require(:token),
|
||||
signature: params.require(:signature)
|
||||
).authenticated?
|
||||
else
|
||||
raise ArgumentError, <<~MESSAGE.squish
|
||||
Missing required Mailgun API key. Set action_mailbox.mailgun_api_key in your application's
|
||||
encrypted credentials or provide the MAILGUN_INGRESS_API_KEY environment variable.
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
def key
|
||||
Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) || ENV["MAILGUN_INGRESS_API_KEY"]
|
||||
end
|
||||
|
||||
class Authenticator
|
||||
attr_reader :key, :timestamp, :token, :signature
|
||||
|
||||
def initialize(key:, timestamp:, token:, signature:)
|
||||
@key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature
|
||||
private
|
||||
def authenticate
|
||||
head :unauthorized unless authenticated?
|
||||
end
|
||||
|
||||
def authenticated?
|
||||
signed? && recent?
|
||||
if key.present?
|
||||
Authenticator.new(
|
||||
key: key,
|
||||
timestamp: params.require(:timestamp),
|
||||
token: params.require(:token),
|
||||
signature: params.require(:signature)
|
||||
).authenticated?
|
||||
else
|
||||
raise ArgumentError, <<~MESSAGE.squish
|
||||
Missing required Mailgun API key. Set action_mailbox.mailgun_api_key in your application's
|
||||
encrypted credentials or provide the MAILGUN_INGRESS_API_KEY environment variable.
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def signed?
|
||||
ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
|
||||
def key
|
||||
Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) || ENV["MAILGUN_INGRESS_API_KEY"]
|
||||
end
|
||||
|
||||
class Authenticator
|
||||
attr_reader :key, :timestamp, :token, :signature
|
||||
|
||||
def initialize(key:, timestamp:, token:, signature:)
|
||||
@key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature
|
||||
end
|
||||
|
||||
# Allow for 2 minutes of drift between Mailgun time and local server time.
|
||||
def recent?
|
||||
Time.at(timestamp) >= 2.minutes.ago
|
||||
def authenticated?
|
||||
signed? && recent?
|
||||
end
|
||||
|
||||
def expected_signature
|
||||
OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}"
|
||||
end
|
||||
end
|
||||
private
|
||||
def signed?
|
||||
ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
|
||||
end
|
||||
|
||||
# Allow for 2 minutes of drift between Mailgun time and local server time.
|
||||
def recent?
|
||||
Time.at(timestamp) >= 2.minutes.ago
|
||||
end
|
||||
|
||||
def expected_signature
|
||||
OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,80 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Ingests inbound emails from Mandrill.
|
||||
#
|
||||
# Requires a +mandrill_events+ parameter containing a JSON array of Mandrill inbound email event objects.
|
||||
# Each event is expected to have a +msg+ object containing a full RFC 822 message in its +raw_msg+ property.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mandrill
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
||||
# - <tt>500 Server Error</tt> if the Mandrill API key is missing, or one of the Active Record database,
|
||||
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
||||
class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate
|
||||
module ActionMailbox
|
||||
# Ingests inbound emails from Mandrill.
|
||||
#
|
||||
# Requires a +mandrill_events+ parameter containing a JSON array of Mandrill inbound email event objects.
|
||||
# Each event is expected to have a +msg+ object containing a full RFC 822 message in its +raw_msg+ property.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mandrill
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
||||
# - <tt>500 Server Error</tt> if the Mandrill API key is missing, or one of the Active Record database,
|
||||
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
||||
class Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate
|
||||
|
||||
def create
|
||||
raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email }
|
||||
head :ok
|
||||
rescue JSON::ParserError => error
|
||||
logger.error error.message
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
def raw_emails
|
||||
events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") }
|
||||
def create
|
||||
raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email }
|
||||
head :ok
|
||||
rescue JSON::ParserError => error
|
||||
logger.error error.message
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
def events
|
||||
JSON.parse params.require(:mandrill_events)
|
||||
end
|
||||
|
||||
|
||||
def authenticate
|
||||
head :unauthorized unless authenticated?
|
||||
end
|
||||
|
||||
def authenticated?
|
||||
if key.present?
|
||||
Authenticator.new(request, key).authenticated?
|
||||
else
|
||||
raise ArgumentError, <<~MESSAGE.squish
|
||||
Missing required Mandrill API key. Set action_mailbox.mandrill_api_key in your application's
|
||||
encrypted credentials or provide the MANDRILL_INGRESS_API_KEY environment variable.
|
||||
MESSAGE
|
||||
private
|
||||
def raw_emails
|
||||
events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") }
|
||||
end
|
||||
end
|
||||
|
||||
def key
|
||||
Rails.application.credentials.dig(:action_mailbox, :mandrill_api_key) || ENV["MANDRILL_INGRESS_API_KEY"]
|
||||
end
|
||||
def events
|
||||
JSON.parse params.require(:mandrill_events)
|
||||
end
|
||||
|
||||
class Authenticator
|
||||
attr_reader :request, :key
|
||||
|
||||
def initialize(request, key)
|
||||
@request, @key = request, key
|
||||
def authenticate
|
||||
head :unauthorized unless authenticated?
|
||||
end
|
||||
|
||||
def authenticated?
|
||||
ActiveSupport::SecurityUtils.secure_compare given_signature, expected_signature
|
||||
if key.present?
|
||||
Authenticator.new(request, key).authenticated?
|
||||
else
|
||||
raise ArgumentError, <<~MESSAGE.squish
|
||||
Missing required Mandrill API key. Set action_mailbox.mandrill_api_key in your application's
|
||||
encrypted credentials or provide the MANDRILL_INGRESS_API_KEY environment variable.
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def given_signature
|
||||
request.headers["X-Mandrill-Signature"]
|
||||
def key
|
||||
Rails.application.credentials.dig(:action_mailbox, :mandrill_api_key) || ENV["MANDRILL_INGRESS_API_KEY"]
|
||||
end
|
||||
|
||||
class Authenticator
|
||||
attr_reader :request, :key
|
||||
|
||||
def initialize(request, key)
|
||||
@request, @key = request, key
|
||||
end
|
||||
|
||||
def expected_signature
|
||||
Base64.strict_encode64 OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)
|
||||
def authenticated?
|
||||
ActiveSupport::SecurityUtils.secure_compare given_signature, expected_signature
|
||||
end
|
||||
|
||||
def message
|
||||
request.url + request.POST.sort.flatten.join
|
||||
end
|
||||
end
|
||||
private
|
||||
def given_signature
|
||||
request.headers["X-Mandrill-Signature"]
|
||||
end
|
||||
|
||||
def expected_signature
|
||||
Base64.strict_encode64 OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)
|
||||
end
|
||||
|
||||
def message
|
||||
request.url + request.POST.sort.flatten.join
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,57 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Ingests inbound emails relayed from Postfix.
|
||||
#
|
||||
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
||||
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
||||
#
|
||||
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
||||
# the Postfix ingress can learn its password. You should only use the Postfix ingress over HTTPS.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request could not be authenticated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postfix
|
||||
# - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message
|
||||
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
||||
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# 1. Tell Action Mailbox to accept emails from Postfix:
|
||||
#
|
||||
# # config/environments/production.rb
|
||||
# config.action_mailbox.ingress = :postfix
|
||||
#
|
||||
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress.
|
||||
#
|
||||
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
||||
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
||||
#
|
||||
# action_mailbox:
|
||||
# ingress_password: ...
|
||||
#
|
||||
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
||||
#
|
||||
# 3. {Configure Postfix}{https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script}
|
||||
# to pipe inbound emails to <tt>bin/rails action_mailbox:ingress:postfix</tt>, providing the +URL+ of the Postfix
|
||||
# ingress and the +INGRESS_PASSWORD+ you previously generated.
|
||||
#
|
||||
# If your application lived at <tt>https://example.com</tt>, the full command would look like this:
|
||||
#
|
||||
# URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix
|
||||
class ActionMailbox::Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate_by_password, :require_valid_rfc822_message
|
||||
module ActionMailbox
|
||||
# Ingests inbound emails relayed from Postfix.
|
||||
#
|
||||
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
||||
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
||||
#
|
||||
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
||||
# the Postfix ingress can learn its password. You should only use the Postfix ingress over HTTPS.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request could not be authenticated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postfix
|
||||
# - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message
|
||||
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
||||
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# 1. Tell Action Mailbox to accept emails from Postfix:
|
||||
#
|
||||
# # config/environments/production.rb
|
||||
# config.action_mailbox.ingress = :postfix
|
||||
#
|
||||
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress.
|
||||
#
|
||||
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
||||
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
||||
#
|
||||
# action_mailbox:
|
||||
# ingress_password: ...
|
||||
#
|
||||
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
||||
#
|
||||
# 3. {Configure Postfix}{https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script}
|
||||
# to pipe inbound emails to <tt>bin/rails action_mailbox:ingress:postfix</tt>, providing the +URL+ of the Postfix
|
||||
# ingress and the +INGRESS_PASSWORD+ you previously generated.
|
||||
#
|
||||
# If your application lived at <tt>https://example.com</tt>, the full command would look like this:
|
||||
#
|
||||
# URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix
|
||||
class Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate_by_password, :require_valid_rfc822_message
|
||||
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read
|
||||
end
|
||||
|
||||
private
|
||||
def require_valid_rfc822_message
|
||||
unless request.content_type == "message/rfc822"
|
||||
head :unsupported_media_type
|
||||
end
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read
|
||||
end
|
||||
|
||||
private
|
||||
def require_valid_rfc822_message
|
||||
unless request.content_type == "message/rfc822"
|
||||
head :unsupported_media_type
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,52 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Ingests inbound emails from SendGrid. Requires an +email+ parameter containing a full RFC 822 message.
|
||||
#
|
||||
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
||||
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
||||
#
|
||||
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
||||
# the SendGrid ingress can learn its password. You should only use the SendGrid ingress over HTTPS.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SendGrid
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +email+ parameter
|
||||
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
||||
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# 1. Tell Action Mailbox to accept emails from SendGrid:
|
||||
#
|
||||
# # config/environments/production.rb
|
||||
# config.action_mailbox.ingress = :sendgrid
|
||||
#
|
||||
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress.
|
||||
#
|
||||
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
||||
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
||||
#
|
||||
# action_mailbox:
|
||||
# ingress_password: ...
|
||||
#
|
||||
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
||||
#
|
||||
# 3. {Configure SendGrid Inbound Parse}{https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/}
|
||||
# to forward inbound emails to +/rails/action_mailbox/sendgrid/inbound_emails+ with the username +actionmailbox+ and
|
||||
# the password you previously generated. If your application lived at <tt>https://example.com</tt>, you would
|
||||
# configure SendGrid with the following fully-qualified URL:
|
||||
#
|
||||
# https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails
|
||||
#
|
||||
# *NOTE:* When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled *"Post the raw,
|
||||
# full MIME message."* Action Mailbox needs the raw MIME message to work.
|
||||
class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate_by_password
|
||||
module ActionMailbox
|
||||
# Ingests inbound emails from SendGrid. Requires an +email+ parameter containing a full RFC 822 message.
|
||||
#
|
||||
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
||||
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
||||
#
|
||||
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
||||
# the SendGrid ingress can learn its password. You should only use the SendGrid ingress over HTTPS.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SendGrid
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +email+ parameter
|
||||
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
||||
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# 1. Tell Action Mailbox to accept emails from SendGrid:
|
||||
#
|
||||
# # config/environments/production.rb
|
||||
# config.action_mailbox.ingress = :sendgrid
|
||||
#
|
||||
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress.
|
||||
#
|
||||
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
||||
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
||||
#
|
||||
# action_mailbox:
|
||||
# ingress_password: ...
|
||||
#
|
||||
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
||||
#
|
||||
# 3. {Configure SendGrid Inbound Parse}{https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/}
|
||||
# to forward inbound emails to +/rails/action_mailbox/sendgrid/inbound_emails+ with the username +actionmailbox+ and
|
||||
# the password you previously generated. If your application lived at <tt>https://example.com</tt>, you would
|
||||
# configure SendGrid with the following fully-qualified URL:
|
||||
#
|
||||
# https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails
|
||||
#
|
||||
# *NOTE:* When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled *"Post the raw,
|
||||
# full MIME message."* Action Mailbox needs the raw MIME message to work.
|
||||
class Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate_by_password
|
||||
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rails::Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController
|
||||
def index
|
||||
@inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def show
|
||||
@inbound_email = ActionMailbox::InboundEmail.find(params[:id])
|
||||
end
|
||||
|
||||
def create
|
||||
inbound_email = create_inbound_email(new_mail)
|
||||
redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
|
||||
end
|
||||
|
||||
private
|
||||
def new_mail
|
||||
Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h
|
||||
module Rails
|
||||
class Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController
|
||||
def index
|
||||
@inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def create_inbound_email(mail)
|
||||
ActionMailbox::InboundEmail.create! raw_email: \
|
||||
{ io: StringIO.new(mail.to_s), filename: "inbound.eml", content_type: "message/rfc822" }
|
||||
def new
|
||||
end
|
||||
|
||||
def show
|
||||
@inbound_email = ActionMailbox::InboundEmail.find(params[:id])
|
||||
end
|
||||
|
||||
def create
|
||||
inbound_email = create_inbound_email(new_mail)
|
||||
redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
|
||||
end
|
||||
|
||||
private
|
||||
def new_mail
|
||||
Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h
|
||||
end
|
||||
|
||||
def create_inbound_email(mail)
|
||||
ActionMailbox::InboundEmail.create! raw_email: \
|
||||
{ io: StringIO.new(mail.to_s), filename: "inbound.eml", content_type: "message/rfc822" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Rerouting will run routing and processing on an email that has already been, or attempted to be, processed.
|
||||
class Rails::Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController
|
||||
def create
|
||||
inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id])
|
||||
reroute inbound_email
|
||||
module Rails
|
||||
# Rerouting will run routing and processing on an email that has already been, or attempted to be, processed.
|
||||
class Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController
|
||||
def create
|
||||
inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id])
|
||||
reroute inbound_email
|
||||
|
||||
redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
|
||||
end
|
||||
|
||||
private
|
||||
def reroute(inbound_email)
|
||||
inbound_email.pending!
|
||||
inbound_email.route_later
|
||||
redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
|
||||
end
|
||||
|
||||
private
|
||||
def reroute(inbound_email)
|
||||
inbound_email.pending!
|
||||
inbound_email.route_later
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# TODO: Move this to Rails::Conductor gem
|
||||
class Rails::Conductor::BaseController < ActionController::Base
|
||||
layout "rails/conductor"
|
||||
before_action :ensure_development_env
|
||||
module Rails
|
||||
# TODO: Move this to Rails::Conductor gem
|
||||
class Conductor::BaseController < ActionController::Base
|
||||
layout "rails/conductor"
|
||||
before_action :ensure_development_env
|
||||
|
||||
private
|
||||
def ensure_development_env
|
||||
head :forbidden unless Rails.env.development?
|
||||
end
|
||||
private
|
||||
def ensure_development_env
|
||||
head :forbidden unless Rails.env.development?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# You can configure when this `IncinerationJob` will be run as a time-after-processing using the
|
||||
# `config.action_mailbox.incinerate_after` or `ActionMailbox.incinerate_after` setting.
|
||||
#
|
||||
# Since this incineration is set for the future, it'll automatically ignore any `InboundEmail`s
|
||||
# that have already been deleted and discard itself if so.
|
||||
class ActionMailbox::IncinerationJob < ActiveJob::Base
|
||||
queue_as { ActionMailbox.queues[:incineration] }
|
||||
module ActionMailbox
|
||||
# You can configure when this `IncinerationJob` will be run as a time-after-processing using the
|
||||
# `config.action_mailbox.incinerate_after` or `ActionMailbox.incinerate_after` setting.
|
||||
#
|
||||
# Since this incineration is set for the future, it'll automatically ignore any `InboundEmail`s
|
||||
# that have already been deleted and discard itself if so.
|
||||
class IncinerationJob < ActiveJob::Base
|
||||
queue_as { ActionMailbox.queues[:incineration] }
|
||||
|
||||
discard_on ActiveRecord::RecordNotFound
|
||||
discard_on ActiveRecord::RecordNotFound
|
||||
|
||||
def self.schedule(inbound_email)
|
||||
set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
|
||||
end
|
||||
def self.schedule(inbound_email)
|
||||
set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
|
||||
end
|
||||
|
||||
def perform(inbound_email)
|
||||
inbound_email.incinerate
|
||||
def perform(inbound_email)
|
||||
inbound_email.incinerate
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly
|
||||
# accept new incoming emails without being burdened to hang while they're actually being processed.
|
||||
class ActionMailbox::RoutingJob < ActiveJob::Base
|
||||
queue_as { ActionMailbox.queues[:routing] }
|
||||
module ActionMailbox
|
||||
# Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly
|
||||
# accept new incoming emails without being burdened to hang while they're actually being processed.
|
||||
class RoutingJob < ActiveJob::Base
|
||||
queue_as { ActionMailbox.queues[:routing] }
|
||||
|
||||
def perform(inbound_email)
|
||||
inbound_email.route
|
||||
def perform(inbound_email)
|
||||
inbound_email.route
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,44 +2,46 @@
|
|||
|
||||
require "mail"
|
||||
|
||||
# The `InboundEmail` is an Active Record that keeps a reference to the raw email stored in Active Storage
|
||||
# and tracks the status of processing. By default, incoming emails will go through the following lifecycle:
|
||||
#
|
||||
# * Pending: Just received by one of the ingress controllers and scheduled for routing.
|
||||
# * Processing: During active processing, while a specific mailbox is running its #process method.
|
||||
# * Delivered: Successfully processed by the specific mailbox.
|
||||
# * Failed: An exception was raised during the specific mailbox's execution of the `#process` method.
|
||||
# * Bounced: Rejected processing by the specific mailbox and bounced to sender.
|
||||
#
|
||||
# Once the `InboundEmail` has reached the status of being either `delivered`, `failed`, or `bounced`,
|
||||
# it'll count as having been `#processed?`. Once processed, the `InboundEmail` will be scheduled for
|
||||
# automatic incineration at a later point.
|
||||
#
|
||||
# When working with an `InboundEmail`, you'll usually interact with the parsed version of the source,
|
||||
# which is available as a `Mail` object from `#mail`. But you can also access the raw source directly
|
||||
# using the `#source` method.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# inbound_email.mail.from # => 'david@loudthinking.com'
|
||||
# inbound_email.source # Returns the full rfc822 source of the email as text
|
||||
class ActionMailbox::InboundEmail < ActiveRecord::Base
|
||||
self.table_name = "action_mailbox_inbound_emails"
|
||||
module ActionMailbox
|
||||
# The `InboundEmail` is an Active Record that keeps a reference to the raw email stored in Active Storage
|
||||
# and tracks the status of processing. By default, incoming emails will go through the following lifecycle:
|
||||
#
|
||||
# * Pending: Just received by one of the ingress controllers and scheduled for routing.
|
||||
# * Processing: During active processing, while a specific mailbox is running its #process method.
|
||||
# * Delivered: Successfully processed by the specific mailbox.
|
||||
# * Failed: An exception was raised during the specific mailbox's execution of the `#process` method.
|
||||
# * Bounced: Rejected processing by the specific mailbox and bounced to sender.
|
||||
#
|
||||
# Once the `InboundEmail` has reached the status of being either `delivered`, `failed`, or `bounced`,
|
||||
# it'll count as having been `#processed?`. Once processed, the `InboundEmail` will be scheduled for
|
||||
# automatic incineration at a later point.
|
||||
#
|
||||
# When working with an `InboundEmail`, you'll usually interact with the parsed version of the source,
|
||||
# which is available as a `Mail` object from `#mail`. But you can also access the raw source directly
|
||||
# using the `#source` method.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# inbound_email.mail.from # => 'david@loudthinking.com'
|
||||
# inbound_email.source # Returns the full rfc822 source of the email as text
|
||||
class InboundEmail < ActiveRecord::Base
|
||||
self.table_name = "action_mailbox_inbound_emails"
|
||||
|
||||
include Incineratable, MessageId, Routable
|
||||
include Incineratable, MessageId, Routable
|
||||
|
||||
has_one_attached :raw_email
|
||||
enum status: %i[ pending processing delivered failed bounced ]
|
||||
has_one_attached :raw_email
|
||||
enum status: %i[ pending processing delivered failed bounced ]
|
||||
|
||||
def mail
|
||||
@mail ||= Mail.from_source(source)
|
||||
end
|
||||
def mail
|
||||
@mail ||= Mail.from_source(source)
|
||||
end
|
||||
|
||||
def source
|
||||
@source ||= raw_email.download
|
||||
end
|
||||
def source
|
||||
@source ||= raw_email.download
|
||||
end
|
||||
|
||||
def processed?
|
||||
delivered? || failed? || bounced?
|
||||
def processed?
|
||||
delivered? || failed? || bounced?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Command class for carrying out the actual incineration of the `InboundMail` that's been scheduled
|
||||
# for removal. Before the incineration – which really is just a call to `#destroy!` – is run, we verify
|
||||
# that it's both eligible (by virtue of having already been processed) and time to do so (that is,
|
||||
# the `InboundEmail` was processed after the `incinerate_after` time).
|
||||
class ActionMailbox::InboundEmail::Incineratable::Incineration
|
||||
def initialize(inbound_email)
|
||||
@inbound_email = inbound_email
|
||||
end
|
||||
|
||||
def run
|
||||
@inbound_email.destroy! if due? && processed?
|
||||
end
|
||||
|
||||
private
|
||||
def due?
|
||||
@inbound_email.updated_at < ActionMailbox.incinerate_after.ago.end_of_day
|
||||
module ActionMailbox
|
||||
# Command class for carrying out the actual incineration of the `InboundMail` that's been scheduled
|
||||
# for removal. Before the incineration – which really is just a call to `#destroy!` – is run, we verify
|
||||
# that it's both eligible (by virtue of having already been processed) and time to do so (that is,
|
||||
# the `InboundEmail` was processed after the `incinerate_after` time).
|
||||
class InboundEmail::Incineratable::Incineration
|
||||
def initialize(inbound_email)
|
||||
@inbound_email = inbound_email
|
||||
end
|
||||
|
||||
def processed?
|
||||
@inbound_email.processed?
|
||||
def run
|
||||
@inbound_email.destroy! if due? && processed?
|
||||
end
|
||||
|
||||
private
|
||||
def due?
|
||||
@inbound_email.updated_at < ActionMailbox.incinerate_after.ago.end_of_day
|
||||
end
|
||||
|
||||
def processed?
|
||||
@inbound_email.processed?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mail::Address
|
||||
def ==(other_address)
|
||||
other_address.is_a?(Mail::Address) && to_s == other_address.to_s
|
||||
module Mail
|
||||
class Address
|
||||
def ==(other_address)
|
||||
other_address.is_a?(Mail::Address) && to_s == other_address.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mail::Address
|
||||
def self.wrap(address)
|
||||
address.is_a?(Mail::Address) ? address : Mail::Address.new(address)
|
||||
module Mail
|
||||
class Address
|
||||
def self.wrap(address)
|
||||
address.is_a?(Mail::Address) ? address : Mail::Address.new(address)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mail::Message
|
||||
def from_address
|
||||
header[:from]&.address_list&.addresses&.first
|
||||
end
|
||||
module Mail
|
||||
class Message
|
||||
def from_address
|
||||
header[:from]&.address_list&.addresses&.first
|
||||
end
|
||||
|
||||
def recipients_addresses
|
||||
to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses
|
||||
end
|
||||
def recipients_addresses
|
||||
to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses
|
||||
end
|
||||
|
||||
def to_addresses
|
||||
Array(header[:to]&.address_list&.addresses)
|
||||
end
|
||||
def to_addresses
|
||||
Array(header[:to]&.address_list&.addresses)
|
||||
end
|
||||
|
||||
def cc_addresses
|
||||
Array(header[:cc]&.address_list&.addresses)
|
||||
end
|
||||
def cc_addresses
|
||||
Array(header[:cc]&.address_list&.addresses)
|
||||
end
|
||||
|
||||
def bcc_addresses
|
||||
Array(header[:bcc]&.address_list&.addresses)
|
||||
end
|
||||
def bcc_addresses
|
||||
Array(header[:bcc]&.address_list&.addresses)
|
||||
end
|
||||
|
||||
def x_original_to_addresses
|
||||
Array(header[:x_original_to]).collect { |header| Mail::Address.new header.to_s }
|
||||
def x_original_to_addresses
|
||||
Array(header[:x_original_to]).collect { |header| Mail::Address.new header.to_s }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mail::Message
|
||||
def recipients
|
||||
Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s)
|
||||
module Mail
|
||||
class Message
|
||||
def recipients
|
||||
Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,40 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when
|
||||
# an inbound_email is received.
|
||||
class ActionMailbox::Router
|
||||
class RoutingError < StandardError; end
|
||||
module ActionMailbox
|
||||
# Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when
|
||||
# an inbound_email is received.
|
||||
class Router
|
||||
class RoutingError < StandardError; end
|
||||
|
||||
def initialize
|
||||
@routes = []
|
||||
end
|
||||
|
||||
def add_routes(routes)
|
||||
routes.each do |(address, mailbox_name)|
|
||||
add_route address, to: mailbox_name
|
||||
def initialize
|
||||
@routes = []
|
||||
end
|
||||
end
|
||||
|
||||
def add_route(address, to:)
|
||||
routes.append Route.new(address, to: to)
|
||||
end
|
||||
|
||||
def route(inbound_email)
|
||||
if mailbox = match_to_mailbox(inbound_email)
|
||||
mailbox.receive(inbound_email)
|
||||
else
|
||||
inbound_email.bounced!
|
||||
|
||||
raise RoutingError
|
||||
def add_routes(routes)
|
||||
routes.each do |(address, mailbox_name)|
|
||||
add_route address, to: mailbox_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :routes
|
||||
|
||||
def match_to_mailbox(inbound_email)
|
||||
routes.detect { |route| route.match?(inbound_email) }.try(:mailbox_class)
|
||||
def add_route(address, to:)
|
||||
routes.append Route.new(address, to: to)
|
||||
end
|
||||
|
||||
def route(inbound_email)
|
||||
if mailbox = match_to_mailbox(inbound_email)
|
||||
mailbox.receive(inbound_email)
|
||||
else
|
||||
inbound_email.bounced!
|
||||
|
||||
raise RoutingError
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :routes
|
||||
|
||||
def match_to_mailbox(inbound_email)
|
||||
routes.detect { |route| route.match?(inbound_email) }.try(:mailbox_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require "action_mailbox/router/route"
|
||||
|
|
|
@ -1,40 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching
|
||||
# mailbox class. See examples for the different route addresses and how to use them in the `ActionMailbox::Base`
|
||||
# documentation.
|
||||
class ActionMailbox::Router::Route
|
||||
attr_reader :address, :mailbox_name
|
||||
module ActionMailbox
|
||||
# Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching
|
||||
# mailbox class. See examples for the different route addresses and how to use them in the `ActionMailbox::Base`
|
||||
# documentation.
|
||||
class Router::Route
|
||||
attr_reader :address, :mailbox_name
|
||||
|
||||
def initialize(address, to:)
|
||||
@address, @mailbox_name = address, to
|
||||
def initialize(address, to:)
|
||||
@address, @mailbox_name = address, to
|
||||
|
||||
ensure_valid_address
|
||||
end
|
||||
|
||||
def match?(inbound_email)
|
||||
case address
|
||||
when :all
|
||||
true
|
||||
when String
|
||||
inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) }
|
||||
when Regexp
|
||||
inbound_email.mail.recipients.any? { |recipient| address.match?(recipient) }
|
||||
when Proc
|
||||
address.call(inbound_email)
|
||||
else
|
||||
address.match?(inbound_email)
|
||||
ensure_valid_address
|
||||
end
|
||||
end
|
||||
|
||||
def mailbox_class
|
||||
"#{mailbox_name.to_s.camelize}Mailbox".constantize
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_valid_address
|
||||
unless [ Symbol, String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?)
|
||||
raise ArgumentError, "Expected a Symbol, String, Regexp, Proc, or matchable, got #{address.inspect}"
|
||||
def match?(inbound_email)
|
||||
case address
|
||||
when :all
|
||||
true
|
||||
when String
|
||||
inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) }
|
||||
when Regexp
|
||||
inbound_email.mail.recipients.any? { |recipient| address.match?(recipient) }
|
||||
when Proc
|
||||
address.call(inbound_email)
|
||||
else
|
||||
address.match?(inbound_email)
|
||||
end
|
||||
end
|
||||
|
||||
def mailbox_class
|
||||
"#{mailbox_name.to_s.camelize}Mailbox".constantize
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_valid_address
|
||||
unless [ Symbol, String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?)
|
||||
raise ArgumentError, "Expected a Symbol, String, Regexp, Proc, or matchable, got #{address.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue