actionmailbox 6.0.2
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 +46 -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,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 = 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,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
|
@@ -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
|
+
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
|