mlist 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +59 -0
- data/README +204 -0
- data/Rakefile +27 -0
- data/TODO +36 -0
- data/VERSION.yml +4 -0
- data/lib/mlist/email.rb +69 -0
- data/lib/mlist/email_post.rb +126 -0
- data/lib/mlist/email_server/base.rb +33 -0
- data/lib/mlist/email_server/default.rb +31 -0
- data/lib/mlist/email_server/fake.rb +16 -0
- data/lib/mlist/email_server/pop.rb +28 -0
- data/lib/mlist/email_server/smtp.rb +24 -0
- data/lib/mlist/email_server.rb +2 -0
- data/lib/mlist/email_subscriber.rb +6 -0
- data/lib/mlist/list.rb +183 -0
- data/lib/mlist/mail_list.rb +277 -0
- data/lib/mlist/manager/database.rb +48 -0
- data/lib/mlist/manager/notifier.rb +31 -0
- data/lib/mlist/manager.rb +30 -0
- data/lib/mlist/message.rb +150 -0
- data/lib/mlist/server.rb +62 -0
- data/lib/mlist/thread.rb +98 -0
- data/lib/mlist/util/email_helpers.rb +155 -0
- data/lib/mlist/util/header_sanitizer.rb +71 -0
- data/lib/mlist/util/quoting.rb +70 -0
- data/lib/mlist/util/tmail_builder.rb +42 -0
- data/lib/mlist/util/tmail_methods.rb +138 -0
- data/lib/mlist/util.rb +12 -0
- data/lib/mlist.rb +46 -0
- data/lib/pop_ssl.rb +999 -0
- data/rails/init.rb +22 -0
- data/spec/fixtures/schema.rb +94 -0
- data/spec/integration/date_formats_spec.rb +12 -0
- data/spec/integration/mlist_spec.rb +232 -0
- data/spec/integration/pop_email_server_spec.rb +22 -0
- data/spec/integration/proof_spec.rb +74 -0
- data/spec/matchers/equal_tmail.rb +53 -0
- data/spec/matchers/have_address.rb +48 -0
- data/spec/matchers/have_header.rb +104 -0
- data/spec/models/email_post_spec.rb +100 -0
- data/spec/models/email_server/base_spec.rb +11 -0
- data/spec/models/email_spec.rb +54 -0
- data/spec/models/mail_list_spec.rb +469 -0
- data/spec/models/message_spec.rb +109 -0
- data/spec/models/thread_spec.rb +83 -0
- data/spec/models/util/email_helpers_spec.rb +47 -0
- data/spec/models/util/header_sanitizer_spec.rb +19 -0
- data/spec/models/util/quoting_spec.rb +96 -0
- data/spec/spec_helper.rb +76 -0
- metadata +103 -0
data/rails/init.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Provides the mechinism to support applications that want to observe MList
|
2
|
+
# models.
|
3
|
+
#
|
4
|
+
# ActiveRecord observers are reloaded at each request in development mode.
|
5
|
+
# They will be registered with the MList models each time. Since the MList
|
6
|
+
# models are required once at initialization, there will always only be one
|
7
|
+
# instance of the model class, and therefore, many instances of the observer
|
8
|
+
# class registered with it; all but the most recent are invalid, since they
|
9
|
+
# were undefined when the dispatcher reloaded the application.
|
10
|
+
#
|
11
|
+
# Should we ever have observers in MList, this will likely need more careful
|
12
|
+
# attention.
|
13
|
+
#
|
14
|
+
unless Rails.configuration.cache_classes
|
15
|
+
class << ActiveRecord::Base
|
16
|
+
def instantiate_observers_with_mlist_observers
|
17
|
+
subclasses.each(&:delete_observers)
|
18
|
+
instantiate_observers_without_mlist_observers
|
19
|
+
end
|
20
|
+
alias_method_chain :instantiate_observers, :mlist_observers
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 20081126181722) do
|
2
|
+
|
3
|
+
# All MList required tables are prefixed with 'mlist_' to ease integration
|
4
|
+
# into other systems' databases.
|
5
|
+
|
6
|
+
|
7
|
+
# The table in which email content is stored.
|
8
|
+
#
|
9
|
+
create_table :mlist_emails, :force => true do |t|
|
10
|
+
t.column :source, :text
|
11
|
+
t.column :created_at, :datetime
|
12
|
+
end
|
13
|
+
|
14
|
+
# The table in which MList will store MList::Messages.
|
15
|
+
#
|
16
|
+
# The identifier is the 'message-id' header value of the finally delivered
|
17
|
+
# email.
|
18
|
+
#
|
19
|
+
# An MList::Message will store a reference to your application's subscriber
|
20
|
+
# instance if it is an ActiveRecord subclass. That subclass must respond to
|
21
|
+
# :email_address. If your subscriber is just a string, it is assumed to be
|
22
|
+
# an email address. Either way, that email address will be stored with the
|
23
|
+
# MList::Message, providing a way for you to associate messages by
|
24
|
+
# subscriber_address. This is less ideal, as you may allow subscribers to
|
25
|
+
# change their email addresses over time.
|
26
|
+
#
|
27
|
+
create_table :mlist_messages, :force => true do |t|
|
28
|
+
t.column :mail_list_id, :integer
|
29
|
+
t.column :thread_id, :integer
|
30
|
+
t.column :email_id, :integer
|
31
|
+
t.column :identifier, :string
|
32
|
+
t.column :parent_identifier, :string
|
33
|
+
t.column :parent_id, :integer
|
34
|
+
t.column :mailer, :string
|
35
|
+
t.column :subject, :string
|
36
|
+
t.column :subscriber_address, :string
|
37
|
+
t.column :subscriber_type, :string
|
38
|
+
t.column :subscriber_id, :integer
|
39
|
+
t.column :created_at, :datetime
|
40
|
+
end
|
41
|
+
add_index :mlist_messages, :mail_list_id
|
42
|
+
add_index :mlist_messages, :thread_id
|
43
|
+
add_index :mlist_messages, :identifier
|
44
|
+
add_index :mlist_messages, :parent_identifier
|
45
|
+
add_index :mlist_messages, :parent_id
|
46
|
+
add_index :mlist_messages, :subject
|
47
|
+
add_index :mlist_messages, :subscriber_address
|
48
|
+
add_index :mlist_messages, [:subscriber_type, :subscriber_id]
|
49
|
+
|
50
|
+
# Every MList::Message is associated with an MList::Thread.
|
51
|
+
#
|
52
|
+
create_table :mlist_threads, :force => true do |t|
|
53
|
+
t.column :mail_list_id, :integer
|
54
|
+
t.column :messages_count, :integer
|
55
|
+
t.timestamps
|
56
|
+
end
|
57
|
+
add_index :mlist_threads, :mail_list_id
|
58
|
+
|
59
|
+
# The table in which MList will store MList::MailLists.
|
60
|
+
#
|
61
|
+
# The manager_list_identifier column stores the MList::List#list_id value.
|
62
|
+
# This is a connection to the application's implementation of MList::List.
|
63
|
+
# These identifiers must be unique and never change for an MList::List.
|
64
|
+
#
|
65
|
+
# An MList::MailList will store a reference to your application's
|
66
|
+
# MList::List instance if it is an ActiveRecord subclass.
|
67
|
+
#
|
68
|
+
create_table :mlist_mail_lists, :force => true do |t|
|
69
|
+
t.column :manager_list_identifier, :string
|
70
|
+
t.column :manager_list_type, :string
|
71
|
+
t.column :manager_list_id, :integer
|
72
|
+
t.column :messages_count, :integer
|
73
|
+
t.column :threads_count, :integer
|
74
|
+
t.timestamps
|
75
|
+
end
|
76
|
+
add_index :mlist_mail_lists, :manager_list_identifier
|
77
|
+
add_index :mlist_mail_lists, [:manager_list_identifier, :manager_list_type, :manager_list_id],
|
78
|
+
:name => :index_mlist_mail_lists_on_manager_association
|
79
|
+
|
80
|
+
|
81
|
+
# Database list manager tables, used for testing purposes.
|
82
|
+
#
|
83
|
+
create_table :lists, :force => true do |t|
|
84
|
+
t.column :address, :string
|
85
|
+
t.column :label, :string
|
86
|
+
t.column :created_at, :datetime
|
87
|
+
end
|
88
|
+
|
89
|
+
create_table :subscribers, :force => true do |t|
|
90
|
+
t.column :list_id, :integer
|
91
|
+
t.column :email_address, :string
|
92
|
+
t.column :created_at, :datetime
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe MList, 'date formats' do
|
4
|
+
specify 'mlist_reply_timestamp should handle single digit days and months' do
|
5
|
+
Time.local(2009, 2, 3, 7).to_s(:mlist_reply_timestamp).should == 'Tue, Feb 3, 2009 at 7:00 AM'
|
6
|
+
end
|
7
|
+
|
8
|
+
specify 'mlist_reply_timestamp should handle double digit days and months' do
|
9
|
+
Time.local(2009, 2, 13, 11).to_s(:mlist_reply_timestamp).should == 'Fri, Feb 13, 2009 at 11:00 AM'
|
10
|
+
Time.local(2009, 2, 13, 14).to_s(:mlist_reply_timestamp).should == 'Fri, Feb 13, 2009 at 2:00 PM'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
require 'mlist/manager/database'
|
4
|
+
|
5
|
+
describe MList do
|
6
|
+
def forward_email(tmail)
|
7
|
+
simple_matcher('forward email') do |email_server|
|
8
|
+
lambda do
|
9
|
+
lambda do
|
10
|
+
email_server.receive(tmail)
|
11
|
+
end.should_not change(email_server.deliveries, :size)
|
12
|
+
end.should_not store_message
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def store_message
|
17
|
+
simple_matcher('store message') do |block|
|
18
|
+
thread_count, message_count = MList::Thread.count, MList::Message.count
|
19
|
+
block.call
|
20
|
+
MList::Thread.count == thread_count + 1 && MList::Message.count == message_count + 1
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def start_new_thread(tmail)
|
25
|
+
simple_matcher('start new thread') do |email_server|
|
26
|
+
delivery_count = email_server.deliveries.size
|
27
|
+
thread_count = MList::Thread.count
|
28
|
+
message_count = MList::Message.count
|
29
|
+
email_server.receive(tmail)
|
30
|
+
email_server.deliveries.size > delivery_count &&
|
31
|
+
MList::Thread.count == (thread_count + 1) &&
|
32
|
+
MList::Message.count == (message_count + 1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
dataset do
|
37
|
+
@list_manager = MList::Manager::Database.new
|
38
|
+
@list_one = @list_manager.create_list('list_one@example.com')
|
39
|
+
@list_one.subscribe('adam@nomail.net')
|
40
|
+
@list_one.subscribe('tom@example.com')
|
41
|
+
@list_one.subscribe('dick@example.com')
|
42
|
+
|
43
|
+
@list_two = @list_manager.create_list('list_two@example.com')
|
44
|
+
@list_two.subscribe('adam@nomail.net')
|
45
|
+
@list_two.subscribe('jane@example.com')
|
46
|
+
|
47
|
+
@list_three = @list_manager.create_list('empty@example.com')
|
48
|
+
@list_three.subscribe('adam@nomail.net')
|
49
|
+
end
|
50
|
+
|
51
|
+
before do
|
52
|
+
@email_server = MList::EmailServer::Fake.new
|
53
|
+
@server = MList::Server.new(
|
54
|
+
:list_manager => @list_manager,
|
55
|
+
:email_server => @email_server
|
56
|
+
)
|
57
|
+
|
58
|
+
# TODO Move this stuff to Dataset
|
59
|
+
ActiveRecord::Base.connection.increment_open_transactions
|
60
|
+
ActiveRecord::Base.connection.begin_db_transaction
|
61
|
+
end
|
62
|
+
|
63
|
+
after do
|
64
|
+
if ActiveRecord::Base.connection.open_transactions != 0
|
65
|
+
ActiveRecord::Base.connection.rollback_db_transaction
|
66
|
+
ActiveRecord::Base.connection.decrement_open_transactions
|
67
|
+
end
|
68
|
+
ActiveRecord::Base.clear_active_connections!
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should have threads and mail_lists updated_at set to last message receive time' do
|
72
|
+
now = Time.now
|
73
|
+
stub(Time).now {now}
|
74
|
+
@email_server.receive(tmail_fixture('single_list'))
|
75
|
+
MList::MailList.last.updated_at.to_s.should == now.to_s
|
76
|
+
MList::Thread.last.updated_at.to_s.should == now.to_s
|
77
|
+
|
78
|
+
later = 5.days.from_now
|
79
|
+
stub(Time).now {later}
|
80
|
+
@email_server.receive(tmail_fixture('single_list_reply'))
|
81
|
+
MList::MailList.last.updated_at.to_s.should == later.to_s
|
82
|
+
MList::Thread.last.updated_at.to_s.should == later.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should associate manager lists to mlist mail lists when they are ActiveRecord instances' do
|
86
|
+
@email_server.receive(tmail_fixture('single_list'))
|
87
|
+
mail_list = MList::MailList.last
|
88
|
+
mail_list.manager_list.should == @list_one
|
89
|
+
mail_list.manager_list_identifier.should == @list_one.list_id
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should not forward mail that has been on this server before' do
|
93
|
+
@email_server.should_not forward_email(tmail_fixture('x-beenthere'))
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'should store message/thread even when there are no recipient subscribers' do
|
97
|
+
tmail = tmail_fixture('single_list')
|
98
|
+
tmail.to = @list_three.address
|
99
|
+
@email_server.should start_new_thread(tmail)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should not forward mail from non-subscriber and notify manager list' do
|
103
|
+
tmail = tmail_fixture('single_list')
|
104
|
+
tmail.from = 'unknown@example.com'
|
105
|
+
stub(@list_manager).lists(is_a(MList::Email)) { [@list_one] }
|
106
|
+
mock(@list_one).non_subscriber_post(is_a(MList::Email))
|
107
|
+
@email_server.should_not forward_email(tmail)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should not forward mail from non-subscriber when inactive and notify as non-subscriber' do
|
111
|
+
tmail = tmail_fixture('single_list')
|
112
|
+
tmail.from = 'unknown@example.com'
|
113
|
+
do_not_call(@list_one).active?
|
114
|
+
stub(@list_manager).lists(is_a(MList::Email)) { [@list_one] }
|
115
|
+
mock(@list_one).non_subscriber_post(is_a(MList::Email))
|
116
|
+
@email_server.should_not forward_email(tmail)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should not forward mail from blocked subscriber, notify the subscriber using list manager notifier' do
|
120
|
+
subscriber = MList::Manager::Database::Subscriber.find_by_email_address('adam@nomail.net')
|
121
|
+
tmail = tmail_fixture('single_list')
|
122
|
+
mock(@list_one).active? {true}
|
123
|
+
mock(@list_one).blocked?(subscriber) { true }
|
124
|
+
stub(@list_manager).lists(is_a(MList::Email)) { [@list_one] }
|
125
|
+
lambda do
|
126
|
+
@email_server.receive(tmail)
|
127
|
+
response_tmail = @email_server.deliveries.last
|
128
|
+
response_tmail.header_string('from').should == 'mlist-list_one@example.com'
|
129
|
+
response_tmail.header_string('to').should == 'adam@nomail.net'
|
130
|
+
response_tmail.header_string('x-mlist-loop').should == 'notice'
|
131
|
+
response_tmail.header_string('x-mlist-notice').should == 'subscriber_blocked'
|
132
|
+
end.should_not store_message
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should not forward mail to inactive list and notify manager list' do
|
136
|
+
tmail = tmail_fixture('single_list')
|
137
|
+
mock(@list_one).active? { false }
|
138
|
+
stub(@list_manager).lists(is_a(MList::Email)) { [@list_one] }
|
139
|
+
mock(@list_one).inactive_post(is_a(MList::Email))
|
140
|
+
@email_server.should_not forward_email(tmail)
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should report unrecognized email to list manager' do
|
144
|
+
tmail = tmail_fixture('single_list')
|
145
|
+
stub(@list_manager).lists(is_a(MList::Email)) { [] }
|
146
|
+
mock(@list_manager).no_lists_found(is_a(MList::Email))
|
147
|
+
@email_server.should_not forward_email(tmail)
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should report bounces to the list manager' do
|
151
|
+
stub(@list_manager).lists(is_a(MList::Email)) { [@list_one] }
|
152
|
+
mock(@list_one).bounce(is_a(MList::Email))
|
153
|
+
@email_server.should_not forward_email(tmail_fixture('bounces/1'))
|
154
|
+
end
|
155
|
+
|
156
|
+
describe 'single list' do
|
157
|
+
before do
|
158
|
+
@tmail_post = tmail_fixture('single_list')
|
159
|
+
@email_server.receive(@tmail_post)
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'should forward emails that are sent to a mailing list' do
|
163
|
+
@email_server.deliveries.size.should == 1
|
164
|
+
email = @email_server.deliveries.first
|
165
|
+
email.should have_address(:to, 'list_one@example.com')
|
166
|
+
# bcc fields are not in the headers of delivered emails
|
167
|
+
email.should have_address(:'reply-to', 'list_one@example.com')
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should start a new thread for a new email' do
|
171
|
+
thread = MList::Thread.last
|
172
|
+
thread.messages.first.email.tmail.should equal_tmail(@tmail_post)
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'should add to an existing thread when reply email' do
|
176
|
+
message = MList::Message.last
|
177
|
+
reply_tmail = tmail_fixture('single_list_reply', 'in-reply-to' => "<#{message.identifier}>")
|
178
|
+
@email_server.receive(reply_tmail)
|
179
|
+
thread = MList::Thread.last
|
180
|
+
thread.messages.size.should be(2)
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'should associate parent message when reply email' do
|
184
|
+
message = MList::Message.last
|
185
|
+
@email_server.receive(tmail_fixture('single_list_reply', 'in-reply-to' => "<#{message.identifier}>"))
|
186
|
+
reply = MList::Message.last
|
187
|
+
reply.parent_identifier.should == message.identifier
|
188
|
+
reply.parent.should == message
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'should not associate a parent when not a reply' do
|
192
|
+
tmail = tmail_fixture('single_list')
|
193
|
+
tmail['message-id'] = 'asdfasdfj'
|
194
|
+
tmail['subject'] = 'other thing'
|
195
|
+
@email_server.should start_new_thread(tmail)
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'should store subscriber address with messages' do
|
199
|
+
MList::Message.last.subscriber_address.should == 'adam@nomail.net'
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'should associate subscriber to messages when they are ActiveRecord instances' do
|
203
|
+
MList::Message.last.subscriber.should == MList::Manager::Database::Subscriber.find_by_email_address('adam@nomail.net')
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe 'multiple lists' do
|
208
|
+
before do
|
209
|
+
@tmail_post = tmail_fixture('multiple_lists')
|
210
|
+
@email_server.receive(@tmail_post)
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'should forward emails that are sent to a mailing list' do
|
214
|
+
@email_server.deliveries.size.should == 2
|
215
|
+
|
216
|
+
email = @email_server.deliveries.first
|
217
|
+
email.should have_address(:to, 'list_one@example.com')
|
218
|
+
# bcc fields are not in the headers of delivered emails
|
219
|
+
email.should have_address(:'reply-to', 'list_one@example.com')
|
220
|
+
|
221
|
+
email = @email_server.deliveries.last
|
222
|
+
email.should have_address(:to, 'list_two@example.com')
|
223
|
+
# bcc fields are not in the headers of delivered emails
|
224
|
+
email.should have_address(:'reply-to', 'list_two@example.com')
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'should start a new thread for each list, both referencing the same email' do
|
228
|
+
threads = MList::Thread.find(:all)
|
229
|
+
threads[0].messages.first.email.should == threads[1].messages.first.email
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'mlist/email_server/pop'
|
3
|
+
|
4
|
+
describe MList::EmailServer::Pop, 'execute' do
|
5
|
+
before do
|
6
|
+
@mails = []
|
7
|
+
@pop = Object.new
|
8
|
+
stub(@pop).mails { @mails }
|
9
|
+
stub(@pop).start do |username, password, block|
|
10
|
+
block.call @pop
|
11
|
+
end
|
12
|
+
stub(Net::POP3).new { @pop }
|
13
|
+
@pop_server = MList::EmailServer::Pop.new({})
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should delete email after successfully receiving' do
|
17
|
+
message = OpenStruct.new(:pop => email_fixture('single_list'))
|
18
|
+
mock(message).delete
|
19
|
+
@mails << message
|
20
|
+
@pop_server.execute
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
require 'mlist/manager/database'
|
4
|
+
|
5
|
+
describe MList do
|
6
|
+
include MList::Util::EmailHelpers
|
7
|
+
|
8
|
+
dataset do
|
9
|
+
subscriber_addresses = %w(adam@nomail.net adam@example.net)
|
10
|
+
|
11
|
+
@list_manager = MList::Manager::Database.new
|
12
|
+
Dir[email_fixtures_path('integration/list*')].each do |list_path|
|
13
|
+
list = @list_manager.create_list("#{File.basename(list_path)}@example.com")
|
14
|
+
subscriber_addresses.each {|a| list.subscribe(a)}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
@email_server = MList::EmailServer::Fake.new
|
20
|
+
@server = MList::Server.new(
|
21
|
+
:list_manager => @list_manager,
|
22
|
+
:email_server => @email_server
|
23
|
+
)
|
24
|
+
|
25
|
+
# TODO Move this stuff to Dataset
|
26
|
+
ActiveRecord::Base.connection.increment_open_transactions
|
27
|
+
ActiveRecord::Base.connection.begin_db_transaction
|
28
|
+
end
|
29
|
+
|
30
|
+
after do
|
31
|
+
if ActiveRecord::Base.connection.open_transactions != 0
|
32
|
+
ActiveRecord::Base.connection.rollback_db_transaction
|
33
|
+
ActiveRecord::Base.connection.decrement_open_transactions
|
34
|
+
end
|
35
|
+
ActiveRecord::Base.clear_active_connections!
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should place messages in threads and threads in lists' do
|
39
|
+
Dir[email_fixtures_path('integration/list*')].each do |list_path|
|
40
|
+
list = MList::Manager::Database::List.find_by_address(File.basename(list_path) + '@example.com')
|
41
|
+
Dir[File.join(list_path, 'thread*')].each do |thread_path|
|
42
|
+
email_paths = Dir[File.join(thread_path, '*.eml')]
|
43
|
+
top_post_path = email_paths.shift
|
44
|
+
top_post_tmail = TMail::Mail.load(top_post_path)
|
45
|
+
stub(@email_server).generate_message_id { remove_brackets(top_post_tmail.message_id) }
|
46
|
+
@email_server.should start_thread(top_post_tmail)
|
47
|
+
email_paths.each do |email_path|
|
48
|
+
tmail = TMail::Mail.load(email_path)
|
49
|
+
stub(@email_server).generate_message_id { remove_brackets(tmail.message_id) }
|
50
|
+
expected = email_path =~ %r{\d+\.eml\Z} ? :should : :should_not
|
51
|
+
@email_server.send expected, accept_message(tmail)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def accept_message(tmail)
|
58
|
+
simple_matcher("to receive message from #{tmail.header_string('from')}") do |email_server|
|
59
|
+
message_count_start = MList::Message.count
|
60
|
+
email_server.receive(tmail)
|
61
|
+
MList::Message.count == message_count_start + 1 && MList::Message.last.identifier == remove_brackets(@email_server.deliveries.last.header_string('message-id'))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def start_thread(tmail)
|
66
|
+
simple_matcher("to begin thread from #{tmail.header_string('from')}") do |email_server|
|
67
|
+
message_count_start = MList::Message.count
|
68
|
+
thread_count_start = MList::Thread.count
|
69
|
+
email_server.receive(tmail)
|
70
|
+
MList::Message.count == message_count_start + 1
|
71
|
+
MList::Thread.count == thread_count_start + 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Spec
|
2
|
+
module Matchers
|
3
|
+
|
4
|
+
class EqualTmail
|
5
|
+
def initialize(expected)
|
6
|
+
@expected = expected
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(tmail)
|
10
|
+
@given = tmail
|
11
|
+
headers_match?
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure_message
|
15
|
+
@failure_message
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def comparable_value(value)
|
20
|
+
case value
|
21
|
+
when Array
|
22
|
+
value.collect {|e| e.to_s.strip}.join(" ").strip
|
23
|
+
when String
|
24
|
+
value.strip
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def headers_match?
|
29
|
+
missing = @expected.header.collect { |name, value| @given[name].nil? ? name : nil }.compact
|
30
|
+
extra = @given.header.collect { |name, value| @expected[name].nil? ? name : nil }.compact
|
31
|
+
if extra.empty? && missing.empty?
|
32
|
+
unequal = []
|
33
|
+
@expected.header.each do |name, value|
|
34
|
+
expected_value, given_value = comparable_value(value), comparable_value(@given[name])
|
35
|
+
unless expected_value == given_value
|
36
|
+
unequal << "expected header #{name.inspect} to be #{expected_value.inspect} but was #{given_value.inspect}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@failure_message = unequal.join("\n") unless unequal.empty?
|
40
|
+
else
|
41
|
+
@failure_message = "expected tmail instances to be equal but headers were not"
|
42
|
+
@failure_message << "\nmissing in given: #{missing.inspect}" unless missing.empty?
|
43
|
+
@failure_message << "\nextra in given: #{extra.inspect}" unless extra.empty?
|
44
|
+
end
|
45
|
+
@failure_message.nil?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def equal_tmail(tmail)
|
50
|
+
EqualTmail.new(tmail)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Spec
|
2
|
+
module Matchers
|
3
|
+
|
4
|
+
class HaveAddress
|
5
|
+
def initialize(field, expected)
|
6
|
+
@field, @expected = field, expected
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(email)
|
10
|
+
@actual = addresses(email)
|
11
|
+
missing = expected_addresses.reject {|e| @actual.include?(e)}
|
12
|
+
extra = @actual.reject {|e| expected_addresses.include?(e)}
|
13
|
+
extra.empty? && missing.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def failure_message
|
17
|
+
"expected #{@field} address to contain #{expected_addresses.inspect} but was #{@actual.inspect}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def negative_failure_message
|
21
|
+
"expected #{@field} address not to contain #{expected_addresses.inspect} but it did"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def addresses(email)
|
26
|
+
email[@field.to_s].addrs.collect(&:address) rescue []
|
27
|
+
end
|
28
|
+
|
29
|
+
def expected_addresses
|
30
|
+
case @expected
|
31
|
+
when Array
|
32
|
+
@expected.collect { |a| extract_address(a) }
|
33
|
+
when String
|
34
|
+
[extract_address(@expected)]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_address(string)
|
39
|
+
address = string.sub(/.*?<(.*?)>/, '\1')
|
40
|
+
address if address =~ /\A([^@\s]+)@(localhost|(?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def have_address(field, expected)
|
45
|
+
HaveAddress.new(field, expected)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Spec
|
2
|
+
module Matchers
|
3
|
+
|
4
|
+
class HaveHeader
|
5
|
+
def initialize(name, expected)
|
6
|
+
@name, @expected = name, expected
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(email)
|
10
|
+
@given = email
|
11
|
+
|
12
|
+
if @expected.blank?
|
13
|
+
if !@given[@name.downcase]
|
14
|
+
@failure_message = "expected header #{@name.inspect} to be present but it was not"
|
15
|
+
end
|
16
|
+
else
|
17
|
+
missing = expected_values.reject {|e| header_values.include?(e)}
|
18
|
+
extra = header_values.reject {|e| expected_values.include?(e)}
|
19
|
+
unless extra.empty? && missing.empty?
|
20
|
+
@failure_message = "expected header #{@name.inspect} to be equal but was not"
|
21
|
+
@failure_message << "\nmissing in given: #{missing.inspect}" unless missing.empty?
|
22
|
+
@failure_message << "\nextra in given: #{extra.inspect}" unless extra.empty?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@failure_message.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def failure_message
|
30
|
+
@failure_message
|
31
|
+
end
|
32
|
+
|
33
|
+
def negative_failure_message
|
34
|
+
if @expected.blank? && @given[@name.downcase]
|
35
|
+
"expected header #{@name.inspect} to not be present but was"
|
36
|
+
else
|
37
|
+
"expected header #{@name.inspect} to not be equal but it was"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def comparable_value(value)
|
43
|
+
case value
|
44
|
+
when Array
|
45
|
+
value.collect {|e| e.to_s.strip}.join(" ").strip
|
46
|
+
when String
|
47
|
+
value.strip
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def expected_values
|
52
|
+
case @expected
|
53
|
+
when Array
|
54
|
+
@expected.collect { |e| comparable_value(e) }
|
55
|
+
when String
|
56
|
+
[comparable_value(@expected)]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def header_values
|
61
|
+
values = []
|
62
|
+
@given.each_header do |k,v|
|
63
|
+
values << v.to_s.strip if k.downcase == @name.downcase
|
64
|
+
end
|
65
|
+
values
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class HaveHeaders
|
70
|
+
def initialize(expected)
|
71
|
+
@matchers = case expected
|
72
|
+
when Array
|
73
|
+
expected.collect {|e| HaveHeader.new(e, nil)}
|
74
|
+
when Hash
|
75
|
+
expected.collect {|k,v| HaveHeader.new(k,v)}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def matches?(given)
|
80
|
+
@matched, @failed = [], []
|
81
|
+
@matchers.each do |matcher|
|
82
|
+
(matcher.matches?(given) ? @matched : @failed) << matcher
|
83
|
+
end
|
84
|
+
@failed.empty?
|
85
|
+
end
|
86
|
+
|
87
|
+
def failure_message
|
88
|
+
@failed.collect(&:failure_message).join("\n")
|
89
|
+
end
|
90
|
+
|
91
|
+
def negative_failure_message
|
92
|
+
raise 'have_headers cannot be used in a negative'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def have_header(name, expected = nil)
|
97
|
+
HaveHeader.new(name, expected)
|
98
|
+
end
|
99
|
+
|
100
|
+
def have_headers(expected)
|
101
|
+
HaveHeaders.new(expected)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|