Skip to content
This repository
Browse code

A very thorough refactoring, resulting in new mail property setters a…

…nd support for attachments and multipart messages.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1359 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 359caef33ca71b37b3ea2feef0960beccfabf4de 1 parent 79d9794
Jamis Buck authored
6  actionmailer/CHANGELOG
... ...
@@ -1,5 +1,9 @@
1 1
 *SVN*
2 2
 
  3
+* Support attachments and multipart messages.
  4
+
  5
+* Added new accessors for the various mail properties.
  6
+
3 7
 * Fix to only perform the charset conversion if a 'from' and a 'to' charset are given (make no assumptions about what the charset was) #1276 [Jamis Buck]
4 8
 
5 9
 * Fix attachments and content-type problems #1276 [Jamis Buck]
@@ -117,4 +121,4 @@
117 121
 
118 122
 *0.3*
119 123
 
120  
-* First release
  124
+* First release
4  actionmailer/lib/action_mailer.rb
@@ -36,11 +36,13 @@
36 36
 
37 37
 require 'action_mailer/base'
38 38
 require 'action_mailer/mail_helper'
  39
+require 'action_mailer/quoting'
39 40
 require 'action_mailer/vendor/tmail'
40 41
 require 'net/smtp'
41 42
 
42 43
 ActionView::Base.class_eval { include MailHelper }
  44
+ActionMailer::Base.class_eval { include ActionMailer::Quoting }
43 45
 
44 46
 old_verbose, $VERBOSE = $VERBOSE, nil
45 47
 TMail::Encoder.const_set("MAX_LINE_LEN", 200)
46  
-$VERBOSE = old_verbose
  48
+$VERBOSE = old_verbose
58  actionmailer/lib/action_mailer/adv_attr_accessor.rb
... ...
@@ -0,0 +1,58 @@
  1
+module ActionMailer
  2
+  module AdvAttrAccessor
  3
+    def self.append_features(base)
  4
+      super
  5
+      base.extend(ClassMethods)
  6
+    end
  7
+
  8
+    module ClassMethods
  9
+
  10
+      def adv_attr_accessor(*names)
  11
+        names.each do |name|
  12
+          define_method("#{name}=") do |value|
  13
+            instance_variable_set("@#{name}", value)
  14
+          end
  15
+
  16
+          define_method(name) do |*parameters|
  17
+            raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1
  18
+            if parameters.empty?
  19
+              instance_variable_get("@#{name}")
  20
+            else
  21
+              instance_variable_set("@#{name}", parameters.first)
  22
+            end
  23
+          end
  24
+        end
  25
+      end
  26
+
  27
+    end
  28
+  end
  29
+end
  30
+module ActionMailer
  31
+  module AdvAttrAccessor
  32
+    def self.append_features(base)
  33
+      super
  34
+      base.extend(ClassMethods)
  35
+    end
  36
+
  37
+    module ClassMethods
  38
+
  39
+      def adv_attr_accessor(*names)
  40
+        names.each do |name|
  41
+          define_method("#{name}=") do |value|
  42
+            instance_variable_set("@#{name}", value)
  43
+          end
  44
+
  45
+          define_method(name) do |*parameters|
  46
+            raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1
  47
+            if parameters.empty?
  48
+              instance_variable_get("@#{name}")
  49
+            else
  50
+              instance_variable_set("@#{name}", parameters.first)
  51
+            end
  52
+          end
  53
+        end
  54
+      end
  55
+
  56
+    end
  57
+  end
  58
+end
323  actionmailer/lib/action_mailer/base.rb
... ...
@@ -1,23 +1,70 @@
  1
+require 'action_mailer/adv_attr_accessor'
  2
+require 'action_mailer/part'
  3
+
1 4
 module ActionMailer #:nodoc:
2 5
   # Usage:
3 6
   #
4 7
   #   class ApplicationMailer < ActionMailer::Base
5  
-  #     def post_notification(recipients, post)
6  
-  #       @recipients          = recipients
7  
-  #       @from                = post.author.email_address_with_name
8  
-  #       @headers["bcc"]      = SYSTEM_ADMINISTRATOR_EMAIL
9  
-  #       @headers["reply-to"] = "notifications@example.com"
10  
-  #       @subject             = "[#{post.account.name} #{post.title}]"
11  
-  #       @body["post"]        = post
  8
+  #     # Set up properties
  9
+  #     # (Properties can also be specified via accessor methods
  10
+  #     # i.e. self.subject = "foo") and instance variables (@subject = "foo").
  11
+  #     def signup_notification(recipient)
  12
+  #       recipients recipient.email_address_with_name
  13
+  #       subject    "New account information"
  14
+  #       body       Hash.new("account" => recipient)
  15
+  #       from       "system@example.com"
  16
+  #     end
  17
+  #
  18
+  #     # explicitly specify multipart messages
  19
