loading
Generated 2020-08-25T22:52:53-04:00

All Files ( 83.05% covered at 28.24 hits/line )

19 files in total.
767 relevant lines, 637 lines covered and 130 lines missed. ( 83.05% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/action_mailer.rb 90.91 % 71 33 30 3 0.91
lib/action_mailer/base.rb 100.00 % 1031 218 218 0 69.52
lib/action_mailer/collector.rb 100.00 % 32 21 21 0 12.86
lib/action_mailer/delivery_job.rb 72.22 % 45 18 13 5 1.00
lib/action_mailer/delivery_methods.rb 100.00 % 82 34 34 0 46.15
lib/action_mailer/gem_version.rb 88.89 % 17 9 8 1 0.89
lib/action_mailer/inline_preview_interceptor.rb 70.83 % 57 24 17 7 1.04
lib/action_mailer/log_subscriber.rb 100.00 % 39 18 18 0 8.61
lib/action_mailer/mail_delivery_job.rb 89.47 % 43 19 17 2 6.37
lib/action_mailer/mail_helper.rb 100.00 % 72 23 23 0 11.35
lib/action_mailer/message_delivery.rb 100.00 % 155 37 37 0 54.78
lib/action_mailer/parameterized.rb 95.45 % 172 44 42 2 4.34
lib/action_mailer/preview.rb 81.36 % 142 59 48 11 2.47
lib/action_mailer/railtie.rb 0.00 % 88 68 0 68 0.00
lib/action_mailer/rescuable.rb 100.00 % 29 14 14 0 47.43
lib/action_mailer/test_case.rb 98.55 % 121 69 68 1 12.51
lib/action_mailer/test_helper.rb 100.00 % 162 26 26 0 5.81
lib/action_mailer/version.rb 75.00 % 11 4 3 1 0.75
lib/rails/generators/mailer/mailer_generator.rb 0.00 % 38 29 0 29 0.00

lib/action_mailer.rb

90.91% lines covered

33 relevant lines. 30 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. #--
  3. # Copyright (c) 2004-2020 David Heinemeier Hansson
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining
  6. # a copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish,
  9. # distribute, sublicense, and/or sell copies of the Software, and to
  10. # permit persons to whom the Software is furnished to do so, subject to
  11. # the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be
  14. # included in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  20. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  21. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  22. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. #++
  24. 1 require "abstract_controller"
  25. 1 require "action_mailer/version"
  26. # Common Active Support usage in Action Mailer
  27. 1 require "active_support"
  28. 1 require "active_support/rails"
  29. 1 require "active_support/core_ext/class"
  30. 1 require "active_support/core_ext/module/attr_internal"
  31. 1 require "active_support/core_ext/string/inflections"
  32. 1 require "active_support/lazy_load_hooks"
  33. 1 module ActionMailer
  34. 1 extend ::ActiveSupport::Autoload
  35. 1 eager_autoload do
  36. 1 autoload :Collector
  37. end
  38. 1 autoload :Base
  39. 1 autoload :DeliveryMethods
  40. 1 autoload :InlinePreviewInterceptor
  41. 1 autoload :MailHelper
  42. 1 autoload :Parameterized
  43. 1 autoload :Preview
  44. 1 autoload :Previews, "action_mailer/preview"
  45. 1 autoload :TestCase
  46. 1 autoload :TestHelper
  47. 1 autoload :MessageDelivery
  48. 1 autoload :DeliveryJob
  49. 1 autoload :MailDeliveryJob
  50. 1 def self.eager_load!
  51. super
  52. require "mail"
  53. Mail.eager_autoload!
  54. end
  55. end
  56. 1 autoload :Mime, "action_dispatch/http/mime_type"
  57. 1 ActiveSupport.on_load(:action_view) do
  58. 1 ActionView::Base.default_formats ||= Mime::SET.symbols
  59. 1 ActionView::Template::Types.delegate_to Mime
  60. 1 ActionView::LookupContext::DetailsKey.clear
  61. end

lib/action_mailer/base.rb

100.0% lines covered

218 relevant lines. 218 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "mail"
  3. 1 require "action_mailer/collector"
  4. 1 require "active_support/core_ext/string/inflections"
  5. 1 require "active_support/core_ext/hash/except"
  6. 1 require "active_support/core_ext/module/anonymous"
  7. 1 require "action_mailer/log_subscriber"
  8. 1 require "action_mailer/rescuable"
  9. 1 module ActionMailer
  10. # Action Mailer allows you to send email from your application using a mailer model and views.
  11. #
  12. # = Mailer Models
  13. #
  14. # To use Action Mailer, you need to create a mailer model.
  15. #
  16. # $ bin/rails generate mailer Notifier
  17. #
  18. # The generated model inherits from <tt>ApplicationMailer</tt> which in turn
  19. # inherits from <tt>ActionMailer::Base</tt>. A mailer model defines methods
  20. # used to generate an email message. In these methods, you can set up variables to be used in
  21. # the mailer views, options on the mail itself such as the <tt>:from</tt> address, and attachments.
  22. #
  23. # class ApplicationMailer < ActionMailer::Base
  24. # default from: 'from@example.com'
  25. # layout 'mailer'
  26. # end
  27. #
  28. # class NotifierMailer < ApplicationMailer
  29. # default from: 'no-reply@example.com',
  30. # return_path: 'system@example.com'
  31. #
  32. # def welcome(recipient)
  33. # @account = recipient
  34. # mail(to: recipient.email_address_with_name,
  35. # bcc: ["bcc@example.com", "Order Watcher <watcher@example.com>"])
  36. # end
  37. # end
  38. #
  39. # Within the mailer method, you have access to the following methods:
  40. #
  41. # * <tt>attachments[]=</tt> - Allows you to add attachments to your email in an intuitive
  42. # manner; <tt>attachments['filename.png'] = File.read('path/to/filename.png')</tt>
  43. #
  44. # * <tt>attachments.inline[]=</tt> - Allows you to add an inline attachment to your email
  45. # in the same manner as <tt>attachments[]=</tt>
  46. #
  47. # * <tt>headers[]=</tt> - Allows you to specify any header field in your email such
  48. # as <tt>headers['X-No-Spam'] = 'True'</tt>. Note that declaring a header multiple times
  49. # will add many fields of the same name. Read #headers doc for more information.
  50. #
  51. # * <tt>headers(hash)</tt> - Allows you to specify multiple headers in your email such
  52. # as <tt>headers({'X-No-Spam' => 'True', 'In-Reply-To' => '1234@message.id'})</tt>
  53. #
  54. # * <tt>mail</tt> - Allows you to specify email to be sent.
  55. #
  56. # The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt>
  57. # will accept (any valid email header including optional fields).
  58. #
  59. # The +mail+ method, if not passed a block, will inspect your views and send all the views with
  60. # the same name as the method, so the above action would send the +welcome.text.erb+ view
  61. # file as well as the +welcome.html.erb+ view file in a +multipart/alternative+ email.
  62. #
  63. # If you want to explicitly render only certain templates, pass a block:
  64. #
  65. # mail(to: user.email) do |format|
  66. # format.text
  67. # format.html
  68. # end
  69. #
  70. # The block syntax is also useful in providing information specific to a part:
  71. #
  72. # mail(to: user.email) do |format|
  73. # format.text(content_transfer_encoding: "base64")
  74. # format.html
  75. # end
  76. #
  77. # Or even to render a special view:
  78. #
  79. # mail(to: user.email) do |format|
  80. # format.text
  81. # format.html { render "some_other_template" }
  82. # end
  83. #
  84. # = Mailer views
  85. #
  86. # Like Action Controller, each mailer class has a corresponding view directory in which each
  87. # method of the class looks for a template with its name.
  88. #
  89. # To define a template to be used with a mailer, create an <tt>.erb</tt> file with the same
  90. # name as the method in your mailer model. For example, in the mailer defined above, the template at
  91. # <tt>app/views/notifier_mailer/welcome.text.erb</tt> would be used to generate the email.
  92. #
  93. # Variables defined in the methods of your mailer model are accessible as instance variables in their
  94. # corresponding view.
  95. #
  96. # Emails by default are sent in plain text, so a sample view for our model example might look like this:
  97. #
  98. # Hi <%= @account.name %>,
  99. # Thanks for joining our service! Please check back often.
  100. #
  101. # You can even use Action View helpers in these views. For example:
  102. #
  103. # You got a new note!
  104. # <%= truncate(@note.body, length: 25) %>
  105. #
  106. # If you need to access the subject, from or the recipients in the view, you can do that through message object:
  107. #
  108. # You got a new note from <%= message.from %>!
  109. # <%= truncate(@note.body, length: 25) %>
  110. #
  111. #
  112. # = Generating URLs
  113. #
  114. # URLs can be generated in mailer views using <tt>url_for</tt> or named routes. Unlike controllers from
  115. # Action Pack, the mailer instance doesn't have any context about the incoming request, so you'll need
  116. # to provide all of the details needed to generate a URL.
  117. #
  118. # When using <tt>url_for</tt> you'll need to provide the <tt>:host</tt>, <tt>:controller</tt>, and <tt>:action</tt>:
  119. #
  120. # <%= url_for(host: "example.com", controller: "welcome", action: "greeting") %>
  121. #
  122. # When using named routes you only need to supply the <tt>:host</tt>:
  123. #
  124. # <%= users_url(host: "example.com") %>
  125. #
  126. # You should use the <tt>named_route_url</tt> style (which generates absolute URLs) and avoid using the
  127. # <tt>named_route_path</tt> style (which generates relative URLs), since clients reading the mail will
  128. # have no concept of a current URL from which to determine a relative path.
  129. #
  130. # It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt>
  131. # option as a configuration option in <tt>config/application.rb</tt>:
  132. #
  133. # config.action_mailer.default_url_options = { host: "example.com" }
  134. #
  135. # You can also define a <tt>default_url_options</tt> method on individual mailers to override these
  136. # default settings per-mailer.
  137. #
  138. # By default when <tt>config.force_ssl</tt> is +true+, URLs generated for hosts will use the HTTPS protocol.
  139. #
  140. # = Sending mail
  141. #
  142. # Once a mailer action and template are defined, you can deliver your message or defer its creation and
  143. # delivery for later:
  144. #
  145. # NotifierMailer.welcome(User.first).deliver_now # sends the email
  146. # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object
  147. # mail.deliver_now # generates and sends the email now
  148. #
  149. # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call
  150. # your method to generate the mail. If you want direct access to the delegator, or <tt>Mail::Message</tt>,
  151. # you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object.
  152. #
  153. # NotifierMailer.welcome(User.first).message # => a Mail::Message object
  154. #
  155. # Action Mailer is nicely integrated with Active Job so you can generate and send emails in the background
  156. # (example: outside of the request-response cycle, so the user doesn't have to wait on it):
  157. #
  158. # NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job
  159. #
  160. # Note that <tt>deliver_later</tt> will execute your method from the background job.
  161. #
  162. # You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
  163. # All instance methods are expected to return a message object to be sent.
  164. #
  165. # = Multipart Emails
  166. #
  167. # Multipart messages can also be used implicitly because Action Mailer will automatically detect and use
  168. # multipart templates, where each template is named after the name of the action, followed by the content
  169. # type. Each such detected template will be added to the message, as a separate part.
  170. #
  171. # For example, if the following templates exist:
  172. # * signup_notification.text.erb
  173. # * signup_notification.html.erb
  174. # * signup_notification.xml.builder
  175. # * signup_notification.yml.erb
  176. #
  177. # Each would be rendered and added as a separate part to the message, with the corresponding content
  178. # type. The content type for the entire message is automatically set to <tt>multipart/alternative</tt>,
  179. # which indicates that the email contains multiple different representations of the same email
  180. # body. The same instance variables defined in the action are passed to all email templates.
  181. #
  182. # Implicit template rendering is not performed if any attachments or parts have been added to the email.
  183. # This means that you'll have to manually add each part to the email and set the content type of the email
  184. # to <tt>multipart/alternative</tt>.
  185. #
  186. # = Attachments
  187. #
  188. # Sending attachment in emails is easy:
  189. #
  190. # class NotifierMailer < ApplicationMailer
  191. # def welcome(recipient)
  192. # attachments['free_book.pdf'] = File.read('path/to/file.pdf')
  193. # mail(to: recipient, subject: "New account information")
  194. # end
  195. # end
  196. #
  197. # Which will (if it had both a <tt>welcome.text.erb</tt> and <tt>welcome.html.erb</tt>
  198. # template in the view directory), send a complete <tt>multipart/mixed</tt> email with two parts,
  199. # the first part being a <tt>multipart/alternative</tt> with the text and HTML email parts inside,
  200. # and the second being a <tt>application/pdf</tt> with a Base64 encoded copy of the file.pdf book
  201. # with the filename +free_book.pdf+.
  202. #
  203. # If you need to send attachments with no content, you need to create an empty view for it,
  204. # or add an empty body parameter like this:
  205. #
  206. # class NotifierMailer < ApplicationMailer
  207. # def welcome(recipient)
  208. # attachments['free_book.pdf'] = File.read('path/to/file.pdf')
  209. # mail(to: recipient, subject: "New account information", body: "")
  210. # end
  211. # end
  212. #
  213. # You can also send attachments with html template, in this case you need to add body, attachments,
  214. # and custom content type like this:
  215. #
  216. # class NotifierMailer < ApplicationMailer
  217. # def welcome(recipient)
  218. # attachments["free_book.pdf"] = File.read("path/to/file.pdf")
  219. # mail(to: recipient,
  220. # subject: "New account information",
  221. # content_type: "text/html",
  222. # body: "<html><body>Hello there</body></html>")
  223. # end
  224. # end
  225. #
  226. # = Inline Attachments
  227. #
  228. # You can also specify that a file should be displayed inline with other HTML. This is useful
  229. # if you want to display a corporate logo or a photo.
  230. #
  231. # class NotifierMailer < ApplicationMailer
  232. # def welcome(recipient)
  233. # attachments.inline['photo.png'] = File.read('path/to/photo.png')
  234. # mail(to: recipient, subject: "Here is what we look like")
  235. # end
  236. # end
  237. #
  238. # And then to reference the image in the view, you create a <tt>welcome.html.erb</tt> file and
  239. # make a call to +image_tag+ passing in the attachment you want to display and then call
  240. # +url+ on the attachment to get the relative content id path for the image source:
  241. #
  242. # <h1>Please Don't Cringe</h1>
  243. #
  244. # <%= image_tag attachments['photo.png'].url -%>
  245. #
  246. # As we are using Action View's +image_tag+ method, you can pass in any other options you want:
  247. #
  248. # <h1>Please Don't Cringe</h1>
  249. #
  250. # <%= image_tag attachments['photo.png'].url, alt: 'Our Photo', class: 'photo' -%>
  251. #
  252. # = Observing and Intercepting Mails
  253. #
  254. # Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to
  255. # register classes that are called during the mail delivery life cycle.
  256. #
  257. # An observer class must implement the <tt>:delivered_email(message)</tt> method which will be
  258. # called once for every email sent after the email has been sent.
  259. #
  260. # An interceptor class must implement the <tt>:delivering_email(message)</tt> method which will be
  261. # called before the email is sent, allowing you to make modifications to the email before it hits
  262. # the delivery agents. Your class should make any needed modifications directly to the passed
  263. # in <tt>Mail::Message</tt> instance.
  264. #
  265. # = Default Hash
  266. #
  267. # Action Mailer provides some intelligent defaults for your emails, these are usually specified in a
  268. # default method inside the class definition:
  269. #
  270. # class NotifierMailer < ApplicationMailer
  271. # default sender: 'system@example.com'
  272. # end
  273. #
  274. # You can pass in any header value that a <tt>Mail::Message</tt> accepts. Out of the box,
  275. # <tt>ActionMailer::Base</tt> sets the following:
  276. #
  277. # * <tt>mime_version: "1.0"</tt>
  278. # * <tt>charset: "UTF-8"</tt>
  279. # * <tt>content_type: "text/plain"</tt>
  280. # * <tt>parts_order: [ "text/plain", "text/enriched", "text/html" ]</tt>
  281. #
  282. # <tt>parts_order</tt> and <tt>charset</tt> are not actually valid <tt>Mail::Message</tt> header fields,
  283. # but Action Mailer translates them appropriately and sets the correct values.
  284. #
  285. # As you can pass in any header, you need to either quote the header as a string, or pass it in as
  286. # an underscored symbol, so the following will work:
  287. #
  288. # class NotifierMailer < ApplicationMailer
  289. # default 'Content-Transfer-Encoding' => '7bit',
  290. # content_description: 'This is a description'
  291. # end
  292. #
  293. # Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash,
  294. # so you can define methods that evaluate as the message is being generated:
  295. #
  296. # class NotifierMailer < ApplicationMailer
  297. # default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address }
  298. #
  299. # private
  300. # def my_method
  301. # 'some complex call'
  302. # end
  303. # end
  304. #
  305. # Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you
  306. # set something in the default hash using a proc, and then set the same thing inside of your
  307. # mailer method, it will get overwritten by the mailer method.
  308. #
  309. # It is also possible to set these default options that will be used in all mailers through
  310. # the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>:
  311. #
  312. # config.action_mailer.default_options = { from: "no-reply@example.org" }
  313. #
  314. # = Callbacks
  315. #
  316. # You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages.
  317. # This may be useful, for example, when you want to add default inline attachments for all
  318. # messages sent out by a certain mailer class:
  319. #
  320. # class NotifierMailer < ApplicationMailer
  321. # before_action :add_inline_attachment!
  322. #
  323. # def welcome
  324. # mail
  325. # end
  326. #
  327. # private
  328. # def add_inline_attachment!
  329. # attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
  330. # end
  331. # end
  332. #
  333. # Callbacks in Action Mailer are implemented using
  334. # <tt>AbstractController::Callbacks</tt>, so you can define and configure
  335. # callbacks in the same manner that you would use callbacks in classes that
  336. # inherit from <tt>ActionController::Base</tt>.
  337. #
  338. # Note that unless you have a specific reason to do so, you should prefer
  339. # using <tt>before_action</tt> rather than <tt>after_action</tt> in your
  340. # Action Mailer classes so that headers are parsed properly.
  341. #
  342. # = Previewing emails
  343. #
  344. # You can preview your email templates visually by adding a mailer preview file to the
  345. # <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting
  346. # with database data, you'll need to write some scenarios to load messages with fake data:
  347. #
  348. # class NotifierMailerPreview < ActionMailer::Preview
  349. # def welcome
  350. # NotifierMailer.welcome(User.first)
  351. # end
  352. # end
  353. #
  354. # Methods must return a <tt>Mail::Message</tt> object which can be generated by calling the mailer
  355. # method without the additional <tt>deliver_now</tt> / <tt>deliver_later</tt>. The location of the
  356. # mailer previews directory can be configured using the <tt>preview_path</tt> option which has a default
  357. # of <tt>test/mailers/previews</tt>:
  358. #
  359. # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
  360. #
  361. # An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt>
  362. # on a running development server instance.
  363. #
  364. # Previews can also be intercepted in a similar manner as deliveries can be by registering
  365. # a preview interceptor that has a <tt>previewing_email</tt> method:
  366. #
  367. # class CssInlineStyler
  368. # def self.previewing_email(message)
  369. # # inline CSS styles
  370. # end
  371. # end
  372. #
  373. # config.action_mailer.preview_interceptors :css_inline_styler
  374. #
  375. # Note that interceptors need to be registered both with <tt>register_interceptor</tt>
  376. # and <tt>register_preview_interceptor</tt> if they should operate on both sending and
  377. # previewing emails.
  378. #
  379. # = Configuration options
  380. #
  381. # These options are specified on the class level, like
  382. # <tt>ActionMailer::Base.raise_delivery_errors = true</tt>
  383. #
  384. # * <tt>default_options</tt> - You can pass this in at a class level as well as within the class itself as
  385. # per the above section.
  386. #
  387. # * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
  388. # Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
  389. #
  390. # * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method:
  391. # * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default
  392. # "localhost" setting.
  393. # * <tt>:port</tt> - On the off chance that your mail server doesn't run on port 25, you can change it.
  394. # * <tt>:domain</tt> - If you need to specify a HELO domain, you can do it here.
  395. # * <tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting.
  396. # * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting.
  397. # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the
  398. # authentication type here.
  399. # This is a symbol and one of <tt>:plain</tt> (will send the password Base64 encoded), <tt>:login</tt> (will
  400. # send the password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange
  401. # information and a cryptographic Message Digest 5 algorithm to hash important information)
  402. # * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts
  403. # to use it. Defaults to <tt>true</tt>.
  404. # * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is
  405. # really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
  406. # of an OpenSSL verify constant (<tt>'none'</tt> or <tt>'peer'</tt>) or directly the constant
  407. # (<tt>OpenSSL::SSL::VERIFY_NONE</tt> or <tt>OpenSSL::SSL::VERIFY_PEER</tt>).
  408. # * <tt>:ssl/:tls</tt> Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection)
  409. #
  410. # * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
  411. # * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
  412. # * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i</tt> with <tt>-f sender@address</tt>
  413. # added automatically before the message is sent.
  414. #
  415. # * <tt>file_settings</tt> - Allows you to override options for the <tt>:file</tt> delivery method.
  416. # * <tt>:location</tt> - The directory into which emails will be written. Defaults to the application
  417. # <tt>tmp/mails</tt>.
  418. #
  419. # * <tt>raise_delivery_errors</tt> - Whether or not errors should be raised if the email fails to be delivered.
  420. #
  421. # * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default),
  422. # <tt>:sendmail</tt>, <tt>:test</tt>, and <tt>:file</tt>. Or you may provide a custom delivery method
  423. # object e.g. +MyOwnDeliveryMethodClass+. See the Mail gem documentation on the interface you need to
  424. # implement for a custom delivery agent.
  425. #
  426. # * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you
  427. # call <tt>.deliver</tt> on an email message or on an Action Mailer method. This is on by default but can
  428. # be turned off to aid in functional testing.
  429. #
  430. # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
  431. # <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
  432. #
  433. # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. Defaults to +mailers+.
  434. 1 class Base < AbstractController::Base
  435. 1 include DeliveryMethods
  436. 1 include Rescuable
  437. 1 include Parameterized
  438. 1 include Previews
  439. 1 abstract!
  440. 1 include AbstractController::Rendering
  441. 1 include AbstractController::Logger
  442. 1 include AbstractController::Helpers
  443. 1 include AbstractController::Translation
  444. 1 include AbstractController::AssetPaths
  445. 1 include AbstractController::Callbacks
  446. 1 include AbstractController::Caching
  447. 1 include ActionView::Layouts
  448. 1 PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
  449. 1 helper ActionMailer::MailHelper
  450. 1 class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob
  451. 1 class_attribute :default_params, default: {
  452. mime_version: "1.0",
  453. charset: "UTF-8",
  454. content_type: "text/plain",
  455. parts_order: [ "text/plain", "text/enriched", "text/html" ]
  456. }.freeze
  457. 1 class << self
  458. # Register one or more Observers which will be notified when mail is delivered.
  459. 1 def register_observers(*observers)
  460. 3 observers.flatten.compact.each { |observer| register_observer(observer) }
  461. end
  462. # Unregister one or more previously registered Observers.
  463. 1 def unregister_observers(*observers)
  464. 3 observers.flatten.compact.each { |observer| unregister_observer(observer) }
  465. end
  466. # Register one or more Interceptors which will be called before mail is sent.
  467. 1 def register_interceptors(*interceptors)
  468. 3 interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
  469. end
  470. # Unregister one or more previously registered Interceptors.
  471. 1 def unregister_interceptors(*interceptors)
  472. 3 interceptors.flatten.compact.each { |interceptor| unregister_interceptor(interceptor) }
  473. end
  474. # Register an Observer which will be notified when mail is delivered.
  475. # Either a class, string or symbol can be passed in as the Observer.
  476. # If a string or symbol is passed in it will be camelized and constantized.
  477. 1 def register_observer(observer)
  478. 5 Mail.register_observer(observer_class_for(observer))
  479. end
  480. # Unregister a previously registered Observer.
  481. # Either a class, string or symbol can be passed in as the Observer.
  482. # If a string or symbol is passed in it will be camelized and constantized.
  483. 1 def unregister_observer(observer)
  484. 5 Mail.unregister_observer(observer_class_for(observer))
  485. end
  486. # Register an Interceptor which will be called before mail is sent.
  487. # Either a class, string or symbol can be passed in as the Interceptor.
  488. # If a string or symbol is passed in it will be camelized and constantized.
  489. 1 def register_interceptor(interceptor)
  490. 5 Mail.register_interceptor(observer_class_for(interceptor))
  491. end
  492. # Unregister a previously registered Interceptor.
  493. # Either a class, string or symbol can be passed in as the Interceptor.
  494. # If a string or symbol is passed in it will be camelized and constantized.
  495. 1 def unregister_interceptor(interceptor)
  496. 5 Mail.unregister_interceptor(observer_class_for(interceptor))
  497. end
  498. 1 def observer_class_for(value) # :nodoc:
  499. 20 case value
  500. when String, Symbol
  501. 12 value.to_s.camelize.constantize
  502. else
  503. 8 value
  504. end
  505. end
  506. 1 private :observer_class_for
  507. # Returns the name of the current mailer. This method is also being used as a path for a view lookup.
  508. # If this is an anonymous mailer, this method will return +anonymous+ instead.
  509. 1 def mailer_name
  510. 555 @mailer_name ||= anonymous? ? "anonymous" : name.underscore
  511. end
  512. # Allows to set the name of current mailer.
  513. 1 attr_writer :mailer_name
  514. 1 alias :controller_path :mailer_name
  515. # Sets the defaults through app configuration:
  516. #
  517. # config.action_mailer.default(from: "no-reply@example.org")
  518. #
  519. # Aliased by ::default_options=
  520. 1 def default(value = nil)
  521. 206 self.default_params = default_params.merge(value).freeze if value
  522. 206 default_params
  523. end
  524. # Allows to set defaults through app configuration:
  525. #
  526. # config.action_mailer.default_options = { from: "no-reply@example.org" }
  527. 1 alias :default_options= :default
  528. # Wraps an email delivery inside of <tt>ActiveSupport::Notifications</tt> instrumentation.
  529. #
  530. # This method is actually called by the <tt>Mail::Message</tt> object itself
  531. # through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>,
  532. # calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
  533. # nothing except tell the logger you sent the email.
  534. 1 def deliver_mail(mail) #:nodoc:
  535. 74 ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
  536. 74 set_payload_for_mail(payload, mail)
  537. 74 yield # Let Mail do the delivery actions
  538. end
  539. end
  540. # Returns an email in the format "Name <email@example.com>".
  541. 1 def email_address_with_name(address, name)
  542. Mail::Address.new.tap do |builder|
  543. 3 builder.address = address
  544. 3 builder.display_name = name
  545. 3 end.to_s
  546. end
  547. 1 private
  548. 1 def set_payload_for_mail(payload, mail)
  549. 74 payload[:mail] = mail.encoded
  550. 74 payload[:mailer] = name
  551. 74 payload[:message_id] = mail.message_id
  552. 74 payload[:subject] = mail.subject
  553. 74 payload[:to] = mail.to
  554. 74 payload[:from] = mail.from
  555. 74 payload[:bcc] = mail.bcc if mail.bcc.present?
  556. 74 payload[:cc] = mail.cc if mail.cc.present?
  557. 74 payload[:date] = mail.date
  558. 74 payload[:perform_deliveries] = mail.perform_deliveries
  559. end
  560. 1 def method_missing(method_name, *args)
  561. 224 if action_methods.include?(method_name.to_s)
  562. 223 MessageDelivery.new(self, method_name, *args)
  563. else
  564. 1 super
  565. end
  566. end
  567. 1 ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
  568. 1 def respond_to_missing?(method, include_all = false)
  569. 12 action_methods.include?(method.to_s) || super
  570. end
  571. end
  572. 1 attr_internal :message
  573. 1 def initialize
  574. 216 super()
  575. 216 @_mail_was_called = false
  576. 216 @_message = Mail.new
  577. end
  578. 1 def process(method_name, *args) #:nodoc:
  579. 188 payload = {
  580. mailer: self.class.name,
  581. action: method_name,
  582. args: args
  583. }
  584. 188 ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
  585. 188 super
  586. 181 @_message = NullMail.new unless @_mail_was_called
  587. end
  588. end
  589. 1 class NullMail #:nodoc:
  590. 2 def body; "" end
  591. 1 def header; {} end
  592. 1 def respond_to?(string, include_all = false)
  593. 1 true
  594. end
  595. 1 def method_missing(*args)
  596. nil
  597. end
  598. end
  599. # Returns the name of the mailer object.
  600. 1 def mailer_name
  601. 25 self.class.mailer_name
  602. end
  603. # Returns an email in the format "Name <email@example.com>".
  604. 1 def email_address_with_name(address, name)
  605. 1 self.class.email_address_with_name(address, name)
  606. end
  607. # Allows you to pass random and unusual headers to the new <tt>Mail::Message</tt>
  608. # object which will add them to itself.
  609. #
  610. # headers['X-Special-Domain-Specific-Header'] = "SecretValue"
  611. #
  612. # You can also pass a hash into headers of header field names and values,
  613. # which will then be set on the <tt>Mail::Message</tt> object:
  614. #
  615. # headers 'X-Special-Domain-Specific-Header' => "SecretValue",
  616. # 'In-Reply-To' => incoming.message_id
  617. #
  618. # The resulting <tt>Mail::Message</tt> will have the following in its header:
  619. #
  620. # X-Special-Domain-Specific-Header: SecretValue
  621. #
  622. # Note about replacing already defined headers:
  623. #
  624. # * +subject+
  625. # * +sender+
  626. # * +from+
  627. # * +to+
  628. # * +cc+
  629. # * +bcc+
  630. # * +reply-to+
  631. # * +orig-date+
  632. # * +message-id+
  633. # * +references+
  634. #
  635. # Fields can only appear once in email headers while other fields such as
  636. # <tt>X-Anything</tt> can appear multiple times.
  637. #
  638. # If you want to replace any header which already exists, first set it to
  639. # +nil+ in order to reset the value otherwise another field will be added
  640. # for the same header.
  641. 1 def headers(args = nil)
  642. 43 if args
  643. 4 @_message.headers(args)
  644. else
  645. 39 @_message
  646. end
  647. end
  648. # Allows you to add attachments to an email, like so:
  649. #
  650. # mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
  651. #
  652. # If you do this, then Mail will take the file name and work out the mime type.
  653. # It will also set the Content-Type, Content-Disposition, Content-Transfer-Encoding
  654. # and encode the contents of the attachment in Base64.
  655. #
  656. # You can also specify overrides if you want by passing a hash instead of a string:
  657. #
  658. # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
  659. # content: File.read('/path/to/filename.jpg')}
  660. #
  661. # If you want to use encoding other than Base64 then you will need to pass encoding
  662. # type along with the pre-encoded content as Mail doesn't know how to decode the
  663. # data:
  664. #
  665. # file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
  666. # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
  667. # encoding: 'SpecialEncoding',
  668. # content: file_content }
  669. #
  670. # You can also search for specific attachments:
  671. #
  672. # # By Filename
  673. # mail.attachments['filename.jpg'] # => Mail::Part object or nil
  674. #
  675. # # or by index
  676. # mail.attachments[0] # => Mail::Part (first attachment)
  677. #
  678. 1 def attachments
  679. 26 if @_mail_was_called
  680. 4 LateAttachmentsProxy.new(@_message.attachments)
  681. else
  682. 22 @_message.attachments
  683. end
  684. end
  685. 1 class LateAttachmentsProxy < SimpleDelegator
  686. 3 def inline; self end
  687. 3 def []=(_name, _content); _raise_error end
  688. 1 private
  689. 1 def _raise_error
  690. 2 raise RuntimeError, "Can't add attachments after `mail` was called.\n" \
  691. "Make sure to use `attachments[]=` before calling `mail`."
  692. end
  693. end
  694. # The main method that creates the message and renders the email templates. There are
  695. # two ways to call this method, with a block, or without a block.
  696. #
  697. # It accepts a headers hash. This hash allows you to specify
  698. # the most used headers in an email message, these are:
  699. #
  700. # * +:subject+ - The subject of the message, if this is omitted, Action Mailer will
  701. # ask the Rails I18n class for a translated +:subject+ in the scope of
  702. # <tt>[mailer_scope, action_name]</tt> or if this is missing, will translate the
  703. # humanized version of the +action_name+
  704. # * +:to+ - Who the message is destined for, can be a string of addresses, or an array
  705. # of addresses.
  706. # * +:from+ - Who the message is from
  707. # * +:cc+ - Who you would like to Carbon-Copy on this email, can be a string of addresses,
  708. # or an array of addresses.
  709. # * +:bcc+ - Who you would like to Blind-Carbon-Copy on this email, can be a string of
  710. # addresses, or an array of addresses.
  711. # * +:reply_to+ - Who to set the Reply-To header of the email to.
  712. # * +:date+ - The date to say the email was sent on.
  713. #
  714. # You can set default values for any of the above headers (except +:date+)
  715. # by using the ::default class method:
  716. #
  717. # class Notifier < ActionMailer::Base
  718. # default from: 'no-reply@test.lindsaar.net',
  719. # bcc: 'email_logger@test.lindsaar.net',
  720. # reply_to: 'bounces@test.lindsaar.net'
  721. # end
  722. #
  723. # If you need other headers not listed above, you can either pass them in
  724. # as part of the headers hash or use the <tt>headers['name'] = value</tt>
  725. # method.
  726. #
  727. # When a +:return_path+ is specified as header, that value will be used as
  728. # the 'envelope from' address for the Mail message. Setting this is useful
  729. # when you want delivery notifications sent to a different address than the
  730. # one in +:from+. Mail will actually use the +:return_path+ in preference
  731. # to the +:sender+ in preference to the +:from+ field for the 'envelope
  732. # from' value.
  733. #
  734. # If you do not pass a block to the +mail+ method, it will find all
  735. # templates in the view paths using by default the mailer name and the
  736. # method name that it is being called from, it will then create parts for
  737. # each of these templates intelligently, making educated guesses on correct
  738. # content type and sequence, and return a fully prepared <tt>Mail::Message</tt>
  739. # ready to call <tt>:deliver</tt> on to send.
  740. #
  741. # For example:
  742. #
  743. # class Notifier < ActionMailer::Base
  744. # default from: 'no-reply@test.lindsaar.net'
  745. #
  746. # def welcome
  747. # mail(to: 'mikel@test.lindsaar.net')
  748. # end
  749. # end
  750. #
  751. # Will look for all templates at "app/views/notifier" with name "welcome".
  752. # If no welcome template exists, it will raise an ActionView::MissingTemplate error.
  753. #
  754. # However, those can be customized:
  755. #
  756. # mail(template_path: 'notifications', template_name: 'another')
  757. #
  758. # And now it will look for all templates at "app/views/notifications" with name "another".
  759. #
  760. # If you do pass a block, you can render specific templates of your choice:
  761. #
  762. # mail(to: 'mikel@test.lindsaar.net') do |format|
  763. # format.text
  764. # format.html
  765. # end
  766. #
  767. # You can even render plain text directly without using a template:
  768. #
  769. # mail(to: 'mikel@test.lindsaar.net') do |format|
  770. # format.text { render plain: "Hello Mikel!" }
  771. # format.html { render html: "<h1>Hello Mikel!</h1>".html_safe }
  772. # end
  773. #
  774. # Which will render a +multipart/alternative+ email with +text/plain+ and
  775. # +text/html+ parts.
  776. #
  777. # The block syntax also allows you to customize the part headers if desired:
  778. #
  779. # mail(to: 'mikel@test.lindsaar.net') do |format|
  780. # format.text(content_transfer_encoding: "base64")
  781. # format.html
  782. # end
  783. #
  784. 1 def mail(headers = {}, &block)
  785. 193 return message if @_mail_was_called && headers.blank? && !block
  786. # At the beginning, do not consider class default for content_type
  787. 191 content_type = headers[:content_type]
  788. 191 headers = apply_defaults(headers)
  789. # Apply charset at the beginning so all fields are properly quoted
  790. 191 message.charset = charset = headers[:charset]
  791. # Set configure delivery behavior
  792. 191 wrap_delivery_behavior!(headers[:delivery_method], headers[:delivery_method_options])
  793. 189 assign_headers_to_message(message, headers)
  794. # Render the templates and blocks
  795. 189 responses = collect_responses(headers, &block)
  796. 187 @_mail_was_called = true
  797. 187 create_parts_from_responses(message, responses)
  798. 187 wrap_inline_attachments(message)
  799. # Set up content type, reapply charset and handle parts order
  800. 187 message.content_type = set_content_type(message, content_type, headers[:content_type])
  801. 187 message.charset = charset
  802. 187 if message.multipart?
  803. 39 message.body.set_sort_order(headers[:parts_order])
  804. 39 message.body.sort_parts!
  805. end
  806. 187 message
  807. end
  808. 1 private
  809. # Used by #mail to set the content type of the message.
  810. #
  811. # It will use the given +user_content_type+, or multipart if the mail
  812. # message has any attachments. If the attachments are inline, the content
  813. # type will be "multipart/related", otherwise "multipart/mixed".
  814. #
  815. # If there is no content type passed in via headers, and there are no
  816. # attachments, or the message is multipart, then the default content type is
  817. # used.
  818. 1 def set_content_type(m, user_content_type, class_default) # :doc:
  819. 187 params = m.content_type_parameters || {}
  820. case
  821. when user_content_type.present?
  822. 5 user_content_type
  823. when m.has_attachments?
  824. 15 if m.attachments.all?(&:inline?)
  825. 3 ["multipart", "related", params]
  826. else
  827. 12 ["multipart", "mixed", params]
  828. end
  829. when m.multipart?
  830. 22 ["multipart", "alternative", params]
  831. else
  832. 145 m.content_type || class_default
  833. 187 end
  834. end
  835. # Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope.
  836. # If it does not find a translation for the +subject+ under the specified scope it will default to a
  837. # humanized version of the <tt>action_name</tt>.
  838. # If the subject has interpolations, you can pass them through the +interpolations+ parameter.
  839. 1 def default_i18n_subject(interpolations = {}) # :doc:
  840. 93 mailer_scope = self.class.mailer_name.tr("/", ".")
  841. 93 I18n.t(:subject, **interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize))
  842. end
  843. # Emails do not support relative path links.
  844. 1 def self.supports_path? # :doc:
  845. 22 false
  846. end
  847. 1 def apply_defaults(headers)
  848. 191 default_values = self.class.default.transform_values do |value|
  849. 1099 compute_default(value)
  850. end
  851. 191 headers_with_defaults = headers.reverse_merge(default_values)
  852. 191 headers_with_defaults[:subject] ||= default_i18n_subject
  853. 191 headers_with_defaults
  854. end
  855. 1 def compute_default(value)
  856. 1099 return value unless value.is_a?(Proc)
  857. 43 if value.arity == 1
  858. 14 instance_exec(self, &value)
  859. else
  860. 29 instance_exec(&value)
  861. end
  862. end
  863. 1 def assign_headers_to_message(message, headers)
  864. 189 assignable = headers.except(:parts_order, :content_type, :body, :template_name,
  865. :template_path, :delivery_method, :delivery_method_options)
  866. 1231 assignable.each { |k, v| message[k] = v }
  867. end
  868. 1 def collect_responses(headers, &block)
  869. 189 if block_given?
  870. 28 collect_responses_from_block(headers, &block)
  871. 161 elsif headers[:body]
  872. 49 collect_responses_from_text(headers)
  873. else
  874. 112 collect_responses_from_templates(headers)
  875. end
  876. end
  877. 1 def collect_responses_from_block(headers)
  878. 28 templates_name = headers[:template_name] || action_name
  879. 34 collector = ActionMailer::Collector.new(lookup_context) { render(templates_name) }
  880. 28 yield(collector)
  881. 27 collector.responses
  882. end
  883. 1 def collect_responses_from_text(headers)
  884. 49 [{
  885. body: headers.delete(:body),
  886. content_type: headers[:content_type] || "text/plain"
  887. }]
  888. end
  889. 1 def collect_responses_from_templates(headers)
  890. 112 templates_path = headers[:template_path] || self.class.mailer_name
  891. 112 templates_name = headers[:template_name] || action_name
  892. 112 each_template(Array(templates_path), templates_name).map do |template|
  893. 124 format = template.format || self.formats.first
  894. 124 {
  895. body: render(template: template, formats: [format]),
  896. content_type: Mime[format].to_s
  897. }
  898. end
  899. end
  900. 1 def each_template(paths, name, &block)
  901. 112 templates = lookup_context.find_all(name, paths)
  902. 112 if templates.empty?
  903. 1 raise ActionView::MissingTemplate.new(paths, name, paths, false, "mailer")
  904. else
  905. 111 templates.uniq(&:format).each(&block)
  906. end
  907. end
  908. 1 def wrap_inline_attachments(message)
  909. # If we have both types of attachment, wrap all the inline attachments
  910. # in multipart/related, but not the actual attachments
  911. 193 if message.attachments.detect(&:inline?) && message.attachments.detect { |a| !a.inline? }
  912. 1 related = Mail::Part.new
  913. 1 related.content_type = "multipart/related"
  914. 1 mixed = [ related ]
  915. 1 message.parts.each do |p|
  916. 3 if p.attachment? && !p.inline?
  917. 1 mixed << p
  918. else
  919. 2 related.add_part(p)
  920. end
  921. end
  922. 1 message.parts.clear
  923. 3 mixed.each { |c| message.add_part(c) }
  924. end
  925. end
  926. 1 def create_parts_from_responses(m, responses)
  927. 187 if responses.size == 1 && !m.has_attachments?
  928. 445 responses[0].each { |k, v| m[k] = v }
  929. 39 elsif responses.size > 1 && m.has_attachments?
  930. 5 container = Mail::Part.new
  931. 5 container.content_type = "multipart/alternative"
  932. 15 responses.each { |r| insert_part(container, r, m.charset) }
  933. 5 m.add_part(container)
  934. else
  935. 91 responses.each { |r| insert_part(m, r, m.charset) }
  936. end
  937. end
  938. 1 def insert_part(container, response, charset)
  939. 67 response[:charset] ||= charset
  940. 67 part = Mail::Part.new(response)
  941. 67 container.add_part(part)
  942. end
  943. # This and #instrument_name is for caching instrument
  944. 1 def instrument_payload(key)
  945. 23 {
  946. mailer: mailer_name,
  947. key: key
  948. }
  949. end
  950. 1 def instrument_name
  951. 23 "action_mailer"
  952. end
  953. 1 def _protected_ivars
  954. 189 PROTECTED_IVARS
  955. end
  956. 1 ActiveSupport.run_load_hooks(:action_mailer, self)
  957. end
  958. end

