feat(mailer): add Prism and Mailer

This commit is contained in:
dualfroz committed 2026-09-07 16:56:31 +02:00
1 parent d37e21fd3a
commit e4165f8cd6
3 files changed
+172

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
module Prism
module Mailer
# ActionMailer-lite: subclass, declare `default from: '...'`, define an
# instance method per mailer action that ends with a call to #mail.
# Calling the action as a class method (`WelcomeMailer.welcome(user)`)
# renders the templates immediately and returns a Delivery.
class Base
class << self
def default(options = nil)
@default_options = (@default_options || {}).merge(options) if options
@default_options || {}
end
def template_roots
@template_roots || [File.join(Dir.pwd, 'app', 'views', 'mailers')]
end
attr_writer :template_roots
# Per-mailer-class override of Mailer.default_url_options, merged
# over it (this class's values win) by #url_for.
def default_url_options(options = nil)
@default_url_options = (@default_url_options || {}).merge(options) if options
@default_url_options || {}
end
def mailer_name
Inflector.underscore(name.to_s.split('::').last.sub(/Mailer\z/, ''))
end
def method_missing(name, *args, &block)
return super unless instance_methods(false).include?(name)
instance = new
instance.send(:run_action, name, args)
Delivery.new(instance.message, mailer_class: self, action_name: name.to_s, args: args)
end
def respond_to_missing?(name, include_private = false)
instance_methods(false).include?(name) || super
end
end
attr_reader :message, :action_name
def initialize
@message = Message.new
apply_defaults
end
def mail(to:, subject:, from: nil, cc: nil, bcc: nil, reply_to: nil, template: nil, locals: {})
message.to = to
message.subject = subject
message.from = from if from
message.cc = cc if cc
message.bcc = bcc if bcc
message.reply_to = reply_to if reply_to
render_bodies(template || action_name, locals)
message
end
# Absolute URL for a named route, for use inside mailer actions and
# templates (there is no incoming request to infer a host from, unlike
# Controller#url_for). Requires Mailer.router to be set and a :host
# somewhere in Mailer.default_url_options / self.class.default_url_options
# / the params passed here.
def url_for(name, params = {})
router = Mailer.router
raise 'no router configured; set Prism::Mailer.router before generating mailer URLs' unless router
options = Mailer.default_url_options.merge(self.class.default_url_options).merge(params)
host = options.delete(:host)
raise ArgumentError, 'default_url_options must include :host to build a mailer URL' unless host
protocol = options.delete(:protocol) || 'http'
port = options.delete(:port)
"#{protocol}://#{host}#{port ? ":#{port}" : ''}#{router.path_to(name, options)}"
end
private
def run_action(name, args)
@action_name = name.to_s
public_send(name, *args)
end
def apply_defaults
self.class.default.each do |key, value|
message.public_send("#{key}=", value)
end
end
def render_bodies(name, locals)
view = Prism::View.new(self.class.template_roots, helpers: self)
merged_locals = auto_locals.merge(locals)
found_any = false
begin
message.text_body = view.render("#{self.class.mailer_name}/#{name}.text", locals: merged_locals)
found_any = true
rescue Prism::View::TemplateNotFound
nil
end
begin
message.html_body = view.render("#{self.class.mailer_name}/#{name}.html", locals: merged_locals)
found_any = true
rescue Prism::View::TemplateNotFound
nil
end
return if found_any
raise Prism::View::TemplateNotFound,
"no mailer template #{name}.text.erb or #{name}.html.erb for " \
"#{self.class.mailer_name} in #{self.class.template_roots.join(', ')}"
end
def auto_locals
instance_variables.each_with_object({}) do |ivar, locals|
next if %i[@message @action_name].include?(ivar)
locals[ivar.to_s.delete_prefix('@').to_sym] = instance_variable_get(ivar)
end
end
end
end
end
+28
View File
@@ -0,0 +1,28 @@
module Prism
module Mailer
# Returned by a mailer action call (`UserMailer.welcome(user)`), in the
# spirit of ActionMailer's MessageDelivery: the message is already
# rendered at this point, #deliver_now hands it to Prism::Mailer.deliver!
# immediately, #deliver_later re-enqueues the action itself (mailer
# class, action name, args) as a Prism::Jobs::Job so a Worker performs
# the rendering and delivery out of process.
class Delivery
def initialize(message, mailer_class:, action_name:, args:)
@message = message
@mailer_class = mailer_class
@action_name = action_name
@args = args
end
attr_reader :message
def deliver_now
Mailer.deliver!(message)
end
def deliver_later(queue: nil, run_at: nil)
DeliveryJob.perform_later(@mailer_class.name, @action_name, @args, queue: queue, run_at: run_at)
end
end
end
end
+16
View File
@@ -0,0 +1,16 @@
module Prism
module Mailer
# Performs a mailer action out of process. Enqueued by
# Delivery#deliver_later with the mailer class name (resolved back
# through Object.const_get, same pattern Jobs::Worker already uses for
# job classes) so the queue only ever persists plain strings/arrays.
class DeliveryJob < Prism::Jobs::Job
queue_as 'mailers'
def perform(mailer_class_name, action_name, args)
mailer_class = Object.const_get(mailer_class_name)
mailer_class.public_send(action_name, *args).deliver_now
end
end
end
end