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/lib/mlist/list.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
module MList
|
2
|
+
|
3
|
+
# Represents the interface of the lists that a list manager must answer.
|
4
|
+
# This is distinct from the MList::MailList to allow for greater flexibility
|
5
|
+
# in processing email coming to a list - that is, whatever you include this
|
6
|
+
# into may re-define behavior appropriately.
|
7
|
+
#
|
8
|
+
# Your 'subscriber' instances MUST respond to :email_address. They may
|
9
|
+
# optionally respond to :display_name.
|
10
|
+
#
|
11
|
+
module List
|
12
|
+
|
13
|
+
# Answers whether this list is active or not. All lists are active all the
|
14
|
+
# time by default.
|
15
|
+
#
|
16
|
+
def active?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
# Answers whether the subscriber is blocked from posting or not. This will
|
21
|
+
# not be asked when the list is not active (answers _active?_ as false).
|
22
|
+
#
|
23
|
+
def blocked?(subscriber)
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
# Answers the footer content for this list. Default implementation is very
|
28
|
+
# simple.
|
29
|
+
#
|
30
|
+
def footer_content(message)
|
31
|
+
%Q{The "#{label}" mailing list\nPost messages: #{post_url}}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Answer a suitable label for the list, which will be used in various
|
35
|
+
# parts of content that is delivered to subscribers, etc.
|
36
|
+
#
|
37
|
+
def label
|
38
|
+
raise 'answer the list label'
|
39
|
+
end
|
40
|
+
|
41
|
+
# Answers the headers that are to be included in the emails delivered for
|
42
|
+
# this list. Any entries that have a nil value will not be included in the
|
43
|
+
# delivered email.
|
44
|
+
#
|
45
|
+
def list_headers
|
46
|
+
{
|
47
|
+
'list-id' => list_id,
|
48
|
+
'list-archive' => archive_url,
|
49
|
+
'list-subscribe' => subscribe_url,
|
50
|
+
'list-unsubscribe' => unsubscribe_url,
|
51
|
+
'list-owner' => owner_url,
|
52
|
+
'list-help' => help_url,
|
53
|
+
'list-post' => post_url
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Answers a unique, never changing value for this list.
|
58
|
+
#
|
59
|
+
def list_id
|
60
|
+
raise 'answer a unique, never changing value'
|
61
|
+
end
|
62
|
+
|
63
|
+
# The web address where an archive of this list may be found, nil if there
|
64
|
+
# is no archive.
|
65
|
+
#
|
66
|
+
def archive_url
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# Should the sender of an email be copied in the publication? Defaults to
|
71
|
+
# false. If _recipients_ includes the sender email address, it will be
|
72
|
+
# removed if this answers false.
|
73
|
+
#
|
74
|
+
# The sending subscriber is provided to support preferential answering.
|
75
|
+
#
|
76
|
+
def copy_sender?(subscriber)
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
# The web address of the list help site, nil if this is not supported.
|
81
|
+
#
|
82
|
+
def help_url
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# The email address of the list owner, nil if this is not supported.
|
87
|
+
#
|
88
|
+
def owner_url
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# The email address where posts should be sent. Defaults to the address of
|
93
|
+
# the list.
|
94
|
+
#
|
95
|
+
def post_url
|
96
|
+
address
|
97
|
+
end
|
98
|
+
|
99
|
+
# Should the reply-to header be set to the list's address? Defaults to
|
100
|
+
# true. If false is returned, the reply-to will be the subscriber address.
|
101
|
+
#
|
102
|
+
def reply_to_list?
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
# The web url where subscriptions to this list may be created, nil if this
|
107
|
+
# is not supported.
|
108
|
+
#
|
109
|
+
def subscribe_url
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
|
113
|
+
# The web url where subscriptions to this list may be deleted, nil if this
|
114
|
+
# is not supported.
|
115
|
+
#
|
116
|
+
def unsubscribe_url
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
# A list is responsible for answering the recipient subscribers. The
|
121
|
+
# answer may or may not include the subscriber; _copy_sender?_ will be
|
122
|
+
# invoked and the subscriber will be added or removed from the Array.
|
123
|
+
#
|
124
|
+
# The sending subscriber is provided if the list would like to utilize it
|
125
|
+
# in calculating the recipients.
|
126
|
+
#
|
127
|
+
def recipients(subscriber)
|
128
|
+
subscribers
|
129
|
+
end
|
130
|
+
|
131
|
+
# A list must answer the subscriber who's email address is that of the one
|
132
|
+
# provided. The default implementation will pick the first instance that
|
133
|
+
# answers subscriber.email_address == email_address. Your implementation
|
134
|
+
# should probably select just one record.
|
135
|
+
#
|
136
|
+
def subscriber(email_address)
|
137
|
+
subscribers.detect {|s| s.email_address == email_address}
|
138
|
+
end
|
139
|
+
|
140
|
+
# A list must answer whether there is a subscriber who's email address is
|
141
|
+
# that of the one provided. This is checked before the subscriber is
|
142
|
+
# requested in order to allow for the lightest weight check possible; that
|
143
|
+
# is, your implementation could avoid loading the actual subscriber
|
144
|
+
# instance.
|
145
|
+
#
|
146
|
+
def subscriber?(email_address)
|
147
|
+
!subscriber(email_address).nil?
|
148
|
+
end
|
149
|
+
|
150
|
+
# Methods that will be invoked on your implementation of Mlist::List when
|
151
|
+
# certain events occur during the processing of email sent to a list.
|
152
|
+
#
|
153
|
+
module Callbacks
|
154
|
+
|
155
|
+
# Called when an email is a post to the list by a subscriber whom the
|
156
|
+
# list claims is blocked (answers true to _blocked?(subscriber)_). This
|
157
|
+
# will not be called if the list is inactive (answers false to
|
158
|
+
# _active?_);
|
159
|
+
#
|
160
|
+
def blocked_subscriber_post(email, subscriber)
|
161
|
+
end
|
162
|
+
|
163
|
+
def bounce(email)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Called when an email is a post to the list while the list is inactive
|
167
|
+
# (answers false to _active?_). This will not be called if the email is
|
168
|
+
# from a non-subscribed sender. Instead, _non_subscriber_post_ will be
|
169
|
+
# called.
|
170
|
+
#
|
171
|
+
def inactive_post(email)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Called when an email is a post to the list from a non-subscribed
|
175
|
+
# sender. This will be called even if the list is inactive.
|
176
|
+
#
|
177
|
+
def non_subscriber_post(email)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
include Callbacks
|
181
|
+
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
module MList
|
2
|
+
class MailList < ActiveRecord::Base
|
3
|
+
set_table_name 'mlist_mail_lists'
|
4
|
+
|
5
|
+
# Provides the MailList for a given implementation of MList::List,
|
6
|
+
# connecting it to the provided email server for delivering posts.
|
7
|
+
#
|
8
|
+
def self.find_or_create_by_list(list, outgoing_server)
|
9
|
+
if list.is_a?(ActiveRecord::Base)
|
10
|
+
mail_list = find_or_create_by_manager_list_identifier_and_manager_list_type_and_manager_list_id(
|
11
|
+
list.list_id, list.class.base_class.name, list.id
|
12
|
+
)
|
13
|
+
else
|
14
|
+
mail_list = find_or_create_by_manager_list_identifier(list.list_id)
|
15
|
+
mail_list.manager_list = list
|
16
|
+
end
|
17
|
+
mail_list.outgoing_server = outgoing_server
|
18
|
+
mail_list
|
19
|
+
end
|
20
|
+
|
21
|
+
include MList::Util::EmailHelpers
|
22
|
+
|
23
|
+
belongs_to :manager_list, :polymorphic => true
|
24
|
+
|
25
|
+
before_destroy :delete_unreferenced_email
|
26
|
+
has_many :messages, :class_name => 'MList::Message', :dependent => :delete_all
|
27
|
+
has_many :threads, :class_name => 'MList::Thread', :dependent => :delete_all
|
28
|
+
|
29
|
+
delegate :address, :label, :post_url, :to => :list
|
30
|
+
|
31
|
+
attr_accessor :outgoing_server
|
32
|
+
|
33
|
+
# Creates a new MList::Message and delivers it to the subscribers of this
|
34
|
+
# list.
|
35
|
+
#
|
36
|
+
def post(email_or_attributes)
|
37
|
+
email = email_or_attributes
|
38
|
+
email = MList::EmailPost.new(email_or_attributes) unless email.is_a?(MList::EmailPost)
|
39
|
+
process_message messages.build(
|
40
|
+
:parent => email.reply_to_message,
|
41
|
+
:parent_identifier => email.parent_identifier,
|
42
|
+
:mail_list => self,
|
43
|
+
:subscriber => email.subscriber,
|
44
|
+
:recipients => list.recipients(email.subscriber),
|
45
|
+
:email => MList::Email.new(:source => email.to_s)
|
46
|
+
), :search_parent => false, :copy_sender => email.copy_sender
|
47
|
+
end
|
48
|
+
|
49
|
+
# Processes the email received by the MList::Server.
|
50
|
+
#
|
51
|
+
def process_email(email, subscriber)
|
52
|
+
recipients = list.recipients(subscriber)
|
53
|
+
process_message messages.build(
|
54
|
+
:mail_list => self,
|
55
|
+
:subscriber => subscriber,
|
56
|
+
:recipients => recipients,
|
57
|
+
:email => email
|
58
|
+
), :copy_sender => list.copy_sender?(subscriber)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Answers the provided subject with superfluous 're:' and this list's
|
62
|
+
# labels removed.
|
63
|
+
#
|
64
|
+
# clean_subject('[List Label] Re: The new Chrome Browser from Google') => 'Re: The new Chrome Browser from Google'
|
65
|
+
# clean_subject('Re: [List Label] Re: The new Chrome Browser from Google') => 'Re: The new Chrome Browser from Google'
|
66
|
+
#
|
67
|
+
def clean_subject(string)
|
68
|
+
without_label = string.gsub(subject_prefix_regex, '')
|
69
|
+
if without_label =~ REGARD_RE
|
70
|
+
"Re: #{remove_regard(without_label)}"
|
71
|
+
else
|
72
|
+
without_label
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_parent_message(email)
|
77
|
+
if in_reply_to = email.header_string('in-reply-to')
|
78
|
+
message = messages.find(:first,
|
79
|
+
:conditions => ['identifier = ?', remove_brackets(in_reply_to)])
|
80
|
+
return message if message
|
81
|
+
end
|
82
|
+
|
83
|
+
if email.references
|
84
|
+
reference_identifiers = email.references.collect {|rid| remove_brackets(rid)}
|
85
|
+
message = messages.find(:first,
|
86
|
+
:conditions => ['identifier in (?)', reference_identifiers],
|
87
|
+
:order => 'created_at desc')
|
88
|
+
return message if message
|
89
|
+
end
|
90
|
+
|
91
|
+
if email.subject =~ REGARD_RE
|
92
|
+
message = messages.find(:first,
|
93
|
+
:conditions => ['subject = ?', remove_regard(clean_subject(email.subject))],
|
94
|
+
:order => 'created_at asc')
|
95
|
+
return message if message
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# The MList::List instance of the list manager.
|
100
|
+
#
|
101
|
+
def list
|
102
|
+
@list ||= manager_list
|
103
|
+
end
|
104
|
+
|
105
|
+
def manager_list_with_dual_type=(list)
|
106
|
+
if list.is_a?(ActiveRecord::Base)
|
107
|
+
self.manager_list_without_dual_type = list
|
108
|
+
@list = list
|
109
|
+
else
|
110
|
+
self.manager_list_without_dual_type = nil
|
111
|
+
@list = list
|
112
|
+
end
|
113
|
+
end
|
114
|
+
alias_method_chain :manager_list=, :dual_type
|
115
|
+
|
116
|
+
# Distinct footer start marker. It is important to realize that changing
|
117
|
+
# this could be problematic.
|
118
|
+
#
|
119
|
+
FOOTER_BLOCK_START = "-~----~~----~----~----~----~---~~-~----~------~--~-~-"
|
120
|
+
|
121
|
+
# Distinct footer end marker. It is important to realize that changing
|
122
|
+
# this could be problematic.
|
123
|
+
#
|
124
|
+
FOOTER_BLOCK_END = "--~--~---~-----~--~----~-----~~~----~---~---~--~----~"
|
125
|
+
|
126
|
+
private
|
127
|
+
FOOTER_BLOCK_START_RE = %r[#{FOOTER_BLOCK_START}]
|
128
|
+
FOOTER_BLOCK_END_RE = %r[#{FOOTER_BLOCK_END}]
|
129
|
+
|
130
|
+
# http://mail.python.org/pipermail/mailman-developers/2006-April/018718.html
|
131
|
+
def bounce_headers
|
132
|
+
# tmail would not correctly quote the label in the sender header, which would break smtp delivery
|
133
|
+
{'sender' => "<mlist-#{address}>", 'errors-to' => "#{label} <mlist-#{address}>"}
|
134
|
+
end
|
135
|
+
|
136
|
+
def delete_unreferenced_email
|
137
|
+
conditions = %Q{
|
138
|
+
mlist_emails.id in (
|
139
|
+
select me.id from mlist_emails me left join mlist_messages mm on mm.email_id = me.id
|
140
|
+
where mm.mail_list_id = #{id}
|
141
|
+
) AND mlist_emails.id not in (
|
142
|
+
select meb.id from mlist_emails meb left join mlist_messages mmb on mmb.email_id = meb.id
|
143
|
+
where mmb.mail_list_id != #{id}
|
144
|
+
)}
|
145
|
+
MList::Email.delete_all(conditions)
|
146
|
+
end
|
147
|
+
|
148
|
+
def strip_list_footers(content)
|
149
|
+
if content =~ FOOTER_BLOCK_START_RE
|
150
|
+
in_footer_block = false
|
151
|
+
content = normalize_new_lines(content)
|
152
|
+
content = content.split("\n").reject do |line|
|
153
|
+
if in_footer_block
|
154
|
+
in_footer_block = line !~ FOOTER_BLOCK_END_RE
|
155
|
+
true
|
156
|
+
else
|
157
|
+
in_footer_block = line =~ FOOTER_BLOCK_START_RE
|
158
|
+
end
|
159
|
+
end.join("\n").rstrip
|
160
|
+
end
|
161
|
+
content
|
162
|
+
end
|
163
|
+
|
164
|
+
# http://www.jamesshuggins.com/h/web1/list-email-headers.htm
|
165
|
+
def list_headers
|
166
|
+
headers = list.list_headers.dup
|
167
|
+
headers['x-beenthere'] = address
|
168
|
+
headers['x-mlist-version'] = MList.version.to_s
|
169
|
+
headers.update(bounce_headers)
|
170
|
+
headers.delete_if {|k,v| v.nil?}
|
171
|
+
end
|
172
|
+
|
173
|
+
# Answer headers values which should be stripped from outgoing email.
|
174
|
+
#
|
175
|
+
def strip_headers
|
176
|
+
%w(return-receipt-to domainkey-signature dkim-signature)
|
177
|
+
end
|
178
|
+
|
179
|
+
def process_message(message, options = {})
|
180
|
+
raise MList::DoubleDeliveryError.new(message) unless message.new_record?
|
181
|
+
|
182
|
+
options = {
|
183
|
+
:search_parent => true,
|
184
|
+
:delivery_time => Time.now,
|
185
|
+
:copy_sender => false
|
186
|
+
}.merge(options)
|
187
|
+
|
188
|
+
transaction do
|
189
|
+
thread = find_thread(message, options)
|
190
|
+
thread.updated_at = options[:delivery_time]
|
191
|
+
|
192
|
+
delivery = prepare_delivery(message, options)
|
193
|
+
thread.messages << message
|
194
|
+
|
195
|
+
self.updated_at = options[:delivery_time]
|
196
|
+
thread.save! && save!
|
197
|
+
|
198
|
+
outgoing_server.deliver(delivery.tmail)
|
199
|
+
end
|
200
|
+
|
201
|
+
message
|
202
|
+
end
|
203
|
+
|
204
|
+
def prepare_delivery(message, options)
|
205
|
+
message.identifier = outgoing_server.generate_message_id
|
206
|
+
message.created_at = options[:delivery_time]
|
207
|
+
message.subject = clean_subject(message.subject)
|
208
|
+
|
209
|
+
recipient_addresses = message.recipient_addresses
|
210
|
+
sender_address = message.subscriber.email_address
|
211
|
+
if options[:copy_sender]
|
212
|
+
recipient_addresses << sender_address unless recipient_addresses.include?(sender_address)
|
213
|
+
else
|
214
|
+
recipient_addresses.delete(sender_address)
|
215
|
+
end
|
216
|
+
|
217
|
+
returning(message.delivery) do |delivery|
|
218
|
+
delivery.date ||= options[:delivery_time]
|
219
|
+
delivery.message_id = message.identifier
|
220
|
+
delivery.mailer = message.mailer
|
221
|
+
delivery.headers = list_headers
|
222
|
+
strip_headers.each {|e| delivery[e] = nil}
|
223
|
+
delivery.subject = list_subject(message.subject)
|
224
|
+
delivery.to = address
|
225
|
+
delivery.cc = []
|
226
|
+
delivery.bcc = recipient_addresses
|
227
|
+
delivery.reply_to ||= reply_to_header(message)
|
228
|
+
prepare_list_footer(delivery, message)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def prepare_list_footer(delivery, message)
|
233
|
+
text_plain_part = delivery.text_plain_part
|
234
|
+
return unless text_plain_part
|
235
|
+
|
236
|
+
content = strip_list_footers(text_plain_part.body)
|
237
|
+
content << "\n\n" unless content.end_with?("\n\n")
|
238
|
+
content << list_footer(message)
|
239
|
+
text_plain_part.body = content
|
240
|
+
end
|
241
|
+
|
242
|
+
def list_footer(message)
|
243
|
+
content = list.footer_content(message)
|
244
|
+
"#{FOOTER_BLOCK_START}\n#{content}\n#{FOOTER_BLOCK_END}"
|
245
|
+
end
|
246
|
+
|
247
|
+
def list_subject(string)
|
248
|
+
list_subject = string.dup
|
249
|
+
if list_subject =~ REGARD_RE
|
250
|
+
"Re: #{subject_prefix} #{remove_regard(list_subject)}"
|
251
|
+
else
|
252
|
+
"#{subject_prefix} #{list_subject}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def find_thread(message, options)
|
257
|
+
message.parent = find_parent_message(message.email) if message.email && options[:search_parent]
|
258
|
+
message.parent ? message.parent.thread : threads.build
|
259
|
+
end
|
260
|
+
|
261
|
+
def reply_to_header(message)
|
262
|
+
if list.reply_to_list?
|
263
|
+
"#{label} #{bracket(address)}"
|
264
|
+
else
|
265
|
+
subscriber_name_and_address(message.subscriber)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def subject_prefix_regex
|
270
|
+
@subject_prefix_regex ||= Regexp.new(Regexp.escape(subject_prefix) + ' ')
|
271
|
+
end
|
272
|
+
|
273
|
+
def subject_prefix
|
274
|
+
@subject_prefix ||= "[#{label}]"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module MList
|
2
|
+
module Manager
|
3
|
+
|
4
|
+
class Database
|
5
|
+
include ::MList::Manager
|
6
|
+
|
7
|
+
def create_list(address, attributes = {})
|
8
|
+
attributes = {
|
9
|
+
:address => address,
|
10
|
+
:label => address.match(/\A(.*?)@/)[1]
|
11
|
+
}.merge(attributes)
|
12
|
+
List.create!(attributes)
|
13
|
+
end
|
14
|
+
|
15
|
+
def lists(email)
|
16
|
+
lists = List.find_all_by_address(email.list_addresses)
|
17
|
+
email.list_addresses.map { |a| lists.detect {|l| l.address == a} }.compact
|
18
|
+
end
|
19
|
+
|
20
|
+
def no_lists_found(email)
|
21
|
+
# TODO: Move to notifier
|
22
|
+
end
|
23
|
+
|
24
|
+
class List < ActiveRecord::Base
|
25
|
+
include ::MList::List
|
26
|
+
|
27
|
+
has_many :subscribers, :dependent => :delete_all
|
28
|
+
|
29
|
+
def label
|
30
|
+
self[:label]
|
31
|
+
end
|
32
|
+
|
33
|
+
def list_id
|
34
|
+
"#{self.class.name}#{id}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def subscribe(address)
|
38
|
+
subscribers.find_or_create_by_email_address(address)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Subscriber < ActiveRecord::Base
|
43
|
+
belongs_to :list
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module MList
|
2
|
+
module Manager
|
3
|
+
|
4
|
+
# Constructs the notices that are sent to list subscribers. Applications
|
5
|
+
# may subclass this to customize the content of a notice delivery.
|
6
|
+
#
|
7
|
+
class Notifier
|
8
|
+
|
9
|
+
# Answers the delivery that will be sent to a subscriber when an
|
10
|
+
# MList::List indicates that the distribution of an email from that
|
11
|
+
# subscriber has been blocked.
|
12
|
+
#
|
13
|
+
def subscriber_blocked(list, email, subscriber)
|
14
|
+
delivery = MList::Util::TMailBuilder.new(TMail::Mail.new)
|
15
|
+
delivery.write_header('x-mlist-loop', 'notice')
|
16
|
+
delivery.write_header('x-mlist-notice', 'subscriber_blocked')
|
17
|
+
delivery.to = subscriber.email_address
|
18
|
+
delivery.from = "mlist-#{list.address}"
|
19
|
+
prepare_subscriber_blocked_content(list, email, subscriber, delivery)
|
20
|
+
delivery
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
def prepare_subscriber_blocked_content(list, email, subscriber, delivery)
|
25
|
+
delivery.set_content_type('text/plain')
|
26
|
+
delivery.body = %{Although you are a subscriber to this list, your message cannot be posted at this time. Please contact the administrator of the list.}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'mlist/manager/notifier'
|
2
|
+
|
3
|
+
module MList
|
4
|
+
|
5
|
+
# The interface of list managers.
|
6
|
+
#
|
7
|
+
# A module is provided instead of a base class to allow implementors to
|
8
|
+
# subclass whatever they like. Practically speaking, they can create an
|
9
|
+
# ActiveRecord subclass.
|
10
|
+
#
|
11
|
+
module Manager
|
12
|
+
|
13
|
+
# Answers an enumeration of MList::List implementations to which the given
|
14
|
+
# email should be published.
|
15
|
+
#
|
16
|
+
def lists(email)
|
17
|
+
raise 'implement in your list manager'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Answers the MList::Manager::Notifier of this list manager. Includers of
|
21
|
+
# this module may initialize the @notifier instance variable with their
|
22
|
+
# own implementation/subclass to generate custom content for the different
|
23
|
+
# notices.
|
24
|
+
#
|
25
|
+
def notifier
|
26
|
+
@notifier ||= MList::Manager::Notifier.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|