newman 0.0.1 → 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/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: []
|