From 22e2b2457a91b40242dfd6c9e7cc3ef2a65785ce Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:31:32 +0530 Subject: [PATCH] feat: enhance privacy email viewer, dynamic pagination, and cinematic loading states --- app/Livewire/Mailbox.php | 194 +++++++++++++-------- laravel_webhook_handover.md | 162 +++++++++++++++++ resources/views/livewire/mailbox.blade.php | 116 ++++++++++-- 3 files changed, 387 insertions(+), 85 deletions(-) create mode 100644 laravel_webhook_handover.md diff --git a/app/Livewire/Mailbox.php b/app/Livewire/Mailbox.php index 9a02873..044d6f6 100644 --- a/app/Livewire/Mailbox.php +++ b/app/Livewire/Mailbox.php @@ -21,6 +21,10 @@ class Mailbox extends Component public $search = ''; + public $viewMode = 'text'; // text | html + + public $allowRemoteContent = false; + public $page = 1; public $totalPages = 5; @@ -38,16 +42,17 @@ class Mailbox extends Component public function getEmailsProperty() { - // Mock emails based on mailbox ID for demonstration - $emails = [ - 1 => [ + // Mock emails based on mailbox ID + $baseEmails = [ + 1 => [ // Inbox [ 'id' => 1, 'from_name' => 'GitHub Security', 'from_email' => 'noreply@github.com', 'subject' => '[GitHub] A new personal access token was created', 'preview' => 'A new personal access token (classic) was recently added to your account.', - 'content' => '

Hi @idevakk,

A new personal access token (classic) was recently added to your account IDEVAKK.

If this was you, you can safely ignore this email.

If this was not you, please visit https://github.com/settings/tokens to revoke the token.

', + 'body_html' => '

Hi @idevakk,

A new personal access token (classic) was recently added to your account IDEVAKK.

If this was you, you can safely ignore this email.

If this was not you, please visit https://github.com/settings/tokens to revoke the token.

', + 'body_text' => "Hi @idevakk,\n\nA new personal access token (classic) was recently added to your account IDEVAKK.\n\nIf this was you, you can safely ignore this email.\n\nIf this was not you, please visit https://github.com/settings/tokens to revoke the token.", 'time' => '10:24 AM', 'unread' => true, 'flagged' => true, @@ -59,112 +64,131 @@ class Mailbox extends Component 'from_email' => 'updates@linear.app', 'subject' => 'New issue assigned: [UI-124] Fix sidebar overflow', 'preview' => 'You have been assigned to a new issue in the UI project. Please review the details...', - 'content' => '

Hello,

You have been assigned to [UI-124] Fix sidebar overflow in mobile view.

Priority: High

Project: Imail Revamp

View details at https://linear.app/imail/issue/UI-124

', + 'body_html' => '

Hello,

You have been assigned to [UI-124] Fix sidebar overflow in mobile view.

Priority: High

Project: Imail Revamp

View details at https://linear.app/imail/issue/UI-124

', + 'body_text' => "Hello,\n\nYou have been assigned to [UI-124] Fix sidebar overflow in mobile view.\n\nPriority: High\nProject: Imail Revamp\n\nView details at https://linear.app/imail/issue/UI-124", 'time' => '11:45 AM', 'unread' => true, 'flagged' => false, 'attachments' => [], ], [ - 'id' => 102, - 'from_name' => 'Canva', - 'from_email' => 'design@canva.com', - 'subject' => 'Your design "Imail Presentation" is ready', - 'preview' => 'Collaborate with your team on your latest design for the Imail product launch.', - 'content' => '

Hey there!

Your team is waiting for your feedback on the Imail Presentation design.

Check the latest comments and approve the final version.

', - 'time' => '9:12 AM', - 'unread' => false, + 'id' => 104, + 'from_name' => 'Unsplash Updates', + 'from_email' => 'hello@unsplash.com', + 'subject' => 'Featured Photos: Cinematic Landscapes', + 'preview' => 'Check out this week\'s curated collection of cinematic landscape photography...', + 'body_html' => '

Hello Zemailer,

We have curated some new cinematic landscapes for your next project:

Mountain LandscapeLake Landscape

Feel free to use them in your designs!

', + 'body_text' => "Hello Zemailer,\n\nWe have curated some new cinematic landscapes for your next project.\n\n[Images are blocked by default in privacy mode]\n\nCheck them out on Unsplash!", + 'time' => '7:45 AM', + 'unread' => true, 'flagged' => false, - 'attachments' => [ - ['name' => 'presentation_v1.pdf', 'size' => '4.2 MB'], - ], - ], - [ - 'id' => 103, - 'from_name' => 'Figma', - 'from_email' => 'notifications@figma.com', - 'subject' => 'Atul Kumar mentioned you in "Mobile App (Draft)"', - 'preview' => '"@idevakk take a look at the revised QR modal design, let me know if..."', - 'content' => '

Atul Kumar mentioned you in a comment on Mobile App (Draft):

"@idevakk take a look at the revised QR modal design, let me know if the proportions look right to you."

