mlist 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/CHANGELOG +59 -0
  2. data/README +204 -0
  3. data/Rakefile +27 -0
  4. data/TODO +36 -0
  5. data/VERSION.yml +4 -0
  6. data/lib/mlist/email.rb +69 -0
  7. data/lib/mlist/email_post.rb +126 -0
  8. data/lib/mlist/email_server/base.rb +33 -0
  9. data/lib/mlist/email_server/default.rb +31 -0
  10. data/lib/mlist/email_server/fake.rb +16 -0
  11. data/lib/mlist/email_server/pop.rb +28 -0
  12. data/lib/mlist/email_server/smtp.rb +24 -0
  13. data/lib/mlist/email_server.rb +2 -0
  14. data/lib/mlist/email_subscriber.rb +6 -0
  15. data/lib/mlist/list.rb +183 -0
  16. data/lib/mlist/mail_list.rb +277 -0
  17. data/lib/mlist/manager/database.rb +48 -0
  18. data/lib/mlist/manager/notifier.rb +31 -0
  19. data/lib/mlist/manager.rb +30 -0
  20. data/lib/mlist/message.rb +150 -0
  21. data/lib/mlist/server.rb +62 -0
  22. data/lib/mlist/thread.rb +98 -0
  23. data/lib/mlist/util/email_helpers.rb +155 -0
  24. data/lib/mlist/util/header_sanitizer.rb +71 -0
  25. data/lib/mlist/util/quoting.rb +70 -0
  26. data/lib/mlist/util/tmail_builder.rb +42 -0
  27. data/lib/mlist/util/tmail_methods.rb +138 -0
  28. data/lib/mlist/util.rb +12 -0
  29. data/lib/mlist.rb +46 -0
  30. data/lib/pop_ssl.rb +999 -0
  31. data/rails/init.rb +22 -0
  32. data/spec/fixtures/schema.rb +94 -0
  33. data/spec/integration/date_formats_spec.rb +12 -0
  34. data/spec/integration/mlist_spec.rb +232 -0
  35. data/spec/integration/pop_email_server_spec.rb +22 -0
  36. data/spec/integration/proof_spec.rb +74 -0
  37. data/spec/matchers/equal_tmail.rb +53 -0
  38. data/spec/matchers/have_address.rb +48 -0
  39. data/spec/matchers/have_header.rb +104 -0
  40. data/spec/models/email_post_spec.rb +100 -0
  41. data/spec/models/email_server/base_spec.rb +11 -0
  42. data/spec/models/email_spec.rb +54 -0
  43. data/spec/models/mail_list_spec.rb +469 -0
  44. data/spec/models/message_spec.rb +109 -0
  45. data/spec/models/thread_spec.rb +83 -0
  46. data/spec/models/util/email_helpers_spec.rb +47 -0
  47. data/spec/models/util/header_sanitizer_spec.rb +19 -0
  48. data/spec/models/util/quoting_spec.rb +96 -0
  49. data/spec/spec_helper.rb +76 -0
  50. 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
@@ -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
@@ -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;', 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 = ("&nbsp;" * $1.length) + $2 if line =~ /^(\s+)(.*?)$/
133
+ line = %{<span class="quote">#{line}</span>} if line =~ /^(&gt;|[|]|[A-Za-z]+&gt;)/
134
+ line = line.gsub(/\s\s/, ' &nbsp;')
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 = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
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