├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── mailspfchecker.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ └── create_mailspfchecker_table.php.stub ├── resources └── views │ └── .gitkeep └── src ├── Commands └── MailspfcheckerCommand.php ├── Facades └── Mailspfchecker.php ├── Mailspfchecker.php └── MailspfcheckerServiceProvider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `mailspfchecker` will be documented in this file. 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) dietercoopman 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![showsql](https://banners.beyondco.de/mailspfchecker.png?theme=light&packageManager=composer+require&packageName=dietercoopman%2Fmailspfchecker&pattern=architect&style=style_1&description=A+Laravel+package+to+check+if+your+application+can+send+e-mail+in+name+of+a+given+address.&md=1&showWatermark=1&fontSize=100px&images=https%3A%2F%2Flaravel.com%2Fimg%2Flogomark.min.svg) 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/dietercoopman/mailspfchecker.svg?style=flat-square)](https://packagist.org/packages/dietercoopman/mailspfchecker) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/dietercoopman/mailspfchecker.svg?style=flat-square)](https://packagist.org/packages/dietercoopman/mailspfchecker) 5 | 6 | # A Laravel package to check if you can send e-mail through a given mailserver in name of a given e-mail address 7 | 8 | # Mail spf checker 9 | 10 | A Laravel package to check if your application can send e-mail in name of a given address. 11 | 12 | ## Use case 13 | 14 | Most of the web applications are sending mails. Mostly through a local mail server or an external mailing service. 15 | When sending in name of a domain without using the legitimate mailserver of the domain it can get tricky. 16 | Most of the time your mail ends up in a spam folder. This can be solved by configuring a correct SPF record for the domain you are sending with. This package 17 | gives you the possibility to check if you can send with a given from address using the mailserver specified in your mail config 18 | or a given mailserver. It also gives the possibility to retrieve a dns txt record to configure your dns. 19 | 20 | ## Compatibility 21 | 22 | This package can be installed in Laravel 6,7,8,9 and 10 23 | 24 | ## Installation 25 | 26 | ```shell 27 | composer require dietercoopman/mailspfchecker 28 | ``` 29 | 30 | ## Examples 31 | 32 | ### Using the mailserver used by your application 33 | 34 | ```php 35 | 36 | if ($mailSpfChecker->canISendAs("hello@dietse.dev")) { 37 | // the happy path 38 | } else { 39 | // you can not send e-mail in name of hello@dietse.dev, but I can tell you what to do 40 | echo $mailSpfChecker->howCanISendAs("hello@dietse.be"); 41 | // Generate a txt-record with a name of dietse.dev and the value v=spf1 ip4:#.#.#.# -all 42 | } 43 | ``` 44 | 45 | ### Using a given mailserver 46 | 47 | ```php 48 | 49 | if ($mailSpfChecker->using('smtp.mandrill.com')->canISendAs("hello@dietse.dev")) { 50 | // the happy path 51 | } else { 52 | // you can not send e-mail in name of hello@dietse.dev, but I can tell you what to do 53 | echo $mailSpfChecker->using('smtp.mandrill.com')->howCanISendAs("hello@dietse.be"); 54 | // Generate a txt-record with a name of dietse.dev and the value v=spf1 ip4:spf.mandrill.com -all 55 | } 56 | ``` 57 | 58 | ## Changelog 59 | 60 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 61 | 62 | ## Contributing 63 | 64 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 65 | 66 | ## Security Vulnerabilities 67 | 68 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 69 | 70 | ## Credits 71 | 72 | - [Dieter Coopman](https://github.com/dietercoopman) 73 | - [All Contributors](../../contributors) 74 | 75 | ## License 76 | 77 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dietercoopman/mailspfchecker", 3 | "description": "A package to check if you can send e-mail through a given mailserver in name of a given e-mail address", 4 | "keywords": [ 5 | "dietercoopman", 6 | "laravel", 7 | "mailspfchecker" 8 | ], 9 | "homepage": "https://github.com/dietercoopman/mailspfchecker", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Dieter Coopman", 14 | "email": "dieter@deltasolutions.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0|^8.1", 20 | "illuminate/contracts": "^8.0|^9.0|^10|^11.0|^12.0", 21 | "mlocati/spf-lib": "^3.1", 22 | "spatie/laravel-package-tools": "^1.9.2" 23 | }, 24 | "require-dev": { 25 | "nunomaduro/collision": "^6.0", 26 | "nunomaduro/larastan": "^2.0.1", 27 | "pestphp/pest": "^1.21", 28 | "pestphp/pest-plugin-laravel": "^1.1", 29 | "phpstan/extension-installer": "^1.1", 30 | "phpstan/phpstan-deprecation-rules": "^1.0", 31 | "phpstan/phpstan-phpunit": "^1.0", 32 | "phpunit/phpunit": "^9.5", 33 | "orchestra/testbench": "^6.15" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Dietercoopman\\Mailspfchecker\\": "src", 38 | "Dietercoopman\\Mailspfchecker\\Database\\Factories\\": "database/factories" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Dietercoopman\\Mailspfchecker\\Tests\\": "tests" 44 | } 45 | }, 46 | "scripts": { 47 | "analyse": "vendor/bin/phpstan analyse", 48 | "test": "vendor/bin/pest", 49 | "test-coverage": "vendor/bin/pest --coverage", 50 | "format": "vendor/bin/pint" 51 | }, 52 | "config": { 53 | "sort-packages": true, 54 | "allow-plugins": { 55 | "pestphp/pest-plugin": true, 56 | "phpstan/extension-installer": true 57 | } 58 | }, 59 | "extra": { 60 | "laravel": { 61 | "providers": [ 62 | "Dietercoopman\\Mailspfchecker\\MailspfcheckerServiceProvider" 63 | ], 64 | "aliases": { 65 | "Mailspfchecker": "Dietercoopman\\Mailspfchecker\\Facades\\Mailspfchecker" 66 | } 67 | } 68 | }, 69 | "minimum-stability": "dev", 70 | "prefer-stable": true 71 | } 72 | -------------------------------------------------------------------------------- /config/mailspfchecker.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | // add fields 15 | 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dietercoopman/mailspfchecker/88f6a6a521f0e5d59e7ee689dba640194ce68404/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Commands/MailspfcheckerCommand.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | 17 | return self::SUCCESS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Facades/Mailspfchecker.php: -------------------------------------------------------------------------------- 1 | setSendingMailServer(); 20 | } 21 | 22 | public function canISendAs(string $emailOrDomain): bool 23 | { 24 | return $this->check($emailOrDomain, true) === 'pass'; 25 | } 26 | 27 | public function using(string $server): self 28 | { 29 | $this->server = $server; 30 | $this->setSendingMailServer(); 31 | 32 | return $this; 33 | } 34 | 35 | private function getDomain(string $emailOrDomain): string 36 | { 37 | if (filter_var($emailOrDomain, FILTER_VALIDATE_EMAIL)) { 38 | $domain = substr($emailOrDomain, strpos($emailOrDomain, '@') + 1); 39 | } else { 40 | $domain = $emailOrDomain; 41 | } 42 | 43 | return $domain; 44 | } 45 | 46 | public function check(string $emailOrDomain, $returnCode = false): mixed 47 | { 48 | $domain = $this->getDomain($emailOrDomain); 49 | $decoder = new \SPFLib\Decoder; 50 | 51 | $code = 'error'; 52 | $codes = []; 53 | 54 | foreach ($this->spfRecords as $spfValueToCheck) { 55 | $record = $decoder->getRecordFromDomain($domain); 56 | if ($record) { 57 | foreach ($record->getTerms() as $term) { 58 | $codes[$spfValueToCheck] = 'error'; 59 | if (strstr(strtolower($term), strtolower($spfValueToCheck))) { 60 | $codes[$spfValueToCheck] = 'pass'; 61 | break; 62 | } 63 | } 64 | } else { 65 | $codes[$spfValueToCheck] = 'error'; 66 | } 67 | } 68 | 69 | if (in_array('pass', $codes)) { 70 | $code = 'pass'; 71 | } 72 | 73 | return ($returnCode) ? $code : $codes; 74 | } 75 | 76 | private function setSendingMailServer(): void 77 | { 78 | if (! blank($this->server)) { 79 | $sendingMailserver = $this->server; 80 | } else { 81 | $sendingMailserver = config('mail.mailers.smtp.host'); 82 | } 83 | 84 | // if the address is localhost, then check wan address via icanhazip 85 | if ($sendingMailserver == '127.0.0.1' || $sendingMailserver == 'localhost') { 86 | $sendingMailserver = trim(file_get_contents('https://icanhazip.com/')); 87 | } 88 | 89 | $this->sendingMailserver = $sendingMailserver; 90 | $this->spfRecords = $this->retreiveSpfRecordsFromSendingServer(); 91 | } 92 | 93 | private function retreiveSpfRecordsFromSendingServer(): array 94 | { 95 | $spfRecords = []; 96 | $server = $this->sendingMailserver; 97 | 98 | if (! filter_var($server, FILTER_VALIDATE_IP)) { 99 | $checker = new Decoder; 100 | $explodedServerUrl = explode('.', $server); 101 | array_shift($explodedServerUrl); 102 | $domain = implode('.', $explodedServerUrl); 103 | if ($domain) { 104 | $record = $checker->getRecordFromDomain($domain); 105 | if ($record) { 106 | foreach ($record->getTerms() as $term) { 107 | if ($term instanceof Mechanism\IncludeMechanism || $term instanceof Mechanism\AMechanism) { 108 | $domainSpec = (string) $term->getDomainSpec(); 109 | if (strstr($domainSpec, $domain)) { 110 | $spfRecords[] = $domainSpec; 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | return array_unique($spfRecords); 119 | } 120 | 121 | public function howCanISendAs(string $emailOrDomain, ?string $overRuleMessage = null): string 122 | { 123 | [$name, $value] = array_values($this->buildDnsString($emailOrDomain)); 124 | if ($overRuleMessage) { 125 | return str_replace([':name', ':value'], [$name, $value], $overRuleMessage); 126 | } 127 | 128 | return "Generate a txt-record with a name of {$name} and the value {$value}"; 129 | } 130 | 131 | public function buildDnsString(string $emailOrDomain): array 132 | { 133 | $domain = $this->getDomain($emailOrDomain); 134 | $record = new \SPFLib\Record; 135 | if (! empty($this->spfRecords)) { 136 | foreach ($this->spfRecords as $server) { 137 | if (filter_var($server, FILTER_VALIDATE_IP)) { 138 | $record->addTerm(new Mechanism\Ip4Mechanism(Mechanism::QUALIFIER_PASS, IPv4::parseString($server))); 139 | } else { 140 | $record->addTerm(new Mechanism\IncludeMechanism(Mechanism::QUALIFIER_PASS, $server)); 141 | } 142 | } 143 | } else { 144 | $server = gethostbyname($this->sendingMailserver); 145 | if (filter_var($server, FILTER_VALIDATE_IP)) { 146 | $record->addTerm(new Mechanism\Ip4Mechanism(Mechanism::QUALIFIER_PASS, IPv4::parseString($server))); 147 | } else { 148 | $record->addTerm(new Mechanism\IncludeMechanism(Mechanism::QUALIFIER_PASS, $server)); 149 | } 150 | } 151 | 152 | $record->addTerm(new Mechanism\AllMechanism(Mechanism::QUALIFIER_FAIL)); 153 | 154 | return ['name' => $domain, 'value' => (string) $record]; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/MailspfcheckerServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('mailspfchecker') 20 | ->hasConfigFile() 21 | ->hasViews() 22 | ->hasMigration('create_mailspfchecker_table') 23 | ->hasCommand(MailspfcheckerCommand::class); 24 | } 25 | } 26 | --------------------------------------------------------------------------------