rumbster 1.0.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/COPYING +515 -0
- data/README +56 -0
- data/Rakefile +12 -0
- data/lib/message_observers.rb +40 -0
- data/lib/rumbster.rb +24 -0
- data/lib/smtp_protocol.rb +42 -0
- data/lib/smtp_states.rb +159 -0
- data/test/message_observers_test.rb +102 -0
- data/test/rumbster_test.rb +69 -0
- data/test/smtp_protocol_test.rb +64 -0
- data/test/smtp_states_test.rb +217 -0
- data/vendor/tmail.rb +4 -0
- data/vendor/tmail/.cvsignore +3 -0
- data/vendor/tmail/Makefile +19 -0
- data/vendor/tmail/address.rb +222 -0
- data/vendor/tmail/base64.rb +52 -0
- data/vendor/tmail/compat.rb +39 -0
- data/vendor/tmail/config.rb +50 -0
- data/vendor/tmail/encode.rb +447 -0
- data/vendor/tmail/header.rb +895 -0
- data/vendor/tmail/info.rb +14 -0
- data/vendor/tmail/loader.rb +1 -0
- data/vendor/tmail/mail.rb +869 -0
- data/vendor/tmail/mailbox.rb +386 -0
- data/vendor/tmail/mbox.rb +1 -0
- data/vendor/tmail/net.rb +260 -0
- data/vendor/tmail/obsolete.rb +122 -0
- data/vendor/tmail/parser.rb +1475 -0
- data/vendor/tmail/parser.y +372 -0
- data/vendor/tmail/port.rb +356 -0
- data/vendor/tmail/scanner.rb +22 -0
- data/vendor/tmail/scanner_r.rb +243 -0
- data/vendor/tmail/stringio.rb +256 -0
- data/vendor/tmail/textutils.rb +197 -0
- data/vendor/tmail/tmail.rb +1 -0
- data/vendor/tmail/utils.rb +23 -0
- metadata +88 -0
data/README
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
Rumbster README
|
2
|
+
===============
|
3
|
+
|
4
|
+
Rumbster is a fake smtp server for email testing in Ruby.
|
5
|
+
Rumbster was developed to as a way to acceptance test email
|
6
|
+
sending applications.
|
7
|
+
|
8
|
+
Requirements
|
9
|
+
------------
|
10
|
+
|
11
|
+
* Ruby 1.8.2 or later (may work with earlier versions)
|
12
|
+
|
13
|
+
License
|
14
|
+
-------
|
15
|
+
|
16
|
+
GNU LGPL, Lesser General Public License version 2.1
|
17
|
+
For details of LGPL, see file "COPYING".
|
18
|
+
|
19
|
+
Example Usage
|
20
|
+
-------------
|
21
|
+
|
22
|
+
A good source for usage information is the unit tests in the
|
23
|
+
test directory. Below is an example of the usage.
|
24
|
+
|
25
|
+
class TestEmails < Test::Unit::TestCase
|
26
|
+
|
27
|
+
def setup
|
28
|
+
@rumbster = Rumbster.new(port)
|
29
|
+
@message_observer = MailMessageObserver.new
|
30
|
+
|
31
|
+
@rumbster.add_observer @message_observer
|
32
|
+
@rumbster.add_observer FileMessageObserver.new('some/directory')
|
33
|
+
|
34
|
+
@rumbster.start
|
35
|
+
end
|
36
|
+
|
37
|
+
def teardown
|
38
|
+
@rumbster.stop
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_email_is_sent
|
42
|
+
send_email
|
43
|
+
assert_equal 1, @message_observer.messages.size
|
44
|
+
assert_equal 'junk@junk.com', @message_observer.messages.first.to
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Bug Report
|
49
|
+
----------
|
50
|
+
|
51
|
+
Any bug reports are welcome.
|
52
|
+
If you encounter a bug, please email me.
|
53
|
+
|
54
|
+
Adam Esterline
|
55
|
+
adam@esterlines.com
|
56
|
+
http://adamesterline.com
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'vendor'))
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tmail'
|
5
|
+
|
6
|
+
class FileMessageObserver
|
7
|
+
include FileUtils
|
8
|
+
|
9
|
+
def initialize(message_directory, system_time = SystemTime.new)
|
10
|
+
@message_directory = message_directory
|
11
|
+
@system_time = system_time
|
12
|
+
mkdir_p(message_directory)
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(message_string)
|
16
|
+
mail = TMail::Mail.parse(message_string)
|
17
|
+
|
18
|
+
file_name = File.join(@message_directory, "#{@system_time.current_time_in_seconds}_#{mail.to}.txt")
|
19
|
+
File.open(file_name, 'w') {|file| file << message_string }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class MailMessageObserver
|
24
|
+
attr_reader :messages
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@messages = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(message_string)
|
31
|
+
@messages << TMail::Mail.parse(message_string)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
class SystemTime
|
37
|
+
def current_time_in_seconds
|
38
|
+
Time.now.to_i
|
39
|
+
end
|
40
|
+
end
|
data/lib/rumbster.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'gserver'
|
2
|
+
require 'smtp_protocol'
|
3
|
+
|
4
|
+
class Rumbster < GServer
|
5
|
+
|
6
|
+
def initialize(port=25, *args)
|
7
|
+
super(port, *args)
|
8
|
+
|
9
|
+
@observers = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def serve(io)
|
13
|
+
protocol = SmtpProtocol.create
|
14
|
+
@observers.each do |observer|
|
15
|
+
protocol.add_observer(observer)
|
16
|
+
end
|
17
|
+
protocol.serve(io)
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_observer(observer)
|
21
|
+
@observers.push(observer)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'observer'
|
2
|
+
require 'smtp_states'
|
3
|
+
|
4
|
+
class SmtpProtocol
|
5
|
+
include Observable
|
6
|
+
|
7
|
+
def SmtpProtocol.create
|
8
|
+
initial_state = :init
|
9
|
+
|
10
|
+
states = {
|
11
|
+
:init => InitState.new,
|
12
|
+
:connect => ConnectState.new,
|
13
|
+
:connected => ConnectedState.new,
|
14
|
+
:read_mail => ReadMailState.new,
|
15
|
+
:quit => QuitState.new
|
16
|
+
}
|
17
|
+
|
18
|
+
SmtpProtocol.new(initial_state, states)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(initial_state, states)
|
22
|
+
states.each_value { |state| state.protocol = self }
|
23
|
+
|
24
|
+
@states = states
|
25
|
+
@state = @states[initial_state]
|
26
|
+
end
|
27
|
+
|
28
|
+
def state=(new_state)
|
29
|
+
@state = @states[new_state]
|
30
|
+
end
|
31
|
+
|
32
|
+
def serve(io)
|
33
|
+
@state.serve(io)
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_message_received(message)
|
37
|
+
changed
|
38
|
+
notify_observers(message)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
data/lib/smtp_states.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
class NotInitializedError < RuntimeError; end
|
2
|
+
|
3
|
+
module State
|
4
|
+
attr_accessor :protocol
|
5
|
+
|
6
|
+
def serve(io)
|
7
|
+
raise NotInitializedError.new if @protocol.nil?
|
8
|
+
|
9
|
+
service_request(io)
|
10
|
+
@protocol.state = @next_state
|
11
|
+
@protocol.serve(io)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Messages
|
16
|
+
|
17
|
+
def greeting(io)
|
18
|
+
io.puts '220 ruby ESMTP'
|
19
|
+
end
|
20
|
+
|
21
|
+
def helo_response(io)
|
22
|
+
io.puts '250 ruby'
|
23
|
+
end
|
24
|
+
|
25
|
+
def ok(io)
|
26
|
+
io.puts '250 ok'
|
27
|
+
end
|
28
|
+
|
29
|
+
def go_ahead(io)
|
30
|
+
io.puts '354 go ahead'
|
31
|
+
end
|
32
|
+
|
33
|
+
def goodbye(io)
|
34
|
+
io.puts '221 ruby goodbye'
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class InitState
|
40
|
+
|
41
|
+
include State, Messages
|
42
|
+
|
43
|
+
def initialize(protocol = nil, next_state = :connect)
|
44
|
+
@protocol = protocol
|
45
|
+
@next_state = next_state
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def service_request(io)
|
51
|
+
greeting(io)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
class ConnectState
|
57
|
+
|
58
|
+
include State, Messages
|
59
|
+
|
60
|
+
def initialize(protocol = nil, next_state = :connected)
|
61
|
+
@protocol = protocol
|
62
|
+
@next_state = next_state
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def service_request(io)
|
68
|
+
read_client_helo(io)
|
69
|
+
helo_response(io)
|
70
|
+
end
|
71
|
+
|
72
|
+
def read_client_helo(io)
|
73
|
+
io.readline
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
class ConnectedState
|
79
|
+
|
80
|
+
include State, Messages
|
81
|
+
|
82
|
+
def initialize(protocol = nil)
|
83
|
+
@protocol = protocol
|
84
|
+
@next_state = :connected
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def service_request(io)
|
90
|
+
request = io.readline
|
91
|
+
|
92
|
+
if request.strip.eql? "DATA"
|
93
|
+
@next_state = :read_mail
|
94
|
+
go_ahead(io)
|
95
|
+
else
|
96
|
+
ok(io)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
class ReadMailState
|
103
|
+
|
104
|
+
include State, Messages
|
105
|
+
|
106
|
+
def initialize(protocol = nil)
|
107
|
+
@protocol = protocol
|
108
|
+
@next_state = :quit
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def service_request(io)
|
114
|
+
message = read_message(io)
|
115
|
+
@protocol.new_message_received(message)
|
116
|
+
ok(io)
|
117
|
+
end
|
118
|
+
|
119
|
+
def not_end_of_message(line)
|
120
|
+
not line.strip.eql?('.')
|
121
|
+
end
|
122
|
+
|
123
|
+
def read_message(io)
|
124
|
+
message = ''
|
125
|
+
|
126
|
+
line = io.readline
|
127
|
+
while not_end_of_message(line)
|
128
|
+
message << line
|
129
|
+
line = io.readline
|
130
|
+
end
|
131
|
+
|
132
|
+
message
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
class QuitState
|
138
|
+
|
139
|
+
include Messages
|
140
|
+
attr_accessor :protocol
|
141
|
+
|
142
|
+
def initialize(protocol = nil)
|
143
|
+
@protocol = protocol
|
144
|
+
end
|
145
|
+
|
146
|
+
def serve(io)
|
147
|
+
raise NotInitializedError.new if @protocol.nil?
|
148
|
+
|
149
|
+
read_quit(io)
|
150
|
+
goodbye(io)
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def read_quit(io)
|
156
|
+
io.readline
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'message_observers'
|
6
|
+
|
7
|
+
class MessageObserversTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
include FileUtils
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@directory = File.join(File.dirname(__FILE__), '..', 'messages')
|
13
|
+
end
|
14
|
+
|
15
|
+
def teardown
|
16
|
+
rm_r(@directory) if File.exists?(@directory)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_directory_is_created_when_directory_is_not_present
|
20
|
+
file_observer = FileMessageObserver.new(@directory)
|
21
|
+
|
22
|
+
assert File.exists?(@directory)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_if_directory_is_already_present_observer_can_still_be_created
|
26
|
+
mkdir_p(@directory)
|
27
|
+
|
28
|
+
assert File.exists?(@directory)
|
29
|
+
assert_not_nil FileMessageObserver.new(@directory)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_message_received_is_saved_to_message_directory
|
33
|
+
file_observer = FileMessageObserver.new(@directory)
|
34
|
+
file_observer.update "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
|
35
|
+
|
36
|
+
assert_one_new_file_added_to @directory
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_message_is_saved_in_a_file_named_time_stamp_underscore_to_dot_txt
|
40
|
+
file_observer = FileMessageObserver.new(@directory, TestSystemTime.new(1))
|
41
|
+
file_observer.update "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
|
42
|
+
|
43
|
+
expected_file_name = File.join(@directory, "1_junk@junk.com.txt")
|
44
|
+
|
45
|
+
assert File.exists?(expected_file_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_message_contents_are_saved_to_the_file
|
49
|
+
message = "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
|
50
|
+
|
51
|
+
file_observer = FileMessageObserver.new(@directory, TestSystemTime.new(1))
|
52
|
+
file_observer.update message
|
53
|
+
|
54
|
+
expected_file_name = File.join(@directory, "1_junk@junk.com.txt")
|
55
|
+
|
56
|
+
assert_equal read_file_contents_into_a_string(expected_file_name), message
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_messages_are_saved_for_later_inspection
|
60
|
+
message = "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
|
61
|
+
|
62
|
+
mail_observer = MailMessageObserver.new
|
63
|
+
mail_observer.update message
|
64
|
+
mail_observer.update message
|
65
|
+
|
66
|
+
assert_equal 2, mail_observer.messages.size
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_message_is_converted_to_tmail
|
70
|
+
message = "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
|
71
|
+
|
72
|
+
mail_observer = MailMessageObserver.new
|
73
|
+
mail_observer.update message
|
74
|
+
|
75
|
+
assert_equal 'What\'s up', mail_observer.messages.first.subject
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def read_file_contents_into_a_string(file_name)
|
81
|
+
File.open(file_name) {|file| file.readlines.join }
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def assert_one_new_file_added_to(directory)
|
86
|
+
number_of_dot_and_dot_dot_entries_in_a_directory_when_it_is_first_created = 2
|
87
|
+
number_of_entries_with_one_file = number_of_dot_and_dot_dot_entries_in_a_directory_when_it_is_first_created + 1
|
88
|
+
|
89
|
+
assert_equal number_of_entries_with_one_file, Dir.entries(@directory).size
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class TestSystemTime
|
94
|
+
def initialize(time)
|
95
|
+
@time = time
|
96
|
+
end
|
97
|
+
|
98
|
+
def current_time_in_seconds
|
99
|
+
@time
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'rumbster'
|
5
|
+
require 'net/smtp'
|
6
|
+
require 'gserver'
|
7
|
+
|
8
|
+
|
9
|
+
class RumbsterTest < Test::Unit::TestCase
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@observer = RumbsterObserver.new
|
13
|
+
@server = Rumbster.new(10025)
|
14
|
+
@server.add_observer(@observer)
|
15
|
+
|
16
|
+
@server.start
|
17
|
+
end
|
18
|
+
|
19
|
+
def teardown
|
20
|
+
@server.stop
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_single_receiver_message_sent_by_client_is_received_by_listener
|
24
|
+
message = "From: <junk@junkster.com>\r\nTo: junk@junk.com\r\nSubject: hi\r\n\r\nThis is a test\r\n"
|
25
|
+
to = 'his_address@example.com'
|
26
|
+
send_message to, message
|
27
|
+
|
28
|
+
assert_equal message, @observer.message
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_multiple_receiver_message_sent_by_client_is_received_by_listener
|
32
|
+
message = "From: <junk@junkster.com>\r\nTo: junk@junk.com\r\nSubject: hi\r\n\r\nThis is a test\r\n"
|
33
|
+
to = ['his_address@example.com', 'her_address@example.com']
|
34
|
+
send_message to, message
|
35
|
+
|
36
|
+
assert_equal message, @observer.message
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_multiple_receiver_messages_sent_by_client_are_received_by_listener
|
40
|
+
message_1 = "From: <junk_1@junkster.com>\r\nTo: junk_1@junk.com\r\nSubject: hi\r\n\r\nThis is a test_1\r\n"
|
41
|
+
to_1 = ['his_address_1@example.com', 'her_address_1@example.com']
|
42
|
+
send_message to_1, message_1
|
43
|
+
|
44
|
+
assert_equal message_1, @observer.message
|
45
|
+
|
46
|
+
message_2 = "From: <junk_2@junkster.com>\r\nTo: junk_2@junk.com\r\nSubject: hi\r\n\r\nThis is a test_2\r\n"
|
47
|
+
to_2 = ['his_address_2@example.com', 'her_address_2@example.com']
|
48
|
+
send_message to_2, message_2
|
49
|
+
|
50
|
+
assert_equal message_2, @observer.message
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def send_message(to, message)
|
56
|
+
Net::SMTP.start('localhost', 10025) do |smtp|
|
57
|
+
smtp.send_message message, 'your@mail.address', to
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class RumbsterObserver
|
64
|
+
attr_accessor :message
|
65
|
+
|
66
|
+
def update(message)
|
67
|
+
@message = message
|
68
|
+
end
|
69
|
+
end
|