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 @@
+
+
+ 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 %}