missive 0.6.2

Compose, deliver, preview, and test emails in Rust - pluggable providers with zero configuration code
Documentation
# Interceptors

Interceptors allow you to modify or block emails before they are sent to a provider.

## Overview

An interceptor sits between your code and the mailer, transforming every email that passes through:

```
Email → Interceptor → Mailer → Provider
```

## Use Cases

### Redirect emails in development

Prevent accidentally emailing real users during development:

```rust
use missive::{Address, Email, InterceptorExt};
use missive::providers::ResendMailer;

let mailer = ResendMailer::new(api_key)
    .with_interceptor(|email: Email| {
        Ok(email
            .put_to(vec![Address::new("[email protected]")])
            .put_cc(vec![])
            .put_bcc(vec![]))
    });

// All emails now go to the test address
mailer.deliver(&email).await?;
```

### Add tracking headers

Inject correlation IDs or debug info into every email:

```rust
let mailer = ResendMailer::new(api_key)
    .with_interceptor(|email: Email| {
        Ok(email.header("X-Request-ID", get_request_id()))
    });
```

### Block emails to certain domains

Prevent sending to competitors or restricted addresses:

```rust
use missive::MailError;

let mailer = ResendMailer::new(api_key)
    .with_interceptor(|email: Email| {
        for recipient in &email.to {
            if recipient.email.ends_with("@competitor.com") {
                return Err(MailError::SendError(
                    "Cannot send to competitor.com".into()
                ));
            }
        }
        Ok(email)
    });
```

### Multi-tenant branding

Automatically add tenant-specific footers:

```rust
struct TenantBranding {
    tenant_id: String,
    footer_html: String,
}

impl Interceptor for TenantBranding {
    fn intercept(&self, email: Email) -> Result<Email, MailError> {
        let html = email.html_body
            .as_ref()
            .map(|h| format!("{}\n{}", h, self.footer_html));
        
        Ok(email
            .header("X-Tenant-ID", &self.tenant_id)
            .html_body(html.unwrap_or_default()))
    }
}

let mailer = ResendMailer::new(api_key)
    .with_interceptor(TenantBranding {
        tenant_id: "acme".into(),
        footer_html: "<p>Sent by Acme Corp</p>".into(),
    });
```

## API Reference

### `Interceptor` trait

```rust
pub trait Interceptor: Send + Sync {
    /// Transform an email before delivery.
    ///
    /// Return `Ok(email)` to continue with the (possibly modified) email.
    /// Return `Err(...)` to block the email from being sent.
    fn intercept(&self, email: Email) -> Result<Email, MailError>;
}
```

### `InterceptorExt::with_interceptor`

```rust
pub trait InterceptorExt: Mailer + Sized {
    /// Wrap this mailer with an interceptor.
    fn with_interceptor<I: Interceptor>(self, interceptor: I) -> WithInterceptor<Self, I>;
}
```

### Closure support

Any closure matching `Fn(Email) -> Result<Email, MailError>` automatically implements `Interceptor`:

```rust
mailer.with_interceptor(|email| Ok(email.header("X-Foo", "bar")))
```

## Chaining Interceptors

Multiple interceptors can be chained:

```rust
let mailer = ResendMailer::new(api_key)
    .with_interceptor(AddTrackingHeaders)
    .with_interceptor(ValidateRecipients)
    .with_interceptor(AddTenantFooter);
```

All interceptors run before delivery. The email passes through each one.

## Best Practices

### Keep interceptors independent

Each interceptor should do one thing and not depend on other interceptors. If you find yourself writing an interceptor that checks for a header added by another interceptor, consolidate them into one:

```rust
// Bad: coupled interceptors
.with_interceptor(|e| Ok(e.header("X-Priority", "high")))
.with_interceptor(|e| {
    // Depends on previous interceptor's header
    if e.headers.get("X-Priority") == Some(&"high".into()) {
        Ok(e.header("X-Route", "fast"))
    } else {
        Ok(e)
    }
})

// Good: single interceptor with related logic
.with_interceptor(|e| {
    Ok(e.header("X-Priority", "high")
       .header("X-Route", "fast"))
})
```

### Don't rely on execution order

Interceptors are independent transformations. The library does not guarantee a specific execution order. If your logic requires ordering, it should be in a single interceptor:

```rust
// Bad: order-dependent logic split across interceptors
.with_interceptor(redirect_to_test)  // Must run before block check?
.with_interceptor(block_production)  // Confusing interaction

// Good: clear, self-contained logic
.with_interceptor(|email| {
    if is_development() {
        Ok(email.put_to(vec![test_address()]))
    } else if is_blocked_domain(&email) {
        Err(MailError::SendError("Blocked".into()))
    } else {
        Ok(email)
    }
})
```

### One concern per interceptor

Good interceptors are focused and reusable:

- `AddRequestId` - adds X-Request-ID header
- `BlockCompetitors` - prevents sending to competitor domains  
- `DevRedirect` - redirects all emails in development
- `TenantBranding` - adds tenant-specific headers/footers

Each handles one cross-cutting concern independently.

## Interaction with Observers

Interceptors run **before** delivery. Observers run **after** delivery.

```
Email
  → Interceptor (can modify/block)
    → Mailer.deliver()
      → Observer (can only observe result)
```

If an interceptor returns `Err(...)`, the email is not sent and observers are not called.

## When NOT to use Interceptors

For simple cases, just modify the email before calling `deliver()`:

```rust
// This is fine for one-off modifications
let email = email.header("X-Campaign", "welcome");
deliver(&email).await?;
```

Use interceptors when:
- You want the behavior to apply to **all** emails through a mailer
- You're building reusable components
- You have multiple call sites and want consistent behavior