mlist 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +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
|