lib/action_mailer/collector.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "abstract_controller/collector"
  3. 1 require "active_support/core_ext/hash/reverse_merge"
  4. 1 require "active_support/core_ext/array/extract_options"
  5. 1 module ActionMailer
  6. 1 class Collector
  7. 1 include AbstractController::Collector
  8. 1 attr_reader :responses
  9. 1 def initialize(context, &block)
  10. 28 @context = context
  11. 28 @responses = []
  12. 28 @default_render = block
  13. end
  14. 1 def any(*args, &block)
  15. 2 options = args.extract_options!
  16. 2 raise ArgumentError, "You have to supply at least one format" if args.empty?
  17. 3 args.each { |type| send(type, options.dup, &block) }
  18. end
  19. 1 alias :all :any
  20. 1 def custom(mime, options = {})
  21. 42 options.reverse_merge!(content_type: mime.to_s)
  22. 42 @context.formats = [mime.to_sym]
  23. 42 options[:body] = block_given? ? yield : @default_render.call
  24. 42 @responses << options
  25. end
  26. end
  27. end

lib/action_mailer/delivery_job.rb

72.22% lines covered

18 relevant lines. 13 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_job"
  3. 1 module ActionMailer
  4. # The <tt>ActionMailer::DeliveryJob</tt> class is used when you
  5. # want to send emails outside of the request-response cycle.
  6. #
  7. # Exceptions are rescued and handled by the mailer class.
  8. 1 class DeliveryJob < ActiveJob::Base # :nodoc:
  9. 5 queue_as { ActionMailer::Base.deliver_later_queue_name }
  10. 1 rescue_from StandardError, with: :handle_exception_with_mailer_class
  11. 1 before_perform do
  12. 2 ActiveSupport::Deprecation.warn <<~MSG.squish
  13. Sending mail with DeliveryJob and Parameterized::DeliveryJob
  14. is deprecated and will be removed in Rails 6.1.
  15. Please use MailDeliveryJob instead.
  16. MSG
  17. end
  18. 1 def perform(mailer, mail_method, delivery_method, *args) #:nodoc:
  19. 1 mailer.constantize.public_send(mail_method, *args).send(delivery_method)
  20. end
  21. 1 ruby2_keywords(:perform) if respond_to?(:ruby2_keywords, true)
  22. 1 private
  23. # "Deserialize" the mailer class name by hand in case another argument
  24. # (like a Global ID reference) raised DeserializationError.
  25. 1 def mailer_class
  26. if mailer = Array(@serialized_arguments).first || Array(arguments).first
  27. mailer.constantize
  28. end
  29. end
  30. 1 def handle_exception_with_mailer_class(exception)
  31. if klass = mailer_class
  32. klass.handle_exception exception
  33. else
  34. raise exception
  35. end
  36. end
  37. end
  38. end

