Skip to content

Commit

Permalink
feat(smtp): detect embedded files and solve links inside html body
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk committed Jun 7, 2024
1 parent 04e41b2 commit 63a2990
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 40 deletions.
25 changes: 25 additions & 0 deletions src/Sender/Frontend/Event/EmbeddedFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Buggregator\Trap\Sender\Frontend\Event;

use Psr\Http\Message\UploadedFileInterface;

/**
* @internal
*/
final class EmbeddedFile extends Asset
{
/**
* @param non-empty-string $id
* @param non-empty-string $name
*/
public function __construct(
string $id,
public readonly UploadedFileInterface $file,
public readonly string $name,
) {
parent::__construct($id);
}
}
45 changes: 29 additions & 16 deletions src/Sender/Frontend/Http/EventAssets.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Buggregator\Trap\Handler\Router\Router as CommonRouter;
use Buggregator\Trap\Logger;
use Buggregator\Trap\Sender\Frontend\Event\AttachedFile;
use Buggregator\Trap\Sender\Frontend\Event\EmbeddedFile;
use Buggregator\Trap\Sender\Frontend\EventStorage;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
Expand Down Expand Up @@ -107,23 +108,35 @@ public function attachment(string $eventId, string $attachId): ?Response
// Find attachment
$attachment = $event->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;
}
}
105 changes: 82 additions & 23 deletions src/Sender/Frontend/Mapper/Smtp.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -19,11 +20,11 @@ public function map(SmtpFrame $frame): Event
{
$message = $frame->message;

/** @var \ArrayAccess<non-empty-string, Event\Asset> $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(),
Expand All @@ -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<non-empty-string, Event\Asset>
*/
private static function fetchAssets(SmtpMessage $message): \ArrayAccess
{
/** @var \ArrayAccess<non-empty-string, Event\Asset> $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],

Check failure on line 82 in src/Sender/Frontend/Mapper/Smtp.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Variable $matches might not be defined.

// 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],

Check failure on line 89 in src/Sender/Frontend/Mapper/Smtp.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Variable $matches might not be defined.
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<non-empty-string, Event\Asset> $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) {

Check failure on line 115 in src/Sender/Frontend/Mapper/Smtp.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

RawObjectIteration

src/Sender/Frontend/Mapper/Smtp.php:115:18: RawObjectIteration: Possibly undesired iteration over regular object ArrayAccess (see https://psalm.dev/111)

Check failure on line 115 in src/Sender/Frontend/Mapper/Smtp.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

MixedAssignment

src/Sender/Frontend/Mapper/Smtp.php:115:29: MixedAssignment: Unable to determine the type that $asset is being assigned to (see https://psalm.dev/032)

Check failure on line 115 in src/Sender/Frontend/Mapper/Smtp.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Argument of an invalid type ArrayAccess<string, Buggregator\Trap\Sender\Frontend\Event\Asset> supplied for foreach, only iterables are supported.
$asset instanceof Event\EmbeddedFile and $toReplace["cid:{$asset->name}"] = self::assetLink($uuid, $asset);

Check failure on line 116 in src/Sender/Frontend/Mapper/Smtp.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Only booleans are allowed in &&, string given on the right side.
}

return \str_replace(\array_keys($toReplace), \array_values($toReplace), $result);
}
}
2 changes: 1 addition & 1 deletion src/Traffic/Message/Multipart/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 63a2990

Please sign in to comment.