Reply in Figma or view the comment online.

', - 'time' => '8:30 AM', - 'unread' => false, - 'flagged' => true, 'attachments' => [], ], + // Generated Inbox items to reach 15 ], - 2 => [ + 2 => [ // Sent [ 'id' => 2, 'from_name' => 'Stripe', 'from_email' => 'support@stripe.com', 'subject' => 'Your weekly payment report', 'preview' => 'Your weekly report for the period of Feb 24 - Mar 2 is now available.', - 'content' => '

Hello,

Your weekly report for the period of Feb 24 - Mar 2 is now available in your dashboard.

Total Volume: $12,450.00

View the full report details online.

', + 'body_html' => '

Hello,

Your weekly report for the period of Feb 24 - Mar 2 is now available in your dashboard.

Total Volume: $12,450.00

View the full report details online.

', + 'body_text' => "Hello,\n\nYour weekly report for the period of Feb 24 - Mar 2 is now available in your dashboard.\n\nTotal Volume: $12,450.00\n\nView the full report details online.", 'time' => 'Yesterday', 'unread' => false, 'flagged' => false, - 'attachments' => [ - ['name' => 'report_mar_02.pdf', 'size' => '1.2 MB'], - ], + 'attachments' => [['name' => 'report_mar_02.pdf', 'size' => '1.2 MB']], ], + ], + 3 => [ // Notifications [ - 'id' => 201, - 'from_name' => 'Postmark', - 'from_email' => 'alerts@postmarkapp.com', - 'subject' => 'Outbound volume spike detected', - 'preview' => 'We noticed a sudden increase in outbound emails from your "Production" server.', - 'content' => '

Alert: Outbound volume spike.

Server: Production

We detected 5,000+ emails sent in the last hour. Please ensure this is expected activity.

', - 'time' => 'Yesterday', + 'id' => 3, + 'from_name' => 'Slack', + 'from_email' => 'notifications@slack.com', + 'subject' => 'You have 12 unread messages from your team', + 'preview' => 'Atul Kumar: "Did you check the new API endpoints? We need them for..."', + 'body_html' => '

You have new activity in Slack.

', + 'body_text' => "You have new activity in Slack.\n\n#dev-chat: 8 new messages\n#announcements: 4 new messages", + 'time' => 'Mar 1', 'unread' => true, 'flagged' => false, 'attachments' => [], ], - [ - 'id' => 202, - 'from_name' => 'Vercel', - 'from_email' => 'deployments@vercel.com', - 'subject' => 'Team "idevakk" deployment successful', - 'preview' => 'Production deployment for the imail-frontend project has completed.', - 'content' => '

Your deployment is live!

Project: imail-frontend

Command: npm run build

View your deployment here: https://imail.app

', - 'time' => 'Mar 2', - 'unread' => false, - 'flagged' => false, - 'attachments' => [], - ], ], ]; - return $emails[$this->currentMailboxId] ?? [ - [ - 'id' => 3, - 'from_name' => 'Slack', - 'from_email' => 'notifications@slack.com', - 'subject' => 'You have 12 unread messages from your team', - 'preview' => 'Atul Kumar: "Did you check the new API endpoints? We need them for..."', - 'content' => '

You have new activity in Slack.

', + // Fill Inbox (MB 1) to 15 + for ($i = 5; $i <= 18; $i++) { + $baseEmails[1][] = [ + 'id' => 1000 + $i, + 'from_name' => "Partner $i", + 'from_email' => "partner-$i@example.com", + 'subject' => "Follow-up proposal #$i", + 'preview' => "I wanted to check in regarding our previous discussion on project $i...", + 'body_html' => "

Hello,

This is a follow-up email #$i regarding our partnership.

", + 'body_text' => "Hello,\n\nThis is a follow-up email #$i regarding our partnership.", 'time' => 'Mar 1', - 'unread' => true, - 'flagged' => false, + 'unread' => $i % 3 === 0, + 'flagged' => $i % 5 === 0, 'attachments' => [], - ], - [ - 'id' => 301, - 'from_name' => 'Zoom', - 'from_email' => 'no-reply@zoom.us', - 'subject' => 'Meeting Reminder: "Sprint Planning"', - 'preview' => 'Your Sprint Planning meeting is scheduled to start in 15 minutes.', - 'content' => '

Friendly reminder that your Sprint Planning call starts soon.

Link: https://zoom.us/j/123456789

', - 'time' => 'Feb 28', + ]; + } + + // Fill Sent (MB 2) to 15 + for ($i = 1; $i <= 14; $i++) { + $baseEmails[2][] = [ + 'id' => 2000 + $i, + 'from_name' => 'Me', + 'from_email' => 'idevakk@imail.com', + 'subject' => "Re: Project Sync $i", + 'preview' => "Sounds good, let's proceed with the plan we discussed for sprint $i.", + 'body_html' => "

Hi team,

Update on project $i: everything is on track.

