aboutsummaryrefslogtreecommitdiffstats
path: root/actionmailer/lib/action_mailer/message_delivery.rb
blob: 0b54e124315fb74ce888f466f08e0f71b9fa258e (plain) (blame)
1
2
3
4
5
6
7
8
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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
106
107
108
109
110
111
112
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
139
140
141
142
require "delegate"

module ActionMailer
  # The <tt>ActionMailer::MessageDelivery</tt> class is used by
  # <tt>ActionMailer::Base</tt> when creating a new mailer.
  # <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy
  # created <tt>Mail::Message</tt>. You can get direct access to the
  # <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent
  # through Active Job.
  #
  #   Notifier.welcome(User.first)               # an ActionMailer::MessageDelivery object
  #   Notifier.welcome(User.first).deliver_now   # sends the email
  #   Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
  #   Notifier.welcome(User.first).message       # a Mail::Message object
  class MessageDelivery < Delegator
    def initialize(mailer_class, action, *args) #:nodoc:
      @mailer_class, @action, @args = mailer_class, action, args

      # The mail is only processed if we try to call any methods on it.
      # Typical usage will leave it unloaded and call deliver_later.
      @processed_mailer = nil
      @mail_message = nil
    end

    # Method calls are delegated to the Mail::Message that's ready to deliver.
    def __getobj__ #:nodoc:
      @mail_message ||= processed_mailer.message
    end

    # Unused except for delegator internals (dup, marshaling).
    def __setobj__(mail_message) #:nodoc:
      @mail_message = mail_message
    end

    # Returns the resulting Mail::Message
    def message
      __getobj__
    end

    # Was the delegate loaded, causing the mailer action to be processed?
    def processed?
      @processed_mailer || @mail_message
    end

    # Enqueues the email to be delivered through Active Job. When the
    # job runs it will send the email using +deliver_now!+. That means
    # that the message will be sent bypassing checking +perform_deliveries+
    # and +raise_delivery_errors+, so use with caution.
    #
    #   Notifier.welcome(User.first).deliver_later!
    #   Notifier.welcome(User.first).deliver_later!(wait: 1.hour)
    #   Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now)
    #
    # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
    # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
    # +delivery_job+.
    #
    #   class AccountRegistrationMailer < ApplicationMailer
    #     self.delivery_job = RegistrationDeliveryJob
    #   end
    #
    # Options:
    #
    # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
    # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
    # * <tt>:queue</tt> - Enqueue the email on the specified queue
    def deliver_later!(options = {})
      enqueue_delivery :deliver_now!, options
    end

    # Enqueues the email to be delivered through Active Job. When the
    # job runs it will send the email using +deliver_now+.
    #
    #   Notifier.welcome(User.first).deliver_later
    #   Notifier.welcome(User.first).deliver_later(wait: 1.hour)
    #   Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now)
    #
    # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
    # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
    # +delivery_job+.
    #
    #   class AccountRegistrationMailer < ApplicationMailer
    #     self.delivery_job = RegistrationDeliveryJob
    #   end
    #
    # Options:
    #
    # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay.
    # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time.
    # * <tt>:queue</tt> - Enqueue the email on the specified queue.
    def deliver_later(options = {})
      enqueue_delivery :deliver_now, options
    end

    # Delivers an email without checking +perform_deliveries+ and +raise_delivery_errors+,
    # so use with caution.
    #
    #   Notifier.welcome(User.first).deliver_now!
    #
    def deliver_now!
      processed_mailer.handle_exceptions do
        message.deliver!
      end
    end

    # Delivers an email:
    #
    #   Notifier.welcome(User.first).deliver_now
    #
    def deliver_now
      processed_mailer.handle_exceptions do
        message.deliver
      end
    end

    private
      # Returns the processed Mailer instance. We keep this instance
      # on hand so we can delegate exception handling to it.
      def processed_mailer
        @processed_mailer ||= @mailer_class.new.tap do |mailer|
          mailer.process @action, *@args
        end
      end

      def enqueue_delivery(delivery_method, options = {})
        if processed?
          ::Kernel.raise "You've accessed the message before asking to " \
            "deliver it later, so you may have made local changes that would " \
            "be silently lost if we enqueued a job to deliver it. Why? Only " \
            "the mailer method *arguments* are passed with the delivery job! " \
            "Do not access the message in any way if you mean to deliver it " \
            "later. Workarounds: 1. don't touch the message before calling " \
            "#deliver_later, 2. only touch the message *within your mailer " \
            "method*, or 3. use a custom Active Job instead of #deliver_later."
        else
          args = @mailer_class.name, @action.to_s, delivery_method.to_s, *@args
          job = @mailer_class.delivery_job
          job.set(options).perform_later(*args)
        end
      end
  end
end