├── .dockerignore ├── .editorconfig ├── .env ├── .env.test ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── integration.yml │ ├── mkdocs.yml │ ├── psalm.yml │ └── security-check.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── UPGRADE.md ├── assets ├── bootstrap.js ├── controllers.json ├── controllers │ └── hello_controller.js ├── css │ └── app.css ├── images │ ├── diff-added.svg │ ├── gist-secret.svg │ ├── key.svg │ ├── logo_small.png │ ├── mail-read.svg │ └── tools.svg └── js │ └── app.js ├── behat.yml ├── bin ├── console ├── github-release.sh ├── local-php-security-checker ├── user-accounts ├── user-aliases └── user-vouchers ├── components ├── require-built.js ├── require.config.js ├── require.css └── require.js ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── assets.yaml │ ├── cache.yaml │ ├── debug.yaml │ ├── doctrine.yaml │ ├── framework.yaml │ ├── mailer.yaml │ ├── monolog.yaml │ ├── mopa_bootstrap.yaml │ ├── nelmio_security.yaml │ ├── routing.yaml │ ├── scheb_2fa.yaml │ ├── security.yaml │ ├── sonata_admin.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── validator.yaml │ └── web_profiler.yaml ├── preload.php ├── reserved_names.txt ├── routes │ ├── annotations.yaml │ ├── framework.yaml │ ├── scheb_2fa.yaml │ ├── security.yaml │ ├── sonata_admin.yaml │ ├── twig.yaml │ └── web_profiler.yaml ├── services.yaml └── services_test.yaml ├── contrib └── userli-dovecot-adapter.lua ├── default_translations ├── de │ ├── messages.de.yml │ └── validators.de.yml ├── en │ ├── messages.en.yml │ └── validators.en.yml ├── es │ ├── messages.es.yml │ └── validators.es.yml ├── fr │ ├── messages.fr.yml │ └── validators.fr.yml ├── gsw │ └── messages.gsw.yml ├── it │ └── messages.it.yml ├── nb │ ├── messages.nb.yml │ └── validators.nb.yml ├── pt │ ├── messages.pt.yml │ └── validators.pt.yml └── zh_Hant │ ├── messages.zh_Hant.yml │ └── validators.zh_Hant.yml ├── docker-compose.yml ├── docker ├── dovecot │ ├── Dockerfile │ ├── conf.d │ │ └── auth-lua.conf.ext │ └── dovecot.conf └── userli │ ├── Dockerfile │ └── apache.conf ├── docs ├── assets │ ├── images │ │ ├── account.png │ │ ├── admin.png │ │ ├── alias.png │ │ ├── index.png │ │ ├── manage_recovery_token.png │ │ ├── new_recovery_token.png │ │ ├── recovery.png │ │ └── voucher.png │ └── logo.png ├── development │ ├── code_of_conduct.md │ ├── coding_style.md │ ├── icons.md │ ├── index.md │ ├── logs.md │ ├── release.md │ └── tests.md ├── features │ ├── integrations.md │ ├── mail_crypt.md │ └── wkd.md ├── index.md ├── installation │ ├── code.md │ ├── commands.md │ ├── configuration.md │ ├── customize.md │ ├── database.md │ ├── dovecot.md │ ├── finalize.md │ ├── index.md │ └── webserver.md ├── screenshots │ └── index.md └── update │ └── index.md ├── features ├── admin.feature ├── init.feature ├── language.feature ├── login.feature ├── mailCryptCommand.feature ├── openpgp.feature ├── quotaCommand.feature ├── recovery.feature ├── registration.feature ├── user.feature └── voucherCreateCommand.feature ├── mkdocs.yml ├── package.json ├── phpunit.xml ├── public ├── .htaccess ├── favicon.ico ├── index.php └── robots.txt ├── rector.php ├── sonar-project.properties ├── src ├── .htaccess ├── Admin │ ├── Admin.php │ ├── AliasAdmin.php │ ├── DomainAdmin.php │ ├── ReservedNameAdmin.php │ ├── UserAdmin.php │ └── VoucherAdmin.php ├── Block │ └── StatisticsBlockService.php ├── Builder │ ├── AliasCreatedMessageBuilder.php │ ├── MenuBuilder.php │ ├── RecoveryProcessMessageBuilder.php │ └── WelcomeMessageBuilder.php ├── Command │ ├── AbstractUsersCommand.php │ ├── AdminPasswordCommand.php │ ├── AliasDeleteCommand.php │ ├── MetricsCommand.php │ ├── MuninAccountCommand.php │ ├── MuninAliasCommand.php │ ├── MuninVoucherCommand.php │ ├── OpenPgpDeleteKeyCommand.php │ ├── OpenPgpExportKeysCommand.php │ ├── OpenPgpImportKeyCommand.php │ ├── OpenPgpShowKeyCommand.php │ ├── ReportWeeklyCommand.php │ ├── ReservedNamesImportCommand.php │ ├── UsersDeleteCommand.php │ ├── UsersListCommand.php │ ├── UsersMailCryptCommand.php │ ├── UsersQuotaCommand.php │ ├── UsersRegistrationMailCommand.php │ ├── UsersRemoveCommand.php │ ├── UsersResetCommand.php │ ├── UsersRestoreCommand.php │ ├── VoucherCountCommand.php │ ├── VoucherCreateCommand.php │ └── VoucherUnlinkCommand.php ├── Controller │ ├── AccountController.php │ ├── AliasCRUDController.php │ ├── AliasController.php │ ├── DeleteController.php │ ├── DovecotController.php │ ├── ErrorController.php │ ├── InitController.php │ ├── KeycloakController.php │ ├── OpenPGPController.php │ ├── PostfixController.php │ ├── RecoveryController.php │ ├── RegistrationController.php │ ├── RetentionController.php │ ├── RoundcubeController.php │ ├── SecurityController.php │ ├── StartController.php │ ├── TwofactorController.php │ ├── UserCRUDController.php │ └── VoucherController.php ├── Creator │ ├── AbstractCreator.php │ ├── AliasCreator.php │ ├── DomainCreator.php │ ├── ReservedNameCreator.php │ └── VoucherCreator.php ├── DataFixtures │ ├── AbstractUserData.php │ ├── LoadAliasData.php │ ├── LoadDomainData.php │ ├── LoadRandomAliasData.php │ ├── LoadRandomUserData.php │ ├── LoadReservedNameData.php │ ├── LoadUserData.php │ └── LoadVoucherData.php ├── Dto │ ├── DovecotPassdbDto.php │ ├── KeycloakUserValidateDto.php │ └── RetentionTouchUserDto.php ├── Entity │ ├── Alias.php │ ├── Domain.php │ ├── Filter │ │ └── DomainFilter.php │ ├── OpenPgpKey.php │ ├── ReservedName.php │ ├── SoftDeletableInterface.php │ ├── User.php │ └── Voucher.php ├── EntityListener │ └── UserChangedListener.php ├── Enum │ ├── MailCrypt.php │ └── Roles.php ├── Event │ ├── AliasCreatedEvent.php │ ├── DomainCreatedEvent.php │ ├── Events.php │ ├── LoginEvent.php │ ├── RandomAliasCreatedEvent.php │ ├── RecoveryProcessEvent.php │ ├── UserCreatedEvent.php │ ├── UserDeletedEvent.php │ └── UserEvent.php ├── EventListener │ ├── AliasCreationListener.php │ ├── BeforeRequestListener.php │ ├── DomainCreationListener.php │ ├── LocaleListener.php │ ├── LoginListener.php │ ├── LogoutListener.php │ ├── RandomAliasCreationListener.php │ ├── RecoveryProcessListener.php │ ├── RegistrationListener.php │ ├── TwigGlobalListener.php │ ├── UserCreatedListener.php │ └── UserDeletedListener.php ├── Exception │ ├── MultipleGpgKeysForUserException.php │ ├── NoGpgDataException.php │ ├── NoGpgKeyForUserException.php │ └── ValidationException.php ├── Factory │ ├── AliasFactory.php │ ├── DomainFactory.php │ ├── ReservedNameFactory.php │ └── VoucherFactory.php ├── Form │ ├── AliasDeleteType.php │ ├── CustomAliasCreateType.php │ ├── DataTransformer │ │ ├── OptionalDomainEmailTransformer.php │ │ └── TextToEmailTransformer.php │ ├── DomainCreateType.php │ ├── Model │ │ ├── AliasCreate.php │ │ ├── Delete.php │ │ ├── DomainCreate.php │ │ ├── OpenPgpKey.php │ │ ├── PasswordChange.php │ │ ├── PlainPassword.php │ │ ├── RecoveryProcess.php │ │ ├── RecoveryResetPassword.php │ │ ├── RecoveryToken.php │ │ ├── RecoveryTokenAck.php │ │ ├── Registration.php │ │ ├── Twofactor.php │ │ ├── TwofactorBackupAck.php │ │ ├── TwofactorConfirm.php │ │ └── VoucherCreate.php │ ├── OpenPgpDeleteType.php │ ├── OpenPgpKeyType.php │ ├── PasswordChangeType.php │ ├── PlainPasswordType.php │ ├── RandomAliasCreateType.php │ ├── RecoveryProcessType.php │ ├── RecoveryResetPasswordType.php │ ├── RecoveryTokenAckType.php │ ├── RecoveryTokenType.php │ ├── RegistrationType.php │ ├── TwofactorBackupAckType.php │ ├── TwofactorConfirmType.php │ ├── TwofactorType.php │ ├── UserDeleteType.php │ └── VoucherCreateType.php ├── Guesser │ └── DomainGuesser.php ├── Handler │ ├── AliasHandler.php │ ├── CryptoSecretHandler.php │ ├── DeleteHandler.php │ ├── MailCryptKeyHandler.php │ ├── MailHandler.php │ ├── PasswordStrengthHandler.php │ ├── RecoveryTokenHandler.php │ ├── RegistrationHandler.php │ ├── SuspiciousChildrenHandler.php │ ├── UserAuthenticationHandler.php │ ├── UserRegistrationInfoHandler.php │ ├── UserRestoreHandler.php │ ├── VoucherHandler.php │ ├── WebhookHandler.php │ └── WkdHandler.php ├── Helper │ ├── AdminPasswordUpdater.php │ ├── MenuHelper.php │ ├── PasswordGenerator.php │ ├── PasswordUpdater.php │ └── RandomStringGenerator.php ├── Importer │ ├── GpgKeyImporter.php │ └── OpenPgpKeyImporterInterface.php ├── Kernel.php ├── Model │ ├── CryptoSecret.php │ └── MailCryptKeyPair.php ├── Remover │ └── VoucherRemover.php ├── Repository │ ├── AliasRepository.php │ ├── DomainRepository.php │ ├── OpenPgpKeyRepository.php │ ├── ReservedNameRepository.php │ ├── UserRepository.php │ └── VoucherRepository.php ├── Security │ ├── ApiAccessTokenHandler.php │ ├── Encoder │ │ └── LegacyPasswordHasher.php │ ├── UserChecker.php │ └── UserProvider.php ├── Sender │ ├── AliasCreatedMessageSender.php │ ├── RecoveryProcessMessageSender.php │ └── WelcomeMessageSender.php ├── Traits │ ├── AliasAwareTrait.php │ ├── CreationTimeTrait.php │ ├── DeleteTrait.php │ ├── DomainAwareTrait.php │ ├── DomainGuesserAwareTrait.php │ ├── EmailTrait.php │ ├── IdTrait.php │ ├── InvitationVoucherTrait.php │ ├── LastLoginTimeTrait.php │ ├── MailCryptEnabledTrait.php │ ├── MailCryptPublicKeyTrait.php │ ├── MailCryptSecretBoxTrait.php │ ├── NameTrait.php │ ├── OpenPgpKeyTrait.php │ ├── PasswordTrait.php │ ├── PasswordVersionTrait.php │ ├── PlainMailCryptPrivateKeyTrait.php │ ├── PlainPasswordTrait.php │ ├── PlainRecoveryTokenTrait.php │ ├── PrivateKeyTrait.php │ ├── PublicKeyTrait.php │ ├── QuotaTrait.php │ ├── RandomTrait.php │ ├── RecoverySecretBoxTrait.php │ ├── RecoveryStartTimeTrait.php │ ├── RecoveryTokenTrait.php │ ├── SaltTrait.php │ ├── TwofactorBackupCodeTrait.php │ ├── TwofactorTrait.php │ ├── UpdatedTimeTrait.php │ └── UserAwareTrait.php ├── Validator │ ├── Constraints │ │ ├── EmailAddress.php │ │ ├── EmailAddressValidator.php │ │ ├── EmailDomain.php │ │ ├── EmailDomainValidator.php │ │ ├── EmailLength.php │ │ ├── EmailLengthValidator.php │ │ ├── Lowercase.php │ │ ├── LowercaseValidator.php │ │ ├── PasswordPolicy.php │ │ ├── TotpSecret.php │ │ ├── VoucherExists.php │ │ ├── VoucherExistsValidator.php │ │ ├── VoucherUser.php │ │ └── VoucherUserValidator.php │ ├── PasswordPolicyValidator.php │ └── TotpSecretValidator.php └── Voter │ └── DomainVoter.php ├── symfony.lock ├── templates ├── Admin │ ├── standard_layout.html.twig │ └── user_block.html.twig ├── Alias │ └── delete.html.twig ├── Block │ └── block_statistics.html.twig ├── Email │ ├── suspicious_children.twig │ └── weekly_report.twig ├── Exception │ └── show.html.twig ├── Footer │ └── logged_in_footer.twig ├── Form │ └── fields.html.twig ├── Init │ ├── domain.html.twig │ └── user.html.twig ├── OpenPgp │ └── delete.html.twig ├── Recovery │ ├── new_recovery_token.html.twig │ ├── recovery.html.twig │ ├── recovery_new.html.twig │ ├── recovery_started.html.twig │ ├── recovery_token.html.twig │ ├── recovery_token_notes.html.twig │ ├── reset_password.html.twig │ └── show_recovery_token.html.twig ├── Registration │ ├── closed.html.twig │ ├── recovery_token.html.twig │ ├── register.html.twig │ └── welcome.html.twig ├── Security │ ├── 2fa_form.html.twig │ └── login.html.twig ├── Start │ ├── account.html.twig │ ├── aliases.html.twig │ ├── change_password.html.twig │ ├── delete_account.html.twig │ ├── index.html.twig │ ├── index_anonymous.html.twig │ ├── index_spam.html.twig │ ├── openpgp.html.twig │ ├── recovery_token.html.twig │ ├── twofactor.html.twig │ └── vouchers.html.twig ├── Twofactor │ └── twofactor_notes.html.twig ├── User │ ├── delete.html.twig │ ├── recovery_token.html.twig │ ├── twofactor.html.twig │ ├── twofactor_backup_ack.html.twig │ └── twofactor_enable.html.twig ├── _locale_switcher.html.twig └── base.html.twig ├── tests ├── Behat │ └── FeatureContext.php ├── Builder │ ├── AliasCreatedMessageBuilderTest.php │ ├── MenuBuilderTest.php │ ├── RecoveryProcessMessageBuilderTest.php │ └── WelcomeMessageBuilderTest.php ├── Command │ ├── AdminPasswordCommandTest.php │ ├── AliasDeleteCommandTest.php │ ├── MetricsCommandTest.php │ ├── MuninAccountCommandTest.php │ ├── MuninVoucherCommandTest.php │ ├── OpenPgpDeleteKeyCommandTest.php │ ├── OpenPgpExportKeysCommandTest.php │ ├── OpenPgpImportKeyCommandTest.php │ ├── OpenPgpShowKeyCommandTest.php │ ├── ReservedNamesImportCommandTest.php │ ├── UsersDeleteCommandTest.php │ ├── UsersResetCommandTest.php │ ├── UsersRestoreCommandTest.php │ ├── VoucherCountCommandTest.php │ ├── VoucherCreateCommandTest.php │ └── VoucherUnlinkCommandTest.php ├── Controller │ ├── AccountControllerTest.php │ ├── AliasControllerTest.php │ ├── DovecotControllerTest.php │ ├── KeycloakControllerTest.php │ ├── OpenPGPControllerTest.php │ ├── PostfixControllerTest.php │ ├── RecoveryControllerTest.php │ ├── RegistrationControllerTest.php │ ├── RetentionControllerTest.php │ ├── RoundcubeControllerTest.php │ ├── StartControllerTest.php │ └── VoucherControllerTest.php ├── Creator │ ├── AliasCreatorTest.php │ ├── DomainCreatorTest.php │ ├── ReservedNameCreatorTest.php │ └── VoucherCreatorTest.php ├── Entity │ ├── Filter │ │ └── DomainFilterTest.php │ └── UserTest.php ├── EntityListener │ └── UserChangedListenerTest.php ├── Enum │ └── MailCryptTest.php ├── EventListener │ ├── BeforeRequestListenerTest.php │ ├── LocaleListenerTest.php │ ├── LoginListenerTest.php │ ├── UserCreatedListenerTest.php │ └── UserDeletedListenerTest.php ├── Factory │ ├── DomainFactoryTest.php │ └── VoucherFactoryTest.php ├── Form │ ├── AliasDeleteTypeTest.php │ ├── DataTransformer │ │ ├── OptionalDomainEmailTransformerTest.php │ │ └── TextToEmailTransformerTest.php │ ├── OpenPgpKeyDeleteTypeTest.php │ ├── PasswordChangeTypeTest.php │ ├── RecoveryProcessTypeTest.php │ ├── RecoveryTokenAckTypeTest.php │ ├── RecoveryTokenTypeTest.php │ ├── TwofactorConfirmTypeTest.php │ └── UserDeleteTypeTest.php ├── Guesser │ └── DomainGuesserTest.php ├── Handler │ ├── AliasHandlerTest.php │ ├── CryptoSecretHandlerTest.php │ ├── DeleteHandlerTest.php │ ├── MailCryptKeyHandlerTest.php │ ├── PasswordStrengthHandlerTest.php │ ├── RecoveryTokenHandlerTest.php │ ├── RegistrationHandlerTest.php │ ├── UserAuthenticationHandlerTest.php │ ├── UserRestoreHandlerTest.php │ ├── VoucherHandlerTest.php │ ├── WebhookHandlerTest.php │ └── WkdHandlerTest.php ├── Helper │ ├── AdminPasswordUpdaterTest.php │ ├── MenuHelperTest.php │ ├── PasswordGeneratorTest.php │ ├── PasswordUpdaterTest.php │ └── RandomStringGeneratorTest.php ├── Importer │ └── GpgKeyImporterTest.php ├── Model │ ├── CryptoSecretTest.php │ └── MailCryptKeyPairTest.php ├── Security │ ├── Encoder │ │ └── PasswordHashEncoderTest.php │ ├── UserCheckerTest.php │ └── UserProviderTest.php ├── Validator │ ├── Constraints │ │ ├── EmailAddressValidatorTest.php │ │ ├── EmailDomainValidatorTest.php │ │ ├── EmailLengthValidatorTest.php │ │ ├── LowercaseValidatorTest.php │ │ ├── PasswordPolicyValidatorTest.php │ │ ├── VoucherExistsValidatorTest.php │ │ └── VoucherUserValidatorTest.php │ └── TotpSecretValidatorTest.php ├── Voter │ └── DomainVoterTest.php ├── autoload.php ├── bootstrap.php └── test_checkpassword_login.sh ├── var └── cache │ └── .gitignore ├── webpack.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .vscode 4 | ansible 5 | build 6 | docs 7 | var 8 | vendor 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.php] 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [*.{yml,yaml}] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | 21 | [Makefile] 22 | indent_style = tab 23 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @0x46616c6b @doobry-systemli @t2d @y3n4 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | npm-dependencies: 10 | patterns: 11 | - "*" 12 | 13 | - package-ecosystem: "composer" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | allow: 18 | - dependency-name: "endroid/qr-code" 19 | - dependency-name: "ircmaxell/password-compat" 20 | - dependency-name: "mopa/bootstrap-bundle" 21 | - dependency-name: "nelmio/security-bundle" 22 | - dependency-name: "pear/crypt_gpg" 23 | - dependency-name: "ramsey/uuid" 24 | - dependency-name: "scheb/*" 25 | - dependency-name: "sonata-project/*" 26 | - dependency-name: "tuupola/base32" 27 | - dependency-name: "friends-of-behat/*" 28 | groups: 29 | php-dependencies: 30 | patterns: 31 | - "*" 32 | 33 | - package-ecosystem: "github-actions" 34 | directory: "/" 35 | schedule: 36 | interval: "weekly" 37 | -------------------------------------------------------------------------------- /.github/workflows/mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | mkdocs: 10 | name: Build & Deploy Documentation 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4.1.1 15 | 16 | - name: Deploy Documentation 17 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | CONFIG_FILE: mkdocs.yml 21 | -------------------------------------------------------------------------------- /.github/workflows/psalm.yml: -------------------------------------------------------------------------------- 1 | name: Psalm Static analysis 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | psalm: 7 | name: Psalm 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Psalm 14 | uses: docker://ghcr.io/psalm/psalm-github-actions 15 | with: 16 | security_analysis: true 17 | report_file: results.sarif 18 | 19 | - name: Upload Security Analysis results to GitHub 20 | uses: github/codeql-action/upload-sarif@v3 21 | with: 22 | sarif_file: results.sarif 23 | -------------------------------------------------------------------------------- /.github/workflows/security-check.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 8 * * 1' 8 | 9 | jobs: 10 | security-check: 11 | runs-on: ubuntu-22.04 12 | name: PHP Security Checker 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Symfony Security Check 18 | uses: symfonycorp/security-checker-action@v5 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | !bin/console 3 | !bin/githup-release.sh 4 | !bin/mailcrypt-encrypt-maildir 5 | !bin/user-accounts 6 | !bin/user-aliases 7 | !bin/user-vouchers 8 | build/ 9 | .idea/ 10 | node_modules 11 | public/build 12 | public/components/* 13 | .vagrant/ 14 | ansible/playbook.retry 15 | translations/ 16 | docker-compose.override.yml 17 | .php_cs.cache 18 | 19 | ###> symfony/phpunit-bridge ### 20 | .phpunit.result.cache 21 | /phpunit.xml 22 | ###< symfony/phpunit-bridge ### 23 | 24 | ###> symfony/framework-bundle ### 25 | /.env.local 26 | /.env.local.php 27 | /.env.*.local 28 | /config/secrets/prod/prod.decrypt.private.php 29 | /public/bundles/ 30 | /var/ 31 | /vendor/ 32 | ###< symfony/framework-bundle ### 33 | 34 | ###> phpstan/phpstan ### 35 | phpstan.neon 36 | ###< phpstan/phpstan ### 37 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/.gitmodules -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thanks for scanning userli for security problems. Our users will appreciate you keeping them safe. 4 | 5 | ## Supported Versions 6 | 7 | Always the latest version. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you encounter a critical security flaw, please contact the maintainers [via mail](userli@systemli.org). 12 | You can expect to receive an answer in at least a week. 13 | -------------------------------------------------------------------------------- /assets/bootstrap.js: -------------------------------------------------------------------------------- 1 | // register any custom, 3rd party controllers here 2 | // app.register('some_controller_name', SomeImportedController); 3 | -------------------------------------------------------------------------------- /assets/controllers.json: -------------------------------------------------------------------------------- 1 | { 2 | "controllers": [], 3 | "entrypoints": [] 4 | } 5 | -------------------------------------------------------------------------------- /assets/controllers/hello_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | 3 | /* 4 | * This is an example Stimulus controller! 5 | * 6 | * Any element with a data-controller="hello" attribute will cause 7 | * this controller to be executed. The name "hello" comes from the filename: 8 | * hello_controller.js -> "hello" 9 | * 10 | * Delete this file or adapt it for your use! 11 | */ 12 | export default class extends Controller { 13 | connect() { 14 | this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /assets/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/assets/images/logo_small.png -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - App\Tests\Behat\FeatureContext: ~ 6 | 7 | extensions: 8 | FriendsOfBehat\SymfonyExtension: 9 | bootstrap: "tests/bootstrap.php" 10 | kernel: 11 | class: ~ 12 | path: ~ 13 | environment: ~ 14 | debug: ~ 15 | Behat\MinkExtension: 16 | sessions: 17 | symfony: 18 | symfony: ~ 19 | 20 | DVDoug\Behat\CodeCoverage\Extension: 21 | filter: 22 | include: 23 | directories: 24 | 'src': ~ 25 | exclude: 26 | directories: 27 | 'src/DataFixtures': ~ 28 | reports: 29 | clover: 30 | target: build/clover-behat.xml 31 | name: 'Behat' 32 | text: 33 | showOnlySummary: true 34 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | HTTPS), 30 | # and send no header to a less secure destination (HTTPS->HTTP). 31 | # If `strict-origin-when-cross-origin` is not supported, use `no-referrer` policy, 32 | # no referrer information is sent along with requests. 33 | referrer_policy: 34 | enabled: true 35 | policies: 36 | - 'no-referrer' 37 | - 'strict-origin-when-cross-origin' 38 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | 9 | when@prod: 10 | framework: 11 | router: 12 | strict_requirements: null 13 | 14 | when@dev: 15 | framework: 16 | router: 17 | strict_requirements: true 18 | 19 | when@test: 20 | framework: 21 | router: 22 | strict_requirements: true 23 | -------------------------------------------------------------------------------- /config/packages/scheb_2fa.yaml: -------------------------------------------------------------------------------- 1 | # See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/6.x/configuration.html 2 | scheb_two_factor: 3 | # Google Authenticator config 4 | totp: 5 | enabled: true 6 | server_name: '%env(APP_DOMAIN)%' 7 | issuer: '%env(PROJECT_NAME)%' 8 | template: Security/2fa_form.html.twig 9 | parameters: 10 | image: '%env(PROJECT_LOGO_URL)%' 11 | # Backup codes config 12 | backup_codes: 13 | enabled: true 14 | 15 | # The security token classes, which trigger two-factor authentication. 16 | security_tokens: 17 | - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken 18 | - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken 19 | -------------------------------------------------------------------------------- /config/packages/sonata_admin.yaml: -------------------------------------------------------------------------------- 1 | sonata_admin: 2 | title: Userli 3 | title_logo: 'build/images/logo_small.png' 4 | show_mosaic_button: false 5 | dashboard: 6 | blocks: 7 | - position: left 8 | type: sonata.admin.block.admin_list 9 | - position: right 10 | type: userli.admin.block.statistics 11 | templates: 12 | layout: 'Admin/standard_layout.html.twig' 13 | user_block: 'Admin/user_block.html.twig' 14 | security: 15 | handler: sonata.admin.security.handler.role 16 | 17 | sonata_block: 18 | http_cache: false 19 | default_contexts: [cms] 20 | blocks: 21 | sonata.admin.block.admin_list: 22 | contexts: [admin] 23 | userli.admin.block.statistics: ~ 24 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: '%locale%' 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | paths: 6 | - '%kernel.project_dir%/default_translations' 7 | fallbacks: 8 | - '%locale%' 9 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | globals: 4 | app_name: '%env(APP_NAME)%' 5 | app_url: '%env(APP_URL)%' 6 | project_name: '%env(PROJECT_NAME)%' 7 | project_url: '%env(PROJECT_URL)%' 8 | webmail_url: '%env(WEBMAIL_URL)%' 9 | locales: '%supported_locales%' 10 | file_name_pattern: '*.twig' 11 | 12 | when@test: 13 | twig: 14 | strict_variables: true 15 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | 5 | # Enables validator auto-mapping support. 6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 7 | #auto_mapping: 8 | # App\Entity\: [] 9 | 10 | when@prod: 11 | framework: 12 | validation: 13 | cache: validator.mapping.cache.doctrine.apc 14 | 15 | when@test: 16 | framework: 17 | validation: 18 | not_compromised_password: false 19 | -------------------------------------------------------------------------------- /config/packages/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | web_profiler: 3 | toolbar: true 4 | intercept_redirects: false 5 | 6 | framework: 7 | profiler: 8 | only_exceptions: false 9 | collect_serializer_data: true 10 | 11 | when@test: 12 | web_profiler: 13 | toolbar: false 14 | intercept_redirects: false 15 | 16 | framework: 17 | profiler: { collect: false } 18 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | /usr/local/etc/php/conf.d/memory_limit.ini 27 | 28 | COPY --from=builder /var/www/html /var/www/html 29 | COPY docker/userli/apache.conf /etc/apache2/sites-available/000-default.conf 30 | -------------------------------------------------------------------------------- /docker/userli/apache.conf: -------------------------------------------------------------------------------- 1 | 2 | DocumentRoot /var/www/html/public 3 | 4 | 5 | AllowOverride All 6 | 7 | 8 | SetEnv APP_ENV dev 9 | 10 | -------------------------------------------------------------------------------- /docs/assets/images/account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/images/account.png -------------------------------------------------------------------------------- /docs/assets/images/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/images/admin.png -------------------------------------------------------------------------------- /docs/assets/images/alias.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/images/alias.png -------------------------------------------------------------------------------- /docs/assets/images/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/images/index.png -------------------------------------------------------------------------------- /docs/assets/images/manage_recovery_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/images/manage_recovery_token.png -------------------------------------------------------------------------------- /docs/assets/images/new_recovery_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/images/new_recovery_token.png -------------------------------------------------------------------------------- /docs/assets/images/recovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/images/recovery.png -------------------------------------------------------------------------------- /docs/assets/images/voucher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/images/voucher.png -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/development/coding_style.md: -------------------------------------------------------------------------------- 1 | # Coding style 2 | 3 | We use the default Symfony coding style. 4 | 5 | Check and adjust coding style by running `php-cs-fixer`: 6 | 7 | ```shell 8 | make cs-fixer 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/development/icons.md: -------------------------------------------------------------------------------- 1 | # Icons 2 | 3 | We're using [Githubs Octicons v9](https://github.com/primer/octicons/releases/tag/v9.6.0) for icons on the index page. 4 | Icons from V10 onwards look a bit different and don't allign with earlier ones. 5 | 6 | All icons should be 14\*14 px, #333333 on transparent background without padding. 7 | -------------------------------------------------------------------------------- /docs/development/logs.md: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | ## Userli 4 | 5 | Userli utilizes the [Monolog](https://symfony.com/doc/6.4/logging.html#monolog) for logging which is configured in 6 | `config/packages/monolog.yaml`. 7 | 8 | Logs are written to `var/log/text.log` and `var/log/dev.log` when running in the `test` or `dev` environment respectively. 9 | 10 | The logs are JSON formatted. 11 | 12 | Inspecting the logs: 13 | 14 | ```shell 15 | tail -f var/log/dev.log | jq 16 | ``` 17 | 18 | ## Docker/Podman 19 | 20 | Sometimes it's necessary to inspect the logs of the containers. 21 | 22 | Say you want to inspect the logs for the `dovecot` container: 23 | 24 | === "podman" 25 | 26 | ```shell 27 | podman compose logs -f dovecot 28 | ``` 29 | 30 | === "docker" 31 | 32 | ```shell 33 | docker compose logs -f dovecot 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /docs/development/release.md: -------------------------------------------------------------------------------- 1 | # Creating release tarballs 2 | 3 | Release tarballs are the preferred way to install Userli. This page explains how to create them. 4 | 5 | 6 | First, you'll need a [Github API token](https://github.com/settings/tokens). 7 | The token needs the following privileges: 8 | 9 | ```text 10 | public_repo, repo:status, repo_deployment 11 | ``` 12 | 13 | Now, execute the following script. It will create a version tag, release and 14 | copy the info from `CHANGELOG.md` to the release info. 15 | 16 | ```shell 17 | GITHUB_API_TOKEN= GPG_SIGN_KEY="" ./bin/github-release.sh 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /docs/features/wkd.md: -------------------------------------------------------------------------------- 1 | # Web Key Directory 2 | 3 | Userli brings support for [OpenPGP Web Key 4 | Directory](https://gnupg.org/faq/wkd.html), a OpenPGP key discovery system. 5 | Users can import and update their OpenPGP key and it will be published in the 6 | Web Key Directory according to the [OpenPGP Web Key Directory Internet 7 | Draft](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service). 8 | 9 | The WKD feature depends on [GnuPG](https://gnupg.org/) being installed. 10 | 11 | The WKD directory path can be configured by setting `WKD_DIRECTORY` in the 12 | dotenv (`.env`) file. Write access to the WKD directory is required. 13 | 14 | The WKD directory format can be configured by setting `WKD_FORMAT` in the 15 | dotenv (`.env`) file. The supported settings are `advanced` (default) and 16 | `direct`. See the [OpenPGP Web Key Directory Internet 17 | Draft](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service) 18 | for details. 19 | 20 | The WKD directory can be regenerated at any time by running the console 21 | command: 22 | 23 | ```shell 24 | bin/console app:wkd:export-keys 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Userli 2 | 3 | Web application to (self-) manage e-mail users and encrypt their mailboxes. 4 | 5 | ![index](./assets/images/index.png) 6 | 7 | ## Features 8 | 9 | * User self-service (change password/recovery token, set aliases, ...) 10 | * Invite code system (new users get three invite codes after one week) 11 | * Domain admins (accounts with admin rights for one domain) 12 | * Random alias feature for users 13 | * Recovery tokens to restore accounts when password got lost 14 | * Support for [Dovecot mailbox encryption](https://wiki.dovecot.org/Plugins/MailCrypt) 15 | * Multi-language support (English, French, Spanish, Portuguese, Bokmål, and German provided) 16 | 17 | ## Roles 18 | 19 | Userli supports a role system to help you run your mail server. 20 | 21 | * User - Default role 22 | * Multiplier - Like user but with unlimited invite codes 23 | * Suspicious - User without invite codes 24 | * Spam - This account is suspected to be hacked and can't send mail anymore 25 | * Permanent - Don't delete this account in user cleanup 26 | * Domain-Admin - Can add/edit/delete users and aliases for their domain 27 | * Admin - Can add/edit/delete all available data 28 | 29 | ## Contribute 30 | 31 | This is a start. Please help to [improve the documentation](https://github.com/systemli/userli/edit/main/docs/index.md). 32 | -------------------------------------------------------------------------------- /docs/installation/code.md: -------------------------------------------------------------------------------- 1 | # Get the code 2 | 3 | Install the [latest release](https://github.com/systemli/userli/releases/latest). 4 | 5 | Download and unpack the actual source code. 6 | 7 | ```shell 8 | mkdir userli && cd userli 9 | wget https://github.com/systemli/userli/releases/download/x.x.x/userli-x.x.x.tar.gz 10 | # Check signature and hash sum, if you know how to 11 | tar -xvzf userli-x.x.x.tar.gz 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/installation/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | This app brings custom commands: 4 | 5 | ``` 6 | app:munin:account # Return number of account to munin 7 | app:munin:alias # Return number of aliases to munin 8 | app:munin:voucher # Return number of vouchers to munin 9 | app:registration:mail # Send a registration mail to a user 10 | app:report:weekly # Send weekly report to all admins 11 | app:reservednames:import # Import reserved names from stdin or file 12 | app:users:check # Check if user is present 13 | app:users:mailcrypt # Get MailCrypt values for user 14 | app:users:quota # Get quota of user if set 15 | app:users:remove # Removes all mailboxes from deleted users 16 | app:voucher:create # Create voucher for a specific user 17 | app:voucher:unlink # Remove connection between vouchers and accounts after 3 months 18 | ``` 19 | 20 | Get more information about each command by running: 21 | 22 | ``` 23 | bin/console {{ command }} --help 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/installation/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | You can personalize your Userli instance by creating `.env.local`, 4 | which overrides some values from `.env`. You should at least configure 5 | the following values. 6 | 7 | 8 | ``` 9 | APP_ENV=prod 10 | APP_SECRET= 11 | APP_URL=https://users.example.org 12 | DATABASE_URL=mysql://userli:@127.0.0.1:3306/userli 13 | MAILER_DSN=smtp://localhost:25 14 | PROJECT_NAME=example.org 15 | PROJECT_URL=https://www.example.org 16 | SENDER_ADDRESS=userli@example.org 17 | NOTIFICATION_ADDRESS=admin@example.org 18 | ``` 19 | 20 | Look into `.env` to get more information about variables and how to handle them. 21 | -------------------------------------------------------------------------------- /docs/installation/customize.md: -------------------------------------------------------------------------------- 1 | # Customize 2 | 3 | You can override translation strings individually by putting them into 4 | override localization files at `translations//messages..yml`. 5 | Don't forget to clear the cache with `bin/console cache:clear` afterwards. 6 | -------------------------------------------------------------------------------- /docs/installation/database.md: -------------------------------------------------------------------------------- 1 | # Create database 2 | 3 | Create Userli database and database user. 4 | 5 | For simplicity, the user has full access to `userli` database. 6 | 7 | ```shell 8 | mysql -e 'CREATE DATABASE userli' 9 | mysql -e 'CREATE USER `userli`@`localhost` IDENTIFIED BY ""' 10 | mysql -e 'GRANT ALL PRIVILEGES ON userli.* TO `userli`@`localhost`' 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/installation/index.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Requirements 4 | 5 | * Webserver (e.g [Caddy](https://caddyserver.com/)) 6 | * [PHP >= 8.0](https://secure.php.net/) with libsodium 7 | * [MariaDB](https://mariadb.org/) or [MySQL](https://mysql.com/) 8 | * [OpenSSL](https://www.openssl.org/) binary (for MailCrypt feature) 9 | * [GnuPG](https://gnupg.org/) version 2.1.14 or newer 10 | 11 | You can also run this application with PostgreSQL oder SQLite. 12 | -------------------------------------------------------------------------------- /docs/screenshots/index.md: -------------------------------------------------------------------------------- 1 | # Screenshots 2 | 3 | Some screenshots of Userli features 4 | 5 | ## Account management 6 | 7 | Change password and delete account 8 | 9 | ![account](../assets/images/account.png) 10 | 11 | 12 | ## Admin Frontend 13 | 14 | Manage domains, users, aliases and more 15 | 16 | ![admin](../assets/images/admin.png) 17 | 18 | ## Invite friends 19 | 20 | Invite codes 21 | 22 | ![voucher](../assets/images/voucher.png) 23 | 24 | ## Alias addresses 25 | 26 | Manage alias addresses 27 | 28 | ![alias](../assets/images/alias.png) 29 | 30 | ## Recover lost password 31 | 32 | Manage recovery token 33 | 34 | ![manage_recovery_token](../assets/images/manage_recovery_token.png) 35 | 36 | Add recovery token 37 | 38 | ![new_recovery_token](../assets/images/new_recovery_token.png) 39 | 40 | Use recovery token 41 | 42 | ![use_recovery_token](../assets/images/recovery.png) 43 | -------------------------------------------------------------------------------- /docs/update/index.md: -------------------------------------------------------------------------------- 1 | # Update 2 | 3 | When updating to a new userli version, please take a look at `UPGRADE.md` 4 | to see whether manual steps are required. 5 | 6 | To automatically update the database schema of userli, run these commands: 7 | 8 | ```shell 9 | # Warm up cache 10 | bin/console cache:warmup 11 | 12 | # Show database schema updates 13 | bin/console doctrine:schema:update --dump-sql 14 | 15 | # If necessary update the database schema 16 | bin/console doctrine:schema:update --force 17 | ``` 18 | -------------------------------------------------------------------------------- /features/init.feature: -------------------------------------------------------------------------------- 1 | Feature: Initialization 2 | 3 | Background: 4 | Given the database is clean 5 | 6 | @init 7 | Scenario: Redirect to init site 8 | When I am on homepage 9 | 10 | Then I should be on "/init" 11 | 12 | @init 13 | Scenario: Input admin password 14 | When the following Domain exists: 15 | | name | 16 | | example.org | 17 | And I am on "/init/user" 18 | And I fill in the following: 19 | | plain_password[plainPassword][first] | P4ssW0rt!!!1 | 20 | | plain_password[plainPassword][second] | P4ssW0rt!!!1 | 21 | And I press "Submit" 22 | 23 | Then I should be on "/" 24 | 25 | @init 26 | Scenario: No more redirect to init site 27 | When the following Domain exists: 28 | | name | 29 | | example.org | 30 | And the following User exists: 31 | | email | password | 32 | | postmaster@example.org | P4ssW0rt | 33 | And I am on homepage 34 | 35 | Then I should be on "/" 36 | -------------------------------------------------------------------------------- /features/language.feature: -------------------------------------------------------------------------------- 1 | Feature: Language detection 2 | 3 | Background: 4 | Given the database is clean 5 | And the following Domain exists: 6 | | name | 7 | | example.org | 8 | And the following User exists: 9 | | email | password | roles | 10 | | postmaster@example.org | asdasd | ROLE_ADMIN | 11 | 12 | @language 13 | Scenario: Default language 14 | When I am on "/" 15 | Then I should see text matching "Welcome" 16 | Then I am on "/?_locale=de" 17 | Then I should see text matching "Willkommen" 18 | Then I am on "/" 19 | Then I should see text matching "Willkommen" 20 | 21 | @language 22 | Scenario: Session language 23 | When I am on "/?_locale=de" 24 | Then I should see text matching "Willkommen" 25 | And I am on "/" 26 | And I should see text matching "Willkommen" 27 | 28 | @language 29 | Scenario: Browser language detection 30 | Given set the HTTP-Header "Accept-Language" to "de" 31 | When I am on "/" 32 | Then I should see text matching "Willkommen" 33 | 34 | @language 35 | Scenario: Browser language fallback 36 | Given set the HTTP-Header "Accept-Language" to "afa" 37 | When I am on "/" 38 | Then I should see text matching "Welcome" 39 | -------------------------------------------------------------------------------- /features/quotaCommand.feature: -------------------------------------------------------------------------------- 1 | Feature: QuotaCommand 2 | 3 | Background: 4 | Given the database is clean 5 | And the following Domain exists: 6 | | name | 7 | | example.org | 8 | And the following User exists: 9 | | email | password | quota | 10 | | noquota@example.org | password | | 11 | | quota@example.org | password | 1000 | 12 | 13 | @quotaCommand 14 | Scenario: Check that user has quota 15 | When I run console command "app:users:quota --user quota@example.org" 16 | Then I should see "1000" in the console output 17 | 18 | When I run console command "app:users:quota --user noquota@example.org" 19 | Then I should not see "1000" in the console output 20 | -------------------------------------------------------------------------------- /features/voucherCreateCommand.feature: -------------------------------------------------------------------------------- 1 | Feature: VoucherCreateCommand 2 | 3 | Background: 4 | Given the database is clean 5 | And the following Domain exists: 6 | | name | 7 | | example.org | 8 | And the following User exists: 9 | | email | password | 10 | | user@example.org | password | 11 | 12 | @voucherCreationCommand 13 | Scenario: Create new voucher 14 | When I run console command "app:voucher:create --user user@example.org -c 1 -p" 15 | Then I should see regex "|^[a-z_\-0-9]{6}$|i" in the console output 16 | 17 | When I run console command "app:voucher:create --user user@example.org -c 1 -l" 18 | Then I should see regex "|^https://users.example.org/register/[a-z_\-0-9]{6}$|i" in the console output 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev-server": "encore dev-server", 4 | "dev": "encore dev", 5 | "watch": "encore dev --watch", 6 | "build": "encore production --progress" 7 | }, 8 | "devDependencies": { 9 | "@babel/core": "^7.26.10", 10 | "@babel/preset-env": "^7.26.9", 11 | "@hotwired/stimulus": "^3.0.0", 12 | "@symfony/stimulus-bridge": "^4.0.0", 13 | "@symfony/webpack-encore": "^5.1.0", 14 | "copy-webpack-plugin": "^13.0.0", 15 | "jquery": "^3.7.1", 16 | "webpack": "^5.99.5", 17 | "webpack-cli": "^6.0.1" 18 | }, 19 | "packageManager": "yarn@1.22.19" 20 | } 21 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemli/userli/5e0025bd53553729e46c75b4d750fbf5d2eff9fd/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | ]); 14 | $rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml'); 15 | $rectorConfig->importNames(); 16 | $rectorConfig->phpVersion(\Rector\ValueObject\PhpVersion::PHP_81); 17 | 18 | $rectorConfig->skip([ 19 | // SonataAdminBundle CRUDController needs the suffix for actions 20 | ActionSuffixRemoverRector::class => [ 21 | __DIR__ . '/src/Controller/AliasCRUDController.php', 22 | __DIR__ . '/src/Controller/UserCRUDController.php', 23 | ], 24 | ]); 25 | 26 | $rectorConfig->sets([ 27 | SetList::PHP_81, 28 | SymfonySetList::SYMFONY_CODE_QUALITY, 29 | SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, 30 | SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES, 31 | DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, 32 | ]); 33 | }; 34 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=systemli_userli 2 | sonar.organization=systemli 3 | 4 | sonar.sources=src/ 5 | sonar.tests=tests/ 6 | sonar.exclusions=src/DataFixtures/** 7 | 8 | sonar.php.tests.reportPath=build/result-phpunit.xml 9 | sonar.php.coverage.reportPaths=build/clover-phpunit.xml,build/clover-behat.xml 10 | sonar.php.file.suffixes=php 11 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/Admin/Admin.php: -------------------------------------------------------------------------------- 1 | getRequest()->get($this->getIdParameter()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Builder/AliasCreatedMessageBuilder.php: -------------------------------------------------------------------------------- 1 | translator->trans( 22 | 'mail.alias-created-body', 23 | [ 24 | '%app_url%' => $this->appUrl, 25 | '%project_name%' => $this->projectName, 26 | '%email%' => $email, 27 | '%alias%' => $alias, 28 | ], 29 | null, 30 | $locale 31 | ); 32 | } 33 | 34 | public function buildSubject(string $locale, string $email): string 35 | { 36 | return $this->translator->trans( 37 | 'mail.alias-created-subject', ['%email%' => $email], null, $locale 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Builder/RecoveryProcessMessageBuilder.php: -------------------------------------------------------------------------------- 1 | translator->trans( 22 | 'mail.recovery-body', 23 | [ 24 | '%app_url%' => $this->appUrl, 25 | '%project_name%' => $this->projectName, 26 | '%email%' => $email, 27 | '%time%' => $time, 28 | ], 29 | null, 30 | $locale 31 | ); 32 | } 33 | 34 | public function buildSubject(string $locale, string $email): string 35 | { 36 | return $this->translator->trans( 37 | 'mail.recovery-subject', ['%email%' => $email], null, $locale 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Command/AbstractUsersCommand.php: -------------------------------------------------------------------------------- 1 | addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User to act upon') 24 | ->addOption('dry-run', null, InputOption::VALUE_NONE); 25 | } 26 | 27 | /** 28 | * @throws UserNotFoundException 29 | */ 30 | protected function getUser(InputInterface $input): User 31 | { 32 | $email = $input->getOption('user'); 33 | if (empty($email) || null === $user = $this->manager->getRepository(User::class)->findByEmail($email)) { 34 | throw new UserNotFoundException(sprintf('User with email %s not found!', $email)); 35 | } 36 | return $user; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Command/ReportWeeklyCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Send weekly report to all admins'); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function execute(InputInterface $input, OutputInterface $output): int 35 | { 36 | $this->handler->sendReport(); 37 | 38 | return 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Command/UsersQuotaCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Get quota of user if set'); 16 | } 17 | 18 | protected function execute(InputInterface $input, OutputInterface $output): int 19 | { 20 | $user = $this->getUser($input); 21 | 22 | // get quota 23 | $quota = $user->getQuota(); 24 | if (null === $quota) { 25 | return 0; 26 | } 27 | 28 | $output->writeln(sprintf('%u', $quota)); 29 | return 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Command/VoucherCountCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Get count of vouchers for a specific user'); 17 | } 18 | 19 | protected function execute(InputInterface $input, OutputInterface $output): int 20 | { 21 | $user = $this->getUser($input); 22 | 23 | $usedCount = $this->manager->getRepository(Voucher::class)->countVouchersByUser($user, true); 24 | $unusedCount = $this->manager->getRepository(Voucher::class)->countVouchersByUser($user, false); 25 | $output->writeln(sprintf("Voucher count for user %s", $user->getEmail())); 26 | $output->writeln(sprintf("Used: %d", $usedCount)); 27 | $output->writeln(sprintf("Unused: %d", $unusedCount)); 28 | 29 | return 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Controller/ErrorController.php: -------------------------------------------------------------------------------- 1 | render('Exception/show.html.twig', [ 15 | 'message' => $exception->getMessage(), 16 | 'status_code' => $exception->getStatusCode(), 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Controller/RoundcubeController.php: -------------------------------------------------------------------------------- 1 | getUser(); 22 | 23 | $aliases = $this->manager->getRepository(Alias::class)->findByUser($user); 24 | $aliasSources = array_map(static function ($alias) { return $alias->getSource(); }, $aliases); 25 | return $this->json($aliasSources); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Controller/SecurityController.php: -------------------------------------------------------------------------------- 1 | getLastAuthenticationError(); 23 | // last username entered by the user 24 | $lastUsername = $authenticationUtils->getLastUsername(); 25 | 26 | return $this->render('Security/login.html.twig', [ 27 | 'last_username' => $lastUsername, 28 | 'error' => $error, 29 | ]); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | #[Route(path: '/logout', name: 'logout', methods: ['GET'])] 36 | public function logout(): void 37 | { 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Creator/AbstractCreator.php: -------------------------------------------------------------------------------- 1 | validator->validate($entity, null, $validationGroups); 30 | 31 | if ($violations->count() > 0) { 32 | throw new ValidationException($violations); 33 | } 34 | } 35 | 36 | /** 37 | * @param $entity 38 | */ 39 | protected function save($entity): void 40 | { 41 | $this->manager->persist($entity); 42 | $this->manager->flush(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Creator/AliasCreator.php: -------------------------------------------------------------------------------- 1 | validate($alias, ['Default', 'unique']); 26 | $this->save($alias); 27 | 28 | $this->eventDispatcher->dispatch(new AliasCreatedEvent($alias), AliasCreatedEvent::NAME); 29 | if (null === $localPart) { 30 | $this->eventDispatcher->dispatch(new RandomAliasCreatedEvent($alias), RandomAliasCreatedEvent::NAME); 31 | } 32 | 33 | return $alias; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Creator/DomainCreator.php: -------------------------------------------------------------------------------- 1 | validate($domain, ['Default', 'lowercase', 'unique']); 20 | $this->save($domain); 21 | 22 | $this->eventDispatcher->dispatch(new DomainCreatedEvent($domain), DomainCreatedEvent::NAME); 23 | 24 | return $domain; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Creator/ReservedNameCreator.php: -------------------------------------------------------------------------------- 1 | validate($reservedName); 22 | $this->save($reservedName); 23 | 24 | return $reservedName; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Creator/VoucherCreator.php: -------------------------------------------------------------------------------- 1 | validate($voucher); 23 | $this->save($voucher); 24 | 25 | return $voucher; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/DataFixtures/AbstractUserData.php: -------------------------------------------------------------------------------- 1 | updatePassword($user, self::PASSWORD); 22 | $this->passwordHash = $user->getPassword(); 23 | } 24 | 25 | protected function buildUser(Domain $domain, string $email, array $roles): User 26 | { 27 | $user = new User(); 28 | $user->setDomain($domain); 29 | $user->setEmail($email); 30 | $user->setRoles($roles); 31 | $user->setPassword($this->passwordHash); 32 | 33 | return $user; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/DataFixtures/LoadAliasData.php: -------------------------------------------------------------------------------- 1 | getRepository(User::class)->findByEmail('user2@example.org'); 20 | 21 | $alias = AliasFactory::create($user, 'alias'); 22 | $manager->persist($alias); 23 | $alias2 = AliasFactory::create($user, 'alias2'); 24 | $manager->persist($alias2); 25 | 26 | $manager->flush(); 27 | $manager->clear(); 28 | } 29 | 30 | public static function getGroups(): array 31 | { 32 | return ['basic']; 33 | } 34 | 35 | public function getDependencies(): array 36 | { 37 | return [ 38 | LoadUserData::class, 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/DataFixtures/LoadDomainData.php: -------------------------------------------------------------------------------- 1 | domains as $name) { 20 | $domain = new Domain(); 21 | $domain->setName($name); 22 | 23 | $manager->persist($domain); 24 | } 25 | 26 | $manager->flush(); 27 | $manager->clear(); 28 | } 29 | 30 | public static function getGroups(): array 31 | { 32 | return ['basic']; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Dto/DovecotPassdbDto.php: -------------------------------------------------------------------------------- 1 | password; 15 | } 16 | 17 | public function setPassword(string $password): void 18 | { 19 | $this->password = $password; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Dto/KeycloakUserValidateDto.php: -------------------------------------------------------------------------------- 1 | password; 16 | } 17 | 18 | public function setPassword(string $password): void { 19 | $this->password = $password; 20 | } 21 | 22 | public function getCredentialType(): string 23 | { 24 | return $this->credentialType; 25 | } 26 | 27 | public function setCredentialType(string $credentialType): void 28 | { 29 | $this->credentialType = $credentialType; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Dto/RetentionTouchUserDto.php: -------------------------------------------------------------------------------- 1 | timestamp; 12 | } 13 | 14 | public function setTimestamp(int $timestamp): void 15 | { 16 | $this->timestamp = $timestamp; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Entity/Domain.php: -------------------------------------------------------------------------------- 1 | creationTime = $currentDateTime; 30 | $this->updatedTime = $currentDateTime; 31 | } 32 | 33 | public function __toString(): string 34 | { 35 | return ($this->getName()) ?: ''; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Entity/Filter/DomainFilter.php: -------------------------------------------------------------------------------- 1 | getDomainId()) { 15 | return ''; 16 | } 17 | 18 | // if domain aware 19 | if (array_key_exists('domain', $targetEntity->getAssociationMappings())) { 20 | return sprintf('%s.domain_id = %s', $targetTableAlias, $domainId); 21 | } 22 | 23 | if (Domain::class === $targetEntity->getName()) { 24 | return sprintf('%s.id = %s', $targetTableAlias, $domainId); 25 | } 26 | 27 | return ''; 28 | } 29 | 30 | public function getDomainId(): ?string 31 | { 32 | try { 33 | return $this->getParameter('domainId'); 34 | } catch (InvalidArgumentException) { 35 | return null; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Entity/OpenPgpKey.php: -------------------------------------------------------------------------------- 1 | getKeyData()) ? base64_decode($this->getKeyData()) : null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Entity/ReservedName.php: -------------------------------------------------------------------------------- 1 | creationTime = $currentDateTime; 32 | $this->updatedTime = $currentDateTime; 33 | } 34 | 35 | public function __toString(): string 36 | { 37 | return ($this->getName()) ?: ''; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Entity/SoftDeletableInterface.php: -------------------------------------------------------------------------------- 1 | self::DISABLED, 16 | '1' => self::ENABLED_OPTIONAL, 17 | '2' => self::ENABLED_ENFORCE_NEW_USERS, 18 | '3' => self::ENABLED_ENFORCE_ALL_USERS, 19 | default => throw new \InvalidArgumentException("Invalid MailCrypt value: $value"), 20 | }; 21 | } 22 | 23 | public function isAtLeast(self $other): bool 24 | { 25 | return $this->value >= $other->value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Enum/Roles.php: -------------------------------------------------------------------------------- 1 | self::PERMANENT, 20 | self::MULTIPLIER => self::MULTIPLIER, 21 | self::SPAM => self::SPAM, 22 | self::SUSPICIOUS => self::SUSPICIOUS, 23 | self::USER => self::USER, 24 | self::DOMAIN_ADMIN => self::DOMAIN_ADMIN, 25 | self::ADMIN => self::ADMIN, 26 | self::KEYCLOAK => self::KEYCLOAK, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Event/AliasCreatedEvent.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Event/DomainCreatedEvent.php: -------------------------------------------------------------------------------- 1 | domain = $domain; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Event/Events.php: -------------------------------------------------------------------------------- 1 | user = $user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Event/RandomAliasCreatedEvent.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Event/RecoveryProcessEvent.php: -------------------------------------------------------------------------------- 1 | user = $user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Event/UserCreatedEvent.php: -------------------------------------------------------------------------------- 1 | user; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Event/UserDeletedEvent.php: -------------------------------------------------------------------------------- 1 | user; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Event/UserEvent.php: -------------------------------------------------------------------------------- 1 | user = $user; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/EventListener/LogoutListener.php: -------------------------------------------------------------------------------- 1 | getRequest()->getSession()->getFlashBag()->add('success', 'flashes.logout-successful'); 14 | } 15 | 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public static function getSubscribedEvents(): array 20 | { 21 | return [ 22 | LogoutEvent::class => 'onLogoutSuccess', 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/EventListener/TwigGlobalListener.php: -------------------------------------------------------------------------------- 1 | manager->getRepository(Domain::class)->getDefaultDomain(); 24 | if (null !== $domain) { 25 | $this->twig->addGlobal('domain', $domain->getName()); 26 | } else { 27 | $this->twig->addGlobal('domain', 'defaultdomain'); 28 | } 29 | } 30 | 31 | public static function getSubscribedEvents(): array 32 | { 33 | return [KernelEvents::CONTROLLER => 'injectGlobalVariables']; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/EventListener/UserCreatedListener.php: -------------------------------------------------------------------------------- 1 | webhookHandler->send($event->getUser(), 'user.created'); 19 | } 20 | 21 | public static function getSubscribedEvents(): array 22 | { 23 | return [ 24 | UserCreatedEvent::class => 'onUserCreated', 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EventListener/UserDeletedListener.php: -------------------------------------------------------------------------------- 1 | webhookHandler->send($event->getUser(), 'user.deleted'); 19 | } 20 | 21 | public static function getSubscribedEvents(): array 22 | { 23 | return [ 24 | UserDeletedEvent::class => 'onUserDeleted', 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exception/MultipleGpgKeysForUserException.php: -------------------------------------------------------------------------------- 1 | getMessage(); 20 | 21 | if (!empty($constraint->getPropertyPath()) && is_string($constraint->getInvalidValue())) { 22 | $message = sprintf('%s [%s => %s]', $message, $constraint->getPropertyPath(), $constraint->getInvalidValue()); 23 | } 24 | 25 | $messages[] = $message; 26 | } 27 | $message = implode(PHP_EOL, $messages); 28 | 29 | parent::__construct($message); 30 | } 31 | 32 | public function getConstraints(): ConstraintViolationListInterface 33 | { 34 | return $this->constraints; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Factory/AliasFactory.php: -------------------------------------------------------------------------------- 1 | getDomain(); 23 | $alias = new Alias(); 24 | $alias->setUser($user); 25 | $alias->setDomain($domain); 26 | $alias->setDestination($user->getEmail()); 27 | if (null === $localPart) { 28 | $localPart = RandomStringGenerator::generate(self::RANDOM_ALIAS_LENGTH, false); 29 | $alias->setRandom(true); 30 | } 31 | 32 | $alias->setSource($localPart.'@'.$domain->getName()); 33 | 34 | return $alias; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Factory/DomainFactory.php: -------------------------------------------------------------------------------- 1 | setName($name); 13 | 14 | return $domain; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Factory/ReservedNameFactory.php: -------------------------------------------------------------------------------- 1 | setName($name); 16 | 17 | return $reservedName; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Factory/VoucherFactory.php: -------------------------------------------------------------------------------- 1 | setUser($user); 22 | $voucher->setCode(RandomStringGenerator::generate(6, true)); 23 | 24 | return $voucher; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Form/AliasDeleteType.php: -------------------------------------------------------------------------------- 1 | add('password', PasswordType::class, ['label' => 'form.delete-password']) 19 | ->add('submit', SubmitType::class, ['label' => 'form.delete-alias']); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function configureOptions(OptionsResolver $resolver) 26 | { 27 | $resolver->setDefaults(['data_class' => 'App\Form\Model\Delete']); 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getBlockPrefix() 34 | { 35 | return self::NAME; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Form/CustomAliasCreateType.php: -------------------------------------------------------------------------------- 1 | add('alias', TextType::class, ['label' => 'form.new-custom-alias']) 19 | ->add('submit', SubmitType::class, ['label' => 'form.create-custom-alias']); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function configureOptions(OptionsResolver $resolver) 26 | { 27 | $resolver->setDefaults(['data_class' => 'App\Form\Model\AliasCreate']); 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getBlockPrefix() 34 | { 35 | return self::NAME; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Form/DataTransformer/OptionalDomainEmailTransformer.php: -------------------------------------------------------------------------------- 1 | domain); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Form/DataTransformer/TextToEmailTransformer.php: -------------------------------------------------------------------------------- 1 | domain); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Form/DomainCreateType.php: -------------------------------------------------------------------------------- 1 | add('domain', TextType::class, ['label' => 'form.domain']) 19 | ->add('submit', SubmitType::class, ['label' => 'form.add']); 20 | } 21 | 22 | public function configureOptions(OptionsResolver $resolver) 23 | { 24 | $resolver->setDefaults(['data_class' => 'App\Form\Model\DomainCreate']); 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getBlockPrefix() 31 | { 32 | return self::NAME; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Form/Model/AliasCreate.php: -------------------------------------------------------------------------------- 1 | keyFile; 16 | } 17 | 18 | /** 19 | * @return $this 20 | */ 21 | public function setKeyFile(string $keyFile): self 22 | { 23 | $this->keyFile = $keyFile; 24 | 25 | return $this; 26 | } 27 | 28 | public function getKeyText(): ?string 29 | { 30 | return $this->keyText; 31 | } 32 | 33 | /** 34 | * @return $this 35 | */ 36 | public function setKeyText(string $keyText): self 37 | { 38 | $this->keyText = $keyText; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Form/Model/PasswordChange.php: -------------------------------------------------------------------------------- 1 | password; 22 | } 23 | 24 | public function setPassword(string $password): void 25 | { 26 | $this->password = $password; 27 | } 28 | 29 | public function getNewPassword(): string 30 | { 31 | return $this->newPassword; 32 | } 33 | 34 | public function setNewPassword(string $newPassword): void 35 | { 36 | $this->newPassword = $newPassword; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Form/Model/PlainPassword.php: -------------------------------------------------------------------------------- 1 | voucher; 26 | } 27 | 28 | public function setVoucher(string $voucher): void 29 | { 30 | $this->voucher = $voucher; 31 | } 32 | 33 | public function getEmail(): ?string 34 | { 35 | return $this->email; 36 | } 37 | 38 | public function setEmail(string $email): void 39 | { 40 | $this->email = strtolower($email); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Form/Model/Twofactor.php: -------------------------------------------------------------------------------- 1 | add('password', PasswordType::class, ['label' => 'form.delete-password']) 19 | ->add('submit', SubmitType::class, ['label' => 'form.openpgp-delete']); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function configureOptions(OptionsResolver $resolver) 26 | { 27 | $resolver->setDefaults(['data_class' => 'App\Form\Model\Delete']); 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getBlockPrefix() 34 | { 35 | return self::NAME; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Form/RandomAliasCreateType.php: -------------------------------------------------------------------------------- 1 | add('submit', SubmitType::class, ['label' => 'form.create-random-alias']); 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function configureOptions(OptionsResolver $resolver): void 25 | { 26 | $resolver->setDefaults(['data_class' => AliasCreate::class]); 27 | } 28 | 29 | public function getBlockPrefix(): string 30 | { 31 | return self::NAME; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Form/RecoveryTokenType.php: -------------------------------------------------------------------------------- 1 | add('password', PasswordType::class, ['label' => 'form.password']) 20 | ->add('submit', SubmitType::class, ['label' => 'form.generate-recovery-token']); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function configureOptions(OptionsResolver $resolver): void 27 | { 28 | $resolver->setDefaults(['data_class' => RecoveryToken::class]); 29 | } 30 | 31 | public function getBlockPrefix(): string 32 | { 33 | return self::NAME; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Form/TwofactorBackupAckType.php: -------------------------------------------------------------------------------- 1 | add('ack', CheckboxType::class, [ 20 | 'required' => true, 21 | 'label' => 'form.twofactor-backup-code-ack', 22 | ]) 23 | ->add('submit', SubmitType::class, ['label' => 'form.verify']); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function configureOptions(OptionsResolver $resolver): void 30 | { 31 | $resolver->setDefaults(['data_class' => TwofactorBackupAck::class]); 32 | } 33 | 34 | public function getBlockPrefix(): string 35 | { 36 | return self::NAME; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Form/TwofactorConfirmType.php: -------------------------------------------------------------------------------- 1 | add('totpSecret', TextType::class, [ 20 | 'required' => true, 21 | 'label' => 'form.twofactor-login-auth-code', 22 | ]) 23 | ->add('submit', SubmitType::class, ['label' => 'form.verify']); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function configureOptions(OptionsResolver $resolver): void 30 | { 31 | $resolver->setDefaults(['data_class' => TwofactorConfirm::class]); 32 | } 33 | 34 | public function getBlockPrefix(): string 35 | { 36 | return self::NAME; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Form/TwofactorType.php: -------------------------------------------------------------------------------- 1 | add('password', PasswordType::class, ['label' => 'form.password']) 20 | ->add('submit', SubmitType::class, ['label' => 'form.twofactor-enable']); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function configureOptions(OptionsResolver $resolver): void 27 | { 28 | $resolver->setDefaults(['data_class' => Twofactor::class]); 29 | } 30 | 31 | public function getBlockPrefix(): string 32 | { 33 | return self::NAME; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Form/UserDeleteType.php: -------------------------------------------------------------------------------- 1 | add('password', PasswordType::class, ['label' => 'form.delete-password']) 20 | ->add('submit', SubmitType::class, ['label' => 'form.delete-account']); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function configureOptions(OptionsResolver $resolver): void 27 | { 28 | $resolver->setDefaults(['data_class' => Delete::class]); 29 | } 30 | 31 | public function getBlockPrefix(): string 32 | { 33 | return self::NAME; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Form/VoucherCreateType.php: -------------------------------------------------------------------------------- 1 | add('submit', SubmitType::class, ['label' => 'form.create-voucher']); 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function configureOptions(OptionsResolver $resolver): void 25 | { 26 | $resolver->setDefaults(['data_class' => VoucherCreate::class]); 27 | } 28 | 29 | public function getBlockPrefix(): string 30 | { 31 | return self::NAME; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Guesser/DomainGuesser.php: -------------------------------------------------------------------------------- 1 | repository = $manager->getRepository(Domain::class); 19 | } 20 | 21 | public function guess(string $email): ?Domain 22 | { 23 | $splitted = explode('@', $email); 24 | 25 | return isset($splitted[1]) ? $this->repository->findByName($splitted[1]) : null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Handler/MailHandler.php: -------------------------------------------------------------------------------- 1 | from(new Address($this->from, $this->name)) 19 | ->to($email) 20 | ->subject($subject) 21 | ->text($plain); 22 | 23 | if (isset($params['bcc'])) { 24 | $message->bcc($params['bcc']); 25 | } 26 | 27 | if (isset($params['html'])) { 28 | $message->html($params['html']); 29 | } 30 | 31 | $this->mailer->send($message); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Handler/PasswordStrengthHandler.php: -------------------------------------------------------------------------------- 1 | errors[] = 'form.forbidden_char'; 20 | } 21 | 22 | if (strlen((string) $value) < 12) { 23 | $this->errors[] = 'form.weak_password'; 24 | } 25 | 26 | return $this->errors; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Handler/SuspiciousChildrenHandler.php: -------------------------------------------------------------------------------- 1 | twig->render('Email/suspicious_children.twig', ['suspiciousChildren' => $suspiciousChildren]); 29 | $this->handler->send($this->to, $message, 'Suspicious users invited more users'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Handler/UserAuthenticationHandler.php: -------------------------------------------------------------------------------- 1 | passwordHasherFactory->getPasswordHasher($user); 25 | if (!$hasher->verify($user->getPassword(), $password)) { 26 | return null; 27 | } 28 | $this->eventDispatcher->dispatch(new LoginEvent($user), LoginEvent::NAME); 29 | 30 | return $user; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Handler/UserRegistrationInfoHandler.php: -------------------------------------------------------------------------------- 1 | manager->getRepository(User::class)->findUsersSince((new DateTime())->modify($from)); 33 | $message = $this->twig->render('Email/weekly_report.twig', ['users' => $users]); 34 | $this->handler->send($this->to, $message, 'Weekly Report: Registered E-mail Accounts'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Helper/AdminPasswordUpdater.php: -------------------------------------------------------------------------------- 1 | manager->getRepository(Domain::class)->getDefaultDomain(); 23 | $adminEmail = 'postmaster@'.$domain; 24 | $admin = $this->manager->getRepository(User::class)->findByEmail($adminEmail); 25 | if (null === $admin) { 26 | // create admin user 27 | $admin = new User(); 28 | $admin->setEmail($adminEmail); 29 | $admin->setRoles([Roles::ADMIN]); 30 | $admin->setDomain($domain); 31 | } 32 | $this->updater->updatePassword($admin, $password); 33 | $this->manager->persist($admin); 34 | $this->manager->flush(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Helper/PasswordGenerator.php: -------------------------------------------------------------------------------- 1 | setPassword($this->passwordHasherFactory->getPasswordHasher($user)->hash($plainPassword)); 17 | $user->updateUpdatedTime(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Helper/RandomStringGenerator.php: -------------------------------------------------------------------------------- 1 | environment, ['dev', 'test'])) { 18 | $projectDir = '/dev/shm/userli'; 19 | } 20 | 21 | return $projectDir.'/var/cache/'.$this->environment; 22 | } 23 | 24 | public function getLogDir(): string 25 | { 26 | $projectDir = parent::getProjectDir(); 27 | if ('/vagrant' === $projectDir && in_array($this->environment, ['dev', 'test'])) { 28 | $projectDir = '/dev/shm/userli'; 29 | } 30 | 31 | return $projectDir.'/var/log'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Model/MailCryptKeyPair.php: -------------------------------------------------------------------------------- 1 | privateKey = $privateKey; 20 | $this->publicKey = $publicKey; 21 | } 22 | 23 | /** 24 | * @throws SodiumException 25 | */ 26 | public function erase(): void 27 | { 28 | sodium_memzero($this->privateKey); 29 | sodium_memzero($this->publicKey); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Remover/VoucherRemover.php: -------------------------------------------------------------------------------- 1 | removeUnredeemedVouchersByUsers([$user]); 25 | } 26 | 27 | public function removeUnredeemedVouchersByUsers(array $users): void 28 | { 29 | $criteria = Criteria::create() 30 | ->where(Criteria::expr()->isNull('redeemedTime')) 31 | ->andWhere(Criteria::expr()->in('user', $users)); 32 | 33 | $this->manager->getRepository(Voucher::class) 34 | ->createQueryBuilder('a') 35 | ->addCriteria($criteria) 36 | ->delete() 37 | ->getQuery() 38 | ->execute(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Repository/DomainRepository.php: -------------------------------------------------------------------------------- 1 | findOneBy(['name' => $name]); 13 | } 14 | 15 | public function getDefaultDomain(): ?Domain 16 | { 17 | return $this->findOneBy([], ['id' => 'ASC']); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Repository/OpenPgpKeyRepository.php: -------------------------------------------------------------------------------- 1 | findBy(['user' => $user]); 17 | } 18 | 19 | public function findByEmail(string $email): ?OpenPgpKey 20 | { 21 | return $this->findOneBy(['email' => $email]); 22 | } 23 | 24 | public function countKeys(): int 25 | { 26 | return $this->count([]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Repository/ReservedNameRepository.php: -------------------------------------------------------------------------------- 1 | findOneBy(['name' => $name]); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Security/ApiAccessTokenHandler.php: -------------------------------------------------------------------------------- 1 | accessTokenDovecot: 22 | return new UserBadge('dovecot'); 23 | case $this->accessTokenKeycloak: 24 | return new UserBadge('keycloak'); 25 | case $this->accessTokenRetention: 26 | return new UserBadge('retention'); 27 | case $this->accessTokenPostfix: 28 | return new UserBadge('postfix'); 29 | default: 30 | throw new BadCredentialsException('Invalid access token'); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Security/UserChecker.php: -------------------------------------------------------------------------------- 1 | isDeleted()) { 19 | throw new CustomUserMessageAccountStatusException('Bad credentials.'); 20 | } 21 | } 22 | 23 | public function checkPostAuth(UserInterface $user): void 24 | { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Sender/AliasCreatedMessageSender.php: -------------------------------------------------------------------------------- 1 | getEmail()) { 29 | throw new Exception('Email should not be null'); 30 | } 31 | 32 | $body = $this->builder->buildBody($locale, $email, $alias->getSource()); 33 | $subject = $this->builder->buildSubject($locale, $email); 34 | $this->handler->send($email, $body, $subject); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Sender/RecoveryProcessMessageSender.php: -------------------------------------------------------------------------------- 1 | getEmail()) { 30 | throw new Exception('Email should not be null'); 31 | } 32 | 33 | $formatter = IntlDateFormatter::create($locale, IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT); 34 | $time = $formatter->format($user->getRecoveryStartTime()->add(new DateInterval('P2D'))); 35 | 36 | $body = $this->builder->buildBody($locale, $email, $time); 37 | $subject = $this->builder->buildSubject($locale, $email); 38 | $this->handler->send($email, $body, $subject); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Sender/WelcomeMessageSender.php: -------------------------------------------------------------------------------- 1 | getRepository(Domain::class)->getDefaultDomain(); 25 | $this->domain = null !== $domain ? $domain->getName() : ''; 26 | } 27 | 28 | /** 29 | * @throws Exception 30 | */ 31 | public function send(User $user, string $locale): void 32 | { 33 | if (null === $email = $user->getEmail()) { 34 | throw new Exception('Email should not be null'); 35 | } 36 | 37 | if (strpos($email, (string) $this->domain) < 0) { 38 | return; 39 | } 40 | 41 | $body = $this->builder->buildBody($locale); 42 | $subject = $this->builder->buildSubject($locale); 43 | $this->handler->send($email, $body, $subject); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Traits/AliasAwareTrait.php: -------------------------------------------------------------------------------- 1 | alias; 14 | } 15 | 16 | public function setAlias(Alias $alias): void 17 | { 18 | $this->alias = $alias; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Traits/CreationTimeTrait.php: -------------------------------------------------------------------------------- 1 | creationTime; 16 | } 17 | 18 | public function setCreationTime(DateTime $creationTime): void 19 | { 20 | $this->creationTime = $creationTime; 21 | } 22 | 23 | #[ORM\PrePersist] 24 | public function updateCreationTime(): void 25 | { 26 | if (null === $this->creationTime) { 27 | $this->setCreationTime(new DateTime()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Traits/DeleteTrait.php: -------------------------------------------------------------------------------- 1 | false])] 10 | private bool $deleted = false; 11 | 12 | public function isDeleted(): bool 13 | { 14 | return $this->deleted; 15 | } 16 | 17 | public function getDeleted(): bool 18 | { 19 | return $this->deleted; 20 | } 21 | 22 | public function setDeleted(bool $deleted): void 23 | { 24 | $this->deleted = $deleted; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Traits/DomainAwareTrait.php: -------------------------------------------------------------------------------- 1 | domain; 19 | } 20 | 21 | public function setDomain(Domain $domain): void 22 | { 23 | $this->domain = $domain; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/DomainGuesserAwareTrait.php: -------------------------------------------------------------------------------- 1 | domainGuesser; 14 | } 15 | 16 | public function setDomainGuesser(DomainGuesser $domainGuesser): void 17 | { 18 | $this->domainGuesser = $domainGuesser; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Traits/EmailTrait.php: -------------------------------------------------------------------------------- 1 | email; 21 | } 22 | 23 | public function setEmail(string $email): void 24 | { 25 | $this->email = $email; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Traits/IdTrait.php: -------------------------------------------------------------------------------- 1 | id; 17 | } 18 | 19 | public function setId(int $id): void 20 | { 21 | $this->id = $id; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Traits/InvitationVoucherTrait.php: -------------------------------------------------------------------------------- 1 | invitationVoucher; 19 | } 20 | 21 | public function setInvitationVoucher(Voucher $invitationVoucher = null): void 22 | { 23 | $this->invitationVoucher = $invitationVoucher; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/LastLoginTimeTrait.php: -------------------------------------------------------------------------------- 1 | lastLoginTime; 16 | } 17 | 18 | public function setLastLoginTime(?DateTime $LastLoginTime): void 19 | { 20 | $this->lastLoginTime = $LastLoginTime; 21 | } 22 | 23 | public function updateLastLoginTime(): void 24 | { 25 | $this->setLastLoginTime(new DateTime()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Traits/MailCryptEnabledTrait.php: -------------------------------------------------------------------------------- 1 | false], name: "mail_crypt")] 10 | private bool $mailCryptEnabled = false; 11 | 12 | public function getMailCryptEnabled(): bool 13 | { 14 | return $this->mailCryptEnabled; 15 | } 16 | 17 | public function setMailCryptEnabled(bool $mailCrypt): void 18 | { 19 | $this->mailCryptEnabled = $mailCrypt; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traits/MailCryptPublicKeyTrait.php: -------------------------------------------------------------------------------- 1 | mailCryptPublicKey; 15 | } 16 | 17 | public function setMailCryptPublicKey(string $mailCryptPublicKey): void 18 | { 19 | $this->mailCryptPublicKey = $mailCryptPublicKey; 20 | } 21 | 22 | public function hasMailCryptPublicKey(): bool 23 | { 24 | return (bool) $this->getMailCryptPublicKey(); 25 | } 26 | 27 | public function eraseMailCryptPublicKey(): void 28 | { 29 | $this->mailCryptPublicKey = null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Traits/MailCryptSecretBoxTrait.php: -------------------------------------------------------------------------------- 1 | mailCryptSecretBox; 15 | } 16 | 17 | public function setMailCryptSecretBox(?string $mailCryptSecretBox): void 18 | { 19 | $this->mailCryptSecretBox = $mailCryptSecretBox; 20 | } 21 | 22 | public function hasMailCryptSecretBox(): bool 23 | { 24 | return (bool) $this->getMailCryptSecretBox(); 25 | } 26 | 27 | public function eraseMailCryptSecretBox(): void 28 | { 29 | $this->mailCryptSecretBox = null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Traits/NameTrait.php: -------------------------------------------------------------------------------- 1 | name; 20 | } 21 | 22 | public function setName(?string $name): void 23 | { 24 | $this->name = $name; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Traits/PasswordTrait.php: -------------------------------------------------------------------------------- 1 | password; 15 | } 16 | 17 | public function setPassword(?string $password): void 18 | { 19 | $this->password = $password; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traits/PasswordVersionTrait.php: -------------------------------------------------------------------------------- 1 | passwordVersion; 15 | } 16 | 17 | public function setPasswordVersion(?int $passwordVersion): void 18 | { 19 | $this->passwordVersion = $passwordVersion; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traits/PlainMailCryptPrivateKeyTrait.php: -------------------------------------------------------------------------------- 1 | plainMailCryptPrivateKey; 15 | } 16 | 17 | public function setPlainMailCryptPrivateKey(?string $plainMailCryptPrivateKey): void 18 | { 19 | $this->plainMailCryptPrivateKey = $plainMailCryptPrivateKey; 20 | } 21 | 22 | public function erasePlainMailCryptPrivateKey(): void 23 | { 24 | $this->plainMailCryptPrivateKey = null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Traits/PlainPasswordTrait.php: -------------------------------------------------------------------------------- 1 | plainPassword; 20 | } 21 | 22 | public function setPlainPassword(?string $plainPassword): void 23 | { 24 | $this->plainPassword = $plainPassword; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Traits/PlainRecoveryTokenTrait.php: -------------------------------------------------------------------------------- 1 | plainRecoveryToken; 15 | } 16 | 17 | public function setPlainRecoveryToken(?string $plainRecoveryToken): void 18 | { 19 | $this->plainRecoveryToken = $plainRecoveryToken; 20 | } 21 | 22 | public function erasePlainRecoveryToken(): void 23 | { 24 | $this->plainRecoveryToken = null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Traits/PrivateKeyTrait.php: -------------------------------------------------------------------------------- 1 | privateKey; 12 | } 13 | 14 | public function setPrivateKey(?string $privateKey): void 15 | { 16 | $this->privateKey = $privateKey; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Traits/PublicKeyTrait.php: -------------------------------------------------------------------------------- 1 | publicKey; 12 | } 13 | 14 | public function setPublicKey(?string $publicKey): void 15 | { 16 | $this->publicKey = $publicKey; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Traits/QuotaTrait.php: -------------------------------------------------------------------------------- 1 | quota; 15 | } 16 | 17 | public function setQuota(?int $quota): void 18 | { 19 | $this->quota = $quota; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traits/RandomTrait.php: -------------------------------------------------------------------------------- 1 | false])] 10 | private bool $random = false; 11 | 12 | public function isRandom(): bool 13 | { 14 | return $this->random; 15 | } 16 | 17 | public function setRandom(bool $random): void 18 | { 19 | $this->random = $random; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traits/RecoverySecretBoxTrait.php: -------------------------------------------------------------------------------- 1 | recoverySecretBox; 15 | } 16 | 17 | public function setRecoverySecretBox(?string $recoverySecretBox): void 18 | { 19 | $this->recoverySecretBox = $recoverySecretBox; 20 | } 21 | 22 | public function hasRecoverySecretBox(): bool 23 | { 24 | return (bool) $this->getRecoverySecretBox(); 25 | } 26 | 27 | public function eraseRecoverySecretBox(): void 28 | { 29 | $this->recoverySecretBox = null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Traits/RecoveryStartTimeTrait.php: -------------------------------------------------------------------------------- 1 | recoveryStartTime; 17 | } 18 | 19 | public function setRecoveryStartTime(DateTime $recoveryStartTime): void 20 | { 21 | $this->recoveryStartTime = $recoveryStartTime; 22 | } 23 | 24 | /** 25 | * @throws Exception 26 | */ 27 | public function updateRecoveryStartTime(): void 28 | { 29 | $this->setRecoveryStartTime(new DateTime()); 30 | } 31 | 32 | public function eraseRecoveryStartTime(): void 33 | { 34 | $this->recoveryStartTime = null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Traits/RecoveryTokenTrait.php: -------------------------------------------------------------------------------- 1 | recoveryToken; 12 | } 13 | 14 | public function setRecoveryToken(?string $recoveryToken): void 15 | { 16 | $this->recoveryToken = $recoveryToken; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Traits/SaltTrait.php: -------------------------------------------------------------------------------- 1 | salt; 12 | } 13 | 14 | public function setSalt(?string $salt): void 15 | { 16 | $this->salt = $salt; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Traits/UpdatedTimeTrait.php: -------------------------------------------------------------------------------- 1 | updatedTime; 16 | } 17 | 18 | public function setUpdatedTime(DateTime $updatedTime): void 19 | { 20 | $this->updatedTime = $updatedTime; 21 | } 22 | 23 | #[ORM\PrePersist] 24 | public function updateUpdatedTime(): void 25 | { 26 | $this->setUpdatedTime(new DateTime()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Traits/UserAwareTrait.php: -------------------------------------------------------------------------------- 1 | user; 16 | } 17 | 18 | public function setUser(User $user): void 19 | { 20 | $this->user = $user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Validator/Constraints/EmailAddress.php: -------------------------------------------------------------------------------- 1 | getEmail(), '@'), 1); 24 | $domain = $this->manager->getRepository(Domain::class)->findOneBy(['name' => $name]); 25 | 26 | if (null === $domain) { 27 | $this->context->addViolation('form.missing-domain'); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Validator/Constraints/EmailLength.php: -------------------------------------------------------------------------------- 1 | minLength = $minLength ?? $this->minLength; 25 | $this->maxLength = $maxLength ?? $this->maxLength; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/Constraints/EmailLengthValidator.php: -------------------------------------------------------------------------------- 1 | minLength) && strlen($localPart) < $minLength) { 28 | $this->context->addViolation('registration.email-too-short', ['%min%' => $constraint->minLength]); 29 | } 30 | 31 | if (is_numeric($maxLength = $constraint->maxLength) && strlen($localPart) > $maxLength) { 32 | $this->context->addViolation('registration.email-too-long', ['%max%' => $constraint->maxLength]); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Validator/Constraints/Lowercase.php: -------------------------------------------------------------------------------- 1 | mode = $mode ?? $this->mode; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Validator/Constraints/LowercaseValidator.php: -------------------------------------------------------------------------------- 1 | context->buildViolation($constraint->message) 33 | ->setParameter('{{ string }}', $value) 34 | ->addViolation(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Validator/Constraints/PasswordPolicy.php: -------------------------------------------------------------------------------- 1 | exists = $exists ?? $this->exists; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/Constraints/VoucherUser.php: -------------------------------------------------------------------------------- 1 | getUser(); 25 | if (null !== $user && $user->hasRole(Roles::SUSPICIOUS)) { 26 | $this->context->addViolation('voucher.suspicious-user'); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Validator/PasswordPolicyValidator.php: -------------------------------------------------------------------------------- 1 | handler->validate($value); 22 | 23 | if (!empty($errors)) { 24 | foreach ($errors as $error) { 25 | $this->context->addViolation($error); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /templates/Admin/standard_layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@SonataAdmin/standard_layout.html.twig' %} 2 | 3 | {% block sonata_sidebar_search %} 4 | {% endblock %} 5 | 6 | {% block side_bar_after_nav %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/Admin/user_block.html.twig: -------------------------------------------------------------------------------- 1 | {% block user_block %} 2 | 3 |
  • 4 | Return to Index 5 |
  • 6 |
  • 7 | Logout 8 |
  • 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templates/Block/block_statistics.html.twig: -------------------------------------------------------------------------------- 1 | {% extends sonata_block.templates.block_base %} 2 | 3 | {% block block %} 4 |
    5 |
    6 |
    7 |

    {{ settings.title }}

    8 |
    9 |
    10 |
    11 |
    {{ "admin.registered-accounts"|trans }}
    12 |
    {{ users_count }}
    13 |
    {{ "admin.registered-accounts-last-week"|trans }}s
    14 |
    {{ users_since }}
    15 | {% if is_granted('ROLE_ADMIN') %} 16 |
    {{ "admin.invite-codes"|trans }}
    17 |
    {{ vouchers_count }}
    18 |
    {{ "admin.invite-codes-redeemed"|trans }}
    19 |
    {{ vouchers_redeemed }} ({{ "Ratio"|trans }}: {{ vouchers_ratio }})
    20 | {% endif %} 21 |
    22 |
    23 |
    24 |
    25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/Email/suspicious_children.twig: -------------------------------------------------------------------------------- 1 | {% for child, parent in suspiciousChildren %} 2 | Suspicious User {{ parent }} has invited {{ child }}. 3 | {% endfor %} -------------------------------------------------------------------------------- /templates/Email/weekly_report.twig: -------------------------------------------------------------------------------- 1 | {{ users|length }} registrations since last week 2 | 3 | ┌────────────────────────────────┬─────────────────────┐ 4 | │ {{ '%-30.30s'|format('Account') }} │ {{ '%-19.19s'|format('Date') }} │ 5 | ├────────────────────────────────┼─────────────────────┤ 6 | {% for user in users %} 7 | │ {{ '%-30.30s'|format(user.email) }} │ {{ user.creationTime|date('d.m.Y H:i:s') }} │ 8 | {% endfor %} 9 | └────────────────────────────────┴─────────────────────┘ 10 | 11 | -------------------------------------------------------------------------------- /templates/Exception/show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "error.title"|trans }}{% endblock %} 4 | 5 | {% block navbar %} 6 | {% embed '@MopaBootstrap/Navbar/navbar.html.twig' with { fixedTop: true, staticTop: true, inverse: true, fluid: true } %} 7 | {% block brand %} 8 | {{ domain }} 9 | {% endblock %} 10 | 11 | {% block menu %} 12 | {{ mopa_bootstrap_menu('navbar-left') }} 13 | {% endblock %} 14 | {% endembed %} 15 | {% endblock %} 16 | 17 | {% block content %} 18 |
    19 |
    20 |

    {{ "error.title"|trans }}

    21 | {% if status_code == 404 %} 22 |

    {{ "error.page_not_found"|trans }}

    23 | {% else %} 24 |

    {{ "error.generic_error"|trans }}

    25 | {% endif %} 26 |

    {{ "error.back_link"|trans }}

    27 |
    28 |
    29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/Footer/logged_in_footer.twig: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /templates/Form/fields.html.twig: -------------------------------------------------------------------------------- 1 | {% block form_errors %} 2 | {% apply spaceless %} 3 | {% if errors|length > 0 %} 4 |
    5 | {% for error in errors %} 6 |

    {{ error.message }}

    7 | {% endfor %} 8 |
    9 | {% endif %} 10 | {% endapply %} 11 | {% endblock form_errors %} 12 | -------------------------------------------------------------------------------- /templates/Init/domain.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% form_theme form 'Form/fields.html.twig' %} 4 | 5 | {% block title %}{{ "init.title"|trans }}{% endblock %} 6 | 7 | {% block subtitle %}{{ "init.title"|trans }}{% endblock %} 8 | 9 | {% block content %} 10 | 11 |
    12 |
    13 |

    {{ "init.title"|trans }}

    14 | 15 |

    {{ "init_domain.lead"|trans }}

    16 |

    {{ "init_domain.text"|trans }}

    17 |
    18 | 19 |
    20 | {{ form_start(form) }} 21 | {{ form_errors(form) }} 22 | 23 |
    24 | {{ form_label(form.domain) }} 25 | {{ form_errors(form.domain) }} 26 | {{ form_widget(form.domain, {'attr': {'class': 'form-control' }}) }} 27 |
    28 | 29 |
    30 | {{ form_widget(form.submit, {'attr': {'class': 'btn btn-primary' }}) }} 31 |
    32 | {{ form_end(form) }} 33 |
    34 |
    35 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /templates/Recovery/new_recovery_token.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "recovery.header"|trans }}{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |

    {{ "recovery.header"|trans }}

    9 |

    {{ "recovery.token-lead"|trans }}

    10 |
    11 |
    12 |
    13 |
    14 | {% include 'Recovery/show_recovery_token.html.twig' %} 15 |
    16 |
    17 | {% include 'Recovery/recovery_token_notes.html.twig' %} 18 |
    19 |
    20 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/Recovery/recovery.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "recovery.header"|trans }}{% endblock %} 4 | 5 | {% form_theme form 'Form/fields.html.twig' %} 6 | 7 | {% block content %} 8 |
    9 |
    10 |

    {{ "recovery.header"|trans }}

    11 |

    {{ "recovery.lead"|trans }}

    12 | {% block recovery_content %} 13 | {% endblock %} 14 |
    15 |
    16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/Recovery/recovery_new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Recovery/recovery.html.twig' %} 2 | 3 | {% block recovery_content %} 4 | {{ form_start(form) }} 5 | {{ form_errors(form) }} 6 |
    7 | {{ form_label(form.email) }} 8 | {{ form_errors(form.email) }} 9 | {{ form_widget(form.email, {'attr': {'class': 'form-control' }}) }} 10 |
    11 | 12 |
    13 | {{ form_label(form.recoveryToken) }} 14 | {{ form_errors(form.recoveryToken) }} 15 | {{ form_widget(form.recoveryToken, {'attr': {'class': 'form-control', 'placeholder': 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' }}) }} 16 |
    17 | 18 |
    19 | {{ form_widget(form.submit, {'attr': {'class': 'btn btn-primary' }}) }} 20 | {{ "recovery.cancel"|trans }} 21 |
    22 | 23 | {{ form_end(form) }} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/Recovery/recovery_started.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Recovery/recovery.html.twig' %} 2 | 3 | {% block recovery_content %} 4 |

    {{ "recovery.started"|trans }}

    {{ "recovery.waiting-info"|trans }}

    5 |

    {{ "recovery.waiting-time"|trans({'%time%': active_time|date("d.m.Y")}) }}

    6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /templates/Recovery/recovery_token.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "recovery.token-lead"|trans }}{% endblock %} 4 | 5 | {% form_theme form 'Form/fields.html.twig' %} 6 | 7 | {% block content %} 8 |
    9 |
    10 |

    {{ "recovery.token-lead"|trans }}

    11 |
    12 |
    13 |
    14 |
    15 | {% include 'Recovery/show_recovery_token.html.twig' %} 16 |
    17 |
    18 | {% include 'Recovery/recovery_token_notes.html.twig' %} 19 |
    20 |
    21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/Recovery/recovery_token_notes.html.twig: -------------------------------------------------------------------------------- 1 |
    2 |
      3 |
    • {{ "recovery-token.created-info"|trans }}
    • 4 |
      5 |
    • {{ "recovery-token.displayed-once"|trans }}
    • 6 |
    7 |
    8 | -------------------------------------------------------------------------------- /templates/Recovery/show_recovery_token.html.twig: -------------------------------------------------------------------------------- 1 |

    {{ "recovery-token.created-headline"|trans }}

    2 |

    {{ recovery_token }}

    3 | 4 | {{ form_start(form) }} 5 | {{ form_errors(form) }} 6 | {{ form_widget(form.recoveryToken) }} 7 |
    8 | {{ form_errors(form.ack) }} 9 | {{ form_widget(form.ack) }} 10 | {{ form_label(form.ack) }} 11 |
    12 |
    13 | {{ form_widget(form.submit, {'attr': {'class': 'btn btn-primary' } }) }} 14 |
    15 | {{ form_end(form) }} 16 | -------------------------------------------------------------------------------- /templates/Registration/closed.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "closed.title"|trans }}{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |

    {{ "closed.headline"|trans }}

    9 | 10 |

    {{ "closed.lead"|trans }}

    11 | 12 | {{ "closed.text"|trans|raw }} 13 |
    14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/Registration/recovery_token.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "registration.recovery-token-headline"|trans }}{% endblock %} 4 | 5 | {% form_theme form 'Form/fields.html.twig' %} 6 | 7 | {% block content %} 8 |
    9 |
    10 |

    {{ "recovery-token.headline"|trans }}

    11 |
    12 |
    13 |
    14 |
    15 | {% include 'Recovery/show_recovery_token.html.twig' %} 16 |
    17 |
    18 | {% include 'Recovery/recovery_token_notes.html.twig' %} 19 |
    20 |
    21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/Registration/welcome.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "welcome.headline"|trans({'%domain%': domain, '%project_name%': project_name}) }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
    8 |
    9 |

    {{ "welcome.headline"|trans({'%domain%': domain, '%project_name%': project_name}) }}

    10 | 11 |

    {{ "welcome.lead"|trans }}

    12 | 13 | {{ "welcome.text"|trans({'%project_name%': project_name, '%app_url%': app_url})|raw }} 14 | 15 |

    {{ "welcome.next-button"|trans }}

    16 |
    17 |
    18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/Start/account.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "start.account-settings"|trans }}{% endblock %} 4 | 5 | {% block breadcrumbs %} 6 | 10 | {% endblock %} 11 | {% block content %} 12 |
    13 |
    14 | {% include 'Start/change_password.html.twig' %} 15 |
    16 |
    17 | {% include 'Start/twofactor.html.twig' %} 18 |
    19 |
    20 | {% include 'Start/recovery_token.html.twig' %} 21 |
    22 |
    23 | {% include 'Start/delete_account.html.twig' %} 24 |
    25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/Start/change_password.html.twig: -------------------------------------------------------------------------------- 1 | {% form_theme password_form 'Form/fields.html.twig' %} 2 | 3 | {{ form_start(password_form) }} 4 |

    {{ "form.change-password"|trans }}

    5 | 6 | {{ form_errors(password_form) }} 7 | 8 |
    9 | {{ form_label(password_form.password) }} 10 | {{ form_errors(password_form.password) }} 11 | {{ form_widget(password_form.password, {'attr': {'class': 'form-control' }}) }} 12 |
    13 | 14 |
    15 | {{ form_label(password_form.newPassword.first) }} 16 | {{ form_errors(password_form.newPassword.first) }} 17 | {{ form_widget(password_form.newPassword.first, {'attr': {'class': 'form-control' }}) }} 18 | 19 | {{ form_label(password_form.newPassword.second) }} 20 | {{ form_widget(password_form.newPassword.second, {'attr': {'class': 'form-control' }}) }} 21 |
    22 | 23 |
    24 | {{ form_widget(password_form.submit, {'attr': {'class': 'btn btn-primary' }}) }} 25 |
    26 | {{ form_end(password_form) }} 27 | 28 |
    29 |

    {{ "registration.information"|trans }}

    30 |

    {{ "registration.information-password-policy"|trans|raw }}

    31 |
    32 | -------------------------------------------------------------------------------- /templates/Start/delete_account.html.twig: -------------------------------------------------------------------------------- 1 |

    {{ "index.delete-headline"|trans }}

    2 |

    {{ "index.delete-description"|trans({'%project_name%': project_name})|raw }}

    3 |

    {{ "index.delete-button"|trans }}

    4 | -------------------------------------------------------------------------------- /templates/Start/index_spam.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block subtitle %}{{ "spam.title"|trans({'%domain%': domain}) }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
    8 |
    9 |

    {{ "spam.title"|trans({'%domain%': domain}) }}

    10 | 11 |

    {{ "spam.text"|trans }}

    12 |
    13 |
    14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/Start/recovery_token.html.twig: -------------------------------------------------------------------------------- 1 |

    {{ "recovery-token.headline"|trans }}

    2 |

    {{ "recovery-token.lead"|trans }}

    3 | {% if not recovery_secret_set %} 4 |

    {{ "recovery-token.unset"|trans }}

    5 | {% endif %} 6 |

    {{ "index.recovery-token-button"|trans }}

    7 | -------------------------------------------------------------------------------- /templates/Start/twofactor.html.twig: -------------------------------------------------------------------------------- 1 |

    {{ "account.twofactor.headline"|trans }}

    2 |

    {{ "account.twofactor.lead"|trans }}

    3 | {% if not twofactor_enabled %} 4 |

    5 | 6 | {{ "account.twofactor.unset"|trans }} 7 |

    8 | {% endif %} 9 |

    {{ "account.twofactor.button"|trans }}

    10 | -------------------------------------------------------------------------------- /templates/Twofactor/twofactor_notes.html.twig: -------------------------------------------------------------------------------- 1 |
    2 |

    3 |

    {{ "account.twofactor.lead"|trans }}

    4 |
    5 |
      6 |
    • {{ "account.twofactor.desc-login"|trans({'%app_url%': app_url}) }}
    • 7 |
    • {{ "account.twofactor.desc-lost"|trans({'%app_url%': app_url})|raw }}
    • 8 |
    9 |
    10 | -------------------------------------------------------------------------------- /templates/User/twofactor_backup_ack.html.twig: -------------------------------------------------------------------------------- 1 | {% form_theme backupAckForm 'Form/fields.html.twig' %} 2 | 3 |
    4 | {{ "account.twofactor.backup-ack-lead"|trans }} 5 |
    6 |
    7 |
     8 |         {% for key, value in twofactor_backup_codes %}
     9 |             {{ value }}
    10 |         {% endfor %}
    11 |     
    12 |
    13 | {{ form_start(backupAckForm) }} 14 | {{ form_errors(backupAckForm) }} 15 |
    16 | {{ form_errors(backupAckForm.ack) }} 17 | {{ form_widget(backupAckForm.ack) }} 18 | {{ form_label(backupAckForm.ack) }} 19 |
    20 |
    21 | {{ form_widget(backupAckForm.submit, {'attr': {'class': 'btn btn-primary' } }) }} 22 |
    23 | {{ form_end(backupAckForm) }} 24 | -------------------------------------------------------------------------------- /templates/User/twofactor_enable.html.twig: -------------------------------------------------------------------------------- 1 | {% form_theme confirmForm 'Form/fields.html.twig' %} 2 | 3 |
    4 | {{ "account.twofactor.enable-lead"|trans }} 5 |
    6 |
    7 | 8 |
    9 | {{ form_start(confirmForm) }} 10 | {{ form_errors(confirmForm) }} 11 |
    12 | {{ form_label(confirmForm.totpSecret) }} 13 | {{ form_errors(confirmForm.totpSecret) }} 14 | {{ form_widget(confirmForm.totpSecret, {'attr': { 15 | 'class': 'form-control', 16 | 'autofocus': '', 17 | 'placeholder': 'form.twofactor-login-placeholder', 18 | 'autocomplete': 'off' 19 | }}) }} 20 |
    21 |
    22 | {{ form_widget(confirmForm.submit, {'attr': {'class': 'btn btn-primary' } }) }} 23 |
    24 | {{ form_end(confirmForm) }} 25 | -------------------------------------------------------------------------------- /templates/_locale_switcher.html.twig: -------------------------------------------------------------------------------- 1 | {% set route = app.request.attributes.get('_route') %} 2 | {% set route_params = app.request.attributes.get('_route_params') %} 3 | {% set params = route_params|merge(app.request.query.all) %} 4 | 5 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Command/AdminPasswordCommandTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(AdminPasswordUpdater::class) 16 | ->disableOriginalConstructor() 17 | ->getMock(); 18 | 19 | $command = new AdminPasswordCommand($updater); 20 | $app = new Application(); 21 | $app->add($command); 22 | $commandTester = new CommandTester($command); 23 | 24 | $commandTester->execute(['password' => 'test']); 25 | 26 | $output = $commandTester->getDisplay(); 27 | $this->assertEquals('', $output); 28 | 29 | $commandTester->setInputs(['password via interactive command\n']); 30 | $commandTester->execute([]); 31 | 32 | $output = $commandTester->getDisplay(); 33 | $this->assertStringContainsString('Please enter new admin password', $output); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Controller/OpenPGPControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/openpgp'); 14 | 15 | $this->assertResponseRedirects('/login'); 16 | } 17 | 18 | public function testVisitingAuthenticated(): void 19 | { 20 | $client = static::createClient(); 21 | $user = $client->getContainer()->get('doctrine')->getRepository(User::class)->findOneBy(['email' => 'user@example.org']); 22 | 23 | $client->loginUser($user); 24 | 25 | $client->request('GET', '/openpgp'); 26 | 27 | $this->assertResponseIsSuccessful(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Controller/RecoveryControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/recovery'); 13 | 14 | $this->assertResponseIsSuccessful(); 15 | } 16 | 17 | public function testVisitRecoveryWitInvalidRecoveryToken() 18 | { 19 | $client = static::createClient(); 20 | $crawler = $client->request('GET', '/recovery'); 21 | 22 | $form = $crawler->selectButton('Recover')->form(); 23 | $form['recovery_process[email]'] = 'user@example.com'; 24 | $form['recovery_process[recoveryToken]'] = 'invalid-token'; 25 | 26 | $client->submit($form); 27 | 28 | $this->assertResponseIsSuccessful(); 29 | $this->assertSelectorTextContains('div.alert-danger', "This token has an invalid format."); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Controller/RegistrationControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/register'); 13 | 14 | $voucher = $crawler->filter('input#registration_voucher')->first(); 15 | $this->assertNotNull($voucher); 16 | $this->assertNull($voucher->attr('readonly')); 17 | } 18 | 19 | public function testRegisterWithVoucher(): void 20 | { 21 | $client = static::createClient(); 22 | $crawler = $client->request('GET', '/register/161161'); 23 | 24 | $voucher = $crawler->filter('input#registration_voucher')->first(); 25 | $this->assertNotNull($voucher); 26 | $this->assertNotNull($voucher->attr('readonly')); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Controller/RoundcubeControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/api/roundcube'); 14 | 15 | self::assertResponseStatusCodeSame(401); 16 | } 17 | 18 | public function testGetUserAliases(): void 19 | { 20 | $client = static::createClient(); 21 | $userRepository = static::getContainer()->get('doctrine')->getRepository(User::class); 22 | $client->loginUser($userRepository->findOneByEmail('user2@example.org')); 23 | $client->request('GET', '/api/roundcube'); 24 | 25 | self::assertResponseIsSuccessful(); 26 | 27 | $expected = [ 28 | 'alias@example.org', 29 | 'alias2@example.org', 30 | ]; 31 | $data = json_decode($client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR); 32 | self::assertEquals($expected, $data); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/EventListener/UserCreatedListenerTest.php: -------------------------------------------------------------------------------- 1 | createMock(WebhookHandler::class); 20 | $webhookHandler->expects($this->once())->method('send')->with($user, 'user.created'); 21 | 22 | $listener = new UserCreatedListener($webhookHandler); 23 | $event = $this->createMock(UserCreatedEvent::class); 24 | $event->method('getUser')->willReturn($user); 25 | $listener->onUserCreated($event); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/EventListener/UserDeletedListenerTest.php: -------------------------------------------------------------------------------- 1 | createMock(WebhookHandler::class); 18 | $webhookHandler->expects($this->once())->method('send')->with($user, 'user.deleted'); 19 | 20 | $listener = new UserDeletedListener($webhookHandler); 21 | $event = $this->createMock(UserDeletedEvent::class); 22 | $event->method('getUser')->willReturn($user); 23 | $listener->onUserDeleted($event); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Factory/DomainFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertSame($name, $domain->getName()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Factory/VoucherFactoryTest.php: -------------------------------------------------------------------------------- 1 | getCreationTime()); 18 | self::assertNotNull($voucher->getUser()); 19 | self::assertNotNull($voucher->getCode()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Form/AliasDeleteTypeTest.php: -------------------------------------------------------------------------------- 1 | $password]; 15 | 16 | $form = $this->factory->create(AliasDeleteType::class); 17 | 18 | $object = new Delete(); 19 | $object->password = $password; 20 | 21 | // submit the data to the form directly 22 | $form->submit($formData); 23 | 24 | $this->assertTrue($form->isSynchronized()); 25 | $this->assertEquals($object, $form->getData()); 26 | 27 | $view = $form->createView(); 28 | $children = $view->children; 29 | 30 | foreach (array_keys($formData) as $key) { 31 | $this->assertArrayHasKey($key, $children); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Form/OpenPgpKeyDeleteTypeTest.php: -------------------------------------------------------------------------------- 1 | $password]; 15 | 16 | $form = $this->factory->create(OpenPgpDeleteType::class); 17 | 18 | $object = new Delete(); 19 | $object->password = $password; 20 | 21 | // submit the data to the form directly 22 | $form->submit($formData); 23 | 24 | self::assertTrue($form->isSynchronized()); 25 | self::assertEquals($object, $form->getData()); 26 | 27 | $view = $form->createView(); 28 | $children = $view->children; 29 | 30 | foreach (array_keys($formData) as $key) { 31 | self::assertArrayHasKey($key, $children); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Form/PasswordChangeTypeTest.php: -------------------------------------------------------------------------------- 1 | $password, 19 | 'newPassword' => [ 20 | 'first' => $newPassword, 21 | 'second' => $newPassword, 22 | ], 23 | ]; 24 | 25 | $model = new PasswordChange(); 26 | $form = $this->factory->create(PasswordChangeType::class, $model); 27 | 28 | $expected = new PasswordChange(); 29 | $expected->setPassword($password); 30 | $expected->setNewPassword($newPassword); 31 | 32 | $form->submit($formData); 33 | 34 | $this->assertTrue($form->isSynchronized()); 35 | 36 | $this->assertEquals($expected, $model); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Form/RecoveryTokenAckTypeTest.php: -------------------------------------------------------------------------------- 1 | true, 17 | 'recoveryToken' => $uuid, 18 | ]; 19 | 20 | $form = $this->factory->create(RecoveryTokenAckType::class); 21 | 22 | $object = new RecoveryTokenAck(); 23 | $object->ack = true; 24 | $object->setRecoveryToken($uuid); 25 | 26 | // submit the data to the form directly 27 | $form->submit($formData); 28 | 29 | $this->assertTrue($form->isSynchronized()); 30 | $this->assertEquals($object, $form->getData()); 31 | 32 | $view = $form->createView(); 33 | $children = $view->children; 34 | 35 | foreach (array_keys($formData) as $key) { 36 | $this->assertArrayHasKey($key, $children); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Form/RecoveryTokenTypeTest.php: -------------------------------------------------------------------------------- 1 | $password]; 15 | 16 | $form = $this->factory->create(RecoveryTokenType::class); 17 | 18 | $object = new RecoveryToken(); 19 | $object->password = $password; 20 | 21 | // submit the data to the form directly 22 | $form->submit($formData); 23 | 24 | $this->assertTrue($form->isSynchronized()); 25 | $this->assertEquals($object, $form->getData()); 26 | 27 | $view = $form->createView(); 28 | $children = $view->children; 29 | 30 | foreach (array_keys($formData) as $key) { 31 | $this->assertArrayHasKey($key, $children); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Form/TwofactorConfirmTypeTest.php: -------------------------------------------------------------------------------- 1 | $totpSecret]; 15 | 16 | $form = $this->factory->create(TwofactorConfirmType::class); 17 | 18 | $object = new TwofactorConfirm(); 19 | $object->totpSecret = $totpSecret; 20 | 21 | // submit the data to the form directly 22 | $form->submit($formData); 23 | 24 | $this->assertTrue($form->isSynchronized()); 25 | $this->assertEquals($object, $form->getData()); 26 | 27 | $view = $form->createView(); 28 | $children = $view->children; 29 | 30 | foreach (array_keys($formData) as $key) { 31 | $this->assertArrayHasKey($key, $children); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Form/UserDeleteTypeTest.php: -------------------------------------------------------------------------------- 1 | $password]; 15 | 16 | $form = $this->factory->create(UserDeleteType::class); 17 | 18 | $object = new Delete(); 19 | $object->password = $password; 20 | 21 | // submit the data to the form directly 22 | $form->submit($formData); 23 | 24 | $this->assertTrue($form->isSynchronized()); 25 | $this->assertEquals($object, $form->getData()); 26 | 27 | $view = $form->createView(); 28 | $children = $view->children; 29 | 30 | foreach (array_keys($formData) as $key) { 31 | $this->assertArrayHasKey($key, $children); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Handler/CryptoSecretHandlerTest.php: -------------------------------------------------------------------------------- 1 | expectException(Exception::class); 22 | $this->expectExceptionMessage('salt should not be null'); 23 | $secret = CryptoSecretHandler::create('message', 'password'); 24 | 25 | $secret->setSalt(null); 26 | CryptoSecretHandler::decrypt($secret, 'password'); 27 | } 28 | 29 | public function testDecrypt(): void 30 | { 31 | $secret = CryptoSecretHandler::create('message', 'password'); 32 | 33 | self::assertNull(CryptoSecretHandler::decrypt($secret, 'wrong_password')); 34 | self::assertEquals('message', CryptoSecretHandler::decrypt($secret, 'password')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Handler/PasswordStrengthHandlerTest.php: -------------------------------------------------------------------------------- 1 | validate($input); 20 | 21 | self::assertEquals($expected, $actual); 22 | } 23 | 24 | public function dataProvider(): array 25 | { 26 | return [ 27 | ['password', ['form.weak_password']], 28 | ['Password', ['form.weak_password']], 29 | ['pässword', ['form.forbidden_char', 'form.weak_password']], 30 | ['PässwordSecure1', ['form.forbidden_char']], 31 | ['PasswördSecure1', ['form.forbidden_char']], 32 | ['PasswordSecüre1', ['form.forbidden_char']], 33 | ['PasswordSecure1\'', ['form.forbidden_char']], 34 | ['passwordpasswordpassword', []], 35 | ['PasswordSecure1', []], 36 | ['PasswordSecure$', []], 37 | ['PasswordSecure!', []], 38 | ['PasswordSecure_', []], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Helper/PasswordGeneratorTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(PasswordHasherFactoryInterface::class) 17 | ->getMock(); 18 | $passwordHasherFactory->method('getPasswordHasher')->willReturn($hasher); 19 | $updater = new PasswordUpdater($passwordHasherFactory); 20 | 21 | $user = new User(); 22 | $updater->updatePassword($user, 'password'); 23 | 24 | $password = $user->getPassword(); 25 | self::assertNotNull($password); 26 | 27 | $updater->updatePassword($user, 'new password'); 28 | 29 | self::assertNotEquals($password, $user->getPassword()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Helper/RandomStringGeneratorTest.php: -------------------------------------------------------------------------------- 1 | expectException(Exception::class); 14 | $this->expectExceptionMessage('Base64 decoding of encrypted message failed'); 15 | $secret = new CryptoSecret('', '', ''); 16 | $secret::decode('brokenbase64%%%'); 17 | } 18 | 19 | public function testDecodeExceptionTruncated(): void 20 | { 21 | $this->expectException(Exception::class); 22 | $this->expectExceptionMessage('The encrypted message was truncated'); 23 | $secret = new CryptoSecret('', '', ''); 24 | $secret::decode('shortcipher'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Model/MailCryptKeyPairTest.php: -------------------------------------------------------------------------------- 1 | getPrivateKey()); 15 | self::assertEquals('public', $keyPair->getPublicKey()); 16 | 17 | $keyPair->erase(); 18 | 19 | self::assertNull($keyPair->getPrivateKey()); 20 | self::assertNull($keyPair->getPublicKey()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Security/UserCheckerTest.php: -------------------------------------------------------------------------------- 1 | userChecker = new UserChecker(); 15 | } 16 | 17 | public function testPreAuth(): void 18 | { 19 | $user = new User(); 20 | 21 | $this->userChecker->checkPreAuth($user); 22 | 23 | $deletedUser = new User(); 24 | $deletedUser->setDeleted(true); 25 | 26 | $this->expectException('Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException'); 27 | $this->userChecker->checkPreAuth($deletedUser); 28 | } 29 | 30 | public function testPostAuth(): void 31 | { 32 | $user = new User(); 33 | 34 | $this->userChecker->checkPostAuth($user); 35 | 36 | $this->expectNotToPerformAssertions(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/autoload.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../.env.test'); 15 | } 16 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 9 | } 10 | 11 | if ($_SERVER['APP_DEBUG']) { 12 | umask(0000); 13 | } 14 | -------------------------------------------------------------------------------- /tests/test_checkpassword_login.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | openssl s_client -connect 192.168.60.99:995 -quiet <