aiwilliams-mlist 0.1.6 → 0.1.7

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 CHANGED
@@ -1,3 +1,14 @@
1
+ *0.1.7 [Bug Fixes, Delivery Improvements] (2009-08-14)
2
+
3
+ *   is maintained as spacing in html_to_text conversions [aiwilliams]
4
+ * Fixed bug where delivered email included the original Cc header. This could cause all kinds of problems. [aiwilliams]
5
+ * Fixed bug where list addresses in Cc header were not be utilized in determining the lists to deliver to. [aiwilliams]
6
+ * Fixed bug where delivery date was left to the mercy of the Rails time configuration. Using Time.now. [aiwilliams]
7
+ * Manager list can indicate if reply-to should be list address or subscriber. [aiwilliams]
8
+ * Leaving the reply-to field intact when it already exists on incoming email. [aiwilliams]
9
+ * Messages will not be delivered to addresses found in the TO and CC fields in order to keep those recipients from getting two emails. [aiwilliams]
10
+ * Added ability to optionally copy sender. [aiwilliams]
11
+
1
12
  *0.1.6 [Bug Fixes] (March 5, 2009)
2
13
 
3
14
  * Messages are processed even when there are no recipients [aiwilliams]
@@ -1,4 +1,4 @@
1
1
  ---
2
- :minor: 1
3
- :patch: 6
2
+ :patch: 7
4
3
  :major: 0
4
+ :minor: 1
@@ -10,6 +10,14 @@ module MList
10
10
  tmail.header_string('x-beenthere') == list.address
11
11
  end
12
12
 
13
+ def date
14
+ if date_from_email = super
15
+ return date_from_email
16
+ else
17
+ self.created_at ||= Time.now
18
+ end
19
+ end
20
+
13
21
  def from
14
22
  tmail.header_string('from')
15
23
  end
@@ -17,7 +25,7 @@ module MList
17
25
  # Answers the usable destination addresses of the email.
18
26
  #
19
27
  def list_addresses
20
- bounce? ? tmail.header_string('to').match(/\Amlist-(.*)\Z/)[1] : tmail.to.collect(&:downcase)
28
+ bounce? ? tmail.header_string('to').match(/\Amlist-(.*)\Z/)[1] : recipient_addresses
21
29
  end
22
30
 
23
31
  # Answers true if this email is a bounce.
@@ -48,6 +56,12 @@ module MList
48
56
  end
49
57
  end
50
58
 
59
+ # Answers the set of addresses found in the TO and CC fields of the email.
60
+ #
61
+ def recipient_addresses
62
+ (Array(tmail.to) + Array(tmail.cc)).collect(&:downcase).uniq
63
+ end
64
+
51
65
  def respond_to?(method)
52
66
  super || (method.to_s !~ /=\Z/ && tmail.respond_to?(method))
53
67
  end
@@ -12,7 +12,7 @@ module MList
12
12
  class EmailPost
13
13
  include MList::Util::EmailHelpers
14
14
 
15
- ATTRIBUTE_NAMES = %w(html text mailer subject subscriber)
15
+ ATTRIBUTE_NAMES = %w(copy_sender html text mailer subject subscriber)
16
16
  ATTRIBUTE_NAMES.each do |attribute_name|
17
17
  define_method(attribute_name) do
18
18
  @attributes[attribute_name]
@@ -44,6 +44,10 @@ module MList
44
44
  end
45
45
  end
46
46
 
47
+ def copy_sender=(value)
48
+ @attributes['copy_sender'] = %w(true 1).include?(value.to_s)
49
+ end
50
+
47
51
  def reply_to_message=(message)
48
52
  if message
49
53
  @parent_identifier = message.identifier
@@ -74,10 +78,7 @@ module MList
74
78
  builder.references = [bracket(parent_identifier)]
75
79
  end
76
80
 
77
- from = subscriber.email_address
78
- from = "#{subscriber.display_name} #{bracket(from)}" if subscriber.respond_to?(:display_name)
79
- builder.from = from
80
-
81
+ builder.from = subscriber_name_and_address(subscriber)
81
82
  builder.subject = subject
82
83
 
83
84
  if html
@@ -94,6 +95,14 @@ module MList
94
95
 
95
96
  # vvv ActiveRecord validations interface implementation vvv
96
97
 
98
+ def self.human_name(options = {})
99
+ self.name.humanize
100
+ end
101
+
102
+ def self.self_and_descendants_from_active_record #nodoc:
103
+ [self]
104
+ end
105
+
97
106
  def self.human_attribute_name(attribute_key_name, options = {})
98
107
  attribute_key_name.humanize
99
108
  end
@@ -25,10 +25,10 @@ module MList
25
25
  end
26
26
 
27
27
  # Answers the footer content for this list. Default implementation is very
28
- # simple right now. Expect improvements in the future.
28
+ # simple.
29
29
  #
30
30
  def footer_content(message)
