base.rb 18.0 KB
Newer Older
1 2
require 'action_mailer/adv_attr_accessor'
require 'action_mailer/part'
3
require 'action_mailer/part_container'
4
require 'action_mailer/utils'
5
require 'tmail/net'
6

D
David Heinemeier Hansson 已提交
7
module ActionMailer #:nodoc:
D
Initial  
David Heinemeier Hansson 已提交
8 9 10
  # Usage:
  #
  #   class ApplicationMailer < ActionMailer::Base
11
  #     # Set up properties
D
David Heinemeier Hansson 已提交
12 13
  #     # Properties can also be specified via accessor methods
  #     # (i.e. self.subject = "foo") and instance variables (@subject = "foo").
14 15 16
  #     def signup_notification(recipient)
  #       recipients recipient.email_address_with_name
  #       subject    "New account information"
17
  #       body       "account" => recipient
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
  #       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
D
Initial  
David Heinemeier Hansson 已提交
48
  #     end
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
  #
  #     # 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.
D
Initial  
David Heinemeier Hansson 已提交
71 72 73
  #     end
  #   end
  #
D
David Heinemeier Hansson 已提交
74
  #   # After this, post_notification will look for "templates/application_mailer/post_notification.rhtml"
D
Initial  
David Heinemeier Hansson 已提交
75 76 77 78
  #   ApplicationMailer.template_root = "templates"
  #  
  #   ApplicationMailer.create_comment_notification(david, hello_world)  # => a tmail object
  #   ApplicationMailer.deliver_comment_notification(david, hello_world) # sends the email
D
David Heinemeier Hansson 已提交
79 80 81 82 83 84 85 86 87 88 89
  #
  # = Configuration options
  #
  # These options are specified on the class level, like <tt>ActionMailer::Base.template_root = "/my/templates"</tt>
  #
  # * <tt>template_root</tt> - template root determines the base from which template references will be made.
  #
  # * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
  #   Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
  #
  # * <tt>server_settings</tt> -  Allows detailed configuration of the server:
D
David Heinemeier Hansson 已提交
90 91
  #   * <tt>:address</tt> Allows you to use a remote mail server. Just change it from its default "localhost" setting.
  #   * <tt>:port</tt> On the off chance that your mail server doesn't run on port 25, you can change it.
D
David Heinemeier Hansson 已提交
92
  #   * <tt>:domain</tt> If you need to specify a HELO domain, you can do it here.
D
David Heinemeier Hansson 已提交
93 94
  #   * <tt>:user_name</tt> If your mail server requires authentication, set the username in this setting.
  #   * <tt>:password</tt> If your mail server requires authentication, set the password in this setting.
D
David Heinemeier Hansson 已提交
95 96 97 98 99 100 101 102 103 104 105 106 107
  #   * <tt>:authentication</tt> If your mail server requires authentication, you need to specify the authentication type here. 
  #     This is a symbol and one of :plain, :login, :cram_md5
  #
  # * <tt>raise_delivery_errors</tt> - whether or not errors should be raised if the email fails to be delivered.
  #
  # * <tt>delivery_method</tt> - Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test.
  #   Sendmail is assumed to be present at "/usr/sbin/sendmail".
  #
  # * <tt>perform_deliveries</tt> - Determines whether deliver_* methods are actually carried out. By default they are,
  #   but this can be turned off to help functional testing.
  #
  # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful
  #   for unit and functional testing.
D
David Heinemeier Hansson 已提交
108 109
  #
  # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also 
110
  #   pick a different charset from inside a method with <tt>@charset</tt>.
D
David Heinemeier Hansson 已提交
111
  # * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You
112
  #   can also pick a different content type from inside a method with <tt>@content_type</tt>. 
113 114 115
  # * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to nil. You
  #   can also pick a different value from inside a method with <tt>@mime_version</tt>. When multipart messages are in
  #   use, <tt>@mime_version</tt> will be set to "1.0" if it is not set inside a method.
D
David Heinemeier Hansson 已提交
116
  # * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates
117 118 119 120
  #   which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to
  #   ["text/html", "text/enriched", "text/plain"]. Items that appear first in the array have higher priority in the mail client
  #   and appear last in the mime encoded message. You can also pick a different order from inside a method with
  #   <tt>@implicit_parts_order</tt>.
D
Initial  
David Heinemeier Hansson 已提交
121
  class Base
122
    include AdvAttrAccessor, PartContainer
123

124 125
    # Action Mailer subclasses should be reloaded by the dispatcher in Rails
    # when Dependencies.mechanism = :load.
126
    include Reloadable::Subclasses
127
    
D
David Heinemeier Hansson 已提交
128
    private_class_method :new #:nodoc:
