Skip to content

Commit

Permalink
Merge pull request #151: add Sender mail to file
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk authored Jan 16, 2025
2 parents 985be79 + 6c65115 commit f306fd0
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 20 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,13 @@ Environment variables can also be used to set endpoints:
Buggregator Trap provides a variety of "senders" that dictate where the dumps will be sent. Currently, the available
sender options include:

- `console`: This option displays dumps directly in the console.
- `server`: With this choice, dumps are sent to a remote Buggregator server.
- `file`: This allows for dumps to be stored in a file for future reference.
- `console`: Shows dumps directly in the console.
- `server`: Sends dumps to a remote Buggregator server.
- `file`: Saves dumps in a file for later use.
- `mail-to-file`: Creates a folder for each recipient and saves each message as a JSON file. Useful for testing mails.
If you send a mail `To: foo@example.com, bar@example2.org`, the following folders will be created:
- `runtime/mail/foo@example.com`
- `runtime/mail/bar@example2.org`

By default, the Trap server is set to display dumps in the console. However, you can easily select your preferred
senders using the `-s` option.
Expand Down
1 change: 1 addition & 0 deletions src/Command/Run.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public function createRegistry(OutputInterface $output): Sender\SenderRegistry
$registry->register('console', Sender\ConsoleSender::create($output));
$registry->register('file', new Sender\EventsToFileSender());
$registry->register('file-body', new Sender\BodyToFileSender());
$registry->register('mail-to-file', new Sender\MailToFileSender());
$registry->register(
'server',
new Sender\RemoteSender(
Expand Down
2 changes: 1 addition & 1 deletion src/Proto/Frame/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function __construct(

public static function fromString(string $payload, \DateTimeImmutable $time): static
{
$payload = \json_decode($payload, true, \JSON_THROW_ON_ERROR);
$payload = \json_decode($payload, true, 64, \JSON_THROW_ON_ERROR);

$request = new ServerRequest(
$payload['method'] ?? 'GET',
Expand Down
6 changes: 3 additions & 3 deletions src/Proto/Frame/Smtp.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public function __construct(

public static function fromString(string $payload, \DateTimeImmutable $time): static
{
/** @var TArrayData $payload */
$payload = \json_decode($payload, true, \JSON_THROW_ON_ERROR);
$message = Message\Smtp::fromArray($payload);
/** @var TArrayData $data */
$data = \json_decode($payload, true, 64, \JSON_THROW_ON_ERROR);
$message = Message\Smtp::fromArray($data);

return new self($message, $time);
}
Expand Down
8 changes: 4 additions & 4 deletions src/Sender/BodyToFileSender.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace Buggregator\Trap\Sender;

use Buggregator\Trap\Proto\Frame;
use Buggregator\Trap\Proto\StreamCarrier;
use Buggregator\Trap\Sender;
use Buggregator\Trap\Support\FileSystem;
use Buggregator\Trap\Support\StreamHelper;
use Nyholm\Psr7\Stream;

Expand All @@ -23,9 +25,7 @@ public function __construct(
string $path = 'runtime/body',
) {
$this->path = \rtrim($path, '/\\');
if (!\is_dir($path) && !\mkdir($path, 0o777, true) && !\is_dir($path)) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $path));
}
FileSystem::mkdir($path);
}

public function send(iterable $frames): void
Expand All @@ -35,7 +35,7 @@ public function send(iterable $frames): void

/** @var Frame $frame */
foreach ($frames as $frame) {
if (!$frame instanceof \Buggregator\Trap\Proto\StreamCarrier) {
if (!$frame instanceof StreamCarrier) {
continue;
}

Expand Down
5 changes: 2 additions & 3 deletions src/Sender/EventsToFileSender.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Buggregator\Trap\Proto\Frame;
use Buggregator\Trap\Sender;
use Buggregator\Trap\Support\FileSystem;

/**
* Store event groups to files.
Expand All @@ -21,9 +22,7 @@ public function __construct(
string $path = 'runtime',
) {
$this->path = \rtrim($path, '/\\');
if (!\is_dir($path) && !\mkdir($path, 0o777, true) && !\is_dir($path)) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $path));
}
FileSystem::mkdir($path);
}

public function send(iterable $frames): void
Expand Down
76 changes: 76 additions & 0 deletions src/Sender/MailToFileSender.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Buggregator\Trap\Sender;

use Buggregator\Trap\Proto\Frame;
use Buggregator\Trap\Proto\Frame\Smtp;
use Buggregator\Trap\Sender;
use Buggregator\Trap\Support\FileSystem;
use Buggregator\Trap\Traffic\Message;
use Buggregator\Trap\Traffic\Message\Smtp\Contact;

/**
* @internal
*/
class MailToFileSender implements Sender
{
private readonly string $path;

public function __construct(
string $path = 'runtime/mail',
) {
$this->path = \rtrim($path, '/\\');
FileSystem::mkdir($path);
}

public function send(iterable $frames): void
{
/** @var Frame $frame */
foreach ($frames as $frame) {
if (!$frame instanceof Smtp) {
continue;
}

foreach (self::fetchDirectories($frame->message) as $dirName) {
$path = $this->path . DIRECTORY_SEPARATOR . $dirName;
FileSystem::mkdir($path);
$filepath = \sprintf("%s/%s.json", $path, $frame->time->format('Y-m-d-H-i-s-v'));

\assert(!\file_exists($filepath));
\file_put_contents($filepath, \json_encode($frame->message, \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR));
}
}
}

/**
* Get normalized email address for file or directory name.
*
* @return non-empty-string

Check failure on line 50 in src/Sender/MailToFileSender.php

View workflow job for this annotation

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

MoreSpecificReturnType

src/Sender/MailToFileSender.php:50:16: MoreSpecificReturnType: The declared return type 'non-empty-string' for Buggregator\Trap\Sender\MailToFileSender::normalizeEmail is more specific than the inferred return type 'null|string' (see https://psalm.dev/070)
*/
private static function normalizeEmail(string $email): string
{
return \preg_replace(

Check failure on line 54 in src/Sender/MailToFileSender.php

View workflow job for this annotation

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

LessSpecificReturnStatement

src/Sender/MailToFileSender.php:54:16: LessSpecificReturnStatement: The type 'null|string' is more general than the declared return type 'non-empty-string' for Buggregator\Trap\Sender\MailToFileSender::normalizeEmail (see https://psalm.dev/129)
['/[^a-z0-9.\\- @]/i', '/\s+/'],
['!', '_'],
$email,
);
}

/**
* @return list<non-empty-string>

Check failure on line 62 in src/Sender/MailToFileSender.php

View workflow job for this annotation

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

MoreSpecificReturnType

src/Sender/MailToFileSender.php:62:16: MoreSpecificReturnType: The declared return type 'list<non-empty-string>' for Buggregator\Trap\Sender\MailToFileSender::fetchDirectories is more specific than the inferred return type 'array<array-key, non-falsy-string>' (see https://psalm.dev/070)
*/
private static function fetchDirectories(Message\Smtp $message): array
{
return
\array_filter(

Check failure on line 67 in src/Sender/MailToFileSender.php

View workflow job for this annotation

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

LessSpecificReturnStatement

src/Sender/MailToFileSender.php:67:13: LessSpecificReturnStatement: The type 'array<array-key, non-falsy-string>' is more general than the declared return type 'list<non-empty-string>' for Buggregator\Trap\Sender\MailToFileSender::fetchDirectories (see https://psalm.dev/129)
\array_unique(
\array_map(
static fn(Contact $c) => self::normalizeEmail($c->email),

Check failure on line 70 in src/Sender/MailToFileSender.php

View workflow job for this annotation

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

PossiblyNullArgument

src/Sender/MailToFileSender.php:70:71: PossiblyNullArgument: Argument 1 of Buggregator\Trap\Sender\MailToFileSender::normalizeEmail cannot be null, possibly null value provided (see https://psalm.dev/078)
\array_merge($message->getBcc(), $message->getTo()),
),
),
);
}
}
19 changes: 19 additions & 0 deletions src/Support/FileSystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Buggregator\Trap\Support;

/**
* @internal
* @psalm-internal Buggregator\Trap
*/
final class FileSystem
{
public static function mkdir(string $path, int $mode = 0777, bool $recursive = true): void
{
\is_dir($path) or \mkdir($path, $mode, $recursive) or \is_dir($path) or throw new \RuntimeException(
\sprintf('Directory "%s" was not created.', $path),
);
}
}
45 changes: 39 additions & 6 deletions src/Traffic/Message/Smtp.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,15 @@ public function getSender(): array
*/
public function getTo(): array
{
return \array_map([$this, 'parseContact'], $this->getHeader('To'));
return $this->normalizeAddressList($this->getHeader('To'));
}

/**
* @return Contact[]
*/
public function getCc(): array
{
return \array_map([$this, 'parseContact'], $this->getHeader('Cc'));
return $this->normalizeAddressList($this->getHeader('Cc'));
}

/**
Expand All @@ -160,15 +160,15 @@ public function getCc(): array
*/
public function getBcc(): array
{
return \array_map([$this, 'parseContact'], $this->protocol['BCC'] ?? []);
return $this->normalizeAddressList($this->protocol['BCC'] ?? []);
}

/**
* @return Contact[]
*/
public function getReplyTo(): array
{
return \array_map([$this, 'parseContact'], $this->getHeader('Reply-To'));
return $this->normalizeAddressList($this->getHeader('Reply-To'));
}

public function getSubject(): string
Expand All @@ -189,10 +189,43 @@ public function getMessage(MessageFormat $type): ?Field

private function parseContact(string $line): Contact
{
if (\preg_match('/^\s*(?<name>.*)\s*<(?<email>.*)>\s*$/', $line, $matches) === 1) {
return new Contact($matches['name'] ?: null, $matches['email'] ?: null);
if (\preg_match('/^\s*+(?<name>.*?)\s*<(?<email>.*)>\s*$/', $line, $matches) === 1) {
$name = match (true) {
\preg_match('/^".*?"$/', $matches['name']) === 1 => \str_replace('\\"', '"', \substr($matches['name'], 1, -1)),
$matches['name'] === '' => null,
default => $matches['name'],
};

return new Contact(
$name,
$matches['email'] === '' ? null : \trim($matches['email']),
);
}

return new Contact(null, $line);
}

/**
* @return array<Contact>
*/
private function parseDestinationAddress(string $line): array
{
// if this is a group recipient
if (\preg_match('/^[^"]+:(.*);$/', $line, $matches) === 1) {
$line = $matches[1];
}

$emailList = \array_map('trim', \explode(',', $line));
return \array_map([$this, 'parseContact'], $emailList);
}

/**
* @return array<Contact>
*/
private function normalizeAddressList(array $param): array
{
return \array_merge(
...\array_map([$this, 'parseDestinationAddress'], $param),

Check failure on line 228 in src/Traffic/Message/Smtp.php

View workflow job for this annotation

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

NamedArgumentNotAllowed

src/Traffic/Message/Smtp.php:228:16: NamedArgumentNotAllowed: Method array_merge called with named unpacked array array<array-key, array<array-key, Buggregator\Trap\Traffic\Message\Smtp\Contact>> (array with string keys) (see https://psalm.dev/268)

Check failure on line 228 in src/Traffic/Message/Smtp.php

View workflow job for this annotation

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

MixedArgument

src/Traffic/Message/Smtp.php:228:63: MixedArgument: Argument 1 of Buggregator\Trap\Traffic\Message\Smtp::parseDestinationAddress cannot be mixed, expecting string (see https://psalm.dev/030)
);
}
}
76 changes: 76 additions & 0 deletions tests/Unit/Sender/MailToFileSenderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Buggregator\Trap\Tests\Unit\Sender;

use Buggregator\Trap\Info;
use Buggregator\Trap\Proto\Frame\Smtp as SmtpFrame;
use Buggregator\Trap\Sender\MailToFileSender;
use Buggregator\Trap\Traffic\Message\Smtp as SmtpMessage;
use PHPUnit\Framework\TestCase;

/**
* @coversDefaultClass \Buggregator\Trap\Sender\MailToFileSender
*/
final class MailToFileSenderTest extends TestCase
{
/** @var list<non-empty-string> */
private array $cleanupFolders = [];

public function testForSmtp(): void
{
$this->cleanupFolders[] = $root = Info::TRAP_ROOT . '/runtime/tests/mail-to-file-sender';

$message = SmtpMessage::create(
protocol: [
'FROM' => ['<someusername@foo.bar>'],
'BCC' => [
'<user1@company.tld>',
'<user2@company.tld>',
],
],
headers: [
'From' => ['Some User <someusername@somecompany.ru>'],
'To' => [
'User1 <user1@company.tld>',
'user2@company.tld',
'User without email', // no email
'User3 <user3@inline.com>, User4 <user4@inline.com>, user5@inline.com',
],
'Subject' => ['Very important theme'],
'Content-Type' => ['text/plain'],
],
);
$frame = new SmtpFrame($message);
$sender = new MailToFileSender($root);
$sender->send([$frame]);

$this->assertRecipient("$root/user1@company.tld");
$this->assertRecipient("$root/user2@company.tld");
$this->assertRecipient("$root/user3@inline.com");
$this->assertRecipient("$root/user4@inline.com");
$this->assertRecipient("$root/user5@inline.com");
}

protected function tearDown(): void
{
foreach ($this->cleanupFolders as $folder) {
\array_map('unlink', \glob("$folder/*/*.*"));
\array_map('rmdir', \glob("$folder/*"));
\rmdir($folder);
}
}

private function assertRecipient(string $folder): void
{
self::assertDirectoryExists($folder);
$files = \glob(\str_replace('[', '[[]', "$folder/*.json"));
self::assertCount(1, $files);
$arr = \json_decode(\file_get_contents($files[0]), true, \JSON_THROW_ON_ERROR);
self::assertArrayHasKey('protocol', $arr);
self::assertArrayHasKey('headers', $arr);
self::assertArrayHasKey('messages', $arr);
self::assertArrayHasKey('attachments', $arr);
}
}
Loading

0 comments on commit f306fd0

Please sign in to comment.