omg-actionmailbox 8.0.0.alpha3
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/CHANGELOG.md +2 -0
- data/MIT-LICENSE +21 -0
- data/README.md +13 -0
- data/app/controllers/action_mailbox/base_controller.rb +34 -0
- data/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +110 -0
- data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +86 -0
- data/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb +70 -0
- data/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb +69 -0
- data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +69 -0
- data/app/controllers/rails/conductor/action_mailbox/inbound_emails/sources_controller.rb +15 -0
- data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +41 -0
- data/app/controllers/rails/conductor/action_mailbox/incinerates_controller.rb +14 -0
- data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +21 -0
- data/app/controllers/rails/conductor/base_controller.rb +14 -0
- data/app/jobs/action_mailbox/incineration_job.rb +25 -0
- data/app/jobs/action_mailbox/routing_job.rb +13 -0
- data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +26 -0
- data/app/models/action_mailbox/inbound_email/incineratable.rb +20 -0
- data/app/models/action_mailbox/inbound_email/message_id.rb +42 -0
- data/app/models/action_mailbox/inbound_email/routable.rb +24 -0
- data/app/models/action_mailbox/inbound_email.rb +55 -0
- data/app/models/action_mailbox/record.rb +9 -0
- data/app/views/layouts/rails/conductor.html.erb +8 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +16 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +52 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +15 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb +12 -0
- data/config/routes.rb +26 -0
- data/db/migrate/20180917164000_create_action_mailbox_tables.rb +19 -0
- data/lib/action_mailbox/base.rb +135 -0
- data/lib/action_mailbox/callbacks.rb +36 -0
- data/lib/action_mailbox/deprecator.rb +7 -0
- data/lib/action_mailbox/engine.rb +40 -0
- data/lib/action_mailbox/gem_version.rb +17 -0
- data/lib/action_mailbox/mail_ext/address_equality.rb +9 -0
- data/lib/action_mailbox/mail_ext/address_wrapping.rb +9 -0
- data/lib/action_mailbox/mail_ext/addresses.rb +44 -0
- data/lib/action_mailbox/mail_ext/from_source.rb +7 -0
- data/lib/action_mailbox/mail_ext/recipients.rb +10 -0
- data/lib/action_mailbox/mail_ext.rb +6 -0
- data/lib/action_mailbox/relayer.rb +75 -0
- data/lib/action_mailbox/router/route.rb +42 -0
- data/lib/action_mailbox/router.rb +44 -0
- data/lib/action_mailbox/routing.rb +26 -0
- data/lib/action_mailbox/test_case.rb +12 -0
- data/lib/action_mailbox/test_helper.rb +95 -0
- data/lib/action_mailbox/version.rb +10 -0
- data/lib/action_mailbox.rb +26 -0
- data/lib/generators/action_mailbox/install/install_generator.rb +28 -0
- data/lib/rails/generators/mailbox/USAGE +10 -0
- data/lib/rails/generators/mailbox/mailbox_generator.rb +32 -0
- data/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +3 -0
- data/lib/rails/generators/mailbox/templates/mailbox.rb.tt +4 -0
- data/lib/rails/generators/test_unit/mailbox_generator.rb +20 -0
- data/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt +11 -0
- data/lib/tasks/ingress.rake +72 -0
- data/lib/tasks/install.rake +6 -0
- metadata +192 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3307d66e86a3f234f8a78670e084f349682c7efe4a5369ae1ac7aa5d01e909f4
|
|
4
|
+
data.tar.gz: 491b9a8c79629ce16c53f6eba972a3908058c99cd171230117d695cd5966643b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7c046baec3be3518d67009f5af88fdcce3225737a0ac39b5b22f44011b47c9626c96265c304fa78040607d23f6ec22292aa5b1e8dbe412e0556841274b2b2a57
|
|
7
|
+
data.tar.gz: 67990495e87c0d25d569df71ebc0b8ae6322cbf7aa37ed2b51f2edfc1c1025ad479ccc7578a8304c6c8573412d0b5440f1ff319f1251a0a75a65c1b09411149a
|
data/CHANGELOG.md
ADDED
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 37signals LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Action Mailbox
|
|
2
|
+
|
|
3
|
+
Action Mailbox routes incoming emails to controller-like mailboxes for processing in \Rails. It ships with ingresses for Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses.
|
|
4
|
+
|
|
5
|
+
The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration.
|
|
6
|
+
|
|
7
|
+
These inbound emails are routed asynchronously using Active Job to one or several dedicated mailboxes, which are capable of interacting directly with the rest of your domain model.
|
|
8
|
+
|
|
9
|
+
You can read more about Action Mailbox in the [Action Mailbox Basics](https://guides.rubyonrails.org/action_mailbox_basics.html) guide.
|
|
10
|
+
|
|
11
|
+
## License
|
|
12
|
+
|
|
13
|
+
Action Mailbox is released under the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# The base class for all Action Mailbox ingress controllers.
|
|
5
|
+
class BaseController < ActionController::Base
|
|
6
|
+
skip_forgery_protection
|
|
7
|
+
|
|
8
|
+
before_action :ensure_configured
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
def ensure_configured
|
|
12
|
+
unless ActionMailbox.ingress == ingress_name
|
|
13
|
+
head :not_found
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ingress_name
|
|
18
|
+
self.class.name.remove(/\AActionMailbox::Ingresses::/, /::InboundEmailsController\z/).underscore.to_sym
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def authenticate_by_password
|
|
23
|
+
if password.present?
|
|
24
|
+
http_basic_authenticate_or_request_with name: "actionmailbox", password: password, realm: "Action Mailbox"
|
|
25
|
+
else
|
|
26
|
+
raise ArgumentError, "Missing required ingress credentials"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def password
|
|
31
|
+
Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# Ingests inbound emails from Mailgun. Requires the following parameters:
|
|
5
|
+
#
|
|
6
|
+
# - +body-mime+: The full RFC 822 message
|
|
7
|
+
# - +timestamp+: The current time according to Mailgun as the number of seconds passed since the UNIX epoch
|
|
8
|
+
# - +token+: A randomly-generated, 50-character string
|
|
9
|
+
# - +signature+: A hexadecimal HMAC-SHA256 of the timestamp concatenated with the token, generated using the Mailgun Signing key
|
|
10
|
+
#
|
|
11
|
+
# Authenticates requests by validating their signatures.
|
|
12
|
+
#
|
|
13
|
+
# Returns:
|
|
14
|
+
#
|
|
15
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
16
|
+
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated, or if its timestamp is more than 2 minutes old
|
|
17
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mailgun
|
|
18
|
+
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
|
19
|
+
# - <tt>500 Server Error</tt> if the Mailgun Signing key is missing, or one of the Active Record database,
|
|
20
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
21
|
+
#
|
|
22
|
+
# == Usage
|
|
23
|
+
#
|
|
24
|
+
# 1. Give Action Mailbox your Mailgun Signing key (which you can find under Settings -> Security & Users -> API security in Mailgun)
|
|
25
|
+
# so it can authenticate requests to the Mailgun ingress.
|
|
26
|
+
#
|
|
27
|
+
# Use <tt>bin/rails credentials:edit</tt> to add your Signing key to your application's encrypted credentials under
|
|
28
|
+
# +action_mailbox.mailgun_signing_key+, where Action Mailbox will automatically find it:
|
|
29
|
+
#
|
|
30
|
+
# action_mailbox:
|
|
31
|
+
# mailgun_signing_key: ...
|
|
32
|
+
#
|
|
33
|
+
# Alternatively, provide your Signing key in the +MAILGUN_INGRESS_SIGNING_KEY+ environment variable.
|
|
34
|
+
#
|
|
35
|
+
# 2. Tell Action Mailbox to accept emails from Mailgun:
|
|
36
|
+
#
|
|
37
|
+
# # config/environments/production.rb
|
|
38
|
+
# config.action_mailbox.ingress = :mailgun
|
|
39
|
+
#
|
|
40
|
+
# 3. {Configure Mailgun}[https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages]
|
|
41
|
+
# to forward inbound emails to +/rails/action_mailbox/mailgun/inbound_emails/mime+.
|
|
42
|
+
#
|
|
43
|
+
# If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
|
44
|
+
# <tt>https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime</tt>.
|
|
45
|
+
class Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController
|
|
46
|
+
before_action :authenticate
|
|
47
|
+
param_encoding :create, "body-mime", Encoding::ASCII_8BIT
|
|
48
|
+
|
|
49
|
+
def create
|
|
50
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! mail
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
def mail
|
|
55
|
+
params.require("body-mime").tap do |raw_email|
|
|
56
|
+
raw_email.prepend("X-Original-To: ", params.require(:recipient), "\n") if params.key?(:recipient)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def authenticate
|
|
61
|
+
head :unauthorized unless authenticated?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def authenticated?
|
|
65
|
+
if key.present?
|
|
66
|
+
Authenticator.new(
|
|
67
|
+
key: key,
|
|
68
|
+
timestamp: params.require(:timestamp),
|
|
69
|
+
token: params.require(:token),
|
|
70
|
+
signature: params.require(:signature)
|
|
71
|
+
).authenticated?
|
|
72
|
+
else
|
|
73
|
+
raise ArgumentError, <<~MESSAGE.squish
|
|
74
|
+
Missing required Mailgun Signing key. Set action_mailbox.mailgun_signing_key in your application's
|
|
75
|
+
encrypted credentials or provide the MAILGUN_INGRESS_SIGNING_KEY environment variable.
|
|
76
|
+
MESSAGE
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def key
|
|
81
|
+
Rails.application.credentials.dig(:action_mailbox, :mailgun_signing_key) || ENV["MAILGUN_INGRESS_SIGNING_KEY"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class Authenticator
|
|
85
|
+
attr_reader :key, :timestamp, :token, :signature
|
|
86
|
+
|
|
87
|
+
def initialize(key:, timestamp:, token:, signature:)
|
|
88
|
+
@key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def authenticated?
|
|
92
|
+
signed? && recent?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
def signed?
|
|
97
|
+
ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Allow for 2 minutes of drift between Mailgun time and local server time.
|
|
101
|
+
def recent?
|
|
102
|
+
Time.at(timestamp) >= 2.minutes.ago
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def expected_signature
|
|
106
|
+
OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# Ingests inbound emails from Mandrill.
|
|
5
|
+
#
|
|
6
|
+
# Requires a +mandrill_events+ parameter containing a JSON array of Mandrill inbound email event objects.
|
|
7
|
+
# Each event is expected to have a +msg+ object containing a full RFC 822 message in its +raw_msg+ property.
|
|
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 Mandrill
|
|
14
|
+
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
|
15
|
+
# - <tt>500 Server Error</tt> if the Mandrill API key is missing, or one of the Active Record database,
|
|
16
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
17
|
+
class Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController
|
|
18
|
+
before_action :authenticate, except: :health_check
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email }
|
|
22
|
+
head :ok
|
|
23
|
+
rescue JSON::ParserError => error
|
|
24
|
+
logger.error error.message
|
|
25
|
+
head :unprocessable_entity
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def health_check
|
|
29
|
+
head :ok
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
def raw_emails
|
|
34
|
+
events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def events
|
|
38
|
+
JSON.parse params.require(:mandrill_events)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def authenticate
|
|
43
|
+
head :unauthorized unless authenticated?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def authenticated?
|
|
47
|
+
if key.present?
|
|
48
|
+
Authenticator.new(request, key).authenticated?
|
|
49
|
+
else
|
|
50
|
+
raise ArgumentError, <<~MESSAGE.squish
|
|
51
|
+
Missing required Mandrill API key. Set action_mailbox.mandrill_api_key in your application's
|
|
52
|
+
encrypted credentials or provide the MANDRILL_INGRESS_API_KEY environment variable.
|
|
53
|
+
MESSAGE
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def key
|
|
58
|
+
Rails.application.credentials.dig(:action_mailbox, :mandrill_api_key) || ENV["MANDRILL_INGRESS_API_KEY"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Authenticator
|
|
62
|
+
attr_reader :request, :key
|
|
63
|
+
|
|
64
|
+
def initialize(request, key)
|
|
65
|
+
@request, @key = request, key
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def authenticated?
|
|
69
|
+
ActiveSupport::SecurityUtils.secure_compare given_signature, expected_signature
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
def given_signature
|
|
74
|
+
request.headers["X-Mandrill-Signature"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def expected_signature
|
|
78
|
+
Base64.strict_encode64 OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def message
|
|
82
|
+
request.url + request.POST.sort.flatten.join
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# Ingests inbound emails from Postmark. Requires a +RawEmail+ parameter containing a full RFC 822 message.
|
|
5
|
+
#
|
|
6
|
+
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
|
7
|
+
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
|
8
|
+
#
|
|
9
|
+
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
|
10
|
+
# the Postmark ingress can learn its password. You should only use the Postmark ingress over HTTPS.
|
|
11
|
+
#
|
|
12
|
+
# Returns:
|
|
13
|
+
#
|
|
14
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
15
|
+
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
|
16
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postmark
|
|
17
|
+
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +RawEmail+ parameter
|
|
18
|
+
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
|
19
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
20
|
+
#
|
|
21
|
+
# == Usage
|
|
22
|
+
#
|
|
23
|
+
# 1. Tell Action Mailbox to accept emails from Postmark:
|
|
24
|
+
#
|
|
25
|
+
# # config/environments/production.rb
|
|
26
|
+
# config.action_mailbox.ingress = :postmark
|
|
27
|
+
#
|
|
28
|
+
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postmark ingress.
|
|
29
|
+
#
|
|
30
|
+
# Use <tt>bin/rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
|
31
|
+
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
|
32
|
+
#
|
|
33
|
+
# action_mailbox:
|
|
34
|
+
# ingress_password: ...
|
|
35
|
+
#
|
|
36
|
+
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
|
37
|
+
#
|
|
38
|
+
# 3. {Configure Postmark}[https://postmarkapp.com/manual#configure-your-inbound-webhook-url] to forward inbound emails
|
|
39
|
+
# to +/rails/action_mailbox/postmark/inbound_emails+ with the username +actionmailbox+ and the password you
|
|
40
|
+
# previously generated. If your application lived at <tt>https://example.com</tt>, you would configure your
|
|
41
|
+
# Postmark inbound webhook with the following fully-qualified URL:
|
|
42
|
+
#
|
|
43
|
+
# https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails
|
|
44
|
+
#
|
|
45
|
+
# *NOTE:* When configuring your Postmark inbound webhook, be sure to check the box labeled *"Include raw email
|
|
46
|
+
# content in JSON payload"*. Action Mailbox needs the raw email content to work.
|
|
47
|
+
class Ingresses::Postmark::InboundEmailsController < ActionMailbox::BaseController
|
|
48
|
+
before_action :authenticate_by_password
|
|
49
|
+
param_encoding :create, "RawEmail", Encoding::ASCII_8BIT
|
|
50
|
+
|
|
51
|
+
def create
|
|
52
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! mail
|
|
53
|
+
rescue ActionController::ParameterMissing => error
|
|
54
|
+
logger.error <<~MESSAGE
|
|
55
|
+
#{error.message}
|
|
56
|
+
|
|
57
|
+
When configuring your Postmark inbound webhook, be sure to check the box
|
|
58
|
+
labeled "Include raw email content in JSON payload".
|
|
59
|
+
MESSAGE
|
|
60
|
+
head :unprocessable_entity
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
def mail
|
|
65
|
+
params.require("RawEmail").tap do |raw_email|
|
|
66
|
+
raw_email.prepend("X-Original-To: ", params.require("OriginalRecipient"), "\n") if params.key?("OriginalRecipient")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# Ingests inbound emails relayed from an SMTP server.
|
|
5
|
+
#
|
|
6
|
+
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
|
7
|
+
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
|
8
|
+
#
|
|
9
|
+
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
|
10
|
+
# the ingress can learn its password. You should only use this ingress over HTTPS.
|
|
11
|
+
#
|
|
12
|
+
# Returns:
|
|
13
|
+
#
|
|
14
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
15
|
+
# - <tt>401 Unauthorized</tt> if the request could not be authenticated
|
|
16
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails relayed from an SMTP server
|
|
17
|
+
# - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message
|
|
18
|
+
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
|
19
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
20
|
+
#
|
|
21
|
+
# == Usage
|
|
22
|
+
#
|
|
23
|
+
# 1. Tell Action Mailbox to accept emails from an SMTP relay:
|
|
24
|
+
#
|
|
25
|
+
# # config/environments/production.rb
|
|
26
|
+
# config.action_mailbox.ingress = :relay
|
|
27
|
+
#
|
|
28
|
+
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the ingress.
|
|
29
|
+
#
|
|
30
|
+
# Use <tt>bin/rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
|
31
|
+
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
|
32
|
+
#
|
|
33
|
+
# action_mailbox:
|
|
34
|
+
# ingress_password: ...
|
|
35
|
+
#
|
|
36
|
+
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
|
37
|
+
#
|
|
38
|
+
# 3. Configure your SMTP server to pipe inbound emails to the appropriate ingress command, providing the +URL+ of the
|
|
39
|
+
# relay ingress and the +INGRESS_PASSWORD+ you previously generated.
|
|
40
|
+
#
|
|
41
|
+
# If your application lives at <tt>https://example.com</tt>, you would configure the Postfix SMTP server to pipe
|
|
42
|
+
# inbound emails to the following command:
|
|
43
|
+
#
|
|
44
|
+
# $ bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=...
|
|
45
|
+
#
|
|
46
|
+
# Built-in ingress commands are available for these popular SMTP servers:
|
|
47
|
+
#
|
|
48
|
+
# - Exim (<tt>bin/rails action_mailbox:ingress:exim</tt>)
|
|
49
|
+
# - Postfix (<tt>bin/rails action_mailbox:ingress:postfix</tt>)
|
|
50
|
+
# - Qmail (<tt>bin/rails action_mailbox:ingress:qmail</tt>)
|
|
51
|
+
class Ingresses::Relay::InboundEmailsController < ActionMailbox::BaseController
|
|
52
|
+
before_action :authenticate_by_password, :require_valid_rfc822_message
|
|
53
|
+
|
|
54
|
+
def create
|
|
55
|
+
if request.body
|
|
56
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read
|
|
57
|
+
else
|
|
58
|
+
head :unprocessable_entity
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
def require_valid_rfc822_message
|
|
64
|
+
unless request.media_type == "message/rfc822"
|
|
65
|
+
head :unsupported_media_type
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# Ingests inbound emails from SendGrid. Requires an +email+ parameter containing a full RFC 822 message.
|
|
5
|
+
#
|
|
6
|
+
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
|
|
7
|
+
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
|
|
8
|
+
#
|
|
9
|
+
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
|
|
10
|
+
# the SendGrid ingress can learn its password. You should only use the SendGrid ingress over HTTPS.
|
|
11
|
+
#
|
|
12
|
+
# Returns:
|
|
13
|
+
#
|
|
14
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
|
15
|
+
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
|
16
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SendGrid
|
|
17
|
+
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +email+ parameter
|
|
18
|
+
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
|
|
19
|
+
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
|
|
20
|
+
#
|
|
21
|
+
# == Usage
|
|
22
|
+
#
|
|
23
|
+
# 1. Tell Action Mailbox to accept emails from SendGrid:
|
|
24
|
+
#
|
|
25
|
+
# # config/environments/production.rb
|
|
26
|
+
# config.action_mailbox.ingress = :sendgrid
|
|
27
|
+
#
|
|
28
|
+
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress.
|
|
29
|
+
#
|
|
30
|
+
# Use <tt>bin/rails credentials:edit</tt> to add the password to your application's encrypted credentials under
|
|
31
|
+
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
|
|
32
|
+
#
|
|
33
|
+
# action_mailbox:
|
|
34
|
+
# ingress_password: ...
|
|
35
|
+
#
|
|
36
|
+
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
|
|
37
|
+
#
|
|
38
|
+
# 3. {Configure SendGrid Inbound Parse}[https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/]
|
|
39
|
+
# to forward inbound emails to +/rails/action_mailbox/sendgrid/inbound_emails+ with the username +actionmailbox+ and
|
|
40
|
+
# the password you previously generated. If your application lived at <tt>https://example.com</tt>, you would
|
|
41
|
+
# configure SendGrid with the following fully-qualified URL:
|
|
42
|
+
#
|
|
43
|
+
# https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails
|
|
44
|
+
#
|
|
45
|
+
# *NOTE:* When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled *"Post the raw,
|
|
46
|
+
# full MIME message."* Action Mailbox needs the raw MIME message to work.
|
|
47
|
+
class Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController
|
|
48
|
+
before_action :authenticate_by_password
|
|
49
|
+
param_encoding :create, :email, Encoding::ASCII_8BIT
|
|
50
|
+
|
|
51
|
+
def create
|
|
52
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! mail
|
|
53
|
+
rescue JSON::ParserError => error
|
|
54
|
+
logger.error error.message
|
|
55
|
+
head :unprocessable_entity
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
def mail
|
|
60
|
+
params.require(:email).tap do |raw_email|
|
|
61
|
+
envelope["to"].each { |to| raw_email.prepend("X-Original-To: ", to, "\n") } if params.key?(:envelope)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def envelope
|
|
66
|
+
JSON.parse(params.require(:envelope))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :enddoc:
|
|
4
|
+
|
|
5
|
+
module Rails
|
|
6
|
+
class Conductor::ActionMailbox::InboundEmails::SourcesController < Rails::Conductor::BaseController # :nodoc:
|
|
7
|
+
def new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
inbound_email = ActionMailbox::InboundEmail.create_and_extract_message_id! params[:source]
|
|
12
|
+
redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :enddoc:
|
|
4
|
+
|
|
5
|
+
module Rails
|
|
6
|
+
class Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController
|
|
7
|
+
def index
|
|
8
|
+
@inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
@inbound_email = ActionMailbox::InboundEmail.find(params[:id])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
inbound_email = create_inbound_email(new_mail)
|
|
20
|
+
redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
def new_mail
|
|
25
|
+
Mail.new(mail_params.except(:attachments).to_h).tap do |mail|
|
|
26
|
+
mail[:bcc]&.include_in_headers = true
|
|
27
|
+
mail_params[:attachments]&.select(&:present?)&.each do |attachment|
|
|
28
|
+
mail.add_file(filename: attachment.original_filename, content: attachment.read)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mail_params
|
|
34
|
+
params.expect(mail: [:from, :to, :cc, :bcc, :x_original_to, :in_reply_to, :subject, :body, attachments: []])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_inbound_email(mail)
|
|
38
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :enddoc:
|
|
4
|
+
|
|
5
|
+
module Rails
|
|
6
|
+
# Incinerating will destroy an email that is due and has already been processed.
|
|
7
|
+
class Conductor::ActionMailbox::IncineratesController < Rails::Conductor::BaseController
|
|
8
|
+
def create
|
|
9
|
+
ActionMailbox::InboundEmail.find(params[:inbound_email_id]).incinerate
|
|
10
|
+
|
|
11
|
+
redirect_to main_app.rails_conductor_inbound_emails_url
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :enddoc:
|
|
4
|
+
|
|
5
|
+
module Rails
|
|
6
|
+
# Rerouting will run routing and processing on an email that has already been, or attempted to be, processed.
|
|
7
|
+
class Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController
|
|
8
|
+
def create
|
|
9
|
+
inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id])
|
|
10
|
+
reroute inbound_email
|
|
11
|
+
|
|
12
|
+
redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
def reroute(inbound_email)
|
|
17
|
+
inbound_email.pending!
|
|
18
|
+
inbound_email.route_later
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
# TODO: Move this to Rails::Conductor gem
|
|
5
|
+
class Conductor::BaseController < ActionController::Base
|
|
6
|
+
layout "rails/conductor"
|
|
7
|
+
before_action :ensure_development_env
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
def ensure_development_env
|
|
11
|
+
head :forbidden unless Rails.env.development?
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# You can configure when this +IncinerationJob+ will be run as a time-after-processing using the
|
|
5
|
+
# +config.action_mailbox.incinerate_after+ or +ActionMailbox.incinerate_after+ setting.
|
|
6
|
+
#
|
|
7
|
+
# Since this incineration is set for the future, it'll automatically ignore any <tt>InboundEmail</tt>s
|
|
8
|
+
# that have already been deleted and discard itself if so.
|
|
9
|
+
#
|
|
10
|
+
# You can disable incinerating processed emails by setting +config.action_mailbox.incinerate+ or
|
|
11
|
+
# +ActionMailbox.incinerate+ to +false+.
|
|
12
|
+
class IncinerationJob < ActiveJob::Base
|
|
13
|
+
queue_as { ActionMailbox.queues[:incineration] }
|
|
14
|
+
|
|
15
|
+
discard_on ActiveRecord::RecordNotFound
|
|
16
|
+
|
|
17
|
+
def self.schedule(inbound_email)
|
|
18
|
+
set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def perform(inbound_email)
|
|
22
|
+
inbound_email.incinerate
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly
|
|
5
|
+
# accept new incoming emails without being burdened to hang while they're actually being processed.
|
|
6
|
+
class RoutingJob < ActiveJob::Base
|
|
7
|
+
queue_as { ActionMailbox.queues[:routing] }
|
|
8
|
+
|
|
9
|
+
def perform(inbound_email)
|
|
10
|
+
inbound_email.route
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
# Command class for carrying out the actual incineration of the +InboundMail+ that's been scheduled
|
|
5
|
+
# for removal. Before the incineration – which really is just a call to +#destroy!+ – is run, we verify
|
|
6
|
+
# that it's both eligible (by virtue of having already been processed) and time to do so (that is,
|
|
7
|
+
# the +InboundEmail+ was processed after the +incinerate_after+ time).
|
|
8
|
+
class InboundEmail::Incineratable::Incineration
|
|
9
|
+
def initialize(inbound_email)
|
|
10
|
+
@inbound_email = inbound_email
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
@inbound_email.destroy! if due? && processed?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
def due?
|
|
19
|
+
@inbound_email.updated_at < ActionMailbox.incinerate_after.ago.end_of_day
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def processed?
|
|
23
|
+
@inbound_email.processed?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|