diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 190b4da..184fa62 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -15,7 +15,6 @@ jobs: - ubuntu-latest php: - - "8.1" - "8.2" - "8.3" diff --git a/.laminas-ci.json b/.laminas-ci.json new file mode 100644 index 0000000..790cfe4 --- /dev/null +++ b/.laminas-ci.json @@ -0,0 +1,6 @@ +{ + "ignore_php_platform_requirements": { + "8.4": true + }, + "backwardCompatibilityCheck": true +} diff --git a/README.md b/README.md index 2fadd40..09c8d2c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ # dot-mail > [!IMPORTANT] -> dot-mail is a wrapper on top of [laminas-mail](https://github.com/laminas/laminas-mail) -> -> ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Flaminas%2Flaminas-mail%2Fproperties%2Fvalues&query=%24%5B%3F(%40.property_name%3D%3D%22maintenance-mode%22)%5D.value&label=Maintenance%20Mode&color=%23d43442) +> dot-mail is a wrapper on top of [symfony mailer](https://github.com/symfony/mailer) ## dot-mail badges ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-mail) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-mail/4.1.1) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-mail/5.0.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-mail)](https://github.com/dotkernel/dot-mail/network) @@ -18,7 +16,13 @@ [![Build Static](https://github.com/dotkernel/dot-mail/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0)](https://github.com/dotkernel/dot-mail/actions/workflows/continuous-integration.yml) [![codecov](https://codecov.io/gh/dotkernel/dot-mail/branch/4.0/graph/badge.svg?token=G51NEHYKD3)](https://codecov.io/gh/dotkernel/dot-mail) -[![SymfonyInsight](https://insight.symfony.com/projects/1995ea7c-3b34-4eee-ac48-3571860d0307/big.svg)](https://insight.symfony.com/projects/1995ea7c-3b34-4eee-ac48-3571860d0307) +## Installation + +Install `dotkernel/dot-mail` by executing the following Composer command: + +```shell +composer require dotkernel/dot-mail +``` ## Configuration @@ -32,7 +36,7 @@ return [ 'dot_mail' => [ 'default' => [ //... - 'transport' => Laminas\Mail\Transport\Sendmail::class, + 'transport' => Symfony\Component\Mailer\Transport\Smtp\SmtpTransport::class, //... ] ] @@ -61,7 +65,7 @@ return [ 'dot_mail' => [ 'default' => [ //... - 'transport' => Laminas\Mail\Transport\Smtp::class, + 'transport' => Symfony\Component\Mailer\Transport\Smtp\SmtpTransport::class, 'message_options' => [ 'from' => '', //... @@ -137,7 +141,7 @@ if (! $result->isValid()) { } ``` -**Note : Invalid e-mail messages will not be sent.** +> Invalid e-mail messages will not be sent. ### Logging outgoing emails @@ -161,26 +165,3 @@ return [ ``` To disable it, set the value of `sent` to `null`. - -### Saving a copy of an outgoing mail into a folder - -### Valid only for SMTP Transport - -First, make sure the `save_sent_message_folder` key is present in config file `mail.local.php` under `dot_mail.default`. Below you can see its placement and default value. - -```php - [ - 'default' => [ - ... - 'save_sent_message_folder' => ['INBOX.Sent'] - ], - ], -]; -``` - -Common folder names are `INBOX`, `INBOX.Archive`, `INBOX.Drafts`, `INBOX.Sent`, `INBOX.Spam`, `INBOX.Trash`. If you have `MailService` available in your class, you can call `$this->mailService->getFolderGlobalNames()` to list the folder global names for the email you are using. - -Multiple folders can be added to the `save_sent_message_folder` key to save a copy of the outgoing email in each folder. diff --git a/SECURITY.md b/SECURITY.md index d2f1064..ca1e3ab 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,8 @@ | Version | Supported | PHP Version | |---------|--------------------|----------------------------------------------------------------------------------------------------------| -| 4.x | :white_check_mark: | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-mail/4.1.1) | +| 5.x | :white_check_mark: | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-mail/5.0.0) | +| 4.x | :white_check_mark: | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-mail/4.3.0) | | <= 3.x | :x: | | diff --git a/composer.json b/composer.json index aa5f497..0530931 100644 --- a/composer.json +++ b/composer.json @@ -1,21 +1,22 @@ { "name": "dotkernel/dot-mail", "type": "library", - "description": "DotKernel mail component based on laminas-mail", + "description": "Dotkernel mail component based on symfony mailer", "license": "MIT", "homepage": "https://github.com/dotkernel/dot-mail", "keywords": [ "mail", "services", - "laminas", - "laminas-mail", + "symfony", + "mailer", "event", "dot-event", + "laminas", "laminas-dependency" ], "authors": [ { - "name": "DotKernel Team", + "name": "Dotkernel Team", "email": "team@dotkernel.com" } ], @@ -26,15 +27,15 @@ } }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "ext-fileinfo": "*", "ext-json": "*", "laminas/laminas-servicemanager": "^3.22", - "laminas/laminas-mail": "^2.25", - "dotkernel/dot-event": "^3.4" + "dotkernel/dot-event": "^3.4", + "symfony/mailer": "v7.1.6" }, "require-dev": { - "laminas/laminas-coding-standard": "^2.5", + "laminas/laminas-coding-standard": "^3.0", "mikey179/vfsstream": "^v1.6.11", "phpunit/phpunit": "^10.5", "vimeo/psalm": "^5.23" diff --git a/config/mail.global.php.dist b/config/mail.global.php.dist index 41ae410..fc90aa3 100644 --- a/config/mail.global.php.dist +++ b/config/mail.global.php.dist @@ -3,7 +3,7 @@ return [ /** - * DotKernel mail module configuration + * Dotkernel mail module configuration * Note that many of these options can be set programmatically too, when sending mail messages * actually that is what you'll usually do, these config provide just default and options that remain the same for all mails */ @@ -16,18 +16,16 @@ return [ /** * the mail transport to use - * can be any class implementing Laminas\Mail\Transport\TransportInterface + * can be any class implementing Symfony\Component\Mailer\Transport\TransportInterface * * for standard mail transports, you can use these aliases - * - sendmail => Laminas\Mail\Transport\Sendmail - * - smtp => Laminas\Mail\Transport\Smtp - * - file => Laminas\Mail\Transport\File - * - in_memory => Laminas\Mail\Transport\InMemory + * - sendmail => Symfony\Component\Mailer\Transport\SendmailTransport + * - smtp => Symfony\Component\Mailer\Transport\Smtp\SmtpTransport * * defaults to sendmail **/ - 'transport' => \Laminas\Mail\Transport\Sendmail::class, + 'transport' => Symfony\Component\Mailer\Transport\Smtp\SmtpTransport::class, // Uncomment the below line if you want to save a copy of all sent emails to a certain IMAP folder // Valid only if the Transport is SMTP @@ -79,7 +77,7 @@ return [ ], ], - //options that will be used only if Laminas\Mail\Transport\Smtp adapter is used + //options that will be used only if Symfony\Component\Mailer\Transport\Smtp\SmtpTransport adapter is used 'smtp_options' => [ //hostname or IP address of the mail server @@ -106,18 +104,6 @@ return [ ] ], - //file options that will be used only if the adapter is Laminas\Mail\Transport\File - /*'file_options' => [ - - //this is the folder where the file is going to be saved - //default value is 'data/mail/output' - 'path' => 'data/mail/output', - - //a callable that will get the Laminas\Mail\Transport\File object as an argument and should return the filename - //if null is used, and empty callable will be used - //'callback' => null, - ],*/ - //listeners to register with the mail service, for mail events 'event_listeners' => [ //[ diff --git a/docs/book/index.md b/docs/book/index.md deleted file mode 100644 index ae42a26..0000000 --- a/docs/book/index.md +++ /dev/null @@ -1 +0,0 @@ -../../README.md diff --git a/docs/book/index.md b/docs/book/index.md new file mode 120000 index 0000000..fe84005 --- /dev/null +++ b/docs/book/index.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/docs/book/v4/configuration.md b/docs/book/v4/configuration.md index f8ab345..ef84037 100644 --- a/docs/book/v4/configuration.md +++ b/docs/book/v4/configuration.md @@ -1,7 +1,7 @@ # Configuration Register `dot-mail` in you project by adding `Dot\Mail\ConfigProvider::class` to your configuration aggregator (to `config/config.php` for example). -After registering the `ConfigProvider` load the configuration file (`config/mail.global.php.dist`) by removing it's `.dist` extension and adding it to the `config/autoload` folder. +After registering the `ConfigProvider` copy the configuration file `config/mail.global.php.dist` into your project's `config/autoload/` directory as `mail.global.php`. The resulting `mail.global.php` contains the necessary configurations for all available transport types, message options and logging options in one place. The config file provides a set of default values available to all mails under the `dot-mail.default` key. @@ -30,12 +30,12 @@ The `dot_mail.default.save_sent_message_folder` key may be uncommented when usin Using `Laminas\Mail\Transport\File` as the transport will require uncommenting the `dot-mail.default.file_options` key. -- Note: the configured path must be a writable directory +> The configured path must be a writable directory ```php 'file_options' => [ - 'path' => 'data/mail/output', - //'callback' => null, + 'path' => 'data/mail/output', + //'callback' => null, ], ``` diff --git a/docs/book/v4/installation.md b/docs/book/v4/installation.md index d0eff73..faf95d5 100644 --- a/docs/book/v4/installation.md +++ b/docs/book/v4/installation.md @@ -2,4 +2,6 @@ Install `dotkernel/dot-mail` by executing the following Composer command: - composer require dotkernel/dot-mail +```shell +composer require dotkernel/dot-mail +``` diff --git a/docs/book/v4/overview.md b/docs/book/v4/overview.md index 3be8047..0fc3a17 100644 --- a/docs/book/v4/overview.md +++ b/docs/book/v4/overview.md @@ -1,8 +1,6 @@ # Overview > dot-mail is a wrapper on top of [laminas-mail](https://github.com/laminas/laminas-mail) -> -> ![OSS Lifecycle](https://img.shields.io/osslifecycle/laminas/laminas-mail) ## Extra features diff --git a/docs/book/v4/transports.md b/docs/book/v4/transports.md index 9216861..406f0e9 100644 --- a/docs/book/v4/transports.md +++ b/docs/book/v4/transports.md @@ -7,7 +7,7 @@ - `Laminas\Mail\Transport\File` - `Laminas\Mail\Transport\InMemory` -- Note: feel free to use any custom transport you desire, provided it implements the mentioned `TransportInterface`. +> Feel free to use any custom transport you desire, provided it implements the mentioned `TransportInterface`. `Sendmail` is a wrapper over PHP's `mail()` function, and as such has a different behaviour on Windows than on *nix systems. Using sendmail on Windows **will not work in combination with** `addBcc()`. diff --git a/docs/book/v4/usage.md b/docs/book/v4/usage.md index c1f6593..e4834ab 100644 --- a/docs/book/v4/usage.md +++ b/docs/book/v4/usage.md @@ -50,7 +50,7 @@ if (! $result->isValid()) { } ``` -**Note : Invalid e-mail messages will not be sent.** +> Invalid e-mail messages will not be sent. ## Logging outgoing emails diff --git a/docs/book/v5/configuration.md b/docs/book/v5/configuration.md new file mode 100644 index 0000000..93b330b --- /dev/null +++ b/docs/book/v5/configuration.md @@ -0,0 +1,55 @@ +# Configuration + +Register `dot-mail` in you project by adding `Dot\Mail\ConfigProvider::class` to your configuration aggregator (to `config/config.php` for example). +After registering the `ConfigProvider` copy the configuration file `config/mail.global.php.dist` into your project's `config/autoload/` directory as `mail.global.php`. + +The resulting `mail.global.php` contains the necessary configurations for all available transport types, message options and logging options in one place. The config file provides a set of default values available to all mails under the `dot-mail.default` key. + +Many of these options can be programmatically set when sending the actual email. + +An example of this is the `dot-mail.default.message_options` key, which can be filled in with default message values to be available to all emails. +When sending the actual email, these values can be overwritten by using an available "setter" function, supplemented by using "add" functions or simply left as the default. + +```php +// setter will overwrite existing destination email +$this->mailService->getMessage()->setTo("receiver@email.com"); + +// existing destination email kept, new destination email added alongside it +$this->mailService->getMessage()->addTo("receiver@email.com"); +``` + +## Transport configuration + +`dot-mail` uses the `transport` key under the main `dot_mail` configuration key to select the email transport. +It has four email transport classes available by default (`SmtpTransport`), one of which is to be added under the `dot_mail.transport` key for use. + +Sending email with the `Smtp` transport requires valid data for the values under `dot-mail.default.smtp_options`, which is only used in this case. + +> The configured path must be a writable directory + +```php +'file_options' => [ + 'path' => 'data/mail/output', + //'callback' => null, +], +``` + +## Logging configuration + +Uncommenting the `dot-mail.log` key will save a copy of all sent emails' subject, recipient addresses, cc and bcc addresses alongside a timestamp. +In order to enable it, make sure that your `mail.local.php` has the below `log` configuration under the `dot_mail` key: + +```php + [ + ... + 'log' => [ + 'sent' => getcwd() . '/log/mail/sent.log' + ] + ] +]; +``` + +To disable it, set the value of `sent` to `null`. diff --git a/docs/book/v5/installation.md b/docs/book/v5/installation.md new file mode 100644 index 0000000..faf95d5 --- /dev/null +++ b/docs/book/v5/installation.md @@ -0,0 +1,7 @@ +# Installation + +Install `dotkernel/dot-mail` by executing the following Composer command: + +```shell +composer require dotkernel/dot-mail +``` diff --git a/docs/book/v5/overview.md b/docs/book/v5/overview.md new file mode 100644 index 0000000..706ac44 --- /dev/null +++ b/docs/book/v5/overview.md @@ -0,0 +1,7 @@ +# Overview + +> dot-mail is a wrapper on top of [symfony mailer](https://github.com/symfony/mailer) + +## Extra features + +- the option to log the results of the mailing process it provides the developer with more information and greater control. diff --git a/docs/book/v5/transports.md b/docs/book/v5/transports.md new file mode 100644 index 0000000..bfbf844 --- /dev/null +++ b/docs/book/v5/transports.md @@ -0,0 +1,14 @@ +# Transports + +`dot-mail` can use any transport class that implements `Symfony\Component\Mailer\Transport\TransportInterface`, with the standard transport available being: + +- `Symfony\Component\Mailer\Transport\Smtp\SmtpTransport,` +- `Symfony\Component\Mailer\Transport\SendmailTransport,` + +> Feel free to use any custom transport you desire, provided it implements the mentioned `TransportInterface`. + +`Sendmail` is a wrapper over PHP's `mail()` function, and as such has a different behaviour on Windows than on *nix systems. Using sendmail on Windows **will not work in combination with** `addBcc()`. + +- Note: emails sent using the sendmail transport will be more often delivered to SPAM. + +`Smtp` connects to the configured SMTP host in order to handle sending emails. diff --git a/docs/book/v5/usage.md b/docs/book/v5/usage.md new file mode 100644 index 0000000..e4834ab --- /dev/null +++ b/docs/book/v5/usage.md @@ -0,0 +1,65 @@ +# Usage + +## Sending an e-mail + +Below is an example of how to use the email in the most basic way. You can add your own code to it e.g. to get the user data from a User object or from a config file, to use a template for the body. + +Note that `addTo` is only one of the methods available for the `Message` class returned by `getMessage()`. Other useful methods that were not included in the example are `addCc()`, `addBcc()`, `addReplyTo()`. + +The returned type is boolean, but if the `isValid()` method is removed, the returned type becomes `MailResult` which allows the use of `getMessage()` for a more detailed error message. See the `Testing if an e-mail message is valid` section below. + +```php +public function sendBasicMail() +{ + $this->mailService->setBody('Email body'); + $this->mailService->setSubject('Email subject'); + $this->mailService->getMessage()->addTo('email@example.com', 'User name'); + $this->mailService->getMessage()->setEncoding('utf-8'); + return $this->mailService->send()->isValid(); +} +``` + +It's optional, but recommended to call the above function in a `try-catch` block to display helpful error messages. The next example calls the `sendBasicMail` function from within `UserController`, but you can implement it in other controllers, just make sure that the controller's construct also includes the `FlashMessenger` parameter `$messenger`. + +```php +try { + $this->userService->sendBasicMail(); + $this->messenger->addSuccess('The mail was sent successfully', 'user-login'); + //more code... +} catch (Exception $exception) { + $this->messenger->addError($exception->getMessage(), 'user-login'); + //more code... +} +``` + +## Testing if an e-mail message is valid + +After sending an e-mail you can check if the message was valid or not. +The `$this->mailService->send()->isValid()` method call will return a boolean value. +If the returned result is `true`, the e-mail was valid, otherwise the e-mail was invalid. +In case your e-mail was invalid, you can check for any errors using `$this->mailService->send()->getMessage()`. + +Using the below logic will let you determine if a message was valid or not and log it. +You can implement your own custom error logging logic. + +```php +$result = $this->mailService->send(); +if (! $result->isValid()) { + //log the error + error_log($result->getMessage()); +} +``` + +> Invalid e-mail messages will not be sent. + +## Logging outgoing emails + +Optionally, you can keep a log of each successfully sent email. This might be useful when you need to know if/when a specific email has been sent out to a recipient. + +Logs are stored in the following format: `[YYYY-MM-DD HH:MM:SS]: {"subject":"Test subject","to":["Test Account "],"cc":[],"bcc":[]}`. + +Each email is saved via the `dotkernel/dot-log` component in the shown `JSON` format, in the single file configured under the `dot-mail.log.sent` key. To disable it, set the value of `dot-mail.log.sent` to `null`. + +## Transport usage + +[Transports](transports.md) diff --git a/mkdocs.yml b/mkdocs.yml index 8889230..e41c791 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ extra: current_version: v4 versions: - v4 + - v5 nav: - Home: index.md - v4: @@ -13,8 +14,14 @@ nav: - Configuration: v4/configuration.md - Usage: v4/usage.md - Transports: v4/transports.md + - v5: + - Overview: v5/overview.md + - Installation: v5/installation.md + - Configuration: v5/configuration.md + - Usage: v5/usage.md + - Transports: v5/transports.md site_name: dot-mail -site_description: "DotKernel's mail service" +site_description: "Dotkernel's mail service" repo_url: "https://github.com/dotkernel/dot-mail" plugins: - search diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..f2f9ee2 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/psalm.xml b/psalm.xml index 7272b57..ed4c186 100644 --- a/psalm.xml +++ b/psalm.xml @@ -7,7 +7,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" -> + errorBaseline="psalm-baseline.xml"> diff --git a/src/Email.php b/src/Email.php new file mode 100644 index 0000000..f9bb236 --- /dev/null +++ b/src/Email.php @@ -0,0 +1,420 @@ + 'Highest', + self::PRIORITY_HIGH => 'High', + self::PRIORITY_NORMAL => 'Normal', + self::PRIORITY_LOW => 'Low', + self::PRIORITY_LOWEST => 'Lowest', + ]; + + private ?string $text = null; + private ?string $textCharset = null; + private ?string $html = null; + + private ?string $htmlCharset = null; + private array $attachments = []; + private ?AbstractPart $cachedBody = null; + + /** + * @return $this + */ + public function subject(string $subject): static + { + return $this->setHeaderBody('Text', 'Subject', $subject); + } + + public function getSubject(): ?string + { + return $this->getHeaders()->getHeaderBody('Subject'); + } + + /** + * @return $this + */ + public function date(DateTimeInterface $dateTime): static + { + return $this->setHeaderBody('Date', 'Date', $dateTime); + } + + public function getDate(): ?DateTimeImmutable + { + return $this->getHeaders()->getHeaderBody('Date'); + } + + /** + * @return $this + */ + public function returnPath(Address|string $address): static + { + return $this->setHeaderBody('Path', 'Return-Path', Address::create($address)); + } + + public function getReturnPath(): ?Address + { + return $this->getHeaders()->getHeaderBody('Return-Path'); + } + + public function setSender(Address|string $addresses, mixed $name = null): static + { + return $this->setHeaderBody('Mailbox', 'Sender', new Address($addresses, $name ?? '')); + } + + public function getSender(): ?Address + { + return $this->getHeaders()->getHeaderBody('Sender'); + } + + public function addFrom(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->addListAddressHeaderBody('From', $updatedAddresses); + } + + public function setFrom(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->setListAddressHeaderBody('From', $updatedAddresses); + } + + public function getFrom(): array + { + return $this->getHeaders()->getHeaderBody('From') ?: []; + } + + public function addReplyTo(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->addListAddressHeaderBody('Reply-To', $updatedAddresses); + } + + public function setReplyTo(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->setListAddressHeaderBody('Reply-To', $updatedAddresses); + } + + public function getReplyTo(): array + { + return $this->getHeaders()->getHeaderBody('Reply-To') ?: []; + } + + public function addTo(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + $this->addListAddressHeaderBody('To', $updatedAddresses); + + return $this; + } + + public function setTo(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->setListAddressHeaderBody('To', $updatedAddresses); + } + + public function getTo(): array + { + return $this->getHeaders()->getHeaderBody('To') ?: []; + } + + public function addCc(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->addListAddressHeaderBody('Cc', $updatedAddresses); + } + + public function setCc(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->setListAddressHeaderBody('Cc', $updatedAddresses); + } + + public function getCc(): array + { + return $this->getHeaders()->getHeaderBody('Cc') ?: []; + } + + public function addBcc(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->addListAddressHeaderBody('Bcc', $updatedAddresses); + } + + public function setBcc(array|Address|string $addresses, ?string $name = null): static + { + $updatedAddresses = $this->updateAddresses($addresses, $name ?? ''); + return $this->setListAddressHeaderBody('Bcc', $updatedAddresses); + } + + public function getBcc(): array + { + return $this->getHeaders()->getHeaderBody('Bcc') ?: []; + } + + public function priority(int $priority): static + { + if ($priority > 5) { + $priority = 5; + } elseif ($priority < 1) { + $priority = 1; + } + + return $this->setHeaderBody('Text', 'X-Priority', sprintf('%d (%s)', $priority, self::PRIORITY_MAP[$priority])); + } + + public function getPriority(): int + { + [$priority] = sscanf($this->getHeaders()->getHeaderBody('X-Priority') ?? '', '%[1-5]'); + + return (int) $priority ?? 3; + } + + public function text(string $body, string $charset = 'utf-8'): static + { + $this->cachedBody = null; + $this->text = $body; + $this->textCharset = $charset; + + return $this; + } + + public function setEncoding(string $encoding): static + { + $this->htmlCharset = $encoding; + + return $this; + } + + public function getEncoding(): ?string + { + return $this->htmlCharset; + } + + public function getTextBody(): ?string + { + return $this->text; + } + + public function getTextCharset(): ?string + { + return $this->textCharset; + } + + public function html(string $body, string $charset = 'utf-8'): static + { + $this->cachedBody = null; + $this->html = $body; + $this->htmlCharset = $charset; + + return $this; + } + + public function getHtmlBody(): ?string + { + return $this->html; + } + + public function getHtmlCharset(): ?string + { + return $this->htmlCharset; + } + + public function getBody(): AbstractPart + { + if (null !== $body = parent::getBody()) { + return $body; + } + + return $this->generateBody(); + } + + public function ensureValidity(): void + { + $this->ensureBodyValid(); + + if ('1' === $this->getHeaders()->getHeaderBody('X-Unsent')) { + throw new LogicException('Cannot send messages marked as "draft".'); + } + + parent::ensureValidity(); + } + + private function ensureBodyValid(): void + { + if (null === $this->text && null === $this->html && ! $this->attachments) { + throw new LogicException('A message must have a text or an HTML part or attachments.'); + } + } + + private function generateBody(): AbstractPart + { + if (null !== $this->cachedBody) { + return $this->cachedBody; + } + + $this->ensureBodyValid(); + + [$htmlPart, $otherParts, $relatedParts] = $this->prepareParts(); + + $part = null === $this->text ? null : new TextPart($this->text, $this->textCharset); + if (null !== $htmlPart) { + if (null !== $part) { + $part = new AlternativePart($part, $htmlPart); + } else { + $part = $htmlPart; + } + } + + if ($relatedParts) { + $part = new RelatedPart($part, ...$relatedParts); + } + + if ($otherParts) { + if ($part) { + $part = new MixedPart($part, ...$otherParts); + } else { + $part = new MixedPart(...$otherParts); + } + } + + return $part ?? new TextPart($this->text, $this->textCharset); + } + + private function prepareParts(): ?array + { + $names = []; + $htmlPart = null; + $html = $this->html; + if (null !== $html) { + $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); + $html = $htmlPart->getBody(); + + $regexes = [ + ']*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))', + '<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))', + ]; + $tmpMatches = []; + foreach ($regexes as $regex) { + preg_match_all('/' . $regex . '/i', $html, $tmpMatches); + $names = array_merge($names, $tmpMatches[2], $tmpMatches[3]); + } + $names = array_filter(array_unique($names)); + } + + $otherParts = $relatedParts = []; + foreach ($this->attachments as $part) { + foreach ($names as $name) { + if ($name !== $part->getName() && (! $part->hasContentId() || $name !== $part->getContentId())) { + continue; + } + if (isset($relatedParts[$name])) { + continue 2; + } + + if ($name !== $part->getContentId()) { + $html = str_replace('cid:' . $name, 'cid:' . $part->getContentId(), $html, $count); + } + $relatedParts[$name] = $part; + $part->setName($part->getContentId())->asInline(); + + continue 2; + } + + $otherParts[] = $part; + } + if (null !== $htmlPart) { + $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); + } + + return [$htmlPart, $otherParts, array_values($relatedParts)]; + } + + private function setHeaderBody(string $type, string $name, mixed $body): static + { + $this->getHeaders()->setHeaderBody($type, $name, $body); + + return $this; + } + + private function addListAddressHeaderBody(string $name, array $addresses): static + { + if (! $header = $this->getHeaders()->get($name)) { + return $this->setListAddressHeaderBody($name, $addresses); + } + $header->addAddresses(Address::createArray($addresses)); + + return $this; + } + + private function setListAddressHeaderBody(string $name, array $addresses): static + { + $addresses = Address::createArray($addresses); + $headers = $this->getHeaders(); + if ($header = $headers->get($name)) { + $header->setAddresses($addresses); + } else { + $headers->addMailboxListHeader($name, $addresses); + } + + return $this; + } + + private function updateAddresses(array|Address|string $addresses, ?string $name = null): array + { + if (is_array($addresses)) { + return $this->createAddresses($addresses, $name); + } + + if (is_string($addresses)) { + $address[] = new Address($addresses, $name ?? ''); + return $address; + } + + return [$addresses]; + } + + private function createAddresses(array $addresses, ?string $name = null): array + { + $createdAddresses = []; + foreach ($addresses as $address) { + $createdAddresses[] = new Address($address, $name ?? ''); + } + + return $createdAddresses; + } +} diff --git a/src/Factory/MailServiceAbstractFactory.php b/src/Factory/MailServiceAbstractFactory.php index 9de095d..7f85c76 100644 --- a/src/Factory/MailServiceAbstractFactory.php +++ b/src/Factory/MailServiceAbstractFactory.php @@ -5,6 +5,7 @@ namespace Dot\Mail\Factory; use DirectoryIterator; +use Dot\Mail\Email; use Dot\Mail\Event\MailEventListenerAwareInterface; use Dot\Mail\Event\MailEventListenerInterface; use Dot\Mail\Exception\InvalidArgumentException; @@ -14,15 +15,14 @@ use Dot\Mail\Service\MailService; use Dot\Mail\Service\MailServiceInterface; use FilesystemIterator; -use Laminas\Mail\Message; -use Laminas\Mail\Transport\File; -use Laminas\Mail\Transport\Smtp; -use Laminas\Mail\Transport\TransportInterface; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use Symfony\Component\Mailer\Transport; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; +use Symfony\Component\Mailer\Transport\TransportInterface; use function explode; use function gettype; @@ -70,6 +70,7 @@ public function __invoke( $mailService->setSubject($this->mailOptions->getMessageOptions()->getSubject()); $body = $this->mailOptions->getMessageOptions()->getBody(); + $mailService->setBody($body->getContent(), $body->getCharset()); //attach files @@ -103,14 +104,14 @@ public function __invoke( return $mailService; } - protected function createMessage(): Message + protected function createMessage(): Email { $options = $this->mailOptions->getMessageOptions(); - $message = new Message(); + $message = new Email(); $from = $options->getFrom(); if (! empty($from)) { - $message->setFrom($from, $options->getFromName()); + $message->addFrom($from); } $replyTo = $options->getReplyTo(); @@ -159,7 +160,7 @@ protected function createTransport(ContainerInterface $container): TransportInte } } - //check is the adapter is one of Laminas's default adapters + //check is the adapter is Symfony default adapter if (is_subclass_of($adapter, TransportInterface::class)) { return $this->setupTransportConfig(new $adapter()); } @@ -176,10 +177,13 @@ protected function createTransport(ContainerInterface $container): TransportInte protected function setupTransportConfig(TransportInterface $transport): TransportInterface { - if ($transport instanceof Smtp) { - $transport->setOptions($this->mailOptions->getSmtpOptions()); - } elseif ($transport instanceof File) { - $transport->setOptions($this->mailOptions->getFileOptions()); + if ($transport instanceof SmtpTransport) { + $user = $this->mailOptions->getSmtpOptions()->getConnectionConfig()['username']; + $pass = $this->mailOptions->getSmtpOptions()->getConnectionConfig()['password']; + $port = $this->mailOptions->getSmtpOptions()->getConnectionConfig()['port']; + $host = $this->mailOptions->getSmtpOptions()->getHost(); + + $transport = Transport::fromDsn('smtp://' . $user . ':' . $pass . '@' . $host . ':' . $port); } return $transport; diff --git a/src/Options/MailOptions.php b/src/Options/MailOptions.php index cb3c0f3..4e309a1 100644 --- a/src/Options/MailOptions.php +++ b/src/Options/MailOptions.php @@ -4,14 +4,10 @@ namespace Dot\Mail\Options; -use Laminas\Mail\Transport\File; -use Laminas\Mail\Transport\FileOptions; -use Laminas\Mail\Transport\InMemory; -use Laminas\Mail\Transport\Sendmail; -use Laminas\Mail\Transport\Smtp; -use Laminas\Mail\Transport\SmtpOptions; -use Laminas\Mail\Transport\TransportInterface; use Laminas\Stdlib\AbstractOptions; +use Symfony\Component\Mailer\Transport\SendmailTransport; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; +use Symfony\Component\Mailer\Transport\TransportInterface; use function array_key_exists; use function class_exists; @@ -26,16 +22,13 @@ class MailOptions extends AbstractOptions { protected array $eventListeners = []; protected array $saveSentMessageFolder = []; - protected TransportInterface|string $transport = Sendmail::class; + protected TransportInterface|string $transport = SmtpTransport::class; protected array $transportMap = [ - 'sendmail' => [Sendmail::class], - 'smtp' => [Smtp::class], - 'in_memory' => [InMemory::class], - 'file' => [File::class], + 'smtp' => [SmtpTransport::class], + 'sendmail' => [SendmailTransport::class], ]; protected MessageOptions $messageOptions; protected SmtpOptions $smtpOptions; - protected FileOptions $fileOptions; public function getTransportMap(): array { @@ -87,16 +80,6 @@ public function setSmtpOptions(array $smtpOptions): void $this->smtpOptions = new SmtpOptions($smtpOptions); } - public function getFileOptions(): FileOptions - { - return $this->fileOptions; - } - - public function setFileOptions(array $fileOptions): void - { - $this->fileOptions = new FileOptions($fileOptions); - } - public function getEventListeners(): array { return $this->eventListeners; diff --git a/src/Options/SmtpOptions.php b/src/Options/SmtpOptions.php new file mode 100644 index 0000000..db491a7 --- /dev/null +++ b/src/Options/SmtpOptions.php @@ -0,0 +1,124 @@ + + */ +class SmtpOptions extends AbstractOptions +{ + protected string $name = 'localhost'; + protected string $connectionClass = 'smtp'; + protected array $connectionConfig = []; + protected string $host = '127.0.0.1'; + protected int $port = 25; + + protected ?int $connectionTimeLimit; + + public function getName(): string + { + return $this->name; + } + + /** + * Set the local client hostname or IP + */ + public function setName(string $name): static + { + $this->name = $name; + return $this; + } + + /** + * Get connection class + */ + public function getConnectionClass(): string + { + return $this->connectionClass; + } + + /** + * Set connection class + */ + public function setConnectionClass(string $connectionClass): static + { + $this->connectionClass = $connectionClass; + return $this; + } + + /** + * Get connection configuration array + */ + public function getConnectionConfig(): array + { + return $this->connectionConfig; + } + + /** + * Set connection configuration array + */ + public function setConnectionConfig(array $connectionConfig): static + { + $this->connectionConfig = $connectionConfig; + return $this; + } + + /** + * Get the host name + */ + public function getHost(): string + { + return $this->host; + } + + /** + * Set the SMTP host + */ + public function setHost(string $host): static + { + $this->host = $host; + return $this; + } + + /** + * Get the port the SMTP server runs on + */ + public function getPort(): int + { + return $this->port; + } + + /** + * Set the port the SMTP server runs on + */ + public function setPort(int $port): static + { + if ($port < 1) { + throw new InvalidArgumentException(sprintf( + 'Port must be greater than 1; received "%d"', + $port + )); + } + $this->port = $port; + return $this; + } + + public function getConnectionTimeLimit(): ?int + { + return $this->connectionTimeLimit; + } + + public function setConnectionTimeLimit(?int $seconds): static + { + $this->connectionTimeLimit = $seconds ?? null; + + return $this; + } +} diff --git a/src/Service/LogService.php b/src/Service/LogService.php index 9f21a20..e919da7 100644 --- a/src/Service/LogService.php +++ b/src/Service/LogService.php @@ -4,8 +4,7 @@ namespace Dot\Mail\Service; -use Laminas\Mail\AddressList; -use Laminas\Mail\Message; +use Dot\Mail\Email; use function date; use function dirname; @@ -28,7 +27,7 @@ public function __construct(array $config) $this->config = $config; } - public function sent(Message $message): false|int|null + public function sent(Email $message): false|int|null { /** * If empty: logging is disabled @@ -65,11 +64,11 @@ public function sent(Message $message): false|int|null return file_put_contents($target, $data, FILE_APPEND); } - public function extractAddresses(AddressList $addressList): array + public function extractAddresses(array $addressList): array { $addresses = []; foreach ($addressList as $address) { - $addresses[] = sprintf('%s <%s>', trim($address->getName() ?? ''), trim($address->getEmail())); + $addresses[] = sprintf('%s <%s>', trim($address->getName() ?? ''), trim($address->getAddress())); } return $addresses; } diff --git a/src/Service/LogServiceInterface.php b/src/Service/LogServiceInterface.php index 3713a67..cf04cb6 100644 --- a/src/Service/LogServiceInterface.php +++ b/src/Service/LogServiceInterface.php @@ -4,9 +4,9 @@ namespace Dot\Mail\Service; -use Laminas\Mail\Message; +use Dot\Mail\Email; interface LogServiceInterface { - public function sent(Message $message): false|int|null; + public function sent(Email $message): false|int|null; } diff --git a/src/Service/MailService.php b/src/Service/MailService.php index 4d20c40..e10a540 100644 --- a/src/Service/MailService.php +++ b/src/Service/MailService.php @@ -4,6 +4,7 @@ namespace Dot\Mail\Service; +use Dot\Mail\Email; use Dot\Mail\Event\MailEvent; use Dot\Mail\Event\MailEventListenerAwareInterface; use Dot\Mail\Event\MailEventListenerAwareTrait; @@ -12,39 +13,31 @@ use Dot\Mail\Result\MailResult; use Dot\Mail\Result\ResultInterface; use Exception; -use finfo; -use Laminas\Mail\Message; -use Laminas\Mail\Storage\Imap; -use Laminas\Mail\Transport\Smtp; -use Laminas\Mail\Transport\TransportInterface; -use Laminas\Mime\Message as MimeMessage; -use Laminas\Mime\Mime; -use Laminas\Mime\Part as MimePart; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\Part\AbstractPart; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\Multipart\MixedPart; use function array_merge; use function basename; use function count; -use function fopen; use function is_file; use function is_string; -use function strip_tags; - -use const FILEINFO_MIME_TYPE; class MailService implements MailServiceInterface, MailEventListenerAwareInterface { use MailEventListenerAwareTrait; protected LogServiceInterface $logService; - protected Message $message; + protected Email $message; protected TransportInterface $transport; protected MailOptions $mailOptions; protected array $attachments = []; - protected ?Imap $storage = null; public function __construct( LogServiceInterface $logService, - Message $message, + Email $message, TransportInterface $transport, MailOptions $mailOptions ) { @@ -55,22 +48,18 @@ public function __construct( } /** - * @throws MailException + * @throws MailException|TransportExceptionInterface */ public function send(): ResultInterface { $result = new MailResult(); - /* - * Enforce the UTF-8 encoding to message body - * @see https://github.com/dotkernel/dot-mail/issues/9 - */ + $this->message->setEncoding('utf-8'); try { $this->getEventManager()->triggerEvent($this->createMailEvent()); //attach files before sending $this->attachFiles(); - $this->getTransport()->send($this->getMessage()); $this->getEventManager()->triggerEvent($this->createMailEvent(MailEvent::EVENT_MAIL_POST_SEND, $result)); @@ -85,41 +74,9 @@ public function send(): ResultInterface $this->logService->sent($this->getMessage()); } - //save copy of sent message to folders - if ( - $this->mailOptions->getTransport() === Smtp::class - && $this->mailOptions->getSaveSentMessageFolder() - ) { - $this->storage = $this->createStorage(); - if ($this->storage) { - foreach ($this->mailOptions->getSaveSentMessageFolder() as $folder) { - $this->storage->appendMessage($this->getMessage()->toString(), $folder); - } - } - } - return $result; } - public function createStorage(): ?Imap - { - $host = $this->mailOptions->getSmtpOptions()->getHost(); - if (empty($host)) { - return null; - } - $connectionConfig = $this->mailOptions->getSmtpOptions()->getConnectionConfig(); - - if (empty($connectionConfig['username']) || empty($connectionConfig['password'])) { - return null; - } - - return new Imap([ - 'host' => $host, - 'user' => $connectionConfig['username'], - 'password' => $connectionConfig['password'], - ]); - } - public function createMailEvent( string $name = MailEvent::EVENT_MAIL_PRE_SEND, ?ResultInterface $result = null @@ -131,7 +88,7 @@ public function createMailEvent( return $event; } - public function attachFiles(): false|Message + public function attachFiles(): false|Email { if (count($this->attachments) === 0) { return false; @@ -139,65 +96,28 @@ public function attachFiles(): false|Message $mimeMessage = $this->message->getBody(); - if (is_string($mimeMessage)) { - $originalBodyPart = new MimePart($mimeMessage); - $originalBodyPart->type = $mimeMessage !== strip_tags($mimeMessage) - ? Mime::TYPE_HTML - : Mime::TYPE_TEXT; - - $this->setBody($originalBodyPart); - $mimeMessage = $this->message->getBody(); - } - - $oldParts = $mimeMessage->getParts(); - //generate a new Part for each attachment - $attachmentParts = []; - $info = new finfo(FILEINFO_MIME_TYPE); - foreach ($this->attachments as $key => $attachment) { if (! is_file($attachment)) { continue; } - $basename = is_string($key) ? $key : basename($attachment); - $part = new MimePart(fopen($attachment, 'r')); - $part->id = $basename; - $part->filename = $basename; - $part->type = $info->file($attachment); - $part->encoding = Mime::ENCODING_BASE64; - $part->disposition = Mime::DISPOSITION_ATTACHMENT; - $attachmentParts[] = $part; + $basename = is_string($key) ? $key : basename($attachment); + $attachedFile = new DataPart($attachment, $basename, null); + $mimeMessage = new MixedPart($mimeMessage, $attachedFile); + + $this->message->setBody($mimeMessage); } - $body = new MimeMessage(); - $body->setParts(array_merge($oldParts, $attachmentParts)); - return $this->message->setBody($body); + return $this->message; } - public function setBody(string|MimePart $body, ?string $charset = null): void + public function setBody(string|AbstractPart $body, ?string $charset = null): void { if (is_string($body)) { - //create a mime\part and wrap it into a mime\message - $mimePart = new MimePart($body); - $mimePart->type = $body !== strip_tags($body) ? Mime::TYPE_HTML : Mime::TYPE_TEXT; - $mimePart->charset = $charset ?: self::DEFAULT_CHARSET; - $body = new MimeMessage(); - $body->setParts([$mimePart]); + $this->message->html($body); } else { - if (isset($charset)) { - $body->charset = $charset; - } - - $mimeMessage = new MimeMessage(); - $mimeMessage->setParts([$body]); - $body = $mimeMessage; + $this->message->setBody($body); } - - // The headers Content-type and Content-transfer-encoding are duplicated every time the body is set. - // Removing them before setting the body prevents this error - $this->message->getHeaders()->removeHeader('content-type'); - $this->message->getHeaders()->removeHeader('content-transfer-encoding'); - $this->message->setBody($body); } public function createMailResultFromException(Exception $e): ResultInterface @@ -205,14 +125,14 @@ public function createMailResultFromException(Exception $e): ResultInterface return new MailResult(false, $e->getMessage(), $e); } - public function getMessage(): Message + public function getMessage(): Email { return $this->message; } public function setSubject(string $subject): void { - $this->message->setSubject($subject); + $this->message->subject($subject); } public function addAttachment(string $path, ?string $filename = null): void @@ -248,33 +168,4 @@ public function setTransport(TransportInterface $transport): void { $this->transport = $transport; } - - public function getStorage(): ?Imap - { - return $this->storage; - } - - public function setStorage(?Imap $storage): void - { - $this->storage = $storage; - } - - public function getFolderGlobalNames(): array|false - { - $this->storage ?? $this->createStorage(); - if (! $this->storage) { - return false; - } - $folderGlobalNames = []; - - foreach ($this->getStorage()->getFolders() as $folder) { - $folderGlobalNames[] = $folder->getGlobalName(); - } - - foreach ($this->getStorage()->getFolders()->getChildren() as $folder) { - $folderGlobalNames[] = $folder->getGlobalName(); - } - - return $folderGlobalNames; - } } diff --git a/src/Service/MailServiceInterface.php b/src/Service/MailServiceInterface.php index 100baa2..5ebf83e 100644 --- a/src/Service/MailServiceInterface.php +++ b/src/Service/MailServiceInterface.php @@ -4,10 +4,9 @@ namespace Dot\Mail\Service; +use Dot\Mail\Email; use Dot\Mail\Result\ResultInterface; -use Laminas\Mail\Message; -use Laminas\Mail\Transport\TransportInterface; -use Laminas\Mime\Part as MimePart; +use Symfony\Component\Mailer\Transport\TransportInterface; interface MailServiceInterface { @@ -15,11 +14,11 @@ interface MailServiceInterface public function send(): ResultInterface; - public function getMessage(): Message; + public function getMessage(): Email; public function getTransport(): TransportInterface; - public function setBody(string|MimePart $body, ?string $charset = null): void; + public function setBody(string $body, ?string $charset = null): void; public function setSubject(string $subject): void; @@ -32,6 +31,4 @@ public function getAttachments(): array; public function setAttachments(array $paths): void; public function setTransport(TransportInterface $transport): void; - - public function getFolderGlobalNames(): array|false; } diff --git a/test/CommonTrait.php b/test/CommonTrait.php index 6c54118..b3a6f45 100644 --- a/test/CommonTrait.php +++ b/test/CommonTrait.php @@ -4,9 +4,9 @@ namespace DotTest\Mail; -use Laminas\Mail\Transport\Smtp; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamDirectory; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; trait CommonTrait { @@ -50,17 +50,15 @@ private function generateConfig(): array /** * the mail transport to use - * can be any class implementing Laminas\Mail\Transport\TransportInterface + * can be any class implementing Symfony\Component\Mailer\Transport\TransportInterface * * for standard mail transports, you can use these aliases - * - sendmail => Laminas\Mail\Transport\Sendmail - * - smtp => Laminas\Mail\Transport\Smtp - * - file => Laminas\Mail\Transport\File - * - in_memory => Laminas\Mail\Transport\InMemory + * - sendmail => Symfony\Component\Mailer\Transport\SendmailTransport + * - smtp => Symfony\Component\Mailer\Transport\Smtp\SmtpTransport * * defaults to sendmail **/ - 'transport' => Smtp::class, + 'transport' => SmtpTransport::class, // Uncomment the below line if you want to save a copy of all sent emails to a certain IMAP folder // Valid only if the Transport is SMTP @@ -92,32 +90,23 @@ private function generateConfig(): array ], ], - //options that will be used only if Laminas\Mail\Transport\Smtp adapter is used + //options that will be used only if Symfony\Component\Mailer\Transport\Smtp\SmtpTransport + // adapter is used 'smtp_options' => [ - 'host' => '', + 'host' => 'qwd', 'port' => 587, 'connection_class' => 'login', 'connection_config' => [ //the smtp authentication identity - //'username' => '', + 'username' => 'qwd', //the smtp authentication credential - //'password' => '', - 'ssl' => 'tls', + 'password' => 'qwd', + 'ssl' => 'tls', ], ], - //file options that will be used only if the adapter is Laminas\Mail\Transport\File - 'file_options' => [ - 'path' => $this->fileSystem->url() . '/data/mail/output', - - //a callable that will get the Laminas\Mail\Transport\File object as an argument and should - // return the filename - //if null is used, and empty callable will be used - //'callback' => null, - ], - //listeners to register with the mail service, for mail events 'event_listeners' => [ //[ diff --git a/test/Factory/LogServiceFactoryTest.php b/test/Factory/LogServiceFactoryTest.php index e6eb55e..279a524 100644 --- a/test/Factory/LogServiceFactoryTest.php +++ b/test/Factory/LogServiceFactoryTest.php @@ -4,9 +4,9 @@ namespace DotTest\Mail\Factory; +use Dot\Mail\Email; use Dot\Mail\Factory\LogServiceFactory; use Dot\Mail\Service\LogService; -use Laminas\Mail\Message; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; @@ -22,7 +22,7 @@ class LogServiceFactoryTest extends TestCase */ public function testServiceCreatedWithoutValidConfig(): void { - $message = $this->createMock(Message::class); + $message = $this->createMock(Email::class); $container = $this->createMock(ContainerInterface::class); $logService = (new LogServiceFactory())($container); diff --git a/test/Factory/MailServiceAbstractFactoryTest.php b/test/Factory/MailServiceAbstractFactoryTest.php index c5db601..1b16e12 100644 --- a/test/Factory/MailServiceAbstractFactoryTest.php +++ b/test/Factory/MailServiceAbstractFactoryTest.php @@ -10,12 +10,11 @@ use Dot\Mail\Options\AttachmentsOptions; use Dot\Mail\Options\MailOptions; use Dot\Mail\Options\MessageOptions; +use Dot\Mail\Options\SmtpOptions; use Dot\Mail\Service\LogService; use Dot\Mail\Service\LogServiceInterface; use Dot\Mail\Service\MailService; use DotTest\Mail\CommonTrait; -use Laminas\Mail\Transport\Sendmail; -use Laminas\Mail\Transport\Smtp; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; @@ -23,6 +22,7 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; class MailServiceAbstractFactoryTest extends TestCase { @@ -30,6 +30,7 @@ class MailServiceAbstractFactoryTest extends TestCase private ContainerInterface|MockObject $container; private MailOptions|MockObject $mailOptions; + private SmtpOptions|MockObject $smtpOptions; private MessageOptions|MockObject $messageOptions; private AttachmentsOptions|MockObject $attachmentsOptions; private Subject $subject; @@ -54,6 +55,7 @@ public function setUp(): void $this->container = $this->createMock(ContainerInterface::class); $this->mailOptions = $this->createMock(MailOptions::class); + $this->smtpOptions = $this->createMock(SmtpOptions::class); $this->messageOptions = $this->createMock(MessageOptions::class); $this->attachmentsOptions = $this->createMock(AttachmentsOptions::class); @@ -88,7 +90,7 @@ public function testGenerateServiceWithProvidedAdapter(): void ->willReturn($this->messageOptions); $this->mailOptions->expects($this->any()) ->method('getTransport') - ->willReturn($this->createMock(Sendmail::class)); + ->willReturn($this->createMock(SmtpTransport::class)); $this->mailOptions->expects($this->any()) ->method('getEventListeners') ->willReturn([AbstractMailEventListener::class]); @@ -108,9 +110,25 @@ public function testGenerateServiceWithProvidedAdapter(): void ->method('has') ->willReturn(true); + $this->smtpOptions->expects($this->any()) + ->method('getConnectionConfig') + ->willReturn([ + 'username' => 'test', + 'password' => 'testPassword', + 'port' => 587, + ]); + + $this->smtpOptions->expects($this->once()) + ->method('getHost') + ->willReturn('localhost'); + + $this->mailOptions->expects($this->any()) + ->method('getSmtpOptions') + ->willReturn($this->smtpOptions); + $mailService = (new Subject())($this->container, $requestedName); $this->assertInstanceOf(MailService::class, $mailService); - $this->assertInstanceOf(Sendmail::class, $mailService->getTransport()); + $this->assertInstanceOf(SmtpTransport::class, $mailService->getTransport()); } /** @@ -140,7 +158,7 @@ public function testGenerateServiceWithoutProvidedAdapter(): void ->willReturn($this->messageOptions); $this->mailOptions->expects($this->any()) ->method('getTransport') - ->willReturn(Smtp::class); + ->willReturn(SmtpTransport::class); $this->mailOptions->expects($this->any()) ->method('getEventListeners') ->willReturn(['Invalid Listener Test']); @@ -150,22 +168,38 @@ public function testGenerateServiceWithoutProvidedAdapter(): void ->willReturnMap([ ['dot-mail.options.default', $this->mailOptions], [LogServiceInterface::class, $this->createMock(LogService::class)], - [Smtp::class, new Smtp()], + [SmtpTransport::class, new SmtpTransport()], ['Invalid Listener Test', 'Invalid Listener provided'], ]); $this->container->expects($this->any()) ->method('has') ->willReturnMap([ - [Smtp::class, true], + [SmtpTransport::class, true], ['Invalid Listener Test', true], ]); + $this->smtpOptions->expects($this->any()) + ->method('getConnectionConfig') + ->willReturn([ + 'username' => 'test', + 'password' => 'testPassword', + 'port' => 587, + ]); + + $this->smtpOptions->expects($this->once()) + ->method('getHost') + ->willReturn('localhost'); + + $this->mailOptions->expects($this->any()) + ->method('getSmtpOptions') + ->willReturn($this->smtpOptions); + $this->expectException(RuntimeException::class); $mailService = (new Subject())($this->container, $requestedName); $this->assertInstanceOf(MailService::class, $mailService); - $this->assertInstanceOf(Smtp::class, $mailService->getTransport()); + $this->assertInstanceOf(SmtpTransport::class, $mailService->getTransport()); $this->assertCount(2, $mailService->getAttachments()); } } diff --git a/test/Options/MailOptionsTest.php b/test/Options/MailOptionsTest.php index 1ea106e..d20dd3b 100644 --- a/test/Options/MailOptionsTest.php +++ b/test/Options/MailOptionsTest.php @@ -7,10 +7,9 @@ use Dot\Mail\Event\AbstractMailEventListener; use Dot\Mail\Options\MailOptions; use Dot\Mail\Options\MessageOptions; -use Laminas\Mail\Transport\FileOptions; -use Laminas\Mail\Transport\Smtp; -use Laminas\Mail\Transport\SmtpOptions; +use Dot\Mail\Options\SmtpOptions; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; class MailOptionsTest extends TestCase { @@ -22,7 +21,6 @@ public function testGettersAndSetters(): void $transportMap = ['test' => 'array']; $messageOptions = ['from' => '', 'to' => []]; $smtpOptions = ['host' => '', 'port' => 587]; - $fileOptions = []; $eventListeners = [AbstractMailEventListener::class]; $saveSentMessageFolder = ['INBOX.Sent']; @@ -30,15 +28,13 @@ public function testGettersAndSetters(): void $subject->setTransportMap($transportMap); $subject->setMessageOptions($messageOptions); $subject->setSmtpOptions($smtpOptions); - $subject->setFileOptions($fileOptions); $subject->setEventListeners($eventListeners); $subject->setSaveSentMessageFolder($saveSentMessageFolder); - $this->assertSame(Smtp::class, $subject->getTransport()); + $this->assertSame(SmtpTransport::class, $subject->getTransport()); $this->assertSame($transportMap, $subject->getTransportMap()); $this->assertInstanceOf(MessageOptions::class, $subject->getMessageOptions()); $this->assertInstanceOf(SmtpOptions::class, $subject->getSmtpOptions()); - $this->assertInstanceOf(FileOptions::class, $subject->getFileOptions()); $this->assertSame($eventListeners, $subject->getEventListeners()); $this->assertSame($saveSentMessageFolder, $subject->getSaveSentMessageFolder()); } diff --git a/test/Service/LogServiceTest.php b/test/Service/LogServiceTest.php index 258e5ae..e2bfe79 100644 --- a/test/Service/LogServiceTest.php +++ b/test/Service/LogServiceTest.php @@ -4,13 +4,13 @@ namespace DotTest\Mail\Service; +use Dot\Mail\Email; use Dot\Mail\Service\LogService; use DotTest\Mail\CommonTrait; -use Laminas\Mail\AddressList; -use Laminas\Mail\Message; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\Address; use function file_get_contents; use function is_file; @@ -42,9 +42,11 @@ public function setUp(): void public function testExtractAddresses(): void { - $addressList = (new AddressList())->add('test1@dotkernel.com', ' dot test ') - ->add('test2@dotkernel.com', ' mail test ') - ->add('test3@dotkernel.com'); + $addressList = [ + new Address('test1@dotkernel.com', 'dot test'), + new Address('test2@dotkernel.com', 'mail test'), + new Address('test3@dotkernel.com'), + ]; $results = $this->logService->extractAddresses($addressList); @@ -59,13 +61,10 @@ public function testExtractAddresses(): void */ public function testSentMailIsLogged(): void { - $message = $this->createMock(Message::class); - $toAddress = new AddressList(); - $toAddress->addFromString('testTo@dotkernel.com'); - $ccAddress = new AddressList(); - $ccAddress->addFromString('testCc@dotkernel.com'); - $bccAddress = new AddressList(); - $bccAddress->addFromString('testBcc@dotkernel.com'); + $message = $this->createMock(Email::class); + $toAddress = [new Address('testTo@dotkernel.com')]; + $ccAddress = [new Address('testCc@dotkernel.com')]; + $bccAddress = [new Address('testBcc@dotkernel.com')]; $message->expects($this->once())->method('getSubject') ->willReturn('testSubject@dotkernel.com'); diff --git a/test/Service/MailServiceTest.php b/test/Service/MailServiceTest.php index f54d65e..95ca3c4 100644 --- a/test/Service/MailServiceTest.php +++ b/test/Service/MailServiceTest.php @@ -4,54 +4,46 @@ namespace DotTest\Mail\Service; +use Dot\Mail\Email; use Dot\Mail\Event\MailEvent; use Dot\Mail\Exception\MailException; +use Dot\Mail\Exception\RuntimeException; use Dot\Mail\Options\MailOptions; use Dot\Mail\Result\MailResult; use Dot\Mail\Service\LogServiceInterface; use Dot\Mail\Service\MailService; use DotTest\Mail\CommonTrait; -use Laminas\Mail\Exception\RuntimeException; -use Laminas\Mail\Message; -use Laminas\Mail\Protocol\Exception\RuntimeException as ProtocolRuntimeException; -use Laminas\Mail\Storage\Folder; -use Laminas\Mail\Storage\Imap; -use Laminas\Mail\Transport\Sendmail; -use Laminas\Mail\Transport\SmtpOptions; -use Laminas\Mail\Transport\TransportInterface; -use Laminas\Mime\Message as MimeMessage; -use Laminas\Mime\Part; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; - -use function array_shift; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\Part\TextPart; class MailServiceTest extends TestCase { use CommonTrait; private MailService $mailService; - private Message|MockObject $message; + private Email|MockObject $message; private TransportInterface|MockObject $transportInterface; - private MailOptions|MockObject $mailOptions; /** * @throws Exception */ public function setUp(): void { - $this->message = new Message(); + $this->message = new Email(); $this->transportInterface = $this->createMock(TransportInterface::class); - $this->mailOptions = $this->createMock(MailOptions::class); + $mailOptions = $this->createMock(MailOptions::class); $logServiceInterface = $this->createMock(LogServiceInterface::class); $this->mailService = new MailService( $logServiceInterface, $this->message, $this->transportInterface, - $this->mailOptions + $mailOptions ); $this->fileSystem = vfsStream::setup('root', 0644, [ @@ -76,16 +68,13 @@ public function setUp(): void public function testGettersAndSetters(): void { $attachments = ['/testAttachment.pdf', '/testDirectory/testAttachment2.xls']; - $storage = $this->createMock(Imap::class); - $transport = $this->createMock(Sendmail::class); + $transport = $this->createMock(SmtpTransport::class); $this->mailService->setAttachments($attachments); - $this->mailService->setStorage($storage); $this->mailService->setTransport($transport); $this->assertSame($attachments, $this->mailService->getAttachments()); $this->assertContains('/testAttachment.pdf', $this->mailService->getAttachments()); - $this->assertSame($storage, $this->mailService->getStorage()); $this->assertSame($transport, $this->mailService->getTransport()); } @@ -105,7 +94,7 @@ public function testCreateMailEvent(): void public function testAttachFilesToStringBody(): void { $this->mailService->setSubject('Test Subject'); - $this->message->setBody('Body as string test'); + $this->message->html('Body as string test'); $this->mailService->addAttachment($this->fileSystem->url() . '/data/mail/attachments/testPdfAttachment.pdf'); $this->mailService->addAttachment( @@ -114,7 +103,7 @@ public function testAttachFilesToStringBody(): void ); $result = $this->mailService->attachFiles(); - $this->assertInstanceOf(Message::class, $result); + $this->assertInstanceOf(Email::class, $result); $this->assertSame('Test Subject', $result->getSubject()); $this->assertCount(2, $this->mailService->getAttachments()); $this->assertArrayHasKey('spreadsheetName', $this->mailService->getAttachments()); @@ -129,8 +118,7 @@ public function testAttachFilesToMimeMessageBody(): void '; - $mimeMessage = new MimeMessage(); - $mimeMessage->setParts([new Part($stringMessage)]); + $mimeMessage = new TextPart($stringMessage); $this->mailService->setSubject('Test Subject'); $this->message->setBody($mimeMessage); $this->mailService->addAttachments([ @@ -139,7 +127,7 @@ public function testAttachFilesToMimeMessageBody(): void ]); $result = $this->mailService->attachFiles(); - $this->assertInstanceOf(Message::class, $result); + $this->assertInstanceOf(Email::class, $result); $this->assertSame('Test Subject', $result->getSubject()); } @@ -165,81 +153,4 @@ public function testMailResultCreatedFromException(): void $this->assertSame($customException, $mailResult->getException()); $this->assertSame('Custom exception test', $mailResult->getMessage()); } - - /** - * @throws Exception - */ - public function testGetFolderNames(): void - { - $childFolder = $this->createMock(Folder::class); - $childFolder->expects($this->once()) - ->method('getGlobalName') - ->willReturn('rootFolderName.childFolderName'); - - $rootFolder = $this->createMock(Folder::class); - $rootFolder->expects($this->once()) - ->method('getChildren') - ->willReturn([$childFolder]); - - $rootFolder->expects($this->once()) - ->method('getGlobalName') - ->willReturn('rootFolderName'); - - $tree = [ - [$rootFolder], - $rootFolder, - ]; - - $storage = $this->createMock(Imap::class); - $storage->method('getFolders')->willReturnCallback(function () use (&$tree) { - return array_shift($tree); - }); - - $this->mailService->setStorage($storage); - $result = $this->mailService->getFolderGlobalNames(); - - $this->assertCount(2, $result); - $this->assertContains('rootFolderName', $result); - $this->assertContains('rootFolderName.childFolderName', $result); - } - - /** - * @throws Exception - */ - public function testCreateStorageThrowsRuntimeExceptionWithInvalidConfig(): void - { - $smtpOptions = $this->createMock(SmtpOptions::class); - $smtpOptions->expects($this->once()) - ->method('getHost') - ->willReturn('127.0.0.1'); - - $smtpOptions->expects($this->once()) - ->method('getConnectionConfig') - ->willReturn([ - 'username' => 'testUsername', - 'password' => 'testPassword', - 'ssl' => 'ssl', - ]); - - $this->mailOptions->expects($this->atMost(2)) - ->method('getSmtpOptions') - ->willReturn($smtpOptions); - - $this->expectException(ProtocolRuntimeException::class); - $this->mailService->createStorage(); - } - - /** - * @throws Exception - */ - public function testCreateStorageReturnsImap(): void - { - $imap = $this->createMock(Imap::class); - $mailService = $this->createPartialMock(MailService::class, ['createStorage']); - $mailService->expects($this->once()) - ->method('createStorage') - ->willReturn($imap); - - $this->assertInstanceOf(Imap::class, $mailService->createStorage()); - } }