+  #     def signup_notification(recipient)
  20
+  #       recipients      recipient.email_address_with_name
  21
+  #       subject         "New account information"
  22
+  #       from            "system@example.com"
  23
+  #
  24
+  #       part :content_type => "text/html",
  25
+  #         :body => render_message("signup-as-html", :account => recipient)
  26
+  #
  27
+  #       part "text/plain" do |p|
  28
+  #         p.body = render_message("signup-as-plain", :account => recipient)
  29
+  #         p.transfer_encoding = "base64"
  30
+  #       end
  31
+  #     end
  32
+  #
  33
+  #     # attachments
  34
+  #     def signup_notification(recipient)
  35
+  #       recipients      recipient.email_address_with_name
  36
+  #       subject         "New account information"
  37
+  #       from            "system@example.com"
  38
+  #
  39
+  #       attachment :content_type => "image/jpeg",
  40
+  #         :body => File.read("an-image.jpg")
  41
+  #
  42
+  #       attachment "application/pdf" do |a|
  43
+  #         a.body = generate_your_pdf_here()
  44
+  #       end
12 45
   #     end
13  
-  #     
14  
-  #     def comment_notification(recipient, comment)
15  
-  #       @recipients      = recipient.email_address_with_name
16  
-  #       @subject         = "[#{comment.post.project.client.firm.account.name}]" +
17  
-  #                          " Re: #{comment.post.title}"
18  
-  #       @body["comment"] = comment
19  
-  #       @from            = comment.author.email_address_with_name
20  
-  #       @sent_on         = comment.posted_on
  46
+  #
  47
+  #     # implicitly multipart messages
  48
+  #     def signup_notification(recipient)
  49
+  #       recipients      recipient.email_address_with_name
  50
+  #       subject         "New account information"
  51
+  #       from            "system@example.com"
  52
+  #       body(:account => "recipient")
  53
+  #
  54
+  #       # ActionMailer will automatically detect and use multipart templates,
  55
+  #       # where each template is named after the name of the action, followed
  56
+  #       # by the content type. Each such detected template will be added as
  57
+  #       # a separate part to the message.
  58
+  #       #
  59
+  #       # for example, if the following templates existed:
  60
+  #       #   * signup_notification.text.plain.rhtml
  61
+  #       #   * signup_notification.text.html.rhtml
  62
+  #       #   * signup_notification.text.xml.rxml
  63
+  #       #   * signup_notification.text.x-yaml.rhtml
  64
+  #       #
  65
+  #       # Each would be rendered and added as a separate part to the message,
  66
+  #       # with the corresponding content type. The same body hash is passed to
  67
+  #       # each template.
21 68
   #     end
22 69
   #   end
23 70
   #
@@ -57,8 +104,10 @@ module ActionMailer #:nodoc:
57 104
   #   for unit and functional testing.
58 105
   #
59 106
   # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also 
60  
-  #    pick a different charset from inside a method with <tt>@encoding</tt>.
  107
+  #    pick a different charset from inside a method with <tt>@charset</tt>.
61 108
   class Base
  109
+    include ActionMailer::AdvAttrAccessor
  110
+
62 111
     private_class_method :new #:nodoc:
63 112
 
64 113
     cattr_accessor :template_root
@@ -89,94 +138,167 @@ class Base
89 138
     @@default_charset = "utf-8"
90 139
     cattr_accessor :default_charset
91 140
 
92  
-    attr_accessor :recipients, :subject, :body, :from, :sent_on, :headers, :bcc, :cc, :charset
  141
+    adv_attr_accessor :recipients, :subject, :body, :from, :sent_on, :headers,
  142
+                      :bcc, :cc, :charset
93 143
 
94  
-    def initialize
95  
-      @bcc = @cc = @from = @recipients = @sent_on = @subject = @body = nil
  144
+    attr_reader       :mail
  145
+
  146
+    # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
  147
+    # will be initialized according to the named method. If not, the mailer will
  148
+    # remain uninitialized (useful when you only need to invoke the "receive"
  149
+    # method, for instance).
  150
+    def initialize(method_name=nil, *parameters)
  151
+      create!(method_name, *parameters) if method_name 
  152
+    end
  153
+
  154
+    # Initialize the mailer via the given +method_name+. The body will be
  155
+    # rendered and a new TMail::Mail object created.
  156
+    def create!(method_name, *parameters)
  157
+      @bcc = @cc = @from = @recipients = @sent_on = @subject = nil
96 158
       @charset = @@default_charset.dup
  159
+      @parts = []
97 160
       @headers = {}
98  
-    end
  161
+      @body = {}
  162
+
  163
+      send(method_name, *parameters)
  164
+
  165
+      # If an explicit, textual body has not been set, we check assumptions.
  166
+      unless String === @body
  167
+        # First, we look to see if there are any likely templates that match,
  168
