aiwilliams-mlist 0.1.4 → 0.1.5
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 +24 -9
- data/README +36 -20
- data/Rakefile +1 -1
- data/TODO +36 -0
- data/VERSION.yml +1 -1
- data/lib/mlist.rb +13 -0
- data/lib/mlist/email.rb +6 -26
- data/lib/mlist/email_post.rb +4 -0
- data/lib/mlist/list.rb +88 -13
- data/lib/mlist/mail_list.rb +123 -20
- data/lib/mlist/manager.rb +30 -0
- data/lib/mlist/manager/database.rb +7 -1
- data/lib/mlist/manager/notifier.rb +31 -0
- data/lib/mlist/message.rb +26 -3
- data/lib/mlist/server.rb +26 -10
- data/lib/mlist/thread.rb +72 -12
- data/lib/mlist/util/email_helpers.rb +86 -3
- data/lib/mlist/util/tmail_methods.rb +34 -8
- data/rails/init.rb +28 -0
- metadata +6 -2
data/CHANGELOG
CHANGED
@@ -1,19 +1,34 @@
|
|
1
|
+
*0.1.5 [Solid Basics] (January 17, 2009)
|
2
|
+
|
3
|
+
* No longer storing the list label in the message subject field, thereby supporting cleaner viewing of threads [aiwilliams]
|
4
|
+
* Improved handling of incoming email subjects by leaving labels alone that aren't obviously the label of the list, thereby allowing for subjects like "[Ann] My New Thing" [aiwilliams]
|
5
|
+
* Notifying subscribers when the list indicates that they are currently blocked from posting messages to a list they are subscribed to [aiwilliams]
|
6
|
+
* Added MList::Manager module to better define the interface of list managers. This needs to be included into list manager implementations. [aiwilliams]
|
7
|
+
* MList::List implementors may now answer the footer content to be appended to the bottom of the text/plain part of messages. [aiwilliams]
|
8
|
+
* List footers are stripped from text/plain part of messages before being delivered. [aiwilliams]
|
9
|
+
* Observers of MList models which are defined in client applications now work without special instruction. [aiwilliams]
|
10
|
+
* A first pass implementation for converting html to text using Hpricot. [aiwilliams]
|
11
|
+
* Better thread tree, supporting message navigation within a thread through a linked list kind of approach. [aiwilliams]
|
12
|
+
* Better parent message associating using in-reply-to, then references, then subject. [aiwilliams]
|
13
|
+
* MList.version is hash of {:major => 0, :minor => 0, :patch => 0}, with a to_s of 'MList 0.0.0'. [aiwilliams]
|
14
|
+
* Fixed bug where original email source content was last in TMail::Mail#to_s usage. [aiwilliams]
|
15
|
+
|
1
16
|
*0.1.4 [] (January 7, 2009)
|
2
17
|
|
3
|
-
* Fixed bug where default email server was not allowing for settings
|
4
|
-
* Made subject for reply place 're:' in front of the list label
|
5
|
-
* Added really simple tree support. Really simple.
|
18
|
+
* Fixed bug where default email server was not allowing for settings [aiwilliams]
|
19
|
+
* Made subject for reply place 're:' in front of the list label [aiwilliams]
|
20
|
+
* Added really simple tree support. Really simple. [aiwilliams]
|
6
21
|
|
7
22
|
*0.1.3 [] (January 7, 2009)
|
8
23
|
|
9
|
-
* Generating message id as UUID
|
10
|
-
* Allowing setting of domain for message id
|
11
|
-
* Fixed bug in storing message id
|
24
|
+
* Generating message id as UUID [aiwilliams]
|
25
|
+
* Allowing setting of domain for message id [aiwilliams]
|
26
|
+
* Fixed bug in storing message id [aiwilliams]
|
12
27
|
|
13
28
|
*0.1.2 [] (January 7, 2009)
|
14
29
|
|
15
|
-
* Added references header when creating a reply post.
|
16
|
-
* Including display_name in from address of EmailPost.
|
17
|
-
* Improved extraction of text when it is nested inside parts.
|
30
|
+
* Added references header when creating a reply post. [aiwilliams]
|
31
|
+
* Including display_name in from address of EmailPost. [aiwilliams]
|
32
|
+
* Improved extraction of text when it is nested inside parts. [aiwilliams]
|
18
33
|
|
19
34
|
*0.1.1 [First Working Release] (January 5, 2009)
|
data/README
CHANGED
@@ -32,34 +32,49 @@ as they seem. Alas, I go boldly forward.
|
|
32
32
|
|
33
33
|
==== Thread Trees
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
A Thread answers a tree of messages where each message in the tree knows it's
|
36
|
+
parent, children, previous and next message, and whether it is the root or a leaf
|
37
|
+
in the tree. The messages are actually delegates, wrappers around the real
|
38
|
+
message instances from the thread.
|
39
|
+
|
40
|
+
This approach makes a few assumptions:
|
41
|
+
|
42
|
+
# You want the tree because you will be displaying it # Moving through a tree
|
43
|
+
message by message should 'feel' right; next is determined by walking the tree
|
44
|
+
depth first.
|
45
|
+
|
46
|
+
It may or may not prove useful someday to have this knowledge in the form of
|
47
|
+
something like awesome_nested_set.
|
40
48
|
|
41
49
|
==== Extracting 'from' IP address from emails is not implemented
|
42
50
|
|
43
51
|
http://compnetworking.about.com/od/workingwithipaddresses/qt/ipaddressemail.htm
|
44
|
-
|
45
|
-
==== Observing MList ActiveRecord subsclasses (ie, MList::Message, MList::Thread)
|
46
52
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
53
|
+
==== Deleting messages
|
54
|
+
|
55
|
+
Mail list servers don't typically deal with this, do they? When an email is sent,
|
56
|
+
it's sent! If you delete one, how can a reply to it be accurately injected into a
|
57
|
+
partially broken tree?
|
58
|
+
|
59
|
+
Since MList is designed to integrate with applications that want to distribute
|
60
|
+
messages, those applications may want to delete messages. I feel, at the time of
|
61
|
+
writing this, that the feature of deleting is NOT a problem that MList should
|
62
|
+
attempt to address in great detail. The models are ActiveRecord descendants, so
|
63
|
+
they can be deleted with #destroy, et. al. MList will not prevent that from
|
64
|
+
happening. This has implications, of course.
|
52
65
|
|
53
|
-
|
54
|
-
things 'just work' such that I can delete this part of the document. For now, do
|
55
|
-
the following in environment.rb:
|
66
|
+
# MList::Thread#tree will likely break if messages are gone.
|
56
67
|
|
57
|
-
|
58
|
-
|
59
|
-
|
68
|
+
In my own usage, I have opted for adding a column to my mlist_messages table,
|
69
|
+
deleted_at. The application will make decisions based on whether that column has
|
70
|
+
a value or not. I would suggest you implement something similar, always favoring
|
71
|
+
leaving the records in place (paranoid delete). Since MList::MailList#messages
|
72
|
+
(and other such has_many associations) don't know about this column, they will
|
73
|
+
always answer collections which still include those messages.
|
60
74
|
|
61
|
-
|
62
|
-
|
75
|
+
When an MList::MailList is destroyed, all it's MList::Messages and MList::Threads
|
76
|
+
will be deleted (:dependent => :delete_all). If no other MList::Messages are
|
77
|
+
referencing the MList::Email records, they will also be deleted.
|
63
78
|
|
64
79
|
== SYNOPSIS:
|
65
80
|
|
@@ -73,6 +88,7 @@ You love Ruby. You want MList.
|
|
73
88
|
|
74
89
|
You'll need some gems.
|
75
90
|
|
91
|
+
* hpricot
|
76
92
|
* uuid (macaddr also)
|
77
93
|
* tmail
|
78
94
|
* activesupport
|
data/Rakefile
CHANGED
@@ -17,7 +17,7 @@ begin
|
|
17
17
|
s.name = 'mlist'
|
18
18
|
s.summary = 'A Ruby mailing list library designed to be integrated into other applications.'
|
19
19
|
s.email = 'adam@thewilliams.ws'
|
20
|
-
s.files = FileList["[A-Z]*", "{lib}/**/*"].exclude("tmp
|
20
|
+
s.files = FileList["[A-Z]*", "{lib,rails}/**/*"].exclude("tmp")
|
21
21
|
s.homepage = "http://github.com/aiwilliams/mlist"
|
22
22
|
s.description = s.summary
|
23
23
|
s.authors = ['Adam Williams']
|
data/TODO
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#html_to_text
|
2
|
+
|
3
|
+
ADDRESS - Address
|
4
|
+
BLOCKQUOTE - Block quotation
|
5
|
+
|
6
|
+
DIV - Generic block-level container
|
7
|
+
DL - Definition list
|
8
|
+
FIELDSET - Form control group
|
9
|
+
FORM - Interactive form
|
10
|
+
H1 - Level-one heading
|
11
|
+
H2 - Level-two heading
|
12
|
+
H3 - Level-three heading
|
13
|
+
H4 - Level-four heading
|
14
|
+
H5 - Level-five heading
|
15
|
+
H6 - Level-six heading
|
16
|
+
HR - Horizontal rule
|
17
|
+
OL - Ordered list
|
18
|
+
P - Paragraph
|
19
|
+
PRE - Preformatted text
|
20
|
+
|
21
|
+
TABLE - Table
|
22
|
+
output TRs as lines, csv the TDs
|
23
|
+
|
24
|
+
UL - Unordered list
|
25
|
+
|
26
|
+
DD - Definition description
|
27
|
+
DT - Definition term
|
28
|
+
LI - List item
|
29
|
+
|
30
|
+
|
31
|
+
a (Links) - Link to Somewhere[1] ---- [1] http://
|
32
|
+
b (Bold) - *bold*
|
33
|
+
i (Italic) - _italic_
|
34
|
+
strong - *strong*
|
35
|
+
em (emphasis) - _emphasis_
|
36
|
+
u (underline) - probably do nothing, maybe something like em
|
data/VERSION.yml
CHANGED
data/lib/mlist.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
1
3
|
require 'uuid'
|
2
4
|
require 'tmail'
|
5
|
+
require 'hpricot'
|
3
6
|
require 'activesupport'
|
4
7
|
require 'activerecord'
|
5
8
|
|
@@ -14,7 +17,17 @@ require 'mlist/email_subscriber'
|
|
14
17
|
require 'mlist/server'
|
15
18
|
require 'mlist/thread'
|
16
19
|
|
20
|
+
require 'mlist/manager'
|
21
|
+
|
17
22
|
module MList
|
23
|
+
mattr_reader :version
|
24
|
+
@@version = YAML.load_file(File.join(File.dirname(__FILE__), '..', "VERSION.yml"))
|
25
|
+
class << @@version
|
26
|
+
def to_s
|
27
|
+
@to_s ||= [self[:major], self[:minor], self[:patch]].join('.')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
18
31
|
class DoubleDeliveryError < StandardError
|
19
32
|
def initialize(message)
|
20
33
|
super("A message should never be delivered more than once. An attempt was made to deliver this message:\n#{message.inspect}")
|
data/lib/mlist/email.rb
CHANGED
@@ -28,33 +28,9 @@ module MList
|
|
28
28
|
tmail.header_string('to') =~ /mlist-/
|
29
29
|
end
|
30
30
|
|
31
|
-
# Extracts the parent message identifier from the source, using
|
32
|
-
# in-reply-to first, then references. Brackets around the identifier are
|
33
|
-
# removed.
|
34
|
-
#
|
35
|
-
# If you provide an MList::MailList, it will be searched for a message
|
36
|
-
# having the same subject as a last resort.
|
37
|
-
#
|
38
|
-
def parent_identifier(mail_list = nil)
|
39
|
-
identifier = tmail.header_string('in-reply-to') || begin
|
40
|
-
references = tmail['references']
|
41
|
-
references.ids.first if references
|
42
|
-
end
|
43
|
-
|
44
|
-
if identifier
|
45
|
-
remove_brackets(identifier)
|
46
|
-
elsif mail_list && subject =~ /(^|[^\w])re:/i
|
47
|
-
parent_message = mail_list.messages.find(:first,
|
48
|
-
:select => 'identifier',
|
49
|
-
:conditions => ['mlist_messages.subject = ?', remove_regard(subject)],
|
50
|
-
:order => 'created_at asc')
|
51
|
-
parent_message.identifier if parent_message
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
31
|
def tmail=(tmail)
|
56
32
|
@tmail = tmail
|
57
|
-
write_attribute(:source, tmail.
|
33
|
+
write_attribute(:source, tmail.port.read_all)
|
58
34
|
end
|
59
35
|
|
60
36
|
def tmail
|
@@ -65,11 +41,15 @@ module MList
|
|
65
41
|
# methods, excluding those overridden by this Class and the [] method (an
|
66
42
|
# ActiveRecord method).
|
67
43
|
def method_missing(symbol, *args, &block) # :nodoc:
|
68
|
-
if symbol.to_s !~ /=\Z/ && symbol != :[] && symbol != :source &&
|
44
|
+
if symbol.to_s !~ /=\Z/ && symbol != :[] && symbol != :source && tmail.respond_to?(symbol)
|
69
45
|
tmail.__send__(symbol, *args, &block)
|
70
46
|
else
|
71
47
|
super
|
72
48
|
end
|
73
49
|
end
|
50
|
+
|
51
|
+
def respond_to?(method)
|
52
|
+
super || (method.to_s !~ /=\Z/ && tmail.respond_to?(method))
|
53
|
+
end
|
74
54
|
end
|
75
55
|
end
|
data/lib/mlist/email_post.rb
CHANGED
data/lib/mlist/list.rb
CHANGED
@@ -5,6 +5,9 @@ module MList
|
|
5
5
|
# in processing email coming to a list - that is, whatever you include this
|
6
6
|
# into may re-define behavior appropriately.
|
7
7
|
#
|
8
|
+
# Your 'subscriber' instances MUST respond to :email_address. They may
|
9
|
+
# optionally respond to :display_name.
|
10
|
+
#
|
8
11
|
module List
|
9
12
|
|
10
13
|
# Answers whether this list is active or not. All lists are active all the
|
@@ -14,53 +17,116 @@ module MList
|
|
14
17
|
true
|
15
18
|
end
|
16
19
|
|
17
|
-
|
18
|
-
|
20
|
+
# Answers whether the subscriber is blocked from posting or not. This will
|
21
|
+
# not be asked when the list is not active (answers _active?_ as false).
|
22
|
+
#
|
23
|
+
def blocked?(subscriber)
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
# Answers the footer content for this list. Default implementation is very
|
28
|
+
# simple right now. Expect improvements in the future.
|
29
|
+
#
|
30
|
+
def footer_content(message)
|
31
|
+
%Q{The "#{label}" mailing list\nTo post messages, send email to #{post_url}}
|
19
32
|
end
|
20
33
|
|
34
|
+
# Answer a suitable label for the list, which will be used in various
|
35
|
+
# parts of content that is delivered to subscribers, etc.
|
36
|
+
#
|
37
|
+
def label
|
38
|
+
raise 'answer the list label'
|
39
|
+
end
|
40
|
+
|
41
|
+
# Answers the headers that are to be included in the emails delivered for
|
42
|
+
# this list. Any entries that have a nil value will not be included in the
|
43
|
+
# delivered email.
|
44
|
+
#
|
21
45
|
def list_headers
|
22
46
|
{
|
23
47
|
'list-id' => list_id,
|
24
|
-
'list-archive' =>
|
25
|
-
'list-subscribe' =>
|
26
|
-
'list-unsubscribe' =>
|
27
|
-
'list-owner' =>
|
28
|
-
'list-help' =>
|
48
|
+
'list-archive' => archive_url,
|
49
|
+
'list-subscribe' => subscribe_url,
|
50
|
+
'list-unsubscribe' => unsubscribe_url,
|
51
|
+
'list-owner' => owner_url,
|
52
|
+
'list-help' => help_url,
|
29
53
|
'list-post' => post_url
|
30
54
|
}
|
31
55
|
end
|
32
56
|
|
57
|
+
# Answers a unique, never changing value for this list.
|
58
|
+
#
|
33
59
|
def list_id
|
34
60
|
raise 'answer a unique, never changing value'
|
35
61
|
end
|
36
62
|
|
37
|
-
|
38
|
-
|
63
|
+
# The web address where an archive of this list may be found, nil if there
|
64
|
+
# is no archive.
|
65
|
+
#
|
66
|
+
def archive_url
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# The web address of the list help site, nil if this is not supported.
|
71
|
+
#
|
72
|
+
def help_url
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# The email address of the list owner, nil if this is not supported.
|
77
|
+
#
|
78
|
+
def owner_url
|
79
|
+
nil
|
39
80
|
end
|
40
81
|
|
82
|
+
# The email address where posts should be sent. Defaults to the address of
|
83
|
+
# the list.
|
84
|
+
#
|
41
85
|
def post_url
|
42
86
|
address
|
43
87
|
end
|
44
88
|
|
45
|
-
#
|
89
|
+
# The web url where subscriptions to this list may be created, nil if this
|
90
|
+
# is not supported.
|
91
|
+
#
|
92
|
+
def subscribe_url
|
93
|
+
nil
|
94
|
+
end
|
46
95
|
|
96
|
+
# The web url where subscriptions to this list may be deleted, nil if this
|
97
|
+
# is not supported.
|
98
|
+
#
|
99
|
+
def unsubscribe_url
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# A list is responsible for answering the recipient subscribers.
|
104
|
+
#
|
47
105
|
# The subscriber of the incoming message is provided if the list would
|
48
106
|
# like to exclude it from the returned list. It is not assumed that it
|
49
107
|
# will be included or excluded, thereby allowing the list to decide. This
|
50
108
|
# default implementation does not include the sending subscriber in the
|
51
109
|
# list of recipients.
|
52
110
|
#
|
53
|
-
# Your 'subscriber' instance MUST respond to :email_address. They may
|
54
|
-
# optionally respond to :display_name.
|
55
|
-
#
|
56
111
|
def recipients(subscriber)
|
57
112
|
subscribers.reject {|s| s.email_address == subscriber.email_address}
|
58
113
|
end
|
59
114
|
|
115
|
+
# A list must answer the subscriber who's email address is that of the one
|
116
|
+
# provided. The default implementation will pick the first instance that
|
117
|
+
# answers subscriber.email_address == email_address. Your implementation
|
118
|
+
# should probably select just one record.
|
119
|
+
#
|
60
120
|
def subscriber(email_address)
|
61
121
|
subscribers.detect {|s| s.email_address == email_address}
|
62
122
|
end
|
63
123
|
|
124
|
+
# A list must answer whether there is a subscriber who's email address is
|
125
|
+
# that of the one provided. This is checked before the subscriber is
|
126
|
+
# requested in order to allow for the lightest weight check possible; that
|
127
|
+
# is, your implementation could avoid loading the actual subscriber
|
128
|
+
# instance.
|
129
|
+
#
|
64
130
|
def subscriber?(email_address)
|
65
131
|
!subscriber(email_address).nil?
|
66
132
|
end
|
@@ -69,6 +135,15 @@ module MList
|
|
69
135
|
# certain events occur during the processing of email sent to a list.
|
70
136
|
#
|
71
137
|
module Callbacks
|
138
|
+
|
139
|
+
# Called when an email is a post to the list by a subscriber whom the
|
140
|
+
# list claims is blocked (answers true to _blocked?(subscriber)_). This
|
141
|
+
# will not be called if the list is inactive (answers false to
|
142
|
+
# _active?_);
|
143
|
+
#
|
144
|
+
def blocked_subscriber_post(email, subscriber)
|
145
|
+
end
|
146
|
+
|
72
147
|
def bounce(email)
|
73
148
|
end
|
74
149
|
|
data/lib/mlist/mail_list.rb
CHANGED
@@ -18,8 +18,11 @@ module MList
|
|
18
18
|
mail_list
|
19
19
|
end
|
20
20
|
|
21
|
+
include MList::Util::EmailHelpers
|
22
|
+
|
21
23
|
belongs_to :manager_list, :polymorphic => true
|
22
24
|
|
25
|
+
before_destroy :delete_unreferenced_email
|
23
26
|
has_many :messages, :class_name => 'MList::Message', :dependent => :delete_all
|
24
27
|
has_many :threads, :class_name => 'MList::Thread', :dependent => :delete_all
|
25
28
|
|
@@ -39,14 +42,13 @@ module MList
|
|
39
42
|
:mail_list => self,
|
40
43
|
:subscriber => email.subscriber,
|
41
44
|
:recipients => list.recipients(email.subscriber),
|
42
|
-
:email => MList::Email.new(:
|
45
|
+
:email => MList::Email.new(:source => email.to_s)
|
43
46
|
), :search_parent => false
|
44
47
|
end
|
45
48
|
|
46
49
|
# Processes the email received by the MList::Server.
|
47
50
|
#
|
48
|
-
def process_email(email)
|
49
|
-
subscriber = list.subscriber(email.from_address)
|
51
|
+
def process_email(email, subscriber)
|
50
52
|
recipients = list.recipients(subscriber)
|
51
53
|
process_message messages.build(
|
52
54
|
:mail_list => self,
|
@@ -56,6 +58,46 @@ module MList
|
|
56
58
|
)
|
57
59
|
end
|
58
60
|
|
61
|
+
# Answers the provided subject with superfluous 're:' and this list's
|
62
|
+
# labels removed.
|
63
|
+
#
|
64
|
+
# clean_subject('[List Label] Re: The new Chrome Browser from Google') => 'Re: The new Chrome Browser from Google'
|
65
|
+
# clean_subject('Re: [List Label] Re: The new Chrome Browser from Google') => 'Re: The new Chrome Browser from Google'
|
66
|
+
#
|
67
|
+
def clean_subject(string)
|
68
|
+
without_label = string.gsub(subject_prefix_regex, '')
|
69
|
+
if without_label =~ REGARD_RE
|
70
|
+
"Re: #{remove_regard(without_label)}"
|
71
|
+
else
|
72
|
+
without_label
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_parent_message(email)
|
77
|
+
if in_reply_to = email.header_string('in-reply-to')
|
78
|
+
message = messages.find(:first,
|
79
|
+
:conditions => ['identifier = ?', remove_brackets(in_reply_to)])
|
80
|
+
return message if message
|
81
|
+
end
|
82
|
+
|
83
|
+
if email.references
|
84
|
+
reference_identifiers = email.references.collect {|rid| remove_brackets(rid)}
|
85
|
+
message = messages.find(:first,
|
86
|
+
:conditions => ['identifier in (?)', reference_identifiers],
|
87
|
+
:order => 'created_at desc')
|
88
|
+
return message if message
|
89
|
+
end
|
90
|
+
|
91
|
+
if email.subject =~ REGARD_RE
|
92
|
+
message = messages.find(:first,
|
93
|
+
:conditions => ['subject = ?', remove_regard(clean_subject(email.subject))],
|
94
|
+
:order => 'created_at asc')
|
95
|
+
return message if message
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# The MList::List instance of the list manager.
|
100
|
+
#
|
59
101
|
def list
|
60
102
|
@list ||= manager_list
|
61
103
|
end
|
@@ -75,17 +117,59 @@ module MList
|
|
75
117
|
!message.recipients.blank?
|
76
118
|
end
|
77
119
|
|
120
|
+
# Distinct footer start marker. It is important to realize that changing
|
121
|
+
# this could be problematic.
|
122
|
+
#
|
123
|
+
FOOTER_BLOCK_START = "-~----~~----~----~----~----~---~~-~----~------~--~-~-"
|
124
|
+
|
125
|
+
# Distinct footer end marker. It is important to realize that changing
|
126
|
+
# this could be problematic.
|
127
|
+
#
|
128
|
+
FOOTER_BLOCK_END = "--~--~---~-----~--~----~-----~~~----~---~---~--~----~"
|
129
|
+
|
78
130
|
private
|
131
|
+
FOOTER_BLOCK_START_RE = %r[#{FOOTER_BLOCK_START}]
|
132
|
+
FOOTER_BLOCK_END_RE = %r[#{FOOTER_BLOCK_END}]
|
133
|
+
|
79
134
|
# http://mail.python.org/pipermail/mailman-developers/2006-April/018718.html
|
80
135
|
def bounce_headers
|
81
|
-
|
82
|
-
|
136
|
+
# tmail would not correctly quote the label in the sender header, which would break smtp delivery
|
137
|
+
{'sender' => "<mlist-#{address}>", 'errors-to' => "#{label} <mlist-#{address}>"}
|
138
|
+
end
|
139
|
+
|
140
|
+
def delete_unreferenced_email
|
141
|
+
conditions = %Q{
|
142
|
+
mlist_emails.id in (
|
143
|
+
select me.id from mlist_emails me left join mlist_messages mm on mm.email_id = me.id
|
144
|
+
where mm.mail_list_id = #{id}
|
145
|
+
) AND mlist_emails.id not in (
|
146
|
+
select meb.id from mlist_emails meb left join mlist_messages mmb on mmb.email_id = meb.id
|
147
|
+
where mmb.mail_list_id != #{id}
|
148
|
+
)}
|
149
|
+
MList::Email.delete_all(conditions)
|
150
|
+
end
|
151
|
+
|
152
|
+
def strip_list_footers(content)
|
153
|
+
if content =~ FOOTER_BLOCK_START_RE
|
154
|
+
in_footer_block = false
|
155
|
+
content = normalize_new_lines(content)
|
156
|
+
content = content.split("\n").reject do |line|
|
157
|
+
if in_footer_block
|
158
|
+
in_footer_block = line !~ FOOTER_BLOCK_END_RE
|
159
|
+
true
|
160
|
+
else
|
161
|
+
in_footer_block = line =~ FOOTER_BLOCK_START_RE
|
162
|
+
end
|
163
|
+
end.join("\n").rstrip
|
164
|
+
end
|
165
|
+
content
|
83
166
|
end
|
84
167
|
|
85
168
|
# http://www.jamesshuggins.com/h/web1/list-email-headers.htm
|
86
169
|
def list_headers
|
87
|
-
headers = list.list_headers
|
170
|
+
headers = list.list_headers.dup
|
88
171
|
headers['x-beenthere'] = address
|
172
|
+
headers['x-mlist-version'] = MList.version.to_s
|
89
173
|
headers.update(bounce_headers)
|
90
174
|
headers.delete_if {|k,v| v.nil?}
|
91
175
|
end
|
@@ -118,36 +202,55 @@ module MList
|
|
118
202
|
def prepare_delivery(message, options)
|
119
203
|
message.identifier = outgoing_server.generate_message_id
|
120
204
|
message.created_at = options[:delivery_time]
|
205
|
+
message.subject = clean_subject(message.subject)
|
121
206
|
returning(message.delivery) do |delivery|
|
122
207
|
delivery.date = message.created_at
|
123
208
|
delivery.message_id = message.identifier
|
124
209
|
delivery.mailer = message.mailer
|
125
210
|
delivery.headers = list_headers
|
126
|
-
delivery.subject = list_subject(message)
|
211
|
+
delivery.subject = list_subject(message.subject)
|
127
212
|
delivery.to = address
|
128
213
|
delivery.bcc = message.recipients.collect(&:email_address)
|
129
214
|
delivery.reply_to = "#{label} <#{post_url}>"
|
215
|
+
prepare_list_footer(delivery, message)
|
130
216
|
end
|
131
217
|
end
|
132
218
|
|
133
|
-
def
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
219
|
+
def prepare_list_footer(delivery, message)
|
220
|
+
text_plain_part = delivery.text_plain_part
|
221
|
+
return unless text_plain_part
|
222
|
+
|
223
|
+
content = strip_list_footers(text_plain_part.body)
|
224
|
+
content << "\n\n" unless content.end_with?("\n\n")
|
225
|
+
content << list_footer(message)
|
226
|
+
text_plain_part.body = content
|
227
|
+
end
|
228
|
+
|
229
|
+
def list_footer(message)
|
230
|
+
content = list.footer_content(message)
|
231
|
+
"#{FOOTER_BLOCK_START}\n#{content}\n#{FOOTER_BLOCK_END}"
|
232
|
+
end
|
233
|
+
|
234
|
+
def list_subject(string)
|
235
|
+
list_subject = string.dup
|
236
|
+
if list_subject =~ REGARD_RE
|
237
|
+
"Re: #{subject_prefix} #{remove_regard(list_subject)}"
|
238
|
+
else
|
239
|
+
"#{subject_prefix} #{list_subject}"
|
139
240
|
end
|
140
|
-
subject.gsub!(/\[.*?\]/, '')
|
141
|
-
subject.strip!
|
142
|
-
"#{prefix} #{subject}"
|
143
241
|
end
|
144
242
|
|
145
243
|
def find_thread(message, options)
|
146
|
-
if options[:search_parent]
|
147
|
-
message.parent_identifier = message.email.parent_identifier(self)
|
148
|
-
message.parent = messages.find_by_identifier(message.parent_identifier)
|
149
|
-
end
|
244
|
+
message.parent = find_parent_message(message.email) if message.email && options[:search_parent]
|
150
245
|
message.parent ? message.parent.thread : threads.build
|
151
246
|
end
|
247
|
+
|
248
|
+
def subject_prefix_regex
|
249
|
+
@subject_prefix_regex ||= Regexp.new(Regexp.escape(subject_prefix) + ' ')
|
250
|
+
end
|
251
|
+
|
252
|
+
def subject_prefix
|
253
|
+
@subject_prefix ||= "[#{label}]"
|
254
|
+
end
|
152
255
|
end
|
153
256
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'mlist/manager/notifier'
|
2
|
+
|
3
|
+
module MList
|
4
|
+
|
5
|
+
# The interface of list managers.
|
6
|
+
#
|
7
|
+
# A module is provided instead of a base class to allow implementors to
|
8
|
+
# subclass whatever they like. Practically speaking, they can create an
|
9
|
+
# ActiveRecord subclass.
|
10
|
+
#
|
11
|
+
module Manager
|
12
|
+
|
13
|
+
# Answers an enumeration of MList::List implementations to which the given
|
14
|
+
# email should be published.
|
15
|
+
#
|
16
|
+
def lists(email)
|
17
|
+
raise 'implement in your list manager'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Answers the MList::Manager::Notifier of this list manager. Includers of
|
21
|
+
# this module may initialize the @notifier instance variable with their
|
22
|
+
# own implementation/subclass to generate custom content for the different
|
23
|
+
# notices.
|
24
|
+
#
|
25
|
+
def notifier
|
26
|
+
@notifier ||= MList::Manager::Notifier.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -2,6 +2,8 @@ module MList
|
|
2
2
|
module Manager
|
3
3
|
|
4
4
|
class Database
|
5
|
+
include ::MList::Manager
|
6
|
+
|
5
7
|
def create_list(address, attributes = {})
|
6
8
|
attributes = {
|
7
9
|
:address => address,
|
@@ -16,7 +18,7 @@ module MList
|
|
16
18
|
end
|
17
19
|
|
18
20
|
def no_lists_found(email)
|
19
|
-
#
|
21
|
+
# TODO: Move to notifier
|
20
22
|
end
|
21
23
|
|
22
24
|
class List < ActiveRecord::Base
|
@@ -24,6 +26,10 @@ module MList
|
|
24
26
|
|
25
27
|
has_many :subscribers, :dependent => :delete_all
|
26
28
|
|
29
|
+
def label
|
30
|
+
self[:label]
|
31
|
+
end
|
32
|
+
|
27
33
|
def list_id
|
28
34
|
"#{self.class.name}#{id}"
|
29
35
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module MList
|
2
|
+
module Manager
|
3
|
+
|
4
|
+
# Constructs the notices that are sent to list subscribers. Applications
|
5
|
+
# may subclass this to customize the content of a notice delivery.
|
6
|
+
#
|
7
|
+
class Notifier
|
8
|
+
|
9
|
+
# Answers the delivery that will be sent to a subscriber when an
|
10
|
+
# MList::List indicates that the distribution of an email from that
|
11
|
+
# subscriber has been blocked.
|
12
|
+
#
|
13
|
+
def subscriber_blocked(list, email, subscriber)
|
14
|
+
delivery = MList::Util::TMailBuilder.new(TMail::Mail.new)
|
15
|
+
delivery.write_header('x-mlist-loop', 'notice')
|
16
|
+
delivery.write_header('x-mlist-notice', 'subscriber_blocked')
|
17
|
+
delivery.to = subscriber.email_address
|
18
|
+
delivery.from = "mlist-#{list.address}"
|
19
|
+
prepare_subscriber_blocked_content(list, email, subscriber, delivery)
|
20
|
+
delivery
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
def prepare_subscriber_blocked_content(list, email, subscriber, delivery)
|
25
|
+
delivery.set_content_type('text/plain')
|
26
|
+
delivery.body = %{Although you are a subscriber to this list, your message cannot be posted at this time. Please contact the administrator of the list.}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/mlist/message.rb
CHANGED
@@ -10,6 +10,8 @@ module MList
|
|
10
10
|
belongs_to :mail_list, :class_name => 'MList::MailList', :counter_cache => :messages_count
|
11
11
|
belongs_to :thread, :class_name => 'MList::Thread', :counter_cache => :messages_count
|
12
12
|
|
13
|
+
after_destroy :delete_unreferenced_email
|
14
|
+
|
13
15
|
# A temporary storage of recipient subscribers, obtained from
|
14
16
|
# MList::Lists. This list is not available when a message is reloaded.
|
15
17
|
#
|
@@ -73,13 +75,29 @@ module MList
|
|
73
75
|
text_to_html(text_for_reply)
|
74
76
|
end
|
75
77
|
|
76
|
-
|
78
|
+
def parent_with_identifier_capture=(parent)
|
79
|
+
if parent
|
80
|
+
self.parent_without_identifier_capture = parent
|
81
|
+
self.parent_identifier = parent.identifier
|
82
|
+
else
|
83
|
+
self.parent_without_identifier_capture = nil
|
84
|
+
self.parent_identifier = nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
alias_method_chain :parent=, :identifier_capture
|
88
|
+
|
89
|
+
# Answers the subject with 'Re:' prefixed. Note that it is the
|
90
|
+
# responsibility of the MList::MailList to perform any processing of the
|
91
|
+
# persisted subject (ie, cleaning up labels, etc).
|
92
|
+
#
|
93
|
+
# message.subject = '[List Label] Re: The new Chrome Browser from Google'
|
94
|
+
# message.subject_for_reply => 'Re: [List Label] The new Chrome Browser from Google'
|
77
95
|
#
|
78
96
|
# message.subject = 'Re: [List Label] Re: The new Chrome Browser from Google'
|
79
|
-
# message.subject_for_reply => 'Re: The new Chrome Browser from Google'
|
97
|
+
# message.subject_for_reply => 'Re: [List Label] The new Chrome Browser from Google'
|
80
98
|
#
|
81
99
|
def subject_for_reply
|
82
|
-
"Re: #{
|
100
|
+
subject =~ REGARD_RE ? subject : "Re: #{subject}"
|
83
101
|
end
|
84
102
|
|
85
103
|
# Answers the subscriber from which this message comes.
|
@@ -113,5 +131,10 @@ module MList
|
|
113
131
|
@subscriber = self.subscriber_address = self.subscriber_type = self.subscriber_id = nil
|
114
132
|
end
|
115
133
|
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def delete_unreferenced_email
|
137
|
+
email.destroy unless MList::Message.count(:conditions => "email_id = #{email_id} and id != #{id}") > 0
|
138
|
+
end
|
116
139
|
end
|
117
140
|
end
|
data/lib/mlist/server.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
module MList
|
2
2
|
class Server
|
3
|
-
attr_reader :list_manager, :email_server
|
3
|
+
attr_reader :list_manager, :email_server, :notifier
|
4
4
|
|
5
5
|
def initialize(config)
|
6
6
|
@list_manager = config[:list_manager]
|
7
7
|
@email_server = config[:email_server]
|
8
|
+
@notifier = MList::Manager::Notifier.new
|
8
9
|
@email_server.receiver(self)
|
9
10
|
end
|
10
11
|
|
@@ -24,23 +25,38 @@ module MList
|
|
24
25
|
end
|
25
26
|
|
26
27
|
protected
|
27
|
-
def process_bounce(list, email)
|
28
|
-
list.bounce(email)
|
29
|
-
end
|
30
|
-
|
31
28
|
def process_post(lists, email)
|
32
29
|
lists.each do |list|
|
33
30
|
next if email.been_here?(list)
|
34
31
|
if list.subscriber?(email.from_address)
|
35
|
-
|
36
|
-
mail_list(list).process_email(email)
|
37
|
-
else
|
38
|
-
list.inactive_post(email)
|
39
|
-
end
|
32
|
+
publish_if_list_active(list, email)
|
40
33
|
else
|
41
34
|
list.non_subscriber_post(email)
|
42
35
|
end
|
43
36
|
end
|
44
37
|
end
|
38
|
+
|
39
|
+
def publish_if_list_active(list, email)
|
40
|
+
if list.active?
|
41
|
+
subscriber = list.subscriber(email.from_address)
|
42
|
+
publish_unless_blocked(list, email, subscriber)
|
43
|
+
else
|
44
|
+
list.inactive_post(email)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def publish_unless_blocked(list, email, subscriber)
|
49
|
+
if list.blocked?(subscriber)
|
50
|
+
notice_delivery = notifier.subscriber_blocked(list, email, subscriber)
|
51
|
+
email_server.deliver(notice_delivery.tmail)
|
52
|
+
else
|
53
|
+
mail_list(list).process_email(email, subscriber)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_bounce(list, email)
|
58
|
+
list.bounce(email)
|
59
|
+
end
|
60
|
+
|
45
61
|
end
|
46
62
|
end
|
data/lib/mlist/thread.rb
CHANGED
@@ -5,34 +5,94 @@ module MList
|
|
5
5
|
belongs_to :mail_list, :class_name => 'MList::MailList', :counter_cache => :threads_count
|
6
6
|
has_many :messages, :class_name => 'MList::Message', :dependent => :delete_all
|
7
7
|
|
8
|
-
def children(message)
|
9
|
-
messages.select {|m| m.parent == message}
|
10
|
-
end
|
11
|
-
|
12
8
|
def first?(message)
|
13
|
-
|
9
|
+
tree_order.first == message
|
14
10
|
end
|
15
11
|
|
16
12
|
def last?(message)
|
17
|
-
|
13
|
+
tree_order.last == message
|
18
14
|
end
|
19
15
|
|
20
16
|
def next(message)
|
21
|
-
i =
|
17
|
+
i = tree_order.index(message)
|
22
18
|
messages[i + 1] unless messages.size < i
|
23
19
|
end
|
24
20
|
|
25
21
|
def previous(message)
|
26
|
-
i =
|
22
|
+
i = tree_order.index(message)
|
27
23
|
messages[i - 1] if i > 0
|
28
24
|
end
|
29
25
|
|
30
|
-
def roots
|
31
|
-
messages.select {|m| m.parent.nil?}
|
32
|
-
end
|
33
|
-
|
34
26
|
def subject
|
35
27
|
messages.first.subject
|
36
28
|
end
|
29
|
+
|
30
|
+
# Answers a tree of messages.
|
31
|
+
#
|
32
|
+
# The nodes of the tree are decorated to act like a linked list, providing
|
33
|
+
# pointers to _next_ and _previous_ in the tree.
|
34
|
+
#
|
35
|
+
def tree
|
36
|
+
return nil if messages.size == 0
|
37
|
+
build_tree unless @tree
|
38
|
+
@tree
|
39
|
+
end
|
40
|
+
|
41
|
+
def tree_order
|
42
|
+
build_tree unless @tree
|
43
|
+
@tree_order
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def build_tree
|
48
|
+
nodes = messages.collect do |m|
|
49
|
+
m.parent = messages.detect {|pm| pm.id == m.parent_id}
|
50
|
+
Node.new(m)
|
51
|
+
end
|
52
|
+
|
53
|
+
nodes.each do |node|
|
54
|
+
if parent_node = nodes.detect {|n| n == node.parent}
|
55
|
+
node.parent_node = parent_node
|
56
|
+
parent_node.children << node
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@tree_order = []
|
61
|
+
previous_node = nil
|
62
|
+
nodes.first.visit do |node|
|
63
|
+
@tree_order << node
|
64
|
+
if previous_node
|
65
|
+
node.previous = previous_node
|
66
|
+
previous_node.next = node
|
67
|
+
end
|
68
|
+
previous_node = node
|
69
|
+
end
|
70
|
+
|
71
|
+
@tree = @tree_order.first
|
72
|
+
end
|
73
|
+
|
74
|
+
class Node < DelegateClass(Message)
|
75
|
+
attr_accessor :parent_node, :previous, :next
|
76
|
+
attr_reader :children
|
77
|
+
|
78
|
+
def initialize(message)
|
79
|
+
super
|
80
|
+
@children = []
|
81
|
+
end
|
82
|
+
|
83
|
+
def leaf?
|
84
|
+
children.empty?
|
85
|
+
end
|
86
|
+
|
87
|
+
def root?
|
88
|
+
parent_node.nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
def visit(&visitor)
|
92
|
+
visitor.call self
|
93
|
+
children.each {|c| c.visit(&visitor)}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
37
97
|
end
|
38
98
|
end
|
@@ -1,6 +1,83 @@
|
|
1
1
|
module MList
|
2
2
|
module Util
|
3
3
|
|
4
|
+
class HtmlTextExtraction
|
5
|
+
def initialize(html)
|
6
|
+
@doc = Hpricot(html)
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute
|
10
|
+
@text, @anchors = '', []
|
11
|
+
@doc.each_child do |node|
|
12
|
+
extract_text_from_node(node) if Hpricot::Elem::Trav === node
|
13
|
+
end
|
14
|
+
@text.strip!
|
15
|
+
unless @anchors.empty?
|
16
|
+
refs = []
|
17
|
+
@anchors.each_with_index do |href, i|
|
18
|
+
refs << "[#{i+1}] #{href}"
|
19
|
+
end
|
20
|
+
@text << "\n\n--\n#{refs.join("\n")}"
|
21
|
+
end
|
22
|
+
@text
|
23
|
+
end
|
24
|
+
|
25
|
+
def extract_text_from_node(node)
|
26
|
+
case node.name
|
27
|
+
when 'head'
|
28
|
+
when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
|
29
|
+
@text << node.inner_text
|
30
|
+
@text << "\n\n"
|
31
|
+
when 'br'
|
32
|
+
@text << "\n"
|
33
|
+
when 'ol'
|
34
|
+
node.children_of_type('li').each_with_index do |li, i|
|
35
|
+
@text << " #{i+1}. #{li.inner_text}"
|
36
|
+
@text << "\n\n"
|
37
|
+
end
|
38
|
+
when 'ul'
|
39
|
+
node.children_of_type('li').each do |li|
|
40
|
+
@text << " * #{li.inner_text.strip}"
|
41
|
+
@text << "\n\n"
|
42
|
+
end
|
43
|
+
when 'strong'
|
44
|
+
@text << "*#{node.inner_text}*"
|
45
|
+
when 'em'
|
46
|
+
@text << "_#{node.inner_text}_"
|
47
|
+
when 'dl'
|
48
|
+
node.traverse_element('dt', 'dd') do |dt_dd|
|
49
|
+
extract_text_from_node(dt_dd)
|
50
|
+
end
|
51
|
+
when 'a'
|
52
|
+
@anchors << node['href']
|
53
|
+
extract_text_from_text_node(node)
|
54
|
+
@text << "[#{@anchors.size}]"
|
55
|
+
when 'p', 'dt', 'dd'
|
56
|
+
extract_text_from_children(node)
|
57
|
+
@text.rstrip!
|
58
|
+
@text << "\n\n"
|
59
|
+
else
|
60
|
+
extract_text_from_children(node)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def extract_text_from_children(elem)
|
65
|
+
elem.each_child do |node|
|
66
|
+
case node
|
67
|
+
when Hpricot::Text::Trav
|
68
|
+
extract_text_from_text_node(node)
|
69
|
+
when Hpricot::Elem::Trav
|
70
|
+
extract_text_from_node(node)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def extract_text_from_text_node(node)
|
76
|
+
text = @text.end_with?("\n") ? node.inner_text.lstrip : node.inner_text
|
77
|
+
@text << text.gsub(/\s{2,}/, ' ').sub(/\n/, '')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
4
81
|
module EmailHelpers
|
5
82
|
def sanitize_header(charset, name, *values)
|
6
83
|
header_sanitizer(name).call(charset, *values)
|
@@ -10,12 +87,15 @@ module MList
|
|
10
87
|
Util.default_header_sanitizers[name]
|
11
88
|
end
|
12
89
|
|
90
|
+
def html_to_text(html)
|
91
|
+
HtmlTextExtraction.new(html).execute
|
92
|
+
end
|
93
|
+
|
13
94
|
def normalize_new_lines(text)
|
14
95
|
text.to_s.gsub(/\r\n?/, "\n")
|
15
96
|
end
|
16
97
|
|
17
98
|
BRACKETS_RE = /\A<(.*?)>\Z/
|
18
|
-
|
19
99
|
def bracket(string)
|
20
100
|
string.blank? || string =~ BRACKETS_RE ? string : "<#{string}>"
|
21
101
|
end
|
@@ -24,9 +104,12 @@ module MList
|
|
24
104
|
string =~ BRACKETS_RE ? $1 : string
|
25
105
|
end
|
26
106
|
|
107
|
+
REGARD_RE = /(^|[^\w])re: /i
|
27
108
|
def remove_regard(string)
|
28
|
-
|
29
|
-
|
109
|
+
while string =~ REGARD_RE
|
110
|
+
string = string.sub(REGARD_RE, ' ')
|
111
|
+
end
|
112
|
+
string.strip
|
30
113
|
end
|
31
114
|
|
32
115
|
def text_to_html(text)
|
@@ -19,8 +19,8 @@ module MList
|
|
19
19
|
when 'text/html'
|
20
20
|
tmail.body.strip
|
21
21
|
when 'multipart/alternative'
|
22
|
-
|
23
|
-
|
22
|
+
html_part = tmail.parts.detect {|part| part.content_type == 'text/html'}
|
23
|
+
html_part.body.strip if html_part
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -33,19 +33,45 @@ module MList
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def text
|
36
|
-
|
36
|
+
text_content = ''
|
37
|
+
extract_text_content(tmail, text_content)
|
38
|
+
return text_content unless text_content.blank?
|
39
|
+
|
40
|
+
html_content = ''
|
41
|
+
extract_html_content(tmail, html_content)
|
42
|
+
return html_to_text(html_content) unless html_content.blank?
|
43
|
+
|
44
|
+
return nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# Answers the first text/plain part it can find, the tmail itself if
|
48
|
+
# it's content type is text/plain.
|
49
|
+
#
|
50
|
+
def text_plain_part(part = tmail)
|
51
|
+
case part.content_type
|
52
|
+
when 'text/plain'
|
53
|
+
part
|
54
|
+
when 'multipart/alternative'
|
55
|
+
part.parts.detect {|part| text_plain_part(part)}
|
56
|
+
end
|
37
57
|
end
|
38
58
|
|
39
59
|
private
|
60
|
+
def extract_html_content(part, collector)
|
61
|
+
case part.content_type
|
62
|
+
when 'text/html'
|
63
|
+
collector << part.body.strip
|
64
|
+
when 'multipart/alternative', 'multipart/mixed', 'multipart/related'
|
65
|
+
part.parts.each {|part| extract_html_content(part, collector)}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
40
69
|
def extract_text_content(part, collector)
|
41
70
|
case part.content_type
|
42
71
|
when 'text/plain'
|
43
72
|
collector << part.body.strip
|
44
|
-
when 'multipart/alternative'
|
45
|
-
|
46
|
-
collector << text_part.body.strip if text_part
|
47
|
-
when 'multipart/mixed', 'multipart/related'
|
48
|
-
part.parts.each {|mixed_part| extract_text_content(mixed_part, collector)}
|
73
|
+
when 'multipart/alternative', 'multipart/mixed', 'multipart/related'
|
74
|
+
part.parts.each {|part| extract_text_content(part, collector)}
|
49
75
|
end
|
50
76
|
end
|
51
77
|
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,28 @@
|
|
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
|
26
|
+
end
|
27
|
+
alias_method_chain :reload_application, :plugin_record_support
|
28
|
+
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.
|
4
|
+
version: 0.1.5
|
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-02-
|
12
|
+
date: 2009-02-17 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -25,6 +25,7 @@ files:
|
|
25
25
|
- CHANGELOG
|
26
26
|
- Rakefile
|
27
27
|
- README
|
28
|
+
- TODO
|
28
29
|
- VERSION.yml
|
29
30
|
- lib/mlist
|
30
31
|
- lib/mlist/email.rb
|
@@ -41,6 +42,8 @@ files:
|
|
41
42
|
- lib/mlist/mail_list.rb
|
42
43
|
- lib/mlist/manager
|
43
44
|
- lib/mlist/manager/database.rb
|
45
|
+
- lib/mlist/manager/notifier.rb
|
46
|
+
- lib/mlist/manager.rb
|
44
47
|
- lib/mlist/message.rb
|
45
48
|
- lib/mlist/server.rb
|
46
49
|
- lib/mlist/thread.rb
|
@@ -53,6 +56,7 @@ files:
|
|
53
56
|
- lib/mlist/util.rb
|
54
57
|
- lib/mlist.rb
|
55
58
|
- lib/pop_ssl.rb
|
59
|
+
- rails/init.rb
|
56
60
|
has_rdoc: false
|
57
61
|
homepage: http://github.com/aiwilliams/mlist
|
58
62
|
post_install_message:
|