mirror of
https://github.com/github/rails.git
synced 2026-01-25 06:18:04 -05:00
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
This commit is contained in:
@@ -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
|
||||
* First release
|
||||
|
||||
@@ -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
|
||||
$VERBOSE = old_verbose
|
||||
|
||||
58
actionmailer/lib/action_mailer/adv_attr_accessor.rb
Normal file
58
actionmailer/lib/action_mailer/adv_attr_accessor.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# # 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
|
||||
#
|
||||
# # 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,96 +138,169 @@ 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
|
||||
@charset = @@default_charset.dup
|
||||
@headers = {}
|
||||
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 = {}
|
||||
@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
|
||||
|
||||
# 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
|
||||
|
||||
# build the mail object itself
|
||||
@mail = create_mail
|
||||
end
|
||||
|
||||
# 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?
|
||||
|
||||
begin
|
||||
send("perform_delivery_#{delivery_method}", @mail) if perform_deliveries
|
||||
rescue Object => e
|
||||
raise e if raise_delivery_errors
|
||||
end
|
||||
|
||||
return @mail
|
||||
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
|
||||
|
||||
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 create_mail
|
||||
m = TMail::Mail.new
|
||||
|
||||
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?
|
||||
|
||||
m.date = sent_on.to_time rescue sent_on if sent_on
|
||||
headers.each { |k, v| m[k] = v }
|
||||
|
||||
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
|
||||
|
||||
@parts.each do |p|
|
||||
part = (TMail::Mail === p ? p : p.to_mail(self))
|
||||
m.parts << part
|
||||
end
|
||||
end
|
||||
|
||||
@mail = m
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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))
|
||||
when /^create_([_a-z]\w*)/ then new($1, *parameters).mail
|
||||
when /^deliver_([_a-z]\w*)/ then new($1, *parameters).deliver!
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
m.date = timestamp.respond_to?("to_time") ? timestamp.to_time : (timestamp || Time.now)
|
||||
|
||||
m.set_content_type "text", "plain", { "charset" => charset }
|
||||
|
||||
headers.each do |k, v|
|
||||
m[k] = v
|
||||
end
|
||||
|
||||
return m
|
||||
end
|
||||
|
||||
def deliver(mail) #:nodoc:
|
||||
logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
|
||||
|
||||
begin
|
||||
send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
|
||||
rescue Object => e
|
||||
raise e if raise_delivery_errors
|
||||
end
|
||||
|
||||
return mail
|
||||
end
|
||||
|
||||
def quoted_printable(text, charset)#:nodoc:
|
||||
text = text.gsub( /[^a-z ]/i ) { "=%02x" % $&[0] }.gsub( / /, "_" )
|
||||
"=?#{charset}?Q?#{text}?="
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def receive(raw_email)
|
||||
logger.info "Received mail:\n #{raw_email}" unless logger.nil?
|
||||
mail = TMail::Mail.parse(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
|
||||
|
||||
84
actionmailer/lib/action_mailer/part.rb
Normal file
84
actionmailer/lib/action_mailer/part.rb
Normal file
@@ -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
|
||||
102
actionmailer/lib/action_mailer/quoting.rb
Normal file
102
actionmailer/lib/action_mailer/quoting.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
10
actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml
vendored
Normal file
10
actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml
vendored
Normal file
@@ -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>
|
||||
2
actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml
vendored
Normal file
2
actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
Plain text to <%= @recipient %>.
|
||||
Plain text to <%= @recipient %>.
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user