+        # which include the content-type in their file name (i.e.,
  169
+        # "the_template_file.text.html.rhtml", etc.).
  170
+        if @parts.empty?
  171
+          templates = Dir.glob("#{template_path}/#{method_name}.*")
  172
+          templates.each do |path|
  173
+            type = (File.basename(path).split(".")[1..-2] || []).join("/")
  174
+            next if type.empty?
  175
+            @parts << Part.new(:content_type => type,
  176
+              :disposition => "inline", :charset => "charset",
  177
+              :body => render_message(File.basename(path).split(".")[0..-2].join('.'), @body))
  178
+          end
  179
+        end
99 180
 
100  
-    class << self
101  
-      def method_missing(method_symbol, *parameters)#:nodoc:
102  
-        case method_symbol.id2name
103  
-          when /^create_([_a-z]\w*)/  then create_from_action($1, *parameters)
104  
-          when /^deliver_([_a-z]\w*)/ then deliver(send("create_" + $1, *parameters))
  181
+        # Then, if there were such templates, we check to see if we ought to
  182
+        # also render a "normal" template (without the content type). If a
  183
+        # normal template exists (or if there were no implicit parts) we render
  184
+        # it.
  185
+        template_exists = @parts.empty?
  186
+        template_exists ||= Dir.glob("#{template_path}/#{method_name}.*").any? { |i| i.split(".").length == 2 }
  187
+        @body = render_message(method_name, @body) if template_exists
  188
+
  189
+        # Finally, if there are other message parts and a textual body exists,
  190
+        # we shift it onto the front of the parts and set the body to nil (so
  191
+        # that create_mail doesn't try to render it in addition to the parts).
  192
+        if !@parts.empty? && String === @body
  193
+          @parts.unshift Part.new(:charset => charset, :body => @body)
  194
+          @body = nil
105 195
         end
106 196
       end
107 197
 
108  
-      def mail(to, subject, body, from, timestamp = nil, headers = {}, charset = @@default_charset) #:nodoc:
109  
-        deliver(create(to, subject, body, from, timestamp, headers, charset))
110  
-      end
  198
+      # build the mail object itself
  199
+      @mail = create_mail
  200
+    end
111 201
 
112  
-      def create(to, subject, body, from, timestamp = nil, headers = {}, charset = @@default_charset) #:nodoc:
113  
-        m = TMail::Mail.new
114  
-        m.body = body
115  
-        m.subject, = quote_any_if_necessary(charset, subject)
116  
-        m.to, m.from = quote_any_address_if_necessary(charset, to, from)
  202
+    # Delivers the cached TMail::Mail object. If no TMail::Mail object has been
  203
+    # created (via the #create! method, for instance) this will fail.
  204
+    def deliver!
  205
+      raise "no mail object available for delivery!" unless @mail
  206
+      logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
117 207
 
118  
-        m.date = timestamp.respond_to?("to_time") ? timestamp.to_time : (timestamp || Time.now)    
  208
+      begin
  209
+        send("perform_delivery_#{delivery_method}", @mail) if perform_deliveries
  210
+      rescue Object => e
  211
+        raise e if raise_delivery_errors
  212
+      end
119 213
 
120  
-        m.set_content_type "text", "plain", { "charset" => charset }
  214
+      return @mail
  215
+    end
121 216
 
122  
-        headers.each do |k, v|
123  
-          m[k] = v
124  
-        end
  217
+    # Add a part to a multipart message, with the given content-type. The
  218
+    # part itself is yielded to the block, so that other properties (charset,
  219
+    # body, headers, etc.) can be set on it.
  220
+    def part(params)
  221
+      params = {:content_type => params} if String === params
  222
+      part = Part.new(params)
  223
+      yield part if block_given?
  224
+      @parts << part
  225
+    end
  226
+
  227
+    # Add an attachment to a multipart message. This is simply a part with the
  228
+    # content-disposition set to "attachment".
  229
+    def attachment(params, &block)
  230
+      params = { :content_type => params } if String === params
  231
+      params = { :disposition => "attachment",
  232
+                 :transfer_encoding => "base64" }.merge(params)
  233
+      part(params, &block)
  234
+    end
125 235
 
126  
-        return m
  236
+    private
  237
+  
  238
+      def render_message(method_name, body)
  239
+        ActionView::Base.new(template_path, body).render_file(method_name)
  240
+      end
  241
+        
  242
+      def template_path
  243
+        template_root + "/" + Inflector.underscore(self.class.name)
127 244
       end
128 245
 
129  
-      def deliver(mail) #:nodoc:
130  
-        logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
  246
+      def create_mail
  247
+        m = TMail::Mail.new
131 248
 
132  
-        begin
133  
-          send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
134  
-        rescue Object => e
135  
-          raise e if raise_delivery_errors
136  
-        end
  249
