diff --git a/.php_cs.dist b/.php_cs.dist index 5fac02b..c04f458 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -7,7 +7,8 @@ $finder = PhpCsFixer\Finder::create() return PhpCsFixer\Config::create() ->setRules([ '@PSR2' => true, - 'strict_param' => false, + 'strict_param' => true, + 'declare_strict_types' => true, 'array_syntax' => ['syntax' => 'short'], ]) ->setFinder($finder); \ No newline at end of file diff --git a/config.php b/config.php index ff4f132..4d96555 100644 --- a/config.php +++ b/config.php @@ -14,4 +14,4 @@ 'websocket' => [ 'host' => '127.0.0.1:1338', ], -]; \ No newline at end of file +]; diff --git a/src/Config.php b/src/Config.php index 0691f71..9232be4 100644 --- a/src/Config.php +++ b/src/Config.php @@ -9,6 +9,9 @@ class Config private array $params; private ?array $altParams; + const VERSION = "1.0.0"; + const DATE_FORMAT = "Y-m-d H:i:s"; + public function __construct(array $params, array $altParams = null) { $this->params = $params; @@ -52,4 +55,4 @@ private static function dotGet(string $key, array $data) return $data; } -} \ No newline at end of file +} diff --git a/src/Emails/Attachment.php b/src/Emails/Attachment.php index 98a5bdb..35af9b7 100644 --- a/src/Emails/Attachment.php +++ b/src/Emails/Attachment.php @@ -4,10 +4,10 @@ class Attachment { - public string $filename; - public string $content; - public string $type; - public string $id; + private string $filename; + private string $content; + private string $type; + private string $id; public function __construct(string $filename, string $content, string $type) { @@ -16,4 +16,24 @@ public function __construct(string $filename, string $content, string $type) $this->content = $content; $this->type = $type; } + + public function getFilename(): string + { + return $this->filename; + } + + public function getContent(): string + { + return $this->content; + } + + public function getType(): string + { + return $this->type; + } + + public function getId(): string + { + return $this->id; + } } diff --git a/src/Emails/Message.php b/src/Emails/Message.php index 1a6a5fe..a446283 100644 --- a/src/Emails/Message.php +++ b/src/Emails/Message.php @@ -2,22 +2,31 @@ namespace Mailamie\Emails; -use DateTime; +use DateTimeImmutable; use Exception; +use Mailamie\Config; +/** + * Class Message + * @package Mailamie\Emails + */ class Message { private string $raw; - public string $id; - public string $sender; - public array $recipients; - public array $ccs; - public string $htmlBody; - public string $textBody; - public string $subject; - public DateTime $created_at; + private string $id; + private string $sender; + /** @var string[] */ + private array $recipients; + /** @var string[] */ + private array $ccs; + private string $htmlBody; + private string $textBody; + private string $subject; + private DateTimeImmutable $created_at; private string $replyTo; + /** @var string[] */ private array $allRecipients; + /** @var Attachment[] */ private array $attachments; public function __construct( @@ -31,8 +40,7 @@ public function __construct( string $replyTo, array $allRecipients, array $attachments - ) - { + ) { $this->id = (string)uniqid(); $this->raw = $raw; $this->sender = $sender; @@ -41,7 +49,7 @@ public function __construct( $this->htmlBody = $htmlBody; $this->textBody = $textBody; $this->subject = $subject; - $this->created_at = new DateTime(); + $this->created_at = new DateTimeImmutable(); $this->replyTo = $replyTo; $this->allRecipients = $allRecipients; $this->attachments = $attachments; @@ -50,7 +58,7 @@ public function __construct( public function getAttachment(string $id): Attachment { $attachments = array_values(array_filter($this->attachments, function (Attachment $attachment) use ($id) { - return $attachment->id === $id; + return $attachment->getId() === $id; })); if (!count($attachments)) { @@ -65,10 +73,46 @@ public function getExcerpt() return mb_strimwidth(strip_tags($this->htmlBody) ?: $this->textBody, 0, 30); } + public function getId(): string + { + return $this->id; + } + + public function getSender(): string + { + return $this->sender; + } + + /** + * @return string[] + */ + public function getRecipients(): array + { + return $this->recipients; + } + + public function getSubject(): string + { + return $this->subject; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->created_at; + } + + /** + * @return Attachment[] + */ + public function getAttachments(): array + { + return $this->attachments; + } + public function toTable() { $table = [ - ['Date', $this->created_at->format('Y-m-d H:i:s')], + ['Date', $this->created_at->format(Config::DATE_FORMAT)], ['Subject', "{$this->subject}"], ['Excerpt', $this->getExcerpt()], ['To', implode("; ", $this->recipients)], @@ -109,18 +153,18 @@ public function toArray(): array 'html' => $this->htmlBody, 'text' => $this->textBody, 'raw' => $this->raw, - 'attachments' => $this->getAttachments(), - 'created_at' => $this->created_at->format('Y-m-d H:i:s') + 'attachments' => $this->attachmentsToArray(), + 'created_at' => $this->created_at->format(Config::DATE_FORMAT) ]; } - private function getAttachments() + private function attachmentsToArray() { return array_map(function (Attachment $attachment) { return [ - 'id' => $attachment->id, - 'name' => $attachment->filename, - 'url' => "/api/messages/{$this->id}/attachments/{$attachment->id}" + 'id' => $attachment->getId(), + 'name' => $attachment->getFilename(), + 'url' => "/api/messages/{$this->id}/attachments/{$attachment->getId()}" ]; }, $this->attachments); } @@ -128,7 +172,7 @@ private function getAttachments() private function getAttachmentNames() { return array_map(function (Attachment $attachment) { - return $attachment->filename; + return $attachment->getFilename(); }, $this->attachments); } diff --git a/src/Emails/Parser.php b/src/Emails/Parser.php index aaec87d..b6e6798 100644 --- a/src/Emails/Parser.php +++ b/src/Emails/Parser.php @@ -18,40 +18,15 @@ public function parse(string $rawContent, array $allRecipients = []): Message $message = ParseMessage::from($rawContent); $from = $message->getHeader('from')->getRawValue(); - - $recipients = array_map(function (AddressPart $addressPart) { - $name = $addressPart->getName(); - $email = $addressPart->getValue(); - if ($name) { - return "{$name} <{$email}>"; - } - return $email; - }, $message->getHeader('to')->getAddresses()); - - $ccs = array_map(function (AddressPart $addressPart) { - $name = $addressPart->getName(); - $email = $addressPart->getValue(); - if ($name) { - return "{$name} <{$email}>"; - } - return $email; - }, $message->getHeader('cc')->getAddresses()); - + $recipients = $this->joinNameAndEmail($message->getHeader('to')->getAddresses()); + $ccs = $this->joinNameAndEmail($message->getHeader('cc')->getAddresses()); $subject = $message->getHeaderValue('subject'); - $html = $message->getHtmlContent(); $text = $message->getTextContent(); - $replyTo = $message->getHeader('reply-to')->getRawValue(); - - $attachments = []; - foreach ($message->getAllAttachmentParts() as $part) { - $attachments[] = new Attachment( - $part->getFilename(), - $part->getContent(), - $part->getContentType() - ); - } + $attachments = $this->buildAttachmentFrom( + $message->getAllAttachmentParts() + ); return new Message( $rawContent, @@ -66,4 +41,33 @@ public function parse(string $rawContent, array $allRecipients = []): Message $attachments ); } + + /** + * @param MessagePart[] $attachments + * @return Attachment[] + */ + private function buildAttachmentFrom(array $attachments): array + { + return array_map(function (MessagePart $part) { + return new Attachment( + $part->getFilename(), + $part->getContent(), + $part->getContentType() + ); + }, $attachments); + } + + /** + * @param AddressPart[] $addresses + * @return string[] + */ + private function joinNameAndEmail(array $addresses): array + { + return array_map(function (AddressPart $addressPart) { + $name = $addressPart->getName(); + $email = $addressPart->getValue(); + + return $name ? "{$name} <{$email}>" : $email; + }, $addresses); + } } diff --git a/src/Emails/Store.php b/src/Emails/Store.php index 3606bcb..ad15e8b 100644 --- a/src/Emails/Store.php +++ b/src/Emails/Store.php @@ -4,6 +4,7 @@ use Closure; use Exception; +use Mailamie\Config; class Store { @@ -19,7 +20,7 @@ class Store public function store(Message $message): void { - $this->messages[$message->id] = $message; + $this->messages[$message->getId()] = $message; foreach ($this->callbacks as $callback) { $callback($message); @@ -42,11 +43,11 @@ public function all(): array { return array_values(array_map(function (Message $message) { return [ - 'id' => $message->id, - 'from' => $message->sender, - 'recipients' => $message->recipients, - 'subject' => $message->subject, - 'created_at' => $message->created_at->format('Y-m-d H:i:s') + 'id' => $message->getId(), + 'from' => $message->getSender(), + 'recipients' => $message->getRecipients(), + 'subject' => $message->getSubject(), + 'created_at' => $message->getCreatedAt()->format(Config::DATE_FORMAT) ]; }, $this->sortedByDate())); } @@ -59,7 +60,7 @@ private function sortedByDate(): array $messages = $this->messages; usort($messages, function (Message $a, Message $b) { - return $a->created_at->getTimestamp() < $b->created_at->getTimestamp() ? 1 : -1; + return $a->getCreatedAt()->getTimestamp() < $b->getCreatedAt()->getTimestamp() ? 1 : -1; }); return $messages; diff --git a/src/Events/DebugEvent.php b/src/Events/DebugEvent.php index 050e244..9e1026d 100644 --- a/src/Events/DebugEvent.php +++ b/src/Events/DebugEvent.php @@ -2,11 +2,11 @@ namespace Mailamie\Events; -class DebugEvent +class DebugEvent implements Event { - public $param; + public string $param; - public function __construct($param) + public function __construct(string $param) { $this->param = $param; } diff --git a/src/Events/Event.php b/src/Events/Event.php new file mode 100644 index 0000000..fbe97b1 --- /dev/null +++ b/src/Events/Event.php @@ -0,0 +1,7 @@ + */ private static array $statusDescriptions = [ @@ -35,14 +40,14 @@ class SmtpConnection public function __construct( ConnectionInterface $connection, EventDispatcherInterface $events - ) - { + ) { $this->connection = $connection; $this->events = $events; } public function ready(): void { + $this->collectingData = false; $this->send(static::READY); } @@ -50,24 +55,33 @@ public function handle(string $data): void { $this->events->dispatch(new Request($data)); - if (preg_match("/^RCPT TO:<(.*)>/", $data, $matches)) { - $this->addRecipient($matches[0]); + if (preg_match("/^(EHLO|HELO|MAIL FROM:)/", $data)) { $this->send(static::OK); - } elseif (preg_match("/^(EHLO|HELO|MAIL FROM:)/", $data)) { + } elseif (preg_match("/^RCPT TO:<(.*)>/", $data, $matches)) { + $this->addRecipient($matches[0]); $this->send(static::OK); - } elseif ($data === "DATA\r\n") { - $this->send(static::START_MAIL_INPUT); } elseif ($data === "QUIT\r\n") { $this->send(static::CLOSING); - } elseif (strpos($data, "\r\n.\r\n")) { - $this->addToMessageBody($data); - $this->send(static::OK); - $this->dispatchMessage(); - } else { - $this->addToMessageBody($data); + } elseif ($data === "DATA\r\n") { + $this->collectingData = true; + $this->send(static::START_MAIL_INPUT); + } elseif ($this->collectingData) { + if ($this->endOfContentDetected($data)) { + $this->addToMessageBody($data); + $this->send(static::OK); + $this->dispatchMessage(); + $this->collectingData = false; + } else { + $this->addToMessageBody($data); + } } } + private function endOfContentDetected(string $data): bool + { + return (bool)preg_match("/\r\n\.\r\n$/", $data); + } + private function dispatchMessage(): void { $this->events->dispatch( diff --git a/src/StartServer.php b/src/StartServer.php index 8bf8c15..f6ef9e4 100644 --- a/src/StartServer.php +++ b/src/StartServer.php @@ -27,6 +27,8 @@ class StartServer extends Command protected static $defaultName = 'mailamie'; private Config $config; + const DATE_FORMAT = ""; + public function __construct(Config $config) { parent::__construct(); @@ -37,8 +39,8 @@ protected function configure() { $this->setDescription('Mailamie is catch all SMTP server for testing.'); $this->setHelp( - "You can define custom configuration from the file ~/.mailamie.config.php,\n". - "check the project readme file at https://github.com/micc83/mailamie\n". + "You can define custom configuration from the file ~/.mailamie.config.php,\n" . + "check the project readme file at https://github.com/micc83/mailamie\n" . "for all the available settings." ); $this->addUsage('--host=127.0.0.1:25 Ex. SMTP Host definition'); @@ -75,7 +77,6 @@ protected function execute(InputInterface $input, OutputInterface $output) $webServer = new WebServer( $this->config->get('web.host'), - $this->config->get('version'), $loop, $messageStore ); diff --git a/src/WebController.php b/src/WebController.php index 7d1260d..7b4d7a9 100644 --- a/src/WebController.php +++ b/src/WebController.php @@ -12,10 +12,10 @@ class WebController private Store $store; private string $version; - public function __construct(Store $store, string $version) + public function __construct(Store $store) { $this->store = $store; - $this->version = $version; + $this->version = Config::VERSION; } public function route(ServerRequestInterface $request): Response @@ -61,6 +61,7 @@ private function handleApiCall(ServerRequestInterface $request): Response if (preg_match('/^\/api\/messages\/([^\/]*)$/i', $path, $matches)) { $id = (string)$matches[1]; $message = $this->store->get($id); + return $this->json($message->toArray()); } @@ -69,10 +70,10 @@ private function handleApiCall(ServerRequestInterface $request): Response $attachmentId = (string)$matches[2]; $message = $this->store->get($messageId); $attachment = $message->getAttachment($attachmentId); + return $this->download( - $attachment->filename, - $attachment->content, - $attachment->type + $attachment->getFilename(), + $attachment->getContent() ); } } catch (Throwable $e) { @@ -170,8 +171,8 @@ private function download(string $filename, string $content): Response return new Response( 200, [ - 'Content-Type' => 'application/force-download', - 'Content-Disposition' => "attachment; filename=\"{$filename}\"", + 'Content-Type' => 'application/force-download', + 'Content-Disposition' => "attachment; filename=\"{$filename}\"", ], $content ); diff --git a/src/WebServer.php b/src/WebServer.php index 5df5898..3ad4bbf 100644 --- a/src/WebServer.php +++ b/src/WebServer.php @@ -13,20 +13,18 @@ class WebServer private StreamSelectLoop $loop; private Emails\Store $messageStore; private string $host; - private string $version; - public function __construct(string $host, string $version, StreamSelectLoop $loop, Store $messageStore) + public function __construct(string $host, StreamSelectLoop $loop, Store $messageStore) { $this->loop = $loop; $this->messageStore = $messageStore; $this->host = $host; - $this->version = $version; } public function start(): void { $server = new Server($this->loop, function (ServerRequestInterface $request) { - return (new WebController($this->messageStore, $this->version)) + return (new WebController($this->messageStore)) ->route($request); }); diff --git a/tests/IntegrationTest.php b/tests/Integration/IntegrationTest.php similarity index 98% rename from tests/IntegrationTest.php rename to tests/Integration/IntegrationTest.php index c4ca5e1..47b4b5e 100644 --- a/tests/IntegrationTest.php +++ b/tests/Integration/IntegrationTest.php @@ -1,6 +1,6 @@ loop->addTimer(2, function () { $this->process->terminate(); - $message = 'Process should have terminated within 3 seconds.'; + $message = 'Process should have terminated within 2 seconds.'; throw new ExpectationFailedException($message); }); } @@ -207,5 +207,4 @@ private function sendMail(): bool return $mail->send(); } - -} \ No newline at end of file +} diff --git a/tests/Traits/Messages.php b/tests/Traits/Messages.php new file mode 100644 index 0000000..5bb20a8 --- /dev/null +++ b/tests/Traits/Messages.php @@ -0,0 +1,36 @@ + "raw content", + 'sender' => "sender@example.com", + 'recipients' => ["recipient1@example.com", "recipient2@example.com"], + 'ccs' => ["cc1@example.com", "cc2@example.com"], + 'subject' => "My great subject is... 42", + 'html' => "

Hello World

", + 'text' => "Hello World", + 'reply_to' => "replyto@example.com", + 'all_recipients' => [ + "recipient1@example.com", + "recipient2@example.com", + "cc1@example.com", + "cc2@example.com", + "bcc1@example.com", + "bcc2@example.com" + ], + 'attachments' => [ + new Attachment("coupon.txt", "My coupont content", "text/plain") + ] + ], $override); + + return new Message(...array_values($params)); + } +} diff --git a/tests/ConfigTest.php b/tests/Unit/ConfigTest.php similarity index 97% rename from tests/ConfigTest.php rename to tests/Unit/ConfigTest.php index 3854510..1d8a1ac 100644 --- a/tests/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -1,6 +1,6 @@ assertNull($config->get('b')); $this->assertEquals('c_default_value', $config->get('c')); } -} \ No newline at end of file +} diff --git a/tests/Unit/Emails/MessageTest.php b/tests/Unit/Emails/MessageTest.php new file mode 100644 index 0000000..2cba41b --- /dev/null +++ b/tests/Unit/Emails/MessageTest.php @@ -0,0 +1,67 @@ +createMessage(); + + $this->assertEquals([ + "id" => $message->getId(), + "from" => "sender@example.com", + "reply_to" => "replyto@example.com", + "subject" => "My great subject is... 42", + "recipients" => ["recipient1@example.com", "recipient2@example.com"], + "ccs" => ["cc1@example.com", "cc2@example.com"], + "bccs" => ["bcc1@example.com", "bcc2@example.com"], + "html" => "

Hello World

", + "text" => "Hello World", + "attachments" => [ + [ + "id" => $message->getAttachments()[0]->getId(), + "name" => "coupon.txt", + "url" => "/api/messages/{$message->getId()}/attachments/{$message->getAttachments()[0]->getId()}" + ] + ], + "created_at" => $message->getCreatedAt()->format(Config::DATE_FORMAT), + "raw" => "raw content" + ], $message->toArray()); + } + + /** @test */ + public function can_be_converted_to_cli_table() + { + $message = $this->createMessage(); + + $this->assertEquals([ + ['Date', $message->getCreatedAt()->format(Config::DATE_FORMAT)], + ['Subject', 'My great subject is... 42'], + ['Excerpt', 'Hello World'], + ['To', 'recipient1@example.com; recipient2@example.com'], + ['From', 'sender@example.com'], + ['Reply-To', 'replyto@example.com'], + ['Cc', 'cc1@example.com; cc2@example.com'], + ['Bcc', 'bcc1@example.com; bcc2@example.com'], + ['Attachments', 'coupon.txt'] + ], $message->toTable()); + } + + /** @test */ + public function it_allows_to_retrieve_an_attachment_by_id() + { + $message = $this->createMessage(); + $attachmentId = $message->getAttachments()[0]->getId(); + + $this->assertInstanceOf(Attachment::class, $message->getAttachment($attachmentId)); + } +} diff --git a/tests/Unit/Emails/ParserTest.php b/tests/Unit/Emails/ParserTest.php new file mode 100644 index 0000000..5d0d61b --- /dev/null +++ b/tests/Unit/Emails/ParserTest.php @@ -0,0 +1,44 @@ +parse( + file_get_contents(dirname(__DIR__, 2) . "/fakes/email-raw-content.eml"), + ['bcc@example.com'] + ); + + $messageData = $message->toArray(); + unset($messageData['raw']); + + $this->assertEquals([ + "id" => $message->getId(), + "from" => "Mailer ", + "reply_to" => "Information ", + "subject" => "Here is the subject, welcome to New york!", + "recipients" => ["Joe User ", "ellen@example.com"], + "ccs" => ["cc@example.com"], + "bccs" => ["bcc@example.com"], + "html" => "This is the HTML message body in bold!\n\n", + "text" => "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n", + "attachments" => [ + [ + "id" => $message->getAttachments()[0]->getId(), + "name" => "vouchers.txt", + "url" => "/api/messages/{$message->getId()}/attachments/{$message->getAttachments()[0]->getId()}" + ] + ], + "created_at" => $message->getCreatedAt()->format(Config::DATE_FORMAT), + ], $messageData); + } +} diff --git a/tests/Unit/Emails/StoreTest.php b/tests/Unit/Emails/StoreTest.php new file mode 100644 index 0000000..a635ef9 --- /dev/null +++ b/tests/Unit/Emails/StoreTest.php @@ -0,0 +1,71 @@ +createMessage(); + + $store = new Store(); + + $store->store($message); + + $this->assertEquals($message, $store->get($message->getId())); + } + + /** @test */ + public function it_allows_to_register_callbacks_for_new_messages() + { + $message = $this->createMessage(); + + $store = new Store(); + + $store->onNewMessage(function (Message $message) use (&$result) { + $result = $message; + }); + + $store->store($message); + + $this->assertEquals($result, $message); + } + + /** @test */ + public function it_allows_to_retrieve_all_messages() + { + $message1 = $this->createMessage(); + $message2 = $this->createMessage(); + + $store = new Store(); + + $store->store($message1); + $store->store($message2); + + $this->assertEquals([ + [ + "id" => $message1->getId(), + "from" => $message1->getSender(), + "recipients" => $message1->getRecipients(), + "subject" => $message1->getSubject(), + "created_at" => $message1->getCreatedAt()->format(Config::DATE_FORMAT), + ], + [ + "id" => $message2->getId(), + "from" => $message2->getSender(), + "recipients" => $message2->getRecipients(), + "subject" => $message2->getSubject(), + "created_at" => $message2->getCreatedAt()->format(Config::DATE_FORMAT), + ] + ], $store->all()); + } +} diff --git a/tests/Unit/SmtpConnectionTest.php b/tests/Unit/SmtpConnectionTest.php new file mode 100644 index 0000000..adeed91 --- /dev/null +++ b/tests/Unit/SmtpConnectionTest.php @@ -0,0 +1,152 @@ +createMocks()); + + $this->expectWrite($connection, 220); + + $this->expectDispatch( + $dispatcher, + new Response(220, "Service ready") + ); + + $smtp->ready(); + } + + /** @test */ + public function it_allows_to_handle_EHLO_handshake() + { + $smtp = new SmtpConnection(...[$connection, $dispatcher] = $this->createMocks()); + + $this->expectWrite($connection, 250); + $this->expectDispatch( + $dispatcher, + new Request("EHLO localhost\r\n"), + new Response(250, "OK") + ); + + $smtp->handle("EHLO localhost\r\n"); + } + + /** @test */ + public function it_allows_to_handle_RCPT_TO_commands() + { + $smtp = new SmtpConnection(...[$connection, $dispatcher] = $this->createMocks()); + + $this->expectWrite($connection, 250); + $this->expectDispatch( + $dispatcher, + new Request("RCPT TO:\r\n"), + new Response(250, "OK") + ); + + $smtp->handle("RCPT TO:\r\n"); + } + + /** @test */ + public function it_allows_to_handle_DATA_commands() + { + $smtp = new SmtpConnection(...[$connection, $dispatcher] = $this->createMocks()); + + $this->expectWrite($connection, 354); + $this->expectDispatch( + $dispatcher, + new Request("DATA\r\n"), + new Response(354, "Start mail input; end with .") + ); + + $smtp->handle("DATA\r\n"); + } + + /** @test */ + public function it_allows_to_capture_a_new_message() + { + $smtp = new SmtpConnection(...[$connection, $dispatcher] = $this->createMocks()); + + $this->expectWrite($connection, 250, 354, 250); + $this->expectDispatch( + $dispatcher, + new Request("RCPT TO:\r\n"), + new Response(250, "OK"), + new Request("DATA\r\n"), + new Response(354, "Start mail input; end with ."), + new Request("My message content..."), + new Request("Final row\r\n.\r\n"), + new Response(250, "OK"), + new Message("My message content...Final row\r\n.\r\n", ["micc83@gmail.com"]) + ); + + $smtp->handle("RCPT TO:\r\n"); + $smtp->handle("DATA\r\n"); + $smtp->handle("My message content..."); + $smtp->handle("Final row\r\n.\r\n"); + } + + /** @test */ + public function it_allows_to_handle_QUIT_commands() + { + $smtp = new SmtpConnection(...[$connection, $dispatcher] = $this->createMocks()); + + $this->expectWrite($connection, 221); + $this->expectDispatch( + $dispatcher, + new Request("QUIT\r\n"), + new Response(221, "Service closing transmission channel") + ); + + $smtp->handle("QUIT\r\n"); + } + + /** + * @return array{0: ConnectionInterface|MockObject, 1?: EventDispatcher|MockObject} + */ + private function createMocks(): array + { + return [ + $this->createMock(ConnectionInterface::class), + $this->createMock(EventDispatcher::class) + ]; + } + + private function expectDispatch(MockObject $dispatcher, Event ...$responses): void + { + $dispatcher + ->expects(self::exactly(count($responses))) + ->method('dispatch') + ->withConsecutive(... array_map(function ($response) { + return [$response]; + }, $responses)); + } + + private function expectWrite(MockObject $connection, int ...$codes): void + { + $connection + ->expects(self::exactly(count($codes))) + ->method('write') + ->withConsecutive(... array_map(function (int $code) { + return ["{$code}\r\n"]; + }, $codes)); + } +} diff --git a/tests/WebSocketComponentTest.php b/tests/Unit/WebSocketComponentTest.php similarity index 85% rename from tests/WebSocketComponentTest.php rename to tests/Unit/WebSocketComponentTest.php index ade5eb7..ea268b5 100644 --- a/tests/WebSocketComponentTest.php +++ b/tests/Unit/WebSocketComponentTest.php @@ -1,21 +1,24 @@ createMessage(); $connection = $this->createMock(ConnectionInterface::class); @@ -27,4 +30,4 @@ public function a_websocket_message_is_sent_to_connect_clients_on_new_email() $websocket->onClose($connection); $store->store($message); } -} \ No newline at end of file +} diff --git a/tests/fakes/email-raw-content.eml b/tests/fakes/email-raw-content.eml new file mode 100644 index 0000000..eeff6f0 --- /dev/null +++ b/tests/fakes/email-raw-content.eml @@ -0,0 +1,40 @@ +Date: Tue, 25 Aug 2020 14:04:04 +0000 +To: Joe User , ellen@example.com +From: Mailer +Cc: cc@example.com +Reply-To: Information +Subject: Here is the subject, welcome to New york! +Message-ID: +X-Mailer: PHPMailer 6.1.7 (https://github.com/PHPMailer/PHPMailer) +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="b1_hc7z4TqDK3PzPcl5KoyBOIi2xeUqhpGZsBe2Bdta6Cs" +Content-Transfer-Encoding: 8bit + +This is a multi-part message in MIME format. + +--b1_hc7z4TqDK3PzPcl5KoyBOIi2xeUqhpGZsBe2Bdta6Cs +Content-Type: multipart/alternative; + boundary="b2_hc7z4TqDK3PzPcl5KoyBOIi2xeUqhpGZsBe2Bdta6Cs" + +--b2_hc7z4TqDK3PzPcl5KoyBOIi2xeUqhpGZsBe2Bdta6Cs +Content-Type: text/plain; charset=us-ascii + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +--b2_hc7z4TqDK3PzPcl5KoyBOIi2xeUqhpGZsBe2Bdta6Cs +Content-Type: text/html; charset=us-ascii + +This is the HTML message body in bold! + + +--b2_hc7z4TqDK3PzPcl5KoyBOIi2xeUqhpGZsBe2Bdta6Cs-- + +--b1_hc7z4TqDK3PzPcl5KoyBOIi2xeUqhpGZsBe2Bdta6Cs +Content-Type: text/plain; name=vouchers.txt +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=vouchers.txt + +TXkgZmlsZSBhdHRhY2htZW50Li4uLg== + +--b1_hc7z4TqDK3PzPcl5KoyBOIi2xeUqhpGZsBe2Bdta6Cs-- \ No newline at end of file diff --git a/todo.md b/todo.md index c50dec5..881123e 100644 --- a/todo.md +++ b/todo.md @@ -1,6 +1,6 @@ # ToDo - [ ] Write missing tests - - [ ] SmtpConnectionTest + - [x] SmtpConnectionTest - [ ] Email/MessageTest - [ ] Email/ParserTest - [ ] Email/StoreTest @@ -11,12 +11,13 @@ - [ ] Add coverage - [ ] Pass static analysis - [ ] Add bin for global install -- [ ] Add license (custom MIT?) +- [ ] Double check SmtpConnection DATA extrapolation +- [ ] Verify other packages license - [ ] Fix issue with top right menu position on show more - [ ] Allow forwarding emails -- [ ] Allow to get the version from CLI -- [ ] Allow to get help from the CLI -- [ ] Double check SmtpConnection DATA extrapolation +- [x] Allow to get the version from CLI +- [x] Allow to get help from the CLI +- [x] Add license MIT - [x] Check different color scheme - [x] Fix issue with pre height + overflow: auto - [x] Check name is free