New post: Sending Email in Wheels 4.0 — sendEmail, Templates, and Background Delivery #3307
bpamiri
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
A new how-to in the Wheels 4.0 series, and the first one to cover email end-to-end. If you've ever gone looking for the mailer class in Wheels and come up empty, this is for you.
Read: https://blog.wheels.dev/posts/sending-email-in-wheels-4
The premise
A user signs up; you want to send a welcome email; you go looking for
WelcomeMailer. There isn't one. Noapp/mailers/, no ActionMailer, nodeliver_later. Email in Wheels is first-class — it's just spelled differently. The entire mail surface is one controller helper:sendEmail. That single decision makes email in Wheels simple, and it's also why one specific thing (sending mail from a background job) trips up everyone who tries it.What the post covers
sendEmail is the whole mailer. There is no
wheels.Mailer, no ActionMailer equivalent, nodeliverMail. One function renders a view template and delivers it viacfmail. A "WelcomeMailer" is a convention — a controller you make, or a job that calls into one.The arg-routing rule. Every named argument is checked against a fixed allow-list of
cfmailattribute names (from,to,cc,subject,type,replyto, etc.). On the list → passed straight tocfmail. Not on the list (and not a Wheels control arg) → stripped out and injected into the controller'svariablesscope so your email template can read it. Elegant — and it means a mistyped header name doesn't error, it just becomes a view variable nobody reads.Template resolution. Email templates are ordinary
.cfmviews underapp/views, resolved (and lower-cased) through the same machinery asrenderView. A bare name resolves under the current controller's folder; a leading slash anchors at the views root regardless of controller. That distinction becomes load-bearing the moment you send from a job.Multipart text+html. Pass two templates (max two). By default Wheels picks which is text and which is HTML by counting
<characters — fewer means text. Fine until your plain-text body has a code sample in it, at which point your parts can flip.detectMultipart=false+ a text-first order forces it.Attachments and previewing.
file="x.pdf"looks under thefiles/folder; anything with a slash is used as-is. Anddeliver=falseis the built-in preview/test path — it renders and assembles everything but skips the send, returning the full result struct. It's how the framework's own suite tests email.The centerpiece correction
This is the part worth flagging here specifically, because it's a known doc bug.
The
Job.cfcdocblock — and our own Background Jobs blog post — show aperform()method whose body is a baresendEmail(to=data.email, ...). That doesn't resolve.sendEmailis a controller mixin.wheels.Jobis a plaincomponent {}with no controller mixins and noonMissingMethod, and the worker runs jobs viaCreateObject("component", jobClass).perform(...)with no controller context established. A baresendEmail(ormodel()) inside a worker-processed job is an undefined method.The working pattern reaches
sendEmailthrough the globalcontroller()factory, which builds a real controller object with the mixin:public void function perform(struct data = {}) { var mailer = controller("Mailer"); mailer.sendEmail( from = "welcome@example.com", to = arguments.data.email, subject = "Welcome!", template = "/mailers/welcome", // leading slash anchors at views root userName = arguments.data.name ); }The post has the full loop: enqueue from the controller action (returns instantly, no SMTP wait), deliver in the worker via
wheels jobs work --queue=mailers.Discussion
The bare-
sendEmail-in-performsnippet appears in bothvendor/wheels/Job.cfc(its docblock) and the Background Jobs post, so it's a doc pattern worth fixing rather than quietly reproducing. Question for the room: should the canonical guidance be "always go throughcontroller('Mailer')," or is there appetite for makingsendEmail/modelresolve directly inside a Job context? Curious what people are doing in their own apps today.Feedback on the post — what's unclear, what's missing, what you'd want covered next — welcome in this thread.
All reactions