+        m.subject, = quote_any_if_necessary(charset, subject)
  250
+        m.to, m.from = quote_any_address_if_necessary(charset, recipients, from)
  251
+        m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil?
  252
+        m.cc  = quote_address_if_necessary(cc, charset) unless cc.nil?
137 253
 
138  
-        return mail
139  
-      end
  254
+        m.date = sent_on.to_time rescue sent_on if sent_on
  255
+        headers.each { |k, v| m[k] = v }
140 256
 
141  
-      def quoted_printable(text, charset)#:nodoc:
142  
-        text = text.gsub( /[^a-z ]/i ) { "=%02x" % $&[0] }.gsub( / /, "_" )
143  
-        "=?#{charset}?Q?#{text}?="
144  
-      end
  257
+        if @parts.empty?
  258
+          m.set_content_type "text", "plain", { "charset" => charset }
  259
+          m.body = body
  260
+        else
  261
+          if String === body
  262
+            part = TMail::Mail.new
  263
+            part.body = body
  264
+            part.set_content_type "text", "plain", { "charset" => charset }
  265
+            part.set_content_disposition "inline"
  266
+            m.parts << part
  267
+          end
145 268
 
146  
-      CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
  269
+          @parts.each do |p|
  270
+            part = (TMail::Mail === p ? p : p.to_mail(self))
  271
+            m.parts << part
  272
+          end
  273
+        end
147 274
 
148  
-      # Quote the given text if it contains any "illegal" characters
149  
-      def quote_if_necessary(text, charset)
150  
-        (text =~ CHARS_NEEDING_QUOTING) ?
151  
-          quoted_printable(text, charset) :
152  
-          text
  275
+        @mail = m
153 276
       end
154 277
 
155  
-      # Quote any of the given strings if they contain any "illegal" characters
156  
-      def quote_any_if_necessary(charset, *args)
157  
-        args.map { |v| quote_if_necessary(v, charset) }
  278
+      def perform_delivery_smtp(mail)
  279
+        Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain], 
  280
+            server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
  281
+          smtp.sendmail(mail.encoded, mail.from, mail.destinations)
  282
+        end
158 283
       end
159 284
 
160  
-      # Quote the given address if it needs to be. The address may be a
161  
-      # regular email address, or it can be a phrase followed by an address in
162  
-      # brackets. The phrase is the only part that will be quoted, and only if
163  
-      # it needs to be. This allows extended characters to be used in the
164  
-      # "to", "from", "cc", and "bcc" headers.
165  
-      def quote_address_if_necessary(address, charset)
166  
-        if Array === address
167  
-          address.map { |a| quote_address_if_necessary(a, charset) }
168  
-        elsif address =~ /^(\S.*)\s+(<.*>)$/
169  
-          address = $2
170  
-          phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
171  
-          "\"#{phrase}\" #{address}"
172  
-        else
173  
-          address
  285
+      def perform_delivery_sendmail(mail)
  286
+        IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
  287
+          sm.print(mail.encoded)
  288
+          sm.flush
174 289
         end
175 290
       end
176 291
 
177  
-      # Quote any of the given addresses, if they need to be.
178  
-      def quote_any_address_if_necessary(charset, *args)
179  
-        args.map { |v| quote_address_if_necessary(v, charset) }
  292
+      def perform_delivery_test(mail)
  293
+        deliveries << mail
  294
+      end
  295
+
  296
+    class << self
  297
+      def method_missing(method_symbol, *parameters)#:nodoc:
  298
+        case method_symbol.id2name
  299
+          when /^create_([_a-z]\w*)/  then new($1, *parameters).mail
  300
+          when /^deliver_([_a-z]\w*)/ then new($1, *parameters).deliver!
  301
+        end
180 302
       end
181 303
 
182 304
       def receive(raw_email)
@@ -186,51 +308,6 @@ def receive(raw_email)
186 308
         new.receive(mail)
187 309
       end
188 310
 
189  
-      private
190  
-        def perform_delivery_smtp(mail)
191  
-          Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain], 
192  
-              server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
193  
-            smtp.sendmail(mail.encoded, mail.from, mail.destinations)
194  
-          end
195  
-        end
196  
-
197  
-        def perform_delivery_sendmail(mail)
198  
-          IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
199  
-            sm.print(mail.encoded)
200  
-            sm.flush
201  
-          end
202  
-        end
203  
-
204  
-        def perform_delivery_test(mail)
205  
-          deliveries << mail
206  
-        end
207  
-
208  
-        def create_from_action(method_name, *parameters)
209  
-          mailer = new
210  
-          mailer.body = {}
211  
-          mailer.send(method_name, *parameters)
212  
-
213  
-          unless String === mailer.body then
214  
-            mailer.body = render_body mailer, method_name
215  
-          end
216  
-
217  
-          mail = create(mailer.recipients, mailer.subject, mailer.body,
218  
-                        mailer.from, mailer.sent_on, mailer.headers,
219  
-                        mailer.charset)
220  
-
221  
-          mail.bcc = quote_address_if_necessary(mailer.bcc, mailer.charset) unless mailer.bcc.nil?
222  
-          mail.cc  = quote_address_if_necessary(mailer.cc, mailer.charset)  unless mailer.cc.nil?
223  
-
224  
-          return mail
225  
-        end
226  
-  
227  
-        def render_body(mailer, method_name)
228  
-          ActionView::Base.new(template_path, mailer.body).render_file(method_name)
229  
-        end
230  
-        
231  
-        def template_path
232  
-          template_root + "/" + Inflector.underscore(self.to_s)
233  
-        end
234 311
     end
