mlist 0.1.11 → 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
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.