lib/action_mailer/delivery_methods.rb

100.0% lines covered

34 relevant lines. 34 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tmpdir"
  3. 1 module ActionMailer
  4. # This module handles everything related to mail delivery, from registering
  5. # new delivery methods to configuring the mail object to be sent.
  6. 1 module DeliveryMethods
  7. 1 extend ActiveSupport::Concern
  8. 1 included do
  9. # Do not make this inheritable, because we always want it to propagate
  10. 1 cattr_accessor :raise_delivery_errors, default: true
  11. 1 cattr_accessor :perform_deliveries, default: true
  12. 1 cattr_accessor :deliver_later_queue_name, default: :mailers
  13. 1 class_attribute :delivery_methods, default: {}.freeze
  14. 1 class_attribute :delivery_method, default: :smtp
  15. 1 add_delivery_method :smtp, Mail::SMTP,
  16. address: "localhost",
  17. port: 25,
  18. domain: "localhost.localdomain",
  19. user_name: nil,
  20. password: nil,
  21. authentication: nil,
  22. enable_starttls_auto: true
  23. 1 add_delivery_method :file, Mail::FileDelivery,
  24. 1 location: defined?(Rails.root) ? "#{Rails.root}/tmp/mails" : "#{Dir.tmpdir}/mails"
  25. 1 add_delivery_method :sendmail, Mail::Sendmail,
  26. location: "/usr/sbin/sendmail",
  27. arguments: "-i"
  28. 1 add_delivery_method :test, Mail::TestMailer
  29. end
  30. # Helpers for creating and wrapping delivery behavior, used by DeliveryMethods.
  31. 1 module ClassMethods
  32. # Provides a list of emails that have been delivered by Mail::TestMailer
  33. 1 delegate :deliveries, :deliveries=, to: Mail::TestMailer
  34. # Adds a new delivery method through the given class using the given
  35. # symbol as alias and the default options supplied.
  36. #
  37. # add_delivery_method :sendmail, Mail::Sendmail,
  38. # location: '/usr/sbin/sendmail',
  39. # arguments: '-i'
  40. 1 def add_delivery_method(symbol, klass, default_options = {})
  41. 10 class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
  42. 10 send(:"#{symbol}_settings=", default_options)
  43. 10 self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze
  44. end
  45. 1 def wrap_delivery_behavior(mail, method = nil, options = nil) # :nodoc:
  46. 191 method ||= delivery_method
  47. 191 mail.delivery_handler = self
  48. 191 case method
  49. when NilClass
  50. 1 raise "Delivery method cannot be nil"
  51. when Symbol
  52. 186 if klass = delivery_methods[method]
  53. 185 mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))
  54. else
  55. 1 raise "Invalid delivery method #{method.inspect}"
  56. end
  57. else
  58. 4 mail.delivery_method(method)
  59. end
  60. 189 mail.perform_deliveries = perform_deliveries
  61. 189 mail.raise_delivery_errors = raise_delivery_errors
  62. end
  63. end
  64. 1 def wrap_delivery_behavior!(*args) # :nodoc:
  65. 191 self.class.wrap_delivery_behavior(message, *args)
  66. end
  67. end
  68. end

