mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Import Action Mailbox
This commit is contained in:
commit
a5b2fff64c
162 changed files with 10906 additions and 177 deletions
|
@ -8,6 +8,7 @@ AllCops:
|
|||
- '**/vendor/**/*'
|
||||
- 'actionpack/lib/action_dispatch/journey/parser.rb'
|
||||
- 'railties/test/fixtures/tmp/**/*'
|
||||
- 'actionmailbox/test/dummy/**/*'
|
||||
- 'node_modules/**/*'
|
||||
|
||||
Performance:
|
||||
|
@ -133,6 +134,7 @@ Style/FrozenStringLiteralComment:
|
|||
- 'actionpack/test/**/*.builder'
|
||||
- 'actionpack/test/**/*.ruby'
|
||||
- 'activestorage/db/migrate/**/*.rb'
|
||||
- 'actionmailbox/db/migrate/**/*.rb'
|
||||
|
||||
Style/RedundantFreeze:
|
||||
Enabled: true
|
||||
|
|
42
.travis.yml
42
.travis.yml
|
@ -37,10 +37,10 @@ before_install:
|
|||
- "travis_retry gem update --system"
|
||||
- "travis_retry gem install bundler -v '2.0.0.pre.2'"
|
||||
- "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d"
|
||||
- "[[ $GEM != 'ac:integration' ]] || yarn install"
|
||||
- "[[ $GEM != 'av:ujs' ]] || nvm install node"
|
||||
- "[[ $GEM != 'av:ujs' ]] || node --version"
|
||||
- "[[ $GEM != 'av:ujs' ]] || (cd actionview && npm install)"
|
||||
- "[[ $GEM != 'actioncable:integration' ]] || yarn install"
|
||||
- "[[ $GEM != 'actionview:ujs' ]] || nvm install node"
|
||||
- "[[ $GEM != 'actionview:ujs' ]] || node --version"
|
||||
- "[[ $GEM != 'actionview:ujs' ]] || (cd actionview && npm install)"
|
||||
- "[[ $GEM != 'railties' ]] || (curl -o- -L https://yarnpkg.com/install.sh | bash)"
|
||||
- "[[ $GEM != 'railties' ]] || export PATH=$HOME/.yarn/bin:$PATH"
|
||||
|
||||
|
@ -56,12 +56,12 @@ env:
|
|||
global:
|
||||
- "JRUBY_OPTS='--dev -J-Xmx1024M'"
|
||||
matrix:
|
||||
- "GEM=ap,ac"
|
||||
- "GEM=am,amo,as,av,aj,ast"
|
||||
- "GEM=as PRESERVE_TIMEZONES=1"
|
||||
- "GEM=ar:sqlite3"
|
||||
- "GEM=actionpack,actioncable"
|
||||
- "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox"
|
||||
- "GEM=activesupport PRESERVE_TIMEZONES=1"
|
||||
- "GEM=activerecord:sqlite3"
|
||||
- "GEM=guides"
|
||||
- "GEM=ac:integration"
|
||||
- "GEM=actioncable:integration"
|
||||
|
||||
rvm:
|
||||
- 2.5.3
|
||||
|
@ -88,10 +88,10 @@ matrix:
|
|||
- "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
|
||||
- "sudo service postgresql restart 10"
|
||||
- rvm: 2.5.3
|
||||
env: "GEM=av:ujs"
|
||||
env: "GEM=actionview:ujs"
|
||||
- rvm: 2.5.3
|
||||
sudo: required
|
||||
env: "GEM=aj:integration"
|
||||
env: "GEM=activejob:integration"
|
||||
services:
|
||||
- memcached
|
||||
- redis-server
|
||||
|
@ -104,7 +104,7 @@ matrix:
|
|||
- "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
|
||||
- rvm: ruby-head
|
||||
sudo: required
|
||||
env: "GEM=aj:integration"
|
||||
env: "GEM=activejob:integration"
|
||||
services:
|
||||
- memcached
|
||||
- redis-server
|
||||
|
@ -116,14 +116,14 @@ matrix:
|
|||
- "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
|
||||
- "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
|
||||
- rvm: 2.5.3
|
||||
env: "GEM=ar:mysql2"
|
||||
env: "GEM=activerecord:mysql2"
|
||||
sudo: required
|
||||
before_install:
|
||||
- "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\""
|
||||
- "sudo mysql_upgrade"
|
||||
- "sudo service mysql restart"
|
||||
- rvm: ruby-head
|
||||
env: "GEM=ar:mysql2"
|
||||
env: "GEM=activerecord:mysql2"
|
||||
sudo: required
|
||||
before_install:
|
||||
- "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\""
|
||||
|
@ -131,20 +131,20 @@ matrix:
|
|||
- "sudo service mysql restart"
|
||||
- rvm: 2.5.3
|
||||
env:
|
||||
- "GEM=ar:mysql2 MYSQL=mariadb"
|
||||
- "GEM=activerecord:mysql2 MYSQL=mariadb"
|
||||
addons:
|
||||
mariadb: 10.3
|
||||
- rvm: 2.5.3
|
||||
env:
|
||||
- "GEM=ar:sqlite3_mem"
|
||||
- "GEM=activerecord:sqlite3_mem"
|
||||
- rvm: 2.5.3
|
||||
env: "GEM=ar:postgresql"
|
||||
env: "GEM=activerecord:postgresql"
|
||||
sudo: required
|
||||
before_install:
|
||||
- "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
|
||||
- "sudo service postgresql restart 10"
|
||||
- rvm: ruby-head
|
||||
env: "GEM=ar:postgresql"
|
||||
env: "GEM=activerecord:postgresql"
|
||||
sudo: required
|
||||
before_install:
|
||||
- "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
|
||||
|
@ -152,15 +152,15 @@ matrix:
|
|||
- rvm: jruby-head
|
||||
jdk: oraclejdk8
|
||||
env:
|
||||
- "GEM=ap"
|
||||
- "GEM=actionpack"
|
||||
- rvm: jruby-head
|
||||
jdk: oraclejdk8
|
||||
env:
|
||||
- "GEM=am,amo,aj"
|
||||
- "GEM=actionmailer,activemodel,activejob"
|
||||
allow_failures:
|
||||
- rvm: ruby-head
|
||||
- rvm: jruby-head
|
||||
- env: "GEM=ac:integration"
|
||||
- env: "GEM=actioncable:integration"
|
||||
fast_finish: true
|
||||
|
||||
notifications:
|
||||
|
|
8
Gemfile
8
Gemfile
|
@ -39,7 +39,7 @@ group :doc do
|
|||
gem "kindlerb", "~> 1.2.0"
|
||||
end
|
||||
|
||||
# Active Support.
|
||||
# Active Support
|
||||
gem "dalli"
|
||||
gem "listen", ">= 3.0.5", "< 3.2", require: false
|
||||
gem "libxml-ruby", platforms: :ruby
|
||||
|
@ -48,7 +48,7 @@ gem "connection_pool", require: false
|
|||
# for railties app_generator_test
|
||||
gem "bootsnap", ">= 1.1.0", require: false
|
||||
|
||||
# Active Job.
|
||||
# Active Job
|
||||
group :job do
|
||||
gem "resque", require: false
|
||||
gem "resque-scheduler", require: false
|
||||
|
@ -88,6 +88,10 @@ group :storage do
|
|||
gem "image_processing", "~> 1.2"
|
||||
end
|
||||
|
||||
# Action Mailbox
|
||||
gem "aws-sdk-sns", require: false
|
||||
gem "webmock"
|
||||
|
||||
group :ujs do
|
||||
gem "qunit-selenium"
|
||||
gem "chromedriver-helper"
|
||||
|
|
25
Gemfile.lock
25
Gemfile.lock
|
@ -39,6 +39,13 @@ PATH
|
|||
actionpack (= 6.0.0.alpha)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.0.alpha)
|
||||
actionpack (= 6.0.0.alpha)
|
||||
activejob (= 6.0.0.alpha)
|
||||
activerecord (= 6.0.0.alpha)
|
||||
activestorage (= 6.0.0.alpha)
|
||||
activesupport (= 6.0.0.alpha)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.0.alpha)
|
||||
actionpack (= 6.0.0.alpha)
|
||||
actionview (= 6.0.0.alpha)
|
||||
|
@ -77,6 +84,7 @@ PATH
|
|||
tzinfo (~> 1.1)
|
||||
rails (6.0.0.alpha)
|
||||
actioncable (= 6.0.0.alpha)
|
||||
actionmailbox (= 6.0.0.alpha)
|
||||
actionmailer (= 6.0.0.alpha)
|
||||
actionpack (= 6.0.0.alpha)
|
||||
actionview (= 6.0.0.alpha)
|
||||
|
@ -129,6 +137,9 @@ GEM
|
|||
aws-sdk-core (~> 3, >= 3.26.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.0)
|
||||
aws-sdk-sns (1.8.1)
|
||||
aws-sdk-core (~> 3, >= 3.37.0)
|
||||
aws-sigv4 (~> 1.0)
|
||||
aws-sigv4 (1.0.3)
|
||||
azure-core (0.1.14)
|
||||
faraday (~> 0.9)
|
||||
|
@ -194,6 +205,8 @@ GEM
|
|||
concurrent-ruby (1.1.3)
|
||||
connection_pool (2.2.2)
|
||||
cookiejar (0.3.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.4)
|
||||
curses (1.0.2)
|
||||
daemons (1.2.6)
|
||||
|
@ -269,6 +282,7 @@ GEM
|
|||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
hashdiff (0.3.7)
|
||||
hiredis (0.6.3)
|
||||
hiredis (0.6.3-java)
|
||||
http_parser.rb (0.6.0)
|
||||
|
@ -308,7 +322,7 @@ GEM
|
|||
mime-types (3.2.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2018.0812)
|
||||
mimemagic (0.3.2)
|
||||
mimemagic (0.3.3)
|
||||
mini_magick (4.9.2)
|
||||
mini_mime (1.0.1)
|
||||
mini_portile2 (2.3.0)
|
||||
|
@ -414,6 +428,7 @@ GEM
|
|||
rubyzip (1.2.2)
|
||||
rufus-scheduler (3.5.2)
|
||||
fugit (~> 1.1, >= 1.1.5)
|
||||
safe_yaml (1.0.4)
|
||||
sass (3.7.2)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
|
@ -493,6 +508,10 @@ GEM
|
|||
json (>= 1.8)
|
||||
nokogiri (~> 1.6)
|
||||
wdm (0.1.1)
|
||||
webmock (3.4.2)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
websocket (1.2.8)
|
||||
websocket-driver (0.7.0)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
|
@ -513,6 +532,7 @@ DEPENDENCIES
|
|||
activerecord-jdbcpostgresql-adapter (>= 1.3.0)
|
||||
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
|
||||
aws-sdk-s3
|
||||
aws-sdk-sns
|
||||
azure-storage
|
||||
backburner
|
||||
bcrypt (~> 3.1.11)
|
||||
|
@ -569,8 +589,9 @@ DEPENDENCIES
|
|||
uglifier (>= 1.3.0)
|
||||
w3c_validators
|
||||
wdm (>= 0.1.0)
|
||||
webmock
|
||||
webpacker!
|
||||
websocket-client-simple!
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.1
|
||||
1.17.2
|
||||
|
|
5
actionmailbox/.gitignore
vendored
Normal file
5
actionmailbox/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.byebug_history
|
||||
*.sqlite3-journal
|
||||
.ruby-version
|
||||
.ruby-gemset
|
||||
/tmp/
|
21
actionmailbox/MIT-LICENSE
Normal file
21
actionmailbox/MIT-LICENSE
Normal file
|
@ -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.
|
279
actionmailbox/README.md
Normal file
279
actionmailbox/README.md
Normal file
|
@ -0,0 +1,279 @@
|
|||
# Action Mailbox
|
||||
|
||||
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.
|
||||
|
||||
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 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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## 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/
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
Configure basic routing:
|
||||
|
||||
```ruby
|
||||
# app/mailboxes/application_mailbox.rb
|
||||
class ApplicationMailbox < ActionMailbox::Base
|
||||
routing /^save@/i => :forwards
|
||||
routing /@replies\./i => :replies
|
||||
end
|
||||
```
|
||||
|
||||
Then setup a mailbox:
|
||||
|
||||
```ruby
|
||||
# Generate new mailbox
|
||||
bin/rails generate mailbox forwards
|
||||
```
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Incineration of InboundEmails
|
||||
|
||||
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.)
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
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 <frank@microsoft.com>
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Action Mailbox is released under the [MIT License](https://opensource.org/licenses/MIT).
|
13
actionmailbox/Rakefile
Normal file
13
actionmailbox/Rakefile
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/setup"
|
||||
require "bundler/gem_tasks"
|
||||
require "rake/testtask"
|
||||
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << "test"
|
||||
t.pattern = "test/**/*_test.rb"
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
task default: :test
|
38
actionmailbox/actionmailbox.gemspec
Normal file
38
actionmailbox/actionmailbox.gemspec
Normal file
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.name = "actionmailbox"
|
||||
s.version = version
|
||||
s.summary = "Inbound email handling framework."
|
||||
s.description = "Receive and process incoming emails in Rails applications."
|
||||
|
||||
s.required_ruby_version = ">= 2.5.0"
|
||||
|
||||
s.license = "MIT"
|
||||
|
||||
s.authors = ["David Heinemeier Hansson", "George Claghorn"]
|
||||
s.email = ["david@loudthinking.com", "george@basecamp.com"]
|
||||
s.homepage = "https://rubyonrails.org"
|
||||
|
||||
s.files = Dir["MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"]
|
||||
s.require_path = "lib"
|
||||
|
||||
s.metadata = {
|
||||
"source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionmailbox",
|
||||
"changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionmailbox/CHANGELOG.md"
|
||||
}
|
||||
|
||||
# NOTE: Please read our dependency guidelines before updating versions:
|
||||
# https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
|
||||
|
||||
s.add_dependency "activesupport", version
|
||||
s.add_dependency "activerecord", version
|
||||
s.add_dependency "activestorage", version
|
||||
s.add_dependency "activejob", version
|
||||
s.add_dependency "actionpack", version
|
||||
|
||||
s.add_dependency "mail", ">= 2.7.1"
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# The base class for all Active Mailbox ingress controllers.
|
||||
class ActionMailbox::BaseController < ActionController::Base
|
||||
skip_forgery_protection
|
||||
|
||||
before_action :ensure_configured
|
||||
|
||||
def self.prepare
|
||||
# Override in concrete controllers to run code on load.
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_configured
|
||||
unless ActionMailbox.ingress == ingress_name
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def ingress_name
|
||||
self.class.name.remove(/\AActionMailbox::Ingresses::/, /::InboundEmailsController\z/).underscore.to_sym
|
||||
end
|
||||
|
||||
|
||||
def authenticate_by_password
|
||||
if password.present?
|
||||
http_basic_authenticate_or_request_with name: "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
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
# 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.
|
||||
#
|
||||
# Returns:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SES
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +content+ parameter
|
||||
# - <tt>500 Server Error</tt> 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 <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
||||
# <tt>https://example.com/rails/action_mailbox/amazon/inbound_emails</tt>.
|
||||
class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
private
|
||||
def authenticate
|
||||
head :unauthorized unless verifier.authentic?(request.body)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# 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:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated, or if its timestamp is more than 2 minutes old
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mailgun
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
||||
# - <tt>500 Server Error</tt> 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 <tt>rails credentials:edit</tt> 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 <tt>https://example.com</tt>, you would specify the fully-qualified URL
|
||||
# <tt>https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime</tt>.
|
||||
class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController
|
||||
before_action :authenticate
|
||||
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime")
|
||||
end
|
||||
|
||||
private
|
||||
def authenticate
|
||||
head :unauthorized unless authenticated?
|
||||
end
|
||||
|
||||
def 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
|
||||
|
||||
def key
|
||||
Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) || ENV["MAILGUN_INGRESS_API_KEY"]
|
||||
end
|
||||
|
||||
class Authenticator
|
||||
attr_reader :key, :timestamp, :token, :signature
|
||||
|
||||
def initialize(key:, timestamp:, token:, signature:)
|
||||
@key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature
|
||||
end
|
||||
|
||||
def authenticated?
|
||||
signed? && recent?
|
||||
end
|
||||
|
||||
private
|
||||
def signed?
|
||||
ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
|
||||
end
|
||||
|
||||
# Allow for 2 minutes of drift between Mailgun time and local server time.
|
||||
def recent?
|
||||
Time.at(timestamp) >= 2.minutes.ago
|
||||
end
|
||||
|
||||
def expected_signature
|
||||
OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# 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:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mandrill
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
|
||||
# - <tt>500 Server Error</tt> 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
|
||||
|
||||
def create
|
||||
raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email }
|
||||
head :ok
|
||||
rescue JSON::ParserError => error
|
||||
logger.error error.message
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
def raw_emails
|
||||
events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") }
|
||||
end
|
||||
|
||||
def events
|
||||
JSON.parse params.require(:mandrill_events)
|
||||
end
|
||||
|
||||
|
||||
def authenticate
|
||||
head :unauthorized unless authenticated?
|
||||
end
|
||||
|
||||
def 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
|
||||
|
||||
def key
|
||||
Rails.application.credentials.dig(:action_mailbox, :mandrill_api_key) || ENV["MANDRILL_INGRESS_API_KEY"]
|
||||
end
|
||||
|
||||
class Authenticator
|
||||
attr_reader :request, :key
|
||||
|
||||
def initialize(request, key)
|
||||
@request, @key = request, key
|
||||
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.strict_encode64 OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)
|
||||
end
|
||||
|
||||
def message
|
||||
request.url + request.POST.sort.flatten.join
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# 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:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request could not be authenticated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postfix
|
||||
# - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message
|
||||
# - <tt>500 Server Error</tt> 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 <tt>rails credentials:edit</tt> 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 <tt>bin/rails action_mailbox:ingress:postfix</tt>, providing the +URL+ of the Postfix
|
||||
# ingress and the +INGRESS_PASSWORD+ you previously generated.
|
||||
#
|
||||
# If your application lived at <tt>https://example.com</tt>, 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
|
||||
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read
|
||||
end
|
||||
|
||||
private
|
||||
def require_valid_rfc822_message
|
||||
unless request.content_type == "message/rfc822"
|
||||
head :unsupported_media_type
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
# 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
|
||||
# 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:
|
||||
#
|
||||
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
|
||||
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
||||
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SendGrid
|
||||
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +email+ parameter
|
||||
# - <tt>500 Server Error</tt> 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 <tt>rails credentials:edit</tt> 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 <tt>https://example.com</tt>, 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
|
||||
|
||||
def create
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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, :in_reply_to, :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" }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# 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
|
||||
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
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# 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
|
20
actionmailbox/app/jobs/action_mailbox/incineration_job.rb
Normal file
20
actionmailbox/app/jobs/action_mailbox/incineration_job.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# 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.
|
||||
#
|
||||
# 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] }
|
||||
|
||||
discard_on ActiveRecord::RecordNotFound
|
||||
|
||||
def self.schedule(inbound_email)
|
||||
set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
|
||||
end
|
||||
|
||||
def perform(inbound_email)
|
||||
inbound_email.incinerate
|
||||
end
|
||||
end
|
11
actionmailbox/app/jobs/action_mailbox/routing_job.rb
Normal file
11
actionmailbox/app/jobs/action_mailbox/routing_job.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# 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
|
||||
queue_as { ActionMailbox.queues[:routing] }
|
||||
|
||||
def perform(inbound_email)
|
||||
inbound_email.route
|
||||
end
|
||||
end
|
45
actionmailbox/app/models/action_mailbox/inbound_email.rb
Normal file
45
actionmailbox/app/models/action_mailbox/inbound_email.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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"
|
||||
|
||||
include Incineratable, MessageId, Routable
|
||||
|
||||
has_one_attached :raw_email
|
||||
enum status: %i[ pending processing delivered failed bounced ]
|
||||
|
||||
def mail
|
||||
@mail ||= Mail.from_source(source)
|
||||
end
|
||||
|
||||
def source
|
||||
@source ||= raw_email.download
|
||||
end
|
||||
|
||||
def processed?
|
||||
delivered? || failed? || bounced?
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# 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`.
|
||||
module ActionMailbox::InboundEmail::Incineratable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
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
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# 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,
|
||||
# the `InboundEmail` was processed after the `incinerate_after` time).
|
||||
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.incinerate_after.ago.end_of_day
|
||||
end
|
||||
|
||||
def processed?
|
||||
@inbound_email.processed?
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# 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.
|
||||
#
|
||||
# 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
|
||||
|
||||
included do
|
||||
before_save :generate_missing_message_id
|
||||
end
|
||||
|
||||
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`.
|
||||
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(source)
|
||||
Mail.from_source(source).message_id rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def generate_missing_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
|
|
@ -0,0 +1,24 @@
|
|||
# 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.
|
||||
#
|
||||
# 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
|
||||
|
||||
included do
|
||||
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
|
||||
end
|
7
actionmailbox/app/views/layouts/rails/conductor.html.erb
Normal file
7
actionmailbox/app/views/layouts/rails/conductor.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Rails Conductor: <%= yield :title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
<% provide :title, "Deliver new inbound email" %>
|
||||
|
||||
<h1>All inbound emails</h1>
|
||||
|
||||
<table>
|
||||
<tr><th>Message ID</th><th>Status</th></tr>
|
||||
<% @inbound_emails.each do |inbound_email| %>
|
||||
<tr>
|
||||
<td><%= link_to inbound_email.message_id, main_app.rails_conductor_inbound_email_path(inbound_email) %></td>
|
||||
<td><%= inbound_email.status %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<%= link_to "Deliver new inbound email", main_app.new_rails_conductor_inbound_email_path %>
|
|
@ -0,0 +1,42 @@
|
|||
<% provide :title, "Deliver new inbound email" %>
|
||||
|
||||
<h1>Deliver new inbound email</h1>
|
||||
|
||||
<%= form_with(url: main_app.rails_conductor_inbound_emails_path, scope: :mail, local: true) do |form| %>
|
||||
<div>
|
||||
<%= form.label :from, "From" %><br>
|
||||
<%= form.text_field :from %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :to, "To" %><br>
|
||||
<%= form.text_field :to %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :cc, "CC" %><br>
|
||||
<%= form.text_field :cc %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :bcc, "BCC" %><br>
|
||||
<%= form.text_field :bcc %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :in_reply_to, "In-Reply-To" %><br>
|
||||
<%= form.text_field :in_reply_to %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :subject, "Subject" %><br>
|
||||
<%= form.text_field :subject %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :body, "Body" %><br>
|
||||
<%= form.text_area :body, size: "40x20" %>
|
||||
</div>
|
||||
|
||||
<%= form.submit "Deliver inbound email" %>
|
||||
<% end %>
|
|
@ -0,0 +1,15 @@
|
|||
<% provide :title, @inbound_email.message_id %>
|
||||
|
||||
<h1><%= @inbound_email.message_id %>: <%= @inbound_email.status %></h1>
|
||||
|
||||
<ul>
|
||||
<li><%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %></li>
|
||||
<li>Incinerate</li>
|
||||
</ul>
|
||||
|
||||
<details>
|
||||
<summary>Full email source</summary>
|
||||
<pre><%= @inbound_email.source %></pre>
|
||||
</details>
|
||||
|
||||
<%= link_to "Back to all inbound emails", main_app.rails_conductor_inbound_emails_path %>
|
5
actionmailbox/bin/test
Executable file
5
actionmailbox/bin/test
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
COMPONENT_ROOT = File.expand_path("..", __dir__)
|
||||
require_relative "../../tools/test"
|
19
actionmailbox/config/routes.rb
Normal file
19
actionmailbox/config/routes.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.routes.draw do
|
||||
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 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?
|
||||
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
|
||||
end
|
|
@ -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
|
16
actionmailbox/lib/action_mailbox.rb
Normal file
16
actionmailbox/lib/action_mailbox.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "action_mailbox/mail_ext"
|
||||
|
||||
module ActionMailbox
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :Base
|
||||
autoload :Router
|
||||
autoload :TestCase
|
||||
|
||||
mattr_accessor :ingress
|
||||
mattr_accessor :logger
|
||||
mattr_accessor :incinerate_after, default: 30.days
|
||||
mattr_accessor :queues, default: {}
|
||||
end
|
113
actionmailbox/lib/action_mailbox/base.rb
Normal file
113
actionmailbox/lib/action_mailbox/base.rb
Normal file
|
@ -0,0 +1,113 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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.
|
||||
# routing /^replies@/i => :replies
|
||||
#
|
||||
# # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string.
|
||||
# routing "help@example.com" => :help
|
||||
#
|
||||
# # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true.
|
||||
# routing ->(inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients
|
||||
#
|
||||
# # Any object responding to #match? is called with the inbound_email record as an argument. Match if true.
|
||||
# routing CustomAddress.new => :custom
|
||||
#
|
||||
# # Any inbound_email that has not been already matched will be sent to the BackstopMailbox.
|
||||
# routing :all => :backstop
|
||||
# end
|
||||
#
|
||||
# Application mailboxes need to overwrite the `#process` method, which is invoked by the framework after
|
||||
# callbacks have been run. The callbacks available are: `before_processing`, `after_processing`, and
|
||||
# `around_processing`. The primary use case is ensure certain preconditions to processing are fulfilled
|
||||
# using `before_processing` callbacks.
|
||||
#
|
||||
# If a precondition fails to be met, you can halt the processing using the `#bounced!` method,
|
||||
# which will silently prevent any further processing, but not actually send out any bounce notice. You
|
||||
# can also pair this behavior with the invocation of an Action Mailer class responsible for sending out
|
||||
# an actual bounce email. This is done using the `#bounce_with` method, which takes the mail object returned
|
||||
# by an Action Mailer method, like so:
|
||||
#
|
||||
# class ForwardsMailbox < ApplicationMailbox
|
||||
# before_processing :ensure_sender_is_a_user
|
||||
#
|
||||
# private
|
||||
# def ensure_sender_is_a_user
|
||||
# unless User.exist?(email_address: mail.from)
|
||||
# bounce_with UserRequiredMailer.missing(inbound_email)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# During the processing of the inbound email, the status will be tracked. Before processing begins,
|
||||
# the email will normally have the `pending` status. Once processing begins, just before callbacks
|
||||
# and the `#process` method is called, the status is changed to `processing`. If processing is allowed to
|
||||
# complete, the status is changed to `delivered`. If a bounce is triggered, then `bounced`. If an unhandled
|
||||
# exception is bubbled up, then `failed`.
|
||||
#
|
||||
# Exceptions can be handled at the class level using the familiar `Rescuable` approach:
|
||||
#
|
||||
# class ForwardsMailbox < ApplicationMailbox
|
||||
# rescue_from(ApplicationSpecificVerificationError) { bounced! }
|
||||
# end
|
||||
class ActionMailbox::Base
|
||||
include ActiveSupport::Rescuable
|
||||
include ActionMailbox::Callbacks, ActionMailbox::Routing
|
||||
|
||||
attr_reader :inbound_email
|
||||
delegate :mail, :delivered!, :bounced!, to: :inbound_email
|
||||
|
||||
delegate :logger, to: ActionMailbox
|
||||
|
||||
def self.receive(inbound_email)
|
||||
new(inbound_email).perform_processing
|
||||
end
|
||||
|
||||
def initialize(inbound_email)
|
||||
@inbound_email = inbound_email
|
||||
end
|
||||
|
||||
def perform_processing
|
||||
track_status_of_inbound_email do
|
||||
run_callbacks :process do
|
||||
process
|
||||
end
|
||||
end
|
||||
rescue => exception
|
||||
# TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier
|
||||
rescue_with_handler(exception) || raise
|
||||
end
|
||||
|
||||
def process
|
||||
# Overwrite in subclasses
|
||||
end
|
||||
|
||||
def finished_processing?
|
||||
inbound_email.delivered? || inbound_email.bounced?
|
||||
end
|
||||
|
||||
|
||||
def bounce_with(message)
|
||||
inbound_email.bounced!
|
||||
message.deliver_later
|
||||
end
|
||||
|
||||
private
|
||||
def track_status_of_inbound_email
|
||||
inbound_email.processing!
|
||||
yield
|
||||
inbound_email.delivered! unless inbound_email.bounced?
|
||||
rescue
|
||||
inbound_email.failed!
|
||||
raise
|
||||
end
|
||||
end
|
34
actionmailbox/lib/action_mailbox/callbacks.rb
Normal file
34
actionmailbox/lib/action_mailbox/callbacks.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/callbacks"
|
||||
|
||||
module ActionMailbox
|
||||
# Defines the callbacks related to processing.
|
||||
module Callbacks
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveSupport::Callbacks
|
||||
|
||||
TERMINATOR = ->(mailbox, chain) do
|
||||
chain.call
|
||||
mailbox.finished_processing?
|
||||
end
|
||||
|
||||
included do
|
||||
define_callbacks :process, terminator: TERMINATOR, skip_after_callbacks_if_terminated: true
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def before_processing(*methods, &block)
|
||||
set_callback(:process, :before, *methods, &block)
|
||||
end
|
||||
|
||||
def after_processing(*methods, &block)
|
||||
set_callback(:process, :after, *methods, &block)
|
||||
end
|
||||
|
||||
def around_processing(*methods, &block)
|
||||
set_callback(:process, :around, *methods, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
37
actionmailbox/lib/action_mailbox/engine.rb
Normal file
37
actionmailbox/lib/action_mailbox/engine.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails/engine"
|
||||
require "action_mailbox"
|
||||
|
||||
module ActionMailbox
|
||||
class Engine < Rails::Engine
|
||||
isolate_namespace ActionMailbox
|
||||
config.eager_load_namespaces << ActionMailbox
|
||||
|
||||
config.action_mailbox = ActiveSupport::OrderedOptions.new
|
||||
config.action_mailbox.incinerate_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
|
||||
|
||||
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
|
17
actionmailbox/lib/action_mailbox/gem_version.rb
Normal file
17
actionmailbox/lib/action_mailbox/gem_version.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActionMailbox
|
||||
# Returns the currently-loaded version of Action Mailbox as a <tt>Gem::Version</tt>.
|
||||
def self.gem_version
|
||||
Gem::Version.new VERSION::STRING
|
||||
end
|
||||
|
||||
module VERSION
|
||||
MAJOR = 6
|
||||
MINOR = 0
|
||||
TINY = 0
|
||||
PRE = "alpha"
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
||||
end
|
||||
end
|
6
actionmailbox/lib/action_mailbox/mail_ext.rb
Normal file
6
actionmailbox/lib/action_mailbox/mail_ext.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "mail"
|
||||
|
||||
# The hope is to upstream most of these basic additions to the Mail gem's Mail object. But until then, here they lay!
|
||||
Dir["#{File.expand_path(File.dirname(__FILE__))}/mail_ext/*"].each { |path| require "action_mailbox/mail_ext/#{File.basename(path)}" }
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mail::Address
|
||||
def ==(other_address)
|
||||
other_address.is_a?(Mail::Address) && to_s == other_address.to_s
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mail::Address
|
||||
def self.wrap(address)
|
||||
address.is_a?(Mail::Address) ? address : Mail::Address.new(address)
|
||||
end
|
||||
end
|
27
actionmailbox/lib/action_mailbox/mail_ext/addresses.rb
Normal file
27
actionmailbox/lib/action_mailbox/mail_ext/addresses.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mail::Message
|
||||
def from_address
|
||||
header[:from]&.address_list&.addresses&.first
|
||||
end
|
||||
|
||||
def recipients_addresses
|
||||
to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses
|
||||
end
|
||||
|
||||
def to_addresses
|
||||
Array(header[:to]&.address_list&.addresses)
|
||||
end
|
||||
|
||||
def cc_addresses
|
||||
Array(header[:cc]&.address_list&.addresses)
|
||||
end
|
||||
|
||||
def bcc_addresses
|
||||
Array(header[:bcc]&.address_list&.addresses)
|
||||
end
|
||||
|
||||
def x_original_to_addresses
|
||||
Array(header[:x_original_to]).collect { |header| Mail::Address.new header.to_s }
|
||||
end
|
||||
end
|
7
actionmailbox/lib/action_mailbox/mail_ext/from_source.rb
Normal file
7
actionmailbox/lib/action_mailbox/mail_ext/from_source.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mail
|
||||
def self.from_source(source)
|
||||
Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s)
|
||||
end
|
||||
end
|
7
actionmailbox/lib/action_mailbox/mail_ext/recipients.rb
Normal file
7
actionmailbox/lib/action_mailbox/mail_ext/recipients.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mail::Message
|
||||
def recipients
|
||||
Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s)
|
||||
end
|
||||
end
|
67
actionmailbox/lib/action_mailbox/postfix_relayer.rb
Normal file
67
actionmailbox/lib/action_mailbox/postfix_relayer.rb
Normal file
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "action_mailbox/version"
|
||||
require "net/http"
|
||||
require "uri"
|
||||
|
||||
module ActionMailbox
|
||||
class PostfixRelayer
|
||||
class Result < Struct.new(:output)
|
||||
def success?
|
||||
!failure?
|
||||
end
|
||||
|
||||
def failure?
|
||||
output.match?(/\A[45]\.\d{1,3}\.\d{1,3}(\s|\z)/)
|
||||
end
|
||||
end
|
||||
|
||||
CONTENT_TYPE = "message/rfc822"
|
||||
USER_AGENT = "Action Mailbox Postfix relayer v#{ActionMailbox.version}"
|
||||
|
||||
attr_reader :uri, :username, :password
|
||||
|
||||
def initialize(url:, username: "actionmailbox", password:)
|
||||
@uri, @username, @password = URI(url), username, password
|
||||
end
|
||||
|
||||
def relay(source)
|
||||
case response = post(source)
|
||||
when Net::HTTPSuccess
|
||||
Result.new "2.0.0 Successfully relayed message to 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, source,
|
||||
"Content-Type" => CONTENT_TYPE,
|
||||
"User-Agent" => USER_AGENT,
|
||||
"Authorization" => "Basic #{Base64.strict_encode64(username + ":" + password)}"
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Net::HTTP.new(uri.host, uri.port).tap do |connection|
|
||||
if uri.scheme == "https"
|
||||
require "openssl"
|
||||
|
||||
connection.use_ssl = true
|
||||
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||||
end
|
||||
|
||||
connection.open_timeout = 1
|
||||
connection.read_timeout = 10
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
40
actionmailbox/lib/action_mailbox/router.rb
Normal file
40
actionmailbox/lib/action_mailbox/router.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
# 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
|
||||
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"
|
40
actionmailbox/lib/action_mailbox/router/route.rb
Normal file
40
actionmailbox/lib/action_mailbox/router/route.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
# 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.
|
||||
class ActionMailbox::Router::Route
|
||||
attr_reader :address, :mailbox_name
|
||||
|
||||
def initialize(address, to:)
|
||||
@address, @mailbox_name = address, to
|
||||
|
||||
ensure_valid_address
|
||||
end
|
||||
|
||||
def match?(inbound_email)
|
||||
case address
|
||||
when :all
|
||||
true
|
||||
when String
|
||||
inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) }
|
||||
when Regexp
|
||||
inbound_email.mail.recipients.any? { |recipient| address.match?(recipient) }
|
||||
when Proc
|
||||
address.call(inbound_email)
|
||||
else
|
||||
address.match?(inbound_email)
|
||||
end
|
||||
end
|
||||
|
||||
def mailbox_class
|
||||
"#{mailbox_name.to_s.camelize}Mailbox".constantize
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_valid_address
|
||||
unless [ Symbol, String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?)
|
||||
raise ArgumentError, "Expected a Symbol, String, Regexp, Proc, or matchable, got #{address.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
22
actionmailbox/lib/action_mailbox/routing.rb
Normal file
22
actionmailbox/lib/action_mailbox/routing.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActionMailbox
|
||||
# See `ActionMailbox::Base` for how to specify routing.
|
||||
module Routing
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
cattr_accessor :router, default: ActionMailbox::Router.new
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def routing(routes)
|
||||
router.add_routes(routes)
|
||||
end
|
||||
|
||||
def route(inbound_email)
|
||||
router.route(inbound_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
10
actionmailbox/lib/action_mailbox/test_case.rb
Normal file
10
actionmailbox/lib/action_mailbox/test_case.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "action_mailbox/test_helper"
|
||||
require "active_support/test_case"
|
||||
|
||||
module ActionMailbox
|
||||
class TestCase < ActiveSupport::TestCase
|
||||
include ActionMailbox::TestHelper
|
||||
end
|
||||
end
|
44
actionmailbox/lib/action_mailbox/test_helper.rb
Normal file
44
actionmailbox/lib/action_mailbox/test_helper.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "mail"
|
||||
|
||||
module ActionMailbox
|
||||
module TestHelper
|
||||
# Create an `InboundEmail` record using an eml fixture in the format of message/rfc822
|
||||
# referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
|
||||
def create_inbound_email_from_fixture(fixture_name, status: :processing)
|
||||
create_inbound_email_from_source file_fixture(fixture_name).read, status: status
|
||||
end
|
||||
|
||||
# Create an `InboundEmail` by specifying it using `Mail.new` options. Example:
|
||||
#
|
||||
# create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!")
|
||||
def create_inbound_email_from_mail(status: :processing, **mail_options)
|
||||
create_inbound_email_from_source Mail.new(mail_options).to_s, status: status
|
||||
end
|
||||
|
||||
# Create an `InboundEmail` using the raw rfc822 `source` as text.
|
||||
def create_inbound_email_from_source(source, status: :processing)
|
||||
ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
|
||||
end
|
||||
|
||||
|
||||
# Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_fixture`
|
||||
# and immediately route it to processing.
|
||||
def receive_inbound_email_from_fixture(*args)
|
||||
create_inbound_email_from_fixture(*args).tap(&:route)
|
||||
end
|
||||
|
||||
# Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_mail`
|
||||
# and immediately route it to processing.
|
||||
def receive_inbound_email_from_mail(**kwargs)
|
||||
create_inbound_email_from_mail(**kwargs).tap(&:route)
|
||||
end
|
||||
|
||||
# Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_source`
|
||||
# and immediately route it to processing.
|
||||
def receive_inbound_email_from_source(**kwargs)
|
||||
create_inbound_email_from_source(**kwargs).tap(&:route)
|
||||
end
|
||||
end
|
||||
end
|
10
actionmailbox/lib/action_mailbox/version.rb
Normal file
10
actionmailbox/lib/action_mailbox/version.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "gem_version"
|
||||
|
||||
module ActionMailbox
|
||||
# Returns the currently-loaded version of Action Mailbox as a <tt>Gem::Version</tt>.
|
||||
def self.version
|
||||
gem_version
|
||||
end
|
||||
end
|
10
actionmailbox/lib/rails/generators/installer.rb
Normal file
10
actionmailbox/lib/rails/generators/installer.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
say "Copying application_mailbox.rb to app/mailboxes"
|
||||
copy_file "#{__dir__}/mailbox/templates/application_mailbox.rb", "app/mailboxes/application_mailbox.rb"
|
||||
|
||||
environment <<~end_of_config, env: "production"
|
||||
# Prepare the ingress controller used to receive mail
|
||||
# config.action_mailbox.ingress = :amazon
|
||||
|
||||
end_of_config
|
12
actionmailbox/lib/rails/generators/mailbox/USAGE
Normal file
12
actionmailbox/lib/rails/generators/mailbox/USAGE
Normal file
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationMailbox < ActionMailbox::Base
|
||||
# routing /something/i => :somewhere
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class <%= class_name %>Mailbox < ApplicationMailbox
|
||||
def process
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class <%= class_name %>MailboxTest < ActionMailbox::TestCase
|
||||
# test "receive mail" do
|
||||
# receive_inbound_email_from_mail \
|
||||
# to: '"someone" <someone@example.com>,
|
||||
# from: '"else" <else@example.com>',
|
||||
# subject: "Hello world!",
|
||||
# body: "Hello?"
|
||||
# end
|
||||
end
|
24
actionmailbox/lib/tasks/ingress.rake
Normal file
24
actionmailbox/lib/tasks/ingress.rake
Normal file
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
namespace :action_mailbox do
|
||||
namespace :ingress do
|
||||
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 "action_mailbox/postfix_relayer"
|
||||
|
||||
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).relay(STDIN.read).tap do |result|
|
||||
print result.output
|
||||
exit result.success?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
20
actionmailbox/lib/tasks/install.rake
Normal file
20
actionmailbox/lib/tasks/install.rake
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
namespace :action_mailbox do
|
||||
# Prevent migration installation task from showing up twice.
|
||||
Rake::Task["install:migrations"].clear_comments
|
||||
|
||||
desc "Copy over the migration"
|
||||
task install: %w[ environment run_installer copy_migrations ]
|
||||
|
||||
task :run_installer do
|
||||
installer_template = File.expand_path("../rails/generators/installer.rb", __dir__)
|
||||
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{installer_template}"
|
||||
end
|
||||
|
||||
task :copy_migrations do
|
||||
Rake::Task["active_storage:install:migrations"].invoke
|
||||
Rake::Task["railties:install:migrations"].reenable # Otherwise you can't run 2 migration copy tasks in one invocation
|
||||
Rake::Task["action_mailbox:install:migrations"].invoke
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
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
|
||||
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
|
|
@ -0,0 +1,91 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
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"
|
||||
post rails_mailgun_inbound_emails_url, params: {
|
||||
timestamp: 1539112500,
|
||||
token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
|
||||
signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
|
||||
"body-mime" => 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 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
|
||||
|
||||
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
|
||||
def switch_key_to(new_key)
|
||||
previous_key, ENV["MAILGUN_INGRESS_API_KEY"] = ENV["MAILGUN_INGRESS_API_KEY"], new_key
|
||||
yield
|
||||
ensure
|
||||
ENV["MAILGUN_INGRESS_API_KEY"] = previous_key
|
||||
end
|
||||
end
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
def switch_key_to(new_key)
|
||||
previous_key, ENV["MANDRILL_INGRESS_API_KEY"] = ENV["MANDRILL_INGRESS_API_KEY"], new_key
|
||||
yield
|
||||
ensure
|
||||
ENV["MANDRILL_INGRESS_API_KEY"] = previous_key
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
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" },
|
||||
params: 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 Postfix" do
|
||||
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
|
||||
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
|
||||
|
||||
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
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
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,
|
||||
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
|
||||
|
||||
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
|
||||
end
|
18
actionmailbox/test/dummy/.babelrc
Normal file
18
actionmailbox/test/dummy/.babelrc
Normal file
|
@ -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 }]
|
||||
]
|
||||
}
|
3
actionmailbox/test/dummy/.gitignore
vendored
Normal file
3
actionmailbox/test/dummy/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
*.log
|
||||
*.sqlite3
|
||||
tmp/*
|
3
actionmailbox/test/dummy/.postcssrc.yml
Normal file
3
actionmailbox/test/dummy/.postcssrc.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
plugins:
|
||||
postcss-import: {}
|
||||
postcss-cssnext: {}
|
6
actionmailbox/test/dummy/Rakefile
Normal file
6
actionmailbox/test/dummy/Rakefile
Normal file
|
@ -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
|
3
actionmailbox/test/dummy/app/assets/config/manifest.js
Normal file
3
actionmailbox/test/dummy/app/assets/config/manifest.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
//= link_tree ../images
|
||||
//= link_directory ../javascripts .js
|
||||
//= link_directory ../stylesheets .css
|
0
actionmailbox/test/dummy/app/assets/images/.keep
Normal file
0
actionmailbox/test/dummy/app/assets/images/.keep
Normal file
|
@ -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
|
||||
*/
|
80
actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css
Normal file
80
actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css
Normal file
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
end
|
0
actionmailbox/test/dummy/app/controllers/concerns/.keep
Normal file
0
actionmailbox/test/dummy/app/controllers/concerns/.keep
Normal file
|
@ -0,0 +1,2 @@
|
|||
module ApplicationHelper
|
||||
end
|
2
actionmailbox/test/dummy/app/jobs/application_job.rb
Normal file
2
actionmailbox/test/dummy/app/jobs/application_job.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class ApplicationJob < ActiveJob::Base
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationMailbox < ActionMailbox::Base
|
||||
# routing /something/i => :somewhere
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
class MessagesMailbox < ApplicationMailbox
|
||||
def process
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: 'from@example.com'
|
||||
layout 'mailer'
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
0
actionmailbox/test/dummy/app/models/concerns/.keep
Normal file
0
actionmailbox/test/dummy/app/models/concerns/.keep
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Dummy</title>
|
||||
<%= csrf_meta_tags %>
|
||||
|
||||
<%= stylesheet_link_tag 'application', media: 'all' %>
|
||||
<%= javascript_pack_tag 'application' %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
13
actionmailbox/test/dummy/app/views/layouts/mailer.html.erb
Normal file
13
actionmailbox/test/dummy/app/views/layouts/mailer.html.erb
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<style>
|
||||
/* Email styles need to be inline */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
<%= yield %>
|
3
actionmailbox/test/dummy/bin/bundle
Executable file
3
actionmailbox/test/dummy/bin/bundle
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env ruby
|
||||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
||||
load Gem.bin_path('bundler', 'bundle')
|
4
actionmailbox/test/dummy/bin/rails
Executable file
4
actionmailbox/test/dummy/bin/rails
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
APP_PATH = File.expand_path('../config/application', __dir__)
|
||||
require_relative '../config/boot'
|
||||
require 'rails/commands'
|
4
actionmailbox/test/dummy/bin/rake
Executable file
4
actionmailbox/test/dummy/bin/rake
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
require_relative '../config/boot'
|
||||
require 'rake'
|
||||
Rake.application.run
|
36
actionmailbox/test/dummy/bin/setup
Executable file
36
actionmailbox/test/dummy/bin/setup
Executable file
|
@ -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
|
31
actionmailbox/test/dummy/bin/update
Executable file
31
actionmailbox/test/dummy/bin/update
Executable file
|
@ -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
|
11
actionmailbox/test/dummy/bin/yarn
Executable file
11
actionmailbox/test/dummy/bin/yarn
Executable file
|
@ -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
|
5
actionmailbox/test/dummy/config.ru
Normal file
5
actionmailbox/test/dummy/config.ru
Normal file
|
@ -0,0 +1,5 @@
|
|||
# This file is used by Rack-based servers to start the application.
|
||||
|
||||
require_relative 'config/environment'
|
||||
|
||||
run Rails.application
|
17
actionmailbox/test/dummy/config/application.rb
Normal file
17
actionmailbox/test/dummy/config/application.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
require_relative 'boot'
|
||||
|
||||
require 'rails/all'
|
||||
|
||||
Bundler.require(*Rails.groups)
|
||||
|
||||
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
|
5
actionmailbox/test/dummy/config/boot.rb
Normal file
5
actionmailbox/test/dummy/config/boot.rb
Normal file
|
@ -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__)
|
10
actionmailbox/test/dummy/config/cable.yml
Normal file
10
actionmailbox/test/dummy/config/cable.yml
Normal file
|
@ -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
|
25
actionmailbox/test/dummy/config/database.yml
Normal file
25
actionmailbox/test/dummy/config/database.yml
Normal file
|
@ -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
|
5
actionmailbox/test/dummy/config/environment.rb
Normal file
5
actionmailbox/test/dummy/config/environment.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Load the Rails application.
|
||||
require_relative 'application'
|
||||
|
||||
# Initialize the Rails application.
|
||||
Rails.application.initialize!
|
63
actionmailbox/test/dummy/config/environments/development.rb
Normal file
63
actionmailbox/test/dummy/config/environments/development.rb
Normal file
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue