actionmailbox 0.1.0

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