lib/action_mailer/gem_version.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionMailer
  3. # Returns the version of the currently loaded Action Mailer as a <tt>Gem::Version</tt>.
  4. 1 def self.gem_version
  5. Gem::Version.new VERSION::STRING
  6. end
  7. 1 module VERSION
  8. 1 MAJOR = 6
  9. 1 MINOR = 1
  10. 1 TINY = 0
  11. 1 PRE = "alpha"
  12. 1 STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
  13. end
  14. end

lib/action_mailer/inline_preview_interceptor.rb

70.83% lines covered

24 relevant lines. 17 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "base64"
  3. 1 module ActionMailer
  4. # Implements a mailer preview interceptor that converts image tag src attributes
  5. # that use inline cid: style URLs to data: style URLs so that they are visible
  6. # when previewing an HTML email in a web browser.
  7. #
  8. # This interceptor is enabled by default. To disable it, delete it from the
  9. # <tt>ActionMailer::Base.preview_interceptors</tt> array:
  10. #
  11. # ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor)
  12. #
  13. 1 class InlinePreviewInterceptor
  14. 1 PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i
  15. 1 include Base64
  16. 1 def self.previewing_email(message) #:nodoc:
  17. 3 new(message).transform!
  18. end
  19. 1 def initialize(message) #:nodoc:
  20. 3 @message = message
  21. end
  22. 1 def transform! #:nodoc:
  23. 3 return message if html_part.blank?
  24. html_part.body = html_part.decoded.gsub(PATTERN) do |match|
  25. if part = find_part(match[9..-2])
  26. %[src="#{data_url(part)}"]
  27. else
  28. match
  29. end
  30. end
  31. message
  32. end
  33. 1 private
  34. 1 attr_reader :message
  35. 1 def html_part
  36. 3 @html_part ||= message.html_part
  37. end
  38. 1 def data_url(part)
  39. "data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}"
  40. end
  41. 1 def find_part(cid)
  42. message.all_parts.find { |p| p.attachment? && p.cid == cid }
  43. end
  44. end
  45. end

