actionmailbox 6.0.1

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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -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 +103 -0
  7. data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +82 -0
  8. data/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb +62 -0
  9. data/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb +65 -0
  10. data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +54 -0
  11. data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +35 -0
  12. data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +19 -0
  13. data/app/controllers/rails/conductor/base_controller.rb +14 -0
  14. data/app/jobs/action_mailbox/incineration_job.rb +25 -0
  15. data/app/jobs/action_mailbox/routing_job.rb +13 -0
  16. data/app/models/action_mailbox/inbound_email.rb +49 -0
  17. data/app/models/action_mailbox/inbound_email/incineratable.rb +20 -0
  18. data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +26 -0
  19. data/app/models/action_mailbox/inbound_email/message_id.rb +38 -0
  20. data/app/models/action_mailbox/inbound_email/routable.rb +24 -0
  21. data/app/views/layouts/rails/conductor.html.erb +8 -0
  22. data/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +15 -0
  23. data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +47 -0
  24. data/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +15 -0
  25. data/config/routes.rb +19 -0
  26. data/db/migrate/20180917164000_create_action_mailbox_tables.rb +13 -0
  27. data/lib/action_mailbox.rb +17 -0
  28. data/lib/action_mailbox/base.rb +118 -0
  29. data/lib/action_mailbox/callbacks.rb +34 -0
  30. data/lib/action_mailbox/engine.rb +33 -0
  31. data/lib/action_mailbox/gem_version.rb +17 -0
  32. data/lib/action_mailbox/mail_ext.rb +6 -0
  33. data/lib/action_mailbox/mail_ext/address_equality.rb +9 -0
  34. data/lib/action_mailbox/mail_ext/address_wrapping.rb +9 -0
  35. data/lib/action_mailbox/mail_ext/addresses.rb +29 -0
  36. data/lib/action_mailbox/mail_ext/from_source.rb +7 -0
  37. data/lib/action_mailbox/mail_ext/recipients.rb +9 -0
  38. data/lib/action_mailbox/relayer.rb +75 -0
  39. data/lib/action_mailbox/router.rb +42 -0
  40. data/lib/action_mailbox/router/route.rb +42 -0
  41. data/lib/action_mailbox/routing.rb +22 -0
  42. data/lib/action_mailbox/test_case.rb +12 -0
  43. data/lib/action_mailbox/test_helper.rb +48 -0
  44. data/lib/action_mailbox/version.rb +10 -0
  45. data/lib/rails/generators/installer.rb +10 -0
  46. data/lib/rails/generators/mailbox/USAGE +12 -0
  47. data/lib/rails/generators/mailbox/mailbox_generator.rb +32 -0
  48. data/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +3 -0
  49. data/lib/rails/generators/mailbox/templates/mailbox.rb.tt +4 -0
  50. data/lib/rails/generators/test_unit/mailbox_generator.rb +20 -0
  51. data/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt +11 -0
  52. data/lib/tasks/ingress.rake +72 -0
  53. data/lib/tasks/install.rake +20 -0
  54. metadata +186 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+
5
+ module ActionMailbox
6
+ # Defines the callbacks related to processing.
7
+ module Callbacks
8
+ extend ActiveSupport::Concern
9
+ include ActiveSupport::Callbacks
10
+
11
+ TERMINATOR = ->(mailbox, chain) do
12
+ chain.call
13
+ mailbox.finished_processing?
14
+ end
15
+
16
+ included do
17
+ define_callbacks :process, terminator: TERMINATOR, skip_after_callbacks_if_terminated: true
18
+ end
19
+
20
+ class_methods do
21
+ def before_processing(*methods, &block)
22
+ set_callback(:process, :before, *methods, &block)
23
+ end
24
+
25
+ def after_processing(*methods, &block)
26
+ set_callback(:process, :after, *methods, &block)
27
+ end
28
+
29
+ def around_processing(*methods, &block)
30
+ set_callback(:process, :around, *methods, &block)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "action_controller/railtie"
5
+ require "active_job/railtie"
6
+ require "active_record/railtie"
7
+ require "active_storage/engine"
8
+
9
+ require "action_mailbox"
10
+
11
+ module ActionMailbox
12
+ class Engine < Rails::Engine
13
+ isolate_namespace ActionMailbox
14
+ config.eager_load_namespaces << ActionMailbox
15
+
16
+ config.action_mailbox = ActiveSupport::OrderedOptions.new
17
+ config.action_mailbox.incinerate = true
18
+ config.action_mailbox.incinerate_after = 30.days
19
+
20
+ config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \
21
+ incineration: :action_mailbox_incineration, routing: :action_mailbox_routing
22
+
23
+ initializer "action_mailbox.config" do
24
+ config.after_initialize do |app|
25
+ ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger
26
+ ActionMailbox.incinerate = app.config.action_mailbox.incinerate.nil? ? true : app.config.action_mailbox.incinerate
27
+ ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
28
+ ActionMailbox.queues = app.config.action_mailbox.queues || {}
29
+ ActionMailbox.ingress = app.config.action_mailbox.ingress
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailbox
4
+ # Returns the currently-loaded version of Action Mailbox as a <tt>Gem::Version</tt>.
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 1
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mail"
4
+
5
+ # The hope is to upstream most of these basic additions to the Mail gem's Mail object. But until then, here they lay!
6
+ Dir["#{File.expand_path(File.dirname(__FILE__))}/mail_ext/*"].each { |path| require "action_mailbox/mail_ext/#{File.basename(path)}" }
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ class Address
5
+ def ==(other_address)
6
+ other_address.is_a?(Mail::Address) && to_s == other_address.to_s
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ class Address
5
+ def self.wrap(address)
6
+ address.is_a?(Mail::Address) ? address : Mail::Address.new(address)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ class Message
5
+ def from_address
6
+ header[:from]&.address_list&.addresses&.first
7
+ end
8
+
9
+ def recipients_addresses
10
+ to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses
11
+ end
12
+
13
+ def to_addresses
14
+ Array(header[:to]&.address_list&.addresses)
15
+ end
16
+
17
+ def cc_addresses
18
+ Array(header[:cc]&.address_list&.addresses)
19
+ end
20
+
21
+ def bcc_addresses
22
+ Array(header[:bcc]&.address_list&.addresses)
23
+ end
24
+
25
+ def x_original_to_addresses
26
+ Array(header[:x_original_to]).collect { |header| Mail::Address.new header.to_s }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ def self.from_source(source)
5
+ Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s)
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ class Message
5
+ def recipients
6
+ Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_mailbox/version"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module ActionMailbox
8
+ class Relayer
9
+ class Result < Struct.new(:status_code, :message)
10
+ def success?
11
+ !failure?
12
+ end
13
+
14
+ def failure?
15
+ transient_failure? || permanent_failure?
16
+ end
17
+
18
+ def transient_failure?
19
+ status_code.start_with?("4.")
20
+ end
21
+
22
+ def permanent_failure?
23
+ status_code.start_with?("5.")
24
+ end
25
+ end
26
+
27
+ CONTENT_TYPE = "message/rfc822"
28
+ USER_AGENT = "Action Mailbox relayer v#{ActionMailbox.version}"
29
+
30
+ attr_reader :uri, :username, :password
31
+
32
+ def initialize(url:, username: "actionmailbox", password:)
33
+ @uri, @username, @password = URI(url), username, password
34
+ end
35
+
36
+ def relay(source)
37
+ case response = post(source)
38
+ when Net::HTTPSuccess
39
+ Result.new "2.0.0", "Successfully relayed message to ingress"
40
+ when Net::HTTPUnauthorized
41
+ Result.new "4.7.0", "Invalid credentials for ingress"
42
+ else
43
+ Result.new "4.0.0", "HTTP #{response.code}"
44
+ end
45
+ rescue IOError, SocketError, SystemCallError => error
46
+ Result.new "4.4.2", "Network error relaying to ingress: #{error.message}"
47
+ rescue Timeout::Error
48
+ Result.new "4.4.2", "Timed out relaying to ingress"
49
+ rescue => error
50
+ Result.new "4.0.0", "Error relaying to ingress: #{error.message}"
51
+ end
52
+
53
+ private
54
+ def post(source)
55
+ client.post uri, source,
56
+ "Content-Type" => CONTENT_TYPE,
57
+ "User-Agent" => USER_AGENT,
58
+ "Authorization" => "Basic #{Base64.strict_encode64(username + ":" + password)}"
59
+ end
60
+
61
+ def client
62
+ @client ||= Net::HTTP.new(uri.host, uri.port).tap do |connection|
63
+ if uri.scheme == "https"
64
+ require "openssl"
65
+
66
+ connection.use_ssl = true
67
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
68
+ end
69
+
70
+ connection.open_timeout = 1
71
+ connection.read_timeout = 10
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailbox
4
+ # Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when
5
+ # an inbound_email is received.
6
+ class Router
7
+ class RoutingError < StandardError; end
8
+
9
+ def initialize
10
+ @routes = []
11
+ end
12
+
13
+ def add_routes(routes)
14
+ routes.each do |(address, mailbox_name)|
15
+ add_route address, to: mailbox_name
16
+ end
17
+ end
18
+
19
+ def add_route(address, to:)
20
+ routes.append Route.new(address, to: to)
21
+ end
22
+
23
+ def route(inbound_email)
24
+ if mailbox = match_to_mailbox(inbound_email)
25
+ mailbox.receive(inbound_email)
26
+ else
27
+ inbound_email.bounced!
28
+
29
+ raise RoutingError
30
+ end
31
+ end
32
+
33
+ private
34
+ attr_reader :routes
35
+
36
+ def match_to_mailbox(inbound_email)
37
+ routes.detect { |route| route.match?(inbound_email) }.try(:mailbox_class)
38
+ end
39
+ end
40
+ end
41
+
42
+ require "action_mailbox/router/route"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailbox
4
+ # Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching
5
+ # mailbox class. See examples for the different route addresses and how to use them in the +ActionMailbox::Base+
6
+ # documentation.
7
+ class Router::Route
8
+ attr_reader :address, :mailbox_name
9
+
10
+ def initialize(address, to:)
11
+ @address, @mailbox_name = address, to
12
+
13
+ ensure_valid_address
14
+ end
15
+
16
+ def match?(inbound_email)
17
+ case address
18
+ when :all
19
+ true
20
+ when String
21
+ inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) }
22
+ when Regexp
23
+ inbound_email.mail.recipients.any? { |recipient| address.match?(recipient) }
24
+ when Proc
25
+ address.call(inbound_email)
26
+ else
27
+ address.match?(inbound_email)
28
+ end
29
+ end
30
+
31
+ def mailbox_class
32
+ "#{mailbox_name.to_s.camelize}Mailbox".constantize
33
+ end
34
+
35
+ private
36
+ def ensure_valid_address
37
+ unless [ Symbol, String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?)
38
+ raise ArgumentError, "Expected a Symbol, String, Regexp, Proc, or matchable, got #{address.inspect}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailbox
4
+ # See +ActionMailbox::Base+ for how to specify routing.
5
+ module Routing
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ cattr_accessor :router, default: ActionMailbox::Router.new
10
+ end
11
+
12
+ class_methods do
13
+ def routing(routes)
14
+ router.add_routes(routes)
15
+ end
16
+
17
+ def route(inbound_email)
18
+ router.route(inbound_email)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_mailbox/test_helper"
4
+ require "active_support/test_case"
5
+
6
+ module ActionMailbox
7
+ class TestCase < ActiveSupport::TestCase
8
+ include ActionMailbox::TestHelper
9
+ end
10
+ end
11
+
12
+ ActiveSupport.run_load_hooks :action_mailbox_test_case, ActionMailbox::TestCase
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mail"
4
+
5
+ module ActionMailbox
6
+ module TestHelper
7
+ # Create an +InboundEmail+ record using an eml fixture in the format of message/rfc822
8
+ # referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
9
+ def create_inbound_email_from_fixture(fixture_name, status: :processing)
10
+ create_inbound_email_from_source file_fixture(fixture_name).read, status: status
11
+ end
12
+
13
+ # Create an +InboundEmail+ by specifying it using +Mail.new+ options. Example:
14
+ #
15
+ # create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!")
16
+ def create_inbound_email_from_mail(status: :processing, **mail_options)
17
+ mail = Mail.new(mail_options)
18
+ # Bcc header is not encoded by default
19
+ mail[:bcc].include_in_headers = true if mail[:bcc]
20
+
21
+ create_inbound_email_from_source mail.to_s, status: status
22
+ end
23
+
24
+ # Create an +InboundEmail+ using the raw rfc822 +source+ as text.
25
+ def create_inbound_email_from_source(source, status: :processing)
26
+ ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
27
+ end
28
+
29
+
30
+ # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_fixture+
31
+ # and immediately route it to processing.
32
+ def receive_inbound_email_from_fixture(*args)
33
+ create_inbound_email_from_fixture(*args).tap(&:route)
34
+ end
35
+
36
+ # Create an +InboundEmail+ using the same arguments as +create_inbound_email_from_mail+ and immediately route it to
37
+ # processing.
38
+ def receive_inbound_email_from_mail(**kwargs)
39
+ create_inbound_email_from_mail(**kwargs).tap(&:route)
40
+ end
41
+
42
+ # Create an +InboundEmail+ using the same arguments as +create_inbound_email_from_source+ and immediately route it
43
+ # to processing.
44
+ def receive_inbound_email_from_source(*args)
45
+ create_inbound_email_from_source(*args).tap(&:route)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gem_version"
4
+
5
+ module ActionMailbox
6
+ # Returns the currently-loaded version of Action Mailbox as a <tt>Gem::Version</tt>.
7
+ def self.version
8
+ gem_version
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ say "Copying application_mailbox.rb to app/mailboxes"
4
+ copy_file "#{__dir__}/mailbox/templates/application_mailbox.rb", "app/mailboxes/application_mailbox.rb"
5
+
6
+ environment <<~end_of_config, env: "production"
7
+ # Prepare the ingress controller used to receive mail
8
+ # config.action_mailbox.ingress = :relay
9
+
10
+ end_of_config
@@ -0,0 +1,12 @@
1
+ Description:
2
+ ============
3
+ Stubs out a new mailbox class in app/mailboxes and invokes your template
4
+ engine and test framework generators.
5
+
6
+ Example:
7
+ ========
8
+ rails generate mailbox inbox
9
+
10
+ creates a InboxMailbox class and test:
11
+ Mailbox: app/mailboxes/inbox_mailbox.rb
12
+ Test: test/mailboxes/inbox_mailbox_test.rb