$payload */ public function send(array $payload, User $user): WebmailMessage { $fromEmail = $user->email ?: config('mail.from.address'); $fromName = $user->name ?: config('mail.from.name'); $mailboxSetting = $user->mailboxSetting; $message = $this->webmailRepository->create([ 'user_id' => $user->id, 'message_uid' => $payload['message_uid'] ?? (string) Str::uuid(), 'direction' => 'outgoing', 'folder' => $payload['folder'] ?? 'sent', 'status' => 'pending', 'from_email' => $fromEmail, 'from_name' => $fromName, 'to_recipients' => $payload['to'], 'cc_recipients' => $payload['cc'] ?? [], 'bcc_recipients' => $payload['bcc'] ?? [], 'subject' => $payload['subject'] ?? null, 'body' => $payload['body'] ?? '', 'snippet' => $this->makeSnippet($payload['body'] ?? ''), 'attachments' => $payload['attachments'] ?? [], 'metadata' => $payload['metadata'] ?? [], ]); try { if ($mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasSmtpConfiguration()) { $this->sendUsingUserSmtp($payload, $mailboxSetting, $fromEmail, $fromName); } else { $mailable = new WebmailMessageMail([ 'subject' => $payload['subject'] ?? '', 'body' => $payload['body'] ?? '', ]); if (! empty($fromEmail)) { $mailable->from($fromEmail, $fromName); $mailable->replyTo($fromEmail, $fromName); } $mailer = Mail::to($payload['to']); if (! empty($payload['cc'])) { $mailer->cc($payload['cc']); } if (! empty($payload['bcc'])) { $mailer->bcc($payload['bcc']); } $mailer->send($mailable); } $this->webmailRepository->update($message->id, [ 'status' => 'sent', 'sent_at' => now(), 'metadata' => array_merge(Arr::wrap($message->metadata), [ 'transport' => $mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasSmtpConfiguration() ? 'user_smtp' : 'app_mailer', ]), ]); } catch (Throwable $throwable) { $metadata = Arr::wrap($message->metadata); $metadata['send_error'] = $throwable->getMessage(); $this->webmailRepository->update($message->id, [ 'status' => 'failed', 'metadata' => $metadata, ]); throw $throwable; } /** @var WebmailMessage $freshMessage */ $freshMessage = $this->webmailRepository->find((string) $message->id); return $freshMessage; } /** * @return array{imported:int, skipped:int, source:string} */ public function syncMailbox(User $user, int $limit = 30): array { $mailboxSetting = $user->mailboxSetting; if ($mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasImapConfiguration()) { return $this->syncImapInbox($user, $mailboxSetting, $limit); } $result = $this->syncMailtrapInbox($user, $limit); $result['source'] = 'mailtrap'; return $result; } /** * @return array{imported:int, skipped:int} */ public function syncMailtrapInbox(User $user, int $limit = 30): array { $accountId = (string) config('services.mailtrap.account_id'); $inboxId = (string) config('services.mailtrap.inbox_id'); $token = (string) config('services.mailtrap.token'); $baseUrl = rtrim((string) config('services.mailtrap.base_url', 'https://mailtrap.io'), '/'); if ($accountId === '' || $inboxId === '' || $token === '') { throw new \RuntimeException('La configuration Mailtrap est incomplete. Renseignez MAILTRAP_ACCOUNT_ID, MAILTRAP_INBOX_ID et MAILTRAP_API_TOKEN.'); } if (empty($user->email)) { throw new \RuntimeException('L\'utilisateur connecte ne possede pas d\'adresse email.'); } $response = Http::baseUrl($baseUrl) ->acceptJson() ->withToken($token) ->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages"); $response->throw(); /** @var array> $messages */ $messages = $response->json(); $imported = 0; $skipped = 0; foreach (array_slice($messages, 0, max(1, $limit)) as $sandboxMessage) { if (! $this->messageTargetsUser($sandboxMessage, (string) $user->email)) { $skipped++; continue; } $messageUid = 'mailtrap-' . (string) ($sandboxMessage['id'] ?? Str::uuid()); $alreadyExists = WebmailMessage::query() ->where('user_id', $user->id) ->where('message_uid', $messageUid) ->exists(); if ($alreadyExists) { $skipped++; continue; } $messageId = (string) ($sandboxMessage['id'] ?? ''); $body = $messageId !== '' ? $this->fetchMailtrapBody($baseUrl, $token, $accountId, $inboxId, $messageId) : ''; $this->receive([ 'message_uid' => $messageUid, 'from_email' => $sandboxMessage['from_email'] ?? 'unknown@mailtrap.local', 'from_name' => $sandboxMessage['from_name'] ?? null, 'to' => $this->extractRecipients($sandboxMessage), 'subject' => $sandboxMessage['subject'] ?? null, 'body' => $body, 'status' => 'received', 'folder' => 'inbox', 'received_at' => $sandboxMessage['sent_at'] ?? now()->toDateTimeString(), 'metadata' => [ 'provider' => 'mailtrap', 'mailtrap_inbox_id' => $inboxId, 'mailtrap_message_id' => $sandboxMessage['id'] ?? null, 'mailtrap_raw' => $sandboxMessage, ], ], $user); $imported++; } return [ 'imported' => $imported, 'skipped' => $skipped, ]; } /** * @param array $payload */ public function receive(array $payload, User $user): WebmailMessage { $receivedAt = !empty($payload['received_at']) ? Carbon::parse((string) $payload['received_at']) : now(); /** @var WebmailMessage $message */ $message = $this->webmailRepository->create([ 'user_id' => $user->id, 'message_uid' => $payload['message_uid'] ?? (string) Str::uuid(), 'direction' => 'incoming', 'folder' => $payload['folder'] ?? 'inbox', 'status' => $payload['status'] ?? 'received', 'from_email' => $payload['from_email'], 'from_name' => $payload['from_name'] ?? null, 'to_recipients' => $payload['to'], 'cc_recipients' => $payload['cc'] ?? [], 'bcc_recipients' => $payload['bcc'] ?? [], 'subject' => $payload['subject'] ?? null, 'body' => $payload['body'] ?? '', 'snippet' => $this->makeSnippet($payload['body'] ?? ''), 'attachments' => $payload['attachments'] ?? [], 'metadata' => $payload['metadata'] ?? [], 'received_at' => $receivedAt, ]); return $message; } private function makeSnippet(string $body): string { return Str::limit(trim(strip_tags($body)), 160, '...'); } private function sendUsingUserSmtp( array $payload, UserMailboxSetting $mailboxSetting, string $fallbackFromEmail, ?string $fallbackFromName, ): void { $transport = Transport::fromDsn($this->buildSmtpDsn($mailboxSetting)); $mailer = new Mailer($transport); $fromEmail = $mailboxSetting->smtp_from_address ?: $fallbackFromEmail; $fromName = $mailboxSetting->smtp_from_name ?: $fallbackFromName; $htmlBody = view('emails.webmail_message', [ 'body' => $payload['body'] ?? '', ])->render(); $email = (new Email()) ->from(new Address($fromEmail, $fromName ?? '')) ->replyTo(new Address($fromEmail, $fromName ?? '')) ->subject((string) ($payload['subject'] ?? '')) ->html($htmlBody) ->text(trim(strip_tags((string) ($payload['body'] ?? '')))); foreach ($payload['to'] as $recipient) { $email->addTo(new Address((string) $recipient)); } foreach ($payload['cc'] ?? [] as $recipient) { $email->addCc(new Address((string) $recipient)); } foreach ($payload['bcc'] ?? [] as $recipient) { $email->addBcc(new Address((string) $recipient)); } $mailer->send($email); } /** * @return array{imported:int, skipped:int, source:string} */ private function syncImapInbox(User $user, UserMailboxSetting $mailboxSetting, int $limit): array { $imported = 0; $skipped = 0; try { $clientManager = new ClientManager([ 'options' => [ 'fetch' => null, ], 'accounts' => [ 'default' => [ 'host' => $mailboxSetting->imap_host, 'port' => $mailboxSetting->imap_port, 'encryption' => $this->normalizeImapEncryption($mailboxSetting->imap_encryption), 'validate_cert' => $mailboxSetting->imap_validate_cert, 'username' => $mailboxSetting->imap_username, 'password' => $mailboxSetting->imap_password, 'protocol' => 'imap', ], ], ]); $client = $clientManager->account('default'); $client->connect(); $folder = $client->getFolder($mailboxSetting->imap_folder ?: 'INBOX'); $query = $folder->query() ->leaveUnread() ->setFetchOrderDesc() ->limit(max(1, $limit)); if ($mailboxSetting->last_synced_at) { $query->whereSince($mailboxSetting->last_synced_at->copy()->subDay()); } /** @var iterable $messages */ $messages = $query->get(); foreach ($messages as $imapMessage) { $messageUid = 'imap-' . (string) $imapMessage->getUid(); $alreadyExists = WebmailMessage::query() ->where('user_id', $user->id) ->where('message_uid', $messageUid) ->exists(); if ($alreadyExists) { $skipped++; continue; } $body = trim($imapMessage->getTextBody()); if ($body === '') { $body = trim(strip_tags($imapMessage->getHTMLBody())); } $from = $this->extractAddressList($imapMessage->getFrom()); $to = $this->extractAddressList($imapMessage->getTo()); $cc = $this->extractAddressList($imapMessage->getCc()); $bcc = $this->extractAddressList($imapMessage->getBcc()); $this->receive([ 'message_uid' => $messageUid, 'from_email' => $from[0]['email'] ?? 'unknown@imap.local', 'from_name' => $from[0]['name'] ?? null, 'to' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $to))), 'cc' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $cc))), 'bcc' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $bcc))), 'subject' => (string) $imapMessage->getSubject(), 'body' => $body, 'status' => 'received', 'folder' => 'inbox', 'received_at' => $imapMessage->getDate()->toDate()->toDateTimeString(), 'attachments' => [], 'metadata' => [ 'provider' => 'imap', 'imap_uid' => (string) $imapMessage->getUid(), ], ], $user); $imported++; } $mailboxSetting->forceFill([ 'last_synced_at' => now(), 'last_sync_error' => null, ])->save(); $client->disconnect(); } catch (Throwable $throwable) { $mailboxSetting->forceFill([ 'last_sync_error' => $throwable->getMessage(), ])->save(); throw $throwable; } return [ 'imported' => $imported, 'skipped' => $skipped, 'source' => 'imap', ]; } private function buildSmtpDsn(UserMailboxSetting $mailboxSetting): string { $scheme = ($mailboxSetting->smtp_encryption ?? '') === 'ssl' ? 'smtps' : 'smtp'; $username = rawurlencode((string) $mailboxSetting->smtp_username); $password = rawurlencode((string) $mailboxSetting->smtp_password); $host = (string) $mailboxSetting->smtp_host; $port = (int) $mailboxSetting->smtp_port; $query = []; if (($mailboxSetting->smtp_encryption ?? '') === 'tls') { $query['encryption'] = 'tls'; } if (! $mailboxSetting->smtp_validate_cert) { $query['verify_peer'] = '0'; } $suffix = $query === [] ? '' : '?' . http_build_query($query); return sprintf('%s://%s:%s@%s:%d%s', $scheme, $username, $password, $host, $port, $suffix); } private function normalizeImapEncryption(?string $encryption): ?string { if ($encryption === null || $encryption === '' || $encryption === 'none') { return null; } return $encryption; } /** * @return array */ private function extractAddressList(mixed $attribute): array { if (! $attribute instanceof Attribute) { return []; } return collect($attribute->toArray()) ->map(function (mixed $item): array { if (is_object($item)) { return [ 'name' => isset($item->personal) ? (string) $item->personal : null, 'email' => isset($item->mail) ? (string) $item->mail : null, ]; } if (is_array($item)) { return [ 'name' => isset($item['personal']) ? (string) $item['personal'] : null, 'email' => isset($item['mail']) ? (string) $item['mail'] : null, ]; } return [ 'name' => null, 'email' => is_string($item) ? $item : null, ]; }) ->filter(fn (array $item): bool => filled($item['email'])) ->values() ->all(); } /** * @param array $sandboxMessage */ private function messageTargetsUser(array $sandboxMessage, string $userEmail): bool { $userEmail = Str::lower(trim($userEmail)); return collect($this->extractRecipients($sandboxMessage)) ->map(fn (mixed $email): string => Str::lower(trim((string) $email))) ->contains($userEmail); } /** * @param array $sandboxMessage * @return array */ private function extractRecipients(array $sandboxMessage): array { $candidates = [ $sandboxMessage['to'] ?? null, $sandboxMessage['to_email'] ?? null, $sandboxMessage['to_emails'] ?? null, ]; return collect($candidates) ->flatten(1) ->map(function (mixed $recipient): ?string { if (is_array($recipient)) { $email = $recipient['email'] ?? $recipient['address'] ?? $recipient['to_email'] ?? null; return is_string($email) ? trim($email) : null; } return is_string($recipient) ? trim($recipient) : null; }) ->filter(fn (?string $email): bool => ! empty($email)) ->unique() ->values() ->all(); } private function fetchMailtrapBody(string $baseUrl, string $token, string $accountId, string $inboxId, string $messageId): string { $textResponse = Http::baseUrl($baseUrl) ->withToken($token) ->accept('text/plain') ->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages/{$messageId}/body.txt"); if ($textResponse->successful()) { $body = trim($textResponse->body()); if ($body !== '') { return $body; } } $htmlResponse = Http::baseUrl($baseUrl) ->withToken($token) ->accept('text/html') ->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages/{$messageId}/body.html"); if (! $htmlResponse->successful()) { return ''; } return trim(strip_tags($htmlResponse->body())); } }