From 7611cf1a0e4e0185371e36362a7670f23cc851bf Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 17:39:07 +0200 Subject: [PATCH] feat(mailer): implement Prism and Mailer --- lib/prism/mailer/preview_app.rb | 99 +++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 lib/prism/mailer/preview_app.rb diff --git a/lib/prism/mailer/preview_app.rb b/lib/prism/mailer/preview_app.rb new file mode 100644 index 0000000..97be41e --- /dev/null +++ b/lib/prism/mailer/preview_app.rb @@ -0,0 +1,99 @@ +module Prism + module Mailer + # A tiny Rack app that browses Mailer::Preview subclasses in a web + # browser, mirroring Rails' /rails/mailers engine: an index of every + # registered preview, a list of its actions, and a rendered view of one + # email (HTML part if present, raw text part otherwise) - all without + # ever touching Mailer.deliver!. Mount it directly: + # BlogApp.mount('/rails/mailers', Prism::Mailer::PreviewApp.new) + class PreviewApp + def call(env) + request = Request.new(env) + return not_found unless request.request_method == 'GET' + + # Links are built from SCRIPT_NAME (where this app was mounted) + # rather than relative to PATH_INFO, so they work the same whether + # this is mounted at "/rails/mailers" or served standalone at "/" - + # relative hrefs would break the moment the current path lacks a + # trailing slash (the classic Rack::URLMap gotcha). + @base = env['SCRIPT_NAME'].to_s + segments = request.path.split('/').reject(&:empty?) + case segments.length + when 0 then index + when 1 then show_preview(segments[0]) + when 2 then show_email(segments[0], segments[1]) + else not_found + end + rescue ArgumentError + not_found + end + + private + + def index + previews = Preview.registry.sort_by(&:preview_name) + items = previews.map do |klass| + %(
  • #{escape(klass.preview_name)}
  • ) + end + body = "

    Mailer previews

    " + body = '

    Mailer previews

    No previews registered.

    ' if previews.empty? + html(body) + end + + def show_preview(name) + klass = Preview.find(name) + return not_found unless klass + + items = klass.emails.map do |email| + %(
  • #{escape(email)}
  • ) + end + body = +%(

    « all previews

    ) + body << "

    #{escape(klass.preview_name)}

    " + html(body) + end + + def show_email(name, email) + klass = Preview.find(name) + return not_found unless klass + + delivery = klass.call(email) + html(render_message(klass, email, delivery.message)) + end + + def render_message(klass, email, message) + body = +%(

    « #{escape(klass.preview_name)}

    ) + body << "

    #{escape(klass.preview_name)}##{escape(email)}

    " + body << '
    ' + body << "
    Subject
    #{escape(message.subject)}
    " + body << "
    From
    #{escape(message.from)}
    " + body << "
    To
    #{escape(message.to.join(', '))}
    " + body << '

    ' + body << render_body(message) + body + end + + def render_body(message) + if message.html_body + message.html_body + elsif message.text_body + "
    #{escape(message.text_body)}
    " + else + '

    empty body

    ' + end + end + + def escape(text) + CGI.escapeHTML(text.to_s) + end + + def html(body) + [200, { 'Content-Type' => 'text/html; charset=UTF-8' }, + ["#{body}"]] + end + + def not_found + [404, { 'Content-Type' => 'text/plain' }, ['Not Found']] + end + end + end +end