aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJamis Buck <jamis@37signals.com>2005-05-29 16:36:22 +0000
committerJamis Buck <jamis@37signals.com>2005-05-29 16:36:22 +0000
commit359caef33ca71b37b3ea2feef0960beccfabf4de (patch)
treefd5598e50baa90e6e84bf0747866d04c8aecb677
parent79d9794f090f02ff56c461893785fbd65877109e (diff)
downloadrails-359caef33ca71b37b3ea2feef0960beccfabf4de.tar.gz
rails-359caef33ca71b37b3ea2feef0960beccfabf4de.tar.bz2
rails-359caef33ca71b37b3ea2feef0960beccfabf4de.zip
A very thorough refactoring, resulting in new mail property setters and support for attachments and multipart messages.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1359 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
-rw-r--r--actionmailer/CHANGELOG6
-rwxr-xr-xactionmailer/lib/action_mailer.rb4
-rw-r--r--actionmailer/lib/action_mailer/adv_attr_accessor.rb58
-rw-r--r--actionmailer/lib/action_mailer/base.rb323
-rw-r--r--actionmailer/lib/action_mailer/part.rb84
-rw-r--r--actionmailer/lib/action_mailer/quoting.rb102
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/encode.rb1
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/facade.rb3
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/utils.rb2
-rw-r--r--actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml10
-rw-r--r--actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml2
-rwxr-xr-xactionmailer/test/mail_service_test.rb96
12 files changed, 539 insertions, 152 deletions
diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG
index b49821bcd5..3a7a511916 100644
--- a/actionmailer/CHANGELOG
+++ b/actionmailer/CHANGELOG
@@ -1,5 +1,9 @@
*SVN*
+* Support attachments and multipart messages.
+
+* Added new accessors for the various mail properties.
+
* 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]
* Fix attachments and content-type problems #1276 [Jamis Buck]
@@ -117,4 +121,4 @@
*0.3*
-* First release \ No newline at end of file
+* First release
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index 3b507f63d1..bc0a7a6ec4 100755
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -36,11 +36,13 @@ $:.unshift(File.dirname(__FILE__) + "/action_mailer/vendor/")
require 'action_mailer/base'
require 'action_mailer/mail_helper'
+require 'action_mailer/quoting'
require 'action_mailer/vendor/tmail'
require 'net/smtp'
ActionView::Base.class_eval { include MailHelper }
+ActionMailer::Base.class_eval { include ActionMailer::Quoting }
old_verbose, $VERBOSE = $VERBOSE, nil
TMail::Encoder.const_set("MAX_LINE_LEN", 200)
-$VERBOSE = old_verbose \ No newline at end of file
+$VERBOSE = old_verbose
diff --git a/actionmailer/lib/action_mailer/adv_attr_accessor.rb b/actionmailer/lib/action_mailer/adv_attr_accessor.rb
new file mode 100644
index 0000000000..47c02ba198
--- /dev/null
+++ b/actionmailer/lib/action_mailer/adv_attr_accessor.rb
@@ -0,0 +1,58 @@
+module ActionMailer
+ module AdvAttrAccessor
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+
+ def adv_attr_accessor(*names)
+ names.each do |name|
+ define_method("#{name}=") do |value|
+ instance_variable_set("@#{name}", value)
+ end
+
+ define_method(name) do |*parameters|
+ raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1
+ if parameters.empty?
+ instance_variable_get("@#{name}")
+ else
+ instance_variable_set("@#{name}", parameters.first)
+ end
+ end
+ end
+ end
+
+ end
+ end
+end
+module ActionMailer
+ module AdvAttrAccessor
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+
+ def adv_attr_accessor(*names)
+ names.each do |name|
+ define_method("#{name}=") do |value|
+ instance_variable_set("@#{name}", value)
+ end
+
+ define_method(name) do |*parameters|
+ raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1
+ if parameters.empty?
+ instance_variable_get("@#{name}")
+ else
+ instance_variable_set("@#{name}", parameters.first)
+ end
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 9afe5138d9..15f08caf40 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -1,23 +1,70 @@
+require 'action_mailer/adv_attr_accessor'
+require 'action_mailer/part'
+
module ActionMailer #:nodoc:
# Usage:
#
# class ApplicationMailer < ActionMailer::Base
- # def post_notification(recipients, post)
- # @recipients = recipients
- # @from = post.author.email_address_with_name
- # @headers["bcc"] = SYSTEM_ADMINISTRATOR_EMAIL
- # @headers["reply-to"] = "notifications@example.com"
- # @subject = "[#{post.account.name} #{post.title}]"
- # @body["post"] = post
+ # # Set up properties
+ # # (Properties can also be specified via accessor methods
+ # # i.e. self.subject = "foo") and instance variables (@subject = "foo").
+ # def signup_notification(recipient)
+ # recipients recipient.email_address_with_name
+ # subject "New account information"
+ # body Hash.new("account" => recipient)
+ # from "system@example.com"
+ # end
+ #
+ # # explicitly specify multipart messages
+ # def signup_notification(recipient)
+ # recipients recipient.email_address_with_name
+ # subject "New account information"
+ # from "system@example.com"
+ #
+ # part :content_type => "text/html",
+ # :body => render_message("signup-as-html", :account => recipient)
+ #
+ # part "text/plain" do |p|
+ # p.body = render_message("signup-as-plain", :account => recipient)
+ # p.transfer_encoding = "base64"
+ # end
+ # end
+ #
+ # # attachments
+ # def signup_notification(recipient)
+ # recipients recipient.email_address_with_name
+ # subject "New account information"
+ # from "system@example.com"
+ #
+ # attachment :content_type => "image/jpeg",
+ # :body => File.read("an-image.jpg")
+ #
+ # attachment "application/pdf" do |a|
+ # a.body = generate_your_pdf_here()
+ # end
# end
- #
- # def comment_notification(recipient, comment)
- # @recipients = recipient.email_address_with_name
- # @subject = "[#{comment.post.project.client.firm.account.name}]" +
- # " Re: #{comment.post.title}"
- # @body["comment"] = comment
- # @from = comment.author.email_address_with_name
- # @sent_on = comment.posted_on
+ #
+ # # implicitly multipart messages
+ # def signup_notification(recipient)
+ # recipients recipient.email_address_with_name
+ # subject "New account information"
+ # from "system@example.com"
+ # body(:account => "recipient")
+ #
+ # # ActionMailer will automatically detect and use multipart templates,
+ # # where each template is named after the name of the action, followed
+ # # by the content type. Each such detected template will be added as
+ # # a separate part to the message.
+ # #
+ # # for example, if the following templates existed:
+ # # * signup_notification.text.plain.rhtml
+ # # * signup_notification.text.html.rhtml
+ # # * signup_notification.text.xml.rxml
+ # # * signup_notification.text.x-yaml.rhtml
+ # #
+ # # Each would be rendered and added as a separate part to the message,
+ # # with the corresponding content type. The same body hash is passed to
+ # # each template.
# end
# end
#
@@ -57,8 +104,10 @@ module ActionMailer #:nodoc:
# for unit and functional testing.
#
# * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also
- # pick a different charset from inside a method with <tt>@encoding</tt>.
+ # pick a different charset from inside a method with <tt>@charset</tt>.
class Base
+ include ActionMailer::AdvAttrAccessor
+
private_class_method :new #:nodoc:
cattr_accessor :template_root
@@ -89,94 +138,167 @@ module ActionMailer #:nodoc:
@@default_charset = "utf-8"
cattr_accessor :default_charset
- attr_accessor :recipients, :subject, :body, :from, :sent_on, :headers, :bcc, :cc, :charset
+ adv_attr_accessor :recipients, :subject, :body, :from, :sent_on, :headers,
+ :bcc, :cc, :charset
- def initialize
- @bcc = @cc = @from = @recipients = @sent_on = @subject = @body = nil
+ attr_reader :mail
+
+ # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
+ # will be initialized according to the named method. If not, the mailer will
+ # remain uninitialized (useful when you only need to invoke the "receive"
+ # method, for instance).
+ def initialize(method_name=nil, *parameters)
+ create!(method_name, *parameters) if method_name
+ end
+
+ # Initialize the mailer via the given +method_name+. The body will be
+ # rendered and a new TMail::Mail object created.
+ def create!(method_name, *parameters)
+ @bcc = @cc = @from = @recipients = @sent_on = @subject = nil
@charset = @@default_charset.dup
+ @parts = []
@headers = {}
- end
+ @body = {}
+
+ send(method_name, *parameters)
+
+ # If an explicit, textual body has not been set, we check assumptions.
+ unless String === @body
+ # First, we look to see if there are any likely templates that match,
+ # which include the content-type in their file name (i.e.,
+ # "the_template_file.text.html.rhtml", etc.).
+ if @parts.empty?
+ templates = Dir.glob("#{template_path}/#{method_name}.*")
+ templates.each do |path|
+ type = (File.basename(path).split(".")[1..-2] || []).join("/")
+ next if type.empty?
+ @parts << Part.new(:content_type => type,
+ :disposition => "inline", :charset => "charset",
+ :body => render_message(File.basename(path).split(".")[0..-2].join('.'), @body))
+ end
+ end
- class << self
- def method_missing(method_symbol, *parameters)#:nodoc:
- case method_symbol.id2name
- when /^create_([_a-z]\w*)/ then create_from_action($1, *parameters)
- when /^deliver_([_a-z]\w*)/ then deliver(send("create_" + $1, *parameters))
+ # Then, if there were such templates, we check to see if we ought to
+ # also render a "normal" template (without the content type). If a
+ # normal template exists (or if there were no implicit parts) we render
+ # it.
+ template_exists = @parts.empty?
+ template_exists ||= Dir.glob("#{template_path}/#{method_name}.*").any? { |i| i.split(".").length == 2 }
+ @body = render_message(method_name, @body) if template_exists
+
+ # Finally, if there are other message parts and a textual body exists,
+ # we shift it onto the front of the parts and set the body to nil (so
+ # that create_mail doesn't try to render it in addition to the parts).
+ if !@parts.empty? && String === @body
+ @parts.unshift Part.new(:charset => charset, :body => @body)
+ @body = nil
end
end
- def mail(to, subject, body, from, timestamp = nil, headers = {}, charset = @@default_charset) #:nodoc:
- deliver(create(to, subject, body, from, timestamp, headers, charset))
- end
+ # build the mail object itself
+ @mail = create_mail
+ end
- def create(to, subject, body, from, timestamp = nil, headers = {}, charset = @@default_charset) #:nodoc:
- m = TMail::Mail.new
- m.body = body
- m.subject, = quote_any_if_necessary(charset, subject)
- m.to, m.from = quote_any_address_if_necessary(charset, to, from)
+ # Delivers the cached TMail::Mail object. If no TMail::Mail object has been
+ # created (via the #create! method, for instance) this will fail.
+ def deliver!
+ raise "no mail object available for delivery!" unless @mail
+ logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
- m.date = timestamp.respond_to?("to_time") ? timestamp.to_time : (timestamp || Time.now)
+ begin
+ send("perform_delivery_#{delivery_method}", @mail) if perform_deliveries
+ rescue Object => e
+ raise e if raise_delivery_errors
+ end
- m.set_content_type "text", "plain", { "charset" => charset }
+ return @mail
+ end
- headers.each do |k, v|
- m[k] = v
- end
+ # Add a part to a multipart message, with the given content-type. The
+ # part itself is yielded to the block, so that other properties (charset,
+ # body, headers, etc.) can be set on it.
+ def part(params)
+ params = {:content_type => params} if String === params
+ part = Part.new(params)
+ yield part if block_given?
+ @parts << part
+ end
+
+ # Add an attachment to a multipart message. This is simply a part with the
+ # content-disposition set to "attachment".
+ def attachment(params, &block)
+ params = { :content_type => params } if String === params
+ params = { :disposition => "attachment",
+ :transfer_encoding => "base64" }.merge(params)
+ part(params, &block)
+ end
- return m
+ private
+
+ def render_message(method_name, body)
+ ActionView::Base.new(template_path, body).render_file(method_name)
+ end
+
+ def template_path
+ template_root + "/" + Inflector.underscore(self.class.name)
end
- def deliver(mail) #:nodoc:
- logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
+ def create_mail
+ m = TMail::Mail.new
- begin
- send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
- rescue Object => e
- raise e if raise_delivery_errors
- end
+ m.subject, = quote_any_if_necessary(charset, subject)
+ m.to, m.from = quote_any_address_if_necessary(charset, recipients, from)
+ m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil?
+ m.cc = quote_address_if_necessary(cc, charset) unless cc.nil?
- return mail
- end
+ m.date = sent_on.to_time rescue sent_on if sent_on
+ headers.each { |k, v| m[k] = v }
- def quoted_printable(text, charset)#:nodoc:
- text = text.gsub( /[^a-z ]/i ) { "=%02x" % $&[0] }.gsub( / /, "_" )
- "=?#{charset}?Q?#{text}?="
- end
+ if @parts.empty?
+ m.set_content_type "text", "plain", { "charset" => charset }
+ m.body = body
+ else
+ if String === body
+ part = TMail::Mail.new
+ part.body = body
+ part.set_content_type "text", "plain", { "charset" => charset }
+ part.set_content_disposition "inline"
+ m.parts << part
+ end
- CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
+ @parts.each do |p|
+ part = (TMail::Mail === p ? p : p.to_mail(self))
+ m.parts << part
+ end
+ end
- # Quote the given text if it contains any "illegal" characters
- def quote_if_necessary(text, charset)
- (text =~ CHARS_NEEDING_QUOTING) ?
- quoted_printable(text, charset) :
- text
+ @mail = m
end
- # Quote any of the given strings if they contain any "illegal" characters
- def quote_any_if_necessary(charset, *args)
- args.map { |v| quote_if_necessary(v, charset) }
+ def perform_delivery_smtp(mail)
+ Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain],
+ server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
+ smtp.sendmail(mail.encoded, mail.from, mail.destinations)
+ end
end
- # Quote the given address if it needs to be. The address may be a
- # regular email address, or it can be a phrase followed by an address in
- # brackets. The phrase is the only part that will be quoted, and only if
- # it needs to be. This allows extended characters to be used in the
- # "to", "from", "cc", and "bcc" headers.
- def quote_address_if_necessary(address, charset)
- if Array === address
- address.map { |a| quote_address_if_necessary(a, charset) }
- elsif address =~ /^(\S.*)\s+(<.*>)$/
- address = $2
- phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
- "\"#{phrase}\" #{address}"
- else
- address
+ def perform_delivery_sendmail(mail)
+ IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
+ sm.print(mail.encoded)
+ sm.flush
end
end
- # Quote any of the given addresses, if they need to be.
- def quote_any_address_if_necessary(charset, *args)
- args.map { |v| quote_address_if_necessary(v, charset) }
+ def perform_delivery_test(mail)
+ deliveries << mail
+ end
+
+ class << self
+ def method_missing(method_symbol, *parameters)#:nodoc:
+ case method_symbol.id2name
+ when /^create_([_a-z]\w*)/ then new($1, *parameters).mail
+ when /^deliver_([_a-z]\w*)/ then new($1, *parameters).deliver!
+ end
end
def receive(raw_email)
@@ -186,51 +308,6 @@ module ActionMailer #:nodoc:
new.receive(mail)
end
- private
- def perform_delivery_smtp(mail)
- Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain],
- server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
- smtp.sendmail(mail.encoded, mail.from, mail.destinations)
- end
- end
-
- def perform_delivery_sendmail(mail)
- IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
- sm.print(mail.encoded)
- sm.flush
- end
- end
-
- def perform_delivery_test(mail)
- deliveries << mail
- end
-
- def create_from_action(method_name, *parameters)
- mailer = new
- mailer.body = {}
- mailer.send(method_name, *parameters)
-
- unless String === mailer.body then
- mailer.body = render_body mailer, method_name
- end
-
- mail = create(mailer.recipients, mailer.subject, mailer.body,
- mailer.from, mailer.sent_on, mailer.headers,
- mailer.charset)
-
- mail.bcc = quote_address_if_necessary(mailer.bcc, mailer.charset) unless mailer.bcc.nil?
- mail.cc = quote_address_if_necessary(mailer.cc, mailer.charset) unless mailer.cc.nil?
-
- return mail
- end
-
- def render_body(mailer, method_name)
- ActionView::Base.new(template_path, mailer.body).render_file(method_name)
- end
-
- def template_path
- template_root + "/" + Inflector.underscore(self.to_s)
- end
end
end
end
diff --git a/actionmailer/lib/action_mailer/part.rb b/actionmailer/lib/action_mailer/part.rb
new file mode 100644
index 0000000000..e26742816a
--- /dev/null
+++ b/actionmailer/lib/action_mailer/part.rb
@@ -0,0 +1,84 @@
+require 'action_mailer/adv_attr_accessor'
+
+module ActionMailer
+
+ class Part #:nodoc:
+ include ActionMailer::AdvAttrAccessor
+
+ adv_attr_accessor :content_type, :content_disposition, :charset, :body
+ adv_attr_accessor :filename, :transfer_encoding, :headers
+
+ def initialize(params)
+ @content_type = params[:content_type] || "text/plain"
+ @content_disposition = params[:disposition] || "inline"
+ @charset = params[:charset]
+ @body = params[:body]
+ @filename = params[:filename]
+ @transfer_encoding = params[:transfer_encoding] || "quoted-printable"
+ @headers = params[:headers] || {}
+ end
+
+ def to_mail(defaults)
+ part = TMail::Mail.new
+ part.set_content_type(content_type, nil,
+ "charset" => charset || defaults.charset, "name" => filename)
+ part.set_content_disposition(content_disposition,
+ "filename" => filename)
+
+ part.content_transfer_encoding = transfer_encoding || "quoted-printable"
+ case (transfer_encoding || "").downcase
+ when "base64" then
+ part.body = TMail::Base64.encode(body)
+ when "quoted-printable"
+ part.body = [body].pack("M*")
+ else
+ part.body = body
+ end
+
+ part
+ end
+ end
+
+end
+require 'action_mailer/adv_attr_accessor'
+
+module ActionMailer
+
+ class Part #:nodoc:
+ include ActionMailer::AdvAttrAccessor
+
+ adv_attr_accessor :content_type, :content_disposition, :charset, :body
+ adv_attr_accessor :filename, :transfer_encoding, :headers
+
+ def initialize(params)
+ @content_type = params[:content_type] || "text/plain"
+ @content_disposition = params[:disposition] || "inline"
+ @charset = params[:charset]
+ @body = params[:body]
+ @filename = params[:filename]
+ @transfer_encoding = params[:transfer_encoding] || "quoted-printable"
+ @headers = params[:headers] || {}
+ end
+
+ def to_mail(defaults)
+ part = TMail::Mail.new
+ part.set_content_type(content_type, nil,
+ "charset" => charset || defaults.charset, "name" => filename)
+ part.set_content_disposition(content_disposition,
+ "filename" => filename)
+
+ part.content_transfer_encoding = transfer_encoding || "quoted-printable"
+ case (transfer_encoding || "").downcase
+ when "base64" then
+ part.body = TMail::Base64.encode(body)
+ when "quoted-printable"
+ part.body = [body].pack("M*")
+ else
+ part.body = body
+ end
+
+ part
+ end
+ end
+
+end
diff --git a/actionmailer/lib/action_mailer/quoting.rb b/actionmailer/lib/action_mailer/quoting.rb
new file mode 100644
index 0000000000..28c456f25d
--- /dev/null
+++ b/actionmailer/lib/action_mailer/quoting.rb
@@ -0,0 +1,102 @@
+module ActionMailer
+ module Quoting
+
+ # Convert the given text into quoted printable format, with an instruction
+ # that the text be eventually interpreted in the given charset.
+ def quoted_printable(text, charset)
+ text = text.gsub( /[^a-z ]/i ) { "=%02x" % $&[0] }.gsub( / /, "_" )
+ "=?#{charset}?Q?#{text}?="
+ end
+
+ # A quick-and-dirty regexp for determining whether a string contains any
+ # characters that need escaping.
+ CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
+
+ # Quote the given text if it contains any "illegal" characters
+ def quote_if_necessary(text, charset)
+ (text =~ CHARS_NEEDING_QUOTING) ?
+ quoted_printable(text, charset) :
+ text
+ end
+
+ # Quote any of the given strings if they contain any "illegal" characters
+ def quote_any_if_necessary(charset, *args)
+ args.map { |v| quote_if_necessary(v, charset) }
+ end
+
+ # Quote the given address if it needs to be. The address may be a
+ # regular email address, or it can be a phrase followed by an address in
+ # brackets. The phrase is the only part that will be quoted, and only if
+ # it needs to be. This allows extended characters to be used in the
+ # "to", "from", "cc", and "bcc" headers.
+ def quote_address_if_necessary(address, charset)
+ if Array === address
+ address.map { |a| quote_address_if_necessary(a, charset) }
+ elsif address =~ /^(\S.*)\s+(<.*>)$/
+ address = $2
+ phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
+ "\"#{phrase}\" #{address}"
+ else
+ address
+ end
+ end
+
+ # Quote any of the given addresses, if they need to be.
+ def quote_any_address_if_necessary(charset, *args)
+ args.map { |v| quote_address_if_necessary(v, charset) }
+ end
+
+ end
+end
+module ActionMailer
+ module Quoting
+
+ # Convert the given text into quoted printable format, with an instruction
+ # that the text be eventually interpreted in the given charset.
+ def quoted_printable(text, charset)
+ text = text.gsub( /[^a-z ]/i ) { "=%02x" % $&[0] }.gsub( / /, "_" )
+ "=?#{charset}?Q?#{text}?="
+ end
+
+ # A quick-and-dirty regexp for determining whether a string contains any
+ # characters that need escaping.
+ if !defined?(CHARS_NEEDING_QUOTING)
+ CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
+ end
+
+ # Quote the given text if it contains any "illegal" characters
+ def quote_if_necessary(text, charset)
+ (text =~ CHARS_NEEDING_QUOTING) ?
+ quoted_printable(text, charset) :
+ text
+ end
+
+ # Quote any of the given strings if they contain any "illegal" characters
+ def quote_any_if_necessary(charset, *args)
+ args.map { |v| quote_if_necessary(v, charset) }
+ end
+
+ # Quote the given address if it needs to be. The address may be a
+ # regular email address, or it can be a phrase followed by an address in
+ # brackets. The phrase is the only part that will be quoted, and only if
+ # it needs to be. This allows extended characters to be used in the
+ # "to", "from", "cc", and "bcc" headers.
+ def quote_address_if_necessary(address, charset)
+ if Array === address
+ address.map { |a| quote_address_if_necessary(a, charset) }
+ elsif address =~ /^(\S.*)\s+(<.*>)$/
+ address = $2
+ phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
+ "\"#{phrase}\" #{address}"
+ else
+ address
+ end
+ end
+
+ # Quote any of the given addresses, if they need to be.
+ def quote_any_address_if_necessary(charset, *args)
+ args.map { |v| quote_address_if_necessary(v, charset) }
+ end
+
+ end
+end
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/encode.rb b/actionmailer/lib/action_mailer/vendor/tmail/encode.rb
index c0909f30d2..c7bbeef4c0 100755
--- a/actionmailer/lib/action_mailer/vendor/tmail/encode.rb
+++ b/actionmailer/lib/action_mailer/vendor/tmail/encode.rb
@@ -253,6 +253,7 @@ module TMail
# FIXME: implement line folding
#
def kv_pair( k, v )
+ return if v.nil?
v = normalize_encoding(v)
if token_safe?(v)
add_text k + '=' + v
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/facade.rb b/actionmailer/lib/action_mailer/vendor/tmail/facade.rb
index 0a12a61a75..cdbd4764f1 100755
--- a/actionmailer/lib/action_mailer/vendor/tmail/facade.rb
+++ b/actionmailer/lib/action_mailer/vendor/tmail/facade.rb
@@ -442,7 +442,8 @@ module TMail
h.disposition = str
h.params.clear
else
- h = store('Content-Disposition', str)
+ store('Content-Disposition', str)
+ h = @header['content-disposition']
end
h.params.replace params if params
end
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/utils.rb b/actionmailer/lib/action_mailer/vendor/tmail/utils.rb
index 4f603b96b9..57df234464 100755
--- a/actionmailer/lib/action_mailer/vendor/tmail/utils.rb
+++ b/actionmailer/lib/action_mailer/vendor/tmail/utils.rb
@@ -27,7 +27,7 @@ module TMail
t = Time.now
sprintf('%x%x_%x%x%d%x',
t.to_i, t.tv_usec,
- $$, Thread.current.id, @uniq, rand(255))
+ $$, Thread.current.object_id, @uniq, rand(255))
end
private_class_method :random_tag
diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml
new file mode 100644
index 0000000000..946d99ede5
--- /dev/null
+++ b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml
@@ -0,0 +1,10 @@
+<html>
+ <body>
+ HTML formatted message to <strong><%= @recipient %></strong>.
+ </body>
+</html>
+<html>
+ <body>
+ HTML formatted message to <strong><%= @recipient %></strong>.
+ </body>
+</html>
diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml
new file mode 100644
index 0000000000..a6c8d54cf9
--- /dev/null
+++ b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml
@@ -0,0 +1,2 @@
+Plain text to <%= @recipient %>.
+Plain text to <%= @recipient %>.
diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb
index 733ec61e41..861206fc54 100755
--- a/actionmailer/test/mail_service_test.rb
+++ b/actionmailer/test/mail_service_test.rb
@@ -14,21 +14,21 @@ class TestMailer < ActionMailer::Base
end
def cancelled_account(recipient)
- @recipients = recipient
- @subject = "[Cancelled] Goodbye #{recipient}"
- @from = "system@loudthinking.com"
- @sent_on = Time.local(2004, 12, 12)
- @body = "Goodbye, Mr. #{recipient}"
+ self.recipients = recipient
+ self.subject = "[Cancelled] Goodbye #{recipient}"
+ self.from = "system@loudthinking.com"
+ self.sent_on = Time.local(2004, 12, 12)
+ self.body = "Goodbye, Mr. #{recipient}"
end
def cc_bcc(recipient)
- @recipients = recipient
- @subject = "testing bcc/cc"
- @from = "system@loudthinking.com"
- @sent_on = Time.local 2004, 12, 12
- @cc = "nobody@loudthinking.com"
- @bcc = "root@loudthinking.com"
- @body = "Nothing to see here."
+ recipients recipient
+ subject "testing bcc/cc"
+ from "system@loudthinking.com"
+ sent_on Time.local(2004, 12, 12)
+ cc "nobody@loudthinking.com"
+ bcc "root@loudthinking.com"
+ body "Nothing to see here."
end
def iso_charset(recipient)
@@ -74,6 +74,30 @@ class TestMailer < ActionMailer::Base
@charset = "utf-8"
end
+ def explicitly_multipart_example(recipient)
+ @recipients = recipient
+ @subject = "multipart example"
+ @from = "test@example.com"
+ @sent_on = Time.local 2004, 12, 12
+ @body = "plain text default"
+
+ part "text/html" do |p|
+ p.charset = "iso-8859-1"
+ p.body = "blah"
+ end
+
+ attachment :content_type => "image/jpeg", :filename => "foo.jpg",
+ :body => "123456789"
+ end
+
+ def implicitly_multipart_example(recipient)
+ @recipients = recipient
+ @subject = "multipart example"
+ @from = "test@example.com"
+ @sent_on = Time.local 2004, 12, 12
+ @body = { "recipient" => recipient }
+ end
+
class <<self
attr_accessor :received_body
end
@@ -86,9 +110,10 @@ end
TestMailer.template_root = File.dirname(__FILE__) + "/fixtures"
class ActionMailerTest < Test::Unit::TestCase
+ include ActionMailer::Quoting
def encode( text, charset="utf-8" )
- ActionMailer::Base.quoted_printable( text, charset )
+ quoted_printable( text, charset )
end
def new_mail( charset="utf-8" )
@@ -312,12 +337,12 @@ EOF
@recipient = "Grytøyr <test@localhost>"
expected = new_mail "iso-8859-1"
- expected.to = TestMailer.quote_address_if_necessary @recipient, "iso-8859-1"
+ expected.to = quote_address_if_necessary @recipient, "iso-8859-1"
expected.subject = "testing extended headers"
expected.body = "Nothing to see here."
- expected.from = TestMailer.quote_address_if_necessary "Grytøyr <stian1@example.net>", "iso-8859-1"
- expected.cc = TestMailer.quote_address_if_necessary "Grytøyr <stian2@example.net>", "iso-8859-1"
- expected.bcc = TestMailer.quote_address_if_necessary "Grytøyr <stian3@example.net>", "iso-8859-1"
+ expected.from = quote_address_if_necessary "Grytøyr <stian1@example.net>", "iso-8859-1"
+ expected.cc = quote_address_if_necessary "Grytøyr <stian2@example.net>", "iso-8859-1"
+ expected.bcc = quote_address_if_necessary "Grytøyr <stian3@example.net>", "iso-8859-1"
expected.date = Time.local 2004, 12, 12
created = nil
@@ -339,12 +364,12 @@ EOF
def test_utf8_body_is_not_quoted
@recipient = "Foo áëô îü <extended@example.net>"
expected = new_mail "utf-8"
- expected.to = TestMailer.quote_address_if_necessary @recipient, "utf-8"
+ expected.to = quote_address_if_necessary @recipient, "utf-8"
expected.subject = "testing utf-8 body"
expected.body = "åœö blah"
- expected.from = TestMailer.quote_address_if_necessary @recipient, "utf-8"
- expected.cc = TestMailer.quote_address_if_necessary @recipient, "utf-8"
- expected.bcc = TestMailer.quote_address_if_necessary @recipient, "utf-8"
+ expected.from = quote_address_if_necessary @recipient, "utf-8"
+ expected.cc = quote_address_if_necessary @recipient, "utf-8"
+ expected.bcc = quote_address_if_necessary @recipient, "utf-8"
expected.date = Time.local 2004, 12, 12
created = TestMailer.create_utf8_body @recipient
@@ -354,12 +379,12 @@ EOF
def test_multiple_utf8_recipients
@recipient = ["\"Foo áëô îü\" <extended@example.net>", "\"Example Recipient\" <me@example.com>"]
expected = new_mail "utf-8"
- expected.to = TestMailer.quote_address_if_necessary @recipient, "utf-8"
+ expected.to = quote_address_if_necessary @recipient, "utf-8"
expected.subject = "testing utf-8 body"
expected.body = "åœö blah"
- expected.from = TestMailer.quote_address_if_necessary @recipient.first, "utf-8"
- expected.cc = TestMailer.quote_address_if_necessary @recipient, "utf-8"
- expected.bcc = TestMailer.quote_address_if_necessary @recipient, "utf-8"
+ expected.from = quote_address_if_necessary @recipient.first, "utf-8"
+ expected.cc = quote_address_if_necessary @recipient, "utf-8"
+ expected.bcc = quote_address_if_necessary @recipient, "utf-8"
expected.date = Time.local 2004, 12, 12
created = TestMailer.create_utf8_body @recipient
@@ -400,5 +425,26 @@ EOF
assert_nothing_raised { mail.body }
end
+ def test_explicitly_multipart_messages
+ mail = TestMailer.create_explicitly_multipart_example(@recipient)
+ assert_equal 3, mail.parts.length
+ assert_equal "text/plain", mail.parts[0].content_type
+
+ assert_equal "text/html", mail.parts[1].content_type
+ assert_equal "inline", mail.parts[1].content_disposition
+
+ assert_equal "image/jpeg", mail.parts[2].content_type
+ assert_equal "attachment", mail.parts[2].content_disposition
+ assert_equal "foo.jpg", mail.parts[2].sub_header("content-disposition", "filename")
+ assert_equal "foo.jpg", mail.parts[2].sub_header("content-type", "name")
+ end
+
+ def test_implicitly_multipart_messages
+ mail = TestMailer.create_implicitly_multipart_example(@recipient)
+ assert_equal 2, mail.parts.length
+ assert_equal "text/html", mail.parts[0].content_type
+ assert_equal "text/plain", mail.parts[1].content_type
+ end
+
end