Skip to content

Commit

Permalink
Merge pull request #192 from buggregator/feature/185
Browse files Browse the repository at this point in the history
Adds embeddings support
  • Loading branch information
butschster committed Jun 5, 2024
2 parents 8b612ba + 5df3c65 commit 6a693c3
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 23 deletions.
1 change: 0 additions & 1 deletion app/modules/Sentry/Interfaces/Http/JavascriptAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,3 @@ public function __invoke(EnvironmentInterface $env, string|int $project): Respon
);
}
}

6 changes: 6 additions & 0 deletions app/modules/Smtp/Application/Mail/Attachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function __construct(
private ?string $filename,
private string $content,
private string $type,
private ?string $contentId = null,
) {
$this->id = (string) Uuid::uuid4();
}
Expand All @@ -37,4 +38,9 @@ public function getId(): string
{
return $this->id;
}

public function getContentId(): ?string
{
return $this->contentId;
}
}
1 change: 1 addition & 0 deletions app/modules/Smtp/Application/Mail/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private function buildAttachmentFrom(array $attachments): array
$part->getFilename(),
$part->getContent(),
$part->getContentType(),
$part->getContentId(),
), $attachments);
}

Expand Down
19 changes: 16 additions & 3 deletions app/modules/Smtp/Application/Storage/AttachmentStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,38 @@ public function __construct(
private AttachmentFactoryInterface $factory,
) {}

public function store(Uuid $eventUuid, array $attachments): void
public function store(Uuid $eventUuid, array $attachments): iterable
{
$result = [];
foreach ($attachments as $attachment) {
$file = $this->bucket->write(
pathname: $eventUuid . '/' . $attachment->getFilename(),
content: $attachment->getContent(),
);

$this->attachments->store(
$this->factory->create(
$a = $this->factory->create(
eventUuid: $eventUuid,
name: $attachment->getFilename(),
path: $file->getPathname(),
size: $file->getSize(),
mime: $file->getMimeType(),
id: $attachment->getId(),
id: $attachment->getContentId() ?? $attachment->getId(),
),
);

if ($attachment->getContentId() === null) {
continue;
}

$result[$attachment->getContentId()] = \sprintf(
'/api/smtp/%s/attachments/preview/%s',
$eventUuid,
$a->getUuid(),
);
}

return $result;
}

public function deleteByEvent(Uuid $eventUuid): void
Expand Down
3 changes: 2 additions & 1 deletion app/modules/Smtp/Domain/AttachmentStorageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ interface AttachmentStorageInterface
{
/**
* @param MailAttachment[] $attachments
* @return iterable<string, string>
*/
public function store(Uuid $eventUuid, array $attachments): void;
public function store(Uuid $eventUuid, array $attachments): iterable;

public function deleteByEvent(Uuid $eventUuid): void;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace Modules\Smtp\Interfaces\Http\Controllers\Attachments;

use App\Application\Commands\FindEventByUuid;
use App\Application\Commands\FindSmtpAttachmentByUuid;
use App\Application\Domain\ValueObjects\Uuid;
use App\Application\HTTP\Response\ErrorResource;
use Modules\Smtp\Domain\AttachmentStorageInterface;
use Nyholm\Psr7\Stream;
use Psr\Http\Message\ResponseInterface;
use Spiral\Cqrs\QueryBusInterface;
use Spiral\Http\Exception\ClientException\ForbiddenException;
use Spiral\Http\ResponseWrapper;
use Spiral\Router\Annotation\Route;
use OpenApi\Attributes as OA;

#[OA\Get(
path: '/api/smtp/{eventUuid}/attachments/preview/{uuid}',
description: 'Preview an attachment by UUID',
tags: ['Smtp'],
parameters: [
new OA\PathParameter(
name: 'eventUuid',
description: 'Event UUID',
required: true,
schema: new OA\Schema(type: 'string', format: 'uuid'),
),
new OA\PathParameter(
name: 'uuid',
description: 'Attachment UUID',
required: true,
schema: new OA\Schema(type: 'string', format: 'uuid'),
),
],
responses: [
new OA\Response(
response: 200,
description: 'Success',
headers: [
new OA\Header(header: 'Content-Type', schema: new OA\Schema(type: 'string', format: 'binary')),
],
),
new OA\Response(
response: 404,
description: 'Not found',
content: new OA\JsonContent(
ref: ErrorResource::class,
),
),
new OA\Response(
response: 403,
description: 'Access denied.',
content: new OA\JsonContent(
ref: ErrorResource::class,
),
),
],
)]
final readonly class PreviewAction
{
public function __construct(
private AttachmentStorageInterface $storage,
) {}

#[Route(route: 'smtp/<eventUuid>/attachments/preview/<uuid>', name: 'smtp.attachments.preview', group: 'api_guest')]
public function __invoke(
QueryBusInterface $bus,
ResponseWrapper $responseWrapper,
Uuid $eventUuid,
Uuid $uuid,
): ResponseInterface {
$event = $bus->ask(new FindEventByUuid($eventUuid));
$attachment = $bus->ask(new FindSmtpAttachmentByUuid($uuid));

if (!$attachment->getEventUuid()->equals($event->getUuid())) {
throw new ForbiddenException('Access denied.');
}

$stream = Stream::create($this->storage->getContent($attachment->getPath()));

return $responseWrapper->create(200)
->withHeader('Content-Type', $attachment->getMime())
->withBody($stream);
}
}
7 changes: 6 additions & 1 deletion app/modules/Smtp/Interfaces/TCP/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ private function dispatchMessage(Message $message, ?string $project = null): voi
$uuid = Uuid::generate();
$data = $message->jsonSerialize();

$this->attachments->store(eventUuid: $uuid, attachments: $message->attachments);

