mlist 0.1.9
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 +59 -0
- data/README +204 -0
- data/Rakefile +27 -0
- data/TODO +36 -0
- data/VERSION.yml +4 -0
- data/lib/mlist/email.rb +69 -0
- data/lib/mlist/email_post.rb +126 -0
- data/lib/mlist/email_server/base.rb +33 -0
- data/lib/mlist/email_server/default.rb +31 -0
- data/lib/mlist/email_server/fake.rb +16 -0
- data/lib/mlist/email_server/pop.rb +28 -0
- data/lib/mlist/email_server/smtp.rb +24 -0
- data/lib/mlist/email_server.rb +2 -0
- data/lib/mlist/email_subscriber.rb +6 -0
- data/lib/mlist/list.rb +183 -0
- data/lib/mlist/mail_list.rb +277 -0
- data/lib/mlist/manager/database.rb +48 -0
- data/lib/mlist/manager/notifier.rb +31 -0
- data/lib/mlist/manager.rb +30 -0
- data/lib/mlist/message.rb +150 -0
- data/lib/mlist/server.rb +62 -0
- data/lib/mlist/thread.rb +98 -0
- data/lib/mlist/util/email_helpers.rb +155 -0
- data/lib/mlist/util/header_sanitizer.rb +71 -0
- data/lib/mlist/util/quoting.rb +70 -0
- data/lib/mlist/util/tmail_builder.rb +42 -0
- data/lib/mlist/util/tmail_methods.rb +138 -0
- data/lib/mlist/util.rb +12 -0
- data/lib/mlist.rb +46 -0
- data/lib/pop_ssl.rb +999 -0
- data/rails/init.rb +22 -0
- data/spec/fixtures/schema.rb +94 -0
- data/spec/integration/date_formats_spec.rb +12 -0
- data/spec/integration/mlist_spec.rb +232 -0
- data/spec/integration/pop_email_server_spec.rb +22 -0
- data/spec/integration/proof_spec.rb +74 -0
- data/spec/matchers/equal_tmail.rb +53 -0
- data/spec/matchers/have_address.rb +48 -0
- data/spec/matchers/have_header.rb +104 -0
- data/spec/models/email_post_spec.rb +100 -0
- data/spec/models/email_server/base_spec.rb +11 -0
- data/spec/models/email_spec.rb +54 -0
- data/spec/models/mail_list_spec.rb +469 -0
- data/spec/models/message_spec.rb +109 -0
- data/spec/models/thread_spec.rb +83 -0
- data/spec/models/util/email_helpers_spec.rb +47 -0
- data/spec/models/util/header_sanitizer_spec.rb +19 -0
- data/spec/models/util/quoting_spec.rb +96 -0
- data/spec/spec_helper.rb +76 -0
- metadata +103 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
module MList
|
2
|
+
|
3
|
+
class Message < ActiveRecord::Base
|
4
|
+
set_table_name 'mlist_messages'
|
5
|
+
|
6
|
+
include MList::Util::EmailHelpers
|
7
|
+
|
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
|
12
|
+
|
13
|
+
after_destroy :delete_unreferenced_email
|
14
|
+
|
15
|
+
# A temporary storage of recipient subscribers, obtained from
|
16
|
+
# MList::Lists. This list is not available when a message is reloaded.
|
17
|
+
#
|
18
|
+
attr_accessor :recipients
|
19
|
+
|
20
|
+
# Answers an MList::TMailBuilder for assembling the TMail::Mail object
|
21
|
+
# that will be fit for delivery. If this is not a new message, the
|
22
|
+
# delivery will be updated to reflect the message-id, x-mailer, etc. of
|
23
|
+
# this message.
|
24
|
+
#
|
25
|
+
def delivery
|
26
|
+
@delivery ||= begin
|
27
|
+
d = MList::Util::TMailBuilder.new(TMail::Mail.parse(email.source))
|
28
|
+
unless new_record?
|
29
|
+
d.message_id = self.identifier
|
30
|
+
d.mailer = self.mailer
|
31
|
+
d.date = self.created_at
|
32
|
+
end
|
33
|
+
d
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def email_with_capture=(email)
|
38
|
+
self.subject = email.subject
|
39
|
+
self.mailer = email.mailer
|
40
|
+
self.email_without_capture = email
|
41
|
+
end
|
42
|
+
alias_method_chain :email=, :capture
|
43
|
+
|
44
|
+
# Answers the html content of the message.
|
45
|
+
#
|
46
|
+
def html
|
47
|
+
email.html
|
48
|
+
end
|
49
|
+
|
50
|
+
# Answers the text content of the message.
|
51
|
+
#
|
52
|
+
def text
|
53
|
+
email.text
|
54
|
+
end
|
55
|
+
|
56
|
+
# Answers the text content of the message as HTML. The structure of this
|
57
|
+
# output is very simple. For examples of what it can handle, please check
|
58
|
+
# out the spec documents for MList::Util::EmailHelpers.
|
59
|
+
#
|
60
|
+
def text_html
|
61
|
+
text_to_html(text)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Answers text suitable for creating a reply message.
|
65
|
+
#
|
66
|
+
def text_for_reply
|
67
|
+
timestamp = email.date.to_s(:mlist_reply_timestamp)
|
68
|
+
"On #{timestamp}, #{email.from} wrote:\n#{text_to_quoted(text)}"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Answers text suitable for creating a reply message, converted to the
|
72
|
+
# same simple html of _text_html_.
|
73
|
+
#
|
74
|
+
def html_for_reply
|
75
|
+
text_to_html(text_for_reply)
|
76
|
+
end
|
77
|
+
|
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 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
|
+
|
99
|
+
# Answers the subject with 'Re:' prefixed. Note that it is the
|
100
|
+
# responsibility of the MList::MailList to perform any processing of the
|
101
|
+
# persisted subject (ie, cleaning up labels, etc).
|
102
|
+
#
|
103
|
+
# message.subject = '[List Label] Re: The new Chrome Browser from Google'
|
104
|
+
# message.subject_for_reply => 'Re: [List Label] The new Chrome Browser from Google'
|
105
|
+
#
|
106
|
+
# message.subject = 'Re: [List Label] Re: The new Chrome Browser from Google'
|
107
|
+
# message.subject_for_reply => 'Re: [List Label] The new Chrome Browser from Google'
|
108
|
+
#
|
109
|
+
def subject_for_reply
|
110
|
+
subject =~ REGARD_RE ? subject : "Re: #{subject}"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Answers the subscriber from which this message comes.
|
114
|
+
#
|
115
|
+
def subscriber
|
116
|
+
@subscriber ||= begin
|
117
|
+
if subscriber_type? && subscriber_id?
|
118
|
+
subscriber_type.constantize.find(subscriber_id)
|
119
|
+
elsif subscriber_address?
|
120
|
+
MList::EmailSubscriber.new(subscriber_address)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Assigns the subscriber from which this message comes.
|
126
|
+
#
|
127
|
+
def subscriber=(subscriber)
|
128
|
+
case subscriber
|
129
|
+
when ActiveRecord::Base
|
130
|
+
@subscriber = subscriber
|
131
|
+
self.subscriber_address = subscriber.email_address
|
132
|
+
self.subscriber_type = subscriber.class.base_class.name
|
133
|
+
self.subscriber_id = subscriber.id
|
134
|
+
when MList::EmailSubscriber
|
135
|
+
@subscriber = subscriber
|
136
|
+
self.subscriber_address = subscriber.email_address
|
137
|
+
self.subscriber_type = self.subscriber_id = nil
|
138
|
+
when String
|
139
|
+
self.subscriber = MList::EmailSubscriber.new(subscriber)
|
140
|
+
else
|
141
|
+
@subscriber = self.subscriber_address = self.subscriber_type = self.subscriber_id = nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
def delete_unreferenced_email
|
147
|
+
email.destroy unless MList::Message.count(:conditions => "email_id = #{email_id} and id != #{id}") > 0
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/mlist/server.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module MList
|
2
|
+
class Server
|
3
|
+
attr_reader :list_manager, :email_server, :notifier
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@list_manager = config[:list_manager]
|
7
|
+
@email_server = config[:email_server]
|
8
|
+
@notifier = MList::Manager::Notifier.new
|
9
|
+
@email_server.receiver(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def receive_email(email)
|
13
|
+
lists = list_manager.lists(email)
|
14
|
+
if lists.empty?
|
15
|
+
list_manager.no_lists_found(email)
|
16
|
+
elsif email.bounce?
|
17
|
+
process_bounce(lists.first, email)
|
18
|
+
else
|
19
|
+
process_post(lists, email)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def mail_list(list)
|
24
|
+
MailList.find_or_create_by_list(list, @email_server)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
def process_post(lists, email)
|
29
|
+
lists.each do |list|
|
30
|
+
next if email.been_here?(list)
|
31
|
+
if list.subscriber?(email.from_address)
|
32
|
+
publish_if_list_active(list, email)
|
33
|
+
else
|
34
|
+
list.non_subscriber_post(email)
|
35
|
+
end
|
36
|
+
end
|
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
|
+
|
61
|
+
end
|
62
|
+
end
|
data/lib/mlist/thread.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
module MList
|
2
|
+
class Thread < ActiveRecord::Base
|
3
|
+
set_table_name 'mlist_threads'
|
4
|
+
|
5
|
+
belongs_to :mail_list, :class_name => 'MList::MailList', :counter_cache => :threads_count
|
6
|
+
has_many :messages, :class_name => 'MList::Message', :dependent => :delete_all
|
7
|
+
|
8
|
+
def first?(message)
|
9
|
+
tree_order.first == message
|
10
|
+
end
|
11
|
+
|
12
|
+
def last?(message)
|
13
|
+
tree_order.last == message
|
14
|
+
end
|
15
|
+
|
16
|
+
def next(message)
|
17
|
+
i = tree_order.index(message)
|
18
|
+
tree_order[i + 1] unless messages.size < i
|
19
|
+
end
|
20
|
+
|
21
|
+
def previous(message)
|
22
|
+
i = tree_order.index(message)
|
23
|
+
tree_order[i - 1] if i > 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def subject
|
27
|
+
messages.first.subject
|
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
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module MList
|
2
|
+
module Util
|
3
|
+
|
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
|
+
|
12
|
+
def initialize(html)
|
13
|
+
@doc = Hpricot(html.gsub(' ', NBSP))
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
@text, @anchors = '', []
|
18
|
+
@doc.each_child do |node|
|
19
|
+
extract_text_from_node(node) if Hpricot::Elem::Trav === node
|
20
|
+
end
|
21
|
+
@text.strip!
|
22
|
+
unless @anchors.empty?
|
23
|
+
refs = []
|
24
|
+
@anchors.each_with_index do |href, i|
|
25
|
+
refs << "[#{i+1}] #{href}"
|
26
|
+
end
|
27
|
+
@text << "\n\n--\n#{refs.join("\n")}"
|
28
|
+
end
|
29
|
+
@text.gsub(NBSP, ' ')
|
30
|
+
end
|
31
|
+
|
32
|
+
def extract_text_from_node(node)
|
33
|
+
case node.name
|
34
|
+
when 'head'
|
35
|
+
when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
|
36
|
+
@text << node.inner_text
|
37
|
+
@text << "\n\n"
|
38
|
+
when 'br'
|
39
|
+
@text << "\n"
|
40
|
+
when 'ol'
|
41
|
+
node.children_of_type('li').each_with_index do |li, i|
|
42
|
+
@text << " #{i+1}. #{li.inner_text}"
|
43
|
+
@text << "\n\n"
|
44
|
+
end
|
45
|
+
when 'ul'
|
46
|
+
node.children_of_type('li').each do |li|
|
47
|
+
@text << " * #{li.inner_text.strip}"
|
48
|
+
@text << "\n\n"
|
49
|
+
end
|
50
|
+
when 'strong'
|
51
|
+
@text << "*#{node.inner_text}*"
|
52
|
+
when 'em'
|
53
|
+
@text << "_#{node.inner_text}_"
|
54
|
+
when 'dl'
|
55
|
+
node.traverse_element('dt', 'dd') do |dt_dd|
|
56
|
+
extract_text_from_node(dt_dd)
|
57
|
+
end
|
58
|
+
when 'a'
|
59
|
+
@anchors << node['href']
|
60
|
+
extract_text_from_text_node(node)
|
61
|
+
@text << "[#{@anchors.size}]"
|
62
|
+
when 'p', 'dt', 'dd'
|
63
|
+
extract_text_from_children(node)
|
64
|
+
@text.rstrip!
|
65
|
+
@text << "\n\n"
|
66
|
+
else
|
67
|
+
extract_text_from_children(node)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_text_from_children(elem)
|
72
|
+
elem.each_child do |node|
|
73
|
+
case node
|
74
|
+
when Hpricot::Text::Trav
|
75
|
+
extract_text_from_text_node(node)
|
76
|
+
when Hpricot::Elem::Trav
|
77
|
+
extract_text_from_node(node)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def extract_text_from_text_node(node)
|
83
|
+
text = @text.end_with?("\n") ? node.inner_text.lstrip : node.inner_text
|
84
|
+
@text << text.gsub(/\s{2,}/, ' ').sub(/\n/, '')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
module EmailHelpers
|
89
|
+
def sanitize_header(charset, name, *values)
|
90
|
+
header_sanitizer(name).call(charset, *values)
|
91
|
+
end
|
92
|
+
|
93
|
+
def header_sanitizer(name)
|
94
|
+
Util.default_header_sanitizers[name]
|
95
|
+
end
|
96
|
+
|
97
|
+
def html_to_text(html)
|
98
|
+
HtmlTextExtraction.new(html).execute
|
99
|
+
end
|
100
|
+
|
101
|
+
def normalize_new_lines(text)
|
102
|
+
text.to_s.gsub(/\r\n?/, "\n")
|
103
|
+
end
|
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
|
+
|
111
|
+
BRACKETS_RE = /\A<(.*?)>\Z/
|
112
|
+
def bracket(string)
|
113
|
+
string.blank? || string =~ BRACKETS_RE ? string : "<#{string}>"
|
114
|
+
end
|
115
|
+
|
116
|
+
def remove_brackets(string)
|
117
|
+
string =~ BRACKETS_RE ? $1 : string
|
118
|
+
end
|
119
|
+
|
120
|
+
REGARD_RE = /(^|[^\w])re: /i
|
121
|
+
def remove_regard(string)
|
122
|
+
while string =~ REGARD_RE
|
123
|
+
string = string.sub(REGARD_RE, ' ')
|
124
|
+
end
|
125
|
+
string.strip
|
126
|
+
end
|
127
|
+
|
128
|
+
def text_to_html(text)
|
129
|
+
lines = normalize_new_lines(text).split("\n")
|
130
|
+
lines.collect! do |line|
|
131
|
+
line = escape_once(line)
|
132
|
+
line = (" " * $1.length) + $2 if line =~ /^(\s+)(.*?)$/
|
133
|
+
line = %{<span class="quote">#{line}</span>} if line =~ /^(>|[|]|[A-Za-z]+>)/
|
134
|
+
line = line.gsub(/\s\s/, ' ')
|
135
|
+
line
|
136
|
+
end
|
137
|
+
lines.join("<br />\n")
|
138
|
+
end
|
139
|
+
|
140
|
+
def text_to_quoted(text)
|
141
|
+
lines = normalize_new_lines(text).split("\n")
|
142
|
+
lines.collect! do |line|
|
143
|
+
'> ' + line
|
144
|
+
end
|
145
|
+
lines.join("\n")
|
146
|
+
end
|
147
|
+
|
148
|
+
HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"' }
|
149
|
+
def escape_once(text)
|
150
|
+
text.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| HTML_ESCAPE[special] }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module MList
|
2
|
+
module Util
|
3
|
+
|
4
|
+
class QuotingSanitizer
|
5
|
+
include Quoting
|
6
|
+
|
7
|
+
def initialize(method, bracket_urls)
|
8
|
+
@method, @bracket_urls = method, bracket_urls
|
9
|
+
end
|
10
|
+
|
11
|
+
def bracket_urls(values)
|
12
|
+
values.map do |value|
|
13
|
+
if value.include?('<') && value.include?('>')
|
14
|
+
value
|
15
|
+
else
|
16
|
+
"<#{value}>"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(charset, *values)
|
22
|
+
values = bracket_urls(values.flatten) if @bracket_urls
|
23
|
+
send(@method, charset, *values)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class HeaderSanitizerHash
|
28
|
+
def initialize
|
29
|
+
@hash = Hash.new
|
30
|
+
initialize_default_sanitizers
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize_default_sanitizers
|
34
|
+
self['message-id'] = quoter(:quote_address_if_necessary)
|
35
|
+
|
36
|
+
self['to'] = quoter(:quote_any_address_if_necessary)
|
37
|
+
self['cc'] = quoter(:quote_any_address_if_necessary)
|
38
|
+
self['bcc'] = quoter(:quote_any_address_if_necessary)
|
39
|
+
self['from'] = quoter(:quote_any_address_if_necessary)
|
40
|
+
self['reply-to'] = quoter(:quote_any_address_if_necessary)
|
41
|
+
self['subject'] = quoter(:quote_any_if_necessary)
|
42
|
+
|
43
|
+
self['sender'] = quoter(:quote_address_if_necessary)
|
44
|
+
self['errors-to'] = quoter(:quote_address_if_necessary)
|
45
|
+
self['in-reply-to'] = quoter(:quote_any_address_if_necessary)
|
46
|
+
self['x-mailer'] = quoter(:quote_if_necessary, false)
|
47
|
+
|
48
|
+
self['list-id'] = quoter(:quote_address_if_necessary)
|
49
|
+
self['list-help'] = quoter(:quote_address_if_necessary)
|
50
|
+
self['list-subscribe'] = quoter(:quote_address_if_necessary)
|
51
|
+
self['list-unsubscribe'] = quoter(:quote_address_if_necessary)
|
52
|
+
self['list-post'] = quoter(:quote_address_if_necessary)
|
53
|
+
self['list-owner'] = quoter(:quote_address_if_necessary)
|
54
|
+
self['list-archive'] = quoter(:quote_address_if_necessary)
|
55
|
+
end
|
56
|
+
|
57
|
+
def [](key)
|
58
|
+
@hash[key.downcase] ||= lambda { |charset, value| value }
|
59
|
+
end
|
60
|
+
|
61
|
+
def []=(key, value)
|
62
|
+
@hash[key.downcase] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
def quoter(method, bracket_urls = true)
|
66
|
+
QuotingSanitizer.new(method, bracket_urls)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module MList
|
2
|
+
module Util
|
3
|
+
|
4
|
+
# Copyright (c) 2004-2008 David Heinemeier Hansson
|
5
|
+
#
|
6
|
+
# Taken from ActionMailer. Modified to make charset first argument in all
|
7
|
+
# signatures, allowing for a consistent pattern of invocation.
|
8
|
+
#
|
9
|
+
module Quoting #:nodoc:
|
10
|
+
# Convert the given text into quoted printable format, with an instruction
|
11
|
+
# that the text be eventually interpreted in the given charset.
|
12
|
+
def quoted_printable(charset, text)
|
13
|
+
text = text.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }.
|
14
|
+
gsub( / /, "_" )
|
15
|
+
"=?#{charset}?Q?#{text}?="
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert the given character to quoted printable format, taking into
|
19
|
+
# account multi-byte characters (if executing with $KCODE="u", for instance)
|
20
|
+
def quoted_printable_encode(character)
|
21
|
+
result = ""
|
22
|
+
character.each_byte { |b| result << "=%02x" % b }
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
# A quick-and-dirty regexp for determining whether a string contains any
|
27
|
+
# characters that need escaping.
|
28
|
+
if !defined?(CHARS_NEEDING_QUOTING)
|
29
|
+
CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
|
30
|
+
end
|
31
|
+
|
32
|
+
# Quote the given text if it contains any "illegal" characters
|
33
|
+
def quote_if_necessary(charset, text)
|
34
|
+
text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding)
|
35
|
+
|
36
|
+
(text =~ CHARS_NEEDING_QUOTING) ?
|
37
|
+
quoted_printable(charset, text) :
|
38
|
+
text
|
39
|
+
end
|
40
|
+
|
41
|
+
# Quote any of the given strings if they contain any "illegal" characters
|
42
|
+
def quote_any_if_necessary(charset, *args)
|
43
|
+
args.map { |v| quote_if_necessary(charset, v) }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Quote the given address if it needs to be. The address may be a
|
47
|
+
# regular email address, or it can be a phrase followed by an address in
|
48
|
+
# brackets. The phrase is the only part that will be quoted, and only if
|
49
|
+
# it needs to be. This allows extended characters to be used in the
|
50
|
+
# "to", "from", "cc", "bcc" and "reply-to" headers.
|
51
|
+
def quote_address_if_necessary(charset, address)
|
52
|
+
if Array === address
|
53
|
+
address.map { |a| quote_address_if_necessary(charset, a) }
|
54
|
+
elsif address =~ /^(\S.*)\s+(<.*>)$/
|
55
|
+
address = $2
|
56
|
+
phrase = quote_if_necessary(charset, $1.gsub(/^['"](.*)['"]$/, '\1'))
|
57
|
+
"\"#{phrase.gsub(/\\/, '\&\&').gsub('"', '\\"')}\" #{address}"
|
58
|
+
else
|
59
|
+
address
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Quote any of the given addresses, if they need to be.
|
64
|
+
def quote_any_address_if_necessary(charset, *args)
|
65
|
+
args.map { |v| quote_address_if_necessary(charset, v) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module MList
|
2
|
+
module Util
|
3
|
+
|
4
|
+
class TMailBuilder
|
5
|
+
include EmailHelpers
|
6
|
+
include TMailReaders
|
7
|
+
include TMailWriters
|
8
|
+
|
9
|
+
attr_reader :tmail
|
10
|
+
|
11
|
+
def initialize(tmail)
|
12
|
+
@tmail = tmail
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_html_part(body)
|
16
|
+
part = TMail::Mail.new
|
17
|
+
part.body = normalize_new_lines(body)
|
18
|
+
part.set_content_type('text/html')
|
19
|
+
self.parts << part
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_text_part(body)
|
23
|
+
part = TMail::Mail.new
|
24
|
+
part.body = normalize_new_lines(body)
|
25
|
+
part.set_content_type('text/plain')
|
26
|
+
self.parts << part
|
27
|
+
end
|
28
|
+
|
29
|
+
# Provide delegation to *most* of the underlying TMail::Mail methods,
|
30
|
+
# excluding those overridden by this Module.
|
31
|
+
#
|
32
|
+
def method_missing(symbol, *args, &block) # :nodoc:
|
33
|
+
if tmail.respond_to?(symbol)
|
34
|
+
tmail.__send__(symbol, *args, &block)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|