> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lettr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> Secure your inbound email pipeline in Lettr by verifying webhook credentials with Basic Auth or OAuth and handling external mail safely.

Receiving emails from external sources introduces security considerations. This guide covers best practices for securing your inbound email processing pipeline.

## Webhook Security

### Verify Webhook Credentials

Always verify that webhooks come from Lettr, not malicious actors. Lettr supports Basic Auth and OAuth 2.0 for webhook authentication — configure your preferred method when creating the webhook in the dashboard.

```javascript theme={null}
app.post('/webhooks/inbound', (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Basic ')) {
    return res.sendStatus(401);
  }

  const credentials = Buffer.from(authHeader.slice(6), 'base64').toString();
  const [username, password] = credentials.split(':');

  if (username !== process.env.WEBHOOK_USERNAME || password !== process.env.WEBHOOK_PASSWORD) {
    return res.sendStatus(401);
  }

  next();
}, express.json(), async (req, res) => {
  for (const event of req.body) {
    const relay = event.msys?.relay_message;
    if (!relay) continue;

    await processEmail(relay);
  }

  res.sendStatus(200);
});
```

See [Webhook Authorization](/learn/webhooks/authorization) for details on configuring Basic Auth and OAuth 2.0.

### IP Allowlisting

You can optionally restrict webhook access to known IP addresses as an additional layer of defense:

```javascript theme={null}
function ipAllowlist(allowedIPs) {
  return (req, res, next) => {
    const clientIP = req.ip || req.connection.remoteAddress;

    if (!allowedIPs.includes(clientIP)) {
      console.warn(`Rejected webhook from unauthorized IP: ${clientIP}`);
      return res.sendStatus(403);
    }

    next();
  };
}

// Configure with your known webhook source IPs
const allowedIPs = process.env.WEBHOOK_ALLOWED_IPS?.split(',') || [];
app.post('/webhooks/inbound', ipAllowlist(allowedIPs), webhookHandler);
```

<Note>
  IP allowlisting should be used as a supplementary security measure alongside credential verification, not as a replacement.
</Note>

## Input Validation

### Validate Email Addresses

Never trust email addresses from inbound emails:

```javascript theme={null}
function validateEmailAddress(email) {
  if (!email || typeof email !== 'string') {
    return false;
  }

  // Basic format check
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    return false;
  }

  // Length limits
  if (email.length > 254) {
    return false;
  }

  const [localPart, domain] = email.split('@');
  if (localPart.length > 64 || domain.length > 255) {
    return false;
  }

  return true;
}
```

### Sanitize Email Content

Sanitize HTML content before displaying or storing:

```javascript theme={null}
import DOMPurify from 'isomorphic-dompurify';

function sanitizeHtml(html) {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: [
      'p', 'br', 'b', 'i', 'u', 'strong', 'em',
      'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
      'ul', 'ol', 'li', 'a', 'img', 'blockquote',
      'table', 'thead', 'tbody', 'tr', 'th', 'td',
      'span', 'div'
    ],
    ALLOWED_ATTR: ['href', 'src', 'alt', 'class', 'style'],
    ALLOW_DATA_ATTR: false,
    FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form'],
    FORBID_ATTR: ['onerror', 'onload', 'onclick']
  });
}

app.post('/webhooks/inbound', express.json(), async (req, res) => {
  for (const event of req.body) {
    const relay = event.msys?.relay_message;
    if (!relay) continue;

    const { html, text } = relay.content;

    // Sanitize before storing
    const safeHtml = html ? sanitizeHtml(html) : null;
    const safeText = text ? escapeHtml(text) : null;

    await storeEmail({ html: safeHtml, text: safeText });
  }

  res.sendStatus(200);
});
```

### Validate and Sanitize Filenames

Never use attachment filenames directly:

```javascript theme={null}
import path from 'path';

function sanitizeFilename(filename) {
  if (!filename || typeof filename !== 'string') {
    return 'unnamed_file';
  }

  // Remove directory traversal attempts
  let safe = path.basename(filename);

  // Remove dangerous characters
  safe = safe.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');

  // Limit length
  safe = safe.substring(0, 200);

  // Ensure not empty
  if (!safe || safe === '.' || safe === '..') {
    safe = 'unnamed_file';
  }

  return safe;
}

// Generate unique filenames to prevent overwrites
function generateSafeFilename(originalFilename) {
  const safe = sanitizeFilename(originalFilename);
  const ext = path.extname(safe);
  const base = path.basename(safe, ext);
  const timestamp = Date.now();
  const random = crypto.randomBytes(4).toString('hex');

  return `${base}_${timestamp}_${random}${ext}`;
}
```

## Attachment Security

### Validate File Types

Never trust the declared content type:

```javascript theme={null}
import { fileTypeFromBuffer } from 'file-type';

const ALLOWED_TYPES = new Map([
  ['application/pdf', ['.pdf']],
  ['image/jpeg', ['.jpg', '.jpeg']],
  ['image/png', ['.png']],
  ['image/gif', ['.gif']],
  ['text/plain', ['.txt']],
  ['text/csv', ['.csv']]
]);

async function validateAttachment(attachment) {
  const response = await fetch(attachment.url);
  const buffer = Buffer.from(await response.arrayBuffer());

  // Detect actual file type from content
  const detected = await fileTypeFromBuffer(buffer);

  if (!detected) {
    // Could be text file
    if (attachment.contentType.startsWith('text/')) {
      return { valid: true, type: attachment.contentType };
    }
    return { valid: false, reason: 'Unknown file type' };
  }

  // Check if type is allowed
  if (!ALLOWED_TYPES.has(detected.mime)) {
    return { valid: false, reason: `File type not allowed: ${detected.mime}` };
  }

  // Verify declared type matches detected type
  if (detected.mime !== attachment.contentType) {
    console.warn(
      `Content-Type mismatch: declared ${attachment.contentType}, detected ${detected.mime}`
    );
    // Use detected type, not declared type
  }

  return { valid: true, type: detected.mime, buffer };
}
```

### Scan for Malware

Integrate with a virus scanner:

```javascript theme={null}
import ClamScan from 'clamscan';

const clamscan = await new ClamScan().init({
  clamdscan: {
    host: process.env.CLAMAV_HOST || 'localhost',
    port: 3310
  }
});

async function scanForMalware(buffer, filename) {
  try {
    const { isInfected, viruses } = await clamscan.scanBuffer(buffer);

    if (isInfected) {
      console.error(`Malware detected in ${filename}: ${viruses.join(', ')}`);
      return {
        safe: false,
        threats: viruses
      };
    }

    return { safe: true };
  } catch (error) {
    console.error('Virus scan failed:', error);
    // Fail closed - treat as unsafe if scanning fails
    return { safe: false, error: 'Scan failed' };
  }
}
```

### Limit File Sizes

Enforce size limits to prevent resource exhaustion:

```javascript theme={null}
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024; // 10 MB
const MAX_TOTAL_SIZE = 25 * 1024 * 1024; // 25 MB

function validateAttachmentSizes(attachments) {
  let totalSize = 0;

  for (const attachment of attachments) {
    if (attachment.size > MAX_ATTACHMENT_SIZE) {
      return {
        valid: false,
        reason: `Attachment ${attachment.filename} exceeds size limit`
      };
    }
    totalSize += attachment.size;
  }

  if (totalSize > MAX_TOTAL_SIZE) {
    return {
      valid: false,
      reason: `Total attachment size exceeds limit`
    };
  }

  return { valid: true };
}
```

## Content Security

### Prevent XSS in Displayed Emails

When displaying email content in a web interface:

```javascript theme={null}
// Use CSP headers
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; img-src 'self' https:; style-src 'self' 'unsafe-inline'"
  );
  next();
});

// Render emails in sandboxed iframe
function renderEmailHtml(sanitizedHtml) {
  return `
    <iframe
      sandbox="allow-same-origin"
      srcdoc="${escapeAttribute(sanitizedHtml)}"
      style="width: 100%; height: 500px; border: none;"
    ></iframe>
  `;
}
```

### Block Tracking Pixels

Strip tracking pixels from received emails:

```javascript theme={null}
function removeTrackingPixels(html) {
  // Remove 1x1 images (common tracking pixels)
  html = html.replace(
    /<img[^>]*(?:width|height)\s*=\s*["']?1["']?[^>]*>/gi,
    ''
  );

  // Remove images from known tracking domains
  const trackingDomains = [
    'mailtrack.io',
    'pixel.watch',
    'track.customer.io',
    'open.convertkit.com'
  ];

  for (const domain of trackingDomains) {
    const regex = new RegExp(`<img[^>]*${domain}[^>]*>`, 'gi');
    html = html.replace(regex, '');
  }

  return html;
}
```

