diff --git a/.drone.yml b/.drone.yml index 5243887..4d9dd2d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -12,7 +12,7 @@ steps: environment: COMPOSER_CACHE_DIR: /tmp/cache commands: - - composer install --no-progress --no-interaction + - composer install --no-progress --no-interaction --ignore-platform-reqs - name: run unit tests image: php:7.4 diff --git a/.env.develop b/.env.develop index 968fd4b..f6a431d 100644 --- a/.env.develop +++ b/.env.develop @@ -3,21 +3,23 @@ # composer: Set cache directory COMPOSER_CACHE_DIR=./.composer -# MariaDB container +# Environment variables for MariaDB container MYSQL_RANDOM_ROOT_PASSWORD=yes MYSQL_DATABASE=mailusers MYSQL_USER=mailaccountadmin MYSQL_PASSWORD=mailaccountadmin # App settings +APP_TITLE="MailAccountAdmin [dev]" +APP_ENV=development APP_DEBUG=true +APP_TIMEZONE=Europe/Berlin -# - Disable Twig cache +# Disable Twig cache TWIG_CACHE_DIR= -TWIG_STRICT=true -# - Database credentials +# Database credentials DB_HOST=db DB_DATABASE=mailusers -DB_USER=mailaccountadmin +DB_USERNAME=mailaccountadmin DB_PASSWORD=mailaccountadmin diff --git a/.gitignore b/.gitignore index 646c542..37cecc0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,9 @@ # PHP /.composer /.phpunit.cache +/.twig.cache /coverage /vendor + +# Production settings +/config/app.yml diff --git a/Dockerfile b/Dockerfile index 695e69b..89effb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,10 @@ FROM php:7.4-apache AS base WORKDIR /var/www RUN apt-get update && \ - apt-get install -y libzip-dev unzip git && \ - docker-php-ext-install pdo pdo_mysql zip + apt-get install -y git unzip libyaml-dev libzip-dev && \ + docker-php-ext-install pdo pdo_mysql zip && \ + pecl install yaml && \ + docker-php-ext-enable yaml RUN a2enmod rewrite && \ sed -ri -e 's!/var/www/html!/var/www/public!g' /etc/apache2/sites-available/*.conf @@ -18,8 +20,6 @@ RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini && \ pecl install xdebug && \ docker-php-ext-enable xdebug -ENV APP_ENV=development - # TODO: production image untested #FROM base AS production # diff --git a/Makefile b/Makefile index f0ffbb1..8f22fbd 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ DOCKER_RUN = $(DOCKER_COMPOSE) run --rm app COMPOSER = $(DOCKER_RUN) composer PHPUNIT = $(DOCKER_RUN) vendor/bin/phpunit -.PHONY: all clean VERSION \ +.PHONY: all clean version \ docker-up docker-up-detached docker-down docker-restart docker-build docker-rebuild docker-purge docker-logs docker-run \ composer-install composer-install-no-dev composer-update composer-cmd \ test phpunit open-coverage @@ -99,7 +99,7 @@ open-coverage: # Create VERSION file from current git tag version: - git describe | tee VERSION + git describe --tags | tee VERSION # Clean up diff --git a/TODO.md b/TODO.md index 2226cf1..4697f74 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,6 @@ ## General -- Settings from a config file - Database migrations - Documentation - App deployment @@ -12,6 +11,7 @@ - Refactor auth and session handling ## Admin user management + - Create/edit/delete admins - Change own password diff --git a/composer.json b/composer.json index 93dcd53..aa59362 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "require": { "php": "^7.4", "ext-pdo": "*", + "ext-yaml": "*", "slim/slim": "^4.8", "slim/psr7": "^1.3", "php-di/php-di": "^6.3", diff --git a/config/app.develop.yml b/config/app.develop.yml new file mode 100644 index 0000000..923ceb4 --- /dev/null +++ b/config/app.develop.yml @@ -0,0 +1,19 @@ +# Config file for local development (same settings as .env.develop) + +# -- App settings +appTitle: "MailAccountAdmin [dev]" +environment: development +debug: true +timezone: Europe/Berlin + +# -- Twig settings +twig: + cacheDir: + +# -- Database settings +database: + host: db + port: 3306 + name: mailusers + username: mailaccountadmin + password: mailaccountadmin diff --git a/config/app.example.yml b/config/app.example.yml new file mode 100644 index 0000000..2683c29 --- /dev/null +++ b/config/app.example.yml @@ -0,0 +1,23 @@ +# This is an example config file for MailAccountAdmin. +# Copy this file to /config/app.yml and change as needed. +# Settings that are commented out represent the default values. + +# -- App settings +# appTitle: MailAccountAdmin +# environment: production +# debug: false +# timezone: UTC +# dateTimeFormat: r + +# -- Twig settings +twig: + # Cache directory for Twig templates (default: unset, which means no cache is used) + cacheDir: .twig.cache + +# -- Database settings +database: + host: localhost + port: 3306 + name: mailusers + username: mailaccountadmin + password: very_secret_password diff --git a/public/index.php b/public/index.php index 37641db..c918330 100644 --- a/public/index.php +++ b/public/index.php @@ -3,19 +3,25 @@ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; +use MailAccountAdmin\Config\Loaders\AutoConfigLoader; use MailAccountAdmin\Dependencies; use MailAccountAdmin\Middlewares; use MailAccountAdmin\Routes; -use MailAccountAdmin\Settings; use Slim\Factory\AppFactory; +const ROOT_DIR = __DIR__ . '/..'; + session_start(); -$settings = new Settings(); -$container = Dependencies::createContainer($settings); +// Load application config (from config file or environment variables) +$configLoader = new AutoConfigLoader(); +$config = $configLoader->loadConfig(); + +// Create application +$container = Dependencies::createContainer($config); $app = AppFactory::createFromContainer($container); -Middlewares::setMiddlewares($app, $settings); +Middlewares::setMiddlewares($app, $config); Routes::setRoutes($app); $app->run(); 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 fde1281..8041257 100644 --- a/src/Common/FormData.php +++ b/src/Common/FormData.php @@ -20,7 +20,7 @@ abstract class FormData throw new InputValidationError("$fieldName is required."); } elseif (strlen($raw) < $minLength) { throw new InputValidationError("$fieldName is too short (minimum $minLength characters)."); - } elseif (strlen($raw) > 100) { + } elseif (strlen($raw) > $maxLength) { throw new InputValidationError("$fieldName is too long (maximum $maxLength characters)."); } return $raw; @@ -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 ($username !== null && 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/Config/AppConfig.php b/src/Config/AppConfig.php new file mode 100644 index 0000000..ce94e39 --- /dev/null +++ b/src/Config/AppConfig.php @@ -0,0 +1,105 @@ +timezone = ini_get('date.timezone') ?: 'UTC'; + } + + public static function createFromArray(array $configArray): self + { + $config = new self(); + + foreach ($configArray as $key => $value) { + assert(property_exists($config, $key)); + + if ($value !== null) { + $config->$key = $value; + } + } + + return $config; + } + + public function getAppTitle(): string + { + return $this->appTitle; + } + + public function getAppEnvironment(): string + { + return $this->environment; + } + + public function isDebugMode(): bool + { + return $this->debug; + } + + public function getTimezone(): string + { + return $this->timezone; + } + + public function getDateTimeFormat(): string + { + // Specify datetime format (https://www.php.net/manual/en/datetime.format.php) + // Default value "r": RFC 2822 formatted date (Thu, 21 Dec 2000 16:01:07 +0200) + return $this->dateTimeFormat; + } + + public function getTwigCacheDir(): ?string + { + if (empty($this->twigCacheDir)) { + return null; + } elseif (substr($this->twigCacheDir, 0, 1) === '/') { + // Absolute path + return $this->twigCacheDir; + } else { + // Relative path + return ROOT_DIR . '/' . $this->twigCacheDir; + } + } + + public function getTwigSettings(): array + { + return [ + 'cache' => $this->getTwigCacheDir() ?: false, + 'debug' => $this->isDebugMode(), + 'strict_variables' => $this->isDebugMode(), + ]; + } + + public function getDatabaseSettings(): array + { + return [ + 'host' => $this->databaseHost, + 'port' => $this->databasePort, + 'dbname' => $this->databaseName, + 'username' => $this->databaseUsername, + 'password' => $this->databasePassword, + ]; + } +} diff --git a/src/Config/Loaders/AutoConfigLoader.php b/src/Config/Loaders/AutoConfigLoader.php new file mode 100644 index 0000000..6ba336c --- /dev/null +++ b/src/Config/Loaders/AutoConfigLoader.php @@ -0,0 +1,28 @@ +configLoader = new YamlConfigLoader($yamlFilePath); + } else { + $this->configLoader = new EnvConfigLoader(); + } + } + + public function loadConfig(): AppConfig + { + return $this->configLoader->loadConfig(); + } +} diff --git a/src/Config/Loaders/ConfigLoaderInterface.php b/src/Config/Loaders/ConfigLoaderInterface.php new file mode 100644 index 0000000..ed8ebe6 --- /dev/null +++ b/src/Config/Loaders/ConfigLoaderInterface.php @@ -0,0 +1,11 @@ + getenv('APP_TITLE') ?: null, + 'environment' => getenv('APP_ENV') ?: null, + 'debug' => getenv('APP_DEBUG') === 'true' ? true : null, + 'timezone' => getenv('APP_TIMEZONE') ?: null, + 'dateTimeFormat' => getenv('APP_DATE_TIME_FORMAT') ?: null, + + // Twig settings + 'twigCacheDir' => getenv('TWIG_CACHE_DIR') ?: null, + + // Database settings + 'databaseHost' => getenv('DB_HOST') ?: null, + 'databasePort' => (int)getenv('DB_PORT') ?: null, + 'databaseName' => getenv('DB_DATABASE') ?: null, + 'databaseUsername' => getenv('DB_USERNAME') ?: null, + 'databasePassword' => getenv('DB_PASSWORD') ?: null, + ]); + } +} diff --git a/src/Config/Loaders/YamlConfigLoader.php b/src/Config/Loaders/YamlConfigLoader.php new file mode 100644 index 0000000..61b5cb1 --- /dev/null +++ b/src/Config/Loaders/YamlConfigLoader.php @@ -0,0 +1,48 @@ +filePath = $filePath; + } + + public function loadConfig(): AppConfig + { + // Parse yml config file + $parsedConfig = yaml_parse_file($this->filePath); + + // Check datatypes + assert(is_array($parsedConfig)); + assert(!isset($parsedConfig['twig']) || is_array($parsedConfig['twig'])); + assert(!isset($parsedConfig['database']) || is_array($parsedConfig['database'])); + + return AppConfig::createFromArray([ + // App settings + 'appTitle' => $parsedConfig['appTitle'] ?? null, + 'environment' => $parsedConfig['environment'] ?? null, + 'debug' => isset($parsedConfig['debug']) ? (bool)$parsedConfig['debug'] : null, + 'timezone' => $parsedConfig['timezone'] ?? null, + 'dateTimeFormat' => $parsedConfig['dateTimeFormat'] ?? null, + + // Twig settings + 'twigCacheDir' => $parsedConfig['twig']['cacheDir'] ?? null, + + // Database settings + 'databaseHost' => $parsedConfig['database']['host'] ?? null, + 'databasePort' => isset($parsedConfig['database']['port']) ? (int)$parsedConfig['database']['port'] : null, + 'databaseName' => $parsedConfig['database']['name'] ?? null, + 'databaseUsername' => $parsedConfig['database']['username'] ?? null, + 'databasePassword' => $parsedConfig['database']['password'] ?? null, + ]); + } +} diff --git a/src/Dependencies.php b/src/Dependencies.php index 94c0d64..30dd467 100644 --- a/src/Dependencies.php +++ b/src/Dependencies.php @@ -7,6 +7,7 @@ use DI\Container; use MailAccountAdmin\Common\PasswordHelper; use MailAccountAdmin\Common\SessionHelper; use MailAccountAdmin\Common\UserHelper; +use MailAccountAdmin\Config\AppConfig; use MailAccountAdmin\Frontend\Accounts\AccountController; use MailAccountAdmin\Frontend\Accounts\AccountHandler; use MailAccountAdmin\Frontend\Domains\DomainController; @@ -23,43 +24,43 @@ use Twig\Extension\CoreExtension as TwigCoreExtension; class Dependencies { - public const SETTINGS = 'settings'; + public const CONFIG = 'config'; public const TWIG = 'view'; private const TWIG_TEMPLATE_DIR = __DIR__ . '/../templates'; public const DATABASE = 'database'; - public static function createContainer(Settings $settings): Container + public static function createContainer(AppConfig $config): Container { $container = new Container(); - // App settings - $container->set(self::SETTINGS, $settings); + // App configuration + $container->set(self::CONFIG, $config); // App information $container->set(AppInfo::class, function (ContainerInterface $c) { - /** @var Settings $settings */ - $settings = $c->get(self::SETTINGS); - $versionHelper = new VersionHelper(); + /** @var AppConfig $config */ + $config = $c->get(self::CONFIG); + $versionHelper = new VersionHelper($config); return new AppInfo( - $settings->getAppTitle(), + $config->getAppTitle(), $versionHelper->getAppVersion(), ); }); // Twig template engine $container->set(self::TWIG, function (ContainerInterface $c) { - /** @var Settings $settings */ - $settings = $c->get(self::SETTINGS); + /** @var AppConfig $config */ + $config = $c->get(self::CONFIG); // Create Twig view - $twig = Twig::create(self::TWIG_TEMPLATE_DIR, $settings->getTwigSettings()); + $twig = Twig::create(self::TWIG_TEMPLATE_DIR, $config->getTwigSettings()); // Set default date format /** @var TwigCoreExtension $coreExtension */ $coreExtension = $twig->getEnvironment()->getExtension(TwigCoreExtension::class); - $coreExtension->setDateFormat($settings->getDateFormat()); - $coreExtension->setTimezone($settings->getTimezone()); + $coreExtension->setDateFormat($config->getDateTimeFormat()); + $coreExtension->setTimezone($config->getTimezone()); // Add app information to globals $appInfo = $c->get(AppInfo::class); @@ -71,9 +72,9 @@ class Dependencies // Database connection $container->set(self::DATABASE, function (ContainerInterface $c) { - /** @var Settings $settings */ - $settings = $c->get(self::SETTINGS); - $dbSettings = $settings->getDatabaseSettings(); + /** @var AppConfig $config */ + $config = $c->get(self::CONFIG); + $dbSettings = $config->getDatabaseSettings(); return new PDO( "mysql:dbname={$dbSettings['dbname']};host={$dbSettings['host']};port={$dbSettings['port']}", @@ -108,7 +109,7 @@ class Dependencies }); // Helper classes - $container->set(SessionHelper::class, function (ContainerInterface $c) { + $container->set(SessionHelper::class, function () { return new SessionHelper(); }); @@ -120,7 +121,7 @@ class Dependencies ); }); - $container->set(PasswordHelper::class, function (ContainerInterface $c) { + $container->set(PasswordHelper::class, function () { return new PasswordHelper(); }); 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/Middlewares.php b/src/Middlewares.php index 375e481..f60987f 100644 --- a/src/Middlewares.php +++ b/src/Middlewares.php @@ -4,14 +4,15 @@ declare(strict_types=1); namespace MailAccountAdmin; use MailAccountAdmin\Auth\AuthMiddleware; +use MailAccountAdmin\Config\AppConfig; use Slim\App; use Slim\Views\TwigMiddleware; class Middlewares { - public static function setMiddlewares(App $app, Settings $settings): void + public static function setMiddlewares(App $app, AppConfig $config): void { - $displayErrorDetails = $settings->isDebugMode(); + $displayErrorDetails = $config->isDebugMode(); $app->addErrorMiddleware($displayErrorDetails, true, true); $app->add(new AuthMiddleware()); 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..9a4d615 100644 --- a/src/Repositories/AliasRepository.php +++ b/src/Repositories/AliasRepository.php @@ -11,11 +11,11 @@ class AliasRepository extends BaseRepository { public function fetchAliasesForUserId(int $userId): array { - $statement = $this->pdo->prepare('SELECT * FROM mail_aliases WHERE user_id = :user_id ORDER BY mail_address'); + $statement = $this->pdo->prepare('SELECT * FROM mail_aliases WHERE user_id = :user_id ORDER BY wildcard_priority, mail_address'); $statement->execute(['user_id' => $userId]); $rows = $statement->fetchAll(PDO::FETCH_ASSOC); - // Create Account models from rows + // Create Alias models from rows $aliasList = []; foreach ($rows as $row) { $aliasList[] = Alias::createFromArray($row); @@ -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/src/Settings.php b/src/Settings.php deleted file mode 100644 index 4d1b989..0000000 --- a/src/Settings.php +++ /dev/null @@ -1,49 +0,0 @@ - getenv('TWIG_CACHE_DIR') ?: false, - 'debug' => $this->isDebugMode(), - 'strict_variables' => getenv('TWIG_STRICT') === 'true', - ]; - } - - public function getDatabaseSettings(): array - { - return [ - 'host' => getenv('DB_HOST') ?: 'localhost', - 'port' => getenv('DB_PORT') ?: 3306, - 'dbname' => getenv('DB_DATABASE') ?: '', - 'username' => getenv('DB_USER') ?: '', - 'password' => getenv('DB_PASSWORD') ?: '', - ]; - } -} diff --git a/src/VersionHelper.php b/src/VersionHelper.php index 4fd370a..1223bce 100644 --- a/src/VersionHelper.php +++ b/src/VersionHelper.php @@ -3,16 +3,18 @@ declare(strict_types=1); namespace MailAccountAdmin; +use MailAccountAdmin\Config\AppConfig; + class VersionHelper { private string $version; - public function __construct() + public function __construct(AppConfig $config) { $version = $this->loadFromVersionFile(); if (!empty($version)) { $this->version = $version; - } elseif ($this->inDevelopmentMode()) { + } elseif ($config->getAppEnvironment() === 'development') { $this->version = '[dev version]'; } else { $this->version = '[undefined version]'; @@ -34,9 +36,4 @@ class VersionHelper } return null; } - - private function inDevelopmentMode(): bool - { - return getenv('APP_ENV') === 'development'; - } } 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 %} diff --git a/templates/base.html.twig b/templates/base.html.twig index c364b6d..bd0d091 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -12,7 +12,11 @@

{{ app_info.getTitle() }}

- {{ app_info.getVersion() }} + {% if logged_in | default() %} + {{ app_info.getVersion() }} + {% else %} + {{ app_info.getVersion() }} + {% endif %}