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.
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