aiwilliams-mlist 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,77 +1,148 @@
1
1
  module MList
2
2
  class MailList < ActiveRecord::Base
3
- def self.find_or_create_by_list(list)
4
- mail_list = find_or_create_by_identifier(list.list_id)
5
- mail_list.manager_list = list
3
+ set_table_name 'mlist_mail_lists'
4
+
5
+ # Provides the MailList for a given implementation of MList::List,
6
+ # connecting it to the provided email server for delivering posts.
7
+ #
8
+ def self.find_or_create_by_list(list, outgoing_server)
9
+ if list.is_a?(ActiveRecord::Base)
10
+ mail_list = find_or_create_by_manager_list_identifier_and_manager_list_type_and_manager_list_id(
11
+ list.list_id, list.class.base_class.name, list.id
12
+ )
13
+ else
14
+ mail_list = find_or_create_by_manager_list_identifier(list.list_id)
15
+ mail_list.manager_list = list
16
+ end
17
+ mail_list.outgoing_server = outgoing_server
6
18
  mail_list
7
19
  end
8
20
 
9
- has_many :messages, :dependent => :delete_all
10
- has_many :threads, :dependent => :delete_all
21
+ belongs_to :manager_list, :polymorphic => true
11
22
 
12
- attr_accessor :manager_list
13
- delegate :address, :recipients, :subscriptions,
14
- :to => :manager_list
23
+ has_many :messages, :class_name => 'MList::Message', :dependent => :delete_all
24
+ has_many :threads, :class_name => 'MList::Thread', :dependent => :delete_all
15
25
 
16
- def post(email_server, message)
17
- return unless process?(message)
18
- prepare_delivery(message)
19
- deliver(message, email_server)
20
- end
26
+ delegate :address, :label, :post_url, :to => :list
27
+
28
+ attr_accessor :outgoing_server
21
29
 
22
- def been_there?(message)
23
- message.header_string('x-beenthere') == address
30
+ # Creates a new MList::Message and delivers it to the subscribers of this
31
+ # list.
32
+ #
33
+ def post(email_or_attributes)
34
+ email = email_or_attributes
35
+ email = MList::EmailPost.new(email_or_attributes) unless email.is_a?(MList::EmailPost)
36
+ process_message messages.build(
37
+ :parent => email.reply_to_message,
38
+ :parent_identifier => email.parent_identifier,
39
+ :mail_list => self,
40
+ :subscriber => email.subscriber,
41
+ :recipients => list.recipients(email.subscriber),
42
+ :email => MList::Email.new(:tmail => email.to_tmail)
43
+ ), :search_parent => false
24
44
  end
25
45
 
26
- # http://mail.python.org/pipermail/mailman-developers/2006-April/018718.html
27
- def bounce_headers
28
- {'sender' => "mlist-#{address}",
29
- 'errors-to' => "mlist-#{address}"}
46
+ # Processes the email received by the MList::Server.
47
+ #
48
+ def process_email(email)
49
+ subscriber = list.subscriber(email.from_address)
50
+ recipients = list.recipients(subscriber)
51
+ process_message messages.build(
52
+ :mail_list => self,
53
+ :subscriber => subscriber,
54
+ :recipients => recipients,
55
+ :email => email
56
+ )
30
57
  end
31
58
 
32
- def deliver(message, email_server)
33
- transaction do
34
- email_server.deliver(message.tmail)
35
- thread = find_thread(message)
36
- thread.messages << message
37
- thread.save!
38
- end
59
+ def list
60
+ @list ||= manager_list
39
61
  end
40
62
 
41
- def find_thread(message)
42
- if message.reply?
43
- threads.find(:first,
44
- :joins => :messages,
45
- :readonly => false,
46
- :conditions => ['messages.identifier = ?', message.parent_identifier]
47
- )
63
+ def manager_list_with_dual_type=(list)
64
+ if list.is_a?(ActiveRecord::Base)
65
+ self.manager_list_without_dual_type = list
66
+ @list = list
48
67
  else
49
- threads.build
68
+ self.manager_list_without_dual_type = nil
69
+ @list = list
50
70
  end
51
71
  end
72
+ alias_method_chain :manager_list=, :dual_type
52
73
 
