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/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
|