mailman 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.md +26 -0
- data/USER_GUIDE.md +217 -0
- data/examples/simple_route.rb +13 -0
- data/lib/mailman.rb +36 -0
- data/lib/mailman/application.rb +78 -0
- data/lib/mailman/configuration.rb +34 -0
- data/lib/mailman/message_processor.rb +29 -0
- data/lib/mailman/receiver.rb +7 -0
- data/lib/mailman/receiver/pop3.rb +46 -0
- data/lib/mailman/route.rb +39 -0
- data/lib/mailman/route/condition.rb +53 -0
- data/lib/mailman/route/conditions.rb +43 -0
- data/lib/mailman/route/matcher.rb +65 -0
- data/lib/mailman/route/regexp_matcher.rb +23 -0
- data/lib/mailman/route/string_matcher.rb +44 -0
- data/lib/mailman/router.rb +63 -0
- data/lib/mailman/version.rb +3 -0
- metadata +162 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2010 Jonathan Rudenberg
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Mailman
|
2
|
+
|
3
|
+
Mailman is an incoming mail processing microframework (with POP3 and Maildir
|
4
|
+
support), that works with Rails "out of the box".
|
5
|
+
|
6
|
+
require 'mailman'
|
7
|
+
Mailman::Application.new do
|
8
|
+
to 'ticket-%id%@example.org' do
|
9
|
+
Ticket.find(params[:id]).add_reply(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
See the {file:USER_GUIDE.md} for more information.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
gem install mailman
|
18
|
+
|
19
|
+
## Thanks
|
20
|
+
|
21
|
+
This project is sponsored by the [Ruby Summer of Code](http://rubysoc.org),
|
22
|
+
and my mentor is [Steven Soroka](http://github.com/ssoroka).
|
23
|
+
|
24
|
+
## Copyright
|
25
|
+
|
26
|
+
Copyright (c) 2010 Jonathan Rudenberg. See LICENSE for details.
|
data/USER_GUIDE.md
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
# Mailman User Guide
|
2
|
+
|
3
|
+
Mailman is a microframework for processing incoming email.
|
4
|
+
|
5
|
+
Here is an example Mailman app that takes incoming messages to a support
|
6
|
+
email account, and adds them to a database.
|
7
|
+
|
8
|
+
# mailman_app.rb
|
9
|
+
require 'mailman'
|
10
|
+
|
11
|
+
Mailman.config.maildir = '~/Maildir'
|
12
|
+
|
13
|
+
Mailman::Application.new do
|
14
|
+
to 'support@example.org' do
|
15
|
+
Ticket.new_from_message(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
The Mailman app could then be started by running `ruby mailman_app.rb`.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Installation is as simple as `gem install mailman`.
|
24
|
+
|
25
|
+
|
26
|
+
## Routes & Conditions
|
27
|
+
|
28
|
+
A **Condition** specifies the part of the message to match against. `to`,
|
29
|
+
`from`, and `subject` are some valid conditions. A **Matcher** is used by a
|
30
|
+
condition to determine whether it matches the message. Matchers can be
|
31
|
+
strings or regular expressions. One or more Condition/Matcher pairs are
|
32
|
+
combined with a block of code to form a **Route**.
|
33
|
+
|
34
|
+
### Matchers
|
35
|
+
|
36
|
+
There are string and regular expression matchers. Both can perform captures.
|
37
|
+
|
38
|
+
#### String
|
39
|
+
|
40
|
+
String matchers are very simple. They search through a whole field for a
|
41
|
+
specific substring. For instance: `'ID'` would match `Ticket ID`, `User ID`,
|
42
|
+
etc.
|
43
|
+
|
44
|
+
They can also perform named captures. `'%username%@example.org'` will match any
|
45
|
+
email address that ends with `@example.org`, and store the user part of the
|
46
|
+
address in a capture called `username`. Captures can be accessed by using
|
47
|
+
the `params` helper inside of blocks, or with block arguments (see below for
|
48
|
+
details).
|
49
|
+
|
50
|
+
The capture names may only contain letters and underscores. Behind the scenes
|
51
|
+
they are compiled to regular expressions, and each capture is the equivalent to
|
52
|
+
`.*`. There is currently no way to escape `%` characters. If a literal `%` is
|
53
|
+
required, and Mailman thinks it is a named capture, use a regular expression
|
54
|
+
matcher instead.
|
55
|
+
|
56
|
+
#### Regular expression
|
57
|
+
|
58
|
+
Regular expressions may be used as matchers. All captures will be available from
|
59
|
+
the params helper (`params[:captures]`) as an Array, and as block arguments.
|
60
|
+
|
61
|
+
|
62
|
+
### Routes
|
63
|
+
|
64
|
+
Routes are defined within a Mailman application block:
|
65
|
+
|
66
|
+
Mailman::Application.new do
|
67
|
+
# routes here
|
68
|
+
end
|
69
|
+
|
70
|
+
Messages are passed through routes in the order they are defined in the
|
71
|
+
application from top to bottom. The first matching route's block will be
|
72
|
+
called.
|
73
|
+
|
74
|
+
#### Condition Chaining
|
75
|
+
|
76
|
+
Conditions can be chained so that the route will only be executed if all
|
77
|
+
conditions pass:
|
78
|
+
|
79
|
+
to('support@example.org').subject(/urgent/) do
|
80
|
+
# process urgent message here
|
81
|
+
end
|
82
|
+
|
83
|
+
#### Special routes
|
84
|
+
|
85
|
+
The `default` route is a catch-all that is run if no other routes match:
|
86
|
+
|
87
|
+
default do
|
88
|
+
# process non-matching messages
|
89
|
+
end
|
90
|
+
|
91
|
+
#### Block Arguments
|
92
|
+
|
93
|
+
All captures from matchers are available as block arguments:
|
94
|
+
|
95
|
+
from('%user%@example.org').subject(/Ticket (\d+)/) do |username, ticket_id|
|
96
|
+
puts "Got message from #{username} about Ticket #{ticket_id}"
|
97
|
+
end
|
98
|
+
|
99
|
+
#### Route Helpers
|
100
|
+
|
101
|
+
There are two helpers available inside of route blocks:
|
102
|
+
|
103
|
+
The `params` hash holds all captures from matchers:
|
104
|
+
|
105
|
+
from('%user%@example.org').subject(/RE: (.*)/) do
|
106
|
+
params[:user] #=> 'chunkybacon'
|
107
|
+
# it is an indifferent hash, so you can use strings and symbols
|
108
|
+
# interchangeably as keys
|
109
|
+
params['captures'][0] #=> 'A very important message about pigs'
|
110
|
+
end
|
111
|
+
|
112
|
+
The `message` helper is a `Mail::Message` object that contains the entire
|
113
|
+
message. See the [mail](http://github.com/mikel/mail/) docs for information on
|
114
|
+
the properties available.
|
115
|
+
|
116
|
+
|
117
|
+
### Conditions
|
118
|
+
|
119
|
+
Currently there are four conditions available: `to`, `from`, `subject`, `body`
|
120
|
+
|
121
|
+
More can be added easily (see `lib/mailman/route/conditions.rb`).
|
122
|
+
|
123
|
+
|
124
|
+
## Receivers
|
125
|
+
|
126
|
+
There are currently three types of receivers in Mailman: Standard Input,
|
127
|
+
Maildir, and POP3. If IMAP or any complex setups are required, use a mail
|
128
|
+
retriever like [getmail](http://pyropus.ca/software/getmail/) with the
|
129
|
+
Maildir receiver.
|
130
|
+
|
131
|
+
|
132
|
+
### Standard Input
|
133
|
+
|
134
|
+
If a message is piped to a Mailman app, this receiver will override any
|
135
|
+
configured receivers. The app will process the message, and then quit. This
|
136
|
+
receiver is useful for testing and debugging.
|
137
|
+
|
138
|
+
**Example**: `cat plain_message.eml | ruby mailman_app.rb`
|
139
|
+
|
140
|
+
|
141
|
+
### POP3
|
142
|
+
|
143
|
+
The POP3 receiver is enabled when the `Mailman.config.pop3` hash is set. It
|
144
|
+
will poll every minute by default (this can be changed with
|
145
|
+
`Mailman.config.poll_interval`). After new messages are processed, they will
|
146
|
+
be deleted from the server. *No copy of messages will be saved anywhere
|
147
|
+
after processing*. If you want to keep a copy of messages, it is recommended
|
148
|
+
that you use a mail retriever with the Maildir receiver.
|
149
|
+
|
150
|
+
|
151
|
+
### Maildir
|
152
|
+
|
153
|
+
The Maildir receiver is enabled when `Mailman.config.maildir` is set to a
|
154
|
+
directory. If the `cur`, `new`, and `tmp` folders do not already exist in
|
155
|
+
the folder, they will be created. All messages in `new` folder will be
|
156
|
+
processed when the application launches, then moved to the `cur` folder, and
|
157
|
+
marked as seen. After processing these messages, Mailman will use the
|
158
|
+
[fssm](http://github.com/ttilley/fssm) gem to monitor the `new` folder, and
|
159
|
+
process messages as they are created.
|
160
|
+
|
161
|
+
## Configuration
|
162
|
+
|
163
|
+
Configuration is stored in the `Mailman.config` object. All paths are
|
164
|
+
relative to the process's working directory or absolute if starting with a
|
165
|
+
`/`.
|
166
|
+
|
167
|
+
|
168
|
+
### Logging
|
169
|
+
|
170
|
+
`Mailman.config.logger` can be set to a `Logger` instance. You should
|
171
|
+
change this if you want to log to a file in production.
|
172
|
+
|
173
|
+
**Example**: `Mailman.config.logger = Logger.new('logs/mailman.log')`
|
174
|
+
|
175
|
+
**Default**: `Logger.new(STDOUT)`
|
176
|
+
|
177
|
+
|
178
|
+
### POP3 Receiver
|
179
|
+
|
180
|
+
`Mailman.config.pop3` stores an optional POP3 configuration hash. If it is
|
181
|
+
set, Mailman will use POP3 polling as the receiver.
|
182
|
+
|
183
|
+
**Example**:
|
184
|
+
|
185
|
+
Mailman.config.pop3 = {
|
186
|
+
:username => 'chunky',
|
187
|
+
:password => 'bacon',
|
188
|
+
:server => 'example.org',
|
189
|
+
:port => 110 # defaults to 110
|
190
|
+
}
|
191
|
+
|
192
|
+
|
193
|
+
### Polling
|
194
|
+
|
195
|
+
`Mailman.config.poll_interval` is the duration in seconds to wait between
|
196
|
+
checking for new messages on the server. It is currently only used by the
|
197
|
+
POP3 reciever. If it is set to `0`, Mailman will do a one-time retrieval and
|
198
|
+
then exit.
|
199
|
+
|
200
|
+
**Default**: `60`
|
201
|
+
|
202
|
+
|
203
|
+
### Maildir
|
204
|
+
|
205
|
+
`Mailman.config.maildir` is the location of a Maildir folder to watch. If it
|
206
|
+
is set, Mailman will use Maildir watching as the receiver.
|
207
|
+
|
208
|
+
**Example**: `Mailman.config.maildir = '~/Maildir'`
|
209
|
+
|
210
|
+
|
211
|
+
### Rails
|
212
|
+
|
213
|
+
`Mailman.config.rails_root` is the location of the root of a Rails app to
|
214
|
+
load the environment from. If this option is set to `nil`, Rails environment
|
215
|
+
loading will be disabled.
|
216
|
+
|
217
|
+
**Default**: `'.'`
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'mailman'
|
4
|
+
|
5
|
+
Mailman::Application.new do
|
6
|
+
|
7
|
+
from('%user%@%domain%') do
|
8
|
+
puts "Got #{message.subject} from #{params[:user]}"
|
9
|
+
end
|
10
|
+
|
11
|
+
end.run
|
12
|
+
|
13
|
+
# cat ../spec/fixtures/example01.eml | ./simple_route.rb
|
data/lib/mailman.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'fcntl'
|
3
|
+
require 'mail'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
7
|
+
|
8
|
+
require 'maildir'
|
9
|
+
require 'fssm'
|
10
|
+
|
11
|
+
require 'mailman/version'
|
12
|
+
|
13
|
+
module Mailman
|
14
|
+
|
15
|
+
[:Application, :Router, :Configuration, :Receiver, :MessageProcessor].each do |constant|
|
16
|
+
autoload constant, "mailman/#{constant.to_s.underscore}"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'mailman/route'
|
20
|
+
|
21
|
+
# @return [Configuration] the application configuration.
|
22
|
+
def self.config
|
23
|
+
@config ||= Configuration.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Resets the app config (useful for testing).
|
27
|
+
def self.reset_config!
|
28
|
+
@config = Configuration.new
|
29
|
+
end
|
30
|
+
|
31
|
+
# Shortcut for the logger object
|
32
|
+
def self.logger
|
33
|
+
Mailman.config.logger
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Mailman
|
2
|
+
# The main application class. Pass a block to {#new} to create a new app.
|
3
|
+
class Application
|
4
|
+
|
5
|
+
# @return [Router] the app's router
|
6
|
+
attr_reader :router
|
7
|
+
|
8
|
+
# @return [MessageProcessor] the app's message processor
|
9
|
+
attr_reader :processor
|
10
|
+
|
11
|
+
# Creates a new router, and sets up any routes passed in the block.
|
12
|
+
# @param [Proc] block a block with routes
|
13
|
+
def initialize(&block)
|
14
|
+
@router = Mailman::Router.new
|
15
|
+
@processor = MessageProcessor.new(:router => @router)
|
16
|
+
instance_eval(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sets the block to run if no routes match a message.
|
20
|
+
def default(&block)
|
21
|
+
@router.default_block = block
|
22
|
+
end
|
23
|
+
|
24
|
+
# Runs the application.
|
25
|
+
def run
|
26
|
+
Mailman.logger.info "Mailman v#{Mailman::VERSION} started"
|
27
|
+
|
28
|
+
rails_env = File.join(Mailman.config.rails_root, 'config', 'environment.rb')
|
29
|
+
if Mailman.config.rails_root && File.exist?(rails_env)
|
30
|
+
Mailman.logger.info "Rails root found in #{Mailman.config.rails_root}, requiring environment..."
|
31
|
+
require rails_env
|
32
|
+
end
|
33
|
+
|
34
|
+
if $stdin.fcntl(Fcntl::F_GETFL, 0) == 0 # we have stdin
|
35
|
+
Mailman.logger.debug "Processing message from STDIN."
|
36
|
+
@processor.process($stdin.read)
|
37
|
+
elsif Mailman.config.pop3
|
38
|
+
options = {:processor => @processor}.merge(Mailman.config.pop3)
|
39
|
+
Mailman.logger.info "POP3 receiver enabled (#{options[:username]}@#{options[:server]})."
|
40
|
+
connection = Receiver::POP3.new(options)
|
41
|
+
begin
|
42
|
+
connection.connect
|
43
|
+
if Mailman.config.poll_interval > 0 # we should poll
|
44
|
+
Mailman.logger.info "Polling enabled. Checking every #{Mailman.config.poll_interval} seconds."
|
45
|
+
loop do
|
46
|
+
Mailman.logger.debug "Polling POP3 server for messages..."
|
47
|
+
connection.get_messages
|
48
|
+
sleep Mailman.config.poll_interval
|
49
|
+
end
|
50
|
+
else # one-time retrieval
|
51
|
+
Mailman.logger.info "Polling disabled. Checking for messages..."
|
52
|
+
connection.get_messages
|
53
|
+
end
|
54
|
+
ensure
|
55
|
+
connection.disconnect
|
56
|
+
end
|
57
|
+
elsif Mailman.config.maildir
|
58
|
+
Mailman.logger.info "Maildir receiver enabled (#{Mailman.config.maildir})."
|
59
|
+
maildir = Maildir.new(Mailman.config.maildir)
|
60
|
+
|
61
|
+
# Process messages queued in the new directory
|
62
|
+
Mailman.logger.debug "Processing new message queue..."
|
63
|
+
maildir.list(:new).each do |message|
|
64
|
+
@processor.process_maildir_message(message)
|
65
|
+
end
|
66
|
+
|
67
|
+
Mailman.logger.debug "Monitoring the Maildir for new messages..."
|
68
|
+
FSSM.monitor File.join(Mailman.config.maildir, 'new') do |monitor|
|
69
|
+
monitor.create { |directory, filename| # a new message was delivered to new
|
70
|
+
message = Maildir::Message.new(maildir, "new/#{filename}")
|
71
|
+
@processor.process_maildir_message(message)
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Mailman
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
# @return [Logger] the application's logger
|
5
|
+
attr_accessor :logger
|
6
|
+
|
7
|
+
# @return [Hash] the configuration hash for POP3
|
8
|
+
attr_accessor :pop3
|
9
|
+
|
10
|
+
# @return [Fixnum] the poll interval for POP3 or IMAP. Setting this to 0
|
11
|
+
# disables polling
|
12
|
+
attr_accessor :poll_interval
|
13
|
+
|
14
|
+
# @return [String] the path to the maildir
|
15
|
+
attr_accessor :maildir
|
16
|
+
|
17
|
+
# @return [String] the path to the rails root. Setting this to nil stops
|
18
|
+
# rails environment loading
|
19
|
+
attr_accessor :rails_root
|
20
|
+
|
21
|
+
def logger
|
22
|
+
@logger ||= Logger.new(STDOUT)
|
23
|
+
end
|
24
|
+
|
25
|
+
def poll_interval
|
26
|
+
@poll_interval ||= 60
|
27
|
+
end
|
28
|
+
|
29
|
+
def rails_root
|
30
|
+
@rails_root ||= '.'
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Mailman
|
2
|
+
# Turns a raw email into a +Mail::Message+ and passes it to the router.
|
3
|
+
class MessageProcessor
|
4
|
+
|
5
|
+
# @param [Hash] options the options to create the processor with
|
6
|
+
# @option options [Router] :router the router to pass processed
|
7
|
+
# messages to
|
8
|
+
def initialize(options)
|
9
|
+
@router = options[:router]
|
10
|
+
end
|
11
|
+
|
12
|
+
# Converts a raw email into a +Mail::Message+ instance, and passes it to the
|
13
|
+
# router.
|
14
|
+
# @param [String] message the message to process
|
15
|
+
def process(message)
|
16
|
+
message = Mail.new(message)
|
17
|
+
Mailman.logger.info "Got new message from '#{message.from.first}' with subject '#{message.subject}'."
|
18
|
+
@router.route(message)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Processes a +Maildir::Message+ instance.
|
22
|
+
def process_maildir_message(message)
|
23
|
+
message.process # move message to cur
|
24
|
+
message.seen!
|
25
|
+
process(message.data)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'net/pop'
|
2
|
+
|
3
|
+
module Mailman
|
4
|
+
class Receiver
|
5
|
+
# Receives messages using POP3, and passes them to a {MessageProcessor}.
|
6
|
+
class POP3
|
7
|
+
|
8
|
+
# @return [Net::POP3] the POP3 connection
|
9
|
+
attr_reader :connection
|
10
|
+
|
11
|
+
# @param [Hash] options the receiver options
|
12
|
+
# @option options [MessageProcessor] :processor the processor to pass new
|
13
|
+
# messages to
|
14
|
+
# @option options [String] :server the server to connect to
|
15
|
+
# @option options [Integer] :port the port to connect to
|
16
|
+
# @option options [String] :username the username to authenticate with
|
17
|
+
# @option options [String] :password the password to authenticate with
|
18
|
+
def initialize(options)
|
19
|
+
@processor = options[:processor]
|
20
|
+
@username = options[:username]
|
21
|
+
@password = options[:password]
|
22
|
+
@connection = Net::POP3.new(options[:server], options[:port])
|
23
|
+
end
|
24
|
+
|
25
|
+
# Connects to the POP3 server.
|
26
|
+
def connect
|
27
|
+
@connection.start(@username, @password)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Disconnects from the POP3 server.
|
31
|
+
def disconnect
|
32
|
+
@connection.finish
|
33
|
+
end
|
34
|
+
|
35
|
+
# Iterates through new messages, passing them to the processor, and
|
36
|
+
# deleting them.
|
37
|
+
def get_messages
|
38
|
+
@connection.each_mail do |message|
|
39
|
+
@processor.process(message.pop)
|
40
|
+
end
|
41
|
+
@connection.delete_all
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Mailman
|
2
|
+
# The main route class. Has condition methods defined on it by
|
3
|
+
# {Route::Condition.inherited}. Stores a route with a set of conditions and
|
4
|
+
# matches against them.
|
5
|
+
class Route
|
6
|
+
|
7
|
+
# @return [Proc] the block that should be run if the conditions match
|
8
|
+
attr_reader :block
|
9
|
+
|
10
|
+
# @return [Array] the list of condition instances associated with the route
|
11
|
+
attr_reader :conditions
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@conditions = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# Checks whether a message matches the route.
|
18
|
+
# @param [Mail::Message] message the message to match against
|
19
|
+
# @return [Hash] the +:block+ associated with the route, the
|
20
|
+
# +:params+ hash, and the block +:args+ array.
|
21
|
+
def match!(message)
|
22
|
+
params = {}
|
23
|
+
args = []
|
24
|
+
@conditions.each do |condition|
|
25
|
+
if result = condition.match(message)
|
26
|
+
params.merge!(result[0])
|
27
|
+
args += result[1]
|
28
|
+
else
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
{ :block => @block, :params => params, :args => args }
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'mailman/route/matcher'
|
39
|
+
require 'mailman/route/condition'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Mailman
|
2
|
+
class Route
|
3
|
+
# The base condition class. All conditions should subclass and override
|
4
|
+
# {#match}.
|
5
|
+
class Condition
|
6
|
+
|
7
|
+
# @return the matcher to match against.
|
8
|
+
attr_reader :matcher
|
9
|
+
|
10
|
+
# @param [String, Regexp] the raw matcher to use in the condition,
|
11
|
+
# converted to a matcher instance by {Matcher.create}
|
12
|
+
def initialize(condition)
|
13
|
+
@matcher = Matcher.create(condition)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @abstract Extracts the attribute from the message, and runs the matcher
|
17
|
+
# on it.
|
18
|
+
# @param message [Mail::Message] The message to match against
|
19
|
+
# @return [(Hash, Array)] a hash to merge into params, and an array of
|
20
|
+
# block arguments.
|
21
|
+
def match(message)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
# Registers a condition subclass, which creates instance methods on
|
26
|
+
# {Route} and {Application}.
|
27
|
+
# @param [Class] condition the condition subclass to register. The method
|
28
|
+
# name is extracted by taking the class name, such as +ToCondition+,
|
29
|
+
# and removing the +Condition+ ending
|
30
|
+
def self.inherited(condition)
|
31
|
+
condition_name = condition.to_s.split('::')[-1][0...-9].downcase
|
32
|
+
Route.class_eval <<-EOM
|
33
|
+
def #{condition_name}(pattern, &block)
|
34
|
+
@conditions << #{condition}.new(pattern)
|
35
|
+
if block_given?
|
36
|
+
@block = block
|
37
|
+
end
|
38
|
+
self
|
39
|
+
end
|
40
|
+
EOM
|
41
|
+
|
42
|
+
Application.class_eval <<-EOM
|
43
|
+
def #{condition_name}(pattern, &block)
|
44
|
+
@router.add_route Route.new.#{condition_name}(pattern, &block)
|
45
|
+
end
|
46
|
+
EOM
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
require 'mailman/route/conditions'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Mailman
|
2
|
+
class Route
|
3
|
+
|
4
|
+
# Matches against the To addresses of a message.
|
5
|
+
class ToCondition < Condition
|
6
|
+
def match(message)
|
7
|
+
message.to.each do |address|
|
8
|
+
if result = @matcher.match(address)
|
9
|
+
return result
|
10
|
+
end
|
11
|
+
end
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Matches against the From addresses of a message.
|
17
|
+
class FromCondition < Condition
|
18
|
+
def match(message)
|
19
|
+
message.from.each do |address|
|
20
|
+
if result = @matcher.match(address)
|
21
|
+
return result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Matches against the Subject of a message.
|
29
|
+
class SubjectCondition < Condition
|
30
|
+
def match(message)
|
31
|
+
@matcher.match(message.subject)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Matches against the Body of a message.
|
36
|
+
class BodyCondition < Condition
|
37
|
+
def match(message)
|
38
|
+
@matcher.match(message.body.decoded)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Mailman
|
2
|
+
class Route
|
3
|
+
# The base matcher class. All matchers should subclass and override {#match}
|
4
|
+
# and {Matcher.valid_pattern?}. Override {#compile!} if a pattern compiler is
|
5
|
+
# needed.
|
6
|
+
class Matcher
|
7
|
+
|
8
|
+
# @return the matcher pattern, normally stored as a +Regexp+.
|
9
|
+
attr_reader :pattern
|
10
|
+
|
11
|
+
# Creates a new matcher and calls {#compile!}.
|
12
|
+
# @param pattern the matcher pattern
|
13
|
+
def initialize(pattern)
|
14
|
+
@pattern = pattern
|
15
|
+
compile!
|
16
|
+
end
|
17
|
+
|
18
|
+
# @abstract Matches a string against the stored pattern.
|
19
|
+
# @param [String] string the string to match against
|
20
|
+
# @return [(Hash, Array)] a hash to merge into params, and an array of
|
21
|
+
# arguments for the block.
|
22
|
+
def match(string)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
# @abstract Compiles the pattern into something easier to work with, usually a
|
27
|
+
# +Regexp+.
|
28
|
+
def compile!
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
|
33
|
+
# @return [<Class>] The array of registered matchers.
|
34
|
+
attr_reader :matchers
|
35
|
+
|
36
|
+
# Registers a matcher so that it can be used in {Matcher.create}.
|
37
|
+
# @param matcher [Class] a matcher subclass
|
38
|
+
def inherited(matcher)
|
39
|
+
@matchers ||= []
|
40
|
+
@matchers << matcher
|
41
|
+
end
|
42
|
+
|
43
|
+
# Finds and creates a valid matcher instance for a given pattern.
|
44
|
+
# @param pattern the pattern to create the matcher with
|
45
|
+
def create(pattern)
|
46
|
+
@matchers.each do |matcher|
|
47
|
+
return matcher.new(pattern) if matcher.valid_pattern?(pattern)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @abstract Checks whether a pattern is valid.
|
52
|
+
# @param pattern the pattern to check
|
53
|
+
# @return [true, false]
|
54
|
+
def valid_pattern?(pattern)
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
require 'mailman/route/regexp_matcher'
|
65
|
+
require 'mailman/route/string_matcher'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Mailman
|
2
|
+
class Route
|
3
|
+
# Matches using a +Regexp+.
|
4
|
+
class RegexpMatcher < Matcher
|
5
|
+
|
6
|
+
# Matches against a string using the stored +Regexp+.
|
7
|
+
# @param [String] string the string to match against
|
8
|
+
# @return [({:captures => <String>}, <String>)] the params hash with
|
9
|
+
# +:captures+ set to an array of captures, and an array of captures.
|
10
|
+
def match(string)
|
11
|
+
if match = @pattern.match(string)
|
12
|
+
captures = match.captures
|
13
|
+
[{:captures => captures}, captures]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.valid_pattern?(pattern)
|
18
|
+
pattern.class == Regexp
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Mailman
|
2
|
+
class Route
|
3
|
+
# Matches using a +String+ with named param captures formatted like
|
4
|
+
# +%user%@example.com+.
|
5
|
+
class StringMatcher < Matcher
|
6
|
+
|
7
|
+
# @return [Array<Symbol>] the names of the param captures
|
8
|
+
attr_reader :keys
|
9
|
+
|
10
|
+
# Matches against a string using the stored pattern.
|
11
|
+
# @param [String] string the string to match against
|
12
|
+
# @return [({Symbol => String}, <String>)] the params hash, and array of
|
13
|
+
# captures.
|
14
|
+
def match(string)
|
15
|
+
params = {}
|
16
|
+
if match = @pattern.match(string)
|
17
|
+
captures = match.captures
|
18
|
+
params.merge!(Hash[*@keys.zip(captures).flatten])
|
19
|
+
[params, captures]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def compile!
|
24
|
+
@keys = []
|
25
|
+
special_chars = %w/* . + ? \\ | ^ $ ( ) [ ] { } /
|
26
|
+
compiled_pattern = @pattern.to_s.gsub(/((%[A-Za-z_]+%)|[\*\\.+?|^$()\[\]{}])/) do |match|
|
27
|
+
case match
|
28
|
+
when *special_chars
|
29
|
+
Regexp.escape(match)
|
30
|
+
else
|
31
|
+
@keys << $2[1..-2].to_sym
|
32
|
+
'(.*)'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@pattern = /#{compiled_pattern}/i
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.valid_pattern?(pattern)
|
39
|
+
pattern.respond_to?(:to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Mailman
|
2
|
+
# The router. Stores routes and uses them to process messages.
|
3
|
+
class Router
|
4
|
+
|
5
|
+
# @return [Array] the list of routes
|
6
|
+
attr_accessor :routes
|
7
|
+
|
8
|
+
# @return [Proc] the block to run if a message has bounced
|
9
|
+
attr_accessor :bounce_block
|
10
|
+
|
11
|
+
# @return [Proc] the block to run if no routes match
|
12
|
+
attr_accessor :default_block
|
13
|
+
|
14
|
+
# @return [Hash] the params of the most recently processed message. Used by
|
15
|
+
# route blocks
|
16
|
+
attr_reader :params
|
17
|
+
|
18
|
+
# @return [Mail::Message] the most recently processed message
|
19
|
+
attr_reader :message
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@routes = []
|
23
|
+
@params = HashWithIndifferentAccess.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Adds a route to the router.
|
27
|
+
# @param [Mailman::Route] the route to add.
|
28
|
+
# @return [Mailman::Route] the route object that was added (allows
|
29
|
+
# chaining).
|
30
|
+
def add_route(route)
|
31
|
+
@routes.push(route)[-1]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Route a message. If the route block accepts arguments, it passes any
|
35
|
+
# captured params. Named params are available from the +params+ helper. The
|
36
|
+
# message is available from the +message+ helper.
|
37
|
+
# @param [Mail::Message] the message to route.
|
38
|
+
def route(message)
|
39
|
+
@message = message
|
40
|
+
result = nil
|
41
|
+
|
42
|
+
if @bounce_block and message.respond_to?(:bounced?) and message.bounced?
|
43
|
+
return instance_exec(&@bounce_block)
|
44
|
+
end
|
45
|
+
|
46
|
+
routes.each do |route|
|
47
|
+
break if result = route.match!(message)
|
48
|
+
end
|
49
|
+
|
50
|
+
if result
|
51
|
+
@params.merge!(result[:params])
|
52
|
+
if result[:block].arity > 0
|
53
|
+
instance_exec(*result[:args], &result[:block])
|
54
|
+
else
|
55
|
+
instance_exec(&result[:block])
|
56
|
+
end
|
57
|
+
elsif @default_block
|
58
|
+
instance_exec(&@default_block)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mailman
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jonathan Rudenberg
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-07-27 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: mail
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 9
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 0
|
33
|
+
- 3
|
34
|
+
version: 2.0.3
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: activesupport
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 11
|
46
|
+
segments:
|
47
|
+
- 2
|
48
|
+
- 3
|
49
|
+
- 4
|
50
|
+
version: 2.3.4
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: fssm
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 19
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 1
|
65
|
+
- 4
|
66
|
+
version: 0.1.4
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: maildir
|
71
|
+
prerelease: false
|
72
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 11
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
- 5
|
81
|
+
- 0
|
82
|
+
version: 0.5.0
|
83
|
+
type: :runtime
|
84
|
+
version_requirements: *id004
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: rspec
|
87
|
+
prerelease: false
|
88
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
hash: 3
|
94
|
+
segments:
|
95
|
+
- 0
|
96
|
+
version: "0"
|
97
|
+
type: :development
|
98
|
+
version_requirements: *id005
|
99
|
+
description: Mailman makes it easy to process incoming emails with a simple routing DSL
|
100
|
+
email:
|
101
|
+
- jonathan@titanous.com
|
102
|
+
executables: []
|
103
|
+
|
104
|
+
extensions: []
|
105
|
+
|
106
|
+
extra_rdoc_files: []
|
107
|
+
|
108
|
+
files:
|
109
|
+
- lib/mailman/application.rb
|
110
|
+
- lib/mailman/configuration.rb
|
111
|
+
- lib/mailman/message_processor.rb
|
112
|
+
- lib/mailman/receiver/pop3.rb
|
113
|
+
- lib/mailman/receiver.rb
|
114
|
+
- lib/mailman/route/condition.rb
|
115
|
+
- lib/mailman/route/conditions.rb
|
116
|
+
- lib/mailman/route/matcher.rb
|
117
|
+
- lib/mailman/route/regexp_matcher.rb
|
118
|
+
- lib/mailman/route/string_matcher.rb
|
119
|
+
- lib/mailman/route.rb
|
120
|
+
- lib/mailman/router.rb
|
121
|
+
- lib/mailman/version.rb
|
122
|
+
- lib/mailman.rb
|
123
|
+
- examples/simple_route.rb
|
124
|
+
- LICENSE
|
125
|
+
- README.md
|
126
|
+
- USER_GUIDE.md
|
127
|
+
has_rdoc: true
|
128
|
+
homepage: http://mailmanrb.com
|
129
|
+
licenses: []
|
130
|
+
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
hash: 3
|
142
|
+
segments:
|
143
|
+
- 0
|
144
|
+
version: "0"
|
145
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
146
|
+
none: false
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
hash: 3
|
151
|
+
segments:
|
152
|
+
- 0
|
153
|
+
version: "0"
|
154
|
+
requirements: []
|
155
|
+
|
156
|
+
rubyforge_project: mailman
|
157
|
+
rubygems_version: 1.3.7
|
158
|
+
signing_key:
|
159
|
+
specification_version: 3
|
160
|
+
summary: A incoming email processing microframework
|
161
|
+
test_files: []
|
162
|
+
|