From 4ab739a72a30911e8a72abeb19428434cbc03a2e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 16:30:33 -0700 Subject: Skeleton --- Gemfile | 4 ++++ LICENSE | 21 +++++++++++++++++++++ README.md | 3 +++ Rakefile | 27 +++++++++++++++++++++++++++ actionmailbox.gemspec | 24 ++++++++++++++++++++++++ lib/action_mailbox.rb | 7 +++++++ lib/action_mailbox/base.rb | 2 ++ lib/action_mailbox/engine.rb | 14 ++++++++++++++ lib/action_mailbox/version.rb | 3 +++ lib/tasks/action_mailbox.rake | 30 ++++++++++++++++++++++++++++++ test/test_helper.rb | 25 +++++++++++++++++++++++++ 11 files changed, 160 insertions(+) create mode 100644 Gemfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Rakefile create mode 100644 actionmailbox.gemspec create mode 100644 lib/action_mailbox.rb create mode 100644 lib/action_mailbox/base.rb create mode 100644 lib/action_mailbox/engine.rb create mode 100644 lib/action_mailbox/version.rb create mode 100644 lib/tasks/action_mailbox.rake create mode 100644 test/test_helper.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..9b78acc762 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" +git_source(:github) { |repo_path| "https://github.com/#{repo_path}.git" } + +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..4a5fe6361d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Basecamp, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..b92e467644 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Action Mailbox + +📬 \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..a2fd477252 --- /dev/null +++ b/Rakefile @@ -0,0 +1,27 @@ +begin + require 'bundler/setup' +rescue LoadError + puts 'You must `gem install bundler` and `bundle install` to run rake tasks' +end + +require 'rdoc/task' + +RDoc::Task.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Action Mailbox' + rdoc.options << '--line-numbers' + rdoc.rdoc_files.include('README.md') + rdoc.rdoc_files.include('lib/**/*.rb') +end + +require 'bundler/gem_tasks' + +require 'rake/testtask' + +Rake::TestTask.new(:test) do |t| + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = false +end + +task default: :test diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec new file mode 100644 index 0000000000..1fdfff7b00 --- /dev/null +++ b/actionmailbox.gemspec @@ -0,0 +1,24 @@ +$:.push File.expand_path("lib", __dir__) + +# Maintain your gem's version: +require "action_mailbox/version" + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = "actionmailbox" + s.version = ActionText::VERSION + s.authors = ["Jeremy Daer", "David Heinemeier Hansson"] + s.email = ["jeremy@basecamp.com", "david@loudthinking.com"] + s.summary = "Receive and process incoming emails in Rails" + s.homepage = "https://github.com/basecamp/actionmailbox" + s.license = "MIT" + + s.required_ruby_version = ">= 2.5.0" + + s.add_dependency "rails", ">= 5.2.0" + + s.add_development_dependency "bundler", "~> 1.15" + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- test/*`.split("\n") +end diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb new file mode 100644 index 0000000000..c4ed71d5cc --- /dev/null +++ b/lib/action_mailbox.rb @@ -0,0 +1,7 @@ +require "action_mailbox/engine" + +module ActionMailbox + extend ActiveSupport::Autoload + + autoload :Base +end diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb new file mode 100644 index 0000000000..75098875eb --- /dev/null +++ b/lib/action_mailbox/base.rb @@ -0,0 +1,2 @@ +class ActionMailbox::Base +end diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb new file mode 100644 index 0000000000..92852a0fa3 --- /dev/null +++ b/lib/action_mailbox/engine.rb @@ -0,0 +1,14 @@ +require "rails/engine" + +module ActionMailbox + class Engine < Rails::Engine + isolate_namespace ActionMailbox + config.eager_load_namespaces << ActionMailbox + + initializer "action_mailbox.config" do + config.after_initialize do |app| + # Configure + end + end + end +end diff --git a/lib/action_mailbox/version.rb b/lib/action_mailbox/version.rb new file mode 100644 index 0000000000..23c615dbbd --- /dev/null +++ b/lib/action_mailbox/version.rb @@ -0,0 +1,3 @@ +module ActionMailbox + VERSION = '0.1.0' +end diff --git a/lib/tasks/action_mailbox.rake b/lib/tasks/action_mailbox.rake new file mode 100644 index 0000000000..58dd59d9eb --- /dev/null +++ b/lib/tasks/action_mailbox.rake @@ -0,0 +1,30 @@ +# 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 and fixtures" + task install: %w( environment active_storage:install copy_migration copy_fixtures ) + + task :copy_migration do + if Rake::Task.task_defined?("action_mailbox:install:migrations") + Rake::Task["action_mailbox:install:migrations"].invoke + else + Rake::Task["app:action_mailbox:install:migrations"].invoke + end + end + + FIXTURE_TEMPLATE_PATH = File.expand_path("../templates/fixtures.yml", __dir__) + FIXTURE_APP_DIR_PATH = Rails.root.join("test/fixtures/action_mailbox") + FIXTURE_APP_PATH = FIXTURE_APP_DIR_PATH.join("inbound_emails.yml") + + task :copy_fixtures do + if File.exist?(FIXTURE_APP_PATH) + puts "Won't copy Action Mailbox fixtures as it already exists" + else + FileUtils.mkdir FIXTURE_APP_DIR_PATH + FileUtils.cp FIXTURE_TEMPLATE_PATH, FIXTURE_APP_PATH + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000000..81d14de111 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,25 @@ +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + +require_relative "../test/dummy/config/environment" +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] +require "rails/test_help" + +require "rails/test_unit/reporter" +Rails::TestUnitReporter.executable = 'bin/test' + +# Load fixtures from the engine +if ActiveSupport::TestCase.respond_to?(:fixture_path=) + ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) + ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path + ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" + ActiveSupport::TestCase.fixtures :all +end + +class ActiveSupport::TestCase + private + def create_file_blob(filename:, content_type:, metadata: nil) + ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, + filename: filename, content_type: content_type, metadata: metadata + end +end -- cgit v1.2.3 From 52b1e2c6cfd5e76d72a6de09b28e51f2440c48c4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 16:51:38 -0700 Subject: Copypasta is delicious! --- actionmailbox.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec index 1fdfff7b00..699ad4d1b8 100644 --- a/actionmailbox.gemspec +++ b/actionmailbox.gemspec @@ -6,7 +6,7 @@ require "action_mailbox/version" # Describe your gem and declare its dependencies: Gem::Specification.new do |s| s.name = "actionmailbox" - s.version = ActionText::VERSION + s.version = ActionMailbox::VERSION s.authors = ["Jeremy Daer", "David Heinemeier Hansson"] s.email = ["jeremy@basecamp.com", "david@loudthinking.com"] s.summary = "Receive and process incoming emails in Rails" -- cgit v1.2.3 From 627bbd34e142fa7caff49fd660a9a586f3ed6826 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 16:55:07 -0700 Subject: Add inbound email --- app/models/action_mailbox/inbound_email.rb | 17 +++++++++++++++++ .../action_mailbox/deliver_inbound_email_to_mailbox.rb | 10 ++++++++++ .../20180917164000_create_action_mailbox_tables.rb | 10 ++++++++++ 3 files changed, 37 insertions(+) create mode 100644 app/models/action_mailbox/inbound_email.rb create mode 100644 app/models/jobs/action_mailbox/deliver_inbound_email_to_mailbox.rb create mode 100644 db/migrate/20180917164000_create_action_mailbox_tables.rb diff --git a/app/models/action_mailbox/inbound_email.rb b/app/models/action_mailbox/inbound_email.rb new file mode 100644 index 0000000000..3e528b6642 --- /dev/null +++ b/app/models/action_mailbox/inbound_email.rb @@ -0,0 +1,17 @@ +class ActionMailbox::InboundEmail < ActiveRecord::Base + self.table_name = "action_mailbox_inbound_email" + + after_create_commit :deliver_to_mailroom_later + has_one_attached :raw_message + + enum status: %i[ pending processing delivered failed bounced ] + + def mail + @mail ||= Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_message.download)) + end + + private + def deliver_to_mailroom_later + ActionMailbox::DeliverInboundEmailToMailroomJob.perform_later self + end +end diff --git a/app/models/jobs/action_mailbox/deliver_inbound_email_to_mailbox.rb b/app/models/jobs/action_mailbox/deliver_inbound_email_to_mailbox.rb new file mode 100644 index 0000000000..a5bd2c0f18 --- /dev/null +++ b/app/models/jobs/action_mailbox/deliver_inbound_email_to_mailbox.rb @@ -0,0 +1,10 @@ +class ActionMailbox::DeliverInboundEmailToMailroomJob < ApplicationJob + queue_as :action_mailbox_inbound_email + + # Occasional `SSL_read: decryption failed or bad record mac` that resolve on retry + retry_on OpenSSL::SSL::SSLError + + def perform(inbound_email) + ApplicationMailbox.receive inbound_email + end +end diff --git a/db/migrate/20180917164000_create_action_mailbox_tables.rb b/db/migrate/20180917164000_create_action_mailbox_tables.rb new file mode 100644 index 0000000000..c2ba8f63b7 --- /dev/null +++ b/db/migrate/20180917164000_create_action_mailbox_tables.rb @@ -0,0 +1,10 @@ +class CreateActionMailboxTables < ActiveRecord::Migration[5.2] + def change + create_table :action_mailbox_inbound_emails do |t| + t.integer :status, default: 0, null: false + + t.datetime :created_at, precision: 6 + t.datetime :updated_at, precision: 6 + end + end +end -- cgit v1.2.3 From 82ff0c235bdddccef76c2aa28a9a4d005dcd918b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 16:56:55 -0700 Subject: Action Mailbox -> Action Mailroom --- README.md | 2 +- actionmailbox.gemspec | 24 ----------------- actionmailroom.gemspec | 24 +++++++++++++++++ app/models/action_mailbox/inbound_email.rb | 17 ------------ app/models/action_mailroom/inbound_email.rb | 17 ++++++++++++ .../deliver_inbound_email_to_mailbox.rb | 10 -------- .../deliver_inbound_email_to_mailbox.rb | 10 ++++++++ .../20180917164000_create_action_mailbox_tables.rb | 4 +-- lib/action_mailbox.rb | 7 ----- lib/action_mailbox/base.rb | 2 -- lib/action_mailbox/engine.rb | 14 ---------- lib/action_mailbox/version.rb | 3 --- lib/action_mailroom.rb | 7 +++++ lib/action_mailroom/base.rb | 2 ++ lib/action_mailroom/engine.rb | 14 ++++++++++ lib/action_mailroom/version.rb | 3 +++ lib/tasks/action_mailbox.rake | 30 ---------------------- lib/tasks/action_mailroom.rake | 30 ++++++++++++++++++++++ 18 files changed, 110 insertions(+), 110 deletions(-) delete mode 100644 actionmailbox.gemspec create mode 100644 actionmailroom.gemspec delete mode 100644 app/models/action_mailbox/inbound_email.rb create mode 100644 app/models/action_mailroom/inbound_email.rb delete mode 100644 app/models/jobs/action_mailbox/deliver_inbound_email_to_mailbox.rb create mode 100644 app/models/jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb delete mode 100644 lib/action_mailbox.rb delete mode 100644 lib/action_mailbox/base.rb delete mode 100644 lib/action_mailbox/engine.rb delete mode 100644 lib/action_mailbox/version.rb create mode 100644 lib/action_mailroom.rb create mode 100644 lib/action_mailroom/base.rb create mode 100644 lib/action_mailroom/engine.rb create mode 100644 lib/action_mailroom/version.rb delete mode 100644 lib/tasks/action_mailbox.rake create mode 100644 lib/tasks/action_mailroom.rake diff --git a/README.md b/README.md index b92e467644..b3f91f3018 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# Action Mailbox +# Action Mailroom 📬 \ No newline at end of file diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec deleted file mode 100644 index 699ad4d1b8..0000000000 --- a/actionmailbox.gemspec +++ /dev/null @@ -1,24 +0,0 @@ -$:.push File.expand_path("lib", __dir__) - -# Maintain your gem's version: -require "action_mailbox/version" - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "actionmailbox" - s.version = ActionMailbox::VERSION - s.authors = ["Jeremy Daer", "David Heinemeier Hansson"] - s.email = ["jeremy@basecamp.com", "david@loudthinking.com"] - s.summary = "Receive and process incoming emails in Rails" - s.homepage = "https://github.com/basecamp/actionmailbox" - s.license = "MIT" - - s.required_ruby_version = ">= 2.5.0" - - s.add_dependency "rails", ">= 5.2.0" - - s.add_development_dependency "bundler", "~> 1.15" - - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- test/*`.split("\n") -end diff --git a/actionmailroom.gemspec b/actionmailroom.gemspec new file mode 100644 index 0000000000..ef9b215ff4 --- /dev/null +++ b/actionmailroom.gemspec @@ -0,0 +1,24 @@ +$:.push File.expand_path("lib", __dir__) + +# Maintain your gem's version: +require "action_mailroom/version" + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = "actionmailroom" + s.version = ActionMailroom::VERSION + s.authors = ["Jeremy Daer", "David Heinemeier Hansson"] + s.email = ["jeremy@basecamp.com", "david@loudthinking.com"] + s.summary = "Receive and process incoming emails in Rails" + s.homepage = "https://github.com/basecamp/actionmailroom" + s.license = "MIT" + + s.required_ruby_version = ">= 2.5.0" + + s.add_dependency "rails", ">= 5.2.0" + + s.add_development_dependency "bundler", "~> 1.15" + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- test/*`.split("\n") +end diff --git a/app/models/action_mailbox/inbound_email.rb b/app/models/action_mailbox/inbound_email.rb deleted file mode 100644 index 3e528b6642..0000000000 --- a/app/models/action_mailbox/inbound_email.rb +++ /dev/null @@ -1,17 +0,0 @@ -class ActionMailbox::InboundEmail < ActiveRecord::Base - self.table_name = "action_mailbox_inbound_email" - - after_create_commit :deliver_to_mailroom_later - has_one_attached :raw_message - - enum status: %i[ pending processing delivered failed bounced ] - - def mail - @mail ||= Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_message.download)) - end - - private - def deliver_to_mailroom_later - ActionMailbox::DeliverInboundEmailToMailroomJob.perform_later self - end -end diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb new file mode 100644 index 0000000000..cb39967e09 --- /dev/null +++ b/app/models/action_mailroom/inbound_email.rb @@ -0,0 +1,17 @@ +class ActionMailroom::InboundEmail < ActiveRecord::Base + self.table_name = "action_mailroom_inbound_email" + + after_create_commit :deliver_to_mailroom_later + has_one_attached :raw_message + + enum status: %i[ pending processing delivered failed bounced ] + + def mail + @mail ||= Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_message.download)) + end + + private + def deliver_to_mailroom_later + ActionMailroom::DeliverInboundEmailToMailroomJob.perform_later self + end +end diff --git a/app/models/jobs/action_mailbox/deliver_inbound_email_to_mailbox.rb b/app/models/jobs/action_mailbox/deliver_inbound_email_to_mailbox.rb deleted file mode 100644 index a5bd2c0f18..0000000000 --- a/app/models/jobs/action_mailbox/deliver_inbound_email_to_mailbox.rb +++ /dev/null @@ -1,10 +0,0 @@ -class ActionMailbox::DeliverInboundEmailToMailroomJob < ApplicationJob - queue_as :action_mailbox_inbound_email - - # Occasional `SSL_read: decryption failed or bad record mac` that resolve on retry - retry_on OpenSSL::SSL::SSLError - - def perform(inbound_email) - ApplicationMailbox.receive inbound_email - end -end diff --git a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb new file mode 100644 index 0000000000..954fc4bd71 --- /dev/null +++ b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb @@ -0,0 +1,10 @@ +class ActionMailroom::DeliverInboundEmailToMailroomJob < ApplicationJob + queue_as :action_mailroom_inbound_email + + # Occasional `SSL_read: decryption failed or bad record mac` that resolve on retry + retry_on OpenSSL::SSL::SSLError + + def perform(inbound_email) + ApplicationMailbox.receive inbound_email + end +end diff --git a/db/migrate/20180917164000_create_action_mailbox_tables.rb b/db/migrate/20180917164000_create_action_mailbox_tables.rb index c2ba8f63b7..f488919138 100644 --- a/db/migrate/20180917164000_create_action_mailbox_tables.rb +++ b/db/migrate/20180917164000_create_action_mailbox_tables.rb @@ -1,6 +1,6 @@ -class CreateActionMailboxTables < ActiveRecord::Migration[5.2] +class CreateActionMailroomTables < ActiveRecord::Migration[5.2] def change - create_table :action_mailbox_inbound_emails do |t| + create_table :action_mailroom_inbound_emails do |t| t.integer :status, default: 0, null: false t.datetime :created_at, precision: 6 diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb deleted file mode 100644 index c4ed71d5cc..0000000000 --- a/lib/action_mailbox.rb +++ /dev/null @@ -1,7 +0,0 @@ -require "action_mailbox/engine" - -module ActionMailbox - extend ActiveSupport::Autoload - - autoload :Base -end diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb deleted file mode 100644 index 75098875eb..0000000000 --- a/lib/action_mailbox/base.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ActionMailbox::Base -end diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb deleted file mode 100644 index 92852a0fa3..0000000000 --- a/lib/action_mailbox/engine.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "rails/engine" - -module ActionMailbox - class Engine < Rails::Engine - isolate_namespace ActionMailbox - config.eager_load_namespaces << ActionMailbox - - initializer "action_mailbox.config" do - config.after_initialize do |app| - # Configure - end - end - end -end diff --git a/lib/action_mailbox/version.rb b/lib/action_mailbox/version.rb deleted file mode 100644 index 23c615dbbd..0000000000 --- a/lib/action_mailbox/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module ActionMailbox - VERSION = '0.1.0' -end diff --git a/lib/action_mailroom.rb b/lib/action_mailroom.rb new file mode 100644 index 0000000000..39fd1ea2e1 --- /dev/null +++ b/lib/action_mailroom.rb @@ -0,0 +1,7 @@ +require "action_mailroom/engine" + +module ActionMailroom + extend ActiveSupport::Autoload + + autoload :Base +end diff --git a/lib/action_mailroom/base.rb b/lib/action_mailroom/base.rb new file mode 100644 index 0000000000..0fbe6d0a92 --- /dev/null +++ b/lib/action_mailroom/base.rb @@ -0,0 +1,2 @@ +class ActionMailroom::Base +end diff --git a/lib/action_mailroom/engine.rb b/lib/action_mailroom/engine.rb new file mode 100644 index 0000000000..6a8d4c23c0 --- /dev/null +++ b/lib/action_mailroom/engine.rb @@ -0,0 +1,14 @@ +require "rails/engine" + +module ActionMailroom + class Engine < Rails::Engine + isolate_namespace ActionMailroom + config.eager_load_namespaces << ActionMailroom + + initializer "action_mailroom.config" do + config.after_initialize do |app| + # Configure + end + end + end +end diff --git a/lib/action_mailroom/version.rb b/lib/action_mailroom/version.rb new file mode 100644 index 0000000000..64a9d8eacd --- /dev/null +++ b/lib/action_mailroom/version.rb @@ -0,0 +1,3 @@ +module ActionMailroom + VERSION = '0.1.0' +end diff --git a/lib/tasks/action_mailbox.rake b/lib/tasks/action_mailbox.rake deleted file mode 100644 index 58dd59d9eb..0000000000 --- a/lib/tasks/action_mailbox.rake +++ /dev/null @@ -1,30 +0,0 @@ -# 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 and fixtures" - task install: %w( environment active_storage:install copy_migration copy_fixtures ) - - task :copy_migration do - if Rake::Task.task_defined?("action_mailbox:install:migrations") - Rake::Task["action_mailbox:install:migrations"].invoke - else - Rake::Task["app:action_mailbox:install:migrations"].invoke - end - end - - FIXTURE_TEMPLATE_PATH = File.expand_path("../templates/fixtures.yml", __dir__) - FIXTURE_APP_DIR_PATH = Rails.root.join("test/fixtures/action_mailbox") - FIXTURE_APP_PATH = FIXTURE_APP_DIR_PATH.join("inbound_emails.yml") - - task :copy_fixtures do - if File.exist?(FIXTURE_APP_PATH) - puts "Won't copy Action Mailbox fixtures as it already exists" - else - FileUtils.mkdir FIXTURE_APP_DIR_PATH - FileUtils.cp FIXTURE_TEMPLATE_PATH, FIXTURE_APP_PATH - end - end -end diff --git a/lib/tasks/action_mailroom.rake b/lib/tasks/action_mailroom.rake new file mode 100644 index 0000000000..ce80fcf55e --- /dev/null +++ b/lib/tasks/action_mailroom.rake @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +namespace :action_mailroom do + # Prevent migration installation task from showing up twice. + Rake::Task["install:migrations"].clear_comments + + desc "Copy over the migration and fixtures" + task install: %w( environment active_storage:install copy_migration copy_fixtures ) + + task :copy_migration do + if Rake::Task.task_defined?("action_mailroom:install:migrations") + Rake::Task["action_mailroom:install:migrations"].invoke + else + Rake::Task["app:action_mailroom:install:migrations"].invoke + end + end + + FIXTURE_TEMPLATE_PATH = File.expand_path("../templates/fixtures.yml", __dir__) + FIXTURE_APP_DIR_PATH = Rails.root.join("test/fixtures/action_mailroom") + FIXTURE_APP_PATH = FIXTURE_APP_DIR_PATH.join("inbound_emails.yml") + + task :copy_fixtures do + if File.exist?(FIXTURE_APP_PATH) + puts "Won't copy Action Mailbox fixtures as it already exists" + else + FileUtils.mkdir FIXTURE_APP_DIR_PATH + FileUtils.cp FIXTURE_TEMPLATE_PATH, FIXTURE_APP_PATH + end + end +end -- cgit v1.2.3 From 6600155aeed3d595f4dc263976d88302fc188b85 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 17:01:52 -0700 Subject: Base -> Mailbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Won't need Base and can separate routing from execution 👍 --- lib/action_mailroom.rb | 2 +- lib/action_mailroom/base.rb | 2 -- lib/action_mailroom/mailbox.rb | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 lib/action_mailroom/base.rb create mode 100644 lib/action_mailroom/mailbox.rb diff --git a/lib/action_mailroom.rb b/lib/action_mailroom.rb index 39fd1ea2e1..5dc0d5a11c 100644 --- a/lib/action_mailroom.rb +++ b/lib/action_mailroom.rb @@ -3,5 +3,5 @@ require "action_mailroom/engine" module ActionMailroom extend ActiveSupport::Autoload - autoload :Base + autoload :Mailbox end diff --git a/lib/action_mailroom/base.rb b/lib/action_mailroom/base.rb deleted file mode 100644 index 0fbe6d0a92..0000000000 --- a/lib/action_mailroom/base.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ActionMailroom::Base -end diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb new file mode 100644 index 0000000000..e19e6d7c6d --- /dev/null +++ b/lib/action_mailroom/mailbox.rb @@ -0,0 +1,2 @@ +class ActionMailroom::Mailbox +end -- cgit v1.2.3 From 802bb8a452e1969e15683a1f7599e3ffc580215f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 17:15:53 -0700 Subject: Plural table name --- app/models/action_mailroom/inbound_email.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index cb39967e09..f280bc192c 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -1,5 +1,5 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base - self.table_name = "action_mailroom_inbound_email" + self.table_name = "action_mailroom_inbound_emails" after_create_commit :deliver_to_mailroom_later has_one_attached :raw_message -- cgit v1.2.3 From a4c0c022d3e07fea5b573bf1b90851e79ad87ac9 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 17:16:06 -0700 Subject: TOC ordering --- app/models/action_mailroom/inbound_email.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index f280bc192c..755d7be138 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -1,11 +1,13 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base self.table_name = "action_mailroom_inbound_emails" - after_create_commit :deliver_to_mailroom_later has_one_attached :raw_message enum status: %i[ pending processing delivered failed bounced ] + after_create_commit :deliver_to_mailroom_later + + def mail @mail ||= Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_message.download)) end -- cgit v1.2.3 From 7dc484a553c0dabfd4b460d5c3e8f56c8e8ac5a6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 17:16:44 -0700 Subject: Full name for the job --- .../jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb | 10 ---------- .../action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 10 ++++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 app/models/jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb create mode 100644 app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb diff --git a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb deleted file mode 100644 index 954fc4bd71..0000000000 --- a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailbox.rb +++ /dev/null @@ -1,10 +0,0 @@ -class ActionMailroom::DeliverInboundEmailToMailroomJob < ApplicationJob - queue_as :action_mailroom_inbound_email - - # Occasional `SSL_read: decryption failed or bad record mac` that resolve on retry - retry_on OpenSSL::SSL::SSLError - - def perform(inbound_email) - ApplicationMailbox.receive inbound_email - end -end diff --git a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb new file mode 100644 index 0000000000..55b87288a8 --- /dev/null +++ b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb @@ -0,0 +1,10 @@ +class ActionMailroom::DeliverInboundEmailToMailroomJob < ApplicationJob + queue_as :action_mailroom_inbound_email + + # Occasional `SSL_read: decryption failed or bad record mac` that resolve on retry + retry_on OpenSSL::SSL::SSLError + + def perform(inbound_email) + ActionMailroom::Router.receive inbound_email + end +end -- cgit v1.2.3 From 12deb313e02680bbd1c07ada79a890d6734d1bf3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 17:16:51 -0700 Subject: Wait on this for later --- .../jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb index 55b87288a8..5a78ebd4b2 100644 --- a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb +++ b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb @@ -1,9 +1,6 @@ class ActionMailroom::DeliverInboundEmailToMailroomJob < ApplicationJob queue_as :action_mailroom_inbound_email - # Occasional `SSL_read: decryption failed or bad record mac` that resolve on retry - retry_on OpenSSL::SSL::SSLError - def perform(inbound_email) ActionMailroom::Router.receive inbound_email end -- cgit v1.2.3 From 03b18410071f4e8a9a2daff13e02881e5f1a1c12 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 17:48:32 -0700 Subject: Add dummy app for testing --- Gemfile.lock | 128 + actionmailroom.gemspec | 1 + test/dummy/.babelrc | 18 + test/dummy/.postcssrc.yml | 3 + test/dummy/Rakefile | 6 + test/dummy/app/assets/config/manifest.js | 3 + test/dummy/app/assets/images/.keep | 0 test/dummy/app/assets/stylesheets/application.css | 15 + test/dummy/app/assets/stylesheets/scaffold.css | 80 + .../app/channels/application_cable/channel.rb | 4 + .../app/channels/application_cable/connection.rb | 4 + .../app/controllers/application_controller.rb | 2 + test/dummy/app/controllers/concerns/.keep | 0 test/dummy/app/helpers/application_helper.rb | 2 + test/dummy/app/javascript/packs/application.js | 0 test/dummy/app/jobs/application_job.rb | 2 + test/dummy/app/mailboxes/application_mailbox.rb | 2 + test/dummy/app/mailboxes/messages_mailbox.rb | 4 + test/dummy/app/mailers/application_mailer.rb | 4 + test/dummy/app/models/application_record.rb | 3 + test/dummy/app/models/concerns/.keep | 0 test/dummy/app/views/layouts/application.html.erb | 14 + test/dummy/app/views/layouts/mailer.html.erb | 13 + test/dummy/app/views/layouts/mailer.text.erb | 1 + test/dummy/bin/bundle | 3 + test/dummy/bin/rails | 4 + test/dummy/bin/rake | 4 + test/dummy/bin/setup | 36 + test/dummy/bin/update | 31 + test/dummy/bin/yarn | 11 + test/dummy/config.ru | 5 + test/dummy/config/application.rb | 19 + test/dummy/config/boot.rb | 5 + test/dummy/config/cable.yml | 10 + test/dummy/config/database.yml | 25 + test/dummy/config/environment.rb | 5 + test/dummy/config/environments/development.rb | 63 + test/dummy/config/environments/production.rb | 96 + test/dummy/config/environments/test.rb | 46 + .../application_controller_renderer.rb | 8 + test/dummy/config/initializers/assets.rb | 14 + .../config/initializers/backtrace_silencers.rb | 7 + .../config/initializers/content_security_policy.rb | 22 + .../config/initializers/cookies_serializer.rb | 5 + .../initializers/filter_parameter_logging.rb | 4 + test/dummy/config/initializers/inflections.rb | 16 + test/dummy/config/initializers/mime_types.rb | 4 + test/dummy/config/initializers/wrap_parameters.rb | 14 + test/dummy/config/locales/en.yml | 33 + test/dummy/config/puma.rb | 34 + test/dummy/config/routes.rb | 4 + test/dummy/config/spring.rb | 6 + test/dummy/config/storage.yml | 35 + test/dummy/config/webpack/development.js | 3 + test/dummy/config/webpack/environment.js | 3 + test/dummy/config/webpack/production.js | 3 + test/dummy/config/webpack/test.js | 3 + test/dummy/config/webpacker.yml | 65 + test/dummy/db/development.sqlite3 | Bin 0 -> 49152 bytes ...20180208205311_create_action_mailroom_tables.rb | 10 + ..._create_active_storage_tables.active_storage.rb | 26 + test/dummy/db/schema.rb | 42 + test/dummy/lib/assets/.keep | 0 test/dummy/log/.keep | 0 test/dummy/log/development.log | 44 + test/dummy/package.json | 11 + test/dummy/public/404.html | 67 + test/dummy/public/422.html | 67 + test/dummy/public/500.html | 66 + test/dummy/public/apple-touch-icon-precomposed.png | 0 test/dummy/public/apple-touch-icon.png | 0 test/dummy/public/favicon.ico | 0 test/dummy/storage/.keep | 0 test/dummy/tmp/.keep | 0 test/dummy/tmp/storage/.keep | 0 test/dummy/yarn.lock | 6071 ++++++++++++++++++++ test/test_helper.rb | 7 +- test/unit/router_test.rb | 36 + 78 files changed, 7400 insertions(+), 2 deletions(-) create mode 100644 Gemfile.lock create mode 100644 test/dummy/.babelrc create mode 100644 test/dummy/.postcssrc.yml create mode 100644 test/dummy/Rakefile create mode 100644 test/dummy/app/assets/config/manifest.js create mode 100644 test/dummy/app/assets/images/.keep create mode 100644 test/dummy/app/assets/stylesheets/application.css create mode 100644 test/dummy/app/assets/stylesheets/scaffold.css create mode 100644 test/dummy/app/channels/application_cable/channel.rb create mode 100644 test/dummy/app/channels/application_cable/connection.rb create mode 100644 test/dummy/app/controllers/application_controller.rb create mode 100644 test/dummy/app/controllers/concerns/.keep create mode 100644 test/dummy/app/helpers/application_helper.rb create mode 100644 test/dummy/app/javascript/packs/application.js create mode 100644 test/dummy/app/jobs/application_job.rb create mode 100644 test/dummy/app/mailboxes/application_mailbox.rb create mode 100644 test/dummy/app/mailboxes/messages_mailbox.rb create mode 100644 test/dummy/app/mailers/application_mailer.rb create mode 100644 test/dummy/app/models/application_record.rb create mode 100644 test/dummy/app/models/concerns/.keep create mode 100644 test/dummy/app/views/layouts/application.html.erb create mode 100644 test/dummy/app/views/layouts/mailer.html.erb create mode 100644 test/dummy/app/views/layouts/mailer.text.erb create mode 100755 test/dummy/bin/bundle create mode 100755 test/dummy/bin/rails create mode 100755 test/dummy/bin/rake create mode 100755 test/dummy/bin/setup create mode 100755 test/dummy/bin/update create mode 100755 test/dummy/bin/yarn create mode 100644 test/dummy/config.ru create mode 100644 test/dummy/config/application.rb create mode 100644 test/dummy/config/boot.rb create mode 100644 test/dummy/config/cable.yml create mode 100644 test/dummy/config/database.yml create mode 100644 test/dummy/config/environment.rb create mode 100644 test/dummy/config/environments/development.rb create mode 100644 test/dummy/config/environments/production.rb create mode 100644 test/dummy/config/environments/test.rb create mode 100644 test/dummy/config/initializers/application_controller_renderer.rb create mode 100644 test/dummy/config/initializers/assets.rb create mode 100644 test/dummy/config/initializers/backtrace_silencers.rb create mode 100644 test/dummy/config/initializers/content_security_policy.rb create mode 100644 test/dummy/config/initializers/cookies_serializer.rb create mode 100644 test/dummy/config/initializers/filter_parameter_logging.rb create mode 100644 test/dummy/config/initializers/inflections.rb create mode 100644 test/dummy/config/initializers/mime_types.rb create mode 100644 test/dummy/config/initializers/wrap_parameters.rb create mode 100644 test/dummy/config/locales/en.yml create mode 100644 test/dummy/config/puma.rb create mode 100644 test/dummy/config/routes.rb create mode 100644 test/dummy/config/spring.rb create mode 100644 test/dummy/config/storage.yml create mode 100644 test/dummy/config/webpack/development.js create mode 100644 test/dummy/config/webpack/environment.js create mode 100644 test/dummy/config/webpack/production.js create mode 100644 test/dummy/config/webpack/test.js create mode 100644 test/dummy/config/webpacker.yml create mode 100644 test/dummy/db/development.sqlite3 create mode 100644 test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb create mode 100644 test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb create mode 100644 test/dummy/db/schema.rb create mode 100644 test/dummy/lib/assets/.keep create mode 100644 test/dummy/log/.keep create mode 100644 test/dummy/log/development.log create mode 100644 test/dummy/package.json create mode 100644 test/dummy/public/404.html create mode 100644 test/dummy/public/422.html create mode 100644 test/dummy/public/500.html create mode 100644 test/dummy/public/apple-touch-icon-precomposed.png create mode 100644 test/dummy/public/apple-touch-icon.png create mode 100644 test/dummy/public/favicon.ico create mode 100644 test/dummy/storage/.keep create mode 100644 test/dummy/tmp/.keep create mode 100644 test/dummy/tmp/storage/.keep create mode 100644 test/dummy/yarn.lock create mode 100644 test/unit/router_test.rb diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..3dad0c02e6 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,128 @@ +PATH + remote: . + specs: + actionmailroom (0.1.0) + rails (>= 5.2.0) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.2.0) + actionpack (= 5.2.0) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailer (5.2.0) + actionpack (= 5.2.0) + actionview (= 5.2.0) + activejob (= 5.2.0) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.2.0) + actionview (= 5.2.0) + activesupport (= 5.2.0) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.2.0) + activesupport (= 5.2.0) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.2.0) + activesupport (= 5.2.0) + globalid (>= 0.3.6) + activemodel (5.2.0) + activesupport (= 5.2.0) + activerecord (5.2.0) + activemodel (= 5.2.0) + activesupport (= 5.2.0) + arel (>= 9.0) + activestorage (5.2.0) + actionpack (= 5.2.0) + activerecord (= 5.2.0) + marcel (~> 0.3.1) + activesupport (5.2.0) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + arel (9.0.0) + builder (3.2.3) + concurrent-ruby (1.0.5) + crass (1.0.4) + erubi (1.7.1) + globalid (0.4.1) + activesupport (>= 4.2.0) + i18n (1.1.0) + concurrent-ruby (~> 1.0) + loofah (2.2.2) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.0) + mini_mime (>= 0.1.1) + marcel (0.3.2) + mimemagic (~> 0.3.2) + method_source (0.9.0) + mimemagic (0.3.2) + mini_mime (1.0.0) + mini_portile2 (2.3.0) + minitest (5.11.3) + nio4r (2.3.1) + nokogiri (1.8.4) + mini_portile2 (~> 2.3.0) + rack (2.0.5) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (5.2.0) + actioncable (= 5.2.0) + actionmailer (= 5.2.0) + actionpack (= 5.2.0) + actionview (= 5.2.0) + activejob (= 5.2.0) + activemodel (= 5.2.0) + activerecord (= 5.2.0) + activestorage (= 5.2.0) + activesupport (= 5.2.0) + bundler (>= 1.3.0) + railties (= 5.2.0) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (5.2.0) + actionpack (= 5.2.0) + activesupport (= 5.2.0) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (12.3.1) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + thor (0.20.0) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + websocket-driver (0.7.0) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) + +PLATFORMS + ruby + +DEPENDENCIES + actionmailroom! + bundler (~> 1.15) + sqlite3 + +BUNDLED WITH + 1.16.4 diff --git a/actionmailroom.gemspec b/actionmailroom.gemspec index ef9b215ff4..491472f4eb 100644 --- a/actionmailroom.gemspec +++ b/actionmailroom.gemspec @@ -18,6 +18,7 @@ Gem::Specification.new do |s| s.add_dependency "rails", ">= 5.2.0" s.add_development_dependency "bundler", "~> 1.15" + s.add_development_dependency "sqlite3" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- test/*`.split("\n") diff --git a/test/dummy/.babelrc b/test/dummy/.babelrc new file mode 100644 index 0000000000..ded31c0d80 --- /dev/null +++ b/test/dummy/.babelrc @@ -0,0 +1,18 @@ +{ + "presets": [ + ["env", { + "modules": false, + "targets": { + "browsers": "> 1%", + "uglify": true + }, + "useBuiltIns": true + }] + ], + + "plugins": [ + "syntax-dynamic-import", + "transform-object-rest-spread", + ["transform-class-properties", { "spec": true }] + ] +} diff --git a/test/dummy/.postcssrc.yml b/test/dummy/.postcssrc.yml new file mode 100644 index 0000000000..150dac3c6c --- /dev/null +++ b/test/dummy/.postcssrc.yml @@ -0,0 +1,3 @@ +plugins: + postcss-import: {} + postcss-cssnext: {} diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile new file mode 100644 index 0000000000..e85f913914 --- /dev/null +++ b/test/dummy/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/test/dummy/app/assets/config/manifest.js b/test/dummy/app/assets/config/manifest.js new file mode 100644 index 0000000000..b16e53d6d5 --- /dev/null +++ b/test/dummy/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/test/dummy/app/assets/images/.keep b/test/dummy/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/app/assets/stylesheets/application.css b/test/dummy/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..0ebd7fe829 --- /dev/null +++ b/test/dummy/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/test/dummy/app/assets/stylesheets/scaffold.css b/test/dummy/app/assets/stylesheets/scaffold.css new file mode 100644 index 0000000000..cd4f3de38d --- /dev/null +++ b/test/dummy/app/assets/stylesheets/scaffold.css @@ -0,0 +1,80 @@ +body { + background-color: #fff; + color: #333; + margin: 33px; +} + +body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { + color: #000; +} + +a:visited { + color: #666; +} + +a:hover { + color: #fff; + background-color: #000; +} + +th { + padding-bottom: 5px; +} + +td { + padding: 0 5px 7px; +} + +div.field, +div.actions { + margin-bottom: 10px; +} + +#notice { + color: green; +} + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; +} + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px 7px 0; + margin-bottom: 20px; + background-color: #f0f0f0; +} + +#error_explanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px -7px 0; + background-color: #c00; + color: #fff; +} + +#error_explanation ul li { + font-size: 12px; + list-style: square; +} + +label { + display: block; +} diff --git a/test/dummy/app/channels/application_cable/channel.rb b/test/dummy/app/channels/application_cable/channel.rb new file mode 100644 index 0000000000..d672697283 --- /dev/null +++ b/test/dummy/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/test/dummy/app/channels/application_cable/connection.rb b/test/dummy/app/channels/application_cable/connection.rb new file mode 100644 index 0000000000..0ff5442f47 --- /dev/null +++ b/test/dummy/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000000..09705d12ab --- /dev/null +++ b/test/dummy/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/test/dummy/app/controllers/concerns/.keep b/test/dummy/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/app/helpers/application_helper.rb b/test/dummy/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/test/dummy/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/test/dummy/app/javascript/packs/application.js b/test/dummy/app/javascript/packs/application.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/app/jobs/application_job.rb b/test/dummy/app/jobs/application_job.rb new file mode 100644 index 0000000000..a009ace51c --- /dev/null +++ b/test/dummy/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/test/dummy/app/mailboxes/application_mailbox.rb b/test/dummy/app/mailboxes/application_mailbox.rb new file mode 100644 index 0000000000..3ea37aaf8f --- /dev/null +++ b/test/dummy/app/mailboxes/application_mailbox.rb @@ -0,0 +1,2 @@ +class ApplicationMailbox < ActionMailroom::Mailbox +end diff --git a/test/dummy/app/mailboxes/messages_mailbox.rb b/test/dummy/app/mailboxes/messages_mailbox.rb new file mode 100644 index 0000000000..1a046ee13d --- /dev/null +++ b/test/dummy/app/mailboxes/messages_mailbox.rb @@ -0,0 +1,4 @@ +class MessagesMailbox < ApplicationMailbox + def process + end +end diff --git a/test/dummy/app/mailers/application_mailer.rb b/test/dummy/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..286b2239d1 --- /dev/null +++ b/test/dummy/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/test/dummy/app/models/application_record.rb b/test/dummy/app/models/application_record.rb new file mode 100644 index 0000000000..10a4cba84d --- /dev/null +++ b/test/dummy/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/test/dummy/app/models/concerns/.keep b/test/dummy/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..f221f512b7 --- /dev/null +++ b/test/dummy/app/views/layouts/application.html.erb @@ -0,0 +1,14 @@ + + + + Dummy + <%= csrf_meta_tags %> + + <%= stylesheet_link_tag 'application', media: 'all' %> + <%= javascript_pack_tag 'application' %> + + + + <%= yield %> + + diff --git a/test/dummy/app/views/layouts/mailer.html.erb b/test/dummy/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..cbd34d2e9d --- /dev/null +++ b/test/dummy/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/test/dummy/app/views/layouts/mailer.text.erb b/test/dummy/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/test/dummy/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/test/dummy/bin/bundle b/test/dummy/bin/bundle new file mode 100755 index 0000000000..f19acf5b5c --- /dev/null +++ b/test/dummy/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +load Gem.bin_path('bundler', 'bundle') diff --git a/test/dummy/bin/rails b/test/dummy/bin/rails new file mode 100755 index 0000000000..0739660237 --- /dev/null +++ b/test/dummy/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/test/dummy/bin/rake b/test/dummy/bin/rake new file mode 100755 index 0000000000..17240489f6 --- /dev/null +++ b/test/dummy/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/test/dummy/bin/setup b/test/dummy/bin/setup new file mode 100755 index 0000000000..94fd4d7977 --- /dev/null +++ b/test/dummy/bin/setup @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:setup' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/test/dummy/bin/update b/test/dummy/bin/update new file mode 100755 index 0000000000..58bfaed518 --- /dev/null +++ b/test/dummy/bin/update @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/test/dummy/bin/yarn b/test/dummy/bin/yarn new file mode 100755 index 0000000000..460dd565b4 --- /dev/null +++ b/test/dummy/bin/yarn @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + begin + exec "yarnpkg", *ARGV + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/test/dummy/config.ru b/test/dummy/config.ru new file mode 100644 index 0000000000..f7ba0b527b --- /dev/null +++ b/test/dummy/config.ru @@ -0,0 +1,5 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb new file mode 100644 index 0000000000..dc57837cde --- /dev/null +++ b/test/dummy/config/application.rb @@ -0,0 +1,19 @@ +require_relative 'boot' + +require 'rails/all' + +Bundler.require(*Rails.groups) +require "action_mailroom" + +module Dummy + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 5.2 + + # Settings in config/environments/* take precedence over those specified here. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. + end +end + diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb new file mode 100644 index 0000000000..c9aef85d40 --- /dev/null +++ b/test/dummy/config/boot.rb @@ -0,0 +1,5 @@ +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/test/dummy/config/cable.yml b/test/dummy/config/cable.yml new file mode 100644 index 0000000000..1cd0f8363e --- /dev/null +++ b/test/dummy/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: dummy_production diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml new file mode 100644 index 0000000000..0d02f24980 --- /dev/null +++ b/test/dummy/config/database.yml @@ -0,0 +1,25 @@ +# SQLite version 3.x +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: db/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: db/test.sqlite3 + +production: + <<: *default + database: db/production.sqlite3 diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb new file mode 100644 index 0000000000..426333bb46 --- /dev/null +++ b/test/dummy/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/test/dummy/config/environments/development.rb b/test/dummy/config/environments/development.rb new file mode 100644 index 0000000000..f09b9a00c4 --- /dev/null +++ b/test/dummy/config/environments/development.rb @@ -0,0 +1,63 @@ +Rails.application.configure do + # Verifies that versions and hashed value of the package contents in the project's package.json + # config.webpacker.check_yarn_integrity = true + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + # config.file_watcher = ActiveSupport::EventedFileUpdateChecker +end diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb new file mode 100644 index 0000000000..2aaa79f620 --- /dev/null +++ b/test/dummy/config/environments/production.rb @@ -0,0 +1,96 @@ +Rails.application.configure do + # Verifies that versions and hashed value of the package contents in the project's package.json + config.webpacker.check_yarn_integrity = false + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "dummy_#{Rails.env}" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false +end diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb new file mode 100644 index 0000000000..0a38fd3ce9 --- /dev/null +++ b/test/dummy/config/environments/test.rb @@ -0,0 +1,46 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true +end diff --git a/test/dummy/config/initializers/application_controller_renderer.rb b/test/dummy/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000000..89d2efab2b --- /dev/null +++ b/test/dummy/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/test/dummy/config/initializers/assets.rb b/test/dummy/config/initializers/assets.rb new file mode 100644 index 0000000000..4b828e80cb --- /dev/null +++ b/test/dummy/config/initializers/assets.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path +# Add Yarn node_modules folder to the asset load path. +Rails.application.config.assets.paths << Rails.root.join('node_modules') + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/test/dummy/config/initializers/backtrace_silencers.rb b/test/dummy/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000000..59385cdf37 --- /dev/null +++ b/test/dummy/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/test/dummy/config/initializers/content_security_policy.rb b/test/dummy/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..edde7f42b8 --- /dev/null +++ b/test/dummy/config/initializers/content_security_policy.rb @@ -0,0 +1,22 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https, :unsafe_inline + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/test/dummy/config/initializers/cookies_serializer.rb b/test/dummy/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000000..5a6a32d371 --- /dev/null +++ b/test/dummy/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/test/dummy/config/initializers/filter_parameter_logging.rb b/test/dummy/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..4a994e1e7b --- /dev/null +++ b/test/dummy/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/test/dummy/config/initializers/inflections.rb b/test/dummy/config/initializers/inflections.rb new file mode 100644 index 0000000000..ac033bf9dc --- /dev/null +++ b/test/dummy/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/test/dummy/config/initializers/mime_types.rb b/test/dummy/config/initializers/mime_types.rb new file mode 100644 index 0000000000..dc1899682b --- /dev/null +++ b/test/dummy/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/test/dummy/config/initializers/wrap_parameters.rb b/test/dummy/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000000..bbfc3961bf --- /dev/null +++ b/test/dummy/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/test/dummy/config/locales/en.yml b/test/dummy/config/locales/en.yml new file mode 100644 index 0000000000..decc5a8573 --- /dev/null +++ b/test/dummy/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/test/dummy/config/puma.rb b/test/dummy/config/puma.rb new file mode 100644 index 0000000000..a5eccf816b --- /dev/null +++ b/test/dummy/config/puma.rb @@ -0,0 +1,34 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb new file mode 100644 index 0000000000..30b05169b3 --- /dev/null +++ b/test/dummy/config/routes.rb @@ -0,0 +1,4 @@ +Rails.application.routes.draw do + resources :messages + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html +end diff --git a/test/dummy/config/spring.rb b/test/dummy/config/spring.rb new file mode 100644 index 0000000000..9fa7863f99 --- /dev/null +++ b/test/dummy/config/spring.rb @@ -0,0 +1,6 @@ +%w[ + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +].each { |path| Spring.watch(path) } diff --git a/test/dummy/config/storage.yml b/test/dummy/config/storage.yml new file mode 100644 index 0000000000..53e562e0fc --- /dev/null +++ b/test/dummy/config/storage.yml @@ -0,0 +1,35 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# path: your_azure_storage_path +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/test/dummy/config/webpack/development.js b/test/dummy/config/webpack/development.js new file mode 100644 index 0000000000..81269f6513 --- /dev/null +++ b/test/dummy/config/webpack/development.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/test/dummy/config/webpack/environment.js b/test/dummy/config/webpack/environment.js new file mode 100644 index 0000000000..d16d9af743 --- /dev/null +++ b/test/dummy/config/webpack/environment.js @@ -0,0 +1,3 @@ +const { environment } = require('@rails/webpacker') + +module.exports = environment diff --git a/test/dummy/config/webpack/production.js b/test/dummy/config/webpack/production.js new file mode 100644 index 0000000000..81269f6513 --- /dev/null +++ b/test/dummy/config/webpack/production.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/test/dummy/config/webpack/test.js b/test/dummy/config/webpack/test.js new file mode 100644 index 0000000000..81269f6513 --- /dev/null +++ b/test/dummy/config/webpack/test.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/test/dummy/config/webpacker.yml b/test/dummy/config/webpacker.yml new file mode 100644 index 0000000000..d3f24e1b4b --- /dev/null +++ b/test/dummy/config/webpacker.yml @@ -0,0 +1,65 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_output_path: packs + cache_path: tmp/cache/webpacker + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + resolved_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + extensions: + - .js + - .sass + - .scss + - .css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: /node_modules/ + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Cache manifest.json for performance + cache_manifest: true diff --git a/test/dummy/db/development.sqlite3 b/test/dummy/db/development.sqlite3 new file mode 100644 index 0000000000..8bd6b8dec3 Binary files /dev/null and b/test/dummy/db/development.sqlite3 differ diff --git a/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb b/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb new file mode 100644 index 0000000000..f488919138 --- /dev/null +++ b/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb @@ -0,0 +1,10 @@ +class CreateActionMailroomTables < ActiveRecord::Migration[5.2] + def change + create_table :action_mailroom_inbound_emails do |t| + t.integer :status, default: 0, null: false + + t.datetime :created_at, precision: 6 + t.datetime :updated_at, precision: 6 + end + end +end diff --git a/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb b/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000000..360e0d1b7a --- /dev/null +++ b/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb @@ -0,0 +1,26 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + create_table :active_storage_blobs do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false + t.references :blob, null: false + + t.datetime :created_at, null: false + + t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true + end + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb new file mode 100644 index 0000000000..cf66062891 --- /dev/null +++ b/test/dummy/db/schema.rb @@ -0,0 +1,42 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2018_02_12_164506) do + + create_table "action_mailroom_inbound_emails", force: :cascade do |t| + t.integer "status", default: 0, null: false + t.datetime "created_at", precision: 6 + t.datetime "updated_at", precision: 6 + end + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.integer "record_id", null: false + t.integer "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.bigint "byte_size", null: false + t.string "checksum", null: false + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + +end diff --git a/test/dummy/lib/assets/.keep b/test/dummy/lib/assets/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/log/.keep b/test/dummy/log/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/log/development.log b/test/dummy/log/development.log new file mode 100644 index 0000000000..892d873374 --- /dev/null +++ b/test/dummy/log/development.log @@ -0,0 +1,44 @@ +  (0.1ms) SELECT sqlite_version(*) + ↳ ./bin/rails:4 +  (0.9ms) CREATE TABLE "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY) + ↳ ./bin/rails:4 +  (0.6ms) CREATE TABLE "ar_internal_metadata" ("key" varchar NOT NULL PRIMARY KEY, "value" varchar, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL) + ↳ ./bin/rails:4 +  (0.1ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC + ↳ ./bin/rails:4 +Migrating to CreateActionMailroomTables (20180208205311) +  (0.0ms) begin transaction + ↳ ./bin/rails:4 +  (0.3ms) CREATE TABLE "action_mailroom_inbound_emails" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer DEFAULT 0 NOT NULL, "created_at" datetime(6), "updated_at" datetime(6)) + ↳ db/migrate/20180208205311_create_action_mailroom_tables.rb:3 + ActiveRecord::SchemaMigration Create (4.0ms) INSERT INTO "schema_migrations" ("version") VALUES (?) [["version", "20180208205311"]] + ↳ ./bin/rails:4 +  (0.5ms) commit transaction + ↳ ./bin/rails:4 +Migrating to CreateActiveStorageTables (20180212164506) +  (0.0ms) begin transaction + ↳ ./bin/rails:4 +  (0.2ms) CREATE TABLE "active_storage_blobs" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "key" varchar NOT NULL, "filename" varchar NOT NULL, "content_type" varchar, "metadata" text, "byte_size" bigint NOT NULL, "checksum" varchar NOT NULL, "created_at" datetime NOT NULL) + ↳ db/migrate/20180212164506_create_active_storage_tables.active_storage.rb:4 +  (0.1ms) CREATE UNIQUE INDEX "index_active_storage_blobs_on_key" ON "active_storage_blobs" ("key") + ↳ db/migrate/20180212164506_create_active_storage_tables.active_storage.rb:4 +  (0.1ms) CREATE TABLE "active_storage_attachments" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "record_type" varchar NOT NULL, "record_id" integer NOT NULL, "blob_id" integer NOT NULL, "created_at" datetime NOT NULL) + ↳ db/migrate/20180212164506_create_active_storage_tables.active_storage.rb:16 +  (0.1ms) CREATE INDEX "index_active_storage_attachments_on_blob_id" ON "active_storage_attachments" ("blob_id") + ↳ db/migrate/20180212164506_create_active_storage_tables.active_storage.rb:16 +  (0.1ms) CREATE UNIQUE INDEX "index_active_storage_attachments_uniqueness" ON "active_storage_attachments" ("record_type", "record_id", "name", "blob_id") + ↳ db/migrate/20180212164506_create_active_storage_tables.active_storage.rb:16 + ActiveRecord::SchemaMigration Create (0.1ms) INSERT INTO "schema_migrations" ("version") VALUES (?) [["version", "20180212164506"]] + ↳ ./bin/rails:4 +  (0.4ms) commit transaction + ↳ ./bin/rails:4 + ActiveRecord::InternalMetadata Load (0.1ms) SELECT "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = ? LIMIT ? [["key", "environment"], ["LIMIT", 1]] + ↳ ./bin/rails:4 +  (0.0ms) begin transaction + ↳ ./bin/rails:4 + ActiveRecord::InternalMetadata Create (0.2ms) INSERT INTO "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["key", "environment"], ["value", "development"], ["created_at", "2018-09-18 00:47:15.287379"], ["updated_at", "2018-09-18 00:47:15.287379"]] + ↳ ./bin/rails:4 +  (0.4ms) commit transaction + ↳ ./bin/rails:4 +  (0.1ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC + ↳ ./bin/rails:4 diff --git a/test/dummy/package.json b/test/dummy/package.json new file mode 100644 index 0000000000..59fdfb8a60 --- /dev/null +++ b/test/dummy/package.json @@ -0,0 +1,11 @@ +{ + "name": "dummy", + "private": true, + "dependencies": { + "@rails/webpacker": "^3.2.2", + "activetext": "file:../.." + }, + "devDependencies": { + "webpack-dev-server": "^2.11.1" + } +} diff --git a/test/dummy/public/404.html b/test/dummy/public/404.html new file mode 100644 index 0000000000..2be3af26fc --- /dev/null +++ b/test/dummy/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/test/dummy/public/422.html b/test/dummy/public/422.html new file mode 100644 index 0000000000..c08eac0d1d --- /dev/null +++ b/test/dummy/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/test/dummy/public/500.html b/test/dummy/public/500.html new file mode 100644 index 0000000000..78a030af22 --- /dev/null +++ b/test/dummy/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/test/dummy/public/apple-touch-icon-precomposed.png b/test/dummy/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/public/apple-touch-icon.png b/test/dummy/public/apple-touch-icon.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/public/favicon.ico b/test/dummy/public/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/storage/.keep b/test/dummy/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/tmp/.keep b/test/dummy/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/tmp/storage/.keep b/test/dummy/tmp/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/dummy/yarn.lock b/test/dummy/yarn.lock new file mode 100644 index 0000000000..af299bfcf8 --- /dev/null +++ b/test/dummy/yarn.lock @@ -0,0 +1,6071 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@rails/webpacker@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-3.2.2.tgz#6d60e1cf729dc2ccc52053c9b6b8d30c9a48a297" + dependencies: + babel-core "^6.26.0" + babel-loader "^7.1.2" + babel-plugin-syntax-dynamic-import "^6.18.0" + babel-plugin-transform-class-properties "^6.24.1" + babel-plugin-transform-object-rest-spread "^6.26.0" + babel-polyfill "^6.26.0" + babel-preset-env "^1.6.1" + case-sensitive-paths-webpack-plugin "^2.1.1" + compression-webpack-plugin "^1.1.6" + css-loader "^0.28.9" + extract-text-webpack-plugin "^3.0.2" + file-loader "^1.1.6" + glob "^7.1.2" + js-yaml "^3.10.0" + node-sass "^4.7.2" + path-complete-extname "^0.1.0" + postcss-cssnext "^3.1.0" + postcss-import "^11.0.0" + postcss-loader "^2.1.0" + sass-loader "^6.0.6" + style-loader "^0.20.1" + uglifyjs-webpack-plugin "^1.1.8" + webpack "^3.10.0" + webpack-manifest-plugin "^1.3.2" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +accepts@~1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" + dependencies: + mime-types "~2.1.16" + negotiator "0.6.1" + +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102" + +"activestorage@>= 5.2.0-rc1": + version "5.2.0-rc1" + resolved "https://registry.yarnpkg.com/activestorage/-/activestorage-5.2.0-rc1.tgz#79898996eceb0f13575eff41fb109051fbfa49b0" + +"activetext@file:../..": + version "0.1" + dependencies: + activestorage ">= 5.2.0-rc1" + trix "^0.11.1" + +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv-keywords@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" + +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.0.0, ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ajv@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.1.1.tgz#978d597fbc2b7d0e5a5c3ddeb149a682f2abfa0e" + dependencies: + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-flatten@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" + +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + +asn1.js@^4.0.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.1.2, async@^2.1.5, async@^2.4.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +atob@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" + +autoprefixer@^6.3.1: + version "6.7.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" + dependencies: + browserslist "^1.7.6" + caniuse-db "^1.0.30000634" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^5.2.16" + postcss-value-parser "^3.2.3" + +autoprefixer@^7.1.1: + version "7.2.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.2.6.tgz#256672f86f7c735da849c4f07d008abb056067dc" + dependencies: + browserslist "^2.11.3" + caniuse-lite "^1.0.30000805" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^6.0.17" + postcss-value-parser "^3.2.3" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" + slash "^1.0.0" + source-map "^0.5.6" + +babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-loader@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" + dependencies: + find-cache-dir "^1.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-to-generator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-class-properties@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" + dependencies: + babel-helper-function-name "^6.24.1" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-object-rest-spread@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-polyfill@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" + dependencies: + babel-runtime "^6.26.0" + core-js "^2.5.0" + regenerator-runtime "^0.10.5" + +babel-preset-env@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^2.1.2" + invariant "^2.2.2" + semver "^5.3.0" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.24.1, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +balanced-match@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + +binary-extensions@^1.0.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^3.5.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +body-parser@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.1" + http-errors "~1.6.2" + iconv-lite "0.4.19" + on-finished "~2.3.0" + qs "6.5.1" + raw-body "2.3.2" + type-is "~1.6.15" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +braces@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + define-property "^1.0.0" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + dependencies: + pako "~1.0.5" + +browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" + dependencies: + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" + +browserslist@^2.0.0, browserslist@^2.1.2, browserslist@^2.11.3: + version "2.11.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2" + dependencies: + caniuse-lite "^1.0.30000792" + electron-to-chromium "^1.3.30" + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + +cacache@^10.0.1: + version "10.0.2" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.2.tgz#105a93a162bbedf3a25da42e1939ed99ffb145f8" + dependencies: + bluebird "^3.5.0" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^1.3.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.1" + ssri "^5.0.0" + unique-filename "^1.1.0" + y18n "^3.2.1" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +caniuse-api@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" + dependencies: + browserslist "^1.3.6" + caniuse-db "^1.0.30000529" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-api@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-2.0.0.tgz#b1ddb5a5966b16f48dc4998444d4bbc6c7d9d834" + dependencies: + browserslist "^2.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: + version "1.0.30000808" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000808.tgz#30dfd83009d5704f02dffb37725068ed12a366bb" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805: + version "1.0.30000808" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000808.tgz#7d759b5518529ea08b6705a19e70dbf401628ffc" + +case-sensitive-paths-webpack-plugin@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.1.tgz#3d29ced8c1f124bf6f53846fb3f5894731fdc909" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.1, chalk@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" + dependencies: + ansi-styles "^3.2.0" + escape-string-regexp "^1.0.5" + supports-color "^5.2.0" + +chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chokidar@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.1.tgz#6e67e9998fe10e8f651e975ca62460456ff8e297" + dependencies: + anymatch "^2.0.0" + async-each "^1.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^2.1.1" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + upath "1.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clap@^1.0.9: + version "1.2.3" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" + dependencies: + chalk "^1.1.3" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone-deep@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8" + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.1" + kind-of "^3.2.2" + shallow-clone "^0.1.2" + +clone@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +coa@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" + dependencies: + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.3.0, color-convert@^1.8.2, color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-name@^1.0.0, color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + dependencies: + color-name "^1.0.0" + +color-string@^1.4.0, color-string@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9" + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +color@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/color/-/color-1.0.3.tgz#e48e832d85f14ef694fb468811c2d5cfe729b55d" + dependencies: + color-convert "^1.8.2" + color-string "^1.4.0" + +color@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color/-/color-2.0.1.tgz#e4ed78a3c4603d0891eba5430b04b86314f4c839" + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colormin@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" + dependencies: + color "^0.11.0" + css-color-names "0.0.4" + has "^1.0.1" + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0, commander@~2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + +compressible@~2.0.11: + version "2.0.12" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66" + dependencies: + mime-db ">= 1.30.0 < 2" + +compression-webpack-plugin@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.6.tgz#450808fe143b4c5216a14f0c315c47bec3d83cec" + dependencies: + async "^2.4.1" + cacache "^10.0.1" + find-cache-dir "^1.0.0" + serialize-javascript "^1.4.0" + webpack-sources "^1.0.1" + +compression@^1.5.2: + version "1.7.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db" + dependencies: + accepts "~1.3.4" + bytes "3.0.0" + compressible "~2.0.11" + debug "2.6.9" + on-headers "~1.0.1" + safe-buffer "5.1.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +connect-history-api-fallback@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +convert-source-map@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + +core-js@^2.4.0, core-js@^2.5.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.1.0" + os-homedir "^1.0.1" + parse-json "^2.2.0" + require-from-string "^1.1.0" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-color-function@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" + dependencies: + balanced-match "0.1.0" + color "^0.11.0" + debug "^3.1.0" + rgb "~0.1.0" + +css-color-names@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + +css-loader@^0.28.9: + version "0.28.9" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.9.tgz#68064b85f4e271d7ce4c48a58300928e535d1c95" + dependencies: + babel-code-frame "^6.26.0" + css-selector-tokenizer "^0.7.0" + cssnano "^3.10.0" + icss-utils "^2.1.0" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + object-assign "^4.1.1" + postcss "^5.0.6" + postcss-modules-extract-imports "^1.2.0" + postcss-modules-local-by-default "^1.2.0" + postcss-modules-scope "^1.1.0" + postcss-modules-values "^1.3.0" + postcss-value-parser "^3.3.0" + source-list-map "^2.0.0" + +css-selector-tokenizer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" + dependencies: + cssesc "^0.1.0" + fastparse "^1.1.1" + regexpu-core "^1.0.0" + +css-unit-converter@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" + +cssesc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" + +cssnano@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" + dependencies: + autoprefixer "^6.3.1" + decamelize "^1.1.2" + defined "^1.0.0" + has "^1.0.1" + object-assign "^4.0.1" + postcss "^5.0.14" + postcss-calc "^5.2.0" + postcss-colormin "^2.1.8" + postcss-convert-values "^2.3.4" + postcss-discard-comments "^2.0.4" + postcss-discard-duplicates "^2.0.1" + postcss-discard-empty "^2.0.1" + postcss-discard-overridden "^0.1.1" + postcss-discard-unused "^2.2.1" + postcss-filter-plugins "^2.0.0" + postcss-merge-idents "^2.1.5" + postcss-merge-longhand "^2.0.1" + postcss-merge-rules "^2.0.3" + postcss-minify-font-values "^1.0.2" + postcss-minify-gradients "^1.0.1" + postcss-minify-params "^1.0.4" + postcss-minify-selectors "^2.0.4" + postcss-normalize-charset "^1.1.0" + postcss-normalize-url "^3.0.7" + postcss-ordered-values "^2.1.0" + postcss-reduce-idents "^2.2.2" + postcss-reduce-initial "^1.0.0" + postcss-reduce-transforms "^1.0.3" + postcss-svgo "^2.1.1" + postcss-unique-selectors "^2.0.2" + postcss-value-parser "^3.2.3" + postcss-zindex "^2.0.1" + +csso@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" + dependencies: + clap "^1.0.9" + source-map "^0.5.3" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +define-properties@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + dependencies: + foreach "^2.0.5" + object-keys "^1.0.8" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + +depd@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + +detect-node@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + dependencies: + buffer-indexof "^1.0.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + +duplexify@^3.4.2, duplexify@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30: + version "1.3.33" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.33.tgz#bf00703d62a7c65238136578c352d6c5c042a545" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +encodeurl@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.7" + +errno@^0.1.3, errno@^0.1.4: + version "0.1.6" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026" + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.7.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + dependencies: + is-callable "^1.1.1" + is-date-object "^1.0.1" + is-symbol "^1.0.1" + +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.38" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.38.tgz#fa7d40d65bbc9bb8a67e1d3f9cc656a00530eed3" + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esprima@^2.6.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +eventsource@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" + dependencies: + original ">=0.0.5" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +express@^4.16.2: + version "4.16.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" + dependencies: + accepts "~1.3.4" + array-flatten "1.1.1" + body-parser "1.18.2" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.1" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.0" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.2" + qs "6.5.1" + range-parser "~1.2.0" + safe-buffer "5.1.1" + send "0.16.1" + serve-static "1.13.1" + setprototypeof "1.1.0" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.0, extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extglob@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extract-text-webpack-plugin@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7" + dependencies: + async "^2.4.1" + loader-utils "^1.1.0" + schema-utils "^0.3.0" + webpack-sources "^1.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fastparse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" + dependencies: + websocket-driver ">=0.5.1" + +file-loader@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.6.tgz#7b9a8f2c58f00a77fddf49e940f7ac978a3ea0e8" + dependencies: + loader-utils "^1.0.2" + schema-utils "^0.3.0" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" + dependencies: + debug "2.6.9" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.3.1" + unpipe "~1.0.0" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +flush-write-stream@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + +for-in@^1.0.1, for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + dependencies: + for-in "^1.0.1" + +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.39" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.0.2, function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" + dependencies: + globule "^1.0.0" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@~7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globule@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" + dependencies: + glob "~7.1.1" + lodash "~4.17.4" + minimatch "~3.0.2" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +handle-thing@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hawk@3.1.3, hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-comment-regex@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + +http-errors@1.6.2, http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-parser-js@>=0.4.0: + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + +http-proxy-middleware@~0.17.4: + version "0.17.4" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" + dependencies: + http-proxy "^1.16.2" + is-glob "^3.1.0" + lodash "^4.17.2" + micromatch "^2.3.11" + +http-proxy@^1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + +icss-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" + dependencies: + postcss "^6.0.1" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +import-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" + dependencies: + pkg-dir "^2.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +internal-ip@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" + dependencies: + meow "^3.3.0" + +interpret@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + +invariant@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + +ipaddr.js@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-arrayish@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.0.2, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-callable@^1.1.1, is-callable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + +is-my-json-valid@^2.12.4: + version "2.17.1" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz#3da98914a70a22f0a8563ef1511a246c6fc55471" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-odd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088" + dependencies: + is-number "^3.0.0" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-svg@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" + dependencies: + html-comment-regex "^1.1.0" + +is-symbol@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isnumeric@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/isnumeric/-/isnumeric-0.2.0.tgz#a2347ba360de19e33d0ffd590fddf7755cbf2e64" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +js-base64@^2.1.8, js-base64@^2.1.9: + version "2.4.3" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.10.0, js-yaml@^3.4.3: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@~3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-loader@^0.5.4: + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json3@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +killable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" + +kind-of@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0, kind-of@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +lazy-cache@^0.2.3: + version "0.2.7" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lazy-cache@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" + dependencies: + set-getter "^0.1.0" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash._reinterpolate@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + +lodash.clonedeep@^4.3.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.mergewith@^4.6.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" + +lodash.tail@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" + +lodash.template@^4.2.4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + dependencies: + lodash._reinterpolate "~3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + dependencies: + lodash._reinterpolate "~3.0.0" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@3.x: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@~4.17.4: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" + +loglevel@^1.4.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lru-cache@^4.0.1, lru-cache@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +macaddress@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" + +make-dir@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" + dependencies: + pify "^3.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^3.3.0, meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^2.1.5, micromatch@^2.3.11: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +micromatch@^3.1.4: + version "3.1.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.0" + define-property "^1.0.0" + extend-shallow "^2.0.1" + extglob "^2.0.2" + fragment-cache "^0.2.1" + kind-of "^6.0.0" + nanomatch "^1.2.5" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +"mime-db@>= 1.30.0 < 2": + version "1.32.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414" + +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + +mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + +mime@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mississippi@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.1.tgz#2a8bb465e86550ac8b36a7b6f45599171d78671e" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^1.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + +mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +nan@^2.3.0, nan@^2.3.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" + +nanomatch@^1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^1.0.0" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + is-odd "^1.0.0" + kind-of "^5.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-forge@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" + +node-gyp@^3.3.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + minimatch "^3.0.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "2" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-libs-browser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.0" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-pre-gyp@^0.6.39: + version "0.6.39" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" + dependencies: + detect-libc "^1.0.2" + hawk "3.1.3" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +node-sass@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e" + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash.assign "^4.2.0" + lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.3.2" + node-gyp "^3.3.1" + npmlog "^4.0.0" + request "~2.79.0" + sass-graph "^2.2.4" + stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" + +"nopt@2 || 3": + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +normalize-url@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1, oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-keys@^1.0.8: + version "1.0.11" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + +obuf@^1.0.0, obuf@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onecolor@^3.0.4: + version "3.0.5" + resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.5.tgz#36eff32201379efdf1180fb445e51a8e2425f9f6" + +opn@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225" + dependencies: + is-wsl "^1.1.0" + +original@>=0.0.5: + version "1.0.0" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" + dependencies: + url-parse "1.0.x" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@0, osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +pako@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parseurl@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-complete-extname@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-0.1.0.tgz#c454702669f31452f8193aa6168915fa31692f4a" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.14" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pixrem@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pixrem/-/pixrem-4.0.1.tgz#2da4a1de6ec4423c5fc3794e930b81d4490ec686" + dependencies: + browserslist "^2.0.0" + postcss "^6.0.0" + reduce-css-calc "^1.2.7" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + +pleeease-filters@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pleeease-filters/-/pleeease-filters-4.0.0.tgz#6632b2fb05648d2758d865384fbced79e1ccaec7" + dependencies: + onecolor "^3.0.4" + postcss "^6.0.1" + +portfinder@^1.0.9: + version "1.0.13" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + +postcss-apply@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/postcss-apply/-/postcss-apply-0.8.0.tgz#14e544bbb5cb6f1c1e048857965d79ae066b1343" + dependencies: + babel-runtime "^6.23.0" + balanced-match "^0.4.2" + postcss "^6.0.0" + +postcss-attribute-case-insensitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-2.0.0.tgz#94dc422c8f90997f16bd33a3654bbbec084963b4" + dependencies: + postcss "^6.0.0" + postcss-selector-parser "^2.2.3" + +postcss-calc@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + dependencies: + postcss "^5.0.2" + postcss-message-helpers "^2.0.0" + reduce-css-calc "^1.2.6" + +postcss-calc@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-6.0.1.tgz#3d24171bbf6e7629d422a436ebfe6dd9511f4330" + dependencies: + css-unit-converter "^1.1.1" + postcss "^6.0.0" + postcss-selector-parser "^2.2.2" + reduce-css-calc "^2.0.0" + +postcss-color-function@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.0.1.tgz#402b3f2cebc3f6947e618fb6be3654fbecef6444" + dependencies: + css-color-function "~1.3.3" + postcss "^6.0.1" + postcss-message-helpers "^2.0.0" + postcss-value-parser "^3.3.0" + +postcss-color-gray@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-4.1.0.tgz#e5581ed57eaa826fb652ca11b1e2b7b136a9f9df" + dependencies: + color "^2.0.1" + postcss "^6.0.14" + postcss-message-helpers "^2.0.0" + reduce-function-call "^1.0.2" + +postcss-color-hex-alpha@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-3.0.0.tgz#1e53e6c8acb237955e8fd08b7ecdb1b8b8309f95" + dependencies: + color "^1.0.3" + postcss "^6.0.1" + postcss-message-helpers "^2.0.0" + +postcss-color-hsl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hsl/-/postcss-color-hsl-2.0.0.tgz#12703666fa310430e3f30a454dac1386317d5844" + dependencies: + postcss "^6.0.1" + postcss-value-parser "^3.3.0" + units-css "^0.4.0" + +postcss-color-hwb@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hwb/-/postcss-color-hwb-3.0.0.tgz#3402b19ef4d8497540c1fb5072be9863ca95571e" + dependencies: + color "^1.0.3" + postcss "^6.0.1" + postcss-message-helpers "^2.0.0" + reduce-function-call "^1.0.2" + +postcss-color-rebeccapurple@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-3.0.0.tgz#eebaf03d363b4300b96792bd3081c19ed66513d3" + dependencies: + postcss "^6.0.1" + postcss-value-parser "^3.3.0" + +postcss-color-rgb@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rgb/-/postcss-color-rgb-2.0.0.tgz#14539c8a7131494b482e0dd1cc265ff6514b5263" + dependencies: + postcss "^6.0.1" + postcss-value-parser "^3.3.0" + +postcss-color-rgba-fallback@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rgba-fallback/-/postcss-color-rgba-fallback-3.0.0.tgz#37d5c9353a07a09270912a82606bb42a0d702c04" + dependencies: + postcss "^6.0.6" + postcss-value-parser "^3.3.0" + rgb-hex "^2.1.0" + +postcss-colormin@^2.1.8: + version "2.2.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" + dependencies: + colormin "^1.0.5" + postcss "^5.0.13" + postcss-value-parser "^3.2.3" + +postcss-convert-values@^2.3.4: + version "2.6.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" + dependencies: + postcss "^5.0.11" + postcss-value-parser "^3.1.2" + +postcss-cssnext@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-cssnext/-/postcss-cssnext-3.1.0.tgz#927dc29341a938254cde38ea60a923b9dfedead9" + dependencies: + autoprefixer "^7.1.1" + caniuse-api "^2.0.0" + chalk "^2.0.1" + pixrem "^4.0.0" + pleeease-filters "^4.0.0" + postcss "^6.0.5" + postcss-apply "^0.8.0" + postcss-attribute-case-insensitive "^2.0.0" + postcss-calc "^6.0.0" + postcss-color-function "^4.0.0" + postcss-color-gray "^4.0.0" + postcss-color-hex-alpha "^3.0.0" + postcss-color-hsl "^2.0.0" + postcss-color-hwb "^3.0.0" + postcss-color-rebeccapurple "^3.0.0" + postcss-color-rgb "^2.0.0" + postcss-color-rgba-fallback "^3.0.0" + postcss-custom-media "^6.0.0" + postcss-custom-properties "^6.1.0" + postcss-custom-selectors "^4.0.1" + postcss-font-family-system-ui "^3.0.0" + postcss-font-variant "^3.0.0" + postcss-image-set-polyfill "^0.3.5" + postcss-initial "^2.0.0" + postcss-media-minmax "^3.0.0" + postcss-nesting "^4.0.1" + postcss-pseudo-class-any-link "^4.0.0" + postcss-pseudoelements "^5.0.0" + postcss-replace-overflow-wrap "^2.0.0" + postcss-selector-matches "^3.0.1" + postcss-selector-not "^3.0.1" + +postcss-custom-media@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-6.0.0.tgz#be532784110ecb295044fb5395a18006eb21a737" + dependencies: + postcss "^6.0.1" + +postcss-custom-properties@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-6.2.0.tgz#5d929a7f06e9b84e0f11334194c0ba9a30acfbe9" + dependencies: + balanced-match "^1.0.0" + postcss "^6.0.13" + +postcss-custom-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-4.0.1.tgz#781382f94c52e727ef5ca4776ea2adf49a611382" + dependencies: + postcss "^6.0.1" + postcss-selector-matches "^3.0.0" + +postcss-discard-comments@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" + dependencies: + postcss "^5.0.14" + +postcss-discard-duplicates@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" + dependencies: + postcss "^5.0.4" + +postcss-discard-empty@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" + dependencies: + postcss "^5.0.14" + +postcss-discard-overridden@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" + dependencies: + postcss "^5.0.16" + +postcss-discard-unused@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" + dependencies: + postcss "^5.0.14" + uniqs "^2.0.0" + +postcss-filter-plugins@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" + dependencies: + postcss "^5.0.4" + uniqid "^4.0.0" + +postcss-font-family-system-ui@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-family-system-ui/-/postcss-font-family-system-ui-3.0.0.tgz#675fe7a9e029669f05f8dba2e44c2225ede80623" + dependencies: + postcss "^6.0" + +postcss-font-variant@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-3.0.0.tgz#08ccc88f6050ba82ed8ef2cc76c0c6a6b41f183e" + dependencies: + postcss "^6.0.1" + +postcss-image-set-polyfill@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/postcss-image-set-polyfill/-/postcss-image-set-polyfill-0.3.5.tgz#0f193413700cf1f82bd39066ef016d65a4a18181" + dependencies: + postcss "^6.0.1" + postcss-media-query-parser "^0.2.3" + +postcss-import@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-11.1.0.tgz#55c9362c9192994ec68865d224419df1db2981f0" + dependencies: + postcss "^6.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-initial@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-2.0.0.tgz#72715f7336e0bb79351d99ee65c4a253a8441ba4" + dependencies: + lodash.template "^4.2.4" + postcss "^6.0.1" + +postcss-load-config@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + postcss-load-options "^1.2.0" + postcss-load-plugins "^2.3.0" + +postcss-load-options@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + +postcss-load-plugins@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" + dependencies: + cosmiconfig "^2.1.1" + object-assign "^4.1.0" + +postcss-loader@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.1.0.tgz#038c2d6d59753fef4667827fd3ae03f5dc5e6a7a" + dependencies: + loader-utils "^1.1.0" + postcss "^6.0.0" + postcss-load-config "^1.2.0" + schema-utils "^0.4.0" + +postcss-media-minmax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-3.0.0.tgz#675256037a43ef40bc4f0760bfd06d4dc69d48d2" + dependencies: + postcss "^6.0.1" + +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + +postcss-merge-idents@^2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" + dependencies: + has "^1.0.1" + postcss "^5.0.10" + postcss-value-parser "^3.1.1" + +postcss-merge-longhand@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" + dependencies: + postcss "^5.0.4" + +postcss-merge-rules@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" + dependencies: + browserslist "^1.5.2" + caniuse-api "^1.5.2" + postcss "^5.0.4" + postcss-selector-parser "^2.2.2" + vendors "^1.0.0" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + +postcss-minify-font-values@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-minify-gradients@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" + dependencies: + postcss "^5.0.12" + postcss-value-parser "^3.3.0" + +postcss-minify-params@^1.0.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.2" + postcss-value-parser "^3.0.2" + uniqs "^2.0.0" + +postcss-minify-selectors@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" + dependencies: + alphanum-sort "^1.0.2" + has "^1.0.1" + postcss "^5.0.14" + postcss-selector-parser "^2.0.0" + +postcss-modules-extract-imports@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" + dependencies: + postcss "^6.0.1" + +postcss-modules-local-by-default@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-scope@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-values@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" + +postcss-nesting@^4.0.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-4.2.1.tgz#0483bce338b3f0828ced90ff530b29b98b00300d" + dependencies: + postcss "^6.0.11" + +postcss-normalize-charset@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" + dependencies: + postcss "^5.0.5" + +postcss-normalize-url@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^1.4.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + +postcss-ordered-values@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.1" + +postcss-pseudo-class-any-link@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-4.0.0.tgz#9152a0613d3450720513e8892854bae42d0ee68e" + dependencies: + postcss "^6.0.1" + postcss-selector-parser "^2.2.3" + +postcss-pseudoelements@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudoelements/-/postcss-pseudoelements-5.0.0.tgz#eef194e8d524645ca520a949e95e518e812402cb" + dependencies: + postcss "^6.0.0" + +postcss-reduce-idents@^2.2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-reduce-initial@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" + dependencies: + postcss "^5.0.4" + +postcss-reduce-transforms@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" + dependencies: + has "^1.0.1" + postcss "^5.0.8" + postcss-value-parser "^3.0.1" + +postcss-replace-overflow-wrap@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-2.0.0.tgz#794db6faa54f8db100854392a93af45768b4e25b" + dependencies: + postcss "^6.0.1" + +postcss-selector-matches@^3.0.0, postcss-selector-matches@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-3.0.1.tgz#e5634011e13950881861bbdd58c2d0111ffc96ab" + dependencies: + balanced-match "^0.4.2" + postcss "^6.0.1" + +postcss-selector-not@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-3.0.1.tgz#2e4db2f0965336c01e7cec7db6c60dff767335d9" + dependencies: + balanced-match "^0.4.2" + postcss "^6.0.1" + +postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2, postcss-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^2.1.1: + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" + dependencies: + is-svg "^2.0.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + svgo "^0.7.0" + +postcss-unique-selectors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" + +postcss-zindex@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" + dependencies: + has "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0, postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.13, postcss@^6.0.14, postcss@^6.0.17, postcss@^6.0.5, postcss@^6.0.6: + version "6.0.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.17.tgz#e259a051ca513f81e9afd0c21f7f82eda50c65c5" + dependencies: + chalk "^2.3.0" + source-map "^0.6.1" + supports-color "^5.1.0" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +private@^0.1.6, private@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + +proxy-addr@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.5.2" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +pump@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" + dependencies: + duplexify "^3.5.3" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + +qs@6.5.1, qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +qs@~6.3.0: + version "6.3.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" + +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +querystringify@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" + +querystringify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.0.3, range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + +rc@^1.1.7: + version "1.2.5" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + dependencies: + pify "^2.3.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +reduce-css-calc@^1.2.6, reduce-css-calc@^1.2.7: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-css-calc@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.4.tgz#c20e9cda8445ad73d4ff4bea960c6f8353791708" + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + +reduce-function-call@^1.0.1, reduce-function-call@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + dependencies: + balanced-match "^0.4.2" + +regenerate@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" + +regenerator-runtime@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +regex-not@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9" + dependencies: + extend-shallow "^2.0.1" + +regexpu-core@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2, repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@2: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +request@2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +request@~2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-from-string@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +requires-port@1.0.x, requires-port@1.x.x, requires-port@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + dependencies: + resolve-from "^3.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + +resolve@^1.1.7: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + +rgb-hex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/rgb-hex/-/rgb-hex-2.1.0.tgz#c773c5fe2268a25578d92539a82a7a5ce53beda6" + +rgb@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sass-graph@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" + +sass-loader@^6.0.6: + version "6.0.6" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9" + dependencies: + async "^2.1.5" + clone-deep "^0.3.0" + loader-utils "^1.0.1" + lodash.tail "^4.1.1" + pify "^3.0.0" + +sax@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + +schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.3.tgz#e2a594d3395834d5e15da22b48be13517859458e" + dependencies: + ajv "^5.0.0" + ajv-keywords "^2.1.0" + +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + +selfsigned@^1.9.1: + version "1.10.2" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.2.tgz#b4449580d99929b65b10a48389301a6592088758" + dependencies: + node-forge "0.7.1" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" + dependencies: + debug "2.6.9" + depd "~1.1.1" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serialize-javascript@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005" + +serve-index@^1.7.2: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.1" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-getter@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" + dependencies: + to-object-path "^0.3.0" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.10" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" + dependencies: + is-extendable "^0.1.1" + kind-of "^2.0.1" + lazy-cache "^0.2.3" + mixin-object "^2.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + dependencies: + is-arrayish "^0.3.1" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^2.0.0" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + +sockjs-client@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" + dependencies: + debug "^2.6.6" + eventsource "0.1.6" + faye-websocket "~0.11.0" + inherits "^2.0.1" + json3 "^3.3.2" + url-parse "^1.1.8" + +sockjs@0.3.19: + version "0.3.19" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" + dependencies: + faye-websocket "^0.10.0" + uuid "^3.0.1" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-map-resolve@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" + dependencies: + atob "^2.0.0" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + +source-map@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +spdy-transport@^2.0.18: + version "2.0.20" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.20.tgz#735e72054c486b2354fe89e702256004a39ace4d" + dependencies: + debug "^2.6.8" + detect-node "^2.0.3" + hpack.js "^2.1.6" + obuf "^1.1.1" + readable-stream "^2.2.9" + safe-buffer "^5.0.1" + wbuf "^1.7.2" + +spdy@^3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" + dependencies: + debug "^2.6.8" + handle-thing "^1.2.5" + http-deceiver "^1.2.7" + safe-buffer "^5.0.1" + select-hose "^2.0.0" + spdy-transport "^2.0.18" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +ssri@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.2.1.tgz#8b6eb873688759bd3c75a88dee74593d179bb73c" + dependencies: + safe-buffer "^5.1.1" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.3.1 < 2": + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + +statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +stdout-stream@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" + dependencies: + readable-stream "^2.0.1" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.3" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.0.0, string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4, stringstream@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +style-loader@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.1.tgz#33ac2bf4d5c65a8906bc586ad253334c246998d0" + dependencies: + loader-utils "^1.1.0" + schema-utils "^0.4.3" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^4.2.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +supports-color@^5.1.0, supports-color@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" + dependencies: + has-flag "^3.0.0" + +svgo@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" + dependencies: + coa "~1.0.1" + colors "~1.1.2" + csso "~2.3.1" + js-yaml "~3.7.0" + mkdirp "~0.5.1" + sax "~1.2.1" + whet.extend "~0.9.9" + +tapable@^0.2.7: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" + +tar-pack@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.0.0, tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +thunky@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371" + +time-stamp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" + +timers-browserify@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae" + dependencies: + setimmediate "^1.0.4" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae" + dependencies: + define-property "^0.2.5" + extend-shallow "^2.0.1" + regex-not "^1.0.0" + +tough-cookie@~2.3.0, tough-cookie@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + dependencies: + punycode "^1.4.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +trix@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/trix/-/trix-0.11.1.tgz#ffe54f2757c2c2385b8424fd5c5d2ab712a09acc" + +"true-case-path@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" + dependencies: + glob "^6.0.4" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-is@~1.6.15: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-es@^3.3.4: + version "3.3.10" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.10.tgz#8b0b7992cebe20edc26de1bf325cef797b8f3fa5" + dependencies: + commander "~2.14.1" + source-map "~0.6.1" + +uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + +uglifyjs-webpack-plugin@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.1.8.tgz#1302fb9471a7daf3d0a5174da6d65f0f415e75ad" + dependencies: + cacache "^10.0.1" + find-cache-dir "^1.0.0" + schema-utils "^0.4.2" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + uglify-es "^3.3.4" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +underscore.string@2.3.x: + version "2.3.3" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.3.3.tgz#71c08bf6b428b1133f37e78fa3a21c82f7329b0d" + +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +uniqid@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" + dependencies: + macaddress "^0.2.8" + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + +unique-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + +units-css@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07" + dependencies: + isnumeric "^0.2.0" + viewport-dimensions "^0.2.0" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.0.tgz#b4706b9461ca8473adf89133d235689ca17f3656" + dependencies: + lodash "3.x" + underscore.string "2.3.x" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +url-parse@1.0.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" + dependencies: + querystringify "0.0.x" + requires-port "1.0.x" + +url-parse@^1.1.8: + version "1.2.0" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986" + dependencies: + querystringify "~1.0.0" + requires-port "~1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" + dependencies: + define-property "^0.2.5" + isobject "^3.0.0" + lazy-cache "^2.0.2" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + +uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + +vendors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +viewport-dimensions@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz#de740747db5387fd1725f5175e91bac76afdf36c" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +watchpack@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + dependencies: + async "^2.1.2" + chokidar "^1.7.0" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" + dependencies: + minimalistic-assert "^1.0.0" + +webpack-dev-middleware@1.12.2: + version "1.12.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e" + dependencies: + memory-fs "~0.4.1" + mime "^1.5.0" + path-is-absolute "^1.0.0" + range-parser "^1.0.3" + time-stamp "^2.0.0" + +webpack-dev-server@^2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.1.tgz#6f9358a002db8403f016e336816f4485384e5ec0" + dependencies: + ansi-html "0.0.7" + array-includes "^3.0.3" + bonjour "^3.5.0" + chokidar "^2.0.0" + compression "^1.5.2" + connect-history-api-fallback "^1.3.0" + debug "^3.1.0" + del "^3.0.0" + express "^4.16.2" + html-entities "^1.2.0" + http-proxy-middleware "~0.17.4" + import-local "^1.0.0" + internal-ip "1.2.0" + ip "^1.1.5" + killable "^1.0.0" + loglevel "^1.4.1" + opn "^5.1.0" + portfinder "^1.0.9" + selfsigned "^1.9.1" + serve-index "^1.7.2" + sockjs "0.3.19" + sockjs-client "1.1.4" + spdy "^3.4.1" + strip-ansi "^3.0.0" + supports-color "^5.1.0" + webpack-dev-middleware "1.12.2" + yargs "6.6.0" + +webpack-manifest-plugin@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-1.3.2.tgz#5ea8ee5756359ddc1d98814324fe43496349a7d4" + dependencies: + fs-extra "^0.30.0" + lodash ">=3.5 <5" + +webpack-sources@^1.0.1, webpack-sources@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@^3.10.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" + async "^2.1.2" + enhanced-resolve "^3.4.0" + escope "^3.6.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" + webpack-sources "^1.0.1" + yargs "^8.0.2" + +websocket-driver@>=0.5.1: + version "0.7.0" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" + dependencies: + http-parser-js ">=0.4.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + +whet.extend@~0.9.9: + version "0.9.9" + resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@1, which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +worker-farm@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + +yargs@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + +yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" diff --git a/test/test_helper.rb b/test/test_helper.rb index 81d14de111..79fde51549 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -5,6 +5,10 @@ require_relative "../test/dummy/config/environment" ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] require "rails/test_help" +# Filter out Minitest backtrace while allowing backtrace from other libraries +# to be shown. +Minitest.backtrace_filter = Minitest::BacktraceFilter.new + require "rails/test_unit/reporter" Rails::TestUnitReporter.executable = 'bin/test' @@ -19,7 +23,6 @@ end class ActiveSupport::TestCase private def create_file_blob(filename:, content_type:, metadata: nil) - ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, - filename: filename, content_type: content_type, metadata: metadata + ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata end end diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb new file mode 100644 index 0000000000..2ff3e24d5a --- /dev/null +++ b/test/unit/router_test.rb @@ -0,0 +1,36 @@ +require_relative '../test_helper' + +class RepliesMailbox < ActionMailroom::Mailbox + def process + @processed = true + end + + def processed? + @processed + end +end + +module ActionMailroom + class RouterTest < ActiveSupport::TestCase + setup do + @router = ActionMailroom::Router.new('replies@example.com' => :replies) + end + + test "routed to mailbox" do + @router.route() + message = Message.new(subject: "Greetings", content: "

Hello world

") + assert_equal "Hello world", message.content.body.to_plain_text + end + + test "without content" do + message = Message.create!(subject: "Greetings") + assert message.content.body.nil? + end + + test "embed extraction" do + blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + message = Message.create!(subject: "Greetings", content: ActionText::Content.new("Hello world").append_attachables(blob)) + assert_equal "racecar.jpg", message.content.embeds.first.filename.to_s + end + end +end -- cgit v1.2.3 From 061d77f7ca949b55c313c1cbbd7b48e65f0aa569 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 17:49:47 -0700 Subject: WIP: Router --- lib/action_mailroom.rb | 1 + lib/action_mailroom/mailbox.rb | 19 +++++++++++++++++++ lib/action_mailroom/router.rb | 16 ++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 lib/action_mailroom/router.rb diff --git a/lib/action_mailroom.rb b/lib/action_mailroom.rb index 5dc0d5a11c..e50d1c4ebe 100644 --- a/lib/action_mailroom.rb +++ b/lib/action_mailroom.rb @@ -4,4 +4,5 @@ module ActionMailroom extend ActiveSupport::Autoload autoload :Mailbox + autoload :Router end diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index e19e6d7c6d..e873b55544 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -1,2 +1,21 @@ class ActionMailroom::Mailbox + class << self + def receive(inbound_email) + new(inbound_email).process + end + + def routing(routes) + @router = ActionMailroom::Router.new(routes) + end + end + + attr_reader :inbound_email + delegate :mail, to: :inbound_email + + def initialize(inbound_email) + @inbound_email = inbound_email + end + + def process + end end diff --git a/lib/action_mailroom/router.rb b/lib/action_mailroom/router.rb new file mode 100644 index 0000000000..8ce3947337 --- /dev/null +++ b/lib/action_mailroom/router.rb @@ -0,0 +1,16 @@ +class ActionMailroom::Router + def initialize(routes) + @routes = routes + end + + def route(inbound_email) + locate_mailbox(inbound_email).receive(inbound_email) + end + + private + attr_reader :routes + + def locate_mailbox(inbound_email) + "#{routes[inbound_email.mail.to].to_s.capitalize}Mailbox" + end +end -- cgit v1.2.3 From a6146d2e89739e0ae09dbbb755aad6d5b2018dc3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 22:15:27 -0700 Subject: First end-to-end rickety test --- Gemfile.lock | 2 + actionmailroom.gemspec | 1 + app/models/action_mailroom/inbound_email.rb | 6 +- bin/test | 5 + lib/action_mailroom/router.rb | 4 +- test/fixtures/files/welcome.eml | 631 ++++++++++++++++++++++++++++ test/test_helper.rb | 8 +- test/unit/router_test.rb | 23 +- 8 files changed, 656 insertions(+), 24 deletions(-) create mode 100755 bin/test create mode 100644 test/fixtures/files/welcome.eml diff --git a/Gemfile.lock b/Gemfile.lock index 3dad0c02e6..0ceac134ff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -50,6 +50,7 @@ GEM tzinfo (~> 1.1) arel (9.0.0) builder (3.2.3) + byebug (10.0.2) concurrent-ruby (1.0.5) crass (1.0.4) erubi (1.7.1) @@ -122,6 +123,7 @@ PLATFORMS DEPENDENCIES actionmailroom! bundler (~> 1.15) + byebug sqlite3 BUNDLED WITH diff --git a/actionmailroom.gemspec b/actionmailroom.gemspec index 491472f4eb..217904d62f 100644 --- a/actionmailroom.gemspec +++ b/actionmailroom.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |s| s.add_development_dependency "bundler", "~> 1.15" s.add_development_dependency "sqlite3" + s.add_development_dependency "byebug" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- test/*`.split("\n") diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 755d7be138..62614abb95 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -1,7 +1,9 @@ +require "mail" + class ActionMailroom::InboundEmail < ActiveRecord::Base self.table_name = "action_mailroom_inbound_emails" - has_one_attached :raw_message + has_one_attached :raw_email enum status: %i[ pending processing delivered failed bounced ] @@ -9,7 +11,7 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base def mail - @mail ||= Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_message.download)) + @mail ||= Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_email.download)) end private diff --git a/bin/test b/bin/test new file mode 100755 index 0000000000..5516a12bcd --- /dev/null +++ b/bin/test @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +$: << File.expand_path("../test", __dir__) + +require "bundler/setup" +require "rails/plugin/test" diff --git a/lib/action_mailroom/router.rb b/lib/action_mailroom/router.rb index 8ce3947337..be2a980927 100644 --- a/lib/action_mailroom/router.rb +++ b/lib/action_mailroom/router.rb @@ -1,3 +1,5 @@ +require "byebug" + class ActionMailroom::Router def initialize(routes) @routes = routes @@ -11,6 +13,6 @@ class ActionMailroom::Router attr_reader :routes def locate_mailbox(inbound_email) - "#{routes[inbound_email.mail.to].to_s.capitalize}Mailbox" + "#{routes[inbound_email.mail.to.first].to_s.capitalize}Mailbox".constantize end end diff --git a/test/fixtures/files/welcome.eml b/test/fixtures/files/welcome.eml new file mode 100644 index 0000000000..5d6b3c1ea5 --- /dev/null +++ b/test/fixtures/files/welcome.eml @@ -0,0 +1,631 @@ +From: Jason Fried +Mime-Version: 1.0 (Apple Message framework v1244.3) +Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74" +Subject: Discussion: Let's debate these attachments +Date: Tue, 13 Sep 2011 15:19:37 -0400 +In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail> +To: "Replies" +References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail> +Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com> +X-Mailer: Apple Mail (2.1244.3) + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=utf-8 + +Let's talk about these images: + + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74 +Content-Type: multipart/related; + type="text/html"; + boundary="Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1" + + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=avatar1.jpeg +Content-Type: image/jpg; + name="avatar1.jpeg" +Content-Id: <7AAEB353-2341-4D46-A054-5CA5CB2363B7> + +/9j/4AAQSkZJRgABAQAAAQABAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdC +IFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAA +AADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFj +cHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAA +ABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAAD +TAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJD +AAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5 +OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEA +AAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAA +AAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAA +AA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBo +dHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt +IHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt +IHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcg +Q29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENv +bmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAA +ABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAA +AAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAK +AA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUA +mgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEy +ATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMC +DAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMh +Ay0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4E +jASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3 +BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDII +RghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqY +Cq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUAN +Wg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBh +EH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT +5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReu +F9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9oc +AhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCY +IMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZcl +xyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2 +K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIx +SjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDec +N9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+ +oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXe +RiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN +3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYP +VlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1f +D19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/ +aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfBy +S3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyB +fOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuH +n4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLj +k02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6f +HZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1 +q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm4 +0blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZG +xsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnU +y9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj +4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozz +GfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDAAICAgIC +AQICAgICAgIDAwYEAwMDAwcFBQQGCAcICAgHCAgJCg0LCQkMCggICw8LDA0ODg4OCQsQEQ8OEQ0O +Dg7/2wBDAQICAgMDAwYEBAYOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O +Dg4ODg4ODg4ODg4ODg7/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAEC +AwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0Kx +wRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1 +dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ +2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QA +tREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk +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 +mk8VWW5WRGJGAOaAP//Z + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=avatar2.jpg +Content-Type: image/jpg; + x-unix-mode=0700; + name="avatar2.jpg" +Content-Id: <4594E827-6E69-4329-8691-6BC35E3E73A0> + +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 + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1-- + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74-- diff --git a/test/test_helper.rb b/test/test_helper.rb index 79fde51549..9344a57fd7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,7 +22,11 @@ end class ActiveSupport::TestCase private - def create_file_blob(filename:, content_type:, metadata: nil) - ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata + def create_inbound_email(fixture_name) + ActionMailroom::InboundEmail.create!.tap do |inbound_email| + inbound_email.raw_email.attach \ + ActiveStorage::Blob.create_after_upload! \ + io: file_fixture(fixture_name).open, filename: fixture_name, content_type: 'message/rfc822' + end end end diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 2ff3e24d5a..53a3d29f40 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -2,11 +2,7 @@ require_relative '../test_helper' class RepliesMailbox < ActionMailroom::Mailbox def process - @processed = true - end - - def processed? - @processed + $processed = true end end @@ -14,23 +10,12 @@ module ActionMailroom class RouterTest < ActiveSupport::TestCase setup do @router = ActionMailroom::Router.new('replies@example.com' => :replies) + $processed = false end test "routed to mailbox" do - @router.route() - message = Message.new(subject: "Greetings", content: "

Hello world

") - assert_equal "Hello world", message.content.body.to_plain_text - end - - test "without content" do - message = Message.create!(subject: "Greetings") - assert message.content.body.nil? - end - - test "embed extraction" do - blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") - message = Message.create!(subject: "Greetings", content: ActionText::Content.new("Hello world").append_attachables(blob)) - assert_equal "racecar.jpg", message.content.embeds.first.filename.to_s + @router.route create_inbound_email("welcome.eml") + assert $processed end end end -- cgit v1.2.3 From 68d0d603a0d787e5cafb82cde9311dd2c8e226de Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 22:15:34 -0700 Subject: Save this for a little later --- app/models/action_mailroom/inbound_email.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 62614abb95..bb3b0daff7 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -7,7 +7,7 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base enum status: %i[ pending processing delivered failed bounced ] - after_create_commit :deliver_to_mailroom_later + # after_create_commit :deliver_to_mailroom_later def mail -- cgit v1.2.3 From a38f41a00e0b6946a24eacf77a022d8ec68adaea Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 22:15:45 -0700 Subject: Keep the riff raff out --- test/dummy/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/dummy/.gitignore diff --git a/test/dummy/.gitignore b/test/dummy/.gitignore new file mode 100644 index 0000000000..dfe3397eab --- /dev/null +++ b/test/dummy/.gitignore @@ -0,0 +1,3 @@ +*.log +*.sqlite3 +tmp/* -- cgit v1.2.3 From bf164da94d462614cb97b3ab65b2431141a9b732 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 22:16:07 -0700 Subject: No byebug history plz --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..36298d2843 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.byebug_history -- cgit v1.2.3 From dd55bf66d27ce59405e7c1487f2f6edc61934dba Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 22:18:55 -0700 Subject: Assert the intended email was processed --- test/unit/router_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 53a3d29f40..adc4cb3d71 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -2,7 +2,7 @@ require_relative '../test_helper' class RepliesMailbox < ActionMailroom::Mailbox def process - $processed = true + $processed = mail.subject end end @@ -15,7 +15,7 @@ module ActionMailroom test "routed to mailbox" do @router.route create_inbound_email("welcome.eml") - assert $processed + assert_equal $processed, "Discussion: Let's debate these attachments" end end end -- cgit v1.2.3 From 65a6d525c6ce36acefd4b1918bbc5f14132e7810 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 17 Sep 2018 22:32:05 -0700 Subject: Add basic, unauthenticated inbound emails controller --- .../action_mailroom/inbound_emails_controller.rb | 14 ++++++++++++++ config/routes.rb | 7 +++++++ test/unit/controller_test.rb | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 app/controllers/action_mailroom/inbound_emails_controller.rb create mode 100644 config/routes.rb create mode 100644 test/unit/controller_test.rb diff --git a/app/controllers/action_mailroom/inbound_emails_controller.rb b/app/controllers/action_mailroom/inbound_emails_controller.rb new file mode 100644 index 0000000000..0c74d4b29a --- /dev/null +++ b/app/controllers/action_mailroom/inbound_emails_controller.rb @@ -0,0 +1,14 @@ +class ActionMailroom::InboundEmailsController < ActionController::Base + skip_forgery_protection + before_action :require_rfc822_message + + def create + ActionMailroom::InboundEmail.create!(raw_email: params[:message]) + head :created + end + + private + def require_rfc822_message + head :unsupported_media_type unless params.require(:message).content_type == 'message/rfc822' + end +end diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..1ba01f4bf9 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Rails.application.routes.draw do + scope :action_mailroom do + post "/inbound_emails" => "action_mailroom/inbound_emails#create", as: :rails_inbound_emails + end +end diff --git a/test/unit/controller_test.rb b/test/unit/controller_test.rb new file mode 100644 index 0000000000..c9ba1ba45f --- /dev/null +++ b/test/unit/controller_test.rb @@ -0,0 +1,19 @@ +require_relative '../test_helper' + +class ActionMailroom::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + test "receiving a valid RFC 822 message" do + assert_difference -> { ActionMailroom::InboundEmail.count }, +1 do + post_inbound_email "welcome.eml" + end + + assert_response :created + + inbound_email = ActionMailroom::InboundEmail.last + assert_equal file_fixture('../files/welcome.eml').read, inbound_email.raw_email.download + end + + private + def post_inbound_email(fixture_name) + post rails_inbound_emails_url, params: { message: fixture_file_upload("files/#{fixture_name}", 'message/rfc822') } + end +end -- cgit v1.2.3 From 4a9b45ce2214573de29eed694e4416d46642244f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:07:06 -0700 Subject: Use a test helper to create fixtures rather than rely on them being predefined Too much hassle to manage all the steps compared to just pointing to an .eml fixture and having it setup for you. --- lib/action_mailroom/test_helper.rb | 13 +++++++++++++ lib/tasks/action_mailroom.rake | 17 ++--------------- test/test_helper.rb | 11 +++-------- 3 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 lib/action_mailroom/test_helper.rb diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb new file mode 100644 index 0000000000..af9e3ad559 --- /dev/null +++ b/lib/action_mailroom/test_helper.rb @@ -0,0 +1,13 @@ +module ActionMailroom + 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(fixture_name) + ActionMailroom::InboundEmail.create!.tap do |inbound_email| + inbound_email.raw_email.attach \ + ActiveStorage::Blob.create_after_upload! \ + io: file_fixture(fixture_name).open, filename: fixture_name, content_type: 'message/rfc822' + end + end + end +end diff --git a/lib/tasks/action_mailroom.rake b/lib/tasks/action_mailroom.rake index ce80fcf55e..2cf692cc0f 100644 --- a/lib/tasks/action_mailroom.rake +++ b/lib/tasks/action_mailroom.rake @@ -4,8 +4,8 @@ namespace :action_mailroom do # Prevent migration installation task from showing up twice. Rake::Task["install:migrations"].clear_comments - desc "Copy over the migration and fixtures" - task install: %w( environment active_storage:install copy_migration copy_fixtures ) + desc "Copy over the migration" + task install: %w( environment active_storage:install copy_migration ) task :copy_migration do if Rake::Task.task_defined?("action_mailroom:install:migrations") @@ -14,17 +14,4 @@ namespace :action_mailroom do Rake::Task["app:action_mailroom:install:migrations"].invoke end end - - FIXTURE_TEMPLATE_PATH = File.expand_path("../templates/fixtures.yml", __dir__) - FIXTURE_APP_DIR_PATH = Rails.root.join("test/fixtures/action_mailroom") - FIXTURE_APP_PATH = FIXTURE_APP_DIR_PATH.join("inbound_emails.yml") - - task :copy_fixtures do - if File.exist?(FIXTURE_APP_PATH) - puts "Won't copy Action Mailbox fixtures as it already exists" - else - FileUtils.mkdir FIXTURE_APP_DIR_PATH - FileUtils.cp FIXTURE_TEMPLATE_PATH, FIXTURE_APP_PATH - end - end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9344a57fd7..c05cc87896 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -20,13 +20,8 @@ if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixtures :all end +require "action_mailroom/test_helper" + class ActiveSupport::TestCase - private - def create_inbound_email(fixture_name) - ActionMailroom::InboundEmail.create!.tap do |inbound_email| - inbound_email.raw_email.attach \ - ActiveStorage::Blob.create_after_upload! \ - io: file_fixture(fixture_name).open, filename: fixture_name, content_type: 'message/rfc822' - end - end + include ActionMailroom::TestHelper end -- cgit v1.2.3 From 31ff0f7b6cc4ec7bbc70a9472e01215cd9b046e8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:08:42 -0700 Subject: Remember to add access protection --- app/controllers/action_mailroom/inbound_emails_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/action_mailroom/inbound_emails_controller.rb b/app/controllers/action_mailroom/inbound_emails_controller.rb index 0c74d4b29a..e5970b38ec 100644 --- a/app/controllers/action_mailroom/inbound_emails_controller.rb +++ b/app/controllers/action_mailroom/inbound_emails_controller.rb @@ -1,3 +1,4 @@ +# TODO: Add access protection using basic auth with verified tokens. Maybe coming from credentials by default? class ActionMailroom::InboundEmailsController < ActionController::Base skip_forgery_protection before_action :require_rfc822_message -- cgit v1.2.3 From 1219b070d73d0e980d5ac58a5a583048976e5539 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:11:03 -0700 Subject: Add test for unsupported content type --- test/fixtures/files/text.txt | 1 + test/unit/controller_test.rb | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 test/fixtures/files/text.txt diff --git a/test/fixtures/files/text.txt b/test/fixtures/files/text.txt new file mode 100644 index 0000000000..84c3f1cf21 --- /dev/null +++ b/test/fixtures/files/text.txt @@ -0,0 +1 @@ +This Is Not An Email! diff --git a/test/unit/controller_test.rb b/test/unit/controller_test.rb index c9ba1ba45f..f2a49f415f 100644 --- a/test/unit/controller_test.rb +++ b/test/unit/controller_test.rb @@ -12,6 +12,14 @@ class ActionMailroom::InboundEmailsControllerTest < ActionDispatch::IntegrationT assert_equal file_fixture('../files/welcome.eml').read, inbound_email.raw_email.download end + test "rejecting a message of an unsupported type" do + assert_no_difference -> { ActionMailroom::InboundEmail.count } do + post rails_inbound_emails_url, params: { message: fixture_file_upload('files/text.txt', 'text/plain') } + end + + assert_response :unsupported_media_type + end + private def post_inbound_email(fixture_name) post rails_inbound_emails_url, params: { message: fixture_file_upload("files/#{fixture_name}", 'message/rfc822') } -- cgit v1.2.3 From 0cb3245b4da59f9f77f5797d37214cb7844772fd Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:13:17 -0700 Subject: Use rails scope by default --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 1ba01f4bf9..a24d5054cf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Rails.application.routes.draw do - scope :action_mailroom do + scope "rails/action_mailroom" do post "/inbound_emails" => "action_mailroom/inbound_emails#create", as: :rails_inbound_emails end end -- cgit v1.2.3 From 016ba4dbfa4946658c4ec6200bad73757f30be30 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:26:30 -0700 Subject: Process inbound emails with state and exceptions --- lib/action_mailroom/mailbox.rb | 16 +++++++++++++++- test/unit/mailbox_test.rb | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 test/unit/mailbox_test.rb diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index e873b55544..7f16165221 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -1,7 +1,11 @@ +require "active_support/rescuable" + class ActionMailroom::Mailbox + include ActiveSupport::Rescuable + class << self def receive(inbound_email) - new(inbound_email).process + new(inbound_email).process_with_state_and_exception_handling end def routing(routes) @@ -16,6 +20,16 @@ class ActionMailroom::Mailbox @inbound_email = inbound_email end + def process_with_state_and_exception_handling + inbound_email.processing! + process + inbound_email.delivered! + rescue => exception + inbound_email.failed! + rescue_with_handler(exception) || raise + end + def process + # Overwrite in subclasses end end diff --git a/test/unit/mailbox_test.rb b/test/unit/mailbox_test.rb new file mode 100644 index 0000000000..505c1f370a --- /dev/null +++ b/test/unit/mailbox_test.rb @@ -0,0 +1,36 @@ +require_relative '../test_helper' + +class SuccessfulMailbox < ActionMailroom::Mailbox + def process + $processed = mail.subject + end +end + +class UnsuccessfulMailbox < ActionMailroom::Mailbox + rescue_from(RuntimeError) { $processed = :failure } + + def process + raise "No way!" + end +end + +module ActionMailroom + class MailboxTest < ActiveSupport::TestCase + setup do + $processed = false + @inbound_email = create_inbound_email("welcome.eml") + end + + test "successful mailbox processing leaves inbound email in delivered state" do + SuccessfulMailbox.receive @inbound_email + assert @inbound_email.delivered? + assert_equal "Discussion: Let's debate these attachments", $processed + end + + test "unsuccessful mailbox processing leaves inbound email in failed state" do + UnsuccessfulMailbox.receive @inbound_email + assert @inbound_email.failed? + assert_equal :failure, $processed + end + end +end -- cgit v1.2.3 From 4d428e078efa16f29110caff9e609e06e09d3072 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:26:35 -0700 Subject: Proper order --- test/unit/router_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index adc4cb3d71..7dbacbb865 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -15,7 +15,7 @@ module ActionMailroom test "routed to mailbox" do @router.route create_inbound_email("welcome.eml") - assert_equal $processed, "Discussion: Let's debate these attachments" + assert_equal "Discussion: Let's debate these attachments", $processed end end end -- cgit v1.2.3 From 1ac1459b0f1c257bef410f166c2ffc90bf36deca Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:31:16 -0700 Subject: More scalable name! There'll probably be other concerns we need to do around the processing. --- lib/action_mailroom/mailbox.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index 7f16165221..44dcd691b1 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -5,7 +5,7 @@ class ActionMailroom::Mailbox class << self def receive(inbound_email) - new(inbound_email).process_with_state_and_exception_handling + new(inbound_email).perform_processing end def routing(routes) @@ -20,7 +20,7 @@ class ActionMailroom::Mailbox @inbound_email = inbound_email end - def process_with_state_and_exception_handling + def perform_processing inbound_email.processing! process inbound_email.delivered! -- cgit v1.2.3 From 5f73fa84fff1b92665de7779c4cb824277930ca8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:42:27 -0700 Subject: Extract mailbox test suites into feature-specific slices --- test/unit/mailbox/state_test.rb | 34 ++++++++++++++++++++++++++++++++++ test/unit/mailbox_test.rb | 36 ------------------------------------ 2 files changed, 34 insertions(+), 36 deletions(-) create mode 100644 test/unit/mailbox/state_test.rb delete mode 100644 test/unit/mailbox_test.rb diff --git a/test/unit/mailbox/state_test.rb b/test/unit/mailbox/state_test.rb new file mode 100644 index 0000000000..41fbafbd67 --- /dev/null +++ b/test/unit/mailbox/state_test.rb @@ -0,0 +1,34 @@ +require_relative '../../test_helper' + +class SuccessfulMailbox < ActionMailroom::Mailbox + def process + $processed = mail.subject + end +end + +class UnsuccessfulMailbox < ActionMailroom::Mailbox + rescue_from(RuntimeError) { $processed = :failure } + + def process + raise "No way!" + end +end + +class ActionMailroom::Mailbox::StateTest < ActiveSupport::TestCase + setup do + $processed = false + @inbound_email = create_inbound_email("welcome.eml") + end + + test "successful mailbox processing leaves inbound email in delivered state" do + SuccessfulMailbox.receive @inbound_email + assert @inbound_email.delivered? + assert_equal "Discussion: Let's debate these attachments", $processed + end + + test "unsuccessful mailbox processing leaves inbound email in failed state" do + UnsuccessfulMailbox.receive @inbound_email + assert @inbound_email.failed? + assert_equal :failure, $processed + end +end diff --git a/test/unit/mailbox_test.rb b/test/unit/mailbox_test.rb deleted file mode 100644 index 505c1f370a..0000000000 --- a/test/unit/mailbox_test.rb +++ /dev/null @@ -1,36 +0,0 @@ -require_relative '../test_helper' - -class SuccessfulMailbox < ActionMailroom::Mailbox - def process - $processed = mail.subject - end -end - -class UnsuccessfulMailbox < ActionMailroom::Mailbox - rescue_from(RuntimeError) { $processed = :failure } - - def process - raise "No way!" - end -end - -module ActionMailroom - class MailboxTest < ActiveSupport::TestCase - setup do - $processed = false - @inbound_email = create_inbound_email("welcome.eml") - end - - test "successful mailbox processing leaves inbound email in delivered state" do - SuccessfulMailbox.receive @inbound_email - assert @inbound_email.delivered? - assert_equal "Discussion: Let's debate these attachments", $processed - end - - test "unsuccessful mailbox processing leaves inbound email in failed state" do - UnsuccessfulMailbox.receive @inbound_email - assert @inbound_email.failed? - assert_equal :failure, $processed - end - end -end -- cgit v1.2.3 From 0f140fe15852b829d22864a2f7664440204ac723 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 16:42:38 -0700 Subject: Add callbacks --- lib/action_mailroom/mailbox.rb | 9 +++++++-- lib/action_mailroom/mailbox/callbacks.rb | 28 ++++++++++++++++++++++++++++ test/unit/mailbox/callbacks_test.rb | 25 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 lib/action_mailroom/mailbox/callbacks.rb create mode 100644 test/unit/mailbox/callbacks_test.rb diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index 44dcd691b1..0d03f4be6b 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -1,7 +1,8 @@ require "active_support/rescuable" +require "action_mailroom/mailbox/callbacks" class ActionMailroom::Mailbox - include ActiveSupport::Rescuable + include ActiveSupport::Rescuable, Callbacks class << self def receive(inbound_email) @@ -22,7 +23,11 @@ class ActionMailroom::Mailbox def perform_processing inbound_email.processing! - process + + run_callbacks :process do + process + end + inbound_email.delivered! rescue => exception inbound_email.failed! diff --git a/lib/action_mailroom/mailbox/callbacks.rb b/lib/action_mailroom/mailbox/callbacks.rb new file mode 100644 index 0000000000..ae5a461d8f --- /dev/null +++ b/lib/action_mailroom/mailbox/callbacks.rb @@ -0,0 +1,28 @@ +require "active_support/callbacks" + +module ActionMailroom + class Mailbox + module Callbacks + extend ActiveSupport::Concern + include ActiveSupport::Callbacks + + included do + define_callbacks :process + end + + module ClassMethods + 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 +end diff --git a/test/unit/mailbox/callbacks_test.rb b/test/unit/mailbox/callbacks_test.rb new file mode 100644 index 0000000000..ada6410876 --- /dev/null +++ b/test/unit/mailbox/callbacks_test.rb @@ -0,0 +1,25 @@ +require_relative '../../test_helper' + +class CallbackMailbox < ActionMailroom::Mailbox + before_processing { $before_processing = "Ran that!" } + after_processing { $after_processing = "Ran that too!" } + around_processing ->(r, block) { block.call; $around_processing = "Ran that as well!" } + + def process + $processed = mail.subject + end +end + +class ActionMailroom::Mailbox::CallbacksTest < ActiveSupport::TestCase + setup do + $before_processing = $after_processing = $around_processing = $processed = false + @inbound_email = create_inbound_email("welcome.eml") + end + + test "all callback types" do + CallbackMailbox.receive @inbound_email + assert_equal "Ran that!", $before_processing + assert_equal "Ran that too!", $after_processing + assert_equal "Ran that as well!", $around_processing + end +end -- cgit v1.2.3 From 029c693f19ee088af329620fb729dd9005080755 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 17:13:49 -0700 Subject: Make a note for tying inbound email to exception Then InboundEmail doesn't need to serialize or track the exception that went with it. The arrow will point the other way. --- lib/action_mailroom/mailbox.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index 0d03f4be6b..a2c097a42f 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -31,6 +31,8 @@ class ActionMailroom::Mailbox inbound_email.delivered! rescue => exception inbound_email.failed! + + # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier rescue_with_handler(exception) || raise end -- cgit v1.2.3 From 0e00529e04d7c870ffd88b00a6e0357d2787efed Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 17:21:18 -0700 Subject: Prefix queue name like we do routes --- .../jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb index 5a78ebd4b2..71544c3ef1 100644 --- a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb +++ b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb @@ -1,5 +1,5 @@ class ActionMailroom::DeliverInboundEmailToMailroomJob < ApplicationJob - queue_as :action_mailroom_inbound_email + queue_as :rails_action_mailroom_inbound_email def perform(inbound_email) ActionMailroom::Router.receive inbound_email -- cgit v1.2.3 From d1329efd815ea0be1d666b0c2b4664349a77e1a0 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 17:27:52 -0700 Subject: Inherit from ActiveJob::Base rather than ApplicationJob Don't want to trigger app-specific behavior here. --- .../action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 9 +++++++++ .../action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 7 ------- 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb delete mode 100644 app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb diff --git a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb new file mode 100644 index 0000000000..835d26213a --- /dev/null +++ b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb @@ -0,0 +1,9 @@ +module ActionMailroom + class DeliverInboundEmailToMailroomJob < ActiveJob::Base + queue_as :rails_action_mailroom_inbound_email + + def perform(inbound_email) + # ActionMailroom::Router.receive inbound_email + end + end +end diff --git a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb deleted file mode 100644 index 71544c3ef1..0000000000 --- a/app/models/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class ActionMailroom::DeliverInboundEmailToMailroomJob < ApplicationJob - queue_as :rails_action_mailroom_inbound_email - - def perform(inbound_email) - ActionMailroom::Router.receive inbound_email - end -end -- cgit v1.2.3 From 26bfd1c59cf2dfa36fc02b87831449a8259c780e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 18 Sep 2018 17:28:35 -0700 Subject: Only deliver pending emails to the mailroom Makes it easier to test without triggering this behavior. --- app/models/action_mailroom/inbound_email.rb | 2 +- lib/action_mailroom/test_helper.rb | 4 ++-- test/unit/inbound_email/deliver_to_mailroom.rb | 11 +++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 test/unit/inbound_email/deliver_to_mailroom.rb diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index bb3b0daff7..92f80f26e3 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -7,7 +7,7 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base enum status: %i[ pending processing delivered failed bounced ] - # after_create_commit :deliver_to_mailroom_later + after_create_commit :deliver_to_mailroom_later, if: ->(r) { r.pending? } def mail diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb index af9e3ad559..94321ecad3 100644 --- a/lib/action_mailroom/test_helper.rb +++ b/lib/action_mailroom/test_helper.rb @@ -2,8 +2,8 @@ module ActionMailroom 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(fixture_name) - ActionMailroom::InboundEmail.create!.tap do |inbound_email| + def create_inbound_email(fixture_name, status: :processing) + ActionMailroom::InboundEmail.create!(status: status).tap do |inbound_email| inbound_email.raw_email.attach \ ActiveStorage::Blob.create_after_upload! \ io: file_fixture(fixture_name).open, filename: fixture_name, content_type: 'message/rfc822' diff --git a/test/unit/inbound_email/deliver_to_mailroom.rb b/test/unit/inbound_email/deliver_to_mailroom.rb new file mode 100644 index 0000000000..c583f4a0a4 --- /dev/null +++ b/test/unit/inbound_email/deliver_to_mailroom.rb @@ -0,0 +1,11 @@ +require_relative '../../test_helper' + +class ActionMailroom::InboundEmail::DeliverToMailroomTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + test "pending emails are delivered to the mailroom" do + assert_enqueued_jobs 1, only: ActionMailroom::DeliverInboundEmailToMailroomJob do + create_inbound_email("welcome.eml", status: :pending) + end + end +end -- cgit v1.2.3 From 01971b48c448fb9548d2a9ca5fb79a71922d73ce Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 15:01:33 -0700 Subject: Missing suffix --- test/unit/inbound_email/deliver_to_mailroom.rb | 11 ----------- test/unit/inbound_email/deliver_to_mailroom_test.rb | 11 +++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 test/unit/inbound_email/deliver_to_mailroom.rb create mode 100644 test/unit/inbound_email/deliver_to_mailroom_test.rb diff --git a/test/unit/inbound_email/deliver_to_mailroom.rb b/test/unit/inbound_email/deliver_to_mailroom.rb deleted file mode 100644 index c583f4a0a4..0000000000 --- a/test/unit/inbound_email/deliver_to_mailroom.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative '../../test_helper' - -class ActionMailroom::InboundEmail::DeliverToMailroomTest < ActiveSupport::TestCase - include ActiveJob::TestHelper - - test "pending emails are delivered to the mailroom" do - assert_enqueued_jobs 1, only: ActionMailroom::DeliverInboundEmailToMailroomJob do - create_inbound_email("welcome.eml", status: :pending) - end - end -end diff --git a/test/unit/inbound_email/deliver_to_mailroom_test.rb b/test/unit/inbound_email/deliver_to_mailroom_test.rb new file mode 100644 index 0000000000..c583f4a0a4 --- /dev/null +++ b/test/unit/inbound_email/deliver_to_mailroom_test.rb @@ -0,0 +1,11 @@ +require_relative '../../test_helper' + +class ActionMailroom::InboundEmail::DeliverToMailroomTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + test "pending emails are delivered to the mailroom" do + assert_enqueued_jobs 1, only: ActionMailroom::DeliverInboundEmailToMailroomJob do + create_inbound_email("welcome.eml", status: :pending) + end + end +end -- cgit v1.2.3 From 1591272f6ae2ed3509eced2295170e2324306b2c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 15:14:42 -0700 Subject: Make debugger available in testing --- test/test_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_helper.rb b/test/test_helper.rb index c05cc87896..114d719905 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -5,6 +5,8 @@ require_relative "../test/dummy/config/environment" ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] require "rails/test_help" +require "byebug" + # Filter out Minitest backtrace while allowing backtrace from other libraries # to be shown. Minitest.backtrace_filter = Minitest::BacktraceFilter.new -- cgit v1.2.3 From 359f10d6b5bee630092b312892066acf5acd073d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 15:15:10 -0700 Subject: Tmp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 36298d2843..4862d67b18 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .byebug_history +*.sqlite3-journal -- cgit v1.2.3 From c6cae759ef0cb9063dc26015132804a6218281b7 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 15:15:19 -0700 Subject: Follow db table convention --- app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb index 835d26213a..0aeccff96e 100644 --- a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb +++ b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb @@ -1,6 +1,6 @@ module ActionMailroom class DeliverInboundEmailToMailroomJob < ActiveJob::Base - queue_as :rails_action_mailroom_inbound_email + queue_as :action_mailroom_inbound_email def perform(inbound_email) # ActionMailroom::Router.receive inbound_email -- cgit v1.2.3 From 3c8fc4e9ae53af9739a0e84b1141a79e03552d4a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 15:20:04 -0700 Subject: Add incineration by default --- .../inbound_email/incineration_job.rb | 11 ++++++++ app/models/action_mailroom/inbound_email.rb | 2 ++ .../action_mailroom/inbound_email/incineratable.rb | 31 ++++++++++++++++++++++ .../inbound_email/incineratable/incineration.rb | 18 +++++++++++++ test/unit/inbound_email/incineration_test.rb | 14 ++++++++++ 5 files changed, 76 insertions(+) create mode 100644 app/jobs/action_mailroom/inbound_email/incineration_job.rb create mode 100644 app/models/action_mailroom/inbound_email/incineratable.rb create mode 100644 app/models/action_mailroom/inbound_email/incineratable/incineration.rb create mode 100644 test/unit/inbound_email/incineration_test.rb diff --git a/app/jobs/action_mailroom/inbound_email/incineration_job.rb b/app/jobs/action_mailroom/inbound_email/incineration_job.rb new file mode 100644 index 0000000000..fa1d346008 --- /dev/null +++ b/app/jobs/action_mailroom/inbound_email/incineration_job.rb @@ -0,0 +1,11 @@ +class ActionMailroom::InboundEmail::IncinerationJob < ApplicationJob + queue_as :action_mailroom_incineration + + def self.schedule(inbound_email) + set(wait: ActionMailroom::InboundEmail::Incineratable::INCINERATABLE_AFTER).perform_later(inbound_email) + end + + def perform(inbound_email) + inbound_email.incinerate + end +end diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 92f80f26e3..5c351b74b8 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -1,6 +1,8 @@ require "mail" class ActionMailroom::InboundEmail < ActiveRecord::Base + include Incineratable + self.table_name = "action_mailroom_inbound_emails" has_one_attached :raw_email diff --git a/app/models/action_mailroom/inbound_email/incineratable.rb b/app/models/action_mailroom/inbound_email/incineratable.rb new file mode 100644 index 0000000000..83ccec89ba --- /dev/null +++ b/app/models/action_mailroom/inbound_email/incineratable.rb @@ -0,0 +1,31 @@ +module ActionMailroom::InboundEmail::Incineratable + extend ActiveSupport::Concern + + # TODO: Extract into framework configuration + INCINERATABLE_AFTER = 30.days + + included do + before_update :remember_to_incinerate_later + after_update_commit :incinerate_later, if: :need_to_incinerate_later? + end + + def incinerate + Incineration.new(self).run + end + + private + # TODO: Use enum change tracking once merged into Active Support + def remember_to_incinerate_later + if status_changed? && (delivered? || failed?) + @incinerate_later = true + end + end + + def need_to_incinerate_later? + @incinerate_later + end + + def incinerate_later + ActionMailroom::InboundEmail::IncinerationJob.schedule(self) + end +end diff --git a/app/models/action_mailroom/inbound_email/incineratable/incineration.rb b/app/models/action_mailroom/inbound_email/incineratable/incineration.rb new file mode 100644 index 0000000000..8b88c1fc5b --- /dev/null +++ b/app/models/action_mailroom/inbound_email/incineratable/incineration.rb @@ -0,0 +1,18 @@ +class ActionMailroom::InboundEmail::Incineratable::Incineration + def initialize(inbound_email) + @inbound_email = inbound_email + end + + def run + @inbound_email.destroy if due? && processed? + end + + private + def due? + @inbound_email.updated_at < ActionMailroom::InboundEmail::Incineratable::INCINERATABLE_AFTER.ago.end_of_day + end + + def processed? + @inbound_email.delivered? || @inbound_email.failed? + end +end diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb new file mode 100644 index 0000000000..10a6e564f8 --- /dev/null +++ b/test/unit/inbound_email/incineration_test.rb @@ -0,0 +1,14 @@ +require_relative '../../test_helper' + +class ActionMailroom::InboundEmail::IncinerationTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + test "incinerate emails 30 days after they have been processed" do + freeze_time + + assert_enqueued_with job: ActionMailroom::InboundEmail::IncinerationJob, at: 30.days.from_now do + inbound_email = create_inbound_email("welcome.eml") + inbound_email.delivered! + end + end +end -- cgit v1.2.3 From ee6d9545dd69a96ae30fdacf264db83fbaa1e4dd Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 15:33:53 -0700 Subject: Included in test helper now --- lib/action_mailroom/router.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/action_mailroom/router.rb b/lib/action_mailroom/router.rb index be2a980927..066d7163e4 100644 --- a/lib/action_mailroom/router.rb +++ b/lib/action_mailroom/router.rb @@ -1,5 +1,3 @@ -require "byebug" - class ActionMailroom::Router def initialize(routes) @routes = routes -- cgit v1.2.3 From da697e84445f8f750b8a7d0fa916f429eda8aae4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 15:52:16 -0700 Subject: Attach a concrete router to the root mailbox and use it Don't think this is how it's going to stay. Doesn't feel like the right place for it. --- .../deliver_inbound_email_to_mailroom_job.rb | 2 +- lib/action_mailroom/mailbox.rb | 18 ++++++++--------- lib/action_mailroom/mailbox/routing.rb | 15 ++++++++++++++ lib/action_mailroom/router.rb | 8 ++++++-- test/unit/mailbox/routing_test.rb | 23 ++++++++++++++++++++++ test/unit/router_test.rb | 3 ++- 6 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 lib/action_mailroom/mailbox/routing.rb create mode 100644 test/unit/mailbox/routing_test.rb diff --git a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb index 0aeccff96e..bc5150afcb 100644 --- a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb +++ b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb @@ -3,7 +3,7 @@ module ActionMailroom queue_as :action_mailroom_inbound_email def perform(inbound_email) - # ActionMailroom::Router.receive inbound_email + ActionMailroom::Mailbox.route inbound_email end end end diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index a2c097a42f..3518449794 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -1,21 +1,19 @@ require "active_support/rescuable" + require "action_mailroom/mailbox/callbacks" +require "action_mailroom/mailbox/routing" class ActionMailroom::Mailbox - include ActiveSupport::Rescuable, Callbacks + include ActiveSupport::Rescuable + include Callbacks, Routing - class << self - def receive(inbound_email) - new(inbound_email).perform_processing - end + attr_reader :inbound_email + delegate :mail, to: :inbound_email - def routing(routes) - @router = ActionMailroom::Router.new(routes) - end + def self.receive(inbound_email) + new(inbound_email).perform_processing end - attr_reader :inbound_email - delegate :mail, to: :inbound_email def initialize(inbound_email) @inbound_email = inbound_email diff --git a/lib/action_mailroom/mailbox/routing.rb b/lib/action_mailroom/mailbox/routing.rb new file mode 100644 index 0000000000..9f082c8aa5 --- /dev/null +++ b/lib/action_mailroom/mailbox/routing.rb @@ -0,0 +1,15 @@ +module ActionMailroom::Mailbox::Routing + extend ActiveSupport::Concern + + class_methods do + attr_reader :router + + def routing(routes) + (@router ||= ActionMailroom::Router.new).add_routes(routes) + end + + def route(inbound_email) + @router.route(inbound_email) + end + end +end diff --git a/lib/action_mailroom/router.rb b/lib/action_mailroom/router.rb index 066d7163e4..bf0001f1ae 100644 --- a/lib/action_mailroom/router.rb +++ b/lib/action_mailroom/router.rb @@ -1,6 +1,10 @@ class ActionMailroom::Router - def initialize(routes) - @routes = routes + def initialize + @routes = {} + end + + def add_routes(routes) + @routes.merge!(routes) end def route(inbound_email) diff --git a/test/unit/mailbox/routing_test.rb b/test/unit/mailbox/routing_test.rb new file mode 100644 index 0000000000..bdb670e956 --- /dev/null +++ b/test/unit/mailbox/routing_test.rb @@ -0,0 +1,23 @@ +require_relative '../../test_helper' + +class ApplicationMailbox < ActionMailroom::Mailbox + routing "replies@example.com" => :replies +end + +class RepliesMailbox < ActionMailroom::Mailbox + def process + $processed = mail.subject + end +end + +class ActionMailroom::Mailbox::RoutingTest < ActiveSupport::TestCase + setup do + $processed = false + @inbound_email = create_inbound_email("welcome.eml") + end + + test "string routing" do + ApplicationMailbox.route @inbound_email + assert_equal "Discussion: Let's debate these attachments", $processed + end +end diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 7dbacbb865..6bfd880b67 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -9,7 +9,8 @@ end module ActionMailroom class RouterTest < ActiveSupport::TestCase setup do - @router = ActionMailroom::Router.new('replies@example.com' => :replies) + @router = ActionMailroom::Router.new + @router.add_routes('replies@example.com' => :replies) $processed = false end -- cgit v1.2.3 From 7143879759ff65ab585016a0a442bb49805342e2 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:18:00 -0700 Subject: Add easy way to get logging information inline for test failure diagnosis --- test/test_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_helper.rb b/test/test_helper.rb index 114d719905..a48ac6eb9c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -26,4 +26,8 @@ require "action_mailroom/test_helper" class ActiveSupport::TestCase include ActionMailroom::TestHelper + +if ARGV.include?("-v") + ActiveRecord::Base.logger = Logger.new(STDOUT) + ActiveJob::Base.logger = Logger.new(STDOUT) end -- cgit v1.2.3 From d481dd565d92dc3ff9f2868b6b6126384f8a8145 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:20:32 -0700 Subject: Simpler class definition Don't actually remember why we've used the explicit module wrapper before? --- .../action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb index bc5150afcb..299e6e6c2e 100644 --- a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb +++ b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb @@ -1,9 +1,7 @@ -module ActionMailroom - class DeliverInboundEmailToMailroomJob < ActiveJob::Base - queue_as :action_mailroom_inbound_email +class ActionMailroom::DeliverInboundEmailToMailroomJob < ActiveJob::Base + queue_as :action_mailroom_inbound_email - def perform(inbound_email) - ActionMailroom::Mailbox.route inbound_email - end + def perform(inbound_email) + ActionMailroom::Mailbox.route inbound_email end end -- cgit v1.2.3 From 67ba89a8c7b4c28cb38184ac15bc3c6aa8a20cbf Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:21:53 -0700 Subject: Ensure raw email is created first Otherwise jobs hanging off the InboundEmail won't be able to access the Active Storage data (as it won't have been uploaded yet). --- lib/action_mailroom/test_helper.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb index 94321ecad3..06a38f75b6 100644 --- a/lib/action_mailroom/test_helper.rb +++ b/lib/action_mailroom/test_helper.rb @@ -3,11 +3,10 @@ module ActionMailroom # 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(fixture_name, status: :processing) - ActionMailroom::InboundEmail.create!(status: status).tap do |inbound_email| - inbound_email.raw_email.attach \ - ActiveStorage::Blob.create_after_upload! \ + raw_email = ActiveStorage::Blob.create_after_upload! \ io: file_fixture(fixture_name).open, filename: fixture_name, content_type: 'message/rfc822' - end - end + + ActionMailroom::InboundEmail.create!(status: status, raw_email: raw_email) + end end end -- cgit v1.2.3 From 9cecd74239bfa1ccf3bf4ee177e79e65986bf7c8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:22:31 -0700 Subject: Everyone needs job testing --- test/test_helper.rb | 3 ++- test/unit/inbound_email/deliver_to_mailroom_test.rb | 2 -- test/unit/inbound_email/incineration_test.rb | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index a48ac6eb9c..50ddc85463 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -25,7 +25,8 @@ end require "action_mailroom/test_helper" class ActiveSupport::TestCase - include ActionMailroom::TestHelper + include ActionMailroom::TestHelper, ActiveJob::TestHelper +end if ARGV.include?("-v") ActiveRecord::Base.logger = Logger.new(STDOUT) diff --git a/test/unit/inbound_email/deliver_to_mailroom_test.rb b/test/unit/inbound_email/deliver_to_mailroom_test.rb index c583f4a0a4..ff67057cb8 100644 --- a/test/unit/inbound_email/deliver_to_mailroom_test.rb +++ b/test/unit/inbound_email/deliver_to_mailroom_test.rb @@ -1,8 +1,6 @@ require_relative '../../test_helper' class ActionMailroom::InboundEmail::DeliverToMailroomTest < ActiveSupport::TestCase - include ActiveJob::TestHelper - test "pending emails are delivered to the mailroom" do assert_enqueued_jobs 1, only: ActionMailroom::DeliverInboundEmailToMailroomJob do create_inbound_email("welcome.eml", status: :pending) diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb index 10a6e564f8..72f4b36891 100644 --- a/test/unit/inbound_email/incineration_test.rb +++ b/test/unit/inbound_email/incineration_test.rb @@ -1,8 +1,6 @@ require_relative '../../test_helper' class ActionMailroom::InboundEmail::IncinerationTest < ActiveSupport::TestCase - include ActiveJob::TestHelper - test "incinerate emails 30 days after they have been processed" do freeze_time -- cgit v1.2.3 From af360244d2ceaa67db421c3beaf7ca47ea69e316 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:22:53 -0700 Subject: Temp routing directly to named controller Need the singleton to live somewhere proper. --- app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb index 299e6e6c2e..2a1ca97481 100644 --- a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb +++ b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb @@ -2,6 +2,6 @@ class ActionMailroom::DeliverInboundEmailToMailroomJob < ActiveJob::Base queue_as :action_mailroom_inbound_email def perform(inbound_email) - ActionMailroom::Mailbox.route inbound_email + ApplicationMailbox.route inbound_email end end -- cgit v1.2.3 From 78e7ceb347ae4ca43e749bf33a6636500918ecd2 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:23:14 -0700 Subject: Test routing runs through a job kicked off by the inbound email --- test/unit/mailbox/routing_test.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/unit/mailbox/routing_test.rb b/test/unit/mailbox/routing_test.rb index bdb670e956..9771efcf40 100644 --- a/test/unit/mailbox/routing_test.rb +++ b/test/unit/mailbox/routing_test.rb @@ -20,4 +20,11 @@ class ActionMailroom::Mailbox::RoutingTest < ActiveSupport::TestCase ApplicationMailbox.route @inbound_email assert_equal "Discussion: Let's debate these attachments", $processed end + + test "delayed routing" do + perform_enqueued_jobs only: ActionMailroom::DeliverInboundEmailToMailroomJob do + another_inbound_email = create_inbound_email("welcome.eml", status: :pending) + assert_equal "Discussion: Let's debate these attachments", $processed + end + end end -- cgit v1.2.3 From 3e199600ec1117b6595e402a27d1ae58c18b39e7 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:38:33 -0700 Subject: Routing is a named concept now --- .../action_mailroom/deliver_inbound_email_to_mailroom_job.rb | 7 ------- app/jobs/action_mailroom/routing_job.rb | 7 +++++++ app/models/action_mailroom/inbound_email.rb | 2 +- test/unit/inbound_email/deliver_to_mailroom_test.rb | 9 --------- test/unit/inbound_email/routing_test.rb | 9 +++++++++ test/unit/mailbox/routing_test.rb | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) delete mode 100644 app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb create mode 100644 app/jobs/action_mailroom/routing_job.rb delete mode 100644 test/unit/inbound_email/deliver_to_mailroom_test.rb create mode 100644 test/unit/inbound_email/routing_test.rb diff --git a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb b/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb deleted file mode 100644 index 2a1ca97481..0000000000 --- a/app/jobs/action_mailroom/deliver_inbound_email_to_mailroom_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class ActionMailroom::DeliverInboundEmailToMailroomJob < ActiveJob::Base - queue_as :action_mailroom_inbound_email - - def perform(inbound_email) - ApplicationMailbox.route inbound_email - end -end diff --git a/app/jobs/action_mailroom/routing_job.rb b/app/jobs/action_mailroom/routing_job.rb new file mode 100644 index 0000000000..85a3c7ab00 --- /dev/null +++ b/app/jobs/action_mailroom/routing_job.rb @@ -0,0 +1,7 @@ +class ActionMailroom::RoutingJob < ActiveJob::Base + queue_as :action_mailroom_routing + + def perform(inbound_email) + ApplicationMailbox.route inbound_email + end +end diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 5c351b74b8..d44007c2f1 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -18,6 +18,6 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base private def deliver_to_mailroom_later - ActionMailroom::DeliverInboundEmailToMailroomJob.perform_later self + ActionMailroom::RoutingJob.perform_later self end end diff --git a/test/unit/inbound_email/deliver_to_mailroom_test.rb b/test/unit/inbound_email/deliver_to_mailroom_test.rb deleted file mode 100644 index ff67057cb8..0000000000 --- a/test/unit/inbound_email/deliver_to_mailroom_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require_relative '../../test_helper' - -class ActionMailroom::InboundEmail::DeliverToMailroomTest < ActiveSupport::TestCase - test "pending emails are delivered to the mailroom" do - assert_enqueued_jobs 1, only: ActionMailroom::DeliverInboundEmailToMailroomJob do - create_inbound_email("welcome.eml", status: :pending) - end - end -end diff --git a/test/unit/inbound_email/routing_test.rb b/test/unit/inbound_email/routing_test.rb new file mode 100644 index 0000000000..8f946b24f5 --- /dev/null +++ b/test/unit/inbound_email/routing_test.rb @@ -0,0 +1,9 @@ +require_relative '../../test_helper' + +class ActionMailroom::InboundEmail::RoutingTest < ActiveSupport::TestCase + test "pending emails are delivered to the mailroom" do + assert_enqueued_jobs 1, only: ActionMailroom::RoutingJob do + create_inbound_email("welcome.eml", status: :pending) + end + end +end diff --git a/test/unit/mailbox/routing_test.rb b/test/unit/mailbox/routing_test.rb index 9771efcf40..6026686bea 100644 --- a/test/unit/mailbox/routing_test.rb +++ b/test/unit/mailbox/routing_test.rb @@ -22,7 +22,7 @@ class ActionMailroom::Mailbox::RoutingTest < ActiveSupport::TestCase end test "delayed routing" do - perform_enqueued_jobs only: ActionMailroom::DeliverInboundEmailToMailroomJob do + perform_enqueued_jobs only: ActionMailroom::RoutingJob do another_inbound_email = create_inbound_email("welcome.eml", status: :pending) assert_equal "Discussion: Let's debate these attachments", $processed end -- cgit v1.2.3 From 4b5e1e982740723648638ab06fbe7e38e2e70dc1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:40:56 -0700 Subject: Extract routable concern --- app/models/action_mailroom/inbound_email.rb | 13 ++----------- app/models/action_mailroom/inbound_email/routable.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 app/models/action_mailroom/inbound_email/routable.rb diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index d44007c2f1..c439988e91 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -1,23 +1,14 @@ require "mail" class ActionMailroom::InboundEmail < ActiveRecord::Base - include Incineratable - self.table_name = "action_mailroom_inbound_emails" - has_one_attached :raw_email + include Incineratable, Routable + has_one_attached :raw_email enum status: %i[ pending processing delivered failed bounced ] - after_create_commit :deliver_to_mailroom_later, if: ->(r) { r.pending? } - - def mail @mail ||= Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_email.download)) end - - private - def deliver_to_mailroom_later - ActionMailroom::RoutingJob.perform_later self - end end diff --git a/app/models/action_mailroom/inbound_email/routable.rb b/app/models/action_mailroom/inbound_email/routable.rb new file mode 100644 index 0000000000..b888c592f5 --- /dev/null +++ b/app/models/action_mailroom/inbound_email/routable.rb @@ -0,0 +1,12 @@ +module ActionMailroom::InboundEmail::Routable + extend ActiveSupport::Concern + + included do + after_create_commit :route_later, if: ->(r) { r.pending? } + end + + private + def route_later + ActionMailroom::RoutingJob.perform_later self + end +end -- cgit v1.2.3 From f08c37e6bf2af69d6308d2b8213d98f7b205b2a9 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:43:53 -0700 Subject: Anemic test better covered in mailbox/routing_test.rb --- test/unit/inbound_email/routing_test.rb | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 test/unit/inbound_email/routing_test.rb diff --git a/test/unit/inbound_email/routing_test.rb b/test/unit/inbound_email/routing_test.rb deleted file mode 100644 index 8f946b24f5..0000000000 --- a/test/unit/inbound_email/routing_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require_relative '../../test_helper' - -class ActionMailroom::InboundEmail::RoutingTest < ActiveSupport::TestCase - test "pending emails are delivered to the mailroom" do - assert_enqueued_jobs 1, only: ActionMailroom::RoutingJob do - create_inbound_email("welcome.eml", status: :pending) - end - end -end -- cgit v1.2.3 From c0b0ea392cbe749ab67af027bf4ff5135cfefaa6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:44:48 -0700 Subject: You're not paying by the character --- app/models/action_mailroom/inbound_email/routable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/action_mailroom/inbound_email/routable.rb b/app/models/action_mailroom/inbound_email/routable.rb index b888c592f5..61a14a0ba1 100644 --- a/app/models/action_mailroom/inbound_email/routable.rb +++ b/app/models/action_mailroom/inbound_email/routable.rb @@ -2,7 +2,7 @@ module ActionMailroom::InboundEmail::Routable extend ActiveSupport::Concern included do - after_create_commit :route_later, if: ->(r) { r.pending? } + after_create_commit :route_later, if: ->(inbound_email) { inbound_email.pending? } end private -- cgit v1.2.3 From 04e8ca0c95cdcb149624c540b29094a42a44e1e6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:45:17 -0700 Subject: Needless local var --- test/unit/inbound_email/incineration_test.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb index 72f4b36891..f2464b8cb8 100644 --- a/test/unit/inbound_email/incineration_test.rb +++ b/test/unit/inbound_email/incineration_test.rb @@ -5,8 +5,7 @@ class ActionMailroom::InboundEmail::IncinerationTest < ActiveSupport::TestCase freeze_time assert_enqueued_with job: ActionMailroom::InboundEmail::IncinerationJob, at: 30.days.from_now do - inbound_email = create_inbound_email("welcome.eml") - inbound_email.delivered! + create_inbound_email("welcome.eml").delivered! end end end -- cgit v1.2.3 From c382d9b1800019010afcb441558cf5ebef4acbd3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:54:49 -0700 Subject: Extract method to encapsulate status tracking on inbound email --- lib/action_mailroom/mailbox.rb | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index 3518449794..85b6662136 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -20,16 +20,12 @@ class ActionMailroom::Mailbox end def perform_processing - inbound_email.processing! - - run_callbacks :process do - process + track_status_of_inbound_email do + run_callbacks :process do + process + end end - - inbound_email.delivered! rescue => exception - inbound_email.failed! - # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier rescue_with_handler(exception) || raise end @@ -37,4 +33,14 @@ class ActionMailroom::Mailbox def process # Overwrite in subclasses end + + private + def track_status_of_inbound_email + inbound_email.processing! + yield + inbound_email.delivered! + rescue => exception + inbound_email.failed! + raise + end end -- cgit v1.2.3 From 8cd299286e9dd0c61731fe2d23f316c74d60b6bf Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 16:56:07 -0700 Subject: before_processing should run before the inbound email has been marked as processing --- lib/action_mailroom/mailbox.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index 85b6662136..f9fbb93862 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -20,8 +20,8 @@ class ActionMailroom::Mailbox end def perform_processing - track_status_of_inbound_email do - run_callbacks :process do + run_callbacks :process do + track_status_of_inbound_email do process end end -- cgit v1.2.3 From a166c21a49ddb5e9cca7ecde7cbb671f14a56fef Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 17:08:35 -0700 Subject: Allow inbound emails to be created on the fly --- lib/action_mailroom/test_helper.rb | 16 +++++++++++----- test/unit/inbound_email/incineration_test.rb | 2 +- test/unit/mailbox/callbacks_test.rb | 2 +- test/unit/mailbox/routing_test.rb | 4 ++-- test/unit/mailbox/state_test.rb | 2 +- test/unit/router_test.rb | 8 ++++++-- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb index 06a38f75b6..7f56831f36 100644 --- a/lib/action_mailroom/test_helper.rb +++ b/lib/action_mailroom/test_helper.rb @@ -2,11 +2,17 @@ module ActionMailroom 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(fixture_name, status: :processing) - raw_email = ActiveStorage::Blob.create_after_upload! \ - io: file_fixture(fixture_name).open, filename: fixture_name, content_type: 'message/rfc822' - - ActionMailroom::InboundEmail.create!(status: status, raw_email: raw_email) + def create_inbound_email_from_fixture(fixture_name, status: :processing) + create_inbound_email file_fixture(fixture_name).open, filename: fixture_name, status: status end + + def create_inbound_email_from_mail(status: :processing, &block) + create_inbound_email(StringIO.new(Mail.new { instance_eval(&block) }.to_s), status: status) + end + + def create_inbound_email(io, filename: 'mail.eml', status: status) + ActionMailroom::InboundEmail.create! status: status, raw_email: + ActiveStorage::Blob.create_after_upload!(io: io, filename: filename, content_type: 'message/rfc822') + end end end diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb index f2464b8cb8..3de0af3225 100644 --- a/test/unit/inbound_email/incineration_test.rb +++ b/test/unit/inbound_email/incineration_test.rb @@ -5,7 +5,7 @@ class ActionMailroom::InboundEmail::IncinerationTest < ActiveSupport::TestCase freeze_time assert_enqueued_with job: ActionMailroom::InboundEmail::IncinerationJob, at: 30.days.from_now do - create_inbound_email("welcome.eml").delivered! + create_inbound_email_from_fixture("welcome.eml").delivered! end end end diff --git a/test/unit/mailbox/callbacks_test.rb b/test/unit/mailbox/callbacks_test.rb index ada6410876..4cafeb3534 100644 --- a/test/unit/mailbox/callbacks_test.rb +++ b/test/unit/mailbox/callbacks_test.rb @@ -13,7 +13,7 @@ end class ActionMailroom::Mailbox::CallbacksTest < ActiveSupport::TestCase setup do $before_processing = $after_processing = $around_processing = $processed = false - @inbound_email = create_inbound_email("welcome.eml") + @inbound_email = create_inbound_email_from_fixture("welcome.eml") end test "all callback types" do diff --git a/test/unit/mailbox/routing_test.rb b/test/unit/mailbox/routing_test.rb index 6026686bea..4a8ed10eb0 100644 --- a/test/unit/mailbox/routing_test.rb +++ b/test/unit/mailbox/routing_test.rb @@ -13,7 +13,7 @@ end class ActionMailroom::Mailbox::RoutingTest < ActiveSupport::TestCase setup do $processed = false - @inbound_email = create_inbound_email("welcome.eml") + @inbound_email = create_inbound_email_from_fixture("welcome.eml") end test "string routing" do @@ -23,7 +23,7 @@ class ActionMailroom::Mailbox::RoutingTest < ActiveSupport::TestCase test "delayed routing" do perform_enqueued_jobs only: ActionMailroom::RoutingJob do - another_inbound_email = create_inbound_email("welcome.eml", status: :pending) + another_inbound_email = create_inbound_email_from_fixture("welcome.eml", status: :pending) assert_equal "Discussion: Let's debate these attachments", $processed end end diff --git a/test/unit/mailbox/state_test.rb b/test/unit/mailbox/state_test.rb index 41fbafbd67..d73ecaa1e2 100644 --- a/test/unit/mailbox/state_test.rb +++ b/test/unit/mailbox/state_test.rb @@ -17,7 +17,7 @@ end class ActionMailroom::Mailbox::StateTest < ActiveSupport::TestCase setup do $processed = false - @inbound_email = create_inbound_email("welcome.eml") + @inbound_email = create_inbound_email_from_fixture("welcome.eml") end test "successful mailbox processing leaves inbound email in delivered state" do diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 6bfd880b67..fa7ac2ba19 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -15,8 +15,12 @@ module ActionMailroom end test "routed to mailbox" do - @router.route create_inbound_email("welcome.eml") - assert_equal "Discussion: Let's debate these attachments", $processed + @router.route create_inbound_email_from_mail { + to "replies@example.com" + subject "This is a reply" + } + + assert_equal "This is a reply", $processed end end end -- cgit v1.2.3 From 35654cdbd87f615e848921e50e5ebf17b0f618d2 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 17:13:57 -0700 Subject: Less fancy --- lib/action_mailroom/test_helper.rb | 4 ++-- test/unit/mailbox/state_test.rb | 5 +++-- test/unit/router_test.rb | 6 ++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb index 7f56831f36..bd9d57bd38 100644 --- a/lib/action_mailroom/test_helper.rb +++ b/lib/action_mailroom/test_helper.rb @@ -6,8 +6,8 @@ module ActionMailroom create_inbound_email file_fixture(fixture_name).open, filename: fixture_name, status: status end - def create_inbound_email_from_mail(status: :processing, &block) - create_inbound_email(StringIO.new(Mail.new { instance_eval(&block) }.to_s), status: status) + def create_inbound_email_from_mail(status: :processing, **mail_options) + create_inbound_email(StringIO.new(Mail.new(mail_options).to_s), status: status) end def create_inbound_email(io, filename: 'mail.eml', status: status) diff --git a/test/unit/mailbox/state_test.rb b/test/unit/mailbox/state_test.rb index d73ecaa1e2..de9da54d3f 100644 --- a/test/unit/mailbox/state_test.rb +++ b/test/unit/mailbox/state_test.rb @@ -17,13 +17,14 @@ end class ActionMailroom::Mailbox::StateTest < ActiveSupport::TestCase setup do $processed = false - @inbound_email = create_inbound_email_from_fixture("welcome.eml") + @inbound_email = create_inbound_email_from_mail \ + to: "replies@example.com", subject: "I was processed" end test "successful mailbox processing leaves inbound email in delivered state" do SuccessfulMailbox.receive @inbound_email assert @inbound_email.delivered? - assert_equal "Discussion: Let's debate these attachments", $processed + assert_equal "I was processed", $processed end test "unsuccessful mailbox processing leaves inbound email in failed state" do diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index fa7ac2ba19..3ce669025e 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -15,10 +15,8 @@ module ActionMailroom end test "routed to mailbox" do - @router.route create_inbound_email_from_mail { - to "replies@example.com" - subject "This is a reply" - } + @router.route \ + create_inbound_email_from_mail(to: "replies@example.com", subject: "This is a reply") assert_equal "This is a reply", $processed end -- cgit v1.2.3 From 66dacce5245f27ceecb527f9890aa120204f947a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 19 Sep 2018 17:20:09 -0700 Subject: Test single and multiple string routes --- test/unit/router_test.rb | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 3ce669025e..8cc7bd8810 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -1,24 +1,46 @@ require_relative '../test_helper' -class RepliesMailbox < ActionMailroom::Mailbox +class RootMailbox < ActionMailroom::Mailbox def process - $processed = mail.subject + $processed_by = self.class.to_s + $processed_mail = mail end end +class FirstMailbox < RootMailbox +end + +class SecondMailbox < RootMailbox +end + module ActionMailroom class RouterTest < ActiveSupport::TestCase setup do @router = ActionMailroom::Router.new - @router.add_routes('replies@example.com' => :replies) - $processed = false + $processed_by = $processed_mail = nil end - test "routed to mailbox" do - @router.route \ - create_inbound_email_from_mail(to: "replies@example.com", subject: "This is a reply") + test "single string route" do + @router.add_routes("first@example.com" => :first) + + inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply") + @router.route inbound_email + assert_equal "FirstMailbox", $processed_by + assert_equal inbound_email.mail, $processed_mail + end + + test "multiple string routes" do + @router.add_routes("first@example.com" => :first, "second@example.com" => :second) + + inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply") + @router.route inbound_email + assert_equal "FirstMailbox", $processed_by + assert_equal inbound_email.mail, $processed_mail - assert_equal "This is a reply", $processed + inbound_email = create_inbound_email_from_mail(to: "second@example.com", subject: "This is a reply") + @router.route inbound_email + assert_equal "SecondMailbox", $processed_by + assert_equal inbound_email.mail, $processed_mail end end end -- cgit v1.2.3 From 2d6c79413d01f48df11ef76e65ba45673dcf580c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 20 Sep 2018 16:32:03 -0700 Subject: Use proper default --- lib/action_mailroom/test_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb index bd9d57bd38..fbfb2797d9 100644 --- a/lib/action_mailroom/test_helper.rb +++ b/lib/action_mailroom/test_helper.rb @@ -10,7 +10,7 @@ module ActionMailroom create_inbound_email(StringIO.new(Mail.new(mail_options).to_s), status: status) end - def create_inbound_email(io, filename: 'mail.eml', status: status) + def create_inbound_email(io, filename: 'mail.eml', status: :processing) ActionMailroom::InboundEmail.create! status: status, raw_email: ActiveStorage::Blob.create_after_upload!(io: io, filename: filename, content_type: 'message/rfc822') end -- cgit v1.2.3 From d14f54b0e085128b5305c806a4fe01f07b97b8fa Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 20 Sep 2018 17:16:19 -0700 Subject: Expand router with real routing object and 4-way address options --- lib/action_mailroom/router.rb | 22 +++++++++++++++---- lib/action_mailroom/router/route.rb | 26 ++++++++++++++++++++++ lib/action_mailroom/test_helper.rb | 2 ++ test/unit/router_test.rb | 43 +++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 lib/action_mailroom/router/route.rb diff --git a/lib/action_mailroom/router.rb b/lib/action_mailroom/router.rb index bf0001f1ae..c3fa183438 100644 --- a/lib/action_mailroom/router.rb +++ b/lib/action_mailroom/router.rb @@ -1,20 +1,34 @@ class ActionMailroom::Router + class RoutingError < StandardError; end + def initialize - @routes = {} + @routes = [] end def add_routes(routes) - @routes.merge!(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) - locate_mailbox(inbound_email).receive(inbound_email) + if mailbox = locate_mailbox(inbound_email) + mailbox.receive(inbound_email) + else + raise RoutingError + end end private attr_reader :routes def locate_mailbox(inbound_email) - "#{routes[inbound_email.mail.to.first].to_s.capitalize}Mailbox".constantize + routes.detect { |route| route.match?(inbound_email) }.try(:mailbox_class) end end + +require "action_mailroom/router/route" diff --git a/lib/action_mailroom/router/route.rb b/lib/action_mailroom/router/route.rb new file mode 100644 index 0000000000..316c77d711 --- /dev/null +++ b/lib/action_mailroom/router/route.rb @@ -0,0 +1,26 @@ +class ActionMailroom::Router::Route + class InvalidAddressError < StandardError; end + + attr_reader :address, :mailbox_name + + def initialize(address, to:) + @address, @mailbox_name = address, to + end + + def match?(inbound_email) + case address + when String + inbound_email.mail.to.include?(address) + when Regexp + inbound_email.mail.to.detect { |recipient| address.match?(recipient) } + when Proc + address.call(inbound_email) + else + address.try(:match?, inbound_email) || raise(InvalidAddressError) + end + end + + def mailbox_class + "#{mailbox_name.to_s.capitalize}Mailbox".constantize + end +end diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb index fbfb2797d9..33d32ec899 100644 --- a/lib/action_mailroom/test_helper.rb +++ b/lib/action_mailroom/test_helper.rb @@ -1,3 +1,5 @@ +require "mail" + module ActionMailroom module TestHelper # Create an InboundEmail record using an eml fixture in the format of message/rfc822 diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 8cc7bd8810..25a2651bb6 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -13,6 +13,12 @@ end class SecondMailbox < RootMailbox end +class FirstMailboxAddress + def match?(inbound_email) + inbound_email.mail.to.include?("replies-class@example.com") + end +end + module ActionMailroom class RouterTest < ActiveSupport::TestCase setup do @@ -42,5 +48,42 @@ module ActionMailroom assert_equal "SecondMailbox", $processed_by assert_equal inbound_email.mail, $processed_mail end + + test "single regexp route" do + @router.add_routes(/replies-\w+@example.com/ => :first, "replies-nowhere@example.com" => :second) + + inbound_email = create_inbound_email_from_mail(to: "replies-okay@example.com", subject: "This is a reply") + @router.route inbound_email + assert_equal "FirstMailbox", $processed_by + end + + test "single proc route" do + @router.add_route \ + ->(inbound_email) { inbound_email.mail.to.include?("replies-proc@example.com") }, + to: :second + + @router.route create_inbound_email_from_mail(to: "replies-proc@example.com", subject: "This is a reply") + assert_equal "SecondMailbox", $processed_by + end + + test "address class route" do + @router.add_route FirstMailboxAddress.new, to: :first + @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply") + assert_equal "FirstMailbox", $processed_by + end + + test "missing route" do + assert_raises(ActionMailroom::Router::RoutingError) do + inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply") + @router.route inbound_email + end + end + + test "invalid address" do + assert_raises(ActionMailroom::Router::Route::InvalidAddressError) do + @router.add_route Array.new, to: :first + @router.route create_inbound_email_from_mail(to: "replies-nowhere@example.com", subject: "This is a reply") + end + end end end -- cgit v1.2.3 From 3fc8e5325fe5d9b53e60ce422f3b29ce5a774142 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 20 Sep 2018 17:27:16 -0700 Subject: Style --- lib/action_mailroom/mailbox.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index f9fbb93862..de02e56f13 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -14,7 +14,6 @@ class ActionMailroom::Mailbox new(inbound_email).perform_processing end - def initialize(inbound_email) @inbound_email = inbound_email end -- cgit v1.2.3 From 43bd726442590125d3d07563822790a8fd9827d2 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 20 Sep 2018 17:27:41 -0700 Subject: Stick with match concept --- lib/action_mailroom/router.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/action_mailroom/router.rb b/lib/action_mailroom/router.rb index c3fa183438..fb0b638978 100644 --- a/lib/action_mailroom/router.rb +++ b/lib/action_mailroom/router.rb @@ -16,7 +16,7 @@ class ActionMailroom::Router end def route(inbound_email) - if mailbox = locate_mailbox(inbound_email) + if mailbox = match_to_mailbox(inbound_email) mailbox.receive(inbound_email) else raise RoutingError @@ -26,7 +26,7 @@ class ActionMailroom::Router private attr_reader :routes - def locate_mailbox(inbound_email) + def match_to_mailbox(inbound_email) routes.detect { |route| route.match?(inbound_email) }.try(:mailbox_class) end end -- cgit v1.2.3 From 26f4e359bf7ffc4612b3170e1423aaedfee62ef8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 20 Sep 2018 17:30:36 -0700 Subject: Remember to deal with message_id --- app/models/action_mailroom/inbound_email.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index c439988e91..8526b47e78 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -1,5 +1,6 @@ require "mail" +# TODO: Add email_message_id to the record extracted from raw_email.message_id to make tracing emails easier class ActionMailroom::InboundEmail < ActiveRecord::Base self.table_name = "action_mailroom_inbound_emails" -- cgit v1.2.3 From 8eb239bd1a1350b151d57a639e589c68aed1f47a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 21 Sep 2018 16:44:48 -0700 Subject: Add simply bounce handling Bouncing is not an exceptional state, so let's not use exceptions to deal with it. --- lib/action_mailroom/mailbox.rb | 4 ++-- test/unit/mailbox/state_test.rb | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb index de02e56f13..936054810f 100644 --- a/lib/action_mailroom/mailbox.rb +++ b/lib/action_mailroom/mailbox.rb @@ -8,7 +8,7 @@ class ActionMailroom::Mailbox include Callbacks, Routing attr_reader :inbound_email - delegate :mail, to: :inbound_email + delegate :mail, :bounced!, to: :inbound_email def self.receive(inbound_email) new(inbound_email).perform_processing @@ -37,7 +37,7 @@ class ActionMailroom::Mailbox def track_status_of_inbound_email inbound_email.processing! yield - inbound_email.delivered! + inbound_email.delivered! unless inbound_email.bounced? rescue => exception inbound_email.failed! raise diff --git a/test/unit/mailbox/state_test.rb b/test/unit/mailbox/state_test.rb index de9da54d3f..6215e02837 100644 --- a/test/unit/mailbox/state_test.rb +++ b/test/unit/mailbox/state_test.rb @@ -14,6 +14,14 @@ class UnsuccessfulMailbox < ActionMailroom::Mailbox end end +class BouncingMailbox < ActionMailroom::Mailbox + def process + $processed = :bounced + bounced! + end +end + + class ActionMailroom::Mailbox::StateTest < ActiveSupport::TestCase setup do $processed = false @@ -32,4 +40,10 @@ class ActionMailroom::Mailbox::StateTest < ActiveSupport::TestCase assert @inbound_email.failed? assert_equal :failure, $processed end + + test "bounced inbound emails are not delivered" do + BouncingMailbox.receive @inbound_email + assert @inbound_email.bounced? + assert_equal :bounced, $processed + end end -- cgit v1.2.3 From 96b6e7ce669cf412f931e700fafc99d6d7ef031a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 25 Sep 2018 16:26:53 -0700 Subject: Extract and associate message_id with newly created inbound emails --- .../action_mailroom/inbound_emails_controller.rb | 2 +- app/models/action_mailroom/inbound_email.rb | 17 ++++++++++++++++- .../20180917164000_create_action_mailbox_tables.rb | 1 + lib/action_mailroom/test_helper.rb | 10 ++++++---- .../20180208205311_create_action_mailroom_tables.rb | 1 + test/dummy/db/schema.rb | 1 + test/unit/inbound_email_test.rb | 10 ++++++++++ 7 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 test/unit/inbound_email_test.rb diff --git a/app/controllers/action_mailroom/inbound_emails_controller.rb b/app/controllers/action_mailroom/inbound_emails_controller.rb index e5970b38ec..2aa2a89323 100644 --- a/app/controllers/action_mailroom/inbound_emails_controller.rb +++ b/app/controllers/action_mailroom/inbound_emails_controller.rb @@ -4,7 +4,7 @@ class ActionMailroom::InboundEmailsController < ActionController::Base before_action :require_rfc822_message def create - ActionMailroom::InboundEmail.create!(raw_email: params[:message]) + ActionMailroom::InboundEmail.create_from_raw_email!(params[:message]) head :created end diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 8526b47e78..790e0ccc4f 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -9,7 +9,22 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base has_one_attached :raw_email enum status: %i[ pending processing delivered failed bounced ] + class << self + def create_from_raw_email!(raw_email, **options) + create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options + end + + def mail_from_raw_content(raw_email_content) + Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_email_content.to_s)) + end + + private + def extract_message_id(raw_email) + mail_from_raw_content(raw_email.read).message_id + end + end + def mail - @mail ||= Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_email.download)) + @mail ||= self.class.mail_from_raw_content(raw_email.download) end end diff --git a/db/migrate/20180917164000_create_action_mailbox_tables.rb b/db/migrate/20180917164000_create_action_mailbox_tables.rb index f488919138..cea05a6437 100644 --- a/db/migrate/20180917164000_create_action_mailbox_tables.rb +++ b/db/migrate/20180917164000_create_action_mailbox_tables.rb @@ -2,6 +2,7 @@ class CreateActionMailroomTables < ActiveRecord::Migration[5.2] def change create_table :action_mailroom_inbound_emails do |t| t.integer :status, default: 0, null: false + t.string :message_id t.datetime :created_at, precision: 6 t.datetime :updated_at, precision: 6 diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb index 33d32ec899..8ffbe94e6c 100644 --- a/lib/action_mailroom/test_helper.rb +++ b/lib/action_mailroom/test_helper.rb @@ -5,16 +5,18 @@ module ActionMailroom # 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 file_fixture(fixture_name).open, filename: fixture_name, status: status + create_inbound_email file_fixture(fixture_name), filename: fixture_name, status: status end def create_inbound_email_from_mail(status: :processing, **mail_options) - create_inbound_email(StringIO.new(Mail.new(mail_options).to_s), status: status) + raw_email = Tempfile.new.tap { |raw_email| raw_email.write Mail.new(mail_options).to_s } + create_inbound_email(raw_email, status: status) end def create_inbound_email(io, filename: 'mail.eml', status: :processing) - ActionMailroom::InboundEmail.create! status: status, raw_email: - ActiveStorage::Blob.create_after_upload!(io: io, filename: filename, content_type: 'message/rfc822') + ActionMailroom::InboundEmail.create_from_raw_email! \ + ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'), + status: status end end end diff --git a/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb b/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb index f488919138..cea05a6437 100644 --- a/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb +++ b/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb @@ -2,6 +2,7 @@ class CreateActionMailroomTables < ActiveRecord::Migration[5.2] def change create_table :action_mailroom_inbound_emails do |t| t.integer :status, default: 0, null: false + t.string :message_id t.datetime :created_at, precision: 6 t.datetime :updated_at, precision: 6 diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index cf66062891..339a6b2afd 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -14,6 +14,7 @@ ActiveRecord::Schema.define(version: 2018_02_12_164506) do create_table "action_mailroom_inbound_emails", force: :cascade do |t| t.integer "status", default: 0, null: false + t.string "message_id" t.datetime "created_at", precision: 6 t.datetime "updated_at", precision: 6 end diff --git a/test/unit/inbound_email_test.rb b/test/unit/inbound_email_test.rb new file mode 100644 index 0000000000..3eedd60472 --- /dev/null +++ b/test/unit/inbound_email_test.rb @@ -0,0 +1,10 @@ +require_relative '../test_helper' + +module ActionMailroom + class InboundEmailTest < ActiveSupport::TestCase + test "message id is extracted from raw email" do + inbound_email = create_inbound_email_from_fixture("welcome.eml") + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end + end +end -- cgit v1.2.3 From 1087d70182e066d11c708b8eee43fee25ba1bc3f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 25 Sep 2018 16:58:00 -0700 Subject: Handle all recipients of an email as part of the routing --- lib/action_mailroom/router/route.rb | 9 +++++++-- test/unit/router_test.rb | 11 ++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/action_mailroom/router/route.rb b/lib/action_mailroom/router/route.rb index 316c77d711..6d0b922275 100644 --- a/lib/action_mailroom/router/route.rb +++ b/lib/action_mailroom/router/route.rb @@ -10,9 +10,9 @@ class ActionMailroom::Router::Route def match?(inbound_email) case address when String - inbound_email.mail.to.include?(address) + recipients_from(inbound_email.mail).include?(address) when Regexp - inbound_email.mail.to.detect { |recipient| address.match?(recipient) } + recipients_from(inbound_email.mail).detect { |recipient| address.match?(recipient) } when Proc address.call(inbound_email) else @@ -23,4 +23,9 @@ class ActionMailroom::Router::Route def mailbox_class "#{mailbox_name.to_s.capitalize}Mailbox".constantize end + + private + def recipients_from(mail) + Array(mail.to) + Array(mail.cc) + Array(mail.bcc) + end end diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 25a2651bb6..75701b669e 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -32,7 +32,16 @@ module ActionMailroom inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply") @router.route inbound_email assert_equal "FirstMailbox", $processed_by - assert_equal inbound_email.mail, $processed_mail + assert_equal inbound_email.mail, $processed_mail + end + + test "single string routing on cc" do + @router.add_routes("first@example.com" => :first) + + inbound_email = create_inbound_email_from_mail(to: "someone@example.com", cc: "first@example.com", subject: "This is a reply") + @router.route inbound_email + assert_equal "FirstMailbox", $processed_by + assert_equal inbound_email.mail, $processed_mail end test "multiple string routes" do -- cgit v1.2.3 From 427fa83325848d497901a45f3cd15fa2dff78d19 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 25 Sep 2018 17:16:09 -0700 Subject: Did that --- app/models/action_mailroom/inbound_email.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 790e0ccc4f..7ec54763b1 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -1,6 +1,5 @@ require "mail" -# TODO: Add email_message_id to the record extracted from raw_email.message_id to make tracing emails easier class ActionMailroom::InboundEmail < ActiveRecord::Base self.table_name = "action_mailroom_inbound_emails" -- cgit v1.2.3 From 7362459d60b9070dce0a5dcbe67f26a5e511d5ca Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 15:21:00 -0700 Subject: Add TODOs based on review call --- app/controllers/action_mailroom/inbound_emails_controller.rb | 2 ++ app/models/action_mailroom/inbound_email.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/controllers/action_mailroom/inbound_emails_controller.rb b/app/controllers/action_mailroom/inbound_emails_controller.rb index 2aa2a89323..cf44367b7b 100644 --- a/app/controllers/action_mailroom/inbound_emails_controller.rb +++ b/app/controllers/action_mailroom/inbound_emails_controller.rb @@ -1,4 +1,6 @@ # TODO: Add access protection using basic auth with verified tokens. Maybe coming from credentials by default? +# TODO: Spam/malware catching? +# TODO: Specific bounces for SMTP good citizenship: 200/404/400 class ActionMailroom::InboundEmailsController < ActionController::Base skip_forgery_protection before_action :require_rfc822_message diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 7ec54763b1..f9842eead7 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -20,6 +20,8 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base private def extract_message_id(raw_email) mail_from_raw_content(raw_email.read).message_id + rescue => e + # TODO: Assign message id if it can't be extracted? end end -- cgit v1.2.3 From b190f0fa65b2633f96b352bb1975cd1ce379b972 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 16:43:53 -0700 Subject: Match file and class name --- db/migrate/20180917164000_create_action_mailbox_tables.rb | 11 ----------- db/migrate/20180917164000_create_action_mailroom_tables.rb | 11 +++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 db/migrate/20180917164000_create_action_mailbox_tables.rb create mode 100644 db/migrate/20180917164000_create_action_mailroom_tables.rb diff --git a/db/migrate/20180917164000_create_action_mailbox_tables.rb b/db/migrate/20180917164000_create_action_mailbox_tables.rb deleted file mode 100644 index cea05a6437..0000000000 --- a/db/migrate/20180917164000_create_action_mailbox_tables.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreateActionMailroomTables < ActiveRecord::Migration[5.2] - def change - create_table :action_mailroom_inbound_emails do |t| - t.integer :status, default: 0, null: false - t.string :message_id - - t.datetime :created_at, precision: 6 - t.datetime :updated_at, precision: 6 - end - end -end diff --git a/db/migrate/20180917164000_create_action_mailroom_tables.rb b/db/migrate/20180917164000_create_action_mailroom_tables.rb new file mode 100644 index 0000000000..cea05a6437 --- /dev/null +++ b/db/migrate/20180917164000_create_action_mailroom_tables.rb @@ -0,0 +1,11 @@ +class CreateActionMailroomTables < ActiveRecord::Migration[5.2] + def change + create_table :action_mailroom_inbound_emails do |t| + t.integer :status, default: 0, null: false + t.string :message_id + + t.datetime :created_at, precision: 6 + t.datetime :updated_at, precision: 6 + end + end +end -- cgit v1.2.3 From b25948bd5092026ae70b88954acbb11b133c889c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 16:44:41 -0700 Subject: Make an email that fails to deliver to a mailbox as bounced Probably need a way to either provide more email on the nature of a bounce or have a separate status code for "undeliverable". --- lib/action_mailroom/router.rb | 2 ++ test/unit/router_test.rb | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/action_mailroom/router.rb b/lib/action_mailroom/router.rb index fb0b638978..29ba803e03 100644 --- a/lib/action_mailroom/router.rb +++ b/lib/action_mailroom/router.rb @@ -19,6 +19,8 @@ class ActionMailroom::Router if mailbox = match_to_mailbox(inbound_email) mailbox.receive(inbound_email) else + inbound_email.bounced! + raise RoutingError end end diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 75701b669e..678810a900 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -85,6 +85,7 @@ module ActionMailroom assert_raises(ActionMailroom::Router::RoutingError) do inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply") @router.route inbound_email + assert inbound_email.bounced? end end -- cgit v1.2.3 From 4f0d6c87b15d96a4dba252be833b367bcd675e2b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 16:44:57 -0700 Subject: active_storage:install may halt chain if its already been run --- lib/tasks/action_mailroom.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/action_mailroom.rake b/lib/tasks/action_mailroom.rake index 2cf692cc0f..5c929af119 100644 --- a/lib/tasks/action_mailroom.rake +++ b/lib/tasks/action_mailroom.rake @@ -5,7 +5,7 @@ namespace :action_mailroom do Rake::Task["install:migrations"].clear_comments desc "Copy over the migration" - task install: %w( environment active_storage:install copy_migration ) + task install: %w( environment copy_migration active_storage:install ) task :copy_migration do if Rake::Task.task_defined?("action_mailroom:install:migrations") -- cgit v1.2.3 From b1e08b468b7041ef5df179213f686722b0ead358 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 16:45:59 -0700 Subject: Provide a basic admin interface BEHOLD THE CONDUCTOR! --- .../action_mailroom/inbound_emails_controller.rb | 27 ++++++++++++++++++++-- .../action_mailroom/inbound_emails/index.html.erb | 15 ++++++++++++ .../action_mailroom/inbound_emails/new.html.erb | 8 +++++++ .../action_mailroom/inbound_emails/show.html.erb | 8 +++++++ app/views/layouts/action_mailroom.html.erb | 7 ++++++ config/routes.rb | 4 ++-- 6 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 app/views/action_mailroom/inbound_emails/index.html.erb create mode 100644 app/views/action_mailroom/inbound_emails/new.html.erb create mode 100644 app/views/action_mailroom/inbound_emails/show.html.erb create mode 100644 app/views/layouts/action_mailroom.html.erb diff --git a/app/controllers/action_mailroom/inbound_emails_controller.rb b/app/controllers/action_mailroom/inbound_emails_controller.rb index cf44367b7b..b8fa6cde49 100644 --- a/app/controllers/action_mailroom/inbound_emails_controller.rb +++ b/app/controllers/action_mailroom/inbound_emails_controller.rb @@ -2,15 +2,38 @@ # TODO: Spam/malware catching? # TODO: Specific bounces for SMTP good citizenship: 200/404/400 class ActionMailroom::InboundEmailsController < ActionController::Base + layout "action_mailroom" + skip_forgery_protection - before_action :require_rfc822_message + before_action :ensure_development_env, except: :create + before_action :require_rfc822_message, only: :create + + def index + @inbound_emails = ActionMailroom::InboundEmail.order(created_at: :desc) + end + + def new + end + + def show + @inbound_email = ActionMailroom::InboundEmail.find(params[:id]) + end def create ActionMailroom::InboundEmail.create_from_raw_email!(params[:message]) - head :created + + respond_to do |format| + format.html { redirect_to main_app.rails_new_inbound_email_url } + format.any { head :created } + end end private + # TODO: Should probably separate the admin interface and do more to ensure that it isn't exposed to the web + def ensure_development_env + head :forbidden unless Rails.env.development? + end + def require_rfc822_message head :unsupported_media_type unless params.require(:message).content_type == 'message/rfc822' end diff --git a/app/views/action_mailroom/inbound_emails/index.html.erb b/app/views/action_mailroom/inbound_emails/index.html.erb new file mode 100644 index 0000000000..6636e351be --- /dev/null +++ b/app/views/action_mailroom/inbound_emails/index.html.erb @@ -0,0 +1,15 @@ +<% provide :title, "Deliver new inbound email" %> + +

All inbound emails

+ + + + <% @inbound_emails.each do |inbound_email| %> + + + + + <% end %> +
Message IDStatus
<%= link_to inbound_email.message_id, main_app.rails_inbound_email_path(inbound_email) %><%= inbound_email.status %>
+ +<%= link_to "Deliver new inbound email", main_app.new_rails_inbound_email_path %> \ No newline at end of file diff --git a/app/views/action_mailroom/inbound_emails/new.html.erb b/app/views/action_mailroom/inbound_emails/new.html.erb new file mode 100644 index 0000000000..dfd0bd7ea5 --- /dev/null +++ b/app/views/action_mailroom/inbound_emails/new.html.erb @@ -0,0 +1,8 @@ +<% provide :title, "Deliver new inbound email" %> + +

Deliver new inbound email

+ +<%= form_with(url: main_app.rails_inbound_emails_url, remote: false) do |form| %> + <%= form.file_field :message, size: "150x50", autofocus: true %>
+ <%= form.submit "Deliver inbound email" %> +<% end %> diff --git a/app/views/action_mailroom/inbound_emails/show.html.erb b/app/views/action_mailroom/inbound_emails/show.html.erb new file mode 100644 index 0000000000..bc443775e4 --- /dev/null +++ b/app/views/action_mailroom/inbound_emails/show.html.erb @@ -0,0 +1,8 @@ +<% provide :title, @inbound_email.message_id %> + +

<%= @inbound_email.message_id %>: <%= @inbound_email.status %>

+ +
    +
  • Retry
  • +
  • Incinerate
  • +
diff --git a/app/views/layouts/action_mailroom.html.erb b/app/views/layouts/action_mailroom.html.erb new file mode 100644 index 0000000000..3efa8c3989 --- /dev/null +++ b/app/views/layouts/action_mailroom.html.erb @@ -0,0 +1,7 @@ + + + <%= yield :title %> + + +<%= yield %> + diff --git a/config/routes.rb b/config/routes.rb index a24d5054cf..e19c641a68 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Rails.application.routes.draw do - scope "rails/action_mailroom" do - post "/inbound_emails" => "action_mailroom/inbound_emails#create", as: :rails_inbound_emails + scope 'rails/action_mailroom', module: 'action_mailroom' do + resources :inbound_emails, as: :rails_inbound_emails end end -- cgit v1.2.3 From ab92058bbdb3240dca2fbf51d86a720ee7bce1ca Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 17:37:34 -0700 Subject: Sharpen terminology raw_email_content => source --- app/models/action_mailroom/inbound_email.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index f9842eead7..6dd643073a 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -13,19 +13,23 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options end - def mail_from_raw_content(raw_email_content) - Mail.new(Mail::Utilities.binary_unsafe_to_crlf(raw_email_content.to_s)) + def mail_from_source(source) + Mail.new(Mail::Utilities.binary_unsafe_to_crlf(source.to_s)) end private def extract_message_id(raw_email) - mail_from_raw_content(raw_email.read).message_id + mail_from_source(raw_email.read).message_id rescue => e # TODO: Assign message id if it can't be extracted? end end def mail - @mail ||= self.class.mail_from_raw_content(raw_email.download) + @mail ||= self.class.mail_from_source(source) + end + + def source + @source ||= raw_email.download end end -- cgit v1.2.3 From e1486aa3535005175ae9859daba44b1e1d67f1d1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 17:39:13 -0700 Subject: Flesh out conductor interface --- .../action_mailroom/inbound_emails_controller.rb | 25 +----------------- .../action_mailroom/inbound_emails_controller.rb | 30 ++++++++++++++++++++++ app/controllers/rails/conductor/base_controller.rb | 10 ++++++++ .../action_mailroom/inbound_emails/index.html.erb | 15 ----------- .../action_mailroom/inbound_emails/new.html.erb | 8 ------ .../action_mailroom/inbound_emails/show.html.erb | 8 ------ app/views/layouts/action_mailroom.html.erb | 7 ----- app/views/layouts/rails/conductor.html.erb | 7 +++++ .../action_mailroom/inbound_emails/index.html.erb | 15 +++++++++++ .../action_mailroom/inbound_emails/new.html.erb | 27 +++++++++++++++++++ .../action_mailroom/inbound_emails/show.html.erb | 13 ++++++++++ config/routes.rb | 7 +++-- 12 files changed, 108 insertions(+), 64 deletions(-) create mode 100644 app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb create mode 100644 app/controllers/rails/conductor/base_controller.rb delete mode 100644 app/views/action_mailroom/inbound_emails/index.html.erb delete mode 100644 app/views/action_mailroom/inbound_emails/new.html.erb delete mode 100644 app/views/action_mailroom/inbound_emails/show.html.erb delete mode 100644 app/views/layouts/action_mailroom.html.erb create mode 100644 app/views/layouts/rails/conductor.html.erb create mode 100644 app/views/rails/conductor/action_mailroom/inbound_emails/index.html.erb create mode 100644 app/views/rails/conductor/action_mailroom/inbound_emails/new.html.erb create mode 100644 app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb diff --git a/app/controllers/action_mailroom/inbound_emails_controller.rb b/app/controllers/action_mailroom/inbound_emails_controller.rb index b8fa6cde49..0723bd39c3 100644 --- a/app/controllers/action_mailroom/inbound_emails_controller.rb +++ b/app/controllers/action_mailroom/inbound_emails_controller.rb @@ -2,38 +2,15 @@ # TODO: Spam/malware catching? # TODO: Specific bounces for SMTP good citizenship: 200/404/400 class ActionMailroom::InboundEmailsController < ActionController::Base - layout "action_mailroom" - skip_forgery_protection - before_action :ensure_development_env, except: :create before_action :require_rfc822_message, only: :create - def index - @inbound_emails = ActionMailroom::InboundEmail.order(created_at: :desc) - end - - def new - end - - def show - @inbound_email = ActionMailroom::InboundEmail.find(params[:id]) - end - def create ActionMailroom::InboundEmail.create_from_raw_email!(params[:message]) - - respond_to do |format| - format.html { redirect_to main_app.rails_new_inbound_email_url } - format.any { head :created } - end + head :created end private - # TODO: Should probably separate the admin interface and do more to ensure that it isn't exposed to the web - def ensure_development_env - head :forbidden unless Rails.env.development? - end - def require_rfc822_message head :unsupported_media_type unless params.require(:message).content_type == 'message/rfc822' end diff --git a/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb b/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb new file mode 100644 index 0000000000..2281f5bcae --- /dev/null +++ b/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb @@ -0,0 +1,30 @@ +class Rails::Conductor::ActionMailroom::InboundEmailsController < Rails::Conductor::BaseController + def index + @inbound_emails = ActionMailroom::InboundEmail.order(created_at: :desc) + end + + def new + end + + def show + @inbound_email = ActionMailroom::InboundEmail.find(params[:id]) + end + + def create + inbound_email = create_inbound_email(new_mail) + redirect_to main_app.rails_conductor_inbound_email_url(inbound_email) + end + + private + def new_mail + Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :subject, :body) + end + + def create_inbound_email(mail) + ActionMailroom::InboundEmail.create! raw_email: new_raw_email(mail), message_id: mail.message_id + end + + def new_raw_email(mail) + { io: StringIO.new(new_mail.to_s), filename: 'inbound.eml', content_type: 'message/rfc822', identify: false } + end +end diff --git a/app/controllers/rails/conductor/base_controller.rb b/app/controllers/rails/conductor/base_controller.rb new file mode 100644 index 0000000000..cfa0b84963 --- /dev/null +++ b/app/controllers/rails/conductor/base_controller.rb @@ -0,0 +1,10 @@ +# TODO: Move this to Rails::Conductor gem +class Rails::Conductor::BaseController < ActionController::Base + layout "rails/conductor" + before_action :ensure_development_env + + private + def ensure_development_env + head :forbidden unless Rails.env.development? + end +end diff --git a/app/views/action_mailroom/inbound_emails/index.html.erb b/app/views/action_mailroom/inbound_emails/index.html.erb deleted file mode 100644 index 6636e351be..0000000000 --- a/app/views/action_mailroom/inbound_emails/index.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<% provide :title, "Deliver new inbound email" %> - -

All inbound emails

- - - - <% @inbound_emails.each do |inbound_email| %> - - - - - <% end %> -
Message IDStatus
<%= link_to inbound_email.message_id, main_app.rails_inbound_email_path(inbound_email) %><%= inbound_email.status %>
- -<%= link_to "Deliver new inbound email", main_app.new_rails_inbound_email_path %> \ No newline at end of file diff --git a/app/views/action_mailroom/inbound_emails/new.html.erb b/app/views/action_mailroom/inbound_emails/new.html.erb deleted file mode 100644 index dfd0bd7ea5..0000000000 --- a/app/views/action_mailroom/inbound_emails/new.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% provide :title, "Deliver new inbound email" %> - -

Deliver new inbound email

- -<%= form_with(url: main_app.rails_inbound_emails_url, remote: false) do |form| %> - <%= form.file_field :message, size: "150x50", autofocus: true %>
- <%= form.submit "Deliver inbound email" %> -<% end %> diff --git a/app/views/action_mailroom/inbound_emails/show.html.erb b/app/views/action_mailroom/inbound_emails/show.html.erb deleted file mode 100644 index bc443775e4..0000000000 --- a/app/views/action_mailroom/inbound_emails/show.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% provide :title, @inbound_email.message_id %> - -

<%= @inbound_email.message_id %>: <%= @inbound_email.status %>

- -
    -
  • Retry
  • -
  • Incinerate
  • -
diff --git a/app/views/layouts/action_mailroom.html.erb b/app/views/layouts/action_mailroom.html.erb deleted file mode 100644 index 3efa8c3989..0000000000 --- a/app/views/layouts/action_mailroom.html.erb +++ /dev/null @@ -1,7 +0,0 @@ - - - <%= yield :title %> - - -<%= yield %> - diff --git a/app/views/layouts/rails/conductor.html.erb b/app/views/layouts/rails/conductor.html.erb new file mode 100644 index 0000000000..75157feb78 --- /dev/null +++ b/app/views/layouts/rails/conductor.html.erb @@ -0,0 +1,7 @@ + + + Rails Conductor: <%= yield :title %> + + +<%= yield %> + diff --git a/app/views/rails/conductor/action_mailroom/inbound_emails/index.html.erb b/app/views/rails/conductor/action_mailroom/inbound_emails/index.html.erb new file mode 100644 index 0000000000..19c53984e2 --- /dev/null +++ b/app/views/rails/conductor/action_mailroom/inbound_emails/index.html.erb @@ -0,0 +1,15 @@ +<% provide :title, "Deliver new inbound email" %> + +

All inbound emails

+ + + + <% @inbound_emails.each do |inbound_email| %> + + + + + <% end %> +
Message IDStatus
<%= link_to inbound_email.message_id, main_app.rails_conductor_inbound_email_path(inbound_email) %><%= inbound_email.status %>
+ +<%= link_to "Deliver new inbound email", main_app.new_rails_conductor_inbound_email_path %> \ No newline at end of file diff --git a/app/views/rails/conductor/action_mailroom/inbound_emails/new.html.erb b/app/views/rails/conductor/action_mailroom/inbound_emails/new.html.erb new file mode 100644 index 0000000000..be989ff0bc --- /dev/null +++ b/app/views/rails/conductor/action_mailroom/inbound_emails/new.html.erb @@ -0,0 +1,27 @@ +<% provide :title, "Deliver new inbound email" %> + +

Deliver new inbound email

+ +<%= form_with(url: main_app.rails_conductor_inbound_emails_path, scope: :mail, local: true) do |form| %> +
+ <%= form.label :from, "From" %>
+ <%= form.text_field :from %> +
+ +
+ <%= form.label :to, "To" %>
+ <%= form.text_field :to %> +
+ +
+ <%= form.label :subject, "Subject" %>
+ <%= form.text_field :subject %> +
+ +
+ <%= form.label :body, "Body" %>
+ <%= form.text_area :body, size: "40x20" %> +
+ + <%= form.submit "Deliver inbound email" %> +<% end %> diff --git a/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb b/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb new file mode 100644 index 0000000000..e6f40b19e2 --- /dev/null +++ b/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb @@ -0,0 +1,13 @@ +<% provide :title, @inbound_email.message_id %> + +

<%= @inbound_email.message_id %>: <%= @inbound_email.status %>

+ +
    +
  • Retry
  • +
  • Incinerate
  • +
+ +
+ Full email source +
<%= @inbound_email.source %>
+
diff --git a/config/routes.rb b/config/routes.rb index e19c641a68..26151c338d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true Rails.application.routes.draw do - scope 'rails/action_mailroom', module: 'action_mailroom' do - resources :inbound_emails, as: :rails_inbound_emails + post "/rails/action_mailroom/inbound_emails" => "action_mailroom/inbound_emails#create", as: :rails_inbound_emails + + # TODO: Should these be mounted within the engine only? + scope "rails/conductor/action_mailroom/", module: "rails/conductor/action_mailroom" do + resources :inbound_emails, as: :rails_conductor_inbound_emails end end -- cgit v1.2.3 From 087cbf12f4c2fc3bd43aadd6dd3c60ede302703d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 17:58:30 -0700 Subject: Convert from Parameters object --- .../rails/conductor/action_mailroom/inbound_emails_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb b/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb index 2281f5bcae..0f8ebb00a3 100644 --- a/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb +++ b/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb @@ -17,7 +17,7 @@ class Rails::Conductor::ActionMailroom::InboundEmailsController < Rails::Conduct private def new_mail - Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :subject, :body) + Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :subject, :body).to_h end def create_inbound_email(mail) -- cgit v1.2.3 From 83a44867c7ead753a5014afa08f3f4ebf2efba77 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 17:58:45 -0700 Subject: Inline anemic method --- .../rails/conductor/action_mailroom/inbound_emails_controller.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb b/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb index 0f8ebb00a3..a5a1f34929 100644 --- a/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb +++ b/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb @@ -21,10 +21,7 @@ class Rails::Conductor::ActionMailroom::InboundEmailsController < Rails::Conduct end def create_inbound_email(mail) - ActionMailroom::InboundEmail.create! raw_email: new_raw_email(mail), message_id: mail.message_id - end - - def new_raw_email(mail) - { io: StringIO.new(new_mail.to_s), filename: 'inbound.eml', content_type: 'message/rfc822', identify: false } + ActionMailroom::InboundEmail.create! raw_email: \ + { io: StringIO.new(mail.to_s), filename: 'inbound.eml', content_type: 'message/rfc822', identify: false } end end -- cgit v1.2.3 From cfb927ce754199e93740e53aee4e6b40acb396c5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Sep 2018 17:59:20 -0700 Subject: Ensure message id is present --- app/models/action_mailroom/inbound_email.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 6dd643073a..1f086b24bf 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -8,6 +8,8 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base has_one_attached :raw_email enum status: %i[ pending processing delivered failed bounced ] + before_save :generate_missing_message_id + class << self def create_from_raw_email!(raw_email, **options) create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options @@ -21,7 +23,7 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base def extract_message_id(raw_email) mail_from_source(raw_email.read).message_id rescue => e - # TODO: Assign message id if it can't be extracted? + # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" end end @@ -32,4 +34,9 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base def source @source ||= raw_email.download end + + private + def generate_missing_message_id + self.message_id ||= Mail::MessageIdField.new.message_id + end end -- cgit v1.2.3 From 456d79b853aaec14f14c4c4deaa04cfb66c5b357 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 28 Sep 2018 11:01:49 -0700 Subject: Extract MessageId concern --- .../action_mailroom/inbound_emails_controller.rb | 2 +- app/models/action_mailroom/inbound_email.rb | 26 +++------------------- .../action_mailroom/inbound_email/message_id.rb | 25 +++++++++++++++++++++ lib/action_mailroom/test_helper.rb | 2 +- 4 files changed, 30 insertions(+), 25 deletions(-) create mode 100644 app/models/action_mailroom/inbound_email/message_id.rb diff --git a/app/controllers/action_mailroom/inbound_emails_controller.rb b/app/controllers/action_mailroom/inbound_emails_controller.rb index 0723bd39c3..57e0530ac6 100644 --- a/app/controllers/action_mailroom/inbound_emails_controller.rb +++ b/app/controllers/action_mailroom/inbound_emails_controller.rb @@ -6,7 +6,7 @@ class ActionMailroom::InboundEmailsController < ActionController::Base before_action :require_rfc822_message, only: :create def create - ActionMailroom::InboundEmail.create_from_raw_email!(params[:message]) + ActionMailroom::InboundEmail.create_and_extract_message_id!(params[:message]) head :created end diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb index 1f086b24bf..cf7370d543 100644 --- a/app/models/action_mailroom/inbound_email.rb +++ b/app/models/action_mailroom/inbound_email.rb @@ -3,28 +3,13 @@ require "mail" class ActionMailroom::InboundEmail < ActiveRecord::Base self.table_name = "action_mailroom_inbound_emails" - include Incineratable, Routable + include Incineratable, MessageId, Routable has_one_attached :raw_email enum status: %i[ pending processing delivered failed bounced ] - before_save :generate_missing_message_id - - class << self - def create_from_raw_email!(raw_email, **options) - create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options - end - - def mail_from_source(source) - Mail.new(Mail::Utilities.binary_unsafe_to_crlf(source.to_s)) - end - - private - def extract_message_id(raw_email) - mail_from_source(raw_email.read).message_id - rescue => e - # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" - end + def self.mail_from_source(source) + Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s) end def mail @@ -34,9 +19,4 @@ class ActionMailroom::InboundEmail < ActiveRecord::Base def source @source ||= raw_email.download end - - private - def generate_missing_message_id - self.message_id ||= Mail::MessageIdField.new.message_id - end end diff --git a/app/models/action_mailroom/inbound_email/message_id.rb b/app/models/action_mailroom/inbound_email/message_id.rb new file mode 100644 index 0000000000..6bdda8c1c7 --- /dev/null +++ b/app/models/action_mailroom/inbound_email/message_id.rb @@ -0,0 +1,25 @@ +module ActionMailroom::InboundEmail::MessageId + extend ActiveSupport::Concern + + included do + before_save :generate_missing_message_id + end + + module ClassMethods + def create_and_extract_message_id!(raw_email, **options) + create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options + end + + private + def extract_message_id(raw_email) + mail_from_source(raw_email.read).message_id + rescue => e + # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" + end + end + + private + def generate_missing_message_id + self.message_id ||= Mail::MessageIdField.new.message_id + end +end diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb index 8ffbe94e6c..3e1ff87839 100644 --- a/lib/action_mailroom/test_helper.rb +++ b/lib/action_mailroom/test_helper.rb @@ -14,7 +14,7 @@ module ActionMailroom end def create_inbound_email(io, filename: 'mail.eml', status: :processing) - ActionMailroom::InboundEmail.create_from_raw_email! \ + ActionMailroom::InboundEmail.create_and_extract_message_id! \ ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'), status: status end -- cgit v1.2.3 From c9d8f6650d6c5c88c5346032b23d037cf3e4aae6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 28 Sep 2018 11:12:51 -0700 Subject: Flesh out testing --- test/unit/inbound_email/message_id_test.rb | 15 +++++++++++++++ test/unit/inbound_email_test.rb | 9 ++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 test/unit/inbound_email/message_id_test.rb diff --git a/test/unit/inbound_email/message_id_test.rb b/test/unit/inbound_email/message_id_test.rb new file mode 100644 index 0000000000..75c15a531a --- /dev/null +++ b/test/unit/inbound_email/message_id_test.rb @@ -0,0 +1,15 @@ +require_relative '../../test_helper' + +class ActionMailroom::InboundEmail::MessageIdTest < ActiveSupport::TestCase + test "message id is extracted from raw email" do + inbound_email = create_inbound_email_from_fixture("welcome.eml") + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end + + test "message id is generated if its missing" do + source_without_message_id = "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!" + inbound_email = create_inbound_email Tempfile.new.tap { |raw_email| raw_email.write source_without_message_id } + + assert_not_nil inbound_email.message_id + end +end diff --git a/test/unit/inbound_email_test.rb b/test/unit/inbound_email_test.rb index 3eedd60472..e4b7be2440 100644 --- a/test/unit/inbound_email_test.rb +++ b/test/unit/inbound_email_test.rb @@ -2,9 +2,12 @@ require_relative '../test_helper' module ActionMailroom class InboundEmailTest < ActiveSupport::TestCase - test "message id is extracted from raw email" do - inbound_email = create_inbound_email_from_fixture("welcome.eml") - assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + test "mail provides the parsed source" do + assert_equal "Discussion: Let's debate these attachments", create_inbound_email_from_fixture("welcome.eml").mail.subject + end + + test "source returns the contents of the raw email" do + assert_equal file_fixture("welcome.eml").read, create_inbound_email_from_fixture("welcome.eml").source end end end -- cgit v1.2.3 From 5ad0813322820a6c42d7b3074531ac40108bfb69 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 28 Sep 2018 11:32:44 -0700 Subject: Add rerouting option --- .../rails/conductor/action_mailroom/reroutes_controller.rb | 14 ++++++++++++++ app/models/action_mailroom/inbound_email/routable.rb | 7 +++---- .../conductor/action_mailroom/inbound_emails/show.html.erb | 4 +++- config/routes.rb | 1 + 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 app/controllers/rails/conductor/action_mailroom/reroutes_controller.rb diff --git a/app/controllers/rails/conductor/action_mailroom/reroutes_controller.rb b/app/controllers/rails/conductor/action_mailroom/reroutes_controller.rb new file mode 100644 index 0000000000..028ed9e2d6 --- /dev/null +++ b/app/controllers/rails/conductor/action_mailroom/reroutes_controller.rb @@ -0,0 +1,14 @@ +class Rails::Conductor::ActionMailroom::ReroutesController < Rails::Conductor::BaseController + def create + inbound_email = ActionMailroom::InboundEmail.find(params[:inbound_email_id]) + reroute inbound_email + + redirect_to main_app.rails_conductor_inbound_email_url(inbound_email) + end + + private + def reroute(inbound_email) + inbound_email.pending! + inbound_email.route_later + end +end diff --git a/app/models/action_mailroom/inbound_email/routable.rb b/app/models/action_mailroom/inbound_email/routable.rb index 61a14a0ba1..5075db326e 100644 --- a/app/models/action_mailroom/inbound_email/routable.rb +++ b/app/models/action_mailroom/inbound_email/routable.rb @@ -5,8 +5,7 @@ module ActionMailroom::InboundEmail::Routable after_create_commit :route_later, if: ->(inbound_email) { inbound_email.pending? } end - private - def route_later - ActionMailroom::RoutingJob.perform_later self - end + def route_later + ActionMailroom::RoutingJob.perform_later self + end end diff --git a/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb b/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb index e6f40b19e2..e761904196 100644 --- a/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb +++ b/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb @@ -3,7 +3,7 @@

<%= @inbound_email.message_id %>: <%= @inbound_email.status %>

    -
  • Retry
  • +
  • <%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %>
  • Incinerate
@@ -11,3 +11,5 @@ Full email source
<%= @inbound_email.source %>
+ +<%= link_to "Back to all inbound emails", main_app.rails_conductor_inbound_emails_path %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 26151c338d..b44ddf648a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,5 +6,6 @@ Rails.application.routes.draw do # TODO: Should these be mounted within the engine only? scope "rails/conductor/action_mailroom/", module: "rails/conductor/action_mailroom" do resources :inbound_emails, as: :rails_conductor_inbound_emails + post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute end end -- cgit v1.2.3 From 8a0a1034955544ee2e4c1f85317c0db84f3aa55b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 28 Sep 2018 12:19:43 -0700 Subject: ActionMailroom -> ActionMailbox We didn't end up using the mailroom metaphor directly, so let's stick with a more conventional naming strategy. --- Gemfile.lock | 4 +- actionmailbox.gemspec | 26 ++++++++++++ actionmailroom.gemspec | 26 ------------ app/.DS_Store | Bin 0 -> 6148 bytes app/controllers/.DS_Store | Bin 0 -> 6148 bytes .../action_mailbox/inbound_emails_controller.rb | 17 ++++++++ .../action_mailroom/inbound_emails_controller.rb | 17 -------- app/controllers/rails/.DS_Store | Bin 0 -> 6148 bytes .../action_mailbox/inbound_emails_controller.rb | 27 +++++++++++++ .../action_mailbox/reroutes_controller.rb | 14 +++++++ .../action_mailroom/inbound_emails_controller.rb | 27 ------------- .../action_mailroom/reroutes_controller.rb | 14 ------- app/jobs/.DS_Store | Bin 0 -> 6148 bytes .../inbound_email/incineration_job.rb | 11 +++++ app/jobs/action_mailbox/routing_job.rb | 7 ++++ .../inbound_email/incineration_job.rb | 11 ----- app/jobs/action_mailroom/routing_job.rb | 7 ---- app/models/.DS_Store | Bin 0 -> 6148 bytes app/models/action_mailbox/inbound_email.rb | 22 ++++++++++ .../action_mailbox/inbound_email/incineratable.rb | 31 ++++++++++++++ .../inbound_email/incineratable/incineration.rb | 18 +++++++++ .../action_mailbox/inbound_email/message_id.rb | 25 ++++++++++++ .../action_mailbox/inbound_email/routable.rb | 11 +++++ app/models/action_mailroom/inbound_email.rb | 22 ---------- .../action_mailroom/inbound_email/incineratable.rb | 31 -------------- .../inbound_email/incineratable/incineration.rb | 18 --------- .../action_mailroom/inbound_email/message_id.rb | 25 ------------ .../action_mailroom/inbound_email/routable.rb | 11 ----- app/views/.DS_Store | Bin 0 -> 6148 bytes app/views/layouts/.DS_Store | Bin 0 -> 6148 bytes app/views/rails/.DS_Store | Bin 0 -> 6148 bytes app/views/rails/conductor/.DS_Store | Bin 0 -> 6148 bytes .../action_mailbox/inbound_emails/index.html.erb | 15 +++++++ .../action_mailbox/inbound_emails/new.html.erb | 27 +++++++++++++ .../action_mailbox/inbound_emails/show.html.erb | 15 +++++++ .../action_mailroom/inbound_emails/index.html.erb | 15 ------- .../action_mailroom/inbound_emails/new.html.erb | 27 ------------- .../action_mailroom/inbound_emails/show.html.erb | 15 ------- config/routes.rb | 4 +- db/.DS_Store | Bin 0 -> 6148 bytes .../20180917164000_create_action_mailbox_tables.rb | 11 +++++ ...20180917164000_create_action_mailroom_tables.rb | 11 ----- lib/.DS_Store | Bin 0 -> 6148 bytes lib/action_mailbox.rb | 10 +++++ lib/action_mailbox/base.rb | 45 +++++++++++++++++++++ lib/action_mailbox/callbacks.rb | 26 ++++++++++++ lib/action_mailbox/engine.rb | 14 +++++++ lib/action_mailbox/router.rb | 36 +++++++++++++++++ lib/action_mailbox/router/route.rb | 31 ++++++++++++++ lib/action_mailbox/routing.rb | 17 ++++++++ lib/action_mailbox/test_helper.rb | 22 ++++++++++ lib/action_mailbox/version.rb | 3 ++ lib/action_mailroom.rb | 8 ---- lib/action_mailroom/engine.rb | 14 ------- lib/action_mailroom/mailbox.rb | 45 --------------------- lib/action_mailroom/mailbox/callbacks.rb | 28 ------------- lib/action_mailroom/mailbox/routing.rb | 15 ------- lib/action_mailroom/router.rb | 36 ----------------- lib/action_mailroom/router/route.rb | 31 -------------- lib/action_mailroom/test_helper.rb | 22 ---------- lib/action_mailroom/version.rb | 3 -- lib/tasks/action_mailbox.rake | 17 ++++++++ lib/tasks/action_mailroom.rake | 17 -------- test/.DS_Store | Bin 0 -> 6148 bytes test/dummy/app/mailboxes/application_mailbox.rb | 2 +- test/dummy/config/application.rb | 2 +- test/dummy/db/development.sqlite3 | Bin 49152 -> 0 bytes ...20180208205311_create_action_mailroom_tables.rb | 4 +- test/dummy/db/schema.rb | 2 +- test/test_helper.rb | 4 +- test/unit/.DS_Store | Bin 0 -> 6148 bytes test/unit/controller_test.rb | 8 ++-- test/unit/inbound_email/incineration_test.rb | 4 +- test/unit/inbound_email/message_id_test.rb | 2 +- test/unit/inbound_email_test.rb | 2 +- test/unit/mailbox/callbacks_test.rb | 4 +- test/unit/mailbox/routing_test.rb | 8 ++-- test/unit/mailbox/state_test.rb | 8 ++-- test/unit/router_test.rb | 10 ++--- 79 files changed, 532 insertions(+), 530 deletions(-) create mode 100644 actionmailbox.gemspec delete mode 100644 actionmailroom.gemspec create mode 100644 app/.DS_Store create mode 100644 app/controllers/.DS_Store create mode 100644 app/controllers/action_mailbox/inbound_emails_controller.rb delete mode 100644 app/controllers/action_mailroom/inbound_emails_controller.rb create mode 100644 app/controllers/rails/.DS_Store create mode 100644 app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb create mode 100644 app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb delete mode 100644 app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb delete mode 100644 app/controllers/rails/conductor/action_mailroom/reroutes_controller.rb create mode 100644 app/jobs/.DS_Store create mode 100644 app/jobs/action_mailbox/inbound_email/incineration_job.rb create mode 100644 app/jobs/action_mailbox/routing_job.rb delete mode 100644 app/jobs/action_mailroom/inbound_email/incineration_job.rb delete mode 100644 app/jobs/action_mailroom/routing_job.rb create mode 100644 app/models/.DS_Store create mode 100644 app/models/action_mailbox/inbound_email.rb create mode 100644 app/models/action_mailbox/inbound_email/incineratable.rb create mode 100644 app/models/action_mailbox/inbound_email/incineratable/incineration.rb create mode 100644 app/models/action_mailbox/inbound_email/message_id.rb create mode 100644 app/models/action_mailbox/inbound_email/routable.rb delete mode 100644 app/models/action_mailroom/inbound_email.rb delete mode 100644 app/models/action_mailroom/inbound_email/incineratable.rb delete mode 100644 app/models/action_mailroom/inbound_email/incineratable/incineration.rb delete mode 100644 app/models/action_mailroom/inbound_email/message_id.rb delete mode 100644 app/models/action_mailroom/inbound_email/routable.rb create mode 100644 app/views/.DS_Store create mode 100644 app/views/layouts/.DS_Store create mode 100644 app/views/rails/.DS_Store create mode 100644 app/views/rails/conductor/.DS_Store create mode 100644 app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb create mode 100644 app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb create mode 100644 app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb delete mode 100644 app/views/rails/conductor/action_mailroom/inbound_emails/index.html.erb delete mode 100644 app/views/rails/conductor/action_mailroom/inbound_emails/new.html.erb delete mode 100644 app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb create mode 100644 db/.DS_Store create mode 100644 db/migrate/20180917164000_create_action_mailbox_tables.rb delete mode 100644 db/migrate/20180917164000_create_action_mailroom_tables.rb create mode 100644 lib/.DS_Store create mode 100644 lib/action_mailbox.rb create mode 100644 lib/action_mailbox/base.rb create mode 100644 lib/action_mailbox/callbacks.rb create mode 100644 lib/action_mailbox/engine.rb create mode 100644 lib/action_mailbox/router.rb create mode 100644 lib/action_mailbox/router/route.rb create mode 100644 lib/action_mailbox/routing.rb create mode 100644 lib/action_mailbox/test_helper.rb create mode 100644 lib/action_mailbox/version.rb delete mode 100644 lib/action_mailroom.rb delete mode 100644 lib/action_mailroom/engine.rb delete mode 100644 lib/action_mailroom/mailbox.rb delete mode 100644 lib/action_mailroom/mailbox/callbacks.rb delete mode 100644 lib/action_mailroom/mailbox/routing.rb delete mode 100644 lib/action_mailroom/router.rb delete mode 100644 lib/action_mailroom/router/route.rb delete mode 100644 lib/action_mailroom/test_helper.rb delete mode 100644 lib/action_mailroom/version.rb create mode 100644 lib/tasks/action_mailbox.rake delete mode 100644 lib/tasks/action_mailroom.rake create mode 100644 test/.DS_Store delete mode 100644 test/dummy/db/development.sqlite3 create mode 100644 test/unit/.DS_Store diff --git a/Gemfile.lock b/Gemfile.lock index 0ceac134ff..b3f3ab1f70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - actionmailroom (0.1.0) + actionmailbox (0.1.0) rails (>= 5.2.0) GEM @@ -121,7 +121,7 @@ PLATFORMS ruby DEPENDENCIES - actionmailroom! + actionmailbox! bundler (~> 1.15) byebug sqlite3 diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec new file mode 100644 index 0000000000..e413ac57f6 --- /dev/null +++ b/actionmailbox.gemspec @@ -0,0 +1,26 @@ +$:.push File.expand_path("lib", __dir__) + +# Maintain your gem's version: +require "action_mailbox/version" + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = "actionmailbox" + s.version = ActionMailbox::VERSION + s.authors = ["Jeremy Daer", "David Heinemeier Hansson"] + s.email = ["jeremy@basecamp.com", "david@loudthinking.com"] + s.summary = "Receive and process incoming emails in Rails" + s.homepage = "https://github.com/basecamp/actionmailbox" + s.license = "MIT" + + s.required_ruby_version = ">= 2.5.0" + + s.add_dependency "rails", ">= 5.2.0" + + s.add_development_dependency "bundler", "~> 1.15" + s.add_development_dependency "sqlite3" + s.add_development_dependency "byebug" + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- test/*`.split("\n") +end diff --git a/actionmailroom.gemspec b/actionmailroom.gemspec deleted file mode 100644 index 217904d62f..0000000000 --- a/actionmailroom.gemspec +++ /dev/null @@ -1,26 +0,0 @@ -$:.push File.expand_path("lib", __dir__) - -# Maintain your gem's version: -require "action_mailroom/version" - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "actionmailroom" - s.version = ActionMailroom::VERSION - s.authors = ["Jeremy Daer", "David Heinemeier Hansson"] - s.email = ["jeremy@basecamp.com", "david@loudthinking.com"] - s.summary = "Receive and process incoming emails in Rails" - s.homepage = "https://github.com/basecamp/actionmailroom" - s.license = "MIT" - - s.required_ruby_version = ">= 2.5.0" - - s.add_dependency "rails", ">= 5.2.0" - - s.add_development_dependency "bundler", "~> 1.15" - s.add_development_dependency "sqlite3" - s.add_development_dependency "byebug" - - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- test/*`.split("\n") -end diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000000..6719b65737 Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/controllers/.DS_Store b/app/controllers/.DS_Store new file mode 100644 index 0000000000..ea1205963a Binary files /dev/null and b/app/controllers/.DS_Store differ diff --git a/app/controllers/action_mailbox/inbound_emails_controller.rb b/app/controllers/action_mailbox/inbound_emails_controller.rb new file mode 100644 index 0000000000..ec9bd6f229 --- /dev/null +++ b/app/controllers/action_mailbox/inbound_emails_controller.rb @@ -0,0 +1,17 @@ +# TODO: Add access protection using basic auth with verified tokens. Maybe coming from credentials by default? +# TODO: Spam/malware catching? +# TODO: Specific bounces for SMTP good citizenship: 200/404/400 +class ActionMailbox::InboundEmailsController < ActionController::Base + skip_forgery_protection + before_action :require_rfc822_message, only: :create + + def create + ActionMailbox::InboundEmail.create_and_extract_message_id!(params[:message]) + head :created + end + + private + def require_rfc822_message + head :unsupported_media_type unless params.require(:message).content_type == 'message/rfc822' + end +end diff --git a/app/controllers/action_mailroom/inbound_emails_controller.rb b/app/controllers/action_mailroom/inbound_emails_controller.rb deleted file mode 100644 index 57e0530ac6..0000000000 --- a/app/controllers/action_mailroom/inbound_emails_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# TODO: Add access protection using basic auth with verified tokens. Maybe coming from credentials by default? -# TODO: Spam/malware catching? -# TODO: Specific bounces for SMTP good citizenship: 200/404/400 -class ActionMailroom::InboundEmailsController < ActionController::Base - skip_forgery_protection - before_action :require_rfc822_message, only: :create - - def create - ActionMailroom::InboundEmail.create_and_extract_message_id!(params[:message]) - head :created - end - - private - def require_rfc822_message - head :unsupported_media_type unless params.require(:message).content_type == 'message/rfc822' - end -end diff --git a/app/controllers/rails/.DS_Store b/app/controllers/rails/.DS_Store new file mode 100644 index 0000000000..627263b12d Binary files /dev/null and b/app/controllers/rails/.DS_Store differ diff --git a/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb b/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb new file mode 100644 index 0000000000..70537da9c4 --- /dev/null +++ b/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb @@ -0,0 +1,27 @@ +class Rails::Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController + def index + @inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc) + end + + def new + end + + def show + @inbound_email = ActionMailbox::InboundEmail.find(params[:id]) + end + + def create + inbound_email = create_inbound_email(new_mail) + redirect_to main_app.rails_conductor_inbound_email_url(inbound_email) + end + + private + def new_mail + Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :subject, :body).to_h + end + + def create_inbound_email(mail) + ActionMailbox::InboundEmail.create! raw_email: \ + { io: StringIO.new(mail.to_s), filename: 'inbound.eml', content_type: 'message/rfc822', identify: false } + end +end diff --git a/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb b/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb new file mode 100644 index 0000000000..226116a3d6 --- /dev/null +++ b/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb @@ -0,0 +1,14 @@ +class Rails::Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController + def create + inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id]) + reroute inbound_email + + redirect_to main_app.rails_conductor_inbound_email_url(inbound_email) + end + + private + def reroute(inbound_email) + inbound_email.pending! + inbound_email.route_later + end +end diff --git a/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb b/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb deleted file mode 100644 index a5a1f34929..0000000000 --- a/app/controllers/rails/conductor/action_mailroom/inbound_emails_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -class Rails::Conductor::ActionMailroom::InboundEmailsController < Rails::Conductor::BaseController - def index - @inbound_emails = ActionMailroom::InboundEmail.order(created_at: :desc) - end - - def new - end - - def show - @inbound_email = ActionMailroom::InboundEmail.find(params[:id]) - end - - def create - inbound_email = create_inbound_email(new_mail) - redirect_to main_app.rails_conductor_inbound_email_url(inbound_email) - end - - private - def new_mail - Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :subject, :body).to_h - end - - def create_inbound_email(mail) - ActionMailroom::InboundEmail.create! raw_email: \ - { io: StringIO.new(mail.to_s), filename: 'inbound.eml', content_type: 'message/rfc822', identify: false } - end -end diff --git a/app/controllers/rails/conductor/action_mailroom/reroutes_controller.rb b/app/controllers/rails/conductor/action_mailroom/reroutes_controller.rb deleted file mode 100644 index 028ed9e2d6..0000000000 --- a/app/controllers/rails/conductor/action_mailroom/reroutes_controller.rb +++ /dev/null @@ -1,14 +0,0 @@ -class Rails::Conductor::ActionMailroom::ReroutesController < Rails::Conductor::BaseController - def create - inbound_email = ActionMailroom::InboundEmail.find(params[:inbound_email_id]) - reroute inbound_email - - redirect_to main_app.rails_conductor_inbound_email_url(inbound_email) - end - - private - def reroute(inbound_email) - inbound_email.pending! - inbound_email.route_later - end -end diff --git a/app/jobs/.DS_Store b/app/jobs/.DS_Store new file mode 100644 index 0000000000..4f6e8d17e4 Binary files /dev/null and b/app/jobs/.DS_Store differ diff --git a/app/jobs/action_mailbox/inbound_email/incineration_job.rb b/app/jobs/action_mailbox/inbound_email/incineration_job.rb new file mode 100644 index 0000000000..a2911efef1 --- /dev/null +++ b/app/jobs/action_mailbox/inbound_email/incineration_job.rb @@ -0,0 +1,11 @@ +class ActionMailbox::InboundEmail::IncinerationJob < ApplicationJob + queue_as :action_mailbox_incineration + + def self.schedule(inbound_email) + set(wait: ActionMailbox::InboundEmail::Incineratable::INCINERATABLE_AFTER).perform_later(inbound_email) + end + + def perform(inbound_email) + inbound_email.incinerate + end +end diff --git a/app/jobs/action_mailbox/routing_job.rb b/app/jobs/action_mailbox/routing_job.rb new file mode 100644 index 0000000000..a2618bb8aa --- /dev/null +++ b/app/jobs/action_mailbox/routing_job.rb @@ -0,0 +1,7 @@ +class ActionMailbox::RoutingJob < ActiveJob::Base + queue_as :action_mailbox_routing + + def perform(inbound_email) + ApplicationMailbox.route inbound_email + end +end diff --git a/app/jobs/action_mailroom/inbound_email/incineration_job.rb b/app/jobs/action_mailroom/inbound_email/incineration_job.rb deleted file mode 100644 index fa1d346008..0000000000 --- a/app/jobs/action_mailroom/inbound_email/incineration_job.rb +++ /dev/null @@ -1,11 +0,0 @@ -class ActionMailroom::InboundEmail::IncinerationJob < ApplicationJob - queue_as :action_mailroom_incineration - - def self.schedule(inbound_email) - set(wait: ActionMailroom::InboundEmail::Incineratable::INCINERATABLE_AFTER).perform_later(inbound_email) - end - - def perform(inbound_email) - inbound_email.incinerate - end -end diff --git a/app/jobs/action_mailroom/routing_job.rb b/app/jobs/action_mailroom/routing_job.rb deleted file mode 100644 index 85a3c7ab00..0000000000 --- a/app/jobs/action_mailroom/routing_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class ActionMailroom::RoutingJob < ActiveJob::Base - queue_as :action_mailroom_routing - - def perform(inbound_email) - ApplicationMailbox.route inbound_email - end -end diff --git a/app/models/.DS_Store b/app/models/.DS_Store new file mode 100644 index 0000000000..4f6e8d17e4 Binary files /dev/null and b/app/models/.DS_Store differ diff --git a/app/models/action_mailbox/inbound_email.rb b/app/models/action_mailbox/inbound_email.rb new file mode 100644 index 0000000000..f2589d7429 --- /dev/null +++ b/app/models/action_mailbox/inbound_email.rb @@ -0,0 +1,22 @@ +require "mail" + +class ActionMailbox::InboundEmail < ActiveRecord::Base + self.table_name = "action_mailbox_inbound_emails" + + include Incineratable, MessageId, Routable + + has_one_attached :raw_email + enum status: %i[ pending processing delivered failed bounced ] + + def self.mail_from_source(source) + Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s) + end + + def mail + @mail ||= self.class.mail_from_source(source) + end + + def source + @source ||= raw_email.download + end +end diff --git a/app/models/action_mailbox/inbound_email/incineratable.rb b/app/models/action_mailbox/inbound_email/incineratable.rb new file mode 100644 index 0000000000..6ba73c0c6d --- /dev/null +++ b/app/models/action_mailbox/inbound_email/incineratable.rb @@ -0,0 +1,31 @@ +module ActionMailbox::InboundEmail::Incineratable + extend ActiveSupport::Concern + + # TODO: Extract into framework configuration + INCINERATABLE_AFTER = 30.days + + included do + before_update :remember_to_incinerate_later + after_update_commit :incinerate_later, if: :need_to_incinerate_later? + end + + def incinerate + Incineration.new(self).run + end + + private + # TODO: Use enum change tracking once merged into Active Support + def remember_to_incinerate_later + if status_changed? && (delivered? || failed?) + @incinerate_later = true + end + end + + def need_to_incinerate_later? + @incinerate_later + end + + def incinerate_later + ActionMailbox::InboundEmail::IncinerationJob.schedule(self) + end +end diff --git a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb new file mode 100644 index 0000000000..bd2bf7d91e --- /dev/null +++ b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb @@ -0,0 +1,18 @@ +class ActionMailbox::InboundEmail::Incineratable::Incineration + def initialize(inbound_email) + @inbound_email = inbound_email + end + + def run + @inbound_email.destroy if due? && processed? + end + + private + def due? + @inbound_email.updated_at < ActionMailbox::InboundEmail::Incineratable::INCINERATABLE_AFTER.ago.end_of_day + end + + def processed? + @inbound_email.delivered? || @inbound_email.failed? + end +end diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb new file mode 100644 index 0000000000..590dbfc4d7 --- /dev/null +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -0,0 +1,25 @@ +module ActionMailbox::InboundEmail::MessageId + extend ActiveSupport::Concern + + included do + before_save :generate_missing_message_id + end + + module ClassMethods + def create_and_extract_message_id!(raw_email, **options) + create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options + end + + private + def extract_message_id(raw_email) + mail_from_source(raw_email.read).message_id + rescue => e + # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" + end + end + + private + def generate_missing_message_id + self.message_id ||= Mail::MessageIdField.new.message_id + end +end diff --git a/app/models/action_mailbox/inbound_email/routable.rb b/app/models/action_mailbox/inbound_email/routable.rb new file mode 100644 index 0000000000..1928f9e387 --- /dev/null +++ b/app/models/action_mailbox/inbound_email/routable.rb @@ -0,0 +1,11 @@ +module ActionMailbox::InboundEmail::Routable + extend ActiveSupport::Concern + + included do + after_create_commit :route_later, if: ->(inbound_email) { inbound_email.pending? } + end + + def route_later + ActionMailbox::RoutingJob.perform_later self + end +end diff --git a/app/models/action_mailroom/inbound_email.rb b/app/models/action_mailroom/inbound_email.rb deleted file mode 100644 index cf7370d543..0000000000 --- a/app/models/action_mailroom/inbound_email.rb +++ /dev/null @@ -1,22 +0,0 @@ -require "mail" - -class ActionMailroom::InboundEmail < ActiveRecord::Base - self.table_name = "action_mailroom_inbound_emails" - - include Incineratable, MessageId, Routable - - has_one_attached :raw_email - enum status: %i[ pending processing delivered failed bounced ] - - def self.mail_from_source(source) - Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s) - end - - def mail - @mail ||= self.class.mail_from_source(source) - end - - def source - @source ||= raw_email.download - end -end diff --git a/app/models/action_mailroom/inbound_email/incineratable.rb b/app/models/action_mailroom/inbound_email/incineratable.rb deleted file mode 100644 index 83ccec89ba..0000000000 --- a/app/models/action_mailroom/inbound_email/incineratable.rb +++ /dev/null @@ -1,31 +0,0 @@ -module ActionMailroom::InboundEmail::Incineratable - extend ActiveSupport::Concern - - # TODO: Extract into framework configuration - INCINERATABLE_AFTER = 30.days - - included do - before_update :remember_to_incinerate_later - after_update_commit :incinerate_later, if: :need_to_incinerate_later? - end - - def incinerate - Incineration.new(self).run - end - - private - # TODO: Use enum change tracking once merged into Active Support - def remember_to_incinerate_later - if status_changed? && (delivered? || failed?) - @incinerate_later = true - end - end - - def need_to_incinerate_later? - @incinerate_later - end - - def incinerate_later - ActionMailroom::InboundEmail::IncinerationJob.schedule(self) - end -end diff --git a/app/models/action_mailroom/inbound_email/incineratable/incineration.rb b/app/models/action_mailroom/inbound_email/incineratable/incineration.rb deleted file mode 100644 index 8b88c1fc5b..0000000000 --- a/app/models/action_mailroom/inbound_email/incineratable/incineration.rb +++ /dev/null @@ -1,18 +0,0 @@ -class ActionMailroom::InboundEmail::Incineratable::Incineration - def initialize(inbound_email) - @inbound_email = inbound_email - end - - def run - @inbound_email.destroy if due? && processed? - end - - private - def due? - @inbound_email.updated_at < ActionMailroom::InboundEmail::Incineratable::INCINERATABLE_AFTER.ago.end_of_day - end - - def processed? - @inbound_email.delivered? || @inbound_email.failed? - end -end diff --git a/app/models/action_mailroom/inbound_email/message_id.rb b/app/models/action_mailroom/inbound_email/message_id.rb deleted file mode 100644 index 6bdda8c1c7..0000000000 --- a/app/models/action_mailroom/inbound_email/message_id.rb +++ /dev/null @@ -1,25 +0,0 @@ -module ActionMailroom::InboundEmail::MessageId - extend ActiveSupport::Concern - - included do - before_save :generate_missing_message_id - end - - module ClassMethods - def create_and_extract_message_id!(raw_email, **options) - create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options - end - - private - def extract_message_id(raw_email) - mail_from_source(raw_email.read).message_id - rescue => e - # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" - end - end - - private - def generate_missing_message_id - self.message_id ||= Mail::MessageIdField.new.message_id - end -end diff --git a/app/models/action_mailroom/inbound_email/routable.rb b/app/models/action_mailroom/inbound_email/routable.rb deleted file mode 100644 index 5075db326e..0000000000 --- a/app/models/action_mailroom/inbound_email/routable.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActionMailroom::InboundEmail::Routable - extend ActiveSupport::Concern - - included do - after_create_commit :route_later, if: ->(inbound_email) { inbound_email.pending? } - end - - def route_later - ActionMailroom::RoutingJob.perform_later self - end -end diff --git a/app/views/.DS_Store b/app/views/.DS_Store new file mode 100644 index 0000000000..bedd266c7a Binary files /dev/null and b/app/views/.DS_Store differ diff --git a/app/views/layouts/.DS_Store b/app/views/layouts/.DS_Store new file mode 100644 index 0000000000..5de81d0983 Binary files /dev/null and b/app/views/layouts/.DS_Store differ diff --git a/app/views/rails/.DS_Store b/app/views/rails/.DS_Store new file mode 100644 index 0000000000..627263b12d Binary files /dev/null and b/app/views/rails/.DS_Store differ diff --git a/app/views/rails/conductor/.DS_Store b/app/views/rails/conductor/.DS_Store new file mode 100644 index 0000000000..4f6e8d17e4 Binary files /dev/null and b/app/views/rails/conductor/.DS_Store differ diff --git a/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb b/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb new file mode 100644 index 0000000000..19c53984e2 --- /dev/null +++ b/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb @@ -0,0 +1,15 @@ +<% provide :title, "Deliver new inbound email" %> + +

All inbound emails

+ + + + <% @inbound_emails.each do |inbound_email| %> + + + + + <% end %> +
Message IDStatus
<%= link_to inbound_email.message_id, main_app.rails_conductor_inbound_email_path(inbound_email) %><%= inbound_email.status %>
+ +<%= link_to "Deliver new inbound email", main_app.new_rails_conductor_inbound_email_path %> \ No newline at end of file diff --git a/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb b/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb new file mode 100644 index 0000000000..be989ff0bc --- /dev/null +++ b/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb @@ -0,0 +1,27 @@ +<% provide :title, "Deliver new inbound email" %> + +

Deliver new inbound email

+ +<%= form_with(url: main_app.rails_conductor_inbound_emails_path, scope: :mail, local: true) do |form| %> +
+ <%= form.label :from, "From" %>
+ <%= form.text_field :from %> +
+ +
+ <%= form.label :to, "To" %>
+ <%= form.text_field :to %> +
+ +
+ <%= form.label :subject, "Subject" %>
+ <%= form.text_field :subject %> +
+ +
+ <%= form.label :body, "Body" %>
+ <%= form.text_area :body, size: "40x20" %> +
+ + <%= form.submit "Deliver inbound email" %> +<% end %> diff --git a/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb b/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb new file mode 100644 index 0000000000..e761904196 --- /dev/null +++ b/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb @@ -0,0 +1,15 @@ +<% provide :title, @inbound_email.message_id %> + +

<%= @inbound_email.message_id %>: <%= @inbound_email.status %>

+ +
    +
  • <%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %>
  • +
  • Incinerate
  • +
+ +
+ Full email source +
<%= @inbound_email.source %>
+
+ +<%= link_to "Back to all inbound emails", main_app.rails_conductor_inbound_emails_path %> \ No newline at end of file diff --git a/app/views/rails/conductor/action_mailroom/inbound_emails/index.html.erb b/app/views/rails/conductor/action_mailroom/inbound_emails/index.html.erb deleted file mode 100644 index 19c53984e2..0000000000 --- a/app/views/rails/conductor/action_mailroom/inbound_emails/index.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<% provide :title, "Deliver new inbound email" %> - -

All inbound emails

- - - - <% @inbound_emails.each do |inbound_email| %> - - - - - <% end %> -
Message IDStatus
<%= link_to inbound_email.message_id, main_app.rails_conductor_inbound_email_path(inbound_email) %><%= inbound_email.status %>
- -<%= link_to "Deliver new inbound email", main_app.new_rails_conductor_inbound_email_path %> \ No newline at end of file diff --git a/app/views/rails/conductor/action_mailroom/inbound_emails/new.html.erb b/app/views/rails/conductor/action_mailroom/inbound_emails/new.html.erb deleted file mode 100644 index be989ff0bc..0000000000 --- a/app/views/rails/conductor/action_mailroom/inbound_emails/new.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<% provide :title, "Deliver new inbound email" %> - -

Deliver new inbound email

- -<%= form_with(url: main_app.rails_conductor_inbound_emails_path, scope: :mail, local: true) do |form| %> -
- <%= form.label :from, "From" %>
- <%= form.text_field :from %> -
- -
- <%= form.label :to, "To" %>
- <%= form.text_field :to %> -
- -
- <%= form.label :subject, "Subject" %>
- <%= form.text_field :subject %> -
- -
- <%= form.label :body, "Body" %>
- <%= form.text_area :body, size: "40x20" %> -
- - <%= form.submit "Deliver inbound email" %> -<% end %> diff --git a/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb b/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb deleted file mode 100644 index e761904196..0000000000 --- a/app/views/rails/conductor/action_mailroom/inbound_emails/show.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<% provide :title, @inbound_email.message_id %> - -

<%= @inbound_email.message_id %>: <%= @inbound_email.status %>

- -
    -
  • <%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %>
  • -
  • Incinerate
  • -
- -
- Full email source -
<%= @inbound_email.source %>
-
- -<%= link_to "Back to all inbound emails", main_app.rails_conductor_inbound_emails_path %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index b44ddf648a..733f137262 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true Rails.application.routes.draw do - post "/rails/action_mailroom/inbound_emails" => "action_mailroom/inbound_emails#create", as: :rails_inbound_emails + post "/rails/action_mailbox/inbound_emails" => "action_mailbox/inbound_emails#create", as: :rails_inbound_emails # TODO: Should these be mounted within the engine only? - scope "rails/conductor/action_mailroom/", module: "rails/conductor/action_mailroom" do + scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do resources :inbound_emails, as: :rails_conductor_inbound_emails post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute end diff --git a/db/.DS_Store b/db/.DS_Store new file mode 100644 index 0000000000..ef5ba06993 Binary files /dev/null and b/db/.DS_Store differ diff --git a/db/migrate/20180917164000_create_action_mailbox_tables.rb b/db/migrate/20180917164000_create_action_mailbox_tables.rb new file mode 100644 index 0000000000..85e19bc3c6 --- /dev/null +++ b/db/migrate/20180917164000_create_action_mailbox_tables.rb @@ -0,0 +1,11 @@ +class CreateActionMailboxTables < ActiveRecord::Migration[5.2] + def change + create_table :action_mailbox_inbound_emails do |t| + t.integer :status, default: 0, null: false + t.string :message_id + + t.datetime :created_at, precision: 6 + t.datetime :updated_at, precision: 6 + end + end +end diff --git a/db/migrate/20180917164000_create_action_mailroom_tables.rb b/db/migrate/20180917164000_create_action_mailroom_tables.rb deleted file mode 100644 index cea05a6437..0000000000 --- a/db/migrate/20180917164000_create_action_mailroom_tables.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreateActionMailroomTables < ActiveRecord::Migration[5.2] - def change - create_table :action_mailroom_inbound_emails do |t| - t.integer :status, default: 0, null: false - t.string :message_id - - t.datetime :created_at, precision: 6 - t.datetime :updated_at, precision: 6 - end - end -end diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000000..4bb82d51a5 Binary files /dev/null and b/lib/.DS_Store differ diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb new file mode 100644 index 0000000000..f638dc3f80 --- /dev/null +++ b/lib/action_mailbox.rb @@ -0,0 +1,10 @@ +require "action_mailbox/engine" + +module ActionMailbox + extend ActiveSupport::Autoload + + autoload :Base + autoload :Router + autoload :Callbacks + autoload :Routing +end diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb new file mode 100644 index 0000000000..476c343415 --- /dev/null +++ b/lib/action_mailbox/base.rb @@ -0,0 +1,45 @@ +require "active_support/rescuable" + +require "action_mailbox/callbacks" +require "action_mailbox/routing" + +class ActionMailbox::Base + include ActiveSupport::Rescuable + include ActionMailbox::Callbacks, ActionMailbox::Routing + + attr_reader :inbound_email + delegate :mail, :bounced!, to: :inbound_email + + def self.receive(inbound_email) + new(inbound_email).perform_processing + end + + def initialize(inbound_email) + @inbound_email = inbound_email + end + + def perform_processing + run_callbacks :process do + track_status_of_inbound_email 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 + + private + def track_status_of_inbound_email + inbound_email.processing! + yield + inbound_email.delivered! unless inbound_email.bounced? + rescue => exception + inbound_email.failed! + raise + end +end diff --git a/lib/action_mailbox/callbacks.rb b/lib/action_mailbox/callbacks.rb new file mode 100644 index 0000000000..33caaafd2a --- /dev/null +++ b/lib/action_mailbox/callbacks.rb @@ -0,0 +1,26 @@ +require "active_support/callbacks" + +module ActionMailbox + module Callbacks + extend ActiveSupport::Concern + include ActiveSupport::Callbacks + + included do + define_callbacks :process + end + + module ClassMethods + 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/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb new file mode 100644 index 0000000000..92852a0fa3 --- /dev/null +++ b/lib/action_mailbox/engine.rb @@ -0,0 +1,14 @@ +require "rails/engine" + +module ActionMailbox + class Engine < Rails::Engine + isolate_namespace ActionMailbox + config.eager_load_namespaces << ActionMailbox + + initializer "action_mailbox.config" do + config.after_initialize do |app| + # Configure + end + end + end +end diff --git a/lib/action_mailbox/router.rb b/lib/action_mailbox/router.rb new file mode 100644 index 0000000000..8ba3ad0bae --- /dev/null +++ b/lib/action_mailbox/router.rb @@ -0,0 +1,36 @@ +class ActionMailbox::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 + +require "action_mailbox/router/route" diff --git a/lib/action_mailbox/router/route.rb b/lib/action_mailbox/router/route.rb new file mode 100644 index 0000000000..7be4407339 --- /dev/null +++ b/lib/action_mailbox/router/route.rb @@ -0,0 +1,31 @@ +class ActionMailbox::Router::Route + class InvalidAddressError < StandardError; end + + attr_reader :address, :mailbox_name + + def initialize(address, to:) + @address, @mailbox_name = address, to + end + + def match?(inbound_email) + case address + when String + recipients_from(inbound_email.mail).include?(address) + when Regexp + recipients_from(inbound_email.mail).detect { |recipient| address.match?(recipient) } + when Proc + address.call(inbound_email) + else + address.try(:match?, inbound_email) || raise(InvalidAddressError) + end + end + + def mailbox_class + "#{mailbox_name.to_s.capitalize}Mailbox".constantize + end + + private + def recipients_from(mail) + Array(mail.to) + Array(mail.cc) + Array(mail.bcc) + end +end diff --git a/lib/action_mailbox/routing.rb b/lib/action_mailbox/routing.rb new file mode 100644 index 0000000000..b40e2774e4 --- /dev/null +++ b/lib/action_mailbox/routing.rb @@ -0,0 +1,17 @@ +module ActionMailbox + module Routing + extend ActiveSupport::Concern + + class_methods do + attr_reader :router + + def routing(routes) + (@router ||= ActionMailbox::Router.new).add_routes(routes) + end + + def route(inbound_email) + @router.route(inbound_email) + end + end + end +end diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb new file mode 100644 index 0000000000..65a15a1281 --- /dev/null +++ b/lib/action_mailbox/test_helper.rb @@ -0,0 +1,22 @@ +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 file_fixture(fixture_name), filename: fixture_name, status: status + end + + def create_inbound_email_from_mail(status: :processing, **mail_options) + raw_email = Tempfile.new.tap { |raw_email| raw_email.write Mail.new(mail_options).to_s } + create_inbound_email(raw_email, status: status) + end + + def create_inbound_email(io, filename: 'mail.eml', status: :processing) + ActionMailbox::InboundEmail.create_and_extract_message_id! \ + ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'), + status: status + end + end +end diff --git a/lib/action_mailbox/version.rb b/lib/action_mailbox/version.rb new file mode 100644 index 0000000000..23c615dbbd --- /dev/null +++ b/lib/action_mailbox/version.rb @@ -0,0 +1,3 @@ +module ActionMailbox + VERSION = '0.1.0' +end diff --git a/lib/action_mailroom.rb b/lib/action_mailroom.rb deleted file mode 100644 index e50d1c4ebe..0000000000 --- a/lib/action_mailroom.rb +++ /dev/null @@ -1,8 +0,0 @@ -require "action_mailroom/engine" - -module ActionMailroom - extend ActiveSupport::Autoload - - autoload :Mailbox - autoload :Router -end diff --git a/lib/action_mailroom/engine.rb b/lib/action_mailroom/engine.rb deleted file mode 100644 index 6a8d4c23c0..0000000000 --- a/lib/action_mailroom/engine.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "rails/engine" - -module ActionMailroom - class Engine < Rails::Engine - isolate_namespace ActionMailroom - config.eager_load_namespaces << ActionMailroom - - initializer "action_mailroom.config" do - config.after_initialize do |app| - # Configure - end - end - end -end diff --git a/lib/action_mailroom/mailbox.rb b/lib/action_mailroom/mailbox.rb deleted file mode 100644 index 936054810f..0000000000 --- a/lib/action_mailroom/mailbox.rb +++ /dev/null @@ -1,45 +0,0 @@ -require "active_support/rescuable" - -require "action_mailroom/mailbox/callbacks" -require "action_mailroom/mailbox/routing" - -class ActionMailroom::Mailbox - include ActiveSupport::Rescuable - include Callbacks, Routing - - attr_reader :inbound_email - delegate :mail, :bounced!, to: :inbound_email - - def self.receive(inbound_email) - new(inbound_email).perform_processing - end - - def initialize(inbound_email) - @inbound_email = inbound_email - end - - def perform_processing - run_callbacks :process do - track_status_of_inbound_email 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 - - private - def track_status_of_inbound_email - inbound_email.processing! - yield - inbound_email.delivered! unless inbound_email.bounced? - rescue => exception - inbound_email.failed! - raise - end -end diff --git a/lib/action_mailroom/mailbox/callbacks.rb b/lib/action_mailroom/mailbox/callbacks.rb deleted file mode 100644 index ae5a461d8f..0000000000 --- a/lib/action_mailroom/mailbox/callbacks.rb +++ /dev/null @@ -1,28 +0,0 @@ -require "active_support/callbacks" - -module ActionMailroom - class Mailbox - module Callbacks - extend ActiveSupport::Concern - include ActiveSupport::Callbacks - - included do - define_callbacks :process - end - - module ClassMethods - 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 -end diff --git a/lib/action_mailroom/mailbox/routing.rb b/lib/action_mailroom/mailbox/routing.rb deleted file mode 100644 index 9f082c8aa5..0000000000 --- a/lib/action_mailroom/mailbox/routing.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActionMailroom::Mailbox::Routing - extend ActiveSupport::Concern - - class_methods do - attr_reader :router - - def routing(routes) - (@router ||= ActionMailroom::Router.new).add_routes(routes) - end - - def route(inbound_email) - @router.route(inbound_email) - end - end -end diff --git a/lib/action_mailroom/router.rb b/lib/action_mailroom/router.rb deleted file mode 100644 index 29ba803e03..0000000000 --- a/lib/action_mailroom/router.rb +++ /dev/null @@ -1,36 +0,0 @@ -class ActionMailroom::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 - -require "action_mailroom/router/route" diff --git a/lib/action_mailroom/router/route.rb b/lib/action_mailroom/router/route.rb deleted file mode 100644 index 6d0b922275..0000000000 --- a/lib/action_mailroom/router/route.rb +++ /dev/null @@ -1,31 +0,0 @@ -class ActionMailroom::Router::Route - class InvalidAddressError < StandardError; end - - attr_reader :address, :mailbox_name - - def initialize(address, to:) - @address, @mailbox_name = address, to - end - - def match?(inbound_email) - case address - when String - recipients_from(inbound_email.mail).include?(address) - when Regexp - recipients_from(inbound_email.mail).detect { |recipient| address.match?(recipient) } - when Proc - address.call(inbound_email) - else - address.try(:match?, inbound_email) || raise(InvalidAddressError) - end - end - - def mailbox_class - "#{mailbox_name.to_s.capitalize}Mailbox".constantize - end - - private - def recipients_from(mail) - Array(mail.to) + Array(mail.cc) + Array(mail.bcc) - end -end diff --git a/lib/action_mailroom/test_helper.rb b/lib/action_mailroom/test_helper.rb deleted file mode 100644 index 3e1ff87839..0000000000 --- a/lib/action_mailroom/test_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -require "mail" - -module ActionMailroom - 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 file_fixture(fixture_name), filename: fixture_name, status: status - end - - def create_inbound_email_from_mail(status: :processing, **mail_options) - raw_email = Tempfile.new.tap { |raw_email| raw_email.write Mail.new(mail_options).to_s } - create_inbound_email(raw_email, status: status) - end - - def create_inbound_email(io, filename: 'mail.eml', status: :processing) - ActionMailroom::InboundEmail.create_and_extract_message_id! \ - ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'), - status: status - end - end -end diff --git a/lib/action_mailroom/version.rb b/lib/action_mailroom/version.rb deleted file mode 100644 index 64a9d8eacd..0000000000 --- a/lib/action_mailroom/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module ActionMailroom - VERSION = '0.1.0' -end diff --git a/lib/tasks/action_mailbox.rake b/lib/tasks/action_mailbox.rake new file mode 100644 index 0000000000..b7c11934f8 --- /dev/null +++ b/lib/tasks/action_mailbox.rake @@ -0,0 +1,17 @@ +# 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 copy_migration active_storage:install ) + + task :copy_migration do + if Rake::Task.task_defined?("action_mailbox:install:migrations") + Rake::Task["action_mailbox:install:migrations"].invoke + else + Rake::Task["app:action_mailbox:install:migrations"].invoke + end + end +end diff --git a/lib/tasks/action_mailroom.rake b/lib/tasks/action_mailroom.rake deleted file mode 100644 index 5c929af119..0000000000 --- a/lib/tasks/action_mailroom.rake +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -namespace :action_mailroom do - # Prevent migration installation task from showing up twice. - Rake::Task["install:migrations"].clear_comments - - desc "Copy over the migration" - task install: %w( environment copy_migration active_storage:install ) - - task :copy_migration do - if Rake::Task.task_defined?("action_mailroom:install:migrations") - Rake::Task["action_mailroom:install:migrations"].invoke - else - Rake::Task["app:action_mailroom:install:migrations"].invoke - end - end -end diff --git a/test/.DS_Store b/test/.DS_Store new file mode 100644 index 0000000000..4baab2932c Binary files /dev/null and b/test/.DS_Store differ diff --git a/test/dummy/app/mailboxes/application_mailbox.rb b/test/dummy/app/mailboxes/application_mailbox.rb index 3ea37aaf8f..47fb2017d6 100644 --- a/test/dummy/app/mailboxes/application_mailbox.rb +++ b/test/dummy/app/mailboxes/application_mailbox.rb @@ -1,2 +1,2 @@ -class ApplicationMailbox < ActionMailroom::Mailbox +class ApplicationMailbox < ActionMailbox::Base end diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index dc57837cde..5018690331 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -3,7 +3,7 @@ require_relative 'boot' require 'rails/all' Bundler.require(*Rails.groups) -require "action_mailroom" +require "action_mailbox" module Dummy class Application < Rails::Application diff --git a/test/dummy/db/development.sqlite3 b/test/dummy/db/development.sqlite3 deleted file mode 100644 index 8bd6b8dec3..0000000000 Binary files a/test/dummy/db/development.sqlite3 and /dev/null differ diff --git a/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb b/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb index cea05a6437..85e19bc3c6 100644 --- a/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb +++ b/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb @@ -1,6 +1,6 @@ -class CreateActionMailroomTables < ActiveRecord::Migration[5.2] +class CreateActionMailboxTables < ActiveRecord::Migration[5.2] def change - create_table :action_mailroom_inbound_emails do |t| + create_table :action_mailbox_inbound_emails do |t| t.integer :status, default: 0, null: false t.string :message_id diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 339a6b2afd..6cfe7de765 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -12,7 +12,7 @@ ActiveRecord::Schema.define(version: 2018_02_12_164506) do - create_table "action_mailroom_inbound_emails", force: :cascade do |t| + create_table "action_mailbox_inbound_emails", force: :cascade do |t| t.integer "status", default: 0, null: false t.string "message_id" t.datetime "created_at", precision: 6 diff --git a/test/test_helper.rb b/test/test_helper.rb index 50ddc85463..b042cfb679 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,10 +22,10 @@ if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixtures :all end -require "action_mailroom/test_helper" +require "action_mailbox/test_helper" class ActiveSupport::TestCase - include ActionMailroom::TestHelper, ActiveJob::TestHelper + include ActionMailbox::TestHelper, ActiveJob::TestHelper end if ARGV.include?("-v") diff --git a/test/unit/.DS_Store b/test/unit/.DS_Store new file mode 100644 index 0000000000..69403a911f Binary files /dev/null and b/test/unit/.DS_Store differ diff --git a/test/unit/controller_test.rb b/test/unit/controller_test.rb index f2a49f415f..508e561244 100644 --- a/test/unit/controller_test.rb +++ b/test/unit/controller_test.rb @@ -1,19 +1,19 @@ require_relative '../test_helper' -class ActionMailroom::InboundEmailsControllerTest < ActionDispatch::IntegrationTest +class ActionMailbox::InboundEmailsControllerTest < ActionDispatch::IntegrationTest test "receiving a valid RFC 822 message" do - assert_difference -> { ActionMailroom::InboundEmail.count }, +1 do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do post_inbound_email "welcome.eml" end assert_response :created - inbound_email = ActionMailroom::InboundEmail.last + inbound_email = ActionMailbox::InboundEmail.last assert_equal file_fixture('../files/welcome.eml').read, inbound_email.raw_email.download end test "rejecting a message of an unsupported type" do - assert_no_difference -> { ActionMailroom::InboundEmail.count } do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do post rails_inbound_emails_url, params: { message: fixture_file_upload('files/text.txt', 'text/plain') } end diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb index 3de0af3225..212325b92d 100644 --- a/test/unit/inbound_email/incineration_test.rb +++ b/test/unit/inbound_email/incineration_test.rb @@ -1,10 +1,10 @@ require_relative '../../test_helper' -class ActionMailroom::InboundEmail::IncinerationTest < ActiveSupport::TestCase +class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase test "incinerate emails 30 days after they have been processed" do freeze_time - assert_enqueued_with job: ActionMailroom::InboundEmail::IncinerationJob, at: 30.days.from_now do + assert_enqueued_with job: ActionMailbox::InboundEmail::IncinerationJob, at: 30.days.from_now do create_inbound_email_from_fixture("welcome.eml").delivered! end end diff --git a/test/unit/inbound_email/message_id_test.rb b/test/unit/inbound_email/message_id_test.rb index 75c15a531a..aa9ce90b4c 100644 --- a/test/unit/inbound_email/message_id_test.rb +++ b/test/unit/inbound_email/message_id_test.rb @@ -1,6 +1,6 @@ require_relative '../../test_helper' -class ActionMailroom::InboundEmail::MessageIdTest < ActiveSupport::TestCase +class ActionMailbox::InboundEmail::MessageIdTest < ActiveSupport::TestCase test "message id is extracted from raw email" do inbound_email = create_inbound_email_from_fixture("welcome.eml") assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id diff --git a/test/unit/inbound_email_test.rb b/test/unit/inbound_email_test.rb index e4b7be2440..7c42188584 100644 --- a/test/unit/inbound_email_test.rb +++ b/test/unit/inbound_email_test.rb @@ -1,6 +1,6 @@ require_relative '../test_helper' -module ActionMailroom +module ActionMailbox class InboundEmailTest < ActiveSupport::TestCase test "mail provides the parsed source" do assert_equal "Discussion: Let's debate these attachments", create_inbound_email_from_fixture("welcome.eml").mail.subject diff --git a/test/unit/mailbox/callbacks_test.rb b/test/unit/mailbox/callbacks_test.rb index 4cafeb3534..b6cfef9868 100644 --- a/test/unit/mailbox/callbacks_test.rb +++ b/test/unit/mailbox/callbacks_test.rb @@ -1,6 +1,6 @@ require_relative '../../test_helper' -class CallbackMailbox < ActionMailroom::Mailbox +class CallbackMailbox < ActionMailbox::Base before_processing { $before_processing = "Ran that!" } after_processing { $after_processing = "Ran that too!" } around_processing ->(r, block) { block.call; $around_processing = "Ran that as well!" } @@ -10,7 +10,7 @@ class CallbackMailbox < ActionMailroom::Mailbox end end -class ActionMailroom::Mailbox::CallbacksTest < ActiveSupport::TestCase +class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase setup do $before_processing = $after_processing = $around_processing = $processed = false @inbound_email = create_inbound_email_from_fixture("welcome.eml") diff --git a/test/unit/mailbox/routing_test.rb b/test/unit/mailbox/routing_test.rb index 4a8ed10eb0..320dee6aab 100644 --- a/test/unit/mailbox/routing_test.rb +++ b/test/unit/mailbox/routing_test.rb @@ -1,16 +1,16 @@ require_relative '../../test_helper' -class ApplicationMailbox < ActionMailroom::Mailbox +class ApplicationMailbox < ActionMailbox::Base routing "replies@example.com" => :replies end -class RepliesMailbox < ActionMailroom::Mailbox +class RepliesMailbox < ActionMailbox::Base def process $processed = mail.subject end end -class ActionMailroom::Mailbox::RoutingTest < ActiveSupport::TestCase +class ActionMailbox::Base::RoutingTest < ActiveSupport::TestCase setup do $processed = false @inbound_email = create_inbound_email_from_fixture("welcome.eml") @@ -22,7 +22,7 @@ class ActionMailroom::Mailbox::RoutingTest < ActiveSupport::TestCase end test "delayed routing" do - perform_enqueued_jobs only: ActionMailroom::RoutingJob do + perform_enqueued_jobs only: ActionMailbox::RoutingJob do another_inbound_email = create_inbound_email_from_fixture("welcome.eml", status: :pending) assert_equal "Discussion: Let's debate these attachments", $processed end diff --git a/test/unit/mailbox/state_test.rb b/test/unit/mailbox/state_test.rb index 6215e02837..32d16ca8fe 100644 --- a/test/unit/mailbox/state_test.rb +++ b/test/unit/mailbox/state_test.rb @@ -1,12 +1,12 @@ require_relative '../../test_helper' -class SuccessfulMailbox < ActionMailroom::Mailbox +class SuccessfulMailbox < ActionMailbox::Base def process $processed = mail.subject end end -class UnsuccessfulMailbox < ActionMailroom::Mailbox +class UnsuccessfulMailbox < ActionMailbox::Base rescue_from(RuntimeError) { $processed = :failure } def process @@ -14,7 +14,7 @@ class UnsuccessfulMailbox < ActionMailroom::Mailbox end end -class BouncingMailbox < ActionMailroom::Mailbox +class BouncingMailbox < ActionMailbox::Base def process $processed = :bounced bounced! @@ -22,7 +22,7 @@ class BouncingMailbox < ActionMailroom::Mailbox end -class ActionMailroom::Mailbox::StateTest < ActiveSupport::TestCase +class ActionMailbox::Base::StateTest < ActiveSupport::TestCase setup do $processed = false @inbound_email = create_inbound_email_from_mail \ diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 678810a900..e244ff45f4 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -1,6 +1,6 @@ require_relative '../test_helper' -class RootMailbox < ActionMailroom::Mailbox +class RootMailbox < ActionMailbox::Base def process $processed_by = self.class.to_s $processed_mail = mail @@ -19,10 +19,10 @@ class FirstMailboxAddress end end -module ActionMailroom +module ActionMailbox class RouterTest < ActiveSupport::TestCase setup do - @router = ActionMailroom::Router.new + @router = ActionMailbox::Router.new $processed_by = $processed_mail = nil end @@ -82,7 +82,7 @@ module ActionMailroom end test "missing route" do - assert_raises(ActionMailroom::Router::RoutingError) do + assert_raises(ActionMailbox::Router::RoutingError) do inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply") @router.route inbound_email assert inbound_email.bounced? @@ -90,7 +90,7 @@ module ActionMailroom end test "invalid address" do - assert_raises(ActionMailroom::Router::Route::InvalidAddressError) do + assert_raises(ActionMailbox::Router::Route::InvalidAddressError) do @router.add_route Array.new, to: :first @router.route create_inbound_email_from_mail(to: "replies-nowhere@example.com", subject: "This is a reply") end -- cgit v1.2.3 From 832116ac49d02471449064f3cb1496a5ad36890e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 28 Sep 2018 12:23:08 -0700 Subject: Reflect name change --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3f91f3018..993a18fae9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# Action Mailroom +# Action Mailbox -📬 \ No newline at end of file +📬 -- cgit v1.2.3 From dd593b98b5ef6e66d72942065dd1c0a9b266b9bc Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 1 Oct 2018 08:16:10 -0400 Subject: Provide a logger --- lib/action_mailbox.rb | 2 ++ lib/action_mailbox/base.rb | 4 +++- lib/action_mailbox/engine.rb | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb index f638dc3f80..ed890c488b 100644 --- a/lib/action_mailbox.rb +++ b/lib/action_mailbox.rb @@ -7,4 +7,6 @@ module ActionMailbox autoload :Router autoload :Callbacks autoload :Routing + + mattr_accessor :logger end diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index 476c343415..ce1aec01a9 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -10,6 +10,8 @@ class ActionMailbox::Base attr_reader :inbound_email delegate :mail, :bounced!, to: :inbound_email + delegate :logger, to: ActionMailbox + def self.receive(inbound_email) new(inbound_email).perform_processing end @@ -32,7 +34,7 @@ class ActionMailbox::Base def process # Overwrite in subclasses end - + private def track_status_of_inbound_email inbound_email.processing! diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb index 92852a0fa3..4e800c54de 100644 --- a/lib/action_mailbox/engine.rb +++ b/lib/action_mailbox/engine.rb @@ -5,9 +5,11 @@ module ActionMailbox isolate_namespace ActionMailbox config.eager_load_namespaces << ActionMailbox + config.action_mailbox = ActiveSupport::OrderedOptions.new + initializer "action_mailbox.config" do config.after_initialize do |app| - # Configure + ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger end end end -- cgit v1.2.3 From 704179251f3a96f7e7f2ed9b28e1524ace3a4927 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 1 Oct 2018 15:42:32 -0400 Subject: Add a test helper for creating and routing an inbound email --- app/jobs/action_mailbox/routing_job.rb | 2 +- app/models/action_mailbox/inbound_email/routable.rb | 4 ++++ lib/action_mailbox/test_helper.rb | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/jobs/action_mailbox/routing_job.rb b/app/jobs/action_mailbox/routing_job.rb index a2618bb8aa..af4aa9f8fa 100644 --- a/app/jobs/action_mailbox/routing_job.rb +++ b/app/jobs/action_mailbox/routing_job.rb @@ -2,6 +2,6 @@ class ActionMailbox::RoutingJob < ActiveJob::Base queue_as :action_mailbox_routing def perform(inbound_email) - ApplicationMailbox.route inbound_email + inbound_email.route end end diff --git a/app/models/action_mailbox/inbound_email/routable.rb b/app/models/action_mailbox/inbound_email/routable.rb index 1928f9e387..48b357af45 100644 --- a/app/models/action_mailbox/inbound_email/routable.rb +++ b/app/models/action_mailbox/inbound_email/routable.rb @@ -8,4 +8,8 @@ module ActionMailbox::InboundEmail::Routable def route_later ActionMailbox::RoutingJob.perform_later self end + + def route + ApplicationMailbox.route self + end end diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb index 65a15a1281..4694f61b8a 100644 --- a/lib/action_mailbox/test_helper.rb +++ b/lib/action_mailbox/test_helper.rb @@ -6,7 +6,7 @@ module ActionMailbox # referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+. def create_inbound_email_from_fixture(fixture_name, status: :processing) create_inbound_email file_fixture(fixture_name), filename: fixture_name, status: status - end + end def create_inbound_email_from_mail(status: :processing, **mail_options) raw_email = Tempfile.new.tap { |raw_email| raw_email.write Mail.new(mail_options).to_s } @@ -18,5 +18,9 @@ module ActionMailbox ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'), status: status end + + def receive_inbound_email_from_mail(**kwargs) + create_inbound_email_from_mail(**kwargs).tap(&:route) + end end end -- cgit v1.2.3 From 622901636b417731e4239359feef7cf6943b8b3c Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 1 Oct 2018 17:48:44 -0400 Subject: Support nesting mailboxes in modules --- lib/action_mailbox/router/route.rb | 2 +- test/unit/router_test.rb | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/action_mailbox/router/route.rb b/lib/action_mailbox/router/route.rb index 7be4407339..e9acf310b1 100644 --- a/lib/action_mailbox/router/route.rb +++ b/lib/action_mailbox/router/route.rb @@ -21,7 +21,7 @@ class ActionMailbox::Router::Route end def mailbox_class - "#{mailbox_name.to_s.capitalize}Mailbox".constantize + "#{mailbox_name.to_s.camelize}Mailbox".constantize end private diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index e244ff45f4..67a5f24c52 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -13,6 +13,11 @@ end class SecondMailbox < RootMailbox end +module Nested + class FirstMailbox < RootMailbox + end +end + class FirstMailboxAddress def match?(inbound_email) inbound_email.mail.to.include?("replies-class@example.com") @@ -57,7 +62,7 @@ module ActionMailbox assert_equal "SecondMailbox", $processed_by assert_equal inbound_email.mail, $processed_mail end - + test "single regexp route" do @router.add_routes(/replies-\w+@example.com/ => :first, "replies-nowhere@example.com" => :second) @@ -81,6 +86,14 @@ module ActionMailbox assert_equal "FirstMailbox", $processed_by end + test "string route to nested mailbox" do + @router.add_route "first@example.com", to: "nested/first" + + inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply") + @router.route inbound_email + assert_equal "Nested::FirstMailbox", $processed_by + end + test "missing route" do assert_raises(ActionMailbox::Router::RoutingError) do inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply") -- cgit v1.2.3 From 16a53e3d5c52a882ff85ec2a678bb49e21a4b3eb Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Tue, 2 Oct 2018 10:06:36 -0400 Subject: Permit receiving an email fixture --- lib/action_mailbox/test_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb index 4694f61b8a..b1d7af33f9 100644 --- a/lib/action_mailbox/test_helper.rb +++ b/lib/action_mailbox/test_helper.rb @@ -19,6 +19,10 @@ module ActionMailbox status: status end + def receive_inbound_email_from_fixture(*args) + create_inbound_email_from_fixture(*args).tap(&:route) + end + def receive_inbound_email_from_mail(**kwargs) create_inbound_email_from_mail(**kwargs).tap(&:route) end -- cgit v1.2.3 From 7bf2cd437dd3b122aa1c8cb91c282cc9d699fc4c Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 3 Oct 2018 15:14:28 -0400 Subject: Add ActionMailbox::Base#bounce_with --- lib/action_mailbox/base.rb | 5 +++++ test/test_helper.rb | 8 ++++++++ test/unit/mailbox/bouncing_test.rb | 29 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 test/unit/mailbox/bouncing_test.rb diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index ce1aec01a9..66a3b7fd32 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -35,6 +35,11 @@ class ActionMailbox::Base # Overwrite in subclasses end + def bounce_with(message) + inbound_email.bounced! + message.deliver_later + end + private def track_status_of_inbound_email inbound_email.processing! diff --git a/test/test_helper.rb b/test/test_helper.rb index b042cfb679..264f9e8482 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -32,3 +32,11 @@ if ARGV.include?("-v") ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveJob::Base.logger = Logger.new(STDOUT) end + +class BounceMailer < ActionMailer::Base + def bounce(to:) + mail from: "receiver@example.com", to: to, subject: "Your email was not delivered" do |format| + format.html { render plain: "Sorry!" } + end + end +end diff --git a/test/unit/mailbox/bouncing_test.rb b/test/unit/mailbox/bouncing_test.rb new file mode 100644 index 0000000000..6ac806ba29 --- /dev/null +++ b/test/unit/mailbox/bouncing_test.rb @@ -0,0 +1,29 @@ +require_relative '../../test_helper' + +class BouncingWithReplyMailbox < ActionMailbox::Base + def process + bounce_with BounceMailer.bounce(to: mail.from) + end +end + +class ActionMailbox::Base::BouncingTest < ActiveSupport::TestCase + include ActionMailer::TestHelper + + setup do + @inbound_email = create_inbound_email_from_mail \ + from: "sender@example.com", to: "replies@example.com", subject: "Bounce me" + end + + test "bouncing with a reply" do + perform_enqueued_jobs only: ActionMailer::DeliveryJob do + BouncingWithReplyMailbox.receive @inbound_email + end + + assert @inbound_email.bounced? + assert_emails 1 + + mail = ActionMailer::Base.deliveries.last + assert_equal %w[ sender@example.com ], mail.to + assert_equal "Your email was not delivered", mail.subject + end +end -- cgit v1.2.3 From b9e220aa41674c974f5a1e8c4fe1684596cab740 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 3 Oct 2018 15:36:40 -0400 Subject: Terminate processing on bounce --- lib/action_mailbox/callbacks.rb | 13 ++++++++++++- test/unit/mailbox/callbacks_test.rb | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/action_mailbox/callbacks.rb b/lib/action_mailbox/callbacks.rb index 33caaafd2a..e8bf2ffd55 100644 --- a/lib/action_mailbox/callbacks.rb +++ b/lib/action_mailbox/callbacks.rb @@ -5,8 +5,19 @@ module ActionMailbox extend ActiveSupport::Concern include ActiveSupport::Callbacks + TERMINATOR = ->(target, chain) do + terminate = true + + catch(:abort) do + chain.call + terminate = target.inbound_email.bounced? + end + + terminate + end + included do - define_callbacks :process + define_callbacks :process, terminator: TERMINATOR end module ClassMethods diff --git a/test/unit/mailbox/callbacks_test.rb b/test/unit/mailbox/callbacks_test.rb index b6cfef9868..fccd89b9df 100644 --- a/test/unit/mailbox/callbacks_test.rb +++ b/test/unit/mailbox/callbacks_test.rb @@ -10,6 +10,21 @@ class CallbackMailbox < ActionMailbox::Base end end +class BouncingCallbackMailbox < ActionMailbox::Base + before_processing { $before_processing = [ "Pre-bounce" ] } + + before_processing do + bounce_with BounceMailer.bounce(to: mail.from) + $before_processing << "Bounce" + end + + before_processing { $before_processing << "Post-bounce" } + + def process + $processed = mail.subject + end +end + class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase setup do $before_processing = $after_processing = $around_processing = $processed = false @@ -22,4 +37,11 @@ class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase assert_equal "Ran that too!", $after_processing assert_equal "Ran that as well!", $around_processing end + + test "bouncing in a callback terminates processing" do + BouncingCallbackMailbox.receive @inbound_email + assert @inbound_email.bounced? + assert_equal [ "Pre-bounce", "Bounce" ], $before_processing + assert_not $processed + end end -- cgit v1.2.3 From 2031d5310a578448713c390e7dba619dcc293bc7 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 3 Oct 2018 15:43:20 -0400 Subject: Delete .DS_Store files --- app/.DS_Store | Bin 6148 -> 0 bytes app/controllers/.DS_Store | Bin 6148 -> 0 bytes app/controllers/rails/.DS_Store | Bin 6148 -> 0 bytes app/jobs/.DS_Store | Bin 6148 -> 0 bytes app/models/.DS_Store | Bin 6148 -> 0 bytes app/views/.DS_Store | Bin 6148 -> 0 bytes app/views/layouts/.DS_Store | Bin 6148 -> 0 bytes app/views/rails/.DS_Store | Bin 6148 -> 0 bytes app/views/rails/conductor/.DS_Store | Bin 6148 -> 0 bytes db/.DS_Store | Bin 6148 -> 0 bytes lib/.DS_Store | Bin 6148 -> 0 bytes test/.DS_Store | Bin 6148 -> 0 bytes test/unit/.DS_Store | Bin 6148 -> 0 bytes 13 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/.DS_Store delete mode 100644 app/controllers/.DS_Store delete mode 100644 app/controllers/rails/.DS_Store delete mode 100644 app/jobs/.DS_Store delete mode 100644 app/models/.DS_Store delete mode 100644 app/views/.DS_Store delete mode 100644 app/views/layouts/.DS_Store delete mode 100644 app/views/rails/.DS_Store delete mode 100644 app/views/rails/conductor/.DS_Store delete mode 100644 db/.DS_Store delete mode 100644 lib/.DS_Store delete mode 100644 test/.DS_Store delete mode 100644 test/unit/.DS_Store diff --git a/app/.DS_Store b/app/.DS_Store deleted file mode 100644 index 6719b65737..0000000000 Binary files a/app/.DS_Store and /dev/null differ diff --git a/app/controllers/.DS_Store b/app/controllers/.DS_Store deleted file mode 100644 index ea1205963a..0000000000 Binary files a/app/controllers/.DS_Store and /dev/null differ diff --git a/app/controllers/rails/.DS_Store b/app/controllers/rails/.DS_Store deleted file mode 100644 index 627263b12d..0000000000 Binary files a/app/controllers/rails/.DS_Store and /dev/null differ diff --git a/app/jobs/.DS_Store b/app/jobs/.DS_Store deleted file mode 100644 index 4f6e8d17e4..0000000000 Binary files a/app/jobs/.DS_Store and /dev/null differ diff --git a/app/models/.DS_Store b/app/models/.DS_Store deleted file mode 100644 index 4f6e8d17e4..0000000000 Binary files a/app/models/.DS_Store and /dev/null differ diff --git a/app/views/.DS_Store b/app/views/.DS_Store deleted file mode 100644 index bedd266c7a..0000000000 Binary files a/app/views/.DS_Store and /dev/null differ diff --git a/app/views/layouts/.DS_Store b/app/views/layouts/.DS_Store deleted file mode 100644 index 5de81d0983..0000000000 Binary files a/app/views/layouts/.DS_Store and /dev/null differ diff --git a/app/views/rails/.DS_Store b/app/views/rails/.DS_Store deleted file mode 100644 index 627263b12d..0000000000 Binary files a/app/views/rails/.DS_Store and /dev/null differ diff --git a/app/views/rails/conductor/.DS_Store b/app/views/rails/conductor/.DS_Store deleted file mode 100644 index 4f6e8d17e4..0000000000 Binary files a/app/views/rails/conductor/.DS_Store and /dev/null differ diff --git a/db/.DS_Store b/db/.DS_Store deleted file mode 100644 index ef5ba06993..0000000000 Binary files a/db/.DS_Store and /dev/null differ diff --git a/lib/.DS_Store b/lib/.DS_Store deleted file mode 100644 index 4bb82d51a5..0000000000 Binary files a/lib/.DS_Store and /dev/null differ diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index 4baab2932c..0000000000 Binary files a/test/.DS_Store and /dev/null differ diff --git a/test/unit/.DS_Store b/test/unit/.DS_Store deleted file mode 100644 index 69403a911f..0000000000 Binary files a/test/unit/.DS_Store and /dev/null differ -- cgit v1.2.3 From a91dc46b32b3062d2c75c226c733b04da50d0bc8 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 3 Oct 2018 16:04:33 -0400 Subject: Skip after callbacks on terminate --- lib/action_mailbox/callbacks.rb | 2 +- test/unit/mailbox/callbacks_test.rb | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/action_mailbox/callbacks.rb b/lib/action_mailbox/callbacks.rb index e8bf2ffd55..a5f7aa7e6f 100644 --- a/lib/action_mailbox/callbacks.rb +++ b/lib/action_mailbox/callbacks.rb @@ -17,7 +17,7 @@ module ActionMailbox end included do - define_callbacks :process, terminator: TERMINATOR + define_callbacks :process, terminator: TERMINATOR, skip_after_callbacks_if_terminated: true end module ClassMethods diff --git a/test/unit/mailbox/callbacks_test.rb b/test/unit/mailbox/callbacks_test.rb index fccd89b9df..617e8a89b1 100644 --- a/test/unit/mailbox/callbacks_test.rb +++ b/test/unit/mailbox/callbacks_test.rb @@ -20,8 +20,10 @@ class BouncingCallbackMailbox < ActionMailbox::Base before_processing { $before_processing << "Post-bounce" } + after_processing { $after_processing = true } + def process - $processed = mail.subject + $processed = true end end @@ -43,5 +45,6 @@ class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase assert @inbound_email.bounced? assert_equal [ "Pre-bounce", "Bounce" ], $before_processing assert_not $processed + assert_not $after_processing end end -- cgit v1.2.3 From 015c33f4cd6758e2c7d43f82ff5da93bac5cf3e3 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 3 Oct 2018 20:44:31 -0400 Subject: Compare addresses case-insensitively --- lib/action_mailbox/router/route.rb | 4 ++-- test/unit/router_test.rb | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/action_mailbox/router/route.rb b/lib/action_mailbox/router/route.rb index e9acf310b1..e123a93bb1 100644 --- a/lib/action_mailbox/router/route.rb +++ b/lib/action_mailbox/router/route.rb @@ -10,9 +10,9 @@ class ActionMailbox::Router::Route def match?(inbound_email) case address when String - recipients_from(inbound_email.mail).include?(address) + recipients_from(inbound_email.mail).any? { |recipient| address.casecmp?(recipient) } when Regexp - recipients_from(inbound_email.mail).detect { |recipient| address.match?(recipient) } + recipients_from(inbound_email.mail).any? { |recipient| address.match?(recipient) } when Proc address.call(inbound_email) else diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 67a5f24c52..cc87ae6c0e 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -49,6 +49,15 @@ module ActionMailbox assert_equal inbound_email.mail, $processed_mail end + test "single string routing case-insensitively" do + @router.add_routes("first@example.com" => :first) + + inbound_email = create_inbound_email_from_mail(to: "FIRST@example.com", subject: "This is a reply") + @router.route inbound_email + assert_equal "FirstMailbox", $processed_by + assert_equal inbound_email.mail, $processed_mail + end + test "multiple string routes" do @router.add_routes("first@example.com" => :first, "second@example.com" => :second) -- cgit v1.2.3 From 7b95ebc9e44b7dba1e4aa32b773a124f75de9474 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 6 Oct 2018 22:15:36 -0400 Subject: Terminate processing if inbound email is marked as delivered in callback --- lib/action_mailbox/base.rb | 7 ++++++- lib/action_mailbox/callbacks.rb | 12 +++--------- test/unit/mailbox/callbacks_test.rb | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index 66a3b7fd32..3b12493662 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -8,7 +8,7 @@ class ActionMailbox::Base include ActionMailbox::Callbacks, ActionMailbox::Routing attr_reader :inbound_email - delegate :mail, :bounced!, to: :inbound_email + delegate :mail, :delivered!, :bounced!, to: :inbound_email delegate :logger, to: ActionMailbox @@ -35,6 +35,11 @@ class ActionMailbox::Base # Overwrite in subclasses end + def finished_processing? + inbound_email.delivered? || inbound_email.bounced? + end + + def bounce_with(message) inbound_email.bounced! message.deliver_later diff --git a/lib/action_mailbox/callbacks.rb b/lib/action_mailbox/callbacks.rb index a5f7aa7e6f..5f39faa01e 100644 --- a/lib/action_mailbox/callbacks.rb +++ b/lib/action_mailbox/callbacks.rb @@ -5,15 +5,9 @@ module ActionMailbox extend ActiveSupport::Concern include ActiveSupport::Callbacks - TERMINATOR = ->(target, chain) do - terminate = true - - catch(:abort) do - chain.call - terminate = target.inbound_email.bounced? - end - - terminate + TERMINATOR = ->(mailbox, chain) do + chain.call + mailbox.finished_processing? end included do diff --git a/test/unit/mailbox/callbacks_test.rb b/test/unit/mailbox/callbacks_test.rb index 617e8a89b1..279bf7e574 100644 --- a/test/unit/mailbox/callbacks_test.rb +++ b/test/unit/mailbox/callbacks_test.rb @@ -27,6 +27,23 @@ class BouncingCallbackMailbox < ActionMailbox::Base end end +class DiscardingCallbackMailbox < ActionMailbox::Base + before_processing { $before_processing = [ "Pre-discard" ] } + + before_processing do + delivered! + $before_processing << "Discard" + end + + before_processing { $before_processing << "Post-discard" } + + after_processing { $after_processing = true } + + def process + $processed = true + end +end + class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase setup do $before_processing = $after_processing = $around_processing = $processed = false @@ -47,4 +64,12 @@ class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase assert_not $processed assert_not $after_processing end + + test "marking the inbound email as delivered in a callback terminates processing" do + DiscardingCallbackMailbox.receive @inbound_email + assert @inbound_email.delivered? + assert_equal [ "Pre-discard", "Discard" ], $before_processing + assert_not $processed + assert_not $after_processing + end end -- cgit v1.2.3 From 40fcc1ea95795b0a69fe4ee690c031b5ea974042 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 6 Oct 2018 22:18:09 -0400 Subject: Shush interpreter warnings regarding uninitialized instance variable --- app/models/action_mailbox/inbound_email/incineratable.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/action_mailbox/inbound_email/incineratable.rb b/app/models/action_mailbox/inbound_email/incineratable.rb index 6ba73c0c6d..e85f3e264d 100644 --- a/app/models/action_mailbox/inbound_email/incineratable.rb +++ b/app/models/action_mailbox/inbound_email/incineratable.rb @@ -6,7 +6,7 @@ module ActionMailbox::InboundEmail::Incineratable included do before_update :remember_to_incinerate_later - after_update_commit :incinerate_later, if: :need_to_incinerate_later? + after_update_commit :incinerate_later, if: :incinerating_later? end def incinerate @@ -17,12 +17,12 @@ module ActionMailbox::InboundEmail::Incineratable # TODO: Use enum change tracking once merged into Active Support def remember_to_incinerate_later if status_changed? && (delivered? || failed?) - @incinerate_later = true + @incinerating_later = true end end - def need_to_incinerate_later? - @incinerate_later + def incinerating_later? + @incinerating_later ||= false end def incinerate_later -- cgit v1.2.3 From a5e023ac204251192f9cb722c0d98695cb3b8400 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 6 Oct 2018 22:18:53 -0400 Subject: Incinerate after bouncing --- app/models/action_mailbox/inbound_email/incineratable.rb | 2 +- app/models/action_mailbox/inbound_email/incineratable/incineration.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/action_mailbox/inbound_email/incineratable.rb b/app/models/action_mailbox/inbound_email/incineratable.rb index e85f3e264d..8a82b87a99 100644 --- a/app/models/action_mailbox/inbound_email/incineratable.rb +++ b/app/models/action_mailbox/inbound_email/incineratable.rb @@ -16,7 +16,7 @@ module ActionMailbox::InboundEmail::Incineratable private # TODO: Use enum change tracking once merged into Active Support def remember_to_incinerate_later - if status_changed? && (delivered? || failed?) + if status_changed? && (delivered? || bounced? || failed?) @incinerating_later = true end end diff --git a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb index bd2bf7d91e..5dd9be5a3d 100644 --- a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +++ b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb @@ -13,6 +13,6 @@ class ActionMailbox::InboundEmail::Incineratable::Incineration end def processed? - @inbound_email.delivered? || @inbound_email.failed? + @inbound_email.delivered? || @inbound_email.bounced? || @inbound_email.failed? end end -- cgit v1.2.3 From 1fddb9475fa10f46efd1afd3bb4359645ec495a4 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 6 Oct 2018 22:23:19 -0400 Subject: Make incineration horizon configurable --- app/jobs/action_mailbox/inbound_email/incineration_job.rb | 2 +- app/models/action_mailbox/inbound_email/incineratable.rb | 3 --- app/models/action_mailbox/inbound_email/incineratable/incineration.rb | 2 +- lib/action_mailbox.rb | 1 + lib/action_mailbox/engine.rb | 2 ++ 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/jobs/action_mailbox/inbound_email/incineration_job.rb b/app/jobs/action_mailbox/inbound_email/incineration_job.rb index a2911efef1..d9422a3fa8 100644 --- a/app/jobs/action_mailbox/inbound_email/incineration_job.rb +++ b/app/jobs/action_mailbox/inbound_email/incineration_job.rb @@ -2,7 +2,7 @@ class ActionMailbox::InboundEmail::IncinerationJob < ApplicationJob queue_as :action_mailbox_incineration def self.schedule(inbound_email) - set(wait: ActionMailbox::InboundEmail::Incineratable::INCINERATABLE_AFTER).perform_later(inbound_email) + set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email) end def perform(inbound_email) diff --git a/app/models/action_mailbox/inbound_email/incineratable.rb b/app/models/action_mailbox/inbound_email/incineratable.rb index 8a82b87a99..364231a443 100644 --- a/app/models/action_mailbox/inbound_email/incineratable.rb +++ b/app/models/action_mailbox/inbound_email/incineratable.rb @@ -1,9 +1,6 @@ module ActionMailbox::InboundEmail::Incineratable extend ActiveSupport::Concern - # TODO: Extract into framework configuration - INCINERATABLE_AFTER = 30.days - included do before_update :remember_to_incinerate_later after_update_commit :incinerate_later, if: :incinerating_later? diff --git a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb index 5dd9be5a3d..ab9311edfb 100644 --- a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +++ b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb @@ -9,7 +9,7 @@ class ActionMailbox::InboundEmail::Incineratable::Incineration private def due? - @inbound_email.updated_at < ActionMailbox::InboundEmail::Incineratable::INCINERATABLE_AFTER.ago.end_of_day + @inbound_email.updated_at < ActionMailbox.incinerate_after.ago.end_of_day end def processed? diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb index ed890c488b..01cc38ec4a 100644 --- a/lib/action_mailbox.rb +++ b/lib/action_mailbox.rb @@ -9,4 +9,5 @@ module ActionMailbox autoload :Routing mattr_accessor :logger + mattr_accessor :incinerate_after, default: 30.days end diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb index 4e800c54de..b4758aacb5 100644 --- a/lib/action_mailbox/engine.rb +++ b/lib/action_mailbox/engine.rb @@ -6,10 +6,12 @@ module ActionMailbox config.eager_load_namespaces << ActionMailbox config.action_mailbox = ActiveSupport::OrderedOptions.new + config.action_mailbox.incinerate_after = 30.days initializer "action_mailbox.config" do config.after_initialize do |app| ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger + ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days end end end -- cgit v1.2.3 From 5714ecf36661423ea77b5cba518701ab98aeb19f Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 6 Oct 2018 22:27:50 -0400 Subject: Expand incineration tests to cover all processed statuses --- test/unit/inbound_email/incineration_test.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb index 212325b92d..72378df05a 100644 --- a/test/unit/inbound_email/incineration_test.rb +++ b/test/unit/inbound_email/incineration_test.rb @@ -1,11 +1,27 @@ require_relative '../../test_helper' class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase - test "incinerate emails 30 days after they have been processed" do + test "incinerating 30 days after delivery" do freeze_time assert_enqueued_with job: ActionMailbox::InboundEmail::IncinerationJob, at: 30.days.from_now do create_inbound_email_from_fixture("welcome.eml").delivered! end end + + test "incinerating 30 days after bounce" do + freeze_time + + assert_enqueued_with job: ActionMailbox::InboundEmail::IncinerationJob, at: 30.days.from_now do + create_inbound_email_from_fixture("welcome.eml").bounced! + end + end + + test "incinerating 30 days after failure" do + freeze_time + + assert_enqueued_with job: ActionMailbox::InboundEmail::IncinerationJob, at: 30.days.from_now do + create_inbound_email_from_fixture("welcome.eml").failed! + end + end end -- cgit v1.2.3 From 96a45285603971f546a89d6c4a59489b9859ddfb Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 8 Oct 2018 21:03:40 -0400 Subject: Test against Rails master --- Gemfile | 2 ++ Gemfile.lock | 114 +++++++++++++++++++++++++++++++---------------------------- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/Gemfile b/Gemfile index 9b78acc762..65f1a6f190 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,5 @@ source "https://rubygems.org" git_source(:github) { |repo_path| "https://github.com/#{repo_path}.git" } gemspec + +gem "rails", github: "rails/rails" diff --git a/Gemfile.lock b/Gemfile.lock index b3f3ab1f70..868d7f0819 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,54 +1,76 @@ -PATH - remote: . +GIT + remote: https://github.com/rails/rails.git + revision: a1ee4a9ff9d4a3cb255365310ead0dc7b739c6be specs: - actionmailbox (0.1.0) - rails (>= 5.2.0) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (5.2.0) - actionpack (= 5.2.0) + actioncable (6.0.0.alpha) + actionpack (= 6.0.0.alpha) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.0) - actionpack (= 5.2.0) - actionview (= 5.2.0) - activejob (= 5.2.0) + actionmailer (6.0.0.alpha) + actionpack (= 6.0.0.alpha) + actionview (= 6.0.0.alpha) + activejob (= 6.0.0.alpha) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.0) - actionview (= 5.2.0) - activesupport (= 5.2.0) + actionpack (6.0.0.alpha) + actionview (= 6.0.0.alpha) + activesupport (= 6.0.0.alpha) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.0) - activesupport (= 5.2.0) + actionview (6.0.0.alpha) + activesupport (= 6.0.0.alpha) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.0) - activesupport (= 5.2.0) + activejob (6.0.0.alpha) + activesupport (= 6.0.0.alpha) globalid (>= 0.3.6) - activemodel (5.2.0) - activesupport (= 5.2.0) - activerecord (5.2.0) - activemodel (= 5.2.0) - activesupport (= 5.2.0) - arel (>= 9.0) - activestorage (5.2.0) - actionpack (= 5.2.0) - activerecord (= 5.2.0) + activemodel (6.0.0.alpha) + activesupport (= 6.0.0.alpha) + activerecord (6.0.0.alpha) + activemodel (= 6.0.0.alpha) + activesupport (= 6.0.0.alpha) + activestorage (6.0.0.alpha) + actionpack (= 6.0.0.alpha) + activerecord (= 6.0.0.alpha) marcel (~> 0.3.1) - activesupport (5.2.0) + activesupport (6.0.0.alpha) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - arel (9.0.0) + rails (6.0.0.alpha) + actioncable (= 6.0.0.alpha) + actionmailer (= 6.0.0.alpha) + actionpack (= 6.0.0.alpha) + actionview (= 6.0.0.alpha) + activejob (= 6.0.0.alpha) + activemodel (= 6.0.0.alpha) + activerecord (= 6.0.0.alpha) + activestorage (= 6.0.0.alpha) + activesupport (= 6.0.0.alpha) + bundler (>= 1.3.0) + railties (= 6.0.0.alpha) + sprockets-rails (>= 2.0.0) + railties (6.0.0.alpha) + actionpack (= 6.0.0.alpha) + activesupport (= 6.0.0.alpha) + method_source + rake (>= 0.8.7) + thor (>= 0.19.0, < 2.0) + +PATH + remote: . + specs: + actionmailbox (0.1.0) + rails (>= 5.2.0) + +GEM + remote: https://rubygems.org/ + specs: builder (3.2.3) byebug (10.0.2) concurrent-ruby (1.0.5) @@ -63,43 +85,24 @@ GEM nokogiri (>= 1.5.9) mail (2.7.0) mini_mime (>= 0.1.1) - marcel (0.3.2) + marcel (0.3.3) mimemagic (~> 0.3.2) method_source (0.9.0) mimemagic (0.3.2) - mini_mime (1.0.0) + mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.11.3) nio4r (2.3.1) - nokogiri (1.8.4) + nokogiri (1.8.5) mini_portile2 (~> 2.3.0) rack (2.0.5) rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.2.0) - actioncable (= 5.2.0) - actionmailer (= 5.2.0) - actionpack (= 5.2.0) - actionview (= 5.2.0) - activejob (= 5.2.0) - activemodel (= 5.2.0) - activerecord (= 5.2.0) - activestorage (= 5.2.0) - activesupport (= 5.2.0) - bundler (>= 1.3.0) - railties (= 5.2.0) - sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - railties (5.2.0) - actionpack (= 5.2.0) - activesupport (= 5.2.0) - method_source - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) rake (12.3.1) sprockets (3.7.2) concurrent-ruby (~> 1.0) @@ -124,7 +127,8 @@ DEPENDENCIES actionmailbox! bundler (~> 1.15) byebug + rails! sqlite3 BUNDLED WITH - 1.16.4 + 1.16.5 -- cgit v1.2.3 From 3b84b7d57c2a06b7d5838266721866ed7db011ef Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 11 Oct 2018 12:31:52 -0400 Subject: Move installation tasks to lib/tasks/install.rake --- lib/tasks/action_mailbox.rake | 17 ----------------- lib/tasks/install.rake | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 17 deletions(-) delete mode 100644 lib/tasks/action_mailbox.rake create mode 100644 lib/tasks/install.rake diff --git a/lib/tasks/action_mailbox.rake b/lib/tasks/action_mailbox.rake deleted file mode 100644 index b7c11934f8..0000000000 --- a/lib/tasks/action_mailbox.rake +++ /dev/null @@ -1,17 +0,0 @@ -# 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 copy_migration active_storage:install ) - - task :copy_migration do - if Rake::Task.task_defined?("action_mailbox:install:migrations") - Rake::Task["action_mailbox:install:migrations"].invoke - else - Rake::Task["app:action_mailbox:install:migrations"].invoke - end - end -end diff --git a/lib/tasks/install.rake b/lib/tasks/install.rake new file mode 100644 index 0000000000..fdd76d8234 --- /dev/null +++ b/lib/tasks/install.rake @@ -0,0 +1,19 @@ +# 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 install:copy_migration active_storage:install ) + + namespace :install do + task :copy_migration do + if Rake::Task.task_defined?("action_mailbox:install:migrations") + Rake::Task["action_mailbox:install:migrations"].invoke + else + Rake::Task["app:action_mailbox:install:migrations"].invoke + end + end + end +end -- cgit v1.2.3 From 47445511862a4c9979fb46889011edf585391091 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 11 Oct 2018 12:32:11 -0400 Subject: Style --- lib/tasks/install.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/install.rake b/lib/tasks/install.rake index fdd76d8234..d5abf60042 100644 --- a/lib/tasks/install.rake +++ b/lib/tasks/install.rake @@ -5,7 +5,7 @@ namespace :action_mailbox do Rake::Task["install:migrations"].clear_comments desc "Copy over the migration" - task install: %w( environment install:copy_migration active_storage:install ) + task install: %w[ environment install:copy_migration active_storage:install ] namespace :install do task :copy_migration do -- cgit v1.2.3 From 6b7eac5c51cbef4acd1ab7f48884e7b614f71678 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 6 Oct 2018 22:02:08 -0400 Subject: Accept inbound emails from a variety of ingresses --- Gemfile | 2 + Gemfile.lock | 13 +++++ app/controllers/action_mailbox/base_controller.rb | 11 ++++ .../action_mailbox/inbound_emails_controller.rb | 17 ------ .../ingresses/amazon/inbound_emails_controller.rb | 26 +++++++++ .../ingresses/mailgun/inbound_emails_controller.rb | 61 ++++++++++++++++++++++ .../ingresses/postfix/inbound_emails_controller.rb | 11 ++++ .../sendgrid/inbound_emails_controller.rb | 16 ++++++ .../action_mailbox/inbound_email/message_id.rb | 9 +++- config/routes.rb | 9 +++- .../amazon/inbound_emails_controller_test.rb | 18 +++++++ .../mailgun/inbound_emails_controller_test.rb | 51 ++++++++++++++++++ .../postfix/inbound_emails_controller_test.rb | 33 ++++++++++++ .../sendgrid/inbound_emails_controller_test.rb | 33 ++++++++++++ test/unit/controller_test.rb | 27 ---------- 15 files changed, 291 insertions(+), 46 deletions(-) create mode 100644 app/controllers/action_mailbox/base_controller.rb delete mode 100644 app/controllers/action_mailbox/inbound_emails_controller.rb create mode 100644 app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb create mode 100644 app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb create mode 100644 app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb create mode 100644 app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb create mode 100644 test/controllers/ingresses/amazon/inbound_emails_controller_test.rb create mode 100644 test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb create mode 100644 test/controllers/ingresses/postfix/inbound_emails_controller_test.rb create mode 100644 test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb delete mode 100644 test/unit/controller_test.rb diff --git a/Gemfile b/Gemfile index 65f1a6f190..bb5cba25a7 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,5 @@ git_source(:github) { |repo_path| "https://github.com/#{repo_path}.git" } gemspec gem "rails", github: "rails/rails" + +gem "aws-sdk-sns" diff --git a/Gemfile.lock b/Gemfile.lock index 868d7f0819..96ed132ea1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,6 +71,17 @@ PATH GEM remote: https://rubygems.org/ specs: + aws-eventstream (1.0.1) + aws-partitions (1.105.0) + aws-sdk-core (3.30.0) + aws-eventstream (~> 1.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-sns (1.5.0) + aws-sdk-core (~> 3, >= 3.26.0) + aws-sigv4 (~> 1.0) + aws-sigv4 (1.0.3) builder (3.2.3) byebug (10.0.2) concurrent-ruby (1.0.5) @@ -80,6 +91,7 @@ GEM activesupport (>= 4.2.0) i18n (1.1.0) concurrent-ruby (~> 1.0) + jmespath (1.4.0) loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -125,6 +137,7 @@ PLATFORMS DEPENDENCIES actionmailbox! + aws-sdk-sns bundler (~> 1.15) byebug rails! diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb new file mode 100644 index 0000000000..680c6a9615 --- /dev/null +++ b/app/controllers/action_mailbox/base_controller.rb @@ -0,0 +1,11 @@ +class ActionMailbox::BaseController < ActionController::Base + skip_forgery_protection + + private + def authenticate + authenticate_or_request_with_http_basic("Action Mailbox") do |given_username, given_password| + ActiveSupport::SecurityUtils.secure_compare(given_username, username) && + ActiveSupport::SecurityUtils.secure_compare(given_password, password) + end + end +end diff --git a/app/controllers/action_mailbox/inbound_emails_controller.rb b/app/controllers/action_mailbox/inbound_emails_controller.rb deleted file mode 100644 index ec9bd6f229..0000000000 --- a/app/controllers/action_mailbox/inbound_emails_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# TODO: Add access protection using basic auth with verified tokens. Maybe coming from credentials by default? -# TODO: Spam/malware catching? -# TODO: Specific bounces for SMTP good citizenship: 200/404/400 -class ActionMailbox::InboundEmailsController < ActionController::Base - skip_forgery_protection - before_action :require_rfc822_message, only: :create - - def create - ActionMailbox::InboundEmail.create_and_extract_message_id!(params[:message]) - head :created - end - - private - def require_rfc822_message - head :unsupported_media_type unless params.require(:message).content_type == 'message/rfc822' - end -end diff --git a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb new file mode 100644 index 0000000000..7412928b56 --- /dev/null +++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb @@ -0,0 +1,26 @@ +class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController + before_action :ensure_verified + + # TODO: Lazy-load the AWS SDK + require "aws-sdk-sns/message_verifier" + cattr_accessor :verifier, default: Aws::SNS::MessageVerifier.new + + def create + ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email + head :no_content + end + + private + def raw_email + StringIO.new params.require(:content) + end + + + def ensure_verified + head :unauthorized unless verified? + end + + def verified? + verifier.authentic?(request.body) + end +end diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb new file mode 100644 index 0000000000..4d194a3e00 --- /dev/null +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -0,0 +1,61 @@ +class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController + before_action :ensure_authenticated + + def create + ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email + head :ok + end + + private + def raw_email + StringIO.new params.require("body-mime") + end + + + def ensure_authenticated + head :unauthorized unless authenticated? + end + + def authenticated? + Authenticator.new(authentication_params).authenticated? + rescue ArgumentError + false + end + + def authentication_params + params.permit(:timestamp, :token, :signature).to_h.symbolize_keys + end + + + class Authenticator + cattr_accessor :key + + attr_reader :timestamp, :token, :signature + + def initialize(timestamp:, token:, signature:) + @timestamp, @token, @signature = timestamp, token, signature + end + + def authenticated? + signed? && recent? + end + + private + def signed? + ActiveSupport::SecurityUtils.secure_compare signature, expected_signature + end + + # Allow for 10 minutes of drift between Mailgun time and local server time. + def recent? + time >= 10.minutes.ago + end + + def expected_signature + OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}" + end + + def time + Time.at Integer(timestamp) + end + end +end diff --git a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb new file mode 100644 index 0000000000..2631302606 --- /dev/null +++ b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb @@ -0,0 +1,11 @@ +class ActionMailbox::Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController + cattr_accessor :username, default: "actionmailbox" + cattr_accessor :password + + before_action :authenticate + + def create + ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:message) + head :no_content + end +end diff --git a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb new file mode 100644 index 0000000000..0b9e2e1866 --- /dev/null +++ b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -0,0 +1,16 @@ +class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController + cattr_accessor :username, default: "actionmailbox" + cattr_accessor :password + + before_action :authenticate + + def create + ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email + head :no_content + end + + private + def raw_email + StringIO.new params.require(:email) + end +end diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb index 590dbfc4d7..601c5f1a7e 100644 --- a/app/models/action_mailbox/inbound_email/message_id.rb +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -7,7 +7,14 @@ module ActionMailbox::InboundEmail::MessageId module ClassMethods def create_and_extract_message_id!(raw_email, **options) - create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options + create! message_id: extract_message_id(raw_email), **options do |inbound_email| + case raw_email + when ActionDispatch::Http::UploadedFile + inbound_email.raw_email.attach raw_email + else + inbound_email.raw_email.attach io: raw_email.tap(&:rewind), filename: "message.eml", content_type: "message/rfc822" + end + end end private diff --git a/config/routes.rb b/config/routes.rb index 733f137262..dea6cbd659 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,14 @@ # frozen_string_literal: true Rails.application.routes.draw do - post "/rails/action_mailbox/inbound_emails" => "action_mailbox/inbound_emails#create", as: :rails_inbound_emails + scope "/rails/action_mailbox" do + post "/amazon/inbound_emails" => "action_mailbox/ingresses/amazon/inbound_emails#create", as: :rails_amazon_inbound_emails + post "/postfix/inbound_emails" => "action_mailbox/ingresses/postfix/inbound_emails#create", as: :rails_postfix_inbound_emails + post "/sendgrid/inbound_emails" => "action_mailbox/ingresses/sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails + + # Mailgun requires that the webhook's URL end in 'mime' for it to receive the raw contents of emails. + post "/mailgun/inbound_emails/mime" => "action_mailbox/ingresses/mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails + end # TODO: Should these be mounted within the engine only? scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do diff --git a/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb b/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..5eda6d8d65 --- /dev/null +++ b/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +ActionMailbox::Ingresses::Amazon::InboundEmailsController.verifier = + Module.new { def self.authentic?(message); true; end } + +class ActionMailbox::Ingresses::Amazon::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + test "receiving an inbound email from Amazon" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_amazon_inbound_emails_url, params: { content: file_fixture("../files/welcome.eml").read }, as: :json + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end +end diff --git a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..873d0e81d8 --- /dev/null +++ b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb @@ -0,0 +1,51 @@ +require "test_helper" + +ActionMailbox::Ingresses::Mailgun::InboundEmailsController::Authenticator.key = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" + +class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + test "receiving an inbound email from Mailgun" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + travel_to "2018-10-09 15:15:00 EDT" + post rails_mailgun_inbound_emails_url, params: { + timestamp: 1539112500, + token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi", + signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc", + "body-mime" => file_fixture("../files/welcome.eml").read + } + end + + assert_response :ok + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end + + test "rejecting a delayed inbound email from Mailgun" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + travel_to "2018-10-09 15:26:00 EDT" + post rails_mailgun_inbound_emails_url, params: { + timestamp: 1539112500, + token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi", + signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc", + "body-mime" => file_fixture("../files/welcome.eml").read + } + end + + assert_response :unauthorized + end + + test "rejecting a forged inbound email from Mailgun" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + travel_to "2018-10-09 15:15:00 EDT" + post rails_mailgun_inbound_emails_url, params: { + timestamp: 1539112500, + token: "Zx8mJBiGmiiyyfWnho3zKyjCg2pxLARoCuBM7X9AKCioShGiMX", + signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc", + "body-mime" => file_fixture("../files/welcome.eml").read + } + end + + assert_response :unauthorized + end +end diff --git a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..bade5215d6 --- /dev/null +++ b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +ActionMailbox::Ingresses::Postfix::InboundEmailsController.password = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" + +class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + test "receiving an inbound email from Postfix" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_postfix_inbound_emails_url, headers: { authorization: credentials }, + params: { message: fixture_file_upload("files/welcome.eml", "message/rfc822") } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end + + test "rejecting an unauthorized inbound email from Postfix" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_postfix_inbound_emails_url, params: { message: fixture_file_upload("files/welcome.eml", "message/rfc822") } + end + + assert_response :unauthorized + end + + private + delegate :username, :password, to: ActionMailbox::Ingresses::Postfix::InboundEmailsController + + def credentials + ActionController::HttpAuthentication::Basic.encode_credentials username, password + end +end diff --git a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..7663c6657e --- /dev/null +++ b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +ActionMailbox::Ingresses::Sendgrid::InboundEmailsController.password = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" + +class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + test "receiving an inbound email from Sendgrid" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_sendgrid_inbound_emails_url, + headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end + + test "rejecting an unauthorized inbound email from Sendgrid" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_sendgrid_inbound_emails_url, params: { email: file_fixture("../files/welcome.eml").read } + end + + assert_response :unauthorized + end + + private + delegate :username, :password, to: ActionMailbox::Ingresses::Sendgrid::InboundEmailsController + + def credentials + ActionController::HttpAuthentication::Basic.encode_credentials username, password + end +end diff --git a/test/unit/controller_test.rb b/test/unit/controller_test.rb deleted file mode 100644 index 508e561244..0000000000 --- a/test/unit/controller_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -require_relative '../test_helper' - -class ActionMailbox::InboundEmailsControllerTest < ActionDispatch::IntegrationTest - test "receiving a valid RFC 822 message" do - assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do - post_inbound_email "welcome.eml" - end - - assert_response :created - - inbound_email = ActionMailbox::InboundEmail.last - assert_equal file_fixture('../files/welcome.eml').read, inbound_email.raw_email.download - end - - test "rejecting a message of an unsupported type" do - assert_no_difference -> { ActionMailbox::InboundEmail.count } do - post rails_inbound_emails_url, params: { message: fixture_file_upload('files/text.txt', 'text/plain') } - end - - assert_response :unsupported_media_type - end - - private - def post_inbound_email(fixture_name) - post rails_inbound_emails_url, params: { message: fixture_file_upload("files/#{fixture_name}", 'message/rfc822') } - end -end -- cgit v1.2.3 From 3984460424b678d844009319598e2b41c350ca3c Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 00:12:03 -0400 Subject: Add Mandrill support --- .../mandrill/inbound_emails_controller.rb | 59 ++++++++++++++++++++++ config/routes.rb | 1 + .../mandrill/inbound_emails_controller_test.rb | 31 ++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb create mode 100644 test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb new file mode 100644 index 0000000000..825ec6eabd --- /dev/null +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -0,0 +1,59 @@ +class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController + before_action :ensure_authenticated + + def create + raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email } + head :ok + rescue JSON::ParserError => error + log.error error.message + head :unprocessable_entity + end + + private + def raw_emails + events.lazy. + select { |event| event["event"] == "inbound" }. + collect { |event| event.dig("msg", "raw_msg") }. + collect { |message| StringIO.new message } + end + + def events + JSON.parse params.require(:mandrill_events) + end + + + def ensure_authenticated + head :unauthorized unless authenticated? + end + + def authenticated? + Authenticator.new(request).authenticated? + end + + class Authenticator + cattr_accessor :key + + attr_reader :request + + def initialize(request) + @request = request + end + + def authenticated? + ActiveSupport::SecurityUtils.secure_compare given_signature, expected_signature + end + + private + def given_signature + request.headers["X-Mandrill-Signature"] + end + + def expected_signature + Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)).strip + end + + def message + [ request.original_url, request.POST.sort ].flatten.join + end + end +end diff --git a/config/routes.rb b/config/routes.rb index dea6cbd659..4a337dcc5c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,7 @@ Rails.application.routes.draw do post "/amazon/inbound_emails" => "action_mailbox/ingresses/amazon/inbound_emails#create", as: :rails_amazon_inbound_emails post "/postfix/inbound_emails" => "action_mailbox/ingresses/postfix/inbound_emails#create", as: :rails_postfix_inbound_emails post "/sendgrid/inbound_emails" => "action_mailbox/ingresses/sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails + post "/mandrill/inbound_emails" => "action_mailbox/ingresses/mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails # Mailgun requires that the webhook's URL end in 'mime' for it to receive the raw contents of emails. post "/mailgun/inbound_emails/mime" => "action_mailbox/ingresses/mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails diff --git a/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..abef6baa4f --- /dev/null +++ b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +ActionMailbox::Ingresses::Mandrill::InboundEmailsController::Authenticator.key = "1l9Qf7lutEf7h73VXfBwhw" + +class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup do + @events = JSON.generate([{ event: "inbound", msg: { raw_msg: file_fixture("../files/welcome.eml").read } }]) + end + + test "receiving an inbound email from Mandrill" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_mandrill_inbound_emails_url, + headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events } + end + + assert_response :ok + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end + + test "rejecting a forged inbound email from Mandrill" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_mandrill_inbound_emails_url, + headers: { "X-Mandrill-Signature" => "forged" }, params: { mandrill_events: @events } + end + + assert_response :unauthorized + end +end -- cgit v1.2.3 From b5ada0de68aadc7da0c6ffabf496dc35d300daa9 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 08:51:34 -0400 Subject: Alphabetize --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 4a337dcc5c..95dee7696c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,9 +3,9 @@ Rails.application.routes.draw do scope "/rails/action_mailbox" do post "/amazon/inbound_emails" => "action_mailbox/ingresses/amazon/inbound_emails#create", as: :rails_amazon_inbound_emails + post "/mandrill/inbound_emails" => "action_mailbox/ingresses/mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails post "/postfix/inbound_emails" => "action_mailbox/ingresses/postfix/inbound_emails#create", as: :rails_postfix_inbound_emails post "/sendgrid/inbound_emails" => "action_mailbox/ingresses/sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails - post "/mandrill/inbound_emails" => "action_mailbox/ingresses/mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails # Mailgun requires that the webhook's URL end in 'mime' for it to receive the raw contents of emails. post "/mailgun/inbound_emails/mime" => "action_mailbox/ingresses/mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails -- cgit v1.2.3 From f8c0ddcf016661f9c508e71224d7722409ef0cec Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 08:53:37 -0400 Subject: Extract module scope --- config/routes.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 95dee7696c..f1bc9847f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true Rails.application.routes.draw do - scope "/rails/action_mailbox" do - post "/amazon/inbound_emails" => "action_mailbox/ingresses/amazon/inbound_emails#create", as: :rails_amazon_inbound_emails - post "/mandrill/inbound_emails" => "action_mailbox/ingresses/mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails - post "/postfix/inbound_emails" => "action_mailbox/ingresses/postfix/inbound_emails#create", as: :rails_postfix_inbound_emails - post "/sendgrid/inbound_emails" => "action_mailbox/ingresses/sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails + scope "/rails/action_mailbox", module: "action_mailbox/ingresses" do + post "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails + post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails + post "/postfix/inbound_emails" => "postfix/inbound_emails#create", as: :rails_postfix_inbound_emails + post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails - # Mailgun requires that the webhook's URL end in 'mime' for it to receive the raw contents of emails. - post "/mailgun/inbound_emails/mime" => "action_mailbox/ingresses/mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails + # Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails. + post "/mailgun/inbound_emails/mime" => "mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails end # TODO: Should these be mounted within the engine only? -- cgit v1.2.3 From 88227658217ee82af7516d6a8013a6c04f037073 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 09:53:02 -0400 Subject: Nest --- config/routes.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index f1bc9847f5..99a15d1d32 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,7 +13,8 @@ Rails.application.routes.draw do # TODO: Should these be mounted within the engine only? scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do - resources :inbound_emails, as: :rails_conductor_inbound_emails - post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute + resources :inbound_emails, as: :rails_conductor_inbound_emails do + post "reroute" => "reroutes#create" + end end end -- cgit v1.2.3 From 8973fbcf08fcbfb7af8ad9fd9465ff3c2de06437 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 10:38:51 -0400 Subject: Simplify incineration --- .../inbound_email/incineration_job.rb | 11 ----------- app/jobs/action_mailbox/incineration_job.rb | 11 +++++++++++ app/models/action_mailbox/inbound_email.rb | 4 ++++ .../action_mailbox/inbound_email/incineratable.rb | 23 +++++----------------- .../inbound_email/incineratable/incineration.rb | 4 ++-- .../action_mailbox/inbound_email/routable.rb | 2 +- test/unit/inbound_email/incineration_test.rb | 6 +++--- 7 files changed, 26 insertions(+), 35 deletions(-) delete mode 100644 app/jobs/action_mailbox/inbound_email/incineration_job.rb create mode 100644 app/jobs/action_mailbox/incineration_job.rb diff --git a/app/jobs/action_mailbox/inbound_email/incineration_job.rb b/app/jobs/action_mailbox/inbound_email/incineration_job.rb deleted file mode 100644 index d9422a3fa8..0000000000 --- a/app/jobs/action_mailbox/inbound_email/incineration_job.rb +++ /dev/null @@ -1,11 +0,0 @@ -class ActionMailbox::InboundEmail::IncinerationJob < ApplicationJob - queue_as :action_mailbox_incineration - - def self.schedule(inbound_email) - set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email) - end - - def perform(inbound_email) - inbound_email.incinerate - end -end diff --git a/app/jobs/action_mailbox/incineration_job.rb b/app/jobs/action_mailbox/incineration_job.rb new file mode 100644 index 0000000000..e9f0c78e0f --- /dev/null +++ b/app/jobs/action_mailbox/incineration_job.rb @@ -0,0 +1,11 @@ +class ActionMailbox::IncinerationJob < ApplicationJob + queue_as :action_mailbox_incineration + + def self.schedule(inbound_email) + set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email) + end + + def perform(inbound_email) + inbound_email.incinerate + end +end diff --git a/app/models/action_mailbox/inbound_email.rb b/app/models/action_mailbox/inbound_email.rb index f2589d7429..7d1a36b705 100644 --- a/app/models/action_mailbox/inbound_email.rb +++ b/app/models/action_mailbox/inbound_email.rb @@ -19,4 +19,8 @@ class ActionMailbox::InboundEmail < ActiveRecord::Base def source @source ||= raw_email.download end + + def processed? + delivered? || failed? || bounced? + end end diff --git a/app/models/action_mailbox/inbound_email/incineratable.rb b/app/models/action_mailbox/inbound_email/incineratable.rb index 364231a443..198846422c 100644 --- a/app/models/action_mailbox/inbound_email/incineratable.rb +++ b/app/models/action_mailbox/inbound_email/incineratable.rb @@ -2,27 +2,14 @@ module ActionMailbox::InboundEmail::Incineratable extend ActiveSupport::Concern included do - before_update :remember_to_incinerate_later - after_update_commit :incinerate_later, if: :incinerating_later? + after_update_commit :incinerate_later, if: -> { status_previously_changed? && processed? } + end + + def incinerate_later + ActionMailbox::IncinerationJob.schedule self end def incinerate Incineration.new(self).run end - - private - # TODO: Use enum change tracking once merged into Active Support - def remember_to_incinerate_later - if status_changed? && (delivered? || bounced? || failed?) - @incinerating_later = true - end - end - - def incinerating_later? - @incinerating_later ||= false - end - - def incinerate_later - ActionMailbox::InboundEmail::IncinerationJob.schedule(self) - end end diff --git a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb index ab9311edfb..801cc0c8b9 100644 --- a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +++ b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb @@ -4,7 +4,7 @@ class ActionMailbox::InboundEmail::Incineratable::Incineration end def run - @inbound_email.destroy if due? && processed? + @inbound_email.destroy! if due? && processed? end private @@ -13,6 +13,6 @@ class ActionMailbox::InboundEmail::Incineratable::Incineration end def processed? - @inbound_email.delivered? || @inbound_email.bounced? || @inbound_email.failed? + @inbound_email.processed? end end diff --git a/app/models/action_mailbox/inbound_email/routable.rb b/app/models/action_mailbox/inbound_email/routable.rb index 48b357af45..8f5b0ddd39 100644 --- a/app/models/action_mailbox/inbound_email/routable.rb +++ b/app/models/action_mailbox/inbound_email/routable.rb @@ -2,7 +2,7 @@ module ActionMailbox::InboundEmail::Routable extend ActiveSupport::Concern included do - after_create_commit :route_later, if: ->(inbound_email) { inbound_email.pending? } + after_create_commit :route_later, if: :pending? end def route_later diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb index 72378df05a..a3267afac9 100644 --- a/test/unit/inbound_email/incineration_test.rb +++ b/test/unit/inbound_email/incineration_test.rb @@ -4,7 +4,7 @@ class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase test "incinerating 30 days after delivery" do freeze_time - assert_enqueued_with job: ActionMailbox::InboundEmail::IncinerationJob, at: 30.days.from_now do + assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do create_inbound_email_from_fixture("welcome.eml").delivered! end end @@ -12,7 +12,7 @@ class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase test "incinerating 30 days after bounce" do freeze_time - assert_enqueued_with job: ActionMailbox::InboundEmail::IncinerationJob, at: 30.days.from_now do + assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do create_inbound_email_from_fixture("welcome.eml").bounced! end end @@ -20,7 +20,7 @@ class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase test "incinerating 30 days after failure" do freeze_time - assert_enqueued_with job: ActionMailbox::InboundEmail::IncinerationJob, at: 30.days.from_now do + assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do create_inbound_email_from_fixture("welcome.eml").failed! end end -- cgit v1.2.3 From 89ada1977ce648b2121cad7cfb95af9686ea016b Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 10:54:58 -0400 Subject: Remove unnecessary TODO Exception handlers are executed in the context of a mailbox instance and already have access to the inbound email. --- lib/action_mailbox/base.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index 3b12493662..103bbc544c 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -27,7 +27,6 @@ class ActionMailbox::Base 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 -- cgit v1.2.3 From 7903e3ada80d7938b1b78b06d0f678e95367a56b Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 12:38:26 -0400 Subject: Use a class attribute --- lib/action_mailbox/routing.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/action_mailbox/routing.rb b/lib/action_mailbox/routing.rb index b40e2774e4..d258b632f9 100644 --- a/lib/action_mailbox/routing.rb +++ b/lib/action_mailbox/routing.rb @@ -2,15 +2,17 @@ module ActionMailbox module Routing extend ActiveSupport::Concern - class_methods do - attr_reader :router + included do + cattr_accessor :router, default: ActionMailbox::Router.new + end + class_methods do def routing(routes) - (@router ||= ActionMailbox::Router.new).add_routes(routes) + router.add_routes(routes) end def route(inbound_email) - @router.route(inbound_email) + router.route(inbound_email) end end end -- cgit v1.2.3 From 2b09cbbe4407ce84b23450b27b2c3b6f35e8ef1b Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 12:42:54 -0400 Subject: Shush various interpreter warnings --- app/models/action_mailbox/inbound_email/message_id.rb | 2 +- lib/action_mailbox/base.rb | 2 +- lib/action_mailbox/test_helper.rb | 2 +- test/unit/mailbox/routing_test.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb index 590dbfc4d7..a1ec5c0437 100644 --- a/app/models/action_mailbox/inbound_email/message_id.rb +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -13,7 +13,7 @@ module ActionMailbox::InboundEmail::MessageId private def extract_message_id(raw_email) mail_from_source(raw_email.read).message_id - rescue => e + rescue # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" end end diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index 103bbc544c..01ddc9c59b 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -49,7 +49,7 @@ class ActionMailbox::Base inbound_email.processing! yield inbound_email.delivered! unless inbound_email.bounced? - rescue => exception + rescue inbound_email.failed! raise end diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb index b1d7af33f9..a74ea8ef57 100644 --- a/lib/action_mailbox/test_helper.rb +++ b/lib/action_mailbox/test_helper.rb @@ -9,7 +9,7 @@ module ActionMailbox end def create_inbound_email_from_mail(status: :processing, **mail_options) - raw_email = Tempfile.new.tap { |raw_email| raw_email.write Mail.new(mail_options).to_s } + raw_email = Tempfile.new.tap { |io| io.write Mail.new(mail_options).to_s } create_inbound_email(raw_email, status: status) end diff --git a/test/unit/mailbox/routing_test.rb b/test/unit/mailbox/routing_test.rb index 320dee6aab..f30199b7af 100644 --- a/test/unit/mailbox/routing_test.rb +++ b/test/unit/mailbox/routing_test.rb @@ -23,7 +23,7 @@ class ActionMailbox::Base::RoutingTest < ActiveSupport::TestCase test "delayed routing" do perform_enqueued_jobs only: ActionMailbox::RoutingJob do - another_inbound_email = create_inbound_email_from_fixture("welcome.eml", status: :pending) + create_inbound_email_from_fixture "welcome.eml", status: :pending assert_equal "Discussion: Let's debate these attachments", $processed end end -- cgit v1.2.3 From 5a8939447b2ae655f0c304ebb226eb5e6f9d8496 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 12:48:30 -0400 Subject: Bump mail to shush mismatched indentation warnings --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 868d7f0819..d01cb7bd76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,7 +83,7 @@ GEM loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.0) + mail (2.7.1) mini_mime (>= 0.1.1) marcel (0.3.3) mimemagic (~> 0.3.2) @@ -131,4 +131,4 @@ DEPENDENCIES sqlite3 BUNDLED WITH - 1.16.5 + 1.16.6 -- cgit v1.2.3 From 1c4a57e0e5b9666f0a05eb4fbdad09a6998ef54b Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 12:48:45 -0400 Subject: Remove needless autoloads ActionMailbox::Callbacks and ActionMailbox::Routing are eagerly loaded where they're used. --- lib/action_mailbox.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb index 01cc38ec4a..ae37cb84ed 100644 --- a/lib/action_mailbox.rb +++ b/lib/action_mailbox.rb @@ -5,8 +5,6 @@ module ActionMailbox autoload :Base autoload :Router - autoload :Callbacks - autoload :Routing mattr_accessor :logger mattr_accessor :incinerate_after, default: 30.days -- cgit v1.2.3 From cf8d76fdb42ab33c778b1787fb2ebe06481e2e3f Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 17 Oct 2018 13:22:52 -0400 Subject: Revert "Remove unnecessary TODO" This reverts commit 89ada1977ce648b2121cad7cfb95af9686ea016b. --- lib/action_mailbox/base.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index 01ddc9c59b..30ecc4b623 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -27,6 +27,7 @@ class ActionMailbox::Base 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 -- cgit v1.2.3 From b3919d01554d31c5486d17332f4a4dde89a23239 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 18 Oct 2018 10:23:17 -0400 Subject: Don't require Postfix to send form data --- .../ingresses/amazon/inbound_emails_controller.rb | 7 +------ .../ingresses/mailgun/inbound_emails_controller.rb | 8 +------- .../ingresses/mandrill/inbound_emails_controller.rb | 3 +-- .../ingresses/postfix/inbound_emails_controller.rb | 11 +++++++++-- .../ingresses/sendgrid/inbound_emails_controller.rb | 7 +------ app/models/action_mailbox/inbound_email/message_id.rb | 15 +++++---------- lib/action_mailbox/test_helper.rb | 11 ++++------- .../ingresses/postfix/inbound_emails_controller_test.rb | 16 +++++++++++++--- test/unit/inbound_email/message_id_test.rb | 4 +--- 9 files changed, 36 insertions(+), 46 deletions(-) diff --git a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb index 7412928b56..53b9bd07ec 100644 --- a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb @@ -6,16 +6,11 @@ class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox: cattr_accessor :verifier, default: Aws::SNS::MessageVerifier.new def create - ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email + ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content) head :no_content end private - def raw_email - StringIO.new params.require(:content) - end - - def ensure_verified head :unauthorized unless verified? end diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 4d194a3e00..77e05d712a 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -2,16 +2,11 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox before_action :ensure_authenticated def create - ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email + ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime") head :ok end private - def raw_email - StringIO.new params.require("body-mime") - end - - def ensure_authenticated head :unauthorized unless authenticated? end @@ -26,7 +21,6 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox params.permit(:timestamp, :token, :signature).to_h.symbolize_keys end - class Authenticator cattr_accessor :key diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index 825ec6eabd..7910359f51 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -13,8 +13,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo def raw_emails events.lazy. select { |event| event["event"] == "inbound" }. - collect { |event| event.dig("msg", "raw_msg") }. - collect { |message| StringIO.new message } + collect { |event| event.dig("msg", "raw_msg") } end def events diff --git a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb index 2631302606..d34257f9e9 100644 --- a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb @@ -2,10 +2,17 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsController < ActionMailbox cattr_accessor :username, default: "actionmailbox" cattr_accessor :password - before_action :authenticate + before_action :authenticate, :require_valid_rfc822_message def create - ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:message) + ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read head :no_content end + + private + def require_valid_rfc822_message + unless request.content_type == "message/rfc822" + head :unsupported_media_type + end + end end diff --git a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb index 0b9e2e1866..19c3b1b2c4 100644 --- a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -5,12 +5,7 @@ class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbo before_action :authenticate def create - ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email + ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email) head :no_content end - - private - def raw_email - StringIO.new params.require(:email) - end end diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb index 601c5f1a7e..5cfcadaba1 100644 --- a/app/models/action_mailbox/inbound_email/message_id.rb +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -6,20 +6,15 @@ module ActionMailbox::InboundEmail::MessageId end module ClassMethods - def create_and_extract_message_id!(raw_email, **options) - create! message_id: extract_message_id(raw_email), **options do |inbound_email| - case raw_email - when ActionDispatch::Http::UploadedFile - inbound_email.raw_email.attach raw_email - else - inbound_email.raw_email.attach io: raw_email.tap(&:rewind), filename: "message.eml", content_type: "message/rfc822" - end + def create_and_extract_message_id!(source, **options) + create! message_id: extract_message_id(source), **options do |inbound_email| + inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822" end end private - def extract_message_id(raw_email) - mail_from_source(raw_email.read).message_id + def extract_message_id(source) + mail_from_source(source).message_id rescue => e # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" end diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb index b1d7af33f9..23b2bb02ca 100644 --- a/lib/action_mailbox/test_helper.rb +++ b/lib/action_mailbox/test_helper.rb @@ -5,18 +5,15 @@ module ActionMailbox # 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 file_fixture(fixture_name), filename: fixture_name, status: status + create_inbound_email file_fixture(fixture_name).read, status: status end def create_inbound_email_from_mail(status: :processing, **mail_options) - raw_email = Tempfile.new.tap { |raw_email| raw_email.write Mail.new(mail_options).to_s } - create_inbound_email(raw_email, status: status) + create_inbound_email Mail.new(mail_options).to_s, status: status end - def create_inbound_email(io, filename: 'mail.eml', status: :processing) - ActionMailbox::InboundEmail.create_and_extract_message_id! \ - ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'), - status: status + def create_inbound_email(source, status: :processing) + ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status end def receive_inbound_email_from_fixture(*args) diff --git a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb index bade5215d6..3fa0854576 100644 --- a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb @@ -5,8 +5,8 @@ ActionMailbox::Ingresses::Postfix::InboundEmailsController.password = "tbsy84uSV class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest test "receiving an inbound email from Postfix" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do - post rails_postfix_inbound_emails_url, headers: { authorization: credentials }, - params: { message: fixture_file_upload("files/welcome.eml", "message/rfc822") } + post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + params: file_fixture("../files/welcome.eml").read end assert_response :no_content @@ -18,12 +18,22 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis test "rejecting an unauthorized inbound email from Postfix" do assert_no_difference -> { ActionMailbox::InboundEmail.count } do - post rails_postfix_inbound_emails_url, params: { message: fixture_file_upload("files/welcome.eml", "message/rfc822") } + post rails_postfix_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" }, + params: file_fixture("../files/welcome.eml").read end assert_response :unauthorized end + test "rejecting an inbound email of an unsupported media type from Postfix" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" }, + params: file_fixture("../files/welcome.eml").read + end + + assert_response :unsupported_media_type + end + private delegate :username, :password, to: ActionMailbox::Ingresses::Postfix::InboundEmailsController diff --git a/test/unit/inbound_email/message_id_test.rb b/test/unit/inbound_email/message_id_test.rb index aa9ce90b4c..c744a5bf99 100644 --- a/test/unit/inbound_email/message_id_test.rb +++ b/test/unit/inbound_email/message_id_test.rb @@ -7,9 +7,7 @@ class ActionMailbox::InboundEmail::MessageIdTest < ActiveSupport::TestCase end test "message id is generated if its missing" do - source_without_message_id = "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!" - inbound_email = create_inbound_email Tempfile.new.tap { |raw_email| raw_email.write source_without_message_id } - + inbound_email = create_inbound_email "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!" assert_not_nil inbound_email.message_id end end -- cgit v1.2.3 From 34b809ce6d6a73296bfbc9f9677f7fb04c218c49 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 18 Oct 2018 16:29:35 -0400 Subject: Fix logger reference --- .../action_mailbox/ingresses/mandrill/inbound_emails_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index 7910359f51..6ee2f5be54 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -5,7 +5,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email } head :ok rescue JSON::ParserError => error - log.error error.message + logger.error error.message head :unprocessable_entity end -- cgit v1.2.3 From 4411095290f24ccb2e263c9534acfd19d081120f Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 18 Oct 2018 16:31:38 -0400 Subject: Inline --- .../action_mailbox/ingresses/mandrill/inbound_emails_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index 6ee2f5be54..afaf3df28e 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -11,9 +11,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo private def raw_emails - events.lazy. - select { |event| event["event"] == "inbound" }. - collect { |event| event.dig("msg", "raw_msg") } + events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") } end def events -- cgit v1.2.3 From dbeef18b3ff04ce9ae8a53511f2afd0c7bae99e1 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 13:11:13 -0400 Subject: Rely on the 204 No Content default response --- .../action_mailbox/ingresses/amazon/inbound_emails_controller.rb | 1 - .../action_mailbox/ingresses/postfix/inbound_emails_controller.rb | 1 - .../action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb | 1 - 3 files changed, 3 deletions(-) diff --git a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb index 53b9bd07ec..557d1aeb04 100644 --- a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb @@ -7,7 +7,6 @@ class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox: def create ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content) - head :no_content end private diff --git a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb index d34257f9e9..72303378a9 100644 --- a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb @@ -6,7 +6,6 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsController < ActionMailbox def create ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read - head :no_content end private diff --git a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb index 19c3b1b2c4..f31845d8cd 100644 --- a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -6,6 +6,5 @@ class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbo def create ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email) - head :no_content end end -- cgit v1.2.3 From 8ff493d683d9fbdb508bd94643dbb11c54ac17b7 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 13:12:03 -0400 Subject: Style --- .../action_mailbox/ingresses/mailgun/inbound_emails_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 77e05d712a..2ca970fa8e 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -23,7 +23,6 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox class Authenticator cattr_accessor :key - attr_reader :timestamp, :token, :signature def initialize(timestamp:, token:, signature:) -- cgit v1.2.3 From fbd4219274f7c30de391ec8d7b6b6c5d76fb57c7 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 15:21:26 -0400 Subject: Don't short-circuit --- app/controllers/action_mailbox/base_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb index 680c6a9615..6f0e7e42d1 100644 --- a/app/controllers/action_mailbox/base_controller.rb +++ b/app/controllers/action_mailbox/base_controller.rb @@ -4,7 +4,7 @@ class ActionMailbox::BaseController < ActionController::Base private def authenticate authenticate_or_request_with_http_basic("Action Mailbox") do |given_username, given_password| - ActiveSupport::SecurityUtils.secure_compare(given_username, username) && + ActiveSupport::SecurityUtils.secure_compare(given_username, username) & ActiveSupport::SecurityUtils.secure_compare(given_password, password) end end -- cgit v1.2.3 From ebe3d0aaab633de77db494e50f720a8723acbd41 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 16:31:53 -0400 Subject: Add a Rake task for piping to the Postfix ingress --- Gemfile.lock | 18 ++++++++++++++++++ actionmailbox.gemspec | 1 + lib/tasks/ingress.rake | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 lib/tasks/ingress.rake diff --git a/Gemfile.lock b/Gemfile.lock index 2251704810..402a6e8a10 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,11 +66,14 @@ PATH remote: . specs: actionmailbox (0.1.0) + http (>= 4.0.0) rails (>= 5.2.0) GEM remote: https://rubygems.org/ specs: + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) aws-eventstream (1.0.1) aws-partitions (1.105.0) aws-sdk-core (3.30.0) @@ -86,9 +89,20 @@ GEM byebug (10.0.2) concurrent-ruby (1.0.5) crass (1.0.4) + domain_name (0.5.20180417) + unf (>= 0.0.5, < 1.0.0) erubi (1.7.1) globalid (0.4.1) activesupport (>= 4.2.0) + http (4.0.0) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 2.0) + http_parser.rb (~> 0.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + http-form_data (2.1.1) + http_parser.rb (0.6.0) i18n (1.1.0) concurrent-ruby (~> 1.0) jmespath (1.4.0) @@ -107,6 +121,7 @@ GEM nio4r (2.3.1) nokogiri (1.8.5) mini_portile2 (~> 2.3.0) + public_suffix (3.0.3) rack (2.0.5) rack-test (1.1.0) rack (>= 1.0, < 3) @@ -128,6 +143,9 @@ GEM thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.5) websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec index e413ac57f6..e3890ab574 100644 --- a/actionmailbox.gemspec +++ b/actionmailbox.gemspec @@ -16,6 +16,7 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 2.5.0" s.add_dependency "rails", ">= 5.2.0" + s.add_dependency "http", ">= 4.0.0" s.add_development_dependency "bundler", "~> 1.15" s.add_development_dependency "sqlite3" diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake new file mode 100644 index 0000000000..051d7a6c94 --- /dev/null +++ b/lib/tasks/ingress.rake @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +namespace :action_mailbox do + namespace :ingress do + desc "Pipe an inbound email from STDIN to the Postfix ingress at the given URL" + task :postfix do + require "active_support" + require "active_support/core_ext/object/blank" + require "http" + + url, username, password = ENV.values_at("URL", "INGRESS_USERNAME", "INGRESS_PASSWORD") + + if url.blank? || username.blank? || password.blank? + abort "URL, INGRESS_USERNAME, and INGRESS_PASSWORD are required" + end + + begin + response = HTTP.basic_auth(user: username, pass: password) + .timeout(connect: 1, write: 10, read: 10) + .post(url, headers: { "Content-Type" => "message/rfc822", "User-Agent" => "Postfix" }, body: STDIN) + + if response.status.success? + puts "2.0.0 HTTP #{response.status}" + exit 0 + else + puts "4.6.0 HTTP #{response.status}" + exit 1 + end + rescue HTTP::ConnectionError => error + puts "4.4.2 Error connecting to the Postfix ingress: #{error.message}" + exit 1 + rescue HTTP::TimeoutError + puts "4.4.7 Timed out piping to the Postfix ingress" + exit 1 + end + end + end +end -- cgit v1.2.3 From ce93bbae01dc7089cbdca510574e0009b7f669ed Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 17:34:06 -0400 Subject: Tighten up the acceptable drift --- .../action_mailbox/ingresses/mailgun/inbound_emails_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 2ca970fa8e..11c47a5ea9 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -38,9 +38,9 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox ActiveSupport::SecurityUtils.secure_compare signature, expected_signature end - # Allow for 10 minutes of drift between Mailgun time and local server time. + # Allow for 2 minutes of drift between Mailgun time and local server time. def recent? - time >= 10.minutes.ago + time >= 2.minutes.ago end def expected_signature -- cgit v1.2.3 From 96f8ca37fb47062e0cdc9e3a2765dfe58dcb6770 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 19:57:38 -0400 Subject: Mailgun copes with a 204 response status --- .../action_mailbox/ingresses/mailgun/inbound_emails_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 11c47a5ea9..10af57c58f 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -3,7 +3,6 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox def create ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime") - head :ok end private -- cgit v1.2.3 From 6b41c48f8ea098943e3948812d147c80eb79dd94 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 20:05:12 -0400 Subject: Update test --- test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb index 873d0e81d8..35e8314618 100644 --- a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb @@ -14,7 +14,7 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDis } end - assert_response :ok + assert_response :no_content inbound_email = ActionMailbox::InboundEmail.last assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download -- cgit v1.2.3 From c64a58f331331feca169b72ae3998ba748bd8e5a Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 22:33:45 -0400 Subject: Style --- .../action_mailbox/ingresses/mandrill/inbound_emails_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index afaf3df28e..19891e7f9c 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -29,7 +29,6 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo class Authenticator cattr_accessor :key - attr_reader :request def initialize(request) -- cgit v1.2.3 From 8b419e6d627c458782aaf9e887c35e428907f389 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 22:41:38 -0400 Subject: Remove unused fixture --- test/fixtures/files/text.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test/fixtures/files/text.txt diff --git a/test/fixtures/files/text.txt b/test/fixtures/files/text.txt deleted file mode 100644 index 84c3f1cf21..0000000000 --- a/test/fixtures/files/text.txt +++ /dev/null @@ -1 +0,0 @@ -This Is Not An Email! -- cgit v1.2.3 From 9182bbd1ebc88699ff101d7a0a304f387b091140 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 22:56:31 -0400 Subject: Inline --- .../ingresses/mailgun/inbound_emails_controller.rb | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 10af57c58f..46b0977592 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -11,13 +11,11 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox end def authenticated? - Authenticator.new(authentication_params).authenticated? - rescue ArgumentError - false - end - - def authentication_params - params.permit(:timestamp, :token, :signature).to_h.symbolize_keys + Authenticator.new( + timestamp: params.require(:timestamp), + token: params.require(:token), + signature: params.require(:signature) + ).authenticated? end class Authenticator @@ -25,7 +23,7 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox attr_reader :timestamp, :token, :signature def initialize(timestamp:, token:, signature:) - @timestamp, @token, @signature = timestamp, token, signature + @timestamp, @token, @signature = Integer(timestamp), token, signature end def authenticated? @@ -39,15 +37,11 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox # Allow for 2 minutes of drift between Mailgun time and local server time. def recent? - time >= 2.minutes.ago + Time.at(timestamp) >= 2.minutes.ago end def expected_signature OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}" end - - def time - Time.at Integer(timestamp) - end end end -- cgit v1.2.3 From b3641075fc4568b783f59b919fc8a5fb795d09ce Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 23:07:46 -0400 Subject: Go strict --- .../action_mailbox/ingresses/mandrill/inbound_emails_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index 19891e7f9c..0b01087c3d 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -45,7 +45,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo end def expected_signature - Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)).strip + Base64.strict_encode64 OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message) end def message -- cgit v1.2.3 From 02fcfec0c682cb3ff175927155a37e934ee1d0fe Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 19 Oct 2018 23:14:47 -0400 Subject: Skip needless array allocation --- .../action_mailbox/ingresses/mandrill/inbound_emails_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index 0b01087c3d..31e1315ccd 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -49,7 +49,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo end def message - [ request.original_url, request.POST.sort ].flatten.join + request.url + request.POST.sort.flatten.join end end end -- cgit v1.2.3 From ec1ad80b0b1b5652459b41957f345779a4d4c246 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 25 Oct 2018 13:37:18 -0500 Subject: Inherit from ActiveJob::Base --- app/jobs/action_mailbox/incineration_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/action_mailbox/incineration_job.rb b/app/jobs/action_mailbox/incineration_job.rb index e9f0c78e0f..f87c9d0cd9 100644 --- a/app/jobs/action_mailbox/incineration_job.rb +++ b/app/jobs/action_mailbox/incineration_job.rb @@ -1,4 +1,4 @@ -class ActionMailbox::IncinerationJob < ApplicationJob +class ActionMailbox::IncinerationJob < ActiveJob::Base queue_as :action_mailbox_incineration def self.schedule(inbound_email) -- cgit v1.2.3 From be0a8bec8701c7df2667dbf1569429218ea30370 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 29 Oct 2018 13:45:24 -0400 Subject: Raise when required config is missing --- app/controllers/action_mailbox/base_controller.rb | 10 ++++-- .../ingresses/mailgun/inbound_emails_controller.rb | 9 +++++ .../mandrill/inbound_emails_controller.rb | 9 +++++ .../mailgun/inbound_emails_controller_test.rb | 38 ++++++++++++++++++++++ .../mandrill/inbound_emails_controller_test.rb | 28 ++++++++++++++++ .../postfix/inbound_emails_controller_test.rb | 27 ++++++++++++++- .../sendgrid/inbound_emails_controller_test.rb | 27 ++++++++++++++- 7 files changed, 143 insertions(+), 5 deletions(-) diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb index 6f0e7e42d1..a64a817b51 100644 --- a/app/controllers/action_mailbox/base_controller.rb +++ b/app/controllers/action_mailbox/base_controller.rb @@ -3,9 +3,13 @@ class ActionMailbox::BaseController < ActionController::Base private def authenticate - authenticate_or_request_with_http_basic("Action Mailbox") do |given_username, given_password| - ActiveSupport::SecurityUtils.secure_compare(given_username, username) & - ActiveSupport::SecurityUtils.secure_compare(given_password, password) + if username.present? && password.present? + authenticate_or_request_with_http_basic("Action Mailbox") do |given_username, given_password| + ActiveSupport::SecurityUtils.secure_compare(given_username, username) & + ActiveSupport::SecurityUtils.secure_compare(given_password, password) + end + else + raise ArgumentError, "Missing required ingress credentials" end end end diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 46b0977592..c7e53b07f4 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -24,6 +24,8 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox def initialize(timestamp:, token:, signature:) @timestamp, @token, @signature = Integer(timestamp), token, signature + + ensure_presence_of_key end def authenticated? @@ -31,6 +33,13 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox end private + def ensure_presence_of_key + unless key.present? + raise ArgumentError, "Missing required Mailgun API key" + end + end + + def signed? ActiveSupport::SecurityUtils.secure_compare signature, expected_signature end diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index 31e1315ccd..bcaa5faf23 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -33,6 +33,8 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo def initialize(request) @request = request + + ensure_presence_of_key end def authenticated? @@ -40,6 +42,13 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo end private + def ensure_presence_of_key + unless key.present? + raise ArgumentError, "Missing required Mandrill API key" + end + end + + def given_signature request.headers["X-Mandrill-Signature"] end diff --git a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb index 35e8314618..8fb3dd28c6 100644 --- a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb @@ -48,4 +48,42 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDis assert_response :unauthorized end + + test "raising when the configured Mailgun API key is nil" do + switch_key_to nil do + assert_raises ArgumentError do + travel_to "2018-10-09 15:15:00 EDT" + post rails_mailgun_inbound_emails_url, params: { + timestamp: 1539112500, + token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi", + signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc", + "body-mime" => file_fixture("../files/welcome.eml").read + } + end + end + end + + test "raising when the configured Mailgun API key is blank" do + switch_key_to "" do + assert_raises ArgumentError do + travel_to "2018-10-09 15:15:00 EDT" + post rails_mailgun_inbound_emails_url, params: { + timestamp: 1539112500, + token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi", + signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc", + "body-mime" => file_fixture("../files/welcome.eml").read + } + end + end + end + + private + delegate :key, :key=, to: ActionMailbox::Ingresses::Mailgun::InboundEmailsController::Authenticator + + def switch_key_to(new_key) + previous_key, self.key = key, new_key + yield + ensure + self.key = previous_key + end end diff --git a/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb index abef6baa4f..1658d85104 100644 --- a/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb @@ -28,4 +28,32 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDi assert_response :unauthorized end + + test "raising when Mandrill API key is nil" do + switch_key_to nil do + assert_raises ArgumentError do + post rails_mandrill_inbound_emails_url, + headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events } + end + end + end + + test "raising when Mandrill API key is blank" do + switch_key_to "" do + assert_raises ArgumentError do + post rails_mandrill_inbound_emails_url, + headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events } + end + end + end + + private + delegate :key, :key=, to: ActionMailbox::Ingresses::Mandrill::InboundEmailsController::Authenticator + + def switch_key_to(new_key) + previous_key, self.key = key, new_key + yield + ensure + self.key = previous_key + end end diff --git a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb index 3fa0854576..a9588791b9 100644 --- a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb @@ -34,10 +34,35 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis assert_response :unsupported_media_type end + test "raising when the configured password is nil" do + switch_password_to nil do + assert_raises ArgumentError do + post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + params: file_fixture("../files/welcome.eml").read + end + end + end + + test "raising when the configured password is blank" do + switch_password_to "" do + assert_raises ArgumentError do + post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + params: file_fixture("../files/welcome.eml").read + end + end + end + private - delegate :username, :password, to: ActionMailbox::Ingresses::Postfix::InboundEmailsController + delegate :username, :password, :password=, to: ActionMailbox::Ingresses::Postfix::InboundEmailsController def credentials ActionController::HttpAuthentication::Basic.encode_credentials username, password end + + def switch_password_to(new_password) + previous_password, self.password = password, new_password + yield + ensure + self.password = previous_password + end end diff --git a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb index 7663c6657e..759a532087 100644 --- a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb @@ -24,10 +24,35 @@ class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDi assert_response :unauthorized end + test "raising when the configured password is nil" do + switch_password_to nil do + assert_raises ArgumentError do + post rails_sendgrid_inbound_emails_url, + headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read } + end + end + end + + test "raising when the configured password is blank" do + switch_password_to "" do + assert_raises ArgumentError do + post rails_sendgrid_inbound_emails_url, + headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read } + end + end + end + private - delegate :username, :password, to: ActionMailbox::Ingresses::Sendgrid::InboundEmailsController + delegate :username, :password, :password=, to: ActionMailbox::Ingresses::Sendgrid::InboundEmailsController def credentials ActionController::HttpAuthentication::Basic.encode_credentials username, password end + + def switch_password_to(new_password) + previous_password, self.password = password, new_password + yield + ensure + self.password = previous_password + end end -- cgit v1.2.3 From cb041ddc7e94da15e2db72188545f78da6cadb53 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 29 Oct 2018 14:19:46 -0400 Subject: Stage an Action Controller extraction --- app/controllers/action_mailbox/base_controller.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb index a64a817b51..d3846b06e1 100644 --- a/app/controllers/action_mailbox/base_controller.rb +++ b/app/controllers/action_mailbox/base_controller.rb @@ -4,12 +4,17 @@ class ActionMailbox::BaseController < ActionController::Base private def authenticate if username.present? && password.present? - authenticate_or_request_with_http_basic("Action Mailbox") do |given_username, given_password| - ActiveSupport::SecurityUtils.secure_compare(given_username, username) & - ActiveSupport::SecurityUtils.secure_compare(given_password, password) - end + http_basic_authenticate_or_request_with username: username, password: password, realm: "Action Mailbox" else raise ArgumentError, "Missing required ingress credentials" end end + + # TODO: Extract to ActionController::HttpAuthentication + def http_basic_authenticate_or_request_with(username:, password:, realm: nil) + authenticate_or_request_with_http_basic(realm || "Application") do |given_username, given_password| + ActiveSupport::SecurityUtils.secure_compare(given_username, username) & + ActiveSupport::SecurityUtils.secure_compare(given_password, password) + end + end end -- cgit v1.2.3 From c474daefb18e9bab96f6f0bb0bb30dfc00058cb3 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Tue, 30 Oct 2018 17:44:11 -0400 Subject: Validate address on route definition --- lib/action_mailbox/router/route.rb | 12 +++++++++--- test/unit/router_test.rb | 3 +-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/action_mailbox/router/route.rb b/lib/action_mailbox/router/route.rb index e123a93bb1..34cc684381 100644 --- a/lib/action_mailbox/router/route.rb +++ b/lib/action_mailbox/router/route.rb @@ -1,10 +1,10 @@ class ActionMailbox::Router::Route - class InvalidAddressError < StandardError; end - attr_reader :address, :mailbox_name def initialize(address, to:) @address, @mailbox_name = address, to + + ensure_valid_address end def match?(inbound_email) @@ -16,7 +16,7 @@ class ActionMailbox::Router::Route when Proc address.call(inbound_email) else - address.try(:match?, inbound_email) || raise(InvalidAddressError) + address.match?(inbound_email) end end @@ -25,6 +25,12 @@ class ActionMailbox::Router::Route end private + def ensure_valid_address + unless [ String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?) + raise ArgumentError, "Expected a String, Regexp, Proc, or matchable, got #{address.inspect}" + end + end + def recipients_from(mail) Array(mail.to) + Array(mail.cc) + Array(mail.bcc) end diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index cc87ae6c0e..b8c187f95e 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -112,9 +112,8 @@ module ActionMailbox end test "invalid address" do - assert_raises(ActionMailbox::Router::Route::InvalidAddressError) do + assert_raises(ArgumentError) do @router.add_route Array.new, to: :first - @router.route create_inbound_email_from_mail(to: "replies-nowhere@example.com", subject: "This is a reply") end end end -- cgit v1.2.3 From 7755f9b381c007ce98e0858473a9f29f1cd25311 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 5 Nov 2018 09:11:01 -0500 Subject: Read ingress passwords/API keys from encrypted credentials Fall back to ENV for people who prefer that approach. --- app/controllers/action_mailbox/base_controller.rb | 24 ++++++++++++-- .../ingresses/mailgun/inbound_emails_controller.rb | 38 ++++++++++++---------- .../mandrill/inbound_emails_controller.rb | 29 +++++++++-------- .../ingresses/postfix/inbound_emails_controller.rb | 5 +-- .../sendgrid/inbound_emails_controller.rb | 5 +-- lib/action_mailbox.rb | 1 + lib/action_mailbox/engine.rb | 1 + .../amazon/inbound_emails_controller_test.rb | 2 ++ .../mailgun/inbound_emails_controller_test.rb | 10 +++--- .../mandrill/inbound_emails_controller_test.rb | 9 +++-- .../postfix/inbound_emails_controller_test.rb | 18 ++-------- .../sendgrid/inbound_emails_controller_test.rb | 15 ++------- test/test_helper.rb | 19 ++++++++--- 13 files changed, 91 insertions(+), 85 deletions(-) diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb index d3846b06e1..a2f7eb4b61 100644 --- a/app/controllers/action_mailbox/base_controller.rb +++ b/app/controllers/action_mailbox/base_controller.rb @@ -1,15 +1,33 @@ class ActionMailbox::BaseController < ActionController::Base skip_forgery_protection + before_action :ensure_configured + private - def authenticate - if username.present? && password.present? - http_basic_authenticate_or_request_with username: username, password: password, realm: "Action Mailbox" + def ensure_configured + unless ActionMailbox.ingress == ingress_name + head :not_found + end + end + + def ingress_name + self.class.name[/^ActionMailbox::Ingresses::(.*?)::/, 1].underscore.to_sym + end + + + def authenticate_by_password + if password.present? + http_basic_authenticate_or_request_with username: "actionmailbox", password: password, realm: "Action Mailbox" else raise ArgumentError, "Missing required ingress credentials" end end + def password + Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"] + end + + # TODO: Extract to ActionController::HttpAuthentication def http_basic_authenticate_or_request_with(username:, password:, realm: nil) authenticate_or_request_with_http_basic(realm || "Application") do |given_username, given_password| diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index c7e53b07f4..0b763dcf18 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -11,21 +11,30 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox end def authenticated? - Authenticator.new( - timestamp: params.require(:timestamp), - token: params.require(:token), - signature: params.require(:signature) - ).authenticated? + if key.present? + Authenticator.new( + key: key, + timestamp: params.require(:timestamp), + token: params.require(:token), + signature: params.require(:signature) + ).authenticated? + else + raise ArgumentError, <<~MESSAGE.squish + Missing required Mailgun API key. Set action_mailbox.mailgun_api_key in your application's + encrypted credentials or provide the MAILGUN_INGRESS_API_KEY environment variable. + MESSAGE + end end - class Authenticator - cattr_accessor :key - attr_reader :timestamp, :token, :signature + def key + Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) || ENV["MAILGUN_INGRESS_API_KEY"] + end - def initialize(timestamp:, token:, signature:) - @timestamp, @token, @signature = Integer(timestamp), token, signature + class Authenticator + attr_reader :key, :timestamp, :token, :signature - ensure_presence_of_key + def initialize(key:, timestamp:, token:, signature:) + @key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature end def authenticated? @@ -33,13 +42,6 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox end private - def ensure_presence_of_key - unless key.present? - raise ArgumentError, "Missing required Mailgun API key" - end - end - - def signed? ActiveSupport::SecurityUtils.secure_compare signature, expected_signature end diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index bcaa5faf23..0601125cdb 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -24,17 +24,25 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo end def authenticated? - Authenticator.new(request).authenticated? + if key.present? + Authenticator.new(request, key).authenticated? + else + raise ArgumentError, <<~MESSAGE.squish + Missing required Mandrill API key. Set action_mailbox.mandrill_api_key in your application's + encrypted credentials or provide the MANDRILL_INGRESS_API_KEY environment variable. + MESSAGE + end end - class Authenticator - cattr_accessor :key - attr_reader :request + def key + Rails.application.credentials.dig(:action_mailbox, :mandrill_api_key) || ENV["MANDRILL_INGRESS_API_KEY"] + end - def initialize(request) - @request = request + class Authenticator + attr_reader :request, :key - ensure_presence_of_key + def initialize(request, key) + @request, @key = request, key end def authenticated? @@ -42,13 +50,6 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo end private - def ensure_presence_of_key - unless key.present? - raise ArgumentError, "Missing required Mandrill API key" - end - end - - def given_signature request.headers["X-Mandrill-Signature"] end diff --git a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb index 72303378a9..133accf651 100644 --- a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb @@ -1,8 +1,5 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController - cattr_accessor :username, default: "actionmailbox" - cattr_accessor :password - - before_action :authenticate, :require_valid_rfc822_message + before_action :authenticate_by_password, :require_valid_rfc822_message def create ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read diff --git a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb index f31845d8cd..b856eb5b94 100644 --- a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -1,8 +1,5 @@ class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController - cattr_accessor :username, default: "actionmailbox" - cattr_accessor :password - - before_action :authenticate + before_action :authenticate_by_password def create ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email) diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb index ae37cb84ed..fbc8122d9d 100644 --- a/lib/action_mailbox.rb +++ b/lib/action_mailbox.rb @@ -6,6 +6,7 @@ module ActionMailbox autoload :Base autoload :Router + mattr_accessor :ingress mattr_accessor :logger mattr_accessor :incinerate_after, default: 30.days end diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb index b4758aacb5..6a0312c65f 100644 --- a/lib/action_mailbox/engine.rb +++ b/lib/action_mailbox/engine.rb @@ -10,6 +10,7 @@ module ActionMailbox initializer "action_mailbox.config" do config.after_initialize do |app| + ActionMailbox.ingress = app.config.action_mailbox.ingress ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days end diff --git a/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb b/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb index 5eda6d8d65..c36c500cbe 100644 --- a/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb @@ -4,6 +4,8 @@ ActionMailbox::Ingresses::Amazon::InboundEmailsController.verifier = Module.new { def self.authentic?(message); true; end } class ActionMailbox::Ingresses::Amazon::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup { ActionMailbox.ingress = :amazon } + test "receiving an inbound email from Amazon" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do post rails_amazon_inbound_emails_url, params: { content: file_fixture("../files/welcome.eml").read }, as: :json diff --git a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb index 8fb3dd28c6..c5ec71013e 100644 --- a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb @@ -1,8 +1,10 @@ require "test_helper" -ActionMailbox::Ingresses::Mailgun::InboundEmailsController::Authenticator.key = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" +ENV["MAILGUN_INGRESS_API_KEY"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup { ActionMailbox.ingress = :mailgun } + test "receiving an inbound email from Mailgun" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do travel_to "2018-10-09 15:15:00 EDT" @@ -78,12 +80,10 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDis end private - delegate :key, :key=, to: ActionMailbox::Ingresses::Mailgun::InboundEmailsController::Authenticator - def switch_key_to(new_key) - previous_key, self.key = key, new_key + previous_key, ENV["MAILGUN_INGRESS_API_KEY"] = ENV["MAILGUN_INGRESS_API_KEY"], new_key yield ensure - self.key = previous_key + ENV["MAILGUN_INGRESS_API_KEY"] = previous_key end end diff --git a/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb index 1658d85104..c8a8e731d6 100644 --- a/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb @@ -1,9 +1,10 @@ require "test_helper" -ActionMailbox::Ingresses::Mandrill::InboundEmailsController::Authenticator.key = "1l9Qf7lutEf7h73VXfBwhw" +ENV["MANDRILL_INGRESS_API_KEY"] = "1l9Qf7lutEf7h73VXfBwhw" class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDispatch::IntegrationTest setup do + ActionMailbox.ingress = :mandrill @events = JSON.generate([{ event: "inbound", msg: { raw_msg: file_fixture("../files/welcome.eml").read } }]) end @@ -48,12 +49,10 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDi end private - delegate :key, :key=, to: ActionMailbox::Ingresses::Mandrill::InboundEmailsController::Authenticator - def switch_key_to(new_key) - previous_key, self.key = key, new_key + previous_key, ENV["MANDRILL_INGRESS_API_KEY"] = ENV["MANDRILL_INGRESS_API_KEY"], new_key yield ensure - self.key = previous_key + ENV["MANDRILL_INGRESS_API_KEY"] = previous_key end end diff --git a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb index a9588791b9..5e0777aa30 100644 --- a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb @@ -1,8 +1,8 @@ require "test_helper" -ActionMailbox::Ingresses::Postfix::InboundEmailsController.password = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" - class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup { ActionMailbox.ingress = :postfix } + test "receiving an inbound email from Postfix" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, @@ -51,18 +51,4 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis end end end - - private - delegate :username, :password, :password=, to: ActionMailbox::Ingresses::Postfix::InboundEmailsController - - def credentials - ActionController::HttpAuthentication::Basic.encode_credentials username, password - end - - def switch_password_to(new_password) - previous_password, self.password = password, new_password - yield - ensure - self.password = previous_password - end end diff --git a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb index 759a532087..bf6ccd8f03 100644 --- a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb @@ -1,8 +1,8 @@ require "test_helper" -ActionMailbox::Ingresses::Sendgrid::InboundEmailsController.password = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" - class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup { ActionMailbox.ingress = :sendgrid } + test "receiving an inbound email from Sendgrid" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do post rails_sendgrid_inbound_emails_url, @@ -43,16 +43,7 @@ class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDi end private - delegate :username, :password, :password=, to: ActionMailbox::Ingresses::Sendgrid::InboundEmailsController - def credentials - ActionController::HttpAuthentication::Basic.encode_credentials username, password - end - - def switch_password_to(new_password) - previous_password, self.password = password, new_password - yield - ensure - self.password = previous_password + ActionController::HttpAuthentication::Basic.encode_credentials "actionmailbox", ENV["RAILS_INBOUND_EMAIL_PASSWORD"] end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 264f9e8482..b4459f3feb 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,5 +1,5 @@ -# Configure Rails Environment ENV["RAILS_ENV"] = "test" +ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" require_relative "../test/dummy/config/environment" ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] @@ -7,14 +7,11 @@ require "rails/test_help" require "byebug" -# Filter out Minitest backtrace while allowing backtrace from other libraries -# to be shown. Minitest.backtrace_filter = Minitest::BacktraceFilter.new require "rails/test_unit/reporter" Rails::TestUnitReporter.executable = 'bin/test' -# Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path @@ -28,6 +25,20 @@ class ActiveSupport::TestCase include ActionMailbox::TestHelper, ActiveJob::TestHelper end +class ActionDispatch::IntegrationTest + private + def credentials + ActionController::HttpAuthentication::Basic.encode_credentials "actionmailbox", ENV["RAILS_INBOUND_EMAIL_PASSWORD"] + end + + def switch_password_to(new_password) + previous_password, ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = ENV["RAILS_INBOUND_EMAIL_PASSWORD"], new_password + yield + ensure + ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = previous_password + end +end + if ARGV.include?("-v") ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveJob::Base.logger = Logger.new(STDOUT) -- cgit v1.2.3 From d02ae4c7ae336412a5ff19400cda6b87ed58a465 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 5 Nov 2018 09:56:02 -0500 Subject: Rename authentication callbacks --- .../ingresses/amazon/inbound_emails_controller.rb | 10 +++------- .../ingresses/mailgun/inbound_emails_controller.rb | 4 ++-- .../ingresses/mandrill/inbound_emails_controller.rb | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb index 557d1aeb04..4d56e27c76 100644 --- a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb @@ -1,5 +1,5 @@ class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController - before_action :ensure_verified + before_action :authenticate # TODO: Lazy-load the AWS SDK require "aws-sdk-sns/message_verifier" @@ -10,11 +10,7 @@ class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox: end private - def ensure_verified - head :unauthorized unless verified? - end - - def verified? - verifier.authentic?(request.body) + def authenticate + head :unauthorized unless verifier.authentic?(request.body) end end diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 0b763dcf18..e878192603 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -1,12 +1,12 @@ class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController - before_action :ensure_authenticated + before_action :authenticate def create ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime") end private - def ensure_authenticated + def authenticate head :unauthorized unless authenticated? end diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index 0601125cdb..b32b254076 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -1,5 +1,5 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController - before_action :ensure_authenticated + before_action :authenticate def create raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email } @@ -19,7 +19,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbo end - def ensure_authenticated + def authenticate head :unauthorized unless authenticated? end -- cgit v1.2.3 From b859eebff8545eea972d479137e14b917e6519dc Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 5 Nov 2018 12:39:37 -0500 Subject: Only load the AWS SDK when the Amazon ingress is configured --- app/controllers/action_mailbox/base_controller.rb | 4 ++++ .../ingresses/amazon/inbound_emails_controller.rb | 11 ++++++++--- lib/action_mailbox/engine.rb | 13 ++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb index a2f7eb4b61..c234ecd250 100644 --- a/app/controllers/action_mailbox/base_controller.rb +++ b/app/controllers/action_mailbox/base_controller.rb @@ -1,6 +1,10 @@ class ActionMailbox::BaseController < ActionController::Base skip_forgery_protection + def self.prepare + # Override in concrete controllers to run code on load. + end + before_action :ensure_configured private diff --git a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb index 4d56e27c76..d3998be2d4 100644 --- a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb @@ -1,9 +1,14 @@ class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate - # TODO: Lazy-load the AWS SDK - require "aws-sdk-sns/message_verifier" - cattr_accessor :verifier, default: Aws::SNS::MessageVerifier.new + cattr_accessor :verifier + + def self.prepare + self.verifier ||= begin + require "aws-sdk-sns/message_verifier" + Aws::SNS::MessageVerifier.new + end + end def create ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content) diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb index 6a0312c65f..cf438d8f24 100644 --- a/lib/action_mailbox/engine.rb +++ b/lib/action_mailbox/engine.rb @@ -10,10 +10,21 @@ module ActionMailbox initializer "action_mailbox.config" do config.after_initialize do |app| - ActionMailbox.ingress = app.config.action_mailbox.ingress ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days end end + + initializer "action_mailbox.ingress" do + config.after_initialize do |app| + if ActionMailbox.ingress = app.config.action_mailbox.ingress.presence + config.to_prepare do + if ingress_controller_class = "ActionMailbox::Ingresses::#{ActionMailbox.ingress.to_s.classify}::InboundEmailsController".safe_constantize + ingress_controller_class.prepare + end + end + end + end + end end end -- cgit v1.2.3 From a7e8fe4d6e3f18eb3431e9fc95b42542ae2946fb Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 5 Nov 2018 12:52:56 -0500 Subject: Remove duplicate method --- .../controllers/ingresses/sendgrid/inbound_emails_controller_test.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb index bf6ccd8f03..0c7d0d6846 100644 --- a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb @@ -41,9 +41,4 @@ class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDi end end end - - private - def credentials - ActionController::HttpAuthentication::Basic.encode_credentials "actionmailbox", ENV["RAILS_INBOUND_EMAIL_PASSWORD"] - end end -- cgit v1.2.3 From 6608bf60aad4d13df3b22e66326c1adbf9a51f3d Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 5 Nov 2018 14:04:05 -0500 Subject: The ingress username is constant --- lib/tasks/ingress.rake | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index 051d7a6c94..5a944a113d 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -8,14 +8,16 @@ namespace :action_mailbox do require "active_support/core_ext/object/blank" require "http" - url, username, password = ENV.values_at("URL", "INGRESS_USERNAME", "INGRESS_PASSWORD") + unless url = ENV["URL"].presence + abort "URL is required" + end - if url.blank? || username.blank? || password.blank? - abort "URL, INGRESS_USERNAME, and INGRESS_PASSWORD are required" + unless password = ENV["INGRESS_PASSWORD"].presence + abort "INGRESS_PASSWORD is required" end begin - response = HTTP.basic_auth(user: username, pass: password) + response = HTTP.basic_auth(user: "actionmailbox", pass: password) .timeout(connect: 1, write: 10, read: 10) .post(url, headers: { "Content-Type" => "message/rfc822", "User-Agent" => "Postfix" }, body: STDIN) -- cgit v1.2.3 From ac7fd0e56886eb134554789e014d2736b95d7042 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 5 Nov 2018 14:18:03 -0500 Subject: Always emit enhanced SMTP status codes --- lib/tasks/ingress.rake | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index 5a944a113d..510951aa2a 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -9,31 +9,32 @@ namespace :action_mailbox do require "http" unless url = ENV["URL"].presence - abort "URL is required" + abort "5.3.5 URL is required" end unless password = ENV["INGRESS_PASSWORD"].presence - abort "INGRESS_PASSWORD is required" + abort "5.3.5 INGRESS_PASSWORD is required" end begin response = HTTP.basic_auth(user: "actionmailbox", pass: password) .timeout(connect: 1, write: 10, read: 10) - .post(url, headers: { "Content-Type" => "message/rfc822", "User-Agent" => "Postfix" }, body: STDIN) + .post(url, headers: { "Content-Type" => "message/rfc822", "User-Agent" => ENV.fetch("USER_AGENT", "Postfix") }, body: STDIN) - if response.status.success? + case + when response.status.success? puts "2.0.0 HTTP #{response.status}" - exit 0 + when response.status.unauthorized? + abort "4.7.0 HTTP #{response.status}" + when response.status.unsupported_media_type? + abort "5.6.1 HTTP #{response.status}" else - puts "4.6.0 HTTP #{response.status}" - exit 1 + abort "4.0.0 HTTP #{response.status}" end rescue HTTP::ConnectionError => error - puts "4.4.2 Error connecting to the Postfix ingress: #{error.message}" - exit 1 + abort "4.4.2 Error connecting to the Postfix ingress: #{error.message}" rescue HTTP::TimeoutError - puts "4.4.7 Timed out piping to the Postfix ingress" - exit 1 + abort "4.4.7 Timed out piping to the Postfix ingress" end end end -- cgit v1.2.3 From 874446cd72f9edac60deb8dcd91cf2f019b5347c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 5 Nov 2018 16:36:21 -0800 Subject: Extract Mail-bound methods into mail_ext for future upstream work --- app/models/action_mailbox/inbound_email.rb | 6 +----- app/models/action_mailbox/inbound_email/message_id.rb | 2 +- lib/action_mailbox.rb | 1 + lib/action_mailbox/mail_ext.rb | 4 ++++ lib/action_mailbox/mail_ext/from_source.rb | 5 +++++ lib/action_mailbox/mail_ext/recipients.rb | 5 +++++ lib/action_mailbox/router/route.rb | 8 ++------ 7 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 lib/action_mailbox/mail_ext.rb create mode 100644 lib/action_mailbox/mail_ext/from_source.rb create mode 100644 lib/action_mailbox/mail_ext/recipients.rb diff --git a/app/models/action_mailbox/inbound_email.rb b/app/models/action_mailbox/inbound_email.rb index 7d1a36b705..ea564a254e 100644 --- a/app/models/action_mailbox/inbound_email.rb +++ b/app/models/action_mailbox/inbound_email.rb @@ -8,12 +8,8 @@ class ActionMailbox::InboundEmail < ActiveRecord::Base has_one_attached :raw_email enum status: %i[ pending processing delivered failed bounced ] - def self.mail_from_source(source) - Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s) - end - def mail - @mail ||= self.class.mail_from_source(source) + @mail ||= Mail.from_source(source) end def source diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb index 5cfcadaba1..70d39d1e33 100644 --- a/app/models/action_mailbox/inbound_email/message_id.rb +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -14,7 +14,7 @@ module ActionMailbox::InboundEmail::MessageId private def extract_message_id(source) - mail_from_source(source).message_id + Mail.from_source(source).message_id rescue => e # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" end diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb index fbc8122d9d..417a1a7431 100644 --- a/lib/action_mailbox.rb +++ b/lib/action_mailbox.rb @@ -1,4 +1,5 @@ require "action_mailbox/engine" +require "action_mailbox/mail_ext" module ActionMailbox extend ActiveSupport::Autoload diff --git a/lib/action_mailbox/mail_ext.rb b/lib/action_mailbox/mail_ext.rb new file mode 100644 index 0000000000..abdaae4f9e --- /dev/null +++ b/lib/action_mailbox/mail_ext.rb @@ -0,0 +1,4 @@ +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/lib/action_mailbox/mail_ext/from_source.rb b/lib/action_mailbox/mail_ext/from_source.rb new file mode 100644 index 0000000000..ddc3cbe385 --- /dev/null +++ b/lib/action_mailbox/mail_ext/from_source.rb @@ -0,0 +1,5 @@ +module Mail + def self.from_source(source) + Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s) + end +end diff --git a/lib/action_mailbox/mail_ext/recipients.rb b/lib/action_mailbox/mail_ext/recipients.rb new file mode 100644 index 0000000000..58561c7f01 --- /dev/null +++ b/lib/action_mailbox/mail_ext/recipients.rb @@ -0,0 +1,5 @@ +class Mail::Message + def recipients + Array(to) + Array(cc) + Array(bcc) + end +end diff --git a/lib/action_mailbox/router/route.rb b/lib/action_mailbox/router/route.rb index 34cc684381..24be8d4804 100644 --- a/lib/action_mailbox/router/route.rb +++ b/lib/action_mailbox/router/route.rb @@ -10,9 +10,9 @@ class ActionMailbox::Router::Route def match?(inbound_email) case address when String - recipients_from(inbound_email.mail).any? { |recipient| address.casecmp?(recipient) } + inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) } when Regexp - recipients_from(inbound_email.mail).any? { |recipient| address.match?(recipient) } + inbound_email.mail.recipients.any? { |recipient| address.match?(recipient) } when Proc address.call(inbound_email) else @@ -30,8 +30,4 @@ class ActionMailbox::Router::Route raise ArgumentError, "Expected a String, Regexp, Proc, or matchable, got #{address.inspect}" end end - - def recipients_from(mail) - Array(mail.to) + Array(mail.cc) + Array(mail.bcc) - end end -- cgit v1.2.3 From 5d851664b8551942fb869c379c0332e5e5bc9f53 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 5 Nov 2018 16:58:07 -0800 Subject: Add addresses convenience method and a test for it --- lib/action_mailbox/mail_ext/recipients.rb | 21 +++++++++++++++++++++ test/unit/mail_ext/recipients_test.rb | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 test/unit/mail_ext/recipients_test.rb diff --git a/lib/action_mailbox/mail_ext/recipients.rb b/lib/action_mailbox/mail_ext/recipients.rb index 58561c7f01..baf97472a6 100644 --- a/lib/action_mailbox/mail_ext/recipients.rb +++ b/lib/action_mailbox/mail_ext/recipients.rb @@ -2,4 +2,25 @@ class Mail::Message def recipients Array(to) + Array(cc) + Array(bcc) end + + def recipients_addresses + convert_to_addresses recipients + end + + def to_addresses + convert_to_addresses to + end + + def cc_addresses + convert_to_addresses cc + end + + def bcc_addresses + convert_to_addresses bcc + end + + private + def convert_to_addresses(recipients) + recipients.collect { |recipient| Mail::Address.new recipient } + end end diff --git a/test/unit/mail_ext/recipients_test.rb b/test/unit/mail_ext/recipients_test.rb new file mode 100644 index 0000000000..86db569a53 --- /dev/null +++ b/test/unit/mail_ext/recipients_test.rb @@ -0,0 +1,29 @@ +require_relative '../../test_helper' + +module MailExt + class RecipientsTest < ActiveSupport::TestCase + setup do + @mail = Mail.new(to: "david@basecamp.com", cc: "jason@basecamp.com", bcc: "andrea@basecamp.com") + end + + test "recipients include everyone from to, cc, and bcc" do + assert_equal %w[ david@basecamp.com jason@basecamp.com andrea@basecamp.com ], @mail.recipients + end + + test "recipients addresses use address objects" do + assert_equal "basecamp.com", @mail.recipients_addresses.first.domain + end + + test "to addresses use address objects" do + assert_equal "basecamp.com", @mail.to_addresses.first.domain + end + + test "cc addresses use address objects" do + assert_equal "basecamp.com", @mail.cc_addresses.first.domain + end + + test "bcc addresses use address objects" do + assert_equal "basecamp.com", @mail.bcc_addresses.first.domain + end + end +end \ No newline at end of file -- cgit v1.2.3 From fb5ce96bc5c293a1178c2ae52de962bcc59ea5c8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 6 Nov 2018 17:03:10 -0800 Subject: Split out all the addresses from recipients --- lib/action_mailbox/mail_ext/addresses.rb | 26 ++++++++++++++++++++++++++ lib/action_mailbox/mail_ext/recipients.rb | 21 --------------------- 2 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 lib/action_mailbox/mail_ext/addresses.rb diff --git a/lib/action_mailbox/mail_ext/addresses.rb b/lib/action_mailbox/mail_ext/addresses.rb new file mode 100644 index 0000000000..d7ace82bbf --- /dev/null +++ b/lib/action_mailbox/mail_ext/addresses.rb @@ -0,0 +1,26 @@ +class Mail::Message + def from_address + Mail::Address.new from + end + + def recipients_addresses + convert_to_addresses recipients + end + + def to_addresses + convert_to_addresses to + end + + def cc_addresses + convert_to_addresses cc + end + + def bcc_addresses + convert_to_addresses bcc + end + + private + def convert_to_addresses(recipients) + recipients.collect { |recipient| Mail::Address.new recipient } + end +end diff --git a/lib/action_mailbox/mail_ext/recipients.rb b/lib/action_mailbox/mail_ext/recipients.rb index baf97472a6..58561c7f01 100644 --- a/lib/action_mailbox/mail_ext/recipients.rb +++ b/lib/action_mailbox/mail_ext/recipients.rb @@ -2,25 +2,4 @@ class Mail::Message def recipients Array(to) + Array(cc) + Array(bcc) end - - def recipients_addresses - convert_to_addresses recipients - end - - def to_addresses - convert_to_addresses to - end - - def cc_addresses - convert_to_addresses cc - end - - def bcc_addresses - convert_to_addresses bcc - end - - private - def convert_to_addresses(recipients) - recipients.collect { |recipient| Mail::Address.new recipient } - end end -- cgit v1.2.3 From 763583f3fbc9fe6e472767baa54d46816c917614 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 6 Nov 2018 17:03:27 -0800 Subject: Provide a default ActionMailbox::TestCase --- lib/action_mailbox/test_case.rb | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 lib/action_mailbox/test_case.rb diff --git a/lib/action_mailbox/test_case.rb b/lib/action_mailbox/test_case.rb new file mode 100644 index 0000000000..1b3aec1393 --- /dev/null +++ b/lib/action_mailbox/test_case.rb @@ -0,0 +1,8 @@ +require "action_mailbox/test_helper" +require "active_support/test_case" + +module ActionMailbox + class TestCase < ActiveSupport::TestCase + include ActionMailbox::TestHelper + end +end -- cgit v1.2.3 From 0683f5aa5b6f617d40a06efd10c7adc96a56d6a7 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 7 Nov 2018 08:40:00 -0500 Subject: Autoload ActionMailbox::TestCase --- lib/action_mailbox.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb index 417a1a7431..a508e861ac 100644 --- a/lib/action_mailbox.rb +++ b/lib/action_mailbox.rb @@ -6,6 +6,7 @@ module ActionMailbox autoload :Base autoload :Router + autoload :TestCase mattr_accessor :ingress mattr_accessor :logger -- cgit v1.2.3 From b4052dc6d4e305c35ba02ae2b878e058622ce868 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 7 Nov 2018 16:38:10 -0800 Subject: Recipient fields may be nil --- lib/action_mailbox/mail_ext/addresses.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/action_mailbox/mail_ext/addresses.rb b/lib/action_mailbox/mail_ext/addresses.rb index d7ace82bbf..75e39686ea 100644 --- a/lib/action_mailbox/mail_ext/addresses.rb +++ b/lib/action_mailbox/mail_ext/addresses.rb @@ -21,6 +21,6 @@ class Mail::Message private def convert_to_addresses(recipients) - recipients.collect { |recipient| Mail::Address.new recipient } + Array(recipients).collect { |recipient| Mail::Address.new recipient } end end -- cgit v1.2.3 From b7dfb8c1cff6d1a400182a85d2f16489e825499c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 7 Nov 2018 21:05:00 -0800 Subject: TIL: The from field can technically have multiple people in it, but it is exceedingly rare. --- lib/action_mailbox/mail_ext/addresses.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/action_mailbox/mail_ext/addresses.rb b/lib/action_mailbox/mail_ext/addresses.rb index 75e39686ea..94bb846d75 100644 --- a/lib/action_mailbox/mail_ext/addresses.rb +++ b/lib/action_mailbox/mail_ext/addresses.rb @@ -1,6 +1,6 @@ class Mail::Message def from_address - Mail::Address.new from + Mail::Address.new from.first end def recipients_addresses -- cgit v1.2.3 From 724c0a54660ec691aaf2aa5f46d78fb69f8ae362 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 7 Nov 2018 21:33:17 -0800 Subject: Use the address lists that have already been supplied to ensure we get the names as well --- lib/action_mailbox/mail_ext/addresses.rb | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/action_mailbox/mail_ext/addresses.rb b/lib/action_mailbox/mail_ext/addresses.rb index 94bb846d75..8059ce0027 100644 --- a/lib/action_mailbox/mail_ext/addresses.rb +++ b/lib/action_mailbox/mail_ext/addresses.rb @@ -1,26 +1,21 @@ class Mail::Message def from_address - Mail::Address.new from.first + header[:from]&.address_list&.addresses&.first end def recipients_addresses - convert_to_addresses recipients + to_addresses + cc_addresses + bcc_addresses end def to_addresses - convert_to_addresses to + Array(header[:to]&.address_list&.addresses) end def cc_addresses - convert_to_addresses cc + Array(header[:cc]&.address_list&.addresses) end def bcc_addresses - convert_to_addresses bcc + Array(header[:bcc]&.address_list&.addresses) end - - private - def convert_to_addresses(recipients) - Array(recipients).collect { |recipient| Mail::Address.new recipient } - end end -- cgit v1.2.3 From 5e1a67c67cbfdbec636b310793fe65be6e5bbb4b Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 8 Nov 2018 07:30:37 -0500 Subject: Emit a transient error code for incorrect usage --- lib/tasks/ingress.rake | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index 510951aa2a..e86f95e859 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -8,12 +8,10 @@ namespace :action_mailbox do require "active_support/core_ext/object/blank" require "http" - unless url = ENV["URL"].presence - abort "5.3.5 URL is required" - end + url, password = ENV.values_at("URL", "INGRESS_PASSWORD") - unless password = ENV["INGRESS_PASSWORD"].presence - abort "5.3.5 INGRESS_PASSWORD is required" + if url.blank? || password.blank? + abort "4.3.5 URL and INGRESS_PASSWORD are required" end begin -- cgit v1.2.3 From a4d429f4a7c417b719270e9164634c83ded3d0ec Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 9 Nov 2018 13:45:51 -0500 Subject: Discard incineration jobs for missing inbound emails --- app/jobs/action_mailbox/incineration_job.rb | 2 ++ test/jobs/incineration_job_test.rb | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 test/jobs/incineration_job_test.rb diff --git a/app/jobs/action_mailbox/incineration_job.rb b/app/jobs/action_mailbox/incineration_job.rb index f87c9d0cd9..81b381b0d0 100644 --- a/app/jobs/action_mailbox/incineration_job.rb +++ b/app/jobs/action_mailbox/incineration_job.rb @@ -1,6 +1,8 @@ class ActionMailbox::IncinerationJob < ActiveJob::Base queue_as :action_mailbox_incineration + discard_on ActiveRecord::RecordNotFound + def self.schedule(inbound_email) set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email) end diff --git a/test/jobs/incineration_job_test.rb b/test/jobs/incineration_job_test.rb new file mode 100644 index 0000000000..a3907898ab --- /dev/null +++ b/test/jobs/incineration_job_test.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionMailbox::IncinerationJobTest < ActiveJob::TestCase + setup { @inbound_email = receive_inbound_email_from_fixture("welcome.eml") } + + test "ignoring a missing inbound email" do + @inbound_email.destroy! + + perform_enqueued_jobs do + assert_nothing_raised do + ActionMailbox::IncinerationJob.perform_later @inbound_email + end + end + end +end -- cgit v1.2.3 From 14b5b5afe2bf7369499243931a88db5f40933def Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sun, 11 Nov 2018 18:23:13 -0500 Subject: Make job queues configurable --- app/jobs/action_mailbox/incineration_job.rb | 2 +- app/jobs/action_mailbox/routing_job.rb | 2 +- lib/action_mailbox.rb | 1 + lib/action_mailbox/engine.rb | 4 ++++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/jobs/action_mailbox/incineration_job.rb b/app/jobs/action_mailbox/incineration_job.rb index 81b381b0d0..4fc0cb0782 100644 --- a/app/jobs/action_mailbox/incineration_job.rb +++ b/app/jobs/action_mailbox/incineration_job.rb @@ -1,5 +1,5 @@ class ActionMailbox::IncinerationJob < ActiveJob::Base - queue_as :action_mailbox_incineration + queue_as { ActionMailbox.queues[:incineration] } discard_on ActiveRecord::RecordNotFound diff --git a/app/jobs/action_mailbox/routing_job.rb b/app/jobs/action_mailbox/routing_job.rb index af4aa9f8fa..786187528a 100644 --- a/app/jobs/action_mailbox/routing_job.rb +++ b/app/jobs/action_mailbox/routing_job.rb @@ -1,5 +1,5 @@ class ActionMailbox::RoutingJob < ActiveJob::Base - queue_as :action_mailbox_routing + queue_as { ActionMailbox.queues[:routing] } def perform(inbound_email) inbound_email.route diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb index a508e861ac..aec2db7b05 100644 --- a/lib/action_mailbox.rb +++ b/lib/action_mailbox.rb @@ -11,4 +11,5 @@ module ActionMailbox mattr_accessor :ingress mattr_accessor :logger mattr_accessor :incinerate_after, default: 30.days + mattr_accessor :queues, default: {} end diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb index cf438d8f24..e0954e4e49 100644 --- a/lib/action_mailbox/engine.rb +++ b/lib/action_mailbox/engine.rb @@ -8,10 +8,14 @@ module ActionMailbox config.action_mailbox = ActiveSupport::OrderedOptions.new 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_after = app.config.action_mailbox.incinerate_after || 30.days + ActionMailbox.queues = app.config.action_mailbox.queues || {} end end -- cgit v1.2.3 From dd43f6e7d90ab0d2c354a9bef1970e4f3a7e6471 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Tue, 13 Nov 2018 17:44:31 -0500 Subject: Consider X-Original-To --- lib/action_mailbox/mail_ext/addresses.rb | 6 +++++- lib/action_mailbox/mail_ext/recipients.rb | 2 +- test/unit/mail_ext/recipients_test.rb | 12 ++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/action_mailbox/mail_ext/addresses.rb b/lib/action_mailbox/mail_ext/addresses.rb index 8059ce0027..f64c3ef5df 100644 --- a/lib/action_mailbox/mail_ext/addresses.rb +++ b/lib/action_mailbox/mail_ext/addresses.rb @@ -4,7 +4,7 @@ class Mail::Message end def recipients_addresses - to_addresses + cc_addresses + bcc_addresses + to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses end def to_addresses @@ -18,4 +18,8 @@ class Mail::Message 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 diff --git a/lib/action_mailbox/mail_ext/recipients.rb b/lib/action_mailbox/mail_ext/recipients.rb index 58561c7f01..87255ce6ce 100644 --- a/lib/action_mailbox/mail_ext/recipients.rb +++ b/lib/action_mailbox/mail_ext/recipients.rb @@ -1,5 +1,5 @@ class Mail::Message def recipients - Array(to) + Array(cc) + Array(bcc) + Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s) end end diff --git a/test/unit/mail_ext/recipients_test.rb b/test/unit/mail_ext/recipients_test.rb index 86db569a53..e39e1eb89c 100644 --- a/test/unit/mail_ext/recipients_test.rb +++ b/test/unit/mail_ext/recipients_test.rb @@ -3,11 +3,15 @@ require_relative '../../test_helper' module MailExt class RecipientsTest < ActiveSupport::TestCase setup do - @mail = Mail.new(to: "david@basecamp.com", cc: "jason@basecamp.com", bcc: "andrea@basecamp.com") + @mail = Mail.new \ + to: "david@basecamp.com", + cc: "jason@basecamp.com", + bcc: "andrea@basecamp.com", + x_original_to: "ryan@basecamp.com" end - test "recipients include everyone from to, cc, and bcc" do - assert_equal %w[ david@basecamp.com jason@basecamp.com andrea@basecamp.com ], @mail.recipients + test "recipients include everyone from to, cc, bcc, and x-original-to" do + assert_equal %w[ david@basecamp.com jason@basecamp.com andrea@basecamp.com ryan@basecamp.com ], @mail.recipients end test "recipients addresses use address objects" do @@ -26,4 +30,4 @@ module MailExt assert_equal "basecamp.com", @mail.bcc_addresses.first.domain end end -end \ No newline at end of file +end -- cgit v1.2.3 From e96dbf984d7574c07232a480d61832c216eca348 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 14 Nov 2018 14:16:03 -0500 Subject: Read STDIN to upload it http.rb can't stream from pipes. --- lib/tasks/ingress.rake | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index e86f95e859..96bd9b184a 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -17,7 +17,8 @@ namespace :action_mailbox do begin response = HTTP.basic_auth(user: "actionmailbox", pass: password) .timeout(connect: 1, write: 10, read: 10) - .post(url, headers: { "Content-Type" => "message/rfc822", "User-Agent" => ENV.fetch("USER_AGENT", "Postfix") }, body: STDIN) + .post(url, body: STDIN.read, + headers: { "Content-Type" => "message/rfc822", "User-Agent" => ENV.fetch("USER_AGENT", "Postfix") }) case when response.status.success? -- cgit v1.2.3 From 7aef5695b8d225a89f41320869b5065fa8f1b158 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 14 Nov 2018 20:47:40 -0500 Subject: Permit redirecting stderr to /dev/null to shush deprecation warnings --- lib/tasks/ingress.rake | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index 96bd9b184a..6620e1fe43 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -11,7 +11,8 @@ namespace :action_mailbox do url, password = ENV.values_at("URL", "INGRESS_PASSWORD") if url.blank? || password.blank? - abort "4.3.5 URL and INGRESS_PASSWORD are required" + puts "4.3.5 URL and INGRESS_PASSWORD are required" + exit 1 end begin @@ -24,16 +25,21 @@ namespace :action_mailbox do when response.status.success? puts "2.0.0 HTTP #{response.status}" when response.status.unauthorized? - abort "4.7.0 HTTP #{response.status}" + puts "4.7.0 HTTP #{response.status}" + exit 1 when response.status.unsupported_media_type? - abort "5.6.1 HTTP #{response.status}" + puts "5.6.1 HTTP #{response.status}" + exit 1 else - abort "4.0.0 HTTP #{response.status}" + puts "4.0.0 HTTP #{response.status}" + exit 1 end rescue HTTP::ConnectionError => error - abort "4.4.2 Error connecting to the Postfix ingress: #{error.message}" + puts "4.4.2 Error connecting to the Postfix ingress: #{error.message}" + exit 1 rescue HTTP::TimeoutError - abort "4.4.7 Timed out piping to the Postfix ingress" + puts "4.4.7 Timed out piping to the Postfix ingress" + exit 1 end end end -- cgit v1.2.3 From be11dbbc401a2a4d7dd4004a4368a26923866981 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 16 Nov 2018 13:24:51 -0500 Subject: Wrap callbacks in status tracking Mark the inbound email as processing prior to running before_processing callbacks. Catch failures in after_processing callbacks. --- lib/action_mailbox/base.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index 30ecc4b623..55914401e1 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -21,8 +21,8 @@ class ActionMailbox::Base end def perform_processing - run_callbacks :process do - track_status_of_inbound_email do + track_status_of_inbound_email do + run_callbacks :process do process end end -- cgit v1.2.3 From 42f9cd62c34891474df7271d148cdb231ab73d90 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 22 Nov 2018 16:40:30 -0500 Subject: Revert "Nest" This reverts commit 88227658217ee82af7516d6a8013a6c04f037073. --- config/routes.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 99a15d1d32..f1bc9847f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,8 +13,7 @@ Rails.application.routes.draw do # TODO: Should these be mounted within the engine only? scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do - resources :inbound_emails, as: :rails_conductor_inbound_emails do - post "reroute" => "reroutes#create" - end + resources :inbound_emails, as: :rails_conductor_inbound_emails + post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute end end -- cgit v1.2.3 From 2e4df7cea354339cbae20252ae14106a0cce12b5 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 24 Nov 2018 12:20:39 -0500 Subject: Test the full incineration flow --- test/unit/inbound_email/incineration_test.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb index a3267afac9..b46101bcc4 100644 --- a/test/unit/inbound_email/incineration_test.rb +++ b/test/unit/inbound_email/incineration_test.rb @@ -7,6 +7,12 @@ class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do create_inbound_email_from_fixture("welcome.eml").delivered! end + + travel 30.days + + assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do + perform_enqueued_jobs only: ActionMailbox::IncinerationJob + end end test "incinerating 30 days after bounce" do @@ -15,6 +21,12 @@ class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do create_inbound_email_from_fixture("welcome.eml").bounced! end + + travel 30.days + + assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do + perform_enqueued_jobs only: ActionMailbox::IncinerationJob + end end test "incinerating 30 days after failure" do @@ -23,5 +35,11 @@ class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do create_inbound_email_from_fixture("welcome.eml").failed! end + + travel 30.days + + assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do + perform_enqueued_jobs only: ActionMailbox::IncinerationJob + end end end -- cgit v1.2.3 From 148110e70c0a408ea418a8e36a6a99305fdd9c99 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sun, 25 Nov 2018 14:30:05 -0500 Subject: Extract ActionMailbox::PostfixRelayer --- Gemfile.lock | 24 ++++------- actionmailbox.gemspec | 2 +- lib/action_mailbox/postfix_relayer.rb | 60 ++++++++++++++++++++++++++ lib/tasks/ingress.rake | 37 ++++------------ test/test_helper.rb | 1 + test/unit/postfix_relayer_test.rb | 80 +++++++++++++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 45 deletions(-) create mode 100644 lib/action_mailbox/postfix_relayer.rb create mode 100644 test/unit/postfix_relayer_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index 402a6e8a10..6d8ef0e9f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,7 +66,6 @@ PATH remote: . specs: actionmailbox (0.1.0) - http (>= 4.0.0) rails (>= 5.2.0) GEM @@ -88,21 +87,13 @@ GEM builder (3.2.3) byebug (10.0.2) concurrent-ruby (1.0.5) + crack (0.4.3) + safe_yaml (~> 1.0.0) crass (1.0.4) - domain_name (0.5.20180417) - unf (>= 0.0.5, < 1.0.0) erubi (1.7.1) globalid (0.4.1) activesupport (>= 4.2.0) - http (4.0.0) - addressable (~> 2.3) - http-cookie (~> 1.0) - http-form_data (~> 2.0) - http_parser.rb (~> 0.6.0) - http-cookie (1.0.3) - domain_name (~> 0.5) - http-form_data (2.1.1) - http_parser.rb (0.6.0) + hashdiff (0.3.7) i18n (1.1.0) concurrent-ruby (~> 1.0) jmespath (1.4.0) @@ -131,6 +122,7 @@ GEM rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) rake (12.3.1) + safe_yaml (1.0.4) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -143,9 +135,10 @@ GEM thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.5) + webmock (3.4.2) + addressable (>= 2.3.6) + crack (>= 0.3.2) + hashdiff websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) @@ -160,6 +153,7 @@ DEPENDENCIES byebug rails! sqlite3 + webmock BUNDLED WITH 1.16.6 diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec index e3890ab574..e3dae277d4 100644 --- a/actionmailbox.gemspec +++ b/actionmailbox.gemspec @@ -16,11 +16,11 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 2.5.0" s.add_dependency "rails", ">= 5.2.0" - s.add_dependency "http", ">= 4.0.0" s.add_development_dependency "bundler", "~> 1.15" s.add_development_dependency "sqlite3" s.add_development_dependency "byebug" + s.add_development_dependency "webmock" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- test/*`.split("\n") diff --git a/lib/action_mailbox/postfix_relayer.rb b/lib/action_mailbox/postfix_relayer.rb new file mode 100644 index 0000000000..de4fdc06d0 --- /dev/null +++ b/lib/action_mailbox/postfix_relayer.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "net/http" +require "uri" +require "openssl" + +module ActionMailbox + class PostfixRelayer + class Result < Struct.new(:output) + def success? + !failure? + end + + def failure? + output.match?(/\A[45]\.\d\.\d /) + end + end + + attr_reader :uri, :username, :password, :user_agent + + def initialize(url:, username: "actionmailbox", password:, user_agent: nil) + @uri, @username, @password, @user_agent = URI(url), username, password, user_agent || "Postfix" + end + + def relay(source) + case response = post(source) + when Net::HTTPSuccess + Result.new "2.0.0 Successfully relayed message to Postfix ingress" + when Net::HTTPUnauthorized + Result.new "4.7.0 Invalid credentials for Postfix 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 Postfix ingress: #{error.message}" + rescue Timeout::Error + Result.new "4.4.2 Timed out relaying to Postfix ingress" + rescue => error + Result.new "4.0.0 Error relaying to Postfix ingress: #{error.message}" + end + + private + def post(source) + client.post uri.path, source, + "Content-Type" => "message/rfc822", + "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| + connection.use_ssl = uri.scheme == "https" + connection.verify_mode = OpenSSL::SSL::VERIFY_PEER + + connection.open_timeout = 1 + connection.read_timeout = 10 + end + end + end +end diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index 6620e1fe43..6d1c0797ff 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -2,45 +2,24 @@ namespace :action_mailbox do namespace :ingress do - desc "Pipe an inbound email from STDIN to the Postfix ingress at the given URL" + desc "Pipe an inbound email from STDIN to the Postfix ingress (URL and INGRESS_PASSWORD required)" task :postfix do require "active_support" require "active_support/core_ext/object/blank" - require "http" + require "action_mailbox/ingresses/postfix/relayer" - url, password = ENV.values_at("URL", "INGRESS_PASSWORD") + url, password, user_agent = ENV.values_at("URL", "INGRESS_PASSWORD", "USER_AGENT") if url.blank? || password.blank? - puts "4.3.5 URL and INGRESS_PASSWORD are required" + echo "4.3.5 URL and INGRESS_PASSWORD are required" exit 1 end - begin - response = HTTP.basic_auth(user: "actionmailbox", pass: password) - .timeout(connect: 1, write: 10, read: 10) - .post(url, body: STDIN.read, - headers: { "Content-Type" => "message/rfc822", "User-Agent" => ENV.fetch("USER_AGENT", "Postfix") }) - - case - when response.status.success? - puts "2.0.0 HTTP #{response.status}" - when response.status.unauthorized? - puts "4.7.0 HTTP #{response.status}" - exit 1 - when response.status.unsupported_media_type? - puts "5.6.1 HTTP #{response.status}" - exit 1 - else - puts "4.0.0 HTTP #{response.status}" - exit 1 + ActionMailbox::PostfixRelayer.new(url: url, password: password, user_agent: user_agent) + .relay(STDIN.read).tap do |result| + echo result.output + exit result.success? ? 0 : 1 end - rescue HTTP::ConnectionError => error - puts "4.4.2 Error connecting to the Postfix ingress: #{error.message}" - exit 1 - rescue HTTP::TimeoutError - puts "4.4.7 Timed out piping to the Postfix ingress" - exit 1 - end end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index b4459f3feb..31fc59bba2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -6,6 +6,7 @@ ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/mi require "rails/test_help" require "byebug" +require "webmock/minitest" Minitest.backtrace_filter = Minitest::BacktraceFilter.new diff --git a/test/unit/postfix_relayer_test.rb b/test/unit/postfix_relayer_test.rb new file mode 100644 index 0000000000..9f5b78e216 --- /dev/null +++ b/test/unit/postfix_relayer_test.rb @@ -0,0 +1,80 @@ +require_relative '../test_helper' + +require 'action_mailbox/postfix_relayer' + +module ActionMailbox + class PostfixRelayerTest < ActiveSupport::TestCase + URL = "https://example.com/rails/action_mailbox/postfix/inbound_emails" + INGRESS_PASSWORD = "secret" + + setup do + @relayer = ActionMailbox::PostfixRelayer.new(url: URL, password: INGRESS_PASSWORD) + end + + test "successfully relaying an email" do + stub_request(:post, URL).to_return status: 204 + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "2.0.0 Successfully relayed message to Postfix ingress", result.output + assert result.success? + assert_not result.failure? + + assert_requested :post, URL, body: file_fixture("welcome.eml").read, + basic_auth: [ "actionmailbox", INGRESS_PASSWORD ], headers: { "Content-Type" => "message/rfc822", "User-Agent" => "Postfix" } + end + + test "unsuccessfully relaying with invalid credentials" do + stub_request(:post, URL).to_return status: 401 + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.7.0 Invalid credentials for Postfix ingress", result.output + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to an unspecified server error" do + stub_request(:post, URL).to_return status: 500 + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.0.0 HTTP 500", result.output + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to a gateway timeout" do + stub_request(:post, URL).to_return status: 504 + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.0.0 HTTP 504", result.output + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to ECONNRESET" do + stub_request(:post, URL).to_raise Errno::ECONNRESET.new + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.4.2 Network error relaying to Postfix ingress: Connection reset by peer", result.output + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to connection failure" do + stub_request(:post, URL).to_raise SocketError.new("Failed to open TCP connection to example.com:443") + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.4.2 Network error relaying to Postfix ingress: Failed to open TCP connection to example.com:443", result.output + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to an unhandled exception" do + stub_request(:post, URL).to_raise StandardError.new("Something went wrong") + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.0.0 Error relaying to Postfix ingress: Something went wrong", result.output + assert_not result.success? + assert result.failure? + end + end +end -- cgit v1.2.3 From 9a3fb1f43be172618a710f0bb170586dc2475591 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sun, 25 Nov 2018 18:31:36 -0500 Subject: Load OpenSSL only when it's used --- lib/action_mailbox/postfix_relayer.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/action_mailbox/postfix_relayer.rb b/lib/action_mailbox/postfix_relayer.rb index de4fdc06d0..ee18c8f6ba 100644 --- a/lib/action_mailbox/postfix_relayer.rb +++ b/lib/action_mailbox/postfix_relayer.rb @@ -2,7 +2,6 @@ require "net/http" require "uri" -require "openssl" module ActionMailbox class PostfixRelayer @@ -49,8 +48,12 @@ module ActionMailbox def client @client ||= Net::HTTP.new(uri.host, uri.port).tap do |connection| - connection.use_ssl = uri.scheme == "https" - connection.verify_mode = OpenSSL::SSL::VERIFY_PEER + 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 -- cgit v1.2.3 From 43801f6a3b2fd4539a1d4fc2689e68c12718b515 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sun, 25 Nov 2018 19:26:00 -0500 Subject: Correct require --- lib/tasks/ingress.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index 6d1c0797ff..f357f098ab 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -6,7 +6,7 @@ namespace :action_mailbox do task :postfix do require "active_support" require "active_support/core_ext/object/blank" - require "action_mailbox/ingresses/postfix/relayer" + require "action_mailbox/postfix_relayer" url, password, user_agent = ENV.values_at("URL", "INGRESS_PASSWORD", "USER_AGENT") -- cgit v1.2.3 From 5ae21598731a8a539c025d2c57576ab44148c3fb Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sun, 25 Nov 2018 19:29:46 -0500 Subject: Fix printing --- lib/tasks/ingress.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index f357f098ab..3f57b79caa 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -11,13 +11,13 @@ namespace :action_mailbox do url, password, user_agent = ENV.values_at("URL", "INGRESS_PASSWORD", "USER_AGENT") if url.blank? || password.blank? - echo "4.3.5 URL and INGRESS_PASSWORD are required" + print "4.3.5 URL and INGRESS_PASSWORD are required" exit 1 end ActionMailbox::PostfixRelayer.new(url: url, password: password, user_agent: user_agent) .relay(STDIN.read).tap do |result| - echo result.output + print result.output exit result.success? ? 0 : 1 end end -- cgit v1.2.3 From dcea1b18dae59a371ef8ac32b4874f3daf748001 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sun, 25 Nov 2018 21:35:27 -0500 Subject: YAGNI user agent customization --- lib/action_mailbox/postfix_relayer.rb | 15 +++++++++------ lib/tasks/ingress.rake | 11 +++++------ test/unit/postfix_relayer_test.rb | 3 ++- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/action_mailbox/postfix_relayer.rb b/lib/action_mailbox/postfix_relayer.rb index ee18c8f6ba..268ca5661b 100644 --- a/lib/action_mailbox/postfix_relayer.rb +++ b/lib/action_mailbox/postfix_relayer.rb @@ -15,10 +15,13 @@ module ActionMailbox end end - attr_reader :uri, :username, :password, :user_agent + CONTENT_TYPE = "message/rfc822" + USER_AGENT = "Action Mailbox Postfix relayer" - def initialize(url:, username: "actionmailbox", password:, user_agent: nil) - @uri, @username, @password, @user_agent = URI(url), username, password, user_agent || "Postfix" + attr_reader :uri, :username, :password + + def initialize(url:, username: "actionmailbox", password:) + @uri, @username, @password = URI(url), username, password end def relay(source) @@ -40,9 +43,9 @@ module ActionMailbox private def post(source) - client.post uri.path, source, - "Content-Type" => "message/rfc822", - "User-Agent" => user_agent, + client.post uri, source, + "Content-Type" => CONTENT_TYPE, + "User-Agent" => USER_AGENT, "Authorization" => "Basic #{Base64.strict_encode64(username + ":" + password)}" end diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index 3f57b79caa..1253f94f71 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -8,18 +8,17 @@ namespace :action_mailbox do require "active_support/core_ext/object/blank" require "action_mailbox/postfix_relayer" - url, password, user_agent = ENV.values_at("URL", "INGRESS_PASSWORD", "USER_AGENT") + 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::PostfixRelayer.new(url: url, password: password, user_agent: user_agent) - .relay(STDIN.read).tap do |result| - print result.output - exit result.success? ? 0 : 1 - end + ActionMailbox::PostfixRelayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print result.output + exit result.success? ? 0 : 1 + end end end end diff --git a/test/unit/postfix_relayer_test.rb b/test/unit/postfix_relayer_test.rb index 9f5b78e216..f11ffb0518 100644 --- a/test/unit/postfix_relayer_test.rb +++ b/test/unit/postfix_relayer_test.rb @@ -20,7 +20,8 @@ module ActionMailbox assert_not result.failure? assert_requested :post, URL, body: file_fixture("welcome.eml").read, - basic_auth: [ "actionmailbox", INGRESS_PASSWORD ], headers: { "Content-Type" => "message/rfc822", "User-Agent" => "Postfix" } + basic_auth: [ "actionmailbox", INGRESS_PASSWORD ], + headers: { "Content-Type" => "message/rfc822", "User-Agent" => "Action Mailbox Postfix relayer" } end test "unsuccessfully relaying with invalid credentials" do -- cgit v1.2.3 From 09da258fab967ac722bca6d63947ec0bfd8ea540 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 26 Nov 2018 16:38:21 -0500 Subject: Kernel#exit accepts a boolean --- lib/tasks/ingress.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake index 1253f94f71..f775bbdfd7 100644 --- a/lib/tasks/ingress.rake +++ b/lib/tasks/ingress.rake @@ -17,7 +17,7 @@ namespace :action_mailbox do ActionMailbox::PostfixRelayer.new(url: url, password: password).relay(STDIN.read).tap do |result| print result.output - exit result.success? ? 0 : 1 + exit result.success? end end end -- cgit v1.2.3 From 9d798ca9c038059df479233ddc4d35711826bd93 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Dec 2018 15:52:08 -0800 Subject: Allow CC, BCC, and In-Reply-To mail attributes to be set on new delivery --- .../conductor/action_mailbox/inbound_emails_controller.rb | 2 +- .../conductor/action_mailbox/inbound_emails/new.html.erb | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb b/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb index 70537da9c4..9804f6ec5b 100644 --- a/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +++ b/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb @@ -17,7 +17,7 @@ class Rails::Conductor::ActionMailbox::InboundEmailsController < Rails::Conducto private def new_mail - Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :subject, :body).to_h + Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h end def create_inbound_email(mail) diff --git a/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb b/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb index be989ff0bc..7f1ec74496 100644 --- a/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +++ b/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb @@ -13,6 +13,21 @@ <%= form.text_field :to %> +
+ <%= form.label :cc, "CC" %>
+ <%= form.text_field :cc %> +
+ +
+ <%= form.label :bcc, "BCC" %>
+ <%= form.text_field :bcc %> +
+ +
+ <%= form.label :in_reply_to, "In-Reply-To" %>
+ <%= form.text_field :in_reply_to %> +
+
<%= form.label :subject, "Subject" %>
<%= form.text_field :subject %> -- cgit v1.2.3 From b8f2835762dc064b44da1ef552140ad610bf6afa Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 3 Dec 2018 22:09:49 -0500 Subject: Match all valid error codes --- lib/action_mailbox/postfix_relayer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/action_mailbox/postfix_relayer.rb b/lib/action_mailbox/postfix_relayer.rb index 268ca5661b..5a0eb7dca3 100644 --- a/lib/action_mailbox/postfix_relayer.rb +++ b/lib/action_mailbox/postfix_relayer.rb @@ -11,7 +11,7 @@ module ActionMailbox end def failure? - output.match?(/\A[45]\.\d\.\d /) + output.match?(/\A[45]\.\d{1,3}\.\d{1,3}(\s|\z)/) end end -- cgit v1.2.3 From b404d4e0b4601e4f084fde878ec0c285615fb039 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 3 Dec 2018 22:15:13 -0500 Subject: Test unsuccessfully relaying due to a client-side timeout --- test/unit/postfix_relayer_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/unit/postfix_relayer_test.rb b/test/unit/postfix_relayer_test.rb index f11ffb0518..550ed2ac7f 100644 --- a/test/unit/postfix_relayer_test.rb +++ b/test/unit/postfix_relayer_test.rb @@ -69,6 +69,15 @@ module ActionMailbox assert result.failure? end + test "unsuccessfully relaying due to client-side timeout" do + stub_request(:post, URL).to_timeout + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.4.2 Timed out relaying to Postfix ingress", result.output + assert_not result.success? + assert result.failure? + end + test "unsuccessfully relaying due to an unhandled exception" do stub_request(:post, URL).to_raise StandardError.new("Something went wrong") -- cgit v1.2.3 From 70c87ea7709ad550c1feaaf7d3831e85d52c032f Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Tue, 4 Dec 2018 13:04:56 -0500 Subject: Include the gem version in the user agent string --- lib/action_mailbox/postfix_relayer.rb | 3 ++- test/unit/postfix_relayer_test.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/action_mailbox/postfix_relayer.rb b/lib/action_mailbox/postfix_relayer.rb index 5a0eb7dca3..eae6184fbb 100644 --- a/lib/action_mailbox/postfix_relayer.rb +++ b/lib/action_mailbox/postfix_relayer.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "action_mailbox/version" require "net/http" require "uri" @@ -16,7 +17,7 @@ module ActionMailbox end CONTENT_TYPE = "message/rfc822" - USER_AGENT = "Action Mailbox Postfix relayer" + USER_AGENT = "Action Mailbox Postfix relayer v#{ActionMailbox::VERSION}" attr_reader :uri, :username, :password diff --git a/test/unit/postfix_relayer_test.rb b/test/unit/postfix_relayer_test.rb index 550ed2ac7f..9d6e49dee3 100644 --- a/test/unit/postfix_relayer_test.rb +++ b/test/unit/postfix_relayer_test.rb @@ -21,7 +21,7 @@ module ActionMailbox assert_requested :post, URL, body: file_fixture("welcome.eml").read, basic_auth: [ "actionmailbox", INGRESS_PASSWORD ], - headers: { "Content-Type" => "message/rfc822", "User-Agent" => "Action Mailbox Postfix relayer" } + headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox Postfix relayer v\d+\./ } end test "unsuccessfully relaying with invalid credentials" do -- cgit v1.2.3 From a1d449e061d163fc0f6cb8c966d9ebf70923b665 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 6 Dec 2018 13:27:45 -0800 Subject: Add a :all catch-all route --- lib/action_mailbox/router/route.rb | 6 ++++-- test/unit/router_test.rb | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/action_mailbox/router/route.rb b/lib/action_mailbox/router/route.rb index 24be8d4804..5254043a67 100644 --- a/lib/action_mailbox/router/route.rb +++ b/lib/action_mailbox/router/route.rb @@ -9,6 +9,8 @@ class ActionMailbox::Router::Route def match?(inbound_email) case address + when :all + true when String inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) } when Regexp @@ -26,8 +28,8 @@ class ActionMailbox::Router::Route private def ensure_valid_address - unless [ String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?) - raise ArgumentError, "Expected a String, Regexp, Proc, or matchable, got #{address.inspect}" + 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 diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index b8c187f95e..70d39ba6a1 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -103,6 +103,23 @@ module ActionMailbox assert_equal "Nested::FirstMailbox", $processed_by end + test "all as the only route" do + @router.add_route :all, to: :first + @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply") + assert_equal "FirstMailbox", $processed_by + end + + test "all as the second route" do + @router.add_route FirstMailboxAddress.new, to: :first + @router.add_route :all, to: :second + + @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply") + assert_equal "FirstMailbox", $processed_by + + @router.route create_inbound_email_from_mail(to: "elsewhere@example.com", subject: "This is a reply") + assert_equal "SecondMailbox", $processed_by + end + test "missing route" do assert_raises(ActionMailbox::Router::RoutingError) do inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply") -- cgit v1.2.3 From 731bfa7cf45b8b849e63990d933fc65a14a6b860 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 6 Dec 2018 13:53:50 -0800 Subject: Add address wrapping Makes it easier to deal with addresses that may already have been converted --- lib/action_mailbox/mail_ext/address_wrapping.rb | 5 +++++ test/unit/mail_ext/address_wrapping_test.rb | 11 +++++++++++ 2 files changed, 16 insertions(+) create mode 100644 lib/action_mailbox/mail_ext/address_wrapping.rb create mode 100644 test/unit/mail_ext/address_wrapping_test.rb diff --git a/lib/action_mailbox/mail_ext/address_wrapping.rb b/lib/action_mailbox/mail_ext/address_wrapping.rb new file mode 100644 index 0000000000..f41087a450 --- /dev/null +++ b/lib/action_mailbox/mail_ext/address_wrapping.rb @@ -0,0 +1,5 @@ +class Mail::Address + def self.wrap(address) + address.is_a?(Mail::Address) ? address : Mail::Address.new(address) + end +end diff --git a/test/unit/mail_ext/address_wrapping_test.rb b/test/unit/mail_ext/address_wrapping_test.rb new file mode 100644 index 0000000000..6baa360f3e --- /dev/null +++ b/test/unit/mail_ext/address_wrapping_test.rb @@ -0,0 +1,11 @@ +require_relative '../../test_helper' + +module MailExt + class AddressWrappingTest < ActiveSupport::TestCase + test "wrap" do + needing_wrapping = Mail::Address.wrap("david@basecamp.com") + wrapping_not_needed = Mail::Address.wrap(Mail::Address.new("david@basecamp.com")) + assert_equal needing_wrapping.address, wrapping_not_needed.address + end + end +end -- cgit v1.2.3 From bb0c13bfdb11e2d8505f2fe87dab5ae8391e9264 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 6 Dec 2018 13:54:09 -0800 Subject: Address objects are value objects --- lib/action_mailbox/mail_ext/address_equality.rb | 5 +++++ test/unit/mail_ext/address_equality_test.rb | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 lib/action_mailbox/mail_ext/address_equality.rb create mode 100644 test/unit/mail_ext/address_equality_test.rb diff --git a/lib/action_mailbox/mail_ext/address_equality.rb b/lib/action_mailbox/mail_ext/address_equality.rb new file mode 100644 index 0000000000..76675365dd --- /dev/null +++ b/lib/action_mailbox/mail_ext/address_equality.rb @@ -0,0 +1,5 @@ +class Mail::Address + def ==(other_address) + other_address.is_a?(Mail::Address) && to_s == other_address.to_s + end +end diff --git a/test/unit/mail_ext/address_equality_test.rb b/test/unit/mail_ext/address_equality_test.rb new file mode 100644 index 0000000000..56e9bfa47f --- /dev/null +++ b/test/unit/mail_ext/address_equality_test.rb @@ -0,0 +1,9 @@ +require_relative '../../test_helper' + +module MailExt + class AddressEqualityTest < ActiveSupport::TestCase + test "two addresses with the same address are equal" do + assert_equal Mail::Address.new("david@basecamp.com"), Mail::Address.new("david@basecamp.com") + end + end +end -- cgit v1.2.3 From 03166d4bd6a353888552ea3ef8e6499d8f1e1bff Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 15:38:45 -0800 Subject: Basic description of the framework --- README.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 993a18fae9..578d12abee 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,93 @@ # Action Mailbox -📬 +Action Mailbox routes incoming emails to controller-like mailboxes for further and encapsulated processing in Rails. + +It ships with ingress handling for AWS SNS, Mailgun, Madrill, and Sendgrid. You can also handle inbound mails directly via the Postfix ingress task/controller combination. + +The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration. + +These inbound emails are routed asynchronously using Active Job to one or several dedicated mailboxes, which are capable of interacting directly with the rest of your domain model. + +## How does this compare to Action Mailer's inbound processing? + +Rails has long had an anemic way of [receiving emails using Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html#receiving-emails), but it was poorly flushed out, lacked cohesion with the task of sending emails, and offered no help on integrating with popular inbound email processing platforms. Action Mailbox supersedes the receiving part of Action Mailer, which will be deprecated in due course. + +## Installing + +Assumes a Rails 5.2+ application: + +1. Install the gem: + + ```ruby + # Gemfile + gem "actionmailbox", github: "rails/actionmailbox", require: "action_mailbox" + ``` + +1. Install migrations needed for InboundEmail (and ensure Active Storage is setup) + + ``` + ./bin/rails action_mailbox:install + ./bin/rails db:migrate + ``` + +## Examples + +Configure basic routing: + +```ruby +# app/models/message.rb +class ApplicationMailbox < ActionMailbox::Base + routing /^save@/i => :forwards + routing /@replies\./i => :replies +end +``` + +Then setup a mailbox: + +```ruby +# app/mailboxes/forwards_mailbox.rb +class ForwardsMailbox < ApplicationMailbox + # Callbacks specify prerequisites to processing + before_processing :require_forward + + def process + if forwarder.buckets.one? + record_forward + else + stage_forward_and_request_more_details + end + end + + private + def require_forward + unless message.forward? + # Use Action Mailers to bounce incoming emails back to sender – this halts processing + bounce_with Forwards::BounceMailer.missing_forward( + inbound_email, forwarder: forwarder + ) + end + end + + def forwarder + @forwarder ||= Person.where(email_address: mail.from) + end + + def record_forward + forwarder.buckets.first.record \ + Forward.new forwarder: forwarder, subject: message.subject, content: mail.content + end + + def stage_forward_and_request_more_details + Forwards::RoutingMailer.choose_project(mail).deliver_now + end +end +``` + + +## Development road map + +Action Mailbox is destined for inclusion in Rails 6, which is due to be released some time in 2019. We will refine the framework in this separate rails/actionmailbox repository until we're ready to promote it via a pull request to rails/rails. + +## License + +Action Mailbox is released under the [MIT License](https://opensource.org/licenses/MIT). \ No newline at end of file -- cgit v1.2.3 From 19730e4891f7408239a154d5390ce67923bc6119 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 15:42:03 -0800 Subject: George ended up doing a lot of the actual code --- actionmailbox.gemspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec index e3dae277d4..84307ee7ef 100644 --- a/actionmailbox.gemspec +++ b/actionmailbox.gemspec @@ -7,10 +7,10 @@ require "action_mailbox/version" Gem::Specification.new do |s| s.name = "actionmailbox" s.version = ActionMailbox::VERSION - s.authors = ["Jeremy Daer", "David Heinemeier Hansson"] - s.email = ["jeremy@basecamp.com", "david@loudthinking.com"] + s.authors = ["David Heinemeier Hansson", "George Claghorn"] + s.email = ["david@loudthinking.com", "george@basecamp.com"] s.summary = "Receive and process incoming emails in Rails" - s.homepage = "https://github.com/basecamp/actionmailbox" + s.homepage = "https://github.com/rails/actionmailbox" s.license = "MIT" s.required_ruby_version = ">= 2.5.0" -- cgit v1.2.3 From 68209ce069e6a33439bf6ec803d6c38853b8755f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 15:46:09 -0800 Subject: Describe the conductor controller --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 578d12abee..db55cf197b 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,12 @@ The inbound emails are turned into `InboundEmail` records using Active Record an These inbound emails are routed asynchronously using Active Job to one or several dedicated mailboxes, which are capable of interacting directly with the rest of your domain model. + ## How does this compare to Action Mailer's inbound processing? Rails has long had an anemic way of [receiving emails using Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html#receiving-emails), but it was poorly flushed out, lacked cohesion with the task of sending emails, and offered no help on integrating with popular inbound email processing platforms. Action Mailbox supersedes the receiving part of Action Mailer, which will be deprecated in due course. + ## Installing Assumes a Rails 5.2+ application: @@ -30,6 +32,10 @@ Assumes a Rails 5.2+ application: ./bin/rails db:migrate ``` +## Configure ingress path and password + +TODO + ## Examples Configure basic routing: @@ -84,6 +90,11 @@ end ``` +## Create incoming email through a conductor module in development + +It's helpful to be able to test incoming emails in development without actually sending and receiving real emails. To accomplish this, there's a conductor controller mounted at `/rails/conductor/action_mailbox/inbound_emails`, which gives you an index of all the InboundEmails in the system, their state of processing, and a form to create a new InboundEmail as well. + + ## Development road map Action Mailbox is destined for inclusion in Rails 6, which is due to be released some time in 2019. We will refine the framework in this separate rails/actionmailbox repository until we're ready to promote it via a pull request to rails/rails. -- cgit v1.2.3 From de5e4d1936ddb5fd744c094f1c97e67c732bd542 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 16:06:48 -0800 Subject: Base note --- app/controllers/action_mailbox/base_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb index c234ecd250..5af59f8140 100644 --- a/app/controllers/action_mailbox/base_controller.rb +++ b/app/controllers/action_mailbox/base_controller.rb @@ -1,3 +1,4 @@ +# The base class for all Active Mailbox ingress controllers. class ActionMailbox::BaseController < ActionController::Base skip_forgery_protection -- cgit v1.2.3 From 1df26841b34d065c111d9b4e2147c1f94ec70c7d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 16:06:56 -0800 Subject: Note about incineration --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index db55cf197b..d779d8adcc 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,12 @@ class ForwardsMailbox < ApplicationMailbox end ``` +## Incineration of InboundEmails + +By default, an InboundEmail that has been marked as successfully processed will be incinerated after 30 days. This ensures you're not holding on to people's data willy-nilly after they may have canceled their accounts or deleted their content. The intention is that after you've processed an email, you should have extracted all the data you needed and turned it into domain models and content on your side of the application. The InboundEmail simply stays in the system for the extra time to provide debugging and forensics options. + +The actual incineration is done via the `IncinerationJob` that's scheduled to run after `config.action_mailbox.incinerate_after` time. This value is by default set to `30.days`, but you can change it in your production.rb configuration. (Note that this far-future incineration scheduling relies on your job queue being able to hold jobs for that long.) + ## Create incoming email through a conductor module in development -- cgit v1.2.3 From 6edccec1b97af46d2133ce701d7c307d213da9c6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 16:34:05 -0800 Subject: Basic docs for most classes --- .../conductor/action_mailbox/reroutes_controller.rb | 1 + app/jobs/action_mailbox/incineration_job.rb | 5 +++++ app/jobs/action_mailbox/routing_job.rb | 2 ++ app/models/action_mailbox/inbound_email.rb | 21 +++++++++++++++++++++ .../action_mailbox/inbound_email/incineratable.rb | 3 +++ .../inbound_email/incineratable/incineration.rb | 4 ++++ .../action_mailbox/inbound_email/message_id.rb | 9 +++++++++ app/models/action_mailbox/inbound_email/routable.rb | 7 +++++++ 8 files changed, 52 insertions(+) diff --git a/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb b/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb index 226116a3d6..6191bda5e5 100644 --- a/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +++ b/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb @@ -1,3 +1,4 @@ +# Rerouting will run routing and processing on an email that has already been, or attempted to be, processed. class Rails::Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController def create inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id]) diff --git a/app/jobs/action_mailbox/incineration_job.rb b/app/jobs/action_mailbox/incineration_job.rb index 4fc0cb0782..3c6c9aae4f 100644 --- a/app/jobs/action_mailbox/incineration_job.rb +++ b/app/jobs/action_mailbox/incineration_job.rb @@ -1,3 +1,8 @@ +# You can configure when this `IncinerationJob` will be run as a time-after-processing using the +# `config.action_mailbox.incinerate_after` or `ActionMailbox.incinerate_after` setting. +# +# Since this incineration is set for the future, it'll automatically ignore any `InboundEmail`s +# that have already been deleted and discard itself if so. class ActionMailbox::IncinerationJob < ActiveJob::Base queue_as { ActionMailbox.queues[:incineration] } diff --git a/app/jobs/action_mailbox/routing_job.rb b/app/jobs/action_mailbox/routing_job.rb index 786187528a..c345227617 100644 --- a/app/jobs/action_mailbox/routing_job.rb +++ b/app/jobs/action_mailbox/routing_job.rb @@ -1,3 +1,5 @@ +# Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly +# accept new incoming emails without being burdened to hang while they're actually being processed. class ActionMailbox::RoutingJob < ActiveJob::Base queue_as { ActionMailbox.queues[:routing] } diff --git a/app/models/action_mailbox/inbound_email.rb b/app/models/action_mailbox/inbound_email.rb index ea564a254e..156e2d4dbc 100644 --- a/app/models/action_mailbox/inbound_email.rb +++ b/app/models/action_mailbox/inbound_email.rb @@ -1,5 +1,26 @@ require "mail" +# The `InboundEmail` is an Active Record that keeps a reference to the raw email stored in Active Storage +# and tracks the status of processing. By default, incoming emails will go through the following lifecycle: +# +# * Pending: Just received by one of the ingress controllers and scheduled for routing. +# * Processing: During active processing, while a specific mailbox is running its #process method. +# * Delivered: Successfully processed by the specific mailbox. +# * Failed: An exception was raised during the specific mailbox's execution of the `#process` method. +# * Bounced: Rejected processing by the specific mailbox and bounced to sender. +# +# Once the `InboundEmail` has reached the status of being either `delivered`, `failed`, or `bounced`, +# it'll count as having been `#processed?`. Once processed, the `InboundEmail` will be scheduled for +# automatic incineration at a later point. +# +# When working with an `InboundEmail`, you'll usually interact with the parsed version of the source, +# which is available as a `Mail` object from `#mail`. But you can also access the raw source directly +# using the `#source` method. +# +# Examples: +# +# inbound_email.mail.from # => 'david@loudthinking.com' +# inbound_email.source # Returns the full rfc822 source of the email as text class ActionMailbox::InboundEmail < ActiveRecord::Base self.table_name = "action_mailbox_inbound_emails" diff --git a/app/models/action_mailbox/inbound_email/incineratable.rb b/app/models/action_mailbox/inbound_email/incineratable.rb index 198846422c..a049b88b69 100644 --- a/app/models/action_mailbox/inbound_email/incineratable.rb +++ b/app/models/action_mailbox/inbound_email/incineratable.rb @@ -1,3 +1,6 @@ +# Ensure that the `InboundEmail` is automatically scheduled for later incineration if the status has been +# changed to `processed`. The later incineration will be invoked at the time specified by the +# `ActionMailbox.incinerate_after` time using the `IncinerationJob`. module ActionMailbox::InboundEmail::Incineratable extend ActiveSupport::Concern diff --git a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb index 801cc0c8b9..50861bc64b 100644 --- a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +++ b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb @@ -1,3 +1,7 @@ +# Command class for carrying out the actual incineration of the `InboundMail` that's been scheduled +# for removal. Before the incineration – which really is just a call to `#destroy!` – is run, we verify +# that it's both eligible (by virtue of having already been processed) and time to do so (that is, +# the `InboundEmail` was processed after the `incinerate_after` time). class ActionMailbox::InboundEmail::Incineratable::Incineration def initialize(inbound_email) @inbound_email = inbound_email diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb index 70d39d1e33..dc17e48e74 100644 --- a/app/models/action_mailbox/inbound_email/message_id.rb +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -1,3 +1,9 @@ +# The `Message-ID` as specified by rfc822 is supposed to be a unique identifier for that individual email. +# That makes it an ideal tracking token for debugging and forensics, just like `X-Request-Id` does for +# web request. +# +# If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated +# using the approach from `Mail::MessageIdField`. module ActionMailbox::InboundEmail::MessageId extend ActiveSupport::Concern @@ -6,6 +12,9 @@ module ActionMailbox::InboundEmail::MessageId end module ClassMethods + # Create a new `InboundEmail` from the raw `source` of the email, which be uploaded as a Active Storage + # attachment called `raw_email`. Before the upload, extract the Message-ID from the `source` and set + # it as an attribute on the new `InboundEmail`. def create_and_extract_message_id!(source, **options) create! message_id: extract_message_id(source), **options do |inbound_email| inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822" diff --git a/app/models/action_mailbox/inbound_email/routable.rb b/app/models/action_mailbox/inbound_email/routable.rb index 8f5b0ddd39..f042fa4f57 100644 --- a/app/models/action_mailbox/inbound_email/routable.rb +++ b/app/models/action_mailbox/inbound_email/routable.rb @@ -1,3 +1,8 @@ +# A newly received `InboundEmail` will not be routed synchronously as part of ingress controller's receival. +# Instead, the routing will be done asynchronously, using a `RoutingJob`, to ensure maximum parallel capacity. +# +# By default, all newly created `InboundEmail` records that have the status of `pending`, which is the default, +# will be scheduled for automatic, deferred routing. module ActionMailbox::InboundEmail::Routable extend ActiveSupport::Concern @@ -5,10 +10,12 @@ module ActionMailbox::InboundEmail::Routable after_create_commit :route_later, if: :pending? end + # Enqueue a `RoutingJob` for this `InboundEmail`. def route_later ActionMailbox::RoutingJob.perform_later self end + # Route this `InboundEmail` using the routing rules declared on the `ApplicationMailbox`. def route ApplicationMailbox.route self end -- cgit v1.2.3 From 0ef2fdb119a7196a010cd8081f1631f23f5cef95 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 17:12:48 -0800 Subject: Consistent naming on all factory methods --- lib/action_mailbox/test_helper.rb | 10 +++++++--- test/unit/inbound_email/message_id_test.rb | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb index 23b2bb02ca..680e8b43be 100644 --- a/lib/action_mailbox/test_helper.rb +++ b/lib/action_mailbox/test_helper.rb @@ -5,14 +5,15 @@ module ActionMailbox # 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 file_fixture(fixture_name).read, status: status + create_inbound_email_from_source file_fixture(fixture_name).read, status: status end def create_inbound_email_from_mail(status: :processing, **mail_options) - create_inbound_email Mail.new(mail_options).to_s, status: status + create_inbound_email_from_source Mail.new(mail_options).to_s, status: status end - def create_inbound_email(source, status: :processing) + # 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 @@ -23,5 +24,8 @@ module ActionMailbox def receive_inbound_email_from_mail(**kwargs) create_inbound_email_from_mail(**kwargs).tap(&:route) end + def receive_inbound_email_from_source(**kwargs) + create_inbound_email_from_source(**kwargs).tap(&:route) + end end end diff --git a/test/unit/inbound_email/message_id_test.rb b/test/unit/inbound_email/message_id_test.rb index c744a5bf99..1cc3de360e 100644 --- a/test/unit/inbound_email/message_id_test.rb +++ b/test/unit/inbound_email/message_id_test.rb @@ -7,7 +7,7 @@ class ActionMailbox::InboundEmail::MessageIdTest < ActiveSupport::TestCase end test "message id is generated if its missing" do - inbound_email = create_inbound_email "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!" + inbound_email = create_inbound_email_from_source "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!" assert_not_nil inbound_email.message_id end end -- cgit v1.2.3 From 316ba4768b9f0cc49afa692d93c0f90260b03f4c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 17:13:16 -0800 Subject: Routing documentation --- lib/action_mailbox/base.rb | 54 ++++++++++++++++++++++++++++++++++++++ lib/action_mailbox/router.rb | 2 ++ lib/action_mailbox/router/route.rb | 3 +++ lib/action_mailbox/routing.rb | 1 + 4 files changed, 60 insertions(+) diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index 55914401e1..0ae83bf691 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -3,6 +3,60 @@ require "active_support/rescuable" require "action_mailbox/callbacks" require "action_mailbox/routing" +# 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. +# route /^replies@/i => :replies +# +# # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string. +# route "help@example.com" => :help +# +# # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true. +# route ->(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. +# route CustomAddress.new => :custom +# +# # Any inbound_email that has not been already matched will be sent to the BackstopMailbox. +# route :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 ActionMailbox::Base include ActiveSupport::Rescuable include ActionMailbox::Callbacks, ActionMailbox::Routing diff --git a/lib/action_mailbox/router.rb b/lib/action_mailbox/router.rb index 8ba3ad0bae..ef55718974 100644 --- a/lib/action_mailbox/router.rb +++ b/lib/action_mailbox/router.rb @@ -1,3 +1,5 @@ +# Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when +# an inbound_email is received. class ActionMailbox::Router class RoutingError < StandardError; end diff --git a/lib/action_mailbox/router/route.rb b/lib/action_mailbox/router/route.rb index 5254043a67..6472b9a31d 100644 --- a/lib/action_mailbox/router/route.rb +++ b/lib/action_mailbox/router/route.rb @@ -1,3 +1,6 @@ +# 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 ActionMailbox::Router::Route attr_reader :address, :mailbox_name diff --git a/lib/action_mailbox/routing.rb b/lib/action_mailbox/routing.rb index d258b632f9..db462e6019 100644 --- a/lib/action_mailbox/routing.rb +++ b/lib/action_mailbox/routing.rb @@ -1,4 +1,5 @@ module ActionMailbox + # See `ActionMailbox::Base` for how to specify routing. module Routing extend ActiveSupport::Concern -- cgit v1.2.3 From 3e7ce697bfae0beff3d2141e978e2dba1ceb3a42 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 17:13:26 -0800 Subject: Stating the obvious --- lib/action_mailbox/callbacks.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/action_mailbox/callbacks.rb b/lib/action_mailbox/callbacks.rb index 5f39faa01e..f1bbd0593d 100644 --- a/lib/action_mailbox/callbacks.rb +++ b/lib/action_mailbox/callbacks.rb @@ -1,6 +1,7 @@ require "active_support/callbacks" module ActionMailbox + # Defines the callbacks related to processing. module Callbacks extend ActiveSupport::Concern include ActiveSupport::Callbacks -- cgit v1.2.3 From c859cd256fda470ad284b21b8595d75b78eebc9f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 17:13:35 -0800 Subject: Explain all test helpers --- lib/action_mailbox/test_helper.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb index 680e8b43be..4e4552629a 100644 --- a/lib/action_mailbox/test_helper.rb +++ b/lib/action_mailbox/test_helper.rb @@ -2,12 +2,15 @@ require "mail" module ActionMailbox module TestHelper - # Create an InboundEmail record using an eml fixture in the format of message/rfc822 + # 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 @@ -17,13 +20,21 @@ module ActionMailbox 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 -- cgit v1.2.3 From f5619125579ad185fb65d479bfc05c3545d43fa3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Dec 2018 17:14:03 -0800 Subject: Need to explain testing too --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d779d8adcc..543cd998ee 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,11 @@ The actual incineration is done via the `IncinerationJob` that's scheduled to ru It's helpful to be able to test incoming emails in development without actually sending and receiving real emails. To accomplish this, there's a conductor controller mounted at `/rails/conductor/action_mailbox/inbound_emails`, which gives you an index of all the InboundEmails in the system, their state of processing, and a form to create a new InboundEmail as well. +## Testing mailboxes + +TODO + + ## Development road map Action Mailbox is destined for inclusion in Rails 6, which is due to be released some time in 2019. We will refine the framework in this separate rails/actionmailbox repository until we're ready to promote it via a pull request to rails/rails. -- cgit v1.2.3 From 7a2ff4ea093dac8b8ad20e6f17679b972f7f967a Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 13 Dec 2018 07:38:03 -0500 Subject: Document ingress configuration --- README.md | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 147 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 543cd998ee..14ddca0f26 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Action Mailbox -Action Mailbox routes incoming emails to controller-like mailboxes for further and encapsulated processing in Rails. +Action Mailbox routes incoming emails to controller-like mailboxes for further and encapsulated processing in Rails. -It ships with ingress handling for AWS SNS, Mailgun, Madrill, and Sendgrid. You can also handle inbound mails directly via the Postfix ingress task/controller combination. +It ships with ingresses for Amazon SES, Mailgun, Mandrill, and SendGrid. You can also handle inbound mails directly via the built-in Postfix ingress. The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration. @@ -24,7 +24,7 @@ Assumes a Rails 5.2+ application: # Gemfile gem "actionmailbox", github: "rails/actionmailbox", require: "action_mailbox" ``` - + 1. Install migrations needed for InboundEmail (and ensure Active Storage is setup) ``` @@ -32,9 +32,150 @@ Assumes a Rails 5.2+ application: ./bin/rails db:migrate ``` -## Configure ingress path and password +## Configuring + +### Amazon SES + +1. Install the [`aws-sdk-sns`](https://rubygems.org/gems/aws-sdk-sns) gem: + + ```ruby + # Gemfile + gem "aws-sdk-sns", ">= 1.9.0", require: false + ``` + +2. Tell Action Mailbox to accept emails from SES: + + ```ruby + # config/environments/production.rb + config.action_mailbox.ingress = :amazon + ``` + +3. [Configure SES][ses-docs] to deliver emails to your application via POST requests + to `/rails/action_mailbox/amazon/inbound_emails`. If your application lived at `https://example.com`, you would specify + the fully-qualified URL `https://example.com/rails/action_mailbox/amazon/inbound_emails`. + +[ses-docs]: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html + +### Mailgun + +1. Give Action Mailbox your [Mailgun API key][mailgun-api-key] so it can authenticate requests to the Mailgun ingress. + + Use `rails credentials:edit` to add your API key to your application's encrypted credentials under + `action_mailbox.mailgun_api_key`, where Action Mailbox will automatically find it: + + ```yaml + action_mailbox: + mailgun_api_key: ... + ``` + + Alternatively, provide your API key in the `MAILGUN_INGRESS_API_KEY` environment variable. + +2. Tell Action Mailbox to accept emails from Mailgun: + + ```ruby + # config/environments/production.rb + config.action_mailbox.ingress = :mailgun + ``` + +3. [Configure Mailgun][mailgun-forwarding] to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`. + If your application lived at `https://example.com`, you would specify the fully-qualified URL + `https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime`. + +[mailgun-api-key]: https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials- +[mailgun-forwarding]: https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages + +### Mandrill + +1. Give Action Mailbox your Mandrill API key so it can authenticate requests to the Mandrill ingress. + + Use `rails credentials:edit` to add your API key to your application's encrypted credentials under + `action_mailbox.mandrill_api_key`, where Action Mailbox will automatically find it: + + ```yaml + action_mailbox: + mandrill_api_key: ... + ``` + + Alternatively, provide your API key in the `MANDRILL_INGRESS_API_KEY` environment variable. + +2. Tell Action Mailbox to accept emails from Mandrill: + + ```ruby + # config/environments/production.rb + config.action_mailbox.ingress = :mandrill + ``` + +3. [Configure Mandrill][mandrill-routing] to route inbound emails to `/rails/action_mailbox/mandrill/inbound_emails`. + If your application lived at `https://example.com`, you would specify the fully-qualified URL + `https://example.com/rails/action_mailbox/mandrill/inbound_emails`. + +[mandrill-routing]: https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview + +### Postfix + +1. Tell Action Mailbox to accept emails from Postfix: + + ```ruby + # config/environments/production.rb + config.action_mailbox.ingress = :postfix + ``` + +2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress. + + Use `rails credentials:edit` to add the password to your application's encrypted credentials under + `action_mailbox.ingress_password`, where Action Mailbox will automatically find it: + + ```yaml + action_mailbox: + ingress_password: ... + ``` + + Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. + +3. [Configure Postfix][postfix-config] to pipe inbound emails to `bin/rails action_mailbox:ingress:postfix`, providing + the `URL` of the Postfix ingress and the `INGRESS_PASSWORD` you previously generated. If your application lived at + `https://example.com`, the full command would look like this: + + ``` + URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix + ``` + +[postfix-config]: https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script + +### SendGrid + +1. Tell Action Mailbox to accept emails from SendGrid: + + ```ruby + # config/environments/production.rb + config.action_mailbox.ingress = :sendgrid + ``` + +2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress. + + Use `rails credentials:edit` to add the password to your application's encrypted credentials under + `action_mailbox.ingress_password`, where Action Mailbox will automatically find it: + + ```yaml + action_mailbox: + ingress_password: ... + ``` + + Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. + +3. [Configure SendGrid Inbound Parse][sendgrid-config] to forward inbound emails to + `/rails/action_mailbox/sendgrid/inbound_emails` with the username `actionmailbox` and the password you previously + generated. If your application lived at `https://example.com`, you would configure SendGrid with the following URL: + + ``` + https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails + ``` + + **⚠️ Note:** When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled **“Post the raw, + full MIME message.”** Action Mailbox needs the raw MIME message to work. + +[sendgrid-config]: https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/ -TODO ## Examples @@ -112,4 +253,4 @@ Action Mailbox is destined for inclusion in Rails 6, which is due to be released ## License -Action Mailbox is released under the [MIT License](https://opensource.org/licenses/MIT). \ No newline at end of file +Action Mailbox is released under the [MIT License](https://opensource.org/licenses/MIT). -- cgit v1.2.3 From cbb2bdafd91edfb515c0e440c050bc5a542f8438 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 13 Dec 2018 10:05:53 -0500 Subject: Document the ingress controllers --- app/controllers/action_mailbox/base_controller.rb | 2 +- .../ingresses/amazon/inbound_emails_controller.rb | 29 +++++++++++++++ .../ingresses/mailgun/inbound_emails_controller.rb | 41 +++++++++++++++++++++ .../mandrill/inbound_emails_controller.rb | 13 +++++++ .../ingresses/postfix/inbound_emails_controller.rb | 41 +++++++++++++++++++++ .../sendgrid/inbound_emails_controller.rb | 43 ++++++++++++++++++++++ 6 files changed, 168 insertions(+), 1 deletion(-) diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb index 5af59f8140..b73f5b7708 100644 --- a/app/controllers/action_mailbox/base_controller.rb +++ b/app/controllers/action_mailbox/base_controller.rb @@ -16,7 +16,7 @@ class ActionMailbox::BaseController < ActionController::Base end def ingress_name - self.class.name[/^ActionMailbox::Ingresses::(.*?)::/, 1].underscore.to_sym + self.class.name.remove(/\AActionMailbox::Ingresses::/, /::InboundEmailsController\z/).underscore.to_sym end diff --git a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb index d3998be2d4..0c04170eae 100644 --- a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb @@ -1,3 +1,32 @@ +# Ingests inbound emails from Amazon's Simple Email Service (SES). +# +# Requires the full RFC 822 message in the +content+ parameter. Authenticates requests by validating their signatures. +# +# Returns: +# +# - 204 No Content if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox +# - 401 Unauthorized if the request's signature could not be validated +# - 404 Not Found if Action Mailbox is not configured to accept inbound emails from SES +# - 422 Unprocessable Entity if the request is missing the required +content+ parameter +# - 500 Server Error if one of the Active Record database, the Active Storage service, or +# the Active Job backend is misconfigured or unavailable +# +# == Usage +# +# 1. Install the {aws-sdk-sns}[https://rubygems.org/gems/aws-sdk-sns] gem: +# +# # Gemfile +# gem "aws-sdk-sns", ">= 1.9.0", require: false +# +# 2. Tell Action Mailbox to accept emails from SES: +# +# # config/environments/production.rb +# config.action_mailbox.ingress = :amazon +# +# 3. {Configure SES}[https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html] +# to deliver emails to your application via POST requests to +/rails/action_mailbox/amazon/inbound_emails+. +# If your application lived at https://example.com, you would specify the fully-qualified URL +# https://example.com/rails/action_mailbox/amazon/inbound_emails. class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index e878192603..7583e25f26 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -1,3 +1,44 @@ +# Ingests inbound emails from Mailgun. Requires the following parameters: +# +# - +body-mime+: The full RFC 822 message +# - +timestamp+: The current time according to Mailgun as the number of seconds passed since the UNIX epoch +# - +token+: A randomly-generated, 50-character string +# - +signature+: A hexadecimal HMAC-SHA256 of the timestamp concatenated with the token, generated using the Mailgun API key +# +# Authenticates requests by validating their signatures. +# +# Returns: +# +# - 204 No Content if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox +# - 401 Unauthorized if the request's signature could not be validated, or if its timestamp is more than 2 minutes old +# - 404 Not Found if Action Mailbox is not configured to accept inbound emails from Mailgun +# - 422 Unprocessable Entity if the request is missing required parameters +# - 500 Server Error if the Mailgun API key is missing, or one of the Active Record database, +# the Active Storage service, or the Active Job backend is misconfigured or unavailable +# +# == Usage +# +# 1. Give Action Mailbox your {Mailgun API key}[https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials-] +# so it can authenticate requests to the Mailgun ingress. +# +# Use rails credentials:edit to add your API key to your application's encrypted credentials under +# +action_mailbox.mailgun_api_key+, where Action Mailbox will automatically find it: +# +# action_mailbox: +# mailgun_api_key: ... +# +# Alternatively, provide your API key in the +MAILGUN_INGRESS_API_KEY+ environment variable. +# +# 2. Tell Action Mailbox to accept emails from Mailgun: +# +# # config/environments/production.rb +# config.action_mailbox.ingress = :mailgun +# +# 3. {Configure Mailgun}[https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages] +# to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`. +# +# If your application lived at https://example.com, you would specify the fully-qualified URL +# https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime. class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index b32b254076..8be27009d2 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -1,3 +1,16 @@ +# Ingests inbound emails from Mandrill. +# +# Requires a +mandrill_events+ parameter containing a JSON array of Mandrill inbound email event objects. +# Each event is expected to have a +msg+ object containing a full RFC 822 message in its +raw_msg+ property. +# +# Returns: +# +# - 204 No Content if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox +# - 401 Unauthorized if the request's signature could not be validated +# - 404 Not Found if Action Mailbox is not configured to accept inbound emails from Mandrill +# - 422 Unprocessable Entity if the request is missing required parameters +# - 500 Server Error if the Mandrill API key is missing, or one of the Active Record database, +# the Active Storage service, or the Active Job backend is misconfigured or unavailable class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate diff --git a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb index 133accf651..ba04effd1f 100644 --- a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb @@ -1,3 +1,44 @@ +# Ingests inbound emails relayed from Postfix. +# +# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the +# password is read from the application's encrypted credentials or an environment variable. See the Usage section below. +# +# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to +# the Postfix ingress can learn its password. You should only use the Postfix ingress over HTTPS. +# +# Returns: +# +# - 204 No Content if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox +# - 401 Unauthorized if the request could not be authenticated +# - 404 Not Found if Action Mailbox is not configured to accept inbound emails from Postfix +# - 415 Unsupported Media Type if the request does not contain an RFC 822 message +# - 500 Server Error if the ingress password is not configured, or if one of the Active Record database, +# the Active Storage service, or the Active Job backend is misconfigured or unavailable +# +# == Usage +# +# 1. Tell Action Mailbox to accept emails from Postfix: +# +# # config/environments/production.rb +# config.action_mailbox.ingress = :postfix +# +# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress. +# +# Use rails credentials:edit to add the password to your application's encrypted credentials under +# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it: +# +# action_mailbox: +# ingress_password: ... +# +# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. +# +# 3. {Configure Postfix}{https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script} +# to pipe inbound emails to bin/rails action_mailbox:ingress:postfix, providing the +URL+ of the Postfix +# ingress and the +INGRESS_PASSWORD+ you previously generated. +# +# If your application lived at https://example.com, the full command would look like this: +# +# URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix class ActionMailbox::Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate_by_password, :require_valid_rfc822_message diff --git a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb index b856eb5b94..8e1d781c2c 100644 --- a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -1,3 +1,46 @@ +# Ingests inbound emails from SendGrid. Requires an +email+ parameter containing a full RFC 822 message. +# +# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the +# password is read from the application's encrypted credentials or an environment variable. See the Usage section below. +# +# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to +# the SendGrid ingress can learn its password. You should only use the SendGrid ingress over HTTPS. +# +# Returns: +# +# - 204 No Content if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox +# - 401 Unauthorized if the request's signature could not be validated +# - 404 Not Found if Action Mailbox is not configured to accept inbound emails from SendGrid +# - 422 Unprocessable Entity if the request is missing the required +email+ parameter +# - 500 Server Error if the ingress password is not configured, or if one of the Active Record database, +# the Active Storage service, or the Active Job backend is misconfigured or unavailable +# +# == Usage +# +# 1. Tell Action Mailbox to accept emails from SendGrid: +# +# # config/environments/production.rb +# config.action_mailbox.ingress = :sendgrid +# +# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress. +# +# Use rails credentials:edit to add the password to your application's encrypted credentials under +# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it: +# +# action_mailbox: +# ingress_password: ... +# +# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. +# +# 3. {Configure SendGrid Inbound Parse}{https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/} +# to forward inbound emails to +/rails/action_mailbox/sendgrid/inbound_emails+ with the username +actionmailbox+ and +# the password you previously generated. If your application lived at https://example.com, you would +# configure SendGrid with the following fully-qualified URL: +# +# https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails +# +# *NOTE:* When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled *"Post the raw, +# full MIME message."* Action Mailbox needs the raw MIME message to work. class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate_by_password -- cgit v1.2.3 From e94864446a57d632214d606b231d58f0c6205d75 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 13 Dec 2018 15:57:28 -0800 Subject: Simpler --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 14ddca0f26..dfa2ee5431 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Action Mailbox -Action Mailbox routes incoming emails to controller-like mailboxes for further and encapsulated processing in Rails. - -It ships with ingresses for Amazon SES, Mailgun, Mandrill, and SendGrid. You can also handle inbound mails directly via the built-in Postfix ingress. +Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, and SendGrid. You can also handle inbound mails directly via the built-in Postfix ingress. The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration. -- cgit v1.2.3 From 2746449085135e6c345b393b0603eea4a7b1a1b4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 13 Dec 2018 16:01:16 -0800 Subject: Tidy up and example testing --- README.md | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dfa2ee5431..33136d47db 100644 --- a/README.md +++ b/README.md @@ -230,19 +230,43 @@ end ## Incineration of InboundEmails -By default, an InboundEmail that has been marked as successfully processed will be incinerated after 30 days. This ensures you're not holding on to people's data willy-nilly after they may have canceled their accounts or deleted their content. The intention is that after you've processed an email, you should have extracted all the data you needed and turned it into domain models and content on your side of the application. The InboundEmail simply stays in the system for the extra time to provide debugging and forensics options. +By default, an InboundEmail that has been successfully processed will be incinerated after 30 days. This ensures you're not holding on to people's data willy-nilly after they may have canceled their accounts or deleted their content. The intention is that after you've processed an email, you should have extracted all the data you needed and turned it into domain models and content on your side of the application. The InboundEmail simply stays in the system for the extra time to provide debugging and forensics options. The actual incineration is done via the `IncinerationJob` that's scheduled to run after `config.action_mailbox.incinerate_after` time. This value is by default set to `30.days`, but you can change it in your production.rb configuration. (Note that this far-future incineration scheduling relies on your job queue being able to hold jobs for that long.) -## Create incoming email through a conductor module in development +## Working with Action Mailbox in development It's helpful to be able to test incoming emails in development without actually sending and receiving real emails. To accomplish this, there's a conductor controller mounted at `/rails/conductor/action_mailbox/inbound_emails`, which gives you an index of all the InboundEmails in the system, their state of processing, and a form to create a new InboundEmail as well. ## Testing mailboxes -TODO +Example: + +```ruby +class ForwardsMailboxTest < ActionMailbox::TestCase + test "directly recording a client forward for a forwarder and forwardee corresponding to one project" do + assert_difference -> { people(:david).buckets.first.recordings.count } do + receive_inbound_email_from_mail \ + to: 'save@example.com', + from: people(:david).email_address, + subject: "Fwd: Status update?", + body: <<~BODY + --- Begin forwarded message --- + From: Frank Holland + + What's the status? + BODY + end + + recording = people(:david).buckets.first.recordings.last + assert_equal people(:david), recording.creator + assert_equal "Status update?", recording.forward.subject + assert_match "What's the status?", recording.forward.content.to_s + end +end +``` ## Development road map -- cgit v1.2.3 From 2c17861470d9d7e63ac60f6782074c9a9491eb4b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 13 Dec 2018 16:34:02 -0800 Subject: Add installer --- lib/tasks/install.rake | 19 ++++++++++--------- lib/templates/installer.rb | 4 ++++ lib/templates/mailboxes/application_mailbox.rb | 3 +++ 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 lib/templates/installer.rb create mode 100644 lib/templates/mailboxes/application_mailbox.rb diff --git a/lib/tasks/install.rake b/lib/tasks/install.rake index d5abf60042..cfc06fbb5f 100644 --- a/lib/tasks/install.rake +++ b/lib/tasks/install.rake @@ -5,15 +5,16 @@ namespace :action_mailbox do Rake::Task["install:migrations"].clear_comments desc "Copy over the migration" - task install: %w[ environment install:copy_migration active_storage:install ] + task install: %w[ environment run_installer copy_migrations ] - namespace :install do - task :copy_migration do - if Rake::Task.task_defined?("action_mailbox:install:migrations") - Rake::Task["action_mailbox:install:migrations"].invoke - else - Rake::Task["app:action_mailbox:install:migrations"].invoke - end - end + task :run_installer do + installer_template = File.expand_path("../templates/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 diff --git a/lib/templates/installer.rb b/lib/templates/installer.rb new file mode 100644 index 0000000000..d739408d58 --- /dev/null +++ b/lib/templates/installer.rb @@ -0,0 +1,4 @@ +say "Copying application_mailbox.rb to app/mailboxes" +copy_file "#{__dir__}/mailboxes/application_mailbox.rb", "app/mailboxes/application_mailbox.rb" + +environment "# Prepare the ingress controller used to receive mail\nconfig.action_mailbox.ingress = :amazon\n\n", env: 'production' diff --git a/lib/templates/mailboxes/application_mailbox.rb b/lib/templates/mailboxes/application_mailbox.rb new file mode 100644 index 0000000000..9386f9eb91 --- /dev/null +++ b/lib/templates/mailboxes/application_mailbox.rb @@ -0,0 +1,3 @@ +class ApplicationMailbox < ActionMailbox::Base + # route /something/i => :somewhere +end -- cgit v1.2.3 From 032769440504a0760ecaf35149bb7786d18da7f8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 13 Dec 2018 16:34:52 -0800 Subject: Don't need to trip it up right away --- lib/templates/installer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/templates/installer.rb b/lib/templates/installer.rb index d739408d58..18a562c0e5 100644 --- a/lib/templates/installer.rb +++ b/lib/templates/installer.rb @@ -1,4 +1,4 @@ say "Copying application_mailbox.rb to app/mailboxes" copy_file "#{__dir__}/mailboxes/application_mailbox.rb", "app/mailboxes/application_mailbox.rb" -environment "# Prepare the ingress controller used to receive mail\nconfig.action_mailbox.ingress = :amazon\n\n", env: 'production' +environment "# Prepare the ingress controller used to receive mail\n# config.action_mailbox.ingress = :amazon\n\n", env: 'production' -- cgit v1.2.3 From b2dc442a30303b9c27f3c8a44eed778223bd28ce Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 13 Dec 2018 18:13:02 -0800 Subject: fix minor typo https://www.merriam-webster.com/words-at-play/top-10-commonly-confused-words-vol-2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33136d47db..47e3b3e1b5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ These inbound emails are routed asynchronously using Active Job to one or severa ## How does this compare to Action Mailer's inbound processing? -Rails has long had an anemic way of [receiving emails using Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html#receiving-emails), but it was poorly flushed out, lacked cohesion with the task of sending emails, and offered no help on integrating with popular inbound email processing platforms. Action Mailbox supersedes the receiving part of Action Mailer, which will be deprecated in due course. +Rails has long had an anemic way of [receiving emails using Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html#receiving-emails), but it was poorly fleshed out, lacked cohesion with the task of sending emails, and offered no help on integrating with popular inbound email processing platforms. Action Mailbox supersedes the receiving part of Action Mailer, which will be deprecated in due course. ## Installing -- cgit v1.2.3 From 08ab646a495e7602604f2cfef5a43ce1f67cb7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Rodr=C3=ADguez?= Date: Fri, 14 Dec 2018 09:17:05 +0100 Subject: Fix typo [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 47e3b3e1b5..f969acd059 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ Assumes a Rails 5.2+ application: Configure basic routing: ```ruby -# app/models/message.rb +# app/mailboxes/application_mailbox.rb class ApplicationMailbox < ActionMailbox::Base routing /^save@/i => :forwards routing /@replies\./i => :replies -- cgit v1.2.3 From 4412ca6dac7dce284e2010678f7f2e276d60e4d1 Mon Sep 17 00:00:00 2001 From: Dino Maric Date: Fri, 14 Dec 2018 11:06:12 +0100 Subject: Add frozen_string_literal: true to match Rails codebase --- app/controllers/action_mailbox/base_controller.rb | 2 ++ .../action_mailbox/ingresses/amazon/inbound_emails_controller.rb | 2 ++ .../action_mailbox/ingresses/mailgun/inbound_emails_controller.rb | 2 ++ .../action_mailbox/ingresses/mandrill/inbound_emails_controller.rb | 2 ++ .../action_mailbox/ingresses/postfix/inbound_emails_controller.rb | 2 ++ .../action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb | 2 ++ .../rails/conductor/action_mailbox/inbound_emails_controller.rb | 2 ++ app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb | 2 ++ app/controllers/rails/conductor/base_controller.rb | 2 ++ app/jobs/action_mailbox/incineration_job.rb | 2 ++ app/jobs/action_mailbox/routing_job.rb | 2 ++ app/models/action_mailbox/inbound_email.rb | 2 ++ app/models/action_mailbox/inbound_email/incineratable.rb | 2 ++ app/models/action_mailbox/inbound_email/incineratable/incineration.rb | 2 ++ app/models/action_mailbox/inbound_email/message_id.rb | 2 ++ app/models/action_mailbox/inbound_email/routable.rb | 2 ++ lib/action_mailbox.rb | 2 ++ lib/action_mailbox/base.rb | 2 ++ lib/action_mailbox/callbacks.rb | 2 ++ lib/action_mailbox/engine.rb | 2 ++ lib/action_mailbox/mail_ext.rb | 2 ++ lib/action_mailbox/mail_ext/address_equality.rb | 2 ++ lib/action_mailbox/mail_ext/address_wrapping.rb | 2 ++ lib/action_mailbox/mail_ext/addresses.rb | 2 ++ lib/action_mailbox/mail_ext/from_source.rb | 2 ++ lib/action_mailbox/mail_ext/recipients.rb | 2 ++ lib/action_mailbox/router.rb | 2 ++ lib/action_mailbox/router/route.rb | 2 ++ lib/action_mailbox/routing.rb | 2 ++ lib/action_mailbox/test_case.rb | 2 ++ lib/action_mailbox/test_helper.rb | 2 ++ lib/action_mailbox/version.rb | 2 ++ lib/templates/mailboxes/application_mailbox.rb | 2 ++ test/controllers/ingresses/amazon/inbound_emails_controller_test.rb | 2 ++ test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb | 2 ++ test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb | 2 ++ test/controllers/ingresses/postfix/inbound_emails_controller_test.rb | 2 ++ test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb | 2 ++ test/test_helper.rb | 2 ++ test/unit/inbound_email/incineration_test.rb | 2 ++ test/unit/inbound_email/message_id_test.rb | 2 ++ test/unit/inbound_email_test.rb | 2 ++ test/unit/mail_ext/address_equality_test.rb | 2 ++ test/unit/mail_ext/address_wrapping_test.rb | 2 ++ test/unit/mail_ext/recipients_test.rb | 2 ++ test/unit/mailbox/bouncing_test.rb | 2 ++ test/unit/mailbox/callbacks_test.rb | 2 ++ test/unit/mailbox/routing_test.rb | 2 ++ test/unit/mailbox/state_test.rb | 2 ++ test/unit/postfix_relayer_test.rb | 2 ++ test/unit/router_test.rb | 2 ++ 51 files changed, 102 insertions(+) diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb index b73f5b7708..f8764a8240 100644 --- a/app/controllers/action_mailbox/base_controller.rb +++ b/app/controllers/action_mailbox/base_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # The base class for all Active Mailbox ingress controllers. class ActionMailbox::BaseController < ActionController::Base skip_forgery_protection diff --git a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb index 0c04170eae..2a8ec375c7 100644 --- a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Ingests inbound emails from Amazon's Simple Email Service (SES). # # Requires the full RFC 822 message in the +content+ parameter. Authenticates requests by validating their signatures. diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index 7583e25f26..225bcc5565 100644 --- a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Ingests inbound emails from Mailgun. Requires the following parameters: # # - +body-mime+: The full RFC 822 message diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb index 8be27009d2..5a85e6b7f0 100644 --- a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Ingests inbound emails from Mandrill. # # Requires a +mandrill_events+ parameter containing a JSON array of Mandrill inbound email event objects. diff --git a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb index ba04effd1f..a3b794c65b 100644 --- a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Ingests inbound emails relayed from Postfix. # # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the diff --git a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb index 8e1d781c2c..c48abae66c 100644 --- a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +++ b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Ingests inbound emails from SendGrid. Requires an +email+ parameter containing a full RFC 822 message. # # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the diff --git a/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb b/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb index 9804f6ec5b..eb96d88691 100644 --- a/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +++ b/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rails::Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController def index @inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc) diff --git a/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb b/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb index 6191bda5e5..c53203049b 100644 --- a/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +++ b/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Rerouting will run routing and processing on an email that has already been, or attempted to be, processed. class Rails::Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController def create diff --git a/app/controllers/rails/conductor/base_controller.rb b/app/controllers/rails/conductor/base_controller.rb index cfa0b84963..c95a7f3b93 100644 --- a/app/controllers/rails/conductor/base_controller.rb +++ b/app/controllers/rails/conductor/base_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # TODO: Move this to Rails::Conductor gem class Rails::Conductor::BaseController < ActionController::Base layout "rails/conductor" diff --git a/app/jobs/action_mailbox/incineration_job.rb b/app/jobs/action_mailbox/incineration_job.rb index 3c6c9aae4f..27dd9b151f 100644 --- a/app/jobs/action_mailbox/incineration_job.rb +++ b/app/jobs/action_mailbox/incineration_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # You can configure when this `IncinerationJob` will be run as a time-after-processing using the # `config.action_mailbox.incinerate_after` or `ActionMailbox.incinerate_after` setting. # diff --git a/app/jobs/action_mailbox/routing_job.rb b/app/jobs/action_mailbox/routing_job.rb index c345227617..fc3388daff 100644 --- a/app/jobs/action_mailbox/routing_job.rb +++ b/app/jobs/action_mailbox/routing_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly # accept new incoming emails without being burdened to hang while they're actually being processed. class ActionMailbox::RoutingJob < ActiveJob::Base diff --git a/app/models/action_mailbox/inbound_email.rb b/app/models/action_mailbox/inbound_email.rb index 156e2d4dbc..d815941922 100644 --- a/app/models/action_mailbox/inbound_email.rb +++ b/app/models/action_mailbox/inbound_email.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "mail" # The `InboundEmail` is an Active Record that keeps a reference to the raw email stored in Active Storage diff --git a/app/models/action_mailbox/inbound_email/incineratable.rb b/app/models/action_mailbox/inbound_email/incineratable.rb index a049b88b69..fb2461cf50 100644 --- a/app/models/action_mailbox/inbound_email/incineratable.rb +++ b/app/models/action_mailbox/inbound_email/incineratable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Ensure that the `InboundEmail` is automatically scheduled for later incineration if the status has been # changed to `processed`. The later incineration will be invoked at the time specified by the # `ActionMailbox.incinerate_after` time using the `IncinerationJob`. diff --git a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb index 50861bc64b..c4aaaa25a9 100644 --- a/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +++ b/app/models/action_mailbox/inbound_email/incineratable/incineration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Command class for carrying out the actual incineration of the `InboundMail` that's been scheduled # for removal. Before the incineration – which really is just a call to `#destroy!` – is run, we verify # that it's both eligible (by virtue of having already been processed) and time to do so (that is, diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb index dc17e48e74..4807a2a293 100644 --- a/app/models/action_mailbox/inbound_email/message_id.rb +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # The `Message-ID` as specified by rfc822 is supposed to be a unique identifier for that individual email. # That makes it an ideal tracking token for debugging and forensics, just like `X-Request-Id` does for # web request. diff --git a/app/models/action_mailbox/inbound_email/routable.rb b/app/models/action_mailbox/inbound_email/routable.rb index f042fa4f57..58d67eb20c 100644 --- a/app/models/action_mailbox/inbound_email/routable.rb +++ b/app/models/action_mailbox/inbound_email/routable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # A newly received `InboundEmail` will not be routed synchronously as part of ingress controller's receival. # Instead, the routing will be done asynchronously, using a `RoutingJob`, to ensure maximum parallel capacity. # diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb index aec2db7b05..578e04a422 100644 --- a/lib/action_mailbox.rb +++ b/lib/action_mailbox.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "action_mailbox/engine" require "action_mailbox/mail_ext" diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index 0ae83bf691..b78110d039 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/rescuable" require "action_mailbox/callbacks" diff --git a/lib/action_mailbox/callbacks.rb b/lib/action_mailbox/callbacks.rb index f1bbd0593d..c6f07d7ccf 100644 --- a/lib/action_mailbox/callbacks.rb +++ b/lib/action_mailbox/callbacks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/callbacks" module ActionMailbox diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb index e0954e4e49..8b010826e9 100644 --- a/lib/action_mailbox/engine.rb +++ b/lib/action_mailbox/engine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails/engine" module ActionMailbox diff --git a/lib/action_mailbox/mail_ext.rb b/lib/action_mailbox/mail_ext.rb index abdaae4f9e..c4d277a1f9 100644 --- a/lib/action_mailbox/mail_ext.rb +++ b/lib/action_mailbox/mail_ext.rb @@ -1,3 +1,5 @@ +# 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! diff --git a/lib/action_mailbox/mail_ext/address_equality.rb b/lib/action_mailbox/mail_ext/address_equality.rb index 76675365dd..69243a666e 100644 --- a/lib/action_mailbox/mail_ext/address_equality.rb +++ b/lib/action_mailbox/mail_ext/address_equality.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Mail::Address def ==(other_address) other_address.is_a?(Mail::Address) && to_s == other_address.to_s diff --git a/lib/action_mailbox/mail_ext/address_wrapping.rb b/lib/action_mailbox/mail_ext/address_wrapping.rb index f41087a450..fcdfbb6f6f 100644 --- a/lib/action_mailbox/mail_ext/address_wrapping.rb +++ b/lib/action_mailbox/mail_ext/address_wrapping.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Mail::Address def self.wrap(address) address.is_a?(Mail::Address) ? address : Mail::Address.new(address) diff --git a/lib/action_mailbox/mail_ext/addresses.rb b/lib/action_mailbox/mail_ext/addresses.rb index f64c3ef5df..377373bee6 100644 --- a/lib/action_mailbox/mail_ext/addresses.rb +++ b/lib/action_mailbox/mail_ext/addresses.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Mail::Message def from_address header[:from]&.address_list&.addresses&.first diff --git a/lib/action_mailbox/mail_ext/from_source.rb b/lib/action_mailbox/mail_ext/from_source.rb index ddc3cbe385..17b7fc80ad 100644 --- a/lib/action_mailbox/mail_ext/from_source.rb +++ b/lib/action_mailbox/mail_ext/from_source.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mail def self.from_source(source) Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s) diff --git a/lib/action_mailbox/mail_ext/recipients.rb b/lib/action_mailbox/mail_ext/recipients.rb index 87255ce6ce..a8ac42d602 100644 --- a/lib/action_mailbox/mail_ext/recipients.rb +++ b/lib/action_mailbox/mail_ext/recipients.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Mail::Message def recipients Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s) diff --git a/lib/action_mailbox/router.rb b/lib/action_mailbox/router.rb index ef55718974..0f041a8389 100644 --- a/lib/action_mailbox/router.rb +++ b/lib/action_mailbox/router.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when # an inbound_email is received. class ActionMailbox::Router diff --git a/lib/action_mailbox/router/route.rb b/lib/action_mailbox/router/route.rb index 6472b9a31d..adb9f94c1a 100644 --- a/lib/action_mailbox/router/route.rb +++ b/lib/action_mailbox/router/route.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # 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. diff --git a/lib/action_mailbox/routing.rb b/lib/action_mailbox/routing.rb index db462e6019..1ea96c8a9d 100644 --- a/lib/action_mailbox/routing.rb +++ b/lib/action_mailbox/routing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionMailbox # See `ActionMailbox::Base` for how to specify routing. module Routing diff --git a/lib/action_mailbox/test_case.rb b/lib/action_mailbox/test_case.rb index 1b3aec1393..a501e8a7ca 100644 --- a/lib/action_mailbox/test_case.rb +++ b/lib/action_mailbox/test_case.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "action_mailbox/test_helper" require "active_support/test_case" diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb index 4e4552629a..02c52fb779 100644 --- a/lib/action_mailbox/test_helper.rb +++ b/lib/action_mailbox/test_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "mail" module ActionMailbox diff --git a/lib/action_mailbox/version.rb b/lib/action_mailbox/version.rb index 23c615dbbd..7e6bd414e8 100644 --- a/lib/action_mailbox/version.rb +++ b/lib/action_mailbox/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionMailbox VERSION = '0.1.0' end diff --git a/lib/templates/mailboxes/application_mailbox.rb b/lib/templates/mailboxes/application_mailbox.rb index 9386f9eb91..7a2ed6b2ac 100644 --- a/lib/templates/mailboxes/application_mailbox.rb +++ b/lib/templates/mailboxes/application_mailbox.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationMailbox < ActionMailbox::Base # route /something/i => :somewhere end diff --git a/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb b/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb index c36c500cbe..e10985553e 100644 --- a/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "test_helper" ActionMailbox::Ingresses::Amazon::InboundEmailsController.verifier = diff --git a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb index c5ec71013e..3f225b8953 100644 --- a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "test_helper" ENV["MAILGUN_INGRESS_API_KEY"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" diff --git a/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb index c8a8e731d6..40f956c930 100644 --- a/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "test_helper" ENV["MANDRILL_INGRESS_API_KEY"] = "1l9Qf7lutEf7h73VXfBwhw" diff --git a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb index 5e0777aa30..d646f5e859 100644 --- a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "test_helper" class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest diff --git a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb index 0c7d0d6846..59a87e9248 100644 --- a/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb +++ b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "test_helper" class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDispatch::IntegrationTest diff --git a/test/test_helper.rb b/test/test_helper.rb index 31fc59bba2..d0d802fdb5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ENV["RAILS_ENV"] = "test" ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL" diff --git a/test/unit/inbound_email/incineration_test.rb b/test/unit/inbound_email/incineration_test.rb index b46101bcc4..bcaee16de4 100644 --- a/test/unit/inbound_email/incineration_test.rb +++ b/test/unit/inbound_email/incineration_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase diff --git a/test/unit/inbound_email/message_id_test.rb b/test/unit/inbound_email/message_id_test.rb index 1cc3de360e..00e65614ec 100644 --- a/test/unit/inbound_email/message_id_test.rb +++ b/test/unit/inbound_email/message_id_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' class ActionMailbox::InboundEmail::MessageIdTest < ActiveSupport::TestCase diff --git a/test/unit/inbound_email_test.rb b/test/unit/inbound_email_test.rb index 7c42188584..42349bc7b4 100644 --- a/test/unit/inbound_email_test.rb +++ b/test/unit/inbound_email_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../test_helper' module ActionMailbox diff --git a/test/unit/mail_ext/address_equality_test.rb b/test/unit/mail_ext/address_equality_test.rb index 56e9bfa47f..7ea4360b76 100644 --- a/test/unit/mail_ext/address_equality_test.rb +++ b/test/unit/mail_ext/address_equality_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' module MailExt diff --git a/test/unit/mail_ext/address_wrapping_test.rb b/test/unit/mail_ext/address_wrapping_test.rb index 6baa360f3e..a9ae820e26 100644 --- a/test/unit/mail_ext/address_wrapping_test.rb +++ b/test/unit/mail_ext/address_wrapping_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' module MailExt diff --git a/test/unit/mail_ext/recipients_test.rb b/test/unit/mail_ext/recipients_test.rb index e39e1eb89c..4730896354 100644 --- a/test/unit/mail_ext/recipients_test.rb +++ b/test/unit/mail_ext/recipients_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' module MailExt diff --git a/test/unit/mailbox/bouncing_test.rb b/test/unit/mailbox/bouncing_test.rb index 6ac806ba29..ce5de36ff6 100644 --- a/test/unit/mailbox/bouncing_test.rb +++ b/test/unit/mailbox/bouncing_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' class BouncingWithReplyMailbox < ActionMailbox::Base diff --git a/test/unit/mailbox/callbacks_test.rb b/test/unit/mailbox/callbacks_test.rb index 279bf7e574..ae720155cf 100644 --- a/test/unit/mailbox/callbacks_test.rb +++ b/test/unit/mailbox/callbacks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' class CallbackMailbox < ActionMailbox::Base diff --git a/test/unit/mailbox/routing_test.rb b/test/unit/mailbox/routing_test.rb index f30199b7af..a83fbdacf2 100644 --- a/test/unit/mailbox/routing_test.rb +++ b/test/unit/mailbox/routing_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' class ApplicationMailbox < ActionMailbox::Base diff --git a/test/unit/mailbox/state_test.rb b/test/unit/mailbox/state_test.rb index 32d16ca8fe..78b7d21cd7 100644 --- a/test/unit/mailbox/state_test.rb +++ b/test/unit/mailbox/state_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../test_helper' class SuccessfulMailbox < ActionMailbox::Base diff --git a/test/unit/postfix_relayer_test.rb b/test/unit/postfix_relayer_test.rb index 9d6e49dee3..b448bf6f9b 100644 --- a/test/unit/postfix_relayer_test.rb +++ b/test/unit/postfix_relayer_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../test_helper' require 'action_mailbox/postfix_relayer' diff --git a/test/unit/router_test.rb b/test/unit/router_test.rb index 70d39ba6a1..ec60fb0889 100644 --- a/test/unit/router_test.rb +++ b/test/unit/router_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../test_helper' class RootMailbox < ActionMailbox::Base -- cgit v1.2.3 From af72d95883697fea79d5f1e76a90083a5f5a2b8a Mon Sep 17 00:00:00 2001 From: Kyle Keesling Date: Fri, 14 Dec 2018 09:21:59 -0500 Subject: remove unimplemented arguement when adding mail message to ActiveStorage The indentify parameter is not yet implemented in Rails 5.2.2 --- .../rails/conductor/action_mailbox/inbound_emails_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb b/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb index eb96d88691..059af0c302 100644 --- a/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +++ b/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb @@ -24,6 +24,6 @@ class Rails::Conductor::ActionMailbox::InboundEmailsController < Rails::Conducto def create_inbound_email(mail) ActionMailbox::InboundEmail.create! raw_email: \ - { io: StringIO.new(mail.to_s), filename: 'inbound.eml', content_type: 'message/rfc822', identify: false } + { io: StringIO.new(mail.to_s), filename: 'inbound.eml', content_type: 'message/rfc822' } end end -- cgit v1.2.3 From d6621cc2659127217c3c4a128f1ba76cf5c231e8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 14 Dec 2018 09:19:21 -0800 Subject: Use the correct route method --- lib/templates/mailboxes/application_mailbox.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/templates/mailboxes/application_mailbox.rb b/lib/templates/mailboxes/application_mailbox.rb index 7a2ed6b2ac..be51eb3639 100644 --- a/lib/templates/mailboxes/application_mailbox.rb +++ b/lib/templates/mailboxes/application_mailbox.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class ApplicationMailbox < ActionMailbox::Base - # route /something/i => :somewhere + # routing /something/i => :somewhere end -- cgit v1.2.3 From d872de3c69a8762834d206d2f080550c39143080 Mon Sep 17 00:00:00 2001 From: David Gil Date: Fri, 14 Dec 2018 21:24:03 +0100 Subject: Add Mailbox and MailboxTest generators --- .gitignore | 3 + lib/rails/generators/mailbox/USAGE | 12 ++++ lib/rails/generators/mailbox/mailbox_generator.rb | 38 +++++++++++ .../mailbox/templates/application_mailbox.rb.tt | 5 ++ .../generators/mailbox/templates/mailbox.rb.tt | 8 +++ .../generators/mailbox/templates/mailer_test.rb.tt | 13 ++++ test/generators/mailbox_generator_test.rb | 77 ++++++++++++++++++++++ 7 files changed, 156 insertions(+) create mode 100644 lib/rails/generators/mailbox/USAGE create mode 100644 lib/rails/generators/mailbox/mailbox_generator.rb create mode 100644 lib/rails/generators/mailbox/templates/application_mailbox.rb.tt create mode 100644 lib/rails/generators/mailbox/templates/mailbox.rb.tt create mode 100644 lib/rails/generators/mailbox/templates/mailer_test.rb.tt create mode 100644 test/generators/mailbox_generator_test.rb diff --git a/.gitignore b/.gitignore index 4862d67b18..ead1a2d308 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .byebug_history *.sqlite3-journal +.ruby-version +.ruby-gemset +/tmp/ diff --git a/lib/rails/generators/mailbox/USAGE b/lib/rails/generators/mailbox/USAGE new file mode 100644 index 0000000000..d679dd63ae --- /dev/null +++ b/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/lib/rails/generators/mailbox/mailbox_generator.rb b/lib/rails/generators/mailbox/mailbox_generator.rb new file mode 100644 index 0000000000..7fcdfd3384 --- /dev/null +++ b/lib/rails/generators/mailbox/mailbox_generator.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Rails + module Generators + class MailboxGenerator < NamedBase + source_root File.expand_path("templates", __dir__) + + argument :actions, type: :array, default: [:process], banner: "method method" + + def check_class_collision + class_collisions "#{class_name}Mailbox", "#{class_name}MailboxTest" + end + + 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 + + template "mailer_test.rb", File.join('test/mailboxes', class_path, "#{file_name}_mailbox_test.rb") + 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/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt b/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt new file mode 100644 index 0000000000..be51eb3639 --- /dev/null +++ b/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationMailbox < ActionMailbox::Base + # routing /something/i => :somewhere +end diff --git a/lib/rails/generators/mailbox/templates/mailbox.rb.tt b/lib/rails/generators/mailbox/templates/mailbox.rb.tt new file mode 100644 index 0000000000..9788bd9bb4 --- /dev/null +++ b/lib/rails/generators/mailbox/templates/mailbox.rb.tt @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class <%= class_name %>Mailbox < ApplicationMailbox +<% actions.each do |action| -%> + def <%= action %> + end +<% end -%> +end diff --git a/lib/rails/generators/mailbox/templates/mailer_test.rb.tt b/lib/rails/generators/mailbox/templates/mailer_test.rb.tt new file mode 100644 index 0000000000..41749808e3 --- /dev/null +++ b/lib/rails/generators/mailbox/templates/mailer_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" , + # from: '"else" ', + # subject: "Hello world!", + # body: "Hello?" + # end +end diff --git a/test/generators/mailbox_generator_test.rb b/test/generators/mailbox_generator_test.rb new file mode 100644 index 0000000000..9295c2d8c8 --- /dev/null +++ b/test/generators/mailbox_generator_test.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require "test_helper" +require "rails/generators/mailbox/mailbox_generator" + +class MailboxGeneratorTest < Rails::Generators::TestCase + destination File.expand_path("../../tmp", File.dirname(__FILE__)) + setup :prepare_destination + tests Rails::Generators::MailboxGenerator + + arguments ["inbox"] + + def test_mailbox_skeleton_is_created + run_generator + assert_file "app/mailboxes/inbox_mailbox.rb" do |mailbox| + assert_match(/class InboxMailbox < ApplicationMailbox/, mailbox) + assert_match(/def process/, mailbox) + assert_no_match(%r{# routing /something/i => :somewhere}, mailbox) + end + + assert_file "app/mailboxes/application_mailbox.rb" do |mailbox| + assert_match(/class ApplicationMailbox < ActionMailbox::Base/, mailbox) + assert_match(%r{# routing /something/i => :somewhere}, mailbox) + assert_no_match(/def process/, mailbox) + end + end + + def test_check_class_collision + Object.send :const_set, :InboxMailbox, Class.new + content = capture(:stderr) { run_generator } + assert_match(/The name 'InboxMailbox' is either already used in your application or reserved/, content) + ensure + Object.send :remove_const, :InboxMailbox + end + + def test_invokes_default_test_framework + run_generator %w(inbox foo bar) + assert_file "test/mailboxes/inbox_mailbox_test.rb" do |test| + assert_match(/class InboxMailboxTest < ActionMailbox::TestCase/, test) + assert_match(/# test "receive mail" do/, test) + assert_match(/# to: '"someone" ,/, test) + end + end + + def test_check_test_class_collision + Object.send :const_set, :InboxMailboxTest, Class.new + content = capture(:stderr) { run_generator } + assert_match(/The name 'InboxMailboxTest' is either already used in your application or reserved/, content) + ensure + Object.send :remove_const, :InboxMailboxTest + end + + def test_actions_are_turned_into_methods + run_generator %w(inbox foo bar) + + assert_file "app/mailboxes/inbox_mailbox.rb" do |mailbox| + assert_instance_method :foo, mailbox + assert_instance_method :bar, mailbox + end + end + + def test_mailbox_on_revoke + run_generator + run_generator ["inbox"], behavior: :revoke + + assert_no_file "app/mailboxes/inbox.rb" + end + + def test_mailbox_suffix_is_not_duplicated + run_generator ["inbox_mailbox"] + + assert_no_file "app/mailboxes/inbox_mailbox_mailbox.rb" + assert_file "app/mailboxes/inbox_mailbox.rb" + + assert_no_file "test/mailboxes/inbox_mailbox_mailbox_test.rb" + assert_file "test/mailboxes/inbox_mailbox_test.rb" + end +end -- cgit v1.2.3 From 5c3f0d2e19bfe56c14c9fbea213e4421003f7b1b Mon Sep 17 00:00:00 2001 From: David Gil Date: Fri, 14 Dec 2018 21:40:04 +0100 Subject: Uses the new generator to install ApplicationMailbox in action_mailbox:install rake --- lib/tasks/install.rake | 7 +++---- lib/templates/installer.rb | 4 ---- lib/templates/mailboxes/application_mailbox.rb | 5 ----- 3 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 lib/templates/installer.rb delete mode 100644 lib/templates/mailboxes/application_mailbox.rb diff --git a/lib/tasks/install.rake b/lib/tasks/install.rake index cfc06fbb5f..1f4c071494 100644 --- a/lib/tasks/install.rake +++ b/lib/tasks/install.rake @@ -5,11 +5,10 @@ namespace :action_mailbox do Rake::Task["install:migrations"].clear_comments desc "Copy over the migration" - task install: %w[ environment run_installer copy_migrations ] + task install: %w[ environment run_generator copy_migrations ] - task :run_installer do - installer_template = File.expand_path("../templates/installer.rb", __dir__) - system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{installer_template}" + task :run_generator do + system "#{RbConfig.ruby} ./bin/rails generate mailbox application" end task :copy_migrations do diff --git a/lib/templates/installer.rb b/lib/templates/installer.rb deleted file mode 100644 index 18a562c0e5..0000000000 --- a/lib/templates/installer.rb +++ /dev/null @@ -1,4 +0,0 @@ -say "Copying application_mailbox.rb to app/mailboxes" -copy_file "#{__dir__}/mailboxes/application_mailbox.rb", "app/mailboxes/application_mailbox.rb" - -environment "# Prepare the ingress controller used to receive mail\n# config.action_mailbox.ingress = :amazon\n\n", env: 'production' diff --git a/lib/templates/mailboxes/application_mailbox.rb b/lib/templates/mailboxes/application_mailbox.rb deleted file mode 100644 index be51eb3639..0000000000 --- a/lib/templates/mailboxes/application_mailbox.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class ApplicationMailbox < ActionMailbox::Base - # routing /something/i => :somewhere -end -- cgit v1.2.3 From acae20e2abee8843ae2b33929553fe1e100dd1c4 Mon Sep 17 00:00:00 2001 From: David Gil Date: Fri, 14 Dec 2018 21:42:20 +0100 Subject: renames file names --- lib/rails/generators/mailbox/mailbox_generator.rb | 2 +- lib/rails/generators/mailbox/templates/mailbox_test.rb.tt | 13 +++++++++++++ lib/rails/generators/mailbox/templates/mailer_test.rb.tt | 13 ------------- 3 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 lib/rails/generators/mailbox/templates/mailbox_test.rb.tt delete mode 100644 lib/rails/generators/mailbox/templates/mailer_test.rb.tt diff --git a/lib/rails/generators/mailbox/mailbox_generator.rb b/lib/rails/generators/mailbox/mailbox_generator.rb index 7fcdfd3384..5511545a98 100644 --- a/lib/rails/generators/mailbox/mailbox_generator.rb +++ b/lib/rails/generators/mailbox/mailbox_generator.rb @@ -20,7 +20,7 @@ module Rails end end - template "mailer_test.rb", File.join('test/mailboxes', class_path, "#{file_name}_mailbox_test.rb") + template "mailbox_test.rb", File.join('test/mailboxes', class_path, "#{file_name}_mailbox_test.rb") end hook_for :test_framework diff --git a/lib/rails/generators/mailbox/templates/mailbox_test.rb.tt b/lib/rails/generators/mailbox/templates/mailbox_test.rb.tt new file mode 100644 index 0000000000..41749808e3 --- /dev/null +++ b/lib/rails/generators/mailbox/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" , + # from: '"else" ', + # subject: "Hello world!", + # body: "Hello?" + # end +end diff --git a/lib/rails/generators/mailbox/templates/mailer_test.rb.tt b/lib/rails/generators/mailbox/templates/mailer_test.rb.tt deleted file mode 100644 index 41749808e3..0000000000 --- a/lib/rails/generators/mailbox/templates/mailer_test.rb.tt +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class <%= class_name %>MailboxTest < ActionMailbox::TestCase - # test "receive mail" do - # receive_inbound_email_from_mail \ - # to: '"someone" , - # from: '"else" ', - # subject: "Hello world!", - # body: "Hello?" - # end -end -- cgit v1.2.3 From 8415a6c5cf883f7ce6fe93b80ec99072fb04be83 Mon Sep 17 00:00:00 2001 From: Dino Maric Date: Sat, 15 Dec 2018 13:41:23 +0100 Subject: Fix Rails generators 1.Don't generate ApplicationMailboxTest when executing installer 2. Hookup test_unit, so console doesn't throw errors --- lib/generators/rails/USAGE | 12 +++++++ lib/generators/rails/mailbox_generator.rb | 34 +++++++++++++++++++ .../rails/templates/application_mailbox.rb.tt | 5 +++ lib/generators/rails/templates/mailbox.rb.tt | 8 +++++ lib/generators/test_unit/mailbox_generator.rb | 20 ++++++++++++ .../test_unit/templates/mailbox_test.rb.tt | 13 ++++++++ lib/rails/generators/mailbox/USAGE | 12 ------- lib/rails/generators/mailbox/mailbox_generator.rb | 38 ---------------------- .../mailbox/templates/application_mailbox.rb.tt | 5 --- .../generators/mailbox/templates/mailbox.rb.tt | 8 ----- .../mailbox/templates/mailbox_test.rb.tt | 13 -------- lib/tasks/install.rake | 2 +- test/generators/mailbox_generator_test.rb | 15 +++------ 13 files changed, 97 insertions(+), 88 deletions(-) create mode 100644 lib/generators/rails/USAGE create mode 100644 lib/generators/rails/mailbox_generator.rb create mode 100644 lib/generators/rails/templates/application_mailbox.rb.tt create mode 100644 lib/generators/rails/templates/mailbox.rb.tt create mode 100644 lib/generators/test_unit/mailbox_generator.rb create mode 100644 lib/generators/test_unit/templates/mailbox_test.rb.tt delete mode 100644 lib/rails/generators/mailbox/USAGE delete mode 100644 lib/rails/generators/mailbox/mailbox_generator.rb delete mode 100644 lib/rails/generators/mailbox/templates/application_mailbox.rb.tt delete mode 100644 lib/rails/generators/mailbox/templates/mailbox.rb.tt delete mode 100644 lib/rails/generators/mailbox/templates/mailbox_test.rb.tt diff --git a/lib/generators/rails/USAGE b/lib/generators/rails/USAGE new file mode 100644 index 0000000000..d679dd63ae --- /dev/null +++ b/lib/generators/rails/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/lib/generators/rails/mailbox_generator.rb b/lib/generators/rails/mailbox_generator.rb new file mode 100644 index 0000000000..7b43173480 --- /dev/null +++ b/lib/generators/rails/mailbox_generator.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Rails + module Generators + class MailboxGenerator < NamedBase + source_root File.expand_path("templates", __dir__) + + argument :actions, type: :array, default: [:process], banner: "method method" + + 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/lib/generators/rails/templates/application_mailbox.rb.tt b/lib/generators/rails/templates/application_mailbox.rb.tt new file mode 100644 index 0000000000..be51eb3639 --- /dev/null +++ b/lib/generators/rails/templates/application_mailbox.rb.tt @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationMailbox < ActionMailbox::Base + # routing /something/i => :somewhere +end diff --git a/lib/generators/rails/templates/mailbox.rb.tt b/lib/generators/rails/templates/mailbox.rb.tt new file mode 100644 index 0000000000..9788bd9bb4 --- /dev/null +++ b/lib/generators/rails/templates/mailbox.rb.tt @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class <%= class_name %>Mailbox < ApplicationMailbox +<% actions.each do |action| -%> + def <%= action %> + end +<% end -%> +end diff --git a/lib/generators/test_unit/mailbox_generator.rb b/lib/generators/test_unit/mailbox_generator.rb new file mode 100644 index 0000000000..2ec7d11a2f --- /dev/null +++ b/lib/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/lib/generators/test_unit/templates/mailbox_test.rb.tt b/lib/generators/test_unit/templates/mailbox_test.rb.tt new file mode 100644 index 0000000000..41749808e3 --- /dev/null +++ b/lib/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" , + # from: '"else" ', + # subject: "Hello world!", + # body: "Hello?" + # end +end diff --git a/lib/rails/generators/mailbox/USAGE b/lib/rails/generators/mailbox/USAGE deleted file mode 100644 index d679dd63ae..0000000000 --- a/lib/rails/generators/mailbox/USAGE +++ /dev/null @@ -1,12 +0,0 @@ -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/lib/rails/generators/mailbox/mailbox_generator.rb b/lib/rails/generators/mailbox/mailbox_generator.rb deleted file mode 100644 index 5511545a98..0000000000 --- a/lib/rails/generators/mailbox/mailbox_generator.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Rails - module Generators - class MailboxGenerator < NamedBase - source_root File.expand_path("templates", __dir__) - - argument :actions, type: :array, default: [:process], banner: "method method" - - def check_class_collision - class_collisions "#{class_name}Mailbox", "#{class_name}MailboxTest" - end - - 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 - - template "mailbox_test.rb", File.join('test/mailboxes', class_path, "#{file_name}_mailbox_test.rb") - 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/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt b/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt deleted file mode 100644 index be51eb3639..0000000000 --- a/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class ApplicationMailbox < ActionMailbox::Base - # routing /something/i => :somewhere -end diff --git a/lib/rails/generators/mailbox/templates/mailbox.rb.tt b/lib/rails/generators/mailbox/templates/mailbox.rb.tt deleted file mode 100644 index 9788bd9bb4..0000000000 --- a/lib/rails/generators/mailbox/templates/mailbox.rb.tt +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class <%= class_name %>Mailbox < ApplicationMailbox -<% actions.each do |action| -%> - def <%= action %> - end -<% end -%> -end diff --git a/lib/rails/generators/mailbox/templates/mailbox_test.rb.tt b/lib/rails/generators/mailbox/templates/mailbox_test.rb.tt deleted file mode 100644 index 41749808e3..0000000000 --- a/lib/rails/generators/mailbox/templates/mailbox_test.rb.tt +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class <%= class_name %>MailboxTest < ActionMailbox::TestCase - # test "receive mail" do - # receive_inbound_email_from_mail \ - # to: '"someone" , - # from: '"else" ', - # subject: "Hello world!", - # body: "Hello?" - # end -end diff --git a/lib/tasks/install.rake b/lib/tasks/install.rake index 1f4c071494..598e30c2d4 100644 --- a/lib/tasks/install.rake +++ b/lib/tasks/install.rake @@ -8,7 +8,7 @@ namespace :action_mailbox do task install: %w[ environment run_generator copy_migrations ] task :run_generator do - system "#{RbConfig.ruby} ./bin/rails generate mailbox application" + system "#{RbConfig.ruby} ./bin/rails generate mailbox application --no-test-framework" end task :copy_migrations do diff --git a/test/generators/mailbox_generator_test.rb b/test/generators/mailbox_generator_test.rb index 9295c2d8c8..46f1dc6ea0 100644 --- a/test/generators/mailbox_generator_test.rb +++ b/test/generators/mailbox_generator_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true + require "test_helper" -require "rails/generators/mailbox/mailbox_generator" +require "generators/rails/mailbox_generator" class MailboxGeneratorTest < Rails::Generators::TestCase destination File.expand_path("../../tmp", File.dirname(__FILE__)) @@ -33,7 +34,7 @@ class MailboxGeneratorTest < Rails::Generators::TestCase end def test_invokes_default_test_framework - run_generator %w(inbox foo bar) + run_generator %w(inbox foo bar -t=test_unit) assert_file "test/mailboxes/inbox_mailbox_test.rb" do |test| assert_match(/class InboxMailboxTest < ActionMailbox::TestCase/, test) assert_match(/# test "receive mail" do/, test) @@ -41,14 +42,6 @@ class MailboxGeneratorTest < Rails::Generators::TestCase end end - def test_check_test_class_collision - Object.send :const_set, :InboxMailboxTest, Class.new - content = capture(:stderr) { run_generator } - assert_match(/The name 'InboxMailboxTest' is either already used in your application or reserved/, content) - ensure - Object.send :remove_const, :InboxMailboxTest - end - def test_actions_are_turned_into_methods run_generator %w(inbox foo bar) @@ -66,7 +59,7 @@ class MailboxGeneratorTest < Rails::Generators::TestCase end def test_mailbox_suffix_is_not_duplicated - run_generator ["inbox_mailbox"] + run_generator %w(inbox_mailbox -t=test_unit) assert_no_file "app/mailboxes/inbox_mailbox_mailbox.rb" assert_file "app/mailboxes/inbox_mailbox.rb" -- cgit v1.2.3 From b91c615662a5d419778927724f89bca85d85f415 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sun, 16 Dec 2018 00:14:14 -0500 Subject: Hook up Travis CI --- .travis.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..b66561a4ac --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: ruby +sudo: false + +rvm: + - 2.4.5 + - 2.5.3 + - ruby-head + +cache: + bundler: true + +matrix: + allow_failures: + - rvm: ruby-head + fast_finish: true + +script: + - bundle exec rake -- cgit v1.2.3 From 9d09c68b6c3ca0c01498e44441a5a27fc2cba6df Mon Sep 17 00:00:00 2001 From: Dino Maric Date: Sun, 16 Dec 2018 12:59:51 +0100 Subject: Subtile mention of generator inside README Just a small mention that we can use generators inside README. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f969acd059..8e45e1f166 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,11 @@ end Then setup a mailbox: +```ruby +# Generate new maiblox +bin/rails generate mailbox forwards +``` + ```ruby # app/mailboxes/forwards_mailbox.rb class ForwardsMailbox < ApplicationMailbox -- cgit v1.2.3 From 8ea1b0f7c7af6b7579bcb431bba28cac7a9e621a Mon Sep 17 00:00:00 2001 From: Dino Maric Date: Sun, 16 Dec 2018 14:06:58 +0100 Subject: Set ruby >= 2.4.1 as min required version This matches Rails master, and it should make TravisCI green. --- actionmailbox.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec index 84307ee7ef..745505ceb9 100644 --- a/actionmailbox.gemspec +++ b/actionmailbox.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.homepage = "https://github.com/rails/actionmailbox" s.license = "MIT" - s.required_ruby_version = ">= 2.5.0" + s.required_ruby_version = ">= 2.4.1" s.add_dependency "rails", ">= 5.2.0" -- cgit v1.2.3 From 272268fda5c13cdf010e6e488ec0986d97653e83 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Tue, 18 Dec 2018 21:45:43 +0100 Subject: Include proper suffix to assert no file correctly. --- test/generators/mailbox_generator_test.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/generators/mailbox_generator_test.rb b/test/generators/mailbox_generator_test.rb index 46f1dc6ea0..9a3b23979a 100644 --- a/test/generators/mailbox_generator_test.rb +++ b/test/generators/mailbox_generator_test.rb @@ -12,6 +12,7 @@ class MailboxGeneratorTest < Rails::Generators::TestCase def test_mailbox_skeleton_is_created run_generator + assert_file "app/mailboxes/inbox_mailbox.rb" do |mailbox| assert_match(/class InboxMailbox < ApplicationMailbox/, mailbox) assert_match(/def process/, mailbox) @@ -55,7 +56,7 @@ class MailboxGeneratorTest < Rails::Generators::TestCase run_generator run_generator ["inbox"], behavior: :revoke - assert_no_file "app/mailboxes/inbox.rb" + assert_no_file "app/mailboxes/inbox_mailbox.rb" end def test_mailbox_suffix_is_not_duplicated -- cgit v1.2.3 From 9082a0928fe1123c6f27344fc026789aab39d1ff Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Tue, 18 Dec 2018 21:46:17 +0100 Subject: Test namespacing is supported. --- test/generators/mailbox_generator_test.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/generators/mailbox_generator_test.rb b/test/generators/mailbox_generator_test.rb index 9a3b23979a..8d3687cbaa 100644 --- a/test/generators/mailbox_generator_test.rb +++ b/test/generators/mailbox_generator_test.rb @@ -26,6 +26,28 @@ class MailboxGeneratorTest < Rails::Generators::TestCase end end + def test_mailbox_skeleton_is_created_with_namespace + run_generator %w(inceptions/inbox -t=test_unit) + + assert_file "app/mailboxes/inceptions/inbox_mailbox.rb" do |mailbox| + assert_match(/class Inceptions::InboxMailbox < ApplicationMailbox/, mailbox) + assert_match(/def process/, mailbox) + assert_no_match(%r{# routing /something/i => :somewhere}, mailbox) + end + + assert_file "test/mailboxes/inceptions/inbox_mailbox_test.rb" do |mailbox| + assert_match(/class Inceptions::InboxMailboxTest < ActionMailbox::TestCase/, mailbox) + assert_match(/# test "receive mail" do/, mailbox) + assert_match(/# to: '"someone" ,/, mailbox) + end + + assert_file "app/mailboxes/application_mailbox.rb" do |mailbox| + assert_match(/class ApplicationMailbox < ActionMailbox::Base/, mailbox) + assert_match(%r{# routing /something/i => :somewhere}, mailbox) + assert_no_match(/def process/, mailbox) + end + end + def test_check_class_collision Object.send :const_set, :InboxMailbox, Class.new content = capture(:stderr) { run_generator } -- cgit v1.2.3 From a2c3b282577d1dc784898681763a400a0caad5c3 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Tue, 18 Dec 2018 21:46:35 +0100 Subject: Remove actions argument from Mailbox generator. Generators only have the public `process` action, so there's nothing else to generate here. Skip the needless indirection. --- lib/generators/rails/mailbox_generator.rb | 2 -- lib/generators/rails/templates/mailbox.rb.tt | 4 +--- test/generators/mailbox_generator_test.rb | 12 ++---------- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/lib/generators/rails/mailbox_generator.rb b/lib/generators/rails/mailbox_generator.rb index 7b43173480..c2c403b8f6 100644 --- a/lib/generators/rails/mailbox_generator.rb +++ b/lib/generators/rails/mailbox_generator.rb @@ -5,8 +5,6 @@ module Rails class MailboxGenerator < NamedBase source_root File.expand_path("templates", __dir__) - argument :actions, type: :array, default: [:process], banner: "method method" - check_class_collision suffix: "Mailbox" def create_mailbox_file diff --git a/lib/generators/rails/templates/mailbox.rb.tt b/lib/generators/rails/templates/mailbox.rb.tt index 9788bd9bb4..56b138e2d9 100644 --- a/lib/generators/rails/templates/mailbox.rb.tt +++ b/lib/generators/rails/templates/mailbox.rb.tt @@ -1,8 +1,6 @@ # frozen_string_literal: true class <%= class_name %>Mailbox < ApplicationMailbox -<% actions.each do |action| -%> - def <%= action %> + def process end -<% end -%> end diff --git a/test/generators/mailbox_generator_test.rb b/test/generators/mailbox_generator_test.rb index 8d3687cbaa..26ab5a44d7 100644 --- a/test/generators/mailbox_generator_test.rb +++ b/test/generators/mailbox_generator_test.rb @@ -57,7 +57,8 @@ class MailboxGeneratorTest < Rails::Generators::TestCase end def test_invokes_default_test_framework - run_generator %w(inbox foo bar -t=test_unit) + run_generator %w(inbox -t=test_unit) + assert_file "test/mailboxes/inbox_mailbox_test.rb" do |test| assert_match(/class InboxMailboxTest < ActionMailbox::TestCase/, test) assert_match(/# test "receive mail" do/, test) @@ -65,15 +66,6 @@ class MailboxGeneratorTest < Rails::Generators::TestCase end end - def test_actions_are_turned_into_methods - run_generator %w(inbox foo bar) - - assert_file "app/mailboxes/inbox_mailbox.rb" do |mailbox| - assert_instance_method :foo, mailbox - assert_instance_method :bar, mailbox - end - end - def test_mailbox_on_revoke run_generator run_generator ["inbox"], behavior: :revoke -- cgit v1.2.3 From 7cf7ba4db535ea45e70e844cb653909be6143fd5 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Tue, 18 Dec 2018 22:03:23 +0100 Subject: Refit generator file structure to match Action Cable. Fits with rails/generators/channel containing: - channel_generator.rb - templates/ - USAGE --- lib/generators/rails/USAGE | 12 -------- lib/generators/rails/mailbox_generator.rb | 32 ---------------------- .../rails/templates/application_mailbox.rb.tt | 5 ---- lib/generators/rails/templates/mailbox.rb.tt | 6 ---- lib/generators/test_unit/mailbox_generator.rb | 20 -------------- .../test_unit/templates/mailbox_test.rb.tt | 13 --------- lib/rails/generators/mailbox/USAGE | 12 ++++++++ lib/rails/generators/mailbox/mailbox_generator.rb | 32 ++++++++++++++++++++++ .../mailbox/templates/application_mailbox.rb.tt | 5 ++++ .../generators/mailbox/templates/mailbox.rb.tt | 6 ++++ .../generators/test_unit/mailbox_generator.rb | 20 ++++++++++++++ .../test_unit/templates/mailbox_test.rb.tt | 13 +++++++++ test/generators/mailbox_generator_test.rb | 4 +-- 13 files changed, 90 insertions(+), 90 deletions(-) delete mode 100644 lib/generators/rails/USAGE delete mode 100644 lib/generators/rails/mailbox_generator.rb delete mode 100644 lib/generators/rails/templates/application_mailbox.rb.tt delete mode 100644 lib/generators/rails/templates/mailbox.rb.tt delete mode 100644 lib/generators/test_unit/mailbox_generator.rb delete mode 100644 lib/generators/test_unit/templates/mailbox_test.rb.tt create mode 100644 lib/rails/generators/mailbox/USAGE create mode 100644 lib/rails/generators/mailbox/mailbox_generator.rb create mode 100644 lib/rails/generators/mailbox/templates/application_mailbox.rb.tt create mode 100644 lib/rails/generators/mailbox/templates/mailbox.rb.tt create mode 100644 lib/rails/generators/test_unit/mailbox_generator.rb create mode 100644 lib/rails/generators/test_unit/templates/mailbox_test.rb.tt diff --git a/lib/generators/rails/USAGE b/lib/generators/rails/USAGE deleted file mode 100644 index d679dd63ae..0000000000 --- a/lib/generators/rails/USAGE +++ /dev/null @@ -1,12 +0,0 @@ -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/lib/generators/rails/mailbox_generator.rb b/lib/generators/rails/mailbox_generator.rb deleted file mode 100644 index c2c403b8f6..0000000000 --- a/lib/generators/rails/mailbox_generator.rb +++ /dev/null @@ -1,32 +0,0 @@ -# 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/lib/generators/rails/templates/application_mailbox.rb.tt b/lib/generators/rails/templates/application_mailbox.rb.tt deleted file mode 100644 index be51eb3639..0000000000 --- a/lib/generators/rails/templates/application_mailbox.rb.tt +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class ApplicationMailbox < ActionMailbox::Base - # routing /something/i => :somewhere -end diff --git a/lib/generators/rails/templates/mailbox.rb.tt b/lib/generators/rails/templates/mailbox.rb.tt deleted file mode 100644 index 56b138e2d9..0000000000 --- a/lib/generators/rails/templates/mailbox.rb.tt +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -class <%= class_name %>Mailbox < ApplicationMailbox - def process - end -end diff --git a/lib/generators/test_unit/mailbox_generator.rb b/lib/generators/test_unit/mailbox_generator.rb deleted file mode 100644 index 2ec7d11a2f..0000000000 --- a/lib/generators/test_unit/mailbox_generator.rb +++ /dev/null @@ -1,20 +0,0 @@ -# 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/lib/generators/test_unit/templates/mailbox_test.rb.tt b/lib/generators/test_unit/templates/mailbox_test.rb.tt deleted file mode 100644 index 41749808e3..0000000000 --- a/lib/generators/test_unit/templates/mailbox_test.rb.tt +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class <%= class_name %>MailboxTest < ActionMailbox::TestCase - # test "receive mail" do - # receive_inbound_email_from_mail \ - # to: '"someone" , - # from: '"else" ', - # subject: "Hello world!", - # body: "Hello?" - # end -end diff --git a/lib/rails/generators/mailbox/USAGE b/lib/rails/generators/mailbox/USAGE new file mode 100644 index 0000000000..d679dd63ae --- /dev/null +++ b/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/lib/rails/generators/mailbox/mailbox_generator.rb b/lib/rails/generators/mailbox/mailbox_generator.rb new file mode 100644 index 0000000000..c2c403b8f6 --- /dev/null +++ b/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/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt b/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt new file mode 100644 index 0000000000..be51eb3639 --- /dev/null +++ b/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationMailbox < ActionMailbox::Base + # routing /something/i => :somewhere +end diff --git a/lib/rails/generators/mailbox/templates/mailbox.rb.tt b/lib/rails/generators/mailbox/templates/mailbox.rb.tt new file mode 100644 index 0000000000..56b138e2d9 --- /dev/null +++ b/lib/rails/generators/mailbox/templates/mailbox.rb.tt @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class <%= class_name %>Mailbox < ApplicationMailbox + def process + end +end diff --git a/lib/rails/generators/test_unit/mailbox_generator.rb b/lib/rails/generators/test_unit/mailbox_generator.rb new file mode 100644 index 0000000000..2ec7d11a2f --- /dev/null +++ b/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/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt b/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt new file mode 100644 index 0000000000..41749808e3 --- /dev/null +++ b/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" , + # from: '"else" ', + # subject: "Hello world!", + # body: "Hello?" + # end +end diff --git a/test/generators/mailbox_generator_test.rb b/test/generators/mailbox_generator_test.rb index 26ab5a44d7..624fbef420 100644 --- a/test/generators/mailbox_generator_test.rb +++ b/test/generators/mailbox_generator_test.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true require "test_helper" -require "generators/rails/mailbox_generator" +require "rails/generators/mailbox/mailbox_generator" class MailboxGeneratorTest < Rails::Generators::TestCase - destination File.expand_path("../../tmp", File.dirname(__FILE__)) + destination File.expand_path("../../tmp", __dir__) setup :prepare_destination tests Rails::Generators::MailboxGenerator -- cgit v1.2.3 From 849f2b6634074d32ab35e4537f9f2852d5052e80 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Tue, 18 Dec 2018 22:11:22 +0100 Subject: Resurrect installer. Running `./bin/rails generate mailbox application --no-test-framework` generates: ``` class ApplicationMailbox < ApplicationMailbox def process end end ``` which is not correct for the application mailbox. It shouldn't respond to process but it should contain a routing hint. Generally generators aren't meant to be used like the previous commit. The mailbox generator can certainly add in the ApplicationMailbox if missing, but it shouldn't be called with "application" as an argument. Also adds back auto inserting an `ingress` config line in `config/environmnets/production.rb`. Fixes #13. [Kasper Timm Hansen, Andrew Babichev] --- lib/rails/generators/installer.rb | 8 ++++++++ lib/tasks/install.rake | 7 ++++--- test/dummy/app/mailboxes/application_mailbox.rb | 3 +++ test/dummy/config/environments/production.rb | 3 +++ 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 lib/rails/generators/installer.rb diff --git a/lib/rails/generators/installer.rb b/lib/rails/generators/installer.rb new file mode 100644 index 0000000000..a2bc4b5412 --- /dev/null +++ b/lib/rails/generators/installer.rb @@ -0,0 +1,8 @@ +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/lib/tasks/install.rake b/lib/tasks/install.rake index 598e30c2d4..0885e2d6a5 100644 --- a/lib/tasks/install.rake +++ b/lib/tasks/install.rake @@ -5,10 +5,11 @@ namespace :action_mailbox do Rake::Task["install:migrations"].clear_comments desc "Copy over the migration" - task install: %w[ environment run_generator copy_migrations ] + task install: %w[ environment run_installer copy_migrations ] - task :run_generator do - system "#{RbConfig.ruby} ./bin/rails generate mailbox application --no-test-framework" + 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 diff --git a/test/dummy/app/mailboxes/application_mailbox.rb b/test/dummy/app/mailboxes/application_mailbox.rb index 47fb2017d6..be51eb3639 100644 --- a/test/dummy/app/mailboxes/application_mailbox.rb +++ b/test/dummy/app/mailboxes/application_mailbox.rb @@ -1,2 +1,5 @@ +# frozen_string_literal: true + class ApplicationMailbox < ActionMailbox::Base + # routing /something/i => :somewhere end diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb index 2aaa79f620..1731582220 100644 --- a/test/dummy/config/environments/production.rb +++ b/test/dummy/config/environments/production.rb @@ -1,4 +1,7 @@ Rails.application.configure do + # Prepare the ingress controller used to receive mail + # config.action_mailbox.ingress = :amazon + # Verifies that versions and hashed value of the package contents in the project's package.json config.webpacker.check_yarn_integrity = false # Settings specified here will take precedence over those in config/application.rb. -- cgit v1.2.3 From 0298725f0269cf2fffb723200d21f21ed3a7de64 Mon Sep 17 00:00:00 2001 From: Dino Maric Date: Sun, 16 Dec 2018 14:27:41 +0100 Subject: Added logging when Message ID wasn't extracted --- app/models/action_mailbox/inbound_email/message_id.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb index 4807a2a293..f10f59ea45 100644 --- a/app/models/action_mailbox/inbound_email/message_id.rb +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -25,14 +25,14 @@ module ActionMailbox::InboundEmail::MessageId private def extract_message_id(source) - Mail.from_source(source).message_id - rescue => e - # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead" + Mail.from_source(source).message_id rescue nil end end private def generate_missing_message_id - self.message_id ||= Mail::MessageIdField.new.message_id + self.message_id ||= Mail::MessageIdField.new.message_id.tap do |message_id| + logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}" + end end end -- cgit v1.2.3 From 78a530282ee7b55ba39c76cf5c67d0d239bb5edc Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Wed, 19 Dec 2018 19:01:33 +0100 Subject: [ci skip] Docs: fix spelling, routing setup call and formatting. --- README.md | 2 +- lib/action_mailbox/base.rb | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8e45e1f166..dd513eb8d3 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ end Then setup a mailbox: ```ruby -# Generate new maiblox +# Generate new mailbox bin/rails generate mailbox forwards ``` diff --git a/lib/action_mailbox/base.rb b/lib/action_mailbox/base.rb index b78110d039..bd76c8ff5f 100644 --- a/lib/action_mailbox/base.rb +++ b/lib/action_mailbox/base.rb @@ -11,19 +11,19 @@ require "action_mailbox/routing" # # class ApplicationMailbox < ActionMailbox::Base # # Any of the recipients of the mail (whether to, cc, bcc) are matched against the regexp. -# route /^replies@/i => :replies +# routing /^replies@/i => :replies # # # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string. -# route "help@example.com" => :help +# routing "help@example.com" => :help # # # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true. -# route ->(inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients +# 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. -# route CustomAddress.new => :custom +# routing CustomAddress.new => :custom # # # Any inbound_email that has not been already matched will be sent to the BackstopMailbox. -# route :all => :backstop +# routing :all => :backstop # end # # Application mailboxes need to overwrite the `#process` method, which is invoked by the framework after @@ -31,7 +31,7 @@ require "action_mailbox/routing" # `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, +# 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 @@ -49,7 +49,7 @@ require "action_mailbox/routing" # 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 +# 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`. @@ -57,7 +57,7 @@ require "action_mailbox/routing" # Exceptions can be handled at the class level using the familiar `Rescuable` approach: # # class ForwardsMailbox < ApplicationMailbox -# rescue_from(ApplicationSpecificVerificationError) { bounced! } +# rescue_from(ApplicationSpecificVerificationError) { bounced! } # end class ActionMailbox::Base include ActiveSupport::Rescuable -- cgit v1.2.3 From fb765de0eed1849c883f0a8b6d1a81bb11cd5353 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Wed, 19 Dec 2018 22:51:42 +0100 Subject: Use class_methods throughout codebase. --- app/models/action_mailbox/inbound_email/message_id.rb | 6 +++--- lib/action_mailbox/callbacks.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb index f10f59ea45..244a2e439b 100644 --- a/app/models/action_mailbox/inbound_email/message_id.rb +++ b/app/models/action_mailbox/inbound_email/message_id.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# The `Message-ID` as specified by rfc822 is supposed to be a unique identifier for that individual email. -# That makes it an ideal tracking token for debugging and forensics, just like `X-Request-Id` does for +# The `Message-ID` as specified by rfc822 is supposed to be a unique identifier for that individual email. +# That makes it an ideal tracking token for debugging and forensics, just like `X-Request-Id` does for # web request. # # If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated @@ -13,7 +13,7 @@ module ActionMailbox::InboundEmail::MessageId before_save :generate_missing_message_id end - module ClassMethods + class_methods do # Create a new `InboundEmail` from the raw `source` of the email, which be uploaded as a Active Storage # attachment called `raw_email`. Before the upload, extract the Message-ID from the `source` and set # it as an attribute on the new `InboundEmail`. diff --git a/lib/action_mailbox/callbacks.rb b/lib/action_mailbox/callbacks.rb index c6f07d7ccf..2b7212284b 100644 --- a/lib/action_mailbox/callbacks.rb +++ b/lib/action_mailbox/callbacks.rb @@ -17,7 +17,7 @@ module ActionMailbox define_callbacks :process, terminator: TERMINATOR, skip_after_callbacks_if_terminated: true end - module ClassMethods + class_methods do def before_processing(*methods, &block) set_callback(:process, :before, *methods, &block) end -- cgit v1.2.3 From 13e2268061aa989ed841f7fdff145b8141b0611a Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 19 Dec 2018 19:34:49 -0500 Subject: Remove Ruby 2.4 from the test matrix Rails 6 will require Ruby 2.5 or newer. See rails/rails#34754. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b66561a4ac..f913b006ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: ruby sudo: false rvm: - - 2.4.5 - 2.5.3 - ruby-head -- cgit v1.2.3 From dcddff1d2d0c695318670686a27429a76f20ae03 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 20 Dec 2018 18:16:35 -0500 Subject: Bump the minimum Ruby version to match Rails master --- actionmailbox.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec index 745505ceb9..84307ee7ef 100644 --- a/actionmailbox.gemspec +++ b/actionmailbox.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.homepage = "https://github.com/rails/actionmailbox" s.license = "MIT" - s.required_ruby_version = ">= 2.4.1" + s.required_ruby_version = ">= 2.5.0" s.add_dependency "rails", ">= 5.2.0" -- cgit v1.2.3