Phase 2: Implement Domain & Mailbox persistence, Analytics, and fix pagination issues
This commit is contained in:
58
app/Filament/Resources/Domains/DomainResource.php
Normal file
58
app/Filament/Resources/Domains/DomainResource.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Domains;
|
||||
|
||||
use App\Filament\Resources\Domains\Pages\CreateDomain;
|
||||
use App\Filament\Resources\Domains\Pages\EditDomain;
|
||||
use App\Filament\Resources\Domains\Pages\ListDomains;
|
||||
use App\Filament\Resources\Domains\Schemas\DomainForm;
|
||||
use App\Filament\Resources\Domains\Tables\DomainsTable;
|
||||
use App\Models\Domain;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class DomainResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Domain::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return DomainForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return DomainsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListDomains::route('/'),
|
||||
'create' => CreateDomain::route('/create'),
|
||||
'edit' => EditDomain::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRecordRouteBindingEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getRecordRouteBindingEloquentQuery()
|
||||
->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Domains/Pages/CreateDomain.php
Normal file
11
app/Filament/Resources/Domains/Pages/CreateDomain.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Domains\Pages;
|
||||
|
||||
use App\Filament\Resources\Domains\DomainResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateDomain extends CreateRecord
|
||||
{
|
||||
protected static string $resource = DomainResource::class;
|
||||
}
|
||||
23
app/Filament/Resources/Domains/Pages/EditDomain.php
Normal file
23
app/Filament/Resources/Domains/Pages/EditDomain.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Domains\Pages;
|
||||
|
||||
use App\Filament\Resources\Domains\DomainResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditDomain extends EditRecord
|
||||
{
|
||||
protected static string $resource = DomainResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Domains/Pages/ListDomains.php
Normal file
19
app/Filament/Resources/Domains/Pages/ListDomains.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Domains\Pages;
|
||||
|
||||
use App\Filament\Resources\Domains\DomainResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListDomains extends ListRecords
|
||||
{
|
||||
protected static string $resource = DomainResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Filament/Resources/Domains/Schemas/DomainForm.php
Normal file
34
app/Filament/Resources/Domains/Schemas/DomainForm.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Domains\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class DomainForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->placeholder('e.g., imail.com'),
|
||||
\Filament\Forms\Components\Select::make('allowed_types')
|
||||
->multiple()
|
||||
->options([
|
||||
'public' => 'Public',
|
||||
'private' => 'Private',
|
||||
'custom' => 'Custom',
|
||||
'premium' => 'Premium',
|
||||
])
|
||||
->required(),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
Toggle::make('is_archived')
|
||||
->default(false),
|
||||
]);
|
||||
}
|
||||
}
|
||||
60
app/Filament/Resources/Domains/Tables/DomainsTable.php
Normal file
60
app/Filament/Resources/Domains/Tables/DomainsTable.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Domains\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class DomainsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('domain_hash')
|
||||
->copyable()
|
||||
->searchable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('allowed_types')
|
||||
->badge()
|
||||
->separator(','),
|
||||
TextColumn::make('mailboxes_count')
|
||||
->counts('mailboxes')
|
||||
->label('Mailboxes')
|
||||
->sortable(),
|
||||
IconColumn::make('is_active')
|
||||
->boolean()
|
||||
->label('Active'),
|
||||
IconColumn::make('is_archived')
|
||||
->boolean()
|
||||
->label('Archived'),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
TrashedFilter::make(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
ForceDeleteBulkAction::make(),
|
||||
RestoreBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,24 @@ class ProcessIncomingEmail implements ShouldQueue
|
||||
]
|
||||
);
|
||||
|
||||
// Track analytics for receiving email
|
||||
$mailbox = \App\Models\Mailbox::where('address', $email->recipient_email)->first();
|
||||
|
||||
TrackAnalytics::dispatch(
|
||||
eventType: 'email_received',
|
||||
mailboxHash: $mailbox?->mailbox_hash ?? 'unknown',
|
||||
domainHash: $mailbox?->domain_hash ?? 'unknown',
|
||||
metadata: [
|
||||
'email_id' => $email->id,
|
||||
'sender' => $email->sender_email,
|
||||
'recipient' => $email->recipient_email,
|
||||
'attachment_count' => count($metadata['attachments'] ?? []),
|
||||
'found_mailbox' => $mailbox !== null,
|
||||
],
|
||||
ipAddress: '0.0.0.0', // Server-side event
|
||||
userAgent: 'MailOps/IncomingWorker'
|
||||
);
|
||||
|
||||
$this->ensureTtlIndex();
|
||||
|
||||
NewEmailReceived::dispatch($email);
|
||||
|
||||
48
app/Jobs/TrackAnalytics.php
Normal file
48
app/Jobs/TrackAnalytics.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class TrackAnalytics implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $eventType,
|
||||
public string $mailboxHash,
|
||||
public ?string $domainHash = null,
|
||||
public array $metadata = [],
|
||||
public ?int $userId = null,
|
||||
public string $userType = 'guest',
|
||||
public ?string $ipAddress = null,
|
||||
public ?string $userAgent = null,
|
||||
) {
|
||||
$this->onQueue('analytics');
|
||||
$this->onConnection('redis');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
\App\Models\AnalyticsEvent::create([
|
||||
'event_type' => $this->eventType,
|
||||
'mailbox_hash' => $this->mailboxHash,
|
||||
'domain_hash' => $this->domainHash,
|
||||
'user_type' => $this->userType,
|
||||
'user_id' => $this->userId,
|
||||
'ip_address' => $this->ipAddress ?? request()->ip(),
|
||||
'user_agent' => $this->userAgent ?? request()->userAgent(),
|
||||
'metadata' => $this->metadata,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,22 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Jobs\TrackAnalytics;
|
||||
use App\Models\Domain;
|
||||
use App\Models\Email;
|
||||
use App\Models\EmailBody;
|
||||
use App\Models\Mailbox as MailboxModel;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
#[Layout('components.layouts.app')]
|
||||
class Mailbox extends Component
|
||||
{
|
||||
public $activeMailboxes = [
|
||||
['id' => 1, 'address' => 'idevakk@imail.com', 'expires_at' => '23:59:12', 'progress' => 90],
|
||||
['id' => 2, 'address' => 'tester_99@devmail.ai', 'expires_at' => '45:12:05', 'progress' => 40],
|
||||
];
|
||||
use WithPagination;
|
||||
|
||||
public $currentMailboxId = 1;
|
||||
public $currentMailboxId = null;
|
||||
|
||||
public $activeFolder = 'inbox';
|
||||
|
||||
@@ -25,10 +29,6 @@ class Mailbox extends Component
|
||||
|
||||
public $allowRemoteContent = false;
|
||||
|
||||
public $page = 1;
|
||||
|
||||
public $totalPages = 5;
|
||||
|
||||
// Create State
|
||||
public $showCreateModal = false;
|
||||
|
||||
@@ -36,9 +36,38 @@ class Mailbox extends Component
|
||||
|
||||
public $customUsername = '';
|
||||
|
||||
public $customDomain = 'imail.com';
|
||||
public $customDomain = '';
|
||||
|
||||
public $availableDomains = ['imail.com', 'devmail.ai', 'temp-inbox.net'];
|
||||
public function mount()
|
||||
{
|
||||
$this->customDomain = Domain::where('is_active', true)->first()?->name ?? 'imail.app';
|
||||
|
||||
// Load current mailbox from session if exists
|
||||
$savedId = Session::get('current_mailbox_id');
|
||||
if ($savedId && $this->getActiveMailboxesProperty()->contains('id', $savedId)) {
|
||||
$this->currentMailboxId = $savedId;
|
||||
} else {
|
||||
$this->currentMailboxId = $this->getActiveMailboxesProperty()->first()?->id;
|
||||
}
|
||||
}
|
||||
|
||||
public function getActiveMailboxesProperty()
|
||||
{
|
||||
return MailboxModel::query()
|
||||
->where(function ($query) {
|
||||
$query->where('session_id', Session::getId());
|
||||
if (auth()->check()) {
|
||||
$query->orWhere('user_id', auth()->id());
|
||||
}
|
||||
})
|
||||
->where('is_blocked', false)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getAvailableDomainsProperty()
|
||||
{
|
||||
return Domain::where('is_active', true)->where('is_archived', false)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Reverb/Echo event listeners for the current mailbox domain.
|
||||
@@ -47,8 +76,8 @@ class Mailbox extends Component
|
||||
*/
|
||||
public function getListeners(): array
|
||||
{
|
||||
$currentMailbox = collect($this->activeMailboxes)->firstWhere('id', $this->currentMailboxId);
|
||||
$domain = $currentMailbox ? explode('@', $currentMailbox['address'])[1] ?? '' : '';
|
||||
$currentMailbox = $this->active_mailboxes->firstWhere('id', $this->currentMailboxId);
|
||||
$domain = $currentMailbox ? explode('@', $currentMailbox->address)[1] ?? '' : '';
|
||||
|
||||
if (empty($domain)) {
|
||||
return [];
|
||||
@@ -59,157 +88,32 @@ class Mailbox extends Component
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new email broadcast event from Reverb.
|
||||
*
|
||||
* @param array<string, mixed> $eventData The email data from the broadcast.
|
||||
*/
|
||||
public function onNewEmail(array $eventData): void
|
||||
{
|
||||
// TODO: When real data integration is complete, prepend the new email to the list.
|
||||
// For now, trigger a component refresh to pick up new data.
|
||||
// Simply refresh the list to pick up the new email from MariaDB
|
||||
// Since we order by received_at DESC, it will appear on top.
|
||||
$this->dispatch('$refresh');
|
||||
}
|
||||
|
||||
public function getEmailsProperty()
|
||||
{
|
||||
// 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.',
|
||||
'body_html' => '<p>Hi @idevakk,</p><p>A new personal access token (classic) was recently added to your account IDEVAKK.</p><p>If this was you, you can safely ignore this email.</p><p>If this was not you, please visit https://github.com/settings/tokens to revoke the token.</p>',
|
||||
'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,
|
||||
'attachments' => [],
|
||||
],
|
||||
[
|
||||
'id' => 101,
|
||||
'from_name' => 'Linear',
|
||||
'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...',
|
||||
'body_html' => '<p>Hello,</p><p>You have been assigned to <strong>[UI-124] Fix sidebar overflow in mobile view</strong>.</p><p>Priority: High</p><p>Project: Imail Revamp</p><p>View details at https://linear.app/imail/issue/UI-124</p>',
|
||||
'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' => 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' => '<p>Hello Zemailer,</p><p>We have curated some new cinematic landscapes for your next project:</p><div style="margin: 20px 0;"><img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=800&q=80" style="width: 100%; border-radius: 12px; margin-bottom: 15px;" alt="Mountain Landscape"><img src="https://images.unsplash.com/photo-1470770841072-f978cf4d019e?auto=format&fit=crop&w=800&q=80" style="width: 100%; border-radius: 12px;" alt="Lake Landscape"></div><p>Feel free to use them in your designs!</p>',
|
||||
'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' => [],
|
||||
],
|
||||
// Generated Inbox items to reach 15
|
||||
],
|
||||
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.',
|
||||
'body_html' => '<p>Hello,</p><p>Your weekly report for the period of Feb 24 - Mar 2 is now available in your dashboard.</p><p>Total Volume: $12,450.00</p><p>View the full report details online.</p>',
|
||||
'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']],
|
||||
],
|
||||
],
|
||||
3 => [ // Notifications
|
||||
[
|
||||
'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' => '<p>You have new activity in Slack.</p><ul><li><strong>#dev-chat</strong>: 8 new messages</li><li><strong>#announcements</strong>: 4 new messages</li></ul>',
|
||||
'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' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
$currentMailbox = $this->active_mailboxes->firstWhere('id', $this->currentMailboxId);
|
||||
|
||||
// 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' => "<p>Hello,</p><p>This is a follow-up email #$i regarding our partnership.</p>",
|
||||
'body_text' => "Hello,\n\nThis is a follow-up email #$i regarding our partnership.",
|
||||
'time' => 'Mar 1',
|
||||
'unread' => $i % 3 === 0,
|
||||
'flagged' => $i % 5 === 0,
|
||||
'attachments' => [],
|
||||
];
|
||||
if (! $currentMailbox) {
|
||||
return Email::query()->whereRaw('1 = 0')->paginate(10);
|
||||
}
|
||||
|
||||
// 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' => "<p>Hi team,</p><p>Update on project $i: everything is on track.</p>",
|
||||
'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' => "<p>A new login was detected on your account.</p><p>Location: City $i</p>",
|
||||
'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);
|
||||
return Email::query()
|
||||
->where('recipient_email', $currentMailbox->address)
|
||||
->when($this->search, function ($query) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('subject', 'like', "%{$this->search}%")
|
||||
->orWhere('sender_email', 'like', "%{$this->search}%")
|
||||
->orWhere('sender_name', 'like', "%{$this->search}%");
|
||||
});
|
||||
})
|
||||
->orderByDesc('received_at')
|
||||
->paginate(10);
|
||||
}
|
||||
|
||||
public function selectEmail($id)
|
||||
@@ -218,8 +122,28 @@ class Mailbox extends Component
|
||||
$this->viewMode = 'text';
|
||||
$this->allowRemoteContent = false;
|
||||
|
||||
// Simulate cinematic loading
|
||||
usleep(500000); // 500ms
|
||||
$email = Email::find($id);
|
||||
|
||||
if ($email) {
|
||||
if (! $email->is_read) {
|
||||
$email->update(['is_read' => true]);
|
||||
}
|
||||
|
||||
// Track analytics for reading email
|
||||
$currentMailbox = $this->active_mailboxes->firstWhere('id', $this->currentMailboxId);
|
||||
if ($currentMailbox) {
|
||||
TrackAnalytics::dispatch(
|
||||
eventType: 'email_read',
|
||||
mailboxHash: $currentMailbox->mailbox_hash,
|
||||
domainHash: $currentMailbox->domain_hash,
|
||||
metadata: ['email_id' => $email->id, 'subject' => $email->subject],
|
||||
userId: auth()->id(),
|
||||
userType: auth()->check() ? 'authenticated' : 'guest',
|
||||
ipAddress: request()->ip(),
|
||||
userAgent: request()->userAgent()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function switchMailbox($id)
|
||||
@@ -227,35 +151,70 @@ class Mailbox extends Component
|
||||
$this->currentMailboxId = $id;
|
||||
$this->selectedEmailId = null;
|
||||
$this->search = '';
|
||||
$this->page = 1;
|
||||
$this->resetPage();
|
||||
Session::put('current_mailbox_id', $id);
|
||||
|
||||
// Track analytics for switching mailbox
|
||||
$currentMailbox = $this->active_mailboxes->firstWhere('id', $id);
|
||||
if ($currentMailbox) {
|
||||
TrackAnalytics::dispatch(
|
||||
eventType: 'mailbox_accessed',
|
||||
mailboxHash: $currentMailbox->mailbox_hash,
|
||||
domainHash: $currentMailbox->domain_hash,
|
||||
userId: auth()->id(),
|
||||
userType: auth()->check() ? 'authenticated' : 'guest',
|
||||
ipAddress: request()->ip(),
|
||||
userAgent: request()->userAgent()
|
||||
);
|
||||
|
||||
$currentMailbox->update([
|
||||
'last_accessed_at' => now(),
|
||||
'last_accessed_ip' => request()->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function createMailbox()
|
||||
{
|
||||
$newAddress = $this->createType === 'random'
|
||||
? fake()->userName().'_'.rand(10, 99).'@'.$this->availableDomains[array_rand($this->availableDomains)]
|
||||
$domainModel = Domain::where('name', $this->customDomain)->first();
|
||||
if (! $domainModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
$address = $this->createType === 'random'
|
||||
? fake()->userName().'_'.rand(10, 99).'@'.$this->customDomain
|
||||
: $this->customUsername.'@'.$this->customDomain;
|
||||
|
||||
$newId = count($this->activeMailboxes) + 1;
|
||||
$this->activeMailboxes[] = [
|
||||
'id' => $newId,
|
||||
'address' => $newAddress,
|
||||
'expires_at' => '24:00:00',
|
||||
'progress' => 100,
|
||||
];
|
||||
$mailbox = MailboxModel::create([
|
||||
'mailbox_hash' => bin2hex(random_bytes(32)),
|
||||
'domain_hash' => $domainModel->domain_hash,
|
||||
'user_id' => auth()->id(),
|
||||
'session_id' => Session::getId(),
|
||||
'address' => $address,
|
||||
'type' => $this->createType === 'random' ? 'public' : 'custom',
|
||||
'created_ip' => request()->ip(),
|
||||
'last_accessed_ip' => request()->ip(),
|
||||
'last_accessed_at' => now(),
|
||||
'expires_at' => now()->addDays(7), // Default expiry
|
||||
]);
|
||||
|
||||
$this->currentMailboxId = $newId;
|
||||
$this->currentMailboxId = $mailbox->id;
|
||||
$this->showCreateModal = false;
|
||||
$this->customUsername = '';
|
||||
Session::put('last_mailbox_id', $mailbox->id);
|
||||
}
|
||||
|
||||
public function deleteMailbox($id)
|
||||
{
|
||||
$this->activeMailboxes = array_filter($this->activeMailboxes, fn ($m) => $m['id'] !== $id);
|
||||
$mailbox = MailboxModel::find($id);
|
||||
if ($mailbox) {
|
||||
$mailbox->delete();
|
||||
}
|
||||
|
||||
if ($this->currentMailboxId === $id) {
|
||||
$this->currentMailboxId = count($this->activeMailboxes) > 0 ? reset($this->activeMailboxes)['id'] : null;
|
||||
$this->currentMailboxId = $this->active_mailboxes->first()?->id;
|
||||
$this->selectedEmailId = null;
|
||||
session(['current_mailbox_id' => $this->currentMailboxId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,12 +262,18 @@ class Mailbox extends Component
|
||||
|
||||
public function getProcessedContent($email)
|
||||
{
|
||||
$content = $email['body_html'];
|
||||
$body = EmailBody::where('unique_id_hash', $email->unique_id_hash)->first();
|
||||
|
||||
if (! $body) {
|
||||
return 'Email body not found.';
|
||||
}
|
||||
|
||||
$content = $body->body_html ?? $body->body_text;
|
||||
$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 && ! empty($body->body_text)) {
|
||||
return trim(e($body->body_text));
|
||||
}
|
||||
|
||||
if ($isText) {
|
||||
@@ -326,7 +291,7 @@ class Mailbox extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
$currentMailbox = collect($this->activeMailboxes)->firstWhere('id', $this->currentMailboxId);
|
||||
$currentMailbox = $this->active_mailboxes->firstWhere('id', $this->currentMailboxId);
|
||||
|
||||
return view('livewire.mailbox', [
|
||||
'emails' => $this->getEmailsProperty(),
|
||||
|
||||
30
app/Models/AnalyticsEvent.php
Normal file
30
app/Models/AnalyticsEvent.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use MongoDB\Laravel\Eloquent\Model;
|
||||
|
||||
class AnalyticsEvent extends Model
|
||||
{
|
||||
protected $connection = 'mongodb';
|
||||
|
||||
protected $collection = 'analytics_events';
|
||||
|
||||
protected $fillable = [
|
||||
'event_type',
|
||||
'mailbox_hash',
|
||||
'domain_hash',
|
||||
'user_type',
|
||||
'user_id',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'metadata' => 'array',
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Models/Domain.php
Normal file
44
app/Models/Domain.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Domain extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\DomainFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'domain_hash',
|
||||
'name',
|
||||
'allowed_types',
|
||||
'is_active',
|
||||
'is_archived',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'allowed_types' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'is_archived' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (Domain $domain) {
|
||||
if (empty($domain->domain_hash)) {
|
||||
$domain->domain_hash = bin2hex(random_bytes(32));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function mailboxes()
|
||||
{
|
||||
return $this->hasMany(Mailbox::class, 'domain_hash', 'domain_hash');
|
||||
}
|
||||
}
|
||||
73
app/Models/Mailbox.php
Normal file
73
app/Models/Mailbox.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Mailbox extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\MailboxFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'mailbox_hash',
|
||||
'domain_hash',
|
||||
'user_id',
|
||||
'session_id',
|
||||
'address',
|
||||
'type',
|
||||
'created_ip',
|
||||
'last_accessed_ip',
|
||||
'last_accessed_at',
|
||||
'is_blocked',
|
||||
'block_reason',
|
||||
'blocked_at',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_blocked' => 'boolean',
|
||||
'last_accessed_at' => 'datetime',
|
||||
'blocked_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (Mailbox $mailbox) {
|
||||
if (empty($mailbox->mailbox_hash)) {
|
||||
$mailbox->mailbox_hash = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
// Record creation analytics
|
||||
\App\Jobs\TrackAnalytics::dispatch(
|
||||
eventType: 'mailbox_created',
|
||||
mailboxHash: $mailbox->mailbox_hash,
|
||||
domainHash: $mailbox->domain_hash,
|
||||
userId: $mailbox->user_id,
|
||||
userType: $mailbox->user_id ? 'authenticated' : 'guest',
|
||||
ipAddress: request()->ip(),
|
||||
userAgent: request()->userAgent()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function emails()
|
||||
{
|
||||
return $this->hasMany(Email::class, 'recipient_email', 'address');
|
||||
}
|
||||
|
||||
public function domain()
|
||||
{
|
||||
return $this->belongsTo(Domain::class, 'domain_hash', 'domain_hash');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user