mailman 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|