- Added Spatie roles (free, pro, enterprise, admin) and access scopes - Implemented delayed, cinematic mailbox provisioning animation - Fixed GSAP and SVG collision issues on creation overlay - Improved component sync with livewire refresh - Added feature tests for tier systems and fixed RegistrationTest
396 lines
12 KiB
PHP
396 lines
12 KiB
PHP
<?php
|
|
|
|
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
|
|
{
|
|
use WithPagination;
|
|
|
|
public $currentMailboxId = null;
|
|
|
|
public $activeFolder = 'inbox';
|
|
|
|
public $selectedEmailId = null;
|
|
|
|
public $search = '';
|
|
|
|
public $viewMode = 'text'; // text | html
|
|
|
|
public $allowRemoteContent = false;
|
|
|
|
// Create State
|
|
public $showCreateModal = false;
|
|
|
|
public $createType = 'random'; // random | custom
|
|
|
|
public $customUsername = '';
|
|
|
|
public $customDomain = '';
|
|
|
|
public bool $isCreatingFirstMailbox = false;
|
|
|
|
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;
|
|
}
|
|
|
|
// Auto-create a random mailbox if none exist (first-time visitor)
|
|
if ($this->getActiveMailboxesProperty()->isEmpty()) {
|
|
$this->isCreatingFirstMailbox = true;
|
|
}
|
|
}
|
|
|
|
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::query()
|
|
->where('is_active', true)
|
|
->where('is_archived', false)
|
|
->accessibleBy(auth()->user())
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Get Reverb/Echo event listeners for the current mailbox domain.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public function getListeners(): array
|
|
{
|
|
$currentMailbox = $this->active_mailboxes->firstWhere('id', $this->currentMailboxId);
|
|
$domain = $currentMailbox ? explode('@', $currentMailbox->address)[1] ?? '' : '';
|
|
|
|
if (empty($domain)) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
"echo:mailbox.{$domain},.new.email" => 'onNewEmail',
|
|
];
|
|
}
|
|
|
|
public function onNewEmail(array $eventData): void
|
|
{
|
|
// 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()
|
|
{
|
|
$currentMailbox = $this->active_mailboxes->firstWhere('id', $this->currentMailboxId);
|
|
|
|
if (! $currentMailbox) {
|
|
return Email::query()->whereRaw('1 = 0')->paginate(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 getUnreadCountProperty(): int
|
|
{
|
|
$currentMailbox = $this->active_mailboxes->firstWhere('id', $this->currentMailboxId);
|
|
|
|
if (! $currentMailbox) {
|
|
return 0;
|
|
}
|
|
|
|
return Email::where('recipient_email', $currentMailbox->address)
|
|
->where('is_read', false)
|
|
->count();
|
|
}
|
|
|
|
public function selectEmail($id)
|
|
{
|
|
$this->selectedEmailId = $id;
|
|
$this->viewMode = 'text';
|
|
$this->allowRemoteContent = false;
|
|
|
|
$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)
|
|
{
|
|
$this->currentMailboxId = $id;
|
|
$this->selectedEmailId = null;
|
|
$this->search = '';
|
|
$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()
|
|
{
|
|
$domainModel = Domain::where('name', $this->customDomain)
|
|
->where('is_active', true)
|
|
->accessibleBy(auth()->user())
|
|
->first();
|
|
|
|
if (! $domainModel) {
|
|
return;
|
|
}
|
|
|
|
$address = $this->createType === 'random'
|
|
? fake()->userName().'_'.rand(10, 99).'@'.$this->customDomain
|
|
: $this->customUsername.'@'.$this->customDomain;
|
|
|
|
$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 = $mailbox->id;
|
|
$this->showCreateModal = false;
|
|
$this->customUsername = '';
|
|
Session::put('last_mailbox_id', $mailbox->id);
|
|
}
|
|
|
|
/**
|
|
* Auto-create a random mailbox on a public domain.
|
|
* Called when no mailboxes exist (first visit, or all deleted).
|
|
*/
|
|
public function autoCreateRandomMailbox(): void
|
|
{
|
|
$domain = Domain::query()
|
|
->where('is_active', true)
|
|
->where('is_archived', false)
|
|
->accessibleBy(auth()->user())
|
|
->whereJsonContains('allowed_types', 'public')
|
|
->inRandomOrder()
|
|
->first();
|
|
|
|
if (! $domain) {
|
|
return;
|
|
}
|
|
|
|
$address = fake()->userName().'_'.rand(10, 99).'@'.$domain->name;
|
|
|
|
$mailbox = MailboxModel::create([
|
|
'mailbox_hash' => bin2hex(random_bytes(32)),
|
|
'domain_hash' => $domain->domain_hash,
|
|
'user_id' => auth()->id(),
|
|
'session_id' => Session::getId(),
|
|
'address' => $address,
|
|
'type' => 'public',
|
|
'created_ip' => request()->ip(),
|
|
'last_accessed_ip' => request()->ip(),
|
|
'last_accessed_at' => now(),
|
|
'expires_at' => now()->addDays(7),
|
|
]);
|
|
|
|
$this->currentMailboxId = $mailbox->id;
|
|
$this->isCreatingFirstMailbox = false;
|
|
Session::put('current_mailbox_id', $mailbox->id);
|
|
}
|
|
|
|
public function finishAutoCreation(): void
|
|
{
|
|
if ($this->getActiveMailboxesProperty()->isEmpty()) {
|
|
$this->autoCreateRandomMailbox();
|
|
}
|
|
$this->isCreatingFirstMailbox = false;
|
|
|
|
// Ensure the component re-renders fully with the new data
|
|
$this->dispatch('$refresh');
|
|
}
|
|
|
|
public function deleteMailbox($id)
|
|
{
|
|
$mailbox = MailboxModel::find($id);
|
|
if ($mailbox) {
|
|
$mailbox->delete();
|
|
}
|
|
|
|
if ($this->currentMailboxId === $id) {
|
|
$this->currentMailboxId = $this->active_mailboxes->first()?->id;
|
|
$this->selectedEmailId = null;
|
|
session(['current_mailbox_id' => $this->currentMailboxId]);
|
|
}
|
|
|
|
// If all mailboxes deleted, auto-create a new random one
|
|
// Use a direct query or re-fetch the property to avoid cache issues
|
|
$hasActive = MailboxModel::query()
|
|
->where(function ($query) {
|
|
$query->where('session_id', Session::getId());
|
|
if (auth()->check()) {
|
|
$query->orWhere('user_id', auth()->id());
|
|
}
|
|
})
|
|
->where('is_blocked', false)
|
|
->exists();
|
|
|
|
if (! $hasActive) {
|
|
$this->isCreatingFirstMailbox = true;
|
|
}
|
|
}
|
|
|
|
public function downloadEmail($id)
|
|
{
|
|
// Mock download logic
|
|
$this->js("alert('Downloading email #{$id}... (Mock Action)')");
|
|
}
|
|
|
|
public function printEmail($id)
|
|
{
|
|
// Mock print logic
|
|
$this->js('window.print()');
|
|
}
|
|
|
|
public function deleteEmail($id)
|
|
{
|
|
// Mock delete logic
|
|
$this->js("alert('Email #{$id} deleted successfully! (Mock Action)')");
|
|
$this->selectedEmailId = null;
|
|
}
|
|
|
|
public function nextPage()
|
|
{
|
|
if ($this->page < $this->totalPages) {
|
|
$this->page++;
|
|
$this->selectedEmailId = null;
|
|
}
|
|
}
|
|
|
|
public function previousPage()
|
|
{
|
|
if ($this->page > 1) {
|
|
$this->page--;
|
|
$this->selectedEmailId = null;
|
|
}
|
|
}
|
|
|
|
public function generateQrCode($address)
|
|
{
|
|
// Mock QR generation with a slight delay
|
|
usleep(800000); // 800ms
|
|
$this->dispatch('qrCodeGenerated', address: $address);
|
|
}
|
|
|
|
public function getProcessedContent($email)
|
|
{
|
|
$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($body->body_text)) {
|
|
return trim(e($body->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('/<img\s[^>]*?\bsrc\s*=\s*([\'"])(.*?)\1/i', '<img $2 data-blocked-src=$1$2$1 src="data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 1 1\'%3E%3C/svg%3E" class="blocked-remote-asset shadow-sm border border-white/5 opacity-50"', $content);
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$currentMailbox = $this->active_mailboxes->firstWhere('id', $this->currentMailboxId);
|
|
|
|
return view('livewire.mailbox', [
|
|
'emails' => $this->getEmailsProperty(),
|
|
'currentMailbox' => $currentMailbox,
|
|
'unreadCount' => $this->unread_count,
|
|
]);
|
|
}
|
|
}
|