31
- %Q{The "#{label}" mailing list\nTo post messages, send email to #{post_url}}
31
+ %Q{The "#{label}" mailing list\nPost messages: #{post_url}}
32
32
  end
33
33
 
34
34
  # Answer a suitable label for the list, which will be used in various
@@ -67,6 +67,16 @@ module MList
67
67
  nil
68
68
  end
69
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
+
70
80
  # The web address of the list help site, nil if this is not supported.
71
81
  #
72
82
  def help_url
@@ -86,6 +96,13 @@ module MList
86
96
  address
87
97
  end
88
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
+
89
106
  # The web url where subscriptions to this list may be created, nil if this
90
107
  # is not supported.
91
108
  #
@@ -100,16 +117,15 @@ module MList
100
117
  nil
101
118
  end
102
119
 
103
- # A list is responsible for answering the recipient subscribers.
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.
104
123
  #
105
- # The subscriber of the incoming message is provided if the list would
106
- # like to exclude it from the returned list. It is not assumed that it
107
- # will be included or excluded, thereby allowing the list to decide. This
108
- # default implementation does not include the sending subscriber in the
109
- # list of recipients.
124
+ # The sending subscriber is provided if the list would like to utilize it
125
+ # in calculating the recipients.
110
126
  #
111
127
  def recipients(subscriber)
112
- subscribers.reject {|s| s.email_address == subscriber.email_address}
128
+ subscribers
113
129
  end
114
130
 
115
131
  # A list must answer the subscriber who's email address is that of the one
@@ -43,7 +43,7 @@ module MList
43
43
  :subscriber => email.subscriber,
44
44
  :recipients => list.recipients(email.subscriber),
45
45
  :email => MList::Email.new(:source => email.to_s)
46
- ), :search_parent => false
46
+ ), :search_parent => false, :copy_sender => email.copy_sender
47
47
  end
48
48
 
49
49
  # Processes the email received by the MList::Server.
@@ -55,7 +55,7 @@ module MList
55
55
  :subscriber => subscriber,
56
56
  :recipients => recipients,
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
@@ -175,7 +175,8 @@ module MList
175
175
 
176
176
  options = {
177
177
  :search_parent => true,
178
- :delivery_time => Time.now
178
+ :delivery_time => Time.now,
179
+ :copy_sender => false
179
180
  }.merge(options)
180
181
 
181
182
  transaction do
@@ -198,15 +199,25 @@ module MList
198
199
  message.identifier = outgoing_server.generate_message_id
199
200
  message.created_at = options[:delivery_time]
200
201
  message.subject = clean_subject(message.subject)
202
+
203
+ recipient_addresses = message.recipient_addresses
204
+ sender_address = message.subscriber.email_address
205
+ if options[:copy_sender]
206
+ recipient_addresses << sender_address unless recipient_addresses.include?(sender_address)
207
+ else
208
+ recipient_addresses.delete(sender_address)
209
+ end
210
+
201
211
  returning(message.delivery) do |delivery|
202
- delivery.date = message.created_at
212
+ delivery.date ||= options[:delivery_time]
203
213
  delivery.message_id = message.identifier
204
214
  delivery.mailer = message.mailer
205
215
  delivery.headers = list_headers
206
216
  delivery.subject = list_subject(message.subject)
207
217
  delivery.to = address
208
- delivery.bcc = message.recipients.collect(&:email_address)
209
- delivery.reply_to = "#{label} <#{post_url}>"
218
+ delivery.cc = []
219
+ delivery.bcc = recipient_addresses
220
+ delivery.reply_to ||= reply_to_header(message)
210
221
  prepare_list_footer(delivery, message)
211
222
  end
212
223
  end
@@ -240,6 +251,14 @@ module MList
240
251
  message.parent ? message.parent.thread : threads.build
241
252
  end
242
253
 
254
+ def reply_to_header(message)
255
+ if list.reply_to_list?
256
+ "#{label} #{bracket(address)}"
257
+ else
258
+ subscriber_name_and_address(message.subscriber)
259
+ end
260
+ end
261
+
243
262
  def subject_prefix_regex
244
263
  @subject_prefix_regex ||= Regexp.new(Regexp.escape(subject_prefix) + ' ')
245
264
  end
@@ -86,6 +86,16 @@ module MList
86
86
  end
87
87
  alias_method_chain :parent=, :identifier_capture
88
88
 
89
+ # Answers the recipient email addresses from the MList::List recipient
90
+ # subscribers, except those that are in the email TO or CC fields as
91
+ # placed there by the sending MUA. It is assumed that those addresses have
92
+ # received a copy of the email already, and that by including them here,
93
+ # we would cause them to receive two copies of the message.
94
+ #
95
+ def recipient_addresses
96
+ @recipients.collect(&:email_address).collect(&:downcase) - email.recipient_addresses
97
+ end
98
+
89
99
  # Answers the subject with 'Re:' prefixed. Note that it is the
90
100
  # responsibility of the MList::MailList to perform any processing of the
91
101
  # persisted subject (ie, cleaning up labels, etc).
@@ -15,12 +15,12 @@ module MList
15
15
 
16
16
  def next(message)
17
17
  i = tree_order.index(message)
18
- messages[i + 1] unless messages.size < i
18
+ tree_order[i + 1] unless messages.size < i
19
19
  end
