Receive & synchronize mail
Build an SES inbox integration that catches up after interruptions without losing changes.
Enable receiving deliberately#
Receiving is optional. Follow Domains & DNS before routing a domain to SES. Keep existing root MX records when another provider handles mail. A sending-only domain will not receive messages through Mailat.
Start with GET /inbox/received, not the legacy /inbox or JMAP thread API. Use email:read. The reference lists folder, search, date, identity, domain, attachment, read/star, label and sorting filters.
curl "$MAILAT_BASE_URL/api/v1/inbox/received?folder=inbox&page=1&pageSize=25" \
--header "Authorization: Bearer $MAILAT_TOKEN"
Traverse returned pages. Use data.totalPages and the actual pagination fields rather than inferring that every collection returns the same envelope. Detail GETs do not mark messages read.
Authenticated aggregate DMARC reports are automatically classified separately by default. Use folder=dmarc-reports for that folder or folder=all for all non-trashed mail. Fetch folder=trash separately if your mirror also needs trashed messages.
Reliable initial synchronization#
- Read
/api/v1/inbox/changes?cursor=nowand persist the high-water cursor. - Traverse the initial snapshot using
folder=all, plusfolder=trashif mirroring trash. The inbox-only example above is not a complete mailbox snapshot. - Fetch
/api/v1/inbox/changes?cursor=<saved-cursor>&limit=100untilhasMoreis false. - Fetch current message state for each change. A deleted message has a tombstone and its detail may return 404.
- Persist
nextCursoronly after processing the page successfully.
Deduplicate by cursor and message UUID: overlap with the initial snapshot and repeated pages is expected. Cursors are opaque decimal strings scoped to the user and ordered by committed changes. Never convert them to a JavaScript number or compare them as timestamps.
Recover after downtime#
The change feed retains 90 days of history. A 410 means your cursor is too old: repeat the initial synchronization procedure. Use signed webhooks for prompt notification and the change feed for durable catch-up.
Headless live notifications are available at /api/v1/sse/connect with a bearer key carrying email:read. A stream is a notification channel, not a replacement for durable change synchronization.
Python read example#
import json, os, urllib.request
url = os.environ['MAILAT_BASE_URL'] + '/api/v1/inbox/received?folder=inbox&page=1&pageSize=25'
request = urllib.request.Request(url, headers={
'Authorization': 'Bearer ' + os.environ['MAILAT_TOKEN'],
'Accept': 'application/json',
})
with urllib.request.urlopen(request, timeout=20) as response:
page = json.load(response)['data']
for email in page['emails']:
print(email['uuid'])