D
Initial  
David Heinemeier Hansson 已提交
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154

    cattr_accessor :template_root
    cattr_accessor :logger

    @@server_settings = { 
      :address        => "localhost", 
      :port           => 25, 
      :domain         => 'localhost.localdomain', 
      :user_name      => nil, 
      :password       => nil, 
      :authentication => nil
    }
    cattr_accessor :server_settings

    @@raise_delivery_errors = true
    cattr_accessor :raise_delivery_errors

    @@delivery_method = :smtp
    cattr_accessor :delivery_method
    
    @@perform_deliveries = true
    cattr_accessor :perform_deliveries
    
    @@deliveries = []
    cattr_accessor :deliveries

155 156 157
    @@default_charset = "utf-8"
    cattr_accessor :default_charset

158 159
    @@default_content_type = "text/plain"
    cattr_accessor :default_content_type
160 161 162
    
    @@default_mime_version = nil
    cattr_accessor :default_mime_version
163

164 165 166
    @@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ]
    cattr_accessor :default_implicit_parts_order

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
    # Specify the BCC addresses for the message
    adv_attr_accessor :bcc
    
    # Define the body of the message. This is either a Hash (in which case it
    # specifies the variables to pass to the template when it is rendered),
    # or a string, in which case it specifies the actual text of the message.
    adv_attr_accessor :body
    
    # Specify the CC addresses for the message.
    adv_attr_accessor :cc
    
    # Specify the charset to use for the message. This defaults to the
    # +default_charset+ specified for ActionMailer::Base.
    adv_attr_accessor :charset
    
    # Specify the content type for the message. This defaults to <tt>text/plain</tt>
    # in most cases, but can be automatically set in some situations.
    adv_attr_accessor :content_type
    
    # Specify the from address for the message.
    adv_attr_accessor :from
    
    # Specify additional headers to be added to the message.
    adv_attr_accessor :headers
    
    # Specify the order in which parts should be sorted, based on content-type.
    # This defaults to the value for the +default_implicit_parts_order+.
    adv_attr_accessor :implicit_parts_order
    
    # Override the mailer name, which defaults to an inflected version of the
    # mailer's class name. If you want to use a template in a non-standard
    # location, you can use this to specify that location.
    adv_attr_accessor :mailer_name
    
    # Defaults to "1.0", but may be explicitly given if needed.
    adv_attr_accessor :mime_version
    
    # The recipient addresses for the message, either as a string (for a single
    # address) or an array (for multiple addresses).
    adv_attr_accessor :recipients
    
    # The date on which the message was sent. If not set (the default), the
    # header will be set by the delivery agent.
    adv_attr_accessor :sent_on
    
    # Specify the subject of the message.
    adv_attr_accessor :subject
    
    # Specify the template name to use for current message. This is the "base"
    # template name, without the extension or directory, and may be used to
    # have multiple mailer methods share the same template.
    adv_attr_accessor :template
219

220 221
    # The mail object instance referenced by this mailer.
    attr_reader :mail
222

223 224 225 226 227 228 229 230 231 232
    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!
          when "new" then nil
          else super
        end
      end

233 234 235 236 237 238 239 240 241 242 243 244
      # Receives a raw email, parses it into an email object, decodes it,
      # instantiates a new mailer, and passes the email object to the mailer
      # object's #receive method. If you want your mailer to be able to
      # process incoming messages, you'll need to implement a #receive
      # method that accepts the email object as a parameter:
      #
      #   class MyMailer < ActionMailer::Base
      #     def receive(mail)
      #       ...
      #     end
      #   end
      def receive(raw_email)
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
        logger.info "Received mail:\n #{raw_email}" unless logger.nil?
        mail = TMail::Mail.parse(raw_email)
        mail.base64_decode
        new.receive(mail)
      end

      # Deliver the given mail object directly. This can be used to deliver
      # a preconstructed mail object, like:
      #
      #   email = MyMailer.create_some_mail(parameters)
      #   email.set_some_obscure_header "frobnicate"
      #   MyMailer.deliver(email)
      def deliver(mail)
        new.deliver!(mail)
      end
    end

262 263 264 265
    # 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).
266
    def initialize(method_name=nil, *parameters) #:nodoc:
267 268 269 270 271
      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.
272
    def create!(method_name, *parameters) #:nodoc:
273
      initialize_defaults(method_name)
274 275 276 277 278 279
      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.,
280 281
        # "the_template_file.text.html.rhtml", etc.). Only do this if parts
        # have not already been specified manually.
282
        if @parts.empty?
283
          templates = Dir.glob("#{template_path}/#{@template}.*")
284
          templates.each do |path|
285
            # TODO: don't hardcode rhtml|rxml
286 287 288
            basename = File.basename(path)
            next unless md = /^([^\.]+)\.([^\.]+\.[^\+]+)\.(rhtml|rxml)$/.match(basename)
            template_name = basename
289 290
            content_type = md.captures[1].gsub('.', '/')
            @parts << Part.new(:content_type => content_type,
291
              :disposition => "inline", :charset => charset,
292
              :body => render_message(template_name, @body))
293
          end