20
20
 
21
21
  def previous(message)
22
22
  i = tree_order.index(message)
23
- messages[i - 1] if i > 0
23
+ tree_order[i - 1] if i > 0
24
24
  end
25
25
 
26
26
  def subject
@@ -2,8 +2,15 @@ module MList
2
2
  module Util
3
3
 
4
4
  class HtmlTextExtraction
5
+
6
+ # We need a way to maintain non-breaking spaces. Hpricot will replace
7
+ # them with ??.chr. We can easily teach it to convert it to a space, but
8
+ # then we lose the information in the Text node that we need to keep the
9
+ # space around, since that is what they would see in a view of the HTML.
10
+ NBSP = '!!!NBSP!!!'
11
+
5
12
  def initialize(html)
6
- @doc = Hpricot(html)
13
+ @doc = Hpricot(html.gsub('&nbsp;', NBSP))
7
14
  end
8
15
 
9
16
  def execute
@@ -19,7 +26,7 @@ module MList
19
26
  end
20
27
  @text << "\n\n--\n#{refs.join("\n")}"
21
28
  end
22
- @text
29
+ @text.gsub(NBSP, ' ')
23
30
  end
24
31
 
25
32
  def extract_text_from_node(node)
@@ -95,6 +102,12 @@ module MList
95
102
  text.to_s.gsub(/\r\n?/, "\n")
96
103
  end
97
104
 
105
+ def subscriber_name_and_address(subscriber)
106
+ a = subscriber.email_address
107
+ a = "#{subscriber.display_name} #{bracket(a)}" if subscriber.respond_to?(:display_name)
108
+ a
109
+ end
110
+
98
111
  BRACKETS_RE = /\A<(.*?)>\Z/
99
112
  def bracket(string)
100
113
  string.blank? || string =~ BRACKETS_RE ? string : "<#{string}>"
@@ -5,8 +5,6 @@ module MList
5
5
  def date
6
6
  if date = tmail.header_string('date')
7
7
  Time.parse(date)
8
- else
9
- self.created_at ||= Time.now
10
8
  end
11
9
  end
12
10
 
@@ -1,28 +1,22 @@
1
- require 'dispatcher' unless defined?(::Dispatcher)
2
-
3
- Dispatcher.module_eval do
4
- # Provides the mechinism to support applications that want to observe MList
5
- # models.
6
- #
7
- # ActiveRecord observers are reloaded at each request in development mode.
8
- # They will be registered with the MList models each time. Since the MList
9
- # models are required once at initialization, there will always only be one
10
- # instance of the model class, and therefore, many instances of the observer
11
- # class registered with it; all but the most recent are invalid, since they
12
- # were undefined when the dispatcher reloaded the application.
13
- #
14
- # Why not an initializer "to_prepare" block? Simply because we must clear
15
- # the observers in the ActiveRecord classes before the
16
- # ActiveRecord::Base.instantiate_observers call is made by the prepare block
17
- # that we cannot get in front of with the initializer approach. Also, it
18
- # lessens the configuration burden of the MList client application.
19
- #
20
- # Should we ever have observers in MList, this will likely need more careful
21
- # attention.
22
- #
23
- def reload_application_with_plugin_record_support
24
- ActiveRecord::Base.send(:subclasses).each(&:delete_observers)
25
- reload_application_without_plugin_record_support
1
+ # Provides the mechinism to support applications that want to observe MList
2
+ # models.
3
+ #
4
+ # ActiveRecord observers are reloaded at each request in development mode.
5
+ # They will be registered with the MList models each time. Since the MList
6
+ # models are required once at initialization, there will always only be one
7
+ # instance of the model class, and therefore, many instances of the observer
8
+ # class registered with it; all but the most recent are invalid, since they
9
+ # were undefined when the dispatcher reloaded the application.
10
+ #
11
+ # Should we ever have observers in MList, this will likely need more careful
12
+ # attention.
13
+ #
14
+ unless Rails.configuration.cache_classes
15
+ class << ActiveRecord::Base
16
+ def instantiate_observers_with_mlist_observers
17
+ subclasses.each(&:delete_observers)
18
+ instantiate_observers_without_mlist_observers
19
+ end
20
+ alias_method_chain :instantiate_observers, :mlist_observers
26
21
  end
27
- alias_method_chain :reload_application, :plugin_record_support
28
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aiwilliams-mlist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Williams
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-03 00:00:00 -08:00
12
+ date: 2009-08-14 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -59,6 +59,7 @@ files:
59
59
  - rails/init.rb
60
60
  has_rdoc: false
61
61
  homepage: http://github.com/aiwilliams/mlist
62
+ licenses:
62
63
  post_install_message:
63
64
  rdoc_options: []
64
65
 
@@ -79,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
80
  requirements: []
80
81
 
81
82
  rubyforge_project:
82
- rubygems_version: 1.2.0
83
+ rubygems_version: 1.3.5
83
84
  signing_key:
84
85
  specification_version: 2
85
86
  summary: A Ruby mailing list library designed to be integrated into other applications.