53
- # http://www.jamesshuggins.com/h/web1/list-email-headers.htm
54
- def list_headers
55
- headers = manager_list.list_headers
56
- headers['x-beenthere'] = address
57
- headers.update(bounce_headers)
58
- headers.delete_if {|k,v| v.nil?}
59
- end
60
-
61
- def prepare_delivery(message)
62
- prepare_list_headers(message)
63
- message.to = address
64
- message.bcc = recipients(message)
74
+ def process?(message)
75
+ !message.recipients.blank?
65
76
  end
66
77
 
67
- def prepare_list_headers(message)
68
- list_headers.each do |k,v|
69
- message.write_header(k,v)
78
+ private
79
+ # http://mail.python.org/pipermail/mailman-developers/2006-April/018718.html
80
+ def bounce_headers
81
+ {'sender' => "mlist-#{address}",
82
+ 'errors-to' => "mlist-#{address}"}
83
+ end
84
+
85
+ # http://www.jamesshuggins.com/h/web1/list-email-headers.htm
86
+ def list_headers
87
+ headers = list.list_headers
88
+ headers['x-beenthere'] = address
89
+ headers.update(bounce_headers)
90
+ headers.delete_if {|k,v| v.nil?}
91
+ end
92
+
93
+ def process_message(message, options = {})
94
+ raise MList::DoubleDeliveryError.new(message) unless message.new_record?
95
+ return message unless process?(message)
96
+
97
+ options = {
98
+ :search_parent => true,
99
+ :delivery_time => Time.now
100
+ }.merge(options)
101
+ transaction do
102
+ thread = find_thread(message, options)
103
+ thread.messages << message
104
+ prepare_delivery(message)
105
+ destinations = message.delivery.destinations
106
+ tmail = message.to_tmail
107
+ self.updated_at = thread.updated_at = options[:delivery_time]
108
+ thread.save! && save!
109
+ outgoing_server.deliver(tmail, destinations)
110
+ end
111
+ message
112
+ end
113
+
114
+ def prepare_delivery(message)
115
+ delivery = message.delivery
116
+ prepare_list_headers(delivery)
117
+ delivery.subject = list_subject(message)
118
+ delivery.to = address
119
+ delivery.bcc = message.recipients
120
+ delivery.reply_to = "#{label} <#{post_url}>"
121
+ end
122
+
123
+ def prepare_list_headers(delivery)
124
+ list_headers.each do |k,v|
125
+ if TMail::Mail::ALLOW_MULTIPLE.include?(k.downcase)
126
+ delivery.prepend_header(k,v)
127
+ else
128
+ delivery.write_header(k,v)
129
+ end
130
+ end
131
+ end
132
+
133
+ def list_subject(message)
134
+ prefix = "[#{label}]"
135
+ subject = message.subject.gsub(%r(#{Regexp.escape(prefix)}\s*), '')
136
+ subject.gsub!(%r{(re:\s*){2,}}i, 'Re: ')
137
+ "#{prefix} #{subject}"
138
+ end
139
+
140
+ def find_thread(message, options)
141
+ if options[:search_parent]
142
+ message.parent_identifier = message.email.parent_identifier(self)
143
+ message.parent = messages.find_by_identifier(message.parent_identifier)
144
+ end
145
+ message.parent ? message.parent.thread : threads.build
70
146
  end
71
- end
72
-
73
- def process?(message)
74
- !been_there?(message) && !recipients(message).blank?
75
- end
76
147
  end
77
148
  end
@@ -12,20 +12,24 @@ module MList
12
12
 
13
13
  def lists(email)
14
14
  lists = List.find_all_by_address(email.list_addresses)
15
- email.list_addresses.map { |a| lists.detect {|l| l.address == a} }
15
+ email.list_addresses.map { |a| lists.detect {|l| l.address == a} }.compact
16
16
  end
17
17
 
18
18
  class List < ActiveRecord::Base
19
19
  include ::MList::List
20
20
 
21
- has_many :subscriptions, :dependent => :delete_all
21
+ has_many :subscribers, :dependent => :delete_all
22
+
23
+ def list_id
24
+ "#{self.class.name}#{id}"
25
+ end
22
26
 
23
27
  def subscribe(address)
24
- subscriptions.find_or_create_by_address(address)
28
+ subscribers.find_or_create_by_email_address(address)
25
29
  end
26
30
  end
27
31
 
28
- class Subscription < ActiveRecord::Base
32
+ class Subscriber < ActiveRecord::Base
29
33
  belongs_to :list
30
34
  end
31
35
  end
@@ -1,107 +1,111 @@
1
1
  module MList
2
2
 
3
- # The persisted version of an email that is processed by MList::MailLists.
4
- #
5
- # The tmail object referenced by these are unique, though they may reference
6
- # the 'same' originating email.
7
- #
8
3
  class Message < ActiveRecord::Base
9
- belongs_to :mail_list
4
+ set_table_name 'mlist_messages'
10
5
 
11
- attr_writer :header_sanitizers
12
- before_save :serialize_tmail
6
+ include MList::Util::EmailHelpers
13
7
 
14
- def charset
15
- 'utf-8'
16
- end
8
+ belongs_to :email, :class_name => 'MList::Email'
9
+ belongs_to :parent, :class_name => 'MList::Message'
10
+ belongs_to :mail_list, :class_name => 'MList::MailList', :counter_cache => :messages_count
11
+ belongs_to :thread, :class_name => 'MList::Thread', :counter_cache => :messages_count
17
12
 
18
- def delete_header(name)
19
- tmail[name] = nil
20
- end
13
+ # A temporary storage of recipient subscribers, obtained from
14
+ # MList::Lists. This list is not available when a message is reloaded.
15
+ #
16
+ attr_accessor :recipients
21
17
 
22
- def from_address
23
- tmail.from.first
18
+ def delivery
19
+ @delivery ||= MList::Util::TMailBuilder.new(TMail::Mail.parse(email.source))
24
20
  end
25
21
 
26
- def parent_identifier
27
- if in_reply_to = header_string('in-reply-to')
28
- identifier = in_reply_to
29
- elsif references = read_header('references')
30
- identifier = references.ids.first
31
- else
32
- parent_message = mail_list.messages.find(:first,
33
- :conditions => ['messages.subject = ?', remove_regard(subject)],
34
- :order => 'created_at asc'
35
- )
36
- identifier = parent_message.identifier if parent_message
37
- end
38
- remove_brackets(identifier) if identifier
22
+ def email_with_capture=(email)
23
+ self.subject = email.subject
24
+ self.mailer = email.mailer
25
+ self.email_without_capture = email
39
26
  end
27
+ alias_method_chain :email=, :capture
40
28
 
41
- def read_header(name)
42
- tmail[name]
29
+ # Answers the html content of the message.
30
+ #
31
+ def html
32
+ email.html
43
33
  end
44
34
 
45
- def reply?
46
- !parent_identifier.nil?
35
+ # Answers the text content of the message.
36
+ #
37
+ def text
38
+ email.text
47
39
  end
48
40
 
49
- def write_header(name, value)
50
- tmail[name] = sanitize_header(name, value)
41
+ # Answers the text content of the message as HTML. The structure of this
42
+ # output is very simple. For examples of what it can handle, please check
43
+ # out the spec documents for MList::Util::EmailHelpers.
44
+ #
45
+ def text_html
46
+ text_to_html(text)
51
47
  end
52
48
 
53
- def tmail=(tmail)
54
- write_attribute(:identifier, remove_brackets(tmail.header_string('message-id')))
55
- write_attribute(:subject, tmail.subject)
56
- @tmail = tmail
49
+ # Answers text suitable for creating a reply message.
50
+ #
51
+ def text_for_reply
52
+ timestamp = email.date.to_s(:mlist_reply_timestamp)
53
+ "On #{timestamp}, #{email.from} wrote:\n#{text_to_quoted(text)}"
57
54
  end
58
55
 
59
- def tmail
60
- @tmail ||= TMail::Mail.parse(email_text)
56
+ # Answers text suitable for creating a reply message, converted to the
57
+ # same simple html of _text_html_.
58
+ #
59
+ def html_for_reply
60
+ text_to_html(text_for_reply)
61
61
  end
62
62
 
63
- def to=(recipients)
64
- tmail.to = sanitize_header('to', recipients)
63
+ # Answers the subject with all prefixes removed.
64
+ #
65
+ # message.subject = 'Re: [List Label] Re: The new Chrome Browser from Google'
66
+ # message.subject_for_reply => 'Re: The new Chrome Browser from Google'
67
+ #
68
+ def subject_for_reply
69
+ "Re: #{remove_regard(subject)}"
65
70
  end
66
71
 
67
- def bcc=(recipients)
68
- tmail.bcc = sanitize_header('bcc', recipients)
72
+ # Answers the subscriber from which this message comes.
73
+ #
74
+ def subscriber
75
+ @subscriber ||= begin
76
+ if subscriber_type? && subscriber_id?
77
+ subscriber_type.constantize.find(subscriber_id)
78
+ elsif subscriber_address?
79
+ MList::EmailSubscriber.new(subscriber_address)
80
+ end
81
+ end
69
82
  end
70
83
 
71
- # Provide delegation to *most* of the underlying TMail::Mail methods,
72
- # excluding those overridden by this class and the [] and []= methods. We
73
- # must maintain the ActiveRecord interface over that of the TMail::Mail
74
- # interface.
84
+ # Assigns the subscriber from which this message comes.
75
85
  #
76
- def method_missing(symbol, *args, &block) # :nodoc:
77
- if @tmail && @tmail.respond_to?(symbol) && !(symbol == :[] || symbol == :[]=)
78
- @tmail.__send__(symbol, *args, &block)
86
+ def subscriber=(subscriber)
87
+ case subscriber
88
+ when ActiveRecord::Base
89
+ @subscriber = subscriber
90
+ self.subscriber_address = subscriber.email_address
91
+ self.subscriber_type = subscriber.class.base_class.name
92
+ self.subscriber_id = subscriber.id
93
+ when MList::EmailSubscriber
94
+ @subscriber = subscriber
95
+ self.subscriber_address = subscriber.email_address
96
+ self.subscriber_type = self.subscriber_id = nil
97
+ when String
98
+ self.subscriber = MList::EmailSubscriber.new(subscriber)
79
99
  else
80
- super
100
+ @subscriber = self.subscriber_address = self.subscriber_type = self.subscriber_id = nil
81
101
  end
82
102
  end
83
103
 
84
- def sanitize_header(name, *values)
85
- header_sanitizer(name).call(charset, *values)
104
+ def to_tmail
105
+ delivery.mailer = mailer
106
+ delivery.ready_to_send
107
+ self.identifier = delivery.identifier
108
+ delivery.tmail
86
109
  end
87
-
88
- private
89
- def header_sanitizer(name)
90
- @header_sanitizers ||= Util.default_header_sanitizers
91
- @header_sanitizers[name]
92
- end
93
-
94
- def remove_brackets(string)
95
- string =~ /\A<(.*?)>\Z/ ? $1 : string
96
- end
97
-
98
- def remove_regard(string)
99
- stripped = string.strip
100
- stripped =~ /\Are:\s+(.*?)\Z/i ? $1 : stripped
101
- end
102
-
103
- def serialize_tmail
104
- write_attribute(:email_text, @tmail.to_s)
105
- end
106
110
  end
107
111
  end
@@ -8,7 +8,7 @@ module MList
8
8
  @email_server.receiver(self)
9
9
  end
10
10
 
11
- def receive(email)
11
+ def receive_email(email)
12
12
  lists = list_manager.lists(email)
13
13
  if email.bounce?
14
14
  process_bounce(lists.first, email)
@@ -17,6 +17,10 @@ module MList
17
17
  end
18
18
  end
19
19
 
20
+ def mail_list(list)
21
+ MailList.find_or_create_by_list(list, @email_server)
22
+ end
23
+
20
24
  protected
21
25
  def process_bounce(list, email)
22
26
  list.bounce(email)
@@ -24,11 +28,15 @@ module MList
24
28
 
25
29
  def process_post(lists, email)
26
30
  lists.each do |list|
31
+ next if email.been_here?(list)
27
32
  if list.subscriber?(email.from_address)
28
- mail_list = MailList.find_or_create_by_list(list)
29
- mail_list.post(email_server, MList::Message.new(:mail_list => mail_list, :tmail => email.tmail))
33
+ if list.active?
34
+ mail_list(list).process_email(email)
35
+ else
36
+ list.inactive_post(email)
37
+ end
30
38
  else
31
- list.non_subscriber_posted(email)
39
+ list.non_subscriber_post(email)
32
40
  end
33
41
  end
34
42
  end