294 295 296 297
          unless @parts.empty?
            @content_type = "multipart/alternative"
            @parts = sort_parts(@parts, @implicit_parts_order)
          end
298
        end
D
Initial  
David Heinemeier Hansson 已提交
299

300 301 302 303 304
        # 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?
305
        template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| File.basename(i).split(".").length == 2 }
306
        @body = render_message(@template, @body) if template_exists
307 308 309 310 311 312 313

        # 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
314
        end
D
Initial  
David Heinemeier Hansson 已提交
315 316
      end

317 318 319 320
      # If this is a multipart e-mail add the mime_version if it is not
      # already set.
      @mime_version ||= "1.0" if !@parts.empty?

321 322 323
      # build the mail object itself
      @mail = create_mail
    end
D
Initial  
David Heinemeier Hansson 已提交
324

325 326 327
    # Delivers a TMail::Mail object. By default, it delivers the cached mail
    # object (from the #create! method). If no cached mail object exists, and
    # no alternate has been given as the parameter, this will fail.
328
    def deliver!(mail = @mail)
329
      raise "no mail object available for delivery!" unless mail
330
      logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
331

332
      begin
333
        send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
334 335 336
      rescue Object => e
        raise e if raise_delivery_errors
      end
337

338
      return mail
339
    end
340

341
    private
342 343 344 345
      # Set up the default values for the various instance variables of this
      # mailer. Subclasses may override this method to provide different
      # defaults.
      def initialize_defaults(method_name)
346 347 348 349 350 351 352 353
        @charset ||= @@default_charset.dup
        @content_type ||= @@default_content_type.dup
        @implicit_parts_order ||= @@default_implicit_parts_order.dup
        @template ||= method_name
        @mailer_name ||= Inflector.underscore(self.class.name)
        @parts ||= []
        @headers ||= {}
        @body ||= {}
354
        @mime_version = @@default_mime_version.dup if @@default_mime_version
355 356
      end

357
      def render_message(method_name, body)
358
        render :file => method_name, :body => body
359
      end
360 361 362 363 364 365

      def render(opts)
        body = opts.delete(:body)
        initialize_template_class(body).render(opts)
      end

366
      def template_path
367
        "#{template_root}/#{mailer_name}"
D
Initial  
David Heinemeier Hansson 已提交
368 369
      end

J
Jamis Buck 已提交
370 371 372 373
      def initialize_template_class(assigns)
        ActionView::Base.new(template_path, assigns, self)
      end

374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
      def sort_parts(parts, order = [])
        order = order.collect { |s| s.downcase }

        parts = parts.sort do |a, b|
          a_ct = a.content_type.downcase
          b_ct = b.content_type.downcase

          a_in = order.include? a_ct
          b_in = order.include? b_ct

          s = case
          when a_in && b_in
            order.index(a_ct) <=> order.index(b_ct)
          when a_in
            -1
          when b_in
            1
          else
            a_ct <=> b_ct
          end

          # reverse the ordering because parts that come last are displayed
          # first in mail clients
          (s * -1)
        end

        parts
      end

403 404
      def create_mail
        m = TMail::Mail.new
405

406 407 408 409
        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?
410

411
        m.mime_version = mime_version unless mime_version.nil?
412 413
        m.date = sent_on.to_time rescue sent_on if sent_on
        headers.each { |k, v| m[k] = v }
D
Initial  
David Heinemeier Hansson 已提交
414

415 416
        real_content_type, ctype_attrs = parse_content_type

417
        if @parts.empty?
418
          m.set_content_type(real_content_type, nil, ctype_attrs)
419
          m.body = Utils.normalize_new_lines(body)
420 421 422
        else
          if String === body
            part = TMail::Mail.new
423
            part.body = Utils.normalize_new_lines(body)
424
            part.set_content_type(real_content_type, nil, ctype_attrs)
425 426 427
            part.set_content_disposition "inline"
            m.parts << part
          end
428

429 430 431 432
          @parts.each do |p|
            part = (TMail::Mail === p ? p : p.to_mail(self))
            m.parts << part
          end
433
          
434 435 436 437
          if real_content_type =~ /multipart/
            ctype_attrs.delete "charset"
            m.set_content_type(real_content_type, nil, ctype_attrs)
          end
438
        end
439

440
        @mail = m
441 442
      end

443
      def perform_delivery_smtp(mail)
444 445 446
        destinations = mail.destinations
        mail.ready_to_send

447 448
        Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain], 
            server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
449
          smtp.sendmail(mail.encoded, mail.from, destinations)
450
        end
451 452
      end

453 454
      def perform_delivery_sendmail(mail)
        IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
455
          sm.print(mail.encoded.gsub(/\r/, ''))
456
          sm.flush
457 458 459
        end
      end

460 461 462
      def perform_delivery_test(mail)
        deliveries << mail
      end
D
Initial  
David Heinemeier Hansson 已提交
463 464
  end
end