├── .editorconfig ├── .env.example ├── .env.testing ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature.md │ └── help.md └── workflows │ └── ci.yml ├── CHANGELOG.md ├── CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── mailer.php ├── data └── mysql.sql ├── docs ├── advanced-usage │ ├── bt.md │ ├── pdo.md │ ├── rabbitmq.md │ ├── redis.md │ └── sqs.md └── queue │ └── methods.md ├── phpcs.xml ├── phpunit.xml.dist.bak └── src ├── Builder ├── Buildable.php ├── MailJobBuilder.php ├── MailerBuilder.php ├── MessageBuilder.php └── QueueBuilder.php ├── Enum ├── MessageBrokerEnum.php └── TransportType.php ├── Event ├── Event.php └── EventHandlerTrait.php ├── Exception ├── InvalidCallException.php ├── InvalidCallbackArgumentException.php ├── InvalidTransportTypeArgumentException.php ├── UndefinedMessageBrokerException.php └── UnknownPropertyException.php ├── Helper ├── ArrayHelper.php ├── ConfigReader.php ├── PhpViewFileHelper.php └── RecipientsHelper.php ├── Mail └── Dto │ ├── EmailAddress.php │ └── File.php ├── Mailer.php ├── Model ├── AbstractMailObject.php ├── MailJob.php └── MailMessage.php ├── Queue ├── Backend │ ├── AbstractQueueStoreConnection.php │ ├── Beanstalkd │ │ ├── BeanstalkdMailJob.php │ │ ├── BeanstalkdQueueStoreAdapter.php │ │ └── BeanstalkdQueueStoreConnection.php │ ├── MailJobInterface.php │ ├── Pdo │ │ ├── PdoMailJob.php │ │ ├── PdoQueueStoreAdapter.php │ │ └── PdoQueueStoreConnection.php │ ├── QueueStoreAdapterInterface.php │ ├── RabbitMq │ │ ├── RabbitMqJob.php │ │ ├── RabbitMqQueueConnection.php │ │ └── RabbitMqQueueStoreAdapter.php │ ├── Redis │ │ ├── RedisMailJob.php │ │ ├── RedisQueueStoreAdapter.php │ │ └── RedisQueueStoreConnection.php │ └── Sqs │ │ ├── SqsMailJob.php │ │ ├── SqsQueueStoreAdapter.php │ │ └── SqsQueueStoreConnection.php ├── Cli │ └── MailMessageWorker.php └── MailQueue.php ├── Security ├── Cypher.php └── CypherInterface.php └── Transport ├── AbstractTransportFactory.php ├── MailTransport.php ├── MailTransportFactory.php ├── SendMailTransport.php ├── SendMailTransportFactory.php ├── SmtpTransport.php ├── SmtpTransportFactory.php ├── TransportFactory.php └── TransportInterface.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MESSAGE_BROKER=redis 2 | MAIL_TRANSPORT=smtp 3 | 4 | REDIS_HOST=null 5 | REDS_PORT= 6 | REDIS_USER= 7 | REDIS_PASSWORD= 8 | 9 | SQS_KEY= 10 | SQS_SECRET= 11 | SQS_REGION= 12 | 13 | BEANSTALKD_HOST= 14 | BEANSTALKD_PORT= 15 | 16 | PDO_USER= 17 | PDO_PASSWORD= 18 | PDO_HOST=localhost 19 | PDO_PORT=3306 20 | PDO_DBNAME= 21 | 22 | RABBITMQ_HOST=localhost 23 | RABBITMQ_PORT=5672 24 | RABBITMQ_USER=guest 25 | RABBITMQ_PASSWORD=guest 26 | 27 | MAILER_SMTP_HOST= 28 | MAILER_SMTP_PORT= 29 | MAILER_SMTP_USER= 30 | MAILER_SMTP_PASSWORD= 31 | MAILER_SMTP_TLS=false 32 | 33 | SENDMAIL_DSN=sendmail://default 34 | 35 | MAILER_DSN=native://default 36 | 37 | MAIL_CHARSET=utf-8 38 | -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- 1 | MESSAGE_BROKER=redis 2 | MAIL_TRANSPORT=smtp 3 | 4 | REDIS_HOST= 5 | REDS_PORT= 6 | REDIS_USER= 7 | REDIS_PASSWORD= 8 | 9 | SQS_KEY=xxx 10 | SQS_SECRET=yyy 11 | SQS_REGION=us-west-2 12 | 13 | BEANSTALKD_HOST= 14 | BEANSTALKD_PORT= 15 | 16 | PDO_USER=root 17 | PDO_PASSWORD=root 18 | PDO_HOST=localhost 19 | PDO_PORT=3306 20 | PDO_DBNAME=mail_queue_test 21 | 22 | RABBITMQ_HOST=localhost 23 | RABBITMQ_PORT=5672 24 | RABBITMQ_USER=guest 25 | RABBITMQ_PASSWORD=guest 26 | 27 | MAILER_SMTP_HOST=127.0.0.1 28 | MAILER_SMTP_PORT=25 29 | MAILER_SMTP_USER=mailing@test.me 30 | MAILER_SMTP_PASSWORD=1234 31 | MAILER_SMTP_TLS=false 32 | 33 | SENDMAIL_DSN=sendmail://null 34 | 35 | MAIL_DSN=native://null 36 | 37 | MAIL_CHARSET=utf-8 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: sJonatas 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Code** 17 | Provide code sample and context where the bug occurred, as related configurations for your case. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. Ubuntu 22.04] 24 | - PHP Version: [e.g. 8.1.6] 25 | - Version [e.g. 1.0] 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: sJonatas 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help 3 | about: Help wanted 4 | title: "[HELP]" 5 | labels: help wanted 6 | assignees: sJonatas 7 | 8 | --- 9 | 10 | **Subject** 11 | What's the subject do you need help with? 12 | 13 | **Description** 14 | Give a clear description of what you're trying to achieve and what's the blocker. 15 | 16 | **Code** 17 | Provide meaningful peace of code and related configurations you have set on your project. 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push] 3 | 4 | jobs: 5 | test: 6 | name: PHPUnit 7 | runs-on: ubuntu-latest 8 | env: 9 | DB_DATABASE: mail_queue_test 10 | DB_USER: root 11 | DB_PASSWORD: root 12 | 13 | steps: 14 | - name: checkout repo 15 | uses: actions/checkout@v3 16 | 17 | - name: setup enviroment 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: '8.1' 21 | 22 | - name: Install dependencies 23 | run: composer install 24 | 25 | 26 | - name: Set up MySQL 27 | run: | 28 | sudo /etc/init.d/mysql start 29 | mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} 30 | 31 | - name: Run unit tests 32 | run: ./vendor/bin/phpunit --coverage-clover ./tests/_output/coverage.xml 33 | 34 | - name: Upload coverage reports to Codacy 35 | uses: codacy/codacy-coverage-reporter-action@v1 36 | with: 37 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 38 | coverage-reports: ./tests/_output/coverage.xml 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Mailer Library 2 | ## Version 1.0.0 3 | - Initial release 4 | 5 | [![2amigOS!](https://s.gravatar.com/avatar/55363394d72945ff7ed312556ec041e0?s=80)](http://www.2amigos.us) 6 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct. 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community in a direct capacity. Personal views, beliefs and values of individuals do not necessarily reflect those of the organisation or affiliated individuals and organisations. 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/2amigos/mailer-library). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ ./vendor/bin/phpunit 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Mailer library is free software. It is released under the terms of the following BSD license. 2 | 3 | Copyright (c) 2014-16, [2amigOS! Consulting Group LLC.](http://2amigos.us). All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | disclaimer. 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | - Neither the name of 2amigOS! Consulting Group, LLC. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | [![2amigOS!](https://s.gravatar.com/avatar/55363394d72945ff7ed312556ec041e0?s=80)](http://www.2amigos.us) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mailer 2 | [![tests](https://github.com/2amigos/mailer-library/actions/workflows/ci.yml/badge.svg)](https://github.com/2amigos/mailer-library/actions/workflows/ci.yml) 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d0e8d6e968944592a95a0911d8f178ff)](https://app.codacy.com/gh/2amigos/mailer-library/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/d0e8d6e968944592a95a0911d8f178ff)](https://app.codacy.com/gh/2amigos/mailer-library/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage) 5 | [![Latest Stable Version](http://poser.pugx.org/2amigos/mailer/v)](https://packagist.org/packages/2amigos/mailer) 6 | [![Total Downloads](http://poser.pugx.org/2amigos/mailer/downloads)](https://packagist.org/packages/2amigos/mailer) 7 | [![PHP Version Require](http://poser.pugx.org/2amigos/mailer/require/php)](https://packagist.org/packages/2amigos/mailer) 8 | 9 | Many times we face a requirement to implement queue mail functionality in our projects. There are queue and 10 | mailing libraries, but there seemed to be none that could actually suit our needs and moreover, we always had to sync their 11 | functionality together. 12 | 13 | The `Mailer` library was built to fill the gaps that we have faced when implementing queue and/or mailing systems. It 14 | features: 15 | 16 | - message encryption/decryption just in case a mail message contains data that should not be publicly exposed. Perfect 17 | for SAS systems. 18 | - queueing on different backends (currently supporting beanstalkd, pdo, redis, sqs and rabbitmq) so we are not forced to use a 19 | queue storage due to the narrowed capabilities of the framework and/or libraries 20 | - unified system. Basic to Middle size projects do have mailing but they do not require another type of queue system. 21 | That's the reason the queue system is not standalone and is coupled with this system. 22 | 23 | 24 | ## Installation 25 | 26 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 27 | 28 | Either run 29 | 30 | ```bash 31 | $ composer require 2amigos/mailer 32 | ``` 33 | 34 | or add 35 | 36 | ``` 37 | "2amigos/mailer": "^2.0" 38 | ``` 39 | 40 | to the `require` section of your `composer.json` file. 41 | 42 | Usage 43 | --- 44 | 45 | ## Configuration 46 | 47 | All the configuration needed to set up the message broker connections as 48 | the mailer transport should be performed on the .env file. A .env.example file 49 | is provided, you can just copy it and start over! 50 | 51 | ```bash 52 | $ cp .evn.example .env 53 | ``` 54 | 55 | The keys `MESSAGE_BROKER` and `MAIL_TRANSPORT` defines the default message broker 56 | and mail transport, and they are mandatory to be filled. By default, its 57 | set to use Redis broker with SMTP transport. 58 | 59 | You can access the related configuration values by calling: 60 | ```php 61 | $values = \Da\Mailer\Helper\ConfigReader::get(); // array 62 | ``` 63 | 64 | ## Mail Messages 65 | 66 | The `MailMessage` class is an abstraction for an email 67 | content. Beside the attachments, you can specify the email content 68 | directly by the constructor or directly accessor. 69 | 70 | ```php 71 | $message = new \Da\Mailer\Model\MailMessage([ 72 | 'from' => 'sarah.connor@gmail.com', 73 | 'to' => 'john.connor@gmail.com', 74 | 'subject' => 'What is up?', 75 | 'textBody' => 'I hope to find you well...' 76 | ]); 77 | 78 | // or 79 | $message->bodyHtml = "I hope I'm finding doing well." 80 | // body html takes priority over body text with both were set. 81 | ``` 82 | 83 | You can also use our `EmailAddress` class to define emails with related name: 84 | 85 | ```php 86 | $message->cc = [ 87 | \Da\Mailer\Mail\Dto\EmailAddress::make('Samn@email.com', 'Samantha'); 88 | \Da\Mailer\Mail\Dto\EmailAddress::make('oliver@email.com', 'Oliver'); 89 | ]; 90 | ``` 91 | 92 | And to add attachments, you can make use of the method `addAttachment(path, name)`: 93 | 94 | ```php 95 | $message->addAttachment(__DIR__ . DIRECTORY_SEPARATOR . 'file-test.pdf', 'Important File.png'); 96 | ``` 97 | 98 | Also, you can set text or html body as a resource path. 99 | 100 | ```php 101 | $message->bodyHtml = __DIR__ . DIRECTORY_SEPARATOR . 'html-template.html'; 102 | ``` 103 | 104 | ### Available public properties: 105 | | Property | Type | 106 | |:--------:|:-------------:| 107 | | from | string, array | 108 | | to | string, array | 109 | | cc | string, array | 110 | | bcc | string, array | 111 | | subject | string | 112 | | bodyText | string | 113 | | bodyHtml | string | 114 | 115 | ### enqueue MailMessage 116 | 117 | You can easily assess the message enqueue by calling the method `enqueue`. 118 | ```php 119 | $message->enqueue(); 120 | ``` 121 | The message will enqueued to the default message broker, and use the default 122 | transport. 123 | 124 | ## MailJob 125 | 126 | The MailJob class will abstract the message behavior for our queue application. 127 | You can create a new MailJob with the `MailJobBuilder` class: 128 | 129 | ```php 130 | $mailJob = \Da\Mailer\Builder\MailJobBuilder::make([ 131 | 'message' => json_encode($message) 132 | ]); 133 | ``` 134 | 135 | Behind the hoods, the builder will build the MailJob specialized to the 136 | default broker you've defined on your .env file. If you ever want a 137 | mail job to be created to a different broker than your default, you 138 | can set it as the second argument, using one value 139 | from the `\Da\Mailer\Enum\MessageBrokerEnum` enum: 140 | 141 | ```php 142 | $mailJob = \Da\Mailer\Builder\MailJobBuilder::make([ 143 | 'message' => json_encode($message) 144 | ], 145 | \Da\Mailer\Enum\MessageBrokerEnum::BROKER_SQS 146 | ); 147 | ``` 148 | 149 | The MailJob class has a set of methods to manipulate it's content 150 | and also to check its status. The next piece of code cover them all: 151 | 152 | ```php 153 | $mailJob->getMessage(); // returns the MailJob message 154 | $mailJob->markAsCompleted(); // void, mark the job as completed 155 | $mailJob->isCompleted(); // returns true if the job has been complete 156 | $mailJob->setMessage(new \Da\Mailer\Model\MailMessage()); // change the job's message 157 | ``` 158 | 159 | ## Mailer 160 | The Mailer class is the one we use for sending the emails. 161 | 162 | ```php 163 | $message = new \Da\Mailer\Model\MailMessage([ 164 | 'from' => 'sarah.connor@gmail.com', 165 | 'to' => 'john.connor@gmail.com', 166 | 'subject' => 'What is up?', 167 | 'textBody' => 'I hope to find you well...' 168 | ]); 169 | 170 | $mailer = \Da\Mailer\Builder\MailerBuilder::make(); 171 | // or if you want to set a transport different from the default 172 | $mailer = \Da\Mailer\Builder\MailerBuilder::make(\Da\Mailer\Enum\TransportType::SEND_MAIL); 173 | $mailer->send($message); // returns \Symfony\Component\Mailer\SentMessage::class|null 174 | ``` 175 | 176 | ## Queues 177 | 178 | To create a queue, you can make use of our `QueueBuilder` class. It will return 179 | a queue object with a few methods to handle the queue. They are: 180 | 181 | - [enqueue(MailJob $job)](docs/queue/methods.md#enqueue): bool 182 | - [dequeue()](docs/queue/methods.md#dequeue): mailjob 183 | - [ack(MailJob $job)](docs/queue/methods.md#ack): void 184 | - [isEmpty()](docs/queue/methods.md#isempty): bool 185 | 186 | ```php 187 | $queue = \Da\Mailer\Builder\QueueBuilder::make(); 188 | 189 | // if you want to use a different broker than the default 190 | $queue = \Da\Mailer\Builder\QueueBuilder::make(\Da\Mailer\Enum\MessageBrokerEnum::BROKER_RABBITMQ); 191 | ``` 192 | 193 | ## Advanced usage 194 | 195 | If you want to handle your message broker and smtp manually, you can follow 196 | through the following topics: 197 | 198 | - [Beanstalkd Backend](docs/advanced-usage/bt.md) 199 | - [Pdo Backend](docs/advanced-usage/pdo.md) 200 | - [RabbitMq Backend](docs/advanced-usage/rabbitmq.md) 201 | - [Redis Backend](docs/advanced-usage/redis.md) 202 | - [SQS Backend](docs/advanced-usage/sqs.md) 203 | 204 | ## Contributing 205 | 206 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 207 | 208 | ## Clean code 209 | 210 | We have added some development tools for you to contribute to the library with clean code: 211 | 212 | - PHP mess detector: Takes a given PHP source code base and look for several potential problems within that source. 213 | - PHP code sniffer: Tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards. 214 | - PHP code fixer: Analyzes some PHP source code and tries to fix coding standards issues. 215 | 216 | And you should use them in that order. 217 | 218 | ### Using php mess detector 219 | 220 | Sample with all options available: 221 | 222 | ```bash 223 | ./vendor/bin/phpmd ./src text codesize,unusedcode,naming,design,controversial,cleancode 224 | ``` 225 | 226 | ### Using code sniffer 227 | 228 | ```bash 229 | ./vendor/bin/phpcs -s --report=source --standard=PSR2 ./src 230 | ``` 231 | 232 | ### Using code fixer 233 | 234 | We have added a PHP code fixer to standardize our code. It includes Symfony, [PSR-12](https://www.php-fig.org/psr/psr-12/) and some contributors rules. 235 | 236 | ```bash 237 | ./vendor/bin/php-cs-fixer --config-file=.php_cs fix ./src 238 | ``` 239 | 240 | ## Testing 241 | 242 | ```bash 243 | $ ./vendor/bin/phpunit 244 | ``` 245 | 246 | 247 | ## Credits 248 | 249 | - [Antonio Ramirez](https://github.com/tonydspaniard) 250 | - [All Contributors](https://github.com/2amigos/mailer-library/graphs/contributors) 251 | 252 | ## License 253 | 254 | The BSD License (BSD). Please see [License File](LICENSE.md) for more information. 255 | 256 |
257 |
258 | web development has never been so fun
259 | www.2amigos.us 260 |
261 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2amigos/mailer", 3 | "description": "Mailer adapter for the Symphony Mailer library with queuing capabilities.", 4 | "config": { 5 | "discard-changes": "true" 6 | }, 7 | "authors": [ 8 | { 9 | "name": "2amigOS! Consulting Group", 10 | "email": "hola@2amigos.us", 11 | "homepage": "https://2am.tech", 12 | "role": "Developer" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=7.4", 17 | "phpseclib/phpseclib": "^3.0", 18 | "aws/aws-sdk-php": "^3.296", 19 | "predis/predis": "^2.2", 20 | "pda/pheanstalk": "^4.0", 21 | "php-amqplib/php-amqplib": "2.*", 22 | "vlucas/phpdotenv": "^5.6", 23 | "marc-mabe/php-enum": "^4.7", 24 | "symfony/mailer": "^5.4", 25 | "symfony/event-dispatcher": "^5.4" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^8.5 || ^10.5", 29 | "mockery/mockery": "^1.6.7", 30 | "squizlabs/php_codesniffer": "^3.8" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Da\\Mailer\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Da\\Mailer\\Test\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "phpunit" 44 | }, 45 | "suggest": { 46 | "aws/aws-sdk-php": "Allows the use of Amazon SQS as a mail queue system", 47 | "predis/predis": "Allows the use of Redis as a mail queue system", 48 | "pda/pheanstalk": "Allows the use of Beanstalkd as mail queue system", 49 | "php-amqplib/php-amqplib": "Allows the use of AMQP protocol required to use RabbitMq as a mail queue system" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/mailer.php: -------------------------------------------------------------------------------- 1 | load(); 9 | 10 | return [ 11 | 'config' => [ 12 | 'message_broker' => $_ENV['MESSAGE_BROKER'], 13 | 'transport' => $_ENV['MAIL_TRANSPORT'] 14 | ], 15 | 16 | 'brokers' => [ 17 | 'redis' => [ 18 | 'host' => $_ENV['REDIS_HOST'], 19 | 'port' => $_ENV['REDS_PORT'], 20 | 'user' => $_ENV['REDIS_USER'], 21 | 'password' => $_ENV['REDIS_PASSWORD'] 22 | ], 23 | 24 | 'sqs' => [ 25 | 'key' => $_ENV['SQS_KEY'], 26 | 'secret' => $_ENV['SQS_SECRET'], 27 | 'region' => $_ENV['SQS_REGION'] 28 | ], 29 | 30 | 'beanstalkd' => [ 31 | 'host' => $_ENV['BEANSTALKD_HOST'], 32 | 'port' => $_ENV['BEANSTALKD_PORT'] 33 | ], 34 | 35 | 'pdo' => [ 36 | 'username' => $_ENV['PDO_USER'], 37 | 'password' => $_ENV['PDO_PASSWORD'], 38 | 'host' => $_ENV['PDO_HOST'], 39 | 'port' => $_ENV['PDO_PORT'] ?: 3306, 40 | 'db' => $_ENV['PDO_DBNAME'] 41 | ], 42 | 43 | 'rabbitmq' => [ 44 | 'host' => $_ENV['RABBITMQ_HOST'], 45 | 'port' => $_ENV['RABBITMQ_PORT'], 46 | 'user' => $_ENV['RABBITMQ_USER'], 47 | 'password' => $_ENV['RABBITMQ_PASSWORD'] 48 | ] 49 | ], 50 | 51 | 'transports' => [ 52 | 'smtp' => [ 53 | 'host' => $_ENV['MAILER_SMTP_HOST'], 54 | 'port' => $_ENV['MAILER_SMTP_PORT'], 55 | 'options' => [ 56 | 'username' => $_ENV['MAILER_SMTP_USER'], 57 | 'password' => $_ENV['MAILER_SMTP_PASSWORD'], 58 | 'tls' => $_ENV['MAILER_SMTP_TLS'] 59 | ] 60 | ], 61 | 'sendMail' => [ 62 | 'dsn' => $_ENV['SENDMAIL_DSN'] 63 | ], 64 | 'mail' => [ 65 | 'dsn' => $_ENV['MAIL_DSN'] 66 | ] 67 | ], 68 | 'mail-charset' => $_ENV['MAIL_CHARSET'] ?: 'utf-8' 69 | ]; 70 | -------------------------------------------------------------------------------- /data/mysql.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS mail_queue; 2 | 3 | CREATE TABLE mail_queue ( 4 | id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, 5 | message MEDIUMTEXT NOT NULL, 6 | attempt TINYINT(1) UNSIGNED NOT NULL DEFAULT 0, 7 | state CHAR(1) NOT NULL DEFAULT 'N', /* 'N': queued (new), 'A': processing (active), 'C': completed */ 8 | sentTime TIMESTAMP NULL DEFAULT NULL, 9 | timeToSend TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | createdTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | updatedTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 12 | PRIMARY KEY (id), 13 | KEY id (id), 14 | KEY time_to_send (timeToSend) 15 | ); 16 | -------------------------------------------------------------------------------- /docs/advanced-usage/bt.md: -------------------------------------------------------------------------------- 1 | # Beanstalkd Backend Usage 2 | 3 | This assumes you have beanstalkd running on your computer and on port 11300. 4 | 5 | ## Add email job to the queue 6 | 7 | ```php 8 | use Da\Mailer\Model\MailMessage; 9 | use Da\Mailer\Queue\MailQueue; 10 | use Da\Mailer\Queue\Backend\Beanstalkd\BeanstalkdMailJob; 11 | use Da\Mailer\Queue\Backend\Beanstalkd\BeanstalkdQueueStoreAdapter; 12 | use Da\Mailer\Queue\Backend\Beanstalkd\BeanstalkdQueueStoreConnection; 13 | use PDO; 14 | 15 | $message = new MailMessage([ 16 | 'from' => 'sarah.connor@gmail.com', 17 | 'to' => 'john.connor@gmail.com', 18 | 'subject' => 'What is up?', 19 | 'bodyText' => 'New mailing' 20 | ]); 21 | 22 | $conn = new BeanstalkdQueueStoreConnection([ 23 | 'host' => 'localhost', 24 | 'port' => 'root']); 25 | 26 | $adapter = new BeanstalkdQueueStoreAdapter($conn); 27 | 28 | $queue = new MailQueue($adapter); 29 | 30 | $job = new BeanstalkdMailJob([ 31 | 'message' => json_encode($message), 32 | ]); 33 | 34 | if (!$queue->enqueue($job)) { 35 | // ... queue operation failed 36 | } 37 | ``` 38 | 39 | ## Fetch email job from queue 40 | 41 | ```php 42 | use Da\Mailer\Queue\MailQueue; 43 | use Da\Mailer\Queue\Backend\Beanstalkd\BeanstalkdQueueStoreAdapter; 44 | use Da\Mailer\Queue\Backend\Beanstalkd\BeanstalkdQueueStoreConnection; 45 | 46 | $conn = new BeanstalkdQueueStoreConnection([ 47 | 'connectionString' => 'mysql:host=localhost;dbname=test', 48 | 'username' => 'root', 49 | 'password' => 'password' 50 | ], [PDO::ATTR_PERSISTENT => true]); 51 | 52 | $adapter = new BeanstalkdQueueStoreAdapter($conn); 53 | 54 | $queue = new MailQueue($adapter); 55 | 56 | if (($job = $queue->dequeue()) !== null) { 57 | // ... do something with received job 58 | // ... send it using `mail()` function for example 59 | // ... or by using MailMessageWorker 60 | 61 | $job->markAsCompleted(); 62 | $queue->ack($job); 63 | } 64 | ``` 65 | 66 | ## Send email job with `MailMessageWorker` 67 | 68 | This could be an example using `MailMessageWorker` and a `MailJob` with specific transport configurations in it. Very 69 | useful if the mail jobs in the queue do belong to your customers and they can specify their SMTP configuration details 70 | for sending emails. 71 | 72 | 73 | ```php 74 | use Da\Mailer\Mailer; 75 | use Da\Mailer\Model\MailMessage; 76 | use Da\Mailer\Transport\TransportFactory 77 | use Da\Mailer\Queue\Cli\MailMessageWorker; 78 | 79 | $mailJob = /* ... get mail job here (see above) ... */; 80 | $mailMessage = json_decode($mailJob->getMessage()); /* ... if you have json encoded ... */ 81 | $transport = TransportFactory::create($mailMessage->transportOptions, $mailMessage->transportType); 82 | $mailer = new Mailer($transport); 83 | 84 | $worker = new MailMessageWorker($mailer, $mailMessage); 85 | 86 | // you could set the event handlers for `onFailure` or `onSuccess` here to do a different action according to the 87 | // results of the work 88 | $worker->attach('onSuccess', $callableHere); 89 | $worker->attach('onFailure', $anotherCallable); 90 | 91 | $worker->run(); 92 | 93 | // this could be added in the 'onSuccess' event handler 94 | $mailJob->markAsCompleted(); 95 | $queue->ack($job); 96 | ``` 97 | -------------------------------------------------------------------------------- /docs/advanced-usage/pdo.md: -------------------------------------------------------------------------------- 1 | # Pdo Backend Usage 2 | 3 | ## Add the storage table to your database 4 | 5 | This backend requires that you create a table like the one specified in the `data` folder. It defaults to `mail_queue` 6 | name but you can name it the way you want it as long as its structure remains the same and then you specify that 7 | custom name on the adapter. 8 | 9 | Currently we have a `mysql.sql` only, feel free to add your PDO compatible database to it. 10 | 11 | ## Add email job to the queue 12 | 13 | ```php 14 | use Da\Mailer\Model\MailMessage; 15 | use Da\Mailer\Queue\MailQueue; 16 | use Da\Mailer\Queue\Backend\Pdo\PdoMailJob; 17 | use Da\Mailer\Queue\Backend\Pdo\PdoQueueStoreAdapter; 18 | use Da\Mailer\Queue\Backend\Pdo\PdoQueueStoreConnection; 19 | use PDO; 20 | 21 | $message = new MailMessage([ 22 | 'from' => 'sarah.connor@gmail.com', 23 | 'to' => 'john.connor@gmail.com', 24 | 'subject' => 'What is up?', 25 | 'bodyText' => 'New mailing' 26 | ]); 27 | 28 | $conn = new PdoQueueStoreConnection([ 29 | 'connectionString' => 'mysql:host=localhost;dbname=test', 30 | 'username' => 'root', 31 | 'password' => 'password' 32 | ], [PDO::ATTR_PERSISTENT => true]); 33 | 34 | $adapter = new PdoQueueStoreAdapter($conn); 35 | 36 | $queue = new MailQueue($adapter); 37 | 38 | $job = new PdoMailJob([ 39 | 'message' => json_encode($message), 40 | ]); 41 | 42 | if (!$queue->enqueue($job)) { 43 | // ... queue operation failed 44 | } 45 | ``` 46 | 47 | ## Fetch email job from queue 48 | 49 | ```php 50 | use Da\Mailer\Queue\MailQueue; 51 | use Da\Mailer\Queue\Backend\Pdo\PdoQueueStoreAdapter; 52 | use Da\Mailer\Queue\Backend\Pdo\PdoQueueStoreConnection; 53 | 54 | $conn = new PdoQueueStoreConnection([ 55 | 'connectionString' => 'mysql:host=localhost;dbname=test', 56 | 'username' => 'root', 57 | 'password' => 'password' 58 | ], [PDO::ATTR_PERSISTENT => true]); 59 | 60 | $adapter = new PdoQueueStoreAdapter($conn); 61 | 62 | $queue = new MailQueue($adapter); 63 | 64 | if (($job = $queue->dequeue()) !== null) { 65 | // ... do something with received job 66 | // ... send it using `mail()` function for example 67 | // ... or by using MailMessageWorker 68 | 69 | $job->markAsCompleted(); 70 | $queue->ack($job); 71 | } 72 | ``` 73 | 74 | ## Send email job with `MailMessageWorker` 75 | 76 | This could be an example using `MailMessageWorker` and a `MailJob` with specific transport configurations in it. Very 77 | useful if the mail jobs in the queue do belong to your customers and they can specify their SMTP configuration details 78 | for sending emails. 79 | 80 | 81 | ```php 82 | use Da\Mailer\Mailer; 83 | use Da\Mailer\Model\MailMessage; 84 | use Da\Mailer\Transport\TransportFactory 85 | use Da\Mailer\Queue\Cli\MailMessageWorker; 86 | 87 | $mailJob = /* ... get mail job here (see above) ... */; 88 | $mailMessage = json_decode($mailJob->getMessage()); /* ... if you have json encoded ... */ 89 | $transport = TransportFactory::create($mailMessage->transportOptions, $mailMessage->transportType); 90 | $mailer = new Mailer($transport); 91 | 92 | $worker = new MailMessageWorker($mailer, $mailMessage); 93 | 94 | // you could set the event handlers for `onFailure` or `onSuccess` here to do a different action according to the 95 | // results of the work 96 | $worker->attach('onSuccess', $callableHere); 97 | $worker->attach('onFailure', $anotherCallable); 98 | 99 | $worker->run(); 100 | 101 | // this could be added in the 'onSuccess' event handler 102 | $mailJob->markAsCompleted(); 103 | $queue->ack($job); 104 | ``` 105 | -------------------------------------------------------------------------------- /docs/advanced-usage/rabbitmq.md: -------------------------------------------------------------------------------- 1 | # RabbitMq Backend Usage 2 | 3 | ## Add an Email to the Queue 4 | 5 | ```php 6 | $message = new MailMessage([ 7 | 'from' => 'sarah.connor@gmail.com', 8 | 'to' => 'john.connor@gmail.com', 9 | 'subject' => 'What is up?', 10 | 'bodyText' => 'New mailing' 11 | ]); 12 | 13 | $connection = new \Da\Mailer\Queue\Backend\RabbitMq\RabbitMqQueueConnection([ 14 | 'host' => 'localhost', 15 | 'port' => 5672, 16 | 'user' => 'guest', 17 | 'password' => 'guest' 18 | ]); 19 | 20 | $adapter = new \Da\Mailer\Queue\Backend\RabbitMq\RabbitMqQueueStoreAdapter($connection); 21 | $queue = new \Da\Mailer\Queue\MailQueue($adapter); 22 | 23 | $job = new \Da\Mailer\Queue\Backend\RabbitMq\RabbitMqJob([ 24 | 'message' => json_encode($message); 25 | ]); 26 | 27 | if (! $queue->enqueue($job)) { 28 | //something wrong happened 29 | } 30 | ``` 31 | 32 | ## Fetch email from the queue 33 | 34 | ```php 35 | $connection = new \Da\Mailer\Queue\Backend\RabbitMq\RabbitMqQueueConnection([ 36 | 'host' => 'localhost', 37 | 'port' => 5672, 38 | 'user' => 'guest', 39 | 'password' => 'guest' 40 | ]); 41 | 42 | $adapter = new \Da\Mailer\Queue\Backend\RabbitMq\RabbitMqQueueStoreAdapter($connection); 43 | $queue = new \Da\Mailer\Queue\MailQueue($adapter); 44 | 45 | $job = $queue->dequeue(); 46 | if ($job !== null) { 47 | // perform some action with the job e.g send email 48 | $job->markAsCompleted(); 49 | $queue->ack($job); 50 | } 51 | ``` 52 | 53 | ## Send email with the mail() function 54 | 55 | ```php 56 | $transport = new \Da\Mailer\Transport\SmtpTransport($host, $user, $options); 57 | $mailer = new \Da\Mailer\Model\MailMessage($transport); 58 | 59 | $mailJob = /* ... get mail job here ... */; 60 | 61 | $status = null; 62 | 63 | try { 64 | $status = $mailer->send( 65 | new MailMessage(json_decode($mailJob->getMessage(), true)) 66 | ); 67 | } catch(Exception $e) { 68 | // log exception; 69 | } 70 | 71 | if (is_null($status)) { 72 | // ... cannot send email 73 | /* ... ack here with job not completed - will be set for later processing ... */ 74 | } else { 75 | $mailJob->markAsCompleted(); 76 | /* ... ack here with job completed - will be ack on the queue backend storage ... */ 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/advanced-usage/redis.md: -------------------------------------------------------------------------------- 1 | # Redis Backend Usage 2 | 3 | ## Add email job to queue 4 | 5 | ```php 6 | use Da\Mailer\Model\MailMessage; 7 | use Da\Mailer\Queue\MailQueue; 8 | use Da\Mailer\Queue\Backend\Redis\RedisMailJob; 9 | use Da\Mailer\Queue\Backend\Redis\RedisQueueStoreAdapter; 10 | use Da\Mailer\Queue\Backend\Redis\RedisQueueStoreConnection; 11 | 12 | $message = new MailMessage([ 13 | 'from' => 'sarah.connor@gmail.com', 14 | 'to' => 'john.connor@gmail.com', 15 | 'subject' => 'What is up?', 16 | 'bodyText' => 'New mailing' 17 | ]); 18 | 19 | $conn = new RedisQueueStoreConnection([ 20 | 'host' => 'localhost', 21 | 'port' => 9367, 22 | ]); 23 | 24 | $adapter = new RedisQueueStoreAdapter($conn); 25 | 26 | $queue = new MailQueue($adapter); 27 | 28 | $job = new RedisMailJob([ 29 | 'message' => json_encode($message), 30 | ]); 31 | 32 | if (!$queue->enqueue($job)) { 33 | // ... queue operation failed 34 | } 35 | ``` 36 | 37 | ## Fetch email job from queue 38 | 39 | ```php 40 | use Da\Mailer\Queue\MailQueue; 41 | use Da\Mailer\Queue\Backend\Redis\RedisQueueStoreAdapter; 42 | use Da\Mailer\Queue\Backend\Redis\RedisQueueStoreConnection; 43 | 44 | $conn = new RedisQueueStoreConnection([ 45 | 'host' => 'localhost', 46 | 'port' => 9367, 47 | ]); 48 | 49 | $adapter = new RedisQueueStoreAdapter($conn); 50 | 51 | $queue = new MailQueue($adapter); 52 | 53 | if (($job = $queue->dequeue()) !== null) { 54 | // ... do something with received job 55 | // ... send it using `mail()` function for example 56 | 57 | $job->markAsCompleted(); 58 | $queue->ack($job); 59 | } 60 | ``` 61 | 62 | ## Send email job with `mail()` function 63 | 64 | ```php 65 | use Da\Mailer\Mailer; 66 | use Da\Mailer\Model\MailMessage; 67 | use Da\Mailer\Transport\MailTransport; 68 | 69 | $transport = new MailTransport(); 70 | 71 | $mailer = new Mailer($transport); 72 | 73 | $mailJob = /* ... get mail job here ... */; 74 | 75 | $status = null; 76 | 77 | try { 78 | $status = $mailer->send( 79 | new MailMessage(json_decode($mailJob->getMessage(), true)) 80 | ); 81 | } catch(Exception $e) { 82 | // log exception; 83 | } 84 | 85 | if (is_null($status)) { 86 | // ... cannot send email 87 | /* ... ack here with job not completed - will be set for later processing ... */ 88 | } else { 89 | $mailJob->markAsCompleted(); 90 | /* ... ack here with job completed - will be ack on the queue backend storage ... */ 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/advanced-usage/sqs.md: -------------------------------------------------------------------------------- 1 | # SQS Backend Usage 2 | 3 | ## Add email job to queue 4 | 5 | ```php 6 | use Da\Mailer\Model\MailMessage; 7 | use Da\Mailer\Queue\MailQueue; 8 | use Da\Mailer\Queue\Backend\Sqs\SqsMailJob; 9 | use Da\Mailer\Queue\Backend\Sqs\SqsQueueStoreAdapter; 10 | use Da\Mailer\Queue\Backend\Sqs\SqsQueueStoreConnection; 11 | 12 | $message = new MailMessage([ 13 | 'from' => 'sarah.connor@gmail.com', 14 | 'to' => 'john.connor@gmail.com', 15 | 'subject' => 'What is up?', 16 | 'bodyText' => 'New mailing' 17 | ]); 18 | 19 | $conn = new SqsQueueStoreConnection([ 20 | 'key' => 'AKIA...', 21 | 'secret' => '...', 22 | 'region' => 'eu-west-1', // https://docs.aws.amazon.com/general/latest/gr/rande.html 23 | ]); 24 | 25 | $adapter = new SqsQueueStoreAdapter($conn); 26 | 27 | $queue = new MailQueue($adapter); 28 | 29 | $job = new SqsMailJob([ 30 | 'message' => json_encode($message), 31 | ]); 32 | 33 | if (!$queue->enqueue($job)) { 34 | // ... queue operation failed 35 | } 36 | ``` 37 | 38 | ## Fetch email job from queue 39 | 40 | ```php 41 | use Da\Mailer\Queue\MailQueue; 42 | use Da\Mailer\Queue\Backend\Sqs\SqsQueueStoreAdapter; 43 | use Da\Mailer\Queue\Backend\Sqs\SqsQueueStoreConnection; 44 | 45 | $conn = new SqsQueueStoreConnection([ 46 | 'key' => 'AKIA...', 47 | 'secret' => '...', 48 | 'region' => 'eu-west-1', // https://docs.aws.amazon.com/general/latest/gr/rande.html 49 | ]); 50 | 51 | $adapter = new SqsQueueStoreAdapter($conn); 52 | 53 | $queue = new MailQueue($adapter); 54 | 55 | if (($job = $queue->dequeue()) !== null) { 56 | // ... do something with received job 57 | // ... send it using `mail()` function for example 58 | 59 | $job->setDeleted(true); 60 | $queue->ack($job); 61 | } 62 | ``` 63 | 64 | ## Send email job with `mail()` function 65 | 66 | ```php 67 | use Da\Mailer\Mailer; 68 | use Da\Mailer\Model\MailMessage; 69 | use Da\Mailer\Transport\MailTransport; 70 | 71 | $transport = new MailTransport(); 72 | 73 | $mailer = new Mailer($transport); 74 | 75 | $mailJob = /* ... get mail job here ... */; 76 | 77 | $status = null; 78 | 79 | try { 80 | $result = $mailer->send( 81 | new MailMessage(json_decode($mailJob->getMessage(), true)) 82 | ); 83 | } catch (Exception $e) { 84 | // log exception 85 | } 86 | 87 | if (is_null($status)) { 88 | // ... cannot send email 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/queue/methods.md: -------------------------------------------------------------------------------- 1 | ## Methods 2 | 3 | enqueue 4 | --- 5 | The enqueue method takes a object from `\Da\Mailer\Model\MailJob` class as 6 | parameter. It pushes the object to the end of the queue. 7 | 8 | ```php 9 | $message = new \Da\Mailer\Model\MailMessage([ 10 | 'from' => 'sarah.connor@gmail.com', 11 | 'to' => 'john.connor@gmail.com', 12 | 'subject' => 'What is up?', 13 | 'textBody' => 'I hope to find you well...' 14 | ]); 15 | 16 | $queue = \Da\Mailer\Queue\MailQueue::make(); 17 | $mailJob = \Da\Mailer\Builder\MailJobBuilder::make([ 18 | 'message' => $message 19 | ]); 20 | 21 | if (! $queue->enqueue($mailJob)) { 22 | // something wrong happened 23 | } 24 | ``` 25 | 26 | dequeue 27 | --- 28 | The dequeue method fetches the very next job on the queue. 29 | 30 | ```php 31 | $mailJob = \Da\Mailer\Queue\MailQueue::make()->dequeue(); 32 | var_dump($mailJob); 33 | 34 | // output: 35 | // class Da\Mailer\Queue\Backend\RabbitMq\RabbitMqJob#32 (5) { 36 | // private $deliveryTag => 37 | // ... 38 | ``` 39 | 40 | ack 41 | --- 42 | The ack method takes an object from the `\Da\Mailer\Model\MailJob` class. It is responsible to inform the broker about a message status. 43 | If the message is full processed and completed (literally have to MailJob with the property `isCompleted` as true. You can check how to assess it [here](../../README.md#mailjob)), the broker will remove it from new rounds, 44 | otherwise, it'll requeue it. 45 | 46 | ```php 47 | $queue->ack($mailJob); 48 | ``` 49 | 50 | isEmpty() 51 | --- 52 | Return true if there is no messages in the queue, otherwise, return false. 53 | 54 | ```php 55 | $queue->isEmpty(); 56 | // false 57 | ``` 58 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PHP_CodeSniffer configuration 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /phpunit.xml.dist.bak: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Builder/Buildable.php: -------------------------------------------------------------------------------- 1 | create(); 23 | 24 | return new Mailer($transport); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Builder/MessageBuilder.php: -------------------------------------------------------------------------------- 1 | subject($mailMessage->subject); 22 | 23 | self::setFrom($mailMessage, $message); 24 | self::setTo($mailMessage, $message); 25 | self::setCc($mailMessage, $message); 26 | self::setBcc($mailMessage, $message); 27 | self::setHtml($mailMessage, $message); 28 | self::setText($mailMessage, $message); 29 | self::setAttachments($mailMessage, $message); 30 | 31 | return $message; 32 | } 33 | 34 | /** 35 | * @param string|array|EmailAddress $emails 36 | * @param string $method 37 | * @param Email $message 38 | * @return void 39 | */ 40 | protected static function setEmail($emails, string $method, Email $message) 41 | { 42 | if (is_string($emails)) { 43 | $message->{$method}($emails); 44 | 45 | return; 46 | } 47 | 48 | if (is_array($emails)) { 49 | foreach ($emails as $email) { 50 | if ($email instanceof EmailAddress) { 51 | $email = $email->parseToMailer(); 52 | } 53 | 54 | $message->{'add' . strtoupper($method)}($email); 55 | } 56 | 57 | return; 58 | } 59 | 60 | $message->{$method}($emails->parseToMailer()); 61 | } 62 | 63 | /** 64 | * @param MailMessage $mailMessage 65 | * @param Email $message 66 | * @return void 67 | */ 68 | public static function setFrom(MailMessage $mailMessage, Email $message): void 69 | { 70 | self::setEmail($mailMessage->from, 'from', $message); 71 | } 72 | 73 | /** 74 | * @param MailMessage $mailMessage 75 | * @param Email $message 76 | * @return void 77 | */ 78 | public static function setTo(MailMessage $mailMessage, Email $message): void 79 | { 80 | self::setEmail($mailMessage->to, 'to', $message); 81 | } 82 | 83 | /** 84 | * @param MailMessage $mailMessage 85 | * @param Email $message 86 | * @return void 87 | */ 88 | protected static function setCc(MailMessage $mailMessage, Email $message) 89 | { 90 | if (! is_null($mailMessage->cc)) { 91 | self::setEmail($mailMessage->cc, 'cc', $message); 92 | } 93 | } 94 | 95 | /** 96 | * @param MailMessage $mailMessage 97 | * @param Email $message 98 | * @return void 99 | */ 100 | protected static function setBcc(MailMessage $mailMessage, Email $message) 101 | { 102 | if (! is_null($mailMessage->bcc)) { 103 | self::setEmail($mailMessage->bcc, 'bcc', $message); 104 | } 105 | } 106 | 107 | /** 108 | * @param MailMessage $mailMessage 109 | * @param Email $message 110 | * @return void 111 | * @throws \Exception 112 | */ 113 | protected static function setHtml(MailMessage $mailMessage, Email $message) 114 | { 115 | $config = self::getConfig(); 116 | $html = $mailMessage->bodyHtml; 117 | 118 | if (isset($html)) { 119 | $html = self::extractBodyMessage($html); 120 | 121 | $message->html($html, $config['mail-charset']); 122 | } 123 | } 124 | 125 | /** 126 | * @param MailMessage $mailMessage 127 | * @param Email $message 128 | * @return void 129 | * @throws \Exception 130 | */ 131 | protected static function setText(MailMessage $mailMessage, Email $message) 132 | { 133 | $config = self::getConfig(); 134 | $text = $mailMessage->bodyText; 135 | 136 | if (isset($mailMessage->bodyText)) { 137 | $text = self::extractBodyMessage($text); 138 | 139 | $message->text($text, $config['mail-charset']); 140 | } 141 | } 142 | 143 | /** 144 | * @param MailMessage $mailMessage 145 | * @param Email $message 146 | * @return void 147 | */ 148 | protected static function setAttachments(MailMessage $mailMessage, Email $message) 149 | { 150 | /** @var File $attachment */ 151 | foreach ($mailMessage->getAttachments() as $attachment) { 152 | $message->attachFromPath($attachment->getPath(), $attachment->getName()); 153 | } 154 | } 155 | 156 | /** 157 | * @param string $message 158 | * @return false|resource|string 159 | */ 160 | protected static function extractBodyMessage(string $message) 161 | { 162 | return realpath($message) 163 | ? fopen($message, 'r') 164 | : $message; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Builder/QueueBuilder.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 32 | } 33 | 34 | /** 35 | * @return mixed 36 | */ 37 | public function __invoke() 38 | { 39 | $this->data = func_get_args(); 40 | return call_user_func($this->handler, $this); 41 | } 42 | 43 | /** 44 | * @return array the collected arguments passed to the event 45 | */ 46 | public function getData() 47 | { 48 | return $this->data; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Event/EventHandlerTrait.php: -------------------------------------------------------------------------------- 1 | events[$name])) { 20 | $this->events[$name] = []; 21 | } 22 | 23 | $this->events[$name][] = $event; 24 | } 25 | 26 | /** 27 | * Removes the handlers of stack by its name. 28 | * 29 | * @param string $name 30 | */ 31 | public function detach($name) 32 | { 33 | if (array_key_exists($name, $this->events)) { 34 | unset($this->events[$name]); 35 | } 36 | } 37 | 38 | /** 39 | * Fires the handlers of a stack by its name. 40 | * 41 | * @param string $name the name of the stack to fire 42 | * @param array $data 43 | */ 44 | public function trigger($name, array $data = []) 45 | { 46 | if (isset($this->events[$name])) { 47 | foreach ($this->events[$name] as $event) { 48 | $event->owner = $this; 49 | call_user_func_array($event, $data); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Exception/InvalidCallException.php: -------------------------------------------------------------------------------- 1 | x->y->z` (if `$array` is an object). If `$array['x']` 16 | * or `$array->x` is neither an array nor an object, the default value will be returned. 17 | * Note that if the array already has an element `x.y.z`, then its value will be returned 18 | * instead of going through the sub-arrays. So it is better to be done specifying an array of key names 19 | * like `['x', 'y', 'z']`. 20 | * 21 | * Below are some usage examples, 22 | * 23 | * ~~~ 24 | * // working with array 25 | * $username = ArrayHelper::getValue($_POST, 'username'); 26 | * // working with object 27 | * $username = ArrayHelper::getValue($user, 'username'); 28 | * // working with anonymous function 29 | * $fullName = ArrayHelper::getValue($user, function ($user, $defaultValue) { 30 | * return $user->firstName . ' ' . $user->lastName; 31 | * }); 32 | * // using dot format to retrieve the property of embedded object 33 | * $street = ArrayHelper::getValue($users, 'address.street'); 34 | * // using an array of keys to retrieve the value 35 | * $value = ArrayHelper::getValue($versions, ['1.0', 'date']); 36 | * ~~~ 37 | * 38 | * @param array|object $array array or object to extract value from 39 | * @param string|\Closure|array $key key name of the array element, an array of keys or property name of the object, 40 | * or an anonymous function returning the value. The anonymous function signature should be: 41 | * `function($array, $defaultValue)`. 42 | * @param mixed $default the default value to be returned if the specified array key does not exist. Not used when 43 | * getting value from an object. 44 | * 45 | * @return mixed the value of the element if found, default value otherwise 46 | */ 47 | public static function getValue($array, $key, $default = null) 48 | { 49 | if ($key instanceof Closure) { 50 | return $key($array, $default); 51 | } 52 | if (is_array($key)) { 53 | $lastKey = array_pop($key); 54 | foreach ($key as $keyPart) { 55 | $array = static::getValue($array, $keyPart); 56 | } 57 | $key = $lastKey; 58 | } 59 | if (is_array($array) && array_key_exists($key, $array)) { 60 | return $array[$key]; 61 | } 62 | if (($pos = strrpos($key, '.')) !== false) { 63 | $array = static::getValue($array, substr($key, 0, $pos), $default); 64 | $key = substr($key, $pos + 1); 65 | } 66 | if (is_object($array)) { 67 | return $array->$key; 68 | } elseif (is_array($array)) { 69 | return array_key_exists($key, $array) ? $array[$key] : $default; 70 | } else { 71 | return $default; 72 | } 73 | } 74 | 75 | /** 76 | * Removes an item from an array and returns the value. If the key does not exist in the array, the default value 77 | * will be returned instead. 78 | * 79 | * Usage examples, 80 | * 81 | * ~~~ 82 | * // $array = ['type' => 'A', 'options' => [1, 2]]; 83 | * // working with array 84 | * $type = ArrayHelper::remove($array, 'type'); 85 | * // $array content 86 | * // $array = ['options' => [1, 2]]; 87 | * ~~~ 88 | * 89 | * @param array $array the array to extract value from 90 | * @param string $key key name of the array element 91 | * @param mixed $default the default value to be returned if the specified key does not exist 92 | * 93 | * @return mixed|null the value of the element if found, default value otherwise 94 | */ 95 | public static function remove(&$array, $key, $default = null) 96 | { 97 | if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) { 98 | $value = $array[$key]; 99 | unset($array[$key]); 100 | return $value; 101 | } 102 | 103 | return $default; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Helper/ConfigReader.php: -------------------------------------------------------------------------------- 1 | email = $email; 25 | $this->name = $name; 26 | } 27 | 28 | /** 29 | * @param string $email 30 | * @param string|null $name 31 | * @return static 32 | */ 33 | public static function make(string $email, ?string $name = null): self 34 | { 35 | return new self($email, $name); 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getEmail(): string 42 | { 43 | return $this->email; 44 | } 45 | 46 | /** 47 | * @return string|null 48 | */ 49 | public function getName(): ?string 50 | { 51 | return $this->name; 52 | } 53 | 54 | /** 55 | * @return Address 56 | */ 57 | public function parseToMailer(): Address 58 | { 59 | return new Address($this->getEmail(), $this->getName()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Mail/Dto/File.php: -------------------------------------------------------------------------------- 1 | path = $path; 23 | $this->name = $name; 24 | } 25 | 26 | /** 27 | * @param string $path 28 | * @param string|null $name 29 | * @return File 30 | */ 31 | public static function make(string $path, ?string $name = ''): self 32 | { 33 | return new self($path, $name); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getPath(): string 40 | { 41 | return $this->path; 42 | } 43 | 44 | /** 45 | * @return string|null 46 | */ 47 | public function getName(): ?string 48 | { 49 | return $this->name; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Mailer.php: -------------------------------------------------------------------------------- 1 | transport = $transport; 31 | $this->logging = $logging; 32 | } 33 | 34 | /** 35 | * Returns the mail transport used. 36 | * 37 | * @return null|TransportInterface 38 | */ 39 | public function getTransport(): TransportInterface 40 | { 41 | return $this->transport; 42 | } 43 | 44 | /** 45 | * Returns the Symfony Mailer Transport instance. 46 | * 47 | * @return null|\Symfony\Component\Mailer\Transport\TransportInterface 48 | */ 49 | public function getTransportInstance() 50 | { 51 | return $this->getTransport()->getInstance(); 52 | } 53 | 54 | /** 55 | * @return null|string logged messages as string 56 | */ 57 | public function getLog() 58 | { 59 | // TODO Add log mechanism 60 | return null; 61 | } 62 | 63 | /** 64 | * Modifies the transport used. 65 | * 66 | * @param \Symfony\Component\Mailer\Transport\TransportInterface $transport 67 | */ 68 | public function setTransport(TransportInterface $transport) 69 | { 70 | $this->transport = $transport; 71 | } 72 | 73 | /** 74 | * Sends a MailMessage instance. 75 | * 76 | * View files can be added to the `$views` array argument and if set, they will be parsed via the `PhpViewFileHelper` 77 | * helper that this library contains. 78 | * 79 | * The `$view` argument has the following syntax: 80 | * 81 | * ``` 82 | * $view = [ 83 | * 'text' => '/path/to/plain/text/email.php', 84 | * 'html' => '/path/to/html/email.php' 85 | * ]; 86 | * ``` 87 | * The `PhpViewFileHelper` will use the `$data` array argument to parse the templates. 88 | * 89 | * The template files must be of `php` type if you wish to use internal system. Otherwise, is highly recommended to 90 | * use your own template parser and set the `bodyHtml` and `bodyText` of the `MailMessage` class. 91 | * 92 | * @param MailMessage $message the MailMessage instance to send 93 | * @param array $views the view files for `text` and `html` templates 94 | * @param array $data the data to be used for parsing the templates 95 | * 96 | * @return SentMessage|null 97 | * 98 | * @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface 99 | * @see MailMessage::$bodyHtml 100 | * @see MailMessage::$bodyText 101 | * @see PhpViewFileHelper::render() 102 | */ 103 | public function send(MailMessage $message, array $views = [], array $data = []): ?SentMessage 104 | { 105 | $message = MessageBuilder::make($message); 106 | return $this->getTransportInstance()->send($message); 107 | } 108 | 109 | /** 110 | * Factory method to create an instance of the mailer based on the configuration of a `MailMessage` instance. 111 | * 112 | * @param MailMessage $mailMessage the instance to create the Mailer from 113 | * 114 | * @return Mailer instance 115 | */ 116 | public static function fromMailMessage(MailMessage $mailMessage) 117 | { 118 | $options = [ 119 | 'host' => $mailMessage->host, 120 | 'port' => $mailMessage->port, 121 | 'options' => $mailMessage->transportOptions, 122 | ]; 123 | $factory = TransportFactory::create($options, $mailMessage->transportType); 124 | return new Mailer($factory->create()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Model/AbstractMailObject.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | if (is_string($name) && $value !== null) { 22 | $this->{$name} = $value; 23 | } 24 | } 25 | } 26 | 27 | /** 28 | * Helper function to work as a builder for collection items iterations. 29 | * 30 | * @param $array 31 | * 32 | * @return static 33 | */ 34 | public static function fromArray(array $array) 35 | { 36 | return new static($array); 37 | } 38 | 39 | /** 40 | * Sets value of an object property. 41 | * 42 | * Do not call this method directly as it is a PHP magic method that 43 | * will be implicitly called when executing `$object->property = $value;`. 44 | * 45 | * @param string $name the property name or the event name 46 | * @param mixed $value the property value 47 | * 48 | * @throws Exception if the property is not defined or read-only 49 | */ 50 | public function __set($name, $value) 51 | { 52 | $setter = 'set' . $name; 53 | if (method_exists($this, $setter)) { 54 | $this->$setter($value); 55 | } elseif (method_exists($this, 'get' . $name)) { 56 | throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); 57 | } else { 58 | throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); 59 | } 60 | } 61 | 62 | /** 63 | * Returns the value of an object property. 64 | * 65 | * @param string $name 66 | * 67 | * @throws Exception if the property is not defined or write-only 68 | * 69 | * @return mixed the property value 70 | * 71 | */ 72 | public function __get($name) 73 | { 74 | $getter = 'get' . $name; 75 | if (method_exists($this, $getter)) { 76 | return $this->$getter(); 77 | } elseif (method_exists($this, 'set' . $name)) { 78 | throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); 79 | } else { 80 | throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); 81 | } 82 | } 83 | 84 | /** 85 | * Checks if a property is set, i.e. defined and not null. 86 | * 87 | * Do not call this method directly as it is a PHP magic method that 88 | * will be implicitly called when executing `isset($object->property)`. 89 | * 90 | * Note that if the property is not defined, false will be returned. 91 | * 92 | * @param string $name the property name or the event name 93 | * 94 | * @return bool whether the named property is set (not null). 95 | * 96 | * @see http://php.net/manual/en/function.isset.php 97 | */ 98 | public function __isset($name) 99 | { 100 | $getter = 'get' . $name; 101 | if (method_exists($this, $getter)) { 102 | return $this->$getter() !== null; 103 | } else { 104 | return false; 105 | } 106 | } 107 | 108 | /** 109 | * Sets an object property to null. 110 | * 111 | * Do not call this method directly as it is a PHP magic method that 112 | * will be implicitly called when executing `unset($object->property)`. 113 | * 114 | * Note that if the property is not defined, this method will do nothing. 115 | * If the property is read-only, it will throw an exception. 116 | * 117 | * @param string $name the property name 118 | * 119 | * @throws InvalidCallException if the property is read only. 120 | * 121 | * @see http://php.net/manual/en/function.unset.php 122 | */ 123 | public function __unset($name) 124 | { 125 | $setter = 'set' . $name; 126 | if (method_exists($this, $setter)) { 127 | $this->$setter(null); 128 | } elseif (method_exists($this, 'get' . $name)) { 129 | throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Model/MailJob.php: -------------------------------------------------------------------------------- 1 | getId() === null; 41 | } 42 | 43 | /** 44 | * @return mixed 45 | */ 46 | public function getId() 47 | { 48 | return $this->id; 49 | } 50 | 51 | /** 52 | * @param mixed $id 53 | */ 54 | public function setId($id) 55 | { 56 | $this->id = $id; 57 | } 58 | 59 | /** 60 | * @return MailMessage|string 61 | */ 62 | public function getMessage() 63 | { 64 | return $this->message; 65 | } 66 | 67 | /** 68 | * @param MailMessage|string $message 69 | */ 70 | public function setMessage($message) 71 | { 72 | $this->message = $message; 73 | } 74 | 75 | /** 76 | * @return int 77 | */ 78 | public function getAttempt() 79 | { 80 | return $this->attempt; 81 | } 82 | 83 | /** 84 | * @param $attempt 85 | */ 86 | public function setAttempt($attempt) 87 | { 88 | $this->attempt = $attempt; 89 | } 90 | 91 | /** 92 | * Increments attempt by one. 93 | */ 94 | public function incrementAttempt() 95 | { 96 | $this->attempt += 1; 97 | } 98 | 99 | /** 100 | * @return bool 101 | */ 102 | public function markAsCompleted() 103 | { 104 | return $this->completed = true; 105 | } 106 | 107 | /** 108 | * @return bool 109 | */ 110 | public function isCompleted() 111 | { 112 | return $this->completed === true; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Model/MailMessage.php: -------------------------------------------------------------------------------- 1 | 'Obiwoan', 20 | * 'password' => 'Kenobi', 21 | * 'encryption' => 'ssl', 22 | * 'authMode' => 'Plain' 23 | * ] 24 | * ``` 25 | * 26 | * MailTransport may contain: 27 | * ``` 28 | * ['f%s'] 29 | * ``` 30 | * 31 | * SendMailTransport may contain: 32 | * ``` 33 | * ['/usr/sbin/sendmail -bs'] 34 | * ``` 35 | */ 36 | public $transportOptions = []; 37 | /** 38 | * @var string the transport type. It could be TransportInterface::TYPE_SMTP, TransportInterface::TYPE_MAIL, o 39 | * TransportInterface::TYPE_SEND_MAIL. 40 | */ 41 | public $transportType; 42 | /** 43 | * @var string the mail server address. 44 | */ 45 | public $host; 46 | /** 47 | * @var int the mail server port. 48 | */ 49 | public $port; 50 | /** 51 | * @var array|string the from address/es 52 | */ 53 | public $from; 54 | /** 55 | * @var array|string the to address/es 56 | */ 57 | public $to; 58 | /** 59 | * @var array|string the cc address/es 60 | */ 61 | public $cc; 62 | /** 63 | * @var array|string the bcc address/es 64 | */ 65 | public $bcc; 66 | /** 67 | * @var string the subject of the mail message 68 | */ 69 | public $subject; 70 | /** 71 | * @var string the body html of the mail message 72 | */ 73 | public $bodyHtml; 74 | /** 75 | * @var string the body 76 | */ 77 | public $bodyText; 78 | /** 79 | * @var array|null the file paths to attach to the Swift_Message instance if `asSwiftMessage()` is called 80 | */ 81 | protected $attachments; 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function __construct(array $config = []) 86 | { 87 | parent::__construct($config); 88 | } 89 | 90 | /** 91 | * @param array $config 92 | * @return MailMessage 93 | */ 94 | public static function make(array $config) 95 | { 96 | return new self($config); 97 | } 98 | 99 | /** 100 | * Specify data which should be serialized to JSON. 101 | * 102 | * @link http://php.net/manual/en/jsonserializable.jsonserialize.php 103 | * 104 | * @return mixed data which can be serialized by json_encode, 105 | * which is a value of any type other than a resource. 106 | * 107 | * @since 5.4.0 108 | */ 109 | public function jsonSerialize() 110 | { 111 | return get_object_vars($this); 112 | } 113 | 114 | /** 115 | * @return void 116 | * @throws \Da\Mailer\Exception\UndefinedMessageBrokerException 117 | */ 118 | public function enqueue() 119 | { 120 | $job = MailJobBuilder::make(['message' => json_encode($this)]); 121 | QueueBuilder::make()->enqueue($job); 122 | } 123 | 124 | /** 125 | * @param string $path 126 | * @param string|null $name 127 | * @return void 128 | */ 129 | public function addAttachment(string $path, ?string $name = null): void 130 | { 131 | if (is_null($this->attachments)) { 132 | $this->attachments = [File::make($path, $name)]; 133 | return; 134 | } 135 | 136 | $this->attachments[] = File::make($path, $name); 137 | } 138 | 139 | /** 140 | * @return array 141 | */ 142 | public function getAttachments(): array 143 | { 144 | return $this->attachments ?? []; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Queue/Backend/AbstractQueueStoreConnection.php: -------------------------------------------------------------------------------- 1 | configuration = $configuration; 25 | } 26 | 27 | /** 28 | * @param $key 29 | * @param null $default 30 | * 31 | * @return mixed 32 | */ 33 | protected function getConfigurationValue($key, $default = null) 34 | { 35 | return ArrayHelper::getValue($this->configuration, $key, $default); 36 | } 37 | 38 | /** 39 | * Disconnects previous connection. 40 | */ 41 | public function disconnect() 42 | { 43 | $this->instance = null; 44 | } 45 | 46 | /** 47 | * @return AbstractQueueStoreConnection 48 | */ 49 | abstract public function connect(); 50 | /** 51 | * @return mixed 52 | */ 53 | abstract public function getInstance(); 54 | } 55 | -------------------------------------------------------------------------------- /src/Queue/Backend/Beanstalkd/BeanstalkdMailJob.php: -------------------------------------------------------------------------------- 1 | timeToSend; 24 | } 25 | 26 | /** 27 | * @param int $timestamp 28 | */ 29 | public function setTimeToSend($timestamp) 30 | { 31 | $this->timeToSend = $timestamp; 32 | } 33 | 34 | /** 35 | * @param PheanstalkJob $job 36 | */ 37 | public function setPheanstalkJob(PheanstalkJob $job) 38 | { 39 | $this->pheanstalkJob = $job; 40 | } 41 | 42 | /** 43 | * @return PheanstalkJob 44 | */ 45 | public function getPheanstalkJob() 46 | { 47 | return $this->pheanstalkJob; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Queue/Backend/Beanstalkd/BeanstalkdQueueStoreAdapter.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 45 | $this->queueName = $queueName; 46 | $this->timeToRun = $timeToRun; 47 | $this->reserveTimeout = $reserveTimeOut; 48 | $this->init(); 49 | } 50 | 51 | /** 52 | * @return BeanstalkdQueueStoreAdapter 53 | */ 54 | public function init() 55 | { 56 | $this->getConnection()->connect(); 57 | return $this; 58 | } 59 | 60 | /** 61 | * @return BeanstalkdQueueStoreConnection 62 | */ 63 | public function getConnection() 64 | { 65 | return $this->connection; 66 | } 67 | 68 | /** 69 | * @param BeanstalkdMailJob|MailJobInterface $mailJob 70 | * 71 | * @return \Pheanstalk\Job 72 | */ 73 | public function enqueue(MailJobInterface $mailJob) 74 | { 75 | $timestamp = $mailJob->getTimeToSend(); 76 | $payload = $this->createPayload($mailJob); 77 | $delay = (int) max(Pheanstalk::DEFAULT_DELAY, $timestamp - time()); 78 | return $this->getConnection() 79 | ->getInstance() 80 | ->useTube($this->queueName) 81 | ->put($payload, Pheanstalk::DEFAULT_PRIORITY, $delay, $this->timeToRun); 82 | } 83 | 84 | /** 85 | * @return BeanstalkdMailJob|null 86 | * @throws \Pheanstalk\Exception\DeadlineSoonException 87 | */ 88 | public function dequeue() 89 | { 90 | $job = $this->getConnection()->getInstance()->watch($this->queueName)->reserveWithTimeout($this->reserveTimeout); 91 | if ($job instanceof PheanstalkJob) { 92 | $data = json_decode($job->getData(), true); 93 | return new BeanstalkdMailJob([ 94 | 'id' => $data['id'], 95 | 'attempt' => $data['attempt'], 96 | 'message' => $data['message'], 97 | 'pheanstalkJob' => $job, 98 | ]); 99 | } 100 | 101 | return null; 102 | } 103 | 104 | /** 105 | * @param BeanstalkdMailJob|MailJobInterface $mailJob 106 | * @return null 107 | */ 108 | public function ack(MailJobInterface $mailJob) 109 | { 110 | if ($mailJob->isNewRecord()) { 111 | throw new InvalidCallException('BeanstalkdMailJob cannot be a new object to be acknowledged'); 112 | } 113 | 114 | $pheanstalk = $this->getConnection()->getInstance()->useTube($this->queueName); 115 | if ($mailJob->isCompleted()) { 116 | $pheanstalk->delete($mailJob->getPheanstalkJob()); 117 | return null; 118 | } 119 | 120 | $timestamp = $mailJob->getTimeToSend(); 121 | $delay = max(0, $timestamp - time()); 122 | // add back to the queue as it wasn't completed maybe due to some transitory error 123 | // could also be failed. 124 | $pheanstalk->release($mailJob->getPheanstalkJob(), Pheanstalk::DEFAULT_PRIORITY, $delay); 125 | return null; 126 | } 127 | 128 | /** 129 | * 130 | * @return bool 131 | */ 132 | public function isEmpty() 133 | { 134 | $stats = $this->getConnection()->getInstance()->statsTube($this->queueName); 135 | return (int) $stats->current_jobs_delayed === 0 136 | && (int) $stats->current_jobs_urgent === 0 137 | && (int) $stats->current_jobs_ready === 0; 138 | } 139 | 140 | /** 141 | * @param BeanstalkdMailJob|MailJobInterface $mailJob 142 | * 143 | * @return string 144 | */ 145 | protected function createPayload(MailJobInterface $mailJob) 146 | { 147 | return json_encode([ 148 | 'id' => $mailJob->isNewRecord() ? sha1(Random::string(32)) : $mailJob->getId(), 149 | 'attempt' => $mailJob->getAttempt(), 150 | 'message' => $mailJob->getMessage(), 151 | ]); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Queue/Backend/Beanstalkd/BeanstalkdQueueStoreConnection.php: -------------------------------------------------------------------------------- 1 | disconnect(); 30 | $host = $this->getConfigurationValue('host', '127.0.0.1'); 31 | $port = $this->getConfigurationValue('port', Pheanstalk::DEFAULT_PORT); 32 | $connectionTimeout = $this->getConfigurationValue('connectionTimeout'); 33 | $connectPersistent = $this->getConfigurationValue('connectPersistent', false); 34 | $connection = new Connection(new SocketFactory($host, $port ?: Pheanstalk::DEFAULT_PORT, $connectionTimeout ?? 0, $connectPersistent ?? SocketFactory::AUTODETECT)); 35 | $this->instance = new Pheanstalk($connection); 36 | return $this; 37 | } 38 | 39 | /** 40 | * @return Pheanstalk 41 | */ 42 | public function getInstance() 43 | { 44 | if ($this->instance === null) { 45 | $this->connect(); 46 | } 47 | 48 | return $this->instance; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Queue/Backend/MailJobInterface.php: -------------------------------------------------------------------------------- 1 | timeToSend ?: date('Y-m-d H:i:s', time()); 49 | } 50 | 51 | /** 52 | * @param string $date 53 | */ 54 | public function setTimeToSend($date) 55 | { 56 | $this->timeToSend = $date; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getState() 63 | { 64 | return $this->state; 65 | } 66 | 67 | /** 68 | * Marks the state as completed. After marking the instance as completed, we should call the 69 | * `PdoQueueStoreAdapter::ack()` method to update the database with new status. 70 | */ 71 | public function markAsCompleted() 72 | { 73 | $this->state = self::STATE_COMPLETED; 74 | parent::markAsCompleted(); 75 | } 76 | 77 | /** 78 | * Marks the state as new. If we update the status back to 'N'ew, we could send it back to queue by using the 79 | * `PdoQueueStoreAdapter::ack()` method. That means even including a new time to be processed in the future by 80 | * setting the `$timeToSend` in a future date. 81 | */ 82 | public function markAsNew() 83 | { 84 | $this->state = self::STATE_NEW; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Queue/Backend/Pdo/PdoQueueStoreAdapter.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 29 | $this->tableName = $tableName; 30 | $this->init(); 31 | } 32 | 33 | /** 34 | * @return PdoQueueStoreAdapter|QueueStoreAdapterInterface 35 | */ 36 | public function init() 37 | { 38 | $this->getConnection()->connect(); 39 | return $this; 40 | } 41 | 42 | /** 43 | * @return PdoQueueStoreConnection 44 | */ 45 | public function getConnection() 46 | { 47 | return $this->connection; 48 | } 49 | 50 | /** 51 | * Adds a MailJob to the queue. 52 | * 53 | * @param MailJobInterface|PdoMailJob $mailJob 54 | * 55 | * @return bool whether it has been successfully inserted or not 56 | */ 57 | public function enqueue(MailJobInterface $mailJob) 58 | { 59 | $sql = sprintf('INSERT INTO `%s` (`message`, `timeToSend`) VALUES (:message, :timeToSend)', $this->tableName); 60 | $query = $this->getConnection()->getInstance()->prepare($sql); 61 | $query->bindValue(':message', $mailJob->getMessage()); 62 | $query->bindValue(':timeToSend', $mailJob->getTimeToSend()); 63 | return $query->execute(); 64 | } 65 | 66 | /** 67 | * Returns a MailJob extracted from the database. The row at the database is marked as 'A'ctive or in process. 68 | * 69 | * @return MailJobInterface|PdoMailJob 70 | */ 71 | public function dequeue() 72 | { 73 | $this->getConnection()->getInstance()->beginTransaction(); 74 | $mailJob = null; 75 | $sqlText = 'SELECT `id`, `message`, `attempt` 76 | FROM `%s` WHERE `timeToSend` <= :timeToSend AND `state`=:state 77 | ORDER BY id ASC LIMIT 1 FOR UPDATE'; 78 | $sql = sprintf($sqlText, $this->tableName); 79 | $query = $this->getConnection()->getInstance()->prepare($sql); 80 | $query->bindValue(':state', PdoMailJob::STATE_NEW); 81 | $query->bindValue(':timeToSend', date('Y-m-d H:i:s'), time()); 82 | $query->execute(); 83 | $queryResult = $query->fetch(PDO::FETCH_ASSOC); 84 | if ($queryResult) { 85 | // 86 | $sqlText = 'UPDATE `%s` SET `state`=:state WHERE `id`=:id'; 87 | $sql = sprintf($sqlText, $this->tableName); 88 | $query = $this->getConnection()->getInstance()->prepare($sql); 89 | $query->bindValue(':state', PdoMailJob::STATE_ACTIVE); 90 | $query->bindValue(':id', $queryResult['id'], PDO::PARAM_INT); 91 | $query->execute(); 92 | $mailJob = new PdoMailJob($queryResult); 93 | } 94 | 95 | $this->getConnection()->getInstance()->commit(); 96 | return $mailJob; 97 | } 98 | 99 | /** 100 | * 'Ack'knowledge the MailJob. Once a MailJob as been processed it could be:. 101 | * 102 | * - Updated its status to 'C'ompleted 103 | * - Updated its status to 'N'ew and set its `timeToSend` attribute to a future date 104 | * 105 | * @param MailJobInterface|PdoMailJob $mailJob 106 | * 107 | * @return bool 108 | */ 109 | public function ack(MailJobInterface $mailJob) 110 | { 111 | if ($mailJob->isNewRecord()) { 112 | throw new InvalidCallException('PdoMailJob cannot be a new object to be acknowledged'); 113 | } 114 | 115 | $sqlText = 'UPDATE `%s` 116 | SET `attempt`=:attempt, `state`=:state, `timeToSend`=:timeToSend, `sentTime`=:sentTime 117 | WHERE `id`=:id'; 118 | $sql = sprintf($sqlText, $this->tableName); 119 | $sentTime = $mailJob->isCompleted() ? date('Y-m-d H:i:s', time()) : null; 120 | $query = $this->getConnection()->getInstance()->prepare($sql); 121 | $query->bindValue(':id', $mailJob->getId(), PDO::PARAM_INT); 122 | $query->bindValue(':attempt', $mailJob->getAttempt(), PDO::PARAM_INT); 123 | $query->bindValue(':state', $mailJob->getState()); 124 | $query->bindValue(':timeToSend', $mailJob->getTimeToSend()); 125 | $query->bindValue(':sentTime', $sentTime); 126 | return $query->execute(); 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | public function isEmpty() 133 | { 134 | $sql = sprintf('SELECT COUNT(`id`) FROM `%s` WHERE `timeToSend` <= :timeToSend AND `state`=:state ORDER BY id ASC LIMIT 1', $this->tableName); 135 | $query = $this->getConnection()->getInstance()->prepare($sql); 136 | $query->bindValue(':state', PdoMailJob::STATE_NEW); 137 | $query->bindValue(':timeToSend', date('Y-m-d H:i:s'), time()); 138 | $query->execute(); 139 | return intval($query->fetchColumn(0)) === 0; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Queue/Backend/Pdo/PdoQueueStoreConnection.php: -------------------------------------------------------------------------------- 1 | defineConnectionString(); 19 | } 20 | 21 | protected function defineConnectionString() 22 | { 23 | if (! isset($this->configuration['dsn'])) { 24 | $this->configuration['dsn'] = sprintf("mysql:host=%s;dbname=%s;port=%s", $this->configuration['host'] ?? '', $this->configuration['db'] ?? '', $this->configuration['port'] ?? 3306); 25 | } 26 | } 27 | 28 | /** 29 | * @return PdoQueueStoreConnection 30 | */ 31 | public function connect() 32 | { 33 | $this->disconnect(); 34 | $username = $this->getConfigurationValue('username'); 35 | $password = $this->getConfigurationValue('password'); 36 | $options = $this->getConfigurationValue('options'); 37 | $this->instance = new PDO($this->getConfigurationValue('dsn'), $username, $password, $options); 38 | $this->instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 39 | return $this; 40 | } 41 | 42 | /** 43 | * Returns the connection instance. 44 | * 45 | * @return PDO 46 | */ 47 | public function getInstance() 48 | { 49 | if ($this->instance === null) { 50 | $this->connect(); 51 | } 52 | 53 | return $this->instance; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Queue/Backend/QueueStoreAdapterInterface.php: -------------------------------------------------------------------------------- 1 | deliveryTag; 18 | } 19 | 20 | /** 21 | * @param $delivery_tag 22 | * @return void 23 | */ 24 | public function setDeliveryTag($deliveryTag) 25 | { 26 | $this->deliveryTag = $deliveryTag; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Queue/Backend/RabbitMq/RabbitMqQueueConnection.php: -------------------------------------------------------------------------------- 1 | connection)) { 32 | $this->disconnect(); 33 | } 34 | 35 | $this->connection = new AMQPStreamConnection( 36 | $this->configuration['host'], 37 | $this->configuration['port'], 38 | $this->configuration['user'], 39 | $this->configuration['password'] 40 | ); 41 | 42 | $this->instance = $this->connection->channel(); 43 | $this->instance->confirm_select(); 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | public function getInstance() 52 | { 53 | if (is_null($this->instance)) { 54 | $this->connect(); 55 | } 56 | 57 | return $this->instance; 58 | } 59 | 60 | public function disconnect() 61 | { 62 | if (is_null($this->connection)) { 63 | return; 64 | } 65 | 66 | $this->instance->close(); 67 | $this->connection->close(); 68 | $this->instance = null; 69 | $this->connection = null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Queue/Backend/RabbitMq/RabbitMqQueueStoreAdapter.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 36 | $this->expireTime = $expireTime; 37 | $this->queueName = $queueName; 38 | 39 | $this->init(); 40 | } 41 | 42 | /** 43 | * @return 44 | */ 45 | public function init() 46 | { 47 | $this->getConnection() 48 | ->connect(); 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @inheritDoc 55 | */ 56 | public function getConnection() 57 | { 58 | return $this->connection; 59 | } 60 | 61 | /** 62 | * @var RabbitMqJob|MailJobInterface $mailJob 63 | * 64 | * @return bool; 65 | */ 66 | public function enqueue(MailJobInterface $mailJob) 67 | { 68 | try { 69 | /** @var AMQPChannel $chanel */ 70 | $chanel = $this->getConnection()->getInstance(); 71 | $chanel->queue_declare($this->queueName, false, false, false, false); 72 | $message = new AMQPMessage($this->createPayload($mailJob)); 73 | $chanel->basic_publish($message, '', $this->queueName); 74 | 75 | return true; 76 | } catch (\Exception $exception) { 77 | return false; 78 | } 79 | } 80 | 81 | /** 82 | * @inheritDoc 83 | * @return RabbitMqJob|MailJobInterface|null 84 | */ 85 | public function dequeue() 86 | { 87 | if ($this->isEmpty()) { 88 | return null; 89 | } 90 | 91 | /** @var AMQPChannel $chanel */ 92 | $chanel = $this->getConnection()->getInstance(); 93 | 94 | /** @var AMQPMessage $message */ 95 | $message = $chanel->basic_get($this->queueName); 96 | 97 | $data = json_decode($message->body, true); 98 | 99 | return new RabbitMqJob([ 100 | 'id' => $data['id'], 101 | 'message' => $data['message'], 102 | 'attempt' => $data['attempt'], 103 | 'deliveryTag' => $message->delivery_info['delivery_tag'], 104 | ]); 105 | } 106 | 107 | /** 108 | * @param RabbitMqJob $mailJob 109 | */ 110 | public function ack(MailJobInterface $mailJob) 111 | { 112 | /** @var AMQPChannel $chanel */ 113 | $chanel = $this->getConnection()->getInstance(); 114 | if ($mailJob->isCompleted()) { 115 | $chanel->basic_ack($mailJob->getDeliveryTag(), false); 116 | return; 117 | } 118 | 119 | $chanel->basic_nack($mailJob->getDeliveryTag(), false, true); 120 | } 121 | 122 | /** 123 | * @inheritDoc 124 | */ 125 | public function isEmpty() 126 | { 127 | /** @var AMQPChannel $chanel */ 128 | $chanel = $this->getConnection()->getInstance(); 129 | $queueProperties = $chanel->queue_declare($this->queueName, false, false, false, false); 130 | 131 | return is_array($queueProperties) && $queueProperties[1] === 0; 132 | } 133 | 134 | /** 135 | * @param MailJobInterface $mailJob 136 | * @return false|string 137 | */ 138 | protected function createPayload(MailJobInterface $mailJob) 139 | { 140 | return json_encode([ 141 | 'id' => $mailJob->isNewRecord() ? sha1(Random::string(32)) : $mailJob->getId(), 142 | 'attempt' => $mailJob->getAttempt(), 143 | 'message' => $mailJob->getMessage(), 144 | 'delivery_tag' => null, 145 | ]); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Queue/Backend/Redis/RedisMailJob.php: -------------------------------------------------------------------------------- 1 | timeToSend; 24 | } 25 | 26 | /** 27 | * @param int $timestamp 28 | */ 29 | public function setTimeToSend($timestamp) 30 | { 31 | $this->timeToSend = $timestamp; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Queue/Backend/Redis/RedisQueueStoreAdapter.php: -------------------------------------------------------------------------------- 1 | expireTime = $expireTime; 34 | $this->connection = $connection; 35 | $this->queueName = $queueName; 36 | $this->init(); 37 | } 38 | 39 | /** 40 | * @return RedisQueueStoreAdapter 41 | */ 42 | public function init() 43 | { 44 | $this->getConnection() 45 | ->connect(); 46 | return $this; 47 | } 48 | 49 | /** 50 | * @return RedisQueueStoreConnection 51 | */ 52 | public function getConnection() 53 | { 54 | return $this->connection; 55 | } 56 | 57 | /** 58 | * @param RedisMailJob|MailJobInterface $mailJob 59 | * 60 | * @return int 61 | */ 62 | public function enqueue(MailJobInterface $mailJob) 63 | { 64 | $timestamp = $mailJob->getTimeToSend(); 65 | $payload = $this->createPayload($mailJob); 66 | return $timestamp !== null && $timestamp > time() 67 | ? $this->getConnection()->getInstance()->zadd($this->queueName . ':delayed', $timestamp, $payload) 68 | : $this->getConnection()->getInstance()->rpush($this->queueName, $payload); 69 | } 70 | 71 | /** 72 | * @return RedisMailJob|null 73 | */ 74 | public function dequeue() 75 | { 76 | $this->migrateExpiredJobs(); 77 | $job = $this->getConnection()->getInstance()->lpop($this->queueName); 78 | if ($job !== null) { 79 | $this->getConnection() 80 | ->getInstance() 81 | ->zadd($this->queueName . ':reserved', time() + $this->expireTime, $job); 82 | $data = json_decode($job, true); 83 | return new RedisMailJob([ 84 | 'id' => $data['id'], 85 | 'attempt' => $data['attempt'], 86 | 'message' => $data['message'], 87 | ]); 88 | } 89 | 90 | return null; 91 | } 92 | 93 | /** 94 | * @param RedisMailJob|MailJobInterface $mailJob 95 | */ 96 | public function ack(MailJobInterface $mailJob) 97 | { 98 | if ($mailJob->isNewRecord()) { 99 | throw new InvalidCallException('RedisMailJob cannot be a new object to be acknowledged'); 100 | } 101 | 102 | $this->removeReserved($mailJob); 103 | if (!$mailJob->isCompleted()) { 104 | if ($mailJob->getTimeToSend() === null || $mailJob->getTimeToSend() < time()) { 105 | $mailJob->setTimeToSend(time() + $this->expireTime); 106 | } 107 | $this->enqueue($mailJob); 108 | } 109 | } 110 | 111 | /** 112 | * @param MailJobInterface $mailJob 113 | */ 114 | public function removeReserved(MailJobInterface $mailJob) 115 | { 116 | $payload = $this->createPayload($mailJob); 117 | $this->getConnection()->getInstance()->zrem($this->queueName . ':reserved', $payload); 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | public function isEmpty() 124 | { 125 | return $this->getConnection()->getInstance()->llen($this->queueName) === 0; 126 | } 127 | 128 | /** 129 | * @param RedisMailJob|MailJobInterface $mailJob 130 | * 131 | * @return string 132 | */ 133 | protected function createPayload(MailJobInterface $mailJob) 134 | { 135 | return json_encode([ 136 | 'id' => $mailJob->isNewRecord() ? sha1(Random::string(32)) : $mailJob->getId(), 137 | 'attempt' => $mailJob->getAttempt(), 138 | 'message' => $mailJob->getMessage(), 139 | ]); 140 | } 141 | 142 | /** 143 | * Migrates all expired jobs from delayed and reserved queues to the main queue to be processed. 144 | */ 145 | protected function migrateExpiredJobs() 146 | { 147 | $this->migrateJobs($this->queueName . ':delayed', $this->queueName); 148 | $this->migrateJobs($this->queueName . ':reserved', $this->queueName); 149 | } 150 | 151 | /** 152 | * Migrates expired jobs from one queue to another. 153 | * 154 | * @param string $from the name of the source queue 155 | * @param string $to the name of the target queue 156 | */ 157 | protected function migrateJobs($from, $to) 158 | { 159 | $options = ['cas' => true, 'watch' => $from, 'retry' => 10]; 160 | $this->getConnection()->getInstance()->transaction($options, function ($transaction) use ($from, $to) { 161 | 162 | $time = time(); 163 | // First we need to get all of jobs that have expired based on the current time 164 | // so that we can push them onto the main queue. After we get them we simply 165 | // remove them from this "delay" queues. All of this within a transaction. 166 | $jobs = $this->getExpiredJobs($transaction, $from, $time); 167 | // If we actually found any jobs, we will remove them from the old queue and we 168 | // will insert them onto the new (ready) "queue". This means they will stand 169 | // ready to be processed by the queue worker whenever their turn comes up. 170 | if (count($jobs) > 0) { 171 | $this->removeExpiredJobs($transaction, $from, $time); 172 | $this->pushExpiredJobsOntoNewQueue($transaction, $to, $jobs); 173 | } 174 | }); 175 | } 176 | 177 | /** 178 | * Get the expired jobs from a given queue. 179 | * 180 | * @param \Predis\Transaction\MultiExec $transaction 181 | * @param string $from 182 | * @param int $time 183 | * 184 | * @return array 185 | */ 186 | protected function getExpiredJobs($transaction, $from, $time) 187 | { 188 | return $transaction->zrangebyscore($from, '-inf', $time); 189 | } 190 | 191 | /** 192 | * Remove the expired jobs from a given queue. 193 | * 194 | * @param \Predis\Transaction\MultiExec $transaction 195 | * @param string $from 196 | * @param int $time 197 | * 198 | */ 199 | protected function removeExpiredJobs($transaction, $from, $time) 200 | { 201 | $transaction->multi(); 202 | $transaction->zremrangebyscore($from, '-inf', $time); 203 | } 204 | 205 | /** 206 | * Push all of the given jobs onto another queue. 207 | * 208 | * @param \Predis\Transaction\MultiExec $transaction 209 | * @param string $to 210 | * @param array $jobs 211 | * 212 | */ 213 | protected function pushExpiredJobsOntoNewQueue($transaction, $to, $jobs) 214 | { 215 | call_user_func_array([$transaction, 'rpush'], array_merge([$to], $jobs)); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/Queue/Backend/Redis/RedisQueueStoreConnection.php: -------------------------------------------------------------------------------- 1 | disconnect(); 29 | $this->instance = new Client($this->configuration); 30 | return $this; 31 | } 32 | 33 | /** 34 | * Returns the client predis instance. 35 | * 36 | * @return Client 37 | */ 38 | public function getInstance() 39 | { 40 | if ($this->instance === null) { 41 | $this->connect(); 42 | } 43 | 44 | return $this->instance; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Queue/Backend/Sqs/SqsMailJob.php: -------------------------------------------------------------------------------- 1 | getId() === null || $this->receiptHandle === null; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getReceiptHandle() 54 | { 55 | return $this->receiptHandle; 56 | } 57 | 58 | /** 59 | * @param string $receiptHandle 60 | */ 61 | public function setReceiptHandle($receiptHandle) 62 | { 63 | $this->receiptHandle = $receiptHandle; 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | public function getDelaySeconds() 70 | { 71 | return $this->delaySeconds; 72 | } 73 | 74 | /** 75 | * @param int $delaySeconds 76 | */ 77 | public function setDelaySeconds($delaySeconds) 78 | { 79 | if ($delaySeconds < 0 || $delaySeconds > 900) { 80 | throw new BadMethodCallException('Delay seconds must be between 0 and 900 seconds interval'); 81 | } 82 | $this->delaySeconds = $delaySeconds; 83 | } 84 | 85 | /** 86 | * @return int 87 | */ 88 | public function getVisibilityTimeout() 89 | { 90 | return $this->visibilityTimeout; 91 | } 92 | 93 | /** 94 | * @param int $visibilityTimeout 95 | */ 96 | public function setVisibilityTimeout($visibilityTimeout) 97 | { 98 | $this->visibilityTimeout = $visibilityTimeout; 99 | } 100 | 101 | /** 102 | * @return bool 103 | */ 104 | public function getDeleted() 105 | { 106 | return $this->deleted; 107 | } 108 | 109 | /** 110 | * @param bool $deleted 111 | */ 112 | public function setDeleted($deleted) 113 | { 114 | $this->deleted = $deleted; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Queue/Backend/Sqs/SqsQueueStoreAdapter.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 32 | $this->queueName = $queueName; 33 | $this->init(); 34 | } 35 | 36 | /** 37 | * @return SqsQueueStoreAdapter 38 | */ 39 | public function init() 40 | { 41 | $this->getConnection()->connect(); 42 | // create new queue or get existing one 43 | $queue = $this->getConnection()->getInstance()->createQueue([ 44 | 'QueueName' => $this->queueName, 45 | ]); 46 | $this->queueUrl = $queue['QueueUrl']; 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return SqsQueueStoreConnection 52 | */ 53 | public function getConnection() 54 | { 55 | return $this->connection; 56 | } 57 | 58 | /** 59 | * @param MailJobInterface|SqsMailJob $mailJob 60 | * 61 | * @return bool whether it has been successfully queued or not 62 | */ 63 | public function enqueue(MailJobInterface $mailJob) 64 | { 65 | $result = $this->getConnection()->getInstance()->sendMessage([ 66 | 'QueueUrl' => $this->queueUrl, 67 | 'MessageBody' => $mailJob->getMessage(), 68 | 'DelaySeconds' => $mailJob->getDelaySeconds(), 69 | 'Attempt' => $mailJob->getAttempt(), 70 | ]); 71 | $messageId = $result['MessageId']; 72 | return $messageId !== null && is_string($messageId); 73 | } 74 | 75 | /** 76 | * Returns a MailJob fetched from Amazon SQS. 77 | * 78 | * @return MailJobInterface|SqsMailJob 79 | */ 80 | public function dequeue() 81 | { 82 | $result = $this->getConnection()->getInstance()->receiveMessage([ 83 | 'QueueUrl' => $this->queueUrl, 84 | ]); 85 | if (empty($result['Messages'])) { 86 | return null; 87 | } 88 | 89 | $result = array_shift($result['Messages']); 90 | return new SqsMailJob([ 91 | 'id' => $result['MessageId'], 92 | 'receiptHandle' => $result['ReceiptHandle'], 93 | 'message' => $result['Body'], 94 | 'attempt' => $result['Attempt'], 95 | ]); 96 | } 97 | 98 | /** 99 | * @param MailJobInterface|SqsMailJob $mailJob 100 | * 101 | * @return bool 102 | */ 103 | public function ack(MailJobInterface $mailJob) 104 | { 105 | if ($mailJob->isNewRecord()) { 106 | throw new InvalidCallException('SqsMailJob cannot be a new object to be acknowledged'); 107 | } 108 | 109 | if ($mailJob->getDeleted()) { 110 | $this->getConnection()->getInstance()->deleteMessage([ 111 | 'QueueUrl' => $this->queueUrl, 112 | 'ReceiptHandle' => $mailJob->getReceiptHandle(), 113 | ]); 114 | return true; 115 | } elseif ($mailJob->getVisibilityTimeout() !== null) { 116 | $this->getConnection()->getInstance()->changeMessageVisibility([ 117 | 'QueueUrl' => $this->queueUrl, 118 | 'ReceiptHandle' => $mailJob->getReceiptHandle(), 119 | 'VisibilityTimeout' => $mailJob->getVisibilityTimeout(), 120 | ]); 121 | return true; 122 | } 123 | 124 | return false; 125 | } 126 | 127 | /** 128 | * {@inheritdoc} 129 | */ 130 | public function isEmpty(): bool 131 | { 132 | $response = $this->getConnection()->getInstance()->getQueueAttributes([ 133 | 'QueueUrl' => $this->queueUrl, 134 | 'AttributeNames' => ['ApproximateNumberOfMessages'], 135 | ]); 136 | return $response['Attributes']['ApproximateNumberOfMessages'] === 0; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Queue/Backend/Sqs/SqsQueueStoreConnection.php: -------------------------------------------------------------------------------- 1 | disconnect(); 26 | $key = $this->getConfigurationValue('key'); 27 | $secret = $this->getConfigurationValue('secret'); 28 | $region = $this->getConfigurationValue('region'); 29 | $this->instance = new SqsClient([ 30 | 'key' => $key, 31 | 'secret' => $secret, 32 | 'region' => $region, 33 | ]); 34 | return $this; 35 | } 36 | 37 | /** 38 | * Returns the connection instance. 39 | * 40 | * @return SqsClient 41 | */ 42 | public function getInstance() 43 | { 44 | if ($this->instance === null) { 45 | $this->connect(); 46 | } 47 | 48 | return $this->instance; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Queue/Cli/MailMessageWorker.php: -------------------------------------------------------------------------------- 1 | mailer = $mailer; 33 | $this->mailMessage = $mailMessage; 34 | } 35 | 36 | /** 37 | * Sends the MailMessage. It does triggers the following events:. 38 | * 39 | * - onSuccess: If the sending has been successful 40 | * - onFailure: If the sending has failed 41 | * 42 | * The events need to be configured by attaching a handler to them. 43 | * 44 | * @see EventHandlerTrait 45 | */ 46 | public function run() 47 | { 48 | $event = 'onSuccess'; 49 | try { 50 | $sentMessage = $this->mailer->send($this->mailMessage); 51 | if (is_null($sentMessage)) { 52 | $event = 'onFailure'; 53 | } 54 | } catch (Exception $e) { 55 | $event = 'onFailure'; 56 | } 57 | $this->trigger($event, [$this->mailMessage, $sentMessage ?? null]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Queue/MailQueue.php: -------------------------------------------------------------------------------- 1 | adapter = $adapter; 30 | } 31 | 32 | /** 33 | * @return MailQueue 34 | * @throws \Da\Mailer\Exception\UndefinedMessageBrokerException 35 | */ 36 | public static function make() 37 | { 38 | return QueueBuilder::make(); 39 | } 40 | 41 | /** 42 | * @return AbstractQueueStoreConnection 43 | */ 44 | public function getConnection() 45 | { 46 | return $this->adapter->getConnection(); 47 | } 48 | 49 | /** 50 | * @param CypherInterface $cypher 51 | */ 52 | public function setCypher(CypherInterface $cypher) 53 | { 54 | $this->cypher = $cypher; 55 | } 56 | 57 | /** 58 | * @return CypherInterface 59 | */ 60 | public function getCypher() 61 | { 62 | return $this->cypher; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function enqueue(MailJobInterface $mailJob) 69 | { 70 | $message = $mailJob->getMessage(); 71 | if (null !== $this->getCypher() && $message instanceof MailMessage) { 72 | $mailJob->setMessage($this->getCypher()->encodeMailMessage($message)); 73 | } 74 | 75 | return $this->adapter->enqueue($mailJob); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function dequeue() 82 | { 83 | $mailJob = $this->adapter->dequeue(); 84 | 85 | if (null !== $this->getCypher()) { 86 | $mailJob->setMessage($this->getCypher()->decodeMailMessage($mailJob->getMessage())); 87 | } 88 | 89 | return $mailJob; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function init() 96 | { 97 | return $this->adapter->init(); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function ack(MailJobInterface $mailJob) 104 | { 105 | return $this->adapter->ack($mailJob); 106 | } 107 | 108 | /** 109 | * @return bool 110 | */ 111 | public function isEmpty() 112 | { 113 | return $this->adapter->isEmpty(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Security/Cypher.php: -------------------------------------------------------------------------------- 1 | key = $key; 26 | $this->iv = $iv; 27 | // initialize cypher with strongest mode and with AES. Is an anti-pattern and should be passed through the 28 | // constructor as an argument, but this way we ensure the library does have the strongest strategy by default. 29 | $this->strategy = new AES('cbc'); 30 | $this->strategy->setKeyLength(256); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function encodeMailMessage(MailMessage $mailMessage) 37 | { 38 | $jsonEncodedMailMessage = json_encode($mailMessage, JSON_NUMERIC_CHECK); 39 | $this->strategy->setKey($this->key); 40 | $this->strategy->setIV($this->iv); 41 | return base64_encode($this->strategy->encrypt($jsonEncodedMailMessage)); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function decodeMailMessage($encodedMailMessage) 48 | { 49 | $this->strategy->setKey($this->key); 50 | $decryptedMailMessage = $this->strategy->decrypt(base64_decode($encodedMailMessage, true)); 51 | $jsonDecodedMailMessageAttributes = json_decode($decryptedMailMessage, true); 52 | return new MailMessage($jsonDecodedMailMessageAttributes); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Security/CypherInterface.php: -------------------------------------------------------------------------------- 1 | options = $options; 21 | } 22 | 23 | /** 24 | * @return TransportInterface 25 | */ 26 | abstract public function create(); 27 | } 28 | -------------------------------------------------------------------------------- /src/Transport/MailTransport.php: -------------------------------------------------------------------------------- 1 | dsn = $dsn; 19 | } 20 | 21 | /** 22 | * @return \Symfony\Component\Mailer\Transport\TransportInterface 23 | */ 24 | public function getInstance(): \Symfony\Component\Mailer\Transport\TransportInterface 25 | { 26 | if ($this->instance === null) { 27 | $dsn = Dsn::fromString($this->dsn); 28 | $this->instance = (new NativeTransportFactory())->create($dsn); 29 | } 30 | 31 | return $this->instance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Transport/MailTransportFactory.php: -------------------------------------------------------------------------------- 1 | options['dsn']); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Transport/SendMailTransport.php: -------------------------------------------------------------------------------- 1 | dsn = $dsn; 20 | } 21 | 22 | /** 23 | * Returns the Swift_SendmailTransport instance. 24 | * 25 | * @return \Symfony\Component\Mailer\Transport\SendmailTransport instance 26 | */ 27 | public function getInstance(): \Symfony\Component\Mailer\Transport\SendmailTransport 28 | { 29 | if ($this->instance === null) { 30 | $sendMailFactory = new \Symfony\Component\Mailer\Transport\SendmailTransportFactory(); 31 | $this->instance = $sendMailFactory->create(Dsn::fromString($this->dsn)); 32 | } 33 | 34 | return $this->instance; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Transport/SendMailTransportFactory.php: -------------------------------------------------------------------------------- 1 | options['dsn']); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Transport/SmtpTransport.php: -------------------------------------------------------------------------------- 1 | host = $host; 37 | $this->port = $port; 38 | $this->options = $options; 39 | } 40 | 41 | /** 42 | * @return EsmtpTransport 43 | */ 44 | public function getInstance(): EsmtpTransport 45 | { 46 | if ($this->instance === null) { 47 | $user = $this->options['username'] ?? null; 48 | $password = $this->options['password'] ?? null; 49 | $this->instance = (new EsmtpTransportFactory())->create(new Dsn('smtp', $this->host, $user, $password, $this->port, $this->options)); 50 | } 51 | 52 | return $this->instance; 53 | } 54 | 55 | private function getScheme() 56 | { 57 | return $this->options['tls'] 58 | ? 'smtps' 59 | : 'smtp'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Transport/SmtpTransportFactory.php: -------------------------------------------------------------------------------- 1 | options, 'host'); 25 | $port = ArrayHelper::remove($this->options, 'port'); 26 | $options = ArrayHelper::getValue($this->options, 'options', []); 27 | return new SmtpTransport($host, $port, $options); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Transport/TransportFactory.php: -------------------------------------------------------------------------------- 1 |