├── .circleci └── config.yml ├── .gitignore ├── .php_cs.dist ├── .travis.yml ├── Dockerfile.dev ├── LICENCE ├── README.md ├── composer.json ├── docker-compose.yml ├── docker-compose ├── mysql │ ├── init │ │ └── 01-databases.sql │ └── my.cnf └── nginx │ └── default.conf ├── phpunit.xml ├── publishable └── config │ └── multimail.php ├── src ├── DatabaseConfigMailSettings.php ├── Exceptions │ ├── EmailNotInConfigException.php │ ├── InvalidConfigKeyException.php │ ├── NoDefaultException.php │ └── NotInitializedException.php ├── Facades │ └── MultiMail.php ├── FileConfigMailSettings.php ├── Jobs │ └── SendMailJob.php ├── MailSettings.php ├── Migrations │ ├── 2021_04_10_141537_create_email_providers_table.php │ └── 2021_04_10_141626_create_email_accounts_table.php ├── Models │ ├── EmailAccount.php │ └── EmailProvider.php ├── MultiMailServiceProvider.php ├── MultiMailer.php ├── PendingMail.php └── TransportManager.php └── tests ├── .env.example ├── Fixtures └── view.blade.php ├── Integration ├── MultiMailDatabaseTest.php ├── MultiMailTest.php ├── SMPTTest.php └── TestMail.php ├── TestCase.php ├── Traits └── MailTrap.php └── Unit ├── ConfigTest.php ├── MultiMailTest.php └── PendingMailTest.php /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # PHP CircleCI 2.0 configuration file 2 | # See: https://circleci.com/docs/2.0/language-php/ 3 | version: 2.1 4 | 5 | orbs: 6 | codecov: codecov/codecov@3.2.2 7 | 8 | # Define a job to be invoked later in a workflow. 9 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 10 | jobs: 11 | build: 12 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 13 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 14 | docker: 15 | # Specify the version you desire here 16 | - image: circleci/php:8.0-node-browsers 17 | 18 | # Specify service dependencies here if necessary 19 | # CircleCI maintains a library of pre-built images 20 | # documented at https://circleci.com/docs/2.0/circleci-images/ 21 | # Using the RAM variation mitigates I/O contention 22 | # for database intensive operations. 23 | # - image: circleci/mysql:5.7-ram 24 | # 25 | # - image: redis:2.8.19 26 | 27 | # Add steps to the job 28 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 29 | steps: 30 | - checkout 31 | 32 | - run: sudo apt update # PHP CircleCI 2.0 Configuration File# PHP CircleCI 2.0 Configuration File sudo apt install zlib1g-dev libsqlite3-dev 33 | - run: sudo docker-php-ext-install zip 34 | 35 | # Download and cache dependencies 36 | - restore_cache: 37 | keys: 38 | # "composer.lock" can be used if it is committed to the repo 39 | - v1-dependencies-{{ checksum "composer.json" }} 40 | # fallback to using the latest cache if no exact match is found 41 | - v1-dependencies- 42 | 43 | - run: composer install -n --prefer-dist 44 | 45 | - save_cache: 46 | key: v1-dependencies-{{ checksum "composer.json" }} 47 | paths: 48 | - ./vendor 49 | 50 | # run tests with phpunit or codecept 51 | #- run: ./vendor/bin/phpunit 52 | 53 | - run: 54 | name: "Run tests" 55 | command: phpdbg -qrr vendor/bin/phpunit --coverage-html build/coverage-report --coverage-clover coverage.xml 56 | - store_artifacts: 57 | path: build/coverage-report 58 | - codecov/upload 59 | 60 | 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .lock 3 | composer.lock 4 | .phpunit.result.cache 5 | tests/_report 6 | .env 7 | .idea 8 | /html 9 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRules([ 9 | '@PSR2' => true, 10 | 'blank_line_after_namespace' => true, 11 | 'class_definition' => true, 12 | 'elseif' => true, 13 | 'encoding' => true, 14 | 'full_opening_tag' => true, 15 | 'function_declaration' => true, 16 | 'indentation_type' => true, 17 | 'lowercase_constants' => true, 18 | 'lowercase_keywords' => true, 19 | 'method_argument_space' => true, 20 | 'no_break_comment' => true, 21 | 'no_closing_tag' => true, 22 | 'no_spaces_after_function_name' => true, 23 | 'no_trailing_whitespace' => true, 24 | 'no_trailing_whitespace_in_comment' => true, 25 | 'no_extra_blank_lines' => true, 26 | 'align_multiline_comment' => true, 27 | 'array_syntax' => ['syntax' => 'short'], 28 | 'array_indentation' => true, 29 | //'binary_operator_spaces' => true, ('=' is binary operator..) 30 | 'blank_line_after_opening_tag' => true, 31 | 'cast_spaces' => true, 32 | 'combine_consecutive_issets' => true, 33 | 'combine_consecutive_unsets' => true, 34 | 'concat_space' => ['spacing' => 'one'], 35 | 'explicit_indirect_variable' => true, 36 | 'fully_qualified_strict_types' => true, 37 | 'function_typehint_space' => true, 38 | 'linebreak_after_opening_tag' => true, 39 | 'lowercase_static_reference' => true, 40 | 'no_blank_lines_after_class_opening' => true, 41 | 'no_empty_comment' => true, 42 | 'no_empty_phpdoc' => true, 43 | 'no_empty_statement' => true, 44 | 'no_multiline_whitespace_around_double_arrow' => true, 45 | 'no_null_property_initialization' => true, 46 | 'no_trailing_comma_in_singleline_array' => true, 47 | 'no_useless_return' => true, 48 | 'no_useless_else' => true, 49 | 'no_unused_imports' => true, 50 | 'ordered_imports' => true, 51 | 'ordered_class_elements' => true, 52 | 'single_quote' => true, 53 | 'whitespace_after_comma_in_array' => true, 54 | 'backtick_to_shell_exec' => true, 55 | 'class_attributes_separation' => true, 56 | 'no_superfluous_elseif' => true, 57 | 'ternary_operator_spaces' => true, 58 | 'ternary_to_null_coalescing' => true, 59 | 'trailing_comma_in_multiline_array' => true, 60 | 'trim_array_spaces' => true, 61 | 'space_after_semicolon' => true, 62 | ]) 63 | ->setFinder(PhpCsFixer\Finder::create() 64 | ->exclude('vendor') 65 | ->in(__DIR__) 66 | ) 67 | ; 68 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - 7.3 6 | - 7.4 7 | 8 | before_script: 9 | - composer self-update 10 | - composer install --prefer-dist --no-interaction 11 | 12 | script: ./vendor/bin/phpunit --coverage-clover clover.xml 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | 17 | # Only runs tests on master or PR to master branch 18 | branches: 19 | only: 20 | - "master" 21 | 22 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM php:7.2-fpm 2 | 3 | # Arguments defined in docker-compose.yml 4 | ARG user 5 | ARG uid 6 | 7 | # Install system dependencies 8 | RUN apt-get update && apt-get install -y \ 9 | git \ 10 | curl \ 11 | libpng-dev \ 12 | libonig-dev \ 13 | libxml2-dev \ 14 | zip \ 15 | unzip \ 16 | libzip-dev \ 17 | -y mariadb-client 18 | 19 | # Install and enable xdebug (for generating PHPUnit Test Coverage Report) 20 | # 21 | # ADVICE: Enabling XDebug renders PHP script execution slower 22 | # 23 | RUN pecl install xdebug-2.9.8 \ 24 | && docker-php-ext-enable xdebug 25 | 26 | # Clear cache 27 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 28 | 29 | # Install PHP extensions 30 | RUN docker-php-ext-install zip mysqli pdo_mysql mbstring exif pcntl bcmath gd && docker-php-ext-enable mysqli 31 | 32 | COPY --from=composer:latest /usr/bin/composer /usr/bin/composer 33 | 34 | # Create system user to run Composer and Artisan Commands 35 | RUN useradd -G www-data,root -u $uid -d /home/$user $user 36 | RUN mkdir -p /home/$user/.composer && \ 37 | chown -R $user:$user /home/$user 38 | 39 | # Set working directory 40 | WORKDIR /var/www 41 | 42 | USER $user 43 | 44 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Dr. Adam Nielsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Multimail 2 | 3 | ![Mail image](https://miro.medium.com/max/640/1*XAhO69eFPH6p32VlylUCaw.png) 4 | 5 | [![CircleCI](https://circleci.com/gh/iwasherefirst2/laravel-multimail/tree/master.svg?style=svg)](https://circleci.com/gh/iwasherefirst2/laravel-multimail/tree/master) 6 | [![codecov](https://codecov.io/gh/iwasherefirst2/laravel-multimail/branch/master/graph/badge.svg?token=3X6ZVRR5EQ)](https://codecov.io/gh/iwasherefirst2/laravel-multimail) 7 | 8 | This package helps you to send mails from your Laravel application from multiple email accounts. 9 | 10 | The package supports sending queued, localized and bulk mails. 11 | 12 | This package works for `SMTP` and `log` drivers. 13 | 14 | ## Table of Contents 15 | 16 | - [Requirments](#requirements) 17 | - [Installation](#installation) 18 | - [Usage](#usage) 19 | - [Basic Examples](#basic-examples) 20 | - [Queued Mails](#queued-mails) 21 | - [Specify in Mailable](#specify-in-mailable) 22 | - [Bulk messages](#bulk-messages) 23 | - [Special Settings](#special-settings) 24 | - [Multiple Mail Providers](#multiple-mail-providers) 25 | - [Default mailaccount](#default-mailaccount) 26 | - [Testing](#testing) 27 | - [Get Mail From Database](#get-mail-from-database) 28 | - [Troubleshoot](#troubleshoot) 29 | - [For Package Developer](#for-package-developer) 30 | 31 | ## Requirements 32 | 33 | Laravel 5, 6,7 or 8. 34 | Larave 9 and 10 are not supported. 35 | 36 | ## Installation 37 | 38 | Install the package into your Laraval application with composer: 39 | 40 | composer require iwasherefirst2/laravel-multimail 41 | 42 | Publish the config file: 43 | 44 | php artisan vendor:publish --provider="IWasHereFirst2\LaravelMultiMail\MultiMailServiceProvider" --tag=config 45 | 46 | Configure your email clients in `config/multimail.php`: 47 | 48 | 'emails' => [ 49 | 'office@example.net' => 50 | [ 51 | 'pass' => env('first_mail_password'), 52 | 'from_name' => "Max Musterman", 53 | ], 54 | 'contact@example.net' => 55 | [ 56 | 'pass' => env('second_mail_password') 57 | ], 58 | ], 59 | 60 | Make sure to put your credentials in the `.env` file, so they don't get tracked in your repository. 61 | 62 | For each mail you may specify multiple columns: 63 | 64 | Attribut | Functionality | required 65 | --- | --- | --- 66 | `pass` | Password of email account | yes 67 | `username` | Username of email account, only neccessary if different from email address | no 68 | `from_name` | Name that should appear in front of email | no 69 | `provider` | Provider of email account, only necessary if mail host/encription/port is not default (see [here](#multiple-mail-providers) for more) | no 70 | 71 | ## Usage 72 | 73 | One may send a mail using `\MultiMail` instead of `\Mail`. The methods `to`, `cc`, `bcc`, `locale` are exactly the same as provided by the [mail facade](https://laravel.com/docs/5.8/mail#sending-mail) (note that `locale` is only available since Laravel 5.6). 74 | 75 | The `from` method from `MultiMail` needs a string of an email provided in `config/multimail.php`. You can pass optionaly a second parameter as from name instetad of using the default falue given in the config. 76 | When using `send` or `queue` the mail will be send from the mail account specified in `cofing/multimail.php`. 77 | 78 | ### Basic Examples 79 | 80 | This example assumes that `office@example.net` and `contact@example.net` have been specified in `config/multimail.php`. 81 | 82 | // Send mail from office@example.net 83 | \MultiMail::to($user)->from('office@example.com')->send(new \App\Mail\Invitation($user)); 84 | 85 | // Send from malaccount email@gmail.com 86 | \MultiMail::to($user)->from('email@example.net')->locale('en')->send(new \App\Mail\Invitation($user)); 87 | 88 | ### Queued Mails 89 | 90 | Queued mails work exactly the same as for the normal [Mail](https://laravel.com/docs/5.8/mail#queueing-mail) facade, 91 | i.e. they are either send explicitly be the `queue` method or the mailable class implements the `ShouldQueue` contract. 92 | 93 | // Queue Mail 94 | \MultiMail::from('contact@foo.org')->queue(new \App\Mail\Invitation($user)); 95 | 96 | It is of course necessary to install a [queue driver](https://laravel.com/docs/5.8/queues#driver-prerequisites). 97 | 98 | ### Specify in mailable 99 | 100 | You may set `to`, `cc`, `bcc`, `locale` and `from` in your mailable class. In this case, you could reduce the basic example from above to: 101 | 102 | // Send mail from office@example.net 103 | \MultiMail::send(new \App\Mail\Invitation($user)); 104 | 105 | Mailable: 106 | 107 | /** 108 | * Create a new message instance. 109 | * 110 | * @return void 111 | */ 112 | public function __construct($user) 113 | { 114 | $this->to = $user; 115 | $this->fromMailer = 'office@example.com' 116 | $this->locale('en'); 117 | } 118 | 119 | /** 120 | * Build the message. 121 | * 122 | * @return $this 123 | */ 124 | public function build() 125 | { 126 | return $this->markdown('emails.invitation') 127 | ->subject('Invitation mail'); 128 | } 129 | 130 | 131 | 132 | ### Bulk messages 133 | 134 | For bulk messages, you may first require a mailer object. You can define a pause in seconds ($timeout) after a number of mails ($frequency) has been send. 135 | 136 | $mailer = \MultiMail::getMailer('office@example.com' , $timeout, $frequency); 137 | 138 | Then you can iterate through your list. 139 | 140 | foreach($users as $user){ 141 | $mailer->send(new \App\Mail\Invitation($user)); 142 | }; 143 | 144 | 145 | ## Special Settings 146 | 147 | ### Multiple Mail Providers 148 | 149 | If you wish to send from mails with different provider, then you may create another provider in the `provider` array and reference it inside the `emails` array: 150 | 151 | 152 | 'emails' => [ 153 | 'office@example.net' => 154 | [ 155 | 'pass' => env('first_mail_password'), 156 | 'username' => env('first_mail_username'), 157 | 'from_name' => "Max Musterman", 158 | // <------ no provider given because 'default' provider is used 159 | ], 160 | 'contact@other_domain.net' => 161 | [ 162 | 'pass' => env('second_mail_password'), 163 | 'username' => env('second_mail_username'), 164 | 'from_name' => "Alice Armania", 165 | 'provider' => 'new_provider', // <------ specify new provider here 166 | ], 167 | ], 168 | 169 | 'provider' => [ 170 | 'default' => 171 | [ 172 | 'host' => env('MAIL_HOST'), 173 | 'port' => env('MAIL_PORT'), 174 | 'encryption' => env('MAIL_ENCRYPTION'), 175 | 'driver' => env('MAIL_DRIVER'), 176 | ], 177 | 'new_provider' => 178 | [ 179 | 'host' => env('MAIL_HOST_PROVIDER_B'), 180 | 'port' => env('MAIL_PORT_PROVIDER_B'), 181 | 'encryption' => env('MAIL_ENCRYPTION_PROVIDER_B'), 182 | 'driver' => env('MAIL_DRIVER_B'), 183 | // you may also add options like `stream`, `source_ip` and `local_domain` 184 | ]' 185 | ], 186 | 187 | 188 | 189 | ### Default mailaccount 190 | 191 | You may provide `default` credentials inside the `email` array from `config/multimail.php`: 192 | 193 | 'emails' => [ 194 | 'office@example.net' => 195 | [ 196 | 'pass' => env('first_mail_password'), 197 | 'username' => env('first_mail_username'), 198 | 'from_name' => "Max Musterman", 199 | ], 200 | 'contact@example.net' => 201 | [ 202 | 'pass' => env('second_mail_password'), 203 | 'username' => env('second_mail_username'), 204 | 'from_name' => "Alice Armania", 205 | ], 206 | 'default' => 207 | [ 208 | 'pass' => env('MAIL_PASSWORD'), 209 | 'username' => env('MAIL_USERNAME'), 210 | ] 211 | ], 212 | 213 | When `first_mail_password` and `first_mail_username` are empty, `office@example.net` will use credentials specified by `default`. This is useful for your local development, when you want to send all mails from one mailaccount while testing. This way you only need to specify `MAIL_PASSWORD` and `MAIL_USERNAME` locally. 214 | 215 | ## Testing 216 | 217 | #### Don't put credentials in local `env` 218 | 219 | Do not specify any email accounts in your local `.env`. Otherwise you may risk to send testing mails to actual users. 220 | 221 | #### Use one fake mail account or log 222 | 223 | Use `log` driver or setup a fake mail SMTP account like [mailtrap](https://mailtrap.io/) or similar services. 224 | 225 | It is not needed to specify the same credentials for all your email accounts. Instead, simply provide a default mail account (see above `Default mail account`). 226 | 227 | #### Use log mail driver on testing 228 | 229 | To avoid latency, I recommend to always use the `log` mail driver when `phpunit` is running. You can set the mail driver in your `phpunit.xml` file like this: ``. 230 | 231 | #### Use Mocking 232 | 233 | If you want to use the mocking feature [Mail fake](https://laravel.com/docs/mocking#mail-fake) during your tests, enable `use_default_mail_facade_in_tests` 234 | in your config file `config/multimail.php`. Note that `assertQueued` will never be true, because `queued` mails are actually send through `sent` through a job. 235 | 236 | ### Get Mail From Database 237 | 238 | If you want to load your mail account configuration from database 239 | publish the package migrations: 240 | 241 | php artisan vendor:publish --provider="IWasHereFirst2\LaravelMultiMail\MultiMailServiceProvider" --tag=migrations 242 | 243 | In your migration folder are now two tabels, email_accounts and email_providers 244 | 245 | Instead of adding emails to the config they should be added to the table email_accounts. 246 | 247 | Make sure to update your config `config/multimail.php`: 248 | 249 | \IWasHereFirst2\LaravelMultiMail\DatabaseConfigMailSettings::class, 254 | 255 | //... 256 | ]; 257 | 258 | The emails will now be read from the database instead from the configuration file. 259 | If no provider is provided in email_accounts (column provider is nullable), 260 | then the default profider from `config/multimail.php` will be considerd. 261 | 262 | If you want to make customizations, copy the class `\IWasHereFirst2\LaravelMultiMail\DatabaseConfigMailSettings` 263 | somewhere in your application, adjust the namespace, and update the reference `mail_settings_class` in your config file. 264 | 265 | ## Troubleshoot 266 | 267 | #### Laravel 7 is not working 268 | 269 | Please update to version 1.2.2 to support Laravel 7 270 | 271 | ## For Package Developer 272 | 273 | If you plan to contribute to this package, please make sure that the unit tests aswell as the integration tests 274 | all succeed. In order to test the integration tests please create a free mailtraip account, copy `tests/.env.example` 275 | to `tests/.env` and add your mailtrap API credentials in `tests/.env`. The integration tests will now send 276 | a test email to your mailtrap account and verify through the API if the mail was successfully send. 277 | 278 | The package ships with a Dockerfile to make it easy to run the tests for you. Simply follow these steps: 279 | 280 | docker-compose up --build 281 | docker-compose exec app bash 282 | composer install 283 | ./vendor/bin/phpunit 284 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iwasherefirst2/laravel-multimail", 3 | "description": "A package to send mails easily from multiple mail accounts with Laravel", 4 | "keywords": ["multiple providers", "laravel", "emails"], 5 | "license": "MIT", 6 | "homepage": "https://github.com/iwasherefirst2/Laravel-MultiMail", 7 | "authors": [ 8 | { 9 | "name": "Dr. Adam Nielsen", 10 | "email": "info@drnielsen.de" 11 | } 12 | ], 13 | "require": { 14 | "laravel/framework": "^5.0|^6.0|^7.0|^8.0" 15 | }, 16 | "require-dev": { 17 | "orchestra/testbench": "^4.0", 18 | "guzzlehttp/guzzle": "^6.4" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "IWasHereFirst2\\LaravelMultiMail\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "IWasHereFirst2\\LaravelMultiMail\\Tests\\": "tests/" 28 | } 29 | }, 30 | "minimum-stability": "stable", 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "IWasHereFirst2\\LaravelMultiMail\\MultiMailServiceProvider" 35 | ], 36 | "aliases":{ 37 | "MultiMail": "IWasHereFirst2\\LaravelMultiMail\\Facades\\MultiMail" 38 | } 39 | } 40 | }, 41 | "scripts": { 42 | "coverage-report": [ 43 | "vendor/bin/phpunit -d xdebug.profiler_enable=On --coverage-html tests/_report/" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | app: 4 | build: 5 | args: 6 | user: sammy 7 | uid: 1000 8 | context: ./ 9 | dockerfile: Dockerfile.dev 10 | image: mailable 11 | container_name: mailable-app 12 | working_dir: /var/www/ 13 | environment: 14 | - COMPOSER_MEMORY_LIMIT=-1 15 | depends_on: 16 | - db 17 | volumes: 18 | - ./:/var/www 19 | networks: 20 | - lahmi 21 | 22 | db: 23 | image: mysql:5.7 24 | container_name: mailable-db 25 | environment: 26 | MYSQL_DATABASE: ${DB_DATABASE} 27 | MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} 28 | MYSQL_PASSWORD: ${DB_PASSWORD} 29 | MYSQL_USER: ${DB_USERNAME} 30 | SERVICE_TAGS: dev 31 | SERVICE_NAME: mysql 32 | MYSQL_ROOT_PASSWORD: local 33 | volumes: 34 | - dbdata:/var/lib/mysql 35 | - ./docker-compose/mysql/my.cnf:/etc/mysql/my.cnf 36 | - ./docker-compose/mysql/init:/docker-entrypoint-initdb.d 37 | ports: 38 | - 3308:3306 39 | networks: 40 | - lahmi 41 | 42 | nginx: 43 | image: nginx:alpine 44 | container_name: mailable-nginx 45 | ports: 46 | - 8006:80 47 | depends_on: 48 | - db 49 | - app 50 | volumes: 51 | - ./:/var/www 52 | - ./docker-compose/nginx:/etc/nginx/conf.d/ 53 | networks: 54 | - lahmi 55 | 56 | networks: 57 | lahmi: 58 | driver: bridge 59 | 60 | volumes: 61 | dbdata: 62 | driver: local 63 | 64 | -------------------------------------------------------------------------------- /docker-compose/mysql/init/01-databases.sql: -------------------------------------------------------------------------------- 1 | # create databases 2 | CREATE DATABASE IF NOT EXISTS `local_laravel`; 3 | 4 | # create local_developer user and grant rights 5 | CREATE USER 'local_developer'@'db' IDENTIFIED BY 'secret'; 6 | GRANT ALL PRIVILEGES ON *.* TO 'local_developer'@'%'; 7 | 8 | -------------------------------------------------------------------------------- /docker-compose/mysql/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | general_log = 1 3 | general_log_file = /var/lib/mysql/general.log 4 | 5 | -------------------------------------------------------------------------------- /docker-compose/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | index index.php index.html; 4 | error_log /var/log/nginx/error.log; 5 | access_log /var/log/nginx/access.log; 6 | root /var/www/public; 7 | location ~ \.php$ { 8 | try_files $uri =404; 9 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 10 | fastcgi_pass app:9000; 11 | fastcgi_index index.php; 12 | include fastcgi_params; 13 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 14 | fastcgi_param PATH_INFO $fastcgi_path_info; 15 | } 16 | location / { 17 | try_files $uri $uri/ /index.php?$query_string; 18 | gzip_static on; 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/Unit 16 | 17 | 18 | 19 | ./tests/Integration 20 | 21 | 22 | 23 | 24 | ./src 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /publishable/config/multimail.php: -------------------------------------------------------------------------------- 1 | true, 13 | 14 | 'emails' => [ 15 | 'office@example.com' => [ 16 | 'pass' => env('MAIL_PASSWORD'), 17 | 'username' => env('MAIL_USERNAME'), 18 | 'from_name' => 'Max Musterman', 19 | 'reply_to_mail' => 'reply@example.com', 20 | ], 21 | 'contact@example.net' => [ 22 | 'pass' => env('second_mail_password'), 23 | ], 24 | ], 25 | 26 | 'provider' => [ 27 | 'default' => [ 28 | 'host' => env('MAIL_HOST'), 29 | 'port' => env('MAIL_PORT'), 30 | 'encryption' => env('MAIL_ENCRYPTION'), 31 | ], 32 | ], 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /src/DatabaseConfigMailSettings.php: -------------------------------------------------------------------------------- 1 | parseEmail($key); 43 | 44 | try { 45 | $this->account = EmailAccount::where('email', '=', $this->email)->firstOrFail(); 46 | } catch (\Exception $e) { 47 | throw new Exceptions\EmailNotInConfigException($this->email); 48 | } 49 | 50 | if (empty($this->name)) { 51 | $this->name = $this->account->from_name; 52 | } 53 | 54 | $this->loadProvider(); 55 | 56 | // If credentials are empty, load default values. 57 | // This makes local testing for many emails 58 | // very convenient. 59 | if ($this->isEmpty()) { 60 | $this->loadDefault(); 61 | } 62 | 63 | return $this; 64 | } 65 | /** 66 | * Check if log driver is used. 67 | * 68 | * @return boolean 69 | */ 70 | public function isLogDriver() 71 | { 72 | return (isset($this->provider['driver']) && $this->provider['driver'] == 'log'); 73 | } 74 | 75 | /** 76 | * Get provider. 77 | * 78 | * @return array 79 | */ 80 | public function getProvider() 81 | { 82 | return $this->provider; 83 | } 84 | 85 | /** 86 | * Get setting. 87 | * 88 | * @return array 89 | */ 90 | public function getSetting() 91 | { 92 | return $this->account->toArray(); 93 | } 94 | 95 | /** 96 | * Return email of sender. 97 | * 98 | * @return string 99 | */ 100 | public function getFromEmail() 101 | { 102 | return $this->account->from_mail ?? $this->email; 103 | } 104 | 105 | /** 106 | * Return name of sender. 107 | * 108 | * @return string 109 | */ 110 | public function getFromName() 111 | { 112 | return $this->name; 113 | } 114 | 115 | /** 116 | * Return email of sender. 117 | * 118 | * @return string 119 | */ 120 | public function getReplyEmail() 121 | { 122 | return $this->account->reply_to_mail; 123 | } 124 | 125 | /** 126 | * Return name of sender. 127 | * 128 | * @return string 129 | */ 130 | public function getReplyName() 131 | { 132 | return $this->account->reply_to_name; 133 | } 134 | 135 | public function getEmail() 136 | { 137 | return $this->email; 138 | } 139 | 140 | /** 141 | * Check if email, pass and username are not empty 142 | * 143 | * @return boolean 144 | */ 145 | private function isEmpty() 146 | { 147 | return (empty($this->email) || empty($this->account) || empty($this->account->pass) || empty($this->account->username)); 148 | } 149 | 150 | /** 151 | * Load default setting. If default setting is 152 | * invalid throw exception 153 | * 154 | * @return void 155 | */ 156 | private function loadDefault() 157 | { 158 | $this->account = (object) config('multimail.emails.default'); 159 | 160 | $this->loadProvider(); 161 | 162 | if ((!isset($this->provider['driver']) || $this->provider['driver'] != 'log') && (empty($this->acount->pass) || empty($this->account->username))) { 163 | throw new Exceptions\NoDefaultException($this->email); 164 | } 165 | } 166 | 167 | /** 168 | * Parse $key into email and possible from name 169 | * 170 | * @param mixed string/array 171 | * @return void 172 | */ 173 | private function parseEmail($key) 174 | { 175 | if (!is_array($key)) { 176 | $this->email = $key; 177 | return; 178 | } 179 | 180 | $this->name = $key['name'] ?? null; 181 | 182 | if (empty($key['email'])) { 183 | throw new Exceptions\InvalidConfigKeyException; 184 | } 185 | 186 | $this->email = $key['email']; 187 | } 188 | 189 | 190 | private function loadProvider() 191 | { 192 | 193 | if (!empty($this->account->provider)) { 194 | $this->provider = $this->account->provider->toArray(); 195 | } 196 | 197 | if (empty($this->provider)) { 198 | $this->provider = config('multimail.provider.default'); 199 | } 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/Exceptions/EmailNotInConfigException.php: -------------------------------------------------------------------------------- 1 | message = 'Email ' . $mail . ' not found in config/multimail.php!'; 10 | parent::__construct(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidConfigKeyException.php: -------------------------------------------------------------------------------- 1 | message = "Mailer name has to be provided in array as column 'email'"; 10 | parent::__construct(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Exceptions/NoDefaultException.php: -------------------------------------------------------------------------------- 1 | message = 'Username or password for ' . $mail . ' is missing in config/multimail.php and no default specified!'; 10 | parent::__construct(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Exceptions/NotInitializedException.php: -------------------------------------------------------------------------------- 1 | message = 'Please call loadConfiguration($key) before anything else.'; 10 | parent::__construct(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Facades/MultiMail.php: -------------------------------------------------------------------------------- 1 | parseEmail($key); 43 | 44 | try { 45 | $this->settings = config('multimail.emails')[$this->email]; 46 | } catch (\Exception $e) { 47 | throw new Exceptions\EmailNotInConfigException($this->email); 48 | } 49 | 50 | if (empty($this->name)) { 51 | $this->name = $this->settings['from_name'] ?? null; 52 | } 53 | 54 | $this->loadProvider(); 55 | 56 | // If credentials are empty, load default values. 57 | // This makes local testing for many emails 58 | // very convenient. 59 | if ($this->isEmpty()) { 60 | $this->loadDefault(); 61 | } 62 | 63 | return $this; 64 | } 65 | /** 66 | * Check if log driver is used. 67 | * 68 | * @return boolean 69 | */ 70 | public function isLogDriver() 71 | { 72 | return (isset($this->provider['driver']) && $this->provider['driver'] == 'log'); 73 | } 74 | 75 | /** 76 | * Get provider. 77 | * 78 | * @return array 79 | */ 80 | public function getProvider() 81 | { 82 | return $this->provider; 83 | } 84 | 85 | /** 86 | * Get setting. 87 | * 88 | * @return array 89 | */ 90 | public function getSetting() 91 | { 92 | return $this->settings; 93 | } 94 | 95 | /** 96 | * Return email of sender. 97 | * 98 | * @return string 99 | */ 100 | public function getFromEmail() 101 | { 102 | return $this->settings['from_mail'] ?? $this->email; 103 | } 104 | 105 | /** 106 | * Return name of sender. 107 | * 108 | * @return string 109 | */ 110 | public function getFromName() 111 | { 112 | return $this->name; 113 | } 114 | 115 | /** 116 | * Return email of sender. 117 | * 118 | * @return string 119 | */ 120 | public function getReplyEmail() 121 | { 122 | return $this->settings['reply_to_mail'] ?? null; 123 | } 124 | 125 | /** 126 | * Return name of sender. 127 | * 128 | * @return string 129 | */ 130 | public function getReplyName() 131 | { 132 | return $this->settings['reply_to_name'] ?? null; 133 | } 134 | 135 | public function getEmail() 136 | { 137 | return $this->email; 138 | } 139 | 140 | /** 141 | * Check if email, pass and username are not empty 142 | * 143 | * @return boolean 144 | */ 145 | private function isEmpty() 146 | { 147 | return (empty($this->email) || empty($this->settings) || empty($this->settings['pass']) || empty($this->settings['username'])); 148 | } 149 | 150 | /** 151 | * Load default setting. If default setting is 152 | * invalid throw exception 153 | * 154 | * @return void 155 | */ 156 | private function loadDefault() 157 | { 158 | $this->settings = config('multimail.emails.default'); 159 | 160 | $this->loadProvider(); 161 | 162 | if ((!isset($this->provider['driver']) || $this->provider['driver'] != 'log') && (empty($this->settings['pass']) || empty($this->settings['username']))) { 163 | throw new Exceptions\NoDefaultException($this->email); 164 | } 165 | } 166 | 167 | /** 168 | * Parse $key into email and possible from name 169 | * 170 | * @param mixed string/array 171 | * @return void 172 | */ 173 | private function parseEmail($key) 174 | { 175 | if (!is_array($key)) { 176 | $this->email = $key; 177 | return; 178 | } 179 | 180 | $this->name = $key['name'] ?? null; 181 | 182 | if (empty($key['email'])) { 183 | throw new Exceptions\InvalidConfigKeyException; 184 | } 185 | 186 | $this->email = $key['email']; 187 | } 188 | 189 | 190 | private function loadProvider() 191 | { 192 | if (!empty($this->settings['provider'])) { 193 | $this->provider = config('multimail.provider.' . $this->settings['provider']); 194 | } 195 | 196 | if (empty($this->provider)) { 197 | $this->provider = config('multimail.provider.default'); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Jobs/SendMailJob.php: -------------------------------------------------------------------------------- 1 | mailer_name = $mailer_name; 28 | $this->mailable = $mailable; 29 | $this->fromName = $fromName; 30 | } 31 | 32 | /** 33 | * Send Email out. This is a queue workaround 34 | * 35 | * @return void 36 | */ 37 | public function handle() 38 | { 39 | MultiMail::sendMail($this->mailable, $this->mailer_name, $this->fromName); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/MailSettings.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('host'); 19 | $table->string('port'); 20 | $table->string('encryption'); 21 | $table->string('driver')->default('smtp'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('email_providers'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Migrations/2021_04_10_141626_create_email_accounts_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('email'); 19 | $table->string('pass'); 20 | $table->string('username')->nullable(); 21 | $table->string('from_name')->nullable(); 22 | $table->string('reply_to_mail')->nullable(); 23 | $table->string('reply_to_name')->nullable(); 24 | $table->bigInteger('provider_id')->unsigned()->nullable(); 25 | $table->timestamps(); 26 | 27 | $table->foreign('provider_id')->references('id')->on('email_providers'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('email_accounts'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Models/EmailAccount.php: -------------------------------------------------------------------------------- 1 | belongsTo(EmailProvider::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Models/EmailProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('iwasherefirst2-laravelmultimail', function () { 26 | if(config()->has('multimail.mail_settings_class')){ 27 | $configClass = config('multimail.mail_settings_class'); 28 | $config = new $configClass(); 29 | } 30 | else{ 31 | $config = new FileConfigMailSettings(); 32 | } 33 | 34 | return new MultiMailer($config); 35 | }); 36 | 37 | $this->publishes([ 38 | dirname(__DIR__) . '/publishable/config/multimail.php' => config_path('multimail.php'), 39 | ], 'config'); 40 | 41 | 42 | $this->publishes([ 43 | __DIR__.'/Migrations/' => database_path('migrations') 44 | ], 'migrations'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/MultiMailer.php: -------------------------------------------------------------------------------- 1 | config = $config; 40 | } 41 | 42 | /** 43 | * Create mailer from config/multimail.php 44 | * If its not a log driver, add AntiFloodPlugin. 45 | * 46 | * @param mixed $key string or array 47 | * @param int timeout 48 | * @param int frequency 49 | * @return \Illuminate\Mail\Mailer 50 | */ 51 | public function getMailer($key, $timeout = null, $frequency = null, $fromName = null) 52 | { 53 | $config = $this->config->initialize($key); 54 | 55 | if (isset($this->mailers[$config->getEmail()])) { 56 | return $this->mailers[$config->getEmail()]; 57 | } 58 | 59 | $swift_mailer = $this->getSwiftMailer($config); 60 | 61 | if (!$config->isLogDriver() && !empty($timeout) && !empty($frequency)) { 62 | $this->plugins[] = new Swift_Plugins_AntiFloodPlugin($frequency, $timeout); 63 | } 64 | 65 | $this->registerPlugins($swift_mailer); 66 | 67 | $view = app()->get('view'); 68 | $events = app()->get('events'); 69 | 70 | if (version_compare(app()->version(), '7.0.0') >= 0) { 71 | $mailer = new Mailer(config('app.name'), $view, $swift_mailer, $events); 72 | } else { 73 | $mailer = new Mailer($view, $swift_mailer, $events); 74 | } 75 | 76 | $mailer->alwaysFrom($config->getFromEmail(), $fromName ?? $config->getFromName()); 77 | 78 | if (!empty($reply_mail = $config->getReplyEmail())) { 79 | $mailer->alwaysReplyTo($reply_mail, $config->getReplyEmail()); 80 | } 81 | 82 | $this->mailers[$config->getEmail()] = $mailer; 83 | 84 | return $mailer; 85 | } 86 | 87 | /** 88 | * Send mail throug mail account form $mailer_name 89 | * @param MailableContract $mailable 90 | * @param [type] $mailer_name ] 91 | * @return [type] [description] 92 | */ 93 | public function sendMail(MailableContract $mailable, $mailer_name, $fromName) 94 | { 95 | if (\App::runningUnitTests() && config('multimail.use_default_mail_facade_in_tests')) { 96 | return \Mail::send($mailable); 97 | } 98 | 99 | if (empty($mailer_name)) { 100 | return \Mail::send($mailable); 101 | } 102 | $mailer = $this->getMailer($mailer_name, null, null, $fromName); 103 | 104 | $mailable->send($mailer); 105 | } 106 | 107 | /** 108 | * [registerPlugin description] 109 | * @param [type] $plugin [description] 110 | * @return [type] [description] 111 | */ 112 | public function registerPlugin($plugin) 113 | { 114 | $this->plugins[] = $plugin; 115 | 116 | // clear stored mailers, they may need new plugins. 117 | $this->mailers = []; 118 | } 119 | 120 | /** 121 | * [registerPlugin description] 122 | * @param [type] $plugin [description] 123 | * @return [type] [description] 124 | */ 125 | public function clearPlugins() 126 | { 127 | $this->plugins = []; 128 | 129 | // clear stored mailers, they may need new plugins. 130 | $this->mailers = []; 131 | } 132 | 133 | public function getPlugins() 134 | { 135 | return $this->plugins; 136 | } 137 | 138 | public function queueMail(MailableContract $mailable, $mailer_name, $fromName) 139 | { 140 | // no mailer given, use default mailer 141 | if (empty($mailer_name)) { 142 | return \Mail::queue($mailable); 143 | } 144 | 145 | Jobs\SendMailJob::dispatch($mailer_name, $mailable, $fromName)->onQueue($mailable->queue ?? null); 146 | } 147 | 148 | /** 149 | * Begin the process of mailing a mailable class instance. 150 | * 151 | * @param mixed $users 152 | * @return \IWasHereFirst2\MultiMail\PendingMail 153 | */ 154 | public function to($users) 155 | { 156 | return (new PendingMail())->to($users); 157 | } 158 | 159 | /** 160 | * Begin the process of mailing a mailable class instance. 161 | * 162 | * @param mixed $users 163 | * @return \IWasHereFirst2\MultiMail\PendingMail 164 | */ 165 | public function from(string $mailerKey, $fromName = null) 166 | { 167 | return (new PendingMail())->from($mailerKey, $fromName); 168 | } 169 | 170 | /** 171 | * Begin the process of mailing a mailable class instance. 172 | * 173 | * @param mixed $users 174 | * @return \IWasHereFirst2\MultiMail\PendingMail 175 | */ 176 | public function cc($users) 177 | { 178 | return (new PendingMail())->cc($users); 179 | } 180 | 181 | /** 182 | * Begin the process of mailing a mailable class instance. 183 | * 184 | * @param mixed $users 185 | * @return \IWasHereFirst2\MultiMail\PendingMail 186 | */ 187 | public function bcc($users) 188 | { 189 | return (new PendingMail())->bcc($users); 190 | } 191 | 192 | /** 193 | * Begin the process of mailing a mailable class instance. 194 | * 195 | * @param string locale 2 char 196 | * @return \IWasHereFirst2\MultiMail\PendingMail 197 | */ 198 | public function locale($locale) 199 | { 200 | return (new PendingMail())->locale($locale); 201 | } 202 | 203 | /** 204 | * Send mail or queue if implements ShouldQueue 205 | * 206 | * @param Mailable 207 | * @return \IWasHereFirst2\MultiMail\MultiMailer 208 | */ 209 | public function send(Mailable $mailable) 210 | { 211 | return (new PendingMail())->send($mailable); 212 | } 213 | 214 | /** 215 | * Queue mail 216 | * 217 | * @param Mailable 218 | * @return \IWasHereFirst2\MultiMail\MultiMailer 219 | */ 220 | public function queue(Mailable $mailable) 221 | { 222 | return (new PendingMail())->queue($mailable); 223 | } 224 | 225 | /** 226 | * Create SwiftMailer with timeout/frequency. Timeout/frequency is ignored 227 | * when Log Driver is used. 228 | * 229 | * @param array 230 | * @return Swift_Mailer 231 | */ 232 | protected function getSwiftMailer($config) 233 | { 234 | if ($config->isLogDriver()) { 235 | $transport = TransportManager::createLogDriver(); 236 | 237 | return new Swift_Mailer($transport); 238 | } 239 | 240 | $transport = TransportManager::createSmtpDriver($config); 241 | 242 | return new Swift_Mailer($transport); 243 | } 244 | 245 | protected function registerPlugins($swift_mailer) 246 | { 247 | if (!empty($this->plugins)) { 248 | foreach ($this->plugins as $plugin) { 249 | $swift_mailer->registerPlugin($plugin); 250 | } 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/PendingMail.php: -------------------------------------------------------------------------------- 1 | .., 'email' => ..., 'reply_to'] 13 | * 14 | * @var string 15 | */ 16 | protected $fromMailer; 17 | 18 | /** 19 | * The locale of the message. 20 | * 21 | * @var array 22 | */ 23 | protected $locale; 24 | 25 | /** 26 | * The "to" recipients of the message. 27 | * 28 | * @var array 29 | */ 30 | protected $to = []; 31 | 32 | /** 33 | * The "cc" recipients of the message. 34 | * 35 | * @var array 36 | */ 37 | protected $cc = []; 38 | 39 | /** 40 | * The "bcc" recipients of the message. 41 | * 42 | * @var array 43 | */ 44 | protected $bcc = []; 45 | 46 | /** 47 | * Overrwrites name of from mail cofnig 48 | * 49 | * @var ?string 50 | */ 51 | protected $fromName = null; 52 | 53 | /** 54 | * Set the locale of the message. 55 | * 56 | * @param string $locale 57 | * @return $this 58 | */ 59 | public function locale($locale) 60 | { 61 | $this->locale = $locale; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Set the recipients of the message. 68 | * 69 | * @param mixed $users 70 | * @return $this 71 | */ 72 | public function to($users) 73 | { 74 | $this->to = $users; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Determine from mailer. 81 | * 82 | * @param string mailer name 83 | * @return $this 84 | */ 85 | public function from($mailerKey, $fromName = null) 86 | { 87 | $this->fromName = $fromName; 88 | $this->fromMailer = $mailerKey; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Set the addtional recipients of the message. 95 | * 96 | * @param mixed $users 97 | * @return $this 98 | */ 99 | public function cc($users) 100 | { 101 | $this->cc = $users; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Set the hidden recipients of the message. 108 | * 109 | * @param mixed $users 110 | * @return $this 111 | */ 112 | public function bcc($users) 113 | { 114 | $this->bcc = $users; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * Send a new mailable message instance. 121 | * 122 | * @param \Illuminate\Mail\Mailable $mailable 123 | * @return mixed 124 | */ 125 | public function send(Mailable $mailable) 126 | { 127 | if ($mailable instanceof ShouldQueue) { 128 | return $this->queue($mailable); 129 | } 130 | 131 | return $this->sendNow($this->fill($mailable)); 132 | } 133 | 134 | /** 135 | * Send a mailable message immediately. 136 | * 137 | * @param \Illuminate\Mail\Mailable $mailable 138 | * @return mixed 139 | */ 140 | public function sendNow(Mailable $mailable) 141 | { 142 | $mailer = $this->fromMailer ?? optional($mailable)->fromMailer; 143 | 144 | return MultiMail::sendMail($this->fill($mailable), $mailer, $this->fromName); 145 | } 146 | 147 | /** 148 | * Push the given mailable onto the queue. 149 | * 150 | * @param \Illuminate\Mail\Mailable $mailable 151 | * @return mixed 152 | */ 153 | public function queue(Mailable $mailable) 154 | { 155 | $mailer = $this->fromMailer ?? optional($mailable)->fromMailer; 156 | 157 | return MultiMail::queueMail($this->fill($mailable), $mailer, $this->fromName); 158 | } 159 | 160 | /** 161 | * Populate the mailable with the addresses. 162 | * 163 | * @param \Illuminate\Mail\Mailable $mailable 164 | * @return \Illuminate\Mail\Mailable 165 | */ 166 | protected function fill(Mailable $mailable) 167 | { 168 | if (!empty($this->locale)) { 169 | $mailable->locale($this->locale); 170 | } 171 | 172 | return $mailable->to($this->to) 173 | ->cc($this->cc) 174 | ->bcc($this->bcc); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/TransportManager.php: -------------------------------------------------------------------------------- 1 | getProvider(); 20 | $setting = $config->getSetting(); 21 | 22 | $transport = new Swift_SmtpTransport($provider['host'], $provider['port'], $provider['encryption']); 23 | $transport->setUsername($setting['username'] ?? $config->getEmail()); 24 | $transport->setPassword($setting['pass']); 25 | 26 | return self::configureSmtpDriver($transport, $provider); 27 | } 28 | 29 | /** 30 | * Configure the additional SMTP driver options. 31 | * 32 | * @param \Swift_SmtpTransport $transport 33 | * @param array $config 34 | * @return \Swift_SmtpTransport 35 | */ 36 | protected static function configureSmtpDriver($transport, $config) 37 | { 38 | if (isset($config['stream'])) { 39 | $transport->setStreamOptions($config['stream']); 40 | } 41 | 42 | if (isset($config['source_ip'])) { 43 | $transport->setSourceIp($config['source_ip']); 44 | } 45 | 46 | if (isset($config['local_domain'])) { 47 | $transport->setLocalDomain($config['local_domain']); 48 | } 49 | 50 | return $transport; 51 | } 52 | 53 | /** 54 | * Create LOG Transport. 55 | * 56 | * @return LogTransport 57 | */ 58 | public static function createLogDriver() 59 | { 60 | return new LogTransport(app()->make(LoggerInterface::class)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/.env.example: -------------------------------------------------------------------------------- 1 | MAIL_DRIVER_SMTP=smtp 2 | MAIL_HOST_SMTP=smtp.mailtrap.io 3 | MAIL_PORT_SMTP=2525 4 | 5 | MAIL_USERNAME_SMTP= 6 | MAIL_PASSWORD_SMTP= 7 | MAIL_ENCRYPTION_SMTP=TLS 8 | 9 | MAILTRAP_API_KEY = 10 | MAILTRAP_INBOX_ID = 11 | -------------------------------------------------------------------------------- /tests/Fixtures/view.blade.php: -------------------------------------------------------------------------------- 1 | {{__('nom')}} 2 | -------------------------------------------------------------------------------- /tests/Integration/MultiMailDatabaseTest.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(realpath(__DIR__ . '/../../src/Migrations')); 23 | 24 | $this->artisan('migrate',['--database' => 'testbench']) 25 | ->run(); 26 | 27 | $provider = EmailProvider::create([ 28 | 'host' => env('MAIL_HOST_SMTP'), 29 | 'port' => env('MAIL_PORT_SMTP'), 30 | 'encryption' => env('MAIL_ENCRYPTION_SMTP'), 31 | 'driver' => env('MAIL_DRIVER_SMTP'), 32 | ]); 33 | 34 | EmailAccount::create([ 35 | 'email' => static::FROM, 36 | 'pass' => env('MAIL_PASSWORD_SMTP'), 37 | 'username' => env('MAIL_USERNAME_SMTP'), 38 | 'from_name' => 'Rayn Roogen', 39 | 'provider_id' => $provider->id 40 | ]); 41 | 42 | $to = 'foo-fighter@foo.com'; 43 | $locale = 'de'; 44 | $from = static::FROM; 45 | $bcc = ['oki@foo.berlin', 'rooky@mooky.de']; 46 | 47 | MultiMail::to($to) 48 | ->locale($locale) 49 | ->from($from) 50 | ->bcc($bcc) 51 | ->send(new TestMail()); 52 | 53 | $message = $this->findMessage('TestMail Subject'); 54 | 55 | $this->assertEquals('Rayn Roogen', $message[0]['from_name']); 56 | $this->emptyInbox(); 57 | } 58 | 59 | /** 60 | * Define environment setup. 61 | * 62 | * @param \Illuminate\Foundation\Application $app 63 | * @return void 64 | */ 65 | protected function getEnvironmentSetUp($app) 66 | { 67 | // Setup default database to use sqlite :memory: 68 | $app['config']->set('database.default', 'testbench'); 69 | $app['config']->set('database.connections.testbench', [ 70 | 'driver' => 'sqlite', 71 | 'database' => ':memory:', 72 | 'prefix' => '', 73 | ]); 74 | 75 | $app->useEnvironmentPath(__DIR__ . '/..'); 76 | $app->bootstrapWith([LoadEnvironmentVariables::class]); 77 | 78 | $app['config']->set('multimail.mail_settings_class', 79 | DatabaseConfigMailSettings::class); 80 | 81 | $app['config']->set('multimail.provider.default', [ 82 | 'driver' => 'log', 83 | ]); 84 | 85 | $app['config']->set('mail.driver', 'log'); 86 | 87 | View::addLocation(__DIR__ . '/../Fixtures'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Integration/MultiMailTest.php: -------------------------------------------------------------------------------- 1 | cc($cc) 26 | ->locale($locale) 27 | ->from($from) 28 | ->bcc($bcc) 29 | ->send(new TestMail()); 30 | 31 | $this->assertNotNull($this->emails); 32 | $this->assertEquals(1, count($this->emails)); 33 | } 34 | 35 | /** @test */ 36 | public function check_if_from_name_works() 37 | { 38 | MultiMail::clearPlugins(); 39 | 40 | MultiMail::to('test@bar.com')->from(['name' => 'Adam', 'email' => 'test@fake.de'])->send(new TestMail()); 41 | 42 | $this->assertEquals([], MultiMail::getPlugins()); 43 | } 44 | 45 | /** @test */ 46 | public function send_mail_directly() 47 | { 48 | MultiMail::send(new TestMailIncludingFrom()); 49 | 50 | $this->assertNotNull($this->emails); 51 | $this->assertEquals(1, count($this->emails)); 52 | } 53 | 54 | /** @test */ 55 | public function send_mail_implementing_queue() 56 | { 57 | MultiMail::send(new QueueTestMailIncludingFrom()); 58 | 59 | $this->assertNotNull($this->emails); 60 | $this->assertEquals(1, count($this->emails)); 61 | } 62 | 63 | /** @test */ 64 | public function send_mail_through_queue() 65 | { 66 | MultiMail::queue(new TestMailIncludingFrom()); 67 | 68 | $this->assertNotNull($this->emails); 69 | $this->assertEquals(1, count($this->emails)); 70 | } 71 | 72 | /** @test */ 73 | public function send_mail_through_default() 74 | { 75 | MultiMail::send(new TestMail()); 76 | 77 | $this->assertNotNull($this->emails); 78 | $this->assertEquals(1, count($this->emails)); 79 | } 80 | 81 | /** @test */ 82 | public function send_mail_through_queue_default() 83 | { 84 | MultiMail::queue(new TestMail()); 85 | 86 | $this->assertNotNull($this->emails); 87 | $this->assertEquals(1, count($this->emails)); 88 | } 89 | 90 | /** 91 | * Define environment setup. 92 | * 93 | * @param \Illuminate\Foundation\Application $app 94 | * @return void 95 | */ 96 | protected function getEnvironmentSetUp($app) 97 | { 98 | $app['config']->set('multimail.emails', 99 | ['test@fake.de' => [ 100 | 'pass' => 'fakepass', 101 | 'username' => 'fakeusername', 102 | 'from' => 'Who knows', 103 | 'reply_to_mail' => 'bs@web.de', 104 | ]]); 105 | 106 | $app['config']->set('multimail.provider.default', [ 107 | 'driver' => 'log', 108 | ]); 109 | 110 | $app['config']->set('mail.driver', 'log'); 111 | 112 | View::addLocation(__DIR__ . '/../Fixtures'); 113 | } 114 | } 115 | 116 | class TestMailIncludingFrom extends Mailable 117 | { 118 | public function __construct() 119 | { 120 | $this->fromMailer = MultiMailTest::FROM; 121 | } 122 | 123 | /** 124 | * Build the message. 125 | * 126 | * @return $this 127 | */ 128 | public function build() 129 | { 130 | $this->subject = 'TestMail Subject'; 131 | return $this->view('view'); 132 | } 133 | } 134 | 135 | class QueueTestMailIncludingFrom extends Mailable implements ShouldQueue 136 | { 137 | public function __construct() 138 | { 139 | $this->fromMailer = MultiMailTest::FROM; 140 | } 141 | 142 | /** 143 | * Build the message. 144 | * 145 | * @return $this 146 | */ 147 | public function build() 148 | { 149 | $this->subject = 'TestMail Subject'; 150 | return $this->view('view'); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/Integration/SMPTTest.php: -------------------------------------------------------------------------------- 1 | locale($locale) 27 | ->from($from) 28 | ->send(new TestMail()); 29 | 30 | $this->assertTrue($this->messageExists('TestMail Subject')); 31 | 32 | $this->emptyInbox(); 33 | } 34 | 35 | /** @test */ 36 | public function check_smtp_mail_from_name() 37 | { 38 | $to = 'test@bar.com'; 39 | $locale = 'de'; 40 | $from = static::FROM; 41 | MultiMail::to($to) 42 | ->locale($locale) 43 | ->from($from) 44 | ->send(new TestMail()); 45 | 46 | $message = $this->findMessage('TestMail Subject'); 47 | 48 | $this->assertEquals('Adam Nielsen', $message[0]['from_name']); 49 | $this->emptyInbox(); 50 | } 51 | 52 | /** @test */ 53 | public function check_if_smtp_mail_can_configure_from() 54 | { 55 | $to = 'test@bar.com'; 56 | $locale = 'de'; 57 | $from = static::FROM; 58 | MultiMail::to($to) 59 | ->locale($locale) 60 | ->from($from, 'Backarony Mockoli') 61 | ->send(new TestMail()); 62 | 63 | $message = $this->findMessage('TestMail Subject'); 64 | 65 | $this->assertEquals('Backarony Mockoli', $message[0]['from_name']); 66 | $this->emptyInbox(); 67 | } 68 | 69 | /** @test */ 70 | public function get_mailer_with_antifloodplugin() 71 | { 72 | $mailer = MultiMail::getMailer(static::FROM, 1, 1); 73 | 74 | $this->assertEquals(2, count(MultiMail::getPlugins())); 75 | } 76 | 77 | /** 78 | * Define environment setup. 79 | * 80 | * @param \Illuminate\Foundation\Application $app 81 | * @return void 82 | */ 83 | protected function getEnvironmentSetUp($app) 84 | { 85 | $app->useEnvironmentPath(__DIR__ . '/..'); 86 | $app->bootstrapWith([LoadEnvironmentVariables::class]); 87 | 88 | parent::getEnvironmentSetUp($app); 89 | 90 | // Setup default database to use sqlite :memory: 91 | $app['config']->set('multimail.emails', 92 | ['smtp@fake.de' => [ 93 | 'pass' => env('MAIL_PASSWORD_SMTP'), 94 | 'username' => env('MAIL_USERNAME_SMTP'), 95 | 'from_name' => 'Adam Nielsen', 96 | 'provider' => 'smtp', 97 | ]]); 98 | 99 | $app['config']->set('multimail.provider.smtp', [ 100 | 'host' => env('MAIL_HOST_SMTP'), 101 | 'port' => env('MAIL_PORT_SMTP'), 102 | 'encryption' => env('MAIL_ENCRYPTION_SMTP'), 103 | 'driver' => env('MAIL_DRIVER_SMTP'), 104 | ]); 105 | 106 | View::addLocation(__DIR__ . '/../Fixtures'); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Integration/TestMail.php: -------------------------------------------------------------------------------- 1 | subject = 'TestMail Subject'; 17 | return $this->view('view'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | registerPlugin(new TestingMailEventListener($this)); 23 | } 24 | 25 | public function addEmail(Swift_Message $email) 26 | { 27 | $this->emails[] = $email; 28 | } 29 | 30 | /** 31 | * add the package provider 32 | * 33 | * @param $app 34 | * @return array 35 | */ 36 | protected function getPackageProviders($app) 37 | { 38 | return [MultiMailServiceProvider::class]; 39 | } 40 | } 41 | 42 | class TestingMailEventListener implements Swift_Events_EventListener 43 | { 44 | protected $test; 45 | 46 | public function __construct($test) 47 | { 48 | $this->test = $test; 49 | } 50 | 51 | public function getDebugInfo() 52 | { 53 | return 'This is the Custom Test Case Event Plugin'; 54 | } 55 | 56 | public function beforeSendPerformed($event) 57 | { 58 | $this->test->addEmail($event->getMessage()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Traits/MailTrap.php: -------------------------------------------------------------------------------- 1 | 9 | * Permission is hereby granted, free of charge, to any person 10 | * obtaining a copy of this software and associated documentation 11 | * files (the "Software"), to deal in the Software without 12 | * restriction, including without limitation the rights to use, 13 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | * copies of the Software, and to permit persons to whom the 15 | * Software is furnished to do so, subject to the following 16 | * conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be 19 | * included in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | * OTHER DEALINGS IN THE SOFTWARE. 29 | */ 30 | 31 | namespace IWasHereFirst2\LaravelMultiMail\Tests\Traits; 32 | 33 | use \Exception; 34 | use GuzzleHttp\Client; 35 | use Illuminate\Support\Facades\Config; 36 | 37 | trait MailTrap 38 | { 39 | /** 40 | * The MailTrap configuration. 41 | * 42 | * @var integer 43 | */ 44 | protected $mailTrapInboxId; 45 | 46 | /** 47 | * The MailTrap API Key. 48 | * 49 | * @var string 50 | */ 51 | protected $mailTrapApiKey; 52 | 53 | /** 54 | * The Guzzle client. 55 | * 56 | * @var Client 57 | */ 58 | protected $client; 59 | 60 | /** 61 | * 62 | * Empty the MailTrap inbox. 63 | * 64 | * @AfterScenario @mail 65 | */ 66 | public function emptyInbox() 67 | { 68 | $this->requestClient()->patch($this->getMailTrapCleanUrl()); 69 | } 70 | 71 | /** 72 | * Get the configuration for MailTrap. 73 | * 74 | * @param integer|null $inboxId 75 | * @throws Exception 76 | */ 77 | protected function applyMailTrapConfiguration($inboxId = null) 78 | { 79 | $this->mailTrapInboxId = env('MAILTRAP_INBOX_ID'); 80 | $this->mailTrapApiKey = env('MAILTRAP_API_KEY'); 81 | } 82 | 83 | /** 84 | * Fetch a MailTrap inbox. 85 | * 86 | * @param integer|null $inboxId 87 | * @return mixed 88 | * @throws RuntimeException 89 | */ 90 | protected function fetchInbox($inboxId = null) 91 | { 92 | if (! $this->alreadyConfigured()) { 93 | $this->applyMailTrapConfiguration($inboxId); 94 | } 95 | 96 | $body = $this->requestClient() 97 | ->get($this->getMailTrapMessagesUrl()) 98 | ->getBody(); 99 | 100 | return $this->parseJson($body); 101 | } 102 | 103 | /** 104 | * Get the MailTrap messages endpoint. 105 | * 106 | * @return string 107 | */ 108 | protected function getMailTrapMessagesUrl() 109 | { 110 | return "/api/v1/inboxes/{$this->mailTrapInboxId}/messages"; 111 | } 112 | 113 | /** 114 | * Get the MailTrap "empty inbox" endpoint. 115 | * 116 | * @return string 117 | */ 118 | protected function getMailTrapCleanUrl() 119 | { 120 | return "/api/v1/inboxes/{$this->mailTrapInboxId}/clean"; 121 | } 122 | 123 | /** 124 | * Determine if MailTrap config has been retrieved yet. 125 | * 126 | * @return boolean 127 | */ 128 | protected function alreadyConfigured() 129 | { 130 | return $this->mailTrapApiKey; 131 | } 132 | 133 | /** 134 | * Request a new Guzzle client. 135 | * 136 | * @return Client 137 | */ 138 | protected function requestClient() 139 | { 140 | if (! $this->client) { 141 | $this->client = new Client([ 142 | 'base_uri' => 'https://mailtrap.io', 143 | 'headers' => ['Api-Token' => $this->mailTrapApiKey], 144 | ]); 145 | } 146 | 147 | return $this->client; 148 | } 149 | 150 | /** 151 | * @param $body 152 | * @return array|mixed 153 | * @throws RuntimeException 154 | */ 155 | protected function parseJson($body) 156 | { 157 | $data = json_decode((string) $body, true); 158 | 159 | if (JSON_ERROR_NONE !== json_last_error()) { 160 | throw new RuntimeException('Unable to parse response body into JSON: ' . json_last_error()); 161 | } 162 | 163 | return $data === null ? [] : $data; 164 | } 165 | 166 | /** 167 | * Search Messages Url 168 | * 169 | * @param string $query Search query 170 | * @return string 171 | */ 172 | protected function searchInboxMessagesUrl($query) 173 | { 174 | return "/api/v1/inboxes/{$this->mailTrapInboxId}/messages?search=" . $query; 175 | } 176 | 177 | /** 178 | * Find and fetch a Message By Query. 179 | * 180 | * @param integer $query Query 181 | * @return mixed 182 | * @throws RuntimeException 183 | */ 184 | protected function findMessage($query) 185 | { 186 | if (! $this->alreadyConfigured()) { 187 | $this->applyMailTrapConfiguration(); 188 | } 189 | 190 | $counter = 0; 191 | 192 | do{ 193 | $body = $this->requestClient() 194 | ->get($this->searchInboxMessagesUrl($query)) 195 | ->getBody(); 196 | 197 | $body = $this->parseJson($body); 198 | $counter++; 199 | 200 | if($counter >= 50){ 201 | return $body; 202 | } 203 | }while(empty($body)); 204 | 205 | return $body; 206 | } 207 | 208 | /** 209 | * Check if a message exists based on a string query. 210 | * 211 | * @param string $query Query string 212 | * @return mixed 213 | * @throws RuntimeException 214 | */ 215 | protected function messageExists($query) 216 | { 217 | $messages = $this->findMessage($query); 218 | 219 | return count($messages) > 0; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /tests/Unit/ConfigTest.php: -------------------------------------------------------------------------------- 1 | expectException(EmailNotInConfigException::class); 17 | 18 | (new FileConfigMailSettings())->initialize('unknown key'); 19 | } 20 | 21 | /** @test */ 22 | public function create_config_with_invalid_array_key() 23 | { 24 | $this->expectException(InvalidConfigKeyException::class); 25 | 26 | (new FileConfigMailSettings())->initialize(['unknown key']); 27 | } 28 | 29 | /** @test */ 30 | public function create_config_with_valid_key() 31 | { 32 | $config = (new FileConfigMailSettings())->initialize(['name' => 'Adam', 'email' => 'test@fake.de']); 33 | 34 | $this->assertEquals('test@fake.de', $config->getFromEmail()); 35 | } 36 | 37 | /** @test */ 38 | /* 39 | public function test_non_existing_mail() 40 | { 41 | $this->expectException(\Exception::class); 42 | $config = new Config(['name' => 'Adam', 'email' => 'test@faki.de']); 43 | }*/ 44 | 45 | /** @test */ 46 | public function get_reply_name() 47 | { 48 | $config = (new FileConfigMailSettings())->initialize(['name' => 'Adam', 'email' => 'test@fake.de']); 49 | 50 | $this->assertEquals('max', $config->getReplyName()); 51 | } 52 | 53 | /** @test */ 54 | public function load_invalid_default() 55 | { 56 | $this->expectException(NoDefaultException::class); 57 | $config = (new FileConfigMailSettings())->initialize(['name' => 'Adam', 'email' => 'test@empty.de']); 58 | } 59 | 60 | /** @test */ 61 | public function load_valid_default() 62 | { 63 | app()['config']->set('multimail.emails.default', 64 | [ 65 | 'pass' => 'fakepass', 66 | 'username' => 'fakeusername', 67 | 'from' => 'Who knows', 68 | 'reply_to_mail' => 'bs@web.de', 69 | 'reply_to_name' => 'max', 70 | ]); 71 | 72 | app()['config']->set('multimail.provider.default', [ 73 | 'driver' => 'log', 74 | ]); 75 | 76 | $config = (new FileConfigMailSettings())->initialize(['name' => 'Adam', 'email' => 'test@empty.de']); 77 | 78 | $this->assertNotNull($config); 79 | } 80 | 81 | /** 82 | * Define environment setup. 83 | * 84 | * @param \Illuminate\Foundation\Application $app 85 | * @return void 86 | */ 87 | protected function getEnvironmentSetUp($app) 88 | { 89 | $app['config']->set('multimail.emails', 90 | ['test@fake.de' => [ 91 | 'pass' => 'fakepass', 92 | 'username' => 'fakeusername', 93 | 'from' => 'Who knows', 94 | 'reply_to_mail' => 'bs@web.de', 95 | 'reply_to_name' => 'max', 96 | ], 97 | 'test@empty.de' => [ 98 | 'pass' => '', 99 | 'username' => '', 100 | 'from' => 'Who knows', 101 | 'reply_to_mail' => 'bs@web.de', 102 | 'reply_to_name' => 'max', 103 | ], ]); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/Unit/MultiMailTest.php: -------------------------------------------------------------------------------- 1 | assertContainsOnlyInstancesOf(PendingMail::class, $classes); 27 | } 28 | 29 | /** @test */ 30 | public function check_if_plugins_deletable() 31 | { 32 | MultiMail::clearPlugins(); 33 | 34 | $this->assertEquals([], MultiMail::getPlugins()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/PendingMailTest.php: -------------------------------------------------------------------------------- 1 | cc($cc) 21 | ->locale($locale) 22 | ->from($from) 23 | ->bcc($bcc); 24 | 25 | $pendingMail2 = MultiMail::locale($locale) 26 | ->from($from) 27 | ->to($to) 28 | ->cc($cc) 29 | ->bcc($bcc); 30 | 31 | $this->assertEquals($pendingMail, $pendingMail2); 32 | } 33 | } 34 | --------------------------------------------------------------------------------