235 312
   end
236 313
 end
84  actionmailer/lib/action_mailer/part.rb
... ...
@@ -0,0 +1,84 @@
  1
+require 'action_mailer/adv_attr_accessor'
  2
+
  3
+module ActionMailer
  4
+
  5
+  class Part #:nodoc:
  6
+    include ActionMailer::AdvAttrAccessor
  7
+
  8
+    adv_attr_accessor :content_type, :content_disposition, :charset, :body
  9
+    adv_attr_accessor :filename, :transfer_encoding, :headers
  10
+
  11
+    def initialize(params)
  12
+      @content_type = params[:content_type] || "text/plain"
  13
+      @content_disposition = params[:disposition] || "inline"
  14
+      @charset = params[:charset]
  15
+      @body = params[:body]
  16
+      @filename = params[:filename]
  17
+      @transfer_encoding = params[:transfer_encoding] || "quoted-printable"
  18
+      @headers = params[:headers] || {}
  19
+    end
  20
+
  21
+    def to_mail(defaults)
  22
+      part = TMail::Mail.new
  23
+      part.set_content_type(content_type, nil,
  24
+        "charset" => charset || defaults.charset, "name" => filename)
  25
+      part.set_content_disposition(content_disposition,
  26
+        "filename" => filename)
  27
+
  28
+      part.content_transfer_encoding = transfer_encoding || "quoted-printable"
  29
+      case (transfer_encoding || "").downcase
  30
+        when "base64" then
  31
+          part.body = TMail::Base64.encode(body)
  32
+        when "quoted-printable"
  33
+          part.body = [body].pack("M*")
  34
+        else
  35
+          part.body = body
  36
+      end
  37
+
  38
+      part
  39
+    end
  40
+  end
  41
+
  42
+end
  43
+require 'action_mailer/adv_attr_accessor'
  44
+
  45
+module ActionMailer
  46
+
  47
+  class Part #:nodoc:
  48
+    include ActionMailer::AdvAttrAccessor
  49
+
  50
+    adv_attr_accessor :content_type, :content_disposition, :charset, :body
  51
+    adv_attr_accessor :filename, :transfer_encoding, :headers
  52
+
  53
+    def initialize(params)
  54
+      @content_type = params[:content_type] || "text/plain"
  55
+      @content_disposition = params[:disposition] || "inline"
  56
+      @charset = params[:charset]
  57
+      @body = params[:body]
  58
+      @filename = params[:filename]
  59
+      @transfer_encoding = params[:transfer_encoding] || "quoted-printable"
  60
+      @headers = params[:headers] || {}
  61
+    end
  62
+
  63
+    def to_mail(defaults)
  64
+      part = TMail::Mail.new
  65
+      part.set_content_type(content_type, nil,
  66
+        "charset" => charset || defaults.charset, "name" => filename)
  67
+      part.set_content_disposition(content_disposition,
  68
+        "filename" => filename)
  69
+
  70
+      part.content_transfer_encoding = transfer_encoding || "quoted-printable"
  71
+      case (transfer_encoding || "").downcase
  72
+        when "base64" then
  73
+          part.body = TMail::Base64.encode(body)
  74
+        when "quoted-printable"
  75
+          part.body = [body].pack("M*")
  76
+        else
  77
+          part.body = body
  78
+      end
  79
+
  80
+      part
  81
+    end
  82
+  end
  83
+
  84
+end
102  actionmailer/lib/action_mailer/quoting.rb
... ...
@@ -0,0 +1,102 @@
  1
+module ActionMailer
  2
+  module Quoting
  3
+
  4
+    # Convert the given text into quoted printable format, with an instruction
  5
+    # that the text be eventually interpreted in the given charset.
  6
+    def quoted_printable(text, charset)
  7
+      text = text.gsub( /[^a-z ]/i ) { "=%02x" % $&[0] }.gsub( / /, "_" )
  8
+      "=?#{charset}?Q?#{text}?="
  9
+    end
  10
+
  11
+    # A quick-and-dirty regexp for determining whether a string contains any
  12
