From 6400962a445fa197c5ccae52dccb9c4b3d6635e2 Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Sun, 2 Jan 2022 03:05:40 +0100 Subject: [PATCH] Implement wildcard aliases --- public/static/style.css | 5 ++- sql/init_tables.sql | 11 +++--- src/Common/FormData.php | 31 ++++++++++++---- src/Frontend/Accounts/AccountAddAliasData.php | 28 +++++++++++++-- src/Frontend/Accounts/AccountHandler.php | 14 ++++++-- src/Models/Alias.php | 27 ++++++++++++-- src/Repositories/AliasRepository.php | 12 +++++-- templates/account_create.html.twig | 2 +- templates/account_details.html.twig | 36 +++++++++++++++++-- 9 files changed, 141 insertions(+), 25 deletions(-) diff --git a/public/static/style.css b/public/static/style.css index 3806bf2..02f1dbc 100644 --- a/public/static/style.css +++ b/public/static/style.css @@ -27,6 +27,10 @@ a:hover, a:focus { text-decoration: underline; } +.monospace { + font-family: monospace; +} + .gray { color: gray; } @@ -248,7 +252,6 @@ details > summary { details > p { margin: 0.75rem 1rem; - font-family: monospace; } /* -- Detail columns -- */ diff --git a/sql/init_tables.sql b/sql/init_tables.sql index 0383dd8..ca50ffa 100644 --- a/sql/init_tables.sql +++ b/sql/init_tables.sql @@ -35,11 +35,12 @@ CREATE TABLE `mail_users` CREATE TABLE `mail_aliases` ( - `alias_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `user_id` int(10) unsigned NOT NULL, - `mail_address` varchar(255) NOT NULL, - `created_at` datetime NOT NULL DEFAULT NOW(), - `modified_at` datetime NOT NULL DEFAULT NOW(), + `alias_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `mail_address` varchar(255) NOT NULL, + `wildcard_priority` smallint(6) NOT NULL DEFAULT 0, + `created_at` datetime NOT NULL DEFAULT NOW(), + `modified_at` datetime NOT NULL DEFAULT NOW(), PRIMARY KEY (`alias_id`), UNIQUE KEY `mail_address` (`mail_address`), KEY `user_id` (`user_id`), diff --git a/src/Common/FormData.php b/src/Common/FormData.php index c8e235c..e21fbe2 100644 --- a/src/Common/FormData.php +++ b/src/Common/FormData.php @@ -44,25 +44,42 @@ abstract class FormData // Input validation - Application specific validators - protected static function validateUsername(string $username, bool $required = true, string $fieldName = 'Username'): ?string + private static function validateMailAddress(string $address, bool $required = true, string $fieldName = 'Mail address'): ?string { - if (!$required && $username === '') { + // Note: This validator allows '%' as wildcard character inside mail addresses. + if (!$required && $address === '') { return null; } - $username = strtolower( - self::validateString($username, 3, 100, $fieldName) + $address = strtolower( + self::validateString($address, 3, 100, $fieldName) ); - if (!preg_match('/^[a-z0-9._+-]+@[a-z0-9.-]+$/', $username) || preg_match('/^\\.|\\.\\.|\\.@|@\\.|\\.$/', $username)) { + if (!preg_match('/^[a-z0-9%._+-]+@[a-z0-9%.-]+$/', $address) || preg_match('/^\\.|\\.\\.|\\.@|@\\.|\\.$/', $address)) { throw new InputValidationError("$fieldName is not valid (must be a valid mail address)."); } + return $address; + } + + protected static function validateUsername(string $username, bool $required = true): ?string + { + $username = self::validateMailAddress($username, $required, 'Username'); + + if (strpos($username, '%') !== false) { + throw new InputValidationError('Username must not contain the wildcard character "%" (use a wildcard alias instead).'); + } return $username; } - protected static function validateAliasAddress(string $aliasAddress): string + protected static function validateAliasAddress(string $aliasAddress, bool $isWildcard): string { - return self::validateUsername($aliasAddress, true, 'Alias address'); + $aliasAddress = self::validateMailAddress($aliasAddress, true, 'Alias address'); + + // Check if the address contains a wildcard character + if (!$isWildcard && strpos($aliasAddress, '%') !== false) { + throw new InputValidationError('Non-wildcard alias address must not contain "%" character.'); + } + return $aliasAddress; } protected static function validatePassword(string $password, string $passwordRepeat, bool $required = true): ?string diff --git a/src/Frontend/Accounts/AccountAddAliasData.php b/src/Frontend/Accounts/AccountAddAliasData.php index 6e5a3c6..4a2469e 100644 --- a/src/Frontend/Accounts/AccountAddAliasData.php +++ b/src/Frontend/Accounts/AccountAddAliasData.php @@ -4,20 +4,34 @@ declare(strict_types=1); namespace MailAccountAdmin\Frontend\Accounts; use MailAccountAdmin\Common\FormData; +use MailAccountAdmin\Exceptions\InputValidationError; class AccountAddAliasData extends FormData { private string $aliasAddress; + private bool $isWildcard; + private int $wildcardPriority; - private function __construct(string $aliasAddress) + private function __construct(string $aliasAddress, bool $isWildcard, int $wildcardPriority) { + if ($isWildcard && $wildcardPriority === 0) { + throw new InputValidationError('Wildcard alias must have a wildcard priority other than 0.'); + } elseif (!$isWildcard) { + $wildcardPriority = 0; + } + $this->aliasAddress = $aliasAddress; + $this->isWildcard = $isWildcard; + $this->wildcardPriority = $wildcardPriority; } public static function createFromArray(array $raw): self { + $isWildcard = self::validateBoolOption(trim($raw['is_wildcard'] ?? '')); return new self( - self::validateAliasAddress(trim($raw['alias_address'] ?? '')), + self::validateAliasAddress(trim($raw['alias_address'] ?? ''), $isWildcard), + $isWildcard, + self::validateInteger(trim($raw['wildcard_priority'] ?? '')), ); } @@ -25,4 +39,14 @@ class AccountAddAliasData extends FormData { return $this->aliasAddress; } + + public function isWildcard(): bool + { + return $this->isWildcard; + } + + public function getWildcardPriority(): int + { + return $this->wildcardPriority; + } } diff --git a/src/Frontend/Accounts/AccountHandler.php b/src/Frontend/Accounts/AccountHandler.php index 4a71fc0..b9a0650 100644 --- a/src/Frontend/Accounts/AccountHandler.php +++ b/src/Frontend/Accounts/AccountHandler.php @@ -278,14 +278,22 @@ class AccountHandler // Check if account exists $this->ensureAccountExists($accountId); + $unescapedAddress = $address = $aliasAddData->getAliasAddress(); + $wildcardPriority = 0; + + if ($aliasAddData->isWildcard()) { + // If it's a wildcard alias, escape underscores + $address = str_replace('_', '\\_', $address); + $wildcardPriority = $aliasAddData->getWildcardPriority(); + } + // Check if alias address is still available - $address = $aliasAddData->getAliasAddress(); if (!$this->accountRepository->checkUsernameAvailable($address) || !$this->aliasRepository->checkAliasAvailable($address)) { - throw new InputValidationError("Alias address \"$address\" is not available."); + throw new InputValidationError("Alias address \"$unescapedAddress\" is not available."); } // Create alias in database - $this->aliasRepository->createNewAlias($accountId, $address); + $this->aliasRepository->createNewAlias($accountId, $address, $wildcardPriority); } diff --git a/src/Models/Alias.php b/src/Models/Alias.php index 3fe7413..e636c82 100644 --- a/src/Models/Alias.php +++ b/src/Models/Alias.php @@ -10,14 +10,23 @@ class Alias private int $id; private int $userId; private string $mailAddress; + private int $wildcardPriority; private DateTimeImmutable $createdAt; private DateTimeImmutable $modifiedAt; - private function __construct(int $id, int $userId, string $mailAddress, DateTimeImmutable $createdAt, DateTimeImmutable $modifiedAt) + private function __construct( + int $id, + int $userId, + string $mailAddress, + int $wildcardPriority, + DateTimeImmutable $createdAt, + DateTimeImmutable $modifiedAt + ) { $this->id = $id; $this->userId = $userId; $this->mailAddress = $mailAddress; + $this->wildcardPriority = $wildcardPriority; $this->createdAt = $createdAt; $this->modifiedAt = $modifiedAt; } @@ -28,6 +37,7 @@ class Alias (int)$data['alias_id'], (int)$data['user_id'], $data['mail_address'], + (int)$data['wildcard_priority'], new DateTimeImmutable($data['created_at']), new DateTimeImmutable($data['modified_at']), ); @@ -43,11 +53,24 @@ class Alias return $this->userId; } - public function getMailAddress(): string + public function getMailAddress(bool $unescape = true): string { + if ($this->isWildcard() && $unescape) { + return str_replace('\\_', '_', $this->mailAddress); + } return $this->mailAddress; } + public function isWildcard(): bool + { + return $this->wildcardPriority !== 0; + } + + public function getWildcardPriority(): int + { + return $this->wildcardPriority; + } + public function getCreatedAt(): DateTimeImmutable { return $this->createdAt; diff --git a/src/Repositories/AliasRepository.php b/src/Repositories/AliasRepository.php index 45ed640..668afbc 100644 --- a/src/Repositories/AliasRepository.php +++ b/src/Repositories/AliasRepository.php @@ -43,12 +43,20 @@ class AliasRepository extends BaseRepository return $statement->rowCount() === 0; } - public function createNewAlias(int $userId, string $mailAddress): void + public function createNewAlias(int $userId, string $mailAddress, int $wildcardPriority = 0): void { - $statement = $this->pdo->prepare('INSERT INTO mail_aliases (user_id, mail_address) VALUES (:user_id, :mail_address)'); + $query = ' + INSERT INTO mail_aliases + (user_id, mail_address, wildcard_priority) + VALUES + (:user_id, :mail_address, :wildcard_priority) + '; + + $statement = $this->pdo->prepare($query); $statement->execute([ 'user_id' => $userId, 'mail_address' => $mailAddress, + 'wildcard_priority' => $wildcardPriority, ]); } diff --git a/templates/account_create.html.twig b/templates/account_create.html.twig index 246aae7..15c8bd2 100644 --- a/templates/account_create.html.twig +++ b/templates/account_create.html.twig @@ -24,7 +24,7 @@

Show list of known domains -

{{ domainList ? domainList | join(', ') : 'No domains exist yet.' }}

+

{{ domainList ? domainList | join(', ') : 'No domains exist yet.' }}

diff --git a/templates/account_details.html.twig b/templates/account_details.html.twig index 0c092cc..3ef481d 100644 --- a/templates/account_details.html.twig +++ b/templates/account_details.html.twig @@ -71,13 +71,21 @@
- + + {% for alias in aliases %} + {% endfor %} @@ -95,7 +103,18 @@ + + + + @@ -104,5 +123,18 @@
AddressAddressWildcard (priority) Created at
+ {% if alias.isWildcard() %} + Yes ({{ alias.getWildcardPriority() }}) + {% else %} + No + {% endif %} + {{ alias.getCreatedAt() | date }}
- + +
Wildcard: + + +
+ +
+ How to define wildcard aliases? +

+ Wildcard aliases use % as a placeholder for any amount of characters (zero or more), + e.g. %@example.com or example-%@example.com. +

+

+ Additionally, a wildcard priority must be specified. When a mail address is looked up that + matches multiple (wildcard) aliases, the alias with the lowest wildcard priority will be used. + The priority must not be 0 (internally, the value 0 stands for regular non-wildcard aliases). +

+
{% endblock %}