actionmailbox 6.0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +51 -0
- data/MIT-LICENSE +21 -0
- data/README.md +13 -0
- data/app/controllers/action_mailbox/base_controller.rb +34 -0
- data/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +103 -0
- data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +82 -0
- data/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb +62 -0
- data/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb +65 -0
- data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +54 -0
- data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +35 -0
- data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +19 -0
- data/app/controllers/rails/conductor/base_controller.rb +14 -0
- data/app/jobs/action_mailbox/incineration_job.rb +25 -0
- data/app/jobs/action_mailbox/routing_job.rb +13 -0
- data/app/models/action_mailbox/inbound_email.rb +49 -0
- data/app/models/action_mailbox/inbound_email/incineratable.rb +20 -0
- data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +26 -0
- data/app/models/action_mailbox/inbound_email/message_id.rb +38 -0
- data/app/models/action_mailbox/inbound_email/routable.rb +24 -0
- data/app/views/layouts/rails/conductor.html.erb +8 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +15 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +47 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +15 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20180917164000_create_action_mailbox_tables.rb +13 -0
- data/lib/action_mailbox.rb +17 -0
- data/lib/action_mailbox/base.rb +118 -0
- data/lib/action_mailbox/callbacks.rb +34 -0
- data/lib/action_mailbox/engine.rb +33 -0
- data/lib/action_mailbox/gem_version.rb +17 -0
- data/lib/action_mailbox/mail_ext.rb +6 -0
- data/lib/action_mailbox/mail_ext/address_equality.rb +9 -0
- data/lib/action_mailbox/mail_ext/address_wrapping.rb +9 -0
- data/lib/action_mailbox/mail_ext/addresses.rb +29 -0
- data/lib/action_mailbox/mail_ext/from_source.rb +7 -0
- data/lib/action_mailbox/mail_ext/recipients.rb +9 -0
- data/lib/action_mailbox/relayer.rb +75 -0
- data/lib/action_mailbox/router.rb +42 -0
- data/lib/action_mailbox/router/route.rb +42 -0
- data/lib/action_mailbox/routing.rb +22 -0
- data/lib/action_mailbox/test_case.rb +12 -0
- data/lib/action_mailbox/test_helper.rb +48 -0
- data/lib/action_mailbox/version.rb +10 -0
- data/lib/rails/generators/installer.rb +10 -0
- data/lib/rails/generators/mailbox/USAGE +12 -0
- data/lib/rails/generators/mailbox/mailbox_generator.rb +32 -0
- data/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +3 -0
- data/lib/rails/generators/mailbox/templates/mailbox.rb.tt +4 -0
- data/lib/rails/generators/test_unit/mailbox_generator.rb +20 -0
- data/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt +11 -0
- data/lib/tasks/ingress.rake +72 -0
- data/lib/tasks/install.rake +20 -0
- metadata +186 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_mailbox/mail_ext"
|
4
|
+
|
5
|
+
module ActionMailbox
|
6
|
+
extend ActiveSupport::Autoload
|
7
|
+
|
8
|
+
autoload :Base
|
9
|
+
autoload :Router
|
10
|
+
autoload :TestCase
|
11
|
+
|
12
|
+
mattr_accessor :ingress
|
13
|
+
mattr_accessor :logger
|
14
|
+
mattr_accessor :incinerate, default: true
|
15
|
+
mattr_accessor :incinerate_after, default: 30.days
|
16
|
+
mattr_accessor :queues, default: {}
|
17
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/rescuable"
|
4
|
+
|
5
|
+
require "action_mailbox/callbacks"
|
6
|
+
require "action_mailbox/routing"
|
7
|
+
|
8
|
+
module ActionMailbox
|
9
|
+
# The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from
|
10
|
+
# +ApplicationMailbox+ instead, as that's where the app-specific routing is configured. This routing
|
11
|
+
# is specified in the following ways:
|
12
|
+
#
|
13
|
+
# class ApplicationMailbox < ActionMailbox::Base
|
14
|
+
# # Any of the recipients of the mail (whether to, cc, bcc) are matched against the regexp.
|
15
|
+
# routing /^replies@/i => :replies
|
16
|
+
#
|
17
|
+
# # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string.
|
18
|
+
# routing "help@example.com" => :help
|
19
|
+
#
|
20
|
+
# # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true.
|
21
|
+
# routing ->(inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients
|
22
|
+
#
|
23
|
+
# # Any object responding to #match? is called with the inbound_email record as an argument. Match if true.
|
24
|
+
# routing CustomAddress.new => :custom
|
25
|
+
#
|
26
|
+
# # Any inbound_email that has not been already matched will be sent to the BackstopMailbox.
|
27
|
+
# routing :all => :backstop
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Application mailboxes need to overwrite the +#process+ method, which is invoked by the framework after
|
31
|
+
# callbacks have been run. The callbacks available are: +before_processing+, +after_processing+, and
|
32
|
+
# +around_processing+. The primary use case is ensure certain preconditions to processing are fulfilled
|
33
|
+
# using +before_processing+ callbacks.
|
34
|
+
#
|
35
|
+
# If a precondition fails to be met, you can halt the processing using the +#bounced!+ method,
|
36
|
+
# which will silently prevent any further processing, but not actually send out any bounce notice. You
|
37
|
+
# can also pair this behavior with the invocation of an Action Mailer class responsible for sending out
|
38
|
+
# an actual bounce email. This is done using the +#bounce_with+ method, which takes the mail object returned
|
39
|
+
# by an Action Mailer method, like so:
|
40
|
+
#
|
41
|
+
# class ForwardsMailbox < ApplicationMailbox
|
42
|
+
# before_processing :ensure_sender_is_a_user
|
43
|
+
#
|
44
|
+
# private
|
45
|
+
# def ensure_sender_is_a_user
|
46
|
+
# unless User.exist?(email_address: mail.from)
|
47
|
+
# bounce_with UserRequiredMailer.missing(inbound_email)
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# During the processing of the inbound email, the status will be tracked. Before processing begins,
|
53
|
+
# the email will normally have the +pending+ status. Once processing begins, just before callbacks
|
54
|
+
# and the +#process+ method is called, the status is changed to +processing+. If processing is allowed to
|
55
|
+
# complete, the status is changed to +delivered+. If a bounce is triggered, then +bounced+. If an unhandled
|
56
|
+
# exception is bubbled up, then +failed+.
|
57
|
+
#
|
58
|
+
# Exceptions can be handled at the class level using the familiar +Rescuable+ approach:
|
59
|
+
#
|
60
|
+
# class ForwardsMailbox < ApplicationMailbox
|
61
|
+
# rescue_from(ApplicationSpecificVerificationError) { bounced! }
|
62
|
+
# end
|
63
|
+
class Base
|
64
|
+
include ActiveSupport::Rescuable
|
65
|
+
include ActionMailbox::Callbacks, ActionMailbox::Routing
|
66
|
+
|
67
|
+
attr_reader :inbound_email
|
68
|
+
delegate :mail, :delivered!, :bounced!, to: :inbound_email
|
69
|
+
|
70
|
+
delegate :logger, to: ActionMailbox
|
71
|
+
|
72
|
+
def self.receive(inbound_email)
|
73
|
+
new(inbound_email).perform_processing
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(inbound_email)
|
77
|
+
@inbound_email = inbound_email
|
78
|
+
end
|
79
|
+
|
80
|
+
def perform_processing #:nodoc:
|
81
|
+
track_status_of_inbound_email do
|
82
|
+
run_callbacks :process do
|
83
|
+
process
|
84
|
+
end
|
85
|
+
end
|
86
|
+
rescue => exception
|
87
|
+
# TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier
|
88
|
+
rescue_with_handler(exception) || raise
|
89
|
+
end
|
90
|
+
|
91
|
+
def process
|
92
|
+
# Overwrite in subclasses
|
93
|
+
end
|
94
|
+
|
95
|
+
def finished_processing? #:nodoc:
|
96
|
+
inbound_email.delivered? || inbound_email.bounced?
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# Enqueues the given +message+ for delivery and changes the inbound email's status to +:bounced+.
|
101
|
+
def bounce_with(message)
|
102
|
+
inbound_email.bounced!
|
103
|
+
message.deliver_later
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
def track_status_of_inbound_email
|
108
|
+
inbound_email.processing!
|
109
|
+
yield
|
110
|
+
inbound_email.delivered! unless inbound_email.bounced?
|
111
|
+
rescue
|
112
|
+
inbound_email.failed!
|
113
|
+
raise
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
ActiveSupport.run_load_hooks :action_mailbox, ActionMailbox::Base
|
@@ -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 = 2
|
13
|
+
PRE = "1"
|
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,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,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
|