XYLEX Group

email protections

email protections

this guide covers the built-in protections for preventing abuse and ensuring reliable email delivery

table of contents


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