mlist 0.1.11 → 0.1.12

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/README CHANGED
@@ -141,7 +141,7 @@ environment.rb after the initialize block (our gem needs to have been loaded):
141
141
  # Please do specify a version, and check for the latest!
142
142
  config.gem "mlist", :version => '0.1.0'
143
143
  end
144
-
144
+
145
145
  MLIST_SERVER = MList::Server.new(
146
146
  :list_manager => MList::Manager::Database.new,
147
147
  :email_server => MList::EmailServer::Default.new(
@@ -185,6 +185,7 @@ list something like this:
185
185
  (The MIT License)
186
186
 
187
187
  Copyright (c) 2008-2009 Adam Williams (aiwilliams)
188
+ Portions from Rails are Copyright (c) 2004-2008 David Heinemeier Hansson
188
189
 
189
190
  Permission is hereby granted, free of charge, to any person obtaining a copy of
190
191
  this software and associated documentation files (the 'Software'), to deal in the
data/VERSION.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- :patch: 11
2
+ :patch: 12
3
3
  :major: 0
4
4
  :build:
5
5
  :minor: 1
@@ -1,7 +1,7 @@
1
1
  module MList
2
2
  class MailList < ActiveRecord::Base
3
3
  set_table_name 'mlist_mail_lists'
4
-
4
+
5
5
  # Provides the MailList for a given implementation of MList::List,
6
6
  # connecting it to the provided email server for delivering posts.
7
7
  #
@@ -17,19 +17,19 @@ module MList
17
17
  mail_list.outgoing_server = outgoing_server
18
18
  mail_list
19
19
  end
20
-
20
+
21
21
  include MList::Util::EmailHelpers
22
-
22
+
23
23
  belongs_to :manager_list, :polymorphic => true
24
-
24
+
25
25
  before_destroy :delete_unreferenced_email
26
26
  has_many :messages, :class_name => 'MList::Message', :dependent => :delete_all
27
27
  has_many :threads, :class_name => 'MList::Thread', :dependent => :delete_all
28
-
28
+
29
29
  delegate :address, :label, :post_url, :to => :list
30
-
30
+
31
31
  attr_accessor :outgoing_server
32
-
32
+
33
33
  # Creates a new MList::Message and delivers it to the subscribers of this
34
34
  # list.
35
35
  #
@@ -45,7 +45,7 @@ module MList
45
45
  :email => MList::Email.new(:source => email.to_s)
46
46
  ), :search_parent => false, :copy_sender => email.copy_sender
47
47
  end
48
-
48
+
49
49
  # Processes the email received by the MList::Server.
50
50
  #
51
51
  def process_email(email, subscriber)
@@ -57,7 +57,7 @@ module MList
57
57
  :email => email
58
58
  ), :copy_sender => list.copy_sender?(subscriber)
59
59
  end
60
-
60
+
61
61
  # Answers the provided subject with superfluous 're:' and this list's
62
62
  # labels removed.
63
63
  #
@@ -72,14 +72,14 @@ module MList
72
72
  without_label
73
73
  end
74
74
  end
75
-
75
+
76
76
  def find_parent_message(email)
77
77
  if in_reply_to = email.header_string('in-reply-to')
78
78
  message = messages.find(:first,
79
79
  :conditions => ['identifier = ?', remove_brackets(in_reply_to)])
80
80
  return message if message
81
81
  end
82
-
82
+
83
83
  if email.references
84
84
  reference_identifiers = email.references.collect {|rid| remove_brackets(rid)}
85
85
  message = messages.find(:first,
@@ -87,7 +87,7 @@ module MList
87
87
  :order => 'created_at desc')
88
88
  return message if message
89
89
  end
90
-
90
+
91
91
  if email.subject =~ REGARD_RE