lib/action_mailer/log_subscriber.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/log_subscriber"
  3. 1 module ActionMailer
  4. # Implements the ActiveSupport::LogSubscriber for logging notifications when
  5. # email is delivered or received.
  6. 1 class LogSubscriber < ActiveSupport::LogSubscriber
  7. # An email was delivered.
  8. 1 def deliver(event)
  9. 2 info do
  10. 2 perform_deliveries = event.payload[:perform_deliveries]
  11. 2 if perform_deliveries
  12. 1 "Delivered mail #{event.payload[:message_id]} (#{event.duration.round(1)}ms)"
  13. else
  14. 1 "Skipped delivery of mail #{event.payload[:message_id]} as `perform_deliveries` is false"
  15. end
  16. end
  17. 4 debug { event.payload[:mail] }
  18. end
  19. # An email was generated.
  20. 1 def process(event)
  21. 2 debug do
  22. 2 mailer = event.payload[:mailer]
  23. 2 action = event.payload[:action]
  24. 2 "#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
  25. end
  26. end
  27. # Use the logger configured for ActionMailer::Base.
  28. 1 def logger
  29. 128 ActionMailer::Base.logger
  30. end
  31. end
  32. end
  33. 1 ActionMailer::LogSubscriber.attach_to :action_mailer

lib/action_mailer/mail_delivery_job.rb

89.47% lines covered

19 relevant lines. 17 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_job"
  3. 1 module ActionMailer
  4. # The <tt>ActionMailer::MailDeliveryJob</tt> class is used when you
  5. # want to send emails outside of the request-response cycle. It supports
  6. # sending either parameterized or normal mail.
  7. #
  8. # Exceptions are rescued and handled by the mailer class.
  9. 1 class MailDeliveryJob < ActiveJob::Base # :nodoc:
  10. 33 queue_as { ActionMailer::Base.deliver_later_queue_name }
  11. 1 rescue_from StandardError, with: :handle_exception_with_mailer_class
  12. 1 def perform(mailer, mail_method, delivery_method, args:, kwargs: nil, params: nil)
  13. 18 mailer_class = params ? mailer.constantize.with(params) : mailer.constantize
  14. 18 message = if kwargs
  15. mailer_class.public_send(mail_method, *args, **kwargs)
  16. else
  17. 18 mailer_class.public_send(mail_method, *args)
  18. end
  19. 18 message.send(delivery_method)
  20. end
  21. 1 private
  22. # "Deserialize" the mailer class name by hand in case another argument
  23. # (like a Global ID reference) raised DeserializationError.
  24. 1 def mailer_class
  25. 2 if mailer = Array(@serialized_arguments).first || Array(arguments).first
  26. 2 mailer.constantize
  27. end
  28. end
  29. 1 def handle_exception_with_mailer_class(exception)
  30. 2 if klass = mailer_class
  31. 2 klass.handle_exception exception
  32. else
  33. raise exception
  34. end
  35. end
  36. end
  37. end

