actionmailbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +159 -0
- data/LICENSE +21 -0
- data/README.md +278 -0
- data/Rakefile +27 -0
- data/actionmailbox.gemspec +27 -0
- data/app/controllers/action_mailbox/base_controller.rb +43 -0
- data/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +50 -0
- data/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +99 -0
- data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +78 -0
- data/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +55 -0
- data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +50 -0
- data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +27 -0
- data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +15 -0
- data/app/controllers/rails/conductor/base_controller.rb +10 -0
- data/app/jobs/action_mailbox/incineration_job.rb +18 -0
- data/app/jobs/action_mailbox/routing_job.rb +9 -0
- data/app/models/action_mailbox/inbound_email.rb +43 -0
- data/app/models/action_mailbox/inbound_email/incineratable.rb +18 -0
- data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +22 -0
- data/app/models/action_mailbox/inbound_email/message_id.rb +36 -0
- data/app/models/action_mailbox/inbound_email/routable.rb +22 -0
- data/app/views/layouts/rails/conductor.html.erb +7 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +15 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +42 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +15 -0
- data/bin/test +5 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20180917164000_create_action_mailbox_tables.rb +11 -0
- data/lib/action_mailbox.rb +15 -0
- data/lib/action_mailbox/base.rb +111 -0
- data/lib/action_mailbox/callbacks.rb +32 -0
- data/lib/action_mailbox/engine.rb +34 -0
- data/lib/action_mailbox/mail_ext.rb +4 -0
- data/lib/action_mailbox/mail_ext/address_equality.rb +5 -0
- data/lib/action_mailbox/mail_ext/address_wrapping.rb +5 -0
- data/lib/action_mailbox/mail_ext/addresses.rb +25 -0
- data/lib/action_mailbox/mail_ext/from_source.rb +5 -0
- data/lib/action_mailbox/mail_ext/recipients.rb +5 -0
- data/lib/action_mailbox/postfix_relayer.rb +67 -0
- data/lib/action_mailbox/router.rb +38 -0
- data/lib/action_mailbox/router/route.rb +38 -0
- data/lib/action_mailbox/routing.rb +20 -0
- data/lib/action_mailbox/test_case.rb +8 -0
- data/lib/action_mailbox/test_helper.rb +42 -0
- data/lib/action_mailbox/version.rb +3 -0
- data/lib/tasks/ingress.rake +24 -0
- data/lib/tasks/install.rake +20 -0
- data/lib/templates/installer.rb +4 -0
- data/lib/templates/mailboxes/application_mailbox.rb +3 -0
- data/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb +20 -0
- data/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb +89 -0
- data/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb +58 -0
- data/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +54 -0
- data/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb +44 -0
- data/test/dummy/.babelrc +18 -0
- data/test/dummy/.gitignore +3 -0
- data/test/dummy/.postcssrc.yml +3 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/config/manifest.js +3 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/assets/stylesheets/scaffold.css +80 -0
- data/test/dummy/app/channels/application_cable/channel.rb +4 -0
- data/test/dummy/app/channels/application_cable/connection.rb +4 -0
- data/test/dummy/app/controllers/application_controller.rb +2 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/javascript/packs/application.js +0 -0
- data/test/dummy/app/jobs/application_job.rb +2 -0
- data/test/dummy/app/mailboxes/application_mailbox.rb +2 -0
- data/test/dummy/app/mailboxes/messages_mailbox.rb +4 -0
- data/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +36 -0
- data/test/dummy/bin/update +31 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/config/application.rb +19 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +63 -0
- data/test/dummy/config/environments/production.rb +96 -0
- data/test/dummy/config/environments/test.rb +46 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/test/dummy/config/initializers/assets.rb +14 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/content_security_policy.rb +22 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +34 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/config/storage.yml +35 -0
- data/test/dummy/config/webpack/development.js +3 -0
- data/test/dummy/config/webpack/environment.js +3 -0
- data/test/dummy/config/webpack/production.js +3 -0
- data/test/dummy/config/webpack/test.js +3 -0
- data/test/dummy/config/webpacker.yml +65 -0
- data/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb +11 -0
- data/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb +26 -0
- data/test/dummy/db/schema.rb +43 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/package.json +11 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/storage/.keep +0 -0
- data/test/dummy/yarn.lock +6071 -0
- data/test/fixtures/files/welcome.eml +631 -0
- data/test/jobs/incineration_job_test.rb +17 -0
- data/test/test_helper.rb +54 -0
- data/test/unit/inbound_email/incineration_test.rb +45 -0
- data/test/unit/inbound_email/message_id_test.rb +13 -0
- data/test/unit/inbound_email_test.rb +13 -0
- data/test/unit/mail_ext/address_equality_test.rb +9 -0
- data/test/unit/mail_ext/address_wrapping_test.rb +11 -0
- data/test/unit/mail_ext/recipients_test.rb +33 -0
- data/test/unit/mailbox/bouncing_test.rb +29 -0
- data/test/unit/mailbox/callbacks_test.rb +75 -0
- data/test/unit/mailbox/routing_test.rb +30 -0
- data/test/unit/mailbox/state_test.rb +49 -0
- data/test/unit/postfix_relayer_test.rb +90 -0
- data/test/unit/router_test.rb +137 -0
- metadata +355 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require 'rdoc/task'
|
|
8
|
+
|
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
11
|
+
rdoc.title = 'Action Mailbox'
|
|
12
|
+
rdoc.options << '--line-numbers'
|
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require 'bundler/gem_tasks'
|
|
18
|
+
|
|
19
|
+
require 'rake/testtask'
|
|
20
|
+
|
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
|
22
|
+
t.libs << 'test'
|
|
23
|
+
t.pattern = 'test/**/*_test.rb'
|
|
24
|
+
t.verbose = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
task default: :test
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
$:.push File.expand_path("lib", __dir__)
|
|
2
|
+
|
|
3
|
+
# Maintain your gem's version:
|
|
4
|
+
require "action_mailbox/version"
|
|
5
|
+
|
|
6
|
+
# Describe your gem and declare its dependencies:
|
|
7
|
+
Gem::Specification.new do |s|
|
|
8
|
+
s.name = "actionmailbox"
|
|
9
|
+
s.version = ActionMailbox::VERSION
|
|
10
|
+
s.authors = ["David Heinemeier Hansson", "George Claghorn"]
|
|
11
|
+
s.email = ["david@loudthinking.com", "george@basecamp.com"]
|
|
12
|
+
s.summary = "Receive and process incoming emails in Rails"
|
|
13
|
+
s.homepage = "https://github.com/rails/actionmailbox"
|
|
14
|
+
s.license = "MIT"
|
|
15
|
+
|
|
16
|
+
s.required_ruby_version = ">= 2.5.0"
|
|
17
|
+
|
|
18
|
+
s.add_dependency "rails", ">= 5.2.0"
|
|
19
|
+
|
|
20
|
+
s.add_development_dependency "bundler", "~> 1.15"
|
|
21
|
+
s.add_development_dependency "sqlite3"
|
|
22
|
+
s.add_development_dependency "byebug"
|
|
23
|
+
s.add_development_dependency "webmock"
|
|
24
|
+
|
|
25
|
+
s.files = `git ls-files`.split("\n")
|
|
26
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
|
27
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# The base class for all Active Mailbox ingress controllers.
|
|
2
|
+
class ActionMailbox::BaseController < ActionController::Base
|
|
3
|
+
skip_forgery_protection
|
|
4
|
+
|
|
5
|
+
def self.prepare
|
|
6
|
+
# Override in concrete controllers to run code on load.
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
before_action :ensure_configured
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
def ensure_configured
|
|
13
|
+
unless ActionMailbox.ingress == ingress_name
|
|
14
|
+
head :not_found
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ingress_name
|
|
19
|
+
self.class.name.remove(/\AActionMailbox::Ingresses::/, /::InboundEmailsController\z/).underscore.to_sym
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def authenticate_by_password
|
|
24
|
+
if password.present?
|
|
25
|
+
http_basic_authenticate_or_request_with username: "actionmailbox", password: password, realm: "Action Mailbox"
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "Missing required ingress credentials"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def password
|
|
32
|
+
Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# TODO: Extract to ActionController::HttpAuthentication
|
|
37
|
+
def http_basic_authenticate_or_request_with(username:, password:, realm: nil)
|
|
38
|
+
authenticate_or_request_with_http_basic(realm || "Application") do |given_username, given_password|
|
|
39
|
+
ActiveSupport::SecurityUtils.secure_compare(given_username, username) &
|
|
40
|
+
ActiveSupport::SecurityUtils.secure_compare(given_password, password)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Ingests inbound emails from Amazon's Simple Email Service (SES).
|
|
2
|
+
#
|
|
3
|
+
# Requires the full RFC 822 message in the +content+ parameter. Authenticates requests by validating their signatures.
|
|
4
|
+
#
|
|
5
|
+
# Returns:
|
|
6
|
+
#
|
|
7
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
8
|
+
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
|
9
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SES
|
|
10
|
+
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +content+ parameter
|
|
11
|
+
# - <tt>500 Server Error</tt> if one of the Active Record database, the Active Storage service, or
|
|
12
|
+
# the Active Job backend is misconfigured or unavailable
|
|
13
|
+
#
|
|
14
|
+
# == Usage
|
|
15
|
+
#
|
|
16
|
+
# 1. Install the {aws-sdk-sns}[https://rubygems.org/gems/aws-sdk-sns] gem:
|
|
17
|
+
#
|
|
18
|
+
# # Gemfile
|
|
19
|
+
# gem "aws-sdk-sns", ">= 1.9.0", require: false
|
|
20
|
+
#
|
|
21
|
+
# 2. Tell Action Mailbox to accept emails from SES:
|
|
22
|
+
#
|
|
23
|
+
# # config/environments/production.rb
|
|
24
|
+
# config.action_mailbox.ingress = :amazon
|
|
25
|
+
#
|
|
26
|
+
# 3. {Configure SES}[https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html]
|
|
27
|
+
# to deliver emails to your application via POST requests to +/rails/action_mailbox/amazon/inbound_emails+.
|
|
28
|
+
# If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
|
29
|
+
# <tt>https://example.com/rails/action_mailbox/amazon/inbound_emails</tt>.
|
|
30
|
+
class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController
|
|
31
|
+
before_action :authenticate
|
|
32
|
+
|
|
33
|
+
cattr_accessor :verifier
|
|
34
|
+
|
|
35
|
+
def self.prepare
|
|
36
|
+
self.verifier ||= begin
|
|
37
|
+
require "aws-sdk-sns/message_verifier"
|
|
38
|
+
Aws::SNS::MessageVerifier.new
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create
|
|
43
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
def authenticate
|
|
48
|
+
head :unauthorized unless verifier.authentic?(request.body)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Ingests inbound emails from Mailgun. Requires the following parameters:
|
|
2
|
+
#
|
|
3
|
+
# - +body-mime+: The full RFC 822 message
|
|
4
|
+
# - +timestamp+: The current time according to Mailgun as the number of seconds passed since the UNIX epoch
|
|
5
|
+
# - +token+: A randomly-generated, 50-character string
|
|
6
|
+
# - +signature+: A hexadecimal HMAC-SHA256 of the timestamp concatenated with the token, generated using the Mailgun API key
|
|
7
|
+
#
|
|
8
|
+
# Authenticates requests by validating their signatures.
|
|
9
|
+
#
|
|
10
|
+
# Returns:
|
|
11
|
+
#
|
|
12
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
13
|
+
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated, or if its timestamp is more than 2 minutes old
|
|
14
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mailgun
|
|
15
|
+
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
|
16
|
+
# - <tt>500 Server Error</tt> if the Mailgun API key is missing, or one of the Active Record database,
|
|
17
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
18
|
+
#
|
|
19
|
+
# == Usage
|
|
20
|
+
#
|
|
21
|
+
# 1. Give Action Mailbox your {Mailgun API key}[https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials-]
|
|
22
|
+
# so it can authenticate requests to the Mailgun ingress.
|
|
23
|
+
#
|
|
24
|
+
# Use <tt>rails credentials:edit</tt> to add your API key to your application's encrypted credentials under
|
|
25
|
+
# +action_mailbox.mailgun_api_key+, where Action Mailbox will automatically find it:
|
|
26
|
+
#
|
|
27
|
+
# action_mailbox:
|
|
28
|
+
# mailgun_api_key: ...
|
|
29
|
+
#
|
|
30
|
+
# Alternatively, provide your API key in the +MAILGUN_INGRESS_API_KEY+ environment variable.
|
|
31
|
+
#
|
|
32
|
+
# 2. Tell Action Mailbox to accept emails from Mailgun:
|
|
33
|
+
#
|
|
34
|
+
# # config/environments/production.rb
|
|
35
|
+
# config.action_mailbox.ingress = :mailgun
|
|
36
|
+
#
|
|
37
|
+
# 3. {Configure Mailgun}[https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages]
|
|
38
|
+
# to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`.
|
|
39
|
+
#
|
|
40
|
+
# If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
|
41
|
+
# <tt>https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime</tt>.
|
|
42
|
+
class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController
|
|
43
|
+
before_action :authenticate
|
|
44
|
+
|
|
45
|
+
def create
|
|
46
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
def authenticate
|
|
51
|
+
head :unauthorized unless authenticated?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def authenticated?
|
|
55
|
+
if key.present?
|
|
56
|
+
Authenticator.new(
|
|
57
|
+
key: key,
|
|
58
|
+
timestamp: params.require(:timestamp),
|
|
59
|
+
token: params.require(:token),
|
|
60
|
+
signature: params.require(:signature)
|
|
61
|
+
).authenticated?
|
|
62
|
+
else
|
|
63
|
+
raise ArgumentError, <<~MESSAGE.squish
|
|
64
|
+
Missing required Mailgun API key. Set action_mailbox.mailgun_api_key in your application's
|
|
65
|
+
encrypted credentials or provide the MAILGUN_INGRESS_API_KEY environment variable.
|
|
66
|
+
MESSAGE
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def key
|
|
71
|
+
Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) || ENV["MAILGUN_INGRESS_API_KEY"]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class Authenticator
|
|
75
|
+
attr_reader :key, :timestamp, :token, :signature
|
|
76
|
+
|
|
77
|
+
def initialize(key:, timestamp:, token:, signature:)
|
|
78
|
+
@key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def authenticated?
|
|
82
|
+
signed? && recent?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
def signed?
|
|
87
|
+
ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Allow for 2 minutes of drift between Mailgun time and local server time.
|
|
91
|
+
def recent?
|
|
92
|
+
Time.at(timestamp) >= 2.minutes.ago
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def expected_signature
|
|
96
|
+
OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Ingests inbound emails from Mandrill.
|
|
2
|
+
#
|
|
3
|
+
# Requires a +mandrill_events+ parameter containing a JSON array of Mandrill inbound email event objects.
|
|
4
|
+
# Each event is expected to have a +msg+ object containing a full RFC 822 message in its +raw_msg+ property.
|
|
5
|
+
#
|
|
6
|
+
# Returns:
|
|
7
|
+
#
|
|
8
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
9
|
+
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
|
10
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mandrill
|
|
11
|
+
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
|
12
|
+
# - <tt>500 Server Error</tt> if the Mandrill API key is missing, or one of the Active Record database,
|
|
13
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
14
|
+
class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController
|
|
15
|
+
before_action :authenticate
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email }
|
|
19
|
+
head :ok
|
|
20
|
+
rescue JSON::ParserError => error
|
|
21
|
+
logger.error error.message
|
|
22
|
+
head :unprocessable_entity
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
def raw_emails
|
|
27
|
+
events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def events
|
|
31
|
+
JSON.parse params.require(:mandrill_events)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def authenticate
|
|
36
|
+
head :unauthorized unless authenticated?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def authenticated?
|
|
40
|
+
if key.present?
|
|
41
|
+
Authenticator.new(request, key).authenticated?
|
|
42
|
+
else
|
|
43
|
+
raise ArgumentError, <<~MESSAGE.squish
|
|
44
|
+
Missing required Mandrill API key. Set action_mailbox.mandrill_api_key in your application's
|
|
45
|
+
encrypted credentials or provide the MANDRILL_INGRESS_API_KEY environment variable.
|
|
46
|
+
MESSAGE
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def key
|
|
51
|
+
Rails.application.credentials.dig(:action_mailbox, :mandrill_api_key) || ENV["MANDRILL_INGRESS_API_KEY"]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class Authenticator
|
|
55
|
+
attr_reader :request, :key
|
|
56
|
+
|
|
57
|
+
def initialize(request, key)
|
|
58
|
+
@request, @key = request, key
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def authenticated?
|
|
62
|
+
ActiveSupport::SecurityUtils.secure_compare given_signature, expected_signature
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
def given_signature
|
|
67
|
+
request.headers["X-Mandrill-Signature"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def expected_signature
|
|
71
|
+
Base64.strict_encode64 OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def message
|
|
75
|
+
request.url + request.POST.sort.flatten.join
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Ingests inbound emails relayed from Postfix.
|
|
2
|
+
#
|
|
3
|
+
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
|
4
|
+
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
|
5
|
+
#
|
|
6
|
+
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
|
7
|
+
# the Postfix ingress can learn its password. You should only use the Postfix ingress over HTTPS.
|
|
8
|
+
#
|
|
9
|
+
# Returns:
|
|
10
|
+
#
|
|
11
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
12
|
+
# - <tt>401 Unauthorized</tt> if the request could not be authenticated
|
|
13
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postfix
|
|
14
|
+
# - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message
|
|
15
|
+
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
|
16
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
17
|
+
#
|
|
18
|
+
# == Usage
|
|
19
|
+
#
|
|
20
|
+
# 1. Tell Action Mailbox to accept emails from Postfix:
|
|
21
|
+
#
|
|
22
|
+
# # config/environments/production.rb
|
|
23
|
+
# config.action_mailbox.ingress = :postfix
|
|
24
|
+
#
|
|
25
|
+
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress.
|
|
26
|
+
#
|
|
27
|
+
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
|
28
|
+
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
|
29
|
+
#
|
|
30
|
+
# action_mailbox:
|
|
31
|
+
# ingress_password: ...
|
|
32
|
+
#
|
|
33
|
+
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
|
34
|
+
#
|
|
35
|
+
# 3. {Configure Postfix}{https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script}
|
|
36
|
+
# to pipe inbound emails to <tt>bin/rails action_mailbox:ingress:postfix</tt>, providing the +URL+ of the Postfix
|
|
37
|
+
# ingress and the +INGRESS_PASSWORD+ you previously generated.
|
|
38
|
+
#
|
|
39
|
+
# If your application lived at <tt>https://example.com</tt>, the full command would look like this:
|
|
40
|
+
#
|
|
41
|
+
# URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix
|
|
42
|
+
class ActionMailbox::Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController
|
|
43
|
+
before_action :authenticate_by_password, :require_valid_rfc822_message
|
|
44
|
+
|
|
45
|
+
def create
|
|
46
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
def require_valid_rfc822_message
|
|
51
|
+
unless request.content_type == "message/rfc822"
|
|
52
|
+
head :unsupported_media_type
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Ingests inbound emails from SendGrid. Requires an +email+ parameter containing a full RFC 822 message.
|
|
2
|
+
#
|
|
3
|
+
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
|
4
|
+
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
|
5
|
+
#
|
|
6
|
+
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
|
7
|
+
# the SendGrid ingress can learn its password. You should only use the SendGrid ingress over HTTPS.
|
|
8
|
+
#
|
|
9
|
+
# Returns:
|
|
10
|
+
#
|
|
11
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
12
|
+
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
|
13
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SendGrid
|
|
14
|
+
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +email+ parameter
|
|
15
|
+
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
|
16
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
17
|
+
#
|
|
18
|
+
# == Usage
|
|
19
|
+
#
|
|
20
|
+
# 1. Tell Action Mailbox to accept emails from SendGrid:
|
|
21
|
+
#
|
|
22
|
+
# # config/environments/production.rb
|
|
23
|
+
# config.action_mailbox.ingress = :sendgrid
|
|
24
|
+
#
|
|
25
|
+
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress.
|
|
26
|
+
#
|
|
27
|
+
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
|
28
|
+
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
|
29
|
+
#
|
|
30
|
+
# action_mailbox:
|
|
31
|
+
# ingress_password: ...
|
|
32
|
+
#
|
|
33
|
+
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
|
34
|
+
#
|
|
35
|
+
# 3. {Configure SendGrid Inbound Parse}{https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/}
|
|
36
|
+
# to forward inbound emails to +/rails/action_mailbox/sendgrid/inbound_emails+ with the username +actionmailbox+ and
|
|
37
|
+
# the password you previously generated. If your application lived at <tt>https://example.com</tt>, you would
|
|
38
|
+
# configure SendGrid with the following fully-qualified URL:
|
|
39
|
+
#
|
|
40
|
+
# https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails
|
|
41
|
+
#
|
|
42
|
+
# *NOTE:* When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled *"Post the raw,
|
|
43
|
+
# full MIME message."* Action Mailbox needs the raw MIME message to work.
|
|
44
|
+
class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController
|
|
45
|
+
before_action :authenticate_by_password
|
|
46
|
+
|
|
47
|
+
def create
|
|
48
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class Rails::Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController
|
|
2
|
+
def index
|
|
3
|
+
@inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc)
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@inbound_email = ActionMailbox::InboundEmail.find(params[:id])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create
|
|
14
|
+
inbound_email = create_inbound_email(new_mail)
|
|
15
|
+
redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
def new_mail
|
|
20
|
+
Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_inbound_email(mail)
|
|
24
|
+
ActionMailbox::InboundEmail.create! raw_email: \
|
|
25
|
+
{ io: StringIO.new(mail.to_s), filename: 'inbound.eml', content_type: 'message/rfc822', identify: false }
|
|
26
|
+
end
|
|
27
|
+
end
|