diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 97f8b52..2b4481e 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -12,16 +12,19 @@ on: jobs: Build: runs-on: 'ubuntu-latest' - container: 'byjg/php:${{ matrix.php-version }}-cli' + container: + image: 'byjg/php:${{ matrix.php-version }}-cli' + options: --user root --privileged strategy: matrix: php-version: + - "8.4" - "8.3" - "8.2" - "8.1" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: composer install - run: ./vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 99c9f68..004001b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ composer.lock vendor .idea .phpunit.result.cache +phpunit.coverage.xml +phpunit.report.xml +*.bak diff --git a/.run/PHPUnit.run.xml b/.run/PHPUnit.run.xml index f81c245..d5231dd 100644 --- a/.run/PHPUnit.run.xml +++ b/.run/PHPUnit.run.xml @@ -3,4 +3,8 @@ + + + + \ No newline at end of file diff --git a/.run/psalm.run.xml b/.run/psalm.run.xml new file mode 100644 index 0000000..d9c1b61 --- /dev/null +++ b/.run/psalm.run.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 429d85f..d3f10da 100644 --- a/README.md +++ b/README.md @@ -6,223 +6,77 @@ [![GitHub license](https://img.shields.io/github/license/byjg/php-mailwrapper.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/php-mailwrapper.svg)](https://github.com/byjg/php-mailwrapper/releases/) -A lightweight wrapper for send mail. The interface is tottaly decoupled from the sender. The motivation is -create a single interface for sending mail doesn't matter the sender. There are three options available: +A lightweight wrapper for sending email. The interface is totally decoupled from the sender, providing a single interface for sending mail regardless of the underlying mail service. -- SMTP (with SSL/TLS) -- AWS SES (using API directly) -- Mailgun (using API directly) +## Available Wrappers -## How to use +- **SMTP** - SMTP with SSL/TLS support +- **AWS SES** - Amazon Simple Email Service (using API directly) +- **Mailgun** - Mailgun API (using API directly) +- **SendMail** - PHP's built-in mail() function +- **FakeSender** - For testing (does nothing) -The MailWrapper has your classes totally decoupled in three parts: +## Install + +```shell +composer require "byjg/mailwrapper" +``` -- The Envelope: the mail envelope. Defines the mail sender, recipients, body, subject, etc; -- The Mailer: the responsible to deal with the process of send the envelope -- The Register: Will register the available Mailers in the system. +## Documentation -### Envelope Class +- **[Getting Started](docs/getting-started.md)** - Installation, quick start, and architecture overview +- **[Envelope](docs/envelope.md)** - Creating and configuring email messages +- **[Connection Strings](docs/connection-strings.md)** - URI patterns for different mail services (SMTP, Mailgun, SES, etc.) +- **[Mailer Factory](docs/mailer-factory.md)** - Registering and creating mailers +- **[Attachments](docs/attachments.md)** - Sending attachments and embedded images +- **[Custom Wrappers](docs/custom-wrappers.md)** - Implementing your own mail wrapper +- **[Exceptions](docs/exceptions.md)** - Error handling and exception types -MailWrapper provides a envelope class with all the basic necessary attributes to create an email. -As this Envelope class are totally decoupled from the Mailer engine, you can use it also as DTO. -See an example below: (do not forget `require "vendor/autoload.php"`) +## Quick Start ```php setFrom('johndoe@example.com', 'John Doe'); $envelope->addTo('jane@example.com'); $envelope->setSubject('Email Subject'); -$envelope->setBody('html text body'); -``` - -### Sending the email +$envelope->setBody('

Hello World

'); -Once you have created the envelope you can send the email. Basically you have to register in the fabric all mailer -you intend to use and then create the mailer: - -```php -send($envelope); -``` - -You can create the mailer directly without the factory: - -```php -send($envelope); -``` - -### Sending attachment +// Create mailer from connection string +$mailer = \ByJG\Mail\MailerFactory::create('smtp://username:password@smtp.example.com:587'); -```php -addAttachment('name_of_attachement', '/path/to/file', 'mime/type'); -$mailer->send($envelope); +// Send the email +$result = $mailer->send($envelope); ``` -### Adding attachment as Embed Image +## Architecture -Adding an image as a inline attachment (or Embed) your mail reader will not show as download but you can -use it as an local image in your email. +MailWrapper is organized into three main components: -See the example: +- **The Envelope**: The mail message. Defines the sender, recipients, body, subject, attachments, etc. +- **The Mailer**: Responsible for the process of sending the envelope +- **The Factory**: Registers and creates the available Mailers in the system -```php -addEmbedImage('mycontentname', '/path/to/image', 'mime/type'); -$envelope->setBody(''); -$mailer->send($envelope); -``` - -## The connection url - -To create a new sender you have to define a URL like that: - -```text -scheme://username:password/smtpserver:port -``` - -The options are: - -| Part | Description | -|:-----------|:--------------------------------------------------------------------------------------------------------| -| scheme | The email scheme: smtp, ssl, tls, mandrill and ses. Note that mandrill and ses use your own private api | -| username | The username | -| password | The password | -| smtpserver | The SMTP Host | -| port | The SMTP Port | - -The protocols available are: - -| Scheme | Description | URI Pattern | Mailer Object | -|:-----------|:-----------------------------------|:-----------------------------------------|:-------------------| -| smtp | SMTP over insecure connection | `smtp://username:password@host:25` | PHPMailerWrapper | -| tls | SMTP over secure TLS connection | `tls://username:password@host:587` | PHPMailerWrapper | -| ssl | SMTP over secure SSL connection | `ssl://username:password@host:587` | PHPMailerWrapper | -| sendmail | Sending Email using PHP mail() | `sendmail://localhost` | SendMailWrapper | -| mailgun | Sending Email using Mailgun API | `mailgun://YOUR_API_KEY@YOUR_DOMAIN` | MailgunApiWrapper | -| ses | Sending Email using Amazon AWS API | `ses://ACCESS_KEY_ID:SECRET_KEY@REGION` | AmazonSesWrapper | -| fakesender | Do nothing | `fakesender://anything` | FakeSenderWrapper | - -### Gmail specifics - -From December 2014, Google started imposing an authentication mechanism called -XOAUTH2 based on OAuth2 for access to their apps, including Gmail. -This change can break both SMTP and IMAP access to gmail, and you may receive -authentication failures (often "5.7.14 Please log in via your web browser") -from many email clients, including PHPMailer, Apple Mail, Outlook, Thunderbird and others. -The error output may include a link to -[https://support.google.com/mail/bin/answer.py?answer=78754](https://support.google.com/mail/bin/answer.py?answer=78754), which -gives a list of possible remedies. - -There are two main solutions: - -#### Sending through SMTP - -You have to enable the option "Allow less secure apps". -It does not really make your app significantly less secure. -Reportedly, changing this setting may take an hour or more to take effect, -so don't expect an immediate fix. You can start changing -[here](https://www.google.com/settings/security/lesssecureapps) - -The connection string for sending emails using SMTP through GMAIL is: - -```text -tls://YOUREMAIL@gmail.com:YOURPASSWORD@smtp.gmail.com:587 -``` +## Connection URL Schemes -#### Sending Through XOAuth2 +| Scheme | Description | URI Pattern | +|:-----------|:-----------------------------------|:-----------------------------------------| +| smtp | SMTP over insecure connection | `smtp://username:password@host:25` | +| tls | SMTP over secure TLS connection | `tls://username:password@host:587` | +| ssl | SMTP over secure SSL connection | `ssl://username:password@host:465` | +| sendmail | PHP's built-in mail() function | `sendmail://localhost` | +| mailgun | Mailgun API | `mailgun://YOUR_API_KEY@YOUR_DOMAIN` | +| ses | Amazon SES API | `ses://ACCESS_KEY_ID:SECRET_KEY@REGION` | +| fakesender | Testing (does nothing) | `fakesender://localhost` | -This option is currently unsupported. - -Further information and documentation on how to set up can be found on this -[wiki](https://github.com/PHPMailer/PHPMailer/wiki/Using-Gmail-with-XOAUTH2) page. - -### Amazon SES API specifics - -The connection url for the AWS SES api is: - -```text -ses://ACCESS_KEY_ID:SECRET_KEY@REGION -``` - -The access_key_id and secret_key are created at AWS Control Panel. The region can be us-east-1, etc. - -### Mailgun API specifics - -The connection url for the Mailgun api is: - -```text -mailgun://YOUR_API_KEY@YOUR_DOMAIN -``` - -The YOUR_API_KEY and YOUR_DOMAIN are defined at Mailgun Control Panel. - -The Region of the endpoint can be configured by query parameter "region" (example: mailgun://api-key@mg.domain.cz?region=eu) - -Valid values are: us and eu. - -### Sendmail Specifics - -The connection url for the Sendmail is: - -```text -sendmail://localhost -``` - -You need to setup in the `php.ini` the email relay. - -## Implementing your Own Wrappers - -To implement your own wrapper you have to create a class inherited from: `ByJG\Mail\Wrapper\BaseWrapper` and implement -how to send in the method: `public function send(Envelope $envelope);` - -```php -class MyWrapper extends \ByJG\Mail\Wrapper\BaseWrapper -{ - public static function schema() - { - return ['mywrapper']; - } - - public function send(Envelope $envelope): \ByJG\Mail\SendResult - { - // Do how to send the email using your library - } - - // You can create your own validation methods. - public function validate(Envelope $envelope) - { - parent::validate($envelope); - } -} -``` - -## Install - -```shell -composer require "byjg/mailwrapper" -``` +See [Connection Strings](docs/connection-strings.md) for detailed configuration examples. ## Running Tests @@ -234,12 +88,9 @@ composer require "byjg/mailwrapper" ```mermaid flowchart TD - byjg/mailwrapper --> ext-curl byjg/mailwrapper --> byjg/convert byjg/mailwrapper --> byjg/webrequest - byjg/mailwrapper --> aws/aws-sdk-php - byjg/mailwrapper --> phpmailer/phpmailer ``` ---- -[Open source ByJG](http://opensource.byjg.com) \ No newline at end of file +[Open source ByJG](http://opensource.byjg.com) diff --git a/composer.json b/composer.json index 6d8aa40..e1d5a08 100644 --- a/composer.json +++ b/composer.json @@ -14,16 +14,20 @@ "prefer-stable": true, "minimum-stability": "dev", "require": { - "php": ">=8.1 <8.4", + "php": ">=8.1 <8.5", "ext-curl": "*", - "byjg/convert": "^5.0", - "byjg/webrequest": "^5.0", + "byjg/convert": "^6.0", + "byjg/webrequest": "^6.0", "aws/aws-sdk-php": "~3.20", "phpmailer/phpmailer": ">=6.4.1" }, "require-dev": { - "phpunit/phpunit": "^9.6", - "vimeo/psalm": "^5.9" + "phpunit/phpunit": "^10|^11", + "vimeo/psalm": "^5.9|^6.12" + }, + "scripts": { + "test": "vendor/bin/phpunit", + "psalm": "vendor/bin/psalm" }, "license": "MIT" } diff --git a/docs/attachments.md b/docs/attachments.md new file mode 100644 index 0000000..0b506f4 --- /dev/null +++ b/docs/attachments.md @@ -0,0 +1,167 @@ +--- +sidebar_position: 5 +--- + +# Attachments + +The Envelope class supports two types of attachments: regular attachments and embedded images. + +## Regular Attachments + +Regular attachments appear as downloadable files in the email client. + +### Adding Attachments + +```php +$envelope = new \ByJG\Mail\Envelope('from@email.com', 'to@email.com', 'Subject', 'Body'); + +$envelope->addAttachment( + 'filename.pdf', // Name shown in email + '/path/to/file.pdf', // Path to file on disk + 'application/pdf' // MIME type +); + +// Add multiple attachments +$envelope->addAttachment('document.docx', '/path/to/doc.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); +$envelope->addAttachment('image.png', '/path/to/image.png', 'image/png'); + +$mailer->send($envelope); +``` + +### Common MIME Types + +| File Type | MIME Type | +|:----------|:----------| +| PDF | `application/pdf` | +| Word (.docx) | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | +| Excel (.xlsx) | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | +| PNG | `image/png` | +| JPEG | `image/jpeg` | +| GIF | `image/gif` | +| ZIP | `application/zip` | +| Plain Text | `text/plain` | +| CSV | `text/csv` | +| JSON | `application/json` | + +## Embedded Images + +Embedded images (inline attachments) are displayed directly in the email body rather than as downloadable files. This is useful for including logos, diagrams, or other images in your HTML email. + +### Adding Embedded Images + +```php +$envelope = new \ByJG\Mail\Envelope('from@email.com', 'to@email.com', 'Subject'); + +// Add the image as an embedded attachment +$envelope->addEmbedImage( + 'company_logo', // Content ID (used in HTML) + '/path/to/logo.png', // Path to image file + 'image/png' // MIME type +); + +// Reference the image in HTML using cid: protocol +$envelope->setBody('Logo'); + +$mailer->send($envelope); +``` + +### Multiple Embedded Images + +```php +$envelope = new \ByJG\Mail\Envelope('from@email.com', 'to@email.com', 'Newsletter'); + +// Add multiple images +$envelope->addEmbedImage('header', '/path/to/header.png', 'image/png'); +$envelope->addEmbedImage('chart', '/path/to/chart.jpg', 'image/jpeg'); +$envelope->addEmbedImage('footer', '/path/to/footer.png', 'image/png'); + +// Use all images in the HTML body +$html = ' + + + Header +

Check out our latest statistics:

+ Chart + Footer + +'; + +$envelope->setBody($html); +$mailer->send($envelope); +``` + +## Combining Both Types + +You can use both regular attachments and embedded images in the same email: + +```php +$envelope = new \ByJG\Mail\Envelope('from@email.com', 'to@email.com', 'Report'); + +// Embed logo in email body +$envelope->addEmbedImage('logo', '/path/to/logo.png', 'image/png'); + +// Attach PDF report for download +$envelope->addAttachment('monthly_report.pdf', '/path/to/report.pdf', 'application/pdf'); + +$html = ' + + + Company Logo +

Monthly Report

+

Please find the attached monthly report.

+ +'; + +$envelope->setBody($html); +$mailer->send($envelope); +``` + +## Getting Attachments + +You can retrieve all attachments from an envelope: + +```php +$attachments = $envelope->getAttachments(); + +// Returns an array like: +// [ +// 'filename.pdf' => [ +// 'content' => '/path/to/file.pdf', +// 'content-type' => 'application/pdf', +// 'disposition' => 'attachment' +// ], +// 'logo' => [ +// 'content' => '/path/to/logo.png', +// 'content-type' => 'image/png', +// 'disposition' => 'inline' +// ] +// ] +``` + +The `disposition` field indicates whether it's a regular attachment (`attachment`) or an embedded image (`inline`). + +## Best Practices + +### File Paths + +- Use absolute paths to ensure files are found regardless of the current working directory +- Verify files exist before adding them as attachments +- Consider file size limits imposed by email servers (typically 25MB total) + +### Content IDs + +- Use descriptive content IDs for embedded images (e.g., `company_logo`, `header_image`) +- Keep content IDs alphanumeric (avoid special characters) +- Content IDs are case-sensitive + +### MIME Types + +- Always specify the correct MIME type for better compatibility +- Use `image/png` for PNG files, `image/jpeg` for JPEG files +- For unknown types, `application/octet-stream` is a safe fallback + +### Email Client Compatibility + +- Not all email clients handle embedded images the same way +- Some clients may show embedded images as attachments in addition to displaying them inline +- Always include `alt` attributes for images for accessibility diff --git a/docs/connection-strings.md b/docs/connection-strings.md new file mode 100644 index 0000000..e44502e --- /dev/null +++ b/docs/connection-strings.md @@ -0,0 +1,109 @@ +--- +sidebar_position: 3 +--- + +# Connection Strings + +Connection strings define how to connect to mail services. They follow a URI format: + +```text +scheme://username:password@host:port +``` + +## URI Components + +| Part | Description | +|:-----------|:-------------------------------------------------------------------------------| +| scheme | The email scheme: smtp, ssl, tls, sendmail, mailgun, ses, fakesender | +| username | The username for authentication | +| password | The password for authentication | +| host | The SMTP host or service endpoint | +| port | The SMTP port | + +## Available Schemes + +| Scheme | Description | URI Pattern | Wrapper Class | +|:-----------|:-----------------------------------|:-----------------------------------------|:-------------------| +| smtp | SMTP over insecure connection | `smtp://username:password@host:25` | PHPMailerWrapper | +| tls | SMTP over secure TLS connection | `tls://username:password@host:587` | PHPMailerWrapper | +| ssl | SMTP over secure SSL connection | `ssl://username:password@host:465` | PHPMailerWrapper | +| sendmail | PHP's built-in mail() function | `sendmail://localhost` | SendMailWrapper | +| mailgun | Mailgun API | `mailgun://YOUR_API_KEY@YOUR_DOMAIN` | MailgunApiWrapper | +| ses | Amazon SES API | `ses://ACCESS_KEY_ID:SECRET_KEY@REGION` | AmazonSesWrapper | +| fakesender | Testing (does nothing) | `fakesender://localhost` | FakeSenderWrapper | + +## Examples + +### SMTP with TLS + +```php +$mailer = \ByJG\Mail\MailerFactory::create( + 'tls://username:password@smtp.example.com:587' +); +``` + +### Gmail + +```php +$mailer = \ByJG\Mail\MailerFactory::create( + 'tls://your.email@gmail.com:your_password@smtp.gmail.com:587' +); +``` + +:::info +Gmail requires you to enable "Allow less secure apps" in your Google account settings. +Visit [https://www.google.com/settings/security/lesssecureapps](https://www.google.com/settings/security/lesssecureapps) to enable this option. + +Changes may take up to an hour to take effect. +::: + +### Mailgun API + +```php +$mailer = \ByJG\Mail\MailerFactory::create( + 'mailgun://YOUR_API_KEY@YOUR_DOMAIN' +); +``` + +You can specify the region using a query parameter: + +```php +// EU region +$mailer = \ByJG\Mail\MailerFactory::create( + 'mailgun://YOUR_API_KEY@YOUR_DOMAIN?region=eu' +); +``` + +Valid regions: `us` (default), `eu` + +### Amazon SES + +```php +$mailer = \ByJG\Mail\MailerFactory::create( + 'ses://ACCESS_KEY_ID:SECRET_KEY@us-east-1' +); +``` + +The region can be any valid AWS region (e.g., `us-east-1`, `eu-west-1`, etc.). + +### SendMail (PHP mail() function) + +```php +$mailer = \ByJG\Mail\MailerFactory::create( + 'sendmail://localhost' +); +``` + +:::warning +You need to configure your email relay in `php.ini` for SendMail to work properly. +::: + +### FakeSender (Testing) + +```php +$mailer = \ByJG\Mail\MailerFactory::create( + 'fakesender://localhost' +); +``` + +The FakeSender wrapper does nothing and always returns success. It's useful for testing without actually sending emails. diff --git a/docs/custom-wrappers.md b/docs/custom-wrappers.md new file mode 100644 index 0000000..23cc6b7 --- /dev/null +++ b/docs/custom-wrappers.md @@ -0,0 +1,290 @@ +--- +sidebar_position: 6 +--- + +# Custom Wrappers + +You can implement your own mail wrapper to integrate with any email service or custom mail handling logic. + +## Creating a Custom Wrapper + +To create a custom wrapper, extend the `BaseWrapper` class and implement the required methods: + +```php +uri + $apiKey = $this->uri->getUsername(); + $apiSecret = $this->uri->getPassword(); + $host = $this->uri->getHost(); + + // Validate the envelope + $this->validate($envelope); + + // Your custom logic to send the email + $messageId = $this->sendViaMyService($envelope, $apiKey, $apiSecret, $host); + + // Return the result + return new SendResult(true, $messageId); + } + + private function sendViaMyService(Envelope $envelope, string $apiKey, string $apiSecret, string $host): string + { + // Implement your email sending logic here + // This might involve: + // - Making API calls + // - Formatting the email data + // - Handling attachments + // - Error handling + + return 'message-id-123'; + } +} +``` + +## Implementing the Schema Method + +The `schema()` method defines which URI schemes your wrapper handles. Return an array of scheme names: + +```php +public static function schema(): array +{ + return ['myservice']; // Handles myservice://... +} +``` + +You can support multiple schemes: + +```php +public static function schema(): array +{ + return ['myservice', 'myservice-ssl', 'myservice-tls']; +} +``` + +## Implementing the Send Method + +The `send()` method must: + +1. Accept an `Envelope` parameter +2. Return a `SendResult` object +3. Handle the actual email delivery + +```php +public function send(Envelope $envelope): SendResult +{ + // Validate the envelope (optional but recommended) + $this->validate($envelope); + + try { + // Your sending logic + $messageId = $this->performSend($envelope); + + // Return success + return new SendResult(true, $messageId); + + } catch (\Exception $e) { + // Handle errors - you might throw an exception or return failure + throw new \ByJG\Mail\Exception\MailApiException( + 'Failed to send email: ' . $e->getMessage() + ); + } +} +``` + +## Accessing Connection Details + +The URI is available via `$this->uri`: + +```php +// Get URI components +$scheme = $this->uri->getScheme(); // e.g., 'myservice' +$username = $this->uri->getUsername(); // e.g., 'api-key' +$password = $this->uri->getPassword(); // e.g., 'secret' +$host = $this->uri->getHost(); // e.g., 'api.myservice.com' +$port = $this->uri->getPort(); // e.g., 443 +$query = $this->uri->getQuery(); // e.g., 'region=us' + +// Get query parameters +$region = $this->uri->getQueryPart('region'); // Extract specific parameter +``` + +## Custom Validation + +You can add custom validation by overriding the `validate()` method: + +```php +public function validate(Envelope $envelope): void +{ + // Call parent validation + parent::validate($envelope); + + // Add your custom validation + if (empty($envelope->getSubject())) { + throw new \ByJG\Mail\Exception\InvalidMessageFormatException( + 'Subject is required' + ); + } + + if (!$envelope->isHtml() && !empty($envelope->getAttachments())) { + throw new \ByJG\Mail\Exception\InvalidMessageFormatException( + 'Attachments require HTML mode' + ); + } +} +``` + +The base `validate()` method checks: +- At least one recipient exists +- From address is set + +## Handling Attachments + +Process attachments from the envelope: + +```php +private function processAttachments(Envelope $envelope): array +{ + $processed = []; + + foreach ($envelope->getAttachments() as $name => $attachment) { + $filePath = $attachment['content']; + $mimeType = $attachment['content-type']; + $disposition = $attachment['disposition']; // 'attachment' or 'inline' + + // Read file content + $content = file_get_contents($filePath); + $encoded = base64_encode($content); + + $processed[] = [ + 'name' => $name, + 'type' => $mimeType, + 'content' => $encoded, + 'disposition' => $disposition, + ]; + } + + return $processed; +} +``` + +## Registering Your Wrapper + +Register your custom wrapper with the factory: + +```php +\ByJG\Mail\MailerFactory::registerMailer(\MyApp\Mail\MyCustomWrapper::class); + +// Now you can use it +$mailer = \ByJG\Mail\MailerFactory::create('myservice://api-key:secret@api.example.com'); +``` + +## Complete Example + +Here's a complete example implementing a simple HTTP API wrapper: + +```php +validate($envelope); + + $apiUrl = sprintf( + 'https://%s/send', + $this->uri->getHost() + ); + + $payload = [ + 'from' => $envelope->getFrom(), + 'to' => $envelope->getTo(), + 'subject' => $envelope->getSubject(), + 'html' => $envelope->getBody(), + 'api_key' => $this->uri->getUsername(), + ]; + + if (!empty($envelope->getCC())) { + $payload['cc'] = $envelope->getCC(); + } + + if (!empty($envelope->getBCC())) { + $payload['bcc'] = $envelope->getBCC(); + } + + $ch = curl_init($apiUrl); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + throw new MailApiException( + sprintf('API returned error: %d - %s', $httpCode, $response) + ); + } + + $data = json_decode($response, true); + + return new SendResult(true, $data['message_id'] ?? null); + } +} +``` + +Usage: + +```php +\ByJG\Mail\MailerFactory::registerMailer(\MyApp\Mail\HttpApiWrapper::class); + +$mailer = \ByJG\Mail\MailerFactory::create('httpapi://YOUR_API_KEY@api.emailservice.com'); +$mailer->send($envelope); +``` + +## Best Practices + +1. **Error Handling**: Always handle errors gracefully and throw appropriate exceptions +2. **Validation**: Call `parent::validate()` before your custom validation +3. **Return Values**: Always return a `SendResult` object with appropriate success status +4. **Message IDs**: Include message IDs when available for tracking +5. **Testing**: Create unit tests for your wrapper using the FakeSender as a reference +6. **Documentation**: Document your wrapper's connection string format and requirements diff --git a/docs/envelope.md b/docs/envelope.md new file mode 100644 index 0000000..62c6e3f --- /dev/null +++ b/docs/envelope.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +--- + +# Envelope + +The Envelope class represents an email message. It's totally decoupled from the Mailer engine, so you can also use it as a DTO (Data Transfer Object). + +## Creating an Envelope + +### Basic Constructor + +```php +$envelope = new \ByJG\Mail\Envelope(); +``` + +### Constructor with Parameters + +```php +$envelope = new \ByJG\Mail\Envelope( + 'from@example.com', // From address + 'to@example.com', // To address + 'Email Subject', // Subject + '

HTML Body

', // Body + true // isHtml (default: true) +); +``` + +## Setting Email Properties + +### From Address + +```php +// Simple from address +$envelope->setFrom('johndoe@example.com'); + +// From address with name +$envelope->setFrom('johndoe@example.com', 'John Doe'); +``` + +### Recipients + +```php +// Set a single recipient (replaces existing) +$envelope->setTo('jane@example.com', 'Jane Doe'); + +// Add multiple recipients +$envelope->addTo('user1@example.com'); +$envelope->addTo('user2@example.com', 'User Two'); +``` + +### CC and BCC + +```php +// Carbon Copy +$envelope->addCC('manager@example.com', 'Manager'); +$envelope->setCC('manager@example.com'); // Replaces all CC + +// Blind Carbon Copy +$envelope->addBCC('admin@example.com'); +$envelope->setBCC('admin@example.com'); // Replaces all BCC +``` + +### Subject and Body + +```php +// Set subject +$envelope->setSubject('Important Notice'); + +// Set HTML body +$envelope->setBody('

Hello

This is an HTML email

'); +$envelope->isHtml(true); + +// Set plain text body +$envelope->setBody('This is plain text'); +$envelope->isHtml(false); +``` + +### Reply-To + +```php +$envelope->setReplyTo('support@example.com'); + +// If not set, defaults to the From address +$replyTo = $envelope->getReplyTo(); +``` + +## Getting Properties + +All properties have corresponding getter methods: + +```php +$from = $envelope->getFrom(); +$to = $envelope->getTo(); // Returns array +$subject = $envelope->getSubject(); +$body = $envelope->getBody(); +$cc = $envelope->getCC(); // Returns array +$bcc = $envelope->getBCC(); // Returns array +$isHtml = $envelope->isHtml(); +``` + +## Text Body Generation + +The Envelope can automatically generate a plain text version of an HTML body: + +```php +$envelope->setBody('

Title

Paragraph

'); +$textBody = $envelope->getBodyText(); +// Returns: "# Title\n\nParagraph\n" +``` + +This is useful for multipart emails that include both HTML and plain text versions. diff --git a/docs/exceptions.md b/docs/exceptions.md new file mode 100644 index 0000000..12fcdb4 --- /dev/null +++ b/docs/exceptions.md @@ -0,0 +1,282 @@ +--- +sidebar_position: 7 +--- + +# Exceptions + +MailWrapper defines several exception classes to handle different error conditions. All exceptions are in the `ByJG\Mail\Exception` namespace. + +## Exception Hierarchy + +All MailWrapper exceptions extend standard PHP exceptions, allowing you to catch them individually or as a group. + +```text +\Exception +├── InvalidEMailException +├── InvalidMailHandlerException +├── InvalidMessageFormatException +├── MailApiException +└── ProtocolNotRegisteredException +``` + +## Exception Types + +### InvalidEMailException + +Thrown when email validation fails. + +**Common causes:** +- Missing sender address +- Missing recipient address +- Invalid email format + +**Example:** + +```php +use ByJG\Mail\Exception\InvalidEMailException; + +try { + $envelope = new \ByJG\Mail\Envelope(); + // No from or to addresses set + $mailer->send($envelope); +} catch (InvalidEMailException $e) { + echo "Email validation failed: " . $e->getMessage(); + // Output: "Destination Email was not provided" +} +``` + +### ProtocolNotRegisteredException + +Thrown when trying to create a mailer for an unregistered protocol scheme. + +**Common causes:** +- Forgetting to register a wrapper with `MailerFactory::registerMailer()` +- Using an invalid or misspelled scheme in the connection string +- Wrapper not installed or available + +**Example:** + +```php +use ByJG\Mail\Exception\ProtocolNotRegisteredException; + +try { + // Forgot to register the SMTP wrapper + $mailer = \ByJG\Mail\MailerFactory::create('smtp://user:pass@host'); +} catch (ProtocolNotRegisteredException $e) { + echo "Protocol error: " . $e->getMessage(); + // Output: "Protocol not found/registered!" +} +``` + +**Solution:** + +```php +// Register the wrapper before creating the mailer +\ByJG\Mail\MailerFactory::registerMailer(\ByJG\Mail\Wrapper\PHPMailerWrapper::class); +$mailer = \ByJG\Mail\MailerFactory::create('smtp://user:pass@host'); +``` + +### InvalidMailHandlerException + +Thrown when trying to register an invalid wrapper class. + +**Common causes:** +- Registering a class that doesn't implement `MailWrapperInterface` +- Passing a non-existent class name + +**Example:** + +```php +use ByJG\Mail\Exception\InvalidMailHandlerException; + +try { + \ByJG\Mail\MailerFactory::registerMailer(\MyApp\InvalidClass::class); +} catch (InvalidMailHandlerException $e) { + echo "Registration error: " . $e->getMessage(); + // Output: "Class not implements ConnectorInterface!" +} +``` + +### InvalidMessageFormatException + +Thrown when the message format is invalid or incomplete. + +**Common causes:** +- Invalid message structure +- Missing required fields +- Malformed content + +**Example:** + +```php +use ByJG\Mail\Exception\InvalidMessageFormatException; + +// This exception might be thrown by custom wrappers with additional validation +try { + // Some wrapper that validates subject is not empty + $mailer->send($envelope); +} catch (InvalidMessageFormatException $e) { + echo "Message format error: " . $e->getMessage(); +} +``` + +### MailApiException + +Thrown when an email API returns an error. + +**Common causes:** +- Invalid API credentials +- API rate limiting +- Network connectivity issues +- Service outages +- Invalid email addresses rejected by the API + +**Example:** + +```php +use ByJG\Mail\Exception\MailApiException; + +try { + $mailer = \ByJG\Mail\MailerFactory::create('mailgun://invalid_key@domain.com'); + $mailer->send($envelope); +} catch (MailApiException $e) { + echo "API error: " . $e->getMessage(); + // Might include API-specific error details +} +``` + +## Exception Handling Best Practices + +### Catch Specific Exceptions + +Handle different exception types appropriately: + +```php +use ByJG\Mail\Exception\{ + InvalidEMailException, + ProtocolNotRegisteredException, + MailApiException +}; + +try { + $mailer = \ByJG\Mail\MailerFactory::create($connection); + $result = $mailer->send($envelope); + +} catch (InvalidEMailException $e) { + // Validation error - fix the envelope data + error_log("Invalid email configuration: " . $e->getMessage()); + +} catch (ProtocolNotRegisteredException $e) { + // Configuration error - register the protocol + error_log("Missing protocol registration: " . $e->getMessage()); + +} catch (MailApiException $e) { + // API error - might be temporary, consider retrying + error_log("Mail service API error: " . $e->getMessage()); + +} catch (\Exception $e) { + // Catch-all for unexpected errors + error_log("Unexpected error: " . $e->getMessage()); +} +``` + +### Validate Before Sending + +Validate the envelope before attempting to send: + +```php +$envelope = new \ByJG\Mail\Envelope(); + +// Validate manually if needed +if (empty($envelope->getFrom())) { + throw new \ByJG\Mail\Exception\InvalidEMailException('From address required'); +} + +if (count($envelope->getTo()) === 0) { + throw new \ByJG\Mail\Exception\InvalidEMailException('At least one recipient required'); +} +``` + +### Logging and Debugging + +Include context in error handling: + +```php +try { + $result = $mailer->send($envelope); + + if (!$result->success) { + error_log(sprintf( + 'Failed to send email to %s: %s', + implode(', ', $envelope->getTo()), + 'Unknown error' + )); + } + +} catch (\Exception $e) { + error_log(sprintf( + 'Exception sending email to %s: %s - %s', + implode(', ', $envelope->getTo()), + get_class($e), + $e->getMessage() + )); + + // Optionally include stack trace in development + if (DEBUG_MODE) { + error_log($e->getTraceAsString()); + } +} +``` + +### Retry Logic + +Implement retry logic for transient failures: + +```php +use ByJG\Mail\Exception\MailApiException; + +function sendWithRetry($mailer, $envelope, $maxAttempts = 3): bool +{ + $attempt = 0; + + while ($attempt < $maxAttempts) { + try { + $result = $mailer->send($envelope); + return $result->success; + + } catch (MailApiException $e) { + $attempt++; + + if ($attempt >= $maxAttempts) { + throw $e; + } + + // Exponential backoff + sleep(pow(2, $attempt)); + } + } + + return false; +} +``` + +### Graceful Degradation + +Consider fallback mechanisms: + +```php +$primaryMailer = \ByJG\Mail\MailerFactory::create('mailgun://key@domain'); +$fallbackMailer = \ByJG\Mail\MailerFactory::create('smtp://user:pass@host:587'); + +try { + $result = $primaryMailer->send($envelope); +} catch (\Exception $e) { + error_log('Primary mailer failed, using fallback: ' . $e->getMessage()); + try { + $result = $fallbackMailer->send($envelope); + } catch (\Exception $e2) { + error_log('Fallback mailer also failed: ' . $e2->getMessage()); + throw $e2; + } +} +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..df93620 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 1 +--- + +# Getting Started + +A lightweight wrapper for sending email. The interface is totally decoupled from the sender, providing a single interface for sending mail regardless of the underlying mail service. + +## Installation + +```shell +composer require "byjg/mailwrapper" +``` + +## Quick Start + +Here's a simple example to get you started: + +```php +setFrom('johndoe@example.com', 'John Doe'); +$envelope->addTo('jane@example.com'); +$envelope->setSubject('Email Subject'); +$envelope->setBody('

Hello World

This is an HTML email

'); + +// Register the available mailers +\ByJG\Mail\MailerFactory::registerMailer(\ByJG\Mail\Wrapper\PHPMailerWrapper::class); +\ByJG\Mail\MailerFactory::registerMailer(\ByJG\Mail\Wrapper\MailgunApiWrapper::class); + +// Create the mailer based on the connection string +$mailer = \ByJG\Mail\MailerFactory::create('smtp://username:password@smtp.example.com:587'); + +// Send the email +$result = $mailer->send($envelope); + +if ($result->success) { + echo "Email sent successfully! ID: " . $result->id; +} +``` + +## Architecture + +MailWrapper is organized into three main components: + +- **The Envelope**: The mail envelope. Defines the mail sender, recipients, body, subject, etc. +- **The Mailer**: Responsible for the process of sending the envelope +- **The Factory**: Registers and creates the available Mailers in the system + +## Available Wrappers + +- **SMTP** - Standard SMTP with SSL/TLS support +- **AWS SES** - Amazon Simple Email Service (using API directly) +- **Mailgun** - Mailgun API +- **SendMail** - PHP's built-in mail() function +- **FakeSender** - For testing (does nothing) + +## Running Tests + +```shell +./vendor/bin/phpunit +``` diff --git a/docs/mailer-factory.md b/docs/mailer-factory.md new file mode 100644 index 0000000..0042354 --- /dev/null +++ b/docs/mailer-factory.md @@ -0,0 +1,162 @@ +--- +sidebar_position: 4 +--- + +# Mailer Factory + +The MailerFactory is responsible for creating mailer instances based on connection strings. It uses a registration pattern that allows you to register which mailer wrappers are available in your application. + +## Registering Mailers + +Before creating a mailer, you need to register the wrapper classes you want to use: + +```php +// Register individual wrappers +\ByJG\Mail\MailerFactory::registerMailer(\ByJG\Mail\Wrapper\PHPMailerWrapper::class); +\ByJG\Mail\MailerFactory::registerMailer(\ByJG\Mail\Wrapper\MailgunApiWrapper::class); +\ByJG\Mail\MailerFactory::registerMailer(\ByJG\Mail\Wrapper\AmazonSesWrapper::class); +``` + +You only need to register the wrappers you plan to use. This keeps your application lightweight by not loading unnecessary dependencies. + +## Creating Mailers + +Once registered, create a mailer using a connection string: + +```php +$mailer = \ByJG\Mail\MailerFactory::create('smtp://username:password@host:587'); +``` + +The factory automatically selects the appropriate wrapper based on the connection string's scheme. + +### Using URI Objects + +You can also pass a URI object instead of a string: + +```php +$uri = new \ByJG\Util\Uri('tls://username:password@smtp.example.com:587'); +$mailer = \ByJG\Mail\MailerFactory::create($uri); +``` + +## Creating Mailers Directly + +You can bypass the factory and instantiate wrappers directly: + +```php +$mailer = new \ByJG\Mail\Wrapper\MailgunApiWrapper( + new \ByJG\Util\Uri('mailgun://YOUR_API_KEY@YOUR_DOMAIN') +); +``` + +This approach is useful when: +- You only use a single mailer type +- You want to avoid the registration step +- You need more control over instantiation + +## Sending Emails + +All mailers implement the same interface: + +```php +$envelope = new \ByJG\Mail\Envelope( + 'from@example.com', + 'to@example.com', + 'Subject', + 'Body content' +); + +$result = $mailer->send($envelope); + +if ($result->success) { + echo "Email sent! Message ID: " . $result->id; +} else { + echo "Failed to send email"; +} +``` + +## Send Result + +The `send()` method returns a `SendResult` object with two properties: + +```php +class SendResult +{ + public readonly bool $success; // true if email was sent + public readonly ?string $id; // Message ID (if available) +} +``` + +Example: + +```php +$result = $mailer->send($envelope); + +if ($result->success) { + // Email was sent successfully + if ($result->id !== null) { + // Some services provide a message ID for tracking + log("Email sent with ID: " . $result->id); + } +} +``` + +## Exception Handling + +The factory may throw exceptions: + +```php +use ByJG\Mail\Exception\ProtocolNotRegisteredException; +use ByJG\Mail\Exception\InvalidMailHandlerException; + +try { + $mailer = \ByJG\Mail\MailerFactory::create('unknown://host'); +} catch (ProtocolNotRegisteredException $e) { + // The scheme 'unknown' hasn't been registered + echo "Protocol not registered: " . $e->getMessage(); +} +``` + +## Best Practices + +### Configuration Management + +Store connection strings in configuration files or environment variables: + +```php +// .env file +MAIL_CONNECTION=tls://user:pass@smtp.example.com:587 + +// In your application +$mailer = \ByJG\Mail\MailerFactory::create($_ENV['MAIL_CONNECTION']); +``` + +### Single Registration Point + +Register all mailers once during application bootstrap: + +```php +// bootstrap.php +$wrappers = [ + \ByJG\Mail\Wrapper\PHPMailerWrapper::class, + \ByJG\Mail\Wrapper\MailgunApiWrapper::class, + \ByJG\Mail\Wrapper\AmazonSesWrapper::class, + \ByJG\Mail\Wrapper\SendMailWrapper::class, +]; + +foreach ($wrappers as $wrapper) { + \ByJG\Mail\MailerFactory::registerMailer($wrapper); +} +``` + +### Testing + +Use FakeSender for testing: + +```php +// Test configuration +if ($isTestEnvironment) { + $mailer = \ByJG\Mail\MailerFactory::create('fakesender://localhost'); +} else { + $mailer = \ByJG\Mail\MailerFactory::create($productionConnection); +} +``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d5df793..c090a6d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,14 +6,21 @@ and open the template in the editor. --> - + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnPhpunitDeprecations="true" + failOnWarning="true" + failOnNotice="true" + failOnDeprecation="true" + failOnPhpunitDeprecation="true" + stopOnFailure="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"> @@ -21,16 +28,15 @@ and open the template in the editor. - - - ./src - - + + + ./src/ + + ./tests/ - - + \ No newline at end of file diff --git a/psalm.xml b/psalm.xml index ebabb1a..b208114 100644 --- a/psalm.xml +++ b/psalm.xml @@ -4,6 +4,7 @@ resolveFromConfigFile="true" findUnusedBaselineEntry="true" findUnusedCode="false" + cacheDirectory="/tmp/psalm" 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" diff --git a/src/MailerFactory.php b/src/MailerFactory.php index 9d2200b..ab7cb93 100644 --- a/src/MailerFactory.php +++ b/src/MailerFactory.php @@ -11,6 +11,7 @@ use ByJG\Mail\Exception\ProtocolNotRegisteredException; use ByJG\Mail\Wrapper\MailWrapperInterface; use ByJG\Util\Uri; +use Psr\Http\Message\UriInterface; class MailerFactory { @@ -18,9 +19,12 @@ class MailerFactory /** * @param string $class + * * @throws InvalidMailHandlerException + * + * @return void */ - public static function registerMailer(string $class) + public static function registerMailer(string $class): void { if (!in_array(MailWrapperInterface::class, class_implements($class))) { throw new InvalidMailHandlerException('Class not implements ConnectorInterface!'); @@ -35,13 +39,16 @@ public static function registerMailer(string $class) /** - * @param string $connection + * @param Uri|string $connection * @return MailWrapperInterface * @throws ProtocolNotRegisteredException */ - public static function create(string $connection): MailWrapperInterface + public static function create(UriInterface|string $connection): MailWrapperInterface { - $uri = new Uri($connection); + $uri = $connection; + if (is_string($connection)) { + $uri = new Uri($connection); + } if (!isset(self::$config[$uri->getScheme()])) { throw new ProtocolNotRegisteredException('Protocol not found/registered!'); diff --git a/src/Wrapper/AmazonSesWrapper.php b/src/Wrapper/AmazonSesWrapper.php index ac27bac..28d8650 100644 --- a/src/Wrapper/AmazonSesWrapper.php +++ b/src/Wrapper/AmazonSesWrapper.php @@ -13,15 +13,16 @@ class AmazonSesWrapper extends PHPMailerWrapper { + #[\Override] public static function schema(): array { return ['ses']; } /** - * @return mixed + * @return SesClient */ - public function getSesClient(): mixed + public function getSesClient(): SesClient { //Send the message (which must be base 64 encoded): return new SesClient([ @@ -43,6 +44,7 @@ public function getSesClient(): mixed * @throws InvalidEMailException * @throws InvalidMessageFormatException */ + #[\Override] public function send(Envelope $envelope): SendResult { $this->validate($envelope); diff --git a/src/Wrapper/FakeSenderWrapper.php b/src/Wrapper/FakeSenderWrapper.php index f634dc5..a87bc0d 100644 --- a/src/Wrapper/FakeSenderWrapper.php +++ b/src/Wrapper/FakeSenderWrapper.php @@ -7,11 +7,13 @@ class FakeSenderWrapper extends BaseWrapper { + #[\Override] public static function schema(): array { return ['fake', 'fakesender']; } + #[\Override] public function send(Envelope $envelope): SendResult { return new SendResult(true, 'fake-id-123'); diff --git a/src/Wrapper/MailgunApiWrapper.php b/src/Wrapper/MailgunApiWrapper.php index cdea6f2..2dc4c47 100644 --- a/src/Wrapper/MailgunApiWrapper.php +++ b/src/Wrapper/MailgunApiWrapper.php @@ -27,12 +27,13 @@ class MailgunApiWrapper extends PHPMailerWrapper 'eu' => 'api.eu.mailgun.net', ]; + #[\Override] public static function schema(): array { return ['mailgun']; } - public function __construct(Uri $uri, ClientInterface $client = null) + public function __construct(Uri $uri, ?ClientInterface $client = null) { parent::__construct($uri); @@ -53,7 +54,7 @@ public function getRequestObject(): RequestInterface $domainName = $this->uri->getHost(); $apiUri = $this->getApiUri(); - $uri = Uri::getInstanceFromString("https://$apiUri/v3/$domainName/messages") + $uri = Uri::getInstance("https://$apiUri/v3/$domainName/messages") ->withUserInfo('api', $this->uri->getUsername()); return Request::getInstance($uri)->withMethod("POST"); @@ -71,6 +72,7 @@ public function getRequestObject(): RequestInterface * @throws NetworkException * @throws RequestException */ + #[\Override] public function send(Envelope $envelope): SendResult { $this->validate($envelope); diff --git a/src/Wrapper/PHPMailerWrapper.php b/src/Wrapper/PHPMailerWrapper.php index 43072f1..55f7ed3 100644 --- a/src/Wrapper/PHPMailerWrapper.php +++ b/src/Wrapper/PHPMailerWrapper.php @@ -13,6 +13,7 @@ class PHPMailerWrapper extends BaseWrapper { + #[\Override] public static function schema(): array { return ['smtp', 'tls', 'ssl']; @@ -106,6 +107,7 @@ protected function prepareMailer(Envelope $envelope): PHPMailerOverride * @throws InvalidEMailException * @throws MailApiException */ + #[\Override] public function send(Envelope $envelope): SendResult { $this->validate($envelope); diff --git a/src/Wrapper/SendMailWrapper.php b/src/Wrapper/SendMailWrapper.php index ac75ede..0d82150 100644 --- a/src/Wrapper/SendMailWrapper.php +++ b/src/Wrapper/SendMailWrapper.php @@ -18,6 +18,7 @@ class SendMailWrapper extends PHPMailerWrapper { + #[\Override] public static function schema(): array { return ['sendmail']; @@ -30,6 +31,7 @@ public static function schema(): array * @throws InvalidEMailException * @throws InvalidMessageFormatException */ + #[\Override] public function send(Envelope $envelope): SendResult { $this->validate($envelope); diff --git a/tests/AmazonSesWrapperTest.php b/tests/AmazonSesTestWrapper.php similarity index 88% rename from tests/AmazonSesWrapperTest.php rename to tests/AmazonSesTestWrapper.php index e449c61..2217462 100644 --- a/tests/AmazonSesWrapperTest.php +++ b/tests/AmazonSesTestWrapper.php @@ -3,6 +3,7 @@ namespace Tests; use Aws\Credentials\Credentials; +use ByJG\Mail\Envelope; use ByJG\Mail\Exception\InvalidEMailException; use ByJG\Mail\Exception\InvalidMessageFormatException; use ByJG\Mail\SendResult; @@ -10,7 +11,7 @@ use ByJG\Util\Uri; use PHPMailer\PHPMailer\Exception; -class AmazonSesWrapperTest extends BaseWrapperTest +class AmazonSesTestWrapper extends BaseTestWrapper { /** * @param $envelope @@ -19,7 +20,7 @@ class AmazonSesWrapperTest extends BaseWrapperTest * @throws InvalidMessageFormatException * @throws Exception */ - public function doMockedRequest($envelope): array + public function doMockedRequest(Envelope $envelope): array { $object = $this->getMockBuilder(AmazonSesWrapper::class) ->onlyMethods(['getSesClient']) @@ -29,14 +30,14 @@ public function doMockedRequest($envelope): array $mock = new MockSender(); $object->expects($this->once()) ->method('getSesClient') - ->will($this->returnValue($mock)); + ->willReturn($mock); $result = $object->send($envelope); return [$mock, $result]; } - public function testGetSesClient() + public function testGetSesClient(): void { $sesWrapper = new AmazonSesWrapper(new Uri('ses://ACCESS_KEY_ID:SECRET_KEY@REGION')); $sesClient = $sesWrapper->getSesClient(); @@ -58,7 +59,7 @@ public function testGetSesClient() * @throws InvalidMessageFormatException * @throws InvalidEMailException */ - protected function send($envelope, $rawEmail): SendResult + protected function send(Envelope $envelope, string $rawEmail): SendResult { [$mock, $result] = $this->doMockedRequest($envelope); $mimeMessage = $this->fixVariableFields(file_get_contents(__DIR__ . '/resources/' . $rawEmail . '.eml')); @@ -80,7 +81,7 @@ protected function send($envelope, $rawEmail): SendResult * @throws InvalidMessageFormatException * @throws InvalidEMailException */ - public function testBasicEnvelope() + public function testBasicEnvelope(): void { $envelope = $this->getBasicEnvelope(); $result = $this->send($envelope, 'basicenvelope'); @@ -94,7 +95,7 @@ public function testBasicEnvelope() * @throws InvalidMessageFormatException * @throws InvalidEMailException */ - public function testFullEnvelope() + public function testFullEnvelope(): void { $envelope = $this->getFullEnvelope(); $result = $this->send($envelope, 'fullenvelope'); @@ -108,7 +109,7 @@ public function testFullEnvelope() * @throws InvalidMessageFormatException * @throws InvalidEMailException */ - public function testAttachmentEnvelope() + public function testAttachmentEnvelope(): void { $envelope = $this->getAttachmentEnvelope(); $result = $this->send($envelope, 'attachmentenvelope'); @@ -122,7 +123,7 @@ public function testAttachmentEnvelope() * @throws InvalidMessageFormatException * @throws InvalidEMailException */ - public function testEmbedImageEnvelope() + public function testEmbedImageEnvelope(): void { $envelope = $this->getEmbedImageEnvelope(); $result = $this->send($envelope, 'embedenvelope'); diff --git a/tests/BaseWrapperTest.php b/tests/BaseTestWrapper.php similarity index 78% rename from tests/BaseWrapperTest.php rename to tests/BaseTestWrapper.php index 80c187c..cac6e23 100644 --- a/tests/BaseWrapperTest.php +++ b/tests/BaseTestWrapper.php @@ -6,12 +6,12 @@ use ByJG\Mail\Util; use PHPUnit\Framework\TestCase; -abstract class BaseWrapperTest extends TestCase +abstract class BaseTestWrapper extends TestCase { /** - * @return \ByJG\Mail\Envelope + * @return Envelope */ - public function getBasicEnvelope() + public function getBasicEnvelope(): Envelope { $envelope = new Envelope( Util::getFullEmail('from@email.com', "João"), @@ -22,7 +22,7 @@ public function getBasicEnvelope() return $envelope; } - public function getFullEnvelope() + public function getFullEnvelope(): Envelope { $envelope = $this->getBasicEnvelope(); $envelope->addTo('to2@email.com', 'Name'); @@ -33,7 +33,7 @@ public function getFullEnvelope() return $envelope; } - public function getAttachmentEnvelope() + public function getAttachmentEnvelope(): Envelope { $envelope = $this->getFullEnvelope(); $envelope->addAttachment('myname', __DIR__ . '/resources/attachment1.txt', 'text/plain'); @@ -41,7 +41,7 @@ public function getAttachmentEnvelope() return $envelope; } - public function getEmbedImageEnvelope() + public function getEmbedImageEnvelope(): Envelope { $envelope = $this->getFullEnvelope(); $envelope->addEmbedImage('myname', __DIR__ . '/resources/pixel.gif', 'image/gif'); @@ -49,7 +49,12 @@ public function getEmbedImageEnvelope() return $envelope; } - protected function fixVariableFields($text) + /** + * @return null|string|string[] + * + * @psalm-return array|null|string + */ + protected function fixVariableFields($text): array|string|null { $text = preg_replace( [ @@ -70,7 +75,12 @@ protected function fixVariableFields($text) return $text; } - protected function fixRequestBody($text) + /** + * @return null|string|string[] + * + * @psalm-return array|null|string + */ + protected function fixRequestBody($text): array|string|null { $text = preg_replace( [ @@ -83,9 +93,9 @@ protected function fixRequestBody($text) [ '', '--12345', - file_get_contents(__DIR__ . "/resources/pixel.gif"), - file_get_contents(__DIR__ . "/resources/moon.png"), - file_get_contents(__DIR__ . "/resources/sun.png"), + file_get_contents(__DIR__ . "/resources/pixel.gif") ?: '', + file_get_contents(__DIR__ . "/resources/moon.png") ?: '', + file_get_contents(__DIR__ . "/resources/sun.png") ?: '', ], $text ); diff --git a/tests/EnvelopeTest.php b/tests/EnvelopeTest.php index 0152840..1aa852c 100644 --- a/tests/EnvelopeTest.php +++ b/tests/EnvelopeTest.php @@ -1,7 +1,8 @@ object = new Envelope; @@ -25,12 +27,13 @@ protected function setUp(): void * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ + #[\Override] protected function tearDown(): void { } - public function testGetFrom() + public function testGetFrom(): void { $this->object->setFrom('some@email.com'); $this->assertEquals('some@email.com', $this->object->getFrom()); @@ -39,7 +42,7 @@ public function testGetFrom() $this->assertEquals('"John Doe" ', $this->object->getFrom()); } - public function testGetTo() + public function testGetTo(): void { $this->object->addTo('some@email.com'); $this->assertEquals(['some@email.com'], $this->object->getTo()); @@ -51,13 +54,13 @@ public function testGetTo() $this->assertEquals(['"Only This" '], $this->object->getTo()); } - public function testGetSubject() + public function testGetSubject(): void { $this->object->setSubject('Test'); $this->assertEquals('Test', $this->object->getSubject()); } - public function testGetCC() + public function testGetCC(): void { $this->object->addCC('some@email.com'); $this->assertEquals(['some@email.com'], $this->object->getCC()); @@ -69,7 +72,7 @@ public function testGetCC() $this->assertEquals(['"Only This" '], $this->object->getCC()); } - public function testGetBCC() + public function testGetBCC(): void { $this->object->addBCC('some@email.com'); $this->assertEquals(['some@email.com'], $this->object->getBCC()); @@ -82,7 +85,7 @@ public function testGetBCC() } - public function testGetBody() + public function testGetBody(): void { $this->object->setBody('

Some title

Other test
Break

'); $this->assertEquals('

Some title

Other test
Break

', $this->object->getBody()); @@ -90,7 +93,7 @@ public function testGetBody() } - public function testGetAttachments() + public function testGetAttachments(): void { $this->object->addAttachment('name1', '/path/to/file', 'mime/type'); $this->assertEquals( diff --git a/tests/FakeSenderWrapperTest.php b/tests/FakeSenderTestWrapper.php similarity index 84% rename from tests/FakeSenderWrapperTest.php rename to tests/FakeSenderTestWrapper.php index 3a8a6fc..76ca88c 100644 --- a/tests/FakeSenderWrapperTest.php +++ b/tests/FakeSenderTestWrapper.php @@ -10,7 +10,7 @@ use ByJG\Mail\Wrapper\FakeSenderWrapper; use ByJG\Util\Uri; -class FakeSenderWrapperTest extends BaseWrapperTest +class FakeSenderTestWrapper extends BaseTestWrapper { /** * @param Envelope $envelope @@ -24,7 +24,7 @@ public function doFakeSend(Envelope $envelope): SendResult return $wrapper->send($envelope); } - public function testBasicEnvelope() + public function testBasicEnvelope(): void { $envelope = $this->getBasicEnvelope(); @@ -34,7 +34,7 @@ public function testBasicEnvelope() $this->assertEquals('fake-id-123', $result->id); } - public function testFullEnvelope() + public function testFullEnvelope(): void { $envelope = $this->getFullEnvelope(); @@ -44,7 +44,7 @@ public function testFullEnvelope() $this->assertEquals('fake-id-123', $result->id); } - public function testAttachmentEnvelope() + public function testAttachmentEnvelope(): void { $envelope = $this->getAttachmentEnvelope(); @@ -54,7 +54,7 @@ public function testAttachmentEnvelope() $this->assertEquals('fake-id-123', $result->id); } - public function testEmbedImageEnvelope() + public function testEmbedImageEnvelope(): void { $envelope = $this->getEmbedImageEnvelope(); diff --git a/tests/Functional/AmazonSesFunctionalTest.php b/tests/Functional/AmazonSesFunctionalTest.php index 9272ac9..4e937f3 100644 --- a/tests/Functional/AmazonSesFunctionalTest.php +++ b/tests/Functional/AmazonSesFunctionalTest.php @@ -2,15 +2,19 @@ namespace Tests\Functional; +use ByJG\Mail\Exception\InvalidMailHandlerException; +use ByJG\Mail\Exception\ProtocolNotRegisteredException; use ByJG\Mail\MailerFactory; use ByJG\Mail\Wrapper\AmazonSesWrapper; +use Override; class AmazonSesFunctionalTest extends FunctionalBase { /** - * @throws \ByJG\Mail\Exception\InvalidMailHandlerException - * @throws \ByJG\Mail\Exception\ProtocolNotRegisteredException + * @throws InvalidMailHandlerException + * @throws ProtocolNotRegisteredException */ + #[Override] public function setUp(): void { MailerFactory::registerMailer(AmazonSesWrapper::class); diff --git a/tests/Functional/FunctionalBase.php b/tests/Functional/FunctionalBase.php index 09b30ac..0bdf8b2 100644 --- a/tests/Functional/FunctionalBase.php +++ b/tests/Functional/FunctionalBase.php @@ -3,21 +3,26 @@ namespace Tests\Functional; use ByJG\Mail\Envelope; +use ByJG\Mail\Exception\ProtocolNotRegisteredException; use ByJG\Mail\MailerFactory; +use ByJG\Mail\Wrapper\MailWrapperInterface; +use ByJG\Util\Uri; +use Override; use PHPUnit\Framework\TestCase; abstract class FunctionalBase extends TestCase { - protected $uri; - protected $from; - protected $toEmail; - protected $mailer; - protected $envelope; - protected $mailerName; + protected Uri|string|false $uri; + protected string|null $from; + protected string|null $toEmail; + protected MailWrapperInterface|null $mailer; + protected Envelope|null $envelope; + protected string $mailerName; /** - * @throws \ByJG\Mail\Exception\ProtocolNotRegisteredException + * @throws ProtocolNotRegisteredException */ + #[Override] public function setUp(): void { if (!$this->uri || !$this->from || !$this->toEmail) { @@ -40,6 +45,7 @@ public function setUp(): void ); } + #[Override] public function tearDown(): void { $this->mailer = null; @@ -48,12 +54,15 @@ public function tearDown(): void $this->envelope = null; } + /** + * @return void + */ public function testSendEmail() { if (empty($this->mailer)) { $this->markTestSkipped('Environment Variables not set'); - return; } - $this->assertTrue($this->mailer->send($this->envelope)); + $result = $this->mailer->send($this->envelope); + $this->assertTrue($result->success); } } diff --git a/tests/Functional/MailgunFunctionalTest.php b/tests/Functional/MailgunFunctionalTest.php index 824f73c..f138228 100644 --- a/tests/Functional/MailgunFunctionalTest.php +++ b/tests/Functional/MailgunFunctionalTest.php @@ -2,15 +2,19 @@ namespace Tests\Functional; +use ByJG\Mail\Exception\InvalidMailHandlerException; +use ByJG\Mail\Exception\ProtocolNotRegisteredException; use ByJG\Mail\MailerFactory; use ByJG\Mail\Wrapper\MailgunApiWrapper; +use Override; class MailgunFunctionalTest extends FunctionalBase { /** - * @throws \ByJG\Mail\Exception\InvalidMailHandlerException - * @throws \ByJG\Mail\Exception\ProtocolNotRegisteredException + * @throws InvalidMailHandlerException + * @throws ProtocolNotRegisteredException */ + #[Override] public function setUp(): void { MailerFactory::registerMailer(MailgunApiWrapper::class); diff --git a/tests/Functional/PHPMailerFunctionalTest.php b/tests/Functional/PHPMailerFunctionalTest.php index d51cca2..9875f3e 100644 --- a/tests/Functional/PHPMailerFunctionalTest.php +++ b/tests/Functional/PHPMailerFunctionalTest.php @@ -2,15 +2,19 @@ namespace Tests\Functional; +use ByJG\Mail\Exception\InvalidMailHandlerException; +use ByJG\Mail\Exception\ProtocolNotRegisteredException; use ByJG\Mail\MailerFactory; use ByJG\Mail\Wrapper\PHPMailerWrapper; +use Override; class PHPMailerFunctionalTest extends FunctionalBase { /** - * @throws \ByJG\Mail\Exception\InvalidMailHandlerException - * @throws \ByJG\Mail\Exception\ProtocolNotRegisteredException + * @throws InvalidMailHandlerException + * @throws ProtocolNotRegisteredException */ + #[Override] public function setUp(): void { MailerFactory::registerMailer(PHPMailerWrapper::class); diff --git a/tests/MailUtilTest.php b/tests/MailUtilTest.php index 9e83080..5aaadb2 100644 --- a/tests/MailUtilTest.php +++ b/tests/MailUtilTest.php @@ -1,5 +1,7 @@ assertTrue(Util::isValidEmail(self::EMAIL_OK)); - $this->assertTrue(!Util::isValidEmail(self::EMAIL_NOK_1)); - $this->assertTrue(!Util::isValidEmail(self::EMAIL_NOK_2)); - $this->assertTrue(!Util::isValidEmail(self::EMAIL_NOK_3)); - $this->assertTrue(!Util::isValidEmail(self::EMAIL_NOK_4)); - $this->assertTrue(!Util::isValidEmail(self::EMAIL_NOK_5)); + $this->assertFalse(Util::isValidEmail(self::EMAIL_NOK_1)); + $this->assertFalse(Util::isValidEmail(self::EMAIL_NOK_2)); + $this->assertFalse(Util::isValidEmail(self::EMAIL_NOK_3)); + $this->assertFalse(Util::isValidEmail(self::EMAIL_NOK_4)); + $this->assertFalse(Util::isValidEmail(self::EMAIL_NOK_5)); } - function test_GetFullEmailName() + function test_GetFullEmailName(): void { $this->assertEquals(Util::getFullEmail("joao@server.com.br", "Joao"), '"Joao" '); $this->assertEquals(Util::getFullEmail("joao@server.com.br", ""), 'joao@server.com.br'); $this->assertEquals(Util::getFullEmail("joao@server.com.br"), 'joao@server.com.br'); } - function test_GetEmailPair() + function test_GetEmailPair(): void { $pair = Util::decomposeEmail('"Name" '); $this->assertEquals($pair["name"], 'Name'); diff --git a/tests/MailerWrapperTest.php b/tests/MailerWrapperTest.php index 15c5509..15b642b 100644 --- a/tests/MailerWrapperTest.php +++ b/tests/MailerWrapperTest.php @@ -1,10 +1,11 @@ expectException(InvalidMailHandlerException::class); MailerFactory::registerMailer(MailerWrapperTest::class); } /** - * @throws \ByJG\Mail\Exception\InvalidMailHandlerException - * @throws \ByJG\Mail\Exception\ProtocolNotRegisteredException + * @throws InvalidMailHandlerException + * @throws ProtocolNotRegisteredException */ - public function testCreate() + public function testCreate(): void { MailerFactory::registerMailer(PHPMailerWrapper::class); MailerFactory::create('smtp://localhost'); @@ -50,10 +51,10 @@ public function testCreate() } /** - * @throws \ByJG\Mail\Exception\ProtocolNotRegisteredException - * @throws \ByJG\Mail\Exception\InvalidMailHandlerException + * @throws ProtocolNotRegisteredException + * @throws InvalidMailHandlerException */ - public function testCreateFail() + public function testCreateFail(): void { $this->expectException(ProtocolNotRegisteredException::class); MailerFactory::registerMailer(PHPMailerWrapper::class); diff --git a/tests/MailgunWrapperTest.php b/tests/MailgunTestWrapper.php similarity index 94% rename from tests/MailgunWrapperTest.php rename to tests/MailgunTestWrapper.php index 9054140..e4bcdc1 100644 --- a/tests/MailgunWrapperTest.php +++ b/tests/MailgunTestWrapper.php @@ -16,7 +16,7 @@ use ByJG\WebRequest\Psr7\MemoryStream; use Psr\Http\Client\ClientExceptionInterface; -class MailgunWrapperTest extends BaseWrapperTest +class MailgunTestWrapper extends BaseTestWrapper { /** @@ -40,7 +40,7 @@ public function doMockedRequest(Envelope $envelope, MockClient $mock): SendResul * @throws RequestException * @throws MessageException */ - public function testGetRequest() + public function testGetRequest(): void { $wrapper = new MailgunApiWrapper(new Uri('mailgun://YOUR_API_KEY@YOUR_DOMAIN')); $request = $wrapper->getRequestObject(); @@ -56,7 +56,7 @@ public function testGetRequest() * @throws InvalidEMailException * @throws MessageException */ - public function testBasicEnvelope() + public function testBasicEnvelope(): void { $expectedResponse = new Response(200); $expectedResponse = $expectedResponse->withBody(new MemoryStream('{"id":"12345"}')); @@ -80,7 +80,7 @@ public function testBasicEnvelope() * @throws ClientExceptionInterface * @throws MessageException */ - public function testFullEnvelope() + public function testFullEnvelope(): void { $expectedResponse = new Response(200); $expectedResponse = $expectedResponse->withBody(new MemoryStream('{"id":"12345"}')); @@ -104,7 +104,7 @@ public function testFullEnvelope() * @throws InvalidEMailException * @throws MessageException */ - public function testAttachmentEnvelope() + public function testAttachmentEnvelope(): void { $expectedResponse = new Response(200); $expectedResponse = $expectedResponse->withBody(new MemoryStream('{"id":"12345"}')); @@ -128,7 +128,7 @@ public function testAttachmentEnvelope() * @throws ClientExceptionInterface * @throws MessageException */ - public function testEmbedImageEnvelope() + public function testEmbedImageEnvelope(): void { $expectedResponse = new Response(200); $expectedResponse = $expectedResponse->withBody(new MemoryStream('{"id":"12345"}')); diff --git a/tests/MockSender.php b/tests/MockSender.php index d361ca4..704bd9e 100644 --- a/tests/MockSender.php +++ b/tests/MockSender.php @@ -6,10 +6,10 @@ class MockSender { - public $result; + public string $result; // AmazonSes - public function sendRawEmail($raw) + public function sendRawEmail(string $raw): Result { $this->result = $raw; @@ -19,7 +19,7 @@ public function sendRawEmail($raw) } // Mailgun Wrapper - public function postMultiPartForm($message) + public function postMultiPartForm($message): string { $this->result = $message; return '{"id": "123445"}'; diff --git a/tests/PHPMailerWrapperTest.php b/tests/PHPMailerTestWrapper.php similarity index 82% rename from tests/PHPMailerWrapperTest.php rename to tests/PHPMailerTestWrapper.php index d0c3e0a..f23aa6a 100644 --- a/tests/PHPMailerWrapperTest.php +++ b/tests/PHPMailerTestWrapper.php @@ -2,6 +2,7 @@ namespace Tests; +use ByJG\Mail\Envelope; use ByJG\Mail\Exception\InvalidEMailException; use ByJG\Mail\Exception\MailApiException; use ByJG\Mail\Override\PHPMailerOverride; @@ -9,16 +10,16 @@ use ByJG\Util\Uri; use PHPMailer\PHPMailer\Exception; -class PHPMailerWrapperTest extends BaseWrapperTest +class PHPMailerTestWrapper extends BaseTestWrapper { /** - * @param $envelope + * @param Envelope $envelope * @return array * @throws Exception * @throws InvalidEMailException * @throws MailApiException */ - public function doMockedRequest($envelope): array + public function doMockedRequest(Envelope $envelope): array { $mock = $this->getMockBuilder(PHPMailerOverride::class) ->onlyMethods(['send', 'getLastMessageID']) @@ -47,7 +48,7 @@ public function doMockedRequest($envelope): array return [$mock, $sendResult]; } - protected function send($envelope, $rawEmail) + protected function send(Envelope $envelope, string $rawEmail): void { [$mock, $sendResult] = $this->doMockedRequest($envelope); $expected = $this->fixVariableFields(file_get_contents(__DIR__ . '/resources/' . $rawEmail . '.eml')); @@ -58,25 +59,25 @@ protected function send($envelope, $rawEmail) $this->assertEquals('mocked-message-id', $sendResult->id); } - public function testBasicEnvelope() + public function testBasicEnvelope(): void { $envelope = $this->getBasicEnvelope(); $this->send($envelope, 'basicenvelope'); } - public function testFullEnvelope() + public function testFullEnvelope(): void { $envelope = $this->getFullEnvelope(); $this->send($envelope, 'fullenvelope'); } - public function testAttachmentEnvelope() + public function testAttachmentEnvelope(): void { $envelope = $this->getAttachmentEnvelope(); $this->send($envelope, 'attachmentenvelope'); } - public function testEmbedImageEnvelope() + public function testEmbedImageEnvelope(): void { $envelope = $this->getEmbedImageEnvelope(); $this->send($envelope, 'embedenvelope'); diff --git a/tests/UriTest.php b/tests/UriTest.php index 75b63ea..d3c1add 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -1,13 +1,13 @@ assertEquals('smtp', $object->getScheme());