filipebraida/adonisjs-starter-kit/mail
Claude Code and agent skill by filipebraida/adonisjs-starter-kit
- community
Checking that the SKILL.md file is still in the repository.
Transactional email in an AdonisJS app — `@adonisjs/mail` classes extending `BaseMail`, MJML templates via a shared `@email.layout` component, and `mailContext()` for appName/appUrl. Never author raw HTML+CSS blocks. Use when adding a new email, editing an existing template, testing mail with `mail.fake()`, or wiring the SMTP env. Trigger on: "add email", "send mail", "MJML template", "reset password mail", "welcome email".
Install
npx vskill@latest install filipebraida/adonisjs-starter-kit/mailSKILL.md
Emails are classes that extend @adonisjs/mail's BaseMail. The prepare() method sets recipients, subject, and calls this.message.htmlView('<mod>::emails/<template>', {...}). Templates are .edge files at app/<mod>/ui/emails/<name>.edge that consume a shared @email.layout component. The layout owns fonts, colors, button styles, header, and footer; each template only fills the body slot. Raw <html><head><style> blocks are never authored — MJML with the shared layout is mandatory. Emails are always dispatched from event listeners, not from actions directly (see [[actions-events]]), so unit tests can fake the emitter and end-to-end tests can fake the mail transport.
Rules
- Mail class location:
app/<mod>/mails/<name>_notification.ts— one file per email. - Class shape: extends
BaseMail, declaresfrom = env.get('EMAIL_FROM'), setssubjecteither as a class field or dynamically inprepare()from atranslationsfield. - `prepare()` does all the work: build any URL (via
signedUrlForfor time-limited links), callthis.message.to(recipient).subject(...), then render withhtmlView('<mod>::emails/<template>', props). Always spread...mailContext()into props so the layout hasappName+appUrl. - Templates live at
app/<mod>/ui/emails/<name>.edge. Every template opens with@email.layout({ title, preview: subtitle })and closes with@end. Between them, only<mj-section>markup — body, button, text. - Shared MJML layout is the single source of truth for typography, colors, header, footer. Change once, all emails update.
- `mailContext()` returns
{ appName, appUrl }from env with a fallback name. Every mail class spreads it intohtmlViewprops so the layout can render brand chrome. - Env:
EMAIL_FROMrequired;APP_NAMEoptional (has a fallback);APP_URLrequired. SMTP vars for local dev (typically Mailpit) and the prod driver's API key (e.g.RESEND_API_KEY). - i18n: the translation shape is a plain interface (
subject,title,subtitle,actionBtn,defaultMessage). Passed from the caller. Keys live per module underemails.<name>.*in the locale JSON — see [[i18n]].
Repo refs
- Mail class +
prepare():app/auth/mails/reset_password_notification.ts,app/users/mails/welcome_notification.ts. - Shared MJML layout:
resources/views/components/email/layout.edge. Template:app/users/ui/emails/welcome.edge. mailContext():app/common/services/mail_context.ts.
Doc refs
- AdonisJS Mail — https://docs.adonisjs.com/guides/digging-deeper/mail
- MJML documentation — https://mjml.io/documentation/
- MJML in AdonisJS — https://docs.adonisjs.com/guides/digging-deeper/mail#using-mjml-for-responsive-emails
Workflow
Add a new email
- Class at
app/<mod>/mails/<name>_notification.ts:
```ts import { BaseMail } from '@adonisjs/mail' import env from '#start/env' import { mailContext } from '#common/services/mail_context'
export default class OrderConfirmedNotification extends BaseMail { from = env.get('EMAIL_FROM') subject = 'Order confirmed'
constructor( private user: User, private order: Order, private translations: MailBasicTranslation, ) { super() this.subject = translations.subject }
async prepare() { const { title, subtitle, actionBtn, defaultMessage } = this.translations this.message.to(this.user.email).subject(this.translations.subject) this.message.htmlView('<mod>::emails/order_confirmed', { ...mailContext(), title, subtitle, actionBtn, defaultMessage, orderUrl: ${env.get('APP_URL')}/orders/${this.order.id}, }) } } ```
- Template at
app/<mod>/ui/emails/<name>.edge:
```edge @email.layout({ title, preview: subtitle }) <mj-section background-color="#ffffff" padding="36px 40px 8px"> <mj-column> <mj-text font-size="28px" font-weight="800" color="#141820">{{ title }}</mj-text> <mj-text padding="24px 0 0" font-size="16px" color="#3A4150">{{ subtitle }}</mj-text> </mj-column> </mj-section>
<mj-section background-color="#ffffff" padding="0 40px 28px"> <mj-column> <mj-button href="{{ orderUrl }}" align="left">{{ actionBtn }}</mj-button> </mj-column> </mj-section> @end ```
- Mount the edge namespace for the module —
app/<mod>/start/view.tscallsedge.mount('<mod>', new URL('ui', BASE_URL)). Preload the file viaadonisrc.ts— see [[module-scaffolding]]. - i18n: add
emails.<name>.{subject,title,subtitle,actionBtn,defaultMessage}to the locale JSON — see [[i18n]]. - Trigger via event: emit the domain event from the action; the listener in
<mod>/start/events.tscallsmail.send(new Mail(...))— see [[actions-events]]. - Verify: a spec that asserts the send with
mail.fake()(see Testing below) — not done until it confirms the mail dispatches.
Change the shared look
Edit the shared MJML layout component. Every email updates. Ask whether a change belongs in the shared layout (font, brand color, corners) or per-template (headline size, hero image).
Signed / time-limited URLs
Use signedUrlFor('route.name', params, { expiresIn, prefixUrl, purpose }) for password-reset-style links so the URL expires and can only be consumed once.
Testing
tests/bootstrap.ts typically calls mail.fake() as a safety net so no spec accidentally emails a real inbox. Per-spec, opt into stricter assertions:
For listener specs, call mail.fake() before emitter.emit(...) — see [[testing]].
Anti-patterns
- ❌ Authoring
<html><head><style>blocks — bypasses the layout, breaks responsive defaults, drifts from other emails. - ❌ Duplicating header/footer HTML in every template — use the layout.
- ❌ Calling
mail.send(...)directly from an action — emit an event; the listener sends. Otherwise unit tests must stub the mail service. - ❌ Hardcoding
appNameinside a template — read{{ appName }}(provided bymailContext()). - ❌ Assembling URLs by string concat when
signedUrlFor(orurlForon the frontend) is available. - ❌ Forgetting
previewin@email.layout({ title, preview })— a good preview text noticeably improves inbox opens. - ❌ Passing a Lucid model into the mail class and dereferencing it inside
prepare()— pull the fields you need into a plain object first, so the class stays serializable and easy to test.
Related skills
[[actions-events]] · [[i18n]] · [[testing]] · [[notifications]]
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingpackages/skills/mail/SKILL.md
Popularity
- Repository stars
- 94
- Repository forks
- 12
- 7-day trend score
- 23
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/filipebraida/adonisjs-starter-kit/mail)