Merge branch 'master' into postfix-logging

master
Dimitri Huisman 3 years ago committed by GitHub
commit 567b5ef172
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,446 @@
name: CI
on:
push:
branches:
- staging
- testing
- '1.5'
- '1.6'
- '1.7'
- '1.8'
- master
# version tags, e.g. 1.7.1
- '[1-9].[0-9].[0-9]'
# pre-releases, e.g. 1.8-pre1
- 1.8-pre[0-9]
# test branches, e.g. test-debian
- test-*
###############################################
# REQUIRED secrets
# DOCKER_UN: ${{ secrets.Docker_Login }}
# Username of docker login for pushing the images to repo $DOCKER_ORG and $DOCKER_ORG_TESTS
# DOCKER_PW: ${{ secrets.Docker_Password }}
# Password of docker login for pushing the images to repo $DOCKER_ORG and $DOCKER_ORG_TESTS
# DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
# The docker repository where the images are pushed to.
# DOCKER_ORG_TESTS: ${{ secrets.DOCKER_ORG_TESTS }}
# The docker repository for test images. Only used for the branch TESTING (BORS try).
# Add the above secrets to your github repo to determine where the images will be pushed.
################################################
jobs:
build:
name: Build images
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
#For branch TESTING, we set the image tag to PR-xxxx
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG_TESTS }}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
sudo chmod 777 /images
- name: Configure images folder for caching
uses: actions/cache@v2
with:
path: /images
key: ${{ env.BRANCH }}-${{ github.run_id }}-${{ github.run_number }}
- name: Check docker-compose version
run: docker-compose -v
- name: Login docker
env:
DOCKER_UN: ${{ secrets.Docker_Login }}
DOCKER_PW: ${{ secrets.Docker_Password }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
run: echo "$DOCKER_PW" | docker login --username $DOCKER_UN --password-stdin
- name: Build all docker images
env:
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
run: docker-compose -f tests/build.yml build
- name: Save all docker images
run: docker save ${{ env.DOCKER_ORG }}/admin ${{ env.DOCKER_ORG }}/clamav ${{ env.DOCKER_ORG }}/docs ${{ env.DOCKER_ORG }}/dovecot ${{ env.DOCKER_ORG }}/fetchmail ${{ env.DOCKER_ORG }}/nginx ${{ env.DOCKER_ORG }}/none ${{ env.DOCKER_ORG }}/postfix ${{ env.DOCKER_ORG }}/postgresql ${{ env.DOCKER_ORG }}/radicale ${{ env.DOCKER_ORG }}/rainloop ${{ env.DOCKER_ORG }}/roundcube ${{ env.DOCKER_ORG }}/rspamd ${{ env.DOCKER_ORG }}/setup ${{ env.DOCKER_ORG }}/traefik-certdumper ${{ env.DOCKER_ORG }}/unbound -o /images/images.tar.gz
test-core:
name: Perform core tests
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG_TESTS }}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
sudo chmod 777 /images
- name: Configure images folder for caching
uses: actions/cache@v2
with:
path: /images
key: ${{ env.BRANCH }}-${{ github.run_id }}-${{ github.run_number }}
- name: Load docker images
run: docker load -i /images/images.tar.gz
- name: Install python packages
run: python3 -m pip install -r tests/requirements.txt
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test core suite
run: python tests/compose/test.py core 2
env:
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
test-fetchmail:
name: Perform fetchmail tests
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG_TESTS }}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
sudo chmod 777 /images
- name: Configure images folder for caching
uses: actions/cache@v2
with:
path: /images
key: ${{ env.BRANCH }}-${{ github.run_id }}-${{ github.run_number }}
- name: Load docker images
run: docker load -i /images/images.tar.gz
- name: Install python packages
run: python3 -m pip install -r tests/requirements.txt
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test fetch
run: python tests/compose/test.py fetchmail 2
env:
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
test-filters:
name: Perform filter tests
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG_TESTS }}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
sudo chmod 777 /images
- name: Configure images folder for caching
uses: actions/cache@v2
with:
path: /images
key: ${{ env.BRANCH }}-${{ github.run_id }}-${{ github.run_number }}
- name: Load docker images
run: docker load -i /images/images.tar.gz
- name: Install python packages
run: python3 -m pip install -r tests/requirements.txt
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test clamvav
run: python tests/compose/test.py filters 3
env:
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
test-rainloop:
name: Perform rainloop tests
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG_TESTS }}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
sudo chmod 777 /images
- name: Configure images folder for caching
uses: actions/cache@v2
with:
path: /images
key: ${{ env.BRANCH }}-${{ github.run_id }}-${{ github.run_number }}
- name: Load docker images
run: docker load -i /images/images.tar.gz
- name: Install python packages
run: python3 -m pip install -r tests/requirements.txt
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test rainloop
run: python tests/compose/test.py rainloop 2
env:
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
test-roundcube:
name: Perform roundcube tests
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG_TESTS }}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
sudo chmod 777 /images
- name: Configure images folder for caching
uses: actions/cache@v2
with:
path: /images
key: ${{ env.BRANCH }}-${{ github.run_id }}-${{ github.run_number }}
- name: Load docker images
run: docker load -i /images/images.tar.gz
- name: Install python packages
run: python3 -m pip install -r tests/requirements.txt
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test roundcube
run: python tests/compose/test.py roundcube 2
env:
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
test-webdav:
name: Perform webdav tests
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG_TESTS }}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
sudo chmod 777 /images
- name: Configure images folder for caching
uses: actions/cache@v2
with:
path: /images
key: ${{ env.BRANCH }}-${{ github.run_id }}-${{ github.run_number }}
- name: Load docker images
run: docker load -i /images/images.tar.gz
- name: Install python packages
run: python3 -m pip install -r tests/requirements.txt
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test webdav
run: python tests/compose/test.py webdav 2
env:
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
deploy:
name: Deploy images
runs-on: ubuntu-latest
needs:
- build
- test-core
- test-fetchmail
- test-filters
- test-rainloop
- test-roundcube
- test-webdav
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
#For branch TESTING, we set the image tag to PR-xxxx
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG_TESTS }}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ secrets.DOCKER_ORG }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
sudo chmod 777 /images
- name: Configure images folder for caching
# For staging we do not deploy images. So we do not have to load them from cache.
if: ${{ env.BRANCH != 'staging' }}
uses: actions/cache@v2
with:
path: /images
key: ${{ env.BRANCH }}-${{ github.run_id }}-${{ github.run_number }}
- name: Load docker images
if: ${{ env.BRANCH != 'staging' }}
run: docker load -i /images/images.tar.gz
- name: Deploy built docker images
env:
DOCKER_UN: ${{ secrets.Docker_Login }}
DOCKER_PW: ${{ secrets.Docker_Password }}
DOCKER_ORG: ${{ env.DOCKER_ORG }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
run: bash tests/deploy.sh
# This job is watched by bors. It only complets if building,testing and deploy worked.
ci-success:
name: CI-Done
#Returns true when none of the **previous** steps have failed or been canceled.
if: ${{ success() }}
needs:
- deploy
runs-on: ubuntu-latest
steps:
- name: CI/CD succeeded.
run: exit 0

@ -27,7 +27,7 @@ pull_request_rules:
- name: Trusted author and 1 approved review; trigger bors r+ - name: Trusted author and 1 approved review; trigger bors r+
conditions: conditions:
- author~=^(kaiyou|muhlemmer|mildred|HorayNarea|adi90x|hoellen|ofthesun9|Nebukadneza|micw|lub|Diman0)$ - author~=^(mergify|kaiyou|muhlemmer|mildred|HorayNarea|hoellen|ofthesun9|Nebukadneza|micw|lub|Diman0|ghostwheel42|nextgens)$
- -title~=(WIP|wip) - -title~=(WIP|wip)
- -label~=^(status/wip|status/blocked|review/need2)$ - -label~=^(status/wip|status/blocked|review/need2)$
- "#approved-reviews-by>=1" - "#approved-reviews-by>=1"

@ -1,55 +0,0 @@
branches:
only:
- staging
- testing
- '1.5'
- '1.6'
- '1.7'
- '1.8'
- master
# version tags, e.g. 1.7.1
- /^1\.[5678]\.\d+$/
# pre-releases, e.g. 1.8-pre1
- /^1\.8-pre\d+$/
# test branches, e.g. test-debian
- /^test-[\w\-\.]+$/
sudo: required
services: docker
addons:
apt:
packages:
- docker-ce
env:
- MAILU_VERSION=${TRAVIS_BRANCH////-}
language: python
python:
- "3.6"
install:
- pip install -r tests/requirements.txt
- sudo curl -L https://github.com/docker/compose/releases/download/1.23.0-rc3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
- sudo chmod +x /usr/local/bin/docker-compose
before_script:
- docker-compose -v
- docker-compose -f tests/build.yml build
- sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
script:
# test.py, test name and timeout between start and tests.
- python tests/compose/test.py core 1
- python tests/compose/test.py fetchmail 1
- travis_wait python tests/compose/test.py filters 10
- python tests/compose/test.py rainloop 1
- python tests/compose/test.py roundcube 1
- python tests/compose/test.py webdav 1
deploy:
provider: script
script: bash tests/deploy.sh
on:
all_branches: true
condition: -n $DOCKER_UN

@ -4,18 +4,49 @@ Changelog
Upgrade should run fine as long as you generate a new compose or stack Upgrade should run fine as long as you generate a new compose or stack
configuration and upgrade your mailu.env. configuration and upgrade your mailu.env.
Please note that the current 1.8 is what we call a "soft release": Its there for everyone to see and use, but to limit possible user-impact of this very big release, its not yet the default in the setup-utility for new users. When upgrading, please treat it with some care, and be sure to always have backups!
There are some changes to the configuration overrides. Override files are now mounted read-only into the containers. There are some changes to the configuration overrides. Override files are now mounted read-only into the containers.
The Dovecot and Postfix overrides are moved in their own sub-directory. The Dovecot and Postfix overrides are moved in their own sub-directory.
If there are local override files, they will need to be moved from overrides/ to overrides/dovecot and overrides/postfix/. If there are local override files, they will need to be moved from overrides/ to overrides/dovecot and overrides/postfix/.
See https://mailu.io/1.8/faq.html#how-can-i-override-settings for all the mappings. See https://mailu.io/1.8/faq.html#how-can-i-override-settings for all the mappings.
Please not that the shipped image for PostgreSQL database is deprecated. One major change for the docker compose file is that the antispam container needs a fixed hostname [#1837](https://github.com/Mailu/Mailu/issues/1837).
We advise to switch to an external database server. This is handled when you regenerate the docker-compose file. A fixed hostname is required to retain rspamd history.
This is also handled in the helm-chart repo.
<!-- TOWNCRIER --> Improvements have been made to protect again session-fixation attacks.
v1.8.0 - 2020-09-28 To be fully protected, it is required to change your SECRET_KEY in Mailu.env after upgrading.
A new SECRET_KEY is generated when you recreate your docker-compose.yml & mailu.env file via setup.mailu.io.
The SECRET_KEY is an uppercase alphanumeric string of length 16. You can manually create such a string via
```cat /dev/urandom | tr -dc 'A-Z0-9' | fold -w ${1:-16} | head -n 1```
After changing mailu.env, it is required to recreate all containers for the changes to be propagated.
Please note that the shipped image for PostgreSQL database is deprecated.
We advise to switch to an external PostgreSQL database server.
1.8.0 - 2021-08-06
--------------------
- Features: Update version of roundcube webmail and carddav plugin. This is a security update. ([#1841](https://github.com/Mailu/Mailu/issues/1841))
- Features: Update version of rainloop webmail to 1.16.0. This is a security update. ([#1845](https://github.com/Mailu/Mailu/issues/1845))
- Features: Changed default value of AUTH_RATELIMIT_SUBNET to false. Increased default value of the rate limit in setup utility (AUTH_RATELIMIT) to a higher value. ([#1867](https://github.com/Mailu/Mailu/issues/1867))
- Features: Update jquery used in setup. Set pinned versions in requirements.txt for setup. This is a security update. ([#1880](https://github.com/Mailu/Mailu/issues/1880))
- Bugfixes: Replace PUBLIC_HOSTNAME and PUBLIC_IP in "Received" headers to ensure that no undue spam points are attributed ([#191](https://github.com/Mailu/Mailu/issues/191))
- Bugfixes: Don't replace nested headers (typically in attached emails) ([#1660](https://github.com/Mailu/Mailu/issues/1660))
- Bugfixes: Fix letsencrypt access to certbot for the mail-letsencrypt flavour ([#1686](https://github.com/Mailu/Mailu/issues/1686))
- Bugfixes: Fix CVE-2020-25275 and CVE-2020-24386 by upgrading alpine for
dovecot which contains a fixed dovecot version. ([#1720](https://github.com/Mailu/Mailu/issues/1720))
- Bugfixes: Antispam service now uses a static hostname. Rspamd history is only retained when the service has a fixed hostname. ([#1837](https://github.com/Mailu/Mailu/issues/1837))
- Bugfixes: Fix a bug preventing colons from being used in passwords when using radicale/webdav. ([#1861](https://github.com/Mailu/Mailu/issues/1861))
- Bugfixes: Remove dot in blueprint name to prevent critical flask startup error in setup. ([#1874](https://github.com/Mailu/Mailu/issues/1874))
- Bugfixes: fix punycode encoding of domain names ([#1891](https://github.com/Mailu/Mailu/issues/1891))
- Improved Documentation: Update fail2ban documentation to use systemd backend instead of filepath for journald ([#1857](https://github.com/Mailu/Mailu/issues/1857))
- Misc: Switch from client side (cookie) sessions to server side sessions and protect against session-fixation attacks. We recommend that you change your SECRET_KEY after upgrading. ([#1783](https://github.com/Mailu/Mailu/issues/1783))
v1.8.0rc - 2020-09-28
-------------------- --------------------
- Features: Add support for backward-forwarding using SRS ([#328](https://github.com/Mailu/Mailu/issues/328)) - Features: Add support for backward-forwarding using SRS ([#328](https://github.com/Mailu/Mailu/issues/328))
@ -66,7 +97,7 @@ configuration and upgrade your mailu.env.
If you run the PostgreSQL server, the database was upgrade, so you will need to If you run the PostgreSQL server, the database was upgrade, so you will need to
dump the database before upgrading and load the dump after the upgrade is dump the database before upgrading and load the dump after the upgrade is
complete. Please not that the shipped image for PostgreSQL database will be complete. Please note that the shipped image for PostgreSQL database will be
deprecated before 1.8.0, you can switch to an external database server by then. deprecated before 1.8.0, you can switch to an external database server by then.
- Deprecation: using the internal postgres image will be deprecated by 1.8.0 - Deprecation: using the internal postgres image will be deprecated by 1.8.0

@ -8,9 +8,9 @@
- Mention an issue like: #001 - Mention an issue like: #001
- Auto close an issue like: closes #001 - Auto close an issue like: closes #001
## Prerequistes ## Prerequisites
Before we can consider review and merge, please make sure the following list is done and checked. Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list. If an entry in not applicable, you can check it or remove it from the list.
- [ ] In case of feature or enhancement: documentation updated accordingly - [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/guide.html#changelog) entry file. - [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.

@ -22,7 +22,7 @@ Main features include:
- **Web access**, multiple Webmails and administration interface - **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts - **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas - **Admin features**, global admins, announcements, per-domain delegation, quotas
- **Security**, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner - **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner
- **Antispam**, auto-learn, greylisting, DMARC and SPF - **Antispam**, auto-learn, greylisting, DMARC and SPF
- **Freedom**, all FOSS components, no tracker included - **Freedom**, all FOSS components, no tracker included

@ -1,3 +1,4 @@
status = [ status = [
"continuous-integration/travis-ci/push" "CI-Done"
] ]

@ -1,39 +1,51 @@
# First stage to build assets # First stage to build assets
ARG DISTRO=alpine:3.12 ARG DISTRO=alpine:3.14.2
ARG ARCH="" ARG ARCH=""
FROM ${ARCH}node:8 as assets
COPY --from=balenalib/rpi-alpine:3.10 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static FROM ${ARCH}node:16 as assets
COPY package.json ./ COPY package.json ./
RUN npm install RUN set -eu \
&& npm config set update-notifier false \
&& npm install --no-fund
COPY ./webpack.config.js ./ COPY webpack.config.js ./
COPY ./assets ./assets COPY assets ./assets
RUN mkdir static \ RUN set -eu \
&& ./node_modules/.bin/webpack-cli && sed -i 's/#007bff/#55a5d9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \
&& for l in ca da de:de_de en:en-gb es:es_es eu fr:fr_fr he hu is it:it_it ja nb_NO:no_nb nl:nl_nl pl pt:pt_pt ru sv:sv_se zh; do \
cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \
done \
&& node_modules/.bin/webpack-cli --color
# Actual application # Actual application
FROM $DISTRO FROM $DISTRO
COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
ENV TZ Etc/UTC
# python3 shared with most images # python3 shared with most images
RUN apk add --no-cache \ RUN set -eu \
python3 py3-pip git bash \ && apk add --no-cache python3 py3-pip py3-wheel git bash tzdata \
&& pip3 install --upgrade pip && pip3 install --upgrade pip
RUN mkdir -p /app RUN mkdir -p /app
WORKDIR /app WORKDIR /app
COPY requirements-prod.txt requirements.txt COPY requirements-prod.txt requirements.txt
RUN apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \ RUN set -eu \
&& apk add --no-cache --virtual build-dep \ && apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \
libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev \ && apk add --no-cache --virtual build-dep libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \
&& pip3 install -r requirements.txt \ && pip install --upgrade pip \
&& pip install -r requirements.txt \
&& apk del --no-cache build-dep && apk del --no-cache build-dep
COPY --from=assets static ./mailu/ui/static COPY --from=assets static ./mailu/static
COPY mailu ./mailu COPY mailu ./mailu
COPY migrations ./migrations COPY migrations ./migrations
COPY start.py /start.py COPY start.py /start.py
COPY audit.py /audit.py
RUN pybabel compile -d mailu/translations RUN pybabel compile -d mailu/translations
@ -43,4 +55,4 @@ ENV FLASK_APP mailu
CMD /start.py CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/ui/login?next=ui.index || exit 1 HEALTHCHECK CMD curl -f -L http://localhost/sso/login?next=ui.index || exit 1

@ -1,22 +1,59 @@
.select2-search--inline .select2-search__field:focus { /* mailu logo */
border: none; .mailu-logo {
opacity: .8;
}
.bg-mailu-logo {
background-color: #2980b9!important;
} }
.sidebar h4 { /* user image */
padding-left: 5px; .div-circle {
padding-right: 5px; position: relative;
overflow: hidden; width: 2.1rem;
text-overflow: ellipsis; height: 2.1rem;
opacity: .8;
background-color: white;
border-radius: 50%;
}
.div-circle > i {
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%)
} }
.sidebar-collapse .sidebar h4 { /* nice round preformatted configuration display */
display: none !important; .pre-config {
padding: 9px;
margin: 0;
white-space: pre-wrap;
word-wrap: anywhere;
border-radius: 4px;
} }
.logo a { /* fieldset */
color: #fff; legend {
font-size: inherit;
}
fieldset:disabled :not(legend) label {
opacity: .5;
}
fieldset:disabled .form-control:disabled {
color: gray;
} }
.sidebar-toggle { /* fix animation for icons in menu text */
padding: unset !important; .sidebar .nav-link p i {
transition: margin-left .3s linear,opacity .3s ease,visibility .3s ease;
}
/* fix select2 text color */
.select2-container--default .select2-selection--multiple .select2-selection__choice {
color: black;
}
/* range input spacing */
.input-group-text {
margin-right: 1em;
} }

@ -1,10 +1,79 @@
require('./app.css'); require('./app.css');
import 'select2'; import logo from './mailu.png';
jQuery("document").ready(function() { import modules from "./*.json";
jQuery(".mailselect").select2({
// TODO: conditionally (or lazy) load select2 and dataTable
$('document').ready(function() {
// intercept anchors with data-clicked attribute and open alternate location instead
$('[data-clicked]').click(function(e) {
e.preventDefault();
window.location.href = $(this).data('clicked');
});
// use post for language selection
$('#mailu-languages > a').click(function(e) {
e.preventDefault();
$.post({
url: $(this).attr('href'),
success: function() {
window.location = window.location.href;
},
});
});
// allow en-/disabling of inputs in fieldset with checkbox in legend
$('fieldset legend input[type=checkbox]').change(function() {
var fieldset = $(this).parents('fieldset');
if (this.checked) {
fieldset.removeAttr('disabled');
fieldset.find('input,textarea').not(this).removeAttr('disabled');
} else {
fieldset.attr('disabled', '');
fieldset.find('input,textarea').not(this).attr('disabled', '');
}
});
// display of range input value
$('input[type=range]').each(function() {
var value_element = $('#'+this.id+'_value');
if (value_element.length) {
value_element = $(value_element[0]);
var infinity = $(this).data('infinity');
var step = $(this).attr('step');
$(this).on('input', function() {
var num = (infinity && this.value == 0) ? '∞' : (this.value/step).toFixed(2);
if (num.endsWith('.00')) num = num.substr(0, num.length - 3);
value_element.text(num);
}).trigger('input');
}
});
// init select2
$('.mailselect').select2({
tags: true, tags: true,
tokenSeparators: [',', ' '] tokenSeparators: [',', ' '],
}) });
// init dataTable
var d = $(document.documentElement);
$('.dataTable').DataTable({
'responsive': true,
language: {
url: d.data('static') + d.attr('lang') + '.json',
},
});
// init clipboard.js
new ClipboardJS('.btn-clip');
// disable login if not possible
var l = $('#login_needs_https');
if (l.length && window.location.protocol != 'https:') {
l.removeClass("d-none");
$('form :input').prop('disabled', true);
}
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

@ -1,19 +1,24 @@
// jQuery
import jQuery from 'jquery';
import 'select2/dist/css/select2.css';
// bootstrap
import 'bootstrap/less/bootstrap.less';
import 'bootstrap';
// FA
import 'font-awesome/scss/font-awesome.scss';
// AdminLTE // AdminLTE
import 'admin-lte/build/less/AdminLTE-without-plugins.less'; import 'admin-lte/plugins/jquery/jquery.min.js';
import 'admin-lte/build/less/select2.less'; import 'admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js';
import 'admin-lte/build/less/skins/skin-blue.less'; import 'admin-lte/build/scss/adminlte.scss';
import 'admin-lte/build/js/Layout.js'; import 'admin-lte/build/js/AdminLTE.js';
import 'admin-lte/build/js/ControlSidebar.js';
import 'admin-lte/build/js/PushMenu.js'; // fontawesome plugin
import 'admin-lte/build/js/BoxRefresh.js'; import 'admin-lte/plugins/fontawesome-free/css/all.min.css';
// select2 plugin
import 'admin-lte/plugins/select2/css/select2.min.css';
import 'admin-lte/plugins/select2/js/select2.min.js';
// dataTables plugin
import 'admin-lte/plugins/datatables-bs4/css/dataTables.bootstrap4.min.css';
import 'admin-lte/plugins/datatables-responsive/css/responsive.bootstrap4.min.css';
import 'admin-lte/plugins/datatables/jquery.dataTables.min.js';
import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.min.js';
import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.min.js';
import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.min.js';
// clipboard.js
import 'clipboard/dist/clipboard.min.js';

@ -1,14 +1,19 @@
from mailu import app #!/usr/bin/python3
import sys import sys
import tabulate import tabulate
sys.path[0:0] = ['/app']
import mailu
app = mailu.create_app()
# Known endpoints without permissions # Known endpoints without permissions
known_missing_permissions = [ known_missing_permissions = [
"index", 'index',
"static", "bootstrap.static", 'static', 'bootstrap.static',
"admin.static", "admin.login" 'admin.static', 'admin.login'
] ]
@ -16,7 +21,7 @@ known_missing_permissions = [
missing_permissions = [] missing_permissions = []
permissions = {} permissions = {}
for endpoint, function in app.view_functions.items(): for endpoint, function in app.view_functions.items():
audit = function.__dict__.get("_audit_permissions") audit = function.__dict__.get('_audit_permissions')
if audit: if audit:
handler, args = audit handler, args = audit
if args: if args:
@ -28,16 +33,15 @@ for endpoint, function in app.view_functions.items():
elif endpoint not in known_missing_permissions: elif endpoint not in known_missing_permissions:
missing_permissions.append(endpoint) missing_permissions.append(endpoint)
# Fail if any endpoint is missing a permission check
if missing_permissions:
print("The following endpoints are missing permission checks:")
print(missing_permissions.join(","))
sys.exit(1)
# Display the permissions table # Display the permissions table
print(tabulate.tabulate([ print(tabulate.tabulate([
[route, *permissions[route.endpoint]] [route, *permissions[route.endpoint]]
for route in app.url_map.iter_rules() if route.endpoint in permissions for route in app.url_map.iter_rules() if route.endpoint in permissions
])) ]))
# Warn if any endpoint is missing a permission check
if missing_permissions:
print()
print('The following endpoints are missing permission checks:')
print(','.join(missing_permissions))

@ -1,22 +1,26 @@
""" Mailu admin app
"""
import flask import flask
import flask_bootstrap import flask_bootstrap
from mailu import utils, debug, models, manage, configuration from mailu import utils, debug, models, manage, configuration
import hmac
def create_app_from_config(config): def create_app_from_config(config):
""" Create a new application based on the given configuration """ Create a new application based on the given configuration
""" """
app = flask.Flask(__name__) app = flask.Flask(__name__, static_folder='static', static_url_path='/static')
app.cli.add_command(manage.mailu) app.cli.add_command(manage.mailu)
# Bootstrap is used for basic JS and CSS loading # Bootstrap is used for error display and flash messages
# TODO: remove this and use statically generated assets instead
app.bootstrap = flask_bootstrap.Bootstrap(app) app.bootstrap = flask_bootstrap.Bootstrap(app)
# Initialize application extensions # Initialize application extensions
config.init_app(app) config.init_app(app)
models.db.init_app(app) models.db.init_app(app)
utils.session.init_app(app)
utils.limiter.init_app(app) utils.limiter.init_app(app)
utils.babel.init_app(app) utils.babel.init_app(app)
utils.login.init_app(app) utils.login.init_app(app)
@ -24,6 +28,19 @@ def create_app_from_config(config):
utils.proxy.init_app(app) utils.proxy.init_app(app)
utils.migrate.init_app(app, models.db) utils.migrate.init_app(app, models.db)
app.device_cookie_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('DEVICE_COOKIE_KEY', 'utf-8'), 'sha256').digest()
app.temp_token_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('WEBMAIL_TEMP_TOKEN_KEY', 'utf-8'), 'sha256').digest()
app.srs_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('SRS_KEY', 'utf-8'), 'sha256').digest()
# Initialize list of translations
app.config.translations = {
str(locale): locale
for locale in sorted(
utils.babel.list_translations(),
key=lambda l: l.get_language_name().title()
)
}
# Initialize debugging tools # Initialize debugging tools
if app.config.get("DEBUG"): if app.config.get("DEBUG"):
debug.toolbar.init_app(app) debug.toolbar.init_app(app)
@ -36,15 +53,24 @@ def create_app_from_config(config):
def inject_defaults(): def inject_defaults():
signup_domains = models.Domain.query.filter_by(signup_enabled=True).all() signup_domains = models.Domain.query.filter_by(signup_enabled=True).all()
return dict( return dict(
signup_domains=signup_domains, signup_domains= signup_domains,
config=app.config config = app.config,
) )
# Import views # Jinja filters
from mailu import ui, internal @app.template_filter()
app.register_blueprint(ui.ui, url_prefix='/ui') def format_date(value):
app.register_blueprint(internal.internal, url_prefix='/internal') return utils.flask_babel.format_date(value) if value else ''
@app.template_filter()
def format_datetime(value):
return utils.flask_babel.format_datetime(value) if value else ''
# Import views
from mailu import ui, internal, sso
app.register_blueprint(ui.ui, url_prefix=app.config['WEB_ADMIN'])
app.register_blueprint(internal.internal, url_prefix='/internal')
app.register_blueprint(sso.sso, url_prefix='/sso')
return app return app

@ -1,6 +1,8 @@
import os import os
from datetime import timedelta
from socrate import system from socrate import system
import ipaddress
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
# Specific to the admin UI # Specific to the admin UI
@ -13,6 +15,7 @@ DEFAULT_CONFIG = {
'DEBUG': False, 'DEBUG': False,
'DOMAIN_REGISTRATION': False, 'DOMAIN_REGISTRATION': False,
'TEMPLATES_AUTO_RELOAD': True, 'TEMPLATES_AUTO_RELOAD': True,
'MEMORY_SESSIONS': False,
# Database settings # Database settings
'DB_FLAVOR': None, 'DB_FLAVOR': None,
'DB_USER': 'mailu', 'DB_USER': 'mailu',
@ -30,9 +33,16 @@ DEFAULT_CONFIG = {
'DOMAIN': 'mailu.io', 'DOMAIN': 'mailu.io',
'HOSTNAMES': 'mail.mailu.io,alternative.mailu.io,yetanother.mailu.io', 'HOSTNAMES': 'mail.mailu.io,alternative.mailu.io,yetanother.mailu.io',
'POSTMASTER': 'postmaster', 'POSTMASTER': 'postmaster',
'WILDCARD_SENDERS': '',
'TLS_FLAVOR': 'cert', 'TLS_FLAVOR': 'cert',
'AUTH_RATELIMIT': '10/minute;1000/hour', 'INBOUND_TLS_ENFORCE': False,
'AUTH_RATELIMIT_SUBNET': True, 'DEFER_ON_TLS_ERROR': True,
'AUTH_RATELIMIT_IP': '60/hour',
'AUTH_RATELIMIT_IP_V4_MASK': 24,
'AUTH_RATELIMIT_IP_V6_MASK': 56,
'AUTH_RATELIMIT_USER': '100/day',
'AUTH_RATELIMIT_EXEMPTION': '',
'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400,
'DISABLE_STATISTICS': False, 'DISABLE_STATISTICS': False,
# Mail settings # Mail settings
'DMARC_RUA': None, 'DMARC_RUA': None,
@ -43,17 +53,27 @@ DEFAULT_CONFIG = {
'DKIM_SELECTOR': 'dkim', 'DKIM_SELECTOR': 'dkim',
'DKIM_PATH': '/dkim/{domain}.{selector}.key', 'DKIM_PATH': '/dkim/{domain}.{selector}.key',
'DEFAULT_QUOTA': 1000000000, 'DEFAULT_QUOTA': 1000000000,
'MESSAGE_RATELIMIT': '200/day',
'MESSAGE_RATELIMIT_EXEMPTION': '',
'RECIPIENT_DELIMITER': '',
# Web settings # Web settings
'SITENAME': 'Mailu', 'SITENAME': 'Mailu',
'WEBSITE': 'https://mailu.io', 'WEBSITE': 'https://mailu.io',
'ADMIN' : 'none',
'WEB_ADMIN': '/admin', 'WEB_ADMIN': '/admin',
'WEB_WEBMAIL': '/webmail', 'WEB_WEBMAIL': '/webmail',
'WEBMAIL': 'none', 'WEBMAIL': 'none',
'RECAPTCHA_PUBLIC_KEY': '', 'RECAPTCHA_PUBLIC_KEY': '',
'RECAPTCHA_PRIVATE_KEY': '', 'RECAPTCHA_PRIVATE_KEY': '',
'LOGO_URL': None,
'LOGO_BACKGROUND': None,
# Advanced settings # Advanced settings
'PASSWORD_SCHEME': 'PBKDF2',
'LOG_LEVEL': 'WARNING', 'LOG_LEVEL': 'WARNING',
'SESSION_KEY_BITS': 128,
'SESSION_LIFETIME': 24,
'SESSION_COOKIE_SECURE': True,
'CREDENTIAL_ROUNDS': 12,
'TZ': 'Etc/UTC',
# Host settings # Host settings
'HOST_IMAP': 'imap', 'HOST_IMAP': 'imap',
'HOST_LMTP': 'imap:2525', 'HOST_LMTP': 'imap:2525',
@ -61,7 +81,6 @@ DEFAULT_CONFIG = {
'HOST_SMTP': 'smtp', 'HOST_SMTP': 'smtp',
'HOST_AUTHSMTP': 'smtp', 'HOST_AUTHSMTP': 'smtp',
'HOST_ADMIN': 'admin', 'HOST_ADMIN': 'admin',
'WEBMAIL': 'none',
'HOST_WEBMAIL': 'webmail', 'HOST_WEBMAIL': 'webmail',
'HOST_WEBDAV': 'webdav:5232', 'HOST_WEBDAV': 'webdav:5232',
'HOST_REDIS': 'redis', 'HOST_REDIS': 'redis',
@ -71,7 +90,7 @@ DEFAULT_CONFIG = {
'POD_ADDRESS_RANGE': None 'POD_ADDRESS_RANGE': None
} }
class ConfigManager(dict): class ConfigManager:
""" Naive configuration manager that uses environment only """ Naive configuration manager that uses environment only
""" """
@ -86,19 +105,25 @@ class ConfigManager(dict):
def get_host_address(self, name): def get_host_address(self, name):
# if MYSERVICE_ADDRESS is defined, use this # if MYSERVICE_ADDRESS is defined, use this
if '{}_ADDRESS'.format(name) in os.environ: if f'{name}_ADDRESS' in os.environ:
return os.environ.get('{}_ADDRESS'.format(name)) return os.environ.get(f'{name}_ADDRESS')
# otherwise use the host name and resolve it # otherwise use the host name and resolve it
return system.resolve_address(self.config['HOST_{}'.format(name)]) return system.resolve_address(self.config[f'HOST_{name}'])
def resolve_hosts(self): def resolve_hosts(self):
self.config["IMAP_ADDRESS"] = self.get_host_address("IMAP") for key in ['IMAP', 'POP3', 'AUTHSMTP', 'SMTP', 'REDIS']:
self.config["POP3_ADDRESS"] = self.get_host_address("POP3") self.config[f'{key}_ADDRESS'] = self.get_host_address(key)
self.config["AUTHSMTP_ADDRESS"] = self.get_host_address("AUTHSMTP") if self.config['WEBMAIL'] != 'none':
self.config["SMTP_ADDRESS"] = self.get_host_address("SMTP") self.config['WEBMAIL_ADDRESS'] = self.get_host_address('WEBMAIL')
self.config["REDIS_ADDRESS"] = self.get_host_address("REDIS")
if self.config["WEBMAIL"] != "none": def __get_env(self, key, value):
self.config["WEBMAIL_ADDRESS"] = self.get_host_address("WEBMAIL") key_file = key + "_FILE"
if key_file in os.environ:
with open(os.environ.get(key_file)) as file:
value_from_file = file.read()
return value_from_file.strip()
else:
return os.environ.get(key, value)
def __coerce_value(self, value): def __coerce_value(self, value):
if isinstance(value, str) and value.lower() in ('true','yes'): if isinstance(value, str) and value.lower() in ('true','yes'):
@ -108,10 +133,11 @@ class ConfigManager(dict):
return value return value
def init_app(self, app): def init_app(self, app):
# get current app config
self.config.update(app.config) self.config.update(app.config)
# get environment variables # get environment variables
self.config.update({ self.config.update({
key: self.__coerce_value(os.environ.get(key, value)) key: self.__coerce_value(self.__get_env(key, value))
for key, value in DEFAULT_CONFIG.items() for key, value in DEFAULT_CONFIG.items()
}) })
self.resolve_hosts() self.resolve_hosts()
@ -121,27 +147,18 @@ class ConfigManager(dict):
template = self.DB_TEMPLATES[self.config['DB_FLAVOR']] template = self.DB_TEMPLATES[self.config['DB_FLAVOR']]
self.config['SQLALCHEMY_DATABASE_URI'] = template.format(**self.config) self.config['SQLALCHEMY_DATABASE_URI'] = template.format(**self.config)
self.config['RATELIMIT_STORAGE_URL'] = 'redis://{0}/2'.format(self.config['REDIS_ADDRESS']) self.config['RATELIMIT_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/2'
self.config['QUOTA_STORAGE_URL'] = 'redis://{0}/1'.format(self.config['REDIS_ADDRESS']) self.config['QUOTA_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/1'
# update the app config itself self.config['SESSION_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/3'
app.config = self self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
self.config['SESSION_COOKIE_HTTPONLY'] = True
self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME']))
hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')]
self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if cidr)
self.config['MESSAGE_RATELIMIT_EXEMPTION'] = set([s for s in self.config['MESSAGE_RATELIMIT_EXEMPTION'].lower().replace(' ', '').split(',') if s])
self.config['HOSTNAMES'] = ','.join(hostnames)
self.config['HOSTNAME'] = hostnames[0]
def setdefault(self, key, value): # update the app config
if key not in self.config: app.config.update(self.config)
self.config[key] = value
return self.config[key]
def get(self, *args):
return self.config.get(*args)
def keys(self):
return self.config.keys()
def __getitem__(self, key):
return self.config.get(key)
def __setitem__(self, key, value):
self.config[key] = value
def __contains__(self, key):
return key in self.config

@ -1,6 +1,6 @@
import flask_debugtoolbar import flask_debugtoolbar
from werkzeug.contrib import profiler as werkzeug_profiler from werkzeug.middleware.profiler import ProfilerMiddleware
# Debugging toolbar # Debugging toolbar
@ -10,7 +10,7 @@ toolbar = flask_debugtoolbar.DebugToolbarExtension()
# Profiler # Profiler
class Profiler(object): class Profiler(object):
def init_app(self, app): def init_app(self, app):
app.wsgi_app = werkzeug_profiler.ProfilerMiddleware( app.wsgi_app = ProfilerMiddleware(
app.wsgi_app, restrictions=[30] app.wsgi_app, restrictions=[30]
) )

@ -5,9 +5,9 @@ import re
import urllib import urllib
import ipaddress import ipaddress
import socket import socket
import sqlalchemy.exc
import tenacity import tenacity
SUPPORTED_AUTH_METHODS = ["none", "plain"] SUPPORTED_AUTH_METHODS = ["none", "plain"]
@ -17,8 +17,34 @@ STATUSES = {
"smtp": "535 5.7.8", "smtp": "535 5.7.8",
"pop3": "-ERR Authentication failed" "pop3": "-ERR Authentication failed"
}), }),
"encryption": ("Must issue a STARTTLS command first", {
"smtp": "530 5.7.0"
}),
"ratelimit": ("Temporary authentication failure (rate-limit)", {
"imap": "LIMIT",
"smtp": "451 4.3.2",
"pop3": "-ERR [LOGIN-DELAY] Retry later"
}),
} }
def check_credentials(user, password, ip, protocol=None):
if not user or not user.enabled or (protocol == "imap" and not user.enable_imap) or (protocol == "pop3" and not user.enable_pop):
return False
is_ok = False
# webmails
if len(password) == 64 and ip == app.config['WEBMAIL_ADDRESS']:
if user.verify_temp_token(password):
is_ok = True
# All tokens are 32 characters hex lowercase
if not is_ok and len(password) == 32:
for token in user.tokens:
if (token.check_password(password) and
(not token.ip or token.ip == ip)):
is_ok = True
break
if not is_ok and user.check_password(password):
is_ok = True
return is_ok
def handle_authentication(headers): def handle_authentication(headers):
""" Handle an HTTP nginx authentication request """ Handle an HTTP nginx authentication request
@ -28,7 +54,22 @@ def handle_authentication(headers):
protocol = headers["Auth-Protocol"] protocol = headers["Auth-Protocol"]
# Incoming mail, no authentication # Incoming mail, no authentication
if method == "none" and protocol == "smtp": if method == "none" and protocol == "smtp":
server, port = get_server(headers["Auth-Protocol"], False) server, port = get_server(protocol, False)
if app.config["INBOUND_TLS_ENFORCE"]:
if "Auth-SSL" in headers and headers["Auth-SSL"] == "on":
return {
"Auth-Status": "OK",
"Auth-Server": server,
"Auth-Port": port
}
else:
status, code = get_status(protocol, "encryption")
return {
"Auth-Status": status,
"Auth-Error-Code" : code,
"Auth-Wait": 0
}
else:
return { return {
"Auth-Status": "OK", "Auth-Status": "OK",
"Auth-Server": server, "Auth-Server": server,
@ -36,41 +77,44 @@ def handle_authentication(headers):
} }
# Authenticated user # Authenticated user
elif method == "plain": elif method == "plain":
server, port = get_server(headers["Auth-Protocol"], True) is_valid_user = False
# According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should # According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should
# be ASCII and are generally considered ISO8859-1. However when passing # be ASCII and are generally considered ISO8859-1. However when passing
# the password, nginx does not transcode the input UTF string, thus # the password, nginx does not transcode the input UTF string, thus
# we need to manually decode. # we need to manually decode.
raw_user_email = urllib.parse.unquote(headers["Auth-User"]) raw_user_email = urllib.parse.unquote(headers["Auth-User"])
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
raw_password = urllib.parse.unquote(headers["Auth-Pass"]) raw_password = urllib.parse.unquote(headers["Auth-Pass"])
user_email = 'invalid'
try:
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
password = raw_password.encode("iso8859-1").decode("utf8") password = raw_password.encode("iso8859-1").decode("utf8")
ip = urllib.parse.unquote(headers["Client-Ip"]) ip = urllib.parse.unquote(headers["Client-Ip"])
except:
app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')
else:
try:
user = models.User.query.get(user_email) user = models.User.query.get(user_email)
status = False is_valid_user = True
if user: except sqlalchemy.exc.StatementError as exc:
for token in user.tokens: exc = str(exc).split('\n', 1)[0]
if (token.check_password(password) and app.logger.warn(f'Invalid user {user_email!r}: {exc}')
(not token.ip or token.ip == ip)): else:
status = True ip = urllib.parse.unquote(headers["Client-Ip"])
if user.check_password(password): if check_credentials(user, password, ip, protocol):
status = True server, port = get_server(headers["Auth-Protocol"], True)
if status:
if protocol == "imap" and not user.enable_imap:
status = False
elif protocol == "pop3" and not user.enable_pop:
status = False
if status and user.enabled:
return { return {
"Auth-Status": "OK", "Auth-Status": "OK",
"Auth-Server": server, "Auth-Server": server,
"Auth-User": user_email,
"Auth-User-Exists": is_valid_user,
"Auth-Port": port "Auth-Port": port
} }
else:
status, code = get_status(protocol, "authentication") status, code = get_status(protocol, "authentication")
return { return {
"Auth-Status": status, "Auth-Status": status,
"Auth-Error-Code": code, "Auth-Error-Code": code,
"Auth-User": user_email,
"Auth-User-Exists": is_valid_user,
"Auth-Wait": 0 "Auth-Wait": 0
} }
# Unexpected # Unexpected

@ -1,3 +1,3 @@
__all__ = [ __all__ = [
'auth', 'postfix', 'dovecot', 'fetch' 'auth', 'postfix', 'dovecot', 'fetch', 'rspamd'
] ]

@ -5,19 +5,24 @@ from flask import current_app as app
import flask import flask
import flask_login import flask_login
import base64 import base64
import ipaddress
@internal.route("/auth/email") @internal.route("/auth/email")
def nginx_authentication(): def nginx_authentication():
""" Main authentication endpoint for Nginx email server """ Main authentication endpoint for Nginx email server
""" """
limiter = utils.limiter.get_limiter(app.config["AUTH_RATELIMIT"], "auth-ip")
client_ip = flask.request.headers["Client-Ip"] client_ip = flask.request.headers["Client-Ip"]
if not limiter.test(client_ip): headers = flask.request.headers
if headers["Auth-Port"] == '25' and headers['Auth-Method'] == 'plain':
response = flask.Response() response = flask.Response()
response.headers['Auth-Status'] = 'Authentication rate limit from one source exceeded' response.headers['Auth-Status'] = 'AUTH not supported'
response.headers['Auth-Error-Code'] = '451 4.3.2' response.headers['Auth-Error-Code'] = '502 5.5.1'
utils.limiter.rate_limit_ip(client_ip)
return response
if utils.limiter.should_rate_limit_ip(client_ip):
status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
response = flask.Response()
response.headers['Auth-Status'] = status
response.headers['Auth-Error-Code'] = code
if int(flask.request.headers['Auth-Login-Attempt']) < 10: if int(flask.request.headers['Auth-Login-Attempt']) < 10:
response.headers['Auth-Wait'] = '3' response.headers['Auth-Wait'] = '3'
return response return response
@ -25,13 +30,26 @@ def nginx_authentication():
response = flask.Response() response = flask.Response()
for key, value in headers.items(): for key, value in headers.items():
response.headers[key] = str(value) response.headers[key] = str(value)
if ("Auth-Status" not in headers) or (headers["Auth-Status"] != "OK"): is_valid_user = False
limit_subnet = str(app.config["AUTH_RATELIMIT_SUBNET"]) != 'False' if response.headers.get("Auth-User-Exists"):
subnet = ipaddress.ip_network(app.config["SUBNET"]) username = response.headers["Auth-User"]
if limit_subnet or ipaddress.ip_address(client_ip) not in subnet: if utils.limiter.should_rate_limit_user(username, client_ip):
limiter.hit(flask.request.headers["Client-Ip"]) # FIXME could be done before handle_authentication()
status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
response = flask.Response()
response.headers['Auth-Status'] = status
response.headers['Auth-Error-Code'] = code
if int(flask.request.headers['Auth-Login-Attempt']) < 10:
response.headers['Auth-Wait'] = '3'
return response
is_valid_user = True
if headers.get("Auth-Status") == "OK":
utils.limiter.exempt_ip_from_ratelimits(client_ip)
elif is_valid_user:
utils.limiter.rate_limit_user(username, client_ip)
else:
utils.limiter.rate_limit_ip(client_ip)
return response return response
@internal.route("/auth/admin") @internal.route("/auth/admin")
def admin_authentication(): def admin_authentication():
@ -43,20 +61,46 @@ def admin_authentication():
return "" return ""
return flask.abort(403) return flask.abort(403)
@internal.route("/auth/user")
def user_authentication():
""" Fails if the user is not authenticated.
"""
if (not flask_login.current_user.is_anonymous
and flask_login.current_user.enabled):
response = flask.Response()
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, flask_login.current_user.get_id(), "")
response.headers["X-User-Token"] = models.User.get_temp_token(flask_login.current_user.get_id())
return response
return flask.abort(403)
@internal.route("/auth/basic") @internal.route("/auth/basic")
def basic_authentication(): def basic_authentication():
""" Tries to authenticate using the Authorization header. """ Tries to authenticate using the Authorization header.
""" """
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
if utils.limiter.should_rate_limit_ip(client_ip):
response = flask.Response(status=401)
response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit from one source exceeded"'
response.headers['Retry-After'] = '60'
return response
authorization = flask.request.headers.get("Authorization") authorization = flask.request.headers.get("Authorization")
if authorization and authorization.startswith("Basic "): if authorization and authorization.startswith("Basic "):
encoded = authorization.replace("Basic ", "") encoded = authorization.replace("Basic ", "")
user_email, password = base64.b64decode(encoded).split(b":") user_email, password = base64.b64decode(encoded).split(b":", 1)
user = models.User.query.get(user_email.decode("utf8")) user_email = user_email.decode("utf8")
if user and user.enabled and user.check_password(password.decode("utf8")): if utils.limiter.should_rate_limit_user(user_email, client_ip):
response = flask.Response() response = flask.Response(status=401)
response.headers["X-User"] = user.email response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"'
response.headers['Retry-After'] = '60'
return response return response
user = models.User.query.get(user_email)
if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"):
response = flask.Response()
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
utils.limiter.exempt_ip_from_ratelimits(client_ip)
return response
utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
response = flask.Response(status=401) response = flask.Response(status=401)
response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"' response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
return response return response

@ -1,10 +1,15 @@
from mailu import models from mailu import models, utils
from mailu.internal import internal from mailu.internal import internal
from flask import current_app as app
import flask import flask
import idna
import re import re
import srslib import srslib
@internal.route("/postfix/dane/<domain_name>")
def postfix_dane_map(domain_name):
return flask.jsonify('dane-only') if utils.has_dane_record(domain_name) else flask.abort(404)
@internal.route("/postfix/domain/<domain_name>") @internal.route("/postfix/domain/<domain_name>")
def postfix_mailbox_domain(domain_name): def postfix_mailbox_domain(domain_name):
@ -30,18 +35,71 @@ def postfix_alias_map(alias):
destination = models.Email.resolve_destination(localpart, domain_name) destination = models.Email.resolve_destination(localpart, domain_name)
return flask.jsonify(",".join(destination)) if destination else flask.abort(404) return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
@internal.route("/postfix/transport/<path:email>") @internal.route("/postfix/transport/<path:email>")
def postfix_transport(email): def postfix_transport(email):
if email == '*' or re.match("(^|.*@)\[.*\]$", email): if email == '*' or re.match("(^|.*@)\[.*\]$", email):
return flask.abort(404) return flask.abort(404)
localpart, domain_name = models.Email.resolve_domain(email) _, domain_name = models.Email.resolve_domain(email)
relay = models.Relay.query.get(domain_name) or flask.abort(404) relay = models.Relay.query.get(domain_name) or flask.abort(404)
ret = "smtp:[{0}]".format(relay.smtp) target = relay.smtp.lower()
if ":" in relay.smtp: port = None
split = relay.smtp.split(':') use_lmtp = False
ret = "smtp:[{0}]:{1}".format(split[0], split[1]) use_mx = False
return flask.jsonify(ret) # strip prefixes mx: and lmtp:
if target.startswith('mx:'):
target = target[3:]
use_mx = True
elif target.startswith('lmtp:'):
target = target[5:]
use_lmtp = True
# split host:port or [host]:port
if target.startswith('['):
if use_mx or ']' not in target:
# invalid target (mx: and [] or missing ])
flask.abort(400)
host, rest = target[1:].split(']', 1)
if rest.startswith(':'):
port = rest[1:]
elif rest:
# invalid target (rest should be :port)
flask.abort(400)
else:
if ':' in target:
host, port = target.rsplit(':', 1)
else:
host = target
# default for empty host part is mx:domain
if not host:
if not use_lmtp:
host = relay.name.lower()
use_mx = True
else:
# lmtp: needs a host part
flask.abort(400)
# detect ipv6 address or encode host
if ':' in host:
host = f'ipv6:{host}'
else:
try:
host = idna.encode(host).decode('ascii')
except idna.IDNAError:
# invalid host (fqdn not encodable)
flask.abort(400)
# validate port
if port is not None:
try:
port = int(port, 10)
except ValueError:
# invalid port (should be numeric)
flask.abort(400)
# create transport
transport = 'lmtp' if use_lmtp else 'smtp'
# use [] when not using MX lookups or host is an ipv6 address
if host.startswith('ipv6:') or (not use_lmtp and not use_mx):
host = f'[{host}]'
# create port suffix
port = '' if port is None else f':{port}'
return flask.jsonify(f'{transport}:{host}{port}')
@internal.route("/postfix/recipient/map/<path:recipient>") @internal.route("/postfix/recipient/map/<path:recipient>")
@ -50,7 +108,7 @@ def postfix_recipient_map(recipient):
This is meant for bounces to go back to the original sender. This is meant for bounces to go back to the original sender.
""" """
srs = srslib.SRS(flask.current_app.config["SECRET_KEY"]) srs = srslib.SRS(flask.current_app.srs_key)
if srslib.SRS.is_srs_address(recipient): if srslib.SRS.is_srs_address(recipient):
try: try:
return flask.jsonify(srs.reverse(recipient)) return flask.jsonify(srs.reverse(recipient))
@ -65,7 +123,7 @@ def postfix_sender_map(sender):
This is for bounces to come back the reverse path properly. This is for bounces to come back the reverse path properly.
""" """
srs = srslib.SRS(flask.current_app.config["SECRET_KEY"]) srs = srslib.SRS(flask.current_app.srs_key)
domain = flask.current_app.config["DOMAIN"] domain = flask.current_app.config["DOMAIN"]
try: try:
localpart, domain_name = models.Email.resolve_domain(sender) localpart, domain_name = models.Email.resolve_domain(sender)
@ -78,12 +136,23 @@ def postfix_sender_map(sender):
@internal.route("/postfix/sender/login/<path:sender>") @internal.route("/postfix/sender/login/<path:sender>")
def postfix_sender_login(sender): def postfix_sender_login(sender):
wildcard_senders = [s for s in flask.current_app.config.get('WILDCARD_SENDERS', '').lower().replace(' ', '').split(',') if s]
localpart, domain_name = models.Email.resolve_domain(sender) localpart, domain_name = models.Email.resolve_domain(sender)
if localpart is None: if localpart is None:
return flask.abort(404) return flask.jsonify(",".join(wildcard_senders)) if wildcard_senders else flask.abort(404)
localpart = localpart[:next((i for i, ch in enumerate(localpart) if ch in flask.current_app.config.get('RECIPIENT_DELIMITER')), None)]
destination = models.Email.resolve_destination(localpart, domain_name, True) destination = models.Email.resolve_destination(localpart, domain_name, True)
destination = [*destination, *wildcard_senders] if destination else [*wildcard_senders]
return flask.jsonify(",".join(destination)) if destination else flask.abort(404) return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
@internal.route("/postfix/sender/rate/<path:sender>")
def postfix_sender_rate(sender):
""" Rate limit outbound emails per sender login
"""
if sender in flask.current_app.config['MESSAGE_RATELIMIT_EXEMPTION']:
flask.abort(404)
user = models.User.get(sender) or flask.abort(404)
return flask.abort(404) if user.sender_limiter.hit() else flask.jsonify("450 4.2.1 You are sending too many emails too fast.")
@internal.route("/postfix/sender/access/<path:sender>") @internal.route("/postfix/sender/access/<path:sender>")
def postfix_sender_access(sender): def postfix_sender_access(sender):

@ -0,0 +1,27 @@
from mailu import models
from mailu.internal import internal
import flask
def vault_error(*messages, status=404):
return flask.make_response(flask.jsonify({'errors':messages}), status)
# rspamd key format:
# {"selectors":[{"pubkey":"...","domain":"...","valid_start":TS,"valid_end":TS,"key":"...","selector":"...","bits":...,"alg":"..."}]}
# hashicorp vault answer format:
# {"request_id":"...","lease_id":"","renewable":false,"lease_duration":2764800,"data":{...see above...},"wrap_info":null,"warnings":null,"auth":null}
@internal.route("/rspamd/vault/v1/dkim/<domain_name>", methods=['GET'])
def rspamd_dkim_key(domain_name):
selectors = []
if domain := models.Domain.query.get(domain_name):
if key := domain.dkim_key:
selectors.append(
{
'domain' : domain.name,
'key' : key.decode('utf8'),
'selector': flask.current_app.config.get('DKIM_SELECTOR', 'dkim'),
}
)
return flask.jsonify({'data': {'selectors': selectors}})

@ -1,7 +1,12 @@
from mailu import utils
from flask import current_app as app
import base64
import limits import limits
import limits.storage import limits.storage
import limits.strategies import limits.strategies
import hmac
import secrets
class LimitWrapper(object): class LimitWrapper(object):
""" Wraps a limit by providing the storage, item and identifiers """ Wraps a limit by providing the storage, item and identifiers
@ -32,3 +37,58 @@ class LimitWraperFactory(object):
def get_limiter(self, limit, *args): def get_limiter(self, limit, *args):
return LimitWrapper(self.limiter, limits.parse(limit), *args) return LimitWrapper(self.limiter, limits.parse(limit), *args)
def is_subject_to_rate_limits(self, ip):
return False if utils.is_exempt_from_ratelimits(ip) else not (self.storage.get(f'exempt-{ip}') > 0)
def exempt_ip_from_ratelimits(self, ip):
self.storage.incr(f'exempt-{ip}', app.config["AUTH_RATELIMIT_EXEMPTION_LENGTH"], True)
def should_rate_limit_ip(self, ip):
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip')
client_network = utils.extract_network_from_ip(ip)
is_rate_limited = self.is_subject_to_rate_limits(ip) and not limiter.test(client_network)
if is_rate_limited:
app.logger.warn(f'Authentication attempt from {ip} has been rate-limited.')
return is_rate_limited
def rate_limit_ip(self, ip):
if ip != app.config['WEBMAIL_ADDRESS']:
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip')
client_network = utils.extract_network_from_ip(ip)
if self.is_subject_to_rate_limits(ip):
limiter.hit(client_network)
def should_rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None):
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user')
is_rate_limited = self.is_subject_to_rate_limits(ip) and not limiter.test(device_cookie if device_cookie_name == username else username)
if is_rate_limited:
app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.')
return is_rate_limited
def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None):
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user')
if self.is_subject_to_rate_limits(ip):
limiter.hit(device_cookie if device_cookie_name == username else username)
""" Device cookies as described on:
https://owasp.org/www-community/Slow_Down_Online_Guessing_Attacks_with_Device_Cookies
"""
def parse_device_cookie(self, cookie):
try:
login, nonce, _ = cookie.split('$')
if hmac.compare_digest(cookie, self.device_cookie(login, nonce)):
return nonce, login
except:
pass
return None, None
""" Device cookies don't require strong crypto:
72bits of nonce, 96bits of signature is more than enough
and these values avoid padding in most cases
"""
def device_cookie(self, username, nonce=None):
if not nonce:
nonce = secrets.token_urlsafe(9)
sig = str(base64.urlsafe_b64encode(hmac.new(app.device_cookie_key, bytearray(f'device_cookie|{username}|{nonce}', 'utf-8'), 'sha256').digest()[20:]), 'utf-8')
return f'{username}${nonce}${sig}'

@ -1,40 +1,46 @@
from mailu import models """ Mailu command line interface
"""
from flask import current_app as app import sys
from flask import cli as flask_cli
import flask
import os import os
import socket import socket
import uuid import uuid
import click import click
import yaml
from flask import current_app as app
from flask.cli import FlaskGroup, with_appcontext
from mailu import models
from mailu.schemas import MailuSchema, Logger, RenderJSON
db = models.db db = models.db
@click.group() @click.group(cls=FlaskGroup, context_settings={'help_option_names': ['-?', '-h', '--help']})
def mailu(cls=flask_cli.FlaskGroup): def mailu():
""" Mailu command line """ Mailu command line
""" """
@mailu.command() @mailu.command()
@flask_cli.with_appcontext @with_appcontext
def advertise(): def advertise():
""" Advertise this server against statistic services. """ Advertise this server against statistic services.
""" """
if os.path.isfile(app.config["INSTANCE_ID_PATH"]): if os.path.isfile(app.config['INSTANCE_ID_PATH']):
with open(app.config["INSTANCE_ID_PATH"], "r") as handle: with open(app.config['INSTANCE_ID_PATH'], 'r') as handle:
instance_id = handle.read() instance_id = handle.read()
else: else:
instance_id = str(uuid.uuid4()) instance_id = str(uuid.uuid4())
with open(app.config["INSTANCE_ID_PATH"], "w") as handle: with open(app.config['INSTANCE_ID_PATH'], 'w') as handle:
handle.write(instance_id) handle.write(instance_id)
if not app.config["DISABLE_STATISTICS"]: if not app.config['DISABLE_STATISTICS']:
try: try:
socket.gethostbyname(app.config["STATS_ENDPOINT"].format(instance_id)) socket.gethostbyname(app.config['STATS_ENDPOINT'].format(instance_id))
except: except OSError:
pass pass
@ -42,57 +48,54 @@ def advertise():
@click.argument('localpart') @click.argument('localpart')
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('password') @click.argument('password')
@click.option('-m', '--mode') @click.option('-m', '--mode', default='create', metavar='MODE', help='''\b'create' (default): create user. it's an error if user already exists
@flask_cli.with_appcontext 'ifmissing': only update password if user is missing
def admin(localpart, domain_name, password, mode='create'): 'update': create user or update password if user exists
''')
@with_appcontext
def admin(localpart, domain_name, password, mode):
""" Create an admin user """ Create an admin user
'mode' can be:
- 'create' (default) Will try to create user and will raise an exception if present
- 'ifmissing': if user exists, nothing happens, else it will be created
- 'update': user is created or, if it exists, its password gets updated
""" """
if not mode in ('create', 'update', 'ifmissing'):
raise click.ClickException(f'invalid mode: {mode!r}')
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
domain = models.Domain(name=domain_name) domain = models.Domain(name=domain_name)
db.session.add(domain) db.session.add(domain)
user = None email = f'{localpart}@{domain_name}'
if mode == 'ifmissing' or mode == 'update': if user := models.User.query.get(email):
email = '{}@{}'.format(localpart, domain_name) if mode == 'ifmissing':
user = models.User.query.get(email) print(f'user {email!r} exists, not updating')
if user and mode == 'ifmissing':
print('user %s exists, not updating' % email)
return return
elif mode == 'update':
if not user: user.set_password(password)
db.session.commit()
print("updated admin password")
else:
raise click.ClickException(f'user {email!r} exists, not created')
else:
user = models.User( user = models.User(
localpart=localpart, localpart=localpart,
domain=domain, domain=domain,
global_admin=True global_admin=True
) )
user.set_password(password)
db.session.add(user) db.session.add(user)
user.set_password(password)
db.session.commit() db.session.commit()
print("created admin user") print("created admin user")
elif mode == 'update':
user.set_password(password)
db.session.commit()
print("updated admin password")
@mailu.command() @mailu.command()
@click.argument('localpart') @click.argument('localpart')
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('password') @click.argument('password')
@click.argument('hash_scheme', required=False) @with_appcontext
@flask_cli.with_appcontext def user(localpart, domain_name, password):
def user(localpart, domain_name, password, hash_scheme=None):
""" Create a user """ Create a user
""" """
if hash_scheme is None:
hash_scheme = app.config['PASSWORD_SCHEME']
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
domain = models.Domain(name=domain_name) domain = models.Domain(name=domain_name)
@ -102,7 +105,7 @@ def user(localpart, domain_name, password, hash_scheme=None):
domain=domain, domain=domain,
global_admin=False global_admin=False
) )
user.set_password(password, hash_scheme=hash_scheme) user.set_password(password)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
@ -111,19 +114,16 @@ def user(localpart, domain_name, password, hash_scheme=None):
@click.argument('localpart') @click.argument('localpart')
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('password') @click.argument('password')
@click.argument('hash_scheme', required=False) @with_appcontext
@flask_cli.with_appcontext def password(localpart, domain_name, password):
def password(localpart, domain_name, password, hash_scheme=None):
""" Change the password of an user """ Change the password of an user
""" """
email = '{0}@{1}'.format(localpart, domain_name) email = f'{localpart}@{domain_name}'
user = models.User.query.get(email) user = models.User.query.get(email)
if hash_scheme is None:
hash_scheme = app.config['PASSWORD_SCHEME']
if user: if user:
user.set_password(password, hash_scheme=hash_scheme) user.set_password(password)
else: else:
print("User " + email + " not found.") print(f'User {email} not found.')
db.session.commit() db.session.commit()
@ -132,7 +132,7 @@ def password(localpart, domain_name, password, hash_scheme=None):
@click.option('-u', '--max-users') @click.option('-u', '--max-users')
@click.option('-a', '--max-aliases') @click.option('-a', '--max-aliases')
@click.option('-q', '--max-quota-bytes') @click.option('-q', '--max-quota-bytes')
@flask_cli.with_appcontext @with_appcontext
def domain(domain_name, max_users=-1, max_aliases=-1, max_quota_bytes=0): def domain(domain_name, max_users=-1, max_aliases=-1, max_quota_bytes=0):
""" Create a domain """ Create a domain
""" """
@ -148,13 +148,10 @@ def domain(domain_name, max_users=-1, max_aliases=-1, max_quota_bytes=0):
@click.argument('localpart') @click.argument('localpart')
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('password_hash') @click.argument('password_hash')
@click.argument('hash_scheme') @with_appcontext
@flask_cli.with_appcontext def user_import(localpart, domain_name, password_hash):
def user_import(localpart, domain_name, password_hash, hash_scheme = None): """ Import a user along with password hash
""" Import a user along with password hash.
""" """
if hash_scheme is None:
hash_scheme = app.config['PASSWORD_SCHEME']
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
domain = models.Domain(name=domain_name) domain = models.Domain(name=domain_name)
@ -164,19 +161,19 @@ def user_import(localpart, domain_name, password_hash, hash_scheme = None):
domain=domain, domain=domain,
global_admin=False global_admin=False
) )
user.set_password(password_hash, hash_scheme=hash_scheme, raw=True) user.set_password(password_hash, raw=True)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
# TODO: remove deprecated config_update function?
@mailu.command() @mailu.command()
@click.option('-v', '--verbose') @click.option('-v', '--verbose')
@click.option('-d', '--delete-objects') @click.option('-d', '--delete-objects')
@flask_cli.with_appcontext @with_appcontext
def config_update(verbose=False, delete_objects=False): def config_update(verbose=False, delete_objects=False):
"""sync configuration with data from YAML-formatted stdin""" """ Sync configuration with data from YAML (deprecated)
import yaml """
import sys
new_config = yaml.safe_load(sys.stdin) new_config = yaml.safe_load(sys.stdin)
# print new_config # print new_config
domains = new_config.get('domains', []) domains = new_config.get('domains', [])
@ -196,13 +193,13 @@ def config_update(verbose=False, delete_objects=False):
max_aliases=max_aliases, max_aliases=max_aliases,
max_quota_bytes=max_quota_bytes) max_quota_bytes=max_quota_bytes)
db.session.add(domain) db.session.add(domain)
print("Added " + str(domain_config)) print(f'Added {domain_config}')
else: else:
domain.max_users = max_users domain.max_users = max_users
domain.max_aliases = max_aliases domain.max_aliases = max_aliases
domain.max_quota_bytes = max_quota_bytes domain.max_quota_bytes = max_quota_bytes
db.session.add(domain) db.session.add(domain)
print("Updated " + str(domain_config)) print(f'Updated {domain_config}')
users = new_config.get('users', []) users = new_config.get('users', [])
tracked_users = set() tracked_users = set()
@ -217,9 +214,8 @@ def config_update(verbose=False, delete_objects=False):
localpart = user_config['localpart'] localpart = user_config['localpart']
domain_name = user_config['domain'] domain_name = user_config['domain']
password_hash = user_config.get('password_hash', None) password_hash = user_config.get('password_hash', None)
hash_scheme = user_config.get('hash_scheme', None)
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
email = '{0}@{1}'.format(localpart, domain_name) email = f'{localpart}@{domain_name}'
optional_params = {} optional_params = {}
for k in user_optional_params: for k in user_optional_params:
if k in user_config: if k in user_config:
@ -239,7 +235,7 @@ def config_update(verbose=False, delete_objects=False):
else: else:
for k in optional_params: for k in optional_params:
setattr(user, k, optional_params[k]) setattr(user, k, optional_params[k])
user.set_password(password_hash, hash_scheme=hash_scheme, raw=True) user.set_password(password_hash, raw=True)
db.session.add(user) db.session.add(user)
aliases = new_config.get('aliases', []) aliases = new_config.get('aliases', [])
@ -249,13 +245,13 @@ def config_update(verbose=False, delete_objects=False):
print(str(alias_config)) print(str(alias_config))
localpart = alias_config['localpart'] localpart = alias_config['localpart']
domain_name = alias_config['domain'] domain_name = alias_config['domain']
if type(alias_config['destination']) is str: if isinstance(alias_config['destination'], str):
destination = alias_config['destination'].split(',') destination = alias_config['destination'].split(',')
else: else:
destination = alias_config['destination'] destination = alias_config['destination']
wildcard = alias_config.get('wildcard', False) wildcard = alias_config.get('wildcard', False)
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
email = '{0}@{1}'.format(localpart, domain_name) email = f'{localpart}@{domain_name}'
if not domain: if not domain:
domain = models.Domain(name=domain_name) domain = models.Domain(name=domain_name)
db.session.add(domain) db.session.add(domain)
@ -285,7 +281,7 @@ def config_update(verbose=False, delete_objects=False):
domain_name = manager_config['domain'] domain_name = manager_config['domain']
user_name = manager_config['user'] user_name = manager_config['user']
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
manageruser = models.User.query.get(user_name + '@' + domain_name) manageruser = models.User.query.get(f'{user_name}@{domain_name}')
if manageruser not in domain.managers: if manageruser not in domain.managers:
domain.managers.append(manageruser) domain.managers.append(manageruser)
db.session.add(domain) db.session.add(domain)
@ -294,26 +290,117 @@ def config_update(verbose=False, delete_objects=False):
if delete_objects: if delete_objects:
for user in db.session.query(models.User).all(): for user in db.session.query(models.User).all():
if not (user.email in tracked_users): if not user.email in tracked_users:
if verbose: if verbose:
print("Deleting user: " + str(user.email)) print(f'Deleting user: {user.email}')
db.session.delete(user) db.session.delete(user)
for alias in db.session.query(models.Alias).all(): for alias in db.session.query(models.Alias).all():
if not (alias.email in tracked_aliases): if not alias.email in tracked_aliases:
if verbose: if verbose:
print("Deleting alias: " + str(alias.email)) print(f'Deleting alias: {alias.email}')
db.session.delete(alias) db.session.delete(alias)
for domain in db.session.query(models.Domain).all(): for domain in db.session.query(models.Domain).all():
if not (domain.name in tracked_domains): if not domain.name in tracked_domains:
if verbose: if verbose:
print("Deleting domain: " + str(domain.name)) print(f'Deleting domain: {domain.name}')
db.session.delete(domain) db.session.delete(domain)
db.session.commit() db.session.commit()
@mailu.command()
@click.option('-v', '--verbose', count=True, help='Increase verbosity.')
@click.option('-s', '--secrets', is_flag=True, help='Show secret attributes in messages.')
@click.option('-d', '--debug', is_flag=True, help='Enable debug output.')
@click.option('-q', '--quiet', is_flag=True, help='Quiet mode - only show errors.')
@click.option('-c', '--color', is_flag=True, help='Force colorized output.')
@click.option('-u', '--update', is_flag=True, help='Update mode - merge input with existing config.')
@click.option('-n', '--dry-run', is_flag=True, help='Perform a trial run with no changes made.')
@click.argument('source', metavar='[FILENAME|-]', type=click.File(mode='r'), default=sys.stdin)
@with_appcontext
def config_import(verbose=0, secrets=False, debug=False, quiet=False, color=False,
update=False, dry_run=False, source=None):
""" Import configuration as YAML or JSON from stdin or file
"""
log = Logger(want_color=color or None, can_color=sys.stdout.isatty(), secrets=secrets, debug=debug)
log.lexer = 'python'
log.strip = True
log.verbose = 0 if quiet else verbose
log.quiet = quiet
context = {
'import': True,
'update': update,
'clear': not update,
'callback': log.track_serialize,
}
schema = MailuSchema(only=MailuSchema.Meta.order, context=context)
try:
# import source
with models.db.session.no_autoflush:
config = schema.loads(source)
# flush session to show/count all changes
if not quiet and (dry_run or verbose):
db.session.flush()
# check for duplicate domain names
config.check()
except Exception as exc:
if msg := log.format_exception(exc):
raise click.ClickException(msg) from exc
raise
# don't commit when running dry
if dry_run:
log.changes('Dry run. Not committing changes.')
db.session.rollback()
else:
log.changes('Committing changes.')
db.session.commit()
@mailu.command()
@click.option('-f', '--full', is_flag=True, help='Include attributes with default value.')
@click.option('-s', '--secrets', is_flag=True,
help='Include secret attributes (dkim-key, passwords).')
@click.option('-d', '--dns', is_flag=True, help='Include dns records.')
@click.option('-c', '--color', is_flag=True, help='Force colorized output.')
@click.option('-o', '--output-file', 'output', default=sys.stdout, type=click.File(mode='w'),
help='Save configuration to file.')
@click.option('-j', '--json', 'as_json', is_flag=True, help='Export configuration in json format.')
@click.argument('only', metavar='[FILTER]...', nargs=-1)
@with_appcontext
def config_export(full=False, secrets=False, color=False, dns=False, output=None, as_json=False, only=None):
""" Export configuration as YAML or JSON to stdout or file
"""
log = Logger(want_color=color or None, can_color=output.isatty())
only = only or MailuSchema.Meta.order
context = {
'full': full,
'secrets': secrets,
'dns': dns,
}
try:
schema = MailuSchema(only=only, context=context)
if as_json:
schema.opts.render_module = RenderJSON
log.lexer = 'json'
log.strip = True
print(log.colorize(schema.dumps(models.MailuConfig())), file=output)
except Exception as exc:
if msg := log.format_exception(exc):
raise click.ClickException(msg) from exc
raise
@mailu.command() @mailu.command()
@click.argument('email') @click.argument('email')
@flask_cli.with_appcontext @with_appcontext
def user_delete(email): def user_delete(email):
"""delete user""" """delete user"""
user = models.User.query.get(email) user = models.User.query.get(email)
@ -324,7 +411,7 @@ def user_delete(email):
@mailu.command() @mailu.command()
@click.argument('email') @click.argument('email')
@flask_cli.with_appcontext @with_appcontext
def alias_delete(email): def alias_delete(email):
"""delete alias""" """delete alias"""
alias = models.Alias.query.get(email) alias = models.Alias.query.get(email)
@ -338,7 +425,7 @@ def alias_delete(email):
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('destination') @click.argument('destination')
@click.option('-w', '--wildcard', is_flag=True) @click.option('-w', '--wildcard', is_flag=True)
@flask_cli.with_appcontext @with_appcontext
def alias(localpart, domain_name, destination, wildcard=False): def alias(localpart, domain_name, destination, wildcard=False):
""" Create an alias """ Create an alias
""" """
@ -351,7 +438,7 @@ def alias(localpart, domain_name, destination, wildcard=False):
domain=domain, domain=domain,
wildcard=wildcard, wildcard=wildcard,
destination=destination.split(','), destination=destination.split(','),
email="%s@%s" % (localpart, domain_name) email=f'{localpart}@{domain_name}'
) )
db.session.add(alias) db.session.add(alias)
db.session.commit() db.session.commit()
@ -362,7 +449,7 @@ def alias(localpart, domain_name, destination, wildcard=False):
@click.argument('max_users') @click.argument('max_users')
@click.argument('max_aliases') @click.argument('max_aliases')
@click.argument('max_quota_bytes') @click.argument('max_quota_bytes')
@flask_cli.with_appcontext @with_appcontext
def setlimits(domain_name, max_users, max_aliases, max_quota_bytes): def setlimits(domain_name, max_users, max_aliases, max_quota_bytes):
""" Set domain limits """ Set domain limits
""" """
@ -377,16 +464,12 @@ def setlimits(domain_name, max_users, max_aliases, max_quota_bytes):
@mailu.command() @mailu.command()
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('user_name') @click.argument('user_name')
@flask_cli.with_appcontext @with_appcontext
def setmanager(domain_name, user_name='manager'): def setmanager(domain_name, user_name='manager'):
""" Make a user manager of a domain """ Make a user manager of a domain
""" """
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
manageruser = models.User.query.get(user_name + '@' + domain_name) manageruser = models.User.query.get(f'{user_name}@{domain_name}')
domain.managers.append(manageruser) domain.managers.append(manageruser)
db.session.add(domain) db.session.add(domain)
db.session.commit() db.session.commit()
if __name__ == '__main__':
cli()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
from flask import Blueprint
sso = Blueprint('sso', __name__, static_folder=None, template_folder='templates')
from mailu.sso.views import *

@ -0,0 +1,11 @@
from wtforms import validators, fields
from flask_babel import lazy_gettext as _
import flask_wtf
class LoginForm(flask_wtf.FlaskForm):
class Meta:
csrf = False
email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
submitAdmin = fields.SubmitField(_('Sign in'))
submitWebmail = fields.SubmitField(_('Sign in'))

@ -0,0 +1,86 @@
{%- import "macros.html" as macros %}
{%- import "bootstrap/utils.html" as utils %}
<!doctype html>
<html lang="{{ session['language'] }}" data-static="/static/">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% trans %}Admin page for{% endtrans %} {{ config["SITENAME"] }}">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Mailu-Admin | {{ config["SITENAME"] }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars" title="{% trans %}toggle sidebar{% endtrans %}" aria-expanded="false"></i><span class="sr-only">{% trans %}toggle sidebar{% endtrans %}</span></a>
</li>
<li class="nav-item">
{%- for page, url in path %}
{%- if loop.index > 1 %}
<i class="fas fa-greater-than text-xs text-gray" aria-hidden="true"></i>
{%- endif %}
{%- if url %}
<a class="nav-link d-inline-block" href="{{ url }}" role="button">{{ page }}</a>
{%- else %}
<span class="nav-link d-inline-block">{{ page }}</span>
{%- endif %}
{%- endfor %}
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">
<i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">Language</span>
<span class="badge badge-primary navbar-badge">{{ session['language'] }}</span></a>
<div class="dropdown-menu dropdown-menu-right p-0" id="mailu-languages">
{%- for locale in config.translations.values() %}
<a class="dropdown-item{% if locale|string() == session['language'] %} active{% endif %}" href="{{ url_for('sso.set_language', language=locale) }}">{{ locale.get_language_name().title() }}</a>
{%- endfor %}
</div>
</li>
</ul>
</nav>
<aside class="main-sidebar sidebar-dark-primary nav-compact elevation-4">
<a class="brand-link bg-mailu-logo"{% if config["LOGO_BACKGROUND"] %} style="background-color:{{ config["LOGO_BACKGROUND"] }}!important;"{% endif %}>
<img src="{{ config["LOGO_URL"] if config["LOGO_URL"] else '/static/mailu.png' }}" width="33" height="33" alt="Mailu" class="brand-image mailu-logo img-circle elevation-3">
<span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span>
</a>
{%- include "sidebar_sso.html" %}
</aside>
<div class="content-wrapper text-sm">
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">{%- block title %}{%- endblock %}</h1>
<small>{% block subtitle %}{% endblock %}</small>
</div>
<div class="col-sm-6">
{%- block main_action %}{%- endblock %}
</div>
</div>
</div>
</section>
<div class="content">
{{ utils.flashed_messages(container=False, default_category='success') }}
{%- block content %}{%- endblock %}
</div>
</div>
<footer class="main-footer">
Built with <i class="fa fa-heart text-danger" aria-hidden="true"></i><span class="sr-only">love</span>
using <a href="https://flask.palletsprojects.com/">Flask</a>
and <a href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a>.
<span class="fa-pull-right">
<i class="fa fa-code-branch" aria-hidden="true"></i><span class="sr-only">fork</span>
on <a href="https://github.com/Mailu/Mailu">Github</a>
</span>
</footer>
</div>
<script src="{{ url_for('static', filename='vendor.js') }}"></script>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>

@ -0,0 +1,11 @@
{%- extends "base_sso.html" %}
{%- block content %}
{%- call macros.card() %}
<form class="form" method="post" role="form">
{{ macros.form_field(form.email) }}
{{ macros.form_field(form.pw) }}
{{ macros.form_fields(fields, label=False, class="btn btn-default") }}
</form>
{%- endcall %}
{%- endblock %}

@ -0,0 +1,5 @@
{%- extends "form_sso.html" %}
{%- block title %}
{% trans %}Sign in{% endtrans %}
{%- endblock %}

@ -0,0 +1,55 @@
<div class="sidebar text-sm">
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" role="menu">
<li class="nav-header text-uppercase text-primary" role="none">{% trans %}Go to{% endtrans %}</li>
{%- if config['ADMIN'] %}
<li class="nav-item">
<a href="{{ url_for('ui.client') }}" class="nav-link">
<i class="nav-icon fa fa-laptop"></i>
<p class="text">{% trans %}Client setup{% endtrans %}</p>
</a>
</li>
{%- endif %}
<li class="nav-item" role="none">
<a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link" role="menuitem" rel="noreferrer">
<i class="nav-icon fa fa-globe"></i>
<p>{% trans %}Website{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
</a>
</li>
<li class="nav-item" role="none">
<a href="https://mailu.io" target="_blank" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-life-ring"></i>
<p class="text">{% trans %}Help{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
</a>
</li>
{#-
Domain self-registration is only available when
- Admin is available
- Domain Self-registration is enabled
- The current user is not logged on
#}
{%- if config['DOMAIN_REGISTRATION'] and not current_user.is_authenticated and config['ADMIN'] %}
<li class="nav-item" role="none">
<a href="{{ url_for('ui.domain_signup') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-plus-square"></i>
<p class="text">{% trans %}Register a domain{% endtrans %}</p>
</a>
</li>
{%- endif %}
{#-
User self-registration is only available when
- Admin is available
- Self-registration is enabled
- The current user is not logged on
#}
{%- if not current_user.is_authenticated and signup_domains and config['ADMIN'] %}
<li class="nav-item" role="none">
<a href="{{ url_for('ui.user_signup') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-user-plus"></i>
<p class="text">{% trans %}Sign up{% endtrans %}</p>
</a>
</li>
{%- endif %}
</ul>
</nav>
</div>

@ -0,0 +1,3 @@
__all__ = [
'base', 'languages'
]

@ -0,0 +1,57 @@
from werkzeug.utils import redirect
from mailu import models, utils
from mailu.sso import sso, forms
from mailu.ui import access
from flask import current_app as app
import flask
import flask_login
@sso.route('/login', methods=['GET', 'POST'])
def login():
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
form = forms.LoginForm()
form.submitAdmin.label.text = form.submitAdmin.label.text + ' Admin'
form.submitWebmail.label.text = form.submitWebmail.label.text + ' Webmail'
fields = []
if str(app.config["ADMIN"]).upper() != "FALSE":
fields.append(form.submitAdmin)
if str(app.config["WEBMAIL"]).upper() != "NONE":
fields.append(form.submitWebmail)
fields = [fields]
if form.validate_on_submit():
if form.submitAdmin.data:
destination = app.config['WEB_ADMIN']
elif form.submitWebmail.data:
destination = app.config['WEB_WEBMAIL']
device_cookie, device_cookie_username = utils.limiter.parse_device_cookie(flask.request.cookies.get('rate_limit'))
username = form.email.data
if username != device_cookie_username and utils.limiter.should_rate_limit_ip(client_ip):
flask.flash('Too many attempts from your IP (rate-limit)', 'error')
return flask.render_template('login.html', form=form)
if utils.limiter.should_rate_limit_user(username, client_ip, device_cookie, device_cookie_username):
flask.flash('Too many attempts for this user (rate-limit)', 'error')
return flask.render_template('login.html', form=form)
user = models.User.login(username, form.pw.data)
if user:
flask.session.regenerate()
flask_login.login_user(user)
response = flask.redirect(destination)
response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('sso.login'), secure=app.config['SESSION_COOKIE_SECURE'], httponly=True)
flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip}.')
return response
else:
utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip)
flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.')
flask.flash('Wrong e-mail or password', 'error')
return flask.render_template('login.html', form=form, fields=fields)
@sso.route('/logout', methods=['GET'])
@access.authenticated
def logout():
flask_login.logout_user()
flask.session.destroy()
return flask.redirect(flask.url_for('.login'))

@ -0,0 +1,7 @@
from mailu.sso import sso
import flask
@sso.route('/language/<language>', methods=['POST'])
def set_language(language=None):
flask.session['language'] = language
return flask.Response(status=200)

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-04-22 12:10+0200\n" "POT-Creation-Date: 2018-04-22 12:10+0200\n"
"PO-Revision-Date: 2020-04-26 13:09+0000\n" "PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n" "Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n"
"Language-Team: Catalan <https://translate.tedomum.net/projects/mailu/admin/" "Language-Team: Catalan <https://translate.tedomum.net/projects/mailu/admin/"
"ca/>\n" "ca/>\n"
@ -139,7 +139,7 @@ msgstr "Nom per mostrar"
#: mailu/ui/forms.py:98 #: mailu/ui/forms.py:98
msgid "Enable spam filter" msgid "Enable spam filter"
msgstr "Activeu filtre d'spam" msgstr "Activeu filtre spam"
#: mailu/ui/forms.py:99 #: mailu/ui/forms.py:99
msgid "Spam filter tolerance" msgid "Spam filter tolerance"
@ -204,7 +204,8 @@ msgstr "Àlies"
#: mailu/ui/forms.py:138 #: mailu/ui/forms.py:138
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
msgstr "Feu servir sintaxi tipus SQL (ex. per a agafar tots els àlies)" msgstr ""
"Feu servir sintaxi tipus SQL (ex. per seleccionar tots els àlies catch-all)"
#: mailu/ui/forms.py:145 #: mailu/ui/forms.py:145
msgid "Admin email" msgid "Admin email"
@ -246,11 +247,11 @@ msgstr "Mantén els correus al servidor"
#: mailu/ui/forms.py:168 #: mailu/ui/forms.py:168
msgid "Announcement subject" msgid "Announcement subject"
msgstr "Tema de l'avís" msgstr "Tema de la notificació"
#: mailu/ui/forms.py:170 #: mailu/ui/forms.py:170
msgid "Announcement body" msgid "Announcement body"
msgstr "Missatge de l'avís" msgstr "Missatge de la notificació"
#: mailu/ui/forms.py:172 #: mailu/ui/forms.py:172
msgid "Send" msgid "Send"
@ -258,7 +259,7 @@ msgstr "Envia"
#: mailu/ui/templates/announcement.html:4 #: mailu/ui/templates/announcement.html:4
msgid "Public announcement" msgid "Public announcement"
msgstr "Avís públic" msgstr "Notificació pública"
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82 #: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
msgid "Client setup" msgid "Client setup"
@ -304,7 +305,7 @@ msgstr "Resposta automàtica"
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26 #: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26
#: mailu/ui/templates/user/list.html:36 #: mailu/ui/templates/user/list.html:36
msgid "Fetched accounts" msgid "Fetched accounts"
msgstr "Comptes trobats" msgstr "Comptes vinculats"
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4 #: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
msgid "Authentication tokens" msgid "Authentication tokens"
@ -316,7 +317,7 @@ msgstr "Administració"
#: mailu/ui/templates/sidebar.html:44 #: mailu/ui/templates/sidebar.html:44
msgid "Announcement" msgid "Announcement"
msgstr "Avís" msgstr "Notificació"
#: mailu/ui/templates/sidebar.html:49 #: mailu/ui/templates/sidebar.html:49
msgid "Administrators" msgid "Administrators"
@ -324,7 +325,7 @@ msgstr "Administradors"
#: mailu/ui/templates/sidebar.html:54 #: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains" msgid "Relayed domains"
msgstr "Dominis tramesos" msgstr "Dominis traspassats"
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15 #: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
msgid "Antispam" msgid "Antispam"
@ -546,18 +547,19 @@ msgid ""
" expires." " expires."
msgstr "" msgstr ""
"Si no sabeu configurar un registre <code>MX</code> a la zona DNS,\n" "Si no sabeu configurar un registre <code>MX</code> a la zona DNS,\n"
"contacteu el vostre proveïdor o administrador de DNS. Per favor, espereu \n" "contacteu amb el vostre proveïdor o administrador de DNS. Per favor, espereu "
"\n"
"uns quants minuts despres d'ajustar el registre <code>MX</code> perquè la " "uns quants minuts despres d'ajustar el registre <code>MX</code> perquè la "
"caixet \n" "caixet \n"
"del servidor local expire." "del servidor local expire."
#: mailu/ui/templates/fetch/create.html:4 #: mailu/ui/templates/fetch/create.html:4
msgid "Add a fetched account" msgid "Add a fetched account"
msgstr "Afegiu un compte (fetched)" msgstr "Afegiu un compte extern"
#: mailu/ui/templates/fetch/edit.html:4 #: mailu/ui/templates/fetch/edit.html:4
msgid "Update a fetched account" msgid "Update a fetched account"
msgstr "Actualitzeu un compte (fetched)" msgstr "Actualitzeu compte extern"
#: mailu/ui/templates/fetch/list.html:12 #: mailu/ui/templates/fetch/list.html:12
msgid "Add an account" msgid "Add an account"
@ -605,11 +607,11 @@ msgstr "Editeu domini llegat (relayed)"
#: mailu/ui/templates/relay/list.html:4 #: mailu/ui/templates/relay/list.html:4
msgid "Relayed domain list" msgid "Relayed domain list"
msgstr "Llista de dominis llegats (relayed)" msgstr "Llista de dominis traspassats"
#: mailu/ui/templates/relay/list.html:9 #: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain" msgid "New relayed domain"
msgstr "Nou domini llegat (relayed)" msgstr "Nou domini traspassat"
#: mailu/ui/templates/token/create.html:4 #: mailu/ui/templates/token/create.html:4
msgid "Create an authentication token" msgid "Create an authentication token"
@ -653,7 +655,7 @@ msgstr "Ajustos d'usuari"
#: mailu/ui/templates/user/list.html:21 #: mailu/ui/templates/user/list.html:21
msgid "Features" msgid "Features"
msgstr "Funcions" msgstr "Característiques"
#: mailu/ui/templates/user/password.html:4 #: mailu/ui/templates/user/password.html:4
msgid "Password update" msgid "Password update"
@ -669,11 +671,11 @@ msgstr "Auto-reenviament"
#: mailu/ui/templates/user/signup_domain.html:8 #: mailu/ui/templates/user/signup_domain.html:8
msgid "pick a domain for the new account" msgid "pick a domain for the new account"
msgstr "tria un domini per al compte nou" msgstr "trieu un domini per al compte nou"
#: mailu/ui/templates/user/signup_domain.html:14 #: mailu/ui/templates/user/signup_domain.html:14
msgid "Domain" msgid "Domain"
msgstr "Domini" msgstr "Nom de domini"
#: mailu/ui/templates/user/signup_domain.html:15 #: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots" msgid "Available slots"

@ -1,11 +1,16 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mailu\n"
"PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: Anonymous <noreply@weblate.org>\n"
"Language-Team: German <https://translate.tedomum.net/projects/mailu/admin/de/"
">\n"
"Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"Project-Id-Version: Mailu\n" "X-Generator: Weblate 4.0.1\n"
"Language: de\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
msgid "Invalid email address." msgid "Invalid email address."
@ -64,7 +69,7 @@ msgstr "Passwort bestätigen"
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22 #: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22
#: mailu/ui/templates/user/signup_domain.html:16 #: mailu/ui/templates/user/signup_domain.html:16
msgid "Quota" msgid "Quota"
msgstr "Quota" msgstr "Kontingent"
#: mailu/ui/forms.py:81 #: mailu/ui/forms.py:81
msgid "Allow IMAP access" msgid "Allow IMAP access"
@ -699,4 +704,3 @@ msgstr "Domain"
#: mailu/ui/templates/user/signup_domain.html:15 #: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots" msgid "Available slots"
msgstr "Verfügbare Plätze" msgstr "Verfügbare Plätze"

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-04-22 12:10+0200\n" "POT-Creation-Date: 2018-04-22 12:10+0200\n"
"PO-Revision-Date: 2020-03-11 23:03+0000\n" "PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: Jae Beojkkoch <jae@jae.moe>\n" "Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n"
"Language-Team: English <https://translate.tedomum.net/projects/mailu/admin/" "Language-Team: English <https://translate.tedomum.net/projects/mailu/admin/"
"en/>\n" "en/>\n"
"Language: en\n" "Language: en\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.11.2\n" "X-Generator: Weblate 4.0.1\n"
"Generated-By: Babel 2.5.3\n" "Generated-By: Babel 2.5.3\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
@ -30,13 +30,13 @@ msgstr "Confirm"
#: mailu/ui/forms.py:40 mailu/ui/forms.py:77 #: mailu/ui/forms.py:40 mailu/ui/forms.py:77
msgid "E-mail" msgid "E-mail"
msgstr "" msgstr "E-mail"
#: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90 #: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90
#: mailu/ui/forms.py:109 mailu/ui/forms.py:162 #: mailu/ui/forms.py:109 mailu/ui/forms.py:162
#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:59 #: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:59
msgid "Password" msgid "Password"
msgstr "" msgstr "Password"
#: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4 #: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4
#: mailu/ui/templates/sidebar.html:111 #: mailu/ui/templates/sidebar.html:111
@ -51,7 +51,7 @@ msgstr ""
#: mailu/ui/forms.py:47 #: mailu/ui/forms.py:47
msgid "Maximum user count" msgid "Maximum user count"
msgstr "" msgstr "Maximum user count"
#: mailu/ui/forms.py:48 #: mailu/ui/forms.py:48
msgid "Maximum alias count" msgid "Maximum alias count"
@ -59,11 +59,11 @@ msgstr ""
#: mailu/ui/forms.py:49 #: mailu/ui/forms.py:49
msgid "Maximum user quota" msgid "Maximum user quota"
msgstr "" msgstr "Maximum user quota"
#: mailu/ui/forms.py:50 #: mailu/ui/forms.py:50
msgid "Enable sign-up" msgid "Enable sign-up"
msgstr "" msgstr "Enable sign-up"
#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83 #: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83
#: mailu/ui/forms.py:128 mailu/ui/forms.py:140 #: mailu/ui/forms.py:128 mailu/ui/forms.py:140
@ -71,28 +71,28 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19 #: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19
#: mailu/ui/templates/user/list.html:23 #: mailu/ui/templates/user/list.html:23
msgid "Comment" msgid "Comment"
msgstr "" msgstr "Comment"
#: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66 #: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66
#: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141 #: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141
msgid "Create" msgid "Create"
msgstr "" msgstr "Create"
#: mailu/ui/forms.py:57 #: mailu/ui/forms.py:57
msgid "Initial admin" msgid "Initial admin"
msgstr "" msgstr "Initial admin"
#: mailu/ui/forms.py:58 #: mailu/ui/forms.py:58
msgid "Admin password" msgid "Admin password"
msgstr "" msgstr "Admin password"
#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91 #: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91
msgid "Confirm password" msgid "Confirm password"
msgstr "" msgstr "Confirm password"
#: mailu/ui/forms.py:65 #: mailu/ui/forms.py:65
msgid "Alternative name" msgid "Alternative name"
msgstr "" msgstr "Alternative name"
#: mailu/ui/forms.py:70 #: mailu/ui/forms.py:70
msgid "Relayed domain name" msgid "Relayed domain name"
@ -105,23 +105,23 @@ msgstr ""
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22 #: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22
#: mailu/ui/templates/user/signup_domain.html:16 #: mailu/ui/templates/user/signup_domain.html:16
msgid "Quota" msgid "Quota"
msgstr "" msgstr "Quota"
#: mailu/ui/forms.py:81 #: mailu/ui/forms.py:81
msgid "Allow IMAP access" msgid "Allow IMAP access"
msgstr "" msgstr "Allow IMAP access"
#: mailu/ui/forms.py:82 #: mailu/ui/forms.py:82
msgid "Allow POP3 access" msgid "Allow POP3 access"
msgstr "" msgstr "Allow POP3 access"
#: mailu/ui/forms.py:84 #: mailu/ui/forms.py:84
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr "Enabled"
#: mailu/ui/forms.py:85 #: mailu/ui/forms.py:85
msgid "Save" msgid "Save"
msgstr "" msgstr "Save"
#: mailu/ui/forms.py:89 #: mailu/ui/forms.py:89
msgid "Email address" msgid "Email address"
@ -131,7 +131,7 @@ msgstr ""
#: mailu/ui/templates/user/signup.html:4 #: mailu/ui/templates/user/signup.html:4
#: mailu/ui/templates/user/signup_domain.html:4 #: mailu/ui/templates/user/signup_domain.html:4
msgid "Sign up" msgid "Sign up"
msgstr "" msgstr "Sign up"
#: mailu/ui/forms.py:97 #: mailu/ui/forms.py:97
msgid "Displayed name" msgid "Displayed name"
@ -139,15 +139,15 @@ msgstr ""
#: mailu/ui/forms.py:98 #: mailu/ui/forms.py:98
msgid "Enable spam filter" msgid "Enable spam filter"
msgstr "" msgstr "Enable spam filter"
#: mailu/ui/forms.py:99 #: mailu/ui/forms.py:99
msgid "Spam filter tolerance" msgid "Spam filter tolerance"
msgstr "" msgstr "Spam filter tolerance"
#: mailu/ui/forms.py:100 #: mailu/ui/forms.py:100
msgid "Enable forwarding" msgid "Enable forwarding"
msgstr "" msgstr "Enable forwarding"
#: mailu/ui/forms.py:101 #: mailu/ui/forms.py:101
msgid "Keep a copy of the emails" msgid "Keep a copy of the emails"
@ -160,7 +160,7 @@ msgstr ""
#: mailu/ui/forms.py:105 #: mailu/ui/forms.py:105
msgid "Save settings" msgid "Save settings"
msgstr "" msgstr "Save settings"
#: mailu/ui/forms.py:110 #: mailu/ui/forms.py:110
msgid "Password check" msgid "Password check"
@ -184,11 +184,11 @@ msgstr ""
#: mailu/ui/forms.py:119 #: mailu/ui/forms.py:119
msgid "End of vacation" msgid "End of vacation"
msgstr "" msgstr "End of vacation"
#: mailu/ui/forms.py:120 #: mailu/ui/forms.py:120
msgid "Update" msgid "Update"
msgstr "" msgstr "Update"
#: mailu/ui/forms.py:125 #: mailu/ui/forms.py:125
msgid "Your token (write it down, as it will never be displayed again)" msgid "Your token (write it down, as it will never be displayed again)"
@ -196,11 +196,11 @@ msgstr ""
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20 #: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
msgid "Authorized IP" msgid "Authorized IP"
msgstr "" msgstr "Authorized IP"
#: mailu/ui/forms.py:136 #: mailu/ui/forms.py:136
msgid "Alias" msgid "Alias"
msgstr "" msgstr "Alias"
#: mailu/ui/forms.py:138 #: mailu/ui/forms.py:138
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
@ -229,7 +229,7 @@ msgstr ""
#: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20 #: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20
#: mailu/ui/templates/client.html:47 #: mailu/ui/templates/client.html:47
msgid "TCP port" msgid "TCP port"
msgstr "" msgstr "TCP port"
#: mailu/ui/forms.py:160 #: mailu/ui/forms.py:160
msgid "Enable TLS" msgid "Enable TLS"
@ -283,7 +283,7 @@ msgstr ""
#: mailu/ui/templates/docker-error.html:4 #: mailu/ui/templates/docker-error.html:4
msgid "Docker error" msgid "Docker error"
msgstr "" msgstr "Docker error"
#: mailu/ui/templates/docker-error.html:12 #: mailu/ui/templates/docker-error.html:12
msgid "An error occurred while talking to the Docker server." msgid "An error occurred while talking to the Docker server."
@ -328,11 +328,11 @@ msgstr ""
#: mailu/ui/templates/sidebar.html:54 #: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains" msgid "Relayed domains"
msgstr "" msgstr "Relayed domains"
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15 #: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
msgid "Antispam" msgid "Antispam"
msgstr "" msgstr "Antispam"
#: mailu/ui/templates/sidebar.html:66 #: mailu/ui/templates/sidebar.html:66
msgid "Mail domains" msgid "Mail domains"
@ -593,7 +593,7 @@ msgstr ""
#: mailu/ui/templates/relay/create.html:4 #: mailu/ui/templates/relay/create.html:4
msgid "New relay domain" msgid "New relay domain"
msgstr "" msgstr "New relay domain"
#: mailu/ui/templates/relay/edit.html:4 #: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain" msgid "Edit relayd domain"
@ -601,11 +601,11 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:4 #: mailu/ui/templates/relay/list.html:4
msgid "Relayed domain list" msgid "Relayed domain list"
msgstr "" msgstr "Relayed domain list"
#: mailu/ui/templates/relay/list.html:9 #: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain" msgid "New relayed domain"
msgstr "" msgstr "New relayed domain"
#: mailu/ui/templates/token/create.html:4 #: mailu/ui/templates/token/create.html:4
msgid "Create an authentication token" msgid "Create an authentication token"
@ -669,7 +669,7 @@ msgstr ""
#: mailu/ui/templates/user/signup_domain.html:14 #: mailu/ui/templates/user/signup_domain.html:14
msgid "Domain" msgid "Domain"
msgstr "" msgstr "Domain"
#: mailu/ui/templates/user/signup_domain.html:15 #: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots" msgid "Available slots"

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mailu\n" "Project-Id-Version: Mailu\n"
"PO-Revision-Date: 2020-03-11 23:03+0000\n" "PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n" "Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n"
"Language-Team: Spanish <https://translate.tedomum.net/projects/mailu/admin/" "Language-Team: Spanish <https://translate.tedomum.net/projects/mailu/admin/"
"es/>\n" "es/>\n"
@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.11.2\n" "X-Generator: Weblate 4.0.1\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
msgid "Invalid email address." msgid "Invalid email address."
@ -425,7 +425,7 @@ msgstr "Añadir una cuenta"
#: mailu/ui/templates/fetch/list.html:19 #: mailu/ui/templates/fetch/list.html:19
msgid "Endpoint" msgid "Endpoint"
msgstr "Punto final" msgstr "Endpoint"
#: mailu/ui/templates/fetch/list.html:22 #: mailu/ui/templates/fetch/list.html:22
msgid "Last check" msgid "Last check"
@ -437,7 +437,7 @@ msgstr "Añadir un gestor"
#: mailu/ui/templates/manager/list.html:4 #: mailu/ui/templates/manager/list.html:4
msgid "Manager list" msgid "Manager list"
msgstr "Gestor de lista" msgstr "Lista de gestores"
#: mailu/ui/templates/manager/list.html:12 #: mailu/ui/templates/manager/list.html:12
msgid "Add manager" msgid "Add manager"
@ -578,7 +578,7 @@ msgstr "Lista de dominios externos (relayed)"
#: mailu/ui/templates/relay/list.html:9 #: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain" msgid "New relayed domain"
msgstr "Nuevo dominio externo (relayed)" msgstr "Editar dominio externo (relay)"
#: mailu/ui/forms.py:125 #: mailu/ui/forms.py:125
msgid "Your token (write it down, as it will never be displayed again)" msgid "Your token (write it down, as it will never be displayed again)"

@ -0,0 +1,672 @@
# Translations template for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-04-22 12:10+0200\n"
"PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n"
"Language-Team: Basque <https://translate.tedomum.net/projects/mailu/admin/eu/"
">\n"
"Language: eu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.0.1\n"
"Generated-By: Babel 2.5.3\n"
#: mailu/ui/forms.py:32
msgid "Invalid email address."
msgstr "baliogabeko helbide elektronikoa."
#: mailu/ui/forms.py:36
msgid "Confirm"
msgstr "Ados"
#: mailu/ui/forms.py:40 mailu/ui/forms.py:77
msgid "E-mail"
msgstr "E-mail"
#: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90
#: mailu/ui/forms.py:109 mailu/ui/forms.py:162
#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:59
msgid "Password"
msgstr "Pasahitza"
#: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4
#: mailu/ui/templates/sidebar.html:111
msgid "Sign in"
msgstr ""
#: mailu/ui/forms.py:46 mailu/ui/forms.py:56
#: mailu/ui/templates/domain/details.html:27
#: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17
msgid "Domain name"
msgstr ""
#: mailu/ui/forms.py:47
msgid "Maximum user count"
msgstr "Erabiltzaileen gehieneko kopurua"
#: mailu/ui/forms.py:48
msgid "Maximum alias count"
msgstr ""
#: mailu/ui/forms.py:49
msgid "Maximum user quota"
msgstr "Erabiltzaile bakoitzeko gehieneko espazioa"
#: mailu/ui/forms.py:50
msgid "Enable sign-up"
msgstr "Gaitu erregistroa"
#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83
#: mailu/ui/forms.py:128 mailu/ui/forms.py:140
#: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21
#: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19
#: mailu/ui/templates/user/list.html:23
msgid "Comment"
msgstr "Iruzkindua"
#: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66
#: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141
msgid "Create"
msgstr "Sortu"
#: mailu/ui/forms.py:57
msgid "Initial admin"
msgstr "Administratzailea"
#: mailu/ui/forms.py:58
msgid "Admin password"
msgstr "Administratzaileko pasahitza"
#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91
msgid "Confirm password"
msgstr "Berretsi pasahitza"
#: mailu/ui/forms.py:65
msgid "Alternative name"
msgstr "Izen alternatiboa"
#: mailu/ui/forms.py:70
msgid "Relayed domain name"
msgstr "Igorritako domeinu izena"
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
msgid "Remote host"
msgstr "Urruneko ostalaria"
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22
#: mailu/ui/templates/user/signup_domain.html:16
msgid "Quota"
msgstr "Espazioa"
#: mailu/ui/forms.py:81
msgid "Allow IMAP access"
msgstr "Baimendu IMAP sarbidea"
#: mailu/ui/forms.py:82
msgid "Allow POP3 access"
msgstr "Baimendu POP3 sarbidea"
#: mailu/ui/forms.py:84
msgid "Enabled"
msgstr "Gaituta"
#: mailu/ui/forms.py:85
msgid "Save"
msgstr "Gorde"
#: mailu/ui/forms.py:89
msgid "Email address"
msgstr ""
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
#: mailu/ui/templates/user/signup.html:4
#: mailu/ui/templates/user/signup_domain.html:4
msgid "Sign up"
msgstr "Erregistratu"
#: mailu/ui/forms.py:97
msgid "Displayed name"
msgstr ""
#: mailu/ui/forms.py:98
msgid "Enable spam filter"
msgstr "Gaitu spam iragazkia"
#: mailu/ui/forms.py:99
msgid "Spam filter tolerance"
msgstr "Spam iragazkiaren tolerantzia"
#: mailu/ui/forms.py:100
msgid "Enable forwarding"
msgstr "Gaitu birbidaltzea"
#: mailu/ui/forms.py:101
msgid "Keep a copy of the emails"
msgstr ""
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139
#: mailu/ui/templates/alias/list.html:20
msgid "Destination"
msgstr ""
#: mailu/ui/forms.py:105
msgid "Save settings"
msgstr "Gorde ezarpenak"
#: mailu/ui/forms.py:110
msgid "Password check"
msgstr ""
#: mailu/ui/forms.py:111 mailu/ui/templates/sidebar.html:16
msgid "Update password"
msgstr ""
#: mailu/ui/forms.py:115
msgid "Enable automatic reply"
msgstr ""
#: mailu/ui/forms.py:116
msgid "Reply subject"
msgstr ""
#: mailu/ui/forms.py:117
msgid "Reply body"
msgstr ""
#: mailu/ui/forms.py:119
msgid "End of vacation"
msgstr "Oporren amaiera"
#: mailu/ui/forms.py:120
msgid "Update"
msgstr "Eguneratu"
#: mailu/ui/forms.py:125
msgid "Your token (write it down, as it will never be displayed again)"
msgstr ""
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
msgid "Authorized IP"
msgstr "Baimendutako IP"
#: mailu/ui/forms.py:136
msgid "Alias"
msgstr "Ezizenza"
#: mailu/ui/forms.py:138
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
msgstr ""
#: mailu/ui/forms.py:145
msgid "Admin email"
msgstr ""
#: mailu/ui/forms.py:146 mailu/ui/forms.py:151 mailu/ui/forms.py:164
msgid "Submit"
msgstr ""
#: mailu/ui/forms.py:150
msgid "Manager email"
msgstr ""
#: mailu/ui/forms.py:155
msgid "Protocol"
msgstr ""
#: mailu/ui/forms.py:158
msgid "Hostname or IP"
msgstr ""
#: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20
#: mailu/ui/templates/client.html:47
msgid "TCP port"
msgstr "TCP ataka"
#: mailu/ui/forms.py:160
msgid "Enable TLS"
msgstr ""
#: mailu/ui/forms.py:161 mailu/ui/templates/client.html:28
#: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20
msgid "Username"
msgstr ""
#: mailu/ui/forms.py:163
msgid "Keep emails on the server"
msgstr ""
#: mailu/ui/forms.py:168
msgid "Announcement subject"
msgstr ""
#: mailu/ui/forms.py:170
msgid "Announcement body"
msgstr ""
#: mailu/ui/forms.py:172
msgid "Send"
msgstr ""
#: mailu/ui/templates/announcement.html:4
msgid "Public announcement"
msgstr ""
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
msgid "Client setup"
msgstr ""
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
msgid "Mail protocol"
msgstr ""
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
msgid "Server name"
msgstr ""
#: mailu/ui/templates/confirm.html:4
msgid "Confirm action"
msgstr ""
#: mailu/ui/templates/confirm.html:13
#, python-format
msgid "You are about to %(action)s. Please confirm your action."
msgstr "Zu zara %(action)s-etan. Mesedez ekintza honen berretsi."
#: mailu/ui/templates/docker-error.html:4
msgid "Docker error"
msgstr "Docker-en errorea"
#: mailu/ui/templates/docker-error.html:12
msgid "An error occurred while talking to the Docker server."
msgstr ""
#: mailu/ui/templates/login.html:8
msgid "to access the administration tools"
msgstr ""
#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34
msgid "Settings"
msgstr ""
#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35
msgid "Auto-reply"
msgstr ""
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26
#: mailu/ui/templates/user/list.html:36
msgid "Fetched accounts"
msgstr ""
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
msgid "Authentication tokens"
msgstr ""
#: mailu/ui/templates/sidebar.html:35
msgid "Administration"
msgstr ""
#: mailu/ui/templates/sidebar.html:44
msgid "Announcement"
msgstr ""
#: mailu/ui/templates/sidebar.html:49
msgid "Administrators"
msgstr ""
#: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains"
msgstr "Igorritako domeinuak"
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
msgid "Antispam"
msgstr "Antispam"
#: mailu/ui/templates/sidebar.html:66
msgid "Mail domains"
msgstr ""
#: mailu/ui/templates/sidebar.html:72
msgid "Go to"
msgstr ""
#: mailu/ui/templates/sidebar.html:76
msgid "Webmail"
msgstr ""
#: mailu/ui/templates/sidebar.html:87
msgid "Website"
msgstr ""
#: mailu/ui/templates/sidebar.html:92
msgid "Help"
msgstr ""
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
msgid "Register a domain"
msgstr ""
#: mailu/ui/templates/sidebar.html:105
msgid "Sign out"
msgstr ""
#: mailu/ui/templates/working.html:4
msgid "We are still working on this feature!"
msgstr ""
#: mailu/ui/templates/admin/create.html:4
msgid "Add a global administrator"
msgstr ""
#: mailu/ui/templates/admin/list.html:4
msgid "Global administrators"
msgstr ""
#: mailu/ui/templates/admin/list.html:9
msgid "Add administrator"
msgstr ""
#: mailu/ui/templates/admin/list.html:16 mailu/ui/templates/alias/list.html:18
#: mailu/ui/templates/alternative/list.html:18
#: mailu/ui/templates/domain/list.html:16 mailu/ui/templates/fetch/list.html:18
#: mailu/ui/templates/manager/list.html:18
#: mailu/ui/templates/relay/list.html:16 mailu/ui/templates/token/list.html:18
#: mailu/ui/templates/user/list.html:18
msgid "Actions"
msgstr ""
#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19
#: mailu/ui/templates/manager/list.html:19 mailu/ui/templates/user/list.html:20
msgid "Email"
msgstr ""
#: mailu/ui/templates/admin/list.html:22 mailu/ui/templates/alias/list.html:29
#: mailu/ui/templates/alternative/list.html:25
#: mailu/ui/templates/domain/list.html:31 mailu/ui/templates/fetch/list.html:31
#: mailu/ui/templates/manager/list.html:24
#: mailu/ui/templates/relay/list.html:27 mailu/ui/templates/token/list.html:26
#: mailu/ui/templates/user/list.html:31
msgid "Delete"
msgstr ""
#: mailu/ui/templates/alias/create.html:4
msgid "Create alias"
msgstr ""
#: mailu/ui/templates/alias/edit.html:4
msgid "Edit alias"
msgstr ""
#: mailu/ui/templates/alias/list.html:4
msgid "Alias list"
msgstr ""
#: mailu/ui/templates/alias/list.html:12
msgid "Add alias"
msgstr ""
#: mailu/ui/templates/alias/list.html:22
#: mailu/ui/templates/alternative/list.html:20
#: mailu/ui/templates/domain/list.html:22 mailu/ui/templates/fetch/list.html:24
#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21
#: mailu/ui/templates/user/list.html:24
msgid "Created"
msgstr ""
#: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23
#: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21
#: mailu/ui/templates/user/list.html:25
msgid "Last edit"
msgstr ""
#: mailu/ui/templates/alias/list.html:28 mailu/ui/templates/domain/list.html:30
#: mailu/ui/templates/fetch/list.html:30 mailu/ui/templates/relay/list.html:26
#: mailu/ui/templates/user/list.html:30
msgid "Edit"
msgstr ""
#: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain"
msgstr ""
#: mailu/ui/templates/alternative/list.html:4
msgid "Alternative domain list"
msgstr ""
#: mailu/ui/templates/alternative/list.html:12
msgid "Add alternative"
msgstr ""
#: mailu/ui/templates/alternative/list.html:19
msgid "Name"
msgstr ""
#: mailu/ui/templates/domain/create.html:4
#: mailu/ui/templates/domain/list.html:9
msgid "New domain"
msgstr ""
#: mailu/ui/templates/domain/details.html:4
msgid "Domain details"
msgstr ""
#: mailu/ui/templates/domain/details.html:15
msgid "Regenerate keys"
msgstr ""
#: mailu/ui/templates/domain/details.html:17
msgid "Generate keys"
msgstr ""
#: mailu/ui/templates/domain/details.html:31
msgid "DNS MX entry"
msgstr ""
#: mailu/ui/templates/domain/details.html:35
msgid "DNS SPF entries"
msgstr ""
#: mailu/ui/templates/domain/details.html:42
msgid "DKIM public key"
msgstr ""
#: mailu/ui/templates/domain/details.html:46
msgid "DNS DKIM entry"
msgstr ""
#: mailu/ui/templates/domain/details.html:50
msgid "DNS DMARC entry"
msgstr ""
#: mailu/ui/templates/domain/edit.html:4
msgid "Edit domain"
msgstr ""
#: mailu/ui/templates/domain/list.html:4
msgid "Domain list"
msgstr ""
#: mailu/ui/templates/domain/list.html:17
msgid "Manage"
msgstr ""
#: mailu/ui/templates/domain/list.html:19
msgid "Mailbox count"
msgstr ""
#: mailu/ui/templates/domain/list.html:20
msgid "Alias count"
msgstr ""
#: mailu/ui/templates/domain/list.html:28
msgid "Details"
msgstr ""
#: mailu/ui/templates/domain/list.html:35
msgid "Users"
msgstr ""
#: mailu/ui/templates/domain/list.html:36
msgid "Aliases"
msgstr ""
#: mailu/ui/templates/domain/list.html:37
msgid "Managers"
msgstr ""
#: mailu/ui/templates/domain/list.html:39
msgid "Alternatives"
msgstr ""
#: mailu/ui/templates/domain/signup.html:13
msgid ""
"In order to register a new domain, you must first setup the\n"
" domain zone so that the domain <code>MX</code> points to this server"
msgstr ""
#: mailu/ui/templates/domain/signup.html:18
msgid ""
"If you do not know how to setup an <code>MX</code> record for your DNS "
"zone,\n"
" please contact your DNS provider or administrator. Also, please wait "
"a\n"
" couple minutes after the <code>MX</code> is set so the local server "
"cache\n"
" expires."
msgstr ""
#: mailu/ui/templates/fetch/create.html:4
msgid "Add a fetched account"
msgstr ""
#: mailu/ui/templates/fetch/edit.html:4
msgid "Update a fetched account"
msgstr ""
#: mailu/ui/templates/fetch/list.html:12
msgid "Add an account"
msgstr ""
#: mailu/ui/templates/fetch/list.html:19
msgid "Endpoint"
msgstr ""
#: mailu/ui/templates/fetch/list.html:21
msgid "Keep emails"
msgstr ""
#: mailu/ui/templates/fetch/list.html:22
msgid "Last check"
msgstr ""
#: mailu/ui/templates/fetch/list.html:35
msgid "yes"
msgstr ""
#: mailu/ui/templates/fetch/list.html:35
msgid "no"
msgstr ""
#: mailu/ui/templates/manager/create.html:4
msgid "Add a manager"
msgstr ""
#: mailu/ui/templates/manager/list.html:4
msgid "Manager list"
msgstr ""
#: mailu/ui/templates/manager/list.html:12
msgid "Add manager"
msgstr ""
#: mailu/ui/templates/relay/create.html:4
msgid "New relay domain"
msgstr "Igorritako domeinu berria"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgstr "Editatu igorritako domeinua"
#: mailu/ui/templates/relay/list.html:4
msgid "Relayed domain list"
msgstr "Igorritako domeinuen zerrenda"
#: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain"
msgstr "Igorritako domeinu berria"
#: mailu/ui/templates/token/create.html:4
msgid "Create an authentication token"
msgstr ""
#: mailu/ui/templates/token/list.html:12
msgid "New token"
msgstr ""
#: mailu/ui/templates/user/create.html:4
msgid "New user"
msgstr ""
#: mailu/ui/templates/user/create.html:15
msgid "General"
msgstr ""
#: mailu/ui/templates/user/create.html:22
msgid "Features and quotas"
msgstr ""
#: mailu/ui/templates/user/edit.html:4
msgid "Edit user"
msgstr ""
#: mailu/ui/templates/user/forward.html:4
msgid "Forward emails"
msgstr ""
#: mailu/ui/templates/user/list.html:4
msgid "User list"
msgstr ""
#: mailu/ui/templates/user/list.html:12
msgid "Add user"
msgstr ""
#: mailu/ui/templates/user/list.html:19 mailu/ui/templates/user/settings.html:4
msgid "User settings"
msgstr ""
#: mailu/ui/templates/user/list.html:21
msgid "Features"
msgstr ""
#: mailu/ui/templates/user/password.html:4
msgid "Password update"
msgstr ""
#: mailu/ui/templates/user/reply.html:4
msgid "Automatic reply"
msgstr ""
#: mailu/ui/templates/user/settings.html:22
msgid "Auto-forward"
msgstr ""
#: mailu/ui/templates/user/signup_domain.html:8
msgid "pick a domain for the new account"
msgstr ""
#: mailu/ui/templates/user/signup_domain.html:14
msgid "Domain"
msgstr "Domeinu izena"
#: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots"
msgstr ""

@ -1,29 +1,30 @@
# Translations template for PROJECT. # Translations template for Mailu.
# Copyright (C) 2018 ORGANIZATION # Copyright (C) 2018 Mailu
# This file is distributed under the same license as the PROJECT project. # This file is distributed under the same license as the Mailu project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018. # Modi Sacks, 2019-2021.
# Yaron Shahrabani <sh.yaron@gmail.com>, 2021.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: Mailu 1.5.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: heb-bugzap@projects.hamakor.org.il \n"
"POT-Creation-Date: 2018-04-22 12:10+0200\n" "POT-Creation-Date: 2018-04-22 12:10+0200\n"
"PO-Revision-Date: 2019-11-27 22:20+0000\n" "PO-Revision-Date: 2021-07-19 09:04+0300\n"
"Last-Translator: Mordi Sacks <mordisacks@gmail.com>\n" "Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n"
"Language-Team: Hebrew <https://translate.tedomum.net/projects/mailu/admin/he/" "Language-Team: Hebrew <https://translate.tedomum.net/projects/mailu/admin/he/"
">\n" ">\n"
"Language: he\n" "Language: he\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " "Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && "
"n % 10 == 0) ? 2 : 3));\n" "n % 10 == 0) ? 2 : 3));\n"
"X-Generator: Weblate 3.3\n" "X-Generator: Poedit 3.0\n"
"Generated-By: Babel 2.5.3\n" "Generated-By: Babel 2.5.3\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
msgid "Invalid email address." msgid "Invalid email address."
msgstr "כתובת דוא\"ל לא חוקית." msgstr "כתובת דוא״ל שגויה."
#: mailu/ui/forms.py:36 #: mailu/ui/forms.py:36
msgid "Confirm" msgid "Confirm"
@ -31,7 +32,7 @@ msgstr "אישור"
#: mailu/ui/forms.py:40 mailu/ui/forms.py:77 #: mailu/ui/forms.py:40 mailu/ui/forms.py:77
msgid "E-mail" msgid "E-mail"
msgstr "דוא\"ל" msgstr "דוא״ל"
#: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90 #: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90
#: mailu/ui/forms.py:109 mailu/ui/forms.py:162 #: mailu/ui/forms.py:109 mailu/ui/forms.py:162
@ -48,23 +49,23 @@ msgstr "כניסה"
#: mailu/ui/templates/domain/details.html:27 #: mailu/ui/templates/domain/details.html:27
#: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17 #: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17
msgid "Domain name" msgid "Domain name"
msgstr "שם דומיין" msgstr "שם תחום"
#: mailu/ui/forms.py:47 #: mailu/ui/forms.py:47
msgid "Maximum user count" msgid "Maximum user count"
msgstr "" msgstr "כמות המשתמשים המרבית"
#: mailu/ui/forms.py:48 #: mailu/ui/forms.py:48
msgid "Maximum alias count" msgid "Maximum alias count"
msgstr "" msgstr "כמות הכינויים המרבית"
#: mailu/ui/forms.py:49 #: mailu/ui/forms.py:49
msgid "Maximum user quota" msgid "Maximum user quota"
msgstr "" msgstr "מיכסת המשתמשים המרבית"
#: mailu/ui/forms.py:50 #: mailu/ui/forms.py:50
msgid "Enable sign-up" msgid "Enable sign-up"
msgstr "" msgstr "לאפשר הרשמה"
#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83 #: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83
#: mailu/ui/forms.py:128 mailu/ui/forms.py:140 #: mailu/ui/forms.py:128 mailu/ui/forms.py:140
@ -72,53 +73,53 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19 #: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19
#: mailu/ui/templates/user/list.html:23 #: mailu/ui/templates/user/list.html:23
msgid "Comment" msgid "Comment"
msgstr "" msgstr "תגובה"
#: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66 #: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66
#: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141 #: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141
msgid "Create" msgid "Create"
msgstr "" msgstr "יצירה"
#: mailu/ui/forms.py:57 #: mailu/ui/forms.py:57
msgid "Initial admin" msgid "Initial admin"
msgstr "" msgstr "מנהל ראשוני"
#: mailu/ui/forms.py:58 #: mailu/ui/forms.py:58
msgid "Admin password" msgid "Admin password"
msgstr "" msgstr "סיסמת ניהול"
#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91 #: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91
msgid "Confirm password" msgid "Confirm password"
msgstr "" msgstr "אישור סיסמה"
#: mailu/ui/forms.py:65 #: mailu/ui/forms.py:65
msgid "Alternative name" msgid "Alternative name"
msgstr "" msgstr "שם חלופי"
#: mailu/ui/forms.py:70 #: mailu/ui/forms.py:70
msgid "Relayed domain name" msgid "Relayed domain name"
msgstr "" msgstr "שם תחום מועבר"
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18 #: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
msgid "Remote host" msgid "Remote host"
msgstr "" msgstr "מארח מרוחק"
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22 #: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22
#: mailu/ui/templates/user/signup_domain.html:16 #: mailu/ui/templates/user/signup_domain.html:16
msgid "Quota" msgid "Quota"
msgstr "" msgstr "מיכסה"
#: mailu/ui/forms.py:81 #: mailu/ui/forms.py:81
msgid "Allow IMAP access" msgid "Allow IMAP access"
msgstr "" msgstr "לאפשר גישה ב־IMAP"
#: mailu/ui/forms.py:82 #: mailu/ui/forms.py:82
msgid "Allow POP3 access" msgid "Allow POP3 access"
msgstr "" msgstr "לאפשר גישה ב־POP3"
#: mailu/ui/forms.py:84 #: mailu/ui/forms.py:84
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr "מופעל"
#: mailu/ui/forms.py:85 #: mailu/ui/forms.py:85
msgid "Save" msgid "Save"
@ -126,7 +127,7 @@ msgstr "שמירה"
#: mailu/ui/forms.py:89 #: mailu/ui/forms.py:89
msgid "Email address" msgid "Email address"
msgstr "דואר אלקטרוני" msgstr "כתובת דוא״ל"
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117 #: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
#: mailu/ui/templates/user/signup.html:4 #: mailu/ui/templates/user/signup.html:4
@ -136,244 +137,244 @@ msgstr "הרשמה"
#: mailu/ui/forms.py:97 #: mailu/ui/forms.py:97
msgid "Displayed name" msgid "Displayed name"
msgstr "" msgstr "שם מוצג"
#: mailu/ui/forms.py:98 #: mailu/ui/forms.py:98
msgid "Enable spam filter" msgid "Enable spam filter"
msgstr "" msgstr "הפעלת מסנן ספאם"
#: mailu/ui/forms.py:99 #: mailu/ui/forms.py:99
msgid "Spam filter tolerance" msgid "Spam filter tolerance"
msgstr "" msgstr "סובלנות מסנן הספאם"
#: mailu/ui/forms.py:100 #: mailu/ui/forms.py:100
msgid "Enable forwarding" msgid "Enable forwarding"
msgstr "" msgstr "הפעלת העברה"
#: mailu/ui/forms.py:101 #: mailu/ui/forms.py:101
msgid "Keep a copy of the emails" msgid "Keep a copy of the emails"
msgstr "" msgstr "להשאיר עותק מההודעות"
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139 #: mailu/ui/forms.py:103 mailu/ui/forms.py:139
#: mailu/ui/templates/alias/list.html:20 #: mailu/ui/templates/alias/list.html:20
msgid "Destination" msgid "Destination"
msgstr "" msgstr "יעד"
#: mailu/ui/forms.py:105 #: mailu/ui/forms.py:105
msgid "Save settings" msgid "Save settings"
msgstr "" msgstr "שמירת הגדרות"
#: mailu/ui/forms.py:110 #: mailu/ui/forms.py:110
msgid "Password check" msgid "Password check"
msgstr "" msgstr "בדיקת סיסמה"
#: mailu/ui/forms.py:111 mailu/ui/templates/sidebar.html:16 #: mailu/ui/forms.py:111 mailu/ui/templates/sidebar.html:16
msgid "Update password" msgid "Update password"
msgstr "" msgstr "עדכון סיסמה"
#: mailu/ui/forms.py:115 #: mailu/ui/forms.py:115
msgid "Enable automatic reply" msgid "Enable automatic reply"
msgstr "" msgstr "הפעלת תגובה אוטומטית"
#: mailu/ui/forms.py:116 #: mailu/ui/forms.py:116
msgid "Reply subject" msgid "Reply subject"
msgstr "" msgstr "נושא התגובה"
#: mailu/ui/forms.py:117 #: mailu/ui/forms.py:117
msgid "Reply body" msgid "Reply body"
msgstr "" msgstr "גוף התגובה"
#: mailu/ui/forms.py:119 #: mailu/ui/forms.py:119
msgid "End of vacation" msgid "End of vacation"
msgstr "" msgstr "סוף החופשה"
#: mailu/ui/forms.py:120 #: mailu/ui/forms.py:120
msgid "Update" msgid "Update"
msgstr "" msgstr "עדכון"
#: mailu/ui/forms.py:125 #: mailu/ui/forms.py:125
msgid "Your token (write it down, as it will never be displayed again)" msgid "Your token (write it down, as it will never be displayed again)"
msgstr "" msgstr "האסימון שלך (כדאי לשמור עליו היטב כיוון שהוא לא יופיע פעם נוספת)"
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20 #: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
msgid "Authorized IP" msgid "Authorized IP"
msgstr "" msgstr "כתובת IP מורשית"
#: mailu/ui/forms.py:136 #: mailu/ui/forms.py:136
msgid "Alias" msgid "Alias"
msgstr "" msgstr "כינוי"
#: mailu/ui/forms.py:138 #: mailu/ui/forms.py:138
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
msgstr "" msgstr "להשתמש בתחביר דמוי SQL (למשל: catch-all aliases)"
#: mailu/ui/forms.py:145 #: mailu/ui/forms.py:145
msgid "Admin email" msgid "Admin email"
msgstr "" msgstr "דוא״ל ההנהלה"
#: mailu/ui/forms.py:146 mailu/ui/forms.py:151 mailu/ui/forms.py:164 #: mailu/ui/forms.py:146 mailu/ui/forms.py:151 mailu/ui/forms.py:164
msgid "Submit" msgid "Submit"
msgstr "" msgstr "הגשה"
#: mailu/ui/forms.py:150 #: mailu/ui/forms.py:150
msgid "Manager email" msgid "Manager email"
msgstr "" msgstr "דוא״ל המפקח"
#: mailu/ui/forms.py:155 #: mailu/ui/forms.py:155
msgid "Protocol" msgid "Protocol"
msgstr "" msgstr "פרוטוקול"
#: mailu/ui/forms.py:158 #: mailu/ui/forms.py:158
msgid "Hostname or IP" msgid "Hostname or IP"
msgstr "" msgstr "שם מארח או כתובת IP"
#: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20 #: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20
#: mailu/ui/templates/client.html:47 #: mailu/ui/templates/client.html:47
msgid "TCP port" msgid "TCP port"
msgstr "" msgstr "פתחת TCP"
#: mailu/ui/forms.py:160 #: mailu/ui/forms.py:160
msgid "Enable TLS" msgid "Enable TLS"
msgstr "" msgstr "הפעלת TLS"
#: mailu/ui/forms.py:161 mailu/ui/templates/client.html:28 #: mailu/ui/forms.py:161 mailu/ui/templates/client.html:28
#: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20 #: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20
msgid "Username" msgid "Username"
msgstr "" msgstr "שם משתמש"
#: mailu/ui/forms.py:163 #: mailu/ui/forms.py:163
msgid "Keep emails on the server" msgid "Keep emails on the server"
msgstr "" msgstr "להשאיר את ההודעות על השרת"
#: mailu/ui/forms.py:168 #: mailu/ui/forms.py:168
msgid "Announcement subject" msgid "Announcement subject"
msgstr "" msgstr "נושא ההכרזה"
#: mailu/ui/forms.py:170 #: mailu/ui/forms.py:170
msgid "Announcement body" msgid "Announcement body"
msgstr "" msgstr "גוף ההכרזה"
#: mailu/ui/forms.py:172 #: mailu/ui/forms.py:172
msgid "Send" msgid "Send"
msgstr "" msgstr "שליחה"
#: mailu/ui/templates/announcement.html:4 #: mailu/ui/templates/announcement.html:4
msgid "Public announcement" msgid "Public announcement"
msgstr "" msgstr "הכרזה פומבית"
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82 #: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
msgid "Client setup" msgid "Client setup"
msgstr "" msgstr "הגדרת לקוח"
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43 #: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
msgid "Mail protocol" msgid "Mail protocol"
msgstr "" msgstr "פרוטוקול דוא״ל"
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51 #: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
msgid "Server name" msgid "Server name"
msgstr "" msgstr "שם שרת"
#: mailu/ui/templates/confirm.html:4 #: mailu/ui/templates/confirm.html:4
msgid "Confirm action" msgid "Confirm action"
msgstr "" msgstr "אישור הפעולה"
#: mailu/ui/templates/confirm.html:13 #: mailu/ui/templates/confirm.html:13
#, python-format #, python-format
msgid "You are about to %(action)s. Please confirm your action." msgid "You are about to %(action)s. Please confirm your action."
msgstr "" msgstr "פעולה זו תבצע %(action)s. נא לאשר את הפעולה שלך."
#: mailu/ui/templates/docker-error.html:4 #: mailu/ui/templates/docker-error.html:4
msgid "Docker error" msgid "Docker error"
msgstr "" msgstr "שגיאת Docker"
#: mailu/ui/templates/docker-error.html:12 #: mailu/ui/templates/docker-error.html:12
msgid "An error occurred while talking to the Docker server." msgid "An error occurred while talking to the Docker server."
msgstr "" msgstr "אירעה שגיאה בעת החיבור לשרת ה־Docker."
#: mailu/ui/templates/login.html:8 #: mailu/ui/templates/login.html:8
msgid "to access the administration tools" msgid "to access the administration tools"
msgstr "" msgstr "כדי לגשת לכלי הניהול"
#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34 #: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34
msgid "Settings" msgid "Settings"
msgstr "" msgstr "הגדרות"
#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35 #: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35
msgid "Auto-reply" msgid "Auto-reply"
msgstr "" msgstr "מענה אוטומטית"
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26 #: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26
#: mailu/ui/templates/user/list.html:36 #: mailu/ui/templates/user/list.html:36
msgid "Fetched accounts" msgid "Fetched accounts"
msgstr "" msgstr "חשבונות נמשכים"
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4 #: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
msgid "Authentication tokens" msgid "Authentication tokens"
msgstr "" msgstr "אסימוני אימות"
#: mailu/ui/templates/sidebar.html:35 #: mailu/ui/templates/sidebar.html:35
msgid "Administration" msgid "Administration"
msgstr "" msgstr "ניהול"
#: mailu/ui/templates/sidebar.html:44 #: mailu/ui/templates/sidebar.html:44
msgid "Announcement" msgid "Announcement"
msgstr "" msgstr "הכרזה"
#: mailu/ui/templates/sidebar.html:49 #: mailu/ui/templates/sidebar.html:49
msgid "Administrators" msgid "Administrators"
msgstr "" msgstr "מנהלים"
#: mailu/ui/templates/sidebar.html:54 #: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains" msgid "Relayed domains"
msgstr "" msgstr "שמות תחום מועברים"
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15 #: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
msgid "Antispam" msgid "Antispam"
msgstr "" msgstr "מניעת ספאם"
#: mailu/ui/templates/sidebar.html:66 #: mailu/ui/templates/sidebar.html:66
msgid "Mail domains" msgid "Mail domains"
msgstr "" msgstr "דמות תחום לדוא״ל"
#: mailu/ui/templates/sidebar.html:72 #: mailu/ui/templates/sidebar.html:72
msgid "Go to" msgid "Go to"
msgstr "" msgstr "מעבר אל"
#: mailu/ui/templates/sidebar.html:76 #: mailu/ui/templates/sidebar.html:76
msgid "Webmail" msgid "Webmail"
msgstr "" msgstr "דוא״ל בדפדפן"
#: mailu/ui/templates/sidebar.html:87 #: mailu/ui/templates/sidebar.html:87
msgid "Website" msgid "Website"
msgstr "" msgstr "אתר"
#: mailu/ui/templates/sidebar.html:92 #: mailu/ui/templates/sidebar.html:92
msgid "Help" msgid "Help"
msgstr "" msgstr "עזרה"
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98 #: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
msgid "Register a domain" msgid "Register a domain"
msgstr "" msgstr "רישום שם תחום"
#: mailu/ui/templates/sidebar.html:105 #: mailu/ui/templates/sidebar.html:105
msgid "Sign out" msgid "Sign out"
msgstr "" msgstr "יציאה"
#: mailu/ui/templates/working.html:4 #: mailu/ui/templates/working.html:4
msgid "We are still working on this feature!" msgid "We are still working on this feature!"
msgstr "" msgstr "אנחנו עדיין עובדים על היכולת הזאת!"
#: mailu/ui/templates/admin/create.html:4 #: mailu/ui/templates/admin/create.html:4
msgid "Add a global administrator" msgid "Add a global administrator"
msgstr "" msgstr "הוספת מנהל כללי"
#: mailu/ui/templates/admin/list.html:4 #: mailu/ui/templates/admin/list.html:4
msgid "Global administrators" msgid "Global administrators"
msgstr "" msgstr "מנהלים כלליים"
#: mailu/ui/templates/admin/list.html:9 #: mailu/ui/templates/admin/list.html:9
msgid "Add administrator" msgid "Add administrator"
msgstr "" msgstr "הוספת מנהל"
#: mailu/ui/templates/admin/list.html:16 mailu/ui/templates/alias/list.html:18 #: mailu/ui/templates/admin/list.html:16 mailu/ui/templates/alias/list.html:18
#: mailu/ui/templates/alternative/list.html:18 #: mailu/ui/templates/alternative/list.html:18
@ -382,12 +383,12 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:16 mailu/ui/templates/token/list.html:18 #: mailu/ui/templates/relay/list.html:16 mailu/ui/templates/token/list.html:18
#: mailu/ui/templates/user/list.html:18 #: mailu/ui/templates/user/list.html:18
msgid "Actions" msgid "Actions"
msgstr "" msgstr "פעולות"
#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19 #: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19
#: mailu/ui/templates/manager/list.html:19 mailu/ui/templates/user/list.html:20 #: mailu/ui/templates/manager/list.html:19 mailu/ui/templates/user/list.html:20
msgid "Email" msgid "Email"
msgstr "" msgstr "דוא״ל"
#: mailu/ui/templates/admin/list.html:22 mailu/ui/templates/alias/list.html:29 #: mailu/ui/templates/admin/list.html:22 mailu/ui/templates/alias/list.html:29
#: mailu/ui/templates/alternative/list.html:25 #: mailu/ui/templates/alternative/list.html:25
@ -396,23 +397,23 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:27 mailu/ui/templates/token/list.html:26 #: mailu/ui/templates/relay/list.html:27 mailu/ui/templates/token/list.html:26
#: mailu/ui/templates/user/list.html:31 #: mailu/ui/templates/user/list.html:31
msgid "Delete" msgid "Delete"
msgstr "" msgstr "מחיקה"
#: mailu/ui/templates/alias/create.html:4 #: mailu/ui/templates/alias/create.html:4
msgid "Create alias" msgid "Create alias"
msgstr "" msgstr "יצירת כינוי"
#: mailu/ui/templates/alias/edit.html:4 #: mailu/ui/templates/alias/edit.html:4
msgid "Edit alias" msgid "Edit alias"
msgstr "" msgstr "עריכת כינוי"
#: mailu/ui/templates/alias/list.html:4 #: mailu/ui/templates/alias/list.html:4
msgid "Alias list" msgid "Alias list"
msgstr "" msgstr "רשימת כינויים"
#: mailu/ui/templates/alias/list.html:12 #: mailu/ui/templates/alias/list.html:12
msgid "Add alias" msgid "Add alias"
msgstr "" msgstr "הוספת כינוי"
#: mailu/ui/templates/alias/list.html:22 #: mailu/ui/templates/alias/list.html:22
#: mailu/ui/templates/alternative/list.html:20 #: mailu/ui/templates/alternative/list.html:20
@ -420,254 +421,259 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21 #: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21
#: mailu/ui/templates/user/list.html:24 #: mailu/ui/templates/user/list.html:24
msgid "Created" msgid "Created"
msgstr "" msgstr "נוצר"
#: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23 #: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23
#: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21 #: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21
#: mailu/ui/templates/user/list.html:25 #: mailu/ui/templates/user/list.html:25
msgid "Last edit" msgid "Last edit"
msgstr "" msgstr "עריכה אחרונה"
#: mailu/ui/templates/alias/list.html:28 mailu/ui/templates/domain/list.html:30 #: mailu/ui/templates/alias/list.html:28 mailu/ui/templates/domain/list.html:30
#: mailu/ui/templates/fetch/list.html:30 mailu/ui/templates/relay/list.html:26 #: mailu/ui/templates/fetch/list.html:30 mailu/ui/templates/relay/list.html:26
#: mailu/ui/templates/user/list.html:30 #: mailu/ui/templates/user/list.html:30
msgid "Edit" msgid "Edit"
msgstr "" msgstr "עריכה"
#: mailu/ui/templates/alternative/create.html:4 #: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain" msgid "Create alternative domain"
msgstr "" msgstr "יצירת שם תחום חלופי"
#: mailu/ui/templates/alternative/list.html:4 #: mailu/ui/templates/alternative/list.html:4
msgid "Alternative domain list" msgid "Alternative domain list"
msgstr "" msgstr "רשימת שמות תחום חלופיים"
#: mailu/ui/templates/alternative/list.html:12 #: mailu/ui/templates/alternative/list.html:12
msgid "Add alternative" msgid "Add alternative"
msgstr "" msgstr "הוספת חלופה"
#: mailu/ui/templates/alternative/list.html:19 #: mailu/ui/templates/alternative/list.html:19
msgid "Name" msgid "Name"
msgstr "" msgstr "שם"
#: mailu/ui/templates/domain/create.html:4 #: mailu/ui/templates/domain/create.html:4
#: mailu/ui/templates/domain/list.html:9 #: mailu/ui/templates/domain/list.html:9
msgid "New domain" msgid "New domain"
msgstr "" msgstr "שם תחום חדש"
#: mailu/ui/templates/domain/details.html:4 #: mailu/ui/templates/domain/details.html:4
msgid "Domain details" msgid "Domain details"
msgstr "" msgstr "פרטי שם התחום"
#: mailu/ui/templates/domain/details.html:15 #: mailu/ui/templates/domain/details.html:15
msgid "Regenerate keys" msgid "Regenerate keys"
msgstr "" msgstr "יצירת מפתחות מחדש"
#: mailu/ui/templates/domain/details.html:17 #: mailu/ui/templates/domain/details.html:17
msgid "Generate keys" msgid "Generate keys"
msgstr "" msgstr "יצירת מפתחות"
#: mailu/ui/templates/domain/details.html:31 #: mailu/ui/templates/domain/details.html:31
msgid "DNS MX entry" msgid "DNS MX entry"
msgstr "" msgstr "רשומת MX ב־DNS"
#: mailu/ui/templates/domain/details.html:35 #: mailu/ui/templates/domain/details.html:35
msgid "DNS SPF entries" msgid "DNS SPF entries"
msgstr "" msgstr "רשומות SPF ב־DNS"
#: mailu/ui/templates/domain/details.html:42 #: mailu/ui/templates/domain/details.html:42
msgid "DKIM public key" msgid "DKIM public key"
msgstr "" msgstr "מפתח DKIM ציבורי"
#: mailu/ui/templates/domain/details.html:46 #: mailu/ui/templates/domain/details.html:46
msgid "DNS DKIM entry" msgid "DNS DKIM entry"
msgstr "" msgstr "רשומת DKIM ב־DNS"
#: mailu/ui/templates/domain/details.html:50 #: mailu/ui/templates/domain/details.html:50
msgid "DNS DMARC entry" msgid "DNS DMARC entry"
msgstr "" msgstr "רשומת DMARC ב־DNS"
#: mailu/ui/templates/domain/edit.html:4 #: mailu/ui/templates/domain/edit.html:4
msgid "Edit domain" msgid "Edit domain"
msgstr "" msgstr "עריכת שם תחום"
#: mailu/ui/templates/domain/list.html:4 #: mailu/ui/templates/domain/list.html:4
msgid "Domain list" msgid "Domain list"
msgstr "" msgstr "רשימת שמות תחום"
#: mailu/ui/templates/domain/list.html:17 #: mailu/ui/templates/domain/list.html:17
msgid "Manage" msgid "Manage"
msgstr "" msgstr "ניהול"
#: mailu/ui/templates/domain/list.html:19 #: mailu/ui/templates/domain/list.html:19
msgid "Mailbox count" msgid "Mailbox count"
msgstr "" msgstr "כמות תיבות דוא״ל"
#: mailu/ui/templates/domain/list.html:20 #: mailu/ui/templates/domain/list.html:20
msgid "Alias count" msgid "Alias count"
msgstr "" msgstr "כמות כינויים"
#: mailu/ui/templates/domain/list.html:28 #: mailu/ui/templates/domain/list.html:28
msgid "Details" msgid "Details"
msgstr "" msgstr "פרטים"
#: mailu/ui/templates/domain/list.html:35 #: mailu/ui/templates/domain/list.html:35
msgid "Users" msgid "Users"
msgstr "" msgstr "משתמשים"
#: mailu/ui/templates/domain/list.html:36 #: mailu/ui/templates/domain/list.html:36
msgid "Aliases" msgid "Aliases"
msgstr "" msgstr "כינויים"
#: mailu/ui/templates/domain/list.html:37 #: mailu/ui/templates/domain/list.html:37
msgid "Managers" msgid "Managers"
msgstr "" msgstr "מפקחים"
#: mailu/ui/templates/domain/list.html:39 #: mailu/ui/templates/domain/list.html:39
msgid "Alternatives" msgid "Alternatives"
msgstr "" msgstr "חלופות"
#: mailu/ui/templates/domain/signup.html:13 #: mailu/ui/templates/domain/signup.html:13
msgid "" msgid ""
"In order to register a new domain, you must first setup the\n" "In order to register a new domain, you must first setup the\n"
" domain zone so that the domain <code>MX</code> points to this server" " domain zone so that the domain <code>MX</code> points to this server"
msgstr "" msgstr ""
"כדי לרשום שם תחום חדש, תחילה עליך להקים את אזור התחום\n"
" (domain zone) כדי שה־<code>MX</code> של שם התחום יפנה לשרת הזה"
#: mailu/ui/templates/domain/signup.html:18 #: mailu/ui/templates/domain/signup.html:18
msgid "" msgid ""
"If you do not know how to setup an <code>MX</code> record for your DNS " "If you do not know how to setup an <code>MX</code> record for your DNS "
"zone,\n" "zone,\n"
" please contact your DNS provider or administrator. Also, please wait " " please contact your DNS provider or administrator. Also, please wait a\n"
"a\n"
" couple minutes after the <code>MX</code> is set so the local server " " couple minutes after the <code>MX</code> is set so the local server "
"cache\n" "cache\n"
" expires." " expires."
msgstr "" msgstr ""
"אם לא ברור לך איך להקים רשומת <code>MX</code> עבור אזור ה־DNS שלך,\n"
" נא ליצור קשר עם ספק ה־ DNS או ההנהלה שלך. כמו כן, נא להמתין מספר דקות\n"
" לאחר הגדרת ה־<code>MX</code> כדי לאפשר לתוקף המטמון המקורי בשרת\n"
" לפוג."
#: mailu/ui/templates/fetch/create.html:4 #: mailu/ui/templates/fetch/create.html:4
msgid "Add a fetched account" msgid "Add a fetched account"
msgstr "" msgstr "הוספת חשבון נמשך"
#: mailu/ui/templates/fetch/edit.html:4 #: mailu/ui/templates/fetch/edit.html:4
msgid "Update a fetched account" msgid "Update a fetched account"
msgstr "" msgstr "עדכון חשבון שנמשך"
#: mailu/ui/templates/fetch/list.html:12 #: mailu/ui/templates/fetch/list.html:12
msgid "Add an account" msgid "Add an account"
msgstr "" msgstr "הוספת חשבון"
#: mailu/ui/templates/fetch/list.html:19 #: mailu/ui/templates/fetch/list.html:19
msgid "Endpoint" msgid "Endpoint"
msgstr "" msgstr "נקודת גישה"
#: mailu/ui/templates/fetch/list.html:21 #: mailu/ui/templates/fetch/list.html:21
msgid "Keep emails" msgid "Keep emails"
msgstr "" msgstr "לשמור על ההודעות"
#: mailu/ui/templates/fetch/list.html:22 #: mailu/ui/templates/fetch/list.html:22
msgid "Last check" msgid "Last check"
msgstr "" msgstr "בדיקה אחרונה"
#: mailu/ui/templates/fetch/list.html:35 #: mailu/ui/templates/fetch/list.html:35
msgid "yes" msgid "yes"
msgstr "" msgstr "כן"
#: mailu/ui/templates/fetch/list.html:35 #: mailu/ui/templates/fetch/list.html:35
msgid "no" msgid "no"
msgstr "" msgstr "לא"
#: mailu/ui/templates/manager/create.html:4 #: mailu/ui/templates/manager/create.html:4
msgid "Add a manager" msgid "Add a manager"
msgstr "" msgstr "הוספת מנהל"
#: mailu/ui/templates/manager/list.html:4 #: mailu/ui/templates/manager/list.html:4
msgid "Manager list" msgid "Manager list"
msgstr "" msgstr "רשימת מנהלים"
#: mailu/ui/templates/manager/list.html:12 #: mailu/ui/templates/manager/list.html:12
msgid "Add manager" msgid "Add manager"
msgstr "" msgstr "הוספת מנהל"
#: mailu/ui/templates/relay/create.html:4 #: mailu/ui/templates/relay/create.html:4
msgid "New relay domain" msgid "New relay domain"
msgstr "" msgstr "שם תחום מועבר"
#: mailu/ui/templates/relay/edit.html:4 #: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain" msgid "Edit relayd domain"
msgstr "" msgstr "עריכת שמות תחום מועברים"
#: mailu/ui/templates/relay/list.html:4 #: mailu/ui/templates/relay/list.html:4
msgid "Relayed domain list" msgid "Relayed domain list"
msgstr "" msgstr "רשימת שמות תחום מועברים"
#: mailu/ui/templates/relay/list.html:9 #: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain" msgid "New relayed domain"
msgstr "" msgstr "שם תחום מועבר חדש"
#: mailu/ui/templates/token/create.html:4 #: mailu/ui/templates/token/create.html:4
msgid "Create an authentication token" msgid "Create an authentication token"
msgstr "" msgstr "יצירת אסימון אימות"
#: mailu/ui/templates/token/list.html:12 #: mailu/ui/templates/token/list.html:12
msgid "New token" msgid "New token"
msgstr "" msgstr "אסימון חדש"
#: mailu/ui/templates/user/create.html:4 #: mailu/ui/templates/user/create.html:4
msgid "New user" msgid "New user"
msgstr "" msgstr "משתמש חדש"
#: mailu/ui/templates/user/create.html:15 #: mailu/ui/templates/user/create.html:15
msgid "General" msgid "General"
msgstr "" msgstr "כללי"
#: mailu/ui/templates/user/create.html:22 #: mailu/ui/templates/user/create.html:22
msgid "Features and quotas" msgid "Features and quotas"
msgstr "" msgstr "יכולות ומיכסות"
#: mailu/ui/templates/user/edit.html:4 #: mailu/ui/templates/user/edit.html:4
msgid "Edit user" msgid "Edit user"
msgstr "" msgstr "עריכת משתמש"
#: mailu/ui/templates/user/forward.html:4 #: mailu/ui/templates/user/forward.html:4
msgid "Forward emails" msgid "Forward emails"
msgstr "" msgstr "העברת הודעות"
#: mailu/ui/templates/user/list.html:4 #: mailu/ui/templates/user/list.html:4
msgid "User list" msgid "User list"
msgstr "" msgstr "רשימת משתמשים"
#: mailu/ui/templates/user/list.html:12 #: mailu/ui/templates/user/list.html:12
msgid "Add user" msgid "Add user"
msgstr "" msgstr "הוספת משתמש"
#: mailu/ui/templates/user/list.html:19 mailu/ui/templates/user/settings.html:4 #: mailu/ui/templates/user/list.html:19 mailu/ui/templates/user/settings.html:4
msgid "User settings" msgid "User settings"
msgstr "" msgstr "הגדרות משתמש"
#: mailu/ui/templates/user/list.html:21 #: mailu/ui/templates/user/list.html:21
msgid "Features" msgid "Features"
msgstr "" msgstr "יכולות"
#: mailu/ui/templates/user/password.html:4 #: mailu/ui/templates/user/password.html:4
msgid "Password update" msgid "Password update"
msgstr "" msgstr "עדכון סיסמה"
#: mailu/ui/templates/user/reply.html:4 #: mailu/ui/templates/user/reply.html:4
msgid "Automatic reply" msgid "Automatic reply"
msgstr "" msgstr "מענה אוטומטי"
#: mailu/ui/templates/user/settings.html:22 #: mailu/ui/templates/user/settings.html:22
msgid "Auto-forward" msgid "Auto-forward"
msgstr "" msgstr "העברה אוטומטית"
#: mailu/ui/templates/user/signup_domain.html:8 #: mailu/ui/templates/user/signup_domain.html:8
msgid "pick a domain for the new account" msgid "pick a domain for the new account"
msgstr "" msgstr "נא לבחור שם תחום לחשבון החדש"
#: mailu/ui/templates/user/signup_domain.html:14 #: mailu/ui/templates/user/signup_domain.html:14
msgid "Domain" msgid "Domain"
msgstr "" msgstr "שם תחום"
#: mailu/ui/templates/user/signup_domain.html:15 #: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots" msgid "Available slots"
msgstr "" msgstr "מקומות פנויים"

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mailu\n" "Project-Id-Version: Mailu\n"
"PO-Revision-Date: 2020-03-11 23:03+0000\n" "PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n" "Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n"
"Language-Team: Italian <https://translate.tedomum.net/projects/mailu/admin/" "Language-Team: Italian <https://translate.tedomum.net/projects/mailu/admin/"
"it/>\n" "it/>\n"
@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.11.2\n" "X-Generator: Weblate 4.0.1\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
msgid "Invalid email address." msgid "Invalid email address."

@ -528,7 +528,7 @@ msgstr "Alternatieve naam"
#: mailu/ui/forms.py:70 #: mailu/ui/forms.py:70
msgid "Relayed domain name" msgid "Relayed domain name"
msgstr "Relayed domainnaam" msgstr "Relayed domeinnaam"
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18 #: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
msgid "Remote host" msgid "Remote host"
@ -536,7 +536,7 @@ msgstr "Externe host"
#: mailu/ui/templates/sidebar.html:54 #: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains" msgid "Relayed domains"
msgstr "Relayed domainen" msgstr "Relayed domeinen"
#: mailu/ui/templates/alternative/create.html:4 #: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain" msgid "Create alternative domain"

@ -1,188 +1,287 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mailu\n" "Project-Id-Version: Mailu\n"
"POT-Creation-Date: 2021-02-05 16:34+0100\n"
"PO-Revision-Date: 2020-02-17 20:23+0000\n" "PO-Revision-Date: 2020-02-17 20:23+0000\n"
"Last-Translator: NeroPcStation <dareknowacki2001@gmail.com>\n" "Last-Translator: Marcin Siennicki <marcin@siennicki.eu>\n"
"Language-Team: Polish <https://translate.tedomum.net/projects/mailu/admin/pl/"
">\n"
"Language: pl\n" "Language: pl\n"
"Language-Team: Polish "
"<https://translate.tedomum.net/projects/mailu/admin/pl/>\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "Generated-By: Babel 2.9.0\n"
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 3.3\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:33 mailu/ui/forms.py:36
msgid "Invalid email address." msgid "Invalid email address."
msgstr "Nieprawidłowy adres e-mail." msgstr "Nieprawidłowy adres e-mail."
#: mailu/ui/forms.py:36 #: mailu/ui/forms.py:45
msgid "Confirm" msgid "Confirm"
msgstr "Zatwierdź" msgstr "Zatwierdź"
#: mailu/ui/forms.py:40 mailu/ui/forms.py:77 #: mailu/ui/forms.py:49 mailu/ui/forms.py:86
msgid "E-mail" msgid "E-mail"
msgstr "E-mail" msgstr "E-mail"
#: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90 #: mailu/ui/forms.py:50 mailu/ui/forms.py:87 mailu/ui/forms.py:100
#: mailu/ui/forms.py:109 mailu/ui/forms.py:162 #: mailu/ui/forms.py:118 mailu/ui/forms.py:172
#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:59 #: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:59
msgid "Password" msgid "Password"
msgstr "Hasło" msgstr "Hasło"
#: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4 #: mailu/ui/forms.py:51 mailu/ui/templates/login.html:4
#: mailu/ui/templates/sidebar.html:111 #: mailu/ui/templates/sidebar.html:108
msgid "Sign in" msgid "Sign in"
msgstr "Zaloguj" msgstr "Zaloguj"
#: mailu/ui/forms.py:46 mailu/ui/forms.py:56 #: mailu/ui/forms.py:55 mailu/ui/forms.py:65
#: mailu/ui/templates/domain/details.html:27 #: mailu/ui/templates/domain/details.html:27
#: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17 #: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17
msgid "Domain name" msgid "Domain name"
msgstr "Nazwa domeny" msgstr "Nazwa domeny"
#: mailu/ui/forms.py:47 #: mailu/ui/forms.py:56
msgid "Maximum user count" msgid "Maximum user count"
msgstr "Maksymalna liczba użytkowników" msgstr "Maksymalna liczba użytkowników"
#: mailu/ui/forms.py:48 #: mailu/ui/forms.py:57
msgid "Maximum alias count" msgid "Maximum alias count"
msgstr "Maksymalna liczba aliasów" msgstr "Maksymalna liczba aliasów"
#. Needs more context - is that a verb or a noun? #: mailu/ui/forms.py:58
#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83 msgid "Maximum user quota"
#: mailu/ui/forms.py:128 mailu/ui/forms.py:140 msgstr "Maksymalny przydział użytkownika"
#: mailu/ui/forms.py:59
msgid "Enable sign-up"
msgstr "Włącz rejestrację"
#: mailu/ui/forms.py:60 mailu/ui/forms.py:81 mailu/ui/forms.py:93
#: mailu/ui/forms.py:138 mailu/ui/forms.py:150
#: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21 #: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21
#: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19 #: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19
#: mailu/ui/templates/user/list.html:23 #: mailu/ui/templates/user/list.html:23
msgid "Comment" msgid "Comment"
msgstr "Komentarz" msgstr "Komentarz"
#: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66 #: mailu/ui/forms.py:61 mailu/ui/forms.py:75 mailu/ui/forms.py:82
#: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141 #: mailu/ui/forms.py:95 mailu/ui/forms.py:142 mailu/ui/forms.py:151
msgid "Create" msgid "Save"
msgstr "Utwórz" msgstr "Zapisz"
#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91 #: mailu/ui/forms.py:66
msgid "Initial admin"
msgstr "Początkowy administrator"
#: mailu/ui/forms.py:67
msgid "Admin password"
msgstr "Hasło administratora"
#: mailu/ui/forms.py:68 mailu/ui/forms.py:88 mailu/ui/forms.py:101
msgid "Confirm password" msgid "Confirm password"
msgstr "Potwierdź hasło" msgstr "Potwierdź hasło"
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22 #: mailu/ui/forms.py:70
msgid "Create"
msgstr "Utwórz"
#: mailu/ui/forms.py:74
msgid "Alternative name"
msgstr "Alternatywna nazwa"
#: mailu/ui/forms.py:79
msgid "Relayed domain name"
msgstr "Domeny przekierowywane"
#: mailu/ui/forms.py:80 mailu/ui/templates/relay/list.html:18
msgid "Remote host"
msgstr "Zdalny host"
#: mailu/ui/forms.py:89 mailu/ui/templates/user/list.html:22
#: mailu/ui/templates/user/signup_domain.html:16 #: mailu/ui/templates/user/signup_domain.html:16
msgid "Quota" msgid "Quota"
msgstr "Maksymalna przestrzeń na dysku" msgstr "Maksymalna przestrzeń na dysku"
#: mailu/ui/forms.py:81 #: mailu/ui/forms.py:90
msgid "Allow IMAP access" msgid "Allow IMAP access"
msgstr "Zezwalaj na dostęp przez protokół IMAP" msgstr "Zezwalaj na dostęp przez protokół IMAP"
#: mailu/ui/forms.py:82 #: mailu/ui/forms.py:91
msgid "Allow POP3 access" msgid "Allow POP3 access"
msgstr "Zezwalaj na dostęp przez protokół POP3" msgstr "Zezwalaj na dostęp przez protokół POP3"
#: mailu/ui/forms.py:85 #: mailu/ui/forms.py:92 mailu/ui/forms.py:108
msgid "Save" #: mailu/ui/templates/user/settings.html:15
msgstr "Zapisz"
#: mailu/ui/forms.py:97
msgid "Displayed name" msgid "Displayed name"
msgstr "Nazwa wyświetlana" msgstr "Nazwa wyświetlana"
#: mailu/ui/forms.py:98 #: mailu/ui/forms.py:94
msgid "Enabled"
msgstr "Włączone"
#: mailu/ui/forms.py:99
msgid "Email address"
msgstr "Adres e-mail"
#: mailu/ui/forms.py:102 mailu/ui/templates/sidebar.html:114
#: mailu/ui/templates/user/signup.html:4
#: mailu/ui/templates/user/signup_domain.html:4
msgid "Sign up"
msgstr "Utwórz konto"
#: mailu/ui/forms.py:109
msgid "Enable spam filter" msgid "Enable spam filter"
msgstr "Włącz filtr antyspamowy" msgstr "Włącz filtr antyspamowy"
#: mailu/ui/forms.py:80
msgid "Spam filter threshold"
msgstr "Próg filtra antyspamowego"
#: mailu/ui/forms.py:105
msgid "Save settings"
msgstr "Zapisz ustawienia"
#: mailu/ui/forms.py:110 #: mailu/ui/forms.py:110
msgid "Password check" msgid "Spam filter tolerance"
msgstr "" msgstr "Tolerancja filtra spamu"
#: mailu/ui/forms.py:111 mailu/ui/templates/sidebar.html:16 #: mailu/ui/forms.py:111
msgid "Update password"
msgstr "Zaktualizuj hasło"
#: mailu/ui/forms.py:100
msgid "Enable forwarding" msgid "Enable forwarding"
msgstr "Włącz przekierowanie poczty" msgstr "Włącz przekierowanie poczty"
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139 #: mailu/ui/forms.py:112
msgid "Keep a copy of the emails"
msgstr "Przechowuj kopię wiadomości"
#: mailu/ui/forms.py:113 mailu/ui/forms.py:149
#: mailu/ui/templates/alias/list.html:20 #: mailu/ui/templates/alias/list.html:20
msgid "Destination" msgid "Destination"
msgstr "Adres docelowy" msgstr "Adres docelowy"
#: mailu/ui/forms.py:120 #: mailu/ui/forms.py:114
msgid "Update" msgid "Save settings"
msgstr "Aktualizuj" msgstr "Zapisz ustawienia"
#: mailu/ui/forms.py:115 #: mailu/ui/forms.py:119
msgid "Password check"
msgstr "Powtórz hasło"
#: mailu/ui/forms.py:120 mailu/ui/templates/sidebar.html:16
msgid "Update password"
msgstr "Zaktualizuj hasło"
#: mailu/ui/forms.py:124
msgid "Enable automatic reply" msgid "Enable automatic reply"
msgstr "Włącz automatyczną odpowiedź" msgstr "Włącz automatyczną odpowiedź"
#: mailu/ui/forms.py:116 #: mailu/ui/forms.py:125
msgid "Reply subject" msgid "Reply subject"
msgstr "Temat odpowiedzi" msgstr "Temat odpowiedzi"
#: mailu/ui/forms.py:117 #: mailu/ui/forms.py:126
msgid "Reply body" msgid "Reply body"
msgstr "Treść odpowiedzi" msgstr "Treść odpowiedzi"
#: mailu/ui/forms.py:136 #: mailu/ui/forms.py:128
#, fuzzy
msgid "Start of vacation"
msgstr "Rozpoczęcie nieobecności"
#: mailu/ui/forms.py:129
msgid "End of vacation"
msgstr "Koniec nieobecności"
#: mailu/ui/forms.py:130
msgid "Update"
msgstr "Aktualizuj"
#: mailu/ui/forms.py:135
msgid "Your token (write it down, as it will never be displayed again)"
msgstr "Twój token (zapisz go, ponieważ nigdy więcej nie będzie wyświetlany)"
#: mailu/ui/forms.py:140 mailu/ui/templates/token/list.html:20
msgid "Authorized IP"
msgstr "Autoryzowany adres IP"
#: mailu/ui/forms.py:146
msgid "Alias" msgid "Alias"
msgstr "Alias" msgstr "Alias"
#: mailu/ui/forms.py:138 #: mailu/ui/forms.py:148
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
msgstr "Używaj składni SQL LIKE (np. do adresów catch-all)" msgstr "Używaj składni SQL LIKE (np. do adresów catch-all)"
#: mailu/ui/forms.py:145 #: mailu/ui/forms.py:155
msgid "Admin email" msgid "Admin email"
msgstr "E-mail administratora" msgstr "E-mail administratora"
#: mailu/ui/forms.py:146 mailu/ui/forms.py:151 mailu/ui/forms.py:164 #: mailu/ui/forms.py:156 mailu/ui/forms.py:161 mailu/ui/forms.py:174
msgid "Submit" msgid "Submit"
msgstr "Prześlij" msgstr "Prześlij"
#: mailu/ui/forms.py:150 #: mailu/ui/forms.py:160
msgid "Manager email" msgid "Manager email"
msgstr "E-mail menedżera" msgstr "E-mail menedżera"
#: mailu/ui/forms.py:155 #: mailu/ui/forms.py:165
msgid "Protocol" msgid "Protocol"
msgstr "Protokół" msgstr "Protokół"
#: mailu/ui/forms.py:158 #: mailu/ui/forms.py:168
msgid "Hostname or IP" msgid "Hostname or IP"
msgstr "Nazwa hosta lub adres IP" msgstr "Nazwa hosta lub adres IP"
#: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20 #: mailu/ui/forms.py:169 mailu/ui/templates/client.html:20
#: mailu/ui/templates/client.html:47 #: mailu/ui/templates/client.html:47
msgid "TCP port" msgid "TCP port"
msgstr "Port TCP" msgstr "Port TCP"
#: mailu/ui/forms.py:160 #: mailu/ui/forms.py:170
msgid "Enable TLS" msgid "Enable TLS"
msgstr "Włącz TLS" msgstr "Włącz TLS"
#: mailu/ui/forms.py:161 mailu/ui/templates/client.html:28 #: mailu/ui/forms.py:171 mailu/ui/templates/client.html:28
#: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20 #: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20
msgid "Username" msgid "Username"
msgstr "Nazwa użytkownika" msgstr "Nazwa użytkownika"
#: mailu/ui/forms.py:173
msgid "Keep emails on the server"
msgstr "Przechowuj wiadomości na serwerze"
#: mailu/ui/forms.py:178
msgid "Announcement subject"
msgstr "Temat ogłoszenia"
#: mailu/ui/forms.py:180
msgid "Announcement body"
msgstr "Treść ogłoszenia"
#: mailu/ui/forms.py:182
msgid "Send"
msgstr "Wyślij"
#: mailu/ui/templates/announcement.html:4
msgid "Public announcement"
msgstr "Publiczne ogłoszenie"
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:79
msgid "Client setup"
msgstr "Konfiguracja klienta"
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
msgid "Mail protocol"
msgstr "Protokół poczty"
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
msgid "Server name"
msgstr "Nazwa serwera"
#: mailu/ui/templates/confirm.html:4 #: mailu/ui/templates/confirm.html:4
msgid "Confirm action" msgid "Confirm action"
msgstr "Potwierdź wykonanie czynności" msgstr "Potwierdź wykonanie czynności"
#: mailu/ui/templates/confirm.html:13 #: mailu/ui/templates/confirm.html:13
#, python-format
msgid "You are about to %(action)s. Please confirm your action." msgid "You are about to %(action)s. Please confirm your action."
msgstr "Zamierzasz wykonać następujące czynności: %(action)s. Potwierdź wykonanie czynności." msgstr ""
"Zamierzasz wykonać następujące czynności: %(action)s. Potwierdź wykonanie"
" czynności."
#: mailu/ui/templates/docker-error.html:4 #: mailu/ui/templates/docker-error.html:4
msgid "Docker error" msgid "Docker error"
@ -192,54 +291,19 @@ msgstr "Błąd Dockera"
msgid "An error occurred while talking to the Docker server." msgid "An error occurred while talking to the Docker server."
msgstr "Wystąpił błąd komunikacji z serwerem Dockera." msgstr "Wystąpił błąd komunikacji z serwerem Dockera."
#: mailu/admin/templates/login.html:6
msgid "Your account"
msgstr "Twoje konto"
#: mailu/ui/templates/login.html:8 #: mailu/ui/templates/login.html:8
msgid "to access the administration tools" msgid "to access the administration tools"
msgstr "aby uzyskać dostęp do narzędzi administracyjnych" msgstr "aby uzyskać dostęp do narzędzi administracyjnych"
#: mailu/ui/templates/services.html:4 mailu/ui/templates/sidebar.html:39
msgid "Services status"
msgstr "Status usług"
#: mailu/ui/templates/services.html:10
msgid "Service"
msgstr "Usługa"
#: mailu/ui/templates/fetch/list.html:23 mailu/ui/templates/services.html:11
msgid "Status"
msgstr "Status"
#: mailu/ui/templates/services.html:12
msgid "PID"
msgstr "PID"
#: mailu/ui/templates/services.html:13
msgid "Image"
msgstr "Obraz"
#: mailu/ui/templates/services.html:14
msgid "Started"
msgstr ""
#: mailu/ui/templates/services.html:15
msgid "Last update"
msgstr "Ostatnia aktualizacja"
#: mailu/ui/templates/sidebar.html:8 #: mailu/ui/templates/sidebar.html:8
#, fuzzy
msgid "My account" msgid "My account"
msgstr "Moje konto" msgstr "Dodaj konto"
#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34 #: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34
msgid "Settings" msgid "Settings"
msgstr "Ustawienia" msgstr "Ustawienia"
#: mailu/ui/templates/user/settings.html:22
msgid "Auto-forward"
msgstr "Automatyczne przekierowanie"
#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35 #: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35
msgid "Auto-reply" msgid "Auto-reply"
msgstr "Automatyczna odpowiedź" msgstr "Automatyczna odpowiedź"
@ -247,28 +311,60 @@ msgstr "Automatyczna odpowiedź"
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26 #: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26
#: mailu/ui/templates/user/list.html:36 #: mailu/ui/templates/user/list.html:36
msgid "Fetched accounts" msgid "Fetched accounts"
msgstr "" msgstr "Zewnętrzne konta e-mail"
#: mailu/ui/templates/sidebar.html:105 #: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
msgid "Sign out" msgid "Authentication tokens"
msgstr "Wyloguj" msgstr "Tokeny uwierzytelnienia"
#: mailu/ui/templates/sidebar.html:35 #: mailu/ui/templates/sidebar.html:36
msgid "Administration" msgid "Administration"
msgstr "Administracja" msgstr "Administracja"
#: mailu/ui/templates/sidebar.html:49 #: mailu/ui/templates/sidebar.html:41
msgid "Announcement"
msgstr "Ogłoszenie"
#: mailu/ui/templates/sidebar.html:46
msgid "Administrators" msgid "Administrators"
msgstr "Administratorzy" msgstr "Administratorzy"
#: mailu/ui/templates/sidebar.html:66 #: mailu/ui/templates/sidebar.html:51
msgid "Relayed domains"
msgstr "Domeny przekierowywane"
#: mailu/ui/templates/sidebar.html:56 mailu/ui/templates/user/settings.html:19
msgid "Antispam"
msgstr "Filtr antyspamowy"
#: mailu/ui/templates/sidebar.html:63
msgid "Mail domains" msgid "Mail domains"
msgstr "Domeny pocztowe" msgstr "Domeny pocztowe"
#: mailu/ui/templates/sidebar.html:92 #: mailu/ui/templates/sidebar.html:69
msgid "Go to"
msgstr "Przejdź do"
#: mailu/ui/templates/sidebar.html:73
msgid "Webmail"
msgstr "Twoja poczta"
#: mailu/ui/templates/sidebar.html:84
msgid "Website"
msgstr "Strona internetowa"
#: mailu/ui/templates/sidebar.html:89
msgid "Help" msgid "Help"
msgstr "Pomoc" msgstr "Pomoc"
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:95
msgid "Register a domain"
msgstr "Zarejestruj domenę"
#: mailu/ui/templates/sidebar.html:102
msgid "Sign out"
msgstr "Wyloguj"
#: mailu/ui/templates/working.html:4 #: mailu/ui/templates/working.html:4
msgid "We are still working on this feature!" msgid "We are still working on this feature!"
msgstr "Nadal pracujemy nad tą funkcją!" msgstr "Nadal pracujemy nad tą funkcją!"
@ -344,6 +440,22 @@ msgstr "Ostatnia edycja"
msgid "Edit" msgid "Edit"
msgstr "Edytuj" msgstr "Edytuj"
#: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain"
msgstr "Utwórz alternatywną domenę"
#: mailu/ui/templates/alternative/list.html:4
msgid "Alternative domain list"
msgstr "Alternatywna lista domen"
#: mailu/ui/templates/alternative/list.html:12
msgid "Add alternative"
msgstr "Dodaj alternatywę"
#: mailu/ui/templates/alternative/list.html:19
msgid "Name"
msgstr "Nazwa"
#: mailu/ui/templates/domain/create.html:4 #: mailu/ui/templates/domain/create.html:4
#: mailu/ui/templates/domain/list.html:9 #: mailu/ui/templates/domain/list.html:9
msgid "New domain" msgid "New domain"
@ -357,6 +469,10 @@ msgstr "Szczegóły domeny"
msgid "Regenerate keys" msgid "Regenerate keys"
msgstr "Wygeneruj ponownie klucze" msgstr "Wygeneruj ponownie klucze"
#: mailu/ui/templates/domain/details.html:17
msgid "Generate keys"
msgstr "Wygeneruj klucze"
#: mailu/ui/templates/domain/details.html:31 #: mailu/ui/templates/domain/details.html:31
msgid "DNS MX entry" msgid "DNS MX entry"
msgstr "Wpis MX DNS" msgstr "Wpis MX DNS"
@ -365,15 +481,15 @@ msgstr "Wpis MX DNS"
msgid "DNS SPF entries" msgid "DNS SPF entries"
msgstr "Wpisy SPF DNS" msgstr "Wpisy SPF DNS"
#: mailu/ui/templates/domain/details.html:42 #: mailu/ui/templates/domain/details.html:41
msgid "DKIM public key" msgid "DKIM public key"
msgstr "Publiczny klucz DKIM" msgstr "Publiczny klucz DKIM"
#: mailu/ui/templates/domain/details.html:46 #: mailu/ui/templates/domain/details.html:45
msgid "DNS DKIM entry" msgid "DNS DKIM entry"
msgstr "Wpis DKIM DNS" msgstr "Wpis DKIM DNS"
#: mailu/ui/templates/domain/details.html:50 #: mailu/ui/templates/domain/details.html:49
msgid "DNS DMARC entry" msgid "DNS DMARC entry"
msgstr "Wpis DMARC DNS" msgstr "Wpis DMARC DNS"
@ -413,13 +529,42 @@ msgstr "Aliasy"
msgid "Managers" msgid "Managers"
msgstr "Menedżerowie" msgstr "Menedżerowie"
#: mailu/ui/templates/domain/list.html:39
msgid "Alternatives"
msgstr "Alternatywy"
#: mailu/ui/templates/domain/signup.html:13
msgid ""
"In order to register a new domain, you must first setup the\n"
" domain zone so that the domain <code>MX</code> points to this server"
msgstr ""
"Aby zarejestrować nową domenę, musisz najpierw skonfigurować strefę "
"domeny, aby domena <code> MX </code> wskazywała na ten serwer"
#: mailu/ui/templates/domain/signup.html:18
msgid ""
"If you do not know how to setup an <code>MX</code> record for your DNS "
"zone,\n"
" please contact your DNS provider or administrator. Also, please wait "
"a\n"
" couple minutes after the <code>MX</code> is set so the local server "
"cache\n"
" expires."
msgstr ""
"Jeśli nie wiesz, jak skonfigurować rekord <code> MX </code> dla swojej "
"strefy DNS,\n"
"skontaktuj się z dostawcą DNS lub administratorem. Proszę również "
"poczekać\n"
"kilka minut po ustawieniu <code> MX </code>, żeby pamięć podręczna "
"serwera lokalnego wygasła."
#: mailu/ui/templates/fetch/create.html:4 #: mailu/ui/templates/fetch/create.html:4
msgid "Add a fetched account" msgid "Add a fetched account"
msgstr "" msgstr "Dodaj zewnętrzne konto pocztowe"
#: mailu/ui/templates/fetch/edit.html:4 #: mailu/ui/templates/fetch/edit.html:4
msgid "Update a fetched account" msgid "Update a fetched account"
msgstr "" msgstr "Zaktualizuj konto"
#: mailu/ui/templates/fetch/list.html:12 #: mailu/ui/templates/fetch/list.html:12
msgid "Add an account" msgid "Add an account"
@ -427,12 +572,28 @@ msgstr "Dodaj konto"
#: mailu/ui/templates/fetch/list.html:19 #: mailu/ui/templates/fetch/list.html:19
msgid "Endpoint" msgid "Endpoint"
msgstr "" msgstr "Serwer"
#: mailu/ui/templates/fetch/list.html:21
msgid "Keep emails"
msgstr "Przechowuj wiadomości"
#: mailu/ui/templates/fetch/list.html:22 #: mailu/ui/templates/fetch/list.html:22
msgid "Last check" msgid "Last check"
msgstr "Ostatnie sprawdzenie" msgstr "Ostatnie sprawdzenie"
#: mailu/ui/templates/fetch/list.html:23
msgid "Status"
msgstr "Stan"
#: mailu/ui/templates/fetch/list.html:35
msgid "yes"
msgstr "Tak"
#: mailu/ui/templates/fetch/list.html:35
msgid "no"
msgstr "Nie"
#: mailu/ui/templates/manager/create.html:4 #: mailu/ui/templates/manager/create.html:4
msgid "Add a manager" msgid "Add a manager"
msgstr "Dodaj menedżera" msgstr "Dodaj menedżera"
@ -445,34 +606,43 @@ msgstr "Lista menedżerów"
msgid "Add manager" msgid "Add manager"
msgstr "Dodaj menedżera" msgstr "Dodaj menedżera"
#: mailu/ui/forms.py:168 #: mailu/ui/templates/relay/create.html:4
msgid "Announcement subject" msgid "New relay domain"
msgstr "Temat ogłoszenia" msgstr "Nowa domena do przekierowania"
#: mailu/ui/forms.py:170 #: mailu/ui/templates/relay/edit.html:4
msgid "Announcement body" #, fuzzy
msgstr "Treść ogłoszenia" msgid "Edit relayd domain"
msgstr "Edycja domeny"
#: mailu/ui/forms.py:172 #: mailu/ui/templates/relay/list.html:4
msgid "Send" msgid "Relayed domain list"
msgstr "Wyślij" msgstr "Lista domen przekierowywanych"
#: mailu/ui/templates/announcement.html:4 #: mailu/ui/templates/relay/list.html:9
msgid "Public announcement" msgid "New relayed domain"
msgstr "Publiczne ogłoszenie" msgstr "Nowa domena do przekierowania"
#: mailu/ui/templates/announcement.html:8 #: mailu/ui/templates/token/create.html:4
msgid "from" msgid "Create an authentication token"
msgstr "od" msgstr "Utwórz token uwierzytelniający"
#: mailu/ui/templates/sidebar.html:44 #: mailu/ui/templates/token/list.html:12
msgid "Announcement" msgid "New token"
msgstr "Ogłoszenie" msgstr "Nowy token"
#: mailu/ui/templates/user/create.html:4 #: mailu/ui/templates/user/create.html:4
msgid "New user" msgid "New user"
msgstr "Nowy użytkownik" msgstr "Nowy użytkownik"
#: mailu/ui/templates/user/create.html:15
msgid "General"
msgstr "Ogólne"
#: mailu/ui/templates/user/create.html:23
msgid "Features and quotas"
msgstr "Funkcje i limity"
#: mailu/ui/templates/user/edit.html:4 #: mailu/ui/templates/user/edit.html:4
msgid "Edit user" msgid "Edit user"
msgstr "Edytuj użytkownika" msgstr "Edytuj użytkownika"
@ -505,202 +675,9 @@ msgstr "Zmiana hasła"
msgid "Automatic reply" msgid "Automatic reply"
msgstr "Automatyczna odpowiedź" msgstr "Automatyczna odpowiedź"
#: mailu/ui/forms.py:49 #: mailu/ui/templates/user/settings.html:26
msgid "Maximum user quota" msgid "Auto-forward"
msgstr "Maksymalny przydział użytkownika" msgstr "Automatyczne przekierowanie"
#: mailu/ui/forms.py:101
msgid "Keep a copy of the emails"
msgstr "Przechowuj kopię wiadomości"
#: mailu/ui/forms.py:163
msgid "Keep emails on the server"
msgstr "Przechowuj wiadomości na serwerze"
#: mailu/ui/templates/fetch/list.html:21
msgid "Keep emails"
msgstr "Przechowuj wiadomości"
#: mailu/ui/templates/fetch/list.html:35
msgid "yes"
msgstr "Tak"
#: mailu/ui/templates/fetch/list.html:35
msgid "no"
msgstr "Nie"
#: mailu/ui/forms.py:65
msgid "Alternative name"
msgstr "Alternatywna nazwa"
#: mailu/ui/forms.py:70
msgid "Relayed domain name"
msgstr ""
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
msgid "Remote host"
msgstr "Zdalny host"
#: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains"
msgstr ""
#: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain"
msgstr "Utwórz alternatywną domenę"
#: mailu/ui/templates/alternative/list.html:4
msgid "Alternative domain list"
msgstr "Alternatywna lista domen"
#: mailu/ui/templates/alternative/list.html:12
msgid "Add alternative"
msgstr "Dodaj alternatywę"
#: mailu/ui/templates/alternative/list.html:19
msgid "Name"
msgstr "Nazwa"
#: mailu/ui/templates/domain/list.html:39
msgid "Alternatives"
msgstr "Alternatywy"
#: mailu/ui/templates/relay/create.html:4
msgid "New relay domain"
msgstr ""
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgstr ""
#: mailu/ui/templates/relay/list.html:4
msgid "Relayed domain list"
msgstr ""
#: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain"
msgstr ""
#: mailu/ui/forms.py:125
msgid "Your token (write it down, as it will never be displayed again)"
msgstr "Twój token (zapisz go, ponieważ nigdy więcej nie będzie wyświetlany)"
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
msgid "Authorized IP"
msgstr "Autoryzowany adres IP"
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
msgid "Authentication tokens"
msgstr "Tokeny uwierzytelnienia"
#: mailu/ui/templates/sidebar.html:72
msgid "Go to"
msgstr "Przejdź do"
#: mailu/ui/templates/sidebar.html:76
msgid "Webmail"
msgstr ""
#: mailu/ui/templates/sidebar.html:87
msgid "Website"
msgstr "Strona internetowa"
#: mailu/ui/templates/token/create.html:4
msgid "Create an authentication token"
msgstr "Utwórz token uwierzytelniający"
#: mailu/ui/templates/token/list.html:12
msgid "New token"
msgstr "Nowy token"
#: mailu/ui/templates/user/create.html:15
msgid "General"
msgstr ""
#: mailu/ui/templates/user/create.html:22
msgid "Features and quotas"
msgstr ""
#: mailu/ui/templates/user/settings.html:14
msgid "General settings"
msgstr "Ustawienia ogólne"
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
msgid "Antispam"
msgstr "Filtr antyspamowy"
#: mailu/ui/forms.py:99
msgid "Spam filter tolerance"
msgstr "Tolerancja filtra spamu"
#: mailu/ui/forms.py:50
msgid "Enable sign-up"
msgstr "Włącz rejestrację"
#: mailu/ui/forms.py:57
msgid "Initial admin"
msgstr "Początkowy administrator"
#: mailu/ui/forms.py:58
msgid "Admin password"
msgstr "hasło administratora"
#: mailu/ui/forms.py:84
msgid "Enabled"
msgstr "Włączone"
#: mailu/ui/forms.py:89
msgid "Email address"
msgstr "Adres e-mail"
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
#: mailu/ui/templates/user/signup.html:4
#: mailu/ui/templates/user/signup_domain.html:4
msgid "Sign up"
msgstr ""
#: mailu/ui/forms.py:119
msgid "End of vacation"
msgstr "Koniec wakacji"
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
msgid "Client setup"
msgstr "Konfiguracja klienta"
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
msgid "Mail protocol"
msgstr "Protokół poczty"
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
msgid "Server name"
msgstr "Nazwa serwera"
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
msgid "Register a domain"
msgstr "Zarejestruj domenę"
#: mailu/ui/templates/domain/details.html:17
msgid "Generate keys"
msgstr "Wygeneruj klucze"
#: mailu/ui/templates/domain/signup.html:13
msgid "In order to register a new domain, you must first setup the\n"
" domain zone so that the domain <code>MX</code> points to this server"
msgstr ""
"Aby zarejestrować nową domenę, musisz najpierw skonfigurować strefę domeny, "
"aby domena <code> MX </code> wskazywała na ten serwer"
#: mailu/ui/templates/domain/signup.html:18
msgid "If you do not know how to setup an <code>MX</code> record for your DNS zone,\n"
" please contact your DNS provider or administrator. Also, please wait a\n"
" couple minutes after the <code>MX</code> is set so the local server cache\n"
" expires."
msgstr ""
"Jeśli nie wiesz, jak skonfigurować rekord <code> MX </code> dla swojej "
"strefy DNS,\n"
"skontaktuj się z dostawcą DNS lub administratorem. Proszę również poczekać\n"
"kilka minut po ustawieniu <code> MX </code> , żeby pamięć podręczna serwera "
"lokalnego wygasła."
#: mailu/ui/templates/user/signup_domain.html:8 #: mailu/ui/templates/user/signup_domain.html:8
msgid "pick a domain for the new account" msgid "pick a domain for the new account"
@ -713,3 +690,40 @@ msgstr "Domena"
#: mailu/ui/templates/user/signup_domain.html:15 #: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots" msgid "Available slots"
msgstr "Dostępne miejsca" msgstr "Dostępne miejsca"
#~ msgid "Spam filter threshold"
#~ msgstr "Próg filtra antyspamowego"
#~ msgid "Your account"
#~ msgstr "Twoje konto"
#~ msgid "Services status"
#~ msgstr "Status usług"
#~ msgid "Service"
#~ msgstr "Usługa"
#~ msgid "Status"
#~ msgstr "Status"
#~ msgid "PID"
#~ msgstr "PID"
#~ msgid "Image"
#~ msgstr "Obraz"
#~ msgid "Started"
#~ msgstr "Uruchomione"
#~ msgid "Last update"
#~ msgstr "Ostatnia aktualizacja"
#~ msgid "My account"
#~ msgstr "Moje konto"
#~ msgid "from"
#~ msgstr "od"
#~ msgid "General settings"
#~ msgstr "Ustawienia ogólne"

@ -1,11 +1,16 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mailu\n"
"PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n"
"Language-Team: Portuguese <https://translate.tedomum.net/projects/mailu/"
"admin/pt/>\n"
"Language: pt\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"Project-Id-Version: Mailu\n" "X-Generator: Weblate 4.0.1\n"
"Language: pt\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
msgid "Invalid email address." msgid "Invalid email address."
@ -183,7 +188,7 @@ msgstr "Erro no docker"
#: mailu/ui/templates/docker-error.html:12 #: mailu/ui/templates/docker-error.html:12
msgid "An error occurred while talking to the Docker server." msgid "An error occurred while talking to the Docker server."
msgstr "Um erro foi encontrado na conexão com o servidor Docker" msgstr "Um erro foi encontrado na conexão com o servidor Docker."
#: mailu/admin/templates/login.html:6 #: mailu/admin/templates/login.html:6
msgid "Your account" msgid "Your account"
@ -700,4 +705,3 @@ msgstr "Domínio"
#: mailu/ui/templates/user/signup_domain.html:15 #: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots" msgid "Available slots"
msgstr "Slots disponíveis" msgstr "Slots disponíveis"

@ -1,8 +1,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mailu\n" "Project-Id-Version: Mailu\n"
"PO-Revision-Date: 2019-07-22 06:23+0000\n" "PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: kaiyou <pierre@jaury.eu>\n" "Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n"
"Language-Team: Russian <https://translate.tedomum.net/projects/mailu/admin/" "Language-Team: Russian <https://translate.tedomum.net/projects/mailu/admin/"
"ru/>\n" "ru/>\n"
"Language: ru\n" "Language: ru\n"
@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<="
"4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 3.3\n" "X-Generator: Weblate 4.0.1\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
msgid "Invalid email address." msgid "Invalid email address."
@ -189,7 +189,7 @@ msgstr "Ошибка Docker"
#: mailu/ui/templates/docker-error.html:12 #: mailu/ui/templates/docker-error.html:12
msgid "An error occurred while talking to the Docker server." msgid "An error occurred while talking to the Docker server."
msgstr "Произошла ошибка при обращении к серверу Docker" msgstr "Произошла ошибка при обращении к серверу Docker."
#: mailu/admin/templates/login.html:6 #: mailu/admin/templates/login.html:6
msgid "Your account" msgid "Your account"

@ -1,11 +1,16 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mailu\n"
"PO-Revision-Date: 2021-03-04 18:46+0000\n"
"Last-Translator: Jaume Barber <jaumebarber@gmail.com>\n"
"Language-Team: Swedish <https://translate.tedomum.net/projects/mailu/admin/"
"sv/>\n"
"Language: sv\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"Project-Id-Version: Mailu\n" "X-Generator: Weblate 4.0.1\n"
"Language: sk\n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
msgid "Invalid email address." msgid "Invalid email address."
@ -183,7 +188,7 @@ msgstr "Docker fel"
#: mailu/ui/templates/docker-error.html:12 #: mailu/ui/templates/docker-error.html:12
msgid "An error occurred while talking to the Docker server." msgid "An error occurred while talking to the Docker server."
msgstr "Ett fel inträffade vid kommunikation med Docker" msgstr "Ett fel inträffade vid kommunikation med Docker."
#: mailu/admin/templates/login.html:6 #: mailu/admin/templates/login.html:6
msgid "Your account" msgid "Your account"
@ -699,4 +704,3 @@ msgstr ""
#: mailu/ui/templates/user/signup_domain.html:15 #: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots" msgid "Available slots"
msgstr "" msgstr ""

@ -3,9 +3,11 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n" "X-Generator: Poedit 1.5.7\n"
"Project-Id-Version: Mailu\n" "Project-Id-Version: Mailu\n"
"Language: zh-CN\n" "Language: zh\n"
"Last-Translator: Chris Chuan <Chris.chuan@gmail.com>\n"
"Language-Team: \n"
#: mailu/ui/forms.py:32 #: mailu/ui/forms.py:32
msgid "Invalid email address." msgid "Invalid email address."
@ -28,7 +30,7 @@ msgstr "密码"
#: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4 #: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4
#: mailu/ui/templates/sidebar.html:111 #: mailu/ui/templates/sidebar.html:111
msgid "Sign in" msgid "Sign in"
msgstr "注册" msgstr "登录"
#: mailu/ui/forms.py:46 mailu/ui/forms.py:56 #: mailu/ui/forms.py:46 mailu/ui/forms.py:56
#: mailu/ui/templates/domain/details.html:27 #: mailu/ui/templates/domain/details.html:27
@ -44,6 +46,14 @@ msgstr "最大用户数"
msgid "Maximum alias count" msgid "Maximum alias count"
msgstr "最大别名数" msgstr "最大别名数"
#: mailu/ui/forms.py:49
msgid "Maximum user quota"
msgstr "最大用户配额"
#: mailu/ui/forms.py:50
msgid "Enable sign-up"
msgstr "启用注册"
#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83 #: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83
#: mailu/ui/forms.py:128 mailu/ui/forms.py:140 #: mailu/ui/forms.py:128 mailu/ui/forms.py:140
#: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21 #: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21
@ -57,10 +67,30 @@ msgstr "说明"
msgid "Create" msgid "Create"
msgstr "创建" msgstr "创建"
#: mailu/ui/forms.py:57
msgid "Initial admin"
msgstr "初始管理员"
#: mailu/ui/forms.py:58
msgid "Admin password"
msgstr "管理员密码"
#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91 #: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91
msgid "Confirm password" msgid "Confirm password"
msgstr "确认密码" msgstr "确认密码"
#: mailu/ui/forms.py:65
msgid "Alternative name"
msgstr "备用名称"
#: mailu/ui/forms.py:70
msgid "Relayed domain name"
msgstr "中继域域名"
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
msgid "Remote host"
msgstr "远程主机"
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22 #: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22
#: mailu/ui/templates/user/signup_domain.html:16 #: mailu/ui/templates/user/signup_domain.html:16
msgid "Quota" msgid "Quota"
@ -74,10 +104,24 @@ msgstr "允许IMAP访问"
msgid "Allow POP3 access" msgid "Allow POP3 access"
msgstr "允许POP3访问" msgstr "允许POP3访问"
#: mailu/ui/forms.py:84
msgid "Enabled"
msgstr "启用"
#: mailu/ui/forms.py:85 #: mailu/ui/forms.py:85
msgid "Save" msgid "Save"
msgstr "保存" msgstr "保存"
#: mailu/ui/forms.py:89
msgid "Email address"
msgstr "邮件地址"
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
#: mailu/ui/templates/user/signup.html:4
#: mailu/ui/templates/user/signup_domain.html:4
msgid "Sign up"
msgstr "注册"
#: mailu/ui/forms.py:97 #: mailu/ui/forms.py:97
msgid "Displayed name" msgid "Displayed name"
msgstr "显示名称" msgstr "显示名称"
@ -86,10 +130,23 @@ msgstr "显示名称"
msgid "Enable spam filter" msgid "Enable spam filter"
msgstr "启用垃圾邮件过滤" msgstr "启用垃圾邮件过滤"
#: mailu/ui/forms.py:80 #: mailu/ui/forms.py:99
msgid "Spam filter threshold" msgid "Spam filter tolerance"
msgstr "垃圾邮件过滤器阈值" msgstr "垃圾邮件过滤器阈值"
#: mailu/ui/forms.py:100
msgid "Enable forwarding"
msgstr "启用转发"
#: mailu/ui/forms.py:101
msgid "Keep a copy of the emails"
msgstr "保留电子邮件副本"
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139
#: mailu/ui/templates/alias/list.html:20
msgid "Destination"
msgstr "目的地址"
#: mailu/ui/forms.py:105 #: mailu/ui/forms.py:105
msgid "Save settings" msgid "Save settings"
msgstr "保存设置" msgstr "保存设置"
@ -102,19 +159,6 @@ msgstr "检查密码"
msgid "Update password" msgid "Update password"
msgstr "更新密码" msgstr "更新密码"
#: mailu/ui/forms.py:100
msgid "Enable forwarding"
msgstr "启用转发"
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139
#: mailu/ui/templates/alias/list.html:20
msgid "Destination"
msgstr "目的地址"
#: mailu/ui/forms.py:120
msgid "Update"
msgstr "更新"
#: mailu/ui/forms.py:115 #: mailu/ui/forms.py:115
msgid "Enable automatic reply" msgid "Enable automatic reply"
msgstr "启用自动回复" msgstr "启用自动回复"
@ -127,6 +171,22 @@ msgstr "回复主题"
msgid "Reply body" msgid "Reply body"
msgstr "回复正文" msgstr "回复正文"
#: mailu/ui/forms.py:119
msgid "End of vacation"
msgstr "假期结束"
#: mailu/ui/forms.py:120
msgid "Update"
msgstr "更新"
#: mailu/ui/forms.py:125
msgid "Your token (write it down, as it will never be displayed again)"
msgstr "您的令牌(请记录,它只显示这一次)"
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
msgid "Authorized IP"
msgstr "授权IP"
#: mailu/ui/forms.py:136 #: mailu/ui/forms.py:136
msgid "Alias" msgid "Alias"
msgstr "别名" msgstr "别名"
@ -169,11 +229,44 @@ msgstr "启用TLS"
msgid "Username" msgid "Username"
msgstr "用户名" msgstr "用户名"
#: mailu/ui/forms.py:163
msgid "Keep emails on the server"
msgstr "在服务器上保留电子邮件"
#: mailu/ui/forms.py:168
msgid "Announcement subject"
msgstr "公告主题"
#: mailu/ui/forms.py:170
msgid "Announcement body"
msgstr "公告正文"
#: mailu/ui/forms.py:172
msgid "Send"
msgstr "发送"
#: mailu/ui/templates/announcement.html:4
msgid "Public announcement"
msgstr "公开公告"
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
msgid "Client setup"
msgstr "客户端设置"
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
msgid "Mail protocol"
msgstr "邮件协议"
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
msgid "Server name"
msgstr "服务器名称"
#: mailu/ui/templates/confirm.html:4 #: mailu/ui/templates/confirm.html:4
msgid "Confirm action" msgid "Confirm action"
msgstr "确认操作" msgstr "确认操作"
#: mailu/ui/templates/confirm.html:13 #: mailu/ui/templates/confirm.html:13
#, python-format
msgid "You are about to %(action)s. Please confirm your action." msgid "You are about to %(action)s. Please confirm your action."
msgstr "即将%(action)s请确认您的操作。" msgstr "即将%(action)s请确认您的操作。"
@ -185,54 +278,18 @@ msgstr "Docker错误"
msgid "An error occurred while talking to the Docker server." msgid "An error occurred while talking to the Docker server."
msgstr "Docker服务器通信出错" msgstr "Docker服务器通信出错"
#: mailu/admin/templates/login.html:6
msgid "Your account"
msgstr "你的帐户"
#: mailu/ui/templates/login.html:8 #: mailu/ui/templates/login.html:8
msgid "to access the administration tools" msgid "to access the administration tools"
msgstr "访问管理员工具" msgstr "访问管理工具"
#: mailu/ui/templates/services.html:4 mailu/ui/templates/sidebar.html:39
msgid "Services status"
msgstr "服务状态"
#: mailu/ui/templates/services.html:10
msgid "Service"
msgstr "服务"
#: mailu/ui/templates/fetch/list.html:23 mailu/ui/templates/services.html:11
msgid "Status"
msgstr "状态"
#: mailu/ui/templates/services.html:12
msgid "PID"
msgstr "进程ID"
#: mailu/ui/templates/services.html:13
msgid "Image"
msgstr "镜像"
#: mailu/ui/templates/services.html:14
msgid "Started"
msgstr "已开始"
#: mailu/ui/templates/services.html:15
msgid "Last update"
msgstr "最后更新"
#: mailu/ui/templates/sidebar.html:8 #: mailu/ui/templates/sidebar.html:8
msgid "My account" msgid "My account"
msgstr "我的户" msgstr "我的账户"
#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34 #: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34
msgid "Settings" msgid "Settings"
msgstr "设置" msgstr "设置"
#: mailu/ui/templates/user/settings.html:22
msgid "Auto-forward"
msgstr "自动转发"
#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35 #: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35
msgid "Auto-reply" msgid "Auto-reply"
msgstr "自动回复" msgstr "自动回复"
@ -240,39 +297,71 @@ msgstr "自动回复"
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26 #: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26
#: mailu/ui/templates/user/list.html:36 #: mailu/ui/templates/user/list.html:36
msgid "Fetched accounts" msgid "Fetched accounts"
msgstr "代收户" msgstr "代收户"
#: mailu/ui/templates/sidebar.html:105 #: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
msgid "Sign out" msgid "Authentication tokens"
msgstr "登出" msgstr "认证令牌"
#: mailu/ui/templates/sidebar.html:35 #: mailu/ui/templates/sidebar.html:35
msgid "Administration" msgid "Administration"
msgstr "管理" msgstr "管理"
#: mailu/ui/templates/sidebar.html:44
msgid "Announcement"
msgstr "公告"
#: mailu/ui/templates/sidebar.html:49 #: mailu/ui/templates/sidebar.html:49
msgid "Administrators" msgid "Administrators"
msgstr "管理员" msgstr "管理员"
#: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains"
msgstr "中继域"
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
msgid "Antispam"
msgstr "反垃圾邮件"
#: mailu/ui/templates/sidebar.html:66 #: mailu/ui/templates/sidebar.html:66
msgid "Mail domains" msgid "Mail domains"
msgstr "邮件域" msgstr "邮件域"
#: mailu/ui/templates/sidebar.html:72
msgid "Go to"
msgstr "转到"
#: mailu/ui/templates/sidebar.html:76
msgid "Webmail"
msgstr "网页邮箱"
#: mailu/ui/templates/sidebar.html:87
msgid "Website"
msgstr "网站"
#: mailu/ui/templates/sidebar.html:92 #: mailu/ui/templates/sidebar.html:92
msgid "Help" msgid "Help"
msgstr "帮助" msgstr "帮助"
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
msgid "Register a domain"
msgstr "注册域名"
#: mailu/ui/templates/sidebar.html:105
msgid "Sign out"
msgstr "登出"
#: mailu/ui/templates/working.html:4 #: mailu/ui/templates/working.html:4
msgid "We are still working on this feature!" msgid "We are still working on this feature!"
msgstr "该功能开发中……" msgstr "该功能开发中……"
#: mailu/ui/templates/admin/create.html:4 #: mailu/ui/templates/admin/create.html:4
msgid "Add a global administrator" msgid "Add a global administrator"
msgstr "添加超级管理员" msgstr "添加全局管理员"
#: mailu/ui/templates/admin/list.html:4 #: mailu/ui/templates/admin/list.html:4
msgid "Global administrators" msgid "Global administrators"
msgstr "超级管理员" msgstr "全局管理员"
#: mailu/ui/templates/admin/list.html:9 #: mailu/ui/templates/admin/list.html:9
msgid "Add administrator" msgid "Add administrator"
@ -323,7 +412,7 @@ msgstr "添加别名"
#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21 #: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21
#: mailu/ui/templates/user/list.html:24 #: mailu/ui/templates/user/list.html:24
msgid "Created" msgid "Created"
msgstr "创建" msgstr "创建"
#: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23 #: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23
#: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21 #: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21
@ -337,6 +426,22 @@ msgstr "上次编辑"
msgid "Edit" msgid "Edit"
msgstr "编辑" msgstr "编辑"
#: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain"
msgstr "创建替代域"
#: mailu/ui/templates/alternative/list.html:4
msgid "Alternative domain list"
msgstr "替代域名列表"
#: mailu/ui/templates/alternative/list.html:12
msgid "Add alternative"
msgstr "添加替代"
#: mailu/ui/templates/alternative/list.html:19
msgid "Name"
msgstr "名称"
#: mailu/ui/templates/domain/create.html:4 #: mailu/ui/templates/domain/create.html:4
#: mailu/ui/templates/domain/list.html:9 #: mailu/ui/templates/domain/list.html:9
msgid "New domain" msgid "New domain"
@ -344,11 +449,15 @@ msgstr "新域"
#: mailu/ui/templates/domain/details.html:4 #: mailu/ui/templates/domain/details.html:4
msgid "Domain details" msgid "Domain details"
msgstr "域详" msgstr "域详细信息"
#: mailu/ui/templates/domain/details.html:15 #: mailu/ui/templates/domain/details.html:15
msgid "Regenerate keys" msgid "Regenerate keys"
msgstr "重新生成密钥" msgstr "重新生成秘钥"
#: mailu/ui/templates/domain/details.html:17
msgid "Generate keys"
msgstr "生成秘钥"
#: mailu/ui/templates/domain/details.html:31 #: mailu/ui/templates/domain/details.html:31
msgid "DNS MX entry" msgid "DNS MX entry"
@ -392,7 +501,7 @@ msgstr "别名数量"
#: mailu/ui/templates/domain/list.html:28 #: mailu/ui/templates/domain/list.html:28
msgid "Details" msgid "Details"
msgstr "详" msgstr "详细信息"
#: mailu/ui/templates/domain/list.html:35 #: mailu/ui/templates/domain/list.html:35
msgid "Users" msgid "Users"
@ -406,26 +515,60 @@ msgstr "别名"
msgid "Managers" msgid "Managers"
msgstr "管理员" msgstr "管理员"
#: mailu/ui/templates/domain/list.html:39
msgid "Alternatives"
msgstr "备选方案"
#: mailu/ui/templates/domain/signup.html:13
msgid ""
"In order to register a new domain, you must first setup the\n"
" domain zone so that the domain <code>MX</code> points to this server"
msgstr "在注册一个新的域名前,您必须先为该域名设置 <code>MX</code> 记录,并使其指向本服务器"
#: mailu/ui/templates/domain/signup.html:18
msgid ""
"If you do not know how to setup an <code>MX</code> record for your DNS "
"zone,\n"
" please contact your DNS provider or administrator. Also, please wait "
"a\n"
" couple minutes after the <code>MX</code> is set so the local server "
"cache\n"
" expires."
msgstr "如果您不知道如何为域名设置 <code>MX</code> 记录请联系你的DNS提供商或者系统管理员。在设置完成 <code>MX</code> 记录后,请等待本地域名服务器的缓存过期。"
#: mailu/ui/templates/fetch/create.html:4 #: mailu/ui/templates/fetch/create.html:4
msgid "Add a fetched account" msgid "Add a fetched account"
msgstr "添加一个代收帐户" msgstr "添加一个代收户"
#: mailu/ui/templates/fetch/edit.html:4 #: mailu/ui/templates/fetch/edit.html:4
msgid "Update a fetched account" msgid "Update a fetched account"
msgstr "更新代收帐户" msgstr "更新代收户"
#: mailu/ui/templates/fetch/list.html:12 #: mailu/ui/templates/fetch/list.html:12
msgid "Add an account" msgid "Add an account"
msgstr "添加一个帐户" msgstr "添加一个户"
#: mailu/ui/templates/fetch/list.html:19 #: mailu/ui/templates/fetch/list.html:19
msgid "Endpoint" msgid "Endpoint"
msgstr "端点" msgstr "端点"
#: mailu/ui/templates/fetch/list.html:21
msgid "Keep emails"
msgstr "保留电子邮件"
#: mailu/ui/templates/fetch/list.html:22 #: mailu/ui/templates/fetch/list.html:22
msgid "Last check" msgid "Last check"
msgstr "上次检查" msgstr "上次检查"
#: mailu/ui/templates/fetch/list.html:35
msgid "yes"
msgstr "是"
#: mailu/ui/templates/fetch/list.html:35
msgid "no"
msgstr "否"
#: mailu/ui/templates/manager/create.html:4 #: mailu/ui/templates/manager/create.html:4
msgid "Add a manager" msgid "Add a manager"
msgstr "添加一个管理员" msgstr "添加一个管理员"
@ -438,41 +581,49 @@ msgstr "管理员列表"
msgid "Add manager" msgid "Add manager"
msgstr "添加管理员" msgstr "添加管理员"
#: mailu/ui/forms.py:168 #: mailu/ui/templates/relay/create.html:4
msgid "Announcement subject" msgid "New relay domain"
msgstr "公告主题" msgstr "新的中继域"
#: mailu/ui/forms.py:170 #: mailu/ui/templates/relay/edit.html:4
msgid "Announcement body" msgid "Edit relayd domain"
msgstr "公告正文" msgstr "编辑中继域"
#: mailu/ui/forms.py:172 #: mailu/ui/templates/relay/list.html:4
msgid "Send" msgid "Relayed domain list"
msgstr "发送" msgstr "中继域列表"
#: mailu/ui/templates/announcement.html:4 #: mailu/ui/templates/relay/list.html:9
msgid "Public announcement" msgid "New relayed domain"
msgstr "公告" msgstr "新的中继域"
#: mailu/ui/templates/announcement.html:8 #: mailu/ui/templates/token/create.html:4
msgid "from" msgid "Create an authentication token"
msgstr "来自" msgstr "创建一个认证令牌"
#: mailu/ui/templates/sidebar.html:44 #: mailu/ui/templates/token/list.html:12
msgid "Announcement" msgid "New token"
msgstr "公告" msgstr "新令牌"
#: mailu/ui/templates/user/create.html:4 #: mailu/ui/templates/user/create.html:4
msgid "New user" msgid "New user"
msgstr "新用户" msgstr "新用户"
#: mailu/ui/templates/user/create.html:15
msgid "General"
msgstr "通用"
#: mailu/ui/templates/user/create.html:22
msgid "Features and quotas"
msgstr "功能和配额"
#: mailu/ui/templates/user/edit.html:4 #: mailu/ui/templates/user/edit.html:4
msgid "Edit user" msgid "Edit user"
msgstr "编辑用户" msgstr "编辑用户"
#: mailu/ui/templates/user/forward.html:4 #: mailu/ui/templates/user/forward.html:4
msgid "Forward emails" msgid "Forward emails"
msgstr "转发电子邮件" msgstr "转发邮件"
#: mailu/ui/templates/user/list.html:4 #: mailu/ui/templates/user/list.html:4
msgid "User list" msgid "User list"
@ -492,201 +643,15 @@ msgstr "功能"
#: mailu/ui/templates/user/password.html:4 #: mailu/ui/templates/user/password.html:4
msgid "Password update" msgid "Password update"
msgstr "密码更新" msgstr "更新密码"
#: mailu/ui/templates/user/reply.html:4 #: mailu/ui/templates/user/reply.html:4
msgid "Automatic reply" msgid "Automatic reply"
msgstr "自动回复" msgstr "自动回复"
#: mailu/ui/forms.py:49 #: mailu/ui/templates/user/settings.html:22
msgid "Maximum user quota" msgid "Auto-forward"
msgstr "最大用户容量" msgstr "自动转发"
#: mailu/ui/forms.py:101
msgid "Keep a copy of the emails"
msgstr "保留电子邮件副本"
#: mailu/ui/forms.py:163
msgid "Keep emails on the server"
msgstr "保留电子邮件在服务器上"
#: mailu/ui/templates/fetch/list.html:21
msgid "Keep emails"
msgstr "保存电子邮件"
#: mailu/ui/templates/fetch/list.html:35
msgid "yes"
msgstr "是"
#: mailu/ui/templates/fetch/list.html:35
msgid "no"
msgstr "否"
#: mailu/ui/forms.py:65
msgid "Alternative name"
msgstr "替代名称"
#: mailu/ui/forms.py:70
msgid "Relayed domain name"
msgstr "中继域域名"
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
msgid "Remote host"
msgstr "远程主机"
#: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains"
msgstr "中继域"
#: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain"
msgstr "创建替代域"
#: mailu/ui/templates/alternative/list.html:4
msgid "Alternative domain list"
msgstr "替代域名列表"
#: mailu/ui/templates/alternative/list.html:12
msgid "Add alternative"
msgstr "添加替代"
#: mailu/ui/templates/alternative/list.html:19
msgid "Name"
msgstr "名称"
#: mailu/ui/templates/domain/list.html:39
msgid "Alternatives"
msgstr "备择方案"
#: mailu/ui/templates/relay/create.html:4
msgid "New relay domain"
msgstr "新的中继域"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgstr "编辑中继域"
#: mailu/ui/templates/relay/list.html:4
msgid "Relayed domain list"
msgstr "中继域列表"
#: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain"
msgstr "新的中继域"
#: mailu/ui/forms.py:125
msgid "Your token (write it down, as it will never be displayed again)"
msgstr "您的令牌(请记录,它只显示这一次)"
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
msgid "Authorized IP"
msgstr "授权IP"
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
msgid "Authentication tokens"
msgstr "认证令牌"
#: mailu/ui/templates/sidebar.html:72
msgid "Go to"
msgstr "转到"
#: mailu/ui/templates/sidebar.html:76
msgid "Webmail"
msgstr "网页邮箱"
#: mailu/ui/templates/sidebar.html:87
msgid "Website"
msgstr "网站"
#: mailu/ui/templates/token/create.html:4
msgid "Create an authentication token"
msgstr "创建一个认证令牌"
#: mailu/ui/templates/token/list.html:12
msgid "New token"
msgstr "新的令牌"
#: mailu/ui/templates/user/create.html:15
msgid "General"
msgstr "通用"
#: mailu/ui/templates/user/create.html:22
msgid "Features and quotas"
msgstr "功能和配额"
#: mailu/ui/templates/user/settings.html:14
msgid "General settings"
msgstr "常规设置"
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
msgid "Antispam"
msgstr "反垃圾邮件"
#: mailu/ui/forms.py:99
msgid "Spam filter tolerance"
msgstr "垃圾邮件过滤器容忍度"
#: mailu/ui/forms.py:50
msgid "Enable sign-up"
msgstr "启用用户注册"
#: mailu/ui/forms.py:57
msgid "Initial admin"
msgstr "初始管理员"
#: mailu/ui/forms.py:58
msgid "Admin password"
msgstr "管理员密码"
#: mailu/ui/forms.py:84
msgid "Enabled"
msgstr "启用"
#: mailu/ui/forms.py:89
msgid "Email address"
msgstr "邮件地址"
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
#: mailu/ui/templates/user/signup.html:4
#: mailu/ui/templates/user/signup_domain.html:4
msgid "Sign up"
msgstr "注册"
#: mailu/ui/forms.py:119
msgid "End of vacation"
msgstr "假期结束"
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
msgid "Client setup"
msgstr "客户端设置"
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
msgid "Mail protocol"
msgstr "邮件协议"
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
msgid "Server name"
msgstr "服务器名"
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
msgid "Register a domain"
msgstr "注册域名"
#: mailu/ui/templates/domain/details.html:17
msgid "Generate keys"
msgstr "生成密钥"
#: mailu/ui/templates/domain/signup.html:13
msgid "In order to register a new domain, you must first setup the\n"
" domain zone so that the domain <code>MX</code> points to this server"
msgstr "在注册一个新的域名前,您必须先为该域名设置 <code>MX</code> 记录,并使其指向本服务器"
#: mailu/ui/templates/domain/signup.html:18
msgid "If you do not know how to setup an <code>MX</code> record for your DNS zone,\n"
" please contact your DNS provider or administrator. Also, please wait a\n"
" couple minutes after the <code>MX</code> is set so the local server cache\n"
" expires."
msgstr "如果您不知道如何为域名设置 <code>MX</code> 记录请联系你的DNS提供商或者系统管理员。在设置完成 <code>MX</code> 记录后,请等待本地域名服务器的缓存过期。"
#: mailu/ui/templates/user/signup_domain.html:8 #: mailu/ui/templates/user/signup_domain.html:8
msgid "pick a domain for the new account" msgid "pick a domain for the new account"
@ -700,3 +665,14 @@ msgstr "域名"
msgid "Available slots" msgid "Available slots"
msgstr "可用" msgstr "可用"
#~ msgid "Your account"
#~ msgstr ""
#~ msgid "Spam filter threshold"
#~ msgstr ""
#~ msgid "from"
#~ msgstr ""
#~ msgid "General settings"
#~ msgstr ""

@ -1,6 +1,6 @@
from flask import Blueprint from flask import Blueprint
ui = Blueprint('ui', __name__, static_folder='static', template_folder='templates') ui = Blueprint('ui', __name__, static_folder=None, template_folder='templates')
from mailu.ui.views import * from mailu.ui.views import *

@ -44,13 +44,6 @@ class MultipleEmailAddressesVerify(object):
class ConfirmationForm(flask_wtf.FlaskForm): class ConfirmationForm(flask_wtf.FlaskForm):
submit = fields.SubmitField(_('Confirm')) submit = fields.SubmitField(_('Confirm'))
class LoginForm(flask_wtf.FlaskForm):
email = fields.StringField(_('E-mail'), [validators.Email()])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
submit = fields.SubmitField(_('Sign in'))
class DomainForm(flask_wtf.FlaskForm): class DomainForm(flask_wtf.FlaskForm):
name = fields.StringField(_('Domain name'), [validators.DataRequired()]) name = fields.StringField(_('Domain name'), [validators.DataRequired()])
max_users = fields_.IntegerField(_('Maximum user count'), [validators.NumberRange(min=-1)], default=10) max_users = fields_.IntegerField(_('Maximum user count'), [validators.NumberRange(min=-1)], default=10)
@ -86,7 +79,7 @@ class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)]) localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password')) pw = fields.PasswordField(_('Password'))
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000) quota_bytes = fields_.IntegerSliderField(_('Quota'), default=10**9)
enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True) enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True) enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True)
displayed_name = fields.StringField(_('Displayed name')) displayed_name = fields.StringField(_('Displayed name'))

@ -1,15 +1,15 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Add a global administrator{% endtrans %} {% trans %}Add a global administrator{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.box() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.admin, class_='mailselect') }} {{ macros.form_field(form.admin, class_='mailselect') }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,28 +1,32 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Global administrators{% endtrans %} {% trans %}Global administrators{% endtrans %}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary" href="{{ url_for('.admin_create') }}"> <a class="btn btn-primary float-right" href="{{ url_for('.admin_create') }}">
{% trans %}Add administrator{% endtrans %} {% trans %}Add administrator{% endtrans %}
</a> </a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th> <th>{% trans %}Email{% endtrans %}</th>
</tr> </tr>
{% for admin in admins %} </thead>
<tr> <tbody>
{%- for admin in admins %}
<tr>
<td> <td>
<a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td> </td>
<td>{{ admin }}</td> <td>{{ admin }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} </tbody>
{% endblock %} {%- endcall %}
{%- endblock %}

@ -1,22 +1,22 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Create alias{% endtrans %} {% trans %}Create alias{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.box() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }} {{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
{{ macros.form_field(form.wildcard) }} {{ macros.form_field(form.wildcard) }}
{{ macros.form_field(form.destination, class_='mailselect') }} {{ macros.form_field(form.destination, class_='mailselect') }}
{{ macros.form_field(form.comment) }} {{ macros.form_field(form.comment) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "alias/create.html" %} {%- extends "alias/create.html" %}
{% block title %} {%- block title %}
{% trans %}Edit alias{% endtrans %} {% trans %}Edit alias{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ alias }} {{ alias }}
{% endblock %} {%- endblock %}

@ -1,29 +1,32 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Alias list{% endtrans %} {% trans %}Alias list{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary" href="{{ url_for('.alias_create', domain_name=domain.name) }}">{% trans %}Add alias{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.alias_create', domain_name=domain.name) }}">{% trans %}Add alias{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th> <th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Destination{% endtrans %}</th> <th>{% trans %}Destination{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th> <th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th> <th>{% trans %}Last edit{% endtrans %}</th>
</tr> </tr>
{% for alias in domain.aliases %} </thead>
<tr> <tbody>
{%- for alias in domain.aliases %}
<tr>
<td> <td>
<a href="{{ url_for('.alias_edit', alias=alias.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp; <a href="{{ url_for('.alias_edit', alias=alias.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.alias_delete', alias=alias.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.alias_delete', alias=alias.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
@ -31,9 +34,10 @@
<td>{{ alias }}</td> <td>{{ alias }}</td>
<td>{{ alias.destination|join(', ') or '-' }}</td> <td>{{ alias.destination|join(', ') or '-' }}</td>
<td>{{ alias.comment or '' }}</td> <td>{{ alias.comment or '' }}</td>
<td>{{ alias.created_at }}</td> <td>{{ alias.created_at | format_date }}</td>
<td>{{ alias.updated_at or '' }}</td> <td>{{ alias.updated_at | format_date }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} </tbody>
{% endblock %} {%- endcall %}
{%- endblock %}

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Create alternative domain{% endtrans %} {% trans %}Create alternative domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}

@ -1,32 +1,38 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Alternative domain list{% endtrans %} {% trans %}Alternative domain list{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary" href="{{ url_for('.alternative_create', domain_name=domain.name) }}">{% trans %}Add alternative{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.alternative_create', domain_name=domain.name) }}">{% trans %}Add alternative{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Name{% endtrans %}</th> <th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
</tr> <th>{% trans %}Last edit{% endtrans %}</th>
{% for alternative in domain.alternatives %} </tr>
<tr> </thead>
<tbody>
{%- for alternative in domain.alternatives %}
<tr>
<td> <td>
<a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td> </td>
<td>{{ alternative }}</td> <td>{{ alternative }}</td>
<td>{{ alternative.created_at }}</td> <td>{{ alternative.created_at | format_date }}</td>
</tr> <td>{{ alternative.updated_at | format_date }}</td>
{% endfor %} </tr>
{% endcall %} {%- endfor %}
{% endblock %} </tbody>
{%- endcall %}
{%- endblock %}

@ -1,16 +1,16 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Public announcement{% endtrans %} {% trans %}Public announcement{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.box() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.announcement_subject) }} {{ macros.form_field(form.announcement_subject) }}
{{ macros.form_field(form.announcement_body, rows=10) }} {{ macros.form_field(form.announcement_body, rows=10) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -0,0 +1,15 @@
{%- extends "base.html" %}
{%- block title %}
{% trans %}Antispam{% endtrans %}
{%- endblock %}
{%- block subtitle %}
{% trans %}RSPAMD status page{% endtrans %}
{%- endblock %}
{%- block content %}
<div class="embed-responsive embed-responsive-1by1">
<iframe class="embed-responsive-item" src="{{ config["WEB_ADMIN"] }}/antispam/"></iframe>
</div>
{%- endblock %}

@ -1,52 +1,86 @@
{% import "macros.html" as macros %} {%- import "macros.html" as macros %}
{%- import "bootstrap/utils.html" as utils %}
<!doctype html> <!doctype html>
<html> <html lang="{{ session['language'] }}" data-static="/static/">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta charset="utf-8">
<link rel="stylesheet" href="{{ url_for('.static', filename='vendor.css') }}"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('.static', filename='app.css') }}"> <meta name="description" content="{% trans %}Admin page for{% endtrans %} {{ config["SITENAME"] }}">
<title>Mailu-Admin - {{ config["SITENAME"] }}</title> <meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Mailu-Admin | {{ config["SITENAME"] }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
</head> </head>
<body class="hold-transition skin-blue sidebar-mini"> <body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper"> <div class="wrapper">
<header class="main-header"> <nav class="main-header navbar navbar-expand navbar-white navbar-light">
<div class="logo"> <ul class="navbar-nav">
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button"> <li class="nav-item">
<span class="sr-only">Toggle navigation</span> <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars" title="{% trans %}toggle sidebar{% endtrans %}" aria-expanded="false"></i><span class="sr-only">{% trans %}toggle sidebar{% endtrans %}</span></a>
</a> </li>
<a href="{{ config["WEB_ADMIN"] }}"> <li class="nav-item">
<span class="logo-lg">{{ config["SITENAME"] }}</span> {%- for page, url in path %}
</a> {%- if loop.index > 1 %}
<i class="fas fa-greater-than text-xs text-gray" aria-hidden="true"></i>
{%- endif %}
{%- if url %}
<a class="nav-link d-inline-block" href="{{ url }}" role="button">{{ page }}</a>
{%- else %}
<span class="nav-link d-inline-block">{{ page }}</span>
{%- endif %}
{%- endfor %}
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">
<i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">Language</span>
<span class="badge badge-primary navbar-badge">{{ session['language'] }}</span></a>
<div class="dropdown-menu dropdown-menu-right p-0" id="mailu-languages">
{%- for locale in config.translations.values() %}
<a class="dropdown-item{% if locale|string() == session['language'] %} active{% endif %}" href="{{ url_for('.set_language', language=locale) }}">{{ locale.get_language_name().title() }}</a>
{%- endfor %}
</div> </div>
</header> </li>
<aside class="main-sidebar"> </ul>
{% block sidebar %} </nav>
{% include "sidebar.html" %} <aside class="main-sidebar sidebar-dark-primary nav-compact elevation-4">
{% endblock %} <a href="{{ url_for('.domain_list' if current_user.manager_of or current_user.global_admin else '.user_settings') }}" class="brand-link bg-mailu-logo"{% if config["LOGO_BACKGROUND"] %} style="background-color:{{ config["LOGO_BACKGROUND"] }}!important;"{% endif %}>
<img src="{{ config["LOGO_URL"] if config["LOGO_URL"] else url_for('static', filename='mailu.png') }}" width="33" height="33" alt="Mailu" class="brand-image mailu-logo img-circle elevation-3">
<span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span>
</a>
{%- include "sidebar.html" %}
</aside> </aside>
<div class="content-wrapper"> <div class="content-wrapper text-sm">
<section class="content-header"> <section class="content-header">
<div class="pull-right"> <div class="container-fluid">
{% block main_action %} <div class="row mb-2">
{% endblock %} <div class="col-sm-6">
</div> <h1 class="m-0">{%- block title %}{%- endblock %}</h1>
<h1>
{% block title %}{% endblock %}
<small>{% block subtitle %}{% endblock %}</small> <small>{% block subtitle %}{% endblock %}</small>
</h1> </div>
</section> <div class="col-sm-6">
{%- block main_action %}{%- endblock %}
<section class="content"> </div>
{% block content %}{% endblock %} </div>
</div>
</section> </section>
<div class="content">
{{ utils.flashed_messages(container=False, default_category='success') }}
{%- block content %}{%- endblock %}
</div>
</div> </div>
<footer class="main-footer"> <footer class="main-footer">
Built with <i class="fa fa-heart"></i> using <a class="white-text" href="http://flask.pocoo.org/">Flask</a> and Built with <i class="fa fa-heart text-danger" aria-hidden="true"></i><span class="sr-only">love</span>
<a class="white-text" href="https://almsaeedstudio.com/preview">AdminLTE</a> using <a href="https://flask.palletsprojects.com/">Flask</a>
<span class="pull-right"><i class="fa fa-code-fork"></i> on <a class="white-text" href="https://github.com/Mailu/Mailu">Github</a></a></span> and <a href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a>.
<span class="fa-pull-right">
<i class="fa fa-code-branch" aria-hidden="true"></i><span class="sr-only">fork</span>
on <a href="https://github.com/Mailu/Mailu">Github</a>
</span>
</footer> </footer>
</div> </div>
<script src="{{ url_for('.static', filename='vendor.js') }}"></script> <script src="{{ url_for('static', filename='vendor.js') }}"></script>
<script src="{{ url_for('.static', filename='app.js') }}"></script> <script src="{{ url_for('static', filename='app.js') }}"></script>
</body> </body>
</html> </html>

@ -1,16 +1,15 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Client setup{% endtrans %} {% trans %}Client setup{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
configure your email client {% trans %}configure your email client{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.box(title="Incoming mail") %} {%- call macros.table(title=_("Incoming mail"), datatable=False) %}
<table class="table table-bordered">
<tbody> <tbody>
<tr> <tr>
<th>{% trans %}Mail protocol{% endtrans %}</th> <th>{% trans %}Mail protocol{% endtrans %}</th>
@ -22,22 +21,20 @@ configure your email client
</tr> </tr>
<tr> <tr>
<th>{% trans %}Server name{% endtrans %}</th> <th>{% trans %}Server name{% endtrans %}</th>
<td><pre>{{ config["HOSTNAMES"].split(',')[0] }}</pre></td> <td><pre class="pre-config border bg-light">{{ config["HOSTNAMES"] }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Username{% endtrans %}</th> <th>{% trans %}Username{% endtrans %}</th>
<td><pre>{{ current_user if current_user.is_authenticated else "******" }}</pre></td> <td><pre class="pre-config border bg-light">{{ current_user if current_user.is_authenticated else "******" }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Password{% endtrans %}</th> <th>{% trans %}Password{% endtrans %}</th>
<td><pre>*******</pre></td> <td><pre class="pre-config border bg-light">*******</pre></td>
</tr> </tr>
</tbody> </tbody>
</table> {%- endcall %}
{% endcall %}
{% call macros.box(title="Outgoing mail") %} {%- call macros.table(title=_("Outgoing mail"), datatable=False) %}
<table class="table table-bordered">
<tbody> <tbody>
<tr> <tr>
<th>{% trans %}Mail protocol{% endtrans %}</th> <th>{% trans %}Mail protocol{% endtrans %}</th>
@ -49,17 +46,16 @@ configure your email client
</tr> </tr>
<tr> <tr>
<th>{% trans %}Server name{% endtrans %}</th> <th>{% trans %}Server name{% endtrans %}</th>
<td><pre>{{ config["HOSTNAMES"].split(',')[0] }}</pre></td> <td><pre class="pre-config border bg-light">{{ config["HOSTNAMES"] }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Username{% endtrans %}</th> <th>{% trans %}Username{% endtrans %}</th>
<td><pre>{{ current_user if current_user.is_authenticated else "******" }}</pre></td> <td><pre class="pre-config border bg-light">{{ current_user if current_user.is_authenticated else "******" }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Password{% endtrans %}</th> <th>{% trans %}Password{% endtrans %}</th>
<td><pre>*******</pre></td> <td><pre class="pre-config border bg-light">*******</pre></td>
</tr> </tr>
</tbody> </tbody>
</table> {%- endcall %}
{% endcall %} {%- endblock %}
{% endblock %}

@ -1,16 +1,16 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Confirm action{% endtrans %} {% trans %}Confirm action{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ action }} {{ action }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.box(theme="warning") %} {%- call macros.card(theme="warning") %}
<p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p> <p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p>
{{ macros.form(form) }} {{ macros.form(form) }}
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,14 +1,14 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Docker error{% endtrans %} {% trans %}Docker error{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ action }} {{ action }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p> <p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p>
<pre>{{ error }}</pre> <pre class="pre-config border bg-light">{{ error }}</pre>
{% endblock %} {%- endblock %}

@ -1,21 +1,20 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}New domain{% endtrans %} {% trans %}New domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.box() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.name) }} {{ macros.form_field(form.name) }}
{{ macros.form_fields((form.max_users, form.max_aliases)) }} {{ macros.form_fields((form.max_users, form.max_aliases)) }}
{{ macros.form_field(form.max_quota_bytes, step=1000000000, max=50000000000, {{ macros.form_field(form.max_quota_bytes, step=10**9, max=50*10**9, data_infinity="true",
prepend='<span class="input-group-addon"><span id="quota">'+((form.max_quota_bytes.data//1000000000).__str__() if form.max_quota_bytes.data else '∞')+'</span> GiB</span>', prepend='<span class="input-group-text"><span id="max_quota_bytes_value"></span>&nbsp;GB</span>') }}
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
{{ macros.form_field(form.signup_enabled) }} {{ macros.form_field(form.signup_enabled) }}
{{ macros.form_field(form.comment) }} {{ macros.form_field(form.comment) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,54 +1,71 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Domain details{% endtrans %} {% trans %}Domain details{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a class="btn btn-primary" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}"> <a class="btn btn-primary float-right" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}">
{% if domain.dkim_publickey %} {%- if domain.dkim_publickey %}
{% trans %}Regenerate keys{% endtrans %} {% trans %}Regenerate keys{% endtrans %}
{% else %} {%- else %}
{% trans %}Generate keys{% endtrans %} {% trans %}Generate keys{% endtrans %}
{% endif %} {%- endif %}
</a> </a>
{% endif %} {%- endif %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table(datatable=False) %}
{% set hostname = config["HOSTNAMES"].split(",")[0] %}
<tr> <tr>
<th>{% trans %}Domain name{% endtrans %}</th> <th>{% trans %}Domain name{% endtrans %}</th>
<td>{{ domain.name }}</td> <td>{{ domain.name }}</td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}DNS MX entry{% endtrans %} <i class="fa {{ 'fa-check-circle' if domain.check_mx() else 'fa-exclamation-circle' }}"></i></th> <th>{% trans %}DNS MX entry{% endtrans %} <i class="fa {{ 'fa-check-circle text-success' if domain.check_mx() else 'fa-exclamation-circle text-danger' }}"></i></th>
<td><pre>{{ domain.name }}. 600 IN MX 10 {{ hostname }}.</pre></td> <td>{{ macros.clip("dns_mx") }}<pre id="dns_mx" class="pre-config border bg-light">{{ domain.dns_mx }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}DNS SPF entries{% endtrans %}</th> <th>{% trans %}DNS SPF entries{% endtrans %}</th>
<td><pre> <td>{{ macros.clip("dns_spf") }}<pre id="dns_spf" class="pre-config border bg-light">{{ domain.dns_spf }}</pre>
{{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ hostname }} -all"</pre></td> </td>
</tr> </tr>
{% if domain.dkim_publickey %} {%- if domain.dkim_publickey %}
<tr> <tr>
<th>{% trans %}DKIM public key{% endtrans %}</th> <th>{% trans %}DKIM public key{% endtrans %}</th>
<td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ domain.dkim_publickey }}</pre></td> <td>{{ macros.clip("dkim_key") }}<pre id="dkim_key" class="pre-config border bg-light">{{ domain.dkim_publickey }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}DNS DKIM entry{% endtrans %}</th> <th>{% trans %}DNS DKIM entry{% endtrans %}</th>
<td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ config["DKIM_SELECTOR"] }}._domainkey.{{ domain.name }}. 600 IN TXT "v=DKIM1; k=rsa; p={{ domain.dkim_publickey }}"</pre></td> <td>{{ macros.clip("dns_dkim") }}<pre id="dns_dkim" class="pre-config border bg-light">{{ domain.dns_dkim }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}DNS DMARC entry{% endtrans %}</th> <th>{% trans %}DNS DMARC entry{% endtrans %}</th>
<td><pre>_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject;{% if config["DMARC_RUA"] %} rua=mailto:{{ config["DMARC_RUA"] }}@{{ config["DOMAIN"] }};{% endif %}{% if config["DMARC_RUF"] %} ruf=mailto:{{ config["DMARC_RUF"] }}@{{ config["DOMAIN"] }};{% endif %} adkim=s; aspf=s"</pre></td> <td>
{{ macros.clip("dns_dmarc") }}<pre id="dns_dmarc" class="pre-config border bg-light">{{ domain.dns_dmarc }}</pre>
{{ macros.clip("dns_dmarc_report") }}<pre id="dns_dmarc_report" class="pre-config border bg-light">{{ domain.dns_dmarc_report }}</pre>
</td>
</tr> </tr>
{% endif %} {%- endif %}
{% endcall %} {%- set tlsa_record=domain.dns_tlsa %}
{% endblock %} {%- if tlsa_record %}
<tr>
<th>{% trans %}DNS TLSA entry{% endtrans %}</br><span class="text-secondary text-xs font-weight-normal">Let's Encrypt</br>ISRG Root X1</span></th>
<td>{{ macros.clip("dns_tlsa") }}<pre id="dns_tlsa" class="pre-config border bg-light">{{ tlsa_record }}</pre></td>
</tr>
{%- endif %}
<tr>
<th>{% trans %}DNS client auto-configuration (RFC6186) entries{% endtrans %}</th>
<td>{{ macros.clip("dns_autoconfig") }}<pre id="dns_autoconfig" class="pre-config border bg-light">
{%- for line in domain.dns_autoconfig %}
{{ line }}
{%- endfor -%}
</pre></td>
</tr>
{%- endcall %}
{%- endblock %}

@ -1,9 +1,9 @@
{% extends "domain/create.html" %} {%- extends "domain/create.html" %}
{% block title %} {%- block title %}
{% trans %}Edit domain{% endtrans %} {% trans %}Edit domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}

@ -1,18 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Domain list{% endtrans %} {% trans %}Domain list{% endtrans %}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a class="btn btn-primary" href="{{ url_for('.domain_create') }}">{% trans %}New domain{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.domain_create') }}">{% trans %}New domain{% endtrans %}</a>
{% endif %} {%- endif %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Manage{% endtrans %}</th> <th>{% trans %}Manage{% endtrans %}</th>
<th>{% trans %}Domain name{% endtrans %}</th> <th>{% trans %}Domain name{% endtrans %}</th>
@ -21,31 +22,34 @@
<th>{% trans %}Comment{% endtrans %}</th> <th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th> <th>{% trans %}Last edit{% endtrans %}</th>
</tr> </tr>
{% for domain in current_user.get_managed_domains() %} </thead>
<tr> <tbody>
{%- for domain in current_user.get_managed_domains() %}
<tr>
<td> <td>
<a href="{{ url_for('.domain_details', domain_name=domain.name) }}" title="{% trans %}Details{% endtrans %}"><i class="fa fa-list"></i></a>&nbsp; <a href="{{ url_for('.domain_details', domain_name=domain.name) }}" title="{% trans %}Details{% endtrans %}"><i class="fa fa-list"></i></a>&nbsp;
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a href="{{ url_for('.domain_edit', domain_name=domain.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp; <a href="{{ url_for('.domain_edit', domain_name=domain.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp;
<a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp; <a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp;
{% endif %} {%- endif %}
</td> </td>
<td> <td>
<a href="{{ url_for('.user_list', domain_name=domain.name) }}" title="{% trans %}Users{% endtrans %}"><i class="fa fa-envelope-o"></i></a>&nbsp; <a href="{{ url_for('.user_list', domain_name=domain.name) }}" title="{% trans %}Users{% endtrans %}"><i class="far fa-envelope"></i></a>&nbsp;
<a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>&nbsp; <a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>&nbsp;
<a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>&nbsp; <a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>&nbsp;
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a href="{{ url_for('.alternative_list', domain_name=domain.name) }}" title="{% trans %}Alternatives{% endtrans %}"><i class="fa fa-asterisk"></i></a>&nbsp; <a href="{{ url_for('.alternative_list', domain_name=domain.name) }}" title="{% trans %}Alternatives{% endtrans %}"><i class="fa fa-asterisk"></i></a>&nbsp;
{% endif %} {%- endif %}
</td> </td>
<td>{{ domain.name }}</td> <td>{{ domain.name }}</td>
<td>{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td> <td>{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td>
<td>{{ domain.aliases | count }} / {{ '∞' if domain.max_aliases == -1 else domain.max_aliases }}</td> <td>{{ domain.aliases | count }} / {{ '∞' if domain.max_aliases == -1 else domain.max_aliases }}</td>
<td>{{ domain.comment or '' }}</td> <td>{{ domain.comment or '' }}</td>
<td>{{ domain.created_at }}</td> <td>{{ domain.created_at | format_date }}</td>
<td>{{ domain.updated_at or '' }}</td> <td>{{ domain.updated_at | format_date }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} </tbody>
{% endblock %} {%- endcall %}
{%- endblock %}

@ -1,18 +1,18 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Register a domain{% endtrans %} {% trans %}Register a domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.box(title="Requirements") %} {%- call macros.card(title="Requirements") %}
<p>{% trans %}In order to register a new domain, you must first setup the <p>{% trans %}In order to register a new domain, you must first setup the
domain zone so that the domain <code>MX</code> points to this server{% endtrans %} domain zone so that the domain <code>MX</code> points to this server{% endtrans %}
(<code>{{ config["HOSTNAMES"].split(",")[0] }}</code>). (<code>{{ config["HOSTNAME"] }}</code>).
</p> </p>
<p> <p>
{% trans %}If you do not know how to setup an <code>MX</code> record for your DNS zone, {% trans %}If you do not know how to setup an <code>MX</code> record for your DNS zone,
@ -20,17 +20,17 @@
couple minutes after the <code>MX</code> is set so the local server cache couple minutes after the <code>MX</code> is set so the local server cache
expires.{% endtrans %} expires.{% endtrans %}
</p> </p>
{% endcall %} {%- endcall %}
{% call macros.box() %} {%- call macros.card() %}
{% if form.localpart %} {%- if form.localpart %}
{{ macros.form_fields((form.localpart, form.name), append='<span class="input-group-addon">@</span>') }} {{ macros.form_fields((form.localpart, form.name), append='<span class="input-group-text">@</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }} {{ macros.form_fields((form.pw, form.pw2)) }}
{% else %} {%- else %}
{{ macros.form_field(form.name) }} {{ macros.form_field(form.name) }}
{% endif %} {%- endif %}
{{ macros.form_field(form.captcha) }} {{ macros.form_field(form.captcha) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
{% endcall %} {%- endcall %}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,31 +1,31 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Add a fetched account{% endtrans %} {% trans %}Add a fetched account{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.box(title="Remote server") %} {%- call macros.card(title="Remote server") %}
{{ macros.form_field(form.protocol) }} {{ macros.form_field(form.protocol) }}
{{ macros.form_fields((form.host, form.port)) }} {{ macros.form_fields((form.host, form.port)) }}
{{ macros.form_field(form.tls) }} {{ macros.form_field(form.tls) }}
{% endcall %} {%- endcall %}
{% call macros.box(title="Authentication") %} {%- call macros.card(title="Authentication") %}
{{ macros.form_field(form.username) }} {{ macros.form_field(form.username) }}
{{ macros.form_field(form.password) }} {{ macros.form_field(form.password) }}
{% endcall %} {%- endcall %}
{% call macros.box(title="Settings") %} {%- call macros.card(title="Settings") %}
{{ macros.form_field(form.keep) }} {{ macros.form_field(form.keep) }}
{% endcall %} {%- endcall %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "fetch/create.html" %} {%- extends "fetch/create.html" %}
{% block title %} {%- block title %}
{% trans %}Update a fetched account{% endtrans %} {% trans %}Update a fetched account{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}

@ -1,20 +1,21 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Fetched accounts{% endtrans %} {% trans %}Fetched accounts{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary" href="{{ url_for('.fetch_create', user_email=user.email) }}">{% trans %}Add an account{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.fetch_create', user_email=user.email) }}">{% trans %}Add an account{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Endpoint{% endtrans %}</th> <th>{% trans %}Endpoint{% endtrans %}</th>
<th>{% trans %}Username{% endtrans %}</th> <th>{% trans %}Username{% endtrans %}</th>
@ -23,9 +24,11 @@
<th>{% trans %}Status{% endtrans %}</th> <th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th> <th>{% trans %}Last edit{% endtrans %}</th>
</tr> </tr>
{% for fetch in user.fetches %} </thead>
<tr> <tbody>
{%- for fetch in user.fetches %}
<tr>
<td> <td>
<a href="{{ url_for('.fetch_edit', fetch_id=fetch.id) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp; <a href="{{ url_for('.fetch_edit', fetch_id=fetch.id) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.fetch_delete', fetch_id=fetch.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.fetch_delete', fetch_id=fetch.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
@ -33,11 +36,12 @@
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td> <td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
<td>{{ fetch.username }}</td> <td>{{ fetch.username }}</td>
<td>{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td> <td>{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td>{{ fetch.last_check or '-' }}</td> <td>{{ fetch.last_check | format_datetime or '-' }}</td>
<td>{{ fetch.error or '-' }}</td> <td>{{ fetch.error or '-' }}</td>
<td>{{ fetch.created_at }}</td> <td>{{ fetch.created_at | format_date }}</td>
<td>{{ fetch.updated_at or '' }}</td> <td>{{ fetch.updated_at | format_date }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} </tbody>
{% endblock %} {%- endcall %}
{%- endblock %}

@ -1,7 +1,7 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block content %} {%- block content %}
{% call macros.box() %} {%- call macros.card() %}
{{ macros.form(form) }} {{ macros.form(form) }}
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,9 +0,0 @@
{% extends "form.html" %}
{% block title %}
{% trans %}Sign in{% endtrans %}
{% endblock %}
{% block subtitle %}
{% trans %}to access the administration tools{% endtrans %}
{% endblock %}

@ -1,98 +1,133 @@
{% macro form_errors(form) %} {%- macro form_errors(form) %}
{% if form.errors %} {%- if form.errors %}
{% for fieldname, errors in form.errors.items() %} {%- for fieldname, errors in form.errors.items() %}
{% if bootstrap_is_hidden_field(form[fieldname]) %} {%- if bootstrap_is_hidden_field(form[fieldname]) %}
{% for error in errors %} {%- for error in errors %}
<p class="error">{{error}}</p> <p class="error">{{error}}</p>
{% endfor %} {%- endfor %}
{% endif %} {%- endif %}
{% endfor %} {%- endfor %}
{% endif %} {%- endif %}
{% endmacro %} {%- endmacro %}
{% macro form_field_errors(field) %} {%- macro form_field_errors(field) %}
{% if field.errors %} {%- if field.errors %}
{% for error in field.errors %} {%- for error in field.errors %}
<p class="help-block inline">{{ error }}</p> <p class="help-block inline">{{ error }}</p>
{% endfor %} {%- endfor %}
{% endif %} {%- endif %}
{% endmacro %} {%- endmacro %}
{% macro form_fields(fields, prepend='', append='', label=True) %} {%- macro form_fields(fields, prepend='', append='', label=True) %}
{% set width = (12 / fields|length)|int %} {%- set width = (12 / fields|length)|int %}
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
{% for field in fields %} {%- for field in fields %}
<div class="col-lg-{{ width }} col-xs-12 {{ 'has-error' if field.errors else '' }}"> <div class="col-lg-{{ width }} col-xs-12 {{ 'has-error' if field.errors else '' }}">
{%- if field.__class__.__name__ == 'list' %}
{%- for subfield in field %}
{{ form_individual_field(subfield, prepend=prepend, append=append, label=label, **kwargs) }}
{%- endfor %}
{%- else %}
{{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }} {{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }}
{%- endif %}
</div> </div>
{% endfor %} {%- endfor %}
</div> </div>
</div> </div>
{% endmacro %} {%- endmacro %}
{% macro form_individual_field(field, prepend='', append='', label=True, class_="") %} {%- macro form_individual_field(field, prepend='', append='', label=True, class_="") %}
{% if field.type == "BooleanField" %} {%- if field.type == "BooleanField" %}
{{ field(**kwargs) }}<span>&nbsp;&nbsp;</span> {{ field(**kwargs) }}<span>&nbsp;&nbsp;</span>{{ field.label if label else '' }}
{{ field.label if label else '' }} {%- else %}
{% else %}
{{ field.label if label else '' }}{{ form_field_errors(field) }} {{ field.label if label else '' }}{{ form_field_errors(field) }}
{% if prepend or append %}<div class="input-group">{% endif %} {%- if prepend %}<div class="input-group-prepend">{%- elif append %}<div class="input-group-append">{%- endif %}
{{ prepend|safe }}{{ field(class_="form-control " + class_, **kwargs) }}{{ append|safe }} {{ prepend|safe }}{{ field(class_=("form-control " + class_) if class_ else "form-control", **kwargs) }}{{ append|safe }}
{% if prepend or append %}</div>{% endif %} {%- if prepend or append %}</div>{%- endif %}
{% endif %} {%- endif %}
{% endmacro %} {%- endmacro %}
{% macro form_field(field) %} {%- macro form_field(field) %}
{% if field.type == 'SubmitField' %} {%- if field.type == 'SubmitField' %}
{{ form_fields((field,), label=False, class="btn btn-default", **kwargs) }} {{- form_fields((field,), label=False, class="btn btn-default", **kwargs) }}
{% else %} {%- else %}
{{ form_fields((field,), **kwargs) }} {{- form_fields((field,), **kwargs) }}
{% endif %} {%- endif %}
{% endmacro %} {%- endmacro %}
{% macro form(form) %} {%- macro form(form) %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% for field in form %} {%- for field in form %}
{% if bootstrap_is_hidden_field(field) %} {%- if bootstrap_is_hidden_field(field) %}
{{ field() }} {{ field() }}
{% else %} {%- else %}
{{ form_field(field) }} {{ form_field(field) }}
{% endif %} {%- endif %}
{% endfor %} {%- endfor %}
</form> </form>
{% endmacro %} {%- endmacro %}
{% macro box(title=None, theme="primary", header=True) %} {%- macro card(title=None, theme="primary", header=True) %}
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="box box-{{ theme }}"> <div class="card card-outline card-{{ theme }}">
{% if header %} {%- if header %}
<div class="box-header"> <div class="card-header border-0">
{% if title %} {%- if title %}
<h3 class="box-title">{{ title }}</h3> <h3 class="card-title">{{ title }}</h3>
{% endif %} {%- endif %}
</div> </div>
{% endif %} {%- endif %}
<div class="box-body"> <div class="card-body">
{{ caller() }} {{- caller() }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endmacro %} {%- endmacro %}
{% macro table(theme="primary") %} {%- macro table(title=None, theme="primary", datatable=True) %}
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="box box-{{ theme }}"> <div class="card card-outline card-{{ theme }}">
<table class="table table-bordered"> {%- if title %}
<tbody> <div class="card-header border-0">
{{ caller() }} <h3 class="card-title">{{ title }}</h3>
</tbody> </div>
{%- endif %}
<div class="card-body">
<table class="table table-bordered{% if datatable %} dataTable{% endif %}">
{{- caller() }}
</table> </table>
</div> </div>
</div> </div>
</div>
</div> </div>
{% endmacro %} {%- endmacro %}
{%- macro fieldset(title=None, field=None, enabled=None, fields=None) %}
{%- if field or title %}
<fieldset{% if not enabled %} disabled{% endif %}>
{%- if field %}
<legend>{{ form_individual_field(field) }}</legend>
{%- else %}
<legend>{{ title }}</legend>
{%- endif %}
{%- endif %}
{{- caller() }}
{%- if fields %}
{%- set kwargs = {"enabled" if enabled else "disabled": ""} %}
{%- for field in fields %}
{{ form_field(field, **kwargs) }}
{%- endfor %}
{%- endif %}
</fieldset>
{%- endmacro %}
{%- macro clip(target, title=_("copy to clipboard"), icon="copy", color="primary", action="copy") %}
<button class="btn btn-{{ color }} btn-xs btn-clip float-right ml-2 mt-1" data-clipboard-action="{{ action }}" data-clipboard-target="#{{ target }}">
<i class="fas fa-{{ icon }}" title="{{ title }}" aria-expanded="false"></i><span class="sr-only">{{ title }}</span>
</button>
{%- endmacro %}

@ -1,19 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Add a manager{% endtrans %} {% trans %}Add a manager{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.box() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.manager, class_='mailselect') }} {{ macros.form_field(form.manager, class_='mailselect') }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,30 +1,34 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Manager list{% endtrans %} {% trans %}Manager list{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary" href="{{ url_for('.manager_create', domain_name=domain.name) }}">{% trans %}Add manager{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.manager_create', domain_name=domain.name) }}">{% trans %}Add manager{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th> <th>{% trans %}Email{% endtrans %}</th>
</tr> </tr>
{% for manager in domain.managers %} </thead>
<tbody>
{%- for manager in domain.managers %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.manager_delete', domain_name=domain.name, user_email=manager.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.manager_delete', domain_name=domain.name, user_email=manager.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td> </td>
<td>{{ manager }}</td> <td>{{ manager }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} </tbody>
{% endblock %} {%- endcall %}
{%- endblock %}

@ -1,5 +1,5 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}New relay domain{% endtrans %} {% trans %}New relay domain{% endtrans %}
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Edit relayd domain{% endtrans %} {% trans %}Edit relayd domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ relay }} {{ relay }}
{% endblock %} {%- endblock %}

@ -1,37 +1,41 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Relayed domain list{% endtrans %} {% trans %}Relayed domain list{% endtrans %}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a class="btn btn-primary" href="{{ url_for('.relay_create') }}">{% trans %}New relayed domain{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.relay_create') }}">{% trans %}New relayed domain{% endtrans %}</a>
{% endif %} {%- endif %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Domain name{% endtrans %}</th> <th>{% trans %}Domain name{% endtrans %}</th>
<th>{% trans %}Remote host{% endtrans %}</th> <th>{% trans %}Remote host{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th> <th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th> <th>{% trans %}Last edit{% endtrans %}</th>
</tr> </tr>
{% for relay in relays %} </thead>
<tr> <tbody>
{%- for relay in relays %}
<tr>
<td> <td>
<a href="{{ url_for('.relay_edit', relay_name=relay.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp; <a href="{{ url_for('.relay_edit', relay_name=relay.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.relay_delete', relay_name=relay.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp; <a href="{{ url_for('.relay_delete', relay_name=relay.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp;
</td> </td>
<td>{{ relay.name }}</td> <td>{{ relay.name }}</td>
<td>{{ relay.smtp or '-' }}</td> <td>{{ relay.smtp or '-' }}</td>
<td>{{ relay.comment or '' }}</td> <td>{{ relay.comment or '' }}</td>
<td>{{ relay.created_at }}</td> <td>{{ relay.created_at | format_date }}</td>
<td>{{ relay.updated_at or '' }}</td> <td>{{ relay.updated_at | format_date }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} </tbody>
{% endblock %} {%- endcall %}
{%- endblock %}

@ -1,120 +1,156 @@
<section class="sidebar"> <div class="sidebar text-sm">
{% if current_user.is_authenticated %} {%- if current_user.is_authenticated %}
<h4 class="text-center text-primary">{{ current_user }}</h4> <div class="user-panel mt-3 pb-3 mb-3 d-flex">
{% endif %} <div class="image">
<div class="div-circle elevation-2"><i class="fa fa-user text-lg text-dark"></i></div>
</div>
<div class="info">
<a href="{{ url_for('.user_settings') }}" class="d-block">{{ current_user }}</a>
</div>
</div>
{%- endif %}
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" role="menu">
{%- if current_user.is_authenticated %}
<li class="nav-header text-uppercase text-primary" role="none">{% trans %}My account{% endtrans %}</li>
<li class="nav-item" role="none">
<a href="{{ url_for('.user_settings') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-wrench"></i>
<p>{% trans %}Settings{% endtrans %}</p>
</a>
</li>
<li class="nav-item" role="none">
<a href="{{ url_for('.user_password') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-lock"></i>
<p>{% trans %}Update password{% endtrans %}</p>
</a>
</li>
<li class="nav-item" role="none">
<a href="{{ url_for('.user_reply') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-plane"></i>
<p>{% trans %}Auto-reply{% endtrans %}</p>
</a>
</li>
<li class="nav-item" role="none">
<a href="{{ url_for('.fetch_list') }}" class="nav-link" role="menuitem">
<i class="nav-icon fas fa-download"></i>
<p>{% trans %}Fetched accounts{% endtrans %}</p>
</a>
</li>
<li class="nav-item" role="none">
<a href="{{ url_for('.token_list') }}" class="nav-link" role="menuitem">
<i class="nav-icon fas fa-ticket-alt"></i>
<p>{% trans %}Authentication tokens{% endtrans %}</p>
</a>
</li>
{%- if current_user.is_authenticated %}
<li class="nav-item" role="none">
<a href="{{ url_for('.client') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-laptop"></i>
<p>{% trans %}Client setup{% endtrans %}</p>
</a>
</li>
{%- endif %}
<ul class="sidebar-menu" data-widget="tree"> {%- if current_user.manager_of or current_user.global_admin %}
{% if current_user.is_authenticated %} <li class="nav-header text-uppercase text-primary" role="none">{% trans %}Administration{% endtrans %}</li>
<li class="header">{% trans %}My account{% endtrans %}</li> {%- endif %}
<li> {%- if current_user.global_admin %}
<a href="{{ url_for('.user_settings') }}"> <li class="nav-item" role="none">
<i class="fa fa-wrench"></i> <span>{% trans %}Settings{% endtrans %}</span> <a href="{{ url_for('.announcement') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-bullhorn"></i>
<p>{% trans %}Announcement{% endtrans %}</p>
</a> </a>
</li> </li>
<li> <li class="nav-item" role="none">
<a href="{{ url_for('.user_password') }}"> <a href="{{ url_for('.admin_list') }}" class="nav-link" role="menuitem">
<i class="fa fa-lock"></i> <span>{% trans %}Update password{% endtrans %}</span> <i class="nav-icon fa fa-user"></i>
<p>{% trans %}Administrators{% endtrans %}</p>
</a> </a>
</li> </li>
<li> <li class="nav-item" role="none">
<a href="{{ url_for('.user_reply') }}"> <a href="{{ url_for('.relay_list') }}" class="nav-link" role="menuitem">
<i class="fa fa-plane"></i> <span>{% trans %}Auto-reply{% endtrans %}</span> <i class="nav-icon fa fa-reply-all"></i>
<p>{% trans %}Relayed domains{% endtrans %}</p>
</a> </a>
</li> </li>
<li> <li class="nav-item" role="none">
<a href="{{ url_for('.fetch_list') }}"> <a href="{{ config["WEB_ADMIN"] }}/antispam/" data-clicked="{{ url_for('.antispam') }}" target="_blank" class="nav-link" role="menuitem">
<i class="fa fa-download"></i> <span>{% trans %}Fetched accounts{% endtrans %}</span> <i class="nav-icon fas fa-trash-alt"></i>
<p>{% trans %}Antispam{% endtrans %}</p>
</a> </a>
</li> </li>
<li> {%- endif %}
<a href="{{ url_for('.token_list') }}"> {%- if current_user.manager_of or current_user.global_admin %}
<i class="fa fa-ticket"></i> <span>{% trans %}Authentication tokens{% endtrans %}</span> <li class="nav-item" role="none">
<a href="{{ url_for('.domain_list') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-envelope"></i>
<p>{% trans %}Mail domains{% endtrans %}</p>
</a> </a>
</li> </li>
{%- endif %}
{%- endif %}
{% if current_user.manager_of or current_user.global_admin %} <li class="nav-header text-uppercase text-primary" role="none">{% trans %}Go to{% endtrans %}</li>
<li class="header">{% trans %}Administration{% endtrans %}</li> {%- if config["WEBMAIL"] != "none" and current_user.is_authenticated %}
{% endif %} <li class="nav-item" role="none">
{% if current_user.global_admin %} <a href="{{ config["WEB_WEBMAIL"] }}" target="_blank" class="nav-link" role="menuitem">
<li> <i class="nav-icon far fa-envelope"></i>
<a href="{{ url_for('.announcement') }}"> <p>{% trans %}Webmail{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
<i class="fa fa-bullhorn"></i> <span>{% trans %}Announcement{% endtrans %}</span>
</a> </a>
</li> </li>
<li> {%- endif %}
<a href="{{ url_for('.admin_list') }}"> {%- if not current_user.is_authenticated %}
<i class="fa fa-user"></i> <span>{% trans %}Administrators{% endtrans %}</span> <li class="nav-item" role="none">
<a href="{{ url_for('.client') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-laptop"></i>
<p>{% trans %}Client setup{% endtrans %}</p>
</a> </a>
</li> </li>
<li> {%- endif %}
<a href="{{ url_for('.relay_list') }}"> <li class="nav-item" role="none">
<i class="fa fa-reply-all"></i> <span>{% trans %}Relayed domains{% endtrans %}</span> <a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link" role="menuitem" rel="noreferrer">
<i class="nav-icon fa fa-globe"></i>
<p>{% trans %}Website{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
</a> </a>
</li> </li>
<li> <li class="nav-item" role="none">
<a href="{{ config["WEB_ADMIN"] }}/antispam/" target="_blank"> <a href="https://mailu.io" target="_blank" class="nav-link" role="menuitem" rel="noreferrer">
<i class="fa fa-trash-o"></i> <span>{% trans %}Antispam{% endtrans %}</span> <i class="nav-icon fa fa-life-ring"></i>
<p>{% trans %}Help{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
</a> </a>
</li> </li>
{% endif %} {%- if config['DOMAIN_REGISTRATION'] %}
{% if current_user.manager_of or current_user.global_admin %} <li class="nav-item" role="none">
<li> <a href="{{ url_for('.domain_signup') }}" class="nav-link" role="menuitem">
<a href="{{ url_for('.domain_list') }}"> <i class="nav-icon fa fa-plus-square"></i>
<i class="fa fa-envelope"></i> <span>{% trans %}Mail domains{% endtrans %}</span> <p>{% trans %}Register a domain{% endtrans %}</p>
</a> </a>
</li> </li>
{% endif %} {%- endif %}
{% endif %} {%- if current_user.is_authenticated %}
<li class="nav-item" role="none">
<li class="header">{% trans %}Go to{% endtrans %}</li> <a href="{{ url_for('sso.logout') }}" class="nav-link" role="menuitem">
{% if config["WEBMAIL"] != "none" %} <i class="nav-icon fas fa-sign-out-alt"></i>
<li> <p>{% trans %}Sign out{% endtrans %}</p>
<a href="{{ config["WEB_WEBMAIL"] }}" target="_blank">
<i class="fa fa-envelope-o"></i> <span>{% trans %}Webmail{% endtrans %}</span>
</a>
</li>
{% endif %}
<li>
<a href="{{ url_for('.client') }}">
<i class="fa fa-laptop"></i> <span>{% trans %}Client setup{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ config["WEBSITE"] }}" target="_blank">
<i class="fa fa-globe"></i> <span>{% trans %}Website{% endtrans %}</span>
</a>
</li>
<li>
<a href="https://mailu.io" target="_blank">
<i class="fa fa-life-ring"></i> <span>{% trans %}Help{% endtrans %}</span>
</a>
</li>
{% if config['DOMAIN_REGISTRATION'] %}
<li>
<a href="{{ url_for('.domain_signup') }}">
<i class="fa fa-plus-square"></i> <span>{% trans %}Register a domain{% endtrans %}</span>
</a>
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li>
<a href="{{ url_for('.logout') }}">
<i class="fa fa-sign-out"></i> <span>{% trans %}Sign out{% endtrans %}</span>
</a> </a>
</li> </li>
{% else %} {% else %}
<li> <li class="nav-item" role="none">
<a href="{{ url_for('.login') }}"> <a href="{{ url_for('sso.login') }}" class="nav-link" role="menuitem">
<i class="fa fa-sign-in"></i> <span>{% trans %}Sign in{% endtrans %}</span> <i class="nav-icon fas fa-sign-in-alt"></i>
<p>{% trans %}Sign in{% endtrans %}</p>
</a> </a>
</li> </li>
{% if signup_domains %} {%- if signup_domains %}
<li> <li class="nav-item" role="none">
<a href="{{ url_for('.user_signup') }}"> <a href="{{ url_for('.user_signup') }}" class="nav-link" role="menuitem">
<i class="fa fa-user-plus"></i> <span>{% trans %}Sign up{% endtrans %}</span> <i class="nav-icon fa fa-user-plus"></i>
<p>{% trans %}Sign up{% endtrans %}</p>
</a> </a>
</li> </li>
{% endif %} {%- endif %}
{% endif %} {%- endif %}
</ul> </ul>
</section> </nav>
</div>

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Create an authentication token{% endtrans %} {% trans %}Create an authentication token{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}

@ -1,34 +1,40 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Authentication tokens{% endtrans %} {% trans %}Authentication tokens{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary" href="{{ url_for('.token_create', user_email=user.email) }}">{% trans %}New token{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.token_create', user_email=user.email) }}">{% trans %}New token{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th> <th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Authorized IP{% endtrans %}</th> <th>{% trans %}Authorized IP{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
</tr> <th>{% trans %}Last edit{% endtrans %}</th>
{% for token in user.tokens %} </tr>
<tr> </thead>
<tbody>
{%- for token in user.tokens %}
<tr>
<td> <td>
<a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td> </td>
<td>{{ token.comment }}</td> <td>{{ token.comment }}</td>
<td>{{ token.ip or "any" }}</td> <td>{{ token.ip or "any" }}</td>
<td>{{ token.created_at }}</td> <td>{{ token.created_at | format_date }}</td>
</tr> <td>{{ token.updated_at | format_date }}</td>
{% endfor %} </tr>
{% endcall %} {%- endfor %}
{% endblock %} </tbody>
{%- endcall %}
{%- endblock %}

@ -1,33 +1,32 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}New user{% endtrans %} {% trans %}New user{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.box(_("General")) %} {%- call macros.card(_("General")) %}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }} {{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }} {{ macros.form_fields((form.pw, form.pw2)) }}
{{ macros.form_field(form.displayed_name) }} {{ macros.form_field(form.displayed_name) }}
{{ macros.form_field(form.comment) }} {{ macros.form_field(form.comment) }}
{{ macros.form_field(form.enabled) }} {{ macros.form_field(form.enabled) }}
{% endcall %} {%- endcall %}
{% call macros.box(_("Features and quotas"), theme="success") %} {%- call macros.card(_("Features and quotas"), theme="success") %}
{{ macros.form_field(form.quota_bytes, step=1000000000, max=(max_quota_bytes or domain.max_quota_bytes or 50000000000), {{ macros.form_field(form.quota_bytes, step=1000000000, max=(max_quota_bytes or domain.max_quota_bytes or 50*10**9), data_infinity="true",
prepend='<span class="input-group-addon"><span id="quota">'+((form.quota_bytes.data//1000000000).__str__() if form.quota_bytes.data else '∞')+'</span> GiB</span>', prepend='<span class="input-group-text"><span id="quota_bytes_value"></span>&nbsp;GB</span>') }}
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
{{ macros.form_field(form.enable_imap) }} {{ macros.form_field(form.enable_imap) }}
{{ macros.form_field(form.enable_pop) }} {{ macros.form_field(form.enable_pop) }}
{% endcall %} {%- endcall %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "user/create.html" %} {%- extends "user/create.html" %}
{% block title %} {%- block title %}
{% trans %}Edit user{% endtrans %} {% trans %}Edit user{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}

@ -1,25 +0,0 @@
{% extends "base.html" %}
{% block title %}
{% trans %}Forward emails{% endtrans %}
{% endblock %}
{% block subtitle %}
{{ user }}
{% endblock %}
{% block content %}
{% call macros.box() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.forward_enabled,
onchange="if(this.checked){$('#forward_destination,#forward_keep').removeAttr('disabled')}
else{$('#forward_destination,#forward_keep').attr('disabled', '')}") }}
{{ macros.form_field(form.forward_keep,
**{("enabled" if user.forward_enabled else "disabled"): ""}) }}
{{ macros.form_field(form.forward_destination,
**{("enabled" if user.forward_enabled else "disabled"): ""}) }}
{{ macros.form_field(form.submit) }}
</form>
{% endcall %}
{% endblock %}

@ -1,20 +1,21 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}User list{% endtrans %} {% trans %}User list{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary" href="{{ url_for('.user_create', domain_name=domain.name) }}">{% trans %}Add user{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.user_create', domain_name=domain.name) }}">{% trans %}Add user{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}User settings{% endtrans %}</th> <th>{% trans %}User settings{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th> <th>{% trans %}Email{% endtrans %}</th>
@ -23,11 +24,13 @@
<th>{% trans %}Comment{% endtrans %}</th> <th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th> <th>{% trans %}Last edit{% endtrans %}</th>
</tr> </tr>
{% for user in domain.users %} </thead>
<tr {% if not user.enabled %}class="warning"{% endif %}> <tbody>
{%- for user in domain.users %}
<tr{% if not user.enabled %} class="warning"{% endif %}>
<td> <td>
<a href="{{ url_for('.user_edit', user_email=user.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp; <a href="{{ url_for('.user_edit', user_email=user.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp;
<a href="{{ url_for('.user_delete', user_email=user.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.user_delete', user_email=user.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td> </td>
<td> <td>
@ -37,14 +40,15 @@
</td> </td>
<td>{{ user }}</td> <td>{{ user }}</td>
<td> <td>
{% if user.enable_imap %}<span class="label label-info">imap</span>{% endif %} {% if user.enable_imap %}<span class="badge bg-info">imap</span>{% endif %}
{% if user.enable_pop %}<span class="label label-info">pop3</span>{% endif %} {% if user.enable_pop %}<span class="badge bg-info">pop3</span>{% endif %}
</td> </td>
<td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td> <td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td>
<td>{{ user.comment or '-' }}</td> <td>{{ user.comment or '-' }}</td>
<td>{{ user.created_at }}</td> <td>{{ user.created_at | format_date }}</td>
<td>{{ user.updated_at or '' }}</td> <td>{{ user.updated_at | format_date }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} </tbody>
{% endblock %} {%- endcall %}
{%- endblock %}

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Password update{% endtrans %} {% trans %}Password update{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}

@ -1,30 +1,23 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Automatic reply{% endtrans %} {% trans %}Automatic reply{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.box() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.reply_enabled, {%- call macros.fieldset(
onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').removeAttr('readonly')} field=form.reply_enabled,
else{$('#reply_subject,#reply_body,#reply_enddate').attr('readonly', '')}") }} enabled=user.reply_enabled,
{{ macros.form_field(form.reply_subject, fields=[form.reply_subject, form.reply_body, form.reply_enddate, form.reply_startdate]) %}
**{("rw" if user.reply_enabled else "readonly"): ""}) }} {%- endcall %}
{{ macros.form_field(form.reply_body, rows=10,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.reply_enddate,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.reply_startdate,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,38 +1,36 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}User settings{% endtrans %} {% trans %}User settings{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.box(title=_("Displayed name")) %} {%- call macros.card(title=_("Displayed name")) %}
{{ macros.form_field(form.displayed_name) }} {{ macros.form_field(form.displayed_name) }}
{% endcall %} {%- endcall %}
{% call macros.box(title=_("Antispam")) %} {%- call macros.card(title=_("Antispam")) %}
{{ macros.form_field(form.spam_enabled) }} {%- call macros.fieldset(field=form.spam_enabled, enabled=user.spam_enabled) %}
{{ macros.form_field(form.spam_threshold, step=1, max=100, {{ macros.form_field(form.spam_threshold, step=1, max=100,
prepend='<span class="input-group-addon"><span id="threshold">'+form.spam_threshold.data.__str__()+'</span>&nbsp;/&nbsp;100</span>', prepend='<span class="input-group-text"><span id="spam_threshold_value"></span>&nbsp;/&nbsp;100</span>') }}
oninput='$("#threshold").text(this.value);') }} {%- endcall %}
{% endcall %} {%- endcall %}
{% call macros.box(title=_("Auto-forward")) %} {%- call macros.card(title=_("Auto-forward")) %}
{{ macros.form_field(form.forward_enabled, {%- call macros.fieldset(
onchange="if(this.checked){$('#forward_destination,#forward_keep').removeAttr('disabled')} field=form.forward_enabled,
else{$('#forward_destination,#forward_keep').attr('disabled', '')}") }} enabled=user.forward_enabled,
{{ macros.form_field(form.forward_keep, fields=[form.forward_keep, form.forward_destination]) %}
**{("enabled" if user.forward_enabled else "disabled"): ""}) }} {%- endcall %}
{{ macros.form_field(form.forward_destination, {%- endcall %}
**{("enabled" if user.forward_enabled else "disabled"): ""}) }}
{% endcall %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,23 +1,23 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Sign up{% endtrans %} {% trans %}Sign up{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.box() %} {%- call macros.card() %}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }} {{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }} {{ macros.form_fields((form.pw, form.pw2)) }}
{% if form.captcha %} {%- if form.captcha %}
{{ macros.form_field(form.captcha) }} {{ macros.form_field(form.captcha) }}
{% endif %} {%- endif %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
{% endcall %} {%- endcall %}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,26 +1,26 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Sign up{% endtrans %} {% trans %}Sign up{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{% trans %}pick a domain for the new account{% endtrans %} {% trans %}pick a domain for the new account{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <tr>
<th>{% trans %}Domain{% endtrans %}</th> <th>{% trans %}Domain{% endtrans %}</th>
<th>{% trans %}Available slots{% endtrans %}</th> <th>{% trans %}Available slots{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th> <th>{% trans %}Quota{% endtrans %}</th>
</tr> </tr>
{% for domain_name, domain in available_domains.items() %} {%- for domain_name, domain in available_domains.items() %}
<tr> <tr>
<td><a href="{{ url_for('.user_signup', domain_name=domain_name) }}">{{ domain_name }}</a></td> <td><a href="{{ url_for('.user_signup', domain_name=domain_name) }}">{{ domain_name }}</a></td>
<td>{{ '∞' if domain.max_users == -1 else domain.max_users - (domain.users | count)}}</td> <td>{{ '∞' if domain.max_users == -1 else domain.max_users - (domain.users | count)}}</td>
<td>{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }}</td> <td>{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,5 +1,5 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block content %} {%- block content %}
<div class="alert alert-warning" role="alert">{% trans %}We are still working on this feature!{% endtrans %}</div> <div class="alert alert-warning" role="alert">{% trans %}We are still working on this feature!{% endtrans %}</div>
{% endblock %} {%- endblock %}

@ -1,4 +1,4 @@
__all__ = [ __all__ = [
'admins', 'aliases', 'alternatives', 'base', 'domains', 'fetches', 'admins', 'aliases', 'alternatives', 'base', 'domains', 'fetches',
'managers', 'users', 'relays', 'tokens' 'managers', 'users', 'relays', 'tokens', 'languages'
] ]

@ -1,6 +1,7 @@
from mailu import models from mailu import models, utils
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
from flask import current_app as app
import flask import flask
import flask_login import flask_login
@ -10,29 +11,6 @@ import flask_login
def index(): def index():
return flask.redirect(flask.url_for('.user_settings')) return flask.redirect(flask.url_for('.user_settings'))
@ui.route('/login', methods=['GET', 'POST'])
def login():
form = forms.LoginForm()
if form.validate_on_submit():
user = models.User.login(form.email.data, form.pw.data)
if user:
flask_login.login_user(user)
endpoint = flask.request.args.get('next', '.index')
return flask.redirect(flask.url_for(endpoint)
or flask.url_for('.index'))
else:
flask.flash('Wrong e-mail or password', 'error')
return flask.render_template('login.html', form=form)
@ui.route('/logout', methods=['GET'])
@access.authenticated
def logout():
flask_login.logout_user()
return flask.redirect(flask.url_for('.index'))
@ui.route('/announcement', methods=['GET', 'POST']) @ui.route('/announcement', methods=['GET', 'POST'])
@access.global_admin @access.global_admin
def announcement(): def announcement():
@ -47,7 +25,14 @@ def announcement():
flask.flash('Your announcement was sent', 'success') flask.flash('Your announcement was sent', 'success')
return flask.render_template('announcement.html', form=form) return flask.render_template('announcement.html', form=form)
@ui.route('/webmail', methods=['GET'])
def webmail():
return flask.redirect(app.config['WEB_WEBMAIL'])
@ui.route('/client', methods=['GET']) @ui.route('/client', methods=['GET'])
def client(): def client():
return flask.render_template('client.html') return flask.render_template('client.html')
@ui.route('/webui_antispam', methods=['GET'])
def antispam():
return flask.render_template('antispam.html')

@ -5,7 +5,6 @@ from flask import current_app as app
import flask import flask
import flask_login import flask_login
import wtforms_components import wtforms_components
import dns.resolver
@ui.route('/domain', methods=['GET']) @ui.route('/domain', methods=['GET'])
@ -74,6 +73,8 @@ def domain_details(domain_name):
def domain_genkeys(domain_name): def domain_genkeys(domain_name):
domain = models.Domain.query.get(domain_name) or flask.abort(404) domain = models.Domain.query.get(domain_name) or flask.abort(404)
domain.generate_dkim_key() domain.generate_dkim_key()
models.db.session.add(domain)
models.db.session.commit()
return flask.redirect( return flask.redirect(
flask.url_for(".domain_details", domain_name=domain_name)) flask.url_for(".domain_details", domain_name=domain_name))

@ -0,0 +1,9 @@
from mailu.ui import ui, forms, access
import flask
@ui.route('/language/<language>', methods=['POST'])
def set_language(language=None):
flask.session['language'] = language
return flask.Response(status=200)

@ -26,7 +26,7 @@ def token_create(user_email):
form = forms.TokenForm() form = forms.TokenForm()
wtforms_components.read_only(form.displayed_password) wtforms_components.read_only(form.displayed_password)
if not form.raw_password.data: if not form.raw_password.data:
form.raw_password.data = pwd.genword(entropy=128, charset="hex") form.raw_password.data = pwd.genword(entropy=128, length=32, charset="hex")
form.displayed_password.data = form.raw_password.data form.displayed_password.data = form.raw_password.data
if form.validate_on_submit(): if form.validate_on_submit():
token = models.Token(user=user) token = models.Token(user=user)

@ -119,6 +119,7 @@ def user_password(user_email):
if form.pw.data != form.pw2.data: if form.pw.data != form.pw2.data:
flask.flash('Passwords do not match', 'error') flask.flash('Passwords do not match', 'error')
else: else:
flask.session.regenerate()
user.set_password(form.pw.data) user.set_password(form.pw.data)
models.db.session.commit() models.db.session.commit()
flask.flash('Password updated for %s' % user) flask.flash('Password updated for %s' % user)
@ -128,23 +129,6 @@ def user_password(user_email):
return flask.render_template('user/password.html', form=form, user=user) return flask.render_template('user/password.html', form=form, user=user)
@ui.route('/user/forward', methods=['GET', 'POST'], defaults={'user_email': None})
@ui.route('/user/forward/<path:user_email>', methods=['GET', 'POST'])
@access.owner(models.User, 'user_email')
def user_forward(user_email):
user_email_or_current = user_email or flask_login.current_user.email
user = models.User.query.get(user_email_or_current) or flask.abort(404)
form = forms.UserForwardForm(obj=user)
if form.validate_on_submit():
form.populate_obj(user)
models.db.session.commit()
flask.flash('Forward destination updated for %s' % user)
if user_email:
return flask.redirect(
flask.url_for('.user_list', domain_name=user.domain.name))
return flask.render_template('user/forward.html', form=form, user=user)
@ui.route('/user/reply', methods=['GET', 'POST'], defaults={'user_email': None}) @ui.route('/user/reply', methods=['GET', 'POST'], defaults={'user_email': None})
@ui.route('/user/reply/<path:user_email>', methods=['GET', 'POST']) @ui.route('/user/reply/<path:user_email>', methods=['GET', 'POST'])
@access.owner(models.User, 'user_email') @access.owner(models.User, 'user_email')
@ -186,6 +170,7 @@ def user_signup(domain_name=None):
if domain.has_email(form.localpart.data): if domain.has_email(form.localpart.data):
flask.flash('Email is already used', 'error') flask.flash('Email is already used', 'error')
else: else:
flask.session.regenerate()
user = models.User(domain=domain) user = models.User(domain=domain)
form.populate_obj(user) form.populate_obj(user)
user.set_password(form.pw.data) user.set_password(form.pw.data)

@ -1,46 +1,115 @@
from mailu import models, limiter """ Mailu admin app utilities
"""
try:
import cPickle as pickle
except ImportError:
import pickle
import dns.resolver
import dns.exception
import dns.flags
import dns.rdtypes
import dns.rdatatype
import dns.rdataclass
import hmac
import secrets
import time
from multiprocessing import Value
from mailu import limiter
from flask import current_app as app
import flask import flask
import flask_login import flask_login
import flask_script
import flask_migrate import flask_migrate
import flask_babel import flask_babel
import ipaddress
import redis
from werkzeug.contrib import fixers from flask.sessions import SessionMixin, SessionInterface
from itsdangerous.encoding import want_bytes
from werkzeug.datastructures import CallbackDict
from werkzeug.middleware.proxy_fix import ProxyFix
# Login configuration # Login configuration
login = flask_login.LoginManager() login = flask_login.LoginManager()
login.login_view = "ui.login" login.login_view = "sso.login"
@login.unauthorized_handler @login.unauthorized_handler
def handle_needs_login(): def handle_needs_login():
""" redirect unauthorized requests to login page """
return flask.redirect( return flask.redirect(
flask.url_for('ui.login', next=flask.request.endpoint) flask.url_for('sso.login')
) )
# DNS stub configured to do DNSSEC enabled queries
resolver = dns.resolver.Resolver()
resolver.use_edns(0, 0, 1232)
resolver.flags = dns.flags.AD | dns.flags.RD
def has_dane_record(domain, timeout=10):
try:
result = resolver.query(f'_25._tcp.{domain}', dns.rdatatype.TLSA,dns.rdataclass.IN, lifetime=timeout)
if result.response.flags & dns.flags.AD:
for record in result:
if isinstance(record, dns.rdtypes.ANY.TLSA.TLSA):
record.validate()
if record.usage in [2,3] and record.selector in [0,1] and record.mtype in [0,1,2]:
return True
except dns.resolver.NoNameservers:
# If the DNSSEC data is invalid and the DNS resolver is DNSSEC enabled
# we will receive this non-specific exception. The safe behaviour is to
# accept to defer the email.
app.logger.warn(f'Unable to lookup the TLSA record for {domain}. Is the DNSSEC zone okay on https://dnsviz.net/d/{domain}/dnssec/?')
return app.config['DEFER_ON_TLS_ERROR']
except dns.exception.Timeout:
app.logger.warn(f'Timeout while resolving the TLSA record for {domain} ({timeout}s).')
except dns.resolver.NXDOMAIN:
pass # this is expected, not TLSA record is fine
except Exception as e:
app.logger.error(f'Error while looking up the TLSA record for {domain} {e}')
pass
# Rate limiter # Rate limiter
limiter = limiter.LimitWraperFactory() limiter = limiter.LimitWraperFactory()
def extract_network_from_ip(ip):
n = ipaddress.ip_network(ip)
if n.version == 4:
return str(n.supernet(prefixlen_diff=(32-int(app.config["AUTH_RATELIMIT_IP_V4_MASK"]))).network_address)
else:
return str(n.supernet(prefixlen_diff=(128-int(app.config["AUTH_RATELIMIT_IP_V6_MASK"]))).network_address)
def is_exempt_from_ratelimits(ip):
ip = ipaddress.ip_address(ip)
return any(ip in cidr for cidr in app.config['AUTH_RATELIMIT_EXEMPTION'])
# Application translation # Application translation
babel = flask_babel.Babel() babel = flask_babel.Babel()
@babel.localeselector @babel.localeselector
def get_locale(): def get_locale():
translations = list(map(str, babel.list_translations())) """ selects locale for translation """
return flask.request.accept_languages.best_match(translations) language = flask.session.get('language')
if not language in app.config.translations:
language = flask.request.accept_languages.best_match(app.config.translations.keys())
flask.session['language'] = language
return language
# Proxy fixer # Proxy fixer
class PrefixMiddleware(object): class PrefixMiddleware(object):
""" fix proxy headers """
def __init__(self):
self.app = None
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
prefix = environ.get('HTTP_X_FORWARDED_PREFIX', '')
if prefix:
environ['SCRIPT_NAME'] = prefix
return self.app(environ, start_response) return self.app(environ, start_response)
def init_app(self, app): def init_app(self, app):
self.app = fixers.ProxyFix(app.wsgi_app, x_for=1, x_proto=1) self.app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
app.wsgi_app = self app.wsgi_app = self
proxy = PrefixMiddleware() proxy = PrefixMiddleware()
@ -48,3 +117,384 @@ proxy = PrefixMiddleware()
# Data migrate # Data migrate
migrate = flask_migrate.Migrate() migrate = flask_migrate.Migrate()
# session store (inspired by https://github.com/mbr/flask-kvsession)
class RedisStore:
""" Stores session data in a redis db. """
has_ttl = True
def __init__(self, redisstore):
self.redis = redisstore
def get(self, key):
""" load item from store. """
value = self.redis.get(key)
if value is None:
raise KeyError(key)
return value
def put(self, key, value, ttl=None):
""" save item to store. """
if ttl:
self.redis.setex(key, int(ttl), value)
else:
self.redis.set(key, value)
def delete(self, key):
""" delete item from store. """
self.redis.delete(key)
def list(self, prefix=None):
""" return list of keys starting with prefix """
if prefix:
prefix += b'*'
return list(self.redis.scan_iter(match=prefix))
class DictStore:
""" Stores session data in a python dict. """
has_ttl = False
def __init__(self):
self.dict = {}
def get(self, key):
""" load item from store. """
return self.dict[key]
def put(self, key, value, ttl_secs=None):
""" save item to store. """
self.dict[key] = value
def delete(self, key):
""" delete item from store. """
try:
del self.dict[key]
except KeyError:
pass
def list(self, prefix=None):
""" return list of keys starting with prefix """
if prefix is None:
return list(self.dict.keys())
return [key for key in self.dict if key.startswith(prefix)]
class MailuSession(CallbackDict, SessionMixin):
""" Custom flask session storage. """
# default modified to false
modified = False
def __init__(self, key=None, app=None):
self.app = app or flask.current_app
initial = None
key = want_bytes(key)
if parsed := self.app.session_config.parse_key(key, self.app):
try:
initial = pickle.loads(app.session_store.get(key))
except (KeyError, EOFError, pickle.UnpicklingError):
# either the cookie was manipulated or we did not find the
# session in the backend or the pickled data is invalid.
# => start new session
pass
else:
(self._uid, self._sid, self._created) = parsed
self._key = key
if initial is None:
# start new session
self.new = True
self._uid = None
self._sid = None
self._created = self.app.session_config.gen_created()
self._key = None
def _on_update(obj):
obj.modified = True
CallbackDict.__init__(self, initial, _on_update)
@property
def saved(self):
""" this reflects if the session was saved. """
return self._key is not None
@property
def sid(self):
""" this reflects the session's id. """
if self._sid is None or self._uid is None or self._created is None:
return None
return b''.join([self._uid, self._sid, self._created])
def destroy(self):
""" destroy session for security reasons. """
self.delete()
self._uid = None
self._sid = None
self._created = None
self.clear()
self.modified = True
self.new = False
def regenerate(self):
""" generate new id for session to avoid `session fixation`. """
self.delete()
self._sid = None
self._created = self.app.session_config.gen_created()
self.modified = True
def delete(self):
""" Delete stored session. """
if self.saved:
self.app.session_store.delete(self._key)
self._key = None
def save(self):
""" Save session to store. """
set_cookie = False
# set uid from dict data
if self._uid is None:
self._uid = self.app.session_config.gen_uid(self.get('_user_id', ''))
# create new session id for new or regenerated sessions and force setting the cookie
if self._sid is None:
self._sid = self.app.session_config.gen_sid()
set_cookie = True
# get new session key
key = self.sid
# delete old session if key has changed
if key != self._key:
self.delete()
# remember time to refresh
self['_refresh'] = int(time.time()) + self.app.permanent_session_lifetime.total_seconds()/2
# save session
self.app.session_store.put(
key,
pickle.dumps(dict(self)),
self.app.permanent_session_lifetime.total_seconds()
)
self._key = key
self.new = False
self.modified = False
return set_cookie
def needs_refresh(self):
""" Checks if server side session needs to be refreshed. """
return int(time.time()) > self.get('_refresh', 0)
class MailuSessionConfig:
""" Stores sessions crypto config """
# default size of session key parts
uid_bits = 64 # default if SESSION_KEY_BITS is not set in config
sid_bits = 128 # for now. must be multiple of 8!
time_bits = 32 # for now. must be multiple of 8!
def __init__(self, app=None):
if app is None:
app = flask.current_app
bits = app.config.get('SESSION_KEY_BITS', self.uid_bits)
if not 64 <= bits <= 256:
raise ValueError('SESSION_KEY_BITS must be between 64 and 256!')
uid_bytes = bits//8 + (bits%8>0)
sid_bytes = self.sid_bits//8
key = want_bytes(app.secret_key)
self._hmac = hmac.new(hmac.digest(key, b'SESSION_UID_HASH', digest='sha256'), digestmod='sha256')
self._uid_len = uid_bytes
self._uid_b64 = len(self._encode(bytes(uid_bytes)))
self._sid_len = sid_bytes
self._sid_b64 = len(self._encode(bytes(sid_bytes)))
self._key_min = self._uid_b64 + self._sid_b64
self._key_max = self._key_min + len(self._encode(bytes(self.time_bits//8)))
def gen_sid(self):
""" Generate random session id. """
return self._encode(secrets.token_bytes(self._sid_len))
def gen_uid(self, uid):
""" Generate hashed user id part of session key. """
_hmac = self._hmac.copy()
_hmac.update(want_bytes(uid))
return self._encode(_hmac.digest()[:self._uid_len])
def gen_created(self, now=None):
""" Generate base64 representation of creation time. """
return self._encode(int(now or time.time()).to_bytes(8, byteorder='big').lstrip(b'\0'))
def parse_key(self, key, app=None, validate=False, now=None):
""" Split key into sid, uid and creation time. """
if not (isinstance(key, bytes) and self._key_min <= len(key) <= self._key_max):
return None
uid = key[:self._uid_b64]
sid = key[self._uid_b64:self._key_min]
crt = key[self._key_min:]
# validate if parts are decodeable
created = self._decode(crt)
if created is None or self._decode(uid) is None or self._decode(sid) is None:
return None
# validate creation time when requested or store does not support ttl
if validate or not app.session_store.has_ttl:
if now is None:
now = int(time.time())
created = int.from_bytes(created, byteorder='big')
if not created < now < created + app.permanent_session_lifetime.total_seconds():
return None
return (uid, sid, crt)
def _encode(self, value):
return secrets.base64.urlsafe_b64encode(value).rstrip(b'=')
def _decode(self, value):
try:
return secrets.base64.urlsafe_b64decode(value + b'='*(4-len(value)%4))
except secrets.binascii.Error:
return None
class MailuSessionInterface(SessionInterface):
""" Custom flask session interface. """
def open_session(self, app, request):
""" Load or create session. """
return MailuSession(request.cookies.get(app.config['SESSION_COOKIE_NAME'], None), app)
def save_session(self, app, session, response):
""" Save modified session. """
# If the session is modified to be empty, remove the cookie.
# If the session is empty, return without setting the cookie.
if not session:
if session.modified:
session.delete()
response.delete_cookie(
app.session_cookie_name,
domain=self.get_cookie_domain(app),
path=self.get_cookie_path(app),
)
return
# Add a "Vary: Cookie" header if the session was accessed
if session.accessed:
response.vary.add('Cookie')
set_cookie = session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST']
need_refresh = session.needs_refresh()
# save modified session or refresh unmodified session
if session.modified or need_refresh:
set_cookie |= session.save()
# set cookie on refreshed permanent sessions
if need_refresh and session.permanent:
set_cookie = True
# set or update cookie if necessary
if set_cookie:
response.set_cookie(
app.session_cookie_name,
session.sid,
expires=self.get_expiration_time(app, session),
httponly=self.get_cookie_httponly(app),
domain=self.get_cookie_domain(app),
path=self.get_cookie_path(app),
secure=self.get_cookie_secure(app),
samesite=self.get_cookie_samesite(app)
)
class MailuSessionExtension:
""" Server side session handling """
@staticmethod
def cleanup_sessions(app=None):
""" Remove invalid or expired sessions. """
app = app or flask.current_app
now = int(time.time())
count = 0
for key in app.session_store.list():
if not app.session_config.parse_key(key, app, validate=True, now=now):
app.session_store.delete(key)
count += 1
return count
@staticmethod
def prune_sessions(uid=None, keep=None, app=None):
""" Remove sessions
uid: remove all sessions (NONE) or sessions belonging to a specific user
keep: keep listed sessions
"""
keep = keep or set()
app = app or flask.current_app
prefix = None if uid is None else app.session_config.gen_uid(uid)
count = 0
for key in app.session_store.list(prefix):
if key not in keep:
app.session_store.delete(key)
count += 1
return count
def init_app(self, app):
""" Replace session management of application. """
if app.config.get('MEMORY_SESSIONS'):
# in-memory session store for use in development
app.session_store = DictStore()
else:
# redis-based session store for use in production
app.session_store = RedisStore(
redis.StrictRedis().from_url(app.config['SESSION_STORAGE_URL'])
)
# clean expired sessions oonce on first use in case lifetime was changed
def cleaner():
with cleaned.get_lock():
if not cleaned.value:
cleaned.value = True
app.logger.info('cleaning session store')
MailuSessionExtension.cleanup_sessions(app)
app.before_first_request(cleaner)
app.session_config = MailuSessionConfig(app)
app.session_interface = MailuSessionInterface()
cleaned = Value('i', False)
session = MailuSessionExtension()

@ -1,10 +1,12 @@
from __future__ import with_statement import logging
import tenacity
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig from logging.config import fileConfig
import logging
import tenacity from flask import current_app
from tenacity import retry from mailu import models
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
@ -17,20 +19,12 @@ logger = logging.getLogger('alembic.env')
# add your model's MetaData object here # add your model's MetaData object here
# for 'autogenerate' support # for 'autogenerate' support
# from myapp import mymodel config.set_main_option(
# target_metadata = mymodel.Base.metadata 'sqlalchemy.url',
from flask import current_app current_app.config.get('SQLALCHEMY_DATABASE_URI')
config.set_main_option('sqlalchemy.url', )
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
#target_metadata = current_app.extensions['migrate'].db.metadata
from mailu import models
target_metadata = models.Base.metadata target_metadata = models.Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline(): def run_migrations_offline():
"""Run migrations in 'offline' mode. """Run migrations in 'offline' mode.
@ -44,7 +38,7 @@ def run_migrations_offline():
script output. script output.
""" """
url = config.get_main_option("sqlalchemy.url") url = config.get_main_option('sqlalchemy.url')
context.configure(url=url) context.configure(url=url)
with context.begin_transaction(): with context.begin_transaction():
@ -69,27 +63,34 @@ def run_migrations_online():
directives[:] = [] directives[:] = []
logger.info('No changes in schema detected.') logger.info('No changes in schema detected.')
engine = engine_from_config(config.get_section(config.config_ini_section), engine = engine_from_config(
prefix='sqlalchemy.', config.get_section(config.config_ini_section),
poolclass=pool.NullPool) prefix = 'sqlalchemy.',
poolclass = pool.NullPool
)
connection = tenacity.Retrying( @tenacity.retry(
stop=tenacity.stop_after_attempt(100), stop = tenacity.stop_after_attempt(100),
wait=tenacity.wait_random(min=2, max=5), wait = tenacity.wait_random(min=2, max=5),
before=tenacity.before_log(logging.getLogger("tenacity.retry"), logging.DEBUG), before = tenacity.before_log(logging.getLogger('tenacity.retry'), logging.DEBUG),
before_sleep=tenacity.before_sleep_log(logging.getLogger("tenacity.retry"), logging.INFO), before_sleep = tenacity.before_sleep_log(logging.getLogger('tenacity.retry'), logging.INFO),
after=tenacity.after_log(logging.getLogger("tenacity.retry"), logging.DEBUG) after = tenacity.after_log(logging.getLogger('tenacity.retry'), logging.DEBUG)
).call(engine.connect) )
def try_connect(db):
return db.connect()
context.configure(connection=connection, with try_connect(engine) as connection:
target_metadata=target_metadata,
process_revision_directives=process_revision_directives, context.configure(
**current_app.extensions['migrate'].configure_args) connection = connection,
target_metadata = target_metadata,
process_revision_directives = process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
try:
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
finally:
connection.close() connection.close()
if context.is_offline_mode(): if context.is_offline_mode():

@ -2,34 +2,29 @@
"name": "mailu", "name": "mailu",
"version": "1.0.0", "version": "1.0.0",
"description": "Mailu admin assets", "description": "Mailu admin assets",
"main": "assest/index.js", "main": "assets/index.js",
"directories": {
"lib": "lib"
},
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@babel/core": "^7.4.4", "admin-lte": "^3.1.0",
"@babel/preset-env": "^7.4.4", "babel-loader": "^8.2.2",
"admin-lte": "^2.4.10", "clipboard": "^2.0.8",
"babel-loader": "^8.0.5", "compression-webpack-plugin": "^8.0.1",
"bootstrap": "^3.4.1", "css-loader": "^6.2.0",
"css-loader": "^2.1.1", "css-minimizer-webpack-plugin": "^3.0.2",
"expose-loader": "^0.7.5", "datatables.net-plugins": "^1.10.24",
"file-loader": "^3.0.1", "import-glob": "^1.5.0",
"font-awesome": "^4.7.0", "less-loader": "^10.0.1",
"font-awesome-loader": "^1.0.2", "mini-css-extract-plugin": "^2.2.0",
"jQuery": "^1.7.4", "sass": "<1.33.0",
"less": "^3.9.0", "sass-loader": "^12.1.0",
"less-loader": "^5.0.0", "terser-webpack-plugin": "^5.2.0",
"mini-css-extract-plugin": "^0.6.0", "webpack-cli": "^4.8.0"
"node-sass": "^4.12.0",
"popper.js": "^1.15.0",
"sass-loader": "^7.1.0",
"select2": "^4.0.7-rc.0",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.2"
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save