base.rb 12.5 KB
Newer Older
1 2
require 'action_mailer/adv_attr_accessor'
require 'action_mailer/part'
3
require 'tmail/net'
4

D
Initial  
David Heinemeier Hansson 已提交
5 6 7 8
module ActionMailer #:nodoc:
  # Usage:
  #
  #   class ApplicationMailer < ActionMailer::Base
9 10 11 12 13 14 15 16 17 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
  #     # 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
D
Initial  
David Heinemeier Hansson 已提交
46
  #     end
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
  #
  #     # 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 已提交
69 70 71 72 73 74 75 76
  #     end
  #   end
  #
  #   # After this post_notification will look for "templates/application_mailer/post_notification.rhtml"
  #   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 已提交
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
  #
  # = 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:
  #   * <tt>:address</tt> Allows you to use a remote mail server. Just change it away from it's default "localhost" setting.
  #   * <tt>:port</tt> On the off change that your mail server doesn't run on port 25, you can change it.
  #   * <tt>:domain</tt> If you need to specify a HELO domain, you can do it here.
  #   * <tt>:user_name</tt> If your mail server requires authentication, set the username and password in these two settings.
  #   * <tt>:password</tt> If your mail server requires authentication, set the username and password in these two settings.
  #   * <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 已提交
106 107
  #
  # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also 
108
  #    pick a different charset from inside a method with <tt>@charset</tt>.
D
Initial  
David Heinemeier Hansson 已提交
109
  class Base
110 111
    include ActionMailer::AdvAttrAccessor

D
David Heinemeier Hansson 已提交
112
    private_class_method :new #:nodoc:
D
Initial  
David Heinemeier Hansson 已提交
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138

    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

139 140 141
    @@default_charset = "utf-8"
    cattr_accessor :default_charset

142 143 144
    @@default_content_type = "text/plain"
    cattr_accessor :default_content_type

145
    adv_attr_accessor :recipients, :subject, :body, :from, :sent_on, :headers,
146
                      :bcc, :cc, :charset, :content_type
147

148 149 150 151 152 153 154 155 156 157 158 159 160 161
    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
162
      @charset = @@default_charset.dup
163
      @content_type = @@default_content_type.dup
164
      @parts = []
165
      @headers = {}
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
      @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,
181
              :disposition => "inline", :charset => charset,
182 183 184
              :body => render_message(File.basename(path).split(".")[0..-2].join('.'), @body))
          end
        end
D
Initial  
David Heinemeier Hansson 已提交
185

186 187 188 189 190 191 192 193 194 195 196 197 198 199
        # 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
200
        end
D
Initial  
David Heinemeier Hansson 已提交
201 202
      end

203 204 205
      # build the mail object itself
      @mail = create_mail
    end
D
Initial  
David Heinemeier Hansson 已提交
206

207 208 209 210 211
    # 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?
212

213 214 215 216 217
      begin
        send("perform_delivery_#{delivery_method}", @mail) if perform_deliveries
      rescue Object => e
        raise e if raise_delivery_errors
      end
218

219 220
      return @mail
    end
221

222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    # 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
240

241 242
    private
      def render_message(method_name, body)
J
Jamis Buck 已提交
243
        initialize_template_class(body).render_file(method_name)
244 245 246 247
      end
        
      def template_path
        template_root + "/" + Inflector.underscore(self.class.name)
D
Initial  
David Heinemeier Hansson 已提交
248 249
      end

J
Jamis Buck 已提交
250 251 252 253
      def initialize_template_class(assigns)
        ActionView::Base.new(template_path, assigns, self)
      end

254 255
      def create_mail
        m = TMail::Mail.new
256

257 258 259 260
        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?
261

262 263
        m.date = sent_on.to_time rescue sent_on if sent_on
        headers.each { |k, v| m[k] = v }
D
Initial  
David Heinemeier Hansson 已提交
264

265
        if @parts.empty?
266
          m.set_content_type content_type, nil, { "charset" => charset }
267 268 269 270 271
          m.body = body
        else
          if String === body
            part = TMail::Mail.new
            part.body = body
272
            part.set_content_type content_type, nil, { "charset" => charset }
273 274 275
            part.set_content_disposition "inline"
            m.parts << part
          end
276

277 278 279 280
          @parts.each do |p|
            part = (TMail::Mail === p ? p : p.to_mail(self))
            m.parts << part
          end
281 282
          
          m.set_content_type(content_type, nil, { "charset" => charset }) if content_type =~ /multipart/
283
        end
284

285
        @mail = m
286 287
      end

288
      def perform_delivery_smtp(mail)
289 290 291
        destinations = mail.destinations
        mail.ready_to_send

292 293
        Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain], 
            server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
294
          smtp.sendmail(mail.encoded, mail.from, destinations)
295
        end
296 297
      end

298 299 300 301
      def perform_delivery_sendmail(mail)
        IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
          sm.print(mail.encoded)
          sm.flush
302 303 304
        end
      end

305 306 307 308 309 310 311 312 313
      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!
J
Jamis Buck 已提交
314 315
          when "new" then nil
          else super
316
        end
317 318
      end

319 320
      def receive(raw_email)
        logger.info "Received mail:\n #{raw_email}" unless logger.nil?
321 322 323
        mail = TMail::Mail.parse(raw_email)
        mail.base64_decode
        new.receive(mail)
324 325
      end

D
Initial  
David Heinemeier Hansson 已提交
326 327 328
    end
  end
end