newman 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +2 -1
- data/examples/acid_tests.rb +59 -0
- data/examples/config/dreamhost.rb.example +17 -0
- data/examples/config/gmail.rb.example +21 -0
- data/examples/config/test.rb +7 -0
- data/examples/example_helper.rb +1 -0
- data/examples/jester_clone.rb +43 -0
- data/examples/libre_list_clone.rb +71 -0
- data/examples/live_test.rb +47 -0
- data/examples/ping_pong.rb +19 -0
- data/examples/simple_mailing_list.rb +53 -0
- data/examples/views/non-subscriber-error.erb +1 -0
- data/examples/views/subscribe-error.erb +1 -0
- data/examples/views/subscribe-success.erb +1 -0
- data/examples/views/unsubscribe-error.erb +2 -0
- data/examples/views/unsubscribe-success.erb +2 -0
- data/lib/newman.rb +17 -0
- data/lib/newman/application.rb +23 -0
- data/lib/newman/commands.rb +72 -0
- data/lib/newman/controller.rb +49 -0
- data/lib/newman/mailer.rb +41 -0
- data/lib/newman/mailing_list.rb +29 -0
- data/lib/newman/server.rb +45 -0
- data/lib/newman/settings.rb +21 -0
- data/lib/newman/store.rb +34 -0
- data/lib/newman/store/recorder.rb +63 -0
- data/lib/newman/test_mailer.rb +27 -0
- data/lib/newman/version.rb +8 -0
- metadata +51 -2
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
## Newman:
|
1
|
+
## Newman: Because the mail never stops!
|
2
2
|
|
3
3
|
This is going to be an experiment in building a simple framework for developing
|
4
4
|
email-based applications. It is based on ideas from an exercise in
|
5
5
|
Mendicant University's January 2012 core skills course, and also serves as a
|
6
6
|
demo application for an article in the Practicing Ruby Journal.
|
7
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative "ping_pong"
|
2
|
+
require_relative "simple_mailing_list"
|
3
|
+
|
4
|
+
require "minitest/autorun"
|
5
|
+
|
6
|
+
server = Newman::Server
|
7
|
+
server.test_mode("config/test.rb")
|
8
|
+
|
9
|
+
mailer = server.mailer
|
10
|
+
|
11
|
+
describe "Ping Pong" do
|
12
|
+
it "responds to an email sent to test+ping@test.com" do
|
13
|
+
mailer.deliver_message(:to => "test+ping@test.com")
|
14
|
+
server.tick(Newman::Examples::PingPong)
|
15
|
+
mailer.messages.first.subject.must_equal("pong")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "responds to an email sent to test+bad@test.com" do
|
19
|
+
mailer.deliver_message(:to => "test+bad@test.com")
|
20
|
+
server.tick(Newman::Examples::PingPong)
|
21
|
+
mailer.messages.first.subject.must_equal("unknown command")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "SimpleList" do
|
26
|
+
it "emulates a simple mailing list" do
|
27
|
+
mailer.deliver_message(:from => "tester@test.com",
|
28
|
+
:to => "test@test.com")
|
29
|
+
|
30
|
+
server.tick(Newman::Examples::SimpleList)
|
31
|
+
mailer.messages.first.subject.must_equal("You are not subscribed")
|
32
|
+
|
33
|
+
mailer.deliver_message(:from => "tester@test.com",
|
34
|
+
:to => "test+subscribe@test.com")
|
35
|
+
|
36
|
+
server.tick(Newman::Examples::SimpleList)
|
37
|
+
mailer.messages.first.subject.must_equal("SUBSCRIBED!")
|
38
|
+
|
39
|
+
|
40
|
+
mailer.deliver_message(:from => "tester@test.com",
|
41
|
+
:to => "test@test.com",
|
42
|
+
:subject => "WIN!")
|
43
|
+
|
44
|
+
server.tick(Newman::Examples::SimpleList)
|
45
|
+
mailer.messages.first.subject.must_equal("WIN!")
|
46
|
+
|
47
|
+
mailer.deliver_message(:from => "tester@test.com",
|
48
|
+
:to => "test+unsubscribe@test.com")
|
49
|
+
|
50
|
+
server.tick(Newman::Examples::SimpleList)
|
51
|
+
mailer.messages.first.subject.must_equal("UNSUBSCRIBED!")
|
52
|
+
end
|
53
|
+
|
54
|
+
after do
|
55
|
+
if File.exist?(server.settings.application.simplelist_db)
|
56
|
+
File.unlink(server.settings.application.simplelist_db)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
imap.address = "mail.<<DOMAIN>>"
|
2
|
+
imap.user = "<<USERNAME>>@<<DOMAIN>>"
|
3
|
+
imap.password = "<<PASSWORD>>"
|
4
|
+
|
5
|
+
smtp.address = "mail.<<DOMAIN>>"
|
6
|
+
smtp.user = "<<USER>>@<<DOMAIN>>"
|
7
|
+
smtp.password = "<<PASSWORD>>"
|
8
|
+
|
9
|
+
service.domain = "<<DOMAIN>>"
|
10
|
+
service.templates_dir = "views"
|
11
|
+
service.default_sender = "<<USERNAME>>@<<DOMAIN>>"
|
12
|
+
service.polling_interval = 10
|
13
|
+
|
14
|
+
application.simplelist_db = "db/lists/simple_mailing_list.store"
|
15
|
+
application.librelist_db = "db/lists/libre.store"
|
16
|
+
application.ping_email = "<<USERNAME>>+ping@<<DOMAIN>>"
|
17
|
+
application.live_test_delay = 5
|
@@ -0,0 +1,21 @@
|
|
1
|
+
imap.address = "imap.gmail.com"
|
2
|
+
imap.user = "<<USERNAME>>@gmail.com"
|
3
|
+
imap.password = "<<PASSWORD>>"
|
4
|
+
imap.ssl_enabled = true
|
5
|
+
imap.port = 993
|
6
|
+
|
7
|
+
smtp.address = "smtp.gmail.com"
|
8
|
+
smtp.user = "<<USERNAME>>@gmail.com"
|
9
|
+
smtp.password = "<<PASSWORD>>"
|
10
|
+
smtp.starttls_enabled = true
|
11
|
+
smtp.port = 587
|
12
|
+
|
13
|
+
service.domain = "gmail.com"
|
14
|
+
service.templates_dir = "views"
|
15
|
+
service.default_sender = "<<USERNAME>>@gmail.com"
|
16
|
+
service.polling_interval = 10
|
17
|
+
|
18
|
+
application.simplelist_db = "db/lists/simple_mailing_list.store"
|
19
|
+
application.librelist_db = "db/lists/libre.store"
|
20
|
+
application.ping_email = "<<USERNAME>>+ping@gmail.com"
|
21
|
+
application.live_test_delay = 5
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "../lib/newman"
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative "example_helper"
|
2
|
+
|
3
|
+
module Newman
|
4
|
+
module Examples
|
5
|
+
Jester = Newman::Application.new do
|
6
|
+
match :genre, '\S+'
|
7
|
+
match :title, '.*'
|
8
|
+
match :email, '[\w\d\._-]+@[\w\d\._-]+' #not legit, I'm sure
|
9
|
+
|
10
|
+
subject(:match, "a {genre} story '{title}'") do
|
11
|
+
respond :subject => "No one would consider '#{params[:title]}' to be #{params[:genre]}"
|
12
|
+
end
|
13
|
+
|
14
|
+
subject(:match, "what stories do you know?") do
|
15
|
+
respond :subject => "What stories do YOU know?"
|
16
|
+
end
|
17
|
+
|
18
|
+
subject(:match, "tell something {genre} to {email}") do
|
19
|
+
respond :subject => "Why don't you tell something #{params[:genre]} to #{params[:email]}?"
|
20
|
+
end
|
21
|
+
|
22
|
+
subject(:match, "tell me something {genre}") do
|
23
|
+
respond :subject => "Your face is #{params[:genre]}"
|
24
|
+
end
|
25
|
+
|
26
|
+
subject(:match, "tell me '{title}'") do
|
27
|
+
respond :subject => "Once upon a time there was a #{params[:title]}. THE END."
|
28
|
+
end
|
29
|
+
|
30
|
+
subject(:match, "help") do
|
31
|
+
respond :subject => "Please deposit $10, and then try again"
|
32
|
+
end
|
33
|
+
|
34
|
+
default do
|
35
|
+
respond :subject => "FAIL"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if __FILE__ == $PROGRAM_NAME
|
42
|
+
Newman::Server.simple(Newman::Examples::Jester, "config/environment.rb")
|
43
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# This is a cheap and possibly buggy clone of librelist.org's automatic
|
2
|
+
# mailing list creation
|
3
|
+
|
4
|
+
require_relative "example_helper"
|
5
|
+
|
6
|
+
module Newman
|
7
|
+
module Examples
|
8
|
+
LibreList = Newman::Application.new do
|
9
|
+
helpers do
|
10
|
+
def load_list(name)
|
11
|
+
store = Newman::Store.new(settings.application.librelist_db)
|
12
|
+
Newman::MailingList.new(name, store)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
match :list_id, "[^.]+"
|
17
|
+
|
18
|
+
to(:tag, "{list_id}.subscribe") do
|
19
|
+
list = load_list(params[:list_id])
|
20
|
+
|
21
|
+
if list.subscriber?(sender)
|
22
|
+
respond :subject => "ERROR: Already subscribed",
|
23
|
+
:body => template("subscribe-error"),
|
24
|
+
:reply_to => "test+#{params[:list_id]}@#{domain}"
|
25
|
+
else
|
26
|
+
list.subscribe(sender)
|
27
|
+
|
28
|
+
respond :subject => "SUBSCRIBED!",
|
29
|
+
:body => template("subscribe-success"),
|
30
|
+
:reply_to => "test+#{params[:list_id]}@#{domain}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
to(:tag, "{list_id}.unsubscribe") do
|
35
|
+
list = load_list(params[:list_id])
|
36
|
+
|
37
|
+
if list.subscriber?(sender)
|
38
|
+
list.unsubscribe(sender)
|
39
|
+
|
40
|
+
respond :subject => "UNSUBSCRIBED!",
|
41
|
+
:body => template("unsubscribe-success")
|
42
|
+
else
|
43
|
+
respond :subject => "ERROR: Not on subscriber list",
|
44
|
+
:body => template("unsubscribe-error"),
|
45
|
+
:reply_to => "test+#{params[:list_id]}@#{domain}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
to(:tag, "{list_id}") do
|
50
|
+
list = load_list(params[:list_id])
|
51
|
+
|
52
|
+
if list.subscriber?(sender)
|
53
|
+
forward_message :bcc => list.subscribers.join(", "),
|
54
|
+
:reply_to => "test+#{params[:list_id]}@#{domain}"
|
55
|
+
else
|
56
|
+
respond :subject => "You are not subscribed",
|
57
|
+
:body => template("non-subscriber-error"),
|
58
|
+
:reply_to => "test+#{params[:list_id]}@#{domain}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
default do
|
63
|
+
respond :subject => "FAIL"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
if __FILE__ == $PROGRAM_NAME
|
70
|
+
Newman::Server.simple(Newman::Examples::LibreList, "config/environment.rb")
|
71
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative "ping_pong"
|
2
|
+
|
3
|
+
|
4
|
+
settings = Newman::Settings.from_file("config/environment.rb")
|
5
|
+
|
6
|
+
mailer = Newman::Mailer
|
7
|
+
mailer.configure(settings)
|
8
|
+
|
9
|
+
server = Newman::Server
|
10
|
+
server.settings = settings
|
11
|
+
server.mailer = mailer
|
12
|
+
|
13
|
+
mailer.deliver_message(:to => settings.application.ping_email,
|
14
|
+
:from => settings.service.default_sender)
|
15
|
+
|
16
|
+
puts "Checking in..."
|
17
|
+
settings.application.live_test_delay.downto(1) do |i|
|
18
|
+
print "#{i}. "
|
19
|
+
sleep(1)
|
20
|
+
end
|
21
|
+
puts
|
22
|
+
|
23
|
+
Newman::Server.tick(Newman::Examples::PingPong)
|
24
|
+
|
25
|
+
remaining_attempts = 3
|
26
|
+
loop do
|
27
|
+
if remaining_attempts == 0
|
28
|
+
abort "FAIL: Did not receive mail yet"
|
29
|
+
else
|
30
|
+
incoming = mailer.messages
|
31
|
+
case
|
32
|
+
when incoming.empty?
|
33
|
+
sleep settings.service.polling_interval
|
34
|
+
when incoming.length > 1
|
35
|
+
abort "FAIL: More than one message in inbox"
|
36
|
+
else
|
37
|
+
if incoming.first.subject == "pong"
|
38
|
+
puts "OK"
|
39
|
+
exit
|
40
|
+
else
|
41
|
+
abort "FAIL: Expected 'pong', got #{incoming.first.subject}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
remaining_attempts -= 1
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "example_helper"
|
2
|
+
|
3
|
+
module Newman
|
4
|
+
module Examples
|
5
|
+
PingPong = Newman::Application.new do
|
6
|
+
to(:tag, "ping") do
|
7
|
+
respond(:subject => "pong")
|
8
|
+
end
|
9
|
+
|
10
|
+
default do
|
11
|
+
respond(:subject => "unknown command")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
if __FILE__ == $PROGRAM_NAME
|
18
|
+
Newman::Server.simple(Newman::Examples::PingPong, "config/environment.rb")
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative "example_helper"
|
2
|
+
|
3
|
+
module Newman
|
4
|
+
module Examples
|
5
|
+
|
6
|
+
SimpleList = Newman::Application.new do
|
7
|
+
helpers do
|
8
|
+
def list
|
9
|
+
store = Newman::Store.new(settings.application.simplelist_db)
|
10
|
+
|
11
|
+
Newman::MailingList.new("simple_list", store)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
to(:tag, "subscribe") do
|
16
|
+
if list.subscriber?(sender)
|
17
|
+
respond :subject => "ERROR: Already subscribed",
|
18
|
+
:body => template("subscribe-error")
|
19
|
+
else
|
20
|
+
list.subscribe(sender)
|
21
|
+
|
22
|
+
respond :subject => "SUBSCRIBED!",
|
23
|
+
:body => template("subscribe-success")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
to(:tag, "unsubscribe") do
|
28
|
+
if list.subscriber?(sender)
|
29
|
+
list.unsubscribe(sender)
|
30
|
+
|
31
|
+
respond :subject => "UNSUBSCRIBED!",
|
32
|
+
:body => template("unsubscribe-success")
|
33
|
+
else
|
34
|
+
respond :subject => "ERROR: Not on subscriber list",
|
35
|
+
:body => template("unsubscribe-error")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
default do
|
40
|
+
if list.subscriber?(sender)
|
41
|
+
forward_message :bcc => list.subscribers.join(", ")
|
42
|
+
else
|
43
|
+
respond :subject => "You are not subscribed",
|
44
|
+
:body => template("non-subscriber-error")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if __FILE__ == $PROGRAM_NAME
|
52
|
+
Newman::Server.simple(Newman::Examples::SimpleList, "config/environment.rb")
|
53
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Sorry, we don't have <%= sender %> on our list of subscribers. Your message was dropped.
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= sender %> is already a subscriber of this mailing list.
|
@@ -0,0 +1 @@
|
|
1
|
+
Welcome to the club, pal! We've added <%= sender %> to our list of people to spam.
|
data/lib/newman.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "pstore"
|
2
|
+
require "ostruct"
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
require "mail"
|
6
|
+
require "tilt"
|
7
|
+
|
8
|
+
require_relative "newman/server"
|
9
|
+
require_relative "newman/commands"
|
10
|
+
require_relative "newman/application"
|
11
|
+
require_relative "newman/controller"
|
12
|
+
require_relative "newman/mailing_list"
|
13
|
+
require_relative "newman/settings"
|
14
|
+
require_relative "newman/store"
|
15
|
+
require_relative "newman/mailer"
|
16
|
+
require_relative "newman/test_mailer"
|
17
|
+
require_relative "newman/version"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Newman
|
2
|
+
class Application
|
3
|
+
include Commands
|
4
|
+
|
5
|
+
def initialize(&block)
|
6
|
+
self.callbacks = []
|
7
|
+
self.matchers = {}
|
8
|
+
self.extensions = []
|
9
|
+
|
10
|
+
instance_eval(&block) if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(params)
|
14
|
+
controller = Controller.new(params)
|
15
|
+
extensions.each { |mod| controller.extend(mod) }
|
16
|
+
trigger_callbacks(controller)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_accessor :callbacks, :default_callback, :matchers, :extensions
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Newman
|
2
|
+
module Commands
|
3
|
+
def use(extension)
|
4
|
+
extensions << extension
|
5
|
+
end
|
6
|
+
|
7
|
+
def helpers(&block)
|
8
|
+
extensions << Module.new(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def match(id, pattern)
|
12
|
+
matchers[id.to_s] = pattern
|
13
|
+
end
|
14
|
+
|
15
|
+
def to(pattern_type, pattern, &callback)
|
16
|
+
raise NotImplementedError unless pattern_type == :tag
|
17
|
+
|
18
|
+
pattern = compile_pattern(pattern)
|
19
|
+
|
20
|
+
matcher = lambda do
|
21
|
+
request.to.each do |e|
|
22
|
+
md = e.match(/\+#{pattern}@#{Regexp.escape(domain)}/)
|
23
|
+
return md if md
|
24
|
+
end
|
25
|
+
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
callbacks << { :matcher => matcher, :callback => callback }
|
30
|
+
end
|
31
|
+
|
32
|
+
def subject(pattern_type, pattern, &callback)
|
33
|
+
raise NotImplementedError unless pattern_type == :match
|
34
|
+
|
35
|
+
pattern = compile_pattern(pattern)
|
36
|
+
|
37
|
+
matcher = lambda do
|
38
|
+
md = request.subject.match(/#{pattern}/)
|
39
|
+
|
40
|
+
md || false
|
41
|
+
end
|
42
|
+
|
43
|
+
callbacks << { :matcher => matcher, :callback => callback }
|
44
|
+
end
|
45
|
+
|
46
|
+
def default(&callback)
|
47
|
+
self.default_callback = callback
|
48
|
+
end
|
49
|
+
|
50
|
+
def trigger_callbacks(controller)
|
51
|
+
matched_callbacks = callbacks.select do |e|
|
52
|
+
matcher = e[:matcher]
|
53
|
+
e[:match_data] = controller.instance_exec(&matcher)
|
54
|
+
end
|
55
|
+
|
56
|
+
if matched_callbacks.empty?
|
57
|
+
controller.instance_exec(&default_callback)
|
58
|
+
else
|
59
|
+
matched_callbacks.each do |e|
|
60
|
+
callback = e[:callback]
|
61
|
+
controller.params = e[:match_data] || {}
|
62
|
+
controller.instance_exec(&callback)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def compile_pattern(pattern)
|
68
|
+
pattern.gsub('.','\.')
|
69
|
+
.gsub(/\{(.*?)\}/) { |m| "(?<#{$1}>#{matchers[$1]})" }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Newman
|
2
|
+
class Controller
|
3
|
+
def initialize(params)
|
4
|
+
self.settings = params.fetch(:settings)
|
5
|
+
self.request = params.fetch(:request)
|
6
|
+
self.response = params.fetch(:response)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :params
|
10
|
+
|
11
|
+
def respond(params)
|
12
|
+
params.each { |k,v| response.send("#{k}=", v) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def template(name)
|
16
|
+
Tilt.new(Dir.glob("#{settings.service.templates_dir}/#{name}.*").first)
|
17
|
+
.render(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def sender
|
21
|
+
request.from.first.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def domain
|
25
|
+
settings.service.domain
|
26
|
+
end
|
27
|
+
|
28
|
+
def forward_message(params={})
|
29
|
+
response.from = request.from
|
30
|
+
response.reply_to = settings.service.default_sender
|
31
|
+
response.subject = request.subject
|
32
|
+
|
33
|
+
params.each do |k,v|
|
34
|
+
response.send("#{k}=", v)
|
35
|
+
end
|
36
|
+
|
37
|
+
if request.multipart?
|
38
|
+
response.text_part = request.text_part
|
39
|
+
response.html_part = request.html_part
|
40
|
+
else
|
41
|
+
response.body = request.body.to_s
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_accessor :settings, :request, :response
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Newman
|
2
|
+
Mailer = Object.new
|
3
|
+
|
4
|
+
class << Mailer
|
5
|
+
def configure(settings)
|
6
|
+
imap = settings.imap
|
7
|
+
smtp = settings.smtp
|
8
|
+
|
9
|
+
Mail.defaults do
|
10
|
+
retriever_method :imap,
|
11
|
+
:address => imap.address,
|
12
|
+
:user_name => imap.user,
|
13
|
+
:password => imap.password,
|
14
|
+
:enable_ssl => imap.ssl_enabled || false,
|
15
|
+
:port => imap.port
|
16
|
+
|
17
|
+
delivery_method :smtp,
|
18
|
+
:address => smtp.address,
|
19
|
+
:user_name => smtp.user,
|
20
|
+
:password => smtp.password,
|
21
|
+
:authentication => :plain,
|
22
|
+
:enable_starttls_auto => smtp.starttls_enabled || false,
|
23
|
+
:port => smtp.port
|
24
|
+
end
|
25
|
+
|
26
|
+
def messages
|
27
|
+
Mail.all(:delete_after_find => true)
|
28
|
+
end
|
29
|
+
|
30
|
+
def new_message(*a, &b)
|
31
|
+
Mail.new(*a, &b)
|
32
|
+
end
|
33
|
+
|
34
|
+
def deliver_message(*a, &b)
|
35
|
+
new_message(*a, &b).deliver
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_accessor :settings
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Newman
|
2
|
+
class MailingList
|
3
|
+
def initialize(name, store)
|
4
|
+
self.name = name
|
5
|
+
self.store = store
|
6
|
+
end
|
7
|
+
|
8
|
+
def subscribe(email)
|
9
|
+
store[name].create(email)
|
10
|
+
end
|
11
|
+
|
12
|
+
def unsubscribe(email)
|
13
|
+
record = store[name].find { |e| e.contents == email }
|
14
|
+
store[name].destroy(record.id)
|
15
|
+
end
|
16
|
+
|
17
|
+
def subscriber?(email)
|
18
|
+
store[name].any? { |r| r.contents == email }
|
19
|
+
end
|
20
|
+
|
21
|
+
def subscribers
|
22
|
+
store[name].map { |r| r.contents }
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_accessor :name, :store
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Newman
|
2
|
+
Server = Object.new
|
3
|
+
|
4
|
+
class << Server
|
5
|
+
attr_accessor :settings, :mailer
|
6
|
+
|
7
|
+
def test_mode(settings_file)
|
8
|
+
self.settings = Newman::Settings.from_file(settings_file)
|
9
|
+
self.mailer = Newman::TestMailer
|
10
|
+
|
11
|
+
mailer.configure(settings)
|
12
|
+
end
|
13
|
+
|
14
|
+
def simple(apps, settings_file)
|
15
|
+
self.settings = Newman::Settings.from_file(settings_file)
|
16
|
+
self.mailer = Newman::Mailer
|
17
|
+
|
18
|
+
mailer.configure(settings)
|
19
|
+
|
20
|
+
run(apps)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run(apps)
|
24
|
+
loop do
|
25
|
+
tick(apps)
|
26
|
+
sleep settings.service.polling_interval
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def tick(apps)
|
31
|
+
mailer.messages.each do |request|
|
32
|
+
response = mailer.new_message(:to => request.from,
|
33
|
+
:from => settings.service.default_sender)
|
34
|
+
|
35
|
+
Array(apps).each do |a|
|
36
|
+
a.call(:request => request,
|
37
|
+
:response => response,
|
38
|
+
:settings => settings)
|
39
|
+
end
|
40
|
+
|
41
|
+
response.deliver
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Newman
|
2
|
+
class Settings
|
3
|
+
def self.from_file(filename)
|
4
|
+
new.tap { |o| o.load_config(filename) }
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.imap = OpenStruct.new
|
9
|
+
self.smtp = OpenStruct.new
|
10
|
+
self.service = OpenStruct.new
|
11
|
+
self.application = OpenStruct.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_config(filename)
|
15
|
+
eval(File.read(filename), binding)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
attr_accessor :imap, :smtp, :service, :application
|
20
|
+
end
|
21
|
+
end
|
data/lib/newman/store.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative "store/recorder"
|
2
|
+
|
3
|
+
module Newman
|
4
|
+
class Store
|
5
|
+
def initialize(filename)
|
6
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
7
|
+
|
8
|
+
self.data = PStore.new(filename)
|
9
|
+
|
10
|
+
write do
|
11
|
+
data[:identifiers] ||= {}
|
12
|
+
data[:columns] ||= {}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :identifiers
|
17
|
+
|
18
|
+
def [](column)
|
19
|
+
Recorder.new(column, self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def read
|
23
|
+
data.transaction(:read_only) { yield(data) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def write
|
27
|
+
data.transaction { yield(data) }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_accessor :data
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Newman
|
2
|
+
class Store
|
3
|
+
Record = Struct.new(:column, :id, :contents)
|
4
|
+
|
5
|
+
class Recorder
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(column, store)
|
9
|
+
self.column = column
|
10
|
+
self.store = store
|
11
|
+
|
12
|
+
store.write do |data|
|
13
|
+
data[:identifiers][column] ||= 0
|
14
|
+
data[:columns][column] ||= {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
store.read do |data|
|
20
|
+
data[:columns][column].each do |id, contents|
|
21
|
+
yield(Record.new(column, id,contents))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def create(contents)
|
27
|
+
store.write do |data|
|
28
|
+
id = (data[:identifiers][column] += 1)
|
29
|
+
|
30
|
+
data[:columns][column][id] = contents
|
31
|
+
|
32
|
+
Record.new(column, id, contents)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def read(id)
|
37
|
+
store.read do |data|
|
38
|
+
Record.new(column, id, data[:columns][column][id])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def update(id)
|
43
|
+
store.write do |data|
|
44
|
+
data[:columns][column][id] = yield(data[:columns][column][id])
|
45
|
+
|
46
|
+
Record.new(column, id, data[:columns][column][id])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy(id)
|
51
|
+
store.write do |data|
|
52
|
+
data[:columns][column].delete(id)
|
53
|
+
end
|
54
|
+
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
attr_accessor :column, :store
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Newman
|
2
|
+
TestMailer = Object.new
|
3
|
+
|
4
|
+
class << TestMailer
|
5
|
+
def configure(settings)
|
6
|
+
Mail.defaults do
|
7
|
+
retriever_method :test
|
8
|
+
delivery_method :test
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def messages
|
13
|
+
msgs = Marshal.load(Marshal.dump(Mail::TestMailer.deliveries))
|
14
|
+
Mail::TestMailer.deliveries.clear
|
15
|
+
|
16
|
+
msgs
|
17
|
+
end
|
18
|
+
|
19
|
+
def new_message(*a, &b)
|
20
|
+
Mail.new(*a, &b)
|
21
|
+
end
|
22
|
+
|
23
|
+
def deliver_message(*a, &b)
|
24
|
+
new_message(*a, &b).deliver
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: newman
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,29 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
date: 2012-02-02 00:00:00.000000000 Z
|
13
|
-
dependencies:
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mail
|
16
|
+
requirement: &2171081700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.3.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2171081700
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: tilt
|
27
|
+
requirement: &2171081200 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.3.3
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2171081200
|
14
36
|
description: A microframework for mail-centric applications
|
15
37
|
email:
|
16
38
|
- gregory.t.brown@gmail.com
|
@@ -18,6 +40,33 @@ executables: []
|
|
18
40
|
extensions: []
|
19
41
|
extra_rdoc_files: []
|
20
42
|
files:
|
43
|
+
- lib/newman/application.rb
|
44
|
+
- lib/newman/commands.rb
|
45
|
+
- lib/newman/controller.rb
|
46
|
+
- lib/newman/mailer.rb
|
47
|
+
- lib/newman/mailing_list.rb
|
48
|
+
- lib/newman/server.rb
|
49
|
+
- lib/newman/settings.rb
|
50
|
+
- lib/newman/store/recorder.rb
|
51
|
+
- lib/newman/store.rb
|
52
|
+
- lib/newman/test_mailer.rb
|
53
|
+
- lib/newman/version.rb
|
54
|
+
- lib/newman.rb
|
55
|
+
- examples/acid_tests.rb
|
56
|
+
- examples/config/dreamhost.rb.example
|
57
|
+
- examples/config/gmail.rb.example
|
58
|
+
- examples/config/test.rb
|
59
|
+
- examples/example_helper.rb
|
60
|
+
- examples/jester_clone.rb
|
61
|
+
- examples/libre_list_clone.rb
|
62
|
+
- examples/live_test.rb
|
63
|
+
- examples/ping_pong.rb
|
64
|
+
- examples/simple_mailing_list.rb
|
65
|
+
- examples/views/non-subscriber-error.erb
|
66
|
+
- examples/views/subscribe-error.erb
|
67
|
+
- examples/views/subscribe-success.erb
|
68
|
+
- examples/views/unsubscribe-error.erb
|
69
|
+
- examples/views/unsubscribe-success.erb
|
21
70
|
- README.md
|
22
71
|
homepage: http://github.com/mendicant-university/newman
|
23
72
|
licenses: []
|