lib/action_mailer/mail_helper.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionMailer
  3. # Provides helper methods for ActionMailer::Base that can be used for easily
  4. # formatting messages, accessing mailer or message instances, and the
  5. # attachments list.
  6. 1 module MailHelper
  7. # Take the text and format it, indented two spaces for each line, and
  8. # wrapped at 72 columns:
  9. #
  10. # text = <<-TEXT
  11. # This is
  12. # the paragraph.
  13. #
  14. # * item1 * item2
  15. # TEXT
  16. #
  17. # block_format text
  18. # # => " This is the paragraph.\n\n * item1\n * item2\n"
  19. 1 def block_format(text)
  20. 2 formatted = text.split(/\n\r?\n/).collect { |paragraph|
  21. 4 format_paragraph(paragraph)
  22. }.join("\n\n")
  23. # Make list points stand on their own line
  24. 5 formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" }
  25. 2 formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" }
  26. 2 formatted
  27. end
  28. # Access the mailer instance.
  29. 1 def mailer
  30. 6 @_controller
  31. end
  32. # Access the message instance.
  33. 1 def message
  34. 1 @_message
  35. end
  36. # Access the message attachments list.
  37. 1 def attachments
  38. 5 mailer.attachments
  39. end
  40. # Returns +text+ wrapped at +len+ columns and indented +indent+ spaces.
  41. # By default column length +len+ equals 72 characters and indent
  42. # +indent+ equal two spaces.
  43. #
  44. # my_text = 'Here is a sample text with more than 40 characters'
  45. #
  46. # format_paragraph(my_text, 25, 4)
  47. # # => " Here is a sample text with\n more than 40 characters"
  48. 1 def format_paragraph(text, len = 72, indent = 2)
  49. 6 sentences = [[]]
  50. 6 text.split.each do |word|
  51. 94 if sentences.first.present? && (sentences.last + [word]).join(" ").length > len
  52. 9 sentences << [word]
  53. else
  54. 85 sentences.last << word
  55. end
  56. end
  57. 6 indentation = " " * indent
  58. 6 sentences.map! { |sentence|
  59. 15 "#{indentation}#{sentence.join(' ')}"
  60. }.join "\n"
  61. end
  62. end
  63. end

lib/action_mailer/message_delivery.rb

100.0% lines covered

37 relevant lines. 37 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "delegate"
  3. 1 module ActionMailer
  4. # The <tt>ActionMailer::MessageDelivery</tt> class is used by
  5. # ActionMailer::Base when creating a new mailer.
  6. # <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy
  7. # created <tt>Mail::Message</tt>. You can get direct access to the
  8. # <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent
  9. # through Active Job.
  10. #
  11. # Notifier.welcome(User.first) # an ActionMailer::MessageDelivery object
  12. # Notifier.welcome(User.first).deliver_now # sends the email
  13. # Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
  14. # Notifier.welcome(User.first).message # a Mail::Message object
  15. 1 class MessageDelivery < Delegator
  16. 1 def initialize(mailer_class, action, *args) #:nodoc:
  17. 238 @mailer_class, @action, @args = mailer_class, action, args
  18. # The mail is only processed if we try to call any methods on it.
  19. # Typical usage will leave it unloaded and call deliver_later.
  20. 238 @processed_mailer = nil
  21. 238 @mail_message = nil
  22. end
  23. 1 ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
  24. # Method calls are delegated to the Mail::Message that's ready to deliver.
  25. 1 def __getobj__ #:nodoc:
  26. 406 @mail_message ||= processed_mailer.message
  27. end
  28. # Unused except for delegator internals (dup, marshalling).
  29. 1 def __setobj__(mail_message) #:nodoc:
  30. 10 @mail_message = mail_message
  31. end
  32. # Returns the resulting Mail::Message
  33. 1 def message
  34. 87 __getobj__
  35. end
  36. # Was the delegate loaded, causing the mailer action to be processed?
  37. 1 def processed?
  38. 38 @processed_mailer || @mail_message
  39. end
  40. # Enqueues the email to be delivered through Active Job. When the
  41. # job runs it will send the email using +deliver_now!+. That means
  42. # that the message will be sent bypassing checking +perform_deliveries+
  43. # and +raise_delivery_errors+, so use with caution.
  44. #
  45. # Notifier.welcome(User.first).deliver_later!
  46. # Notifier.welcome(User.first).deliver_later!(wait: 1.hour)
  47. # Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now)
  48. # Notifier.welcome(User.first).deliver_later!(priority: 10)
  49. #
  50. # Options:
  51. #
  52. # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
  53. # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
  54. # * <tt>:queue</tt> - Enqueue the email on the specified queue
  55. # * <tt>:priority</tt> - Enqueues the email with the specified priority
  56. #
  57. # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
  58. # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
  59. # +delivery_job+.
  60. #
  61. # class AccountRegistrationMailer < ApplicationMailer
  62. # self.delivery_job = RegistrationDeliveryJob
  63. # end
  64. 1 def deliver_later!(options = {})
  65. 2 enqueue_delivery :deliver_now!, options
  66. end
  67. # Enqueues the email to be delivered through Active Job. When the
  68. # job runs it will send the email using +deliver_now+.
  69. #
  70. # Notifier.welcome(User.first).deliver_later
  71. # Notifier.welcome(User.first).deliver_later(wait: 1.hour)
  72. # Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now)
  73. # Notifier.welcome(User.first).deliver_later(priority: 10)
  74. #
  75. # Options:
  76. #
  77. # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay.
  78. # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time.
  79. # * <tt>:queue</tt> - Enqueue the email on the specified queue.
  80. # * <tt>:priority</tt> - Enqueues the email with the specified priority
  81. #
  82. # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
  83. # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
  84. # +delivery_job+.
  85. #
  86. # class AccountRegistrationMailer < ApplicationMailer
  87. # self.delivery_job = RegistrationDeliveryJob
  88. # end
  89. 1 def deliver_later(options = {})
  90. 36 enqueue_delivery :deliver_now, options
  91. end
  92. # Delivers an email without checking +perform_deliveries+ and +raise_delivery_errors+,
  93. # so use with caution.
  94. #
  95. # Notifier.welcome(User.first).deliver_now!
  96. #
  97. 1 def deliver_now!
  98. 2 processed_mailer.handle_exceptions do
  99. 2 message.deliver!
  100. end
  101. end
  102. # Delivers an email:
  103. #
  104. # Notifier.welcome(User.first).deliver_now
  105. #
  106. 1 def deliver_now
  107. 81 processed_mailer.handle_exceptions do
  108. 77 message.deliver
  109. end
  110. end
  111. 1 private
  112. # Returns the processed Mailer instance. We keep this instance
  113. # on hand so we can delegate exception handling to it.
  114. 1 def processed_mailer
  115. 256 @processed_mailer ||= @mailer_class.new.tap do |mailer|
  116. 182 mailer.process @action, *@args
  117. end
  118. end
  119. 1 def enqueue_delivery(delivery_method, options = {})
  120. 30 if processed?
  121. 1 ::Kernel.raise "You've accessed the message before asking to " \
  122. "deliver it later, so you may have made local changes that would " \
  123. "be silently lost if we enqueued a job to deliver it. Why? Only " \
  124. "the mailer method *arguments* are passed with the delivery job! " \
  125. "Do not access the message in any way if you mean to deliver it " \
  126. "later. Workarounds: 1. don't touch the message before calling " \
  127. "#deliver_later, 2. only touch the message *within your mailer " \
  128. "method*, or 3. use a custom Active Job instead of #deliver_later."
  129. else
  130. 29 job = @mailer_class.delivery_job
  131. 29 if job <= MailDeliveryJob
  132. 27 job.set(options).perform_later(
  133. @mailer_class.name, @action.to_s, delivery_method.to_s, args: @args)
  134. else
  135. 2 job.set(options).perform_later(
  136. @mailer_class.name, @action.to_s, delivery_method.to_s, *@args)
  137. end
  138. end
  139. end
  140. end
  141. end

lib/action_mailer/parameterized.rb

95.45% lines covered

44 relevant lines. 42 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionMailer
  3. # Provides the option to parameterize mailers in order to share instance variable
  4. # setup, processing, and common headers.
  5. #
  6. # Consider this example that does not use parameterization:
  7. #
  8. # class InvitationsMailer < ApplicationMailer
  9. # def account_invitation(inviter, invitee)
  10. # @account = inviter.account
  11. # @inviter = inviter
  12. # @invitee = invitee
  13. #
  14. # subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
  15. #
  16. # mail \
  17. # subject: subject,
  18. # to: invitee.email_address,
  19. # from: common_address(inviter),
  20. # reply_to: inviter.email_address_with_name
  21. # end
  22. #
  23. # def project_invitation(project, inviter, invitee)
  24. # @account = inviter.account
  25. # @project = project
  26. # @inviter = inviter
  27. # @invitee = invitee
  28. # @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
  29. #
  30. # subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
  31. #
  32. # mail \
  33. # subject: subject,
  34. # to: invitee.email_address,
  35. # from: common_address(inviter),
  36. # reply_to: inviter.email_address_with_name
  37. # end
  38. #
  39. # def bulk_project_invitation(projects, inviter, invitee)
  40. # @account = inviter.account
  41. # @projects = projects.sort_by(&:name)
  42. # @inviter = inviter
  43. # @invitee = invitee
  44. #
  45. # subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
  46. #
  47. # mail \
  48. # subject: subject,
  49. # to: invitee.email_address,
  50. # from: common_address(inviter),
  51. # reply_to: inviter.email_address_with_name
  52. # end
  53. # end
  54. #
  55. # InvitationsMailer.account_invitation(person_a, person_b).deliver_later
  56. #
  57. # Using parameterized mailers, this can be rewritten as:
  58. #
  59. # class InvitationsMailer < ApplicationMailer
  60. # before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
  61. # before_action { @account = params[:inviter].account }
  62. #
  63. # default to: -> { @invitee.email_address },
  64. # from: -> { common_address(@inviter) },
  65. # reply_to: -> { @inviter.email_address_with_name }
  66. #
  67. # def account_invitation
  68. # mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
  69. # end
  70. #
  71. # def project_invitation
  72. # @project = params[:project]
  73. # @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
  74. #
  75. # mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
  76. # end
  77. #
  78. # def bulk_project_invitation
  79. # @projects = params[:projects].sort_by(&:name)
  80. #
  81. # mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
  82. # end
  83. # end
  84. #
  85. # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
  86. 1 module Parameterized
  87. 1 extend ActiveSupport::Concern
  88. 1 included do
  89. 1 attr_accessor :params
  90. end
  91. 1 module ClassMethods
  92. # Provide the parameters to the mailer in order to use them in the instance methods and callbacks.
  93. #
  94. # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
  95. #
  96. # See Parameterized documentation for full example.
  97. 1 def with(params)
  98. 16 ActionMailer::Parameterized::Mailer.new(self, params)
  99. end
  100. end
  101. 1 class Mailer # :nodoc:
  102. 1 def initialize(mailer, params)
  103. 16 @mailer, @params = mailer, params
  104. end
  105. 1 private
  106. 1 def method_missing(method_name, *args)
  107. 15 if @mailer.action_methods.include?(method_name.to_s)
  108. 15 ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args)
  109. else
  110. super
  111. end
  112. end
  113. 1 ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
  114. 1 def respond_to_missing?(method, include_all = false)
  115. 4 @mailer.respond_to?(method, include_all)
  116. end
  117. end
  118. 1 class DeliveryJob < ActionMailer::DeliveryJob # :nodoc:
  119. 1 def perform(mailer, mail_method, delivery_method, params, *args)
  120. 1 mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method)
  121. end
  122. 1 ruby2_keywords(:perform) if respond_to?(:ruby2_keywords, true)
  123. end
  124. 1 class MessageDelivery < ActionMailer::MessageDelivery # :nodoc:
  125. 1 def initialize(mailer_class, action, params, *args)
  126. 15 super(mailer_class, action, *args)
  127. 15 @params = params
  128. end
  129. 1 ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
  130. 1 private
  131. 1 def processed_mailer
  132. 11 @processed_mailer ||= @mailer_class.new.tap do |mailer|
  133. 6 mailer.params = @params
  134. 6 mailer.process @action, *@args
  135. end
  136. end
  137. 1 def enqueue_delivery(delivery_method, options = {})
  138. 8 if processed?
  139. super
  140. else
  141. 8 job = delivery_job_class
  142. 8 if job <= MailDeliveryJob
  143. 6 job.set(options).perform_later(
  144. @mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args)
  145. else
  146. 2 job.set(options).perform_later(
  147. @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args)
  148. end
  149. end
  150. end
  151. 1 def delivery_job_class
  152. 8 if @mailer_class.delivery_job <= MailDeliveryJob
  153. 6 @mailer_class.delivery_job
  154. else
  155. 2 Parameterized::DeliveryJob
  156. end
  157. end
  158. end
  159. end
  160. end

lib/action_mailer/preview.rb

81.36% lines covered

59 relevant lines. 48 lines covered and 11 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/descendants_tracker"
  3. 1 module ActionMailer
  4. 1 module Previews #:nodoc:
  5. 1 extend ActiveSupport::Concern
  6. 1 included do
  7. # Set the location of mailer previews through app configuration:
  8. #
  9. # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
  10. #
  11. 1 mattr_accessor :preview_path, instance_writer: false
  12. # Enable or disable mailer previews through app configuration:
  13. #
  14. # config.action_mailer.show_previews = true
  15. #
  16. # Defaults to +true+ for development environment
  17. #
  18. 1 mattr_accessor :show_previews, instance_writer: false
  19. # :nodoc:
  20. 1 mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor]
  21. end
  22. 1 module ClassMethods
  23. # Register one or more Interceptors which will be called before mail is previewed.
  24. 1 def register_preview_interceptors(*interceptors)
  25. 3 interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
  26. end
  27. # Unregister one or more previously registered Interceptors.
  28. 1 def unregister_preview_interceptors(*interceptors)
  29. 3 interceptors.flatten.compact.each { |interceptor| unregister_preview_interceptor(interceptor) }
  30. end
  31. # Register an Interceptor which will be called before mail is previewed.
  32. # Either a class or a string can be passed in as the Interceptor. If a
  33. # string is passed in it will be constantized.
  34. 1 def register_preview_interceptor(interceptor)
  35. 5 preview_interceptor = interceptor_class_for(interceptor)
  36. 5 unless preview_interceptors.include?(preview_interceptor)
  37. 5 preview_interceptors << preview_interceptor
  38. end
  39. end
  40. # Unregister a previously registered Interceptor.
  41. # Either a class or a string can be passed in as the Interceptor. If a
  42. # string is passed in it will be constantized.
  43. 1 def unregister_preview_interceptor(interceptor)
  44. 5 preview_interceptors.delete(interceptor_class_for(interceptor))
  45. end
  46. 1 private
  47. 1 def interceptor_class_for(interceptor)
  48. 10 case interceptor
  49. when String, Symbol
  50. 6 interceptor.to_s.camelize.constantize
  51. else
  52. 4 interceptor
  53. end
  54. end
  55. end
  56. end
  57. 1 class Preview
  58. 1 extend ActiveSupport::DescendantsTracker
  59. 1 attr_reader :params
  60. 1 def initialize(params = {})
  61. 10 @params = params
  62. end
  63. 1 class << self
  64. # Returns all mailer preview classes.
  65. 1 def all
  66. load_previews if descendants.empty?
  67. descendants
  68. end
  69. # Returns the mail object for the given email name. The registered preview
  70. # interceptors will be informed so that they can transform the message
  71. # as they would if the mail was actually being delivered.
  72. 1 def call(email, params = {})
  73. 10 preview = new(params)
  74. 10 message = preview.public_send(email)
  75. 10 inform_preview_interceptors(message)
  76. 10 message
  77. end
  78. # Returns all of the available email previews.
  79. 1 def emails
  80. public_instance_methods(false).map(&:to_s).sort
  81. end
  82. # Returns +true+ if the email exists.
  83. 1 def email_exists?(email)
  84. emails.include?(email)
  85. end
  86. # Returns +true+ if the preview exists.
  87. 1 def exists?(preview)
  88. all.any? { |p| p.preview_name == preview }
  89. end
  90. # Find a mailer preview by its underscored class name.
  91. 1 def find(preview)
  92. all.find { |p| p.preview_name == preview }
  93. end
  94. # Returns the underscored name of the mailer preview without the suffix.
  95. 1 def preview_name
  96. name.delete_suffix("Preview").underscore
  97. end
  98. 1 private
  99. 1 def load_previews
  100. if preview_path
  101. Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
  102. end
  103. end
  104. 1 def preview_path
  105. Base.preview_path
  106. end
  107. 1 def show_previews
  108. Base.show_previews
  109. end
  110. 1 def inform_preview_interceptors(message)
  111. 10 Base.preview_interceptors.each do |interceptor|
  112. 8 interceptor.previewing_email(message)
  113. end
  114. end
  115. end
  116. end
  117. end

lib/action_mailer/railtie.rb

0.0% lines covered

68 relevant lines. 0 lines covered and 68 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_job/railtie"
  3. require "action_mailer"
  4. require "rails"
  5. require "abstract_controller/railties/routes_helpers"
  6. module ActionMailer
  7. class Railtie < Rails::Railtie # :nodoc:
  8. config.action_mailer = ActiveSupport::OrderedOptions.new
  9. config.eager_load_namespaces << ActionMailer
  10. initializer "action_mailer.logger" do
  11. ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger }
  12. end
  13. initializer "action_mailer.set_configs" do |app|
  14. paths = app.config.paths
  15. options = app.config.action_mailer
  16. options.assets_dir ||= paths["public"].first
  17. options.javascripts_dir ||= paths["public/javascripts"].first
  18. options.stylesheets_dir ||= paths["public/stylesheets"].first
  19. options.show_previews = Rails.env.development? if options.show_previews.nil?
  20. options.cache_store ||= Rails.cache
  21. if options.show_previews
  22. options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
  23. end
  24. # make sure readers methods get compiled
  25. options.asset_host ||= app.config.asset_host
  26. options.relative_url_root ||= app.config.relative_url_root
  27. ActiveSupport.on_load(:action_mailer) do
  28. include AbstractController::UrlFor
  29. extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false)
  30. include app.routes.mounted_helpers
  31. register_interceptors(options.delete(:interceptors))
  32. register_preview_interceptors(options.delete(:preview_interceptors))
  33. register_observers(options.delete(:observers))
  34. if delivery_job = options.delete(:delivery_job)
  35. self.delivery_job = delivery_job.constantize
  36. end
  37. options.each { |k, v| send("#{k}=", v) }
  38. end
  39. ActiveSupport.on_load(:action_dispatch_integration_test) do
  40. include ActionMailer::TestHelper
  41. include ActionMailer::TestCase::ClearTestDeliveries
  42. end
  43. end
  44. initializer "action_mailer.set_autoload_paths" do |app|
  45. options = app.config.action_mailer
  46. if options.show_previews && options.preview_path
  47. ActiveSupport::Dependencies.autoload_paths << options.preview_path
  48. end
  49. end
  50. initializer "action_mailer.compile_config_methods" do
  51. ActiveSupport.on_load(:action_mailer) do
  52. config.compile_methods! if config.respond_to?(:compile_methods!)
  53. end
  54. end
  55. initializer "action_mailer.eager_load_actions" do
  56. ActiveSupport.on_load(:after_initialize) do
  57. ActionMailer::Base.descendants.each(&:action_methods) if config.eager_load
  58. end
  59. end
  60. config.after_initialize do |app|
  61. options = app.config.action_mailer
  62. if options.show_previews
  63. app.routes.prepend do
  64. get "/rails/mailers" => "rails/mailers#index", internal: true
  65. get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
  66. end
  67. end
  68. end
  69. end
  70. end

lib/action_mailer/rescuable.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionMailer #:nodoc:
  3. # Provides +rescue_from+ for mailers. Wraps mailer action processing,
  4. # mail job processing, and mail delivery.
  5. 1 module Rescuable
  6. 1 extend ActiveSupport::Concern
  7. 1 include ActiveSupport::Rescuable
  8. 1 class_methods do
  9. 1 def handle_exception(exception) #:nodoc:
  10. 2 rescue_with_handler(exception) || raise(exception)
  11. end
  12. end
  13. 1 def handle_exceptions #:nodoc:
  14. 267 yield
  15. rescue => exception
  16. 10 rescue_with_handler(exception) || raise
  17. end
  18. 1 private
  19. 1 def process(*)
  20. 188 handle_exceptions do
  21. 188 super
  22. end
  23. end
  24. end
  25. end

lib/action_mailer/test_case.rb

98.55% lines covered

69 relevant lines. 68 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/test_case"
  3. 1 require "rails-dom-testing"
  4. 1 module ActionMailer
  5. 1 class NonInferrableMailerError < ::StandardError
  6. 1 def initialize(name)
  7. 1 super "Unable to determine the mailer to test from #{name}. " \
  8. "You'll need to specify it using tests YourMailer in your " \
  9. "test case definition"
  10. end
  11. end
  12. 1 class TestCase < ActiveSupport::TestCase
  13. 1 module ClearTestDeliveries
  14. 1 extend ActiveSupport::Concern
  15. 1 included do
  16. 1 setup :clear_test_deliveries
  17. 1 teardown :clear_test_deliveries
  18. end
  19. 1 private
  20. 1 def clear_test_deliveries
  21. 2 if ActionMailer::Base.delivery_method == :test
  22. 2 ActionMailer::Base.deliveries.clear
  23. end
  24. end
  25. end
  26. 1 module Behavior
  27. 1 extend ActiveSupport::Concern
  28. 1 include ActiveSupport::Testing::ConstantLookup
  29. 1 include TestHelper
  30. 1 include Rails::Dom::Testing::Assertions::SelectorAssertions
  31. 1 include Rails::Dom::Testing::Assertions::DomAssertions
  32. 1 included do
  33. 1 class_attribute :_mailer_class
  34. 1 setup :initialize_test_deliveries
  35. 1 setup :set_expected_mail
  36. 1 teardown :restore_test_deliveries
  37. 1 ActiveSupport.run_load_hooks(:action_mailer_test_case, self)
  38. end
  39. 1 module ClassMethods
  40. 1 def tests(mailer)
  41. 5 case mailer
  42. when String, Symbol
  43. 2 self._mailer_class = mailer.to_s.camelize.constantize
  44. when Module
  45. 3 self._mailer_class = mailer
  46. else
  47. raise NonInferrableMailerError.new(mailer)
  48. end
  49. end
  50. 1 def mailer_class
  51. 5 if mailer = _mailer_class
  52. 4 mailer
  53. else
  54. 1 tests determine_default_mailer(name)
  55. end
  56. end
  57. 1 def determine_default_mailer(name)
  58. 2 mailer = determine_constant_from_test_name(name) do |constant|
  59. 2 Class === constant && constant < ActionMailer::Base
  60. end
  61. 2 raise NonInferrableMailerError.new(name) if mailer.nil?
  62. 1 mailer
  63. end
  64. end
  65. 1 private
  66. 1 def initialize_test_deliveries
  67. 56 set_delivery_method :test
  68. 56 @old_perform_deliveries = ActionMailer::Base.perform_deliveries
  69. 56 ActionMailer::Base.perform_deliveries = true
  70. 56 ActionMailer::Base.deliveries.clear
  71. end
  72. 1 def restore_test_deliveries
  73. 56 restore_delivery_method
  74. 56 ActionMailer::Base.perform_deliveries = @old_perform_deliveries
  75. end
  76. 1 def set_delivery_method(method)
  77. 56 @old_delivery_method = ActionMailer::Base.delivery_method
  78. 56 ActionMailer::Base.delivery_method = method
  79. end
  80. 1 def restore_delivery_method
  81. 58 ActionMailer::Base.deliveries.clear
  82. 58 ActionMailer::Base.delivery_method = @old_delivery_method
  83. end
  84. 1 def set_expected_mail
  85. 56 @expected = Mail.new
  86. 56 @expected.content_type ["text", "plain", { "charset" => charset }]
  87. 56 @expected.mime_version = "1.0"
  88. end
  89. 1 def charset
  90. 58 "UTF-8"
  91. end
  92. 1 def encode(subject)
  93. 1 Mail::Encodings.q_value_encode(subject, charset)
  94. end
  95. 1 def read_fixture(action)
  96. 1 IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
  97. end
  98. end
  99. 1 include Behavior
  100. end
  101. end

lib/action_mailer/test_helper.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_job"
  3. 1 module ActionMailer
  4. # Provides helper methods for testing Action Mailer, including #assert_emails
  5. # and #assert_no_emails.
  6. 1 module TestHelper
  7. 1 include ActiveJob::TestHelper
  8. # Asserts that the number of emails sent matches the given number.
  9. #
  10. # def test_emails
  11. # assert_emails 0
  12. # ContactMailer.welcome.deliver_now
  13. # assert_emails 1
  14. # ContactMailer.welcome.deliver_now
  15. # assert_emails 2
  16. # end
  17. #
  18. # If a block is passed, that block should cause the specified number of
  19. # emails to be sent.
  20. #
  21. # def test_emails_again
  22. # assert_emails 1 do
  23. # ContactMailer.welcome.deliver_now
  24. # end
  25. #
  26. # assert_emails 2 do
  27. # ContactMailer.welcome.deliver_now
  28. # ContactMailer.welcome.deliver_later
  29. # end
  30. # end
  31. 1 def assert_emails(number, &block)
  32. 14 if block_given?
  33. 12 original_count = ActionMailer::Base.deliveries.size
  34. 16 perform_enqueued_jobs(only: ->(job) { delivery_job_filter(job) }, &block)
  35. 12 new_count = ActionMailer::Base.deliveries.size
  36. 12 assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent"
  37. else
  38. 2 assert_equal number, ActionMailer::Base.deliveries.size
  39. end
  40. end
  41. # Asserts that no emails have been sent.
  42. #
  43. # def test_emails
  44. # assert_no_emails
  45. # ContactMailer.welcome.deliver_now
  46. # assert_emails 1
  47. # end
  48. #
  49. # If a block is passed, that block should not cause any emails to be sent.
  50. #
  51. # def test_emails_again
  52. # assert_no_emails do
  53. # # No emails should be sent from this block
  54. # end
  55. # end
  56. #
  57. # Note: This assertion is simply a shortcut for:
  58. #
  59. # assert_emails 0, &block
  60. 1 def assert_no_emails(&block)
  61. 3 assert_emails 0, &block
  62. end
  63. # Asserts that the number of emails enqueued for later delivery matches
  64. # the given number.
  65. #
  66. # def test_emails
  67. # assert_enqueued_emails 0
  68. # ContactMailer.welcome.deliver_later
  69. # assert_enqueued_emails 1
  70. # ContactMailer.welcome.deliver_later
  71. # assert_enqueued_emails 2
  72. # end
  73. #
  74. # If a block is passed, that block should cause the specified number of
  75. # emails to be enqueued.
  76. #
  77. # def test_emails_again
  78. # assert_enqueued_emails 1 do
  79. # ContactMailer.welcome.deliver_later
  80. # end
  81. #
  82. # assert_enqueued_emails 2 do
  83. # ContactMailer.welcome.deliver_later
  84. # ContactMailer.welcome.deliver_later
  85. # end
  86. # end
  87. 1 def assert_enqueued_emails(number, &block)
  88. 19 assert_enqueued_jobs(number, only: ->(job) { delivery_job_filter(job) }, &block)
  89. end
  90. # Asserts that a specific email has been enqueued, optionally
  91. # matching arguments.
  92. #
  93. # def test_email
  94. # ContactMailer.welcome.deliver_later
  95. # assert_enqueued_email_with ContactMailer, :welcome
  96. # end
  97. #
  98. # def test_email_with_arguments
  99. # ContactMailer.welcome("Hello", "Goodbye").deliver_later
  100. # assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"]
  101. # end
  102. #
  103. # If a block is passed, that block should cause the specified email
  104. # to be enqueued.
  105. #
  106. # def test_email_in_block
  107. # assert_enqueued_email_with ContactMailer, :welcome do
  108. # ContactMailer.welcome.deliver_later
  109. # end
  110. # end
  111. #
  112. # If +args+ is provided as a Hash, a parameterized email is matched.
  113. #
  114. # def test_parameterized_email
  115. # assert_enqueued_email_with ContactMailer, :welcome,
  116. # args: {email: 'user@example.com'} do
  117. # ContactMailer.with(email: 'user@example.com').welcome.deliver_later
  118. # end
  119. # end
  120. 1 def assert_enqueued_email_with(mailer, method, args: nil, queue: "mailers", &block)
  121. 7 args = if args.is_a?(Hash)
  122. 2 [mailer.to_s, method.to_s, "deliver_now", params: args, args: []]
  123. else
  124. 5 [mailer.to_s, method.to_s, "deliver_now", args: Array(args)]
  125. end
  126. 7 assert_enqueued_with(job: mailer.delivery_job, args: args, queue: queue, &block)
  127. end
  128. # Asserts that no emails are enqueued for later delivery.
  129. #
  130. # def test_no_emails
  131. # assert_no_enqueued_emails
  132. # ContactMailer.welcome.deliver_later
  133. # assert_enqueued_emails 1
  134. # end
  135. #
  136. # If a block is provided, it should not cause any emails to be enqueued.
  137. #
  138. # def test_no_emails
  139. # assert_no_enqueued_emails do
  140. # # No emails should be enqueued from this block
  141. # end
  142. # end
  143. 1 def assert_no_enqueued_emails(&block)
  144. 3 assert_enqueued_emails 0, &block
  145. end
  146. 1 private
  147. 1 def delivery_job_filter(job)
  148. 13 job_class = job.is_a?(Hash) ? job.fetch(:job) : job.class
  149. 13 Base.descendants.map(&:delivery_job).include?(job_class) ||
  150. ActionMailer::Parameterized::DeliveryJob == job_class
  151. end
  152. end
  153. end

lib/action_mailer/version.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "gem_version"
  3. 1 module ActionMailer
  4. # Returns the version of the currently loaded Action Mailer as a
  5. # <tt>Gem::Version</tt>.
  6. 1 def self.version
  7. gem_version
  8. end
  9. end

lib/rails/generators/mailer/mailer_generator.rb

0.0% lines covered

29 relevant lines. 0 lines covered and 29 lines missed.
    
  1. # frozen_string_literal: true
  2. module Rails
  3. module Generators
  4. class MailerGenerator < NamedBase
  5. source_root File.expand_path("templates", __dir__)
  6. argument :actions, type: :array, default: [], banner: "method method"
  7. check_class_collision suffix: "Mailer"
  8. def create_mailer_file
  9. template "mailer.rb", File.join("app/mailers", class_path, "#{file_name}_mailer.rb")
  10. in_root do
  11. if behavior == :invoke && !File.exist?(application_mailer_file_name)
  12. template "application_mailer.rb", application_mailer_file_name
  13. end
  14. end
  15. end
  16. hook_for :template_engine, :test_framework
  17. private
  18. def file_name # :doc:
  19. @_file_name ||= super.sub(/_mailer\z/i, "")
  20. end
  21. def application_mailer_file_name
  22. @_application_mailer_file_name ||= if mountable_engine?
  23. "app/mailers/#{namespaced_path}/application_mailer.rb"
  24. else
  25. "app/mailers/application_mailer.rb"
  26. end
  27. end
  28. end
  29. end
  30. end