6.1 KiB
MailOps Webhook Handover Document
This document provides the exact specifications needed to implement the receiving end of the MailOps email synchronization system within the Laravel application.
1. Webhook Endpoint Specification
The MailOps worker will push new emails to this exact endpoint on your Laravel server:
- URL:
POST https://your-laravel-app.com/api/webhooks/incoming_email - Headers:
Content-Type: application/jsonAuthorization: Bearer <CONFIGURED_WEBHOOK_SECRET>(You must configure this secret in both MailOps and Laravel).
A. Expected JSON Payload (With Attachments)
{
"hash": "a1b2c3d4e5f6g7h8i9j0...",
"metadata": {
"hash": "a1b2c3d4e5f6g7h8i9j0...",
"recipientEmail": "user@example.com",
"recipientName": "John Doe",
"senderEmail": "alert@service.com",
"senderName": "Service Alerts",
"domain": "example.com",
"subject": "Important Notification",
"received_at": "2026-02-26T17:35:00Z",
"attachments": [
{
"filename": "invoice.pdf",
"mimeType": "application/pdf",
"size": 102400,
"s3_path": "mail-attachments/2026/02/26/hash_invoice.pdf"
}
],
"attachmentSize": 102400
},
"bodyText": "Plain text content...",
"bodyHtml": "<html>HTML content...</html>"
}
(Note: received_at is in ISO 8601 format ending with Z to explicitly denote UTC. bodyHtml and bodyText are completely separated from the metadata to optimize database payload sizes).
B. Expected JSON Payload (NO Attachments)
When an email has no attachments, the attachments array will be empty and attachmentSize will be zero. Also, depending on the email client, bodyHtml or bodyText might be null.
{
"hash": "b2c3d4e5f6g7h8i9j0a1...",
"metadata": {
"hash": "b2c3d4e5f6g7h8i9j0a1...",
"recipientEmail": "user@example.com",
"recipientName": "",
"senderEmail": "friend@service.com",
"senderName": "Friend",
"domain": "example.com",
"subject": "Quick Question",
"received_at": "2026-02-26T17:38:12Z",
"attachments": [],
"attachmentSize": 0
},
"bodyText": "Hey, are we still fast approaching the deadline?",
"bodyHtml": null
}
2. Laravel Implementation Checklist
When you switch to the Laravel project, you need to build the following:
Step 1: Route & Middleware
Define the API route and protect it with a simple Bearer token check.
// routes/api.php
Route::post('/webhooks/incoming_email', [EmailWebhookController::class, 'handle'])
->middleware('verify.webhook.secret');
Step 2: The Controller
The controller persists the metadata to MariaDB and the heavy body to MongoDB. Crucially, it also checks if the MongoDB TTL index exists, and if not, automatically creates it using the value defined in your Laravel .env file (e.g., EMAIL_BODY_TTL_SECONDS=259200).
// app/Http/Controllers/EmailWebhookController.php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
public function handle(Request $request)
{
$payload = $request->all();
$meta = $payload['metadata'];
$hash = $payload['hash'];
// 1. Auto-Setup MongoDB TTL Index (Executes only once via Cache)
$this->ensureMongoTtlIndexExists();
// 2. MariaDB: Save Metadata
Email::updateOrCreate(
['unique_id_hash' => $hash],
[
'recipient_email' => $meta['recipientEmail'],
'sender_email' => $meta['senderEmail'],
'subject' => $meta['subject'] ?? '',
'is_read' => false,
// Parse the ISO 8601 UTC timestamp format explicitly for SQL
'received_at' => Carbon::parse($meta['received_at'])->setTimezone('UTC')->toDateTimeString(),
// Store attachments JSON. If empty, ensure it's saved as an empty array '[]'
'attachments' => !empty($meta['attachments']) ? json_encode($meta['attachments']) : '[]',
'attachment_size' => $meta['attachmentSize'] ?? 0
]
);
// 3. MongoDB: Save the heavy body with TTL
// Assuming you have the jenssegers/mongodb package installed
RecentEmailBody::updateOrCreate(
['unique_id_hash' => $hash],
[
// Handle cases where the sender only sends Text or only HTML
'body_text' => $payload['bodyText'] ?? '',
'body_html' => $payload['bodyHtml'] ?? '',
'created_at' => new \MongoDB\BSON\UTCDateTime(now()->timestamp * 1000), // BSON required for TTL
]
);
return response()->json(['status' => 'success'], 200);
}
/**
* Ensures the TTL index is created on the MongoDB collection.
* Uses Laravel Cache to avoid checking the database on every single webhook.
*/
private function ensureMongoTtlIndexExists()
{
Cache::rememberForever('mongo_ttl_index_created', function () {
// Fetch TTL from Laravel .env (Default: 72 hours / 259200 seconds)
$ttlSeconds = (int) env('EMAIL_BODY_TTL_SECONDS', 259200);
$collection = DB::connection('mongodb')->getCollection('recent_email_bodies');
// Background creation prevents locking the database during webhook execution
$collection->createIndex(
['created_at' => 1],
[
'expireAfterSeconds' => $ttlSeconds,
'background' => true,
'name' => 'ttl_created_at_index' // Named index prevents duplicate recreation errors
]
);
return true;
});
}
3. Resiliency Notes
- Idempotency: The MailOps worker might retry a webhook if a network timeout occurs even after Laravel successfully saved it. Your Laravel code MUST use
updateOrCreateorINSERT IGNORE(like the example above) so it doesn't create duplicate emails if the same payload hash is received twice. - Timeouts: The MailOps worker expects a response within 5 to 10 seconds. Do not perform long-running synchronous tasks (like connecting to external APIs or sending heavy push notifications) inside the webhook controller. Dispatch those to a Laravel Queue instead.