├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── psalm.xml.dist └── src ├── UniqueGmailAddress.php └── UniqueGmailAddressRule.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `php-unique-gmail-address` will be documented in this file 4 | 5 | ## 1.0.0 - 202X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Imliam bvba 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 | # Unique Gmail Address 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/imliam/php-unique-gmail-address.svg)](https://packagist.org/packages/imliam/php-unique-gmail-address) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/imliam/php-unique-gmail-address.svg)](https://packagist.org/packages/imliam/php-unique-gmail-address) 5 | 6 | A package to ensure that a Gmail address is unique. 7 | 8 | The Gmail platform offers some features that other email platforms don't. There can be an infinite number of different Gmail addresses that point to a single Gmail inbox/account, which can make it very easy for malicious users to abuse with no effort. 9 | 10 | To learn more about how a user can make a infinite addresses for one Gmail inbox, [check out this article](https://liamhammett.com/make-infinite-gmail-addresses-for-one-inbox-nqoVprjX). 11 | 12 | This package makes it possible to **detect duplicate addresses for one Gmail account** and to ensure they're unique. 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```bash 19 | composer require imliam/php-unique-gmail-address 20 | ``` 21 | 22 | ## Usage 23 | 24 | The primary usage of this package revolves around the supplied `UniqueGmailAddress` class, which can be passed an email address to compare: 25 | 26 | ``` php 27 | $email = new UniqueGmailAddress('example@gmail.com'); 28 | ``` 29 | 30 | The `isGmailAddress` method will check if the given address belongs to a Gmail account: 31 | 32 | ``` php 33 | $one = new UniqueGmailAddress('example@gmail.com'); 34 | $one->isGmailAddress(); // true 35 | 36 | $two = new UniqueGmailAddress('example@googlemail.com'); 37 | $two->isGmailAddress(); // true 38 | 39 | $three = new UniqueGmailAddress('example@example.com'); 40 | $three->isGmailAddress(); // false 41 | ``` 42 | 43 | The `normalizeAddress` method will take an email address and normalize it to the simplest variation: 44 | 45 | ```php 46 | $email = new UniqueGmailAddress('ex.am.ple+helloworld@googlemail.com'); 47 | $email->normalizeAddress(); // example@gmail.com 48 | ``` 49 | 50 | > ⚠️ It's best to save and use the email address exactly as the user gave instead of only saving a normalized version. If a user enters a denormalized email address, they probably expect any emails they receive to be at that exact address and not the normalized version. 51 | 52 | The `getRegex` and `getRegexWithDelimeters` methods will return a regular expression that can be used to compare the original email address to another one, using a function like `preg_match`: 53 | 54 | ```php 55 | $email = new UniqueGmailAddress('example@gmail.com'); 56 | $email->getRegex(); // ^e(\.?)+x(\.?)+a(\.?)+m(\.?)+p(\.?)+l(\.?)+e(\+.*)?\@(gmail|googlemail).com$ 57 | ``` 58 | 59 | The `matches` method will immediately match the regular expression against another value, returning `true` if both email addresses belong to the same Gmail account: 60 | 61 | ```php 62 | $email = new UniqueGmailAddress('example@gmail.com'); 63 | $email->matches('ex.am.ple+helloworld@googlemail.com'); // true 64 | ``` 65 | 66 | ## Laravel Rule 67 | 68 | One of the most common use cases for wanting to check duplicate Gmail accounts is to prevent multiple users signing up to your service with the same email address. 69 | 70 | To handle this case, the package also provides a [Laravel validation rule class](https://laravel.com/docs/master/validation) that can be used to check the database if a matching email address is already present. 71 | 72 | ```php 73 | $request->validate([ 74 | 'email' => [new UniqueGmailAddressRule()], 75 | ]); 76 | ``` 77 | 78 | By default, it will check the `email` column in the `users` table, but these can be overriden if needed when using the rule: 79 | 80 | ```php 81 | $request->validate([ 82 | 'email' => [new UniqueGmailAddressRule('contacts', 'email_address')], 83 | ]); 84 | ``` 85 | 86 | **This rule assumes that you are storing denormalized email addresses in the database.** This rule will use a regex match against every row in the database to check if 87 | 88 | > 💡 If you don't want to use a regex match against every row of the database, you could instead store the normalized email address in a second column alongside the original email address, for example in a `normalized_email` column. 89 | > 90 | > This would allow you use the regular [exists](https://laravel.com/docs/master/validation#rule-exists) rule to do a direct match against, much more efficiently. 91 | 92 | ## Changelog 93 | 94 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 95 | 96 | ## Contributing 97 | 98 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 99 | 100 | ## Security 101 | 102 | If you discover any security related issues, please email liam@liamhammett.com instead of using the issue tracker. 103 | 104 | ## Credits 105 | 106 | - [Liam Hammett](https://github.com/ImLiam) 107 | - [All Contributors](../../contributors) 108 | 109 | ## License 110 | 111 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imliam/php-unique-gmail-address", 3 | "description": "Ensure that a Gmail address is unique", 4 | "keywords": [ 5 | "imliam", 6 | "php-unique-gmail-address" 7 | ], 8 | "homepage": "https://github.com/imliam/php-unique-gmail-address", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Liam Hammett", 13 | "email": "liam@liamhammett.com", 14 | "homepage": "https://liamhammett.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0" 20 | }, 21 | "require-dev": { 22 | "friendsofphp/php-cs-fixer": "^3.0", 23 | "orchestra/testbench": "^9.0|^10.0", 24 | "phpunit/phpunit": "^11.0", 25 | "vimeo/psalm": "^5.0|^6.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "ImLiam\\UniqueGmailAddress\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "ImLiam\\UniqueGmailAddress\\Tests\\": "tests" 35 | } 36 | }, 37 | "scripts": { 38 | "psalm": "vendor/bin/psalm", 39 | "test": "vendor/bin/phpunit", 40 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 41 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "minimum-stability": "dev", 47 | "prefer-stable": true 48 | } 49 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/UniqueGmailAddress.php: -------------------------------------------------------------------------------- 1 | originalEmail = $email; 13 | $this->email = $this->normalizeAddress(); 14 | } 15 | 16 | public function isGmailAddress() 17 | { 18 | return $this->stringEndsWith($this->originalEmail, '@googlemail.com') 19 | || $this->stringEndsWith($this->originalEmail, '@gmail.com'); 20 | } 21 | 22 | public function normalizeAddress() 23 | { 24 | if (! $this->isGmailAddress()) { 25 | return $this->originalEmail; 26 | } 27 | 28 | $username = trim(strstr($this->originalEmail, '@', true)); 29 | $username = explode('+', $username)[0]; 30 | $username = str_replace('.', '', $username); 31 | 32 | return "{$username}@gmail.com"; 33 | } 34 | 35 | private function stringEndsWith(string $haystack, string $needle): bool 36 | { 37 | if ($needle === '') { 38 | return true; 39 | } 40 | 41 | return substr($haystack, -strlen($needle)) === $needle; 42 | } 43 | 44 | private function getEscapedEmailUsername() 45 | { 46 | $emailUsername = strstr($this->email, '@', true); 47 | 48 | $usernameCharacters = array_map(function ($character) { 49 | return preg_quote($character); 50 | }, str_split($emailUsername)); 51 | 52 | return join('(\.?)+', $usernameCharacters); // Each character in the username can have optional dots between them 53 | } 54 | 55 | public function getRegexWithDelimiters() 56 | { 57 | return '/' . $this->getRegex() . '/'; 58 | } 59 | 60 | public function getRegex() 61 | { 62 | if (! $this->isGmailAddress()) { 63 | return '^' . preg_quote($this->email) . '$'; // Must be an exact match 64 | } 65 | 66 | return '^' . $this->getEscapedEmailUsername() // Must start with the email username 67 | . '(\+.*)?' // Can optionally include anything after a literal plus 68 | . '\@(gmail|googlemail).com$'; // Must end with @gmail.com or @googlemail.com 69 | } 70 | 71 | public function matches($emailToCompare) 72 | { 73 | /** @var array $matches */ 74 | preg_match($this->getRegexWithDelimiters(), $emailToCompare, $matches); 75 | 76 | return $matches !== []; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/UniqueGmailAddressRule.php: -------------------------------------------------------------------------------- 1 | isGmailAddress()) { 23 | return; 24 | } 25 | 26 | if (DB::table($this->table)->where($this->column, 'REGEXP', $validator->getRegexWithDelimiters())->exists()) { 27 | $fail('A variation of the given email address has already been used.'); 28 | } 29 | } 30 | } 31 | --------------------------------------------------------------------------------