omg-actionmailbox 8.0.0.alpha3

Sign up to get free protection for your applications and to get access to all the features.
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