email protections
email protections
this guide covers the built-in protections for preventing abuse and ensuring reliable email delivery
table of contents
- overview
- rate limiting
- duplicate detection
- domain blocking
- email validation
- bulk send protection
- monitoring
overview
the email system includes multiple layers of protection:
| protection | purpose | default | |------------|---------|---------| | rate limiting | prevent spam | 10/min 100/hr 500/day | | duplicate detection | prevent accidental re-sends | 60 second window | | domain blocking | block disposable emails | enabled | | email validation | ensure valid addresses | enabled | | bulk confirmation | prevent mass sends | 10+ emails |
rate limiting
how it works
rate limits are tracked per user using localstorage:
User sends email → Check rate limits → Allow/Block → Track if allowed
```typescript
### default limits
| window | limit | error |
|--------|-------|-------|
| per minute | 10 emails | "too many emails sent please wait a minute" |
| per hour | 100 emails | "hourly email limit reached" |
| per day | 500 emails | "daily email limit reached" |
### checking rate limits
```tsx
const { getRateLimitStatus, canSendEmail } = useSendEmail();
// Get current status
const status = getRateLimitStatus();
console.log(status);
// {
// emailsLastMinute: 5,
// emailsLastHour: 42,
// emailsLastDay: 156,
// remainingMinute: 5,
// remainingHour: 58,
// remainingDay: 344,
// }
// Check before sending
const { canSend, reason } = canSendEmail("user@example.com", "Subject");
if (!canSend) {
alert(reason); // Shows rate limit message
}
```typescript
### custom rate limits
```tsx
const { sendEmail } = useSendEmail({
enableRateLimiting: true,
maxEmailsPerMinute: 5, // Stricter
maxEmailsPerHour: 50,
maxEmailsPerDay: 200,
});
```typescript
### disabling rate limits
```tsx
// Not recommended for production
const { sendEmail } = useSendEmail({
enableRateLimiting: false,
});
```typescript
### rate limit callbacks
```tsx
const { sendEmail } = useSendEmail({
onRateLimited: () => {
// Show upgrade prompt
showUpgradeModal();
// Log to analytics
analytics.track("rate_limited");
},
});
```typescript
---
## duplicate detection
### how it works
detects duplicate emails based on recipient + subject hash:
```typescript
New email → Hash(recipient + subject) → Check recent hashes → Allow/Block
```typescript
### default settings
- **window**: 60 seconds
- **behavior**: block duplicates within window
### configuration
```tsx
const { sendEmail } = useSendEmail({
enableDuplicateDetection: true,
duplicateWindowSeconds: 120, // 2 minutes
});
```typescript
### use cases
**prevent double-click sends**
```tsx
// User clicks "Send" twice quickly
// First click: Sends email
// Second click: Blocked as duplicate
```typescript
**allow re-sends after window**
```tsx
// After duplicateWindowSeconds passes,
// same email can be sent again
```typescript
### disabling duplicate detection
```tsx
const { sendEmail } = useSendEmail({
enableDuplicateDetection: false,
});
```typescript
---
## domain blocking
### default blocked domains
the following disposable email domains are blocked by default:
```typescript
[
"mailinator.com",
"guerrillamail.com",
"tempmail.com",
"throwaway.email",
"fakeinbox.com",
"10minutemail.com",
"temp-mail.org",
"getnada.com",
]
```typescript
### adding blocked domains
```tsx
const { sendEmail } = useSendEmail({
blockedRecipientDomains: [
// Keep defaults
"mailinator.com",
"guerrillamail.com",
// Add more
"yopmail.com",
"trashmail.com",
],
});
```typescript
### using domain allowlist
restrict to specific domains only:
```tsx
const { sendEmail } = useSendEmail({
allowedRecipientDomains: [
"company.com",
"partner.com",
"gmail.com",
],
blockedRecipientDomains: [], // Clear blocklist
});
```typescript
### error handling
```tsx
const result = await sendEmail({
to: "user@mailinator.com", // Blocked domain
// ...
});
if (!result.success) {
console.log(result.error);
// "Emails to this domain are not allowed."
}
```typescript
---
## email validation
### validation rules
| rule | description |
|------|-------------|
| format | rfc 5322 compliant regex |
| subject length | max 200 characters |
| html body size | max 1mb |
| attachment size | max 10mb each |
| attachment count | max 10 |
| recipient count | max 50 |
### configuration
```tsx
const { sendEmail } = useSendEmail({
enableEmailValidation: true, // Default
});
```typescript
### pre-validation
check before sending:
```tsx
const { canSendEmail } = useSendEmail();
// Invalid email format
const result1 = canSendEmail("not-an-email", "Subject");
// { canSend: false, reason: "Invalid email address provided." }
// Valid email
const result2 = canSendEmail("user@example.com", "Subject");
// { canSend: true }
```typescript
---
## bulk send protection
### how it works
when sending 10+ emails requires explicit confirmation:
```tsx
const result = await sendBulkEmails(emails);
// If emails.length >= 10:
// { success: false, error: "Bulk email send requires confirmation." }
```typescript
### confirming bulk sends
```tsx
const result = await sendBulkEmails(emails, {
skipConfirmation: true, // Bypass confirmation
});
```typescript
### custom threshold
```tsx
const { sendBulkEmails } = useSendEmail({
requireBulkConfirmation: true,
bulkThreshold: 5, // Require confirmation for 5+ emails
});
```typescript
### ui example
```tsx
function BulkSender({ emails }) {
const { sendBulkEmails } = useSendEmail();
const [confirmed, setConfirmed] = useState(false);
const handleSend = async () => {
if (emails.length >= 10 && !confirmed) {
const userConfirmed = await showConfirmDialog(
`Send ${emails.length} emails?`
);
if (!userConfirmed) return;
setConfirmed(true);
}
await sendBulkEmails(emails, {
skipConfirmation: confirmed,
});
};
return <button onClick={handleSend}>Send {emails.length} emails</button>;
}
```typescript
---
## monitoring
### email status tracking
all emails are stored in the database with status tracking:
```sql
SELECT
status,
COUNT(*) as count
FROM emails
WHERE created_at > NOW() - INTERVAL '24 hours'
GROUP BY status;
```typescript
### status values
| status | description |
|--------|-------------|
| `pending` | queued for sending |
| `sent` | sent to resend |
| `delivered` | delivered to recipient |
| `opened` | recipient opened email |
| `bounced` | email bounced |
| `failed` | failed to send |
### open tracking
emails include a tracking pixel:
```tsx
// Automatically added to emails
<img src="/api/email/track?view_key=xxx" width="1" height="1" />
```typescript
### query examples
**failed emails today**
```sql
SELECT * FROM emails
WHERE status = 'failed'
AND created_at > NOW() - INTERVAL '24 hours';
```typescript
**emails by template**
```sql
SELECT template_id, COUNT(*)
FROM emails
GROUP BY template_id;
```typescript
**open rate**
```sql
SELECT
COUNT(*) FILTER (WHERE opened = true) as opened,
COUNT(*) as total,
ROUND(
COUNT(*) FILTER (WHERE opened = true)::numeric / COUNT(*)::numeric * 100, 2
) as open_rate
FROM emails
WHERE created_at > NOW() - INTERVAL '7 days';
```typescript
### alerts
set up alerts for:
- high failure rate
- rate limit hits
- blocked domain attempts
- bounce spikes
---
## best practices
### 1 keep default protections
don't disable protections in production:
```tsx
// Production
const { sendEmail } = useSendEmail();
// Risky
const { sendEmail } = useSendEmail({
enableRateLimiting: false,
enableDuplicateDetection: false,
});
```typescript
### 2 handle protected cases gracefully
```tsx
const { canSendEmail, sendEmail } = useSendEmail();
const handleSend = async () => {
const { canSend, reason } = canSendEmail(email, subject);
if (!canSend) {
notification({ message: reason, success: false });
return;
}
await sendEmail({ ... });
};
```typescript
### 3 monitor blocked attempts
log when protections trigger:
```tsx
const { sendEmail } = useSendEmail({
onRateLimited: () => {
analytics.track("email_rate_limited", { userId });
},
onError: (error) => {
if (error.includes("domain")) {
analytics.track("blocked_domain_attempt", { userId });
}
},
});
```typescript
### 4 adjust limits per use case
```tsx
// Transactional emails - stricter limits
const transactionalEmail = useSendEmail({
maxEmailsPerMinute: 5,
});
// Bulk marketing - higher limits with confirmation
const marketingEmail = useSendEmail({
maxEmailsPerMinute: 50,
requireBulkConfirmation: true,
bulkThreshold: 100,
});
```typescript
### 5 test protections
```tsx
// Test rate limiting
for (let i = 0; i < 15; i++) {
const result = await sendEmail({ ... });
console.log(`Email ${i + 1}:`, result.success ? "sent" : result.error);
}
// Expected: First 10 succeed, next 5 fail with rate limit
```typescript