skills/tools-email/SKILL.md
Email delivery using Resend API. Use this skill when implementing email verification flows, password reset, transactional emails, configuring DNS (SPF/DKIM/DMARC), setting up the Resend MCP server, or following email best practices for deliverability. Includes Next.js 16 proxy patterns, OAuth vs password user handling, and token security patterns.
npx skillsauth add aussiegingersnap/cursor-skills tools-emailInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides workflows, best practices, and code patterns for sending transactional emails using Resend.
Resend is a modern email API designed for developers. It provides:
# .env.local
RESEND_API_KEY=re_xxxxx
RESEND_FROM_EMAIL="App Name <[email protected]>"
Before sending from your domain, you must verify it:
| Type | Name | Value | Purpose |
|------|------|-------|---------|
| TXT | @ or domain | v=spf1 include:_spf.resend.com ~all | SPF |
| CNAME | resend._domainkey | Provided by Resend | DKIM |
| TXT | _dmarc | v=DMARC1; p=none; rua=mailto:[email protected] | DMARC |
To use Resend directly from Cursor's AI:
cd ~/Desktop/Code
git clone https://github.com/resend/mcp-send-email.git mcp-send-email
cd mcp-send-email && npm install && npm run build
realpath ~/Desktop/Code/mcp-send-email/build/index.js
# Output: /Users/YOUR_USERNAME/Desktop/Code/mcp-send-email/build/index.js
.cursor/mcp.json:{
"mcpServers": {
"resend": {
"type": "command",
"command": "node /Users/YOUR_USERNAME/Desktop/Code/mcp-send-email/build/index.js --key=re_xxxxx [email protected]"
}
}
}
Common Gotcha: The path must be absolute and correct.
~/Code/vs~/Desktop/Code/will causeMODULE_NOT_FOUNDerrors. Always verify withrealpath.
// lib/email/index.ts
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
const FROM_EMAIL = process.env.RESEND_FROM_EMAIL || 'App <[email protected]>'
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
export interface SendEmailOptions {
to: string
subject: string
html: string
text?: string
replyTo?: string
}
export async function sendEmail(options: SendEmailOptions) {
const { data, error } = await resend.emails.send({
from: FROM_EMAIL,
to: options.to,
subject: options.subject,
html: options.html,
text: options.text,
replyTo: options.replyTo,
})
if (error) {
console.error('[Email] Failed to send:', error)
throw new Error(`Failed to send email: ${error.message}`)
}
return data
}
export async function sendVerificationEmail(email: string, token: string) {
const verifyUrl = `${APP_URL}/verify-email?token=${token}`
return sendEmail({
to: email,
subject: 'Verify your email address',
html: `
<h2>Welcome!</h2>
<p>Please verify your email address by clicking the button below:</p>
<a href="${verifyUrl}" style="
display: inline-block;
background: #000;
color: #fff;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 16px 0;
">Verify Email</a>
<p>Or copy this link: ${verifyUrl}</p>
<p>This link expires in 24 hours.</p>
<p style="color: #666; font-size: 12px;">
If you didn't create an account, you can ignore this email.
</p>
`,
text: `Verify your email: ${verifyUrl}`,
})
}
export async function sendPasswordResetEmail(email: string, token: string) {
const resetUrl = `${APP_URL}/reset-password?token=${token}`
return sendEmail({
to: email,
subject: 'Reset your password',
html: `
<h2>Password Reset Request</h2>
<p>We received a request to reset your password. Click below to choose a new one:</p>
<a href="${resetUrl}" style="
display: inline-block;
background: #000;
color: #fff;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 16px 0;
">Reset Password</a>
<p>Or copy this link: ${resetUrl}</p>
<p>This link expires in 1 hour.</p>
<p style="color: #666; font-size: 12px;">
If you didn't request this, you can safely ignore this email.
</p>
`,
text: `Reset your password: ${resetUrl}`,
})
}
export async function sendWelcomeEmail(email: string, name?: string) {
return sendEmail({
to: email,
subject: 'Welcome to App Name!',
html: `
<h2>Welcome${name ? `, ${name}` : ''}!</h2>
<p>Your email has been verified and your account is ready.</p>
<p>Here are some things you can do:</p>
<ul>
<li>Complete your profile</li>
<li>Explore features</li>
<li>Check out the documentation</li>
</ul>
<a href="${APP_URL}" style="
display: inline-block;
background: #000;
color: #fff;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 16px 0;
">Get Started</a>
`,
text: `Welcome! Your account is ready. Get started at ${APP_URL}`,
})
}
Add these tables for email verification and password reset:
-- Add to users table
ALTER TABLE users ADD COLUMN email_verified_at TEXT;
-- Email verification tokens (one-time use)
CREATE TABLE email_verification_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE, -- SHA-256 hash, never store raw
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Password reset tokens (one-time use)
CREATE TABLE password_reset_tokens (
id TEXT PRIMARY KEY,
email TEXT NOT NULL, -- Use email, not user_id (user might not exist)
token_hash TEXT NOT NULL UNIQUE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_verification_tokens_hash ON email_verification_tokens(token_hash);
CREATE INDEX idx_reset_tokens_hash ON password_reset_tokens(token_hash);
Use secure, URL-safe tokens for verification and reset links:
// lib/auth/tokens.ts
import { sha256 } from 'oslo/crypto'
import { encodeBase64url, encodeHex } from 'oslo/encoding'
// Token valid for 24 hours
const VERIFICATION_TOKEN_EXPIRY = 24 * 60 * 60 * 1000
// Reset token valid for 1 hour
const RESET_TOKEN_EXPIRY = 60 * 60 * 1000
/**
* Generate a cryptographically secure token
*/
export function generateToken(): string {
const bytes = new Uint8Array(32)
crypto.getRandomValues(bytes)
return encodeBase64url(bytes)
}
/**
* Hash a token for database storage
* Never store raw tokens - always hash them
*/
export async function hashToken(token: string): Promise<string> {
return encodeHex(await sha256(new TextEncoder().encode(token)))
}
Prevent email abuse with rate limiting:
// Per-email rate limits
const EMAIL_RATE_LIMITS = {
verification: { max: 3, window: 60 * 60 * 1000 }, // 3 per hour
passwordReset: { max: 3, window: 60 * 60 * 1000 }, // 3 per hour
general: { max: 10, window: 24 * 60 * 60 * 1000 }, // 10 per day
}
| Plan | Emails/day | Emails/month | Rate limit | |------|------------|--------------|------------| | Free | 100 | 3,000 | 2/second | | Pro | 5,000+ | Based on plan | 10/second |
When implementing email verification, handle OAuth and password users differently:
// OAuth users (Google, Apple, etc.) - pre-verified
const user = createUser({
// ...
email_verified_at: new Date().toISOString(), // Trust OAuth provider
})
// Password users - require verification
const user = createUser({
// ...
email_verified_at: null, // Must verify via email
})
| Auth Type | Email Verified? | Verification Required? | |-----------|----------------|----------------------| | Google OAuth | Yes (by Google) | No | | Apple OAuth | Yes (by Apple) | No | | Email/Password | No | Yes - block until verified | | Magic Link | Yes (implicit) | No |
// In login API
if (!user.email_verified_at) {
return NextResponse.json({
error: 'Please verify your email before signing in.',
code: 'EMAIL_NOT_VERIFIED',
requiresVerification: true,
}, { status: 403 })
}
If a user tries to sign up with an email that exists but isn't verified, resend the verification:
const existingUser = getUserByEmail(email)
if (existingUser && !existingUser.email_verified_at) {
// Resend verification instead of returning error
const { token } = await createVerificationToken(existingUser.id)
await sendVerificationEmail(email, token)
return { requiresVerification: true }
}
Breaking Change: Next.js 16 replaced
middleware.tswithproxy.ts
For protected routes, use proxy.ts for fast redirects only:
// src/proxy.ts
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// Allow auth routes
if (['/login', '/verify-email', '/forgot-password', '/reset-password']
.some(r => pathname.startsWith(r))) {
return NextResponse.next()
}
// Let API routes handle their own auth
return NextResponse.next()
}
Important: Don't do heavy auth validation in proxy. Check email_verified_at in your API routes and server components instead.
Use middleware.ts with the same logic, but export as middleware instead of proxy.
| Strategy | UX | Security | Use Case | |----------|----|---------|----| | Block until verified | Friction | High | Financial, healthcare | | Soft verification (banner) | Smooth | Medium | Social, content apps | | No verification | Seamless | Low | Low-risk apps |
email_verified_at column to users tableemail_verification_tokens tableresend and oslo packageslib/email/index.ts)lib/auth/tokens.ts)/api/auth/verify-email endpoint/api/auth/resend-verification endpoint (rate limited)/verify-email page with resend UIemail_verified_atpassword_reset_tokens table/api/auth/forgot-password endpoint/api/auth/reset-password endpoint/forgot-password page/reset-password pageRESEND_API_KEY=re_xxxxx
RESEND_FROM_EMAIL="App Name <[email protected]>"
NEXT_PUBLIC_APP_URL=https://yourapp.com
Cause: Wrong path in .cursor/mcp.json
Fix: Use absolute path, verify with realpath:
realpath ~/Desktop/Code/mcp-send-email/build/index.js
OAuth-only users don't have passwords. Check for password_hash:
if (!user.password_hash) {
return { error: 'This account uses social login.' }
}
src/proxy.ts (not middleware.ts)proxy (not middleware)Install the Resend SDK:
npm install resend
For token hashing (recommended):
npm install oslo
For React Email templates (optional):
npm install @react-email/components react-email
tools
# Versioning Skill Semantic versioning automation based on conventional commits. Automatically manages version bumps, changelogs, and git tags using `standard-version`. ## When to Use - Before releasing a new version - When preparing a deployment - To generate/update CHANGELOG.md - When the user asks about version management - Setting up versioning for a new project ## Prerequisites - Conventional commits enforced (recommended: lefthook) - Node.js project with package.json ## Setup (One-Ti
tools
Theme generation with tweakcn for shadcn/ui and Magic UI animations. Use when setting up project themes, customizing color schemes, adding dark mode, or integrating animated components.
tools
shadcn/studio component library with MCP integration, theme generation, and block patterns. This skill should be used when building UI with shadcn components, selecting dashboard layouts, or generating landing pages. Canonical source for all shadcn-based work.
development
Enforce a precise, minimal design system inspired by Linear, Notion, and Stripe. Use this skill when building dashboards, admin interfaces, or any UI that needs Jony Ive-level precision - clean, modern, minimalist with taste. Every pixel matters.