mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #1 from basecamp/ingresses
Accept inbound emails from a variety of ingresses
This commit is contained in:
commit
152a442b19
25 changed files with 599 additions and 67 deletions
2
Gemfile
2
Gemfile
|
@ -4,3 +4,5 @@ git_source(:github) { |repo_path| "https://github.com/#{repo_path}.git" }
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
gem "rails", github: "rails/rails"
|
gem "rails", github: "rails/rails"
|
||||||
|
|
||||||
|
gem "aws-sdk-sns"
|
||||||
|
|
31
Gemfile.lock
31
Gemfile.lock
|
@ -66,20 +66,46 @@ PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
actionmailbox (0.1.0)
|
actionmailbox (0.1.0)
|
||||||
|
http (>= 4.0.0)
|
||||||
rails (>= 5.2.0)
|
rails (>= 5.2.0)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
|
addressable (2.5.2)
|
||||||
|
public_suffix (>= 2.0.2, < 4.0)
|
||||||
|
aws-eventstream (1.0.1)
|
||||||
|
aws-partitions (1.105.0)
|
||||||
|
aws-sdk-core (3.30.0)
|
||||||
|
aws-eventstream (~> 1.0)
|
||||||
|
aws-partitions (~> 1.0)
|
||||||
|
aws-sigv4 (~> 1.0)
|
||||||
|
jmespath (~> 1.0)
|
||||||
|
aws-sdk-sns (1.5.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.26.0)
|
||||||
|
aws-sigv4 (~> 1.0)
|
||||||
|
aws-sigv4 (1.0.3)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
byebug (10.0.2)
|
byebug (10.0.2)
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
crass (1.0.4)
|
crass (1.0.4)
|
||||||
|
domain_name (0.5.20180417)
|
||||||
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
erubi (1.7.1)
|
erubi (1.7.1)
|
||||||
globalid (0.4.1)
|
globalid (0.4.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
|
http (4.0.0)
|
||||||
|
addressable (~> 2.3)
|
||||||
|
http-cookie (~> 1.0)
|
||||||
|
http-form_data (~> 2.0)
|
||||||
|
http_parser.rb (~> 0.6.0)
|
||||||
|
http-cookie (1.0.3)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
http-form_data (2.1.1)
|
||||||
|
http_parser.rb (0.6.0)
|
||||||
i18n (1.1.0)
|
i18n (1.1.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
jmespath (1.4.0)
|
||||||
loofah (2.2.2)
|
loofah (2.2.2)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
|
@ -95,6 +121,7 @@ GEM
|
||||||
nio4r (2.3.1)
|
nio4r (2.3.1)
|
||||||
nokogiri (1.8.5)
|
nokogiri (1.8.5)
|
||||||
mini_portile2 (~> 2.3.0)
|
mini_portile2 (~> 2.3.0)
|
||||||
|
public_suffix (3.0.3)
|
||||||
rack (2.0.5)
|
rack (2.0.5)
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
|
@ -116,6 +143,9 @@ GEM
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tzinfo (1.2.5)
|
tzinfo (1.2.5)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
|
unf (0.1.4)
|
||||||
|
unf_ext
|
||||||
|
unf_ext (0.0.7.5)
|
||||||
websocket-driver (0.7.0)
|
websocket-driver (0.7.0)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.3)
|
websocket-extensions (0.1.3)
|
||||||
|
@ -125,6 +155,7 @@ PLATFORMS
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
actionmailbox!
|
actionmailbox!
|
||||||
|
aws-sdk-sns
|
||||||
bundler (~> 1.15)
|
bundler (~> 1.15)
|
||||||
byebug
|
byebug
|
||||||
rails!
|
rails!
|
||||||
|
|
|
@ -16,6 +16,7 @@ Gem::Specification.new do |s|
|
||||||
s.required_ruby_version = ">= 2.5.0"
|
s.required_ruby_version = ">= 2.5.0"
|
||||||
|
|
||||||
s.add_dependency "rails", ">= 5.2.0"
|
s.add_dependency "rails", ">= 5.2.0"
|
||||||
|
s.add_dependency "http", ">= 4.0.0"
|
||||||
|
|
||||||
s.add_development_dependency "bundler", "~> 1.15"
|
s.add_development_dependency "bundler", "~> 1.15"
|
||||||
s.add_development_dependency "sqlite3"
|
s.add_development_dependency "sqlite3"
|
||||||
|
|
42
app/controllers/action_mailbox/base_controller.rb
Normal file
42
app/controllers/action_mailbox/base_controller.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
class ActionMailbox::BaseController < ActionController::Base
|
||||||
|
skip_forgery_protection
|
||||||
|
|
||||||
|
def self.prepare
|
||||||
|
# Override in concrete controllers to run code on load.
|
||||||
|
end
|
||||||
|
|
||||||
|
before_action :ensure_configured
|
||||||
|
|
||||||
|
private
|
||||||
|
def ensure_configured
|
||||||
|
unless ActionMailbox.ingress == ingress_name
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ingress_name
|
||||||
|
self.class.name[/^ActionMailbox::Ingresses::(.*?)::/, 1].underscore.to_sym
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_by_password
|
||||||
|
if password.present?
|
||||||
|
http_basic_authenticate_or_request_with username: "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
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Extract to ActionController::HttpAuthentication
|
||||||
|
def http_basic_authenticate_or_request_with(username:, password:, realm: nil)
|
||||||
|
authenticate_or_request_with_http_basic(realm || "Application") do |given_username, given_password|
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare(given_username, username) &
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare(given_password, password)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,17 +0,0 @@
|
||||||
# TODO: Add access protection using basic auth with verified tokens. Maybe coming from credentials by default?
|
|
||||||
# TODO: Spam/malware catching?
|
|
||||||
# TODO: Specific bounces for SMTP good citizenship: 200/404/400
|
|
||||||
class ActionMailbox::InboundEmailsController < ActionController::Base
|
|
||||||
skip_forgery_protection
|
|
||||||
before_action :require_rfc822_message, only: :create
|
|
||||||
|
|
||||||
def create
|
|
||||||
ActionMailbox::InboundEmail.create_and_extract_message_id!(params[:message])
|
|
||||||
head :created
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def require_rfc822_message
|
|
||||||
head :unsupported_media_type unless params.require(:message).content_type == 'message/rfc822'
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController
|
||||||
|
before_action :authenticate
|
||||||
|
|
||||||
|
cattr_accessor :verifier
|
||||||
|
|
||||||
|
def self.prepare
|
||||||
|
self.verifier ||= begin
|
||||||
|
require "aws-sdk-sns/message_verifier"
|
||||||
|
Aws::SNS::MessageVerifier.new
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,58 @@
|
||||||
|
class ActionMailbox::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?
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticated?
|
||||||
|
signed? && recent?
|
||||||
|
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
|
|
@ -0,0 +1,65 @@
|
||||||
|
class ActionMailbox::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") }
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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 authenticated?
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare given_signature, expected_signature
|
||||||
|
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
|
|
@ -0,0 +1,14 @@
|
||||||
|
class ActionMailbox::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
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController
|
||||||
|
before_action :authenticate_by_password
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,14 +6,16 @@ module ActionMailbox::InboundEmail::MessageId
|
||||||
end
|
end
|
||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
def create_and_extract_message_id!(raw_email, **options)
|
def create_and_extract_message_id!(source, **options)
|
||||||
create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options
|
create! message_id: extract_message_id(source), **options do |inbound_email|
|
||||||
|
inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def extract_message_id(raw_email)
|
def extract_message_id(source)
|
||||||
mail_from_source(raw_email.read).message_id
|
mail_from_source(source).message_id
|
||||||
rescue
|
rescue => e
|
||||||
# FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead"
|
# FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
post "/rails/action_mailbox/inbound_emails" => "action_mailbox/inbound_emails#create", as: :rails_inbound_emails
|
scope "/rails/action_mailbox", module: "action_mailbox/ingresses" do
|
||||||
|
post "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails
|
||||||
|
post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails
|
||||||
|
post "/postfix/inbound_emails" => "postfix/inbound_emails#create", as: :rails_postfix_inbound_emails
|
||||||
|
post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails
|
||||||
|
|
||||||
|
# Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails.
|
||||||
|
post "/mailgun/inbound_emails/mime" => "mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: Should these be mounted within the engine only?
|
# TODO: Should these be mounted within the engine only?
|
||||||
scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do
|
scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do
|
||||||
resources :inbound_emails, as: :rails_conductor_inbound_emails
|
resources :inbound_emails, as: :rails_conductor_inbound_emails do
|
||||||
post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
|
post "reroute" => "reroutes#create"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,7 @@ module ActionMailbox
|
||||||
autoload :Base
|
autoload :Base
|
||||||
autoload :Router
|
autoload :Router
|
||||||
|
|
||||||
|
mattr_accessor :ingress
|
||||||
mattr_accessor :logger
|
mattr_accessor :logger
|
||||||
mattr_accessor :incinerate_after, default: 30.days
|
mattr_accessor :incinerate_after, default: 30.days
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,5 +14,17 @@ module ActionMailbox
|
||||||
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
|
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
initializer "action_mailbox.ingress" do
|
||||||
|
config.after_initialize do |app|
|
||||||
|
if ActionMailbox.ingress = app.config.action_mailbox.ingress.presence
|
||||||
|
config.to_prepare do
|
||||||
|
if ingress_controller_class = "ActionMailbox::Ingresses::#{ActionMailbox.ingress.to_s.classify}::InboundEmailsController".safe_constantize
|
||||||
|
ingress_controller_class.prepare
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,18 +5,15 @@ module ActionMailbox
|
||||||
# Create an InboundEmail record using an eml fixture in the format of message/rfc822
|
# Create an InboundEmail record using an eml fixture in the format of message/rfc822
|
||||||
# referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
|
# referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
|
||||||
def create_inbound_email_from_fixture(fixture_name, status: :processing)
|
def create_inbound_email_from_fixture(fixture_name, status: :processing)
|
||||||
create_inbound_email file_fixture(fixture_name), filename: fixture_name, status: status
|
create_inbound_email file_fixture(fixture_name).read, status: status
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_inbound_email_from_mail(status: :processing, **mail_options)
|
def create_inbound_email_from_mail(status: :processing, **mail_options)
|
||||||
raw_email = Tempfile.new.tap { |io| io.write Mail.new(mail_options).to_s }
|
create_inbound_email Mail.new(mail_options).to_s, status: status
|
||||||
create_inbound_email(raw_email, status: status)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_inbound_email(io, filename: 'mail.eml', status: :processing)
|
def create_inbound_email(source, status: :processing)
|
||||||
ActionMailbox::InboundEmail.create_and_extract_message_id! \
|
ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
|
||||||
ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'),
|
|
||||||
status: status
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def receive_inbound_email_from_fixture(*args)
|
def receive_inbound_email_from_fixture(*args)
|
||||||
|
|
41
lib/tasks/ingress.rake
Normal file
41
lib/tasks/ingress.rake
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
namespace :action_mailbox do
|
||||||
|
namespace :ingress do
|
||||||
|
desc "Pipe an inbound email from STDIN to the Postfix ingress at the given URL"
|
||||||
|
task :postfix do
|
||||||
|
require "active_support"
|
||||||
|
require "active_support/core_ext/object/blank"
|
||||||
|
require "http"
|
||||||
|
|
||||||
|
unless url = ENV["URL"].presence
|
||||||
|
abort "5.3.5 URL is required"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless password = ENV["INGRESS_PASSWORD"].presence
|
||||||
|
abort "5.3.5 INGRESS_PASSWORD is required"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
response = HTTP.basic_auth(user: "actionmailbox", pass: password)
|
||||||
|
.timeout(connect: 1, write: 10, read: 10)
|
||||||
|
.post(url, headers: { "Content-Type" => "message/rfc822", "User-Agent" => ENV.fetch("USER_AGENT", "Postfix") }, body: STDIN)
|
||||||
|
|
||||||
|
case
|
||||||
|
when response.status.success?
|
||||||
|
puts "2.0.0 HTTP #{response.status}"
|
||||||
|
when response.status.unauthorized?
|
||||||
|
abort "4.7.0 HTTP #{response.status}"
|
||||||
|
when response.status.unsupported_media_type?
|
||||||
|
abort "5.6.1 HTTP #{response.status}"
|
||||||
|
else
|
||||||
|
abort "4.0.0 HTTP #{response.status}"
|
||||||
|
end
|
||||||
|
rescue HTTP::ConnectionError => error
|
||||||
|
abort "4.4.2 Error connecting to the Postfix ingress: #{error.message}"
|
||||||
|
rescue HTTP::TimeoutError
|
||||||
|
abort "4.4.7 Timed out piping to the Postfix ingress"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
ActionMailbox::Ingresses::Amazon::InboundEmailsController.verifier =
|
||||||
|
Module.new { def self.authentic?(message); true; end }
|
||||||
|
|
||||||
|
class ActionMailbox::Ingresses::Amazon::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup { ActionMailbox.ingress = :amazon }
|
||||||
|
|
||||||
|
test "receiving an inbound email from Amazon" do
|
||||||
|
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
|
||||||
|
post rails_amazon_inbound_emails_url, params: { content: file_fixture("../files/welcome.eml").read }, as: :json
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
|
||||||
|
inbound_email = ActionMailbox::InboundEmail.last
|
||||||
|
assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
|
||||||
|
assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,89 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
ENV["MAILGUN_INGRESS_API_KEY"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
|
||||||
|
|
||||||
|
class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup { ActionMailbox.ingress = :mailgun }
|
||||||
|
|
||||||
|
test "receiving an inbound email from Mailgun" do
|
||||||
|
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
|
||||||
|
travel_to "2018-10-09 15:15:00 EDT"
|
||||||
|
post rails_mailgun_inbound_emails_url, params: {
|
||||||
|
timestamp: 1539112500,
|
||||||
|
token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
|
||||||
|
signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
|
||||||
|
"body-mime" => file_fixture("../files/welcome.eml").read
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
|
||||||
|
inbound_email = ActionMailbox::InboundEmail.last
|
||||||
|
assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
|
||||||
|
assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejecting a delayed inbound email from Mailgun" do
|
||||||
|
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
|
||||||
|
travel_to "2018-10-09 15:26:00 EDT"
|
||||||
|
post rails_mailgun_inbound_emails_url, params: {
|
||||||
|
timestamp: 1539112500,
|
||||||
|
token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
|
||||||
|
signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
|
||||||
|
"body-mime" => file_fixture("../files/welcome.eml").read
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejecting a forged inbound email from Mailgun" do
|
||||||
|
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
|
||||||
|
travel_to "2018-10-09 15:15:00 EDT"
|
||||||
|
post rails_mailgun_inbound_emails_url, params: {
|
||||||
|
timestamp: 1539112500,
|
||||||
|
token: "Zx8mJBiGmiiyyfWnho3zKyjCg2pxLARoCuBM7X9AKCioShGiMX",
|
||||||
|
signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
|
||||||
|
"body-mime" => file_fixture("../files/welcome.eml").read
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raising when the configured Mailgun API key is nil" do
|
||||||
|
switch_key_to nil do
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
travel_to "2018-10-09 15:15:00 EDT"
|
||||||
|
post rails_mailgun_inbound_emails_url, params: {
|
||||||
|
timestamp: 1539112500,
|
||||||
|
token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
|
||||||
|
signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
|
||||||
|
"body-mime" => file_fixture("../files/welcome.eml").read
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raising when the configured Mailgun API key is blank" do
|
||||||
|
switch_key_to "" do
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
travel_to "2018-10-09 15:15:00 EDT"
|
||||||
|
post rails_mailgun_inbound_emails_url, params: {
|
||||||
|
timestamp: 1539112500,
|
||||||
|
token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
|
||||||
|
signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
|
||||||
|
"body-mime" => file_fixture("../files/welcome.eml").read
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def switch_key_to(new_key)
|
||||||
|
previous_key, ENV["MAILGUN_INGRESS_API_KEY"] = ENV["MAILGUN_INGRESS_API_KEY"], new_key
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ENV["MAILGUN_INGRESS_API_KEY"] = previous_key
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,58 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
ENV["MANDRILL_INGRESS_API_KEY"] = "1l9Qf7lutEf7h73VXfBwhw"
|
||||||
|
|
||||||
|
class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
ActionMailbox.ingress = :mandrill
|
||||||
|
@events = JSON.generate([{ event: "inbound", msg: { raw_msg: file_fixture("../files/welcome.eml").read } }])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "receiving an inbound email from Mandrill" do
|
||||||
|
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
|
||||||
|
post rails_mandrill_inbound_emails_url,
|
||||||
|
headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
|
||||||
|
inbound_email = ActionMailbox::InboundEmail.last
|
||||||
|
assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
|
||||||
|
assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejecting a forged inbound email from Mandrill" do
|
||||||
|
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
|
||||||
|
post rails_mandrill_inbound_emails_url,
|
||||||
|
headers: { "X-Mandrill-Signature" => "forged" }, params: { mandrill_events: @events }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raising when Mandrill API key is nil" do
|
||||||
|
switch_key_to nil do
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
post rails_mandrill_inbound_emails_url,
|
||||||
|
headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raising when Mandrill API key is blank" do
|
||||||
|
switch_key_to "" do
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
post rails_mandrill_inbound_emails_url,
|
||||||
|
headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def switch_key_to(new_key)
|
||||||
|
previous_key, ENV["MANDRILL_INGRESS_API_KEY"] = ENV["MANDRILL_INGRESS_API_KEY"], new_key
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ENV["MANDRILL_INGRESS_API_KEY"] = previous_key
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,54 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup { ActionMailbox.ingress = :postfix }
|
||||||
|
|
||||||
|
test "receiving an inbound email from Postfix" do
|
||||||
|
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
|
||||||
|
post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
|
||||||
|
params: file_fixture("../files/welcome.eml").read
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
|
||||||
|
inbound_email = ActionMailbox::InboundEmail.last
|
||||||
|
assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
|
||||||
|
assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejecting an unauthorized inbound email from Postfix" do
|
||||||
|
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
|
||||||
|
post rails_postfix_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" },
|
||||||
|
params: file_fixture("../files/welcome.eml").read
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejecting an inbound email of an unsupported media type from Postfix" do
|
||||||
|
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
|
||||||
|
post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" },
|
||||||
|
params: file_fixture("../files/welcome.eml").read
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :unsupported_media_type
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raising when the configured password is nil" do
|
||||||
|
switch_password_to nil do
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
|
||||||
|
params: file_fixture("../files/welcome.eml").read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raising when the configured password is blank" do
|
||||||
|
switch_password_to "" do
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
|
||||||
|
params: file_fixture("../files/welcome.eml").read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,44 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup { ActionMailbox.ingress = :sendgrid }
|
||||||
|
|
||||||
|
test "receiving an inbound email from Sendgrid" do
|
||||||
|
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
|
||||||
|
post rails_sendgrid_inbound_emails_url,
|
||||||
|
headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
|
||||||
|
inbound_email = ActionMailbox::InboundEmail.last
|
||||||
|
assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
|
||||||
|
assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejecting an unauthorized inbound email from Sendgrid" do
|
||||||
|
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
|
||||||
|
post rails_sendgrid_inbound_emails_url, params: { email: file_fixture("../files/welcome.eml").read }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raising when the configured password is nil" do
|
||||||
|
switch_password_to nil do
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
post rails_sendgrid_inbound_emails_url,
|
||||||
|
headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raising when the configured password is blank" do
|
||||||
|
switch_password_to "" do
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
post rails_sendgrid_inbound_emails_url,
|
||||||
|
headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
1
test/fixtures/files/text.txt
vendored
1
test/fixtures/files/text.txt
vendored
|
@ -1 +0,0 @@
|
||||||
This Is Not An Email!
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Configure Rails Environment
|
|
||||||
ENV["RAILS_ENV"] = "test"
|
ENV["RAILS_ENV"] = "test"
|
||||||
|
ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
|
||||||
|
|
||||||
require_relative "../test/dummy/config/environment"
|
require_relative "../test/dummy/config/environment"
|
||||||
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
|
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
|
||||||
|
@ -7,14 +7,11 @@ require "rails/test_help"
|
||||||
|
|
||||||
require "byebug"
|
require "byebug"
|
||||||
|
|
||||||
# Filter out Minitest backtrace while allowing backtrace from other libraries
|
|
||||||
# to be shown.
|
|
||||||
Minitest.backtrace_filter = Minitest::BacktraceFilter.new
|
Minitest.backtrace_filter = Minitest::BacktraceFilter.new
|
||||||
|
|
||||||
require "rails/test_unit/reporter"
|
require "rails/test_unit/reporter"
|
||||||
Rails::TestUnitReporter.executable = 'bin/test'
|
Rails::TestUnitReporter.executable = 'bin/test'
|
||||||
|
|
||||||
# Load fixtures from the engine
|
|
||||||
if ActiveSupport::TestCase.respond_to?(:fixture_path=)
|
if ActiveSupport::TestCase.respond_to?(:fixture_path=)
|
||||||
ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
|
ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
|
||||||
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
|
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
|
||||||
|
@ -28,6 +25,20 @@ class ActiveSupport::TestCase
|
||||||
include ActionMailbox::TestHelper, ActiveJob::TestHelper
|
include ActionMailbox::TestHelper, ActiveJob::TestHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class ActionDispatch::IntegrationTest
|
||||||
|
private
|
||||||
|
def credentials
|
||||||
|
ActionController::HttpAuthentication::Basic.encode_credentials "actionmailbox", ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def switch_password_to(new_password)
|
||||||
|
previous_password, ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = ENV["RAILS_INBOUND_EMAIL_PASSWORD"], new_password
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = previous_password
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if ARGV.include?("-v")
|
if ARGV.include?("-v")
|
||||||
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
||||||
ActiveJob::Base.logger = Logger.new(STDOUT)
|
ActiveJob::Base.logger = Logger.new(STDOUT)
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
require_relative '../test_helper'
|
|
||||||
|
|
||||||
class ActionMailbox::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
|
|
||||||
test "receiving a valid RFC 822 message" do
|
|
||||||
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
|
|
||||||
post_inbound_email "welcome.eml"
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_response :created
|
|
||||||
|
|
||||||
inbound_email = ActionMailbox::InboundEmail.last
|
|
||||||
assert_equal file_fixture('../files/welcome.eml').read, inbound_email.raw_email.download
|
|
||||||
end
|
|
||||||
|
|
||||||
test "rejecting a message of an unsupported type" do
|
|
||||||
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
|
|
||||||
post rails_inbound_emails_url, params: { message: fixture_file_upload('files/text.txt', 'text/plain') }
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_response :unsupported_media_type
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def post_inbound_email(fixture_name)
|
|
||||||
post rails_inbound_emails_url, params: { message: fixture_file_upload("files/#{fixture_name}", 'message/rfc822') }
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -7,9 +7,7 @@ class ActionMailbox::InboundEmail::MessageIdTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "message id is generated if its missing" do
|
test "message id is generated if its missing" do
|
||||||
source_without_message_id = "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!"
|
inbound_email = create_inbound_email "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!"
|
||||||
inbound_email = create_inbound_email Tempfile.new.tap { |raw_email| raw_email.write source_without_message_id }
|
|
||||||
|
|
||||||
assert_not_nil inbound_email.message_id
|
assert_not_nil inbound_email.message_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue