actionmailbox 0.1.0 → 6.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/{LICENSE → MIT-LICENSE} +1 -1
  4. data/README.md +2 -267
  5. data/app/controllers/action_mailbox/base_controller.rb +26 -31
  6. data/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +48 -44
  7. data/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +88 -84
  8. data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +64 -60
  9. data/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb +62 -0
  10. data/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb +65 -0
  11. data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +51 -47
  12. data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +27 -20
  13. data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +15 -11
  14. data/app/controllers/rails/conductor/base_controller.rb +12 -8
  15. data/app/jobs/action_mailbox/incineration_job.rb +17 -13
  16. data/app/jobs/action_mailbox/routing_job.rb +10 -6
  17. data/app/models/action_mailbox/inbound_email.rb +43 -37
  18. data/app/models/action_mailbox/inbound_email/incineratable.rb +5 -3
  19. data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +21 -17
  20. data/app/models/action_mailbox/inbound_email/message_id.rb +22 -20
  21. data/app/models/action_mailbox/inbound_email/routable.rb +7 -5
  22. data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +5 -0
  23. data/config/routes.rb +2 -1
  24. data/db/migrate/20180917164000_create_action_mailbox_tables.rb +10 -4
  25. data/lib/action_mailbox.rb +2 -1
  26. data/lib/action_mailbox/base.rb +100 -93
  27. data/lib/action_mailbox/callbacks.rb +3 -1
  28. data/lib/action_mailbox/engine.rb +9 -1
  29. data/lib/action_mailbox/gem_version.rb +17 -0
  30. data/lib/action_mailbox/mail_ext.rb +2 -0
  31. data/lib/action_mailbox/mail_ext/address_equality.rb +7 -3
  32. data/lib/action_mailbox/mail_ext/address_wrapping.rb +7 -3
  33. data/lib/action_mailbox/mail_ext/addresses.rb +22 -18
  34. data/lib/action_mailbox/mail_ext/from_source.rb +2 -0
  35. data/lib/action_mailbox/mail_ext/recipients.rb +7 -3
  36. data/lib/action_mailbox/{postfix_relayer.rb → relayer.rb} +18 -10
  37. data/lib/action_mailbox/router.rb +30 -26
  38. data/lib/action_mailbox/router/route.rb +34 -30
  39. data/lib/action_mailbox/routing.rb +3 -1
  40. data/lib/action_mailbox/test_case.rb +4 -0
  41. data/lib/action_mailbox/test_helper.rb +8 -6
  42. data/lib/action_mailbox/version.rb +8 -1
  43. data/lib/rails/generators/installer.rb +10 -0
  44. data/lib/rails/generators/mailbox/USAGE +12 -0
  45. data/lib/rails/generators/mailbox/mailbox_generator.rb +32 -0
  46. data/lib/{templates/mailboxes/application_mailbox.rb → rails/generators/mailbox/templates/application_mailbox.rb.tt} +1 -1
  47. data/lib/rails/generators/mailbox/templates/mailbox.rb.tt +4 -0
  48. data/lib/rails/generators/test_unit/mailbox_generator.rb +20 -0
  49. data/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt +13 -0
  50. data/lib/tasks/ingress.rake +53 -5
  51. data/lib/tasks/install.rake +1 -1
  52. metadata +66 -237
  53. data/.gitignore +0 -2
  54. data/Gemfile +0 -8
  55. data/Gemfile.lock +0 -159
  56. data/Rakefile +0 -27
  57. data/actionmailbox.gemspec +0 -27
  58. data/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +0 -55
  59. data/bin/test +0 -5
  60. data/lib/templates/installer.rb +0 -4
  61. data/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb +0 -20
  62. data/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb +0 -89
  63. data/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb +0 -58
  64. data/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +0 -54
  65. data/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb +0 -44
  66. data/test/dummy/.babelrc +0 -18
  67. data/test/dummy/.gitignore +0 -3
  68. data/test/dummy/.postcssrc.yml +0 -3
  69. data/test/dummy/Rakefile +0 -6
  70. data/test/dummy/app/assets/config/manifest.js +0 -3
  71. data/test/dummy/app/assets/images/.keep +0 -0
  72. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  73. data/test/dummy/app/assets/stylesheets/scaffold.css +0 -80
  74. data/test/dummy/app/channels/application_cable/channel.rb +0 -4
  75. data/test/dummy/app/channels/application_cable/connection.rb +0 -4
  76. data/test/dummy/app/controllers/application_controller.rb +0 -2
  77. data/test/dummy/app/controllers/concerns/.keep +0 -0
  78. data/test/dummy/app/helpers/application_helper.rb +0 -2
  79. data/test/dummy/app/javascript/packs/application.js +0 -0
  80. data/test/dummy/app/jobs/application_job.rb +0 -2
  81. data/test/dummy/app/mailboxes/application_mailbox.rb +0 -2
  82. data/test/dummy/app/mailboxes/messages_mailbox.rb +0 -4
  83. data/test/dummy/app/mailers/application_mailer.rb +0 -4
  84. data/test/dummy/app/models/application_record.rb +0 -3
  85. data/test/dummy/app/models/concerns/.keep +0 -0
  86. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  87. data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
  88. data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
  89. data/test/dummy/bin/bundle +0 -3
  90. data/test/dummy/bin/rails +0 -4
  91. data/test/dummy/bin/rake +0 -4
  92. data/test/dummy/bin/setup +0 -36
  93. data/test/dummy/bin/update +0 -31
  94. data/test/dummy/bin/yarn +0 -11
  95. data/test/dummy/config.ru +0 -5
  96. data/test/dummy/config/application.rb +0 -19
  97. data/test/dummy/config/boot.rb +0 -5
  98. data/test/dummy/config/cable.yml +0 -10
  99. data/test/dummy/config/database.yml +0 -25
  100. data/test/dummy/config/environment.rb +0 -5
  101. data/test/dummy/config/environments/development.rb +0 -63
  102. data/test/dummy/config/environments/production.rb +0 -96
  103. data/test/dummy/config/environments/test.rb +0 -46
  104. data/test/dummy/config/initializers/application_controller_renderer.rb +0 -8
  105. data/test/dummy/config/initializers/assets.rb +0 -14
  106. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  107. data/test/dummy/config/initializers/content_security_policy.rb +0 -22
  108. data/test/dummy/config/initializers/cookies_serializer.rb +0 -5
  109. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  110. data/test/dummy/config/initializers/inflections.rb +0 -16
  111. data/test/dummy/config/initializers/mime_types.rb +0 -4
  112. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  113. data/test/dummy/config/locales/en.yml +0 -33
  114. data/test/dummy/config/puma.rb +0 -34
  115. data/test/dummy/config/routes.rb +0 -4
  116. data/test/dummy/config/spring.rb +0 -6
  117. data/test/dummy/config/storage.yml +0 -35
  118. data/test/dummy/config/webpack/development.js +0 -3
  119. data/test/dummy/config/webpack/environment.js +0 -3
  120. data/test/dummy/config/webpack/production.js +0 -3
  121. data/test/dummy/config/webpack/test.js +0 -3
  122. data/test/dummy/config/webpacker.yml +0 -65
  123. data/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb +0 -11
  124. data/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb +0 -26
  125. data/test/dummy/db/schema.rb +0 -43
  126. data/test/dummy/lib/assets/.keep +0 -0
  127. data/test/dummy/log/.keep +0 -0
  128. data/test/dummy/package.json +0 -11
  129. data/test/dummy/public/404.html +0 -67
  130. data/test/dummy/public/422.html +0 -67
  131. data/test/dummy/public/500.html +0 -66
  132. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  133. data/test/dummy/public/apple-touch-icon.png +0 -0
  134. data/test/dummy/public/favicon.ico +0 -0
  135. data/test/dummy/storage/.keep +0 -0
  136. data/test/dummy/yarn.lock +0 -6071
  137. data/test/fixtures/files/welcome.eml +0 -631
  138. data/test/jobs/incineration_job_test.rb +0 -17
  139. data/test/test_helper.rb +0 -54
  140. data/test/unit/inbound_email/incineration_test.rb +0 -45
  141. data/test/unit/inbound_email/message_id_test.rb +0 -13
  142. data/test/unit/inbound_email_test.rb +0 -13
  143. data/test/unit/mail_ext/address_equality_test.rb +0 -9
  144. data/test/unit/mail_ext/address_wrapping_test.rb +0 -11
  145. data/test/unit/mail_ext/recipients_test.rb +0 -33
  146. data/test/unit/mailbox/bouncing_test.rb +0 -29
  147. data/test/unit/mailbox/callbacks_test.rb +0 -75
  148. data/test/unit/mailbox/routing_test.rb +0 -30
  149. data/test/unit/mailbox/state_test.rb +0 -49
  150. data/test/unit/postfix_relayer_test.rb +0 -90
  151. data/test/unit/router_test.rb +0 -137
@@ -1,50 +1,54 @@
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
1
+ # frozen_string_literal: true
46
2
 
47
- def create
48
- ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
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>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
+
50
+ def create
51
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
52
+ end
49
53
  end
50
54
  end
@@ -1,27 +1,34 @@
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
1
+ # frozen_string_literal: true
8
2
 
9
- def show
10
- @inbound_email = ActionMailbox::InboundEmail.find(params[:id])
11
- end
3
+ module Rails
4
+ class Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController
5
+ def index
6
+ @inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc)
7
+ end
12
8
 
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
9
+ def new
10
+ end
17
11
 
18
- private
19
- def new_mail
20
- Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h
12
+ def show
13
+ @inbound_email = ActionMailbox::InboundEmail.find(params[:id])
21
14
  end
22
15
 
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 }
16
+ def create
17
+ inbound_email = create_inbound_email(new_mail)
18
+ redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
26
19
  end
20
+
21
+ private
22
+ def new_mail
23
+ Mail.new(params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h).tap do |mail|
24
+ params[:mail][:attachments].to_a.each do |attachment|
25
+ mail.attachments[attachment.original_filename] = { filename: attachment.path, content_type: attachment.content_type }
26
+ end
27
+ end
28
+ end
29
+
30
+ def create_inbound_email(mail)
31
+ ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s)
32
+ end
33
+ end
27
34
  end
@@ -1,15 +1,19 @@
1
- # Rerouting will run routing and processing on an email that has already been, or attempted to be, processed.
2
- class Rails::Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController
3
- def create
4
- inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id])
5
- reroute inbound_email
1
+ # frozen_string_literal: true
6
2
 
7
- redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
8
- end
3
+ module Rails
4
+ # Rerouting will run routing and processing on an email that has already been, or attempted to be, processed.
5
+ class Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController
6
+ def create
7
+ inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id])
8
+ reroute inbound_email
9
9
 
10
- private
11
- def reroute(inbound_email)
12
- inbound_email.pending!
13
- inbound_email.route_later
10
+ redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
14
11
  end
12
+
13
+ private
14
+ def reroute(inbound_email)
15
+ inbound_email.pending!
16
+ inbound_email.route_later
17
+ end
18
+ end
15
19
  end
@@ -1,10 +1,14 @@
1
- # TODO: Move this to Rails::Conductor gem
2
- class Rails::Conductor::BaseController < ActionController::Base
3
- layout "rails/conductor"
4
- before_action :ensure_development_env
1
+ # frozen_string_literal: true
5
2
 
6
- private
7
- def ensure_development_env
8
- head :forbidden unless Rails.env.development?
9
- end
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
10
14
  end
@@ -1,18 +1,22 @@
1
- # You can configure when this `IncinerationJob` will be run as a time-after-processing using the
2
- # `config.action_mailbox.incinerate_after` or `ActionMailbox.incinerate_after` setting.
3
- #
4
- # Since this incineration is set for the future, it'll automatically ignore any `InboundEmail`s
5
- # that have already been deleted and discard itself if so.
6
- class ActionMailbox::IncinerationJob < ActiveJob::Base
7
- queue_as { ActionMailbox.queues[:incineration] }
1
+ # frozen_string_literal: true
8
2
 
9
- discard_on ActiveRecord::RecordNotFound
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
+ class IncinerationJob < ActiveJob::Base
10
+ queue_as { ActionMailbox.queues[:incineration] }
10
11
 
11
- def self.schedule(inbound_email)
12
- set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
13
- end
12
+ discard_on ActiveRecord::RecordNotFound
13
+
14
+ def self.schedule(inbound_email)
15
+ set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
16
+ end
14
17
 
15
- def perform(inbound_email)
16
- inbound_email.incinerate
18
+ def perform(inbound_email)
19
+ inbound_email.incinerate
20
+ end
17
21
  end
18
22
  end
@@ -1,9 +1,13 @@
1
- # Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly
2
- # accept new incoming emails without being burdened to hang while they're actually being processed.
3
- class ActionMailbox::RoutingJob < ActiveJob::Base
4
- queue_as { ActionMailbox.queues[:routing] }
1
+ # frozen_string_literal: true
5
2
 
6
- def perform(inbound_email)
7
- inbound_email.route
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
8
12
  end
9
13
  end
@@ -1,43 +1,49 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "mail"
2
4
 
3
- # The `InboundEmail` is an Active Record that keeps a reference to the raw email stored in Active Storage
4
- # and tracks the status of processing. By default, incoming emails will go through the following lifecycle:
5
- #
6
- # * Pending: Just received by one of the ingress controllers and scheduled for routing.
7
- # * Processing: During active processing, while a specific mailbox is running its #process method.
8
- # * Delivered: Successfully processed by the specific mailbox.
9
- # * Failed: An exception was raised during the specific mailbox's execution of the `#process` method.
10
- # * Bounced: Rejected processing by the specific mailbox and bounced to sender.
11
- #
12
- # Once the `InboundEmail` has reached the status of being either `delivered`, `failed`, or `bounced`,
13
- # it'll count as having been `#processed?`. Once processed, the `InboundEmail` will be scheduled for
14
- # automatic incineration at a later point.
15
- #
16
- # When working with an `InboundEmail`, you'll usually interact with the parsed version of the source,
17
- # which is available as a `Mail` object from `#mail`. But you can also access the raw source directly
18
- # using the `#source` method.
19
- #
20
- # Examples:
21
- #
22
- # inbound_email.mail.from # => 'david@loudthinking.com'
23
- # inbound_email.source # Returns the full rfc822 source of the email as text
24
- class ActionMailbox::InboundEmail < ActiveRecord::Base
25
- self.table_name = "action_mailbox_inbound_emails"
26
-
27
- include Incineratable, MessageId, Routable
28
-
29
- has_one_attached :raw_email
30
- enum status: %i[ pending processing delivered failed bounced ]
31
-
32
- def mail
33
- @mail ||= Mail.from_source(source)
34
- end
5
+ module ActionMailbox
6
+ # The +InboundEmail+ is an Active Record that keeps a reference to the raw email stored in Active Storage
7
+ # and tracks the status of processing. By default, incoming emails will go through the following lifecycle:
8
+ #
9
+ # * Pending: Just received by one of the ingress controllers and scheduled for routing.
10
+ # * Processing: During active processing, while a specific mailbox is running its #process method.
11
+ # * Delivered: Successfully processed by the specific mailbox.
12
+ # * Failed: An exception was raised during the specific mailbox's execution of the +#process+ method.
13
+ # * Bounced: Rejected processing by the specific mailbox and bounced to sender.
14
+ #
15
+ # Once the +InboundEmail+ has reached the status of being either +delivered+, +failed+, or +bounced+,
16
+ # it'll count as having been +#processed?+. Once processed, the +InboundEmail+ will be scheduled for
17
+ # automatic incineration at a later point.
18
+ #
19
+ # When working with an +InboundEmail+, you'll usually interact with the parsed version of the source,
20
+ # which is available as a +Mail+ object from +#mail+. But you can also access the raw source directly
21
+ # using the +#source+ method.
22
+ #
23
+ # Examples:
24
+ #
25
+ # inbound_email.mail.from # => 'david@loudthinking.com'
26
+ # inbound_email.source # Returns the full rfc822 source of the email as text
27
+ class InboundEmail < ActiveRecord::Base
28
+ self.table_name = "action_mailbox_inbound_emails"
35
29
 
36
- def source
37
- @source ||= raw_email.download
38
- end
30
+ include Incineratable, MessageId, Routable
31
+
32
+ has_one_attached :raw_email
33
+ enum status: %i[ pending processing delivered failed bounced ]
39
34
 
40
- def processed?
41
- delivered? || failed? || bounced?
35
+ def mail
36
+ @mail ||= Mail.from_source(source)
37
+ end
38
+
39
+ def source
40
+ @source ||= raw_email.download
41
+ end
42
+
43
+ def processed?
44
+ delivered? || failed? || bounced?
45
+ end
42
46
  end
43
47
  end
48
+
49
+ ActiveSupport.run_load_hooks :action_mailbox_inbound_email, ActionMailbox::InboundEmail
@@ -1,6 +1,8 @@
1
- # Ensure that the `InboundEmail` is automatically scheduled for later incineration if the status has been
2
- # changed to `processed`. The later incineration will be invoked at the time specified by the
3
- # `ActionMailbox.incinerate_after` time using the `IncinerationJob`.
1
+ # frozen_string_literal: true
2
+
3
+ # Ensure that the +InboundEmail+ is automatically scheduled for later incineration if the status has been
4
+ # changed to +processed+. The later incineration will be invoked at the time specified by the
5
+ # +ActionMailbox.incinerate_after+ time using the +IncinerationJob+.
4
6
  module ActionMailbox::InboundEmail::Incineratable
5
7
  extend ActiveSupport::Concern
6
8
 
@@ -1,22 +1,26 @@
1
- # Command class for carrying out the actual incineration of the `InboundMail` that's been scheduled
2
- # for removal. Before the incineration – which really is just a call to `#destroy!` – is run, we verify
3
- # that it's both eligible (by virtue of having already been processed) and time to do so (that is,
4
- # the `InboundEmail` was processed after the `incinerate_after` time).
5
- class ActionMailbox::InboundEmail::Incineratable::Incineration
6
- def initialize(inbound_email)
7
- @inbound_email = inbound_email
8
- end
9
-
10
- def run
11
- @inbound_email.destroy! if due? && processed?
12
- end
1
+ # frozen_string_literal: true
13
2
 
14
- private
15
- def due?
16
- @inbound_email.updated_at < ActionMailbox.incinerate_after.ago.end_of_day
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
17
11
  end
18
12
 
19
- def processed?
20
- @inbound_email.processed?
13
+ def run
14
+ @inbound_email.destroy! if due? && processed?
21
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
22
26
  end
@@ -1,36 +1,38 @@
1
- # The `Message-ID` as specified by rfc822 is supposed to be a unique identifier for that individual email.
2
- # That makes it an ideal tracking token for debugging and forensics, just like `X-Request-Id` does for
1
+ # frozen_string_literal: true
2
+
3
+ # The +Message-ID+ as specified by rfc822 is supposed to be a unique identifier for that individual email.
4
+ # That makes it an ideal tracking token for debugging and forensics, just like +X-Request-Id+ does for
3
5
  # web request.
4
6
  #
5
7
  # If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated
6
- # using the approach from `Mail::MessageIdField`.
8
+ # using the approach from <tt>Mail::MessageIdField</tt>.
7
9
  module ActionMailbox::InboundEmail::MessageId
8
10
  extend ActiveSupport::Concern
9
11
 
10
- included do
11
- before_save :generate_missing_message_id
12
- end
13
-
14
- module ClassMethods
15
- # Create a new `InboundEmail` from the raw `source` of the email, which be uploaded as a Active Storage
16
- # attachment called `raw_email`. Before the upload, extract the Message-ID from the `source` and set
17
- # it as an attribute on the new `InboundEmail`.
12
+ class_methods do
13
+ # Create a new +InboundEmail+ from the raw +source+ of the email, which be uploaded as a Active Storage
14
+ # attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set
15
+ # it as an attribute on the new +InboundEmail+.
18
16
  def create_and_extract_message_id!(source, **options)
19
- create! message_id: extract_message_id(source), **options do |inbound_email|
17
+ message_checksum = Digest::SHA1.hexdigest(source)
18
+ message_id = extract_message_id(source) || generate_missing_message_id(message_checksum)
19
+
20
+ create! options.merge(message_id: message_id, message_checksum: message_checksum) do |inbound_email|
20
21
  inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
21
22
  end
23
+ rescue ActiveRecord::RecordNotUnique
24
+ nil
22
25
  end
23
26
 
24
27
  private
25
28
  def extract_message_id(source)
26
- Mail.from_source(source).message_id
27
- rescue => e
28
- # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead"
29
+ Mail.from_source(source).message_id rescue nil
29
30
  end
30
- end
31
31
 
32
- private
33
- def generate_missing_message_id
34
- self.message_id ||= Mail::MessageIdField.new.message_id
35
- end
32
+ def generate_missing_message_id(message_checksum)
33
+ Mail::MessageIdField.new("<#{message_checksum}@#{::Socket.gethostname}.mail>").message_id.tap do |message_id|
34
+ logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}"
35
+ end
36
+ end
37
+ end
36
38
  end