+    # characters that need escaping.
  13
+    CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
  14
+
  15
+    # Quote the given text if it contains any "illegal" characters
  16
+    def quote_if_necessary(text, charset)
  17
+      (text =~ CHARS_NEEDING_QUOTING) ?
  18
+        quoted_printable(text, charset) :
  19
+        text
  20
+    end
  21
+
  22
+    # Quote any of the given strings if they contain any "illegal" characters
  23
+    def quote_any_if_necessary(charset, *args)
  24
+      args.map { |v| quote_if_necessary(v, charset) }
  25
+    end
  26
+
  27
+    # Quote the given address if it needs to be. The address may be a
  28
+    # regular email address, or it can be a phrase followed by an address in
  29
+    # brackets. The phrase is the only part that will be quoted, and only if
  30
+    # it needs to be. This allows extended characters to be used in the
  31
+    # "to", "from", "cc", and "bcc" headers.
  32
+    def quote_address_if_necessary(address, charset)
  33
+      if Array === address
  34
+        address.map { |a| quote_address_if_necessary(a, charset) }
  35
+      elsif address =~ /^(\S.*)\s+(<.*>)$/
  36
+        address = $2
  37
+        phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
  38
+        "\"#{phrase}\" #{address}"
  39
+      else
  40
+        address
  41
+      end
  42
+    end
  43
+
  44
+    # Quote any of the given addresses, if they need to be.
  45
+    def quote_any_address_if_necessary(charset, *args)
  46
+      args.map { |v| quote_address_if_necessary(v, charset) }
  47
+    end
  48
+
  49
+  end
  50
+end
  51
+module ActionMailer
  52
+  module Quoting
  53
+
  54
+    # Convert the given text into quoted printable format, with an instruction
  55
+    # that the text be eventually interpreted in the given charset.
  56
+    def quoted_printable(text, charset)
  57
+      text = text.gsub( /[^a-z ]/i ) { "=%02x" % $&[0] }.gsub( / /, "_" )
  58
+      "=?#{charset}?Q?#{text}?="
  59
+    end
  60
+
  61
+    # A quick-and-dirty regexp for determining whether a string contains any
  62
+    # characters that need escaping.
  63
+    if !defined?(CHARS_NEEDING_QUOTING)
  64
+      CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
  65
+    end
  66
+
  67
+    # Quote the given text if it contains any "illegal" characters
  68
+    def quote_if_necessary(text, charset)
  69
+      (text =~ CHARS_NEEDING_QUOTING) ?
  70
+        quoted_printable(text, charset) :
  71
+        text
  72
+    end
  73
+
  74
+    # Quote any of the given strings if they contain any "illegal" characters
  75
+    def quote_any_if_necessary(charset, *args)
  76
+      args.map { |v| quote_if_necessary(v, charset) }
  77
+    end
  78
+
  79
+    # Quote the given address if it needs to be. The address may be a
  80
+    # regular email address, or it can be a phrase followed by an address in
  81
+    # brackets. The phrase is the only part that will be quoted, and only if
  82
+    # it needs to be. This allows extended characters to be used in the
  83
+    # "to", "from", "cc", and "bcc" headers.
  84
+    def quote_address_if_necessary(address, charset)
  85
+      if Array === address
  86
+        address.map { |a| quote_address_if_necessary(a, charset) }
  87
+      elsif address =~ /^(\S.*)\s+(<.*>)$/
  88
+        address = $2
  89
+        phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
  90
+        "\"#{phrase}\" #{address}"
  91
+      else
  92
+        address
  93
+      end
  94
+    end
  95
+
  96
+    # Quote any of the given addresses, if they need to be.
  97
+    def quote_any_address_if_necessary(charset, *args)
  98
+      args.map { |v| quote_address_if_necessary(v, charset) }
  99
+    end
  100
+
  101
+  end
  102
+end
1  actionmailer/lib/action_mailer/vendor/tmail/encode.rb
@@ -253,6 +253,7 @@ def phrase( str )
253 253
     # FIXME: implement line folding
254 254
     #
255 255
     def kv_pair( k, v )
  256
+      return if v.nil?
256 257
       v = normalize_encoding(v)
257 258
       if token_safe?(v)
258 259
         add_text k + '=' + v
3  actionmailer/lib/action_mailer/vendor/tmail/facade.rb
@@ -442,7 +442,8 @@ def set_disposition( str, params = nil )
442 442
         h.disposition = str
443 443
         h.params.clear
444 444
       else
445  
-        h = store('Content-Disposition', str)
  445
+        store('Content-Disposition', str)
  446
+        h = @header['content-disposition']
446 447
       end
447 448
       h.params.replace params if params
448 449
     end
2  actionmailer/lib/action_mailer/vendor/tmail/utils.rb
@@ -27,7 +27,7 @@ def TMail.random_tag
27 27
     t = Time.now
