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 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
+