Use Symfony Mailer instead of SwiftMailer because they killed the latter for no reason.

This commit is contained in:
flash 2022-02-15 22:14:38 +00:00
parent f9181fc6f9
commit 2c57107eb9
5 changed files with 608 additions and 153 deletions

View file

@ -1,12 +1,12 @@
{
"require": {
"twig/twig": "^3.0",
"swiftmailer/swiftmailer": "~6.0",
"erusev/parsedown": "~1.6",
"geoip2/geoip2": "~2.0",
"jublonet/codebird-php": "^3.1",
"chillerlan/php-qrcode": "^3.0",
"whichbrowser/parser": "^2.0"
"whichbrowser/parser": "^2.0",
"symfony/mailer": "^6.0"
},
"autoload": {
"classmap": [

633
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "74b2bbca53906a66f78c659eb5d47a89",
"content-hash": "4d6120e23f666aa6c5e45c2e2709199f",
"packages": [
{
"name": "chillerlan/php-qrcode",
@ -854,42 +854,204 @@
"time": "2016-08-06T20:24:11+00:00"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v6.3.0",
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
"reference": "8a5d5072dca8f48460fce2f4131fcc495eec654c"
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8a5d5072dca8f48460fce2f4131fcc495eec654c",
"reference": "8a5d5072dca8f48460fce2f4131fcc495eec654c",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"egulias/email-validator": "^2.0|^3.1",
"php": ">=7.0.0",
"symfony/polyfill-iconv": "^1.0",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"symfony/phpunit-bridge": "^4.4|^5.4"
},
"suggest": {
"ext-intl": "Needed to support internationalized email addresses"
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"files": [
"lib/swift_required.php"
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/event-dispatcher.git",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\EventDispatcher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Standard interfaces for event handling.",
"keywords": [
"events",
"psr",
"psr-14"
],
"support": {
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/log",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.0"
},
"time": "2021-07-14T16:46:02+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v6.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "6472ea2dd415e925b90ca82be64b8bc6157f3934"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/6472ea2dd415e925b90ca82be64b8bc6157f3934",
"reference": "6472ea2dd415e925b90ca82be64b8bc6157f3934",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/event-dispatcher-contracts": "^2|^3"
},
"conflict": {
"symfony/dependency-injection": "<5.4"
},
"provide": {
"psr/event-dispatcher-implementation": "1.0",
"symfony/event-dispatcher-implementation": "2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/config": "^5.4|^6.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/error-handler": "^5.4|^6.0",
"symfony/expression-language": "^5.4|^6.0",
"symfony/http-foundation": "^5.4|^6.0",
"symfony/service-contracts": "^1.1|^2|^3",
"symfony/stopwatch": "^5.4|^6.0"
},
"suggest": {
"symfony/dependency-injection": "",
"symfony/http-kernel": ""
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\EventDispatcher\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
@ -897,37 +1059,269 @@
"MIT"
],
"authors": [
{
"name": "Chris Corbyn"
},
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Swiftmailer, free feature-rich PHP mailer",
"homepage": "https://swiftmailer.symfony.com",
"keywords": [
"email",
"mail",
"mailer"
],
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"issues": "https://github.com/swiftmailer/swiftmailer/issues",
"source": "https://github.com/swiftmailer/swiftmailer/tree/v6.3.0"
"source": "https://github.com/symfony/event-dispatcher/tree/v6.0.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer",
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"abandoned": "symfony/mailer",
"time": "2021-10-18T15:26:12+00:00"
"time": "2022-01-02T09:55:41+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
"reference": "aa5422287b75594b90ee9cd807caf8f0df491385"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/aa5422287b75594b90ee9cd807caf8f0df491385",
"reference": "aa5422287b75594b90ee9cd807caf8f0df491385",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"psr/event-dispatcher": "^1"
},
"suggest": {
"symfony/event-dispatcher-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.0-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\EventDispatcher\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to dispatching event",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.0.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-07-15T12:33:35+00:00"
},
{
"name": "symfony/mailer",
"version": "v6.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "d958befe7dbee9d2b2157ef6dfa9b103efa94f82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/d958befe7dbee9d2b2157ef6dfa9b103efa94f82",
"reference": "d958befe7dbee9d2b2157ef6dfa9b103efa94f82",
"shasum": ""
},
"require": {
"egulias/email-validator": "^2.1.10|^3",
"php": ">=8.0.2",
"psr/event-dispatcher": "^1",
"psr/log": "^1|^2|^3",
"symfony/event-dispatcher": "^5.4|^6.0",
"symfony/mime": "^5.4|^6.0",
"symfony/service-contracts": "^1.1|^2|^3"
},
"conflict": {
"symfony/http-kernel": "<5.4"
},
"require-dev": {
"symfony/http-client-contracts": "^1.1|^2|^3",
"symfony/messenger": "^5.4|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mailer\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v6.0.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-01-02T09:55:41+00:00"
},
{
"name": "symfony/mime",
"version": "v6.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "2cd9601efd040e56f43360daa68f3c6b0534923a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/2cd9601efd040e56f43360daa68f3c6b0534923a",
"reference": "2cd9601efd040e56f43360daa68f3c6b0534923a",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<5.4"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/property-access": "^5.4|^6.0",
"symfony/property-info": "^5.4|^6.0",
"symfony/serializer": "^5.4|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mime\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows manipulating MIME messages",
"homepage": "https://symfony.com",
"keywords": [
"mime",
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v6.0.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-01-02T09:55:41+00:00"
},
{
"name": "symfony/polyfill-ctype",
@ -1011,89 +1405,6 @@
],
"time": "2021-10-20T20:35:02+00:00"
},
{
"name": "symfony/polyfill-iconv",
"version": "v1.24.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
"reference": "f1aed619e28cb077fc83fac8c4c0383578356e40"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/f1aed619e28cb077fc83fac8c4c0383578356e40",
"reference": "f1aed619e28cb077fc83fac8c4c0383578356e40",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-iconv": "*"
},
"suggest": {
"ext-iconv": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Iconv\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Iconv extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"iconv",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-iconv/tree/v1.24.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-01-04T09:04:05+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.24.0",
@ -1424,6 +1735,88 @@
],
"time": "2021-05-27T09:17:38+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/36715ebf9fb9db73db0cb24263c79077c6fe8603",
"reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"psr/container": "^2.0"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
"suggest": {
"symfony/service-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.0-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.0.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-11-04T17:53:12+00:00"
},
{
"name": "twig/twig",
"version": "v3.3.8",

View file

@ -30,6 +30,23 @@ $canEditPerms = $canEdit && perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ
$permissions = manage_perms_list(perms_get_user_raw($userId));
if(CSRF::validateRequest() && $canEdit) {
if(!empty($_POST['send_test_email'])) {
if(!$currentUser->isSuper()) {
$notices[] = 'You must be a super user to do this.';
} elseif(!is_string($_POST['send_test_email']) || $_POST['send_test_email'] !== 'yes_send_it') {
$notices[] = 'Invalid request thing shut the fuck up.';
} else {
$testMail = Mailer::sendMessage(
[$userInfo->getEMailAddress() => $userInfo->getUsername()],
'Flashii Test E-mail',
'You were sent this e-mail to validate if you can receive e-mails from Flashii. You may discard it.'
);
if(!$testMail)
$notices[] = 'Failed to send test e-mail.';
}
}
if(!empty($_POST['roles']) && is_array($_POST['roles']) && array_test($_POST['roles'], 'ctype_digit')) {
// Fetch existing roles
$existingRoles = $userInfo->getRoles();

View file

@ -2,53 +2,83 @@
namespace Misuzu;
use InvalidArgumentException;
use Swift_Message;
use Swift_NullTransport;
use Swift_SmtpTransport;
use Symfony\Component\Mime\Email as SymfonyMessage;
use Symfony\Component\Mime\Address as SymfonyAddress;
use Symfony\Component\Mailer\Mailer as SymfonyMailer;
use Symfony\Component\Mailer\Transport as SymfonyTransport;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
final class Mailer {
private const TEMPLATE_PATH = MSZ_ROOT . '/config/emails/%s.txt';
private static $mailer = null;
private static $mailerClass = '';
private static $mailerConfig = [];
private static $dsn = 'null://null';
private static $transport = null;
public static function init(string $method, array $config): void {
self::$mailerClass = $method === 'smtp' ? Swift_SmtpTransport::class : Swift_NullTransport::class;
self::$mailerConfig = $config;
}
public static function getMailer() {
if(self::$mailer === null) {
self::$mailer = new self::$mailerClass;
if(self::$mailerClass === Swift_SmtpTransport::class) {
self::$mailer->setHost(self::$mailerConfig['host'] ?? '');
self::$mailer->setPort(self::$mailerConfig['port'] ?? 25);
self::$mailer->setUsername(self::$mailerConfig['username'] ?? '');
self::$mailer->setPassword(self::$mailerConfig['password'] ?? '');
self::$mailer->setEncryption(self::$mailerConfig['encryption'] ?? '');
}
if($method !== 'smtp') {
self::$dsn = 'null://null';
return;
}
return self::$mailer;
$dsn = $method;
// guess this is applied automatically based on the port?
//if(!empty($config['encryption']))
// $dsn .= 't';
$dsn .= '://';
if(!empty($config['username'])) {
$dsn .= $config['username'];
if(!empty($config['password']))
$dsn .= ':' . $config['password'];
$dsn .= '@';
}
$dsn .= $config['host'] ?? '';
$dsn .= ':';
$dsn .= $config['port'] ?? 25;
self::$dsn = $dsn;
}
public static function getTransport() {
if(self::$transport === null)
self::$transport = SymfonyTransport::fromDsn(self::$dsn);
return self::$transport;
}
public static function sendMessage(array $to, string $subject, string $contents, bool $bcc = false): bool {
$message = new Swift_Message($subject);
foreach($to as $email => $name) {
$to = new SymfonyAddress($email, $name);
break;
}
$message->setFrom([
$config['sender_addr'] ?? 'sys@flashii.net' => $config['sender_name'] ?? 'Flashii',
]);
$message = new SymfonyMessage;
$message->from(new SymfonyAddress(
$config['sender_addr'] ?? 'sys@flashii.net',
$config['sender_name'] ?? 'Flashii'
));
if($bcc)
$message->setBcc($to);
$message->bcc($to);
else
$message->setTo($to);
$message->to($to);
$message->setBody($contents);
$message->subject(trim($subject));
$message->text(trim($contents));
return self::getMailer()->send($message);
try {
self::getTransport()->send($message);
return true;
} catch(TransportExceptionInterface $ex) {
if(MSZ_DEBUG)
throw $ex;
return false;
}
}
public static function template(string $name, array $vars = []): array {

View file

@ -1,7 +1,7 @@
{% extends 'manage/users/master.twig' %}
{% from 'macros.twig' import container_title %}
{% from 'manage/macros.twig' import permissions_table %}
{% from '_layout/input.twig' import input_csrf, input_text, input_checkbox, input_file, input_select, input_colour %}
{% from '_layout/input.twig' import input_csrf, input_text, input_checkbox, input_file, input_select, input_colour, input_hidden %}
{% set site_link = url('user-profile', {'user': user_info.id}) %}
@ -132,6 +132,21 @@
{% endif %}
</form>
{% if current_user.super %}
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
{{ container_title('Send test e-mail to ' ~ user_info.username ~ ' (' ~ user_info.id ~ ')') }}
<p style="font-size: 2em; line-height: 2em; text-align: center; font-weight: 700;">DO NOT ABUSE THIS SHIT</p>
{{ input_csrf() }}
{{ input_hidden('send_test_email', 'yes_send_it') }}
<div class="manage__user__buttons">
<button class="input__button manage__user__button">Send</button>
</div>
</form>
{% endif %}
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
{{ container_title('Permissions for ' ~ user_info.username ~ ' (' ~ user_info.id ~ ')') }}