newman 0.1.1 → 0.2.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/CHANGELOG.md +23 -0
- data/Gemfile +3 -0
- data/README.md +126 -7
- data/examples/config/dreamhost.rb.example +5 -1
- data/examples/config/gmail.rb.example +4 -0
- data/examples/live_test.rb +4 -7
- data/examples/simple_mailing_list.rb +2 -2
- data/lib/newman.rb +97 -1
- data/lib/newman/application.rb +238 -1
- data/lib/newman/controller.rb +93 -8
- data/lib/newman/email_logger.rb +54 -0
- data/lib/newman/filters.rb +123 -0
- data/lib/newman/mailer.rb +98 -32
- data/lib/newman/mailing_list.rb +83 -0
- data/lib/newman/recorder.rb +132 -0
- data/lib/newman/request_logger.rb +41 -0
- data/lib/newman/response_logger.rb +41 -0
- data/lib/newman/server.rb +197 -23
- data/lib/newman/settings.rb +72 -4
- data/lib/newman/store.rb +68 -4
- data/lib/newman/test_mailer.rb +79 -6
- data/lib/newman/version.rb +5 -2
- data/test/helper.rb +18 -0
- data/{examples → test/integration}/acid_tests.rb +30 -20
- data/test/integration/skip_response_test.rb +22 -0
- data/test/integration/subject_filter_test.rb +25 -0
- data/test/settings.rb +4 -0
- data/test/suite.rb +6 -0
- metadata +74 -9
- data/lib/newman/commands.rb +0 -72
- data/lib/newman/store/recorder.rb +0 -63
data/lib/newman/controller.rb
CHANGED
@@ -1,30 +1,99 @@
|
|
1
|
+
# `Newman::Controller` provides a context for application callbacks to run in,
|
2
|
+
# and provides most of the core functionality for preparing a response email.
|
3
|
+
#
|
4
|
+
# For a full example of `Newman::Controller` in action, be sure to check out
|
5
|
+
# [Jester](https://github.com/mendicant-university/jester).
|
6
|
+
#
|
7
|
+
# `Newman::Controller` is part of Newman's **external interface**.
|
8
|
+
|
1
9
|
module Newman
|
2
10
|
class Controller
|
11
|
+
|
12
|
+
#---
|
13
|
+
|
14
|
+
# A `Newman::Controller` object is initialized with parameters that match
|
15
|
+
# what is provided by the low level `Newman::Server` object. Generally
|
16
|
+
# speaking, you won't instantiate controller objects yourself, but instead
|
17
|
+
# will rely on `Newman::Application` to instantiate them for you.
|
18
|
+
|
3
19
|
def initialize(params)
|
4
20
|
self.settings = params.fetch(:settings)
|
5
21
|
self.request = params.fetch(:request)
|
6
22
|
self.response = params.fetch(:response)
|
23
|
+
self.logger = params.fetch(:logger)
|
7
24
|
end
|
8
25
|
|
9
|
-
|
26
|
+
# ---
|
27
|
+
|
28
|
+
# All of the fields on `Newman::Controller` are public, so that they can
|
29
|
+
# freely be manipulated by callbacks. We may lock this down a bit more
|
30
|
+
# in a future version of Newman once we figure out what data actually needs
|
31
|
+
# to be exposed in callbacks, but for now you can feel free to depend on
|
32
|
+
# any of these fields.
|
33
|
+
|
34
|
+
attr_accessor :settings, :request, :response, :logger, :params
|
35
|
+
|
36
|
+
|
37
|
+
# ---
|
38
|
+
|
39
|
+
# `Newman::Controller#respond` is used to modify the response email object,
|
40
|
+
# and is used in the manner shown below:
|
41
|
+
#
|
42
|
+
# respond :subject => "Hello There",
|
43
|
+
# :body => "It's nice to meet you, pal!"
|
44
|
+
#
|
45
|
+
# Because this method simply provides syntactic sugar on top of the
|
46
|
+
# `Mail::Message` object's interface, you should be sure to take a look at
|
47
|
+
# the documentation for the [mail gem](http://github.com/mikel/mail) to
|
48
|
+
# discover what options are available.
|
10
49
|
|
11
50
|
def respond(params)
|
12
51
|
params.each { |k,v| response.send("#{k}=", v) }
|
13
52
|
end
|
14
53
|
|
54
|
+
# ---
|
55
|
+
|
56
|
+
# `Newman::Controller#template` is used to invoke a template file within the
|
57
|
+
# context of the current controller object using Tilt. A name for the template is
|
58
|
+
# provided and then looked up in the directory referenced by
|
59
|
+
# `settings.service.templates_dir`. While an example of using templates is
|
60
|
+
# included in Newman's source, this feature hasn't really been tested
|
61
|
+
# adequately. Please report any problems with this method in our
|
62
|
+
# [issue tracker](https://github.com/mendicant-university/newman/issues).
|
63
|
+
|
15
64
|
def template(name)
|
16
65
|
Tilt.new(Dir.glob("#{settings.service.templates_dir}/#{name}.*").first)
|
17
66
|
.render(self)
|
18
67
|
end
|
19
68
|
|
20
|
-
|
21
|
-
request.from.first.to_s
|
22
|
-
end
|
69
|
+
# ---
|
23
70
|
|
24
|
-
|
25
|
-
|
71
|
+
# `Newman::Controller#skip_response` is used for disabling the delivery of
|
72
|
+
# the response email. Use this for situations where no response is required,
|
73
|
+
# such as when a spam email or a bounce has been detected, or if you are
|
74
|
+
# building an application which simply passively monitors incoming email
|
75
|
+
# rather than replying to it.
|
76
|
+
|
77
|
+
def skip_response
|
78
|
+
response.perform_deliveries = false
|
26
79
|
end
|
27
80
|
|
81
|
+
# ---
|
82
|
+
|
83
|
+
# `Newman::Controller#forward_message` works in a similar fashion to
|
84
|
+
# `Newman::Controller#response`, but copies the request FROM, SUBJECT
|
85
|
+
# and BODY fields and sets the REPLY TO field to be equal to
|
86
|
+
# `settings.service.default_sender`. This feature is convenient for
|
87
|
+
# implementing mailing-list style functionality, such as in the
|
88
|
+
# following example:
|
89
|
+
#
|
90
|
+
# if list.subscriber?(sender)
|
91
|
+
# forward_message :bcc => list.subscribers.join(", ")
|
92
|
+
# else
|
93
|
+
# respond :subject => "You are not subscribed",
|
94
|
+
# :body => template("non-subscriber-error")
|
95
|
+
# end
|
96
|
+
|
28
97
|
def forward_message(params={})
|
29
98
|
response.from = request.from
|
30
99
|
response.reply_to = settings.service.default_sender
|
@@ -42,8 +111,24 @@ module Newman
|
|
42
111
|
end
|
43
112
|
end
|
44
113
|
|
45
|
-
|
114
|
+
# ---
|
115
|
+
|
116
|
+
# `Newman::Controller#sender` is used as a convenient shortcut for
|
117
|
+
# retrieving the sender's email address from the request object.
|
118
|
+
|
119
|
+
def sender
|
120
|
+
request.from.first.to_s
|
121
|
+
end
|
122
|
+
|
123
|
+
# ---
|
124
|
+
|
125
|
+
# `Newman::Controller#domain` is used as a convenient shortcut for
|
126
|
+
# referencing `settings.service.domain`.
|
127
|
+
|
128
|
+
def domain
|
129
|
+
settings.service.domain
|
130
|
+
end
|
131
|
+
|
46
132
|
|
47
|
-
attr_accessor :settings, :request, :response
|
48
133
|
end
|
49
134
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# `Newman::EmailLogger` provides rudimentary logging support for email objects,
|
2
|
+
# and primarily exists to support the `Newman::RequestLogger` and
|
3
|
+
# `Newman::ResponseLogger` objects.
|
4
|
+
#
|
5
|
+
# If you are only interested in making use of this logging functionality and not
|
6
|
+
# extending it or changing it in some way, you do not need to be familiar with
|
7
|
+
# the code in this file. Just be sure to note that if you add
|
8
|
+
# `service.debug_mode = true` to your configuration file, or set the Ruby
|
9
|
+
# `$DEBUG` global variable, you will get much more verbose output from
|
10
|
+
# Newman's logging system.
|
11
|
+
#
|
12
|
+
# `Newman::EmailLogger` is part of Newman's **internal interface**.
|
13
|
+
|
14
|
+
module Newman
|
15
|
+
module EmailLogger
|
16
|
+
|
17
|
+
# ---
|
18
|
+
|
19
|
+
# `Newman::EmailLogger#log_email` takes a logger object, a prefix, and a `Mail` object and
|
20
|
+
# then outputs relevant debugging details.
|
21
|
+
#
|
22
|
+
# This method always at least provides a summary of the provided `email`
|
23
|
+
# at the `INFO` level When in debugging mode, the full contents of
|
24
|
+
# the `email` will also gets logged.
|
25
|
+
#
|
26
|
+
# The main purpose of this method is to be used by `Newman::RequestLogger`
|
27
|
+
# and `Newman::ResponseLogger`, but may also optionally be used as a helper
|
28
|
+
# for those who are rolling their own logging functionality.
|
29
|
+
|
30
|
+
def log_email(logger, prefix, email)
|
31
|
+
logger.debug(prefix) { "\n#{email}" }
|
32
|
+
logger.info(prefix) { email_summary(email) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# ---
|
36
|
+
|
37
|
+
# **NOTE: Methods below this point in the file are implementation details,
|
38
|
+
# and should not be depended upon**
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# ---
|
43
|
+
|
44
|
+
# `Newman::EmailLogger#email_summary` returns a hash with a summary of the provided `email` object.
|
45
|
+
|
46
|
+
def email_summary(email)
|
47
|
+
{ :from => email.from,
|
48
|
+
:to => email.to,
|
49
|
+
:bcc => email.bcc,
|
50
|
+
:subject => email.subject,
|
51
|
+
:reply_to => email.reply_to }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# `Newman::Filters` provides the standard filtering mechanisms for
|
2
|
+
# Newman applications.
|
3
|
+
#
|
4
|
+
# Unless you are building a server-side extension for
|
5
|
+
# Newman, you probably only need to be familiar with how these filter methods
|
6
|
+
# are used and can treat their implementation details as a black box.
|
7
|
+
#
|
8
|
+
# `Newman::Filters` is part of Newman's **external interface**.
|
9
|
+
|
10
|
+
module Newman
|
11
|
+
module Filters
|
12
|
+
|
13
|
+
# ---
|
14
|
+
|
15
|
+
# `Newman::Filters#to` takes a `filter_type`, a `pattern`,
|
16
|
+
# and an `action` and then registers a callback which gets run for
|
17
|
+
# each new request the application handles. If the filter matches the
|
18
|
+
# incoming message, the `action` block gets run in the context of
|
19
|
+
# a `Newman::Controller` object. Otherwise, the `action` block
|
20
|
+
# does not get run at all.
|
21
|
+
#
|
22
|
+
# Currently, the only supported `filter_type` is `:tag`, which leverages the
|
23
|
+
# `+` extension syntax for email addresses to filter out emails with certain
|
24
|
+
# tags in their TO field. For example, we could build a filter that responds
|
25
|
+
# to messages sent to `USERNAME+ping@HOST` using the following filter
|
26
|
+
# setup:
|
27
|
+
#
|
28
|
+
# to(:tag, "ping") do
|
29
|
+
# respond(:subject => "pong")
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Because this method runs the `pattern` through
|
33
|
+
# `Newman::Application#compile_regex`, it can also be used in
|
34
|
+
# combination with `Newman::Application#match` to do more
|
35
|
+
# complex matching. For example, the code below could be used to support
|
36
|
+
# complex TO field mappings, such as `USERNAME+somelist.subscribe@HOST`:
|
37
|
+
#
|
38
|
+
# match :list_id, "[^.]+"
|
39
|
+
#
|
40
|
+
# to(:tag, "{list_id}.subscribe") do
|
41
|
+
# list = load_list(params[:list_id])
|
42
|
+
#
|
43
|
+
# if list.subscriber?(sender)
|
44
|
+
# # send failure email, already subscribed
|
45
|
+
# else
|
46
|
+
# # add user to list and send success email
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# Note that currently everything before the `+` in the email address is
|
51
|
+
# ignored, and that the domain is hardcoded to match the
|
52
|
+
# `Controller#domain`, which currently directly references the
|
53
|
+
# `service.domain` setting. It'd be nice to make this a bit more
|
54
|
+
# flexible and also support other filter types such as a match against the
|
55
|
+
# whole email address at some point in the future.
|
56
|
+
|
57
|
+
def to(filter_type, pattern, &action)
|
58
|
+
raise NotImplementedError unless filter_type == :tag
|
59
|
+
|
60
|
+
regex = compile_regex(pattern)
|
61
|
+
|
62
|
+
callback action, ->(controller) {
|
63
|
+
controller.request.to.each do |e|
|
64
|
+
md = e.match(/\+#{regex}@#{Regexp.escape(controller.domain)}/)
|
65
|
+
return md if md
|
66
|
+
end
|
67
|
+
|
68
|
+
false
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
# ---
|
73
|
+
|
74
|
+
# `Newman::Filters#subject` takes a `filter_type`,
|
75
|
+
# a `pattern`, and an `action` and
|
76
|
+
# then registers a callback which gets run for each new request the
|
77
|
+
# application handles. If the filter matches the incoming message, the
|
78
|
+
# `action` block gets run in the context of a `Newman::Controller` object.
|
79
|
+
# Otherwise, the `action` block does not get run at all.
|
80
|
+
#
|
81
|
+
# Currently, the only supported `filter_type` is `:match`, which matches the
|
82
|
+
# pattern against the full SUBJECT field. This can be used for simple
|
83
|
+
# subject based filtering, such as the code shown below:
|
84
|
+
#
|
85
|
+
# subject(:match, "what stories do you know?") do
|
86
|
+
# respond :subject => "All of Jester's stories",
|
87
|
+
# :body => story_library.map { |e| e.title }.join("\n")
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# Because this method runs the `pattern` through
|
91
|
+
# `Newman::Application#compile_regex`, it can also be used in
|
92
|
+
# combination with `Newman::Application#match` to do more
|
93
|
+
# complex matching, such as in the following example:
|
94
|
+
#
|
95
|
+
# match :genre, '\S+'
|
96
|
+
# match :title, '.*'
|
97
|
+
#
|
98
|
+
# subject(:match, "a {genre} story '{title}'") do
|
99
|
+
# story_library.add_story(:genre => params[:genre],
|
100
|
+
# :title => params[:title],
|
101
|
+
# :body => request.body.to_s)
|
102
|
+
#
|
103
|
+
# respond :subject => "Jester saved '#{params[:title]}'"
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# It'd be nice to support more kinds of matching strategies at some point in
|
107
|
+
# the future, so be sure to let us know if you have ideas.
|
108
|
+
|
109
|
+
def subject(filter_type, pattern, &action)
|
110
|
+
raise NotImplementedError unless filter_type == :match
|
111
|
+
|
112
|
+
regex = compile_regex(pattern)
|
113
|
+
|
114
|
+
callback action, ->(controller) {
|
115
|
+
subject = controller.request.subject
|
116
|
+
|
117
|
+
return false unless subject
|
118
|
+
|
119
|
+
subject.match(/#{regex}/) || false
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/newman/mailer.rb
CHANGED
@@ -1,41 +1,107 @@
|
|
1
|
+
# `Newman::Mailer` allows you to easily receive mail via IMAP and send mail via
|
2
|
+
# SMTP, and is the default mailing strategy used by `Newman::Server.simple`.
|
3
|
+
# This class mostly exists to serve as an adapter that bridges the gap between
|
4
|
+
# the mail gem and Newman's configuration system.
|
5
|
+
#
|
6
|
+
# `Newman::Mailer`'s interface is minimal by design so that other objects
|
7
|
+
# can easily stand in for it as long as they respond to the same set of
|
8
|
+
# messages. Be sure to see `Newman::TestMailer` for an example of how to build a
|
9
|
+
# custom object that can be used in place of a `Newman::Mailer` object.
|
10
|
+
#
|
11
|
+
#
|
12
|
+
# `Newman::Mailer` is part of Newman's **internal interface**.
|
13
|
+
|
1
14
|
module Newman
|
2
|
-
Mailer
|
15
|
+
class Mailer
|
16
|
+
|
17
|
+
# ---
|
18
|
+
|
19
|
+
# To initialize a `Newman::Mailer` object, a settings object must be
|
20
|
+
# provided, i.e:
|
21
|
+
#
|
22
|
+
# settings = Newman::Settings.from_file('config/environment.rb')
|
23
|
+
# mailer = Newman::Mailer.new(settings)
|
24
|
+
#
|
25
|
+
# This is done automatically for you by `Newman::Server.simple`, but must be
|
26
|
+
# done manually if you are creating a `Newman::Server` instance from
|
27
|
+
# scratch.
|
28
|
+
#
|
29
|
+
# Currently, not all of the settings supported by the mail gem are mapped by
|
30
|
+
# Newman. This is by design, to limit the amount of configuration options
|
31
|
+
# need to think about. However, if this is causing you a problem,
|
32
|
+
# please [file an issue](https://github.com/mendicant-university/newman/issues).
|
3
33
|
|
4
|
-
|
5
|
-
def configure(settings)
|
34
|
+
def initialize(settings)
|
6
35
|
imap = settings.imap
|
7
36
|
smtp = settings.smtp
|
8
37
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
self.retriever_settings = {
|
39
|
+
:address => imap.address,
|
40
|
+
:user_name => imap.user,
|
41
|
+
:password => imap.password,
|
42
|
+
:enable_ssl => imap.ssl_enabled || false,
|
43
|
+
:port => imap.port
|
44
|
+
}
|
45
|
+
|
46
|
+
self.delivery_settings = {
|
47
|
+
:address => smtp.address,
|
48
|
+
:user_name => smtp.user,
|
49
|
+
:password => smtp.password,
|
50
|
+
:authentication => :plain,
|
51
|
+
:enable_starttls_auto => smtp.starttls_enabled || false,
|
52
|
+
:port => smtp.port
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
# ---
|
57
|
+
|
58
|
+
# `Newman::Mailer#messages` is used to retrieve all messages currently in the inbox
|
59
|
+
# and then delete them from the server. This method returns an array of
|
60
|
+
# `Mail::Message` objects if any messages were found, and returns
|
61
|
+
# an empty array otherwise.
|
62
|
+
|
63
|
+
def messages
|
64
|
+
Mail::IMAP.new(retriever_settings).all(:delete_after_find => true)
|
65
|
+
end
|
66
|
+
|
67
|
+
# ---
|
68
|
+
|
69
|
+
# `Newman::Mailer#new_message` is used to construct a new `Mail::Message` object,
|
70
|
+
# with the delivery settings that were set up at initialization time.
|
71
|
+
# This method passes all its arguments on to `Mail.new`, so be sure
|
72
|
+
# to refer to the [mail gem's documentation](http://github.com/mikel/mail)
|
73
|
+
# for details.
|
74
|
+
#
|
75
|
+
def new_message(*a, &b)
|
76
|
+
msg = Mail.new(*a, &b)
|
77
|
+
msg.delivery_method(:smtp, delivery_settings)
|
78
|
+
|
79
|
+
msg
|
37
80
|
end
|
38
81
|
|
39
|
-
|
82
|
+
# ---
|
83
|
+
|
84
|
+
# `Newman::Mailer#deliver_message` is used to construct and immediately deliver a
|
85
|
+
# message using the delivery settings that were set up at initialization
|
86
|
+
# time.
|
87
|
+
|
88
|
+
def deliver_message(*a, &b)
|
89
|
+
new_message(*a, &b).deliver
|
90
|
+
end
|
91
|
+
|
92
|
+
# ---
|
93
|
+
|
94
|
+
# **NOTE: Methods below this point in the file are implementation
|
95
|
+
# details, and should not be depended upon**
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# ---
|
100
|
+
|
101
|
+
# These accessors have been made private to reflect the fact that
|
102
|
+
# `Newman::Mailer` objects are meant to be treated as immutable constructs
|
103
|
+
# once they are created.
|
104
|
+
|
105
|
+
attr_accessor :retriever_settings, :delivery_settings
|
40
106
|
end
|
41
107
|
end
|