action_mailer.cr
action_mailer.cr
A Crystal port of Rails ActionMailer 8.1.4 — mailer DSL, MIME message assembly, SMTP/sendmail/file/test delivery, deliver_later via ActiveJob, parameterized mailers and previews. Namespace ActionMailer mirrors the original 1:1; the hidden bulk of the work is a minimal port of the mail gem (MIME assembly + SMTP client). Estimated parity ~80%. Suite: 66/66.
⚠️ EXPERIMENTAL FRAMEWORK, DO NOT USE IN PRODUCTION YET.
Installation
dependencies:
action_mailer_cr:
github: OrelSokolov/action_mailer.cr
shards install
Depends on active_support.cr, action_view.cr (templates), active_job.cr (deliver_later).
Usage
class ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
layout "mailer"
end
class UserMailer < ApplicationMailer
def welcome_email(user)
attachments["terms.pdf"] = {mime_type: "application/pdf", content: pdf}
headers["X-Custom"] = "value"
mail(to: user.email, subject: "Welcome!")
end
end
UserMailer.welcome_email(user).deliver_now
UserMailer.with(user: user).welcome_email.deliver_later(wait: 5.minutes)
Feature comparison vs actionmailer 8.1.4 (+ mail 2.9.1)
DSL & messages
| Feature | Original (actionmailer) | Port (action_mailer.cr) | Status |
|---|---|---|---|
class XMailer < ApplicationMailer, default from:, layout |
✅ | Same | ✅ |
Action = method; class-level UserMailer.welcome_email(user) → MessageDelivery |
✅ (class-level method_missing) |
Macro-generated via compile-time registry + codegen → lazy memoized MessageDelivery | ✅ |
mail(to:, subject:), mail do |format| format.text {} format.html {} end |
✅ | Fixed collector methods instead of method_missing | 🔶 |
attachments["x.pdf"] = {mime_type:, content:}, attachments.inline, headers[...] |
✅ | attachments ✅; inline attachments not ported | 🔶 |
Multipart auto-discovery (welcome_email.{html,text}.ecr → multipart/alternative) |
✅ | Same (1 format → single part, 2 → alternative, attachments → mixed on top) | ✅ |
Callbacks: before/after/around_action, before/after/around_deliver, rescue_from |
✅ | Same | ✅ |
Subject via I18n (user_mailer.welcome_email.subject) |
✅ | Not ported | ⬜ |
Parameterized .with(user: u).action |
✅ | Parameterized::Mailer, params["user"] in mailer methods, params carried by MessageDelivery and deliver_later serialization (byte-1:1 with Ruby args) |
✅ |
register_observer / register_interceptor |
✅ | Not ported | ⬜ |
Previews: XMailerPreview < ActionMailer::Preview, /rails/mailers dev page |
✅ | Compile-time registry (Preview.all/find/exists?, .call), preview server with ?part=text|html |
✅ |
Delivery
| Feature | Original | Port | Status |
|---|---|---|---|
deliver_now / deliver_now! / deliver_later(wait:, queue:, priority:) / .message |
✅ | Same; deliver_later enqueues MailDeliveryJob (queue :mailers) with Rails 8.1-identical arguments |
✅ |
Delivery methods :smtp/:sendmail/:file/:test + registry (add_delivery_method), smtp_settings, raise_delivery_errors, perform_deliveries, deliveries |
✅ | Same registry + adapters; SMTP client (RFC 5321 subset: EHLO/MAIL/RCPT/DATA/QUIT, dot-stuffing) | 🔶 |
| STARTTLS / SMTP auth | ✅ | Not ported (P1) | ⬜ |
| Resend (API delivery) | separate gem/HTTP | :resend adapter registered out of the box (POST /emails, Bearer key) |
➕ |
| MessageDelivery facade | method_missing delegation | Explicit method list (no runtime delegation) | 🔶 |
mail-gem port (mail/ inside the shard)
| Feature | Original (mail gem) | Port | Status |
|---|---|---|---|
| MIME assembly: multipart/alternative, mixed, related; quoted-printable/base64 | ✅ | CTE 7bit/base64 chosen by content; QP bodies not emitted (UTF-8 passthrough, oracle-confirmed) | 🔶 |
| RFC 5322/2045/2047 subset covering transactional email | ✅ | src/rfc2822.cr wire renderer |
✅ |
Deviations & Crystal-specific notes
- Params keys are Strings, not Symbols (
params["user"].as(String)); the_aj_symbol_keysmarker is emitted duringdeliver_laterserialization so Ruby-side ActiveJob can deserialize. - MessageDelivery is an explicit facade — no
method_missing. - Oracle semantics captured: ASCII attachments ride 7bit; from/to read as arrays.
Testing
crystal spec # 66/66
Oracle: oracle/cases.rb → oracle/expected.yml, recorded with actionmailer 8.1.4 + mail 2.9.1 (delivery :test; DSL/MIME/multipart/ attachments/headers/unicode/deliver_later/parameterized/delivery methods), replayed without editing the cases. The SMTP protocol is pinned by a fake-server spec (spec/smtp_spec.cr); delivery adapters are unit-tested against a local stub server (spec/delivery_spec.cr, no network).
action_mailer.cr
- 0
- 0
- 0
- 2
- 2
- about 3 hours ago
- October 12, 2026
Thu, 15 Oct 2026 21:44:39 GMT