diff options
Diffstat (limited to 'actionmailbox/lib')
27 files changed, 730 insertions, 0 deletions
diff --git a/actionmailbox/lib/action_mailbox.rb b/actionmailbox/lib/action_mailbox.rb new file mode 100644 index 0000000000..772dbd6529 --- /dev/null +++ b/actionmailbox/lib/action_mailbox.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "action_mailbox/mail_ext" + +module ActionMailbox + extend ActiveSupport::Autoload + + autoload :Base + autoload :Router + autoload :TestCase + + mattr_accessor :ingress + mattr_accessor :logger + mattr_accessor :incinerate, default: true + mattr_accessor :incinerate_after, default: 30.days + mattr_accessor :queues, default: {} +end diff --git a/actionmailbox/lib/action_mailbox/base.rb b/actionmailbox/lib/action_mailbox/base.rb new file mode 100644 index 0000000000..ff8587acd1 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/base.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require "active_support/rescuable" + +require "action_mailbox/callbacks" +require "action_mailbox/routing" + +module ActionMailbox + # The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from + # +ApplicationMailbox+ instead, as that's where the app-specific routing is configured. This routing + # is specified in the following ways: + # + # class ApplicationMailbox < ActionMailbox::Base + # # Any of the recipients of the mail (whether to, cc, bcc) are matched against the regexp. + # routing /^replies@/i => :replies + # + # # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string. + # routing "help@example.com" => :help + # + # # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true. + # routing ->(inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients + # + # # Any object responding to #match? is called with the inbound_email record as an argument. Match if true. + # routing CustomAddress.new => :custom + # + # # Any inbound_email that has not been already matched will be sent to the BackstopMailbox. + # routing :all => :backstop + # end + # + # Application mailboxes need to overwrite the +#process+ method, which is invoked by the framework after + # callbacks have been run. The callbacks available are: +before_processing+, +after_processing+, and + # +around_processing+. The primary use case is ensure certain preconditions to processing are fulfilled + # using +before_processing+ callbacks. + # + # If a precondition fails to be met, you can halt the processing using the +#bounced!+ method, + # which will silently prevent any further processing, but not actually send out any bounce notice. You + # can also pair this behavior with the invocation of an Action Mailer class responsible for sending out + # an actual bounce email. This is done using the +#bounce_with+ method, which takes the mail object returned + # by an Action Mailer method, like so: + # + # class ForwardsMailbox < ApplicationMailbox + # before_processing :ensure_sender_is_a_user + # + # private + # def ensure_sender_is_a_user + # unless User.exist?(email_address: mail.from) + # bounce_with UserRequiredMailer.missing(inbound_email) + # end + # end + # end + # + # During the processing of the inbound email, the status will be tracked. Before processing begins, + # the email will normally have the +pending+ status. Once processing begins, just before callbacks + # and the +#process+ method is called, the status is changed to +processing+. If processing is allowed to + # complete, the status is changed to +delivered+. If a bounce is triggered, then +bounced+. If an unhandled + # exception is bubbled up, then +failed+. + # + # Exceptions can be handled at the class level using the familiar +Rescuable+ approach: + # + # class ForwardsMailbox < ApplicationMailbox + # rescue_from(ApplicationSpecificVerificationError) { bounced! } + # end + class Base + include ActiveSupport::Rescuable + include ActionMailbox::Callbacks, ActionMailbox::Routing + + attr_reader :inbound_email + delegate :mail, :delivered!, :bounced!, to: :inbound_email + + delegate :logger, to: ActionMailbox + + def self.receive(inbound_email) + new(inbound_email).perform_processing + end + + def initialize(inbound_email) + @inbound_email = inbound_email + end + + def perform_processing #:nodoc: + track_status_of_inbound_email do + run_callbacks :process do + process + end + end + rescue => exception + # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier + rescue_with_handler(exception) || raise + end + + def process + # Overwrite in subclasses + end + + def finished_processing? #:nodoc: + inbound_email.delivered? || inbound_email.bounced? + end + + + # Enqueues the given +message+ for delivery and changes the inbound email's status to +:bounced+. + def bounce_with(message) + inbound_email.bounced! + message.deliver_later + end + + private + def track_status_of_inbound_email + inbound_email.processing! + yield + inbound_email.delivered! unless inbound_email.bounced? + rescue + inbound_email.failed! + raise + end + end +end + +ActiveSupport.run_load_hooks :action_mailbox, ActionMailbox::Base diff --git a/actionmailbox/lib/action_mailbox/callbacks.rb b/actionmailbox/lib/action_mailbox/callbacks.rb new file mode 100644 index 0000000000..2b7212284b --- /dev/null +++ b/actionmailbox/lib/action_mailbox/callbacks.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "active_support/callbacks" + +module ActionMailbox + # Defines the callbacks related to processing. + module Callbacks + extend ActiveSupport::Concern + include ActiveSupport::Callbacks + + TERMINATOR = ->(mailbox, chain) do + chain.call + mailbox.finished_processing? + end + + included do + define_callbacks :process, terminator: TERMINATOR, skip_after_callbacks_if_terminated: true + end + + class_methods do + def before_processing(*methods, &block) + set_callback(:process, :before, *methods, &block) + end + + def after_processing(*methods, &block) + set_callback(:process, :after, *methods, &block) + end + + def around_processing(*methods, &block) + set_callback(:process, :around, *methods, &block) + end + end + end +end diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb new file mode 100644 index 0000000000..039f04ac2f --- /dev/null +++ b/actionmailbox/lib/action_mailbox/engine.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "rails" +require "action_controller/railtie" +require "active_job/railtie" +require "active_record/railtie" +require "active_storage/engine" + +require "action_mailbox" + +module ActionMailbox + class Engine < Rails::Engine + isolate_namespace ActionMailbox + config.eager_load_namespaces << ActionMailbox + + config.action_mailbox = ActiveSupport::OrderedOptions.new + config.action_mailbox.incinerate = true + config.action_mailbox.incinerate_after = 30.days + + config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \ + incineration: :action_mailbox_incineration, routing: :action_mailbox_routing + + initializer "action_mailbox.config" do + config.after_initialize do |app| + ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger + ActionMailbox.incinerate = app.config.action_mailbox.incinerate.nil? ? true : app.config.action_mailbox.incinerate + ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days + ActionMailbox.queues = app.config.action_mailbox.queues || {} + end + end + + initializer "action_mailbox.ingress" do |app| + config.to_prepare do + if ActionMailbox.ingress = app.config.action_mailbox.ingress.presence + if ingress_controller_class = "ActionMailbox::Ingresses::#{ActionMailbox.ingress.to_s.classify}::InboundEmailsController".safe_constantize + ingress_controller_class.prepare + end + end + end + end + end +end diff --git a/actionmailbox/lib/action_mailbox/gem_version.rb b/actionmailbox/lib/action_mailbox/gem_version.rb new file mode 100644 index 0000000000..a063553471 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/gem_version.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActionMailbox + # Returns the currently-loaded version of Action Mailbox as a <tt>Gem::Version</tt>. + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 6 + MINOR = 0 + TINY = 0 + PRE = "beta3" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/actionmailbox/lib/action_mailbox/mail_ext.rb b/actionmailbox/lib/action_mailbox/mail_ext.rb new file mode 100644 index 0000000000..c4d277a1f9 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/mail_ext.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "mail" + +# The hope is to upstream most of these basic additions to the Mail gem's Mail object. But until then, here they lay! +Dir["#{File.expand_path(File.dirname(__FILE__))}/mail_ext/*"].each { |path| require "action_mailbox/mail_ext/#{File.basename(path)}" } diff --git a/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb b/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb new file mode 100644 index 0000000000..39a43b3468 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Mail + class Address + def ==(other_address) + other_address.is_a?(Mail::Address) && to_s == other_address.to_s + end + end +end diff --git a/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb b/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb new file mode 100644 index 0000000000..19eb624c1c --- /dev/null +++ b/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Mail + class Address + def self.wrap(address) + address.is_a?(Mail::Address) ? address : Mail::Address.new(address) + end + end +end diff --git a/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb b/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb new file mode 100644 index 0000000000..5eab1feb3d --- /dev/null +++ b/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mail + class Message + def from_address + header[:from]&.address_list&.addresses&.first + end + + def recipients_addresses + to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses + end + + def to_addresses + Array(header[:to]&.address_list&.addresses) + end + + def cc_addresses + Array(header[:cc]&.address_list&.addresses) + end + + def bcc_addresses + Array(header[:bcc]&.address_list&.addresses) + end + + def x_original_to_addresses + Array(header[:x_original_to]).collect { |header| Mail::Address.new header.to_s } + end + end +end diff --git a/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb b/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb new file mode 100644 index 0000000000..17b7fc80ad --- /dev/null +++ b/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Mail + def self.from_source(source) + Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s) + end +end diff --git a/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb b/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb new file mode 100644 index 0000000000..1f8a713218 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Mail + class Message + def recipients + Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s) + end + end +end diff --git a/actionmailbox/lib/action_mailbox/relayer.rb b/actionmailbox/lib/action_mailbox/relayer.rb new file mode 100644 index 0000000000..e2890acb60 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/relayer.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "action_mailbox/version" +require "net/http" +require "uri" + +module ActionMailbox + class Relayer + class Result < Struct.new(:status_code, :message) + def success? + !failure? + end + + def failure? + transient_failure? || permanent_failure? + end + + def transient_failure? + status_code.start_with?("4.") + end + + def permanent_failure? + status_code.start_with?("5.") + end + end + + CONTENT_TYPE = "message/rfc822" + USER_AGENT = "Action Mailbox relayer v#{ActionMailbox.version}" + + attr_reader :uri, :username, :password + + def initialize(url:, username: "actionmailbox", password:) + @uri, @username, @password = URI(url), username, password + end + + def relay(source) + case response = post(source) + when Net::HTTPSuccess + Result.new "2.0.0", "Successfully relayed message to ingress" + when Net::HTTPUnauthorized + Result.new "4.7.0", "Invalid credentials for ingress" + else + Result.new "4.0.0", "HTTP #{response.code}" + end + rescue IOError, SocketError, SystemCallError => error + Result.new "4.4.2", "Network error relaying to ingress: #{error.message}" + rescue Timeout::Error + Result.new "4.4.2", "Timed out relaying to ingress" + rescue => error + Result.new "4.0.0", "Error relaying to ingress: #{error.message}" + end + + private + def post(source) + client.post uri, source, + "Content-Type" => CONTENT_TYPE, + "User-Agent" => USER_AGENT, + "Authorization" => "Basic #{Base64.strict_encode64(username + ":" + password)}" + end + + def client + @client ||= Net::HTTP.new(uri.host, uri.port).tap do |connection| + if uri.scheme == "https" + require "openssl" + + connection.use_ssl = true + connection.verify_mode = OpenSSL::SSL::VERIFY_PEER + end + + connection.open_timeout = 1 + connection.read_timeout = 10 + end + end + end +end diff --git a/actionmailbox/lib/action_mailbox/router.rb b/actionmailbox/lib/action_mailbox/router.rb new file mode 100644 index 0000000000..71370e409d --- /dev/null +++ b/actionmailbox/lib/action_mailbox/router.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module ActionMailbox + # Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when + # an inbound_email is received. + class Router + class RoutingError < StandardError; end + + def initialize + @routes = [] + end + + def add_routes(routes) + routes.each do |(address, mailbox_name)| + add_route address, to: mailbox_name + end + end + + def add_route(address, to:) + routes.append Route.new(address, to: to) + end + + def route(inbound_email) + if mailbox = match_to_mailbox(inbound_email) + mailbox.receive(inbound_email) + else + inbound_email.bounced! + + raise RoutingError + end + end + + private + attr_reader :routes + + def match_to_mailbox(inbound_email) + routes.detect { |route| route.match?(inbound_email) }.try(:mailbox_class) + end + end +end + +require "action_mailbox/router/route" diff --git a/actionmailbox/lib/action_mailbox/router/route.rb b/actionmailbox/lib/action_mailbox/router/route.rb new file mode 100644 index 0000000000..7e98e83382 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/router/route.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module ActionMailbox + # Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching + # mailbox class. See examples for the different route addresses and how to use them in the +ActionMailbox::Base+ + # documentation. + class Router::Route + attr_reader :address, :mailbox_name + + def initialize(address, to:) + @address, @mailbox_name = address, to + + ensure_valid_address + end + + def match?(inbound_email) + case address + when :all + true + when String + inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) } + when Regexp + inbound_email.mail.recipients.any? { |recipient| address.match?(recipient) } + when Proc + address.call(inbound_email) + else + address.match?(inbound_email) + end + end + + def mailbox_class + "#{mailbox_name.to_s.camelize}Mailbox".constantize + end + + private + def ensure_valid_address + unless [ Symbol, String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?) + raise ArgumentError, "Expected a Symbol, String, Regexp, Proc, or matchable, got #{address.inspect}" + end + end + end +end diff --git a/actionmailbox/lib/action_mailbox/routing.rb b/actionmailbox/lib/action_mailbox/routing.rb new file mode 100644 index 0000000000..58462a44c6 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/routing.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module ActionMailbox + # See +ActionMailbox::Base+ for how to specify routing. + module Routing + extend ActiveSupport::Concern + + included do + cattr_accessor :router, default: ActionMailbox::Router.new + end + + class_methods do + def routing(routes) + router.add_routes(routes) + end + + def route(inbound_email) + router.route(inbound_email) + end + end + end +end diff --git a/actionmailbox/lib/action_mailbox/test_case.rb b/actionmailbox/lib/action_mailbox/test_case.rb new file mode 100644 index 0000000000..5e78e428d3 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/test_case.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "action_mailbox/test_helper" +require "active_support/test_case" + +module ActionMailbox + class TestCase < ActiveSupport::TestCase + include ActionMailbox::TestHelper + end +end + +ActiveSupport.run_load_hooks :action_mailbox_test_case, ActionMailbox::TestCase diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb new file mode 100644 index 0000000000..0ec9152844 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/test_helper.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "mail" + +module ActionMailbox + module TestHelper + # Create an +InboundEmail+ record using an eml fixture in the format of message/rfc822 + # referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+. + def create_inbound_email_from_fixture(fixture_name, status: :processing) + create_inbound_email_from_source file_fixture(fixture_name).read, status: status + end + + # Create an +InboundEmail+ by specifying it using +Mail.new+ options. Example: + # + # create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!") + def create_inbound_email_from_mail(status: :processing, **mail_options) + create_inbound_email_from_source Mail.new(mail_options).to_s, status: status + end + + # Create an +InboundEmail+ using the raw rfc822 +source+ as text. + def create_inbound_email_from_source(source, status: :processing) + ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status + end + + + # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_fixture+ + # and immediately route it to processing. + def receive_inbound_email_from_fixture(*args) + create_inbound_email_from_fixture(*args).tap(&:route) + end + + # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_mail+ + # and immediately route it to processing. + def receive_inbound_email_from_mail(**kwargs) + create_inbound_email_from_mail(**kwargs).tap(&:route) + end + + # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_source+ + # and immediately route it to processing. + def receive_inbound_email_from_source(**kwargs) + create_inbound_email_from_source(**kwargs).tap(&:route) + end + end +end diff --git a/actionmailbox/lib/action_mailbox/version.rb b/actionmailbox/lib/action_mailbox/version.rb new file mode 100644 index 0000000000..e65d27f5dd --- /dev/null +++ b/actionmailbox/lib/action_mailbox/version.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative "gem_version" + +module ActionMailbox + # Returns the currently-loaded version of Action Mailbox as a <tt>Gem::Version</tt>. + def self.version + gem_version + end +end diff --git a/actionmailbox/lib/rails/generators/installer.rb b/actionmailbox/lib/rails/generators/installer.rb new file mode 100644 index 0000000000..25cf528ef5 --- /dev/null +++ b/actionmailbox/lib/rails/generators/installer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +say "Copying application_mailbox.rb to app/mailboxes" +copy_file "#{__dir__}/mailbox/templates/application_mailbox.rb", "app/mailboxes/application_mailbox.rb" + +environment <<~end_of_config, env: "production" + # Prepare the ingress controller used to receive mail + # config.action_mailbox.ingress = :amazon + +end_of_config diff --git a/actionmailbox/lib/rails/generators/mailbox/USAGE b/actionmailbox/lib/rails/generators/mailbox/USAGE new file mode 100644 index 0000000000..d679dd63ae --- /dev/null +++ b/actionmailbox/lib/rails/generators/mailbox/USAGE @@ -0,0 +1,12 @@ +Description: +============ + Stubs out a new mailbox class in app/mailboxes and invokes your template + engine and test framework generators. + +Example: +======== + rails generate mailbox inbox + + creates a InboxMailbox class and test: + Mailbox: app/mailboxes/inbox_mailbox.rb + Test: test/mailboxes/inbox_mailbox_test.rb diff --git a/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb b/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb new file mode 100644 index 0000000000..c2c403b8f6 --- /dev/null +++ b/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Rails + module Generators + class MailboxGenerator < NamedBase + source_root File.expand_path("templates", __dir__) + + check_class_collision suffix: "Mailbox" + + def create_mailbox_file + template "mailbox.rb", File.join("app/mailboxes", class_path, "#{file_name}_mailbox.rb") + + in_root do + if behavior == :invoke && !File.exist?(application_mailbox_file_name) + template "application_mailbox.rb", application_mailbox_file_name + end + end + end + + hook_for :test_framework + + private + def file_name # :doc: + @_file_name ||= super.sub(/_mailbox\z/i, "") + end + + def application_mailbox_file_name + "app/mailboxes/application_mailbox.rb" + end + end + end +end diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt new file mode 100644 index 0000000000..ac22d03cd2 --- /dev/null +++ b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt @@ -0,0 +1,3 @@ +class ApplicationMailbox < ActionMailbox::Base + # routing /something/i => :somewhere +end diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt new file mode 100644 index 0000000000..110b3b9d7e --- /dev/null +++ b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt @@ -0,0 +1,4 @@ +class <%= class_name %>Mailbox < ApplicationMailbox + def process + end +end diff --git a/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb b/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb new file mode 100644 index 0000000000..2ec7d11a2f --- /dev/null +++ b/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module TestUnit + module Generators + class MailboxGenerator < ::Rails::Generators::NamedBase + source_root File.expand_path("templates", __dir__) + + check_class_collision suffix: "MailboxTest" + + def create_test_files + template "mailbox_test.rb", File.join("test/mailboxes", class_path, "#{file_name}_mailbox_test.rb") + end + + private + def file_name # :doc: + @_file_name ||= super.sub(/_mailbox\z/i, "") + end + end + end +end diff --git a/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt b/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt new file mode 100644 index 0000000000..0b51f29fe4 --- /dev/null +++ b/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "test_helper" + +class <%= class_name %>MailboxTest < ActionMailbox::TestCase + # test "receive mail" do + # receive_inbound_email_from_mail \ + # to: '"someone" <someone@example.com>', + # from: '"else" <else@example.com>', + # subject: "Hello world!", + # body: "Hello?" + # end +end diff --git a/actionmailbox/lib/tasks/ingress.rake b/actionmailbox/lib/tasks/ingress.rake new file mode 100644 index 0000000000..43b613ea12 --- /dev/null +++ b/actionmailbox/lib/tasks/ingress.rake @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +namespace :action_mailbox do + namespace :ingress do + task :environment do + require "active_support" + require "active_support/core_ext/object/blank" + require "action_mailbox/relayer" + end + + desc "Relay an inbound email from Exim to Action Mailbox (URL and INGRESS_PASSWORD required)" + task exim: "action_mailbox:ingress:environment" do + url, password = ENV.values_at("URL", "INGRESS_PASSWORD") + + if url.blank? || password.blank? + print "URL and INGRESS_PASSWORD are required" + exit 64 # EX_USAGE + end + + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print result.message + + case + when result.success? + exit 0 + when result.transient_failure? + exit 75 # EX_TEMPFAIL + else + exit 69 # EX_UNAVAILABLE + end + end + end + + desc "Relay an inbound email from Postfix to Action Mailbox (URL and INGRESS_PASSWORD required)" + task postfix: "action_mailbox:ingress:environment" do + url, password = ENV.values_at("URL", "INGRESS_PASSWORD") + + if url.blank? || password.blank? + print "4.3.5 URL and INGRESS_PASSWORD are required" + exit 1 + end + + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print "#{result.status_code} #{result.message}" + exit result.success? + end + end + + desc "Relay an inbound email from Qmail to Action Mailbox (URL and INGRESS_PASSWORD required)" + task qmail: "action_mailbox:ingress:environment" do + url, password = ENV.values_at("URL", "INGRESS_PASSWORD") + + if url.blank? || password.blank? + print "URL and INGRESS_PASSWORD are required" + exit 111 + end + + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print result.message + + case + when result.success? + exit 0 + when result.transient_failure? + exit 111 + else + exit 100 + end + end + end + end +end diff --git a/actionmailbox/lib/tasks/install.rake b/actionmailbox/lib/tasks/install.rake new file mode 100644 index 0000000000..0885e2d6a5 --- /dev/null +++ b/actionmailbox/lib/tasks/install.rake @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +namespace :action_mailbox do + # Prevent migration installation task from showing up twice. + Rake::Task["install:migrations"].clear_comments + + desc "Copy over the migration" + task install: %w[ environment run_installer copy_migrations ] + + task :run_installer do + installer_template = File.expand_path("../rails/generators/installer.rb", __dir__) + system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{installer_template}" + end + + task :copy_migrations do + Rake::Task["active_storage:install:migrations"].invoke + Rake::Task["railties:install:migrations"].reenable # Otherwise you can't run 2 migration copy tasks in one invocation + Rake::Task["action_mailbox:install:migrations"].invoke + end +end |