├── .github ├── dependabot.yml └── workflows │ └── ruby.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin └── http_post ├── incoming.gemspec ├── lib ├── incoming.rb └── incoming │ ├── strategies │ ├── cloudmailin.rb │ ├── http_post.rb │ ├── mailgun.rb │ ├── mandrill.rb │ ├── postmark.rb │ └── sendgrid.rb │ └── strategy.rb └── spec ├── fixtures ├── hello.txt ├── hullo.txt ├── mailgun.env ├── mailgun.raw.env ├── mandrill.env ├── notification.eml └── postmark.env ├── incoming ├── strategies │ ├── cloudmailin_spec.rb │ ├── http_post_spec.rb │ ├── mailgun_spec.rb │ ├── mandrill_spec.rb │ ├── postmark_spec.rb │ └── sendgrid_spec.rb └── strategy_spec.rb ├── integration_spec.rb ├── recorder.rb └── spec_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "09:00" 8 | timezone: America/Los_Angeles 9 | open-pull-requests-limit: 99 10 | assignees: 11 | - joshuap 12 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ main ] 13 | pull_request: 14 | branches: [ main ] 15 | 16 | jobs: 17 | test: 18 | 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | ruby-version: ['2.6', '2.7', '3.0'] 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Set up Ruby 27 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 28 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 29 | # uses: ruby/setup-ruby@v1 30 | uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e 31 | with: 32 | ruby-version: ${{ matrix.ruby-version }} 33 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 34 | - name: Run tests 35 | run: bundle exec rake 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | tmp/ 5 | spec/fixtures/records/favicon.ico.env 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - 2.4 5 | - 2.5 6 | - 2.6 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.2.0] - 2019-11-27 10 | ### Added 11 | - Bumped all dependencies. 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development, :test do 6 | gem 'guard' 7 | gem 'guard-rspec' 8 | gem 'rb-fsevent', '~> 0.11' 9 | end 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | incoming (0.2.0) 5 | mail (~> 2.4) 6 | rack 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | coderay (1.1.3) 12 | date (3.3.3) 13 | diff-lcs (1.5.1) 14 | ffi (1.15.5) 15 | formatador (1.1.0) 16 | guard (2.18.1) 17 | formatador (>= 0.2.4) 18 | listen (>= 2.7, < 4.0) 19 | lumberjack (>= 1.0.12, < 2.0) 20 | nenv (~> 0.1) 21 | notiffany (~> 0.0) 22 | pry (>= 0.13.0) 23 | shellany (~> 0.0) 24 | thor (>= 0.18.1) 25 | guard-compat (1.2.1) 26 | guard-rspec (4.7.3) 27 | guard (~> 2.1) 28 | guard-compat (~> 1.1) 29 | rspec (>= 2.99.0, < 4.0) 30 | listen (3.8.0) 31 | rb-fsevent (~> 0.10, >= 0.10.3) 32 | rb-inotify (~> 0.9, >= 0.9.10) 33 | lumberjack (1.2.9) 34 | mail (2.8.1) 35 | mini_mime (>= 0.1.1) 36 | net-imap 37 | net-pop 38 | net-smtp 39 | method_source (1.0.0) 40 | mini_mime (1.1.2) 41 | nenv (0.3.0) 42 | net-imap (0.3.4) 43 | date 44 | net-protocol 45 | net-pop (0.1.2) 46 | net-protocol 47 | net-protocol (0.2.1) 48 | timeout 49 | net-smtp (0.3.3) 50 | net-protocol 51 | notiffany (0.1.3) 52 | nenv (~> 0.1) 53 | shellany (~> 0.0) 54 | pry (0.14.2) 55 | coderay (~> 1.1) 56 | method_source (~> 1.0) 57 | rack (3.1.3) 58 | rake (13.2.1) 59 | rb-fsevent (0.11.2) 60 | rb-inotify (0.10.1) 61 | ffi (~> 1.0) 62 | rspec (3.13.0) 63 | rspec-core (~> 3.13.0) 64 | rspec-expectations (~> 3.13.0) 65 | rspec-mocks (~> 3.13.0) 66 | rspec-core (3.13.0) 67 | rspec-support (~> 3.13.0) 68 | rspec-expectations (3.13.0) 69 | diff-lcs (>= 1.2.0, < 2.0) 70 | rspec-support (~> 3.13.0) 71 | rspec-its (1.3.0) 72 | rspec-core (>= 3.0.0) 73 | rspec-expectations (>= 3.0.0) 74 | rspec-mocks (3.13.0) 75 | diff-lcs (>= 1.2.0, < 2.0) 76 | rspec-support (~> 3.13.0) 77 | rspec-support (3.13.0) 78 | shellany (0.0.1) 79 | thor (1.2.2) 80 | timeout (0.3.1) 81 | 82 | PLATFORMS 83 | ruby 84 | 85 | DEPENDENCIES 86 | guard 87 | guard-rspec 88 | incoming! 89 | rake 90 | rb-fsevent (~> 0.11) 91 | rspec 92 | rspec-its 93 | 94 | BUNDLED WITH 95 | 1.17.2 96 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rspec', :cli => '--fail-fast', :all_after_pass => false do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec" } 5 | end 6 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2019 Honeybadger Industries LLC 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Incoming! 2 | ----------- 3 | 4 | ### Receive email in your Rack apps. 5 | 6 | Incoming! receives a `Rack::Request` and hands you a [`Mail::Message`](https://github.com/mikel/mail/), much 7 | like `ActionMailer::Base.receive` does with a raw email. We currently 8 | support the following services: 9 | 10 | * SendGrid 11 | * Mailgun 12 | * Postmark 13 | * CloudMailin 14 | * Mandrill 15 | * Any mail server capable of routing messages to a system command 16 | 17 | Brought to you by :zap: **Honeybadger.io**, painless [Rails exception tracking](https://www.honeybadger.io/). 18 | 19 | [![Build Status](https://travis-ci.org/honeybadger-io/incoming.png)](https://travis-ci.org/honeybadger-io/incoming) 20 | [![Gem Version](https://badge.fury.io/rb/incoming.png)](http://badge.fury.io/rb/incoming) 21 | 22 | ## Installation 23 | 24 | 1. Add Incoming! to your Gemfile and run `bundle install`: 25 | 26 | ```ruby 27 | gem "incoming" 28 | ``` 29 | 30 | 2. Create a new class to receive emails (see examples below) 31 | 32 | 3. Implement an HTTP endpoint to receive HTTP post hooks, and pass the 33 | request to your receiver. (see examples below) 34 | 35 | ## SendGrid Example 36 | 37 | ```ruby 38 | class EmailReceiver < Incoming::Strategies::SendGrid 39 | def receive(mail) 40 | %(Got message from #{mail.to.first} with subject "#{mail.subject}") 41 | end 42 | end 43 | 44 | req = Rack::Request.new(env) 45 | result = EmailReceiver.receive(req) # => Got message from whoever@wherever.com with subject "hello world" 46 | ``` 47 | 48 | [Sendgrid API reference](http://sendgrid.com/docs/API_Reference/Webhooks/parse.html) 49 | 50 | ## Mailgun Example 51 | 52 | ```ruby 53 | class EmailReceiver < Incoming::Strategies::Mailgun 54 | setup api_key: "asdf" 55 | 56 | def receive(mail) 57 | %(Got message from #{mail.to.first} with subject "#{mail.subject}") 58 | end 59 | end 60 | 61 | req = Rack::Request.new(env) 62 | result = EmailReceiver.receive(req) # => Got message from whoever@wherever.com with subject "hello world" 63 | ``` 64 | 65 | [Mailgun API reference](http://documentation.mailgun.net/user_manual.html#receiving-messages) 66 | 67 | ## Postmark Example 68 | 69 | ```ruby 70 | class EmailReceiver < Incoming::Strategies::Postmark 71 | def receive(mail) 72 | %(Got message from #{mail.to.first} with subject "#{mail.subject}") 73 | end 74 | end 75 | 76 | req = Rack::Request.new(env) 77 | result = EmailReceiver.receive(req) # => Got message from whoever@wherever.com with subject "hello world" 78 | ``` 79 | 80 | [Postmark API reference](http://developer.postmarkapp.com/developer-inbound.html) 81 | 82 | ## CloudMailin Example 83 | 84 | Use the Raw Format when setting up your address target. 85 | 86 | ```ruby 87 | class EmailReceiver < Incoming::Strategies::CloudMailin 88 | def receive(mail) 89 | %(Got message from #{mail.to.first} with subject "#{mail.subject}") 90 | end 91 | end 92 | 93 | req = Rack::Request.new(env) 94 | result = EmailReceiver.receive(req) # => Got message from whoever@wherever.com with subject "hello world" 95 | ``` 96 | 97 | [CloudMailin API reference](http://docs.cloudmailin.com/http_post_formats/) 98 | 99 | ## Mandrill Example 100 | 101 | Mandrill is capable of sending multiple events in a single webhook, so 102 | the Mandrill strategy works a bit differently than the others. Namely, 103 | the `.receive` method returns an Array of return values from your 104 | `#receive` method for each inbound event in the payload. Otherwise, the 105 | implementation is the same: 106 | 107 | ```ruby 108 | class EmailReceiver < Incoming::Strategies::Mandrill 109 | def receive(mail) 110 | %(Got message from #{mail.to.first} with subject "#{mail.subject}") 111 | end 112 | end 113 | 114 | req = Rack::Request.new(env) 115 | result = EmailReceiver.receive(req) # => Got message from whoever@wherever.com with subject "hello world" 116 | ``` 117 | 118 | [Mandrill API reference](http://help.mandrill.com/entries/22092308-What-is-the-format-of-inbound-email-webhooks-) 119 | 120 | ## Postfix Example 121 | 122 | ```ruby 123 | class EmailReceiver < Incoming::Strategies::HTTPPost 124 | setup secret: "6d7e5337a0cd69f52c3fcf9f5af438b1" 125 | 126 | def receive(mail) 127 | %(Got message from #{mail.to.first} with subject "#{mail.subject}") 128 | end 129 | end 130 | 131 | req = Rack::Request.new(env) 132 | result = EmailReceiver.receive(req) # => Got message from whoever@wherever.com with subject "hello world" 133 | ``` 134 | 135 | ``` 136 | # /etc/postfix/virtual 137 | @example.com http_post 138 | 139 | # /etc/mail/aliases 140 | http_post: "|http_post -s 6d7e5337a0cd69f52c3fcf9f5af438b1 http://www.example.com/emails" 141 | ``` 142 | ## Qmail Example: 143 | 144 | ```ruby 145 | class EmailReceiver < Incoming::Strategies::HTTPPost 146 | setup secret: "6d7e5337a0cd69f52c3fcf9f5af438b1" 147 | 148 | def receive(mail) 149 | %(Got message from #{mail.to.first} with subject "#{mail.subject}") 150 | end 151 | end 152 | 153 | req = Rack::Request.new(env) 154 | result = EmailReceiver.receive(req) # => Got message from whoever@wherever.com with subject "hello world" 155 | ``` 156 | 157 | To setup a *global* incoming email alias: 158 | 159 | ``` 160 | # /var/qmail/alias/.qmail-whoever - mails to whoever@ will be delivered to this alias. 161 | |http_post -s 6d7e5337a0cd69f52c3fcf9f5af438b1 http://www.example.com/emails 162 | ``` 163 | 164 | Domain-specific incoming aliases can be set as follows: 165 | 166 | ``` 167 | #/var/qmail/control/virtualdomains 168 | example.com:example 169 | 170 | #~example/.qmail-whoever 171 | |http_post -s 6d7e5337a0cd69f52c3fcf9f5af438b1 http://www.example.com/emails 172 | ``` 173 | Now mails to `whoever@example.com` will be posted to the corresponding URL above. To post all mails for `example.com`, just add the above line to `~example/.qmail-default`. 174 | 175 | ## Example Rails Controller 176 | 177 | ```ruby 178 | # app/controllers/emails_controller.rb 179 | class EmailsController < ActionController::Base 180 | def create 181 | if EmailReceiver.receive(request) 182 | render json: { status: "ok" } 183 | else 184 | render json: { status: "rejected" }, status: 403 185 | end 186 | end 187 | end 188 | ``` 189 | 190 | ```ruby 191 | # config/routes.rb 192 | Rails.application.routes.draw do 193 | post "/emails" => "emails#create" 194 | end 195 | ``` 196 | 197 | ```ruby 198 | # spec/controllers/emails_controller_spec.rb 199 | require "spec_helper" 200 | 201 | describe EmailsController, "#create" do 202 | it "responds with success when request is valid" do 203 | allow(EmailReceiver).to receive(:receive).and_return(true) 204 | post :create 205 | expect(response.success?).to eq(true) 206 | expect(response.body).to eq(%({"status":"ok"})) 207 | end 208 | 209 | it "responds with 403 when request is invalid" do 210 | allow(EmailReceiver).to receive(:receive).and_return(false) 211 | post :create 212 | expect(response.status).to eq(403) 213 | expect(response.body).to eq(%({"status":"rejected"})) 214 | end 215 | end 216 | ``` 217 | 218 | ## TODO 219 | 220 | 1. Provide authentication for all strategies where possible (currently 221 | only Mailgun requests are authenticated.) 222 | 223 | ## Contributing 224 | 225 | 1. Fork it. 226 | 2. Create a topic branch `git checkout -b my_branch` 227 | 3. Commit your changes `git commit -am "Boom"` 228 | 3. Push to your branch `git push origin my_branch` 229 | 4. Send a [pull request](https://github.com/honeybadger-io/incoming/pulls) 230 | 231 | ## License 232 | 233 | Incoming! is free software, and may be redistributed under the terms specified 234 | in the MIT-LICENSE file. 235 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | require 'date' 9 | 10 | require 'rspec/core/rake_task' 11 | RSpec::Core::RakeTask.new(:spec) 12 | task :default => :spec 13 | 14 | ############################################################################# 15 | # 16 | # Helper functions 17 | # 18 | ############################################################################# 19 | 20 | def name 21 | @name ||= Dir['*.gemspec'].first.split('.').first 22 | end 23 | 24 | def version 25 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 26 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 27 | end 28 | 29 | def date 30 | Date.today.to_s 31 | end 32 | 33 | def gemspec_file 34 | "#{name}.gemspec" 35 | end 36 | 37 | def gem_file 38 | "#{name}-#{version}.gem" 39 | end 40 | 41 | def replace_header(head, header_name) 42 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 43 | end 44 | 45 | ############################################################################# 46 | # 47 | # Packaging tasks 48 | # 49 | ############################################################################# 50 | 51 | desc "Create tag v#{version} and build and push #{gem_file} to Rubygems" 52 | task :release => :build do 53 | unless `git branch` =~ /^\* master$/ 54 | puts "You must be on the master branch to release!" 55 | exit! 56 | end 57 | sh "git commit --allow-empty -a -m 'Release #{version}'" 58 | sh "git tag v#{version}" 59 | sh "git push origin master" 60 | sh "git push origin v#{version}" 61 | sh "gem push pkg/#{name}-#{version}.gem" 62 | end 63 | 64 | desc "Build #{gem_file} into the pkg directory" 65 | task :build => :gemspec do 66 | sh "mkdir -p pkg" 67 | sh "gem build #{gemspec_file}" 68 | sh "mv #{gem_file} pkg" 69 | end 70 | 71 | desc "Generate #{gemspec_file}" 72 | task :gemspec => :validate do 73 | # read spec file and split out manifest section 74 | spec = File.read(gemspec_file) 75 | 76 | # replace name version and date 77 | replace_header(spec, :name) 78 | replace_header(spec, :version) 79 | replace_header(spec, :date) 80 | 81 | # piece file back together and write 82 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 83 | puts "Updated #{gemspec_file}" 84 | end 85 | 86 | desc "Validate #{gemspec_file}" 87 | task :validate do 88 | libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}", "lib/tasks", "lib/generators"] 89 | unless libfiles.empty? 90 | puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir." 91 | exit! 92 | end 93 | unless Dir['VERSION*'].empty? 94 | puts "A `VERSION` file at root level violates Gem best practices." 95 | exit! 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /bin/http_post: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'incoming' 4 | require 'net/http' 5 | require 'uri' 6 | require 'openssl' 7 | require 'securerandom' 8 | require 'optparse' 9 | 10 | options = { 11 | secret: nil 12 | } 13 | 14 | OptionParser.new do |opts| 15 | opts.banner = "Usage: http_post [options] [http endpoint] < input" 16 | opts.on('-s', '--secret [secret]') { |s| options[:secret] = s } 17 | 18 | opts.parse! 19 | end 20 | 21 | begin 22 | http_endpoint = URI.parse(ARGV[0]) 23 | rescue URI::InvalidURIError 24 | puts 'Unable to parse http endpoint. Is it a valid URI?' 25 | exit 26 | end 27 | 28 | timestamp = Time.now.to_i 29 | token = SecureRandom.hash 30 | signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('SHA256'), options[:secret], [timestamp, token].join) 31 | 32 | data = { :message => STDIN.read, :timestamp => timestamp, :token => token, :signature => signature } 33 | headers = {} 34 | 35 | req = Net::HTTP::Post.new(http_endpoint.path, headers) 36 | req.form_data = data 37 | req.basic_auth http_endpoint.user, http_endpoint.password if http_endpoint.user 38 | 39 | resp, data = Net::HTTP.new(http_endpoint.host, http_endpoint.port).start {|http| 40 | http.request(req) 41 | } 42 | 43 | puts resp.body 44 | -------------------------------------------------------------------------------- /incoming.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | # The following lines are updated automatically by `rake gemspec` 3 | s.name = 'incoming' 4 | s.version = '0.2.0' 5 | s.date = '2019-11-27' 6 | 7 | s.summary = 'Incoming! helps you receive email in your Rack apps.' 8 | s.description = 'Incoming! standardizes various mail parse apis, making it a snap to receive emails through HTTP post hooks.' 9 | 10 | s.authors = ['Joshua Wood'] 11 | s.email = ['josh@honeybadger.io'] 12 | s.homepage = 'https://github.com/honeybadger-io/incoming' 13 | 14 | s.files = Dir['{app,config,db,lib}/**/*'] + ['MIT-LICENSE', 'Rakefile', 'README.md'] 15 | s.require_paths = %w[lib] 16 | 17 | s.executables << 'http_post' 18 | 19 | s.add_dependency 'rack' 20 | s.add_dependency 'mail', '~> 2.4' 21 | 22 | s.add_development_dependency 'rake' 23 | s.add_development_dependency 'rspec' 24 | s.add_development_dependency 'rspec-its' 25 | end 26 | -------------------------------------------------------------------------------- /lib/incoming.rb: -------------------------------------------------------------------------------- 1 | require 'mail' 2 | require 'incoming/strategy' 3 | 4 | module Incoming 5 | VERSION = '0.2.0' 6 | 7 | module Strategies 8 | autoload :Mandrill, 'incoming/strategies/mandrill' 9 | autoload :SendGrid, 'incoming/strategies/sendgrid' 10 | autoload :Mailgun, 'incoming/strategies/mailgun' 11 | autoload :Postmark, 'incoming/strategies/postmark' 12 | autoload :CloudMailin, 'incoming/strategies/cloudmailin' 13 | autoload :HTTPPost, 'incoming/strategies/http_post' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/incoming/strategies/cloudmailin.rb: -------------------------------------------------------------------------------- 1 | module Incoming 2 | module Strategies 3 | class CloudMailin 4 | include Incoming::Strategy 5 | 6 | def initialize(request) 7 | @message = Mail.new(request.params['message']) 8 | end 9 | end 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/incoming/strategies/http_post.rb: -------------------------------------------------------------------------------- 1 | module Incoming 2 | module Strategies 3 | class HTTPPost 4 | include Incoming::Strategy 5 | 6 | option :secret 7 | 8 | attr_accessor :signature, :token, :timestamp 9 | 10 | def initialize(request) 11 | params = request.params 12 | 13 | @signature = params['signature'] 14 | @token = params['token'] 15 | @timestamp = params['timestamp'] 16 | @message = Mail.new(params['message']) 17 | end 18 | 19 | def authenticate 20 | hexdigest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), self.class.default_options[:secret], [timestamp, token].join) 21 | hexdigest.eql?(signature) or false 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/incoming/strategies/mailgun.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Incoming 4 | module Strategies 5 | # Public: MailGun Incoming! Mail Strategy 6 | # 7 | # API Documentation: 8 | # http://documentation.mailgun.net/user_manual.html#receiving-messages 9 | # 10 | # Examples: 11 | # 12 | # class MailReceiver < Incoming::Strategies::Mailgun 13 | # setup api_key: 'asdf' 14 | # 15 | # def receive(mail) 16 | # puts "Got message from mailgun: #{mail.subject}" 17 | # end 18 | # end 19 | class Mailgun 20 | include Incoming::Strategy 21 | 22 | # Mailgun API key 23 | option :api_key 24 | 25 | # Use the stripped- parameters from the Mailgun API (strips out quoted parts and signatures) 26 | option :stripped, false 27 | 28 | attr_accessor :signature, :token, :timestamp 29 | 30 | def initialize(request) 31 | params = request.params 32 | 33 | if self.class.default_options[:api_key].nil? 34 | raise RequiredOptionError.new(':api_key option is required.') 35 | end 36 | 37 | @signature = params['signature'] 38 | @token = params['token'] 39 | @timestamp = params['timestamp'] 40 | 41 | html_content = params[ self.class.default_options[:stripped] ? 'stripped-html' : 'body-html' ] 42 | text_content = params[ self.class.default_options[:stripped] ? 'stripped-text' : 'body-plain' ] 43 | 44 | if self.class.default_options[:stripped] && text_content.to_s == '' 45 | html_content = params['body-html'] 46 | text_content = params['body-plain'] 47 | end 48 | 49 | attachments = 1.upto(params['attachment-count'].to_i).map do |num| 50 | attachment_from_params(params["attachment-#{num}"]) 51 | end 52 | 53 | @message = Mail.new do 54 | headers Hash[JSON.parse(params['message-headers'])] 55 | 56 | body text_content 57 | 58 | html_part do 59 | content_type 'text/html; charset=UTF-8' 60 | body html_content 61 | end if html_content 62 | 63 | attachments.each do |attachment| 64 | add_file(attachment) 65 | end 66 | end 67 | end 68 | 69 | def authenticate 70 | api_key = self.class.default_options[:api_key] 71 | 72 | hexdigest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), api_key, [timestamp, token].join) 73 | hexdigest.eql?(signature) or false 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/incoming/strategies/mandrill.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Incoming 4 | module Strategies 5 | class Mandrill 6 | include Incoming::Strategy 7 | 8 | def self.receive(request) 9 | JSON.parse(request.params['mandrill_events']).map do |event| 10 | next unless event['event'] == 'inbound' 11 | result = super(event['msg']) 12 | yield(result) if block_given? 13 | result 14 | end.compact 15 | end 16 | 17 | def initialize(msg) 18 | @message = Mail.new(msg['raw_msg']) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/incoming/strategies/postmark.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'base64' 3 | 4 | module Incoming 5 | module Strategies 6 | class Postmark 7 | include Incoming::Strategy 8 | 9 | def initialize(request) 10 | source = JSON.parse(request.body.read) 11 | 12 | headers = parse_headers(source['Headers']) 13 | from = parse_address(source['FromFull']) 14 | to = source['ToFull'].map{|h| parse_address(h)} 15 | cc = source['CcFull'].map{|h| parse_address(h)} 16 | 17 | @message = Mail.new do 18 | headers headers 19 | from from 20 | to to 21 | cc cc 22 | reply_to source['ReplyTo'] 23 | subject source['Subject'] 24 | date source['Date'] 25 | 26 | body source['TextBody'] 27 | 28 | html_part do 29 | content_type 'text/html; charset=UTF-8' 30 | body CGI.unescapeHTML(source['HtmlBody']) 31 | end if source['HtmlBody'] 32 | 33 | source['Attachments'].each do |a| 34 | add_file :filename => a['Name'], :content => Base64.decode64(a['Content']) 35 | end 36 | 37 | %w(MailboxHash MessageID Tag).each do |key| 38 | if source[key] =~ /\S/ 39 | header[key] = source[key] 40 | end 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | def parse_headers(source) 48 | source.inject({}){|hash,obj| hash[obj['Name']] = obj['Value']; hash} 49 | end 50 | 51 | def parse_address(hash) 52 | Mail::Address.new(hash['Email']).tap do |address| 53 | address.display_name = hash['Name'] 54 | end.to_s 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/incoming/strategies/sendgrid.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'mail/utilities' 3 | 4 | module Incoming 5 | module Strategies 6 | class SendGrid 7 | include Incoming::Strategy 8 | 9 | def initialize(request) 10 | params = request.params.dup 11 | 12 | encodings = JSON.parse(params['charsets']) 13 | 14 | attachments = 1.upto(params['attachments'].to_i).map do |num| 15 | attachment_from_params(params["attachment#{num}"]) 16 | end 17 | 18 | @message = Mail.new do 19 | header params['headers'] 20 | header['Content-Transfer-Encoding'] = nil 21 | 22 | if Mail::Utilities.blank?(encodings['text']) 23 | body params['text'] 24 | else 25 | body params['text'].force_encoding(encodings['text']).encode('UTF-8') 26 | end 27 | 28 | html_part do 29 | content_type 'text/html; charset=UTF-8' 30 | if Mail::Utilities.blank?(encodings['html']) 31 | body params['html'] 32 | else 33 | body params['html'].force_encoding(encodings['html']).encode('UTF-8') 34 | end 35 | end if params['html'] 36 | 37 | attachments.each do |attachment| 38 | add_file(attachment) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/incoming/strategy.rb: -------------------------------------------------------------------------------- 1 | module Incoming 2 | module Strategy 3 | def self.included(base) 4 | base.extend ClassMethods 5 | base.class_eval do 6 | attr_reader :options 7 | end 8 | end 9 | 10 | class RequiredOptionError < StandardError ; end 11 | class InvalidOptionError < StandardError ; end 12 | 13 | module ClassMethods 14 | # Public: Global receiver 15 | # 16 | # args - Arguments used to initialize strategy. Should be defined 17 | # by `initialize` method in strategy class. 18 | # 19 | # Returns nothing 20 | def receive(*args) 21 | strategy = new(*args) 22 | strategy.authenticate and strategy.receive(strategy.message) 23 | end 24 | 25 | # Public 26 | # Returns an inherited set of default options set at the class-level 27 | # for each strategy. 28 | def default_options 29 | return @default_options if @default_options 30 | @default_options = superclass.respond_to?(:default_options) ? superclass.default_options : {} 31 | end 32 | 33 | # Public: Defines a valid class-level option for strategy 34 | # 35 | # Examples: 36 | # 37 | # class Incoming::Strategies::MyStrategy 38 | # include Incoming::Strategy 39 | # option :api_key 40 | # option :mime, false 41 | # end 42 | # 43 | # Returns nothing 44 | def option(name, value = nil) 45 | default_options[name] = value 46 | end 47 | 48 | # Public: Configures strategy-specific options. 49 | # 50 | # opts - A hash of valid options. 51 | # 52 | # Examples: 53 | # 54 | # class MailReceiver < Incoming::Strategies::MyStrategy 55 | # setup api_key: 'asdf', mime: true 56 | # end 57 | # 58 | # Returns nothing 59 | def setup(opts = {}) 60 | opts.keys.each do |key| 61 | next if default_options.keys.include?(key) 62 | raise InvalidOptionError.new(":#{key} is not a valid option for #{self.superclass.name}.") 63 | end 64 | 65 | @default_options = default_options.merge(opts) 66 | end 67 | end 68 | 69 | # Public: A Mail::Message object, constructed by #initialize 70 | attr_accessor :message 71 | 72 | # Public: Translates arguments into a Mail::Message object 73 | def initialize(*args) ; end 74 | 75 | # Public: Evaluates message and performs appropriate action. 76 | # Override in subclass 77 | # 78 | # mail - A Mail::Message object 79 | # 80 | # Returns nothing 81 | def receive(mail) 82 | raise NotImplementedError.new('You must implement #receive') 83 | end 84 | 85 | # Public: Authenticates request before performing #receive 86 | # 87 | # Examples: 88 | # 89 | # class MailReceiver < Incoming::Strategies::MyStrategy 90 | # def initialize(request) 91 | # @secret_word = request.params['secret_word'] 92 | # end 93 | # 94 | # def protected 95 | # @secret_word == 'my secret word' 96 | # end 97 | # end 98 | # 99 | # Returns true by default 100 | def authenticate 101 | true 102 | end 103 | 104 | protected 105 | 106 | # Protected: Normalize file from params 107 | # 108 | # uploaded_file_or_hash - ActionController::UploadedFile, 109 | # ActionDispatch::Http::UploadedFile, or Hash 110 | # 111 | # Returns Hash for Mail::Message#add_file 112 | def attachment_from_params(uploaded_file_or_hash) 113 | filename, content = if Hash === uploaded_file_or_hash 114 | [uploaded_file_or_hash[:filename], uploaded_file_or_hash[:tempfile].read] 115 | else 116 | [uploaded_file_or_hash.original_filename, uploaded_file_or_hash.read] 117 | end 118 | 119 | { 120 | :filename => filename, 121 | :content => content 122 | } 123 | end 124 | 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/fixtures/hello.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /spec/fixtures/hullo.txt: -------------------------------------------------------------------------------- 1 | hullo world 2 | -------------------------------------------------------------------------------- /spec/fixtures/mailgun.env: -------------------------------------------------------------------------------- 1 | {"I"CONTENT_LENGTH:ET" 6921I"CONTENT_TYPE;T"Gmultipart/form-data; boundary=bcba9a63-f2ab-4d7b-9c09-232af882a419I"GATEWAY_INTERFACE;TI" CGI/1.1;TI"PATH_INFO;TI" /mailgun;TI"QUERY_STRING;TI";TI"REMOTE_ADDR;T"127.0.0.1I"REMOTE_HOST;T"localhostI"REQUEST_METHOD;T" POSTI"REQUEST_URI;TI"&http://1e3851d0.ngrok.com/mailgun;TI"SCRIPT_NAME;TI";TI"SERVER_NAME;TI"1e3851d0.ngrok.com;TI"SERVER_PORT;TI"80;FI"SERVER_PROTOCOL;TI" HTTP/1.1;TI"SERVER_SOFTWARE;TI"*WEBrick/1.3.1 (Ruby/2.0.0/2013-06-27);TI"HTTP_HOST;T"1e3851d0.ngrok.comI"HTTP_X_REAL_IP;T"198.61.253.119I"HTTP_X_FORWARDED_PROTO;T" httpI"HTTP_CONNECTION;T" 2 | closeI"HTTP_ACCEPT_ENCODING;T" gzipI"HTTP_USER_AGENT;T"mailgun/treq-0.2.0I"rack.version;T[iiI"rack.input;T" --bcba9a63-f2ab-4d7b-9c09-232af882a419 3 | Content-Disposition: form-data; name="Content-Type" 4 | 5 | multipart/mixed; boundary="047d7bb043b060c69c04edd9f9da" 6 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 7 | Content-Disposition: form-data; name="Date" 8 | 9 | Wed, 18 Dec 2013 18:32:33 -0800 10 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 11 | Content-Disposition: form-data; name="From" 12 | 13 | Joshua Wood 14 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 15 | Content-Disposition: form-data; name="Message-Id" 16 | 17 | 18 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 19 | Content-Disposition: form-data; name="Mime-Version" 20 | 21 | 1.0 22 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 23 | Content-Disposition: form-data; name="Received" 24 | 25 | by luna.mailgun.net with SMTP mgrt 8786857200837; Thu, 19 Dec 2013 02:32:36 +0000 26 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 27 | Content-Disposition: form-data; name="Received" 28 | 29 | from mail-qe0-f50.google.com (mail-qe0-f50.google.com [209.85.128.50]) by mxa.mailgun.org with ESMTP id 52b25ac2.7ff7e854beb0-in3; Thu, 19 Dec 2013 02:32:34 -0000 (UTC) 30 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 31 | Content-Disposition: form-data; name="Received" 32 | 33 | by mail-qe0-f50.google.com with SMTP id 1so473903qec.37 for ; Wed, 18 Dec 2013 18:32:34 -0800 (PST) 34 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 35 | Content-Disposition: form-data; name="Received" 36 | 37 | by 10.229.247.1 with HTTP; Wed, 18 Dec 2013 18:32:33 -0800 (PST) 38 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 39 | Content-Disposition: form-data; name="Subject" 40 | 41 | Test subject 42 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 43 | Content-Disposition: form-data; name="To" 44 | 45 | testing@hinttest.mailgun.org, testing@example.com 46 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 47 | Content-Disposition: form-data; name="X-Envelope-From" 48 | 49 | 50 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 51 | Content-Disposition: form-data; name="X-Gm-Message-State" 52 | 53 | ALoCoQkO3w6279mvcJHnyPRQSq/LvDwvzTk4G94AA9GtrnVMOposg3VZ4CAHKLDca8t4t/AgShVQ 54 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 55 | Content-Disposition: form-data; name="X-Google-Dkim-Signature" 56 | 57 | v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:mime-version:date:message-id:subject:from:to :content-type; bh=ofZ56XAsnVDBfoCOAR2GQSCcIaf65gPLXCLeW32Nmv0=; b=g8IhesI+UfiHvwOyf13pB+hO78bFTxlWglO692+g2vST/MttSB0s9wolFe9epi2I38 gcWEIuuBH9doqk1iJlxVyr5TYTXNnds7aR9USNTgbnHl7mCioLY+tAccLfjv7Un3tCda INWLhBVWGx2yiMgLwbs1OT2YxXnlvIcghMZrYwpIfrX844LC1c9IbGQZcnfZvl7mE2H+ 4MkP0C43lTEud8n/utfHyUAEP6G9KM2FKtRqnqHcQSc3ovL7idLvSCaFMZVA05dFQRq2 xkztRGg2OuKzIInjPK9S9vTk3i2/aLVXWI8YbJu1RR8DQttoZeto+5gXgH39Q8TETAoW 2qAQ== 58 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 59 | Content-Disposition: form-data; name="X-Mailgun-Incoming" 60 | 61 | Yes 62 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 63 | Content-Disposition: form-data; name="X-Originating-Ip" 64 | 65 | [76.105.197.61] 66 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 67 | Content-Disposition: form-data; name="X-Received" 68 | 69 | by 10.49.38.37 with SMTP id d5mr59711437qek.17.1387420354389; Wed, 18 Dec 2013 18:32:34 -0800 (PST) 70 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 71 | Content-Disposition: form-data; name="attachment-count" 72 | 73 | 2 74 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 75 | Content-Disposition: form-data; name="body-html" 76 | 77 |
Test body.
78 | 79 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 80 | Content-Disposition: form-data; name="body-plain" 81 | 82 | Test body. 83 | 84 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 85 | Content-Disposition: form-data; name="from" 86 | 87 | Joshua Wood 88 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 89 | Content-Disposition: form-data; name="message-headers" 90 | 91 | [["Received", "by luna.mailgun.net with SMTP mgrt 8786857200837; Thu, 19 Dec 2013 02:32:36 +0000"], ["X-Envelope-From", ""], ["Received", "from mail-qe0-f50.google.com (mail-qe0-f50.google.com [209.85.128.50]) by mxa.mailgun.org with ESMTP id 52b25ac2.7ff7e854beb0-in3; Thu, 19 Dec 2013 02:32:34 -0000 (UTC)"], ["Received", "by mail-qe0-f50.google.com with SMTP id 1so473903qec.37 for ; Wed, 18 Dec 2013 18:32:34 -0800 (PST)"], ["X-Google-Dkim-Signature", "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:mime-version:date:message-id:subject:from:to :content-type; bh=ofZ56XAsnVDBfoCOAR2GQSCcIaf65gPLXCLeW32Nmv0=; b=g8IhesI+UfiHvwOyf13pB+hO78bFTxlWglO692+g2vST/MttSB0s9wolFe9epi2I38 gcWEIuuBH9doqk1iJlxVyr5TYTXNnds7aR9USNTgbnHl7mCioLY+tAccLfjv7Un3tCda INWLhBVWGx2yiMgLwbs1OT2YxXnlvIcghMZrYwpIfrX844LC1c9IbGQZcnfZvl7mE2H+ 4MkP0C43lTEud8n/utfHyUAEP6G9KM2FKtRqnqHcQSc3ovL7idLvSCaFMZVA05dFQRq2 xkztRGg2OuKzIInjPK9S9vTk3i2/aLVXWI8YbJu1RR8DQttoZeto+5gXgH39Q8TETAoW 2qAQ=="], ["X-Gm-Message-State", "ALoCoQkO3w6279mvcJHnyPRQSq/LvDwvzTk4G94AA9GtrnVMOposg3VZ4CAHKLDca8t4t/AgShVQ"], ["Mime-Version", "1.0"], ["X-Received", "by 10.49.38.37 with SMTP id d5mr59711437qek.17.1387420354389; Wed, 18 Dec 2013 18:32:34 -0800 (PST)"], ["Received", "by 10.229.247.1 with HTTP; Wed, 18 Dec 2013 18:32:33 -0800 (PST)"], ["X-Originating-Ip", "[76.105.197.61]"], ["Date", "Wed, 18 Dec 2013 18:32:33 -0800"], ["Message-Id", ""], ["Subject", "Test subject"], ["From", "Joshua Wood "], ["To", "testing@hinttest.mailgun.org, testing@example.com"], ["Content-Type", "multipart/mixed; boundary=\"047d7bb043b060c69c04edd9f9da\""], ["X-Mailgun-Incoming", "Yes"]] 92 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 93 | Content-Disposition: form-data; name="recipient" 94 | 95 | testing@hinttest.mailgun.org 96 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 97 | Content-Disposition: form-data; name="sender" 98 | 99 | josh@honeybadger.io 100 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 101 | Content-Disposition: form-data; name="signature" 102 | 103 | a54be88286992aa581c2c9770372023574d7c7eed41f70c3cb1e7c737a91f130 104 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 105 | Content-Disposition: form-data; name="stripped-html" 106 | 107 |
Test body.
108 | 109 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 110 | Content-Disposition: form-data; name="stripped-signature" 111 | 112 | 113 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 114 | Content-Disposition: form-data; name="stripped-text" 115 | 116 | Test body. 117 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 118 | Content-Disposition: form-data; name="subject" 119 | 120 | Test subject 121 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 122 | Content-Disposition: form-data; name="timestamp" 123 | 124 | 1387420357 125 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 126 | Content-Disposition: form-data; name="token" 127 | 128 | 4ez9rwzhuv7u90p20ujvioaf3dhmjlmjtwr178eubb8qvhrna3 129 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 130 | Content-Disposition: form-data; name="attachment-1"; filename="hello.txt" 131 | Content-Type: text/plain 132 | Content-Length: 12 133 | 134 | hello world 135 | 136 | --bcba9a63-f2ab-4d7b-9c09-232af882a419 137 | Content-Disposition: form-data; name="attachment-2"; filename="hullo.txt" 138 | Content-Type: text/plain 139 | Content-Length: 12 140 | 141 | hullo world 142 | 143 | --bcba9a63-f2ab-4d7b-9c09-232af882a419-- 144 | I"rack.multithread;TTI"rack.multiprocess;TFI"rack.run_once;TFI"rack.url_scheme;TI" http;TI"HTTP_VERSION;T@I"REQUEST_PATH;TI" /mailgun;TI"fixture_file_path;TI"@/Users/josh/code/incoming/spec/fixtures/records/mailgun.env;T -------------------------------------------------------------------------------- /spec/fixtures/mailgun.raw.env: -------------------------------------------------------------------------------- 1 | {"I"CONTENT_LENGTH:ET" 4636I"CONTENT_TYPE;T"&application/x-www-form-urlencodedI"GATEWAY_INTERFACE;TI" CGI/1.1;TI"PATH_INFO;TI"/mailgun.raw;TI"QUERY_STRING;TI";TI"REMOTE_ADDR;T"127.0.0.1I"REMOTE_HOST;T"localhostI"REQUEST_METHOD;T" POSTI"REQUEST_URI;TI"*http://57c3f4b7.ngrok.com/mailgun.raw;TI"SCRIPT_NAME;TI";TI"SERVER_NAME;TI"57c3f4b7.ngrok.com;TI"SERVER_PORT;TI"80;FI"SERVER_PROTOCOL;TI" HTTP/1.1;TI"SERVER_SOFTWARE;TI"*WEBrick/1.3.1 (Ruby/2.0.0/2013-06-27);TI"HTTP_HOST;T"57c3f4b7.ngrok.comI"HTTP_X_REAL_IP;T"198.61.253.117I"HTTP_X_FORWARDED_PROTO;T" httpI"HTTP_CONNECTION;T" 2 | closeI"HTTP_ACCEPT_ENCODING;T" gzipI"HTTP_USER_AGENT;T"mailgun/treq-0.2.0I"rack.version;T[iiI"rack.input;T"recipient=testing-raw%40hinttest.mailgun.org&sender=josh%40hintmedia.com&subject=Test+subject&from=Joshua+Wood+%3Cjosh%40hintmedia.com%3E&Received=by+luna.mailgun.net+with+SMTP+mgrt+4145733%3B+Thu%2C+19+Dec+2013+01%3A25%3A33+%2B0000&X-Envelope-From=%3Cjosh%40hintmedia.com%3E&Received=from+mail-bk0-f51.google.com+%28mail-bk0-f51.google.com+%5B209.85.214.51%5D%29+by+mxa.mailgun.org+with+ESMTP+id+52b24b0b.7ff7f817bfb0-in3%3B+Thu%2C+19+Dec+2013+01%3A25%3A31+-0000+%28UTC%29&Received=by+mail-bk0-f51.google.com+with+SMTP+id+6so477895bkj.38+for+%3Ctesting-raw%40hinttest.mailgun.org%3E%3B+Wed%2C+18+Dec+2013+17%3A25%3A30+-0800+%28PST%29&X-Google-Dkim-Signature=v%3D1%3B+a%3Drsa-sha256%3B+c%3Drelaxed%2Frelaxed%3B+d%3D1e100.net%3B+s%3D20130820%3B+h%3Dx-gm-message-state%3Amime-version%3Adate%3Amessage-id%3Asubject%3Afrom%3Ato+%3Acontent-type%3B+bh%3DYJHyUe2fufgHd8hjV6y20Oab%2BttMphO2MyaIZkR9npM%3D%3B+b%3DeZkzET4bRchbWfTmObvYD254jVeQ59LdLrpY%2BYUijD2%2F%2F28SRm57jZxpHDrQPmTKWU+xOmFUz896eM2dotZ%2FqY3Y73iPajZhsPJK507fZl13gR%2F8iDbbQNJNoHrkwNp%2FfkK94zX+%2Fth03k2D1aFuJaFQxwOFUfQzsECQBn1gCDLEEgnGZzUqlyXGj3S2izL8VV7mJIHUmiTy+2ti1yKzgNKMMNqDg6HH69Eq%2BewGtsJK04vk6wx%2Fov8CuCvlC14kiC5eBMh7ktQ2sC0xs+xbSX597osZ4qqc%2BVw7FeWzmXhtndwCbHUq7gxP2FoF1jEtGSMtPqxXt2Hu91UOnY%2BVw8+WwJg%3D%3D&X-Gm-Message-State=ALoCoQl%2FQ2i69yjPN6GOlpxqtZBzPuHp6jUNaOIJrMOmRhdwHRIhMHHeKmPvlHX%2B%2FMfOWZIiahpa&Mime-Version=1.0&X-Received=by+10.204.228.14+with+SMTP+id+jc14mr10803bkb.175.1387416330225%3B+Wed%2C+18+Dec+2013+17%3A25%3A30+-0800+%28PST%29&Received=by+10.204.195.3+with+HTTP%3B+Wed%2C+18+Dec+2013+17%3A25%3A30+-0800+%28PST%29&X-Originating-Ip=%5B76.105.197.61%5D&Date=Wed%2C+18+Dec+2013+17%3A25%3A30+-0800&Message-Id=%3CCAEd0qJ9QFqno7Pzu19_ipFg%2B2KLOe46Ydxy%3D3aDt_u_c1r_k-A%40mail.gmail.com%3E&Subject=Test+subject&From=Joshua+Wood+%3Cjosh%40hintmedia.com%3E&To=testing-raw%40hinttest.mailgun.org&Content-Type=multipart%2Falternative%3B+boundary%3D%22485b3970d5a0852eb104edd909f7%22&X-Mailgun-Incoming=Yes&message-headers=%5B%5B%22Received%22%2C+%22by+luna.mailgun.net+with+SMTP+mgrt+4145733%3B+Thu%2C+19+Dec+2013+01%3A25%3A33+%2B0000%22%5D%2C+%5B%22X-Envelope-From%22%2C+%22%3Cjosh%40hintmedia.com%3E%22%5D%2C+%5B%22Received%22%2C+%22from+mail-bk0-f51.google.com+%28mail-bk0-f51.google.com+%5B209.85.214.51%5D%29+by+mxa.mailgun.org+with+ESMTP+id+52b24b0b.7ff7f817bfb0-in3%3B+Thu%2C+19+Dec+2013+01%3A25%3A31+-0000+%28UTC%29%22%5D%2C+%5B%22Received%22%2C+%22by+mail-bk0-f51.google.com+with+SMTP+id+6so477895bkj.38+for+%3Ctesting-raw%40hinttest.mailgun.org%3E%3B+Wed%2C+18+Dec+2013+17%3A25%3A30+-0800+%28PST%29%22%5D%2C+%5B%22X-Google-Dkim-Signature%22%2C+%22v%3D1%3B+a%3Drsa-sha256%3B+c%3Drelaxed%2Frelaxed%3B+d%3D1e100.net%3B+s%3D20130820%3B+h%3Dx-gm-message-state%3Amime-version%3Adate%3Amessage-id%3Asubject%3Afrom%3Ato+%3Acontent-type%3B+bh%3DYJHyUe2fufgHd8hjV6y20Oab%2BttMphO2MyaIZkR9npM%3D%3B+b%3DeZkzET4bRchbWfTmObvYD254jVeQ59LdLrpY%2BYUijD2%2F%2F28SRm57jZxpHDrQPmTKWU+xOmFUz896eM2dotZ%2FqY3Y73iPajZhsPJK507fZl13gR%2F8iDbbQNJNoHrkwNp%2FfkK94zX+%2Fth03k2D1aFuJaFQxwOFUfQzsECQBn1gCDLEEgnGZzUqlyXGj3S2izL8VV7mJIHUmiTy+2ti1yKzgNKMMNqDg6HH69Eq%2BewGtsJK04vk6wx%2Fov8CuCvlC14kiC5eBMh7ktQ2sC0xs+xbSX597osZ4qqc%2BVw7FeWzmXhtndwCbHUq7gxP2FoF1jEtGSMtPqxXt2Hu91UOnY%2BVw8+WwJg%3D%3D%22%5D%2C+%5B%22X-Gm-Message-State%22%2C+%22ALoCoQl%2FQ2i69yjPN6GOlpxqtZBzPuHp6jUNaOIJrMOmRhdwHRIhMHHeKmPvlHX%2B%2FMfOWZIiahpa%22%5D%2C+%5B%22Mime-Version%22%2C+%221.0%22%5D%2C+%5B%22X-Received%22%2C+%22by+10.204.228.14+with+SMTP+id+jc14mr10803bkb.175.1387416330225%3B+Wed%2C+18+Dec+2013+17%3A25%3A30+-0800+%28PST%29%22%5D%2C+%5B%22Received%22%2C+%22by+10.204.195.3+with+HTTP%3B+Wed%2C+18+Dec+2013+17%3A25%3A30+-0800+%28PST%29%22%5D%2C+%5B%22X-Originating-Ip%22%2C+%22%5B76.105.197.61%5D%22%5D%2C+%5B%22Date%22%2C+%22Wed%2C+18+Dec+2013+17%3A25%3A30+-0800%22%5D%2C+%5B%22Message-Id%22%2C+%22%3CCAEd0qJ9QFqno7Pzu19_ipFg%2B2KLOe46Ydxy%3D3aDt_u_c1r_k-A%40mail.gmail.com%3E%22%5D%2C+%5B%22Subject%22%2C+%22Test+subject%22%5D%2C+%5B%22From%22%2C+%22Joshua+Wood+%3Cjosh%40hintmedia.com%3E%22%5D%2C+%5B%22To%22%2C+%22testing-raw%40hinttest.mailgun.org%22%5D%2C+%5B%22Content-Type%22%2C+%22multipart%2Falternative%3B+boundary%3D%5C%22485b3970d5a0852eb104edd909f7%5C%22%22%5D%2C+%5B%22X-Mailgun-Incoming%22%2C+%22Yes%22%5D%5D×tamp=1387416337&token=4ju1vcs4fqe2w1u35ujg7lkr-hma0-t4-sf3mv-njh1bix1j00&signature=55e7e8e42785307a1b16e2b8ecefab0bd276a76b74b3a807101b1d9dead30d1c&body-plain=Test+body.%0D%0A&body-html=%3Cdiv+dir%3D%22ltr%22%3ETest+body.%3C%2Fdiv%3E%0D%0A&stripped-html=%3Cdiv+dir%3D%22ltr%22%3ETest+body.%3C%2Fdiv%3E%0D%0A&stripped-text=Test+body.&stripped-signature=I"rack.multithread;TTI"rack.multiprocess;TFI"rack.run_once;TFI"rack.url_scheme;TI" http;TI"HTTP_VERSION;T@I"REQUEST_PATH;TI"/mailgun.raw;TI"fixture_file_path;TI"D/Users/josh/code/incoming/spec/fixtures/records/mailgun.raw.env;T -------------------------------------------------------------------------------- /spec/fixtures/mandrill.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honeybadger-io/incoming/b21dcbacb42f98055675ea5d1d212bf25f848068/spec/fixtures/mandrill.env -------------------------------------------------------------------------------- /spec/fixtures/notification.eml: -------------------------------------------------------------------------------- 1 | Delivered-To: josh@joshuawood.net 2 | Received: by 10.231.14.5 with SMTP id e5cs8149iba; 3 | Sat, 20 Aug 2011 20:55:15 -0700 (PDT) 4 | Received: by 10.231.45.206 with SMTP id g14mr2403740ibf.97.1313898914676; 5 | Sat, 20 Aug 2011 20:55:14 -0700 (PDT) 6 | Return-Path: 7 | Received: from Josh-MacBook-Air.local (c-11-22-33-444.555.xx.yy.zzz [127.0.0.1]) 8 | by mx.google.com with ESMTP id a20si4199205ibi.32.2011.08.20.20.55.13; 9 | Sat, 20 Aug 2011 20:55:13 -0700 (PDT) 10 | Received-SPF: softfail (google.com: domain of transitioning notifications@incoming.test does not designate 127.0.0.1 as permitted sender) client-ip=127.0.0.1; 11 | Authentication-Results: mx.google.com; spf=softfail (google.com: domain of transitioning notifications@incoming.test does not designate 127.0.0.1 as permitted sender) smtp.mail=notifications@incoming.test 12 | Received: by Josh-MacBook-Air.local (Postfix, from userid 501) 13 | id 0D949114264D; Sat, 20 Aug 2011 20:55:12 -0700 (PDT) 14 | Date: Sat, 20 Aug 2011 20:55:12 -0700 15 | From: notifications@incoming.test 16 | Reply-To: Test 17 | To: Joshua Wood 18 | Message-ID: <4e5081a0adb86_9baa8043823c40559@Josh-MacBook-Air.local.mail> 19 | Subject: Jack Kerouac has replied to Test 20 | Mime-Version: 1.0 21 | Content-Type: text/plain; 22 | charset=UTF-8 23 | Content-Transfer-Encoding: 7bit 24 | 25 | Reply ABOVE THIS LINE to add a comment to this message. 26 | 27 | Hello Joshua, 28 | 29 | Jack Kerouac has posted a reply to Test: 30 | 31 | Test test 32 | 33 | View/reply: http://incoming.test/messages/1-test#comment_1 34 | -------------------------------------------------------------------------------- /spec/fixtures/postmark.env: -------------------------------------------------------------------------------- 1 | {"I"CONTENT_LENGTH:ET" 3106I"CONTENT_TYPE;T"application/jsonI"GATEWAY_INTERFACE;TI" CGI/1.1;TI"PATH_INFO;TI"/postmark;TI"QUERY_STRING;TI";TI"REMOTE_ADDR;T"127.0.0.1I"REMOTE_HOST;T"localhostI"REQUEST_METHOD;T" POSTI"REQUEST_URI;TI"'http://67ce7966.ngrok.com/postmark;TI"SCRIPT_NAME;TI";TI"SERVER_NAME;TI"67ce7966.ngrok.com;TI"SERVER_PORT;TI"80;FI"SERVER_PROTOCOL;TI" HTTP/1.1;TI"SERVER_SOFTWARE;TI"*WEBrick/1.3.1 (Ruby/2.0.0/2013-06-27);TI"HTTP_HOST;T"67ce7966.ngrok.comI"HTTP_X_REAL_IP;T"50.31.156.105I"HTTP_X_FORWARDED_PROTO;T" httpI"HTTP_CONNECTION;T" 2 | closeI"HTTP_ACCEPT;T"application/jsonI"HTTP_USER_AGENT;T" PostmarkI"rack.version;T[iiI"rack.input;T"" { 3 | "FromName": "Joshua Wood", 4 | "From": "josh@honeybadger.io", 5 | "FromFull": { 6 | "Email": "josh@honeybadger.io", 7 | "Name": "Joshua Wood" 8 | }, 9 | "To": "9cebd9cfe7e7a96f140d0accbd187b42@inbound.postmarkapp.com", 10 | "ToFull": [ 11 | { 12 | "Email": "9cebd9cfe7e7a96f140d0accbd187b42@inbound.postmarkapp.com", 13 | "Name": "" 14 | } 15 | ], 16 | "Cc": "josh+incoming@honeybadger.io", 17 | "CcFull": [ 18 | { 19 | "Email": "josh+incoming@honeybadger.io", 20 | "Name": "" 21 | } 22 | ], 23 | "ReplyTo": "", 24 | "Subject": "Test subject", 25 | "MessageID": "a697fa2c-744a-40d2-a9c1-af874e273fe3", 26 | "Date": "Fri, 27 Dec 2013 12:03:36 -0800", 27 | "MailboxHash": "", 28 | "TextBody": "Test body.\r\n", 29 | "HtmlBody": "<div dir="ltr">Test body.<\/div>\r\n", 30 | "Tag": "", 31 | "Headers": [ 32 | { 33 | "Name": "X-Spam-Checker-Version", 34 | "Value": "SpamAssassin 3.3.1 (2010-03-16) on sc-iad-inbound1" 35 | }, 36 | { 37 | "Name": "X-Spam-Status", 38 | "Value": "No" 39 | }, 40 | { 41 | "Name": "X-Spam-Score", 42 | "Value": "-0.7" 43 | }, 44 | { 45 | "Name": "X-Spam-Tests", 46 | "Value": "HTML_MESSAGE,RCVD_IN_DNSWL_LOW,SPF_PASS" 47 | }, 48 | { 49 | "Name": "Received-SPF", 50 | "Value": "Pass (sender SPF authorized) identity=mailfrom; client-ip=209.85.216.178; helo=mail-qc0-f178.google.com; envelope-from=josh@honeybadger.io; receiver=9cebd9cfe7e7a96f140d0accbd187b42@inbound.postmarkapp.com" 51 | }, 52 | { 53 | "Name": "X-Google-DKIM-Signature", 54 | "Value": "v=1; a=rsa-sha256; c=relaxed\/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:mime-version:date:message-id:subject:from:to:cc :content-type; bh=PkFoFPx1XgdwXSaARwq4yO8sSmzItvoICywPJ2QTvjg=; b=PZUJHlsrfS8Z\/0NUXuCgHI8Ts5D+DeNaT4sSW5B4F3t5Vdfq25FVAUkh+7lAZspHyF l+rEq2UbByKcvH\/YjUjOIlxcoOH8+esyiZQLDETZitZlN7ufnSu5Ik0aTs+eh5cv98Op FZAaEg34mPRBILFZBsGXw0yf2IkZ7X+KwhUWU92Kju2caTRNAeiHN3+Gn\/brAC9Sutp3 E2VSxqIiVkqagKpeWAIrTVtM272j6uc5Wyl4X\/QJZv9vC8OiDqxh+h8G9Kt7HC0WRm2a f\/vRY48fhByf9RyWXl2z5UWvtvOr+tEtULl4n8KYMPtCW7Y2LG9C0aZUaknyV5bJRGOK YIbg==" 55 | }, 56 | { 57 | "Name": "X-Gm-Message-State", 58 | "Value": "ALoCoQkK5Y\/4I3fceAuYQuLH5LLw1OSl7ZPJDudlmHQTyhggu838ZJlbLIt9WsK233PvezJ\/ngwp" 59 | }, 60 | { 61 | "Name": "MIME-Version", 62 | "Value": "1.0" 63 | }, 64 | { 65 | "Name": "X-Received", 66 | "Value": "by 10.224.12.5 with SMTP id v5mr83088232qav.4.1388174616995; Fri, 27 Dec 2013 12:03:36 -0800 (PST)" 67 | }, 68 | { 69 | "Name": "X-Originating-IP", 70 | "Value": "[76.105.197.61]" 71 | }, 72 | { 73 | "Name": "Message-ID", 74 | "Value": "" 75 | } 76 | ], 77 | "Attachments": [ 78 | { 79 | "Name": "hello.txt", 80 | "Content": "aGVsbG8gd29ybGQK", 81 | "ContentType": "text/plain", 82 | "ContentID": "", 83 | "ContentLength": 16 84 | }, 85 | { 86 | "Name": "hullo.txt", 87 | "Content": "aHVsbG8gd29ybGQK", 88 | "ContentType": "text/plain", 89 | "ContentID": "", 90 | "ContentLength": 16 91 | } 92 | ] 93 | }I"rack.multithread;TTI"rack.multiprocess;TFI"rack.run_once;TFI"rack.url_scheme;TI" http;TI"HTTP_VERSION;T@I"REQUEST_PATH;TI"/postmark;TI"fixture_file_path;TI"9/Users/josh/code/incoming/spec/fixtures/postmark.env;T -------------------------------------------------------------------------------- /spec/incoming/strategies/cloudmailin_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Incoming::Strategies::CloudMailin do 4 | before do 5 | raw_email = File.open(File.expand_path("../../../fixtures/notification.eml", __FILE__)).read 6 | 7 | params = { 8 | 'message' => raw_email, 9 | 'envelope' => { 10 | 'to' => 'asdf@cloudmailin.net', 11 | 'from' => 'josh@joshuawood.net', 12 | 'helo_domain' => 'mail-da0-f41.google.com', 13 | 'remote_ip' => '127.0.0.1', 14 | 'spf' => { 15 | 'result' => 'pass', 16 | 'domain' => 'joshuawood.net' 17 | } 18 | } 19 | } 20 | 21 | @mock_request = double() 22 | @mock_request.stub(:params).and_return(params) 23 | end 24 | 25 | describe 'message' do 26 | subject { receiver.receive(@mock_request) } 27 | 28 | it { should be_a Mail::Message } 29 | 30 | its('to.first') { should eq 'josh@joshuawood.net' } 31 | its('from.first') { should eq 'notifications@incoming.test' } 32 | its('subject') { should eq 'Jack Kerouac has replied to Test' } 33 | its('body.decoded') { should match(/Reply ABOVE THIS LINE/) } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/incoming/strategies/http_post_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Incoming::Strategies::HTTPPost do 4 | before do 5 | raw_email = File.open(File.expand_path("../../../fixtures/notification.eml", __FILE__)).read 6 | 7 | params = { 8 | 'signature' => 'foo', 9 | 'message' => raw_email 10 | } 11 | 12 | @mock_request = double() 13 | @mock_request.stub(:params).and_return(params) 14 | end 15 | 16 | describe 'message' do 17 | subject { receiver.receive(@mock_request) } 18 | before(:each) { OpenSSL::HMAC.stub(:hexdigest).and_return('foo') } 19 | 20 | it { should be_a Mail::Message } 21 | 22 | its('to.first') { should eq 'josh@joshuawood.net' } 23 | its('from.first') { should eq 'notifications@incoming.test' } 24 | its('subject') { should eq 'Jack Kerouac has replied to Test' } 25 | its('body.decoded') { should match /Reply ABOVE THIS LINE/ } 26 | end 27 | 28 | it 'returns false from #authenticate when hexidigest is invalid' do 29 | OpenSSL::HMAC.stub(:hexdigest).and_return('bar') 30 | http_post = Incoming::Strategies::HTTPPost.new(@mock_request) 31 | http_post.authenticate.should eq(false) 32 | end 33 | 34 | it 'returns true from #authenticate when hexidigest is valid' do 35 | OpenSSL::HMAC.stub(:hexdigest).and_return('foo') 36 | http_post = Incoming::Strategies::HTTPPost.new(@mock_request) 37 | http_post.authenticate.should eq(true) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/incoming/strategies/mailgun_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Incoming::Strategies::Mailgun do 4 | let(:receiver) { test_receiver(:api_key => 'asdf') } 5 | 6 | before do 7 | @params = { 8 | 'body-plain' => "We should do that again sometime.\n> Quoted part", 9 | 'body-html' => 'We should do that again sometime.\r\n> Quoted part', 10 | 'stripped-text' => 'We should do that again sometime.', 11 | 'stripped-html' => 'We should do that again sometime.', 12 | 'signature' => 'foo', 13 | 'message-headers' => [ [ "Received", "by luna.mailgun.net with SMTP mgrt 8747120609393; Wed, 01 May 2013 02:16:10 +0000" ], [ "X-Envelope-From", "" ], [ "Received", "from mail-pa0-f41.google.com (mail-pa0-f41.google.com [209.85.220.41]) by mxa.mailgun.org with ESMTP id 51807ae9.7f7248df4ef0-in3; Wed, 01 May 2013 02:16:09 -0000 (UTC)" ], [ "Received", "by mail-pa0-f41.google.com with SMTP id kq12so669142pab.0 for ; Tue, 30 Apr 2013 19:16:05 -0700 (PDT)" ], [ "X-Google-Dkim-Signature", "v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20120113; h=x-received:message-id:date:from:user-agent:mime-version:to:subject :x-enigmail-version:content-type:content-transfer-encoding :x-gm-message-state; bh=Ba3gj8+xBPQLJTahTfzW6RbWQ/XPgESxkCi2B66PSQg=; b=A2UmxKvNxZAr4yAWPWDfnBELJC8yBfthL7pxex51u3JYLvWdPjhmuJMeaUtSNkBVq1 7nWT8/OrnaXAACeBZc/WFjMNP5BTC+B3Eic4387QseWkqrE17oQSQWBupkzUzW+JLdeD /BEDVhPglhkeOQhtdd3iooRCVAZ++DzDv4zKYOXlNbuvNnDO1D+RVoRkBx6sA+fom8vJ l0HYkqfzVqelt8FKVfQ50J/KFPfItzYuh37Uck54lV6zoE2dGyK5uvNqyEMzNLPnd1PI XC1t8G/61I8lGl0MMdju6KD31flClhcmEr282c1YMXzGTWtgD22t39snAk9sI0Ce31Dv dGlw==" ], [ "X-Received", "by 10.66.220.10 with SMTP id ps10mr2456146pac.117.1367374564998; Tue, 30 Apr 2013 19:16:04 -0700 (PDT)" ], [ "Return-Path", "" ], [ "Received", "from Josh-MacBook-Air.local (c-67-171-132-75.hsd1.or.comcast.net. [67.171.132.75]) by mx.google.com with ESMTPSA id ya4sm939634pbb.24.2013.04.30.19.16.02 for (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); Tue, 30 Apr 2013 19:16:04 -0700 (PDT)" ], [ "Message-Id", "<51807AE0.2020403@honeybadger.io>" ], [ "Date", "Tue, 30 Apr 2013 19:16:00 -0700" ], [ "From", "Joshua Wood " ], [ "User-Agent", "Postbox 3.0.8 (Macintosh/20130427)" ], [ "Mime-Version", "1.0" ], [ "To", "testing@honeybadger.mailgun.org, Wood Joshua " ], [ "Subject", "Matterhorn" ], [ "X-Enigmail-Version", "1.2.3" ], [ "Content-Type", "text/plain; charset=\"ISO-8859-1\"" ], [ "Content-Transfer-Encoding", "7bit" ], [ "X-Gm-Message-State", "ALoCoQmejbb0rnWD00BBdqBXLoONkEpAYyufP56rBpr80ZKyDXNN1/t/O9MRqscRmoLVPrE9tu+G" ], [ "X-Mailgun-Incoming", "Yes" ] ].to_json, 14 | 'attachment-count' => '2', 15 | 'attachment-1' => double(:original_filename => 'foo.txt', :read => 'hello world'), 16 | 'attachment-2' => { 17 | :filename => 'bar.txt', 18 | :tempfile => double(:read => 'hullo world') 19 | } 20 | } 21 | 22 | @mock_request = double() 23 | @mock_request.stub(:params).and_return(@params) 24 | end 25 | 26 | describe 'non-mime request' do 27 | describe 'receive' do 28 | subject { receiver.receive(@mock_request) } 29 | before(:each) { OpenSSL::HMAC.stub(:hexdigest).and_return('foo') } 30 | 31 | it { should be_a Mail::Message } 32 | 33 | its('to') { should include 'testing@honeybadger.mailgun.org' } 34 | its('to') { should include 'joshuawood@gmail.com' } 35 | its('from.first') { should eq 'josh@honeybadger.io' } 36 | its('subject') { should eq 'Matterhorn' } 37 | its('body.decoded') { should eq @params['body-plain'] } 38 | its('text_part.body.decoded') { should eq @params['body-plain'] } 39 | its('html_part.body.decoded') { should eq @params['body-html'] } 40 | its('attachments.first.filename') { should eq 'foo.txt' } 41 | its('attachments.first.read') { should eq 'hello world' } 42 | its('attachments.last.filename') { should eq 'bar.txt' } 43 | its('attachments.last.read') { should eq 'hullo world' } 44 | 45 | context 'stripped option is true' do 46 | let(:receiver) { test_receiver(:api_key => 'asdf', :stripped => true) } 47 | 48 | it { should be_a Mail::Message } 49 | 50 | its('body.decoded') { should eq @params['stripped-text'] } 51 | its('text_part.body.decoded') { should eq @params['stripped-text'] } 52 | its('html_part.body.decoded') { should eq @params['stripped-html'] } 53 | end 54 | end 55 | end 56 | 57 | it 'returns false from #authenticate when hexidigest is invalid' do 58 | OpenSSL::HMAC.stub(:hexdigest).and_return('bar') 59 | mailgun = receiver.new(@mock_request) 60 | mailgun.authenticate.should eq(false) 61 | end 62 | 63 | it 'authenticates when hexidigest is valid' do 64 | OpenSSL::HMAC.stub(:hexdigest).and_return('foo') 65 | mailgun = receiver.new(@mock_request) 66 | mailgun.authenticate.should eq(true) 67 | end 68 | 69 | it 'raises an exception when api key is not provided' do 70 | expect { 71 | test_receiver(:api_key => nil).new(@mock_request) 72 | }.to raise_error Incoming::Strategy::RequiredOptionError 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/incoming/strategies/mandrill_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Incoming::Strategies::Mandrill do 4 | let(:request) { recorded_request('mandrill') } 5 | 6 | describe '.receive' do 7 | it 'returns an Array of results from each message' do 8 | result = receiver.receive(request) 9 | expect(result).to be_a Array 10 | expect(result.map { |r| r.class }).to eq [Mail::Message, Mail::Message] 11 | end 12 | 13 | it 'skips non-inbound events' do 14 | mandrill_events = JSON.parse(request.params['mandrill_events']) 15 | mandrill_events << {'event' => 'other'} 16 | request.params['mandrill_events'] = mandrill_events.to_json 17 | expect(receiver.receive(request).size).to eq 2 18 | end 19 | 20 | context 'with a block' do 21 | specify { expect { |b| receiver.receive(request, &b) }.to yield_successive_args(Mail::Message, Mail::Message) } 22 | end 23 | end 24 | 25 | describe 'message' do 26 | subject { receiver.receive(request).first } 27 | 28 | it { should be_a Mail::Message } 29 | 30 | its('to') { should include 'testing@inbound.honeybadger.io' } 31 | its('from') { should include 'example.sender@mandrillapp.com' } 32 | its('subject') { should eq 'This is an example webhook message' } 33 | its('text_part.body.decoded') { should eq 'This is an example inbound message.' } 34 | its('html_part.body.decoded') { should match /

This is an example inbound message\.<\/p>/ } 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/incoming/strategies/postmark_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Incoming::Strategies::Postmark do 4 | let(:request) { recorded_request('postmark') } 5 | 6 | context 'json parse error' do 7 | it 'raises exception to caller' do 8 | request.stub_chain(:body, :read).and_return('{invalid') 9 | expect { receiver.receive(request) }.to raise_error(JSON::ParserError) 10 | end 11 | end 12 | 13 | describe '#message' do 14 | subject { receiver.receive(request) } 15 | 16 | it { should be_a Mail::Message } 17 | 18 | specify { expect(subject.header['MailboxHash']).to be_nil } 19 | specify { expect(subject.header['MessageID']).not_to be_nil } 20 | specify { expect(subject.header['Tag']).to be_nil } 21 | 22 | %w(MailboxHash Tag).each do |header| 23 | it "includes #{header} header when present" do 24 | parsed_json = JSON.parse(request.body.read) 25 | parsed_json[header] = 'asdf' 26 | JSON.stub(:parse).and_return(parsed_json) 27 | expect(subject.header[header]).not_to be_nil 28 | end 29 | end 30 | 31 | its('to') { should include '9cebd9cfe7e7a96f140d0accbd187b42@inbound.postmarkapp.com' } 32 | its('cc') { should include 'josh+incoming@honeybadger.io' } 33 | its('from') { should include 'josh@honeybadger.io' } 34 | its('subject') { should eq 'Test subject' } 35 | its('body.decoded') { should match 'Test body' } 36 | its('html_part.body.decoded') { should match 'Test body' } 37 | its('html_part.body.decoded') { should_not match '<' } 38 | its('html_part.body.decoded') { should match '<' } 39 | its('attachments.first.filename') { should eq 'hello.txt' } 40 | its('attachments.first.read') { should match 'hello world' } 41 | its('attachments.last.filename') { should eq 'hullo.txt' } 42 | its('attachments.last.read') { should match 'hullo world' } 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/incoming/strategies/sendgrid_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Incoming::Strategies::SendGrid do 4 | 5 | subject { receiver.receive(request) } 6 | 7 | let(:request){ double(params: params) } 8 | let(:params){ 9 | { 10 | 'SPF' => 'pass', 11 | 'charsets' => '{"from": "UTF-8", "subject": "UTF-8", "text": "ISO-8859-1", "to": "UTF-8"}', 12 | 'dkim' => 'none', 13 | 'headers' => "Received: by 127.0.0.1 with SMTP id IQklCWgXx9 Tue, 30 Apr 2013 20:38:01 -0500 (CDT)\r\nReceived: from mail-da0-f49.google.com (mail-da0-f49.google.com [209.85.210.49]) by mx3.sendgrid.net (Postfix) with ESMTPS id 15F7314F48C1 for ; Tue, 30 Apr 2013 20:38:01 -0500 (CDT)\r\nReceived: by mail-da0-f49.google.com with SMTP id t11so483187daj.22 for ; Tue, 30 Apr 2013 18:38:00 -0700 (PDT)\r\nX-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20120113; h=x-received:message-id:date:from:user-agent:mime-version:to:subject :x-enigmail-version:content-type:content-transfer-encoding :x-gm-message-state; bh=EVfAHeUMDygbJe0SkMWJHjgXGjtiTLZnMQbyWqzsrCY=; b=Y5UefHXzM6KLBH/TQejxrMlJ3sPJThU1DS0/uNLWUZkjeG4wAwOEfE8XuesBL7SUF1 0G6OAlCqpnZMl8F8iitlt2iuuW1+5NqOiBIsBnYFJ5Y5EP8XiPivUaigWZLtGfZH7sTY 2tNQ74UfVSzxx5gyjajkzuT8qNa+zYiNVL14wQy2HWu/FxTUBGy5VRko7hVXdJIck7fn 4E+4p34f1j2CGPSYXg4qDgBwE4m4nllQqu0/k/xHB66+iJl05uMZ2BUGYoRQI2aE4jC1 GlbxQhUU5I/kefIjrdm1wuf92sarKTtFA6kcao1Z3pgExARbcpuXTdgIsgq/WUBf424U JCnQ==\r\nX-Received: by 10.66.177.46 with SMTP id cn14mr2412679pac.4.1367372280251; Tue, 30 Apr 2013 18:38:00 -0700 (PDT)\r\nReceived: from Josh-MacBook-Air.local (c-67-171-132-75.hsd1.or.comcast.net. [67.171.132.75]) by mx.google.com with ESMTPSA id ef4sm782898pbd.38.2013.04.30.18.37.57 for (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); Tue, 30 Apr 2013 18:37:59 -0700 (PDT)\r\nMessage-ID: <518071F1.5010108@honeybadger.io>\r\nDate: Tue, 30 Apr 2013 18:37:53 -0700\r\nFrom: Joshua Wood \r\nUser-Agent: Postbox 3.0.8 (Macintosh/20130427)\r\nMIME-Version: 1.0\r\nTo: testing@sendgrid.honeybadger.io\r\nSubject: Matterhorn\r\nX-Enigmail-Version: 1.2.3\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Transfer-Encoding: 7bit\r\nX-Gm-Message-State: ALoCoQkHpc+fg75T6PPAtGWRMs8lDGZtyRSNSt4DRh7Gm29YjOKj4VThC/EdJAiqrqkuRX1HKyA6", 14 | 'text' => 'We should do that again sometime.', 15 | 'html' => 'We should do that again sometime', 16 | 'attachments' => '2', 17 | 'attachment1' => double(:original_filename => 'hello.txt', :read => 'hello world'), 18 | 'attachment2' => { 19 | :filename => 'bar.txt', 20 | :tempfile => double(:read => 'hullo world') 21 | } 22 | } 23 | } 24 | 25 | describe 'message' do 26 | 27 | it { should be_a Mail::Message } 28 | 29 | its('to') { should include 'testing@sendgrid.honeybadger.io' } 30 | its('from') { should include 'josh@honeybadger.io' } 31 | its('subject') { should eq 'Matterhorn' } 32 | its('body.decoded') { should eq 'We should do that again sometime.' } 33 | its('text_part.body.decoded') { should eq 'We should do that again sometime.' } 34 | its('html_part.body.decoded') { should eq 'We should do that again sometime' } 35 | its('attachments.first.filename') { should eq 'hello.txt' } 36 | its('attachments.first.read') { should eq 'hello world' } 37 | its('attachments.last.filename') { should eq 'bar.txt' } 38 | its('attachments.last.read') { should eq 'hullo world' } 39 | 40 | context "with base64 Content-Transfer-Encoding" do 41 | 42 | let(:params){ 43 | { 44 | 'charsets' => '{"from": "UTF-8", "subject": "UTF-8", "text": "UTF-8", "to": "UTF-8"}', 45 | 'headers' => ["Content-Transfer-Encoding: base64", "Content-Type: text/plain; charset=UTF-8"].join("\r\n"), 46 | 'text' => 'We should do that again sometime.', 47 | 'attachments' => '0', 48 | } 49 | } 50 | 51 | its('body.decoded') { should eq 'We should do that again sometime.' } 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/incoming/strategy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class TestStrategy 4 | include Incoming::Strategy 5 | option :api_key, nil 6 | end 7 | 8 | describe TestStrategy do 9 | describe 'self.setup with hash' do 10 | it 'raises an exception when invalid options have been set' do 11 | expect { 12 | receiver.class_eval { setup({ :foo => 'invalid' }) } 13 | }.to raise_error(Incoming::Strategy::InvalidOptionError) 14 | end 15 | 16 | it 'raises no exception when valid options are set' do 17 | expect { 18 | receiver.class_eval { setup({ :api_key => 'valid' }) } 19 | }.to_not raise_error 20 | end 21 | end 22 | 23 | describe 'self.receive' do 24 | it 'initializes itself and calls #receive' do 25 | args = [1, 2, 3] 26 | 27 | object = double() 28 | object.stub(:authenticate).once.and_return(true) 29 | object.stub(:message).once.and_return('foo') 30 | object.stub(:receive).with('foo').once 31 | 32 | receiver.should_receive(:new).with(*args).and_return(object) 33 | receiver.receive(*args) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Incoming::Strategies::Mailgun do 4 | let(:receiver) { test_receiver(:api_key => 'asdf') } 5 | 6 | describe 'end-to-end' do 7 | let(:request) { recorded_request('mailgun') } 8 | before { OpenSSL::HMAC.stub(:hexdigest).and_return(request.params['signature']) } 9 | 10 | it 'receives the request' do 11 | expect(receiver.receive(request)).to be_a Mail::Message 12 | end 13 | end 14 | end 15 | 16 | describe Incoming::Strategies::Postmark do 17 | let(:receiver) { test_receiver } 18 | 19 | describe 'end-to-end' do 20 | let(:request) { recorded_request('postmark') } 21 | 22 | it 'receives the request' do 23 | expect(receiver.receive(request)).to be_a Mail::Message 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/recorder.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | 3 | FIXTURES_DIR = File.expand_path('../../spec/fixtures', __FILE__) 4 | 5 | class FixtureRecorder 6 | def initialize(app) 7 | @app = app 8 | end 9 | 10 | def call(env) 11 | env['fixture_file_path'] = file_path_from(env) 12 | begin 13 | @app.call(env) 14 | ensure 15 | File.open(env['fixture_file_path'], 'w') do |file| 16 | file.write(dump_env(env)) 17 | end 18 | end 19 | end 20 | 21 | def file_path_from(env) 22 | file_path = env['PATH_INFO'].downcase.gsub('/', '_')[/[^_].+[^_]/] 23 | file_path = 'root' unless file_path =~ /\S/ 24 | File.join(FIXTURES_DIR, [file_path, 'env'].join('.')) 25 | end 26 | 27 | def dump_env(env) 28 | safe_env = env.dup 29 | safe_env.merge!({ 'rack.input' => env['rack.input'].read }) 30 | safe_env = safe_env.select { |_,v| Marshal.dump(v) rescue false } 31 | Marshal.dump(safe_env) 32 | end 33 | end 34 | 35 | app = Rack::Builder.new do 36 | use FixtureRecorder 37 | run Proc.new { |env| 38 | [200, {}, StringIO.new(env['fixture_file_path'])] 39 | } 40 | end 41 | 42 | Rack::Handler::WEBrick.run app, Port: 4567 43 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'rspec/its' 3 | require 'incoming' 4 | require 'rack' 5 | 6 | RSpec.configure do |c| 7 | c.mock_with :rspec 8 | c.tty = true 9 | 10 | module Helpers 11 | def self.included(base) 12 | base.let(:receiver) { test_receiver } 13 | end 14 | 15 | def recorded_request(name) 16 | env = Marshal.load(File.read(File.join(File.expand_path('../../spec/fixtures', __FILE__), "#{name}.env"))) 17 | env['rack.input'] = StringIO.new(env['rack.input']) 18 | Rack::Request.new(env) 19 | end 20 | 21 | def test_receiver(options = {}) 22 | Class.new(described_class) do 23 | setup(options) 24 | 25 | def receive(mail) 26 | mail 27 | end 28 | end 29 | end 30 | end 31 | 32 | c.include Helpers 33 | end 34 | --------------------------------------------------------------------------------