1
0
Fork 0
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:
George Claghorn 2018-11-05 14:21:27 -05:00 committed by GitHub
commit 152a442b19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 599 additions and 67 deletions

View file

@ -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"

View file

@ -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!

View file

@ -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"

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1 +0,0 @@
This Is Not An Email!

View file

@ -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)

View file

@ -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

View file

@ -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