diff --git a/src/Sender/Frontend/Event/EmbeddedFile.php b/src/Sender/Frontend/Event/EmbeddedFile.php new file mode 100644 index 00000000..b1b0fb3f --- /dev/null +++ b/src/Sender/Frontend/Event/EmbeddedFile.php @@ -0,0 +1,25 @@ +assets[$attachId] ?? null; - if (!$attachment instanceof AttachedFile) { - $this->logger->debug('Get attachment `%s` for event `%s`. Attached file not found.', $attachId, $eventId); - return null; + if ($attachment instanceof AttachedFile) { + return new Response( + 200, + [ + 'Content-Type' => $attachment->file->getClientMediaType(), + 'Content-Disposition' => \sprintf( + "attachment; filename=\"%s\"", + \rawurlencode($attachment->file->getClientFilename() ?? 'unnamed'), + ), + 'Content-Length' => (string) $attachment->file->getSize(), + 'Cache-Control' => 'no-cache', + ], + $attachment->file->getStream(), + ); } - return new Response( - 200, - [ - 'Content-Type' => $attachment->file->getClientMediaType(), - 'Content-Disposition' => \sprintf( - "attachment; filename=\"%s\"", - \rawurlencode($attachment->file->getClientFilename() ?? 'unnamed'), - ), - 'Content-Length' => (string) $attachment->file->getSize(), - 'Cache-Control' => 'no-cache', - ], - $attachment->file->getStream(), - ); + if ($attachment instanceof EmbeddedFile) { + return new Response( + 200, + [ + 'Content-Type' => $attachment->file->getClientMediaType(), + 'Content-Length' => (string) $attachment->file->getSize(), + 'Cache-Control' => 'no-cache', + ], + $attachment->file->getStream(), + ); + } + + $this->logger->debug('Get attachment `%s` for event `%s`. Attached file not found.', $attachId, $eventId); + return null; } } diff --git a/src/Sender/Frontend/Mapper/Smtp.php b/src/Sender/Frontend/Mapper/Smtp.php index d6f6b764..b97ac411 100644 --- a/src/Sender/Frontend/Mapper/Smtp.php +++ b/src/Sender/Frontend/Mapper/Smtp.php @@ -8,6 +8,7 @@ use Buggregator\Trap\Sender\Frontend\Event; use Buggregator\Trap\Support\Uuid; use Buggregator\Trap\Traffic\Message\Multipart\File; +use Buggregator\Trap\Traffic\Message\Smtp as SmtpMessage; use Buggregator\Trap\Traffic\Message\Smtp\MessageFormat; /** @@ -19,11 +20,11 @@ public function map(SmtpFrame $frame): Event { $message = $frame->message; - /** @var \ArrayAccess $assets */ - $assets = new \ArrayObject(); + $uuid = Uuid::generate(); + $assets = self::fetchAssets($message); return new Event( - uuid: $uuid = Uuid::generate(), + uuid: $uuid, type: 'smtp', payload: [ 'from' => $message->getSender(), @@ -33,30 +34,88 @@ public function map(SmtpFrame $frame): Event 'cc' => $message->getCc(), 'bcc' => $message->getBcc(), 'text' => $message->getMessage(MessageFormat::Plain)?->getValue() ?? '', - 'html' => $message->getMessage(MessageFormat::Html)?->getValue() ?? '', + 'html' => self::html($uuid, $message, $assets), 'raw' => (string) $message->getBody(), - 'attachments' => \array_map( - static function (File $attachment) use ($assets, $uuid): array { - $asset = new Event\AttachedFile( - id: Uuid::generate(), - file: $attachment, - ); - $uri = $uuid . '/' . $asset->uuid; - $assets->offsetSet($asset->uuid, $asset); - - return [ - 'id' => $asset->uuid, - 'name' => $attachment->getClientFilename(), - 'uri' => $uri, - 'size' => $attachment->getSize(), - 'mime' => $attachment->getClientMediaType(), - ]; - }, - $message->getAttachments(), - ), ], timestamp: (float) $frame->time->format('U.u'), assets: $assets, ); } + + /** + * @return \ArrayAccess + */ + private static function fetchAssets(SmtpMessage $message): \ArrayAccess + { + /** @var \ArrayAccess $assets */ + $assets = new \ArrayObject(); + + foreach ($message->getAttachments() as $attachment) { + $asset = self::asset($attachment); + $assets->offsetSet($asset->uuid, $asset); + } + + return $assets; + } + + /** + * @param non-empty-string $uuid UUID of the event + */ + private static function assetLink(string $uuid, Event\Asset $asset): string + { + return "/api/smtp/$uuid/attachment/$asset->uuid"; + } + + private static function asset(File $attachment): Event\Asset + { + /** + * Detect if the file is an embedded image + * + * @var non-empty-string|null $embedded + */ + $embedded = match (true) { + // Content-Disposition is inline and name is present + \str_starts_with($attachment->getHeaderLine('Content-Disposition'), 'inline') && \preg_match( + '/name=(?:\"([^\"]++)\"|\'([^\']++)\'|([^;,\\s]++))/', + $attachment->getHeaderLine('Content-Disposition'), + $matches, + ) === 1 => $matches[1], + + // Content-Type is image/* and has name + \str_starts_with($attachment->getHeaderLine('Content-Type'), 'image/') && \preg_match( + '/name=(?:\"([^\"]++)\"|\'([^\']++)\'|([^;,\\s]++))/', + $attachment->getHeaderLine('Content-Type'), + $matches, + ) === 1 => $matches[1], + default => null, + }; + + return $embedded === null + ? new Event\AttachedFile( + id: Uuid::generate(), + file: $attachment, + ) + : new Event\EmbeddedFile( + id: Uuid::generate(), + file: $attachment, + name: $embedded, + ); + } + + /** + * @param non-empty-string $uuid + * @param \ArrayAccess $assets + */ + private static function html(string $uuid, SmtpMessage $message, \ArrayAccess $assets): string + { + $result = $message->getMessage(MessageFormat::Html)?->getValue() ?? ''; + + // Replace CID links with actual asset links + $toReplace = []; + foreach ($assets as $asset) { + $asset instanceof Event\EmbeddedFile and $toReplace["cid:{$asset->name}"] = self::assetLink($uuid, $asset); + } + + return \str_replace(\array_keys($toReplace), \array_values($toReplace), $result); + } } diff --git a/src/Traffic/Message/Multipart/File.php b/src/Traffic/Message/Multipart/File.php index dfe9c030..b774106a 100644 --- a/src/Traffic/Message/Multipart/File.php +++ b/src/Traffic/Message/Multipart/File.php @@ -115,7 +115,7 @@ public function getClientFilename(): ?string public function getClientMediaType(): ?string { - return $this->getHeader('Content-Type')[0] ?? null; + return \explode(';', $this->getHeader('Content-Type')[0], 2)[0] ?? null; } private function getUploadedFile(): UploadedFileInterface