├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── lib └── recipient_interceptor.rb ├── recipient_interceptor.gemspec └── spec └── recipient_interceptor_spec.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: ruby 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Ruby 11 | uses: ruby/setup-ruby@v1 12 | with: 13 | ruby-version: head 14 | - name: Test 15 | run: | 16 | bundle 17 | bundle exec rspec 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rspec" 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | recipient_interceptor (0.3.3) 5 | mail 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | date (3.4.0) 11 | diff-lcs (1.5.1) 12 | mail (2.8.1) 13 | mini_mime (>= 0.1.1) 14 | net-imap 15 | net-pop 16 | net-smtp 17 | mini_mime (1.1.5) 18 | net-imap (0.5.1) 19 | date 20 | net-protocol 21 | net-pop (0.1.2) 22 | net-protocol 23 | net-protocol (0.2.2) 24 | timeout 25 | net-smtp (0.5.0) 26 | net-protocol 27 | rspec (3.13.0) 28 | rspec-core (~> 3.13.0) 29 | rspec-expectations (~> 3.13.0) 30 | rspec-mocks (~> 3.13.0) 31 | rspec-core (3.13.2) 32 | rspec-support (~> 3.13.0) 33 | rspec-expectations (3.13.3) 34 | diff-lcs (>= 1.2.0, < 2.0) 35 | rspec-support (~> 3.13.0) 36 | rspec-mocks (3.13.2) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.13.0) 39 | rspec-support (3.13.1) 40 | timeout (0.4.2) 41 | 42 | PLATFORMS 43 | ruby 44 | 45 | DEPENDENCIES 46 | recipient_interceptor! 47 | rspec 48 | 49 | BUNDLED WITH 50 | 2.5.23 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Dan Croak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # recipient_interceptor 2 | 3 | Use this [Ruby gem](https://rubygems.org/gems/recipient_interceptor) 4 | to avoid emailing your users from non-production environments. 5 | 6 | ```ruby 7 | # Gemfile 8 | gem "recipient_interceptor" 9 | 10 | # config/environments/staging.rb 11 | Mail.register_interceptor( 12 | RecipientInterceptor.new("staging@example.com") 13 | ) 14 | 15 | # config/environments/production.rb 16 | My::Application.configure do 17 | config.action_mailer.delivery_method = :smtp 18 | config.action_mailer.smtp_settings = { 19 | address: ENV.fetch("SMTP_ADDRESS"), # example: "smtp.sendgrid.net" 20 | authentication: :plain, 21 | domain: ENV.fetch("SMTP_DOMAIN"), # example: "heroku.com" 22 | enable_starttls_auto: true, 23 | password: ENV.fetch("SMTP_PASSWORD"), 24 | port: "587", 25 | user_name: ENV.fetch("SMTP_USERNAME") 26 | } 27 | end 28 | ``` 29 | 30 | Email will be intercepted and delivered to the provided address with 31 | headers `X-Intercepted-To`, `X-Intercepted-Cc`, and `X-Intercepted-Bcc` added. 32 | 33 | ## Configuration options and examples 34 | 35 | Deliver intercepted email to multiple email addresses: 36 | 37 | ```ruby 38 | Mail.register_interceptor( 39 | RecipientInterceptor.new(["one@example.com", "two@example.com"]) 40 | ) 41 | ``` 42 | 43 | Use a comma-delimited string: 44 | 45 | ```ruby 46 | Mail.register_interceptor( 47 | RecipientInterceptor.new("one@example.com,two@example.com") 48 | ) 49 | ``` 50 | 51 | Use an environment variable: 52 | 53 | ```ruby 54 | # heroku config:set EMAIL_RECIPIENTS="one@example.com,two@example.com" --app staging 55 | Mail.register_interceptor( 56 | RecipientInterceptor.new(ENV["EMAIL_RECIPIENTS"]) 57 | ) 58 | ``` 59 | 60 | Prefix the subject line with static text: 61 | 62 | ```ruby 63 | Mail.register_interceptor( 64 | RecipientInterceptor.new( 65 | ENV["EMAIL_RECIPIENTS"], 66 | subject_prefix: "[staging]", 67 | ), 68 | ) 69 | ``` 70 | 71 | Prefix the subject line with contents from the original message: 72 | 73 | ```ruby 74 | Mail.register_interceptor( 75 | RecipientInterceptor.new( 76 | ENV["EMAIL_RECIPIENTS"], 77 | subject_prefix: proc { |msg| "[staging] [#{(msg.to + msg.cc + msg.bcc).sort.join(",")}]" } 78 | ), 79 | ) 80 | ``` 81 | 82 | The object passed to the proc is an instance of 83 | [`Mail::Message`](https://www.rubydoc.info/github/mikel/mail/Mail/Message). 84 | 85 | ## Alternatives 86 | 87 | - [Postmark's Sandbox mode](https://postmarkapp.com/developer/user-guide/sandbox-mode/server-sandbox-mode) 88 | 89 | ## Contributing 90 | 91 | Fork the repo. 92 | 93 | ``` 94 | bundle 95 | bundle exec rspec 96 | ``` 97 | 98 | Make a change. 99 | Run tests. 100 | Open a pull request. 101 | Discuss/address any feedback with maintainer. 102 | Maintainer will merge. 103 | -------------------------------------------------------------------------------- /lib/recipient_interceptor.rb: -------------------------------------------------------------------------------- 1 | require "mail" 2 | 3 | class RecipientInterceptor 4 | def initialize(recipients, opts = {}) 5 | @recipients = if recipients.respond_to?(:split) 6 | recipients.split(",") 7 | else 8 | recipients 9 | end 10 | 11 | @subject_prefix = opts[:subject_prefix] 12 | end 13 | 14 | def delivering_email(msg) 15 | if @subject_prefix.respond_to?(:call) 16 | msg.subject = "#{@subject_prefix.call(msg)} #{msg.subject}" 17 | elsif @subject_prefix 18 | msg.subject = "#{@subject_prefix} #{msg.subject}" 19 | end 20 | 21 | msg.header["X-Intercepted-To"] = msg.to || [] 22 | msg.header["X-Intercepted-Cc"] = msg.cc || [] 23 | msg.header["X-Intercepted-Bcc"] = msg.bcc || [] 24 | 25 | msg.to = @recipients 26 | msg.cc = nil if msg.cc 27 | msg.bcc = nil if msg.bcc 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /recipient_interceptor.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.add_dependency "mail" 3 | spec.authors = ["Dan Croak"] 4 | 5 | spec.description = <<-STRING 6 | Avoid emailing your users from non-production environments. 7 | STRING 8 | 9 | spec.files = ["lib/recipient_interceptor.rb"] 10 | spec.homepage = "http://github.com/croaky/recipient_interceptor" 11 | spec.license = "MIT" 12 | spec.name = "recipient_interceptor" 13 | spec.require_paths = ["lib"] 14 | spec.summary = "Intercept recipients when delivering email with the Mail gem." 15 | spec.version = "0.3.3" 16 | end 17 | -------------------------------------------------------------------------------- /spec/recipient_interceptor_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "..", "lib", "recipient_interceptor") 2 | 3 | describe RecipientInterceptor do 4 | before do 5 | Mail.defaults do 6 | delivery_method :test 7 | end 8 | 9 | module Mail 10 | @@delivery_interceptors = [] 11 | end 12 | end 13 | 14 | it "overrides to/cc/bcc fields and adds custom headers" do 15 | Mail.register_interceptor( 16 | RecipientInterceptor.new("staging@example.com") 17 | ) 18 | 19 | mail = Mail.deliver { 20 | from "from@example.com" 21 | to "to@example.com" 22 | cc "cc@example.com" 23 | bcc "bcc@example.com" 24 | subject "some subject" 25 | } 26 | 27 | expect(mail.to).to eq ["staging@example.com"] 28 | expect(mail.cc).to eq nil 29 | expect(mail.bcc).to eq nil 30 | expect(mail.header["X-Intercepted-To"].to_s).to eq "to@example.com" 31 | expect(mail.header["X-Intercepted-Cc"].to_s).to eq "cc@example.com" 32 | expect(mail.header["X-Intercepted-Bcc"].to_s).to eq "bcc@example.com" 33 | end 34 | 35 | it "overrides to/cc/bcc even if they were already missing" do 36 | Mail.register_interceptor( 37 | RecipientInterceptor.new("staging@example.com") 38 | ) 39 | 40 | mail = Mail.deliver { 41 | from "from@example.com" 42 | to "to@example.com" 43 | } 44 | 45 | expect(mail.to).to eq ["staging@example.com"] 46 | expect(mail.cc).to eq nil 47 | expect(mail.bcc).to eq nil 48 | end 49 | 50 | it "accepts an array of recipients" do 51 | Mail.register_interceptor( 52 | RecipientInterceptor.new(["one@example.com", "two@example.com"]) 53 | ) 54 | 55 | mail = Mail.deliver { 56 | from "from@example.com" 57 | to "to@example.com" 58 | cc "cc@example.com" 59 | bcc "bcc@example.com" 60 | subject "some subject" 61 | } 62 | 63 | expect(mail.to).to eq ["one@example.com", "two@example.com"] 64 | end 65 | 66 | it "accepts a comma-delimited list of recipients" do 67 | Mail.register_interceptor( 68 | RecipientInterceptor.new("one@example.com,two@example.com") 69 | ) 70 | 71 | mail = Mail.deliver { 72 | from "from@example.com" 73 | to "to@example.com" 74 | subject "some subject" 75 | } 76 | 77 | expect(mail.to).to eq ["one@example.com", "two@example.com"] 78 | end 79 | 80 | it "does not prefix subject by default" do 81 | Mail.register_interceptor( 82 | RecipientInterceptor.new("staging@example.com") 83 | ) 84 | 85 | mail = Mail.deliver { 86 | from "from@example.com" 87 | to "to@example.com" 88 | subject "some subject" 89 | } 90 | 91 | expect(mail.subject).to eq "some subject" 92 | end 93 | 94 | it "prefixes subject with string" do 95 | Mail.register_interceptor( 96 | RecipientInterceptor.new( 97 | "staging@example.com", 98 | subject_prefix: "[staging]" 99 | ) 100 | ) 101 | 102 | mail = Mail.deliver { 103 | from "from@example.com" 104 | to "to@example.com" 105 | subject "some subject" 106 | } 107 | 108 | expect(mail.subject).to eq "[staging] some subject" 109 | end 110 | 111 | it "prefixes subject with proc" do 112 | Mail.register_interceptor( 113 | RecipientInterceptor.new( 114 | "staging@example.com", 115 | subject_prefix: proc { |msg| "[staging] [#{(msg.to + msg.cc + msg.bcc).sort.join(",")}]" } 116 | ) 117 | ) 118 | 119 | mail = Mail.deliver { 120 | from "from@example.com" 121 | to ["to1@example.com", "to2@example.com"] 122 | cc "cc@example.com" 123 | bcc "bcc@example.com" 124 | subject "some subject" 125 | } 126 | 127 | expect(mail.subject).to eq "[staging] [bcc@example.com,cc@example.com,to1@example.com,to2@example.com] some subject" 128 | end 129 | end 130 | --------------------------------------------------------------------------------