# Email processing

> How EmailConnect receives, processes, and delivers emails to your webhooks

Source: https://emailconnect.eu/help/email-processing/

EmailConnect transforms incoming emails into structured webhook payloads. This guide explains the processing pipeline from email receipt to webhook delivery.

## Processing pipeline

### 1. Email reception

When an email arrives at your configured domain:

1. **SMTP connection** - Sender's mail server connects to `mx1.emailconnect.eu`
2. **Spam & virus check** - Email is scanned for spam. On Business+ plans, attachments are also scanned for viruses using ClamAV
3. **Rule evaluation** - Alias rules are checked (Maker+ feature)
4. **Acceptance** - Email is accepted for processing

### 2. Parsing and extraction

EmailConnect extracts structured data from the raw email and enriches it based on your plan.

**Message parsing** (all plans):
- Sender and recipient addresses, display names
- Subject line, date, and timestamps
- HTML and plain text body
- Link extraction from both HTML and plain text
- Attachments with filenames, MIME types, and sizes (Base64 inline, or a `downloadUrl` for offloaded files)
- Email classification — `classification.type` is one of `bounce`, `auto-reply`, `mailing-list`, `read-receipt`, or `normal`. It is derived from RFC headers, not from content, so it is a fact about the message rather than a guess. `normal` means none of the automated shapes matched
- Classification `confidence` (`definite` or `likely`) and `signals` — the exact headers behind the verdict, as `{ header, value }` pairs, so you can audit the call instead of trusting it

**Content enrichment** (Maker+):
- Markdown conversion — HTML body converted to clean Markdown, useful for LLM pipelines and content extraction. Requires the `includeMarkdown` alias setting, which is off by default
- Link `anchorText` and `source` — Free plans receive `url` and `domain` only
- Integrity hashes — SHA-256 hashes of both the processed content and the raw email for audit trails and deduplication

