mailman 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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.
@@ -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
@@ -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,7 @@
1
+ module Mailman
2
+ class Receiver
3
+
4
+ autoload :POP3, 'mailman/receiver/pop3'
5
+
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ module Mailman
2
+ VERSION = '0.1.0'
3
+ 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
+