28 28
     sprintf('%x%x_%x%x%d%x',
29 29
             t.to_i, t.tv_usec,
30  
-            $$, Thread.current.id, @uniq, rand(255))
  30
+            $$, Thread.current.object_id, @uniq, rand(255))
31 31
   end
32 32
   private_class_method :random_tag
33 33
 
10  actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml
... ...
@@ -0,0 +1,10 @@
  1
+<html>
  2
+  <body>
  3
+    HTML formatted message to <strong><%= @recipient %></strong>.
  4
+  </body>
  5
+</html>
  6
+<html>
  7
+  <body>
  8
+    HTML formatted message to <strong><%= @recipient %></strong>.
  9
+  </body>
  10
+</html>
2  actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml
... ...
@@ -0,0 +1,2 @@
  1
+Plain text to <%= @recipient %>.
  2
+Plain text to <%= @recipient %>.
96  actionmailer/test/mail_service_test.rb
@@ -14,21 +14,21 @@ def signed_up(recipient)
14 14
   end
15 15
 
16 16
   def cancelled_account(recipient)
17  
-    @recipients = recipient
18  
-    @subject    = "[Cancelled] Goodbye #{recipient}"
19  
-    @from       = "system@loudthinking.com"
20  
-    @sent_on    = Time.local(2004, 12, 12)
21  
-    @body       = "Goodbye, Mr. #{recipient}"
  17
+    self.recipients = recipient
  18
+    self.subject    = "[Cancelled] Goodbye #{recipient}"
  19
+    self.from       = "system@loudthinking.com"
  20
+    self.sent_on    = Time.local(2004, 12, 12)
  21
+    self.body       = "Goodbye, Mr. #{recipient}"
22 22
   end
23 23
 
24 24
   def cc_bcc(recipient)
25  
-    @recipients = recipient
26  
-    @subject    = "testing bcc/cc"
27  
-    @from       = "system@loudthinking.com"
28  
-    @sent_on    = Time.local 2004, 12, 12
29  
-    @cc         = "nobody@loudthinking.com"
30  
-    @bcc        = "root@loudthinking.com"
31  
-    @body       = "Nothing to see here."
  25
+    recipients recipient
  26
+    subject    "testing bcc/cc"
  27
+    from       "system@loudthinking.com"
  28
+    sent_on    Time.local(2004, 12, 12)
  29
+    cc         "nobody@loudthinking.com"
  30
+    bcc        "root@loudthinking.com"
  31
+    body       "Nothing to see here."
32 32
   end
33 33
 
34 34
   def iso_charset(recipient)
@@ -74,6 +74,30 @@ def utf8_body(recipient)
74 74
     @charset    = "utf-8"
75 75
   end
76 76
 
  77
+  def explicitly_multipart_example(recipient)
  78
+    @recipients = recipient
  79
+    @subject    = "multipart example"
  80
+    @from       = "test@example.com"
  81
+    @sent_on    = Time.local 2004, 12, 12
  82
+    @body       = "plain text default"
  83
+
  84
+    part "text/html" do |p|
  85
+      p.charset = "iso-8859-1"
  86
+      p.body = "blah"
  87
+    end
  88
+
  89
+    attachment :content_type => "image/jpeg", :filename => "foo.jpg",
  90
+      :body => "123456789"
  91
+  end
  92
+
  93
+  def implicitly_multipart_example(recipient)
  94
+    @recipients = recipient
  95
+    @subject    = "multipart example"
  96
+    @from       = "test@example.com"
  97
+    @sent_on    = Time.local 2004, 12, 12
  98
+    @body       = { "recipient" => recipient }
  99
+  end
  100
+
77 101
   class <<self
78 102
     attr_accessor :received_body
79 103
   end
@@ -86,9 +110,10 @@ def receive(mail)
86 110
 TestMailer.template_root = File.dirname(__FILE__) + "/fixtures"
87 111
 
88 112
 class ActionMailerTest < Test::Unit::TestCase
  113
+  include ActionMailer::Quoting
89 114
 
90 115
   def encode( text, charset="utf-8" )
91  
-    ActionMailer::Base.quoted_printable( text, charset )
  116
+    quoted_printable( text, charset )
92 117
   end
93 118
 
94 119
   def new_mail( charset="utf-8" )
@@ -312,12 +337,12 @@ def test_extended_headers
312 337
     @recipient = "Grytøyr <test@localhost>"
313 338
 
314 339
     expected = new_mail "iso-8859-1"
315  
-    expected.to      = TestMailer.quote_address_if_necessary @recipient, "iso-8859-1"
  340
+    expected.to      = quote_address_if_necessary @recipient, "iso-8859-1"
316 341
     expected.subject = "testing extended headers"
317 342
     expected.body    = "Nothing to see here."
