├── .editorconfig ├── .env.testing ├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── src ├── Console │ └── Commands │ │ ├── AddMail.php │ │ ├── DeleteMail.php │ │ ├── ListMail.php │ │ └── ResetThreshold.php ├── Database │ ├── Factories │ │ └── MailSwitcherCredentialFactory.php │ └── Migrations │ │ └── 2021_04_23_000000_create_mail_switcher_credentials_table.php ├── Exceptions │ └── EmptyCredentialException.php ├── Listeners │ ├── IncreaseCurrentUsageAfterSentEmail.php │ └── OverwriteMailSMTPCredential.php ├── Models │ └── MailCredential.php └── ServiceProvider.php └── tests ├── TestCase.php └── Unit ├── Listeners ├── IncreaseCurrentUsageAfterSentEmailTest.php └── OverwriteMailSMTPCredentialTest.php └── Models └── MailCredentialTest.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 | 17 | [composer.json] 18 | indent_size = 4 19 | indent_style = space -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- 1 | APP_NAME=MailSwitcher 2 | APP_ENV=testing 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_LOG_LEVEL=debug 6 | 7 | DB_CONNECTION=mysql 8 | DB_HOST=127.0.0.1 9 | DB_PORT=3306 10 | DB_DATABASE=mstest 11 | DB_USERNAME=root 12 | DB_PASSWORD=root 13 | 14 | MAIL_DRIVER=smtp 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | on: 3 | pull_request: 4 | branches: 5 | - 'master' 6 | types: [ opened, synchronize, reopened, ready_for_review ] 7 | push: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | build_php81: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | if: success() 17 | 18 | - name: Setup PHP with coverage driver 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: '8.1' 22 | coverage: pcov 23 | 24 | - name: Setup 25 | if: success() 26 | run: | 27 | php -v 28 | composer install --no-interaction 29 | 30 | - name: PHPUnit tests with coverage 31 | if: success() && github.event.pull_request.draft == false 32 | run: | 33 | composer test-coverage 34 | 35 | - name: upload coverage to codecov.io 36 | uses: codecov/codecov-action@v1 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | file: ./coverage.xml 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.phar 2 | /vendor 3 | /.idea 4 | /.idea/* 5 | .env 6 | .phpunit* 7 | .php-cs-fixer.cache 8 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | true, 8 | '@PHP56Migration:risky' => true, 9 | 'array_push' => true, 10 | 'ordered_imports' => [ 11 | 'imports_order' => [ 12 | 'class', 'function', 'const', 13 | ], 14 | 'sort_algorithm' => 'length', 15 | ], 16 | 'no_leading_import_slash' => true, 17 | 'return_assignment' => true, 18 | 'phpdoc_no_empty_return' => true, 19 | 'no_blank_lines_after_phpdoc' => true, 20 | 'general_phpdoc_tag_rename' => true, 21 | 'phpdoc_inline_tag_normalizer' => true, 22 | 23 | 'combine_consecutive_issets' => true, 24 | 'combine_consecutive_unsets' => true, 25 | 'no_useless_else' => true, 26 | 'lowercase_keywords' => true, 27 | 'modernize_types_casting' => true, 28 | 'no_short_bool_cast' => true, 29 | 'no_php4_constructor' => true, 30 | 'php_unit_construct' => [ 31 | 'assertions' => ['assertSame', 'assertEquals', 'assertNotEquals', 'assertNotSame'], 32 | ], 33 | ]; 34 | 35 | $finder = Finder::create() 36 | ->in([ 37 | __DIR__.'/src', 38 | __DIR__.'/tests', 39 | ]) 40 | ->name('*.php') 41 | ->notName('*.blade.php') 42 | ->ignoreDotFiles(true) 43 | ->ignoreVCS(true); 44 | 45 | $config = new Config(); 46 | 47 | return $config->setFinder($finder) 48 | ->setRules($rules) 49 | ->setRiskyAllowed(true) 50 | ->setUsingCache(true); 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Seth Phat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Mail Switcher 2 | ![Build and Test](https://github.com/sethsandaru/laravel-mail-switcher/actions/workflows/build.yaml/badge.svg) 3 | [![codecov](https://codecov.io/gh/sethsandaru/laravel-mail-switcher/branch/master/graph/badge.svg?token=S1GSHCQB55)](https://codecov.io/gh/sethsandaru/laravel-mail-switcher) 4 | [![Latest Stable Version](https://poser.pugx.org/sethsandaru/laravel-mail-switcher/v)](//packagist.org/packages/sethsandaru/laravel-mail-switcher) 5 | 6 | Laravel Mail Credentials Switcher is a library which helps you to: 7 | 8 | - Manage your Mail Service Credentials 9 | - Configure the Laravel's Mail Driver and using the available credential 10 | - Switch to another credential if the previous one was out of usage of the current day/week/month 11 | - Automatically reset the usage (daily/weekly/monthly) of the credentials 12 | 13 | ## Use-case 14 | 15 | You have a personal Laravel Application (small or medium) or even you're a Startup. Of course, you have a **tight budget**. 16 | 17 | So you probably can't spend much money for Email Provider Services to send out your email to your Users/Customers. 18 | 19 | There are a lot of Email Provider Services out there that actually give you a specific amount per month (for free) to send out emails. 20 | 21 | So, with Laravel Mail Switcher, you will have a big advantage to achieve that. 22 | 23 | - You don't have to change `ENV` everytime one of services is running out of usage. 24 | - You don't need to manually check to see if the email is running out. 25 | 26 | All you need to do, is prepare the credential/information and let Laravel Mail Switcher will do that for you. 27 | 28 | ### Email Services with Free Usage 29 | - Mailgun: 5000 emails for 3-month (about 1666/month) 30 | - Mailjet: 6000 emails per month (but 200 per day) 31 | - Sendgrid: 100 emails per day (3000/month) 32 | - Socketlabs: 2000/month (first month: 40000) 33 | - Sendinblue: 300 per day (9000/month) 34 | 35 | And many more... With Laravel Mail Switcher, you can manage the credentials and use all of them until the free usage ran out! 36 | 37 | ### Limitation 38 | 39 | Laravel Mail Switcher is only support for `SMTP` driver at the moment. 40 | 41 | Coming soon for others. 42 | 43 | ## Requirement 44 | - Laravel 9.x 45 | - PHP 8.1 46 | 47 | ## Installation 48 | ``` 49 | composer require sethsandaru/laravel-mail-switcher 50 | ``` 51 | 52 | ## How to use? 53 | Laravel Mail Switcher doesn't need a GUI to work with. We will do all the stuff in **Artisan Console**. 54 | 55 | First, you need to run the migration: 56 | 57 | ``` 58 | php artisan migrate 59 | ``` 60 | 61 | Then, you can traverse the instructions below!! 62 | 63 | ### List All Emails 64 | ``` 65 | php artisan ms:list 66 | ``` 67 | 68 | Note: You can add **--force** to show all Credentials (even the exceeded usage credentials) 69 | 70 | ### Add Email Credential 71 | ``` 72 | php artisan ms:add 73 | ``` 74 | 75 | You will see some questions that need your answers in order to add. Follow the instruction!! 76 | 77 | ### Delete an Credential 78 | ``` 79 | php artisan ms:delete {credentialId} 80 | ``` 81 | 82 | ### Reset Threshold of expired Credentials 83 | ``` 84 | php artisan ms:reset 85 | ``` 86 | 87 | Like, your email credential is `daily` usage and exceeded yesterday. So, today we're gonna recover it to use it again. 88 | 89 | ## Cronjob Setup 90 | By default, I will let you configure the Cron Job / Task Scheduling in your `Kernal.php` 91 | 92 | Best practice should be daily check at 00:00 93 | 94 | ```php 95 | $schedule->command('ms:reset')->dailyAt("00:00"); 96 | ``` 97 | 98 | or every minute: 99 | 100 | ```php 101 | $schedule->command('ms:reset')->everyMinute(); 102 | ``` 103 | 104 | ## Tech Specs / QA Times 105 | 106 | ### Why did I choose to Overwrite the SMTP by listen to Mail's Events instead of ServiceProvider? 107 | Because in real-life projects, not all the time, we will send out the emails. If I go with that way, then it probably costs 1 query everytime 108 | there is a connection to our application which isn't good and nice at all. 109 | 110 | 111 | ## Improve this library? 112 | 113 | Feel free to fork it and send me the PR, I'll happily review and merge it (if it's totally make sense of course). 114 | 115 | Remember to write Unit Test also! Otherwise, I'll reject it. 116 | 117 | Coding Style must follow PSR-1 & PSR-12. 118 | 119 | ## Note 120 | After your project is growing up, making good, then, don't forget to subscribe to an Email Service Provider for long-term supports and absolutely stable in production. 121 | 122 | ## Copyright 123 | 2022 by Seth Phat 124 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sethsandaru/laravel-mail-switcher", 3 | "type": "library", 4 | "description": "Laravel Mail Switcher for Budget Laravel Application", 5 | "keywords": [ 6 | "laravel library", 7 | "laravel mail switcher", 8 | "laravel switching email credentials to send out emails" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Seth Chen", 13 | "email": "me@sethphat.dev" 14 | } 15 | ], 16 | "license": "MIT", 17 | "require": { 18 | "php": "^8.1", 19 | "laravel/framework": "^9|dev-master" 20 | }, 21 | "require-dev": { 22 | "fakerphp/faker": "^v1.20.0", 23 | "mockery/mockery": "^1.5.1", 24 | "phpunit/phpunit": "^9.5.25", 25 | "orchestra/testbench": "^7", 26 | "phpunit/php-code-coverage": "^9.2.17" 27 | }, 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "SethPhat\\MailSwitcher\\ServiceProvider" 32 | ], 33 | "alias": { 34 | "MailSwitcher": "\\SethPhat\\MailSwitcher\\Facade" 35 | } 36 | } 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "SethPhat\\MailSwitcher\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "SethPhat\\MailSwitcher\\Tests\\": "tests/" 46 | } 47 | }, 48 | "scripts": { 49 | "test-coverage": [ 50 | "@php vendor/bin/phpunit --coverage-clover coverage.xml" 51 | ], 52 | "test": [ 53 | "@php vendor/bin/phpunit" 54 | ] 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true 58 | } 59 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./src 16 | 17 | 18 | ./src/Console 19 | ./src/Database 20 | ./src/Exceptions 21 | ./src/ServiceProvider.php 22 | 23 | 24 | 25 | 26 | ./tests/ 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Console/Commands/AddMail.php: -------------------------------------------------------------------------------- 1 | '(Required) Login Credential (Username or Email)', 20 | 'password' => '(Required) Login Password', 21 | 'server' => '(Required) SMTP Server', 22 | 'port' => '(Required) SMTP Port', 23 | 'encryption' => [ 24 | 'question' => 'Email Encryption (Default: TLS)', 25 | 'default' => 'TLS', 26 | ], 27 | 'threshold' => [ 28 | 'question' => 'Email Threshold (Per Month) (Default: 1000)', 29 | 'default' => 1000, 30 | ], 31 | 'threshold_type' => [ 32 | 'question' => 'Threshold Type (daily/weekly/monthly) (Default: monthly)', 33 | 'default' => MailCredential::THRESHOLD_TYPE_MONTHLY, 34 | ], 35 | ]; 36 | 37 | $payload = []; 38 | foreach ($questions as $key => $question) { 39 | $payload[$key] = $this->ask( 40 | $question['question'] ?? $question 41 | ); 42 | 43 | if (empty($payload[$key])) { 44 | if (!is_array($question)) { 45 | return $this->error(ucfirst($key).' is required. Aborted'); 46 | } 47 | $payload[$key] = $question['default']; 48 | } 49 | } 50 | 51 | // okay add new 52 | MailCredential::create($payload); 53 | 54 | $this->info('Your SMTP Email credential has been added!'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Console/Commands/DeleteMail.php: -------------------------------------------------------------------------------- 1 | argument('credentialId'); 19 | 20 | /** @var MailCredential $mailCredential */ 21 | $mailCredential = MailCredential::findOrFail($credentialId); 22 | 23 | $this->warn('You might want to backup this again:'); 24 | $this->info("Server: {$mailCredential->server}"); 25 | $this->info("Port: {$mailCredential->port}"); 26 | $this->info("User/Email: {$mailCredential->email}"); 27 | $this->info("Password: {$mailCredential->password}"); 28 | $this->info("Encryption: {$mailCredential->encryption}"); 29 | 30 | $mailCredential->delete(); 31 | 32 | $this->info('Your SMTP Email credential has been deleted!'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Console/Commands/ListMail.php: -------------------------------------------------------------------------------- 1 | hasOption('force'); 19 | 20 | $query = MailCredential::query()->orderBy('current_threshold', 'DESC'); 21 | 22 | if (!$isShowAll) { 23 | $query->whereColumn('current_threshold', '<', 'threshold'); 24 | } 25 | 26 | $mailCredentials = $query->get(); 27 | 28 | $headers = ['ID', 'Email / Host', 'Threshold Type', 'Threshold', 'Current Usage']; 29 | $rows = $mailCredentials->map(function ($mailCredential) { 30 | return [ 31 | $mailCredential->id, 32 | $mailCredential->email.' / '.$mailCredential->server, 33 | $mailCredential->threshold_type, 34 | $mailCredential->threshold, 35 | $mailCredential->current_threshold, 36 | ]; 37 | }); 38 | 39 | $this->info('SMTP Credentials List'); 40 | if ($isShowAll) { 41 | $this->warn('[Force Mode is on - Show ALL]'); 42 | } 43 | 44 | $this->table($headers, $rows); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Console/Commands/ResetThreshold.php: -------------------------------------------------------------------------------- 1 | info('Clearing the Credential with pass the threshold date.'); 17 | 18 | foreach ($mailCredentials as $mailCredential) { 19 | if ($mailCredential->isAvailableToClearThreshold) { 20 | $mailCredential->threshold_start = null; 21 | $mailCredential->save(); 22 | 23 | $this->info("Cleared for ID: {$mailCredential->id} - Server: {$mailCredential->server} - {$mailCredential->email}"); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Database/Factories/MailSwitcherCredentialFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->email, 16 | 'password' => $this->faker->password, 17 | 'server' => $this->faker->ipv4, 18 | 'port' => 465, 19 | 'encryption' => 'tls', 20 | 'threshold' => $this->faker->numberBetween(1, 99), 21 | 'current_threshold' => 0, 22 | 'threshold_type' => MailCredential::THRESHOLD_TYPE_MONTHLY, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Database/Migrations/2021_04_23_000000_create_mail_switcher_credentials_table.php: -------------------------------------------------------------------------------- 1 | id(); 14 | $table->string('email'); 15 | $table->string('password'); 16 | $table->string('server'); 17 | $table->string('port'); 18 | $table->string('encryption'); 19 | 20 | $table->integer('threshold'); 21 | $table->integer('current_threshold')->default(0); 22 | $table->enum('threshold_type', [ 23 | 'daily', 24 | 'weekly', 25 | 'monthly', 26 | ])->default(MailCredential::THRESHOLD_TYPE_MONTHLY); 27 | $table->timestamp('threshold_start')->nullable(); 28 | 29 | $table->timestamps(); 30 | }); 31 | } 32 | 33 | public function down(): void 34 | { 35 | Schema::dropIfExists('mail_switcher_credentials'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exceptions/EmptyCredentialException.php: -------------------------------------------------------------------------------- 1 | current_threshold; 20 | $mailCredential->save(); 21 | 22 | Log::info("[MailSwitcher] Mail Sent by using: {$mailCredential->email}|{$mailCredential->server}. Usage Left: {$mailCredential->usageLeft}"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/OverwriteMailSMTPCredential.php: -------------------------------------------------------------------------------- 1 | app = app(MailManager::class); 22 | } 23 | 24 | /** 25 | * Handle the Overwrite Process. 26 | */ 27 | public function handle(MessageSending $event) 28 | { 29 | $mailCredential = MailCredential::getAvailableCredential(); 30 | 31 | if (is_null($mailCredential)) { 32 | throw new EmptyCredentialException(); 33 | } 34 | 35 | // cache 36 | MailCredential::$currentInstance = $mailCredential; 37 | 38 | /** @var EsmtpTransport $smtpTransport */ 39 | $smtpTransport = $this->app->createSymfonyTransport([ 40 | 'transport' => 'smtp', 41 | 'encryption' => $mailCredential->encryption, 42 | 'host' => $mailCredential->server, 43 | 'port' => $mailCredential->port, 44 | 'username' => $mailCredential->email, 45 | 'password' => $mailCredential->password, 46 | ]); 47 | 48 | // set to MailManager 49 | $this->app->forgetMailers(); 50 | $this->app->extend('smtp', fn () => $smtpTransport); 51 | 52 | Log::info("[MailSwitcher] Switched the MailCredential to: {$mailCredential->email}|{$mailCredential->server}"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Models/MailCredential.php: -------------------------------------------------------------------------------- 1 | 'datetime', 49 | ]; 50 | 51 | /** 52 | * Get the available Mail Credential to send out. 53 | */ 54 | public static function getAvailableCredential(): ?MailCredential 55 | { 56 | // if cached => prefer to use the cached credential 57 | if (!is_null(static::$currentInstance)) { 58 | // if out of usage 59 | if (0 === static::$currentInstance->usageLeft) { 60 | // need to retrieve the new one 61 | static::$currentInstance = null; 62 | 63 | return static::getAvailableCredential(); 64 | } 65 | 66 | return static::$currentInstance; 67 | } 68 | 69 | static::$currentInstance = self::available() 70 | ->orderBy('current_threshold', 'DESC') 71 | ->first(); 72 | 73 | return static::$currentInstance; 74 | } 75 | 76 | public function scopeAvailable(Builder $query): Builder 77 | { 78 | return $query->whereColumn('current_threshold', '<', 'threshold'); 79 | } 80 | 81 | public function getUsageLeftAttribute(): int 82 | { 83 | $this->refresh(); 84 | 85 | return $this->threshold - $this->current_threshold; 86 | } 87 | 88 | public function getIsAvailableToClearThresholdAttribute(): bool 89 | { 90 | if ($this->threshold_start) { 91 | $cNow = now(); 92 | 93 | switch ($this->threshold_type) { 94 | case self::THRESHOLD_TYPE_DAILY: 95 | $nextThreshold = $this->threshold_start->addDay(); 96 | break; 97 | 98 | case self::THRESHOLD_TYPE_WEEKLY: 99 | $nextThreshold = $this->threshold_start->addDays(7); 100 | break; 101 | 102 | case self::THRESHOLD_TYPE_MONTHLY: 103 | default: 104 | $nextThreshold = $this->threshold_start->addDays(31); 105 | } 106 | 107 | return $cNow->greaterThanOrEqualTo($nextThreshold); 108 | } 109 | 110 | return false; 111 | } 112 | 113 | protected static function newFactory(): MailSwitcherCredentialFactory 114 | { 115 | return MailSwitcherCredentialFactory::new(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/Database/Migrations'); 24 | $this->commands([ 25 | ListMail::class, 26 | AddMail::class, 27 | DeleteMail::class, 28 | ResetThreshold::class, 29 | ]); 30 | 31 | Event::listen(MessageSending::class, OverwriteMailSMTPCredential::class); 32 | Event::listen(MessageSent::class, IncreaseCurrentUsageAfterSentEmail::class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | up(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Unit/Listeners/IncreaseCurrentUsageAfterSentEmailTest.php: -------------------------------------------------------------------------------- 1 | createMock(SentMessage::class), [])); 24 | 25 | // asserts 26 | Event::assertDispatched(MessageSent::class); 27 | } 28 | 29 | public function testCurrentThresholdOfCredentialDidIncreaseAfterSentEmail(): void 30 | { 31 | Event::fake(); 32 | 33 | $mailCredential = MailCredential::factory()->create(); 34 | 35 | event(new MessageSent($this->createMock(SentMessage::class), [])); 36 | (new IncreaseCurrentUsageAfterSentEmail())->handle( 37 | new MessageSent($this->createMock(SentMessage::class), []) 38 | ); 39 | 40 | $mailCredential->refresh(); 41 | $this->assertNotEquals(0, $mailCredential->current_threshold); 42 | $this->assertGreaterThan(0, $mailCredential->current_threshold); 43 | $this->assertEquals(1, $mailCredential->current_threshold); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unit/Listeners/OverwriteMailSMTPCredentialTest.php: -------------------------------------------------------------------------------- 1 | mailManager = app(MailManager::class); 26 | } 27 | 28 | public function testClassDidListenToMailSendingEvent(): void 29 | { 30 | Event::fake(); 31 | 32 | Event::assertListening( 33 | MessageSending::class, 34 | OverwriteMailSMTPCredential::class 35 | ); 36 | 37 | event(new MessageSending(new Email(), [])); 38 | 39 | // asserts 40 | Event::assertDispatched(MessageSending::class); 41 | } 42 | 43 | public function testEmailCredentialOverwroteFromLaravelSuccessfully(): void 44 | { 45 | Event::fake(); 46 | 47 | $mailCredential = MailCredential::factory()->create(); 48 | 49 | event(new MessageSending(new Email(), [])); 50 | (new OverwriteMailSMTPCredential()) 51 | ->handle( 52 | new MessageSending(new Email(), []) 53 | ); 54 | 55 | // asserts 56 | /** @var EsmtpTransport $smtpTransport */ 57 | $smtpTransport = app(MailManager::class)->mailer()->getSymfonyTransport(); 58 | 59 | $this->assertSame($smtpTransport->getUsername(), $mailCredential->email); 60 | $this->assertSame($smtpTransport->getPassword(), $mailCredential->password); 61 | $this->assertSame($smtpTransport->getStream()->getHost(), $mailCredential->server); 62 | $this->assertSame($smtpTransport->getStream()->getPort(), $mailCredential->port); 63 | $this->assertSame($smtpTransport->getStream()->isTls(), true); 64 | } 65 | 66 | public function testFailToOverwriteBecauseNoMoreCredentialThrowException(): void 67 | { 68 | $this->expectException(EmptyCredentialException::class); 69 | MailCredential::factory()->create([ 70 | 'threshold' => 10, 71 | 'current_threshold' => 10, 72 | ]); 73 | 74 | Event::fake(); 75 | event(new MessageSending(new Email(), [])); 76 | (new OverwriteMailSMTPCredential()) 77 | ->handle( 78 | new MessageSending(new Email(), []) 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Unit/Models/MailCredentialTest.php: -------------------------------------------------------------------------------- 1 | create(); 14 | 15 | $this->assertDatabaseHas('mail_switcher_credentials', [ 16 | 'id' => $credential->id, 17 | 'email' => $credential->email, 18 | 'password' => $credential->password, 19 | 'port' => $credential->port, 20 | 'server' => $credential->server, 21 | ]); 22 | } 23 | 24 | public function testGetAvailableCredentialSuccessfullyAndFullyCached(): void 25 | { 26 | $credential = MailCredential::factory()->create(); 27 | 28 | // from query 29 | $availableCredential = MailCredential::getAvailableCredential(); 30 | 31 | $this->assertNotNull($availableCredential); 32 | $this->assertSame($credential->id, $availableCredential->id); 33 | $this->assertTrue($credential->is($availableCredential)); 34 | 35 | // from cache 36 | $cachedCredential = MailCredential::getAvailableCredential(); 37 | 38 | $this->assertNotNull($availableCredential); 39 | $this->assertSame($credential->id, $cachedCredential->id); 40 | 41 | $this->assertTrue($credential->is($cachedCredential)); 42 | } 43 | 44 | public function testGetAvailableCredentialCacheDataOutOfUsageAutoSwitchToNewCredential(): void 45 | { 46 | $credential = MailCredential::factory()->create(); 47 | 48 | // from query 49 | $availableCredential = MailCredential::getAvailableCredential(); 50 | 51 | $this->assertNotNull($availableCredential); 52 | $this->assertSame($credential->id, $availableCredential->id); 53 | $this->assertTrue($credential->is($availableCredential)); 54 | 55 | // set to out of threshold 56 | $credential->current_threshold = $credential->threshold; 57 | $credential->save(); 58 | 59 | // create new credential 60 | MailCredential::factory()->create(); 61 | 62 | $newAvailableCredential = MailCredential::getAvailableCredential(); 63 | 64 | $this->assertNotNull($newAvailableCredential); 65 | $this->assertNotSame($availableCredential->id, $newAvailableCredential->id); 66 | $this->assertEquals(0, $newAvailableCredential->current_threshold); 67 | $this->assertFalse($availableCredential->is($newAvailableCredential)); 68 | } 69 | 70 | public function testUsageLeftAttribute(): void 71 | { 72 | $credential = MailCredential::factory()->create(); 73 | 74 | $timesLeft = $credential->threshold - $credential->current_threshold; 75 | $this->assertEquals($timesLeft, $credential->usageLeft); 76 | 77 | $credential->update([ 78 | 'threshold' => 50, 79 | 'current_threshold' => 50, 80 | ]); 81 | 82 | $this->assertEquals(0, $credential->usageLeft); 83 | } 84 | 85 | public function testTypeOfThresholdIsCarbonInstance(): void 86 | { 87 | $credential = MailCredential::factory()->create([ 88 | 'threshold_type' => MailCredential::THRESHOLD_TYPE_DAILY, 89 | 'threshold_start' => now(), 90 | ]); 91 | 92 | $credential->refresh(); 93 | 94 | $this->assertNotNull($credential->threshold_start); 95 | $this->assertInstanceOf(Carbon::class, $credential->threshold_start); 96 | } 97 | 98 | /** 99 | * @covers \SethPhat\MailSwitcher\Models\MailCredential::getIsAvailableToClearThresholdAttribute 100 | */ 101 | public function testGetAvailableCredentialThresholdToClearDaily(): void 102 | { 103 | $credential = MailCredential::factory()->create([ 104 | 'threshold_type' => MailCredential::THRESHOLD_TYPE_DAILY, 105 | 'threshold_start' => now(), 106 | ]); 107 | 108 | Carbon::setTestNow(now()->addDay()->addSecond()); 109 | 110 | $this->assertTrue($credential->isAvailableToClearThreshold); 111 | } 112 | 113 | /** 114 | * @covers \SethPhat\MailSwitcher\Models\MailCredential::getIsAvailableToClearThresholdAttribute 115 | */ 116 | public function testGetAvailableCredentialThresholdToClearWeekly(): void 117 | { 118 | $credential = MailCredential::factory()->create([ 119 | 'threshold_type' => MailCredential::THRESHOLD_TYPE_WEEKLY, 120 | 'threshold_start' => now(), 121 | ]); 122 | 123 | Carbon::setTestNow(now()->addDays(7)); 124 | 125 | $this->assertTrue($credential->isAvailableToClearThreshold); 126 | } 127 | 128 | /** 129 | * @covers \SethPhat\MailSwitcher\Models\MailCredential::getIsAvailableToClearThresholdAttribute 130 | */ 131 | public function testGetAvailableCredentialThresholdToClearMonthly(): void 132 | { 133 | $credential = MailCredential::factory()->create([ 134 | 'threshold_type' => MailCredential::THRESHOLD_TYPE_MONTHLY, 135 | 'threshold_start' => now(), 136 | ]); 137 | 138 | Carbon::setTestNow(now()->addDays(31)); 139 | 140 | $this->assertTrue($credential->isAvailableToClearThreshold); 141 | } 142 | 143 | /** 144 | * @covers \SethPhat\MailSwitcher\Models\MailCredential::getIsAvailableToClearThresholdAttribute 145 | */ 146 | public function testGetAvailableCredentialThresholdToClearFalseBecauseHasNotStartedYet(): void 147 | { 148 | $credential = MailCredential::factory()->create([ 149 | 'threshold_type' => MailCredential::THRESHOLD_TYPE_DAILY, 150 | 'threshold_start' => null, 151 | ]); 152 | 153 | $this->assertFalse($credential->isAvailableToClearThreshold); 154 | } 155 | } 156 | --------------------------------------------------------------------------------