**Spam analysis** (Maker+):
- rspamd composite score — you compare it against your own cut-off; we don't ship a verdict
- SPF, DKIM, and DMARC authentication results
- Matched rules with individual weights
- Score-based [alias rules](https://emailconnect.eu/help/alias-rules/) to accept, block, or tag mail before delivery

**Security scanning** (Business+):
- ClamAV virus scanning on all attachments
- Infected files are excluded from delivery with `excludeReason: "virus-detected"`
- Per-attachment and summary-level scan results

### 3. Webhook delivery

The structured payload is delivered to your configured endpoint:

1. **Payload construction** - JSON payload is built
2. **Signing** - Request is signed for verification
3. **Delivery** - POST request sent to your webhook URL
4. **Retry handling** - Failed deliveries are retried

## Webhook payload structure

> **Looking for the full field-by-field reference?** See [What's in your webhook payload](https://emailconnect.eu/help/webhook-payload-reference/) for every field, enrichment feature, and plan availability.

### Basic payload

```json
{
  "message": {
    "date": "2026-02-12T10:30:00.000Z",
    "sender": {
      "name": "Alice Martin",
      "email": "alice@example.com"
    },
    "content": {
      "text": "Hi there!\n\nJust wanted to follow up on our conversation.\n\nBest regards,\nAlice",
      "html": "<p>Hi there!</p><p>Just wanted to follow up on our conversation.</p><p>Best regards,<br>Alice</p>",
      "links": []
    },
    "subject": "Following up",
    "recipient": {
      "name": null,
      "email": "support@yourdomain.com",
      "tag": null
    },
    "attachments": []
  },
  "classification": {
    "type": "normal"
  }
}
```

### Full payload with all options

```json
{
  "spam": {
    "score": -0.99,
    "engine": "rspamd",
    "symbols": [
      { "name": "DMARC_POLICY_ALLOW", "weight": -0.5, "description": "example.com,none" },
      { "name": "R_DKIM_ALLOW", "weight": -0.2, "description": "example.com:s=selector1" }
    ],
    "authentication": {
      "spf": { "result": "pass" },
      "dkim": { "result": "pass" },
      "dmarc": { "result": "pass" }
    }
  },
  "message": {
    "date": "2026-02-12T10:30:00.000Z",
    "sender": {
      "name": "Alice Martin",
      "email": "alice@example.com"
    },
    "content": {
      "text": "Hi team,\n\nPlease find invoice #2026-0042 attached.\n\nBest,\nAlice",
      "html": "<p>Hi team,</p><p>Please find invoice #2026-0042 attached.</p><p>Best,<br>Alice</p>",
      "markdown": "Hi team,\n\nPlease find invoice #2026-0042 attached.\n\nBest,\nAlice",
      "links": [
        {
          "url": "https://billing.example.com/inv/2026-0042",
          "domain": "billing.example.com",
          "anchorText": "View it online",
          "source": "html"
        }
      ]
    },
    "subject": "Invoice #2026-0042 attached",
    "recipient": {
      "name": null,
      "email": "invoices+acme@yourdomain.com",
      "tag": "acme"
    },
    "attachments": [
      {
        "filename": "invoice-2026-0042.pdf",
        "contentType": "application/pdf",
        "size": 245680,
        "content": "JVBERi0xLjQK...",
        "virusScan": { "status": "clean" }
      }
    ]
  },
  "integrity": {
    "contentHash": "a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
    "rawEmailHash": "9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e"
  },
  "classification": {
    "type": "normal",
    "confidence": "definite",
    "signals": []
  },
  "security": {
    "virusScan": {
      "scanned": true,
      "engine": "clamav",
      "engineVersion": "1.5.1",
      "attachmentsScanned": 1,
      "attachmentsSkipped": 0,
      "threatsFound": 0
    }
  }
}
```

This example was addressed to `invoices+acme@yourdomain.com`, so `recipient.tag` is `"acme"` — anything after the `+` is handed to you pre-parsed, and the mail is still delivered to the webhook on your `invoices@` alias. Untagged mail sets `tag` to `null`. See the [webhook payload reference](https://emailconnect.eu/help/webhook-payload-reference/) for the full rules.

A few things worth calling out in that example:

- **Links carry `anchorText`, not `text`.** Each entry is `{ url, domain, anchorText?, source? }`. `url` and `domain` are on every plan; `anchorText` and `source` are Maker+.
- **`classification.signals` is a list of objects**, not strings — each is `{ header, value }` naming the real RFC header that produced the verdict. A `normal` message has an empty list.
- **Mailing lists are recognised by their `List-*` headers.** `List-Id` gives `mailing-list` with confidence `definite`. `List-Unsubscribe` or `List-Post`, or `Precedence: list` or `bulk`, gives `mailing-list` with confidence `likely`. With the envelope on, those three `List-*` headers are in `envelope.headers` under `list-id`, `list-unsubscribe` and `list-post`. The classification is information for your handler: EmailConnect does not filter or route on it.
- **The `envelope` block is not shown above because it is off by default.** Unless the alias enables "include envelope", the envelope is reduced to `{ messageId }`. Turn it on to receive `headers`, `allRecipients`, `returnPath`, and `processed`.

See the [webhook payload reference](https://emailconnect.eu/help/webhook-payload-reference/) for every field and its plan.

## Delivery and retries

### Successful delivery

A delivery is considered successful when your endpoint returns:
- HTTP 200-299 status code
- Response within timeout period (30 seconds)

### Retry policy

If delivery fails, EmailConnect retries on a fixed schedule, up to 7 attempts in total. Each delay is counted from the previous attempt:

| Attempt | Delay after the previous attempt |
|---------|-------|
| 1 | Immediate |
| 2 | ~1 minute |
| 3 | ~5 minutes |
| 4 | ~30 minutes |
| 5 | ~2 hours |
| 6 | ~12 hours |
| 7 | ~7 hours |

Actual delays vary by ±10% from the listed values to prevent thundering-herd effects. The seventh attempt is made about 21.6 hours after the email was received, and at most about 23.8 hours after, so all seven fit inside a 24-hour retention window.

**Retries are capped by your data retention policy.** If the next retry would fall after your retention window expires, the email is marked as failed instead. For example, Free and Maker plans (1-hour retention) get up to 4 attempts — attempt 5 would be at +2h 36m, past the retention window. Business and Platform plans get all 7 attempts on their default 24-hour retention, or on any longer one.

After 7 failed attempts or retention expiry (whichever comes first), the email is marked as failed and no further retries occur.

### Failure reasons

Common delivery failures:
- Endpoint not reachable (network error)
- Endpoint returns 4xx/5xx error
- Timeout (response takes > 30 seconds)
- SSL/TLS certificate errors
- Payload too large for endpoint

## Webhook security

### Request signing

EmailConnect signs webhook requests following the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) convention. Each request includes three headers:

```
webhook-id: msg_2KWPBgLl...
webhook-timestamp: 1716300000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
```

The signed content is `{webhook-id}.{webhook-timestamp}.{body}`, signed with HMAC-SHA256 using your base64-decoded secret.

### Node.js
```javascript
const crypto = require('crypto');

function verifyWebhook(headers, rawBody, secret) {
  const signedContent = `${headers['webhook-id']}.${headers['webhook-timestamp']}.${rawBody}`;
  const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64');

  return headers['webhook-signature'].split(' ').some(sig => {
    const [v, val] = sig.split(',');
    return v === 'v1' && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(val));
  });
}
```

### Python
```python
import hmac, hashlib, base64

def verify_webhook(headers, raw_body, secret):
    signed_content = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.{raw_body}"
    secret_bytes = base64.b64decode(secret.removeprefix('whsec_'))
    expected = base64.b64encode(
        hmac.new(secret_bytes, signed_content.encode(), hashlib.sha256).digest()
    ).decode()

    return any(
        v == 'v1' and hmac.compare_digest(expected, val)
        for sig in headers['webhook-signature'].split(' ')
        for v, _, val in [sig.partition(',')]
    )
```

For full verification examples including timestamp checks, see the [webhook signing guide](https://emailconnect.eu/help/webhook-signing/).

## Processing order

Emails are processed in order of receipt. However, webhook delivery may complete out of order due to:
- Retry delays for failed deliveries
- Processing time variations
- Network latency differences

If order matters for your application, sort on a timestamp from the payload. There is no top-level `timestamp` field — the two you can use are:

- **`message.date`** — when the sender's mail client says the email was sent. Present on every payload. Sender-supplied, so it can be skewed or absent.
- **`envelope.processed.timestamp`** — when EmailConnect processed the message. This is our clock, so it is the reliable one for ordering — but it only reaches you if the alias has **"include envelope"** enabled, which is **off by default**. With the envelope off, the block is reduced to `{ messageId }`.

For deduplication rather than ordering, use the `webhook-id` header, or the top-level `id` in the body, which holds the same value. It is the same on every retry and replay of an email, and it is sent on every delivery, signed or not. See [Deduplicating deliveries](https://emailconnect.eu/help/webhook-signing/#deduplicating-deliveries). `envelope.messageId` is the sender's own Message-ID: useful for threading, but the sender chooses it, so it can repeat or be missing.

## Rate limits

API requests are rate-limited; see [API keys & integrations](https://emailconnect.eu/help/api-keys-integrations/#rate-limits).

Incoming mail is limited to 200 messages a minute per domain. Mail above that is refused, not queued, and the sender gets a bounce.

## Monitoring and debugging

### Email logs

View processed emails in your dashboard:
- Delivery status (success/failed/pending)
- Response codes from your endpoint
- Retry history
- Full payload preview

### Webhook testing

EmailConnect integrates directly with [WebhookTest.eu](https://webhooktest.eu) — click "Use WebhookTest" when creating an alias and you can inspect the full payload without leaving your dashboard. No external tools or tab-switching needed.

See [Testing webhooks with WebhookTest](https://emailconnect.eu/help/webhooktest-integration/) for detailed setup instructions.

## Related topics

- [What's in your webhook payload](https://emailconnect.eu/help/webhook-payload-reference/) - Complete field reference with enrichment features by plan
- [Webhook configuration](https://emailconnect.eu/help/webhook-configuration/) - Set up webhook endpoints
- [Custom webhook headers](https://emailconnect.eu/help/custom-webhook-headers/) - Add authentication headers
- [Alias options](https://emailconnect.eu/help/alias-options-explained/) - Configure processing options
- [Attachment processing](https://emailconnect.eu/help/attachment-processing/) - Handle file attachments