92
92
  message = messages.find(:first,
93
93
  :conditions => ['subject = ?', remove_regard(clean_subject(email.subject))],
@@ -95,13 +95,13 @@ module MList
95
95
  return message if message
96
96
  end
97
97
  end
98
-
98
+
99
99
  # The MList::List instance of the list manager.
100
100
  #
101
101
  def list
102
102
  @list ||= manager_list
103
103
  end
104
-
104
+
105
105
  def manager_list_with_dual_type=(list)
106
106
  if list.is_a?(ActiveRecord::Base)
107
107
  self.manager_list_without_dual_type = list
@@ -112,27 +112,27 @@ module MList
112
112
  end
113
113
  end
114
114
  alias_method_chain :manager_list=, :dual_type
115
-
115
+
116
116
  # Distinct footer start marker. It is important to realize that changing
117
117
  # this could be problematic.
118
118
  #
119
119
  FOOTER_BLOCK_START = "-~----~~----~----~----~----~---~~-~----~------~--~-~-"
120
-
120
+
121
121
  # Distinct footer end marker. It is important to realize that changing
122
122
  # this could be problematic.
123
123
  #
124
124
  FOOTER_BLOCK_END = "--~--~---~-----~--~----~-----~~~----~---~---~--~----~"
125
-
125
+
126
126
  private
127
127
  FOOTER_BLOCK_START_RE = %r[#{FOOTER_BLOCK_START}]
128
128
  FOOTER_BLOCK_END_RE = %r[#{FOOTER_BLOCK_END}]
129
-
129
+
130
130
  # http://mail.python.org/pipermail/mailman-developers/2006-April/018718.html
131
131
  def bounce_headers
132
132
  # tmail would not correctly quote the label in the sender header, which would break smtp delivery
133
133
  {'sender' => "<mlist-#{address}>", 'errors-to' => "#{label} <mlist-#{address}>"}
134
134
  end
135
-
135
+
136
136
  def delete_unreferenced_email
137
137
  conditions = %Q{
138
138
  mlist_emails.id in (
@@ -144,7 +144,7 @@ module MList
144
144
  )}
145
145
  MList::Email.delete_all(conditions)
146
146
  end
147
-
147
+
148
148
  def strip_list_footers(content)
149
149
  if content =~ FOOTER_BLOCK_START_RE
150
150
  in_footer_block = false
@@ -160,7 +160,7 @@ module MList
160
160
  end
161
161
  content
162
162
  end
163
-
163
+
164
164
  # http://www.jamesshuggins.com/h/web1/list-email-headers.htm
165
165
  def list_headers
166
166
  headers = list.list_headers.dup
@@ -169,43 +169,43 @@ module MList
169
169
  headers.update(bounce_headers)
170
170
  headers.delete_if {|k,v| v.nil?}
171
171
  end
172
-
172
+
173
173
  # Answer headers values which should be stripped from outgoing email.
174
174
  #
175
175
  def strip_headers
176
176
  %w(return-receipt-to domainkey-signature dkim-signature)
177
177
  end
178
-
178
+
179
179
  def process_message(message, options = {})
180
180
  raise MList::DoubleDeliveryError.new(message) unless message.new_record?
181
-
181
+
182
182
  options = {
183
183
  :search_parent => true,
184
184
  :delivery_time => Time.now,
185
185
  :copy_sender => false
186
186
  }.merge(options)
187
-
187
+
188
188
  transaction do
189
189
  thread = find_thread(message, options)
190
190
  thread.updated_at = options[:delivery_time]
191
-
191
+
192
192
  delivery = prepare_delivery(message, options)
193
193
  thread.messages << message
194
-
194
+
195
195
  self.updated_at = options[:delivery_time]
196
196
  thread.save! && save!
197
-
197
+
198
198
  outgoing_server.deliver(delivery.tmail)
199
199
  end
200
-
200
+
201
201
  message
202
202
  end
203
-
203
+
204
204
  def prepare_delivery(message, options)
205
205
  message.identifier = outgoing_server.generate_message_id
206
206
  message.created_at = options[:delivery_time]
207
207
  message.subject = clean_subject(message.subject)
208
-
208
+
209
209
  recipient_addresses = message.recipient_addresses
210
210
  sender_address = message.subscriber.email_address
211
211
  if options[:copy_sender]
@@ -213,7 +213,7 @@ module MList
213
213
  else
214
214
  recipient_addresses.delete(sender_address)
215
215
  end
216
-
216
+
217
217
  returning(message.delivery) do |delivery|
218
218
  delivery.date ||= options[:delivery_time]
219
219
  delivery.message_id = message.identifier
@@ -228,22 +228,40 @@ module MList
228
228
  prepare_list_footer(delivery, message)
229
229
  end
230
230
  end
231
-
231
+
232
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)
233
+ footer = list_footer(message)
234
+ prepare_html_footer(delivery.text_html_part, footer)
235
+ prepare_text_footer(delivery.text_plain_part, footer)
236
+ end
237
+
238
+ def prepare_html_footer(part, footer)
239
+ return unless part
240
+ content = part.body
241
+ content.gsub!(%r(<p>\s*#{FOOTER_BLOCK_START_RE}.*?#{FOOTER_BLOCK_END_RE}\s*<\/p>)im, '')
242
+ content.strip!
243
+ html_footer = "<p>#{auto_link_urls(text_to_html(footer))}</p>"
244
+
245
+ unless content.sub!(/<\/body>/, "#{html_footer}</body>")
246
+ # no body was there, substitution failed
247
+ content << html_footer
248
+ end
249
+ part.body = content
250
+ end
251
+
252
+ def prepare_text_footer(part, footer)
253
+ return unless part
254
+ content = strip_list_footers(part.body)
237
255
  content << "\n\n" unless content.end_with?("\n\n")
238
- content << list_footer(message)
239
- text_plain_part.body = content
256
+ content << footer
257
+ part.body = content
240
258
  end
241
-
259
+
242
260
  def list_footer(message)
243
261
  content = list.footer_content(message)
244
262
  "#{FOOTER_BLOCK_START}\n#{content}\n#{FOOTER_BLOCK_END}"
245
263
  end
246
-
264
+
247
265
  def list_subject(string)
248
266
  list_subject = string.dup
249
267
  if list_subject =~ REGARD_RE
@@ -252,12 +270,12 @@ module MList
252
270
  "#{subject_prefix} #{list_subject}"
253
271
  end
254
272
  end
255
-
273
+
256
274
  def find_thread(message, options)
257
275
  message.parent = find_parent_message(message.email) if message.email && options[:search_parent]
258
276
  message.parent ? message.parent.thread : threads.build
259
277
  end
260
-
278
+
261
279
  def reply_to_header(message)
262
280
  if list.reply_to_list?
263
281
  "#{label} #{bracket(address)}"
@@ -265,13 +283,13 @@ module MList
265
283
  subscriber_name_and_address(message.subscriber)
266
284
  end
267
285
  end
268
-
286
+
269
287
  def subject_prefix_regex
270
288
  @subject_prefix_regex ||= Regexp.new(Regexp.escape(subject_prefix) + ' ')
271
289
  end
272
-
290
+
273
291
  def subject_prefix
274
292
  @subject_prefix ||= "[#{label}]"
275
293
  end
276
294
  end
277
- end
295
+ end
@@ -1,18 +1,18 @@
1
1
  module MList
2
2
  module Util
3
-
3
+
4
4
  class HtmlTextExtraction
5
-
5
+
6
6
  # We need a way to maintain non-breaking spaces. Hpricot will replace
7
7
  # them with ??.chr. We can easily teach it to convert it to a space, but
8
8
  # then we lose the information in the Text node that we need to keep the
9
9
  # space around, since that is what they would see in a view of the HTML.
10
10
  NBSP = '!!!NBSP!!!'
11
-
11
+
12
12
  def initialize(html)
13
13
  @doc = Hpricot(html.gsub('&nbsp;', NBSP))
14
14
  end
15
-
15
+
16
16
  def execute
17
17
  @text, @anchors = '', []
18
18
  @doc.each_child do |node|
@@ -28,7 +28,7 @@ module MList
28
28
  end
29
29
  @text.gsub(NBSP, ' ')
30
30
  end
31
-
31
+
32
32
  def extract_text_from_node(node)
33
33
  case node.name
34
34
  when 'head'
@@ -67,7 +67,7 @@ module MList
67
67
  extract_text_from_children(node)
68
68
  end
69
69
  end
70
-
70
+
71
71
  def extract_text_from_children(elem)
72
72
  elem.each_child do |node|
73
73
  case node
@@ -78,45 +78,81 @@ module MList
78
78
  end
79
79
  end
80
80
  end
81
-
81
+
82
82
  def extract_text_from_text_node(node)
83
83
  text = @text.end_with?("\n") ? node.inner_text.lstrip : node.inner_text
84
84
  @text << text.gsub(/\s{2,}/, ' ').sub(/\n/, '')
85
85
  end
86
86
  end
87
-
87
+
88
88
  module EmailHelpers
89
89
  def sanitize_header(charset, name, *values)
90
90
  header_sanitizer(name).call(charset, *values)
91
91
  end
92
-
92
+
93
93
  def header_sanitizer(name)
94
94
  Util.default_header_sanitizers[name]
95
95
  end
96
-
96
+
97
97
  def html_to_text(html)
98
98
  HtmlTextExtraction.new(html).execute
99
99
  end
100
-
100
+
101
101
  def normalize_new_lines(text)
102
102
  text.to_s.gsub(/\r\n?/, "\n")
103
103
  end
104
-
104
+
105
105
  def subscriber_name_and_address(subscriber)
106
106
  a = subscriber.email_address
107
107
  a = "#{subscriber.display_name} #{bracket(a)}" if subscriber.respond_to?(:display_name)
108
108
  a
109
109
  end
110
-
110
+
111
+ AUTO_LINK_RE = %r{
112
+ ( https?:// | www\. )
113
+ [^\s<]+
114
+ }x unless const_defined?(:AUTO_LINK_RE)
115
+
116
+ BRACKETS = { ']' => '[', ')' => '(', '}' => '{' }
117
+
118
+ # Turns all urls into clickable links. If a block is given, each url
119
+ # is yielded and the result is used as the link text.
120
+ def auto_link_urls(text)
121
+ text.gsub(AUTO_LINK_RE) do
122
+ href = $&
123
+ punctuation = ''
124
+ left, right = $`, $'
125
+ # detect already linked URLs and URLs in the middle of a tag
126
+ if left =~ /<[^>]+$/ && right =~ /^[^>]*>/
127
+ # do not change string; URL is alreay linked
128
+ href
129
+ else
130
+ # don't include trailing punctuation character as part of the URL
131
+ if href.sub!(/[^\w\/-]$/, '') and punctuation = $& and opening = BRACKETS[punctuation]
132
+ if href.scan(opening).size > href.scan(punctuation).size
133
+ href << punctuation
134
+ punctuation = ''
135
+ end
136
+ end
137
+
138
+ link_text = block_given?? yield(href) : href
139
+ href = 'http://' + href unless href.index('http') == 0
140
+
141
+ %Q(<a href="#{href}">#{link_text}</a>)
142
+ end
143
+ end
144
+ end
145
+
146
+
111
147
  BRACKETS_RE = /\A<(.*?)>\Z/
112
148
  def bracket(string)
113
149
  string.blank? || string =~ BRACKETS_RE ? string : "<#{string}>"
114
150
  end
115
-
151
+
116
152
  def remove_brackets(string)
117
153
  string =~ BRACKETS_RE ? $1 : string
118
154
  end
119
-
155
+
120
156
  REGARD_RE = /(^|[^\w])re: /i
121
157
  def remove_regard(string)
122
158
  while string =~ REGARD_RE
@@ -124,7 +160,7 @@ module MList
124
160
  end
125
161
  string.strip
126
162
  end
127
-
163
+
128
164
  def text_to_html(text)
129
165
  lines = normalize_new_lines(text).split("\n")
130
166
  lines.collect! do |line|
@@ -136,7 +172,7 @@ module MList
136
172
  end
137
173
  lines.join("<br />\n")
138
174
  end
139
-
175
+
140
176
  def text_to_quoted(text)
141
177
  lines = normalize_new_lines(text).split("\n")
142
178
  lines.collect! do |line|
@@ -144,12 +180,12 @@ module MList
144
180
  end
145
181
  lines.join("\n")
146
182
  end
147
-
183
+
148
184
  HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
149
185
  def escape_once(text)
150
186
  text.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| HTML_ESCAPE[special] }
151
187
  end
152
188
  end
153
-
189
+
154
190
  end
155
- end
191
+ end
@@ -1,8 +1,6 @@
1
1
  module MList
2
2
  module Util
3
-
4
- # Copyright (c) 2004-2008 David Heinemeier Hansson
5
- #
3
+
6
4
  # Taken from ActionMailer. Modified to make charset first argument in all
7
5
  # signatures, allowing for a consistent pattern of invocation.
8
6
  #
@@ -1,17 +1,17 @@
1
1
  module MList
2
2
  module Util
3
-
3
+
4
4
  module TMailReaders
5
5
  def date
6
6
  if date = tmail.header_string('date')
7
7
  Time.parse(date)
8
8
  end
9
9
  end
10
-
10
+
11
11
  def from_address
12
12
  tmail.from.first.downcase
13
13
  end
14
-
14
+
15
15
  def html
16
16
  case tmail.content_type
17
17
  when 'text/html'
@@ -21,27 +21,39 @@ module MList
21
21
  html_part.body.strip if html_part
22
22
  end
23
23
  end
24
-
24
+
25
25
  def identifier
26
26
  remove_brackets(tmail.header_string('message-id'))
27
27
  end
28
-
28
+
29
29
  def mailer
30
30
  tmail.header_string('x-mailer')
31
31
  end
32
-
32
+
33
33
  def text
34
34
  text_content = ''
35
35
  extract_text_content(tmail, text_content)
36
36
  return text_content unless text_content.blank?
37
-
37
+
38
38
  html_content = ''
39
39
  extract_html_content(tmail, html_content)
40
40
  return html_to_text(html_content) unless html_content.blank?
41
-
41
+
42
42
  return nil
43
43
  end
44
-
44
+
45
+ # Answers the first text/html part it can find, the tmail itself if
46
+ # it's content type is text/html.
47
+ #
48
+ def text_html_part(part = tmail)
49
+ case part.content_type
50
+ when 'text/html'
51
+ part
52
+ when 'multipart/alternative'
53
+ part.parts.detect {|part| text_html_part(part)}
54
+ end
55
+ end
56
+
45
57
  # Answers the first text/plain part it can find, the tmail itself if
46
58
  # it's content type is text/plain.
47
59
  #
@@ -53,7 +65,7 @@ module MList
53
65
  part.parts.detect {|part| text_plain_part(part)}
54
66
  end
55
67
  end
56
-
68
+
57
69
  private
58
70
  def extract_html_content(part, collector)
59
71
  case part.content_type
@@ -63,7 +75,7 @@ module MList
63
75
  part.parts.each {|part| extract_html_content(part, collector)}
64
76
  end
65
77
  end
66
-
78
+
67
79
  def extract_text_content(part, collector)
68
80
  case part.content_type
69
81
  when 'text/plain'
@@ -73,16 +85,16 @@ module MList
73
85
  end
74
86
  end
75
87
  end
76
-
88
+
77
89
  module TMailWriters
78
90
  def charset
79
91
  'utf-8'
80
92
  end
81
-
93
+
82
94
  def delete_header(name)
83
95
  tmail[name] = nil
84
96
  end
85
-
97
+
86
98
  def headers=(updates)
87
99
  updates.each do |k,v|
88
100
  if TMail::Mail::ALLOW_MULTIPLE.include?(k.downcase)
@@ -92,7 +104,7 @@ module MList
92
104
  end
93
105
  end
94
106
  end
95
-
107
+
96
108
  # Add another value for the named header, it's position being earlier in
97
109
  # the email than those that are already present. This will raise an error
98
110
  # if the header does not allow multiple values according to
@@ -104,35 +116,35 @@ module MList
104
116
  tmail[name] = sanitize_header(charset, name, value)
105
117
  tmail[name] = tmail[name] + original
106
118
  end
107
-
119
+
108
120
  def write_header(name, value)
109
121
  tmail[name] = sanitize_header(charset, name, value)
110
122
  end
111
-
123
+
112
124
  def to=(recipient_addresses)
113
125
  tmail.to = sanitize_header(charset, 'to', recipient_addresses)
114
126
  end
115
-
127
+
116
128
  def bcc=(recipient_addresses)
117
129
  tmail.bcc = sanitize_header(charset, 'bcc', recipient_addresses)
118
130
  end
119
-
131
+
120
132
  def from=(from_address)
121
133
  tmail.from = sanitize_header(charset, 'from', from_address)
122
134
  end
123
-
135
+
124
136
  def in_reply_to=(*values)
125
137
  tmail.in_reply_to = sanitize_header(charset, 'in-reply-to', *values)
126
138
  end
127
-
139
+
128
140
  def mailer=(value)
129
141
  write_header('x-mailer', value)
130
142
  end
131
-
143
+
132
144
  def message_id=(value)
133
145
  tmail.message_id = sanitize_header(charset, 'message-id', value)
134
146
  end
135
147
  end
136
-
148
+
137
149
  end
138
- end
150
+ end
@@ -4,38 +4,38 @@ describe MList::MailList do
4
4
  class ManagerList
5
5
  include MList::List
6
6
  end
7
-
7
+
8
8
  before do
9
9
  @manager_list = ManagerList.new
10
10
  stub(@manager_list).label {'Discussions'}
11
11
  stub(@manager_list).address {'list_one@example.com'}
12
12
  stub(@manager_list).list_id {'list_one@example.com'}
13
-
13
+
14
14
  @subscriber_one = MList::EmailSubscriber.new('adam@nomail.net')
15
15
  @subscriber_two = MList::EmailSubscriber.new('john@example.com')
16
16
  stub(@manager_list).subscribers {[@subscriber_one, @subscriber_two]}
17
-
17
+
18
18
  @outgoing_server = MList::EmailServer::Fake.new
19
19
  @mail_list = MList::MailList.create!(
20
20
  :manager_list => @manager_list,
21
21
  :outgoing_server => @outgoing_server)
22
22
  end
23
-
23
+
24
24
  it 'should not require the manager list be an ActiveRecord type' do
25
25
  @mail_list.list.should == @manager_list
26
26
  @mail_list.manager_list.should be_nil
27
27
  end
28
-
28
+
29
29
  it 'should have messages counted' do
30
30
  MList::Message.reflect_on_association(:mail_list).counter_cache_column.should == :messages_count
31
31
  MList::MailList.column_names.should include('messages_count')
32
32
  end
33
-
33
+
34
34
  it 'should have threads counted' do
35
35
  MList::Thread.reflect_on_association(:mail_list).counter_cache_column.should == :threads_count
36
36
  MList::MailList.column_names.should include('threads_count')
37
37
  end
38
-
38
+
39
39
  it 'should delete email not referenced by other lists' do
40
40
  email_in_other = MList::Email.create!(:tmail => tmail_fixture('single_list'))
41
41
  email_not_other = MList::Email.create!(:tmail => tmail_fixture('single_list'))
@@ -48,7 +48,7 @@ describe MList::MailList do
48
48
  MList::Email.exists?(email_in_other).should be_true
49
49
  MList::Email.exists?(email_not_other).should be_false
50
50
  end
51
-
51
+
52
52
  describe 'post' do
53
53
  it 'should allow posting a new message to the list' do
54
54
  lambda do
@@ -60,12 +60,12 @@ describe MList::MailList do
60
60
  )
61
61
  end.should change(MList::Message, :count).by(1)
62
62
  end.should change(MList::Thread, :count).by(1)
63
-
63
+
64
64
  tmail = @outgoing_server.deliveries.last
65
65
  tmail.subject.should =~ /I'm a Program!/
66
66
  tmail.from.should == ['adam@nomail.net']
67
67
  end
68
-
68
+
69
69
  it 'should answer the message for use by the application' do
70
70
  @mail_list.post(
71
71
  :subscriber => @subscriber_one,
@@ -73,7 +73,7 @@ describe MList::MailList do
73
73
  :text => 'Are you a programmer or what?'
74
74
  ).should be_instance_of(MList::Message)
75
75
  end
76
-
76
+
77
77
  it 'should allow posting a reply to an existing message' do
78
78
  @mail_list.process_email(MList::Email.new(:tmail => tmail_fixture('single_list')), @subscriber_one)
79
79
  existing_message = @mail_list.messages.last
@@ -89,7 +89,7 @@ describe MList::MailList do
89
89
  new_message = MList::Message.last
90
90
  new_message.subject.should == "Re: Test"
91
91
  end
92
-
92
+
93
93
  it 'should not associate a posting to a parent if not reply' do
94
94
  @mail_list.process_email(MList::Email.new(:tmail => tmail_fixture('single_list')), @subscriber_one)
95
95
  lambda do
@@ -105,7 +105,7 @@ describe MList::MailList do
105
105
  message.parent.should be_nil
106
106
  message.parent_identifier.should be_nil
107
107
  end
108
-
108
+
109
109
  it 'should capture the message-id of delivered email' do
110
110
  message = @mail_list.post(
111
111
  :subscriber => @subscriber_one,
@@ -113,91 +113,91 @@ describe MList::MailList do
113
113
  :text => 'Email must have a message id for threading')
114
114
  message.reload.identifier.should_not be_nil
115
115
  end
116
-
116
+
117
117
  it 'should copy the subscriber if desired' do
118
118
  @mail_list.post(
119
119
  :subscriber => @subscriber_one,
120
120
  :subject => 'Copy Me',
121
121
  :text => 'Email should be sent to subscriber if desired',
122
122
  :copy_sender => true)
123
-
123
+
124
124
  tmail = @outgoing_server.deliveries.last
125
125
  tmail.bcc.should include(@subscriber_one.email_address)
126
126
  end
127
-
127
+
128
128
  it 'should not copy the subscriber if undesired and list includes the subscriber' do
129
129
  # The MList::List implementor may include the sending subscriber
130
130
  stub(@manager_list).recipients {[@subscriber_one, @subscriber_two]}
131
-
131
+
132
132
  @mail_list.post(
133
133
  :subscriber => @subscriber_one,
134
134
  :subject => 'Do Not Copy Me',
135
135
  :text => 'Email should not be sent to subscriber if undesired',
136
136
  :copy_sender => false)
137
-
137
+
138
138
  tmail = @outgoing_server.deliveries.last
139
139
  tmail.bcc.should_not include(@subscriber_one.email_address)
140
140
  end
141
141
  end
142
-
142
+
143
143
  describe 'message storage' do
144
144
  def process_post
145
145
  @mail_list.process_email(MList::Email.new(:tmail => @post_tmail), @subscriber)
146
146
  MList::Message.last
147
147
  end
148
-
148
+
149
149
  before do
150
150
  @post_tmail = tmail_fixture('single_list')
151
151
  @subscriber = @subscriber_one
152
152
  end
153
-
153
+
154
154
  it 'should not include list label in subject' do
155
155
  @post_tmail.subject = '[Discussions] Test'
156
156
  process_post.subject.should == 'Test'
157
157
  end
158
-
158
+
159
159
  it 'should not include list label in reply subject' do
160
160
  @post_tmail.subject = 'Re: [Discussions] Test'
161
161
  process_post.subject.should == 'Re: Test'
162
162
  end
163
-
163
+
164
164
  it 'should not bother labels it does not understand in subject' do
165
165
  @post_tmail.subject = '[Ann] Test'
166
166
  process_post.subject.should == '[Ann] Test'
167
167
  end
168
-
168
+
169
169
  it 'should not bother labels it does not understand in reply subject' do
170
170
  @post_tmail.subject = 'Re: [Ann] Test'
171
171
  process_post.subject.should == 'Re: [Ann] Test'
172
172
  end
173
-
173
+
174
174
  it 'should be careful of multiple re:' do
175
175
  @post_tmail.subject = 'Re: [Ann] RE: Test'
176
176
  process_post.subject.should == 'Re: [Ann] Test'
177
177
  end
178
178
  end
179
-
179
+
180
180
  describe 'finding parent message' do
181
181
  def email(path)
182
182
  MList::Email.new(:tmail => tmail_fixture(path))
183
183
  end
184
-
184
+
185
185
  before do
186
186
  @parent_message = MList::Message.new
187
187
  end
188
-
188
+
189
189
  it 'should be nil if none found' do
190
190
  do_not_call(@mail_list.messages).find
191
191
  @mail_list.find_parent_message(email('single_list')).should be_nil
192
192
  end
193
-
193
+
194
194
  it 'should use in-reply-to field when present' do
195
195
  mock(@mail_list.messages).find(:first, :conditions => [
196
196
  'identifier = ?', 'F5F9DC55-CB54-4F2C-9B46-A05F241BCF22@recursivecreative.com'
197
197
  ]) { @parent_message }
198
198
  @mail_list.find_parent_message(email('single_list_reply')).should == @parent_message
199
199
  end
200
-
200
+
201
201
  it 'should be references field if present and no in-reply-to' do
202
202
  tmail = tmail_fixture('single_list_reply')
203
203
  tmail['in-reply-to'] = nil
@@ -206,7 +206,7 @@ describe MList::MailList do
206
206
  :order => 'created_at desc') { @parent_message }
207
207
  @mail_list.find_parent_message(MList::Email.new(:tmail => tmail)).should == @parent_message
208
208
  end
209
-
209
+
210
210
  describe 'by subject' do
211
211
  def search_subject(subject = nil)
212
212
  simple_matcher("search by the subject '#{subject}'") do |email|
@@ -223,7 +223,7 @@ describe MList::MailList do
223
223
  !subject.nil?
224
224
  end
225
225
  end
226
-
226
+
227
227
  before do
228
228
  @parent_message = MList::Message.new
229
229
  @mail_list = MList::MailList.new
@@ -231,16 +231,16 @@ describe MList::MailList do
231
231
  @reply_tmail = tmail_fixture('single_list')
232
232
  @reply_email = MList::Email.new(:tmail => @reply_tmail)
233
233
  end
234
-
234
+
235
235
  it 'should be employed if it has "re:" in it' do
236
236
  @reply_tmail.subject = "Re: Test"
237
237
  @reply_email.should search_subject('Test')
238
238
  end
239
-
239
+
240
240
  it 'should not be employed when no "re:"' do
241
241
  @reply_email.should_not search_subject
242
242
  end
243
-
243
+
244
244
  ['RE: [list name] Re: Test', 'Re: [list name] Re: [list name] Test', '[list name] Re: Test'].each do |subject|
245
245
  it "should handle '#{subject}'" do
246
246
  @reply_tmail.subject = subject
@@ -249,20 +249,20 @@ describe MList::MailList do
249
249
  end
250
250
  end
251
251
  end
252
-
252
+
253
253
  describe 'delivery' do
254
254
  include MList::Util::EmailHelpers
255
-
255
+
256
256
  def process_post
257
257
  @mail_list.process_email(MList::Email.new(:source => @post_tmail.to_s), @subscriber)
258
258
  @outgoing_server.deliveries.last
259
259
  end
260
-
260
+
261
261
  before do
262
262
  @post_tmail = tmail_fixture('single_list')
263
263
  @subscriber = @subscriber_one
264
264
  end
265
-
265
+
266
266
  it 'should be blind copied to recipients' do
267
267
  mock.proxy(@mail_list.messages).build(anything) do |message|
268
268
  mock(message.delivery).bcc=(%w(john@example.com))
@@ -270,7 +270,7 @@ describe MList::MailList do
270
270
  end
271
271
  process_post
272
272
  end
273
-
273
+
274
274
  it 'should not deliver to addresses found in the to header' do
275
275
  @post_tmail.to = ['john@example.com', 'list_one@example.com']
276
276
  mock.proxy(@mail_list.messages).build(anything) do |message|
@@ -279,7 +279,7 @@ describe MList::MailList do
279
279
  end
280
280
  process_post
281
281
  end
282
-
282
+
283
283
  it 'should not deliver to addresses found in the cc header' do
284
284
  @post_tmail.cc = ['john@example.com']
285
285
  mock.proxy(@mail_list.messages).build(anything) do |message|
@@ -288,88 +288,88 @@ describe MList::MailList do
288
288
  end
289
289
  process_post
290
290
  end
291
-
291
+
292
292
  it 'should use list address as reply-to by default' do
293
293
  process_post.should have_header('reply-to', 'Discussions <list_one@example.com>')
294
294
  end
295
-
295
+
296
296
  it 'should use subscriber address as reply-to if list says to not use address' do
297
297
  mock(@manager_list).reply_to_list? { false }
298
298
  process_post.should have_header('reply-to', 'adam@nomail.net')
299
299
  end
300
-
300
+
301
301
  it 'should use the reply-to already in an email - should not override it' do
302
302
  @post_tmail['reply-to'] = 'theotheradam@nomail.net'
303
303
  process_post.should have_header('reply-to', 'theotheradam@nomail.net')
304
304
  end
305
-
305
+
306
306
  it 'should set x-beenthere on emails it delivers to keep from re-posting them' do
307
307
  process_post.should have_header('x-beenthere', 'list_one@example.com')
308
308
  end
309
-
309
+
310
310
  it 'should not remove any existing x-beenthere headers' do
311
311
  @post_tmail['x-beenthere'] = 'somewhere@nomail.net'
312
312
  process_post.should have_header('x-beenthere', %w(list_one@example.com somewhere@nomail.net))
313
313
  end
314
-
314
+
315
315
  it 'should not modify existing headers' do
316
316
  @post_tmail['x-something-custom'] = 'existing'
317
317
  process_post.should have_header('x-something-custom', 'existing')
318
318
  end
319
-
319
+
320
320
  it 'should delete Return-Receipt-To headers since they cause clients to spam the list (the sender address)' do
321
321
  @post_tmail['return-receipt-to'] = 'somewhere@nomail.net'
322
322
  process_post.should_not have_header('return-receipt-to')
323
323
  end
324
-
324
+
325
325
  it 'should not have any cc addresses' do
326
326
  @post_tmail['cc'] = 'billybob@anywhere.com'
327
327
  process_post.should_not have_header('cc')
328
328
  end
329
-
329
+
330
330
  it 'should prefix the list label to the subject of messages' do
331
331
  process_post.subject.should == '[Discussions] Test'
332
332
  end
333
-
333
+
334
334
  it 'should move the list label to the front of subjects that already include the label' do
335
335
  @post_tmail.subject = 'Re: [Discussions] Test'
336
336
  process_post.subject.should == 'Re: [Discussions] Test'
337
337
  end
338
-
338
+
339
339
  it 'should remove multiple occurrences of Re:' do
340
340
  @post_tmail.subject = 'Re: [Discussions] Re: Test'
341
341
  process_post.subject.should == 'Re: [Discussions] Test'
342
342
  end
343
-
343
+
344
344
  it 'should remove DomainKey-Signature headers so that we can sign the redistribution' do
345
345
  @post_tmail['DomainKey-Signature'] = "a whole bunch of junk"
346
346
  process_post.should_not have_header('domainkey-signature')
347
347
  end
348
-
348
+
349
349
  it 'should remove DKIM-Signature headers so that we can sign the redistribution' do
350
350
  @post_tmail['DKIM-Signature'] = "a whole bunch of junk"
351
351
  process_post.should_not have_header('dkim-signature')
352
352
  end
353
-
353
+
354
354
  it 'should capture the new message-ids' do
355
355
  delivered = process_post
356
356
  delivered.header_string('message-id').should_not be_blank
357
357
  MList::Message.last.identifier.should == remove_brackets(delivered.header_string('message-id'))
358
358
  delivered.header_string('message-id').should_not match(/F5F9DC55-CB54-4F2C-9B46-A05F241BCF22@recursivecreative\.com/)
359
359
  end
360
-
360
+
361
361
  it 'should maintain the content-id part headers (inline images, etc)' do
362
362
  @post_tmail = tmail_fixture('embedded_content')
363
363
  process_post.parts[1].parts[1]['content-id'].to_s.should == "<CF68EC17-F8ED-478A-A4A1-AEBF165A8830/bg_pattern.jpg>"
364
364
  end
365
-
365
+
366
366
  it 'should add standard list headers when they are available' do
367
367
  stub(@manager_list).help_url {'http://list_manager.example.com/help'}
368
368
  stub(@manager_list).subscribe_url {'http://list_manager.example.com/subscribe'}
369
369
  stub(@manager_list).unsubscribe_url {'http://list_manager.example.com/unsubscribe'}
370
370
  stub(@manager_list).owner_url {"<mailto:list_manager@example.com>\n(Jimmy Fish)"}
371
371
  stub(@manager_list).archive_url {'http://list_manager.example.com/archive'}
372
-
372
+
373
373
  tmail = process_post
374
374
  tmail.should have_headers(
375
375
  'list-id' => "<list_one@example.com>",
@@ -385,33 +385,39 @@ describe MList::MailList do
385
385
  )
386
386
  tmail.header_string('x-mlist-version').should =~ /\d+\.\d+\.\d+/
387
387
  end
388
-
388
+
389
389
  it 'should not add list headers that are not available or nil' do
390
390
  stub(@manager_list).help_url {nil}
391
391
  delivery = process_post
392
392
  delivery.should_not have_header('list-help')
393
393
  delivery.should_not have_header('list-subscribe')
394
394
  end
395
-
395
+
396
396
  it 'should append the list footer to text/plain emails' do
397
397
  @post_tmail.body = "My Email\n\n\n\n\n"
398
398
  mock(@manager_list).footer_content(is_a(MList::Message)) { 'my footer' }
399
399
  process_post.body.should == "My Email\n\n\n\n\n#{MList::MailList::FOOTER_BLOCK_START}\nmy footer\n#{MList::MailList::FOOTER_BLOCK_END}"
400
400
  end
401
-
401
+
402
402
  it 'should append the list footer to multipart/alternative, text/plain part of emails' do
403
403
  @post_tmail = tmail_fixture('content_types/multipart_alternative_simple')
404
404
  mock(@manager_list).footer_content(is_a(MList::Message)) { 'my footer' }
405
405
  process_post.parts[0].body.should match(/#{MList::MailList::FOOTER_BLOCK_START}\nmy footer\n#{MList::MailList::FOOTER_BLOCK_END}/)
406
406
  end
407
-
407
+
408
+ it 'should append the list footer to multipart/alternative, text/html part of emails' do
409
+ @post_tmail = tmail_fixture('content_types/multipart_alternative_simple')
410
+ mock(@manager_list).footer_content(is_a(MList::Message)) { "my footer\nis here\nhttp://links/here" }
411
+ process_post.parts[1].body.should match(/<p>#{MList::MailList::FOOTER_BLOCK_START}<br \/>\nmy footer<br \/>\nis here<br \/>\n<a href="http:\/\/links\/here">http:\/\/links\/here<\/a><br \/>\n#{MList::MailList::FOOTER_BLOCK_END}<\/p>/)
412
+ end
413
+
408
414
  it 'should handle whitespace well when appending footer' do
409
415
  @post_tmail.body = "My Email"
410
416
  mock(@manager_list).footer_content(is_a(MList::Message)) { 'my footer' }
411
417
  process_post.body.should == "My Email\n\n#{MList::MailList::FOOTER_BLOCK_START}\nmy footer\n#{MList::MailList::FOOTER_BLOCK_END}"
412
418
  end
413
-
414
- it 'should strip out any existing footers from the list' do
419
+
420
+ it 'should strip out any existing text footers from the list' do
415
421
  mock(@manager_list).footer_content(is_a(MList::Message)) { 'my footer' }
416
422
  @post_tmail.body = %{My Email
417
423
 
@@ -429,32 +435,53 @@ this is without any in front
429
435
  }
430
436
  process_post.body.should == "My Email\n\n#{MList::MailList::FOOTER_BLOCK_START}\nmy footer\n#{MList::MailList::FOOTER_BLOCK_END}"
431
437
  end
432
-
438
+
439
+ it 'should strip out any existing html footers from the list' do
440
+ @post_tmail = tmail_fixture('content_types/multipart_alternative_simple')
441
+ mock(@manager_list).footer_content(is_a(MList::Message)) { 'my footer' }
442
+ @post_tmail.parts[1].body = %{<p>My Email</p>
443
+ <blockquote>
444
+ <p> Stuff in my email</p><p> #{MList::MailList::FOOTER_BLOCK_START}
445
+ > > content at front shouldn't matter
446
+ > > #{MList::MailList::FOOTER_BLOCK_END} </p>
447
+
448
+ >> not in our p!<p>#{MList::MailList::FOOTER_BLOCK_START}
449
+ >> this is fine to be removed
450
+ >> #{MList::MailList::FOOTER_BLOCK_END}</p>
451
+
452
+ <p>#{MList::MailList::FOOTER_BLOCK_START}
453
+ this is without any in front
454
+ #{MList::MailList::FOOTER_BLOCK_END}
455
+ </p>
456
+ }
457
+ process_post.parts[1].body.should == "<p>My Email</p>\n<blockquote>\n <p> Stuff in my email</p>\n\n>> not in our p!<p>#{MList::MailList::FOOTER_BLOCK_START}<br />\nmy footer<br />\n#{MList::MailList::FOOTER_BLOCK_END}</p>"
458
+ end
459
+
433
460
  describe 'time' do
434
461
  include TMail::TextUtils
435
-
462
+
436
463
  before do
437
464
  @old_zone_default = Time.zone_default
438
465
  @system_time = Time.parse('Thu, 2 Apr 2009 15:22:04')
439
466
  mock(Time).now.times(any_times) { @system_time }
440
467
  end
441
-
468
+
442
469
  after do
443
470
  Time.zone_default = @old_zone_default
444
471
  end
445
-
472
+
446
473
  it 'should keep date of email post' do
447
474
  @post_tmail['date'] = 'Thu, 2 Apr 2009 15:22:04 -0400'
448
475
  process_post.header_string('date').should == 'Thu, 2 Apr 2009 15:22:04 -0400'
449
476
  end
450
-
477
+
451
478
  it 'should store the delivery time as created_at of message record' do
452
479
  Time.zone_default = 'Pacific Time (US & Canada)'
453
480
  @post_tmail['date'] = 'Wed, 1 Apr 2009 15:22:04 -0400'
454
481
  process_post.header_string('date').should == 'Wed, 1 Apr 2009 15:22:04 -0400'
455
482
  MList::Message.last.created_at.should == @system_time
456
483
  end
457
-
484
+
458
485
  # I think that what TMail is doing is evil, but it's reference to
459
486
  # a ruby-talk discussion leads to Japanese, which I cannot read.
460
487
  # I'd prefer that it leave the problem of timezones up to the client,
@@ -466,4 +493,4 @@ this is without any in front
466
493
  end
467
494
  end
468
495
  end
469
- end
496
+ end
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mlist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 12
9
+ version: 0.1.12
5
10
  platform: ruby
6
11
  authors:
7
12
  - Adam Williams
@@ -9,7 +14,7 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2010-01-11 00:00:00 -05:00
17
+ date: 2010-03-26 00:00:00 -04:00
13
18
  default_executable:
14
19
  dependencies: []
15
20
 
@@ -67,18 +72,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
72
  requirements:
68
73
  - - ">="
69
74
  - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
70
77
  version: "0"
71
- version:
72
78
  required_rubygems_version: !ruby/object:Gem::Requirement
73
79
  requirements:
74
80
  - - ">="
75
81
  - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
76
84
  version: "0"
77
- version:
78
85
  requirements: []
79
86
 
80
87
  rubyforge_project:
81
- rubygems_version: 1.3.5
88
+ rubygems_version: 1.3.6
82
89
  signing_key:
83
90
  specification_version: 3
84
91
  summary: A Ruby mailing list library designed to be integrated into other applications.