aboutsummaryrefslogtreecommitdiffstats
path: root/actionmailer/lib/action_mailer/base.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionmailer/lib/action_mailer/base.rb')
-rw-r--r--actionmailer/lib/action_mailer/base.rb323
1 files changed, 200 insertions, 123 deletions
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