## Rate Limiting

Protect against email flooding:

```javascript theme={null}
import { RateLimiterRedis } from 'rate-limiter-flexible';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

const senderLimiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'inbound_sender',
  points: 100, // 100 emails
  duration: 3600 // per hour
});

const domainLimiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'inbound_domain',
  points: 500,
  duration: 3600
});

const globalLimiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'inbound_global',
  points: 10000,
  duration: 3600
});

async function checkRateLimits(email) {
  const sender = email.from.toLowerCase();
  const domain = sender.split('@')[1];

  try {
    await Promise.all([
      senderLimiter.consume(sender),
      domainLimiter.consume(domain),
      globalLimiter.consume('global')
    ]);
    return { allowed: true };
  } catch (error) {
    return {
      allowed: false,
      reason: 'Rate limit exceeded',
      retryAfter: Math.ceil(error.msBeforeNext / 1000)
    };
  }
}
```

## Logging and Monitoring

### Audit Logging

Log all inbound email processing:

```javascript theme={null}
async function logInboundEmail(email, result) {
  await auditLog.create({
    type: 'inbound_email',
    emailId: email.id,
    from: email.from,
    to: email.to,
    subject: email.subject?.substring(0, 100),
    attachmentCount: email.attachments?.length || 0,
    result: result.status,
    processingTime: result.duration,
    timestamp: new Date()
  });
}
```

### Anomaly Detection

Monitor for suspicious patterns:

```javascript theme={null}
class AnomalyDetector {
  constructor() {
    this.senderHistory = new Map();
  }

  async analyze(email) {
    const sender = email.from.toLowerCase();
    const alerts = [];

    // Track sender frequency
    const history = this.senderHistory.get(sender) || { count: 0, firstSeen: Date.now() };
    history.count++;
    this.senderHistory.set(sender, history);

    // New sender sending many emails quickly
    if (history.count > 10 && Date.now() - history.firstSeen < 60000) {
      alerts.push({
        type: 'rapid_sender',
        sender,
        count: history.count,
        duration: Date.now() - history.firstSeen
      });
    }

    // Unusual attachment patterns
    if (email.attachments?.length > 5) {
      alerts.push({
        type: 'many_attachments',
        sender,
        count: email.attachments.length
      });
    }

    return alerts;
  }
}
```

## Security Checklist

<AccordionGroup>
  <Accordion title="Webhook Security">
    * [ ] Verify webhook credentials (Basic Auth or OAuth 2.0) on every request
    * [ ] Store credentials securely in environment variables
    * [ ] Use HTTPS for your webhook endpoint
    * [ ] Consider IP allowlisting as additional protection
  </Accordion>

  <Accordion title="Input Validation">
    * [ ] Validate all email addresses
    * [ ] Sanitize HTML content before storage/display
    * [ ] Sanitize filenames before use
    * [ ] Validate attachment content types
  </Accordion>

  <Accordion title="Attachment Security">
    * [ ] Verify file types from content, not headers
    * [ ] Scan attachments for malware
    * [ ] Enforce size limits
    * [ ] Store attachments outside web root
  </Accordion>

  <Accordion title="Monitoring">
    * [ ] Log all inbound email processing
    * [ ] Monitor for rate limit violations
    * [ ] Alert on anomalous patterns
    * [ ] Track spam score distributions
  </Accordion>
</AccordionGroup>

<Warning>
  Never trust any data from inbound emails. Senders can forge headers, spoof addresses, and include malicious content. Always validate and sanitize everything.
</Warning>

## Related Topics

<CardGroup cols={2}>
  <Card title="Webhook Authorization" icon="lock" href="/learn/webhooks/authorization">
    Secure webhook endpoints
  </Card>

  <Card title="Spam Filtering" icon="shield" href="/learn/inbound/spam-filtering">
    Filter spam emails
  </Card>

  <Card title="Attachments" icon="paperclip" href="/learn/inbound/attachments">
    Handle attachments safely
  </Card>

  <Card title="Best Practices" icon="star" href="/learn/inbound/best-practices">
    Overall best practices
  </Card>
</CardGroup>