", + 'body_text' => "Hi team,\n\nUpdate on project $i: everything is on track.", + 'time' => 'Feb 26', 'unread' => false, + 'flagged' => $i % 4 === 0, + 'attachments' => [], + ]; + } + + // Fill Others (MB 3) to 15 + for ($i = 1; $i <= 14; $i++) { + $baseEmails[3][] = [ + 'id' => 3000 + $i, + 'from_name' => "System Notification $i", + 'from_email' => 'noreply@system.com', + 'subject' => "Security Alert $i: New Login", + 'preview' => "We detected a new login to your account from a new device on day $i...", + 'body_html' => "

A new login was detected on your account.

Location: City $i

", + 'body_text' => "A new login was detected on your account.\n\nLocation: City $i", + 'time' => 'Feb 25', + 'unread' => $i % 2 === 0, 'flagged' => false, 'attachments' => [], - ], - ]; + ]; + } + + $allData = $baseEmails[$this->currentMailboxId] ?? $baseEmails[3]; + $total = count($allData); + $this->totalPages = ceil($total / 10); + + // Ensure page is within bounds + if ($this->page > $this->totalPages && $this->totalPages > 0) { + $this->page = $this->totalPages; + } + + return array_slice($allData, ($this->page - 1) * 10, 10); } public function selectEmail($id) { $this->selectedEmailId = $id; + $this->viewMode = 'text'; + $this->allowRemoteContent = false; + + // Simulate cinematic loading + usleep(500000); // 500ms } public function switchMailbox($id) @@ -172,6 +196,7 @@ class Mailbox extends Component $this->currentMailboxId = $id; $this->selectedEmailId = null; $this->search = ''; + $this->page = 1; } public function createMailbox() @@ -226,6 +251,7 @@ class Mailbox extends Component { if ($this->page < $this->totalPages) { $this->page++; + $this->selectedEmailId = null; } } @@ -233,6 +259,7 @@ class Mailbox extends Component { if ($this->page > 1) { $this->page--; + $this->selectedEmailId = null; } } @@ -243,6 +270,29 @@ class Mailbox extends Component $this->dispatch('qrCodeGenerated', address: $address); } + public function getProcessedContent($email) + { + $content = $email['body_html']; + $isText = $this->viewMode === 'text'; + + // Fallback to HTML if text is selected but body_text is empty + if ($isText && ! empty($email['body_text'])) { + return trim(e($email['body_text'])); + } + + if ($isText) { + // If fallback occurred, we sanitize the HTML to text + return trim(strip_tags($content)); + } + + if (! $this->allowRemoteContent) { + // Block remote assets by replacing src with data-src for img tags + return preg_replace('/]*?\bsrc\s*=\s*([\'"])(.*?)\1/i', 'activeMailboxes)->firstWhere('id', $this->currentMailboxId); diff --git a/laravel_webhook_handover.md b/laravel_webhook_handover.md new file mode 100644 index 0000000..daad4f1 --- /dev/null +++ b/laravel_webhook_handover.md @@ -0,0 +1,162 @@ +# 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/json` + * `Authorization: Bearer ` (You must configure this secret in both MailOps and Laravel). + +### A. Expected JSON Payload (With Attachments) + +```json +{ + "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 content..." +} +``` +*(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`. + +```json +{ + "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. +```php +// 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`). + +```php +// 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 `updateOrCreate` or `INSERT 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. diff --git a/resources/views/livewire/mailbox.blade.php b/resources/views/livewire/mailbox.blade.php index b11fefc..4af1009 100644 --- a/resources/views/livewire/mailbox.blade.php +++ b/resources/views/livewire/mailbox.blade.php @@ -46,9 +46,9 @@ 'xl:w-20': !sidebarOpen && window.innerWidth >= 1280 }">
-
+ -
+
-
+
@foreach($emails as $email) -
- -
@@ -257,6 +257,7 @@
-
+ +
+
+
+
+
+
+
+ + + +
+
+
+ Establishing Secure Channel +
+
+
+
+
+
+
+
+ @php $currentEmail = $selectedEmailId ? collect($emails)->firstWhere('id', $selectedEmailId) : null; @endphp @if($currentEmail) @@ -362,11 +388,26 @@
+ + +
+ + +
-
-
+
+
+
@@ -386,9 +427,57 @@ {{ $currentEmail['subject'] }} -
- {!! $currentEmail['content'] !!} -
+ @if($viewMode === 'html') +
+
+ @if(!$allowRemoteContent) +
+ +
+
+
Remote assets blocked
+

For your privacy, Zemail has disabled automatic loading of remote images.

+
+ @else +
+ +
+
+
Remote content enabled
+

Images and remote resources are currently active for this email.

+
+ @endif +
+ + @if(!$allowRemoteContent) + + @else + + @endif +
+ @endif + + @if($viewMode === 'text' && empty($currentEmail['body_text'])) +
+
+ +
+
+
Plain text version missing
+

Viewing sanitized HTML version instead.

+
+
+ @endif + +
{!! $this->getProcessedContent($currentEmail) !!}
@if(count($currentEmail['attachments']) > 0)
@@ -430,10 +519,11 @@
@endif
+
@else -
+