From acb93cc4357866926c3a83e297b13a0a0dc7fe17 Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Wed, 19 Nov 2025 23:04:07 +0000 Subject: [PATCH 01/11] Added initial version of Mailtrap transport. --- src/Illuminate/Mail/MailManager.php | 19 +++ .../Mail/Transport/MailtrapTransport.php | 124 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/Illuminate/Mail/Transport/MailtrapTransport.php diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index c1ca0f4cb8a9..9d62b721e2be 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -9,6 +9,7 @@ use Illuminate\Log\LogManager; use Illuminate\Mail\Transport\ArrayTransport; use Illuminate\Mail\Transport\LogTransport; +use Illuminate\Mail\Transport\MailtrapTransport; use Illuminate\Mail\Transport\ResendTransport; use Illuminate\Mail\Transport\SesTransport; use Illuminate\Mail\Transport\SesV2Transport; @@ -16,6 +17,7 @@ use Illuminate\Support\ConfigurationUrlParser; use Illuminate\Support\Str; use InvalidArgumentException; +use Mailtrap\MailtrapClient; use Psr\Log\LoggerInterface; use Resend; use Symfony\Component\HttpClient\HttpClient; @@ -321,6 +323,23 @@ protected function createResendTransport(array $config) ); } + /** + * Create an instance of the Mailtrap Transport driver. + * + * @param array $config + * @return \Illuminate\Mail\Transport\MailtrapTransport + */ + protected function createMailtrapTransport(array $config) + { + return new MailtrapTransport( + MailtrapClient::initSendingEmails( + $config['key'], + $config['is_bulk'], + $config['inbox_id'], + ), + ); + } + /** * Create an instance of the Symfony Mail Transport driver. * diff --git a/src/Illuminate/Mail/Transport/MailtrapTransport.php b/src/Illuminate/Mail/Transport/MailtrapTransport.php new file mode 100644 index 000000000000..0426c2e35f96 --- /dev/null +++ b/src/Illuminate/Mail/Transport/MailtrapTransport.php @@ -0,0 +1,124 @@ +getOriginalMessage()); + + $envelope = $message->getEnvelope(); + + $headers = []; + + $headersToBypass = ['from', 'to', 'cc', 'bcc', 'reply-to', 'sender', 'subject', 'content-type']; + + foreach ($email->getHeaders()->all() as $name => $header) { + if (in_array($name, $headersToBypass, true)) { + continue; + } + + $headers[$header->getName()] = $header->getBodyAsString(); + } + + $attachments = []; + + if ($email->getAttachments()) { + foreach ($email->getAttachments() as $attachment) { + $attachmentHeaders = $attachment->getPreparedHeaders(); + $contentType = $attachmentHeaders->get('Content-Type')->getBody(); + $disposition = $attachmentHeaders->getHeaderBody('Content-Disposition'); + $filename = $attachmentHeaders->getHeaderParameter('Content-Disposition', 'filename'); + + if ($contentType == 'text/calendar') { + $content = $attachment->getBody(); + } else { + $content = str_replace("\r\n", '', $attachment->bodyToString()); + } + + $item = [ + 'content_type' => $contentType, + 'content' => $content, + 'filename' => $filename, + ]; + + if ($disposition === 'inline') { + $item['content_id'] = $attachment->hasContentId() ? $attachment->getContentId() : $filename; + } + + $attachments[] = $item; + } + } + + try { + // TODO Add headers. + // TODO Add attachments. + $email = (new MailtrapEmail) + ->from($envelope->getSender()) + ->to(...$this->getRecipients($email, $envelope)) + ->cc(...$email->getCc()) + ->bcc(...$email->getBcc()) + ->replyTo(...$email->getReplyTo()) + ->subject($email->getSubject()) + ->html($email->getHtmlBody()) + ->text($email->getTextBody()); + + $this->mailtrap->send($email); + + // TODO Update this exception to be correct + throw_if( + isset($result['statusCode']) && $result['statusCode'] != Response::HTTP_OK, + Exception::class, + $result['message'], + ); + } catch (Exception $exception) { + throw new TransportException( + sprintf('Request to Mailtrap API failed. Reason: %s.', $exception->getMessage()), + is_int($exception->getCode()) ? $exception->getCode() : 0, + $exception + ); + } + } + + /** + * Get the recipients without CC or BCC. + */ + protected function getRecipients(Email $email, Envelope $envelope): array + { + return array_filter($envelope->getRecipients(), function (Address $address) use ($email): bool { + return in_array($address, array_merge($email->getCc(), $email->getBcc()), true) === false; + }); + } + + /** + * Get the string representation of the transport. + */ + public function __toString(): string + { + return 'mailtrap'; + } +} From 240f1e5882df76687cfe79e3cc99955c607e6322 Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Wed, 19 Nov 2025 23:43:30 +0000 Subject: [PATCH 02/11] Add headers and attachments to Mailtrap emails. --- .../Mail/Transport/MailtrapTransport.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Mail/Transport/MailtrapTransport.php b/src/Illuminate/Mail/Transport/MailtrapTransport.php index 0426c2e35f96..3e07d376e7e1 100644 --- a/src/Illuminate/Mail/Transport/MailtrapTransport.php +++ b/src/Illuminate/Mail/Transport/MailtrapTransport.php @@ -75,8 +75,6 @@ protected function doSend(SentMessage $message): void } try { - // TODO Add headers. - // TODO Add attachments. $email = (new MailtrapEmail) ->from($envelope->getSender()) ->to(...$this->getRecipients($email, $envelope)) @@ -87,11 +85,22 @@ protected function doSend(SentMessage $message): void ->html($email->getHtmlBody()) ->text($email->getTextBody()); - $this->mailtrap->send($email); + foreach ($headers as $headerName => $headerBody) { + $email->getHeaders()->addTextHeader($headerName, $headerBody); + } + + foreach ($attachments as $attachment) { + $email->attach( + $attachment['content'], + $attachment['filename'], + $attachment['content_type'], + ); + } + + $result = $this->mailtrap->send($email); - // TODO Update this exception to be correct throw_if( - isset($result['statusCode']) && $result['statusCode'] != Response::HTTP_OK, + $result->getStatusCode() !== Response::HTTP_OK, Exception::class, $result['message'], ); From 5cac75277d2a939d626c8ce4668dcdc36f0f7b1c Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Wed, 19 Nov 2025 23:43:40 +0000 Subject: [PATCH 03/11] Add missing argument. --- src/Illuminate/Mail/MailManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index 9d62b721e2be..a05e1238060d 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -335,6 +335,7 @@ protected function createMailtrapTransport(array $config) MailtrapClient::initSendingEmails( $config['key'], $config['is_bulk'], + $config['is_sandbox'], $config['inbox_id'], ), ); From e3fd3ea6b566705217736c0e5681540710f9fc02 Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Wed, 19 Nov 2025 23:51:49 +0000 Subject: [PATCH 04/11] Split method into smaller methods. --- .../Mail/Transport/MailtrapTransport.php | 97 +++++++++++-------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/src/Illuminate/Mail/Transport/MailtrapTransport.php b/src/Illuminate/Mail/Transport/MailtrapTransport.php index 3e07d376e7e1..c46aa881038f 100644 --- a/src/Illuminate/Mail/Transport/MailtrapTransport.php +++ b/src/Illuminate/Mail/Transport/MailtrapTransport.php @@ -33,18 +33,27 @@ protected function doSend(SentMessage $message): void $envelope = $message->getEnvelope(); - $headers = []; - - $headersToBypass = ['from', 'to', 'cc', 'bcc', 'reply-to', 'sender', 'subject', 'content-type']; + $mailtrapEmail = $this->prepareMailtrapEmail($email, $envelope); - foreach ($email->getHeaders()->all() as $name => $header) { - if (in_array($name, $headersToBypass, true)) { - continue; - } + try { + $result = $this->mailtrap->send($mailtrapEmail); - $headers[$header->getName()] = $header->getBodyAsString(); + throw_if( + $result->getStatusCode() !== Response::HTTP_OK, + Exception::class, + $result['message'], + ); + } catch (Exception $exception) { + throw new TransportException( + sprintf('Request to Mailtrap API failed. Reason: %s.', $exception->getMessage()), + is_int($exception->getCode()) ? $exception->getCode() : 0, + $exception + ); } + } + protected function determineAttachments(Email $email): array + { $attachments = []; if ($email->getAttachments()) { @@ -54,7 +63,7 @@ protected function doSend(SentMessage $message): void $disposition = $attachmentHeaders->getHeaderBody('Content-Disposition'); $filename = $attachmentHeaders->getHeaderParameter('Content-Disposition', 'filename'); - if ($contentType == 'text/calendar') { + if ($contentType === 'text/calendar') { $content = $attachment->getBody(); } else { $content = str_replace("\r\n", '', $attachment->bodyToString()); @@ -74,43 +83,51 @@ protected function doSend(SentMessage $message): void } } - try { - $email = (new MailtrapEmail) - ->from($envelope->getSender()) - ->to(...$this->getRecipients($email, $envelope)) - ->cc(...$email->getCc()) - ->bcc(...$email->getBcc()) - ->replyTo(...$email->getReplyTo()) - ->subject($email->getSubject()) - ->html($email->getHtmlBody()) - ->text($email->getTextBody()); - - foreach ($headers as $headerName => $headerBody) { - $email->getHeaders()->addTextHeader($headerName, $headerBody); - } + return $attachments; + } - foreach ($attachments as $attachment) { - $email->attach( - $attachment['content'], - $attachment['filename'], - $attachment['content_type'], - ); + protected function determineHeaders(Email $email): array + { + $headers = []; + + $headersToBypass = ['from', 'to', 'cc', 'bcc', 'reply-to', 'sender', 'subject', 'content-type']; + + foreach ($email->getHeaders()->all() as $name => $header) { + if (in_array($name, $headersToBypass, true)) { + continue; } - $result = $this->mailtrap->send($email); + $headers[$header->getName()] = $header->getBodyAsString(); + } - throw_if( - $result->getStatusCode() !== Response::HTTP_OK, - Exception::class, - $result['message'], - ); - } catch (Exception $exception) { - throw new TransportException( - sprintf('Request to Mailtrap API failed. Reason: %s.', $exception->getMessage()), - is_int($exception->getCode()) ? $exception->getCode() : 0, - $exception + return $headers; + } + + protected function prepareMailtrapEmail(Email $email, Envelope $envelope): MailtrapEmail + { + $mailtrapEmail = (new MailtrapEmail) + ->from($envelope->getSender()) + ->to(...$this->getRecipients($email, $envelope)) + ->cc(...$email->getCc()) + ->bcc(...$email->getBcc()) + ->replyTo(...$email->getReplyTo()) + ->subject($email->getSubject()) + ->html($email->getHtmlBody()) + ->text($email->getTextBody()); + + foreach ($this->determineHeaders($email) as $headerName => $headerBody) { + $email->getHeaders()->addTextHeader($headerName, $headerBody); + } + + foreach ($this->determineAttachments($email) as $attachment) { + $email->attach( + $attachment['content'], + $attachment['filename'], + $attachment['content_type'], ); } + + return $mailtrapEmail; } /** From 2ba189eb33ffe7db8de3faea58e7260360a5894c Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Wed, 19 Nov 2025 23:55:30 +0000 Subject: [PATCH 05/11] Added docblocks. --- .../Mail/Transport/MailtrapTransport.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Illuminate/Mail/Transport/MailtrapTransport.php b/src/Illuminate/Mail/Transport/MailtrapTransport.php index c46aa881038f..c145f1069840 100644 --- a/src/Illuminate/Mail/Transport/MailtrapTransport.php +++ b/src/Illuminate/Mail/Transport/MailtrapTransport.php @@ -52,6 +52,16 @@ protected function doSend(SentMessage $message): void } } + /** + * Determine the attachments for the email. + * + * @return list + */ protected function determineAttachments(Email $email): array { $attachments = []; @@ -75,6 +85,7 @@ protected function determineAttachments(Email $email): array 'filename' => $filename, ]; + // TODO Is this needed? if ($disposition === 'inline') { $item['content_id'] = $attachment->hasContentId() ? $attachment->getContentId() : $filename; } @@ -86,6 +97,11 @@ protected function determineAttachments(Email $email): array return $attachments; } + /** + * Determine the headers for the email. + * + * @return array + */ protected function determineHeaders(Email $email): array { $headers = []; @@ -103,6 +119,9 @@ protected function determineHeaders(Email $email): array return $headers; } + /** + * Build the Mailtrap email instance which will be sent. + */ protected function prepareMailtrapEmail(Email $email, Envelope $envelope): MailtrapEmail { $mailtrapEmail = (new MailtrapEmail) From 6edaccdd44c88710d6eace40d2cd86f9024647bd Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Mon, 1 Dec 2025 14:12:04 +0000 Subject: [PATCH 06/11] Simplify the Mailtrap transport. --- .../Mail/Transport/MailtrapTransport.php | 117 +----------------- 1 file changed, 1 insertion(+), 116 deletions(-) diff --git a/src/Illuminate/Mail/Transport/MailtrapTransport.php b/src/Illuminate/Mail/Transport/MailtrapTransport.php index c145f1069840..3cf88c149841 100644 --- a/src/Illuminate/Mail/Transport/MailtrapTransport.php +++ b/src/Illuminate/Mail/Transport/MailtrapTransport.php @@ -4,14 +4,10 @@ use Exception; use Mailtrap\Api\EmailsSendApiInterface; -use Mailtrap\Mime\MailtrapEmail; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; -use Symfony\Component\Mime\Address; -use Symfony\Component\Mime\Email; use Symfony\Component\Mime\MessageConverter; class MailtrapTransport extends AbstractTransport @@ -31,12 +27,8 @@ protected function doSend(SentMessage $message): void { $email = MessageConverter::toEmail($message->getOriginalMessage()); - $envelope = $message->getEnvelope(); - - $mailtrapEmail = $this->prepareMailtrapEmail($email, $envelope); - try { - $result = $this->mailtrap->send($mailtrapEmail); + $result = $this->mailtrap->send($email); throw_if( $result->getStatusCode() !== Response::HTTP_OK, @@ -52,113 +44,6 @@ protected function doSend(SentMessage $message): void } } - /** - * Determine the attachments for the email. - * - * @return list - */ - protected function determineAttachments(Email $email): array - { - $attachments = []; - - if ($email->getAttachments()) { - foreach ($email->getAttachments() as $attachment) { - $attachmentHeaders = $attachment->getPreparedHeaders(); - $contentType = $attachmentHeaders->get('Content-Type')->getBody(); - $disposition = $attachmentHeaders->getHeaderBody('Content-Disposition'); - $filename = $attachmentHeaders->getHeaderParameter('Content-Disposition', 'filename'); - - if ($contentType === 'text/calendar') { - $content = $attachment->getBody(); - } else { - $content = str_replace("\r\n", '', $attachment->bodyToString()); - } - - $item = [ - 'content_type' => $contentType, - 'content' => $content, - 'filename' => $filename, - ]; - - // TODO Is this needed? - if ($disposition === 'inline') { - $item['content_id'] = $attachment->hasContentId() ? $attachment->getContentId() : $filename; - } - - $attachments[] = $item; - } - } - - return $attachments; - } - - /** - * Determine the headers for the email. - * - * @return array - */ - protected function determineHeaders(Email $email): array - { - $headers = []; - - $headersToBypass = ['from', 'to', 'cc', 'bcc', 'reply-to', 'sender', 'subject', 'content-type']; - - foreach ($email->getHeaders()->all() as $name => $header) { - if (in_array($name, $headersToBypass, true)) { - continue; - } - - $headers[$header->getName()] = $header->getBodyAsString(); - } - - return $headers; - } - - /** - * Build the Mailtrap email instance which will be sent. - */ - protected function prepareMailtrapEmail(Email $email, Envelope $envelope): MailtrapEmail - { - $mailtrapEmail = (new MailtrapEmail) - ->from($envelope->getSender()) - ->to(...$this->getRecipients($email, $envelope)) - ->cc(...$email->getCc()) - ->bcc(...$email->getBcc()) - ->replyTo(...$email->getReplyTo()) - ->subject($email->getSubject()) - ->html($email->getHtmlBody()) - ->text($email->getTextBody()); - - foreach ($this->determineHeaders($email) as $headerName => $headerBody) { - $email->getHeaders()->addTextHeader($headerName, $headerBody); - } - - foreach ($this->determineAttachments($email) as $attachment) { - $email->attach( - $attachment['content'], - $attachment['filename'], - $attachment['content_type'], - ); - } - - return $mailtrapEmail; - } - - /** - * Get the recipients without CC or BCC. - */ - protected function getRecipients(Email $email, Envelope $envelope): array - { - return array_filter($envelope->getRecipients(), function (Address $address) use ($email): bool { - return in_array($address, array_merge($email->getCc(), $email->getBcc()), true) === false; - }); - } - /** * Get the string representation of the transport. */ From 00bcd8fd675ccb57c0bb306c208e8d2b1269449b Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Tue, 2 Dec 2025 16:59:35 +0000 Subject: [PATCH 07/11] Remove unneeded status code check. --- src/Illuminate/Mail/Transport/MailtrapTransport.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Illuminate/Mail/Transport/MailtrapTransport.php b/src/Illuminate/Mail/Transport/MailtrapTransport.php index 3cf88c149841..d367375c76de 100644 --- a/src/Illuminate/Mail/Transport/MailtrapTransport.php +++ b/src/Illuminate/Mail/Transport/MailtrapTransport.php @@ -28,13 +28,7 @@ protected function doSend(SentMessage $message): void $email = MessageConverter::toEmail($message->getOriginalMessage()); try { - $result = $this->mailtrap->send($email); - - throw_if( - $result->getStatusCode() !== Response::HTTP_OK, - Exception::class, - $result['message'], - ); + $this->mailtrap->send($email); } catch (Exception $exception) { throw new TransportException( sprintf('Request to Mailtrap API failed. Reason: %s.', $exception->getMessage()), From cdbd01a0ec40012c122a6290875cbfb7e3efe973 Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Tue, 2 Dec 2025 17:02:24 +0000 Subject: [PATCH 08/11] Cast the Mailtrap config values correctly. --- src/Illuminate/Mail/MailManager.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index a05e1238060d..fe35f7b2d5c9 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -333,10 +333,10 @@ protected function createMailtrapTransport(array $config) { return new MailtrapTransport( MailtrapClient::initSendingEmails( - $config['key'], - $config['is_bulk'], - $config['is_sandbox'], - $config['inbox_id'], + $config['key'] ?? $this->app['config']->get('services.mailtrap.key'), + (bool) ($config['is_bulk'] ?? $this->app['config']->get('services.mailtrap.is_bulk')), + (bool) ($config['is_sandbox'] ?? $this->app['config']->get('services.mailtrap.is_sandbox')), + $config['inbox_id'] ?? $this->app['config']->get('services.mailtrap.inbox_id'), ), ); } From 53a5211115647851d80bdcb3a75ccb172eebbf4e Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Tue, 2 Dec 2025 17:37:33 +0000 Subject: [PATCH 09/11] Remove unneeded import. --- src/Illuminate/Mail/Transport/MailtrapTransport.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Mail/Transport/MailtrapTransport.php b/src/Illuminate/Mail/Transport/MailtrapTransport.php index d367375c76de..edbd1c6625b3 100644 --- a/src/Illuminate/Mail/Transport/MailtrapTransport.php +++ b/src/Illuminate/Mail/Transport/MailtrapTransport.php @@ -4,7 +4,6 @@ use Exception; use Mailtrap\Api\EmailsSendApiInterface; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; From 929e4c0ae317034ca0cbc20a8ad30c84d3e3f818 Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Tue, 2 Dec 2025 19:43:26 +0000 Subject: [PATCH 10/11] Added config fields for Mailtrap. --- config/mail.php | 4 ++++ config/services.php | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/config/mail.php b/config/mail.php index 22c03b032d76..489c1a1a2480 100644 --- a/config/mail.php +++ b/config/mail.php @@ -65,6 +65,10 @@ 'transport' => 'resend', ], + 'mailtrap' => [ + 'transport' => 'mailtrap', + ], + 'sendmail' => [ 'transport' => 'sendmail', 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), diff --git a/config/services.php b/config/services.php index 6182e4b90c94..09291c7e013b 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,10 @@ ], ], + 'mailtrap' => [ + 'key' => env('MAILTRAP_API_KEY'), + 'is_sandbox' => env('MAILTRAP_SANDBOX_ENABLED', false), + 'inbox_id' => env('MAILTRAP_INBOX_ID'), + ], + ]; From 9b60d836249fbda4782ae02e7cef07865e750d04 Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Tue, 2 Dec 2025 20:22:24 +0000 Subject: [PATCH 11/11] Fix PHPStan errors for Mailtrap integration. --- phpstan.src.neon.dist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpstan.src.neon.dist b/phpstan.src.neon.dist index 59996713063e..ca01b0886c6f 100644 --- a/phpstan.src.neon.dist +++ b/phpstan.src.neon.dist @@ -15,3 +15,5 @@ parameters: - "#has invalid type#" - "#Instantiated class [a-zA-Z0-9\\\\_]+ not found.#" - "#Unsafe usage of new static#" + - '#^Call to static method initSendingEmails\(\) on an unknown class Mailtrap\\MailtrapClient\.$#' + - '#^Property Illuminate\\Mail\\Transport\\MailtrapTransport\:\:\$mailtrap has unknown class Mailtrap\\Api\\EmailsSendApiInterface as its type\.$#'