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
|
||||
|
||||
gem "rails", github: "rails/rails"
|
||||
|
||||
gem "aws-sdk-sns"
|
||||
|
|
31
Gemfile.lock
31
Gemfile.lock
|
@ -66,20 +66,46 @@ PATH
|
|||
remote: .
|
||||
specs:
|
||||
actionmailbox (0.1.0)
|
||||
http (>= 4.0.0)
|
||||
rails (>= 5.2.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
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)
|
||||
byebug (10.0.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
crass (1.0.4)
|
||||
domain_name (0.5.20180417)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
erubi (1.7.1)
|
||||
globalid (0.4.1)
|
||||
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)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.4.0)
|
||||
loofah (2.2.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
|
@ -95,6 +121,7 @@ GEM
|
|||
nio4r (2.3.1)
|
||||
nokogiri (1.8.5)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
public_suffix (3.0.3)
|
||||
rack (2.0.5)
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
|
@ -116,6 +143,9 @@ GEM
|
|||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.5)
|
||||
websocket-driver (0.7.0)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.3)
|
||||
|
@ -125,6 +155,7 @@ PLATFORMS
|
|||
|
||||
DEPENDENCIES
|
||||
actionmailbox!
|
||||
aws-sdk-sns
|
||||
bundler (~> 1.15)
|
||||
byebug
|
||||
rails!
|
||||
|
|
|
@ -16,6 +16,7 @@ Gem::Specification.new do |s|
|
|||
s.required_ruby_version = ">= 2.5.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 "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
|
||||
|
||||
module ClassMethods
|
||||
def create_and_extract_message_id!(raw_email, **options)
|
||||
create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options
|
||||
def create_and_extract_message_id!(source, **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
|
||||
|
||||
private
|
||||
def extract_message_id(raw_email)
|
||||
mail_from_source(raw_email.read).message_id
|
||||
rescue
|
||||
def extract_message_id(source)
|
||||
mail_from_source(source).message_id
|
||||
rescue => e
|
||||
# FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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?
|
||||
scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do
|
||||
resources :inbound_emails, as: :rails_conductor_inbound_emails
|
||||
post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
|
||||
resources :inbound_emails, as: :rails_conductor_inbound_emails do
|
||||
post "reroute" => "reroutes#create"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ module ActionMailbox
|
|||
autoload :Base
|
||||
autoload :Router
|
||||
|
||||
mattr_accessor :ingress
|
||||
mattr_accessor :logger
|
||||
mattr_accessor :incinerate_after, default: 30.days
|
||||
end
|
||||
|
|
|
@ -14,5 +14,17 @@ module ActionMailbox
|
|||
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
|
||||
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
|
||||
|
|
|
@ -5,18 +5,15 @@ module ActionMailbox
|
|||
# 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+.
|
||||
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
|
||||
|
||||
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(raw_email, status: status)
|
||||
create_inbound_email Mail.new(mail_options).to_s, status: status
|
||||
end
|
||||
|
||||
def create_inbound_email(io, filename: 'mail.eml', status: :processing)
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! \
|
||||
ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'),
|
||||
status: status
|
||||
def create_inbound_email(source, status: :processing)
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
|
||||
end
|
||||
|
||||
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_INBOUND_EMAIL_PASSWORD"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
|
||||
|
||||
require_relative "../test/dummy/config/environment"
|
||||
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
|
||||
|
@ -7,14 +7,11 @@ require "rails/test_help"
|
|||
|
||||
require "byebug"
|
||||
|
||||
# Filter out Minitest backtrace while allowing backtrace from other libraries
|
||||
# to be shown.
|
||||
Minitest.backtrace_filter = Minitest::BacktraceFilter.new
|
||||
|
||||
require "rails/test_unit/reporter"
|
||||
Rails::TestUnitReporter.executable = 'bin/test'
|
||||
|
||||
# Load fixtures from the engine
|
||||
if ActiveSupport::TestCase.respond_to?(:fixture_path=)
|
||||
ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
|
||||
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
|
||||
|
@ -28,6 +25,20 @@ class ActiveSupport::TestCase
|
|||
include ActionMailbox::TestHelper, ActiveJob::TestHelper
|
||||
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")
|
||||
ActiveRecord::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
|
||||
|
||||
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 Tempfile.new.tap { |raw_email| raw_email.write source_without_message_id }
|
||||
|
||||
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!"
|
||||
assert_not_nil inbound_email.message_id
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue