Skip to content

Commit 33ef66e

Browse files
bpamiriclaude
andauthored
docs(web/guides): fix sending-email guide mailer pattern, attachment paths, and SMTP wording (#3093)
Behavioral audit (P1 batch 2, p1-7-email) corrections to digging-deeper/sending-email.mdx: - Mailer pattern: replace new wheels.Controller().sendEmail(...) — throws on every engine (no variables.params) — with the verified controller(name, params) factory form; add a caution citing #3078. - Attachments: examples used slash-containing paths that skip filePath resolution; switch to bare filenames, document the separator rule and that filePath expands relative to the web root (public/files/). - sendEmail defaults: from/to/subject are engine-required parameters, so configured defaults for them never apply; drop from= from both set(functionName="sendEmail", ...) examples and document why. - Background-job rationale: cfmail spools by default, so the request does not normally wait for the SMTP handoff; reword the latency claim. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent f180fb5 commit 33ef66e

1 file changed

Lines changed: 24 additions & 12 deletions

File tree

web/sites/guides/src/content/docs/v4-0-0/digging-deeper/sending-email.mdx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,33 @@ You don't want to repeat `server`, `username`, `password` on every `sendEmail()`
5252
```cfm {test:compile}
5353
// config/settings.cfm
5454
set(functionName="sendEmail",
55-
from="no-reply@example.com",
5655
server="smtp.example.com",
5756
port=587,
5857
useTLS=true,
5958
username="smtp-user",
6059
password=env("SMTP_PASSWORD"));
6160
```
6261

63-
Every argument `cfmail` accepts is available here: `server`, `port`, `username`, `password`, `useSSL`, `useTLS`, `from`, `replyto`, `failto`, `subject`, and more. Pulling secrets from `env()` keeps credentials out of source control.
62+
Every optional argument `cfmail` accepts is available here: `server`, `port`, `username`, `password`, `useSSL`, `useTLS`, `replyto`, `failto`, and more. Pulling secrets from `env()` keeps credentials out of source control.
63+
64+
Don't set defaults for `from`, `to`, or `subject`, though — those three are *required* parameters of `sendEmail()`, and your CFML engine enforces them before Wheels gets a chance to apply configured defaults. A `from` default set here is never used; omitting `from=` at a call site throws a missing-parameter error on every engine even with the default configured. Always pass `from=` (and `to=`, `subject=`) explicitly.
6465

6566
<Aside type="caution">
66-
Wheels does not define a `mailerSettings` struct. SMTP connection arguments go through the `sendEmail` function defaults (above) or via your CFML engine's mail service configuration. Anything you can pass to `cfmail`, you can set as a `sendEmail` default.
67+
Wheels does not define a `mailerSettings` struct. SMTP connection arguments go through the `sendEmail` function defaults (above) or via your CFML engine's mail service configuration. Anything you can pass to `cfmail`, you can set as a `sendEmail` default — except `from`, `to`, and `subject`, which are required at every call site as described above.
6768
</Aside>
6869

6970
## Organize sends in `app/mailers/`
7071

71-
For anything beyond a one-liner, move the send call out of the controller and into a mailer component. Mailers are plain CFCs — no framework base class — that wrap `sendEmail()` behind a named method:
72+
For anything beyond a one-liner, move the send call out of the controller and into a mailer component. Mailers are plain CFCs — no framework base class — that wrap `sendEmail()` behind a named method. Because `sendEmail()` is a controller function and needs a request-capable controller instance (one with a `params` struct), each mailer method obtains one through the `controller()` factory:
7273

7374
```cfm {test:compile} title="app/mailers/UserMailer.cfc"
7475
component {
7576
public any function sendWelcome(required any user) {
76-
return new wheels.Controller().sendEmail(
77+
local.mailer = new wheels.Global().controller(
78+
name="Mailer",
79+
params={controller: "mailer", action: "sendWelcome"}
80+
);
81+
return local.mailer.sendEmail(
7782
template="/mailers/user/welcome",
7883
layout="/mailers/layout",
7984
from="no-reply@example.com",
@@ -84,7 +89,11 @@ component {
8489
}
8590
8691
public any function sendPasswordReset(required any user, required string token) {
87-
return new wheels.Controller().sendEmail(
92+
local.mailer = new wheels.Global().controller(
93+
name="Mailer",
94+
params={controller: "mailer", action: "sendPasswordReset"}
95+
);
96+
return local.mailer.sendEmail(
8897
template="/mailers/user/password_reset",
8998
layout="/mailers/layout",
9099
from="no-reply@example.com",
@@ -97,6 +106,10 @@ component {
97106
}
98107
```
99108

109+
<Aside type="caution">
110+
Don't instantiate the controller directly: `new wheels.Controller().sendEmail(...)` throws on every engine ("Component [wheels.Controller] has no accessible Member with name [PARAMS]" on Lucee, "Element PARAMS is undefined" on Adobe) because a bare controller instance has no `params` struct — the rendering pipeline dereferences it even for absolute template paths. The `controller(name, params)` factory builds a request-capable instance. Hardening `sendEmail()` for out-of-request senders is tracked in [#3078](https://github.com/wheels-dev/wheels/issues/3078).
111+
</Aside>
112+
100113
Put the views under `app/views/mailers/user/welcome.cfm` and `app/views/mailers/user/password_reset.cfm`. The leading slash on `template=` makes the path absolute (rooted at `app/views/`) so the mailer works regardless of which controller triggered it.
101114

102115
Call it from any controller:
@@ -130,7 +143,7 @@ Pass `onlyPath=false` to `linkTo()` inside email templates — a relative path l
130143

131144
## Send in a background job
132145

133-
`sendEmail()` runs synchronously. On a signup form that means the user's browser waits for SMTP to hand off the message before the redirect fires — adds hundreds of milliseconds on a good day, seconds when the mail server hiccups. Push the send into a job:
146+
`sendEmail()` runs synchronously: the template render, message composition, and spool/handoff all happen inside the request before the redirect fires. By default both Lucee and Adobe spool the message to disk rather than waiting for the SMTP handoff, so the cost is usually the render and compose — but with spooling disabled, or a slow template, the user's browser eats that latency on every signup. Push the send into a job:
134147

135148
```cfm {test:compile} title="app/jobs/SendWelcomeEmailJob.cfc"
136149
component extends="wheels.Job" {
@@ -194,15 +207,15 @@ Most mail clients prefer the HTML part when both are present; the plain-text par
194207

195208
## Attachments
196209

197-
Pass `file=` (or its alias `files=`) with one or more paths. Paths without a slash are resolved relative to `application.wheels.filePath` (defaults to `files/` at the app root):
210+
Pass `file=` (or its alias `files=`) with one or more paths. Paths without any directory separator (`/` or `\`) are resolved relative to the `filePath` setting — default `files`, which expands relative to the **web root**, so `public/files/` in the standard app template:
198211

199212
```cfm {test:compile}
200213
sendEmail(
201214
template="/mailers/billing/invoice",
202215
from="billing@example.com",
203216
to=user.email,
204217
subject="Your invoice",
205-
file="invoices/#user.id#-2026-04.pdf",
218+
file="#user.id#-2026-04.pdf",
206219
user=user
207220
);
208221
```
@@ -215,12 +228,12 @@ sendEmail(
215228
from="billing@example.com",
216229
to=user.email,
217230
subject="Your invoice and receipt",
218-
files="invoices/#user.id#.pdf,receipts/#user.id#.pdf",
231+
files="invoice-#user.id#.pdf,receipt-#user.id#.pdf",
219232
user=user
220233
);
221234
```
222235

223-
Absolute paths and URLs work too (`/var/app/pdfs/invoice.pdf`, `https://cdn.example.com/logo.png`) — anything `cfmailparam`'s `file` attribute accepts is valid.
236+
A path that *does* contain a separator skips the `filePath` resolution entirely and reaches `cfmailparam` unchanged — so a relative path like `invoices/123.pdf` resolves against the JVM working directory at delivery time, which is almost never what you want. For files outside `public/files/`, build an absolute path yourself (e.g. `file="#ExpandPath('../storage/invoices/#user.id#.pdf')#"`). Absolute paths and URLs work too (`/var/app/pdfs/invoice.pdf`, `https://cdn.example.com/logo.png`) — anything `cfmailparam`'s `file` attribute accepts is valid.
224237

225238
## Per-environment SMTP
226239

@@ -237,7 +250,6 @@ set(functionName="sendEmail",
237250

238251
```cfm {test:compile} title="config/production/settings.cfm"
239252
set(functionName="sendEmail",
240-
from="no-reply@example.com",
241253
server="smtp.postmarkapp.com",
242254
port=587,
243255
useTLS=true,

0 commit comments

Comments
 (0)