Skip to content

Commit f306fd0

Browse files
authored
Merge pull request #151: add Sender mail to file
2 parents 985be79 + 6c65115 commit f306fd0

File tree

11 files changed

+307
-20
lines changed

11 files changed

+307
-20
lines changed

README.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,13 @@ Environment variables can also be used to set endpoints:
214214
Buggregator Trap provides a variety of "senders" that dictate where the dumps will be sent. Currently, the available
215215
sender options include:
216216

217-
- `console`: This option displays dumps directly in the console.
218-
- `server`: With this choice, dumps are sent to a remote Buggregator server.
219-
- `file`: This allows for dumps to be stored in a file for future reference.
217+
- `console`: Shows dumps directly in the console.
218+
- `server`: Sends dumps to a remote Buggregator server.
219+
- `file`: Saves dumps in a file for later use.
220+
- `mail-to-file`: Creates a folder for each recipient and saves each message as a JSON file. Useful for testing mails.
221+
If you send a mail `To: [email protected], [email protected]`, the following folders will be created:
222+
- `runtime/mail/[email protected]`
223+
- `runtime/mail/[email protected]`
220224

221225
By default, the Trap server is set to display dumps in the console. However, you can easily select your preferred
222226
senders using the `-s` option.

src/Command/Run.php

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public function createRegistry(OutputInterface $output): Sender\SenderRegistry
8383
$registry->register('console', Sender\ConsoleSender::create($output));
8484
$registry->register('file', new Sender\EventsToFileSender());
8585
$registry->register('file-body', new Sender\BodyToFileSender());
86+
$registry->register('mail-to-file', new Sender\MailToFileSender());
8687
$registry->register(
8788
'server',
8889
new Sender\RemoteSender(

src/Proto/Frame/Http.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function __construct(
3737

3838
public static function fromString(string $payload, \DateTimeImmutable $time): static
3939
{
40-
$payload = \json_decode($payload, true, \JSON_THROW_ON_ERROR);
40+
$payload = \json_decode($payload, true, 64, \JSON_THROW_ON_ERROR);
4141

4242
$request = new ServerRequest(
4343
$payload['method'] ?? 'GET',

src/Proto/Frame/Smtp.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ public function __construct(
2727

2828
public static function fromString(string $payload, \DateTimeImmutable $time): static
2929
{
30-
/** @var TArrayData $payload */
31-
$payload = \json_decode($payload, true, \JSON_THROW_ON_ERROR);
32-
$message = Message\Smtp::fromArray($payload);
30+
/** @var TArrayData $data */
31+
$data = \json_decode($payload, true, 64, \JSON_THROW_ON_ERROR);
32+
$message = Message\Smtp::fromArray($data);
3333

3434
return new self($message, $time);
3535
}

src/Sender/BodyToFileSender.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace Buggregator\Trap\Sender;
66

77
use Buggregator\Trap\Proto\Frame;
8+
use Buggregator\Trap\Proto\StreamCarrier;
89
use Buggregator\Trap\Sender;
10+
use Buggregator\Trap\Support\FileSystem;
911
use Buggregator\Trap\Support\StreamHelper;
1012
use Nyholm\Psr7\Stream;
1113

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

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

3636
/** @var Frame $frame */
3737
foreach ($frames as $frame) {
38-
if (!$frame instanceof \Buggregator\Trap\Proto\StreamCarrier) {
38+
if (!$frame instanceof StreamCarrier) {
3939
continue;
4040
}
4141

src/Sender/EventsToFileSender.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Buggregator\Trap\Proto\Frame;
88
use Buggregator\Trap\Sender;
9+
use Buggregator\Trap\Support\FileSystem;
910

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

2928
public function send(iterable $frames): void

src/Sender/MailToFileSender.php

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Buggregator\Trap\Sender;
6+
7+
use Buggregator\Trap\Proto\Frame;
8+
use Buggregator\Trap\Proto\Frame\Smtp;
9+
use Buggregator\Trap\Sender;
10+
use Buggregator\Trap\Support\FileSystem;
11+
use Buggregator\Trap\Traffic\Message;
12+
use Buggregator\Trap\Traffic\Message\Smtp\Contact;
13+
14+
/**
15+
* @internal
16+
*/
17+
class MailToFileSender implements Sender
18+
{
19+
private readonly string $path;
20+
21+
public function __construct(
22+
string $path = 'runtime/mail',
23+
) {
24+
$this->path = \rtrim($path, '/\\');
25+
FileSystem::mkdir($path);
26+
}
27+
28+
public function send(iterable $frames): void
29+
{
30+
/** @var Frame $frame */
31+
foreach ($frames as $frame) {
32+
if (!$frame instanceof Smtp) {
33+
continue;
34+
}
35+
36+
foreach (self::fetchDirectories($frame->message) as $dirName) {
37+
$path = $this->path . DIRECTORY_SEPARATOR . $dirName;
38+
FileSystem::mkdir($path);
39+
$filepath = \sprintf("%s/%s.json", $path, $frame->time->format('Y-m-d-H-i-s-v'));
40+
41+
\assert(!\file_exists($filepath));
42+
\file_put_contents($filepath, \json_encode($frame->message, \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR));
43+
}
44+
}
45+
}
46+
47+
/**
48+
* Get normalized email address for file or directory name.
49+
*
50+
* @return non-empty-string
51+
*/
52+
private static function normalizeEmail(string $email): string
53+
{
54+
return \preg_replace(
55+
['/[^a-z0-9.\\- @]/i', '/\s+/'],
56+
['!', '_'],
57+
$email,
58+
);
59+
}
60+
61+
/**
62+
* @return list<non-empty-string>
63+
*/
64+
private static function fetchDirectories(Message\Smtp $message): array
65+
{
66+
return
67+
\array_filter(
68+
\array_unique(
69+
\array_map(
70+
static fn(Contact $c) => self::normalizeEmail($c->email),
71+
\array_merge($message->getBcc(), $message->getTo()),
72+
),
73+
),
74+
);
75+
}
76+
}

src/Support/FileSystem.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Buggregator\Trap\Support;
6+
7+
/**
8+
* @internal
9+
* @psalm-internal Buggregator\Trap
10+
*/
11+
final class FileSystem
12+
{
13+
public static function mkdir(string $path, int $mode = 0777, bool $recursive = true): void
14+
{
15+
\is_dir($path) or \mkdir($path, $mode, $recursive) or \is_dir($path) or throw new \RuntimeException(
16+
\sprintf('Directory "%s" was not created.', $path),
17+
);
18+
}
19+
}

src/Traffic/Message/Smtp.php

+39-6
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,15 @@ public function getSender(): array
141141
*/
142142
public function getTo(): array
143143
{
144-
return \array_map([$this, 'parseContact'], $this->getHeader('To'));
144+
return $this->normalizeAddressList($this->getHeader('To'));
145145
}
146146

147147
/**
148148
* @return Contact[]
149149
*/
150150
public function getCc(): array
151151
{
152-
return \array_map([$this, 'parseContact'], $this->getHeader('Cc'));
152+
return $this->normalizeAddressList($this->getHeader('Cc'));
153153
}
154154

155155
/**
@@ -160,15 +160,15 @@ public function getCc(): array
160160
*/
161161
public function getBcc(): array
162162
{
163-
return \array_map([$this, 'parseContact'], $this->protocol['BCC'] ?? []);
163+
return $this->normalizeAddressList($this->protocol['BCC'] ?? []);
164164
}
165165

166166
/**
167167
* @return Contact[]
168168
*/
169169
public function getReplyTo(): array
170170
{
171-
return \array_map([$this, 'parseContact'], $this->getHeader('Reply-To'));
171+
return $this->normalizeAddressList($this->getHeader('Reply-To'));
172172
}
173173

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

190190
private function parseContact(string $line): Contact
191191
{
192-
if (\preg_match('/^\s*(?<name>.*)\s*<(?<email>.*)>\s*$/', $line, $matches) === 1) {
193-
return new Contact($matches['name'] ?: null, $matches['email'] ?: null);
192+
if (\preg_match('/^\s*+(?<name>.*?)\s*<(?<email>.*)>\s*$/', $line, $matches) === 1) {
193+
$name = match (true) {
194+
\preg_match('/^".*?"$/', $matches['name']) === 1 => \str_replace('\\"', '"', \substr($matches['name'], 1, -1)),
195+
$matches['name'] === '' => null,
196+
default => $matches['name'],
197+
};
198+
199+
return new Contact(
200+
$name,
201+
$matches['email'] === '' ? null : \trim($matches['email']),
202+
);
194203
}
195204

196205
return new Contact(null, $line);
197206
}
207+
208+
/**
209+
* @return array<Contact>
210+
*/
211+
private function parseDestinationAddress(string $line): array
212+
{
213+
// if this is a group recipient
214+
if (\preg_match('/^[^"]+:(.*);$/', $line, $matches) === 1) {
215+
$line = $matches[1];
216+
}
217+
218+
$emailList = \array_map('trim', \explode(',', $line));
219+
return \array_map([$this, 'parseContact'], $emailList);
220+
}
221+
222+
/**
223+
* @return array<Contact>
224+
*/
225+
private function normalizeAddressList(array $param): array
226+
{
227+
return \array_merge(
228+
...\array_map([$this, 'parseDestinationAddress'], $param),
229+
);
230+
}
198231
}
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Buggregator\Trap\Tests\Unit\Sender;
6+
7+
use Buggregator\Trap\Info;
8+
use Buggregator\Trap\Proto\Frame\Smtp as SmtpFrame;
9+
use Buggregator\Trap\Sender\MailToFileSender;
10+
use Buggregator\Trap\Traffic\Message\Smtp as SmtpMessage;
11+
use PHPUnit\Framework\TestCase;
12+
13+
/**
14+
* @coversDefaultClass \Buggregator\Trap\Sender\MailToFileSender
15+
*/
16+
final class MailToFileSenderTest extends TestCase
17+
{
18+
/** @var list<non-empty-string> */
19+
private array $cleanupFolders = [];
20+
21+
public function testForSmtp(): void
22+
{
23+
$this->cleanupFolders[] = $root = Info::TRAP_ROOT . '/runtime/tests/mail-to-file-sender';
24+
25+
$message = SmtpMessage::create(
26+
protocol: [
27+
'FROM' => ['<[email protected]>'],
28+
'BCC' => [
29+
30+
31+
],
32+
],
33+
headers: [
34+
'From' => ['Some User <[email protected]>'],
35+
'To' => [
36+
'User1 <[email protected]>',
37+
38+
'User without email', // no email
39+
40+
],
41+
'Subject' => ['Very important theme'],
42+
'Content-Type' => ['text/plain'],
43+
],
44+
);
45+
$frame = new SmtpFrame($message);
46+
$sender = new MailToFileSender($root);
47+
$sender->send([$frame]);
48+
49+
$this->assertRecipient("$root/[email protected]");
50+
$this->assertRecipient("$root/[email protected]");
51+
$this->assertRecipient("$root/[email protected]");
52+
$this->assertRecipient("$root/[email protected]");
53+
$this->assertRecipient("$root/[email protected]");
54+
}
55+
56+
protected function tearDown(): void
57+
{
58+
foreach ($this->cleanupFolders as $folder) {
59+
\array_map('unlink', \glob("$folder/*/*.*"));
60+
\array_map('rmdir', \glob("$folder/*"));
61+
\rmdir($folder);
62+
}
63+
}
64+
65+
private function assertRecipient(string $folder): void
66+
{
67+
self::assertDirectoryExists($folder);
68+
$files = \glob(\str_replace('[', '[[]', "$folder/*.json"));
69+
self::assertCount(1, $files);
70+
$arr = \json_decode(\file_get_contents($files[0]), true, \JSON_THROW_ON_ERROR);
71+
self::assertArrayHasKey('protocol', $arr);
72+
self::assertArrayHasKey('headers', $arr);
73+
self::assertArrayHasKey('messages', $arr);
74+
self::assertArrayHasKey('attachments', $arr);
75+
}
76+
}

0 commit comments

Comments
 (0)