318  
-    expected.from    = TestMailer.quote_address_if_necessary "Grytøyr <stian1@example.net>", "iso-8859-1"
319  
-    expected.cc      = TestMailer.quote_address_if_necessary "Grytøyr <stian2@example.net>", "iso-8859-1"
320  
-    expected.bcc     = TestMailer.quote_address_if_necessary "Grytøyr <stian3@example.net>", "iso-8859-1"
  343
+    expected.from    = quote_address_if_necessary "Grytøyr <stian1@example.net>", "iso-8859-1"
  344
+    expected.cc      = quote_address_if_necessary "Grytøyr <stian2@example.net>", "iso-8859-1"
  345
+    expected.bcc     = quote_address_if_necessary "Grytøyr <stian3@example.net>", "iso-8859-1"
321 346
     expected.date    = Time.local 2004, 12, 12
322 347
 
323 348
     created = nil
@@ -339,12 +364,12 @@ def test_extended_headers
339 364
   def test_utf8_body_is_not_quoted
340 365
     @recipient = "Foo áëô îü <extended@example.net>"
341 366
     expected = new_mail "utf-8"
342  
-    expected.to      = TestMailer.quote_address_if_necessary @recipient, "utf-8"
  367
+    expected.to      = quote_address_if_necessary @recipient, "utf-8"
343 368
     expected.subject = "testing utf-8 body"
344 369
     expected.body    = "åœö blah"
345  
-    expected.from    = TestMailer.quote_address_if_necessary @recipient, "utf-8"
346  
-    expected.cc      = TestMailer.quote_address_if_necessary @recipient, "utf-8"
347  
-    expected.bcc     = TestMailer.quote_address_if_necessary @recipient, "utf-8"
  370
+    expected.from    = quote_address_if_necessary @recipient, "utf-8"
  371
+    expected.cc      = quote_address_if_necessary @recipient, "utf-8"
  372
+    expected.bcc     = quote_address_if_necessary @recipient, "utf-8"
348 373
     expected.date    = Time.local 2004, 12, 12
349 374
 
350 375
     created = TestMailer.create_utf8_body @recipient
@@ -354,12 +379,12 @@ def test_utf8_body_is_not_quoted
354 379
   def test_multiple_utf8_recipients
355 380
     @recipient = ["\"Foo áëô îü\" <extended@example.net>", "\"Example Recipient\" <me@example.com>"]
356 381
     expected = new_mail "utf-8"
357  
-    expected.to      = TestMailer.quote_address_if_necessary @recipient, "utf-8"
  382
+    expected.to      = quote_address_if_necessary @recipient, "utf-8"
358 383
     expected.subject = "testing utf-8 body"
359 384
     expected.body    = "åœö blah"
360  
-    expected.from    = TestMailer.quote_address_if_necessary @recipient.first, "utf-8"
361  
-    expected.cc      = TestMailer.quote_address_if_necessary @recipient, "utf-8"
362  
-    expected.bcc     = TestMailer.quote_address_if_necessary @recipient, "utf-8"
  385
+    expected.from    = quote_address_if_necessary @recipient.first, "utf-8"
  386
+    expected.cc      = quote_address_if_necessary @recipient, "utf-8"
  387
+    expected.bcc     = quote_address_if_necessary @recipient, "utf-8"
363 388
     expected.date    = Time.local 2004, 12, 12
364 389
 
365 390
     created = TestMailer.create_utf8_body @recipient
@@ -400,5 +425,26 @@ def test_decode_message_without_content_type
400 425
     assert_nothing_raised { mail.body }
401 426
   end
402 427
 
  428
+  def test_explicitly_multipart_messages
  429
+    mail = TestMailer.create_explicitly_multipart_example(@recipient)
  430
+    assert_equal 3, mail.parts.length
  431
+    assert_equal "text/plain", mail.parts[0].content_type
  432
+
  433
+    assert_equal "text/html", mail.parts[1].content_type
  434
+    assert_equal "inline", mail.parts[1].content_disposition
  435
+
  436
+    assert_equal "image/jpeg", mail.parts[2].content_type
  437
+    assert_equal "attachment", mail.parts[2].content_disposition
  438
+    assert_equal "foo.jpg", mail.parts[2].sub_header("content-disposition", "filename")
  439
+    assert_equal "foo.jpg", mail.parts[2].sub_header("content-type", "name")
  440
+  end
  441
+
  442
+  def test_implicitly_multipart_messages
  443
+    mail = TestMailer.create_implicitly_multipart_example(@recipient)
  444
+    assert_equal 2, mail.parts.length
  445
+    assert_equal "text/html", mail.parts[0].content_type
  446
+    assert_equal "text/plain", mail.parts[1].content_type
  447
+  end
  448
+
403 449
 end
404 450
 

0 notes on commit 359caef

Please sign in to comment.
Something went wrong with that request. Please try again.