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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +13 -0
  5. data/app/controllers/action_mailbox/base_controller.rb +34 -0
  6. data/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +110 -0
  7. data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +86 -0
  8. data/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb +70 -0
  9. data/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb +69 -0
  10. data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +69 -0
  11. data/app/controllers/rails/conductor/action_mailbox/inbound_emails/sources_controller.rb +15 -0
  12. data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +41 -0
  13. data/app/controllers/rails/conductor/action_mailbox/incinerates_controller.rb +14 -0
  14. data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +21 -0
  15. data/app/controllers/rails/conductor/base_controller.rb +14 -0
  16. data/app/jobs/action_mailbox/incineration_job.rb +25 -0
  17. data/app/jobs/action_mailbox/routing_job.rb +13 -0
  18. data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +26 -0
  19. data/app/models/action_mailbox/inbound_email/incineratable.rb +20 -0
  20. data/app/models/action_mailbox/inbound_email/message_id.rb +42 -0
  21. data/app/models/action_mailbox/inbound_email/routable.rb +24 -0
  22. data/app/models/action_mailbox/inbound_email.rb +55 -0
  23. data/app/models/action_mailbox/record.rb +9 -0
  24. data/app/views/layouts/rails/conductor.html.erb +8 -0
  25. data/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +16 -0
  26. data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +52 -0
  27. data/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +15 -0
  28. data/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb +12 -0
  29. data/config/routes.rb +26 -0
  30. data/db/migrate/20180917164000_create_action_mailbox_tables.rb +19 -0
  31. data/lib/action_mailbox/base.rb +135 -0
  32. data/lib/action_mailbox/callbacks.rb +36 -0
  33. data/lib/action_mailbox/deprecator.rb +7 -0
  34. data/lib/action_mailbox/engine.rb +40 -0
  35. data/lib/action_mailbox/gem_version.rb +17 -0
  36. data/lib/action_mailbox/mail_ext/address_equality.rb +9 -0
  37. data/lib/action_mailbox/mail_ext/address_wrapping.rb +9 -0
  38. data/lib/action_mailbox/mail_ext/addresses.rb +44 -0
  39. data/lib/action_mailbox/mail_ext/from_source.rb +7 -0
  40. data/lib/action_mailbox/mail_ext/recipients.rb +10 -0
  41. data/lib/action_mailbox/mail_ext.rb +6 -0
  42. data/lib/action_mailbox/relayer.rb +75 -0
  43. data/lib/action_mailbox/router/route.rb +42 -0
  44. data/lib/action_mailbox/router.rb +44 -0
  45. data/lib/action_mailbox/routing.rb +26 -0
  46. data/lib/action_mailbox/test_case.rb +12 -0
  47. data/lib/action_mailbox/test_helper.rb +95 -0
  48. data/lib/action_mailbox/version.rb +10 -0
  49. data/lib/action_mailbox.rb +26 -0
  50. data/lib/generators/action_mailbox/install/install_generator.rb +28 -0
  51. data/lib/rails/generators/mailbox/USAGE +10 -0
  52. data/lib/rails/generators/mailbox/mailbox_generator.rb +32 -0
  53. data/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +3 -0
  54. data/lib/rails/generators/mailbox/templates/mailbox.rb.tt +4 -0
  55. data/lib/rails/generators/test_unit/mailbox_generator.rb +20 -0
  56. data/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt +11 -0
  57. data/lib/tasks/ingress.rake +72 -0
  58. data/lib/tasks/install.rake +6 -0
  59. 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
@@ -0,0 +1,2 @@
1
+
2
+ Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/actionmailbox/CHANGELOG.md) for previous changes.
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