Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Nest Action Mailbox classes in the API docs
- Loading branch information
1 parent
11a8ba1
commit 6c168aa
Showing
19 changed files
with
546 additions
and
508 deletions.
There are no files selected for viewing
50 changes: 26 additions & 24 deletions
50
actionmailbox/app/controllers/action_mailbox/base_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
end | ||
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 | ||
private | ||
def ensure_configured | ||
unless ActionMailbox.ingress == ingress_name | ||
head :not_found | ||
end | ||
end | ||
end | ||
|
||
def ingress_name | ||
self.class.name.remove(/\AActionMailbox::Ingresses::/, /::InboundEmailsController\z/).underscore.to_sym | ||
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" | ||
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 | ||
end | ||
|
||
def password | ||
Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"] | ||
end | ||
def password | ||
Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"] | ||
end | ||
end | ||
end |
90 changes: 46 additions & 44 deletions
90
actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
172 changes: 87 additions & 85 deletions
172
actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
private | ||
def authenticate | ||
head :unauthorized unless authenticated? | ||
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 | ||
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 authenticated? | ||
signed? && recent? | ||
def key | ||
Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) || ENV["MAILGUN_INGRESS_API_KEY"] | ||
end | ||
|
||
private | ||
def signed? | ||
ActiveSupport::SecurityUtils.secure_compare signature, expected_signature | ||
end | ||
class Authenticator | ||
attr_reader :key, :timestamp, :token, :signature | ||
|
||
# Allow for 2 minutes of drift between Mailgun time and local server time. | ||
def recent? | ||
Time.at(timestamp) >= 2.minutes.ago | ||
def initialize(key:, timestamp:, token:, signature:) | ||
@key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature | ||
end | ||
|
||
def expected_signature | ||
OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}" | ||
def authenticated? | ||
signed? && recent? | ||
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 |
Oops, something went wrong.