Security audit workflow - vulnerability scan β verification
npx skills add aussiegingersnap/cursor-skills --skill "tools-email"
Install specific skill from multi-skill repository
# Description
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.
# SKILL.md
name: tools-email
description: 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.
Email Resend Skill
This skill provides workflows, best practices, and code patterns for sending transactional emails using Resend.
Overview
Resend is a modern email API designed for developers. It provides:
- Simple REST API and SDKs
- React Email integration for beautiful templates
- Built-in analytics and deliverability monitoring
- MCP server for AI-assisted email workflows
Prerequisites
1. Resend Account Setup
- Sign up at resend.com
- Generate an API key in the dashboard
- Add to your environment:
# .env.local
RESEND_API_KEY=re_xxxxx
RESEND_FROM_EMAIL="App Name <[email protected]>"
2. Domain Verification
Before sending from your domain, you must verify it:
- Go to Resend Dashboard β Domains β Add Domain
- Add these DNS records at your provider:
| 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 |
- Wait for verification (usually 5-60 minutes)
3. MCP Server Setup (Optional - for Cursor AI)
To use Resend directly from Cursor's AI:
- Clone and build the MCP server:
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
- Get the absolute path (this is critical):
realpath ~/Desktop/Code/mcp-send-email/build/index.js
# Output: /Users/YOUR_USERNAME/Desktop/Code/mcp-send-email/build/index.js
- Add to your
.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.
Code Patterns
Basic Resend Client (TypeScript)
// 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
}
Verification Email
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}`,
})
}
Password Reset Email
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}`,
})
}
Welcome Email (after verification)
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}`,
})
}
Database Schema
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);
Token Generation Pattern
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)))
}
Token Security Rules
- Never store raw tokens - Always hash with SHA-256 before storing
- One-time use - Delete token after successful validation
- Delete existing tokens - When creating new token, delete any existing ones for that user/email
- Short expiry for reset - Password reset tokens should expire in 1 hour max
- Longer expiry for verification - Email verification can be 24-48 hours
Rate Limiting
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
}
Resend API Limits
| Plan | Emails/day | Emails/month | Rate limit |
|---|---|---|---|
| Free | 100 | 3,000 | 2/second |
| Pro | 5,000+ | Based on plan | 10/second |
OAuth vs Password Users
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
})
Verification Flow by Auth Type
| 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 |
Handling Unverified Login Attempts
// 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 })
}
Re-signup for Unverified Users
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 }
}
Next.js Integration
Next.js 16+ (proxy.ts)
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.
Next.js 16 and earlier
Use middleware.ts with the same logic, but export as middleware instead of proxy.
Best Practices
Subject Lines
- Keep under 60 characters
- Be specific and action-oriented
- Avoid spam triggers (see references/deliverability.md)
Email Copy
- Front-load important information
- Use clear CTAs
- Always include plain text fallback
- Keep emails focused on one purpose
Transactional vs Marketing
- Transactional: Triggered by user action (verification, reset, receipts)
- Marketing: Promotional content (newsletters, announcements)
- Keep them separate - different sending reputations
Error Handling
- Log all email failures
- Have fallback mechanisms (show token in UI for dev)
- Don't block user actions on email failures
Verification Strategies
| 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 |
Implementation Checklist
Email Verification Flow
- [ ] Add
email_verified_atcolumn to users table - [ ] Create
email_verification_tokenstable - [ ] Install
resendandoslopackages - [ ] Create email service (
lib/email/index.ts) - [ ] Create token utilities (
lib/auth/tokens.ts) - [ ] Modify signup to send verification email
- [ ] Create
/api/auth/verify-emailendpoint - [ ] Create
/api/auth/resend-verificationendpoint (rate limited) - [ ] Create
/verify-emailpage with resend UI - [ ] Update login to check
email_verified_at - [ ] Handle OAuth users as pre-verified
Password Reset Flow
- [ ] Create
password_reset_tokenstable - [ ] Create
/api/auth/forgot-passwordendpoint - [ ] Create
/api/auth/reset-passwordendpoint - [ ] Create
/forgot-passwordpage - [ ] Create
/reset-passwordpage - [ ] Add "Forgot password?" link to login page
Environment Variables
RESEND_API_KEY=re_xxxxx
RESEND_FROM_EMAIL="App Name <[email protected]>"
NEXT_PUBLIC_APP_URL=https://yourapp.com
References
- Deliverability Guide - DNS, spam prevention, reputation
- Email Templates - Copy best practices, compliance
- React Email Patterns - Component-based email templates
Common Issues & Troubleshooting
MCP Server "MODULE_NOT_FOUND"
Cause: Wrong path in .cursor/mcp.json
Fix: Use absolute path, verify with realpath:
realpath ~/Desktop/Code/mcp-send-email/build/index.js
Emails Going to Spam
- Verify DNS records (SPF, DKIM, DMARC) are correct
- Check sender domain matches authenticated domain
- Review email content for spam trigger words
- Test with mail-tester.com
Token Validation Failing
- Ensure you're hashing the token before lookup
- Check token hasn't expired
- Verify token wasn't already consumed (one-time use)
- Check for URL encoding issues in the token
OAuth Users Can't Reset Password
OAuth-only users don't have passwords. Check for password_hash:
if (!user.password_hash) {
return { error: 'This account uses social login.' }
}
Next.js 16 Proxy Not Working
- File must be
src/proxy.ts(notmiddleware.ts) - Export must be
proxy(notmiddleware) - Proxy runs on Node.js runtime, not Edge
Dependencies
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
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.