Skip to content
This repository has been archived by the owner on Nov 27, 2018. It is now read-only.

Commit

Permalink
Merge pull request #122 from mailman/v0.8.0
Browse files Browse the repository at this point in the history
Aggregated release v0.8.0
  • Loading branch information
jphastings committed Jun 6, 2015
2 parents 636e561 + bccbc74 commit 72c8184
Show file tree
Hide file tree
Showing 22 changed files with 557 additions and 20 deletions.
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--require spec_helper
--format progress
-cfd
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ matrix:
- rvm: ruby-head
- rvm: jruby-19mode
- rvm: jruby-head
- rvm: rbx-2
- rvm: rbx-19mode
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## 0.8.0 (April 1, 2015)

Features

- An HTTP Receiver for creating email-only applications with services like Sendgrid and Cloudmailin
- Header conditions; route by matching email headers

Bugfixes

- RBX build issues

Notes

- Uses the master branch version of `mail` because of a bug when using RBX (see [#116](https://github.com/mailman/mailman/issues/116)). This will be removed when the mail gem is next released.

## 0.7.3 (March 17, 2015)

Features
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ gemspec

gem 'rake'
gem 'jruby-openssl', :platforms => :jruby
gem 'mail', :git => 'git://github.com/mikel/mail.git'
3 changes: 2 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ task :gemspec do
end

task :package => :gemspec
task :default => :spec
task :test => :spec
task :default => :test
88 changes: 85 additions & 3 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ combined with a block of code to form a **Route**.

### Matchers

There are string and regular expression matchers. Both can perform captures.
There are string, regular expression and header matchers. All can perform captures.

#### String

Expand All @@ -57,9 +57,42 @@ matcher instead.

#### Regular expression

Regular expressions may be used as matchers. All captures will be available from
the params helper (`params[:captures]`) as an Array, and as block arguments.
Regular expressions may be used as matchers. All captures will be available from the params helper (`params[:captures]`) as an Array, and as block arguments.

#### Headers

You can match against headers using strings or regular expressions. Multiple headers can be listed and all of them must be matched in order for the route to progress.

Capture groups are available for headers matched with regular expressions, although they are provided in a slightly more complex form, as some headers can appear multiple times.

For an email that looks like this:

```
To: hello@mailman.example.com
X-Forwarded-To: someone@example.com
X-Forwarded-To: other@example.in
```

You can expect the captures to work like this:

```ruby
Mailman::Application.run do
header to: /h(ell)o/, x_forwarded_to: /(.+)@example.(.+)/ do
p params[:captures]
# => {
# to: [
# [ 'ell' ]
# ],
# x_forwarded_to: [
# [ 'someone', 'com' ],
# [ 'other', 'in' ]
# ]
# }
end
end
```

Using block arguments isn't advised for header matching as the exact number of arguments is limited only by the inbound email.

### Routes

Expand Down Expand Up @@ -211,6 +244,55 @@ Mailman.config.imap = {
* When using gmail, remember to [enable IMAP](https://support.google.com/mail/troubleshooter/1668960)
* You can pass a Hash to `ssl`, just like with POP3.

### HTTP

If `Mailman.config.http` is set then the HTTP receiver will be used. This will set up an HTTP server which expects emails to be delivered to `http://0.0.0.0:6245/` by default. You can alter the listening endpoint with the options below and altering the `parser` will allow different SMTP-HTTP gateways to be used.

**NB.** You will need to make sure `rack` (and optionally `thin`) are included in your gemset in order to use the HTTP receiver.

The default options are:

```ruby
Mailman.config.http = {
host: '0.0.0.0',
port: 6245,
path: '/',
parser: :raw_post,
parser_opts: {
part_name: 'message'
}
}
```

#### Raw Post

The Raw Post Parser (`:raw_post`) expects the raw contents of emails to be delivered via multipart POST request to the specified endpoint. You can specify the name of the part which will contain the email data with the `:part_name` parser option ('message' by default).

The default HTTP configuration is perfect for [Cloudmailin](http://www.cloudmailin.com). If you set your target as `http://yourpublicserver:6245/` then mailman will work with the most basic config:

```ruby
Mailman.config.http = {}

Mailman::Application.run do
# ... etc
end
```

#### Sendgrid

The parser for [Sendgrid](https://sendgrid.com) (`:sendgrid`) expects requests to conform to the [Inbound Parse Webhook](https://sendgrid.com/docs/API_Reference/Webhooks/parse.html) specification. Once you have pointed your domain's MX record at `mx.sendgrid.net` and configured an [inbound address](https://sendgrid.com/developer/reply) that points to your mailman server and the path you specified, all requests should flow as expected.

```ruby
Mailman.config.http = {
parser: :sendgrid,
path: "/emails"
}

Mailman::Application.run do
# ... etc
end
```

### Maildir

The Maildir receiver is enabled when `Mailman.config.maildir` is set to a
Expand Down
26 changes: 26 additions & 0 deletions examples/sendgrid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.join(__dir__, '../lib'))
require 'mailman'

# This example will process emails sent via Sendgrid and create
# a text file for every email sent to 'message-<number>@yourdomain.com' containing
# the email address that last sent a message to that number.
#
# 1. Set up a Sendgrid account
# 2. Point your domain's MX record at `mx.sendgrid.net`
# 3. Configure Sendgrid to point to wherever you're running this example (port 6245)
# 4. Profit!

Mailman.config.http = {
host: "0.0.0.0",
port: 6245,
parser: :sendgrid
}

Mailman::Application.run do
to %r{^message-(\d+)@} do
open("#{params['captures'].first}.txt", 'w') do |f|
f.write message.from.join(', ')
end
end
end
6 changes: 6 additions & 0 deletions lib/mailman/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ def run
Mailman.logger.info "POP3 receiver enabled (#{options[:username]}@#{options[:server]})."
polling_loop Receiver::POP3.new(options)

# HTTP
elsif config.http
options = {:processor => @processor}.merge(config.http)
Mailman.logger.info "HTTP server started"
Receiver::HTTP.new(options).start_and_block

# Maildir
elsif config.maildir

Expand Down
3 changes: 3 additions & 0 deletions lib/mailman/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class Configuration
# @return [Hash] the configuration hash for IMAP
attr_accessor :imap

# @return [Hash] the configuration hash for HTTP receiving
attr_accessor :http

# @return [Fixnum] the poll interval for POP3 or IMAP. Setting this to 0
# disables polling
attr_accessor :poll_interval
Expand Down
2 changes: 1 addition & 1 deletion lib/mailman/message_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def initialize(options)
# router.
# @param [String] message the message to process
def process(message)
mail = Mail.new(message)
mail = message.is_a?(Mail::Message) ? message : Mail.new(message)
from = mail.from.nil? ? "unknown" : mail.from.first
Mailman.logger.info "Got new message from '#{from}' with subject '#{mail.subject}'."

Expand Down
1 change: 1 addition & 0 deletions lib/mailman/receiver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Receiver

autoload :POP3, 'mailman/receiver/pop3'
autoload :IMAP, 'mailman/receiver/imap'
autoload :HTTP, 'mailman/receiver/http'

end
end
131 changes: 131 additions & 0 deletions lib/mailman/receiver/http.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
require 'rack'
require 'uri'

module Mailman
module Receiver
# Receives messages over HTTP, and passes them to a {MessageProcessor}.
#
# If using CloudMailIn (Raw format) you would make your target "http://yourserver:6245/" and use this code:
#
# Mailman.config.http = {
# host: '0.0.0.0',
# port: 6245,
# path: '/',
# parser: :raw_post,
# parser_opts: {
# part_name: 'message'
# }
# }
#
# Mailman::Application.run do
# # ... etc
# end
#
# However those are all the defaults, so you could also just use:
#
# Mailman.config.http = {}
#
# Mailman::Application.run do
# # ... etc
# end
#
# Sendgrid format is also available, which is simply:
#
# Mailman.config.http = { parser: :sendgrid }
#
# Mailman::Application.run do
# # ... etc
# end
class HTTP

# @param [Hash] options the receiver options
# @option options [MessageProcessor] :processor the processor to pass new
# messages to
# @option options [String] :host ('0.0.0.0') The host the server should listen on
# @option options [Integer] :port (6245) The port the server should listen on
# @option options [String] :path ('/') The path that should trigger email delivery
# @option options [Symbol] :handler (:thin, :webrick) The rack server to use. Falls back on thin, then webrick.
# @option options [Symbol] :parser (:raw_post) The parser which should be used to extract the email content from the HTTP request.
# @option options [Symbol] :parser_opts ({}) Options to be passed to the parser.
def initialize(options)
@processor = options[:processor]
@listen = URI::HTTP.build(
host: options[:host] || "0.0.0.0",
port: options[:port] || 6245,
path: options[:path] || "/"
)

options[:parser] ||= :raw_post
parser_klass = "#{options[:parser].to_s.camelize}Parser"
begin
@parser = Mailman::Receiver::HTTP.const_get(parser_klass).new(options[:parser_opts] || {})
rescue NameError
raise "The Mailman::Receiver::HTTP::#{parser_klass} parser isn't defined."
end

@handler = Rack::Handler.pick([options[:handler], :thin, :webrick].compact)
end

# Starts the HTTP server
def start_and_block
@handler.run(self, {:Host => @listen.host, :Port => @listen.port}) do |server|
Mailman.logger.info "Listening for emails at #{@listen} using #{@parser.class.name.demodulize} processing"
end
end

## Web server components

def call(env)
return [404, {}, []] if env['REQUEST_PATH'] != @listen.path
begin
@processor.process(@parser.parse(env))
return [200, {}, []]
rescue Exception => e
Mailman.logger.error(e.message + "\n#{e.backtrace}")
return [500, {}, ["Email processing failed"]]
end
end

# A class which abstracts the processing of CloudMailIn style 'Raw' emails over HTTP. To use:
#
# Mailman.config.http = {
# parser: :raw_post,
# parser_opts: {
# part_name: 'message'
# }
# }
class RawPostParser
# @param [Hash] opts The parser's options
# @option opts [String] :part_name ('message') The name of the mulipart segment which will contain the email
def initialize(opts = {})
@opts = opts
@opts['part_name'] ||= 'message'
end

# Parses a Rack `env` variable and creates a +Mail::Message+ from the email contents found.
def parse(env)
multipart = Rack::Multipart.parse_multipart(env)
Mail.new(multipart[@opts['part_name']])
end
end

# A class which abstracts the processing of SendGrid style emails over HTTP. To use:
#
# Mailman.config.http = { parser: :sendgrid }
class SendgridParser
# @param [Hash] opts The parser's options - not used for this parser.
def initialize(opts = {}); end

# Parses a Rack `env` variable and creates a +Mail::Message+ from the email contents found.
def parse(env)
parts = Rack::Multipart.parse_multipart(env)
Mail.new do
header parts['headers']
text_part { parts['text'] }
html_part { parts['html'] }
end
end
end
end
end
end
11 changes: 8 additions & 3 deletions lib/mailman/route/conditions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ class Route
class ToCondition < Condition
def match(message)
if !message.to.nil?
message.to.each do |address|
messageto = message.to.is_a?(Array) ? message.to : [message.to]
messageto.each do |address|
if result = @matcher.match(address)
return result
end
Expand Down Expand Up @@ -62,7 +63,11 @@ def match(message)
nil
end
end



class HeaderCondition < Condition
def match(message)
@matcher.match(message.header)
end
end
end
end

0 comments on commit 72c8184

Please sign in to comment.