$result = $this->attachments->store(eventUuid: $uuid, attachments: $message->attachments);
// TODO: Refactor this
foreach ($result as $cid => $url) {
$data['html'] = \str_replace("cid:$cid", $url, $data['html']);
}

$this->bus->dispatch(
new HandleReceivedEvent(
Expand Down
59 changes: 44 additions & 15 deletions tests/Feature/Interfaces/TCP/Smtp/EmailTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,21 @@
use Spiral\RoadRunnerBridge\Tcp\Response\CloseConnection;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\File;
use Tests\Feature\Interfaces\TCP\TCPTestCase;

final class EmailTest extends TCPTestCase
{
private \Spiral\Storage\BucketInterface $bucket;
private \Mockery\MockInterface|AttachmentRepositoryInterface $accounts;
private \Mockery\MockInterface|AttachmentRepositoryInterface $attachments;

protected function setUp(): void
{
parent::setUp();

$this->bucket = $this->fakeStorage()->bucket('smtp');
$this->accounts = $this->mockContainer(AttachmentRepositoryInterface::class);
$this->attachments = $this->mockContainer(AttachmentRepositoryInterface::class);
}

public function testSendEmail(): void
Expand All @@ -41,8 +43,23 @@ public function testSendEmail(): void
uuid: $connectionUuid = Uuid::uuid7(),
);

// Assert logo-embeddable is persisted to a database
$this->attachments->shouldReceive('store')
->once()
->with(
\Mockery::on(function (Attachment $attachment) {
$this->assertSame('logo-embeddable', $attachment->getFilename());
$this->assertSame(1206, $attachment->getSize());
$this->assertSame('image/svg+xml', $attachment->getMime());

// Check attachments storage
$this->bucket->assertCreated($attachment->getPath());
return true;
}),
);

// Assert hello.txt is persisted to a database
$this->accounts->shouldReceive('store')
$this->attachments->shouldReceive('store')
->once()
->with(
\Mockery::on(function (Attachment $attachment) {
Expand All @@ -56,17 +73,17 @@ public function testSendEmail(): void
}),
);


// Assert world.txt is persisted to a database
$this->accounts->shouldReceive('store')
// Assert hello.txt is persisted to a database
$this->attachments->shouldReceive('store')
->once()
->with(
\Mockery::on(function (Attachment $attachment) {
$this->assertSame('logo.svg', $attachment->getFilename());
$this->assertSame('image/svg+xml', $attachment->getMime());
$this->assertSame(1206, $attachment->getSize());
$this->bucket->assertCreated($attachment->getPath());
$this->assertSame('image/svg+xml', $attachment->getMime());

// Check attachments storage
$this->bucket->assertCreated($attachment->getPath());
return true;
}),
);
Expand Down Expand Up @@ -141,12 +158,15 @@ private function validateMessage(string $messageId, string $uuid): void

$this->assertSame([], $parsedMessage->getBccs());

$this->assertSame(
'Hello Alice.<br>This is a test message with 5 header fields and 4 lines in the message body.',
$parsedMessage->textBody,
$this->assertStringEqualsStringIgnoringLineEndings(
<<<'HTML'
<img src="cid:test-cid@buggregator">
Hello Alice.<br>This is a test message with 5 header fields and 4 lines in the message body.
HTML
,
$parsedMessage->htmlBody,
);

$this->assertSame('', $parsedMessage->htmlBody);
$this->assertStringContainsString(
"Subject: Test message\r
Date: Thu, 02 May 2024 16:01:33 +0000\r
Expand Down Expand Up @@ -213,9 +233,18 @@ public function buildEmail(): Email
)
->addFrom(new Address('no-reply@site.com', 'Bob Example'),)
->attachFromPath(path: __DIR__ . '/hello.txt',)
->attachFromPath(path: __DIR__ . '/logo.svg',)
->text(
body: 'Hello Alice.<br>This is a test message with 5 header fields and 4 lines in the message body.',
->attachFromPath(path: __DIR__ . '/logo.svg')
->addPart(
(new DataPart(new File(__DIR__ . '/logo.svg'), 'logo-embeddable'))->asInline()->setContentId(
'test-cid@buggregator',
),
)
->html(
body: <<<'TEXT'
<img src="cid:logo-embeddable">
Hello Alice.<br>This is a test message with 5 header fields and 4 lines in the message body.
TEXT
,
);
}
}
11 changes: 9 additions & 2 deletions tests/Unit/Modules/Smtp/AttachmentStorageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function testStore(): void
filename: 'file1.txt',
content: 'Hello, world!',
type: 'text/plain',
contentId: 'file1@buggregator',
);

$attachment2 = new Attachment(
Expand All @@ -64,11 +65,13 @@ public function testStore(): void
$path1,
12,
'text/plain',
$attachment1->getId(),
$attachment1->getContentId(),
)
->once()
->andReturn($entity1 = $this->mockContainer(\Modules\Smtp\Domain\Attachment::class));

$entity1->shouldReceive('getUuid')->andReturn($uuid = Uuid::generate());

$this->bucket->shouldReceive('write')
->with($path2 = $eventUuid . '/image.png', 'image content')
->once()
Expand All @@ -93,7 +96,11 @@ public function testStore(): void
$this->attachments->shouldReceive('store')->with($entity1)->once();
$this->attachments->shouldReceive('store')->with($entity2)->once();

$this->storage->store($eventUuid, [$attachment1, $attachment2]);
$result = $this->storage->store($eventUuid, [$attachment1, $attachment2]);

$this->assertSame([
'file1@buggregator' => '/api/smtp/' . $eventUuid . '/attachments/preview/' . $uuid,
], $result);
}

public function testRemove(): void
Expand Down

0 comments on commit 6a693c3

Please sign in to comment.