mailwolt/app/Livewire/Ui/Webmail/Compose.php

299 lines
10 KiB
PHP

<?php
namespace App\Livewire\Ui\Webmail;
use App\Models\MailUser;
use App\Services\ImapService;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithFileUploads;
#[Layout('layouts.webmail')]
#[Title('Schreiben · Webmail')]
class Compose extends Component
{
use WithFileUploads;
public string $to = '';
public string $subject = '';
public string $body = '';
public bool $sent = false;
// Livewire temp upload binding — cleared immediately after moving to stagedAttachments
public array $attachments = [];
// Persistent staged attachments: [{name, size, mime, path}] — survives reloads via session
public array $stagedAttachments = [];
// Signature — loaded from DB, kept separate from body
public string $signatureRaw = '';
public bool $signatureIsHtml = false;
// Reply mode
#[Url] public int $replyUid = 0;
#[Url] public string $replyFolder = 'INBOX';
public bool $isReply = false;
public string $replyMeta = '';
// Draft editing
#[Url] public int $draftUid = 0;
#[Url] public string $draftFolder = 'Drafts';
public bool $isDraft = false;
public function mount(): void
{
if (! session('webmail_email')) {
$this->redirect(route('ui.webmail.login'));
return;
}
// Restore staged attachments from session, drop any whose files were cleaned up
$staged = session('compose_attachments', []);
$this->stagedAttachments = array_values(
array_filter($staged, fn($a) => Storage::exists($a['path']))
);
$user = MailUser::where('email', session('webmail_email'))->first();
$prefs = $user?->webmail_prefs ?? [];
$sig = (string) ($user?->signature ?? '');
if ($this->draftUid > 0) {
$this->loadDraft();
return;
}
// Signatur laden — wird NICHT in den Body injiziert, sondern separat angezeigt
if ($sig) {
$this->signatureIsHtml = (bool) preg_match('/<[a-zA-Z][^>]*>/', $sig);
$applyNew = ($prefs['signature_new'] ?? true);
$applyReply = ($prefs['signature_reply'] ?? false);
if ($this->replyUid > 0 && $applyReply) {
$this->signatureRaw = $sig;
} elseif ($this->replyUid === 0 && $applyNew) {
$this->signatureRaw = $sig;
}
}
if ($this->replyUid > 0) {
$this->loadReply();
return;
}
}
// Called by Livewire when $attachments changes (after upload completes)
public function updatedAttachments(): void
{
$dir = 'webmail-tmp/' . session()->getId();
foreach ($this->attachments as $file) {
$filename = uniqid('att_') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file->getClientOriginalName());
$path = $file->storeAs($dir, $filename, 'local');
$this->stagedAttachments[] = [
'name' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'mime' => $file->getMimeType() ?: 'application/octet-stream',
'path' => $path,
];
}
$this->attachments = [];
session(['compose_attachments' => $this->stagedAttachments]);
}
private function loadDraft(): void
{
$this->isDraft = true;
try {
$imap = app(ImapService::class);
$client = $imap->client(session('webmail_email'), session('webmail_password'));
$orig = $imap->getMessage($client, $this->draftFolder, $this->draftUid);
$client->disconnect();
$this->to = $orig['to'] ?? '';
$this->subject = $orig['subject'] ?? '';
$this->body = $orig['text'] ?? strip_tags($orig['html'] ?? '');
} catch (\Throwable) {}
}
private function loadReply(): void
{
$this->isReply = true;
try {
$imap = app(ImapService::class);
$client = $imap->client(session('webmail_email'), session('webmail_password'));
$orig = $imap->getMessage($client, $this->replyFolder, $this->replyUid);
$client->disconnect();
$this->to = $orig['from'] ?? '';
$sub = $orig['subject'] ?? '';
$this->subject = str_starts_with(strtolower($sub), 're:') ? $sub : 'Re: ' . $sub;
$date = $orig['date']
? \Carbon\Carbon::parse($orig['date'])->format('d.m.Y \u\m H:i \U\h\r')
: '';
$fromName = $orig['from_name'] ?: ($orig['from'] ?? '');
$fromAddr = $orig['from'] ?? '';
$this->replyMeta = "Am {$date} schrieb {$fromName} <{$fromAddr}>:";
$quoteText = $orig['text'] ?: strip_tags($orig['html'] ?? '');
$quoted = implode("\n", array_map(
fn($l) => '> ' . $l,
explode("\n", rtrim($quoteText))
));
$this->body = "\n\n\n" . $this->replyMeta . "\n" . $quoted;
} catch (\Throwable) {}
}
public function back(): void
{
$hasContent = trim($this->to)
|| trim($this->subject)
|| trim(preg_replace('/^[\s>]+$/m', '', $this->body));
if ($hasContent) {
$this->saveCurrentDraft(true);
}
$this->clearStagedAttachments();
$this->redirect(route('ui.webmail.inbox'));
}
public function send(): void
{
$this->validate([
'to' => 'required|email',
'subject' => 'required|string|max:255',
'body' => 'required|string',
]);
try {
$imap = app(ImapService::class);
if ($this->signatureIsHtml && $this->signatureRaw) {
$htmlBody = '<div style="font-family:inherit;font-size:13px;line-height:1.6;white-space:pre-wrap;">'
. htmlspecialchars($this->body)
. '</div>'
. '<br><div style="border-top:1px solid #e5e7eb;margin-top:12px;padding-top:10px;font-size:12.5px;">'
. $this->signatureRaw
. '</div>';
$imap->sendMessage(
session('webmail_email'),
session('webmail_password'),
$this->to,
$this->subject,
$htmlBody,
$this->stagedAttachments,
true,
);
} else {
$plainBody = $this->body;
if ($this->signatureRaw) {
$plainBody .= "\n\n" . $this->normalizeSignature($this->signatureRaw);
}
$imap->sendMessage(
session('webmail_email'),
session('webmail_password'),
$this->to,
$this->subject,
$plainBody,
$this->stagedAttachments,
);
}
if ($this->draftUid > 0) {
try {
$client = $imap->client(session('webmail_email'), session('webmail_password'));
$imap->deleteMessage($client, $this->draftFolder, $this->draftUid);
$client->disconnect();
} catch (\Throwable) {}
}
$this->clearStagedAttachments();
$this->sent = true;
$this->to = '';
$this->subject = '';
$this->body = '';
$this->isReply = false;
$this->isDraft = false;
$this->dispatch('toast', type: 'done', badge: 'Webmail',
title: 'Gesendet', text: 'Nachricht wurde erfolgreich gesendet.', duration: 4000);
} catch (\Throwable $e) {
$this->addError('to', 'Senden fehlgeschlagen: ' . $e->getMessage());
}
}
public function removeAttachment(int $index): void
{
if (isset($this->stagedAttachments[$index])) {
Storage::delete($this->stagedAttachments[$index]['path']);
}
array_splice($this->stagedAttachments, $index, 1);
session(['compose_attachments' => $this->stagedAttachments]);
}
private function normalizeSignature(string $sig): string
{
$trimmed = ltrim($sig, "\r\n");
// Prüfen ob der User bereits "-- " als Trenner eingetragen hat
if (str_starts_with($trimmed, '-- ') || str_starts_with($trimmed, "--\n") || str_starts_with($trimmed, "--\r\n")) {
return $trimmed;
}
return "-- \n" . $trimmed;
}
private function clearStagedAttachments(): void
{
foreach ($this->stagedAttachments as $att) {
Storage::delete($att['path']);
}
$this->stagedAttachments = [];
session()->forget('compose_attachments');
}
private function saveCurrentDraft(bool $notify = false): void
{
try {
$imap = app(ImapService::class);
$client = $imap->client(session('webmail_email'), session('webmail_password'));
if ($this->draftUid > 0) {
$imap->deleteMessage($client, $this->draftFolder, $this->draftUid);
}
$imap->saveDraft($client, session('webmail_email'), $this->to, $this->subject, $this->body);
$client->disconnect();
if ($notify) {
$this->dispatch('toast', type: 'done', badge: 'Webmail',
title: 'Entwurf gespeichert', text: 'Nachricht in Entwürfe gespeichert.', duration: 3000);
}
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::warning('Draft save failed', ['error' => $e->getMessage()]);
}
}
public function autoSave(): void
{
if ($this->sent) return;
$hasContent = trim($this->to)
|| trim($this->subject)
|| trim(preg_replace('/^[\s>]+$/m', '', $this->body));
if (! $hasContent) return;
$this->saveCurrentDraft(false);
}
public function render()
{
return view('livewire.ui.webmail.compose');
}
}