├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app └── controllers │ └── action_mailbox │ └── ingresses │ └── mailpace │ └── inbound_emails_controller.rb ├── bin └── test ├── config └── routes.rb ├── lib ├── mailpace-rails.rb └── mailpace-rails │ ├── engine.rb │ └── version.rb ├── mailpace-rails.gemspec └── test ├── dummy ├── .ruby-version ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── javascript │ │ └── packs │ │ │ └── application.js │ ├── jobs │ │ └── application_job.rb │ ├── mailboxes │ │ └── application_mailbox.rb │ ├── mailers │ │ ├── application_mailer.rb │ │ ├── complex_mailer.rb │ │ ├── full_name_mailer.rb │ │ ├── idempotent_mailer.rb │ │ ├── list_unsubscribe_mailer.rb │ │ ├── plaintext_mailer.rb │ │ ├── tag_mailer.rb │ │ └── test_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ └── concerns │ │ │ └── .keep │ └── views │ │ ├── complex_mailer │ │ ├── complex_email.html.erb │ │ └── complex_email.text.erb │ │ ├── full_name_mailer │ │ ├── full_name_email.html.erb │ │ └── full_name_email.text.erb │ │ ├── idempotent_mailer │ │ ├── idempotent_email.html.erb │ │ └── idempotent_email.text.erb │ │ ├── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb │ │ ├── list_unsubscribe_mailer │ │ ├── unsubscribe.html.erb │ │ └── unsubscribe.text.erb │ │ ├── plaintext_mailer │ │ └── plain_only_email.text.erb │ │ ├── tag_mailer │ │ ├── multi_tag.text.erb │ │ └── single_tag.html.erb │ │ └── test_mailer │ │ ├── welcome_email.html.erb │ │ └── welcome_email.text.erb ├── bin │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── credentials │ │ ├── test.key │ │ └── test.yml.enc │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── spring.rb │ └── storage.yml ├── db │ ├── migrate │ │ ├── 20211222122019_create_active_storage_tables.active_storage.rb │ │ └── 20211222122020_create_action_mailbox_tables.action_mailbox.rb │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ └── favicon.ico └── test │ └── mailers │ ├── previews │ └── test_mailer_preview.rb │ └── test_mailer_test.rb ├── logo.png ├── mailpace ├── email_helper.rb ├── ingress_test.rb └── mailer_test.rb └── test_helper.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 # Use 2.1 to enable using orbs and other features. 2 | 3 | # Declare the orbs that we'll use in our config. 4 | # read more about orbs: https://circleci.com/docs/2.0/using-orbs/ 5 | orbs: 6 | ruby: circleci/ruby@1.0 7 | 8 | jobs: 9 | 'rails-6-1': 10 | docker: 11 | - image: cimg/ruby:2.7-node # this is our primary docker image, where step commands run. 12 | # environment variables specific to Ruby/Rails, applied to the primary container. 13 | environment: 14 | BUNDLE_JOBS: "3" 15 | BUNDLE_RETRY: "3" 16 | RAILS_ENV: test 17 | RAILS_TEST_VERSION: "6.1.4.4" 18 | # A series of steps to run, some are similar to those in "build". 19 | steps: 20 | - checkout 21 | - run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev 22 | - run: bundle update && bundle install 23 | - ruby/install-deps: 24 | with-cache: false 25 | - run: bundle show 26 | - run: bin/test 27 | 28 | 'rails-7-0': 29 | docker: 30 | - image: cimg/ruby:2.7-node # this is our primary docker image, where step commands run. 31 | # environment variables specific to Ruby/Rails, applied to the primary container. 32 | environment: 33 | BUNDLE_JOBS: "3" 34 | BUNDLE_RETRY: "3" 35 | RAILS_ENV: test 36 | RAILS_TEST_VERSION: "7.0.8.7" 37 | # A series of steps to run, some are similar to those in "build". 38 | steps: 39 | - checkout 40 | - run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev 41 | - run: bundle update && bundle install 42 | - ruby/install-deps: 43 | with-cache: false 44 | - run: bundle show 45 | - run: bin/test 46 | 47 | 48 | 'rails-7-2': 49 | docker: 50 | - image: cimg/ruby:3.2.5-node # this is our primary docker image, where step commands run. 51 | # environment variables specific to Ruby/Rails, applied to the primary container. 52 | environment: 53 | BUNDLE_JOBS: "3" 54 | BUNDLE_RETRY: "3" 55 | RAILS_ENV: test 56 | RAILS_TEST_VERSION: "7.2.1" 57 | # A series of steps to run, some are similar to those in "build". 58 | steps: 59 | - checkout 60 | - run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev 61 | - run: bundle update && bundle install 62 | - ruby/install-deps: 63 | with-cache: false 64 | - run: bundle show 65 | - run: bin/test 66 | 67 | 'rails-8-0': 68 | docker: 69 | - image: cimg/ruby:3.4.4-node # this is our primary docker image, where step commands run. 70 | # environment variables specific to Ruby/Rails, applied to the primary container. 71 | environment: 72 | BUNDLE_JOBS: "3" 73 | BUNDLE_RETRY: "3" 74 | RAILS_ENV: test 75 | RAILS_TEST_VERSION: "8.0.2" 76 | # A series of steps to run, some are similar to those in "build". 77 | steps: 78 | - checkout 79 | - run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev 80 | - run: bundle update && bundle install 81 | - ruby/install-deps: 82 | with-cache: false 83 | - run: bundle show 84 | - run: bin/test 85 | 86 | # We use workflows to orchestrate the jobs that we declared above. 87 | workflows: 88 | version: 2 89 | build_and_test: # The name of our workflow is "build_and_test" 90 | jobs: # The list of jobs we run as part of this workflow. 91 | - 'rails-6-1' 92 | - 'rails-7-0' 93 | - 'rails-7-2' 94 | - 'rails-8-0' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/db/*.sqlite3-* 7 | test/dummy/log/*.log 8 | test/dummy/storage/ 9 | test/dummy/tmp/ 10 | *.gem 11 | *DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.2 2 | 3 | - Send text-only if not multipart and content-type is text/plain (thanks [@zachasme](https://github.com/zachasme)) 4 | - Support full names in To / Reply To addresses (thanks [@lazyatom](https://github.com/lazyatom)) 5 | - Support multiple reply to addresses (thanks [@lazyatom](https://github.com/lazyatom)) 6 | - CI now tests against latest versions of Rails (8.0.2) and Ruby (3.4.4) 7 | 8 | # 0.4.1 9 | 10 | - Fix bug where Idempotent Key is set to '', when not set 11 | 12 | # 0.4.0 13 | 14 | - Add support for `InReplyTo` and `References` headers 15 | - Now raises a `Mailpace::DeliveryError` on failure (thanks to [@dpaluy](https://github.com/dpaluy)) 16 | - Idempotent Requests (https://docs.mailpace.com/guide/idempotency/) 17 | - Now tested against Rails 7.2, 8.0.0 and Ruby 3.2 18 | - Declares a dependency on actionmailbox and activestorage (resolves https://github.com/mailpace/mailpace-rails/issues/27) 19 | 20 | # 0.3.2 21 | 22 | - Support for Rails 7.0.4.1, which uses Mail 2.8 under the hood 23 | - Support full email address names (e.g. Name `` in Bcc and Cc fields) 24 | - Dependency upgrade 25 | 26 | # 0.3.1 27 | 28 | - Fix bug with ReplyTo attribute 29 | 30 | # 0.3.0 31 | 32 | - Dropped support for applications older than Rails 6 33 | - Added ActionMailbox support for Inbound Emails 34 | - Gem is now tested against a Rails 7 app by default 35 | 36 | # 0.2.0 37 | 38 | - List_Unsubscribe support 39 | - Deliver function now returns API response 40 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Declare your gem's dependencies in mailpace-rails.gemspec. 5 | # Bundler will treat runtime dependencies like base dependencies, and 6 | # development dependencies will be added by default to the :development group. 7 | gemspec 8 | 9 | # Declare any dependencies that are still in development here instead of in 10 | # your gemspec. These might include edge Rails or gems from your path or 11 | # Git. Remember to move these dependencies to your gemspec before releasing 12 | # your gem to rubygems.org. 13 | 14 | # To use a debugger 15 | # gem 'byebug', group: [:development, :test] 16 | 17 | group :test do 18 | # Fix concurrent-ruby removing logger dependency in Rails 6 & 7.0 19 | gem 'concurrent-ruby', '<1.3.5' if ENV['RAILS_TEST_VERSION']&.start_with?('7.') || ENV['RAILS_TEST_VERSION']&.start_with?('6.') 20 | gem 'sprockets-rails' 21 | gem 'sqlite3', '~> 1.4'if ENV['RAILS_TEST_VERSION']&.start_with?('7.') || ENV['RAILS_TEST_VERSION']&.start_with?('6.') 22 | gem 'webmock' 23 | end 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | mailpace-rails (0.4.2) 5 | actionmailbox (>= 6.0.0) 6 | actionmailer (>= 6.0.0) 7 | activestorage (>= 6.0.0) 8 | httparty (>= 0.18.1) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | actioncable (7.2.1) 14 | actionpack (= 7.2.1) 15 | activesupport (= 7.2.1) 16 | nio4r (~> 2.0) 17 | websocket-driver (>= 0.6.1) 18 | zeitwerk (~> 2.6) 19 | actionmailbox (7.2.1) 20 | actionpack (= 7.2.1) 21 | activejob (= 7.2.1) 22 | activerecord (= 7.2.1) 23 | activestorage (= 7.2.1) 24 | activesupport (= 7.2.1) 25 | mail (>= 2.8.0) 26 | actionmailer (7.2.1) 27 | actionpack (= 7.2.1) 28 | actionview (= 7.2.1) 29 | activejob (= 7.2.1) 30 | activesupport (= 7.2.1) 31 | mail (>= 2.8.0) 32 | rails-dom-testing (~> 2.2) 33 | actionpack (7.2.1) 34 | actionview (= 7.2.1) 35 | activesupport (= 7.2.1) 36 | nokogiri (>= 1.8.5) 37 | racc 38 | rack (>= 2.2.4, < 3.2) 39 | rack-session (>= 1.0.1) 40 | rack-test (>= 0.6.3) 41 | rails-dom-testing (~> 2.2) 42 | rails-html-sanitizer (~> 1.6) 43 | useragent (~> 0.16) 44 | actiontext (7.2.1) 45 | actionpack (= 7.2.1) 46 | activerecord (= 7.2.1) 47 | activestorage (= 7.2.1) 48 | activesupport (= 7.2.1) 49 | globalid (>= 0.6.0) 50 | nokogiri (>= 1.8.5) 51 | actionview (7.2.1) 52 | activesupport (= 7.2.1) 53 | builder (~> 3.1) 54 | erubi (~> 1.11) 55 | rails-dom-testing (~> 2.2) 56 | rails-html-sanitizer (~> 1.6) 57 | activejob (7.2.1) 58 | activesupport (= 7.2.1) 59 | globalid (>= 0.3.6) 60 | activemodel (7.2.1) 61 | activesupport (= 7.2.1) 62 | activerecord (7.2.1) 63 | activemodel (= 7.2.1) 64 | activesupport (= 7.2.1) 65 | timeout (>= 0.4.0) 66 | activestorage (7.2.1) 67 | actionpack (= 7.2.1) 68 | activejob (= 7.2.1) 69 | activerecord (= 7.2.1) 70 | activesupport (= 7.2.1) 71 | marcel (~> 1.0) 72 | activesupport (7.2.1) 73 | base64 74 | bigdecimal 75 | concurrent-ruby (~> 1.0, >= 1.3.1) 76 | connection_pool (>= 2.2.5) 77 | drb 78 | i18n (>= 1.6, < 2) 79 | logger (>= 1.4.2) 80 | minitest (>= 5.1) 81 | securerandom (>= 0.3) 82 | tzinfo (~> 2.0, >= 2.0.5) 83 | addressable (2.8.7) 84 | public_suffix (>= 2.0.2, < 7.0) 85 | base64 (0.2.0) 86 | bigdecimal (3.1.8) 87 | builder (3.3.0) 88 | concurrent-ruby (1.3.4) 89 | connection_pool (2.4.1) 90 | crack (1.0.0) 91 | bigdecimal 92 | rexml 93 | crass (1.0.6) 94 | csv (3.3.0) 95 | date (3.3.4) 96 | drb (2.2.1) 97 | erubi (1.13.0) 98 | globalid (1.2.1) 99 | activesupport (>= 6.1) 100 | hashdiff (1.1.1) 101 | httparty (0.22.0) 102 | csv 103 | mini_mime (>= 1.0.0) 104 | multi_xml (>= 0.5.2) 105 | i18n (1.14.6) 106 | concurrent-ruby (~> 1.0) 107 | io-console (0.7.2) 108 | irb (1.14.1) 109 | rdoc (>= 4.0.0) 110 | reline (>= 0.4.2) 111 | logger (1.6.1) 112 | loofah (2.22.0) 113 | crass (~> 1.0.2) 114 | nokogiri (>= 1.12.0) 115 | mail (2.8.1) 116 | mini_mime (>= 0.1.1) 117 | net-imap 118 | net-pop 119 | net-smtp 120 | marcel (1.0.4) 121 | mini_mime (1.1.5) 122 | minitest (5.25.1) 123 | multi_xml (0.7.1) 124 | bigdecimal (~> 3.1) 125 | net-imap (0.4.17) 126 | date 127 | net-protocol 128 | net-pop (0.1.2) 129 | net-protocol 130 | net-protocol (0.2.2) 131 | timeout 132 | net-smtp (0.5.0) 133 | net-protocol 134 | nio4r (2.7.3) 135 | nokogiri (1.16.7-aarch64-linux) 136 | racc (~> 1.4) 137 | nokogiri (1.16.7-arm-linux) 138 | racc (~> 1.4) 139 | nokogiri (1.16.7-arm64-darwin) 140 | racc (~> 1.4) 141 | nokogiri (1.16.7-x86-linux) 142 | racc (~> 1.4) 143 | nokogiri (1.16.7-x86_64-darwin) 144 | racc (~> 1.4) 145 | nokogiri (1.16.7-x86_64-linux) 146 | racc (~> 1.4) 147 | psych (5.1.2) 148 | stringio 149 | public_suffix (6.0.1) 150 | racc (1.8.1) 151 | rack (3.1.8) 152 | rack-session (2.0.0) 153 | rack (>= 3.0.0) 154 | rack-test (2.1.0) 155 | rack (>= 1.3) 156 | rackup (2.1.0) 157 | rack (>= 3) 158 | webrick (~> 1.8) 159 | rails (7.2.1) 160 | actioncable (= 7.2.1) 161 | actionmailbox (= 7.2.1) 162 | actionmailer (= 7.2.1) 163 | actionpack (= 7.2.1) 164 | actiontext (= 7.2.1) 165 | actionview (= 7.2.1) 166 | activejob (= 7.2.1) 167 | activemodel (= 7.2.1) 168 | activerecord (= 7.2.1) 169 | activestorage (= 7.2.1) 170 | activesupport (= 7.2.1) 171 | bundler (>= 1.15.0) 172 | railties (= 7.2.1) 173 | rails-dom-testing (2.2.0) 174 | activesupport (>= 5.0.0) 175 | minitest 176 | nokogiri (>= 1.6) 177 | rails-html-sanitizer (1.6.0) 178 | loofah (~> 2.21) 179 | nokogiri (~> 1.14) 180 | railties (7.2.1) 181 | actionpack (= 7.2.1) 182 | activesupport (= 7.2.1) 183 | irb (~> 1.13) 184 | rackup (>= 1.0.0) 185 | rake (>= 12.2) 186 | thor (~> 1.0, >= 1.2.2) 187 | zeitwerk (~> 2.6) 188 | rake (13.2.1) 189 | rdoc (6.7.0) 190 | psych (>= 4.0.0) 191 | reline (0.5.10) 192 | io-console (~> 0.5) 193 | rexml (3.3.8) 194 | securerandom (0.3.1) 195 | sprockets (4.2.1) 196 | concurrent-ruby (~> 1.0) 197 | rack (>= 2.2.4, < 4) 198 | sprockets-rails (3.5.2) 199 | actionpack (>= 6.1) 200 | activesupport (>= 6.1) 201 | sprockets (>= 3.0.0) 202 | sqlite3 (2.6.0-arm64-darwin) 203 | stringio (3.1.1) 204 | thor (1.3.2) 205 | timeout (0.4.1) 206 | tzinfo (2.0.6) 207 | concurrent-ruby (~> 1.0) 208 | useragent (0.16.10) 209 | webmock (3.24.0) 210 | addressable (>= 2.8.0) 211 | crack (>= 0.3.2) 212 | hashdiff (>= 0.4.0, < 2.0.0) 213 | webrick (1.8.2) 214 | websocket-driver (0.7.6) 215 | websocket-extensions (>= 0.1.0) 216 | websocket-extensions (0.1.5) 217 | zeitwerk (2.7.0) 218 | 219 | PLATFORMS 220 | aarch64-linux 221 | aarch64-linux-gnu 222 | aarch64-linux-musl 223 | arm-linux 224 | arm-linux-gnu 225 | arm-linux-musl 226 | arm64-darwin 227 | x86-linux 228 | x86-linux-gnu 229 | x86-linux-musl 230 | x86_64-darwin 231 | x86_64-linux 232 | x86_64-linux-gnu 233 | x86_64-linux-musl 234 | 235 | DEPENDENCIES 236 | mailpace-rails! 237 | rails (>= 7.2.0) 238 | sprockets-rails 239 | sqlite3 (>= 1.4.2) 240 | webmock 241 | 242 | BUNDLED WITH 243 | 2.4.15 244 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 OhMySMTP 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MailPace::Rails 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 4 | [![Gem Version](https://badge.fury.io/rb/mailpace-rails.svg)](https://badge.fury.io/rb/mailpace-rails) 5 | [![MailPace Rails](https://circleci.com/gh/mailpace/mailpace-rails.svg?style=svg)](https://app.circleci.com/pipelines/github/mailpace/mailpace-rails) 6 | 7 | [MailPace](https://mailpace.com) lets you send transactional emails from your app over an easy to use API. 8 | 9 | The MailPace Rails Gem is a plug in for ActionMailer to send emails via [MailPace](https://mailpace.com) to make sending emails from Rails apps super simple. 10 | 11 | > **New in 0.4.0: [Idempotent Requests](https://docs.mailpace.com/guide/idempotency/), Improved Error Handling, InReplyTo and References support** 12 | 13 | > **New in 0.3.0: The ability to consume [inbound emails](https://docs.mailpace.com/guide/inbound/) from MailPace via ActionMailbox** 14 | 15 | ## Usage 16 | 17 | Once installed and configured, continue to send emails using [ActionMailer](https://guides.rubyonrails.org/action_mailer_basics.html) and receive emails with [ActionMailbox](https://edgeguides.rubyonrails.org/action_mailbox_basics.html) like normal. 18 | 19 | ## Other Requirements 20 | 21 | You will need an MailPace account with a verified domain and organization with an active plan. 22 | 23 | ## Installation 24 | 25 | ### Account Setup 26 | 27 | Set up an account at [MailPace](https://app.mailpace.com/users/sign_up) and complete the Onboarding steps 28 | 29 | ### Gem Installation 30 | 31 | Add this line to your application's Gemfile: 32 | 33 | ```ruby 34 | gem 'mailpace-rails' 35 | ``` 36 | 37 | And then execute: 38 | ```bash 39 | $ bundle 40 | ``` 41 | 42 | Or install it yourself as: 43 | ```bash 44 | $ gem install mailpace-rails 45 | ``` 46 | 47 | ### Configure the Gem 48 | 49 | First you will need to retrieve your API token for your sending domain from [MailPace](https://app.mailpace.com). You can find it under Organization -> Domain -> API Tokens. 50 | 51 | Use the encrypted secret management to save your API Token to `config/credentials.yml.enc` by running the following: 52 | 53 | ```bash 54 | rails secret 55 | rails credentials:edit 56 | ``` 57 | 58 | Then add your token: 59 | 60 | ```yaml 61 | mailpace_api_token: "TOKEN_GOES_HERE" 62 | ``` 63 | 64 | Set MailPace as your mail delivery method in `config/application.rb`: 65 | 66 | ```ruby 67 | config.action_mailer.delivery_method = :mailpace 68 | config.action_mailer.mailpace_settings = { api_token: Rails.application.credentials.mailpace_api_token } 69 | ``` 70 | 71 | ## Tagging 72 | 73 | You can tag messages and filter them later in the MailPace UI. To do this, pass the tags as a header by adding a tag variable to your `mail` method call. 74 | 75 | ```ruby 76 | class TestMailer < ApplicationMailer 77 | default from: 'notifications@example.com', 78 | to: 'fake@sdfasdfsdaf.com' 79 | 80 | def single_tag 81 | mail( 82 | tags: 'test tag' # One tag 83 | ) 84 | end 85 | 86 | def multi_tag 87 | mail( 88 | tags: "['test tag', 'another-tag']" # Multiple tags 89 | ) 90 | end 91 | end 92 | ``` 93 | 94 | Note that this should always be a string, even if using an array of multiple tags. 95 | 96 | ## List-Unsubscribe 97 | 98 | To add a List-Unsubscribe header, pass a `list_unsubscribe` string to the `mail` function: 99 | 100 | ```ruby 101 | class TestMailer < ApplicationMailer 102 | default from: 'notifications@example.com', 103 | to: 'fake@sdfasdfsdaf.com' 104 | 105 | def list_unsub_header 106 | mail( 107 | list_unsubscribe: 'https://listunsublink.com' 108 | ) 109 | end 110 | end 111 | ``` 112 | 113 | ## ActionMailbox (for receiving inbound emails) 114 | 115 | As of v0.3.0, this Gem supports handling Inbound Emails (see https://docs.mailpace.com/guide/inbound/ for more details) via ActionMailbox. To set this up: 116 | 117 | 1. Tell Action Mailbox to accept emails from MailPace in `config/environments/production.rb` 118 | 119 | ```ruby 120 | config.action_mailbox.ingress = :mailpace 121 | ``` 122 | 123 | 2. Generate a strong password that Action Mailbox can use to authenticate requests to the MailPace ingress. 124 | Use `bin/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: 125 | 126 | ```yaml 127 | action_mailbox: 128 | ingress_password: ... 129 | ``` 130 | 131 | Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. 132 | 133 | 3. Configure MailPace to forward inbound emails to `/rails/action_mailbox/mailpace/inbound_emails` with the username `actionmailbox` and the password you previously generated. If your application lived at `https://example.com` you would configure your MailPace inbound endpoint URL with the following fully-qualified URL: 134 | 135 | `https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/mailpace/inbound_emails` 136 | 137 | That's it! Emails should start flowing into your app just like magic. 138 | 139 | ## Idempotent Requests 140 | 141 | Mailpace supports [idempotency](https://docs.mailpace.com/guide/idempotency) for safely retrying requests without accidentally sending the same email twice. This is useful to guarantee that an email is not sent to the same recipient multiple times, e.g. through a network error, or a bug in your application logic. 142 | 143 | To do this, when writing your mailer, generate and add a unique `idempotency_key`: 144 | 145 | ```ruby 146 | class TestMailer < ApplicationMailer 147 | default from: 'notifications@example.com' 148 | def idempotent_mail 149 | email = 'email@example.com' 150 | mail( 151 | to: email, 152 | idempotency_key: Digest::SHA256.hexdigest("#{email}-#{Time.now.to_i / 3600}") 153 | ) 154 | end 155 | end 156 | ``` 157 | 158 | ## ActiveStorage Configuration 159 | 160 | This gem depends on ActiveStorage for handling inbound emails. If you do not have ActiveStorage configured in your Rails App you may need to create an empty file `config/storage.yml` for this Gem to work. 161 | 162 | ## Support 163 | 164 | For support please check the [MailPace Documentation](https://docs.mailpace.com) or contact us at support@mailpace.com 165 | 166 | ## Contributing 167 | 168 | Please ensure to add a test for any change you make. To run the tests: 169 | 170 | `bin/test` 171 | 172 | Pull requests always welcome 173 | 174 | ## License 175 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 176 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'Mailpace::Rails' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.md') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | require 'bundler/gem_tasks' 18 | 19 | require 'rake/testtask' 20 | 21 | Rake::TestTask.new(:test) do |t| 22 | t.libs << 'test' 23 | t.pattern = 'test/**/*_test.rb' 24 | t.verbose = false 25 | end 26 | 27 | task default: :test 28 | -------------------------------------------------------------------------------- /app/controllers/action_mailbox/ingresses/mailpace/inbound_emails_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActionMailbox 4 | module Ingresses 5 | module Mailpace 6 | # Ingests inbound emails from MailPace. Uses the a +raw+ parameter containing the full RFC 822 message. 7 | # 8 | # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the 9 | # password is read from the application's encrypted credentials or an environment variable. See the Usage section. 10 | # 11 | # Returns: 12 | # 13 | # - 204 No Content if an inbound email is successfully recorded and enqueued for routing 14 | # - 401 Unauthorized if the request's signature could not be validated 15 | # - 404 Not Found if Action Mailbox is not configured to accept inbound emails from MailPace 16 | # - 422 Unprocessable Entity if the request is missing the required +RawEmail+ parameter 17 | # - 500 Server Error if the ingress password is not configured, or if one of the Active Record database, 18 | # the Active Storage service, or the Active Job backend is misconfigured or unavailable 19 | # 20 | # == Usage 21 | # 22 | # 1. Tell Action Mailbox to accept emails from MailPace: 23 | # 24 | # # config/environments/production.rb 25 | # config.action_mailbox.ingress = :mailpace 26 | # 27 | # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the MailPace ingress. 28 | # 29 | # Use bin/rails credentials:edit to add the password to your application's encrypted credentials under 30 | # +action_mailbox.ingress_password+, where Action Mailbox will automatically find it: 31 | # 32 | # action_mailbox: 33 | # ingress_password: ... 34 | # 35 | # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. 36 | # 37 | # 3. {Configure MailPace}[https://docs.mailpace.com/guide/inbound] to forward inbound emails 38 | # to +/rails/action_mailbox/mailpace/inbound_emails+ with the username +actionmailbox+ and the password you 39 | # previously generated. If your application lived at https://example.com, you would configure your 40 | # MailPace inbound endpoint URL with the following fully-qualified URL: 41 | # 42 | # https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/mailpace/inbound_emails 43 | # 44 | class InboundEmailsController < ActionMailbox::BaseController 45 | before_action :authenticate_by_password 46 | 47 | def create 48 | ActionMailbox::InboundEmail.create_and_extract_message_id! params.require('raw') 49 | rescue ActionController::ParameterMissing => e 50 | logger.error e.message 51 | 52 | head :unprocessable_entity 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.expand_path("../test", __dir__) 3 | 4 | require "bundler/setup" 5 | require "rails/plugin/test" 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | post 'rails/action_mailbox/mailpace/inbound_emails', to: 'action_mailbox/ingresses/mailpace/inbound_emails#create', 3 | as: 'rails_mailpace_inbound_emails' 4 | end 5 | -------------------------------------------------------------------------------- /lib/mailpace-rails.rb: -------------------------------------------------------------------------------- 1 | require 'action_mailer' 2 | require 'action_mailbox/engine' 3 | require 'httparty' 4 | require 'uri' 5 | require 'json' 6 | require 'mailpace-rails/version' 7 | require 'mailpace-rails/engine' if defined? Rails 8 | 9 | module Mailpace 10 | # MailPace ActionMailer delivery method 11 | class DeliveryMethod 12 | attr_accessor :settings 13 | 14 | def initialize(values) 15 | check_api_token(values) 16 | self.settings = { return_response: true }.merge!(values) 17 | end 18 | 19 | def deliver!(mail) 20 | if mail.multipart? 21 | htmlbody = mail.html_part.body.decoded, 22 | textbody = mail.text_part.body.decoded 23 | elsif mail.mime_type == "text/plain" 24 | textbody = mail.body.to_s 25 | else 26 | htmlbody = mail.body.to_s 27 | end 28 | 29 | check_delivery_params(mail) 30 | result = HTTParty.post( 31 | 'https://app.mailpace.com/api/v1/send', 32 | body: { 33 | from: address_list(mail.header[:from])&.addresses&.first.to_s, 34 | to: address_list(mail.header[:to])&.addresses&.join(','), 35 | subject: mail.subject, 36 | htmlbody: htmlbody, 37 | textbody: textbody, 38 | cc: address_list(mail.header[:cc])&.addresses&.join(','), 39 | bcc: address_list(mail.header[:bcc])&.addresses&.join(','), 40 | replyto: address_list(mail.header[:reply_to])&.addresses&.join(','), 41 | inreplyto: mail.header['In-Reply-To'].to_s, 42 | references: mail.header['References'].to_s, 43 | list_unsubscribe: mail.header['list_unsubscribe'].to_s, 44 | attachments: format_attachments(mail.attachments), 45 | tags: mail.header['tags'].to_s 46 | }.delete_if { |_key, value| value.blank? }.to_json, 47 | headers: { 48 | 'User-Agent' => "MailPace Rails Gem v#{Mailpace::Rails::VERSION}", 49 | 'Accept' => 'application/json', 50 | 'Content-Type' => 'application/json', 51 | 'Mailpace-Server-Token' => settings[:api_token] 52 | }.tap do |h| 53 | h['Idempotency-Key'] = mail.header['idempotency_key'].to_s if mail.header['idempotency_key'] 54 | end 55 | ) 56 | 57 | handle_response(result) 58 | end 59 | 60 | private 61 | 62 | def check_api_token(values) 63 | return if values[:api_token].present? 64 | 65 | raise ArgumentError, 'MailPace API token is not set' 66 | end 67 | 68 | def check_delivery_params(mail) 69 | return unless mail.from.nil? || mail.to.nil? 70 | 71 | raise ArgumentError, 'Missing to or from address in email' 72 | end 73 | 74 | def handle_response(result) 75 | return result unless result.code != 200 76 | 77 | parsed_response = result.parsed_response 78 | error_message = join_error_messages(parsed_response) 79 | 80 | raise DeliveryError, "MAILPACE Error: #{error_message}" unless error_message.empty? 81 | end 82 | 83 | def format_attachments(attachments) 84 | attachments.map do |attachment| 85 | { 86 | name: attachment.filename, 87 | content_type: attachment.mime_type, 88 | content: Base64.encode64(attachment.body.encoded), 89 | cid: attachment.content_id 90 | }.compact 91 | end 92 | end 93 | 94 | def address_list(obj) 95 | if obj&.respond_to?(:element) 96 | # Mail 2.8+ 97 | obj.element 98 | else 99 | # Mail <= 2.7.x 100 | obj&.address_list 101 | end 102 | end 103 | 104 | def join_error_messages(response) 105 | # Join 'error' and 'errors' keys from response into a single string 106 | [response['error'], response['errors']].compact.join(', ') 107 | end 108 | end 109 | 110 | class Error < StandardError; end 111 | class DeliveryError < StandardError; end 112 | 113 | def self.root 114 | Pathname.new(File.expand_path(File.join(__dir__, '..'))) 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/mailpace-rails/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mailpace 4 | # Provides the delivery method & sets up action mailbox 5 | class Engine < ::Rails::Engine 6 | initializer 'mailpace.add_delivery_method', before: 'action_mailer.set_configs' do 7 | ActionMailer::Base.add_delivery_method(:mailpace, Mailpace::DeliveryMethod) 8 | end 9 | 10 | config.action_mailbox.mailpace = ActiveSupport::OrderedOptions.new 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mailpace-rails/version.rb: -------------------------------------------------------------------------------- 1 | module Mailpace 2 | module Rails 3 | VERSION = '0.4.2' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /mailpace-rails.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("lib", __dir__) 2 | 3 | # Maintain your gem's version: 4 | require "mailpace-rails/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |spec| 8 | spec.name = "mailpace-rails" 9 | spec.version = Mailpace::Rails::VERSION 10 | spec.authors = ["MailPace"] 11 | spec.email = ["support@mailpace.com"] 12 | spec.homepage = "https://mailpace.com" 13 | spec.summary = "Lets you send transactional emails from your app over an easy to use API" 14 | spec.description = "The MailPace Rails Gem is a plug in for ActionMailer to send emails via MailPace to make sending emails from Rails apps super simple." 15 | spec.license = "MIT" 16 | 17 | spec.metadata['source_code_uri'] = 'https://github.com/mailpace/mailpace-rails' 18 | spec.metadata['changelog_uri'] = 'https://github.com/mailpace/mailpace-rails/blob/master/CHANGELOG.md' 19 | 20 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 21 | 22 | spec.add_dependency('actionmailer', ">= 6.0.0") 23 | spec.add_dependency('actionmailbox', ">= 6.0.0") 24 | spec.add_dependency('activestorage', ">= 6.0.0") 25 | 26 | spec.add_dependency('httparty', '>= 0.18.1') 27 | 28 | spec.add_development_dependency "rails", "#{ENV['RAILS_TEST_VERSION'] || '>=7.2.0'}" 29 | spec.add_development_dependency "sqlite3", ">=1.4.2" 30 | end 31 | -------------------------------------------------------------------------------- /test/dummy/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.1 2 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require activestorage 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /test/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/mailboxes/application_mailbox.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailbox < ActionMailbox::Base 2 | # routing /something/i => :somewhere 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/complex_mailer.rb: -------------------------------------------------------------------------------- 1 | class ComplexMailer < ApplicationMailer 2 | default from: 'My Full Name ', 3 | cc: 'test@test.com, full name cc ', 4 | bcc: 'full name bcc ', 5 | to: 'fake@sdfasdfsdaf.com' 6 | 7 | def complex_email 8 | mail 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/full_name_mailer.rb: -------------------------------------------------------------------------------- 1 | class FullNameMailer < ApplicationMailer 2 | default from: 'My Full Name ', 3 | to: 'Recipient Full Name ' 4 | 5 | def full_name_email 6 | mail 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/idempotent_mailer.rb: -------------------------------------------------------------------------------- 1 | class IdempotentMailer < ApplicationMailer 2 | default from: 'notifications@example.com', 3 | to: 'fake@sdfasdfsdaf.com' 4 | 5 | def idempotent_email 6 | mail( 7 | idempotency_key: 'example key' 8 | ) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/list_unsubscribe_mailer.rb: -------------------------------------------------------------------------------- 1 | class ListUnsubscribeMailer < ApplicationMailer 2 | default from: 'notifications@example.com', 3 | to: 'fake@sdfasdfsdaf.com' 4 | 5 | def unsubscribe 6 | mail( 7 | list_unsubscribe: 'test list-unsubscribe' 8 | ) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/plaintext_mailer.rb: -------------------------------------------------------------------------------- 1 | class PlaintextMailer < ApplicationMailer 2 | default from: 'notifications@example.com', 3 | to: 'fake@sdfasdfsdaf.com' 4 | 5 | def plain_only_email 6 | mail 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/tag_mailer.rb: -------------------------------------------------------------------------------- 1 | class TagMailer < ApplicationMailer 2 | default from: 'notifications@example.com', 3 | to: 'fake@sdfasdfsdaf.com' 4 | 5 | def single_tag 6 | mail( 7 | tags: 'test tag' 8 | ) 9 | end 10 | 11 | def multi_tag 12 | mail( 13 | tags: "['test tag', 'another-tag']" 14 | ) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/test_mailer.rb: -------------------------------------------------------------------------------- 1 | class TestMailer < ApplicationMailer 2 | default from: 'notifications@example.com', 3 | to: 'fake@sdfasdfsdaf.com' 4 | 5 | def welcome_email 6 | mail 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/views/complex_mailer/complex_email.html.erb: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/dummy/app/views/complex_mailer/complex_email.text.erb: -------------------------------------------------------------------------------- 1 | test text only -------------------------------------------------------------------------------- /test/dummy/app/views/full_name_mailer/full_name_email.html.erb: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/dummy/app/views/full_name_mailer/full_name_email.text.erb: -------------------------------------------------------------------------------- 1 | test text only -------------------------------------------------------------------------------- /test/dummy/app/views/idempotent_mailer/idempotent_email.html.erb: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/dummy/app/views/idempotent_mailer/idempotent_email.text.erb: -------------------------------------------------------------------------------- 1 | test text only -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | <%= stylesheet_link_tag 'application', media: 'all' %> 9 | 10 | 11 | 12 | <%= yield %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /test/dummy/app/views/list_unsubscribe_mailer/unsubscribe.html.erb: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/dummy/app/views/list_unsubscribe_mailer/unsubscribe.text.erb: -------------------------------------------------------------------------------- 1 | test text only -------------------------------------------------------------------------------- /test/dummy/app/views/plaintext_mailer/plain_only_email.text.erb: -------------------------------------------------------------------------------- 1 | test text only -------------------------------------------------------------------------------- /test/dummy/app/views/tag_mailer/multi_tag.text.erb: -------------------------------------------------------------------------------- 1 | test text only -------------------------------------------------------------------------------- /test/dummy/app/views/tag_mailer/single_tag.html.erb: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/dummy/app/views/test_mailer/welcome_email.html.erb: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/dummy/app/views/test_mailer/welcome_email.text.erb: -------------------------------------------------------------------------------- 1 | test text only -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:prepare' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require 'mailpace-rails' 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Initialize configuration defaults for originally generated Rails version. 11 | config.load_defaults "#{ ENV['RAILS_TEST_VERSION'] ? ENV['RAILS_TEST_VERSION'][0..2] : '6.0' }" 12 | 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration can go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded after loading 16 | # the framework and any gems in your application. 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 6 | -------------------------------------------------------------------------------- /test/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /test/dummy/config/credentials/test.key: -------------------------------------------------------------------------------- 1 | 37ad1c37aa3fc439ff0e5728ac2911e2 -------------------------------------------------------------------------------- /test/dummy/config/credentials/test.yml.enc: -------------------------------------------------------------------------------- 1 | bPyLpGFj5S52KNoXbE1o6oErAEIPdvD7rhsUhhY5QcGxb1d2u7AZYyUZecmbgDYzxf5m0pdBXUXL/VVY1bh9n3OzXviNSGcK29+f+y/HX7S2Pq1I9mAF/efbGnlj6jl2cZevbpFr--rcR6rzb8RCklGTPE--Ohza8uKwHvlfyxMAe2gEoA== -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 18 | config.action_controller.perform_caching = true 19 | config.action_controller.enable_fragment_cache_logging = true 20 | 21 | config.cache_store = :memory_store 22 | config.public_file_server.headers = { 23 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 24 | } 25 | else 26 | config.action_controller.perform_caching = false 27 | 28 | config.cache_store = :null_store 29 | end 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | config.action_mailer.perform_caching = false 38 | 39 | # Print deprecation notices to the Rails logger. 40 | config.active_support.deprecation = :log 41 | 42 | # Raise an error on page load if there are pending migrations. 43 | config.active_record.migration_error = :page_load 44 | 45 | # Highlight code that triggered database queries in logs. 46 | config.active_record.verbose_query_logs = true 47 | 48 | # Debug mode disables concatenation and preprocessing of assets. 49 | # This option may cause significant delays in view rendering with a large 50 | # number of complex assets. 51 | config.assets.debug = true 52 | 53 | # Suppress logger output for asset requests. 54 | config.assets.quiet = true 55 | 56 | # Raises error for missing translations. 57 | # config.action_view.raise_on_missing_translations = true 58 | 59 | # Use an evented file watcher to asynchronously detect changes in source code, 60 | # routes, locales, etc. This feature depends on the listen gem. 61 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 62 | end 63 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 19 | # config.require_master_key = true 20 | 21 | # Disable serving static files from the `/public` folder by default since 22 | # Apache or NGINX already handles this. 23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 24 | 25 | # Compress CSS using a preprocessor. 26 | # config.assets.css_compressor = :sass 27 | 28 | # Do not fallback to assets pipeline if a precompiled asset is missed. 29 | config.assets.compile = false 30 | 31 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 32 | # config.action_controller.asset_host = 'http://assets.example.com' 33 | 34 | # Specifies the header that your server uses for sending files. 35 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 36 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 37 | 38 | # Store uploaded files on the local file system (see config/storage.yml for options). 39 | config.active_storage.service = :local 40 | 41 | # Mount Action Cable outside main process or domain. 42 | # config.action_cable.mount_path = nil 43 | # config.action_cable.url = 'wss://example.com/cable' 44 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 45 | 46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 47 | # config.force_ssl = true 48 | 49 | # Use the lowest log level to ensure availability of diagnostic information 50 | # when problems arise. 51 | config.log_level = :debug 52 | 53 | # Prepend all log lines with the following tags. 54 | config.log_tags = [ :request_id ] 55 | 56 | # Use a different cache store in production. 57 | # config.cache_store = :mem_cache_store 58 | 59 | # Use a real queuing backend for Active Job (and separate queues per environment). 60 | # config.active_job.queue_adapter = :resque 61 | # config.active_job.queue_name_prefix = "dummy_production" 62 | 63 | config.action_mailer.perform_caching = false 64 | 65 | # Ignore bad email addresses and do not raise email delivery errors. 66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 67 | # config.action_mailer.raise_delivery_errors = false 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Send deprecation notices to registered listeners. 74 | config.active_support.deprecation = :notify 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Use a different logger for distributed setups. 80 | # require 'syslog/logger' 81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 82 | 83 | if ENV["RAILS_LOG_TO_STDOUT"].present? 84 | logger = ActiveSupport::Logger.new(STDOUT) 85 | logger.formatter = config.log_formatter 86 | config.logger = ActiveSupport::TaggedLogging.new(logger) 87 | end 88 | 89 | # Do not dump schema after migrations. 90 | config.active_record.dump_schema_after_migration = false 91 | 92 | # Inserts middleware to perform automatic connection switching. 93 | # The `database_selector` hash is used to pass options to the DatabaseSelector 94 | # middleware. The `delay` is used to determine how long to wait after a write 95 | # to send a subsequent read to the primary. 96 | # 97 | # The `database_resolver` class is used by the middleware to determine which 98 | # database is appropriate to use based on the time delay. 99 | # 100 | # The `database_resolver_context` class is used by the middleware to set 101 | # timestamps for the last write to the primary. The resolver uses the context 102 | # class timestamps to determine how long to wait before reading from the 103 | # replica. 104 | # 105 | # By default Rails will store a last write timestamp in the session. The 106 | # DatabaseSelector middleware is designed as such you can define your own 107 | # strategy for connection switching and pass that into the middleware through 108 | # these configuration options. 109 | # config.active_record.database_selector = { delay: 2.seconds } 110 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 111 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 112 | end 113 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. This avoids loading your whole application 12 | # just for the purpose of running a single test. If you are using a tool that 13 | # preloads Rails for running tests, you may have to set it to true. 14 | config.eager_load = false 15 | 16 | # Configure public file server for tests with Cache-Control for performance. 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 19 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 20 | } 21 | 22 | # Show full error reports and disable caching. 23 | config.consider_all_requests_local = true 24 | config.action_controller.perform_caching = false 25 | config.cache_store = :null_store 26 | 27 | # Raise exceptions instead of rendering exception templates. 28 | config.action_dispatch.show_exceptions = false 29 | 30 | # Disable request forgery protection in test environment. 31 | config.action_controller.allow_forgery_protection = false 32 | 33 | # Store uploaded files on the local file system in a temporary directory. 34 | config.active_storage.service = :test 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | config.action_mailer.perform_deliveries = true 39 | config.action_mailer.raise_delivery_errors = true 40 | 41 | # Tell Action Mailer not to deliver emails to the real world. 42 | # The :test delivery method accumulates sent emails in the 43 | # ActionMailer::Base.deliveries array. 44 | config.action_mailer.delivery_method = :mailpace 45 | config.action_mailer.mailpace_settings = { 46 | api_token: Rails.application.credentials.mailpace_api_token 47 | } 48 | 49 | config.action_mailbox.ingress = :mailpace 50 | 51 | # Print deprecation notices to the stderr. 52 | config.active_support.deprecation = :stderr 53 | 54 | # Raises error for missing translations. 55 | # config.action_view.raise_on_missing_translations = true 56 | end 57 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Set the nonce only to specific directives 23 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 24 | 25 | # Report CSP violations to a specified URI 26 | # For further information see the following documentation: 27 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 28 | # Rails.application.config.content_security_policy_report_only = true 29 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /test/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 12 | # 13 | port ENV.fetch("PORT") { 3000 } 14 | 15 | # Specifies the `environment` that Puma will run in. 16 | # 17 | environment ENV.fetch("RAILS_ENV") { "development" } 18 | 19 | # Specifies the `pidfile` that Puma will use. 20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 21 | 22 | # Specifies the number of `workers` to boot in clustered mode. 23 | # Workers are forked web server processes. If using threads and workers together 24 | # the concurrency of the application would be max `threads` * `workers`. 25 | # Workers do not work on JRuby or Windows (both of which do not support 26 | # processes). 27 | # 28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 29 | 30 | # Use the `preload_app!` method when specifying a `workers` number. 31 | # This directive tells Puma to first boot the application and load code 32 | # before forking the application. This takes advantage of Copy On Write 33 | # process behavior so workers use less memory. 34 | # 35 | # preload_app! 36 | 37 | # Allow puma to be restarted by `rails restart` command. 38 | plugin :tmp_restart 39 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /test/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20211222122019_create_active_storage_tables.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20170806125915) 2 | class CreateActiveStorageTables < ActiveRecord::Migration[5.2] 3 | def change 4 | create_table :active_storage_blobs do |t| 5 | t.string :key, null: false 6 | t.string :filename, null: false 7 | t.string :content_type 8 | t.text :metadata 9 | t.string :service_name, null: false 10 | t.bigint :byte_size, null: false 11 | t.string :checksum, null: false 12 | t.datetime :created_at, null: false 13 | 14 | t.index [ :key ], unique: true 15 | end 16 | 17 | create_table :active_storage_attachments do |t| 18 | t.string :name, null: false 19 | t.references :record, null: false, polymorphic: true, index: false 20 | t.references :blob, null: false 21 | 22 | t.datetime :created_at, null: false 23 | 24 | t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true 25 | t.foreign_key :active_storage_blobs, column: :blob_id 26 | end 27 | 28 | create_table :active_storage_variant_records do |t| 29 | t.belongs_to :blob, null: false, index: false 30 | t.string :variation_digest, null: false 31 | 32 | t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true 33 | t.foreign_key :active_storage_blobs, column: :blob_id 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20211222122020_create_action_mailbox_tables.action_mailbox.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from action_mailbox (originally 20180917164000) 2 | class CreateActionMailboxTables < ActiveRecord::Migration[6.0] 3 | def change 4 | create_table :action_mailbox_inbound_emails do |t| 5 | t.integer :status, default: 0, null: false 6 | t.string :message_id, null: false 7 | t.string :message_checksum, null: false 8 | 9 | t.timestamps 10 | 11 | t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2021_12_22_122020) do 14 | 15 | create_table "action_mailbox_inbound_emails", force: :cascade do |t| 16 | t.integer "status", default: 0, null: false 17 | t.string "message_id", null: false 18 | t.string "message_checksum", null: false 19 | t.datetime "created_at", precision: 6, null: false 20 | t.datetime "updated_at", precision: 6, null: false 21 | t.index ["message_id", "message_checksum"], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true 22 | end 23 | 24 | create_table "active_storage_attachments", force: :cascade do |t| 25 | t.string "name", null: false 26 | t.string "record_type", null: false 27 | t.integer "record_id", null: false 28 | t.integer "blob_id", null: false 29 | t.datetime "created_at", null: false 30 | t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" 31 | t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true 32 | end 33 | 34 | create_table "active_storage_blobs", force: :cascade do |t| 35 | t.string "key", null: false 36 | t.string "filename", null: false 37 | t.string "content_type" 38 | t.text "metadata" 39 | t.string "service_name", null: false 40 | t.bigint "byte_size", null: false 41 | t.string "checksum", null: false 42 | t.datetime "created_at", null: false 43 | t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true 44 | end 45 | 46 | create_table "active_storage_variant_records", force: :cascade do |t| 47 | t.integer "blob_id", null: false 48 | t.string "variation_digest", null: false 49 | t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true 50 | end 51 | 52 | add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" 53 | add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" 54 | end 55 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

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

63 |
64 |

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

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

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

63 |
64 |

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

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

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

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/dummy/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/dummy/public/apple-touch-icon.png -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/test/mailers/previews/test_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/test_mailer 2 | class TestMailerPreview < ActionMailer::Preview 3 | 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/test/mailers/test_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestMailerTest < ActionMailer::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailpace/mailpace-rails/b8466427f206a464bf436d2664743073ebeff705/test/logo.png -------------------------------------------------------------------------------- /test/mailpace/email_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Email 4 | def initialize(mail: default_mail) 5 | @mail = mail 6 | end 7 | 8 | def headers 9 | { 10 | 'content-type': 'application/json', 11 | Authorization: ActionController::HttpAuthentication::Basic.encode_credentials('actionmailbox', 'test') 12 | } 13 | end 14 | 15 | def url 16 | '/rails/action_mailbox/mailpace/inbound_emails' 17 | end 18 | 19 | def params 20 | { 21 | raw: @mail.encoded, 22 | test: 'test' 23 | } 24 | end 25 | 26 | def default_mail 27 | Mail.new 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/mailpace/ingress_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'mailpace/email_helper' 3 | 4 | class MailpaceIngressTest < ActionDispatch::IntegrationTest 5 | test 'ingress has been set' do 6 | assert_equal :mailpace, ActionMailbox.ingress 7 | end 8 | 9 | test 'accept an email' do 10 | email = Email.new 11 | post email.url, params: email.params.to_json, headers: email.headers 12 | assert_response :success 13 | assert_equal ActionMailbox::InboundEmail.count, 1 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/mailpace/mailer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Mailpace::Rails::Test < ActiveSupport::TestCase 4 | setup do 5 | ActionMailer::Base.delivery_method = :mailpace 6 | ActionMailer::Base.mailpace_settings = { api_token: 'api_token' } 7 | 8 | stub_request(:post, 'https://app.mailpace.com/api/v1/send') 9 | .to_return( 10 | body: { "status": 'queued', "id": 1 }.to_json, 11 | headers: { content_type: 'application/json' }, 12 | status: 200 13 | ) 14 | 15 | @test_email = TestMailer.welcome_email 16 | end 17 | 18 | test 'truth' do 19 | assert_kind_of Module, Mailpace::Rails 20 | end 21 | 22 | test 'api token can be set' do 23 | ActionMailer::Base.mailpace_settings = { api_token: 'api-token' } 24 | assert_equal ActionMailer::Base.mailpace_settings[:api_token], 'api-token' 25 | end 26 | 27 | test 'raises ArgumentError if no api token set' do 28 | ActionMailer::Base.mailpace_settings = {} 29 | assert_raise(ArgumentError) { @test_email.deliver! } 30 | end 31 | 32 | test 'raises ArgumentError if no from address in email' do 33 | t = TestMailer.welcome_email 34 | t.from = nil 35 | assert_raise(ArgumentError) { t.deliver! } 36 | end 37 | 38 | test 'raises ArgumentError if no to address in email' do 39 | t = TestMailer.welcome_email 40 | t.to = nil 41 | assert_raise(ArgumentError) { t.deliver! } 42 | end 43 | 44 | test 'send basic emails to endpoint' do 45 | @test_email.deliver! 46 | 47 | assert_requested( 48 | :post, 'https://app.mailpace.com/api/v1/send', 49 | times: 1 50 | ) 51 | end 52 | 53 | test 'supports multiple attachments' do 54 | t = TestMailer.welcome_email 55 | t.attachments['logo.png'] = File.read("#{Dir.pwd}/test/logo.png") 56 | t.attachments['l2.png'] = File.read("#{Dir.pwd}/test/logo.png") 57 | 58 | t.deliver! 59 | 60 | assert_requested( 61 | :post, 'https://app.mailpace.com/api/v1/send', 62 | times: 1 63 | ) do |req| 64 | attachments = JSON.parse(req.body)['attachments'] 65 | attachments[0]['name'] == 'logo.png' && attachments[1]['name'] == 'l2.png' 66 | end 67 | end 68 | 69 | test 'supports custom mime types' do 70 | t = TestMailer.welcome_email 71 | t.attachments['logo.png'] = { 72 | mime_type: 'custom/type', 73 | content: File.read("#{Dir.pwd}/test/logo.png") 74 | } 75 | t.deliver! 76 | 77 | assert_requested( 78 | :post, 'https://app.mailpace.com/api/v1/send', 79 | times: 1 80 | ) do |req| 81 | JSON.parse(req.body)['attachments'][0]['content_type'] == 'custom/type' 82 | end 83 | end 84 | 85 | test 'supports full names in the from address' do 86 | t = FullNameMailer.full_name_email 87 | t.deliver! 88 | 89 | assert_requested( 90 | :post, 'https://app.mailpace.com/api/v1/send', 91 | times: 1 92 | ) do |req| 93 | JSON.parse(req.body)['from'] == 'My Full Name ' 94 | end 95 | end 96 | 97 | test 'supports full names in the to address' do 98 | t = FullNameMailer.full_name_email 99 | t.deliver! 100 | 101 | assert_requested( 102 | :post, 'https://app.mailpace.com/api/v1/send', 103 | times: 1 104 | ) do |req| 105 | JSON.parse(req.body)['to'] == 'Recipient Full Name ' 106 | end 107 | end 108 | 109 | test 'supports single tag' do 110 | t = TagMailer.single_tag 111 | t.deliver! 112 | 113 | assert_requested( 114 | :post, 'https://app.mailpace.com/api/v1/send', 115 | times: 1 116 | ) do |req| 117 | JSON.parse(req.body)['tags'] == 'test tag' 118 | end 119 | end 120 | 121 | test 'supports array of tags tag' do 122 | t = TagMailer.multi_tag 123 | t.deliver! 124 | 125 | assert_requested( 126 | :post, 'https://app.mailpace.com/api/v1/send', 127 | times: 1 128 | ) do |req| 129 | JSON.parse(req.body)['tags'] == "['test tag', 'another-tag']" 130 | end 131 | end 132 | 133 | test 'does not send tags if tags not supplied' do 134 | t = TestMailer.welcome_email 135 | t.deliver! 136 | 137 | assert_requested( 138 | :post, 'https://app.mailpace.com/api/v1/send', 139 | times: 1 140 | ) do |req| 141 | JSON.parse(req.body)['tags'].nil? 142 | end 143 | end 144 | 145 | test 'supports List-Unsubscribe header' do 146 | t = ListUnsubscribeMailer.unsubscribe 147 | t.deliver! 148 | 149 | assert_requested( 150 | :post, 'https://app.mailpace.com/api/v1/send', 151 | times: 1 152 | ) do |req| 153 | JSON.parse(req.body)['list_unsubscribe'] == 'test list-unsubscribe' 154 | end 155 | end 156 | 157 | test 'deliver! returns the API response' do 158 | t = TestMailer.welcome_email 159 | res = t.deliver! 160 | assert_equal res['id'], 1 161 | end 162 | 163 | # See https://github.com/mikel/mail/blob/22a7afc23f253319965bf9228a0a430eec94e06d/lib/mail/fields/reply_to_field.rb 164 | test 'supports reply to' do 165 | t = TestMailer.welcome_email 166 | t.reply_to = 'Reply To Name ' 167 | t.deliver! 168 | 169 | assert_requested( 170 | :post, 'https://app.mailpace.com/api/v1/send', 171 | times: 1 172 | ) do |req| 173 | JSON.parse(req.body)['replyto'] == 'Reply To Name ' 174 | end 175 | end 176 | 177 | test 'supports complex cc and bcc entries' do 178 | t = ComplexMailer.complex_email 179 | t.deliver! 180 | 181 | assert_requested( 182 | :post, 'https://app.mailpace.com/api/v1/send', 183 | times: 1 184 | ) do |req| 185 | JSON.parse(req.body)['cc'] == 'test@test.com,full name cc ' && 186 | JSON.parse(req.body)['bcc'] == 'full name bcc ' 187 | end 188 | end 189 | 190 | test 'raises DeliveryError if response is not 200' do 191 | stub_request(:post, 'https://app.mailpace.com/api/v1/send') 192 | .to_return( 193 | body: { error: 'contains a blocked address' }.to_json, 194 | headers: { content_type: 'application/json' }, 195 | status: 400 196 | ) 197 | 198 | t = TestMailer.welcome_email 199 | 200 | assert_raise(Mailpace::DeliveryError, 'MAILPACE Error: contains a blocked address') do 201 | t.deliver! 202 | end 203 | end 204 | 205 | test 'supports in-reply-to' do 206 | t = TestMailer.welcome_email 207 | t.in_reply_to = '' 208 | t.deliver! 209 | 210 | assert_requested( 211 | :post, 'https://app.mailpace.com/api/v1/send', 212 | times: 1 213 | ) do |req| 214 | JSON.parse(req.body)['inreplyto'] == '' 215 | end 216 | end 217 | 218 | test 'supports single references' do 219 | t = TestMailer.welcome_email 220 | t.references = '' 221 | t.deliver! 222 | 223 | assert_requested( 224 | :post, 'https://app.mailpace.com/api/v1/send', 225 | times: 1 226 | ) do |req| 227 | JSON.parse(req.body)['references'] == '' 228 | end 229 | end 230 | 231 | test 'supports multiple references' do 232 | t = TestMailer.welcome_email 233 | t.references = ' ' 234 | t.deliver! 235 | 236 | assert_requested( 237 | :post, 'https://app.mailpace.com/api/v1/send', 238 | times: 1 239 | ) do |req| 240 | JSON.parse(req.body)['references'] == ' ' 241 | end 242 | end 243 | 244 | test 'supports setting idempotency key directly' do 245 | t = TestMailer.welcome_email 246 | t.header['idempotency_key'] = 'example key' 247 | t.deliver! 248 | 249 | assert_requested( 250 | :post, 'https://app.mailpace.com/api/v1/send', 251 | times: 1 252 | ) do |req| 253 | req.headers['Idempotency-Key'] == 'example key' 254 | end 255 | end 256 | 257 | test 'supports email with idempotency key set' do 258 | t = IdempotentMailer.idempotent_email 259 | t.deliver! 260 | 261 | assert_requested( 262 | :post, 'https://app.mailpace.com/api/v1/send', 263 | times: 1 264 | ) do |req| 265 | req.headers['Idempotency-Key'] == 'example key' 266 | end 267 | end 268 | 269 | test 'idempotency key is not in the request if it is not set in the email' do 270 | @test_email.deliver! 271 | 272 | assert_requested( 273 | :post, 'https://app.mailpace.com/api/v1/send', 274 | times: 1 275 | ) do |req| 276 | req.headers['Idempotency-Key'].nil? 277 | end 278 | end 279 | 280 | test 'supports text-only emails' do 281 | t = PlaintextMailer.plain_only_email 282 | t.references = ' ' 283 | t.deliver! 284 | 285 | assert_requested( 286 | :post, 'https://app.mailpace.com/api/v1/send', 287 | times: 1 288 | ) do |req| 289 | JSON.parse(req.body)['htmlbody'].nil? && 290 | JSON.parse(req.body)['textbody'] == "test text only\n" 291 | end 292 | end 293 | end 294 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require_relative "../test/dummy/config/environment" 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 6 | require "rails/test_help" 7 | 8 | # Filter out the backtrace from minitest while preserving the one from other libraries. 9 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new 10 | 11 | require "rails/test_unit/reporter" 12 | Rails::TestUnitReporter.executable = 'bin/test' 13 | 14 | # Load fixtures from the engine 15 | if ActiveSupport::TestCase.respond_to?(:fixture_path=) 16 | ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) 17 | ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path 18 | ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" 19 | ActiveSupport::TestCase.fixtures :all 20 | end 21 | 22 | require 'webmock/minitest' --------------------------------------------------------------------------------