Compare commits

...

642 Commits
master ... main

Author SHA1 Message Date
lub 2506bc3a7f keep key during certificate renewal 1 year ago
bors[bot] b243ea084d
Merge #2719
2719: Introduce connection string (database url) for roundcube & simplify setup r=mergify[bot] a=Diman0

## What type of PR?

enhancement

## What does this PR do?
As discussed in earlier Mailu meetings (#1582), we want to simplify configuring Mailu and make it more user-friendly. Especially the last part is an important mission statement of the Mailu project. 

This PR will remove the choice of what DB to use from setup. New users are guided now to make the correct choice of using SQLite.

For simplifying the configuration, all the database environment variables have been removed and replaced with a single connection string environment variable. 

For backwards compatibility, the old *DB_* setting can still be used. This is to make sure that master does not immediately break for all users. After X months after the next Mailu release, we can remove the old settings from the software. This provides a transition period. 

### Related issue(s)
- #2533

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
1 year ago
Florent Daigniere 5790b0a84b
Rephrase the doc 1 year ago
Dimitri Huisman 6d31831cf5
Sigh. Forgot to actually save the modified requirements-dev.txt file.
Remove the pinned version for requirements for dev.
The blocking issue is resolved, so no need to pin the old version.
1 year ago
Dimitri Huisman 709edb522b
Introduce connection string (database url) for roundcube.
Remove database choice from setup.
Remove the old *DB_* database env variables from the documentation.
The env vars are deprecated now. They will be removed after the upcoming
Mailu release.
1 year ago
bors[bot] 6f3a01e31c
Merge #2712
2712: Build multi-arch images, retry mechanism for builds, improved releases r=mergify[bot] a=Diman0

## What type of PR?

feature

## What does this PR do?
Switch to building multi-arch images. The images build for pull requests, master and production are now multi-arch images for the architectures:
* linux/amd64
* linux/arm64/v8
* linux/arm/v7

Enhance CI/CD workflow with retry functionality. All steps for building images are now automatically retried. If a build temporarily fails due to a network error, the retried step will still succeed.

Provide a changelog for minor releases. The github release will now:
* Provide the changelog message from the newsfragment of the PR that triggered the backport.
* Provide a github link to the PR/issue of the PR that was backported.

### Related issue(s)
- #1582
- #1200
- #2215
- closes #2653

## Prerequisites
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.

- [x ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
1 year ago
Florent Daigniere 46e803fdff
Update setup.rst 1 year ago
Florent Daigniere 61a40e203a
Update docs/setup.rst 1 year ago
Florent Daigniere cb5e0934cb
Update docs/setup.rst 1 year ago
Florent Daigniere c9df6161ba
Update docs/compose/requirements.rst 1 year ago
Florent Daigniere 8391936dc2
Update docs/compose/requirements.rst 1 year ago
Dimitri Huisman a2c811d28a
Prevent creation of unknown/unknown arch.
Set more forgiving timeouts for scenario where image is build without cache.
Set better readable tags.
1 year ago
Dimitri Huisman 1d2053204a
Also forgot the --push argument. 1 year ago
Dimitri Huisman 64a132fdd9
Forgot to change the target. 1 year ago
Dimitri Huisman 5bd528319b
Provide a changelog for minor releases. The github release will now:
* Provide the changelog message from the newsfragment of the PR that triggered the backport.
* Provide a github link to the PR/issue of the PR that was backported.

Switch to building multi-arch images. The images build for pull requests, master and production
are now multi-arch images for the architectures:
* linux/amd64
* linux/arm64/v8
* linux/arm/v7

Enhance CI/CD workflow with retry functionality. All steps for building images are now automatically
retried. If a build temporarily fails due to a network error, the retried step will still succeed.
1 year ago
bors[bot] 5044c78740
Merge #2709
2709: Validate proxy ip with PROXY_AUTH_WHITELIST r=mergify[bot] a=Diman0

## What type of PR?

bug fix

## What does this PR do?
The Proxy code validated the real client ip against the proxy auth whitelist. It should be the proxy ip that is checked. That is changed with this PR.

### Related issue(s)
- closes #2708
- #2692

## Prerequisites
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.

- [n/a] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
bors[bot] c15595836a
Merge #2690
2690: Change rspamd override system to use .include with lowest priority. r=mergify[bot] a=Diman0

## What does this PR do?

All override files are used as if they were placed in the rspamd local.d folder.

New override system for Rspamd. In the old system, all files were placed in the Rspamd overrides folder. These overrides would override everything, including the Mailu Rspamd config.

Now overrides are placed in /overrides.
If you use your own map files, change the location to /override/myMapFile.map in the corresponding conf file. It works as following.
* If the override file overrides a Mailu defined config file, it will be included in the Mailu config file with lowest priority. It will merge with existing sections.
* If the override file does not override a Mailu defined config file, then the file will be placed in the rspamd local.d folder. It will merge with existing sections.

For more information, see the description of the local.d folder on the rspamd website: https://www.rspamd.com/doc/faq.html#what-are-the-locald-and-overrided-directories

## What type of PR?

enhancement

### Related issue(s)
- closes #2555 

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman ee1f0f94a3
Don't use the header when we don't need it. 1 year ago
Dimitri Huisman f20208fb4b
Fix error in check for proxy scenario 1 year ago
Dimitri Huisman 4912fa1dff
Fix a typo. 1 year ago
Dimitri Huisman 20bf0e8a65
Add fix for wrong redirect in proxy scenario and accessing WEBROOT_REDIRECT 1 year ago
Dimitri Huisman 29bfc9dd9d
Add fallback just in case X-Forwarded-By is empty. 1 year ago
Dimitri Huisman 25b9db4b00
Proxy endpoint was checking real client ip instead of proxy ip
for validating PROXY_AUTH_WHITELIST
1 year ago
bors[bot] 1d9791ceaa
Merge #2703
2703: Paranoia: drop the headers we don't use r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Paranoia: drop the headers we don't use. This ensures there is no misunderstanding in between front and the other containers.

### Related issue(s)


## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 00533d9eea
Merge #2707
2707: Fix build.hcl / CI.yml regarding labels r=mergify[bot] a=Diman0

## What type of PR?

bug-fix

## What does this PR do?
The version label and versions passed to the docs image were based on the tag. Now we first build the images with -build appended to the tag, we cannot use the tag as version label anymore. 

E.g. the docs image now has the version master-build. This causes any requests to https://mailu.io/master to fail now. Due to the label master-build, it expects https://mailu.io/master-build

Two new env vars are introduced to pass the version to the build.hcl file. This will be used to set the VERSION label in the image, and pass as build arguments to the docs image. 



Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman 0d048d24d3
Fix build.hcl / CI.yml regarding labels
The version label and versions passed to docs image were based on
the tag. Now we first build the images with -build appended to the
tag, we cannot use the tag as version label.

A new env var is introduced to pass the version to the build.hcl file.
This will be used to set the VERSION label in the image, and pass
as build arguments to the docs image.
1 year ago
bors[bot] 04a1868a5e
Merge #2706
2706: Extend roundcube's session lifetime r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Extend roundcube's session lifetime

### Related issue(s)
- #2682

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 5fbfb3cb1c
Merge #2566
2566: Make it clear that we don't delete users r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Make it clear that we don't delete users. Users can and should be disabled when not in use anymore.

### Related issue(s)
- closes #1820

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
Co-authored-by: Dimitri Huisman <52963853+Diman0@users.noreply.github.com>
1 year ago
Dimitri Huisman c6c2805196
Update changelog with extra info. 1 year ago
Dimitri Huisman da4934847f
Fix typo and wording in faq.rst 1 year ago
Florent Daigniere 459694f4a2 Extend roundcube's session lifetime 1 year ago
bors[bot] 1d360055b7
Merge #2705
2705: Switch from docker.io to ghcr.io for deploying images r=nextgens a=Diman0

## What type of PR?

enhancement

## What does this PR do?
As we all know, docker has announced that they will stop with free docker organisations. The Mailu project used this. In about a month, the mailu docker org stops to exist and all images will be deleted.

This PR modifies the CI/CD workflow (github actions) to use ghcr.io as the container registry for deployed images. The images are first build with `-build` appended to the tag. These images are also used in the test step. Only in the deploy phase, are the final tags added (as it should be). This makes sure new images are only available after the deployment step.

The setup utility is updated to use `ghcr.io/mailu` as the docker_org.

All references to the docker org `mailu` have been replaced with `ghcr.io/mailu` in the documentation.

I confirmed via my Mailu_Fork repo that the github workflow works fine. 
https://github.com/Diman0/Mailu_Fork/actions/runs/4440118095

### Related issue(s)
- closes #2704 

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
bors[bot] 86ad4c93a9
Merge #2697
2697: Make the login page guess where to redirect r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Make the login page guess where to redirect.

If you access /admin/ and get redirected to /sso/login, it's only fair that it redirects you back to /admin afterwards.

This is also changing the interface for external proxy authentication, making it simpler to configure.

### Related issue(s)
- close #2692
- #1972

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
Co-authored-by: Dimitri Huisman <52963853+Diman0@users.noreply.github.com>
1 year ago
Dimitri Huisman c482c71f6c
Add missing () 1 year ago
Dimitri Huisman 06ac7f507d
Add changelog entry. 1 year ago
Dimitri Huisman 120cd34989
Switch the container registry used for deploying images from docker
to ghcr.io (github). Images are now first build with '-build'
appended to the tag. E.g. ghcr.io/mailu/admin:master-build.
This is to prevent the image being available before automatic testing has completed.
In the deploy job, the final image is pushed (this still works the same).

Update setup & documentation for switch to ghcr.io
1 year ago
Florent Daigniere 698f1f377c Check https://attackshipsonfi.re/p/exploiting-cors-misconfigurations out 1 year ago
Florent Daigniere 8eb1542f64 Paranoia: drop the headers we don't use 1 year ago
Dimitri Huisman 31faee4218
Merge branch 'master' into delete-disable 1 year ago
bors[bot] 03ff2f2132
Merge #2702
2702: Upgrade snappymail to v2.26.4 r=nextgens a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Upgrade snappymail to v2.26.4

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Dimitri Huisman b99828c4f6
Fix broken link. Add extra clarification for login targets. 1 year ago
Florent Daigniere 22bb0594da Upgrade snappymail to v2.26.4 1 year ago
bors[bot] b30540c074
Merge #2682
2682: Set snappymail autologout time according to SESSION_TIMEOUT r=mergify[bot] a=Nebukadneza

## What type of PR?
bug-fix

## What does this PR do?
Set the autologout variable in snappymail according with systemwide session configuration so that autologout does not trigger too early or too late, which confuses and unnerves users.

!!!!! Please note that I currently (due to very limited time resources) cannot test on snappymail yet, so this is a "blind" flight PR !!!!!
I know it’s a bit insolent to open PRs with untested code, deferring the testing work to somebody else, but that’s the best I can do ATM. Sorry!

### Related issue(s)
- closes #2680 
- 1.9 backport siebling: #

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.

 

Co-authored-by: Dario Ernst <dario@kanojo.de>
1 year ago
Dario Ernst 384d11ddaa Set snappymail autologout time according to PERMANENT_SESSION_LIFETIME
closes #2680
1 year ago
Florent Daigniere 1831ca3b1e Handle WEBROOT_REDIRECT better 1 year ago
Florent Daigniere e1739befc0 Make it work for /admin/antispam too 1 year ago
bors[bot] 31a85397dd
Merge #2694
2694: fix #2693 r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Tweak the snuffleupagus rules to make roundcube's caldav work. While at it I have also grepped for other similar cases/problems.

### Related issue(s)
- close #2693

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere f44cd24bf8 doh 1 year ago
Florent Daigniere 925c753f40 Merge branch 'master' of https://github.com/Mailu/Mailu into guess-target 1 year ago
Florent Daigniere b607375603 Fix 2692: make the external auth proxy usable 1 year ago
Florent Daigniere dd912169fb Make the login page guess where to redirect 1 year ago
Florent Daigniere 1b045b4a94 Introduce AUTH_PROXY_LOGOUT_URL 1 year ago
bors[bot] f0b3689732
Merge #2676
2676: Czech translation r=mergify[bot] a=S474N

Czech translation

## What type of PR?

Czech translation

## What does this PR do?
Add czech translation


Co-authored-by: S474N <S474N@users.noreply.github.com>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman 17c68ca86e
Add changelog entry for PR2676 1 year ago
bors[bot] f660ab568e
Merge #2660
2660: Fail2ban update r=mergify[bot] a=nextgens

## What type of PR?

documentation

## What does this PR do?

Change the instructions on how to setup fail2ban

### Related issue(s)


## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere d9527e561e Renumber and clarify 1 year ago
Florent Daigniere 331bda3822 the space may or may not exist 1 year ago
Florent Daigniere 61ca539d6d Merge branch 'master' of https://github.com/Mailu/Mailu into fail2ban-update 1 year ago
bors[bot] 0069b67ada
Merge #2665
2665:  Update to docker compose v2 r=mergify[bot] a=nextgens

## What type of PR?

documentation

## What does this PR do?

Update to docker compose v2

### Related issue(s)
- close #2663

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
1 year ago
Florent Daigniere cef97f78f1 Maybe fix the tests 1 year ago
Florent Daigniere a973fffa9e fixes suggested by diman0 1 year ago
Florent Daigniere 7d21966114 fix #2693 1 year ago
Dimitri Huisman 45177bd25a
bring back removed blank lines 1 year ago
Dimitri Huisman 7ce28bd6e9
Fix some small errors 1 year ago
Dimitri Huisman 8861ce6edb
Change rspamd override system to use include with lowest priority.
All override files are used as if they were placed in the rspamd
local.d folder.

From the newsfragment:
New override system for Rspamd. In the old system, all files were placed in the Rspamd overrides folder.
These overrides would override everything, including the Mailu Rspamd config.

Now overrides are placed in /overrides.
If you use your own map files, change the location to /override/myMapFile.map in the corresponding conf file.
It works as following.
* If the override file overrides a Mailu defined config file,
  it will be included in the Mailu config file with lowest priority.
  It will merge with existing sections.
* If the override file does not override a Mailu defined config file,
  then the file will be placed in the rspamd local.d folder.
  It will merge with existing sections.

For more information, see the description of the local.d folder on the rspamd website:
https://www.rspamd.com/doc/faq.html#what-are-the-locald-and-overrided-directories
1 year ago
S474N 92be819053
Update messages.po 1 year ago
S474N d6757514af
Czech translation
Czech translation
1 year ago
bors[bot] 0de2430868
Merge #2664
2664: Fix the bug reported by fastlorenzo r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Fix the bug reported by fastlorenzo: when using proxy-auth, if the user doesn't exist you have to hit the URL twice.

### Related issue(s)


## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 8c873d3700
Merge #2668
2668: Start unbound with intended arguments r=mergify[bot] a=ScoreUnder

Unbound looked like it was meant to be started as if by `unbound -c /etc/unbound/unbound.conf`, but instead the string `-c /etc/unbound/unbound.conf` was set as argv[0] of the unbound command, meaning it is never parsed.

## What type of PR?

"Bugfix", but it works anyway without it -- it's just misleading to see a couple of arguments looking like they're being passed but actually just setting the display name of the process instead

## What does this PR do?

Calls execv with an argv list of 3 arguments (program name and intended 2 arguments) rather than just a program name. This appeared to be the intent of the original author.

### Related issue(s)

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly (n/a: bug)
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file. (n/a: minor)


Co-authored-by: score <seejay.11@gmail.com>
1 year ago
Florent Daigniere b205f406de
Update docs/cli.rst
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere 2cf4e61fd2
Update docs/cli.rst
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere 8b502b73ee
Update tests/compose/test.py
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere 511cdcf1ba
Update docs/cli.rst
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere 8d8f753796
Update tests/compose/test.py
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere dd21d4bf0c
Update docs/cli.rst
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere 07da831533
Update docs/cli.rst
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere 23ae60e3df
Update docs/cli.rst
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
bors[bot] b50c858823
Merge #2667
2667: Alpine 3.17.2 r=nextgens a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Alpine 3.17.2; This has a bunch of security fixes to openssl. Unclear whether and where we would be affected so I suggest we just patch.

### Related issue(s)
- closes #2669

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere fed5ab1564 Alpine 3.17.2 1 year ago
score bcf2dd8794
Start unbound with intended arguments
Unbound looked like it was meant to be started as if by `unbound -c /etc/unbound/unbound.conf`, but instead the string `-c /etc/unbound/unbound.conf` was set as argv[0] of the unbound command, meaning it is never parsed.
1 year ago
Florent Daigniere b983c64b4b Clarify we expect v2 1 year ago
Florent Daigniere bb5d007882 s/docker\-compose\([^\.]\)/docker compose\1/g 1 year ago
Florent Daigniere 6a4d8603fc Create the user before logging it in 1 year ago
Florent Daigniere f125420400 Fix the bug reported by fastlorenzo 1 year ago
Dimitri Huisman 9dffa11f0f
Update documentation on how to delete disabled users 1 year ago
Florent Daigniere 65288d7291 Escape this just in case 1 year ago
Florent Daigniere b623e1f286 The date matcher seems to fail sometimes 1 year ago
Florent Daigniere c55a06f85d towncrier 1 year ago
Florent Daigniere 6191d3b59e Simplify 1 year ago
Florent Daigniere 0141a7500f Fix the regexps 1 year ago
bors[bot] ae3f656923
Merge #2659
2659: Doh.  r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Fix a bug introduced in #2646. Without this email delivery from RELAYNET is broken (admin throws a 400 that leads to a ``451 4.3.2 Internal server error``)

### Related issue(s)
- #2646

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 66b7c76836 Doh. Without this email delivery from RELAYNET is broken 1 year ago
bors[bot] aea7407044
Merge #2646
2646: Smarter ratelimit r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Only account for **distinct** usernames in the IP rate-limiter.

This enables to have a much tighter default as a user with a misconfigured device will now only account for a single attempt.

The goal here is to make the rate-limiter more acceptable and to avoid people disabling it altogether.

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere bb127d15ff clarify 1 year ago
Florent Daigniere d20c217ae6 Change the default in setup too 1 year ago
Florent Daigniere 83cc23a51a Update comment too 1 year ago
bors[bot] 46429ab247
Merge #2640
2640: Add env variable to set sieve_vacation_to_header_ignore_envelope r=mergify[bot] a=nwinkelstraeter

When used with SRS the vacation plugin creates a reply with SRS in the To: header which does not look nice for the recipient. Setting sieve_vacation_to_header_ignore_envelope will use the headers from the original source message instead of potentially rewritten ones.

Without this option auto-replies are sent with a To header with SRS, e.g `SRS0=uetG=43=sender.com=user@autoresponder.com`
With this option they are sent with just `user@sender.com`

This option is for whatever reason not part of the [pigeonhole docs](https://doc.dovecot.org/configuration_manual/sieve/extensions/vacation/) but it is documented here: 34431d7a67/NEWS (L338)

## What type of PR?

enhancement

## What does this PR do?
This PR adds an environment variable to the set the `sieve_vacation_to_header_ignore_envelope` configuration

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly



Co-authored-by: Nico Winkelsträter <nico.winkelstraeter@initos.com>
Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 9ef96e9c1e towncrier 1 year ago
Nico Winkelsträter 9cb2ef7632 Let vacation plugin ignore envelope sender to avoid SRS recipient
This is done by setting sieve_vacation_to_header_ignore_envelope to yes
The envelope is rewritten by recipent_canonical_maps to reverse SRS after the plugin checks it
so we need the plugin to ignore it at this point.
1 year ago
Florent Daigniere 085bac6e08 Change AUTH_RATELIMIT_IP_V6_MASK from /56 to /48 1 year ago
bors[bot] 712f14a07b
Merge #2656
2656: Zero quota is unlimited r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

Show a quota of zero as unlimkted in roundcube webmail.

### Related issue(s)

see [#2418](https://github.com/Mailu/Mailu/issues/2418#issuecomment-1418714921)


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf fa084d7b1c
Styling only 1 year ago
Alexander Graf d017b3f22a
Zero quota is unlimited 1 year ago
bors[bot] 47fcf7de2d
Merge #2651
2651: Allow + in localpart of addresses to forward to r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Allow + in localpart of addresses to forward to

### Related issue(s)
- close #1236

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere caa8412d82 close #1236: Allow + in localpart of addresses 1 year ago
bors[bot] 3804d0bf5e
Merge #2650
2650: Tell roundcube that sieve scripts should be utf8 encoded r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Tell roundcube that sieve scripts should be utf8 encoded.

### Related issue(s)
- Close #2258

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 0ec9f1797f Close #2258: sieve scripts should be utf8 encoded 1 year ago
Florent Daigniere 294ac4adb2 Revert "Clarify"
This reverts commit 35e9bfb8ab.
1 year ago
Florent Daigniere 35e9bfb8ab Clarify 1 year ago
Florent Daigniere d30f71234d Apply the mask on the IP too 1 year ago
Florent Daigniere a60159a0db update defaults, rephrase doc 1 year ago
Florent Daigniere e2a25c79fc only account attempts for distinct usernames in ratelimits 1 year ago
Alexander Graf fdb819852e
Improve purge script 1 year ago
bors[bot] 5b4f2fb075
Merge #2643
2643: Fix nginx config r=mergify[bot] a=Diman0

Forgot to adapt some IF statements. All config is normalized now for front.

So true/false now matches the boolean value True/False. Instead if {% IF X == 'true' %} we should now use {% IF X %}

## What type of PR?

bug-fix

## What does this PR do?
Fixes a bug in the nginx config. This bug prevents the /admin endpoint from being exposed.

### Related issue(s)

## Prerequisites
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.

- [ n/a] In case of feature or enhancement: documentation updated accordingly
- [ n/a] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman 44ad14811d
Missed some IF statements that must be modified for normalized config. 1 year ago
Dimitri Huisman d9a6777d9d
Forgot to adapt some IF statements. All config is normalized now for front.
So true/false now matches the boolean value True/False.
Instead if {% IF X == 'true' %} we should now use {% IF X %}
1 year ago
bors[bot] 4a24bd9e24
Merge #2638
2638: further finishing touches for restful api r=mergify[bot] a=Diman0

- Fix setup utility setting correct value to env var API. It now also sets `false` when the API is disabled in the setup utility.
- Fix IF statement for enabling API in nginx.conf. Setting a different value than `API=true` in mailu.env now disabled the API endpoint in nginx.
- Use safer command for regenerating example API token. It uses crypto.getRandomValues() (as suggested by nextgens) which should be more random than the previously used method. 

## What type of PR?

bug-fix

## What does this PR do?

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman 7bcac3bbaa
Get the value from the correct dict (args) 1 year ago
bors[bot] 71d4c63c86
Merge #2641
2641: Remove webmail cookies on logout. r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

sometimes at least roundcube webmail is confused when changing users.
this deletes the webmail session cookies to avoid the confusion.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf ab5caac6f7
Remove webmail cookies on logout. 1 year ago
Dimitri Huisman 75afe1092d
Use server-side password generator for generating token.
Fix setup correctly writing the value for API to mailu.env
Normalize env vars for front container.
Update reverse proxy with API information.
1 year ago
bors[bot] 600e0c2203
Merge #2639
2639: Update demo docs with new capabilities r=mergify[bot] a=Diman0

## What type of PR?

documentation

## What does this PR do?
Update the demo documentation that the demo server now also exposes the new RESTful API.

### Related issue(s)
n/a

## Prerequisites
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.

- [ n/a] In case of feature or enhancement: documentation updated accordingly
- [ /na] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman 2ccdfb9a6b
Update demo docs with new capabilities 1 year ago
Dimitri Huisman 0673d32306
Fix setup utility setting correct value to env var API
Fix IF statement for enabling API in nginx.conf
Use safer command for regenerating example API token.
1 year ago
bors[bot] 8cd5c462f8
Merge #2632
2632: Interface cosmetics r=mergify[bot] a=ghostwheel42

## What type of PR?

enhancement, bug-fix

## What does this PR do?

- add default sort order to all tables
- repair sliders
- add quota in list
- improve mailu logo in roundcube webmail when using responsive skin
- show version in the footer (after login)

### Related issue(s)

- closes #2570
 

Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf 50fc1cb8b3
Move version style to app.css 1 year ago
Alexander Graf 8f425ce081
Move unit to data-attr and fix defaulting to 1 1 year ago
Alexander Graf f00059d10c
Show mailu version in web interface after logging in 1 year ago
Alexander Graf 8b0b87984d
Duh. Fix macros call 1 year ago
Alexander Graf 2fa0461803
Fix sliders 1 year ago
Alexander Graf 31e974f829
Add edit button to admin and manager lists 1 year ago
Alexander Graf 3af3aa9395
Show quota in domain list 1 year ago
Alexander Graf 65595d139a
Set default sort order for all lists 1 year ago
Alexander Graf 3c9c01f8eb
Add style for responsive design 1 year ago
bors[bot] 3a1cecbe21
Merge #2636
2636: Fix out of office replies r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Fix sieve/out of office replies by adding SUBNET to rspamd's local_networks.

Webmails are now on a different subnet.

### Related issue(s)


## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere ae7061c561 Doh 1 year ago
bors[bot] 8cffee55be
Merge #2637
2637: Upgrade to alpine 3.17.1 r=mergify[bot] a=nextgens

New openssl, new dovecot

## What type of PR?

enhancement

## What does this PR do?

Upgrade to alpine 3.17.1; new openssl + new dovecot

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 802ab533d2 Upgrade to alpine 3.17.1
New openssl, new dovecot
1 year ago
Florent Daigniere 61f6e6018b towncrier 1 year ago
Florent Daigniere e326393f03 fix ooo 1 year ago
bors[bot] 9bd76536a1
Merge #2630
2630: Improved IPv6 support #2 r=mergify[bot] a=ghostwheel42

## What type of PR?

enhancement, bug-fix, documentation

## What does this PR do?

This is based on #2272 and adds some more fixes.

### Related issue(s)
- closes #1789
- closes #2392


Co-authored-by: Chris <chris@niduroki.net>
Co-authored-by: Chris Schäpers <chris@niduroki.net>
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf 21ac230cce
Make olefy.py listen on all interfaces 1 year ago
Alexander Graf 84d156d02f
Add towncrier file 1 year ago
Alexander Graf 25635396e7
Bind webdav to port only 1 year ago
Alexander Graf 120a7e8368
Still prefer docker-ipv6nat 1 year ago
Alexander Graf 842be9b7c3
Skip listen to v6 when SUBNET6 is not set 1 year ago
Alexander Graf 1ad1d8d95d
Rewrite generation of gunicorn cmdline 1 year ago
Chris 7cc5d1f756
Update documentation to reflect ip6tables support being experimental 1 year ago
Alexander Graf 8b1eb020e2
Put IPv6 address in brackets 1 year ago
Chris Schäpers 35331a4295
Make gunicorn IPv6 conditional
Only listen on [::]:80 in case SUBNET6 is defined, otherwise do the normal :80
1 year ago
Chris 9f6848110a
Make gunicorn listen on ipv6 1 year ago
bors[bot] e1a85a450f
Merge #2634
2634: Upgrade webmails r=mergify[bot] a=nextgens

## What type of PR?

Enhancement

## What does this PR do?

Upgrade webmails: snappymail to 2.25.0, roundcube to 1.6.1 and carddav to 5.0.1

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 926570f1ca Need this too 1 year ago
Florent Daigniere 9803c51d55 Use a hostname 1 year ago
Florent Daigniere 6533f41f48 Trust the IP address from the local subnet
This will only work when SUBNET autodetection is merged
1 year ago
Florent Daigniere 760ec301e3 harden the trusted hosts 1 year ago
Florent Daigniere 9d2046f43f Upgrade webmails 1 year ago
bors[bot] db2a490256
Merge #2633
2633: Don't apply antispoof rules on locally generated emails r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Don't apply antispoof rules on locally generated emails; This was breaking the auto-responder and sieve rules.

### Related issue(s)


## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 3ffe1d2a9e
Merge #2606
2606: Modify the healtchecks to make them disapear from the logs. r=nextgens a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Modify the healtchecks to make them disapear from the logs.
This is not perfect...
- dovecot now complains about waitpid/finding a new process
- postfix is still regularly pinging rspamd / his milter and that generates a few lines worth of logs each time.

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 46f05cb651 Merge branch 'master' of https://github.com/Mailu/Mailu into reduce-logging 1 year ago
Florent Daigniere 5304311e0e Doh 1 year ago
Florent Daigniere 36623188b5 Don't apply antispoof rules on locally generated emails 1 year ago
bors[bot] 179c624116
Merge #2631
2631: Restful api finishing touches r=mergify[bot] a=Diman0

## What type of PR?

enhancement

## What does this PR do?
Some finishing touches for the restful api.

- Make the API configurable via the setup utility.  
  - Configured exactly the same as the ADMIN and WEBMAIL. 
- We have a single config (API) that configures whether it is exposed (via front). Just like ADMIN. The API is always reachable by directly connecting to the admin container.
- API_TOKEN does not enable/disable the API anymore. When it is not configured, an error is returned (via the internet browser) that the API_TOKEN must be configured in mailu.env.
- Fix some small bugs in the setup utility ( selecting none in the dropdown boxes, now correctly changes the config)
- Update Flask-RestX to 1.0.5. This resolves the deprecation warnings introduced by Flask-RestX.

### Related issue(s)

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman 8cb7265eb2
By default disable the API in the setup utility.
Generate a sample token value for API_TOKEN.
Fix small rendering issue when API was disabled in setup.
1 year ago
Alexander Graf dd80fde841
Add script to purge disabled users. 1 year ago
Alexander Graf 30efdf557f
Re-enable cli action user_delete with "disable" as default 1 year ago
bors[bot] 43e500faf5
Merge #2628
2628: Set default for FETCHMAIL_ENABLED r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

Set the default for FETCHMAIL_ENABLED to true in the admin container.
This keeps existing functionality for people upgrading without re-creating the `mailu.env`.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf 02c4862427
Enable fetchmail for fetchmail test case 1 year ago
Dimitri Huisman 18b900699c
Bump version of Flask-RESTX to 1.0.5.
This resolves all deprecation warnings caused by Flask-RESTX.
1 year ago
Dimitri Huisman d6e7314f05
Make API configurable via the setup utility
Fix some small bugs in the setup utility
Improve documentation on the API.
1 year ago
bors[bot] cc6c808838
Merge #2604
2604: Really fix creation of deep structures using import in update mode r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

Fix creation of deep structures using import in update mode

### Related issue(s)

- closes #2493


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf c4ca1cffaf
Set default for FETCHMAIL_ENABLED 1 year ago
Florent Daigniere e43f6524ea towncrier 1 year ago
Alexander Graf 5c968256e6
Really fix creation of deep structures using import in update mode 1 year ago
bors[bot] 151601744f
Merge #2627
2627: Add SUBNET6 to places where SUBNET is used r=nextgens a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

Also add SUBNET6 where SUBNET is used.

Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
bors[bot] 6d994525c4
Merge #2625
2625: Disable fetchmail r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

Only show "fetched accounts" button in user list when fetchmail feature is enabled.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf 10562233ca
Add SUBNET6 to places where SUBNET is used 1 year ago
bors[bot] 7e60ba4e98
Merge #2613
2613: Enhance network segregation r=nextgens a=nextgens

## What type of PR?

enhancement

## What does this PR do?

- put radicale and webmail on their own network: this is done for security: that way they have no privileged access anywhere (no access to redis, no access to XCLIENT, ...)
- remove the EXPOSE statements from the dockerfiles. These ports are for internal comms and are not meant to be exposed in any way to the outside world.

### Related issue(s)
- #2611

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Alexander Graf 1697da6e23
Disable "Fetched accounts" button in user list. 1 year ago
bors[bot] dae9e9242b
Merge #2624
2624: Move runtime environment variables to the end r=nextgens a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

This moves the environment variables used at runtime from the system to the base image.
It's a workaround for a strange build issue observed when building with hardened malloc enabled.



Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
bors[bot] bbf0ac5d47
Merge #2464
2464: Introduce RESTful API r=mergify[bot] a=Diman0

## What type of PR?

Feature

## What does this PR do?
Introduces a RESTful API for changing the complete Mailu config.
Anything that can be configured in the web administration interface, can also be configured via the Mailu RESTful API.

Via the swagger.json endpoint the complete OpenAPI specification can be retrieved.
Via the endpoint swaggerui, a web client is available which shows all the endpoints, data models and allows you to submit requests.

See docs/api.rst and docs/configuration.rst for details for enabling it.

### Related issue(s)
- closes #445 

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Alexander Graf 712679b4d8
Duh 1 year ago
bors[bot] cfebfdbd1f
Merge #2623
2623: Fix ipv6 subnet for xclient_hosts r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

this puts the ipv6 prefix into square brackets in the xclient_hosts configuration.
strictly speaking putting the square brackets also around the netmask is not correct, but it's okay for postfix
this will be cleaned when all configuration variables and normalizations are moved to the base container.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf d558be20f6
Move runtime environment variables to the end 1 year ago
Alexander Graf 3b08b113bf
Fix ipv6 subnet for xclient_hosts 1 year ago
Dimitri Huisman b0569035ae
Change PUT method to PATCH method.
This better reflects what the interface does.
1 year ago
bors[bot] 3acec43224
Merge #2622
2622: Fix smtplib.LMTP wrong argument name: ip -> host r=mergify[bot] a=zozzz

## What type of PR?

bug-fix

## What does this PR do?

Bug fix when sending welcome message.

### Related issue(s)
- Mention an issue like: -
- Auto close an issue like: closes -

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Vetési Zoltán <zozzz@trigon.hu>
1 year ago
Vetési Zoltán e76e857ae7 Fix smtplib.LMTP wrong argument name: ip -> host 1 year ago
bors[bot] e857d242d6
Merge #2621
2621: Upgrade to snuffleupagus 0.9.0 r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Upgrade to snuffleupagus 0.9.0. This has a better way to deal with unserialize() and a better compatibility with PHP8.2

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 052f8e41ba Upgrade to snuffleupagus 0.9.0 1 year ago
bors[bot] 4295eafb64
Merge #2617
2617: doh r=nextgens a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

### Related issue(s)


## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere ee6975b109 doh 1 year ago
Dimitri Huisman bcceac359d
Merge branch 'apiv1' of https://github.com/ghostwheel42/Mailu into feature-445-restful-api-ghostwheel 1 year ago
Florent Daigniere 9d555b0eec Don't expose any port (suggestion from ghost) 1 year ago
bors[bot] da36bc585f
Merge #2615
2615: Fix snappymail r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

This was broken in #2611. The other is non-consequential security-wise as nginx filters XHOST... but it's worth fixing regardless.

### Related issue(s)
- #2580
- #2611

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 8b9bb350ec towncrier 1 year ago
Florent Daigniere f18776fa0f Step2: put radicale and webmails on their own network 1 year ago
Florent Daigniere e85a2a7e99 Step1: expose managesieve, make the webmails use it 1 year ago
Florent Daigniere 92c0016e32 Fix snappymail 1 year ago
Florent Daigniere b263db72df Restrict XHOST to where useful 1 year ago
Florent Daigniere bf0c345bb9 Fix snappymail 1 year ago
bors[bot] 44d36cbb8b
Merge #2612
2612: Remove duplicated 'actionstart = ' in fail2ban conf. r=mergify[bot] a=lucassith

In fail2ban example configuration for ipset `option #2`, there is a duplicated string which makes the ipset and fail2ban fail to create the set. 
Fail2ban will never ban any ip due to this error.

## What type of PR?

documentation

Co-authored-by: Łukasz Sitarski <Lucassith@gmail.com>
1 year ago
Łukasz Sitarski 202ff8db14
Remove duplicated 'actionstart = ' in fail2ban conf.
In fail2ban example configuration for ipset option, there was a duplicated string which makes the ipset and fail2ban fail to create the set.
Fail2ban will never ban any ip due to this error.
1 year ago
bors[bot] e166550bd7
Merge #2611
2611: Fix authenticated submission r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Don't talk haproxy to postfix; it's more headaches than it is currently worth.

### Related issue(s)
- #2603

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 4d80c95c41 Fix authentication submission
Don't talk haproxy to postfix; it's more headaches than it is currently
worth.
1 year ago
bors[bot] bba6c5bb88
Merge #2603
2603: Enable HAPROXY protocol on SUBNET r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

- Enable HAPROXY in between front and imap: With this we avoid running into the limitations of  ``mail_max_userip_connections`` and the logfiles reflect the real IP.
- Enable HAPROXY in between front and smtp: with this postfix and rspamd are aware of whether TLS was used or not on the last hop. In practice this won't work as nginx doesn't send PROTO yet.
- Discard redundant log messages from postfix

With all of this, not only are the logs easier to understand but ``doveadm who`` also works as one would expect.

### Related issue(s)
- closes #894
- #1328
- closes #1364
- #1705

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 52c17411bd
Merge #2596
2596: db.String without length cause an error in migration for MySQL DB r=mergify[bot] a=csthiang

## What type of PR?

bug-fix

## What does this PR do?

For MySQL, `db.String` requires a length because db.String gets translated to `VARCHAR` in MySQL and `VARCHAR` requires a length. I was considering adding a length to it but since the affected fields were used to store CommaSeparatedList and json-encoded string, I have a feeling it can be quite large in the future. `db.Text` seems to fit into this use case but please correct me if I am wrong.

This actually affects a DB migration with the following error:

```
  File "/app/venv/bin/flask", line 8, in <module>
    sys.exit(main())
  File "/app/venv/lib/python3.10/site-packages/flask/cli.py", line 1047, in main
    cli.main()
  File "/app/venv/lib/python3.10/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/app/venv/lib/python3.10/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/app/venv/lib/python3.10/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/app/venv/lib/python3.10/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/app/venv/lib/python3.10/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/app/venv/lib/python3.10/site-packages/click/decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/app/venv/lib/python3.10/site-packages/flask/cli.py", line 357, in decorator
    return __ctx.invoke(f, *args, **kwargs)
  File "/app/venv/lib/python3.10/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/app/venv/lib/python3.10/site-packages/flask_migrate/cli.py", line 149, in upgrade
    _upgrade(directory, revision, sql, tag, x_arg)
  File "/app/venv/lib/python3.10/site-packages/flask_migrate/__init__.py", line 98, in wrapped
    f(*args, **kwargs)
  File "/app/venv/lib/python3.10/site-packages/flask_migrate/__init__.py", line 185, in upgrade
    command.upgrade(config, revision, sql=sql, tag=tag)
  File "/app/venv/lib/python3.10/site-packages/alembic/command.py", line 322, in upgrade
    script.run_env()
  File "/app/venv/lib/python3.10/site-packages/alembic/script/base.py", line 569, in run_env
    util.load_python_file(self.dir, "env.py")
  File "/app/venv/lib/python3.10/site-packages/alembic/util/pyfiles.py", line 94, in load_python_file
    module = load_module_py(module_id, path)
  File "/app/venv/lib/python3.10/site-packages/alembic/util/pyfiles.py", line 110, in load_module_py
    spec.loader.exec_module(module)  # type: ignore
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/app/migrations/env.py", line 99, in <module>
    run_migrations_online()
  File "/app/migrations/env.py", line 92, in run_migrations_online
    context.run_migrations()
  File "<string>", line 8, in run_migrations
  File "/app/venv/lib/python3.10/site-packages/alembic/runtime/environment.py", line 853, in run_migrations
    self.get_context().run_migrations(**kw)
  File "/app/venv/lib/python3.10/site-packages/alembic/runtime/migration.py", line 623, in run_migrations
    step.migration_fn(**kw)
  File "/app/migrations/versions/f4f0f89e0047_.py", line 18, in upgrade
    with op.batch_alter_table('fetch') as batch:
  File "/usr/lib/python3.10/contextlib.py", line 142, in __exit__
    next(self.gen)
  File "/app/venv/lib/python3.10/site-packages/alembic/operations/base.py", line 381, in batch_alter_table
    impl.flush()
  File "/app/venv/lib/python3.10/site-packages/alembic/operations/batch.py", line 111, in flush
    fn(*arg, **kw)
  File "/app/venv/lib/python3.10/site-packages/alembic/ddl/impl.py", line 322, in add_column
    self._exec(base.AddColumn(table_name, column, schema=schema))
  File "/app/venv/lib/python3.10/site-packages/alembic/ddl/impl.py", line 195, in _exec
    return conn.execute(construct, multiparams)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1380, in execute
    return meth(self, multiparams, params, _EMPTY_EXECUTION_OPTS)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/ddl.py", line 80, in _execute_on_connection
    return connection._execute_ddl(
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1469, in _execute_ddl
    compiled = ddl.compile(
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 502, in compile
    return self._compiler(dialect, **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/ddl.py", line 32, in _compiler
    return dialect.ddl_compiler(dialect, self, **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 463, in __init__
    self.string = self.process(self.statement, **compile_kwargs)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 498, in process
    return obj._compiler_dispatch(self, **kwargs)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/ext/compiler.py", line 548, in <lambda>
    lambda *arg, **kw: existing(*arg, **kw),
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/ext/compiler.py", line 604, in __call__
    expr = fn(element, compiler, **kw)
  File "/app/venv/lib/python3.10/site-packages/alembic/ddl/base.py", line 190, in visit_add_column
    add_column(compiler, element.column, **kw),
  File "/app/venv/lib/python3.10/site-packages/alembic/ddl/base.py", line 330, in add_column
    text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/dialects/mysql/base.py", line 1714, in get_column_specification
    self.dialect.type_compiler.process(
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 532, in process
    return type_._compiler_dispatch(self, **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/visitors.py", line 82, in _compiler_dispatch
    return meth(self, **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 5028, in visit_type_decorator
    return self.process(type_.type_engine(self.dialect), **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 532, in process
    return type_._compiler_dispatch(self, **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/visitors.py", line 82, in _compiler_dispatch
    return meth(self, **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 5006, in visit_string
    return self.visit_VARCHAR(type_, **kw)
  File "/app/venv/lib/python3.10/site-packages/sqlalchemy/dialects/mysql/base.py", line 2214, in visit_VARCHAR
    raise exc.CompileError(
sqlalchemy.exc.CompileError: VARCHAR requires a length on dialect mysql
[2022-12-22 09:23:12 +0000] [17] [INFO] Starting gunicorn 20.1.0
[2022-12-22 09:23:12 +0000] [17] [INFO] Listening at: http://0.0.0.0:80 (17)
[2022-12-22 09:23:12 +0000] [17] [INFO] Using worker: gthread
[2022-12-22 09:23:12 +0000] [18] [INFO] Booting worker with pid: 18
```

### Related issue(s)
none

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Johnson Thiang <jthiang@pop-os.localdomain>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
1 year ago
Florent Daigniere ca44ccbe1c
Use the size other implementations default to 1 year ago
bors[bot] fe2b0bedb7
Merge #2607
2607: Update python dependencies as suggested by dependabot r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

Update dependencies to silence dependabot (vulnerabilities are probably not exploitable)
Only the certifi upgrade could be backported.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf 6f71ea833b
Update python dependencies as suggested by dependabot 1 year ago
Florent Daigniere edd303f54d Modify the healtchecks to make them disapear from the logs.
This is not perfect...
- dovecot now complains about waitpid/finding a new process
- postfix is still regularly pinging rspamd / his milter and that
generates a few lines worth of logs each time.
1 year ago
bors[bot] 874e58348f
Merge #2605
2605: Reduce the SSL session caches from 50m each to 3m each r=nextgens a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Reduce the SSL session caches from 50m each to 3m each. This should be good for 12k sessions (within 1day, see http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_cache and our ssl_session_timeout) for each cache and will help reduce memory usage.

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere cd107182c1 comment 1 year ago
Florent Daigniere 8539344331 Reduce nginx ssl_session_cache to 3m each 1 year ago
Florent Daigniere 36b3a9f4fb Will fix it in another PR 1 year ago
Florent Daigniere 83ea708490 fix healthcheck 1 year ago
Florent Daigniere 7a2d06401a Tweak postfix logging 1 year ago
Florent Daigniere 163261d951 Towncrier 1 year ago
Florent Daigniere 55c1e55529 Same for front-smtp
This should enable postfix to have visibility on TLS usage and fix the
following: #1705
1 year ago
Florent Daigniere 4ae0d7d768 Enable HAPROXY protocol in between front and imap
With this we avoid running into the limitations of
 mail_max_userip_connections (see #894 amd #1364) and the
 logfiles as well as ``doveadm who`` give an accurate picture.
1 year ago
bors[bot] c729954b4a
Merge #2601
2601: Fix creation of deep structures using import in update mode r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

Fixes creation of deep structures (ie user with fetch) when using config-import in update mode.

### Related issue(s)

- closes #2493

## Prerequisites

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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf be40781394
Add default for WEB_API, re-add flask-restx to deps, remove whitespace 1 year ago
Alexander Graf 2f0f46c8fa
Add towncrier 1 year ago
Alexander Graf 84ebab2cb4
Fix creation of deep structures using import in update mode 1 year ago
Dimitri Huisman 3cb8358090
Process review comments PR#2464
- When visiting root of WEB_API, the swaggerui is shown
- simplify the condition for endpoint WEB_API
1 year ago
Dimitri Huisman 39b0d44079
Use first() instead of all() for better performance
Actually return all data for Get user
Remove non-used code
1 year ago
Dimitri Huisman f9b26bd934
Update User with newly introduced allow spoofing field 1 year ago
Dimitri Huisman 6347c18f8a
Process review comments (PR2464) 1 year ago
Dimitri Huisman 61d092922c
Process review comments (PR2464) 1 year ago
Dimitri Huisman afb224e796
Update password hash description for user API endpoint 1 year ago
Dimitri Huisman d4e5db5084
Remove unneeded comment 1 year ago
Dimitri Huisman 46d07ec236
Fix syntax styling api documentation. 1 year ago
Dimitri Huisman 67c423d61f
Add URL for accessing swaggerui to documentation 1 year ago
Dimitri Huisman 7a36f6bbb9
Use hmac.compare_digest to prevent timing attacks. 1 year ago
Dimitri Huisman 5c9cdfe1de
Introduction of the Mailu RESTful API.
Anything that can be configured in the web administration interface,
can also be configured via the Mailu RESTful API.
See the section Advanced configuration in the configuration reference
for the relevant settings in mailu.env for enabling the API.
(API, WEB_API, API_TOKEN).
1 year ago
Alexander Graf 866ad89dfc
first try at api using flask-restx & marshmallow 1 year ago
Alexander Graf c30944404d
Add "API" flag to config (default: disabled) 1 year ago
bors[bot] e9175da586
Merge #2598
2598: drop privs better r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Without this we may see the following:
```
Initializing database
PHP Deprecated:  Return type of zipdownload_mbox_filter::filter($in, $out, &$consumed, $closing) should either be compatible with php_user_filter::filter($in, $out, &$consumed, bool $closing): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/roundcube/plugins/zipdownload/zipdownload.php on line 405
PHP Fatal error:  [snuffleupagus][0.0.0.0][readonly_exec][drop] Attempted execution of a writable file (/var/www/roundcube/plugins/mailu/mailu.php) in /var/www/roundcube/program/lib/Roundcube/rcube_plugin_api.php on line 204
Fatal error: Please check the Roundcube error log and/or server error logs for more information.
```

This has been confirmed to fix it.

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 108958cabb drop privs better 1 year ago
bors[bot] 8d2bd6d9ff
Merge #2528
2528: Implement #2510: oletools integration r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

OLETools now flags documents with macros and rejects suspicious ones. We also block executable file extensions by default (but don't perform inspection in archives: you can tell users to zip-up whatever needs sending).

### Related issue(s)
- closes #2510
- closes #2511

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman 6d87fa423c
Mention you must restart rspamd for the changes to take effect. 1 year ago
Dimitri Huisman 33497c8e31
Small extra clarification for new documentation 1 year ago
bors[bot] 8461a11ff4
Merge #2588
2588: IMAP folder names may contain characters outside of \w: [a-zA-Z0-9] r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

IMAP folder names may contain characters outside of \w: [a-zA-Z0-9]. Typically it may be subfolders...

I have also simplified the regexp since we strip spaces the line below.

This is used for "external accounts"/fetchmail.

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] caa27ede4b
Merge #2593
2593: Drop postfix rsyslog localhost messages with IPv6 address r=mergify[bot] a=UbiquitousBear

## What type of PR?


Enhancement

## What does this PR do?

### Related issue(s)
#2594


## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Shamil Nunhuck <shamil@shamil.co.uk>
1 year ago
Johnson Thiang bd20ef04cc change field type to db.text 1 year ago
Shamil Nunhuck 5264a3070b Added missing towncrier newsfragments 1 year ago
Shamil Nunhuck 7225cb0d3e
Drop rsyslog localhost messages with IPv6 address 1 year ago
bors[bot] 23b09518db
Merge #2591
2591: Add button to mailu-admin in roundcube task menu r=mergify[bot] a=ghostwheel42

## What type of PR?

feature

## What does this PR do?

Adds a button to the roundcube interface. This button gets you back to the admin interface.

### Related issue(s)
- Replaces  #2367


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf 15ba442477
Duh #2 1 year ago
Alexander Graf 5a99ab316d
Duh 1 year ago
Alexander Graf 373488148b
Remove useless style for larry skin 1 year ago
Alexander Graf 36a567c783
Add towncrier 1 year ago
Alexander Graf c38e6aae4e
Add button to mailu-admin in roundcube task menu 1 year ago
Florent Daigniere 6370d03f80 merge snafu 1 year ago
Florent Daigniere ef123f1b53 doh 1 year ago
Florent Daigniere 49d458a0f3 try renaming the file 1 year ago
Florent Daigniere 26858b110a Required for the tests to pass now 1 year ago
Florent Daigniere 6241fbeb78 actually make it optional 1 year ago
Florent Daigniere cea533ae57 Merge remote-tracking branch 'upstream/master' into oletools 1 year ago
Florent Daigniere f04be00798 doc 1 year ago
Florent Daigniere 43bf068be2 Enable admin by default 1 year ago
bors[bot] 4315227215
Merge #2587
2587: fix roundcube/sieve r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Without this snuffleupagus is throwing a tantrum on ini_get(), when saving a sieve filter from roundcube.

```
[17-Dec-2022 13:44:08] WARNING: [pool php] child 21853 said into stderr: "NOTICE: PHP message: PHP Fatal error:  [snuffleupagus][0.0.0.0][disabled_function][drop] Ab
orted execution on call of the function 'ini_get', because its argument '$option' content (suhosin.request.max_vars) matched a rule in /var/www/roundcube/plugins/man
agesieve/lib/Roundcube/rcube_sieve_engine.php on line 532"
```

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 44c064ff38 make it configurable 1 year ago
Florent Daigniere b70be29403 document 1 year ago
Florent Daigniere 77d770a2d2 doh 1 year ago
bors[bot] 251db0b1af
Merge #2562
2562: Dynamic address resolution everywhere r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Use dynamic address resolution everywhere.
Derive a new key for admin/SECRET_KEY
Cleanup the environment

This should allow restarting containers.

### Related issue(s)
- closes #1341
- closes #1013
- closes #1430

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere df924b0864 doh 1 year ago
Florent Daigniere 0fa239da11 These tests are not required anymore 1 year ago
Florent Daigniere c634b9ac04 IMAP folder names may contain characters outside of \w: [a-zA-Z0-9] 1 year ago
Florent Daigniere 170b12baf0 fix sieve 1 year ago
bors[bot] 79f01c4e33
Merge #2581
2581: fix missing casting to int for SESSION_KEY_BITS r=nextgens a=fastlorenzo

## What type of PR?

bug-fix

## What does this PR do?

This PR adds a missing env var casting for the `SESSION_KEY_BITS` variable.
When trying to provide a different value via env var, the value is passed as a string and then compared to a int.
The following check then throws a cast error: 50c7fa882e/core/admin/mailu/utils.py (L309-L312)

### Related issue(s)

## Prerequisites
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.


Co-authored-by: fastlorenzo <git@bernardi.be>
1 year ago
bors[bot] 59220ac83b
Merge #2580
2580: Fixed roundcube carddav module r=mergify[bot] a=fastlorenzo

## What type of PR?

bug-fix

## What does this PR do?

This makes the Carddav module of roundcube to work again.

Changes made:
- Add 2 missing packages in the container (`php81-xmlreader` and `php81-xmlwriter`)
- Disable one rule in snuffleupagus that blocked the web request needed from the plugin to interact with carddav



Co-authored-by: fastlorenzo <git@bernardi.be>
1 year ago
fastlorenzo 135207db3e
fix missing casting to int for SESSION_KEY_BITS
Signed-off-by: fastlorenzo <git@bernardi.be>
1 year ago
fastlorenzo 2fa8dcb51d
Fixed roundcube carddav module
Signed-off-by: fastlorenzo <git@bernardi.be>
1 year ago
bors[bot] 50c7fa882e
Merge #2577
2577: Autofocus the login form on /sso/login r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Autofocus the login form on /sso/login

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] f169f81436
Merge #2571
2571: Upgrade to alpine 3.17.0 r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Upgrade to alpine 3.17.0.

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere e42d029c25 normalize booleans 1 year ago
Florent Daigniere ae6af92b1d it's called libretls! 1 year ago
Florent Daigniere b630355d03 Autofocus the login form on /sso/login 1 year ago
Florent Daigniere 4e3874b0c1 Enable dynamic resolution of hostnames 1 year ago
bors[bot] 1a67921b7c
Merge #2576
2576: Add net_bind_service capability for python executable r=mergify[bot] a=fastlorenzo

## What type of PR?

bug-fix

## What does this PR do?

Fixes capabilities needed to bind on privileged port.


Co-authored-by: fastlorenzo <git@bernardi.be>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
1 year ago
Florent Daigniere dfaba5bb17
No need for two commands here 1 year ago
fastlorenzo 0209825277
Add net_bind_service capability for python executable
Signed-off-by: fastlorenzo <git@bernardi.be>
1 year ago
Florent Daigniere 8150ca77b2 this isn't required anymore either 1 year ago
Florent Daigniere 622e093122 not required anymore 1 year ago
Florent Daigniere 73107ba112 libressl-dev is broken in the new release 1 year ago
Florent Daigniere 619a5fbda2 Upgrade to alpine 3.17.0 1 year ago
bors[bot] 0bfe3f92a6
Merge #2564
2564: Misc dovecot config changes r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

- fix RECIPIENT_DELIMITER (wrong scope, was ignored)
This can be confirmed using: ``$nc imap 2525 ...`` and delivering to a VERP address
- drop privileges of the LMTP daemon

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 8c3da2815d
Merge #2565
2565: Fix DB downgrade r=mergify[bot] a=nextgens

Fix DB downgrade. This isn't used in normal operations but is wrong nevertheless.

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] cd5e6c896f
Merge #2568
2568: Fix a bug preventing users without IMAP access to access the webmails r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Fix a bug preventing users without IMAP access to access the webmails

### Related issue(s)
- close #2451

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] e20efc5b99
Merge #2567
2567: Remove the dependency on pyOpenSSL r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Remove the dependency on pyOpenSSL

### Related issue(s)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
1 year ago
Florent Daigniere c565e69a01
as requested 1 year ago
Florent Daigniere b553d025eb
remove newline 1 year ago
Florent Daigniere 00f07ef533 close #2451: prevent an auth-loop on webmails 1 year ago
Florent Daigniere 3e38e7b89d Remove the dependency on pyOpenSSL 1 year ago
Florent Daigniere 83ef6d773d Make it clear that we don't delete users 1 year ago
Florent Daigniere 98f16b1d47 Fix DB downgrade 1 year ago
bors[bot] a366116cae
Merge #2563
2563: Close #1483: remove postfix's /queue/pid/master.pid r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Remove postfix's /queue/pid/master.pid on startup if there is no other instance running.

### Related issue(s)
- closes #1483

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 5da2ab8fd1 drop privs 1 year ago
Florent Daigniere bf588d19a4 Fix RECIPIENT_DELIMITER 1 year ago
Florent Daigniere 86edc3a919 Close #1483: remove postfix's /queue/pid/master.pid 1 year ago
bors[bot] b49d9ce243
Merge #2561
2561: set the umask when using config-export r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Set a better umask when using config-export


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere c1062f3db2 set the umask 1 year ago
bors[bot] 0bde746610
Merge #2557
2557: Remove Swarm from master r=mergify[bot] a=nextgens

Remove Swarm from master as discussed.

This hasn't been tested

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
bors[bot] 033889dc95
Merge #2542 #2559
2542: Implement header authentication via external proxy r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

Implement header authentication via external proxy

### Related issue(s)
- closes #1972
- closes #2183

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


2559: Turns out that php81-ctype is required by roundcube r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

It solves:
```
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "NOTICE: PHP message: PHP Fatal error:  Uncaught Error: Call to undefined function Masterminds\HTML5\Parser\ctype_alpha() in /var/www/roundcube/vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php:140"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "Stack trace:"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#0 /var/www/roundcube/vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php(82): Masterminds\HTML5\Parser\Tokenizer->consumeData()"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#1 /var/www/roundcube/vendor/masterminds/html5/src/HTML5.php(161): Masterminds\HTML5\Parser\Tokenizer->parse()"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#2 /var/www/roundcube/vendor/masterminds/html5/src/HTML5.php(89): Masterminds\HTML5->parse('<html>\n    <hea...', Array)"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#3 /var/www/roundcube/program/lib/Roundcube/rcube_washtml.php(700): Masterminds\HTML5->loadHTML('<html>\n    <hea...')"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#4 /var/www/roundcube/program/actions/mail/index.php(975): rcube_washtml->wash('<html>\n    <hea...')"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#5 /var/www/roundcube/program/actions/mail/index.php(1019): rcmail_action_mail_index::wash_html('<!doctype html>...', Array, Array)"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#6 /var/www/roundcube/program/actions/mail/show.php(720): rcmail_action_mail_index::pr..."
```

see https://github.com/roundcube/roundcubemail/issues/7049


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] e0d42cadc0
Merge #2546
2546: Implement a GUI for WILDCARD_SENDERS r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

- Implement a GUI for WILDCARD_SENDERS

### Related issue(s)
- closes #2372

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf b0990460a4
Fix error display 1 year ago
Alexander Graf 53720876b4
Colorize feature badges 1 year ago
Alexander Graf a5eeab37e1
Add default for column allow_spoofing 1 year ago
Florent Daigniere e927426dfa Turns out that php81-ctype is required by roundcube
see https://github.com/roundcube/roundcubemail/issues/7049
1 year ago
Alexander Graf 7828115102
Re-add flavor and steps to wizard. 1 year ago
bors[bot] 0e0ac201fc
Merge #2558
2558: Don't do it as root r=mergify[bot] a=nextgens

A naive attempt to ensure we don't run the PHP stuff as root; without it we mess the permissions up and fail to upgrade the database schema of roundcube

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere c4595fddca Change perms first 1 year ago
Florent Daigniere 9566c297d9 Don't do it as root 1 year ago
Florent Daigniere 8cba012546 Rspamd needs redis too. 1 year ago
Florent Daigniere b3f534a6ac Wizard.html should still be the default destination 1 year ago
Florent Daigniere d0631558c7 Remove Swarm everywhere.
This hasn't been tested
1 year ago
Florent Daigniere 3721a6aa02 Merge branch 'master' of https://github.com/Mailu/Mailu into HEAD 1 year ago
bors[bot] 2104c04e3b
Merge #2544
2544: Fix #2242: Make quotas adjustable in 50MiB increments r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Make quotas adjustable in 50MiB increments

### Related issue(s)
- closes #2242

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 4c3c628ca4 dedup 1 year ago
Florent Daigniere 19bd9362d3 As suggested by ghost 1 year ago
Florent Daigniere f1e5044dbe Add to the list, sort it 1 year ago
bors[bot] a8630c5a3b
Merge #2550
2550: Webmail hardening r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Add [Snuffleupagus](https://github.com/jvoisin/snuffleupagus/) (a modern Suhosin replacement) to protect webmails.

It may be possible to harden further, by encrypting some of the cookies and auditing the usage of gpg more closely.

This seems to work for me.

### Related issue(s)

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 02f2679dc4 name collision 1 year ago
Florent Daigniere b08d940d09 See https://github.com/decalage2/oletools/issues/659 1 year ago
Florent Daigniere d77bf119f8 towncrier 1 year ago
Florent Daigniere a8061f3ed3 doh 1 year ago
Florent Daigniere 12117cef37 Reduce the scope of the try: except 1 year ago
Florent Daigniere 612db96209 Block executable file extensions (closes #2511) 1 year ago
Florent Daigniere 709023ab5a dimitri said "block it"
So let's block any macro with AUTOEXEC
1 year ago
Florent Daigniere 3bdc57adbc Forgot this 1 year ago
Florent Daigniere 32d44b96c3 Fix the logic 1 year ago
Florent Daigniere e43effab63 Glad there is a test 1 year ago
Florent Daigniere d793c5eed8 Dup symbol 1 year ago
Florent Daigniere 1327f34c2c Add tests to ensure we block macros 1 year ago
Florent Daigniere e03d91a1ec Merge remote-tracking branch 'upstream/master' into oletools 1 year ago
Florent Daigniere 9fcff5e745 Pin what we get from edge 1 year ago
Florent Daigniere 63a12d9857 changes requested by ghost 1 year ago
Florent Daigniere 546884d10c ghost's requested changes 1 year ago
bors[bot] 5a7d73dc3d
Merge #2554
2554: Rollback to mysql-connector-python==8.0.29 r=mergify[bot] a=nextgens

See #2553

## What type of PR?

bug-fix

## What does this PR do?

Rollback to mysql-connector-python==8.0.29

### Related issue(s)
- closes #2553 

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 7e1ab7978e Block VBA Stomping too 1 year ago
Florent Daigniere 4881e0db2a ghost is right, it should be pinned here too 1 year ago
Florent Daigniere c1144612be
fix sorting 1 year ago
Florent Daigniere 4d8bd210c5
Update run_dev.sh 1 year ago
Florent Daigniere ee512112fb
fix flask db history 1 year ago
Florent Daigniere adacf579fc Rollback to mysql-connector-python==8.0.29
See #2553
1 year ago
Florent Daigniere 3e45a791cf Implement oletools to filter out bad macros 1 year ago
bors[bot] 9c6e9b05db
Merge #2543
2543: Fix #2231: make public announcements work r=nextgens a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Ensure public announcements bypass filters.

They can still time-out... but this is already a big improvement that we should be able to backport.

### Related issue(s)
- closes #2231

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 9fa3a3e0c7 doc 1 year ago
Florent Daigniere e94f6eaf33 towncrier 1 year ago
Florent Daigniere 9e61a33cb2 Merge branch 'master' of https://github.com/Mailu/Mailu into webmail-hardening 1 year ago
bors[bot] 6a3daa75ac
Merge #2539
2539: Upgrade alpine, make setup use the base image r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Upgrade alpine, make setup use the base image, introduce a health-check, drop privileges. Drop privileges on admin too.

It may or may not help #2536

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere f994c8687e doh 1 year ago
Florent Daigniere 44c47586ea Fix potential permission problems 1 year ago
Florent Daigniere d3d7916b58 Merge remote-tracking branch 'upstream/master' into upgrade-alpine 1 year ago
bors[bot] c1da586444
Merge #2526
2526: Upgrade Snappymail to 2.21 and merge the webmail containers r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Upgrade Snappymail to 2.21 and merge the webmail containers. This will make the CI faster and should simplify things going forward (hardening but also allow running more than one webmail at the time, ...).

- enable APCu
- add new test to ensure we redirect to SSO and have disabled the admin panel
- add all the packaged dictionaries for spell checking
- harden the configuration of the webmails a bit (more to come in a separate PR)
- turn off deprecation warnings (php8.1 is too new)
- turn off error reporting (log them instead)
- return HTTP302 when we should
- gpg-verify the signature of the webmails we ship
- upgrade to snappymail 2.21, switch to the new json config format
- use socrates as it's meant to so that helm users can do their thing
- run the HTTPd and PHP as different users
- redirect the PHP errors to stderr

## Related issue(s)
- closes #2466
- closes #948
- closes #2250

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere ab852772f9 Bump snappymail to 2.21.3 1 year ago
Florent Daigniere 28d720bbc9 As requested 1 year ago
bors[bot] d650a9cc0f
Merge #2548
2548: Fetchmail improvements (2) r=mergify[bot] a=nextgens

Follow-up to #2529

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 45b01db9de Fix the language switcher 1 year ago
Florent Daigniere 3fc0a0e7fa Merge branch 'master' of https://github.com/Mailu/Mailu into fetchmail-improvements 1 year ago
Florent Daigniere 4da2db1b0b add comment as requested 1 year ago
Florent Daigniere c79e8d3852 Fix display bug 1 year ago
bors[bot] 553b02fb3d
Merge #2529
2529: Improve fetchmail r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Improve fetchmail:
- allow delivery via LMTP (faster, bypassing the filters)
- allow several folders to be retrieved
- run fetchmail as non-root
- tweak the compose file to ensure we have all the dependencies

### Related issue(s)
- closes #1231 
- closes #2246 
- closes #711

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
1 year ago
bors[bot] 31c6c26ec8
Merge #2547
2547: Disable libhardened-malloc for non x86. r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Support is going to be a nightmare if RPI4 is not working; We can always reintroduce it later.

### Related issue(s)
- closes #2541 


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 604eb69122
Merge #2545
2545: Don't force a password reset r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

Don't force a password reset. You may want to edit the user without changing his password.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere dcf11aea48 Don't force a password reset 1 year ago
Florent Daigniere db9ed1fd59 Disable libhardened-malloc for non x86.
@see #2541

Support is going to be a nightmare if RPI4 is not working.
1 year ago
Florent Daigniere f802601a08
Update f4f0f89e0047_.py 1 year ago
Florent Daigniere d5ac9199a0
Update 7ac252f2bbbf_.py 1 year ago
Florent Daigniere 7822b41048 same for domains 1 year ago
Florent Daigniere ef9cc3c866 Show spoofing on /admin/user/list too 1 year ago
Florent Daigniere 38507b2e1b Close #2372: Implement a GUI for WILDCARD_SENDERS 1 year ago
Florent Daigniere 6a22c82c02 Fix run_dev 1 year ago
Florent Daigniere cf7404e26c Fix #2242: Make quotas adjustable in 50MiB increments 1 year ago
Florent Daigniere b20bf996ec Fix #2231: make public announcements work 1 year ago
Florent Daigniere e2d4e3eb2e Implement header authentication via external proxy 1 year ago
Florent Daigniere 840b2bd9df block o:0:{} too 1 year ago
Florent Daigniere 017ea5298e typo 1 year ago
Florent Daigniere 2a4f6836cf protect unserialize() 1 year ago
Florent Daigniere e5ab9821f9 Add snuffleupagus
This seems to work in my limited testing.
1 year ago
Florent Daigniere bdc085048d Restore the Dockerfile like it was 1 year ago
Florent Daigniere b28798c74f doh 1 year ago
Florent Daigniere 1bfab1dbfa Maybe fix the test? 1 year ago
Florent Daigniere 6137f93d23 add a GTUBE test to check the antispam 1 year ago
Florent Daigniere 3cb87b6e49 Update entry 1 year ago
Florent Daigniere e3b875aa6b Well, -i stands for --insecure 1 year ago
Florent Daigniere 3b5b00d87d towncrier 1 year ago
Florent Daigniere e79d7fed55 Reduce the number of warnings on the CI 1 year ago
Florent Daigniere 699be6f9fa Drop privs when running admin too 1 year ago
Florent Daigniere 42cd5bf2dc Move it to base since admin will also use it 1 year ago
Florent Daigniere 80559ecb71 optimize caching 1 year ago
Florent Daigniere 21b9f76ebc setup doesn't need root 1 year ago
Florent Daigniere e5a1a353db Upgrade to alpine 3.16.3
This has PHP fixes and a new rspamd
1 year ago
Florent Daigniere 86637f0259 Make setup use the base image 1 year ago
bors[bot] 68bb8da2b7
Merge #2538
2538: Fix the ARM build again r=mergify[bot] a=nextgens

I have double-checked from the builder and this works.

gcc -v from the alpine image tells me that we have  ``--enable-default-pie``

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere 7745420fe0 Fix the ARM build again 1 year ago
bors[bot] b66f3fe9de
Merge #2537
2537: Fix the armv7 build (again)! r=mergify[bot] a=nextgens

Revert "simplify": ghostwheel42's approach was right
This reverts commit 04f6bd2633.

Without the build still errors-out because of ``set -euxo pipefail``
see https://github.com/Mailu/Mailu/actions/runs/3479399158/jobs/5817902589

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Florent Daigniere b9b0c77d2e Revert "simplify": ghostwheel42's approach was right
This reverts commit 04f6bd2633.
1 year ago
Florent Daigniere 15b889fac8 Specify that this is optional 1 year ago
bors[bot] f43c8c652e
Merge #2483 #2535
2483: Introduce FETCHMAIL_ENABLED r=mergify[bot] a=DjVinnii

## What type of PR?

Enhancement

## What does this PR do?
Add `FETCHMAIL_ENABLED` to enable/disable the Fetchmail functionality in the Admin UI.

### Related issue(s)
- closes #2127

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


2535: fix the linux/arm/v7 build r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

The arm builder is running aarch64 ... and there is no package for arm/v7


Co-authored-by: Vincent Kling <v.kling@vinniict.nl>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
Dimitri Huisman 8afb544a10
Default FETCHMAIL_ENABLED to False 1 year ago
Florent Daigniere 32f3241569 ensure we have -pie too 1 year ago
Florent Daigniere 7ab3d8f9fe There is no good reason not to export them is the base image too 1 year ago
Florent Daigniere aa44a42654 ensure we compile the wheels with bells and whistles too 1 year ago
Florent Daigniere 04f6bd2633 simplify 1 year ago
Florent Daigniere d43e7f72df ghostwheel42's suggestion 1 year ago
Florent Daigniere 1f895d5f82 ghostwheel42's suggestion 1 year ago
Florent Daigniere 031a157ad9 fix the linux/arm/v7 build 1 year ago
bors[bot] 04a196c417
Merge #2525 #2534
2525: Switch to GrapheneOS's hardened_malloc r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

Switch to GrapheneOS's hardened_malloc

This was suggested during the dev meeting of the 18/09/22.

It may break things and it may make things unbearably slow... but it should also make the exploitation of memory corruption bugs a lot harder.

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


2534: Close #2533: document SQLALCHEMY_DATABASE_URI r=mergify[bot] a=nextgens

## What type of PR?

documentation

## What does this PR do?

document SQLALCHEMY_DATABASE_URI

### Related issue(s)
- closes #2533

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 40bdf7a6d9
Merge #2530
2530: disable SESSION_COOKIE_SECURE when TLS_FLAVOR=notls r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

People are unlikely to proxy everything

### Related issue(s)
- closes #2527

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
1 year ago
bors[bot] 3b150ff9a4
Merge #2532
2532: Allow JS debugging, speed-up asset-build, disable redirect-debug. r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

Another bugfix to the run_dev.sh helper

Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere b9e5560fb6 Better way to express the same thing
Thanks @ghostwheel42
1 year ago
Florent Daigniere 63513608b9 Close #2533: document SQLALCHEMY_DATABASE_URI 1 year ago
Florent Daigniere 66de1dcec8 Change the logic
The idea here is that if you have set SESSION_COOKIE_SECURE we should
honor that... and if you haven't we should try to do the right thing.
1 year ago
Florent Daigniere 81628149a2 don't fake the library 1 year ago
Florent Daigniere 9b2f018be6 add --no-cache 1 year ago
Florent Daigniere 76f8517e00 This is still required (as TLS_FLAVOR isn't set) 1 year ago
Florent Daigniere b9564c0bc9 This shouldn't have been commited 1 year ago
Florent Daigniere 19af2944d7 Refactor as requested 1 year ago
Alexander Graf 6b470ac403
Allow proper JS debugging, speed-up assets dev-build, disable redirect-debug by default. 1 year ago
Florent Daigniere 7aad1158fb @ghostwheel42 will fix it in another PR 1 year ago
Florent Daigniere a566cb07d6 fix 1 year ago
Florent Daigniere 08b3a2814b Merge branch 'master' of https://github.com/Mailu/Mailu into notls 1 year ago
Florent Daigniere 385b6ac85d Use string formatting 1 year ago
Florent Daigniere 6474108056 Use a join() instead 1 year ago
Florent Daigniere fb75cca2f4 Merge branch 'master' of https://github.com/Mailu/Mailu into fetchmail-improvements 1 year ago
Florent Daigniere c0c91691fd Fix the issue on /admin/fetch/edit 1 year ago
bors[bot] d8e2a2960b
Merge #2531
2531: run_dev.sh: Use FLASK_DEBUG, fix assets, show startup errors. r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

fixes bug in run_dev.sh

Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Alexander Graf b0b64a8e63
Use FLASK_DEBUG, fix assets, show startup errors. 1 year ago
Florent Daigniere 505bb79a78 Don't set the secure Cookie flag if TLS_FLAVOR=notls 1 year ago
Florent Daigniere 9c7dfbeb24 Doc 1 year ago
Florent Daigniere 08a9ab9a56 Improve fetchmail 1 year ago
Florent Daigniere 455180043d doh 1 year ago
Florent Daigniere 56a106ad60 Only one labs section in the conf file 1 year ago
Florent Daigniere 071ad15a97 Better snappymail defaults 1 year ago
Florent Daigniere 6b2cb95a7d This is not required anymore 1 year ago
Florent Daigniere a508eeaafb Use /dev/shm for tmp 1 year ago
Florent Daigniere f2f430af5d Redirect the logs where they belong 1 year ago
Florent Daigniere 06c0c78956 Hardening: run the http and php as different users 1 year ago
Florent Daigniere d7b80e94a4 try again. 1 year ago
Florent Daigniere 7ebac75045 fix tests 1 year ago
Florent Daigniere f3a91d1a18 enable APCu 1 year ago
Florent Daigniere b488e57602 debug 1 year ago
Florent Daigniere 225322fe88 More hardening 1 year ago
Florent Daigniere ad17b10c8e redirects should be HTTP/302 1 year ago
Florent Daigniere 4517ce23a6 Aliases be damned. 1 year ago
Florent Daigniere 6d8cc9083b test 1 year ago
Florent Daigniere 729838c8fe Grrr. 1 year ago
Florent Daigniere 1379a58352 Basic hardening 1 year ago
Florent Daigniere 50f94a282f doh 1 year ago
Florent Daigniere 710dde1faf Fix #948: ensure the admin panel is disabled 1 year ago
Florent Daigniere 7e722cd0c3 fix #2250: ensure rainloop uses _ADDRESS 1 year ago
Florent Daigniere 224f2f4508 This isn't used anymore
The healthcheck is now done by fpm
1 year ago
Florent Daigniere a8d405cb48 Verify the gpg signature of webmails 1 year ago
Florent Daigniere ae64c6cc30 Doh 1 year ago
Florent Daigniere 13adf4aeec Fix tests 1 year ago
Florent Daigniere 1edef755f1 Fix bug #2466 1 year ago
Florent Daigniere dc9e2a3e70 Upgrade Snappymail to 2.21 and merge the webmail containers 1 year ago
bors[bot] 8a90f83bd0
Merge #2514
2514: Update deps r=mergify[bot] a=ghostwheel42

## What type of PR?

update python dependencies

## What does this PR do?

Update python deps in base image


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
1 year ago
Florent Daigniere f11c451403 Restrict it to arch where there is a package 1 year ago
Florent Daigniere 97df65e9ef Switch to GrapheneOS's hardened_malloc
This was suggested during the dev meeting of the 18/09/22.

It may break things and it may make things unbearably slow
1 year ago
bors[bot] 8d392e8056
Merge #2524
2524: Update the webmail images r=mergify[bot] a=Diman0

Update the webmail images.
Roundcube
  - Switch to base image (alpine)
  - Switch to php-fpm

SnappyMail
  - Switch to base image
  - Upgrade php7 to php8.

## What type of PR?

Feature

## What does this PR do?
Update the webmail images.
Roundcube
  - Switch to base image (alpine)
  - Switch to php-fpm

SnappyMail
  - Switch to base image
  - Upgrade php7 to php8.

### Related issue(s)
- closes #1521

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
1 year ago
Dimitri Huisman 0e5443a867
Update php8 to php81. Update snappymail to 2.19.4 1 year ago
Dimitri Huisman 59c5b152b2
Switch to using set -euxo pipefail for better error handling
-e immediately exit when a command fails. No further commands are processed.
-o pipefail, if a series of piped commands fail, do NOt return the last commands returncode, but DO return the return code of the failing command in the pipeline series
-u, raise an error when an unset variable is used. Not using this results in an empty value being used and the script being executed differently without you knowing why.
-x, print each command before executing it. Actual arguments are expanded. So you see the command with the actual parameter values. This is printed in red in the buildx log output.
1 year ago
Dimitri Huisman f6cdfb3392
Allow Healthcheck requests over IPv6 1 year ago
Dimitri Huisman 2a894cb15d
Process nextgens review remarks 2 years ago
Dimitri Huisman 92f270c94e
Update the webmail images:
Roundcube
  - Switch to base image (alpine)
  - Switch to php-fpm
SnappyMail
  - Switch to base image
  - Upgrade php7 to php8.
2 years ago
bors[bot] 745c211c4a
Merge #2523
2523: fix JS error r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

It fixes a bug whereby one may have to click twice on the submit button depending on timing.

e.trigger() will error out on most browsers.

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
2 years ago
bors[bot] 0839490beb
Merge #2479
2479: Rework the anti-spoofing rule r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

We shouldn't assume that Mailu is the only MTA allowed to send emails on behalf of the domains it hosts.
We should also ensure that it's non-trivial for email-spoofing of hosted domains to happen

Previously we were preventing any spoofing of the envelope from; Now we are preventing spoofing of both the envelope from and the header from unless some form of authentication passes (is a RELAYHOST, SPF, DKIM, ARC)

### Related issue(s)
- close #2475

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
2 years ago
Florent Daigniere c91c9df134 fix error 2 years ago
bors[bot] cf6da1492e
Merge #2157
2157: configure datatables via html5 data attributes r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

allows to sort most columns as a human would expect

### Related issue(s)
- closes #2154 

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
2 years ago
Vincent Kling 728afdd34a Add basic logging for FETCHMAIL_ENABLED and FETCHMAIL_DELAY 2 years ago
Alexander Graf e0d2432c6b
Rename data-ordered to data-sort 2 years ago
Alexander Graf 2a4402cdc2
Fix datatable for list fo sign-up domains 2 years ago
Alexander Graf af6cf5fd1d
Fix language selector without session 2 years ago
Alexander Graf 2778641e78
Fix screen reader title of language selector 2 years ago
Alexander Graf 4776094ea7
Configure datatables on missing tables, add sign in button to sso page. 2 years ago
Alexander Graf 6218b36372
configure datatables via html5 data attributes 2 years ago
Alexander Graf 1ae9156756
Add bcyrpt as direct dependency for better crypto. Also some updates 2 years ago
Alexander Graf a74396a9ef
Fix wtforms usage 2 years ago
Alexander Graf 047413185e
Mask Flask-SQLAlchemy >= 3.0.0 for now as it breaks mailu 2 years ago
Alexander Graf 7e36694b64
Update python deps 2 years ago
Vincent Kling 4a74cd9afe Resolve conflict 2 years ago
Vincent Kling 6901b0f05e Implement FETCHMAIL_ENABLED in fetchmail.py 2 years ago
bors[bot] 896e7fb54b
Merge #2500
2500: Password policy enforcement r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

It enforces that all new passwords set by users are at least 8 characters in length and checks all users' passwords at login time against HIBP.

The HIBP part requires javascript and Mailu to be accessed over HTTPS to work but degrades gracefully (no message will be shown if the requirements are not met).

It was a conscious choice to implement it at this level: administrators can set weaker passwords using non-HTTP based interfaces.

### Related issue(s)
- close #2208
- close #287

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
2 years ago
Alexander Graf 4b179d9008
Merge branch 'master' into hibp 2 years ago
bors[bot] 4563038b32
Merge #2518
2518: Add dev runner for admin container r=mergify[bot] a=ghostwheel42

## What type of PR?

development feature

## What does this PR do?

This adds a shell script (run_dev.sh) to run a live development environment in a container.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
2 years ago
Alexander Graf 36019a8ce9
Don't show Dockerfile before building 2 years ago
Alexander Graf dd3cd1263e
Add development documentation again 2 years ago
Alexander Graf 91e12d510d
Use default password used everywhere else 2 years ago
Alexander Graf defd533319
Don't duplicate hidden fields 2 years ago
Alexander Graf db87a0f3a1
Move temporary db into container and show docker run command 2 years ago
Alexander Graf f7caaddbec
Speed up asset building when developing 2 years ago
Alexander Graf 71263f1a8c
Add more env variables and restyle code 2 years ago
Alexander Graf fd8570ec34
Remove unused QUOTA_STORAGE_URL 2 years ago
Alexander Graf bbeb211d72
Listen to localhost by default 2 years ago
Alexander Graf 1d90dc3ea3
Allow running without redis 2 years ago
Alexander Graf c507b765be
Improve dev runner 2 years ago
Alexander Graf 8732b70b30
Add shell script to run admin dev environment 2 years ago
Alexander Graf ea636a1835
Fix hibp test 2 years ago
bors[bot] ac93e6a9be
Merge #2517
2517: Use the new notation: arm64/v8 instead of arm64 r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

With a modern version of docker compose, on arm64 you get:
```
docker-compose pull 
[+] Running 0/8
 ⠼ admin Pulling                                                                                                                                                                        1.4s
 ⠿ smtp Error                                                                                                                                                                           1.4s
 ⠿ imap Error                                                                                                                                                                           1.4s
 ⠿ webmail Error                                                                                                                                                                        1.4s
 ⠿ antispam Error                                                                                                                                                                       1.4s
 ⠼ redis Pulling                                                                                                                                                                        1.4s
 ⠼ front Pulling                                                                                                                                                                        1.4s
 ⠿ resolver Error                                                                                                                                                                       1.4s
no matching manifest for linux/arm64/v8 in the manifest list entries
```

This may fix it.

It's discussed at https://stackoverflow.com/questions/70819028/relation-between-linux-arm64-and-linux-arm64-v8-are-these-aliases-for-each-othe

Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
2 years ago
Dimitri Huisman 2a3266b6b8
Forgot to update both deploy jobs 2 years ago
Dimitri Huisman b2e47642f7
Tag the images with latest tag as well. 2 years ago
Alexander Graf 311f41c331
Add missing hidden fields 2 years ago
Alexander Graf 27a5f9db65
Reformatting 2 years ago
Vincent Kling 83fdc07a6f Default FETCHMAIL_ENABLED to True 2 years ago
Florent Daigniere 3e9def6cd9 Use the new notation: arm64/v8 instead of arm64 2 years ago
Florent Daigniere 54e9858633 this 2 years ago
Florent Daigniere 14f802fb4a untested but that should work 2 years ago
bors[bot] e0ff135a00
Merge #2498
2498: Implement ITERATE in podop r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

This makes ``doveadm -A`` work.

The easiest way to try it out is:
```
doveadm dict iter proxy:/tmp/podop.socket:auth shared/userdb

or 

doveadm user '*'
```

The protocol is described at https://doc.dovecot.org/developer_manual/design/dict_protocol/
The current version of dovecot is not using flags... so there's little gain in implementing them.

### Related issue(s)
- close #2499

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
2 years ago
Alexander Graf c57706ad27
Duh 2 years ago
Alexander Graf 46773f639b
Return 404 is user-id cannot be parsed 2 years ago
Alexander Graf 595b32cf97
Fix quota return value 2 years ago
Alexander Graf bec0b1c3b2
Fix variable name 2 years ago
Florent Daigniere 001acd60ac doh2 2 years ago
Alexander Graf dec5309ef9
Fix typo 2 years ago
Florent Daigniere 6b7026ef69 Here too 2 years ago
Florent Daigniere 24b2c7c04a doh 2 years ago
Florent Daigniere 66250e396c refactor 2 years ago
bors[bot] 5b2b379c91
Merge #2513
2513: fix(auto-reply): include start and end dates in the auto-reply period r=mergify[bot] a=bb-wkr

## What type of PR?
bug-fix

## What does this PR do?
Include start and end dates in the auto-reply period

### Related issue(s)
closes #2512

## Prerequisites
Before we can consider review and merge, please make sure the following list is done and checked.
If an entry is not applicable, you can check it or remove it from the list.

- [X] In case of feature or enhancement: documentation updated accordingly
- [X] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: wkr <wkr@bitsbeats.com>
2 years ago
wkr d920b3d037 fix(auto-reply): include start and end dates in the auto-reply period; issue #2512 2 years ago
bors[bot] 323f0a4e70
Merge #2509
2509: Login docker.io to prevent rate limiting for pulling images r=mergify[bot] a=Diman0

## What type of PR?

feat/fix

## What does this PR do?
Added login to docker.io for CI/CD workflow. When logged in, we have a higher limit for pulling images. The arm workers were rate limited.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
2 years ago
Dimitri Huisman db7ce8c83e
Login docker.io to prevent rate limiting for pulling images 2 years ago
bors[bot] 4b1143550d
Merge #2508
2508: Actually push the build arm images to ghcr.io r=mergify[bot] a=Diman0

## What type of PR?

fix

## What does this PR do?
Makes sure the images build for arm are actually pushed to ghcr.io.

### Related issue(s)
n/a

## Prerequisites
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.

- [n/a] In case of feature or enhancement: documentation updated accordingly
- [n/a] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
2 years ago
Dimitri Huisman b3151e9904
Actually push the build arm images to ghcr.io 2 years ago
bors[bot] c6deb84ab0
Merge #2507
2507: Fix the CI for ARM builds r=mergify[bot] a=nextgens

We should install the dependencies everywhere where we may have to rebuild the wheels.

If other people use other arch and want their builds to go faster we can whitelist them too after they have confirmed it works.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
2 years ago
Florent Daigniere ff9f152a52 This may be helpful too 2 years ago
Florent Daigniere 5137b235e9 whitelist what we know works
If other people use other arch and want their builds to go faster we can
whitelist them too after they have confirmed it works.
2 years ago
bors[bot] 4d8585a3fe
Merge #2506
2506: Finishing touches for fixing arm builds r=mergify[bot] a=Diman0

- Use self-hosted runners for arm base image
- Use seperate docker image cache for arm build

## What type of PR?

fix

## What does this PR do?

### Related issue(s)
n/a

## Prerequisites
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.

- [n/a] In case of feature or enhancement: documentation updated accordingly
- [n/a] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
2 years ago
Dimitri Huisman 6549dbf247
Sigh. needs.* context is only available if you include it in needs: 2 years ago
Dimitri Huisman c7cba1b075
Finishing touches for fixing arm builds
- Use self-hosted runners for arm base image
- Use seperate docker image cache for arm build
- Remove unneeded needs items.
2 years ago
bors[bot] 0015335f4a
Merge #2505
2505: Fix building wheels when deps need to compile r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

This installs build requiremnets (compiler, etc.) when tehre are no pre-built wheels an the current architecture.

Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
2 years ago
Alexander Graf a2d43be6de
Fix building wheels when deps need to compile 2 years ago
bors[bot] 32edfce12b
Merge #2504
2504: Remove superfluous cache export entry for arm r=mergify[bot] a=Diman0

## What type of PR?

fix

## What does this PR do?
Fix arm build. The arm job contained one cache export entry too many. Only one cache export entry is allowed.

### Related issue(s)


## Prerequisites
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.

- [n/a ] In case of feature or enhancement: documentation updated accordingly
- [n/a] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
2 years ago
Dimitri Huisman e915e444e9
Remove superfluous cache export entry for arm 2 years ago
bors[bot] 659cf8894c
Merge #2502
2502: Resolve using socrate function r=mergify[bot] a=ghostwheel42

## What type of PR?

enhancement

## What does this PR do?

nginx.py had a copy of the socrate function resolve_hostname.
This removes the duplicated code and uses the socrate function.
The socrate functions does the same but prefers ipv4 addresses when resolving.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
2 years ago
bors[bot] 0618fbb472
Merge #2501
2501: Fix armv7 build by manually downloading crates.io index r=mergify[bot] a=ghostwheel42

## What type of PR?

bug-fix

## What does this PR do?

cargo fails with oom when download crates.io index.
this circumvents the problem by cloning crates.io index manually


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
2 years ago
Alexander Graf 91f86a4c2a
Resolve using socrate function 2 years ago
Alexander Graf bba98b320e
Fix armv7 build by manually downloading crates.io index 2 years ago
Florent Daigniere 5d314c49ae towncrier 2 years ago
Florent Daigniere 9cb8df57c6 enforce at least 8 chars 2 years ago
Florent Daigniere afbaabd8cd v1 2 years ago
Florent Daigniere 6def1b555b doh 2 years ago
Florent Daigniere c1f571a4c3 Speed things up.
If we want to go further than this we should change podop's incr(), pass
the flags and make admin process the results.
2 years ago
Florent Daigniere 96d9289630 No need to send an extra \n 2 years ago
Florent Daigniere cdc9b63a46 Guard the message logging too 2 years ago
Florent Daigniere 2a417dbfc2 doesn't belong here 2 years ago
Florent Daigniere 1ce889b91b Do it the pythonic way 2 years ago
Florent Daigniere e10527a4bf This is not used anymore 2 years ago
Florent Daigniere 1ae4c37cb9 Don't do fancy, just re-raise it 2 years ago
Florent Daigniere 5ec4277e1e Make it async. I'm not sure it's a good idea 2 years ago
Florent Daigniere cf34be967c Implement ITERATE 2 years ago
bors[bot] 62c919da09
Merge #2497
2497: Upgrade to alpine 3.16.2 r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

This may fix the build issues on arm (troubles building cryptography)

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
2 years ago
Florent Daigniere 340e359096 doh 2 years ago
Florent Daigniere 076d67b513 follow the protocol 2 years ago
Florent Daigniere 2e467092a2 The newer dovecot sends data podop should ignore 2 years ago
bors[bot] 12480ccbff
Merge #2328
2328: Feature: Configurable default spam threshold used for new users r=mergify[bot] a=enginefeeder101

## What type of PR?

Feature

## What does this PR do?

This PR adds functionality to set a custom default spam threshold
for new users. The environment variable ``DEFAULT_SPAM_THRESHOLD`` is
used for this purpose. When not set, it defaults back to 80%, as the
default value was before.

If ``DEFAULT_SPAM_THRESHOLD`` is set to a value that Python cannot
parse as an integer, a ValueError is thrown. There is no error handling
for that case built-in. Should that be done?

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: enginefeeder101 <enginefeeder101@users.noreply.github.com>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
2 years ago
Florent Daigniere a63bad6bf2 towncrier 2 years ago
Florent Daigniere 8942448561 Upgrade to alpine 3.16.2
This may fix the build issues on arm
2 years ago
Dimitri Huisman 06b784da57
Shorten default function by using lambda 2 years ago
bors[bot] 9975a793fe
Merge #2458
2458: Fix: Don't update updated_at on quota_bytes_used change r=mergify[bot] a=DjVinnii

## What type of PR?

bug-fix

## What does this PR do?

This PR makes sure that the `updated_at` field is not updated when `quota_bytes_used` is updated. All other updates to the `User` model still updates the `updated_at` field. 

This is done by explicitly using an method in the `Base` class triggering [`flag_modified`][url-flag-modified].

### Related issue(s)
- closes #1363

## Prerequisites
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.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.

<!-- LINKS-->
[url-flag-modified]: https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.attributes.flag_modified


Co-authored-by: Vincent Kling <v.kling@vinniict.nl>
2 years ago
Florent Daigniere ec4224123b Use the logger 2 years ago
bors[bot] 5703e97c73
Merge #2460
2460: Switch to a base image containing base tools and the podop and socrate libs r=mergify[bot] a=ghostwheel42

## What type of PR?

enhancement of build process

## What does this PR do?

Changes build.hcl to build core images using a base image.
Also adds a "assets" base image for the admin container.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
Co-authored-by: Pierre Jaury <pierre@jaury.eu>
Co-authored-by: kaiyou <pierre@jaury.eu>
Co-authored-by: Dimitri Huisman <52963853+Diman0@users.noreply.github.com>
2 years ago
Alexander Graf 024b0573b3
Update build reqs and fix armv7 build 2 years ago
Dimitri Huisman 4be0cbf4da
Switch workflow to ghcr.io
- Build images & build cache are pushed to ghcr.io.
- Tests will make use of the images pushed to ghcr.io.
- Deploy step only copies images from ghcr.io to docker.io.
- Resolves strange build errors tied to buildx+intermediate builds
- Results in quicker build times.
2 years ago
Vincent Kling 6363acf30a Add dont_change_updated_at to fetch_done 2 years ago
Vincent Kling 6b785abb01 Rename flag_updated_at_as_modified to dont_change_updated_at 2 years ago
bors[bot] 7e29248980
Merge #2494
2494: Improve ISSUE_TEMPLATE.md r=mergify[bot] a=ghostwheel42

## What type of PR?

documentation

## What does this PR do?

I'm having trouble separating the user input from the instructions in the issue template.
This puts the instructions in comments so the rendered markdown is almost empty.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
2 years ago
Alexander Graf 005a8fa1fc
Improve ISSUE_TEMPLATE.md 2 years ago
Florent Daigniere 89f7d983b4 Don't start rspamd until admin is up and working 2 years ago
Florent Daigniere d8cf0c3848 Revert "Admin may not have started up when this loads"
This reverts commit 0f17299b4e.
2 years ago
Florent Daigniere 0f17299b4e Admin may not have started up when this loads 2 years ago
Florent Daigniere 95a3a3d342 doh 2 years ago
Florent Daigniere 84a722eabc Optimize the query 2 years ago
Florent Daigniere 07bf8ce6df Add anti-spoofing to the feature list 2 years ago
Florent Daigniere bd1b73032c Poke a hole for mailing lists 2 years ago
bors[bot] 9d5d55f969
Merge #2486
2486: Quote SMTP SIZE to avoid splitting keyword and parameter in EHLO response r=mergify[bot] a=TheBeeZee

## What type of PR?

bug-fix

## What does this PR do?

Fixes a syntax issue in the nginx front EHLO response. As currently configured, the SIZE parameter and value are treated as separate keywords and as a result are returned on separate lines in an EHLO response:

```
...
250-SIZE
250-50000000
...
```

By adding quotes, nginx will return the correct response on a single line:

```
...
250-SIZE 50000000
...
```

### Related issue(s)

closes #2485

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Blaž Zupan <blaz@inlimbo.org>
2 years ago
Blaž Zupan 56617bbe12 Quote SMTP SIZE to avoid splitting keyword and parameter in EHLO response 2 years ago
Florent Daigniere c4fcaed7d4 doh 2 years ago
Vincent Kling 8a60b658b4 Implement FETCHMAIL_ENABLED 2 years ago
Florent Daigniere 8929f54de5 clarify
Also cover the case where the DKIM sig is for another domain and there
is no explicit DMARC policy
2 years ago
Florent Daigniere 8da6117bb9 clarify 2 years ago
Florent Daigniere af87456faf this works for me 2 years ago
bors[bot] bbbed4d9ac
Merge #2480
2480: Fix a bunch of typos r=mergify[bot] a=DjVinnii

## What type of PR?

 documentation

## What does this PR do?
Fix a bunch of typos in a variety of files

### Related issue(s)
- None

## Prerequisites
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.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Vincent Kling <v.kling@vinniict.nl>
2 years ago
Vincent Kling 23d06a5761 Fix a bunch of typos 2 years ago
Florent Daigniere be4dd6d84a Spell it out 2 years ago
Florent Daigniere f7b3aad831 Ensure we REJECT when we don't have a DMARC policy
This restores the old behaviour
2 years ago
Dimitri Huisman 6ea2d84a3c Remove outdated wrong documentation 2 years ago
Florent Daigniere 0204c9e59d doh 2 years ago
Florent Daigniere cc2c308d1d update the towncrier entry 2 years ago
Florent Daigniere 8775a2bf04 untested code that may just work 2 years ago
Dimitri Huisman 451738e32b We want the function result. Not the function statement. 2 years ago
Dimitri Huisman f9ba0e688f Removed syntax error 2 years ago
Dimitri Huisman 92cb8c146b Refine build_test_deploy.yml:
Build base image before the other images.
Change cache key to make it is re-used for all builds. This is not
dangerous. The docker build process can determine itself whether
a cache can be safely re-used or not.
2 years ago
Florent Daigniere 5ebcecf4dd Don't need that anymore either 2 years ago
Florent Daigniere 3e51d15b03 Remove the strict anti-spoofing rule. 2 years ago
Alexander Graf d9bf6875e1
Optimize build order for better caching 2 years ago
Alexander Graf 7441a420c4
Fix and speed-up arm build. Allow chosing of prod/dev env. 2 years ago
Alexander Graf 5c31120895
Remove obsolete contexts from base image 2 years ago
Alexander Graf 146921f619
Move curl to base image 2 years ago
Alexander Graf 4c1071a497
Move all requirements*.txt to base image 2 years ago
Alexander Graf a29f066858
Move even more python deps to base image 2 years ago
Alexander Graf 52dd09d452
Fix assets build process #2 2 years ago
Alexander Graf 768c0cc1ce
Fix assets build process 2 years ago
Alexander Graf 8668b269cd
Add requirements.txt for base image 2 years ago
Alexander Graf 9f511faf64
Merge pull request #8 from NeverBehave/master
fix: resolve IPv6 container hostname
2 years ago
Dimitri Huisman b711f930ef
Merge pull request #9 from vavanade/patch-1
fix docstring
2 years ago
Dimitri Huisman c0066abd01
Merge pull request #6 from micw/log-failed-dns
Add logging for failed DNS lookups
2 years ago
kaiyou f63837b8e1
Update to 0.2.0 2 years ago
kaiyou 68d44201ab
Merge pull request #4 from micw/resolve-host-if-address-not-set
Resolve host if address not set
2 years ago
kaiyou b198fde756
Merge pull request #3 from micw/fix-random-failures
Change test hostnames for stable test results
2 years ago
kaiyou 7f6d51904b
Remove wrong dependency to importlib 2 years ago
kaiyou ef344c62f6
Add automated tests 2 years ago
kaiyou 74a3e87de3
Fix a couple syntax typos 2 years ago
kaiyou 351b05b92d
Allow jinja to load from file path or handle 2 years ago
kaiyou 0370b26f3e
Initial commit 2 years ago
Alexander Graf ce9d886195
Merge pull request #10 from ghostwheel42/add_gitignore
Add .gitignore file
2 years ago
kaiyou dbec5f0a6c
Switch to setuptools and bump the version 2 years ago
kaiyou 3d0d831c76
Version 0.2.4 2 years ago
kaiyou e2979f9103
Merge pull request #6 from Nebukadneza/fix_py37
Don’t use deprecated now-keyword "async"
2 years ago
kaiyou 6fadd39aea
Merge pull request #3 from Nebukadneza/add_key_url_quoting
URL-Quote the key in HTTP requests
2 years ago
kaiyou 080e76f972
Merge pull request #1 from rakshith-ravi/patch-1
Fixed a small typo
2 years ago
kaiyou 23e5aa2e05
Escape strings properly in the Dovecot dict dialect 2 years ago
kaiyou 814bb1f36d
Properly miss when the web api returns 404 2 years ago
kaiyou d2b98ae323
Update to 0.2.2 2 years ago
kaiyou 81d171f978
Add some debug logging to the table class 2 years ago
Pierre Jaury d640da8787
Include package data in the package 2 years ago
Pierre Jaury c5fa0280a0
Add support for dovecot dict_set operations 2 years ago
Pierre Jaury eb6b1866f1
Specify dependencies in the setup script 2 years ago
Pierre Jaury b1b0aeb69d
Initial commit 2 years ago
Alexander Graf b501498401
Update .gitignore file 2 years ago
Alexander Graf 9fe452e3d1
Use base image when building core images 2 years ago
Alexander Graf 5e552bae69
Add base image 2 years ago
Alexander Graf 295d7ea675
Move assets to own Dockerfile 2 years ago
Vincent Kling bda404182f Replace before update listener with method in the Base class 2 years ago
Vincent Kling 10583f57dd Add newsfragment 2 years ago
Vincent Kling 102d96bc7d Implement event lister to keep updated_at unchanged on quota_bytes_used updates 2 years ago
Dimitri Huisman 81c9e01d24 finishing touches for PR# 2328
Antispam.rst contained a syntax error.
Move config description to common section which is more fitting.
Fixed wrong assignment of default value for DEFAULT_SPAM_THRESHOLD in models.py.
2 years ago
enginefeeder101 82860d0f80
Moved parsing environment variable to global application config dictionary
Per requested changes added the ``DEFAULT_SPAM_THRESHOLD`` to the main
application configuration dictionary in ``configuration.py`` and updated
``models.py`` accordingly.
No error handling is added, as that was not required.
2 years ago
enginefeeder101 4da0ff1856
Documentation for configurable default spam threshold 2 years ago
enginefeeder101 6c83d25312
Configurable default spam threshold used for new users
This commit adds functionality to set a custom default spam threshold
for new users. The environment variable ``DEFAULT_SPAM_THRESHOLD`` can
be used for this purpose. When not set, it defaults back to 80%, as the
default value was before
If ``DEFAULT_SPAM_THRESHOLD`` is set to a value that Python cannot
parse as an integer, a ValueError is thrown. There is no error handling
for that case built-in.
2 years ago

@ -1,101 +0,0 @@
name: start-linux-arm
on:
push:
branches:
- '1.9'
- master
concurrency: ci-arm-${{ github.ref }}
###############################################
# REQUIRED secrets
# ${{ secrets.Docker_Login }}
# Username of docker login for pushing the images to repo env.DOCKER_ORG and env.DOCKER_ORG_TESTS
# ${{ secrets.Docker_Password }}
# Password of docker login for pushing the images to repo env.DOCKER_ORG and env.DOCKER_ORG_TESTS
# Add the above secrets to your github repo to determine where the images will be pushed.
################################################
# REQUIRED global variables
# DOCKER_ORG, docker org used for pushing release images (branch x.y and master)
# DOCKER_ORG_TEST, docker org used for pushing images for testing (branch testing).
env:
DOCKER_ORG: mailu
DOCKER_ORG_TEST: mailuci
jobs:
# This job calculates all global job variables that are required by all the subsequent jobs.
# All subsequent jobs will retrieve and use these variables. This way the variables only have to be derived once.
derive-variables:
name: derive variables
runs-on: ubuntu-latest
outputs:
MAILU_VERSION: ${{ env.MAILU_VERSION }}
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
DOCKER_ORG: ${{ env.DOCKER_ORG_DERIVED }}
BRANCH: ${{ env.BRANCH }}
DEPLOY: ${{ env.DEPLOY }}
RELEASE: ${{ env.RELEASE }}
steps:
- uses: actions/checkout@v3
with:
# fetch-depth 0 is required to also retrieve all tags.
fetch-depth: 0
- 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 and DEPLOY/RELEASE for other branches than testing
if: env.BRANCH != 'testing'
shell: bash
run: |
echo "MAILU_VERSION=${{ env.BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG_DERIVED=${{ env.DOCKER_ORG }}" >> $GITHUB_ENV
echo "DEPLOY=true" >> $GITHUB_ENV
echo "RELEASE=false" >> $GITHUB_ENV
- name: Derive PINNED_MAILU_VERSION and DEPLOY/RELEASE for normal release x.y
if: env.BRANCH != 'testing' && env.BRANCH != 'staging' && env.BRANCH != 'master'
shell: bash
run: |
version=$( git tag --sort=version:refname --list "${{ env.MAILU_VERSION }}.*" | tail -1 );root_version=${version%.*};patch_version=${version##*.};if [ "$patch_version" == "" ]; then pinned_version=${{ env.MAILU_VERSION }}.0; else pinned_version=$root_version.$(expr $patch_version + 1); fi;echo "PINNED_MAILU_VERSION=$pinned_version" >> $GITHUB_ENV
echo "RELEASE=true" >> $GITHUB_ENV
echo "DEPLOY=true" >> $GITHUB_ENV
echo "RELEASE=true" >> $GITHUB_ENV
- name: Derive PINNED_MAILU_VERSION for staging for master
if: env.BRANCH == 'master'
shell: bash
env:
GITHUB_SHA: ${{ env.GITHUB_SHA }}
run: |
echo "PINNED_MAILU_VERSION=$GITHUB_SHA" >> $GITHUB_ENV
echo "DEPLOY=true" >> $GITHUB_ENV
echo "RELEASE=false" >> $GITHUB_ENV
build-test-deploy:
needs:
- derive-variables
uses: ./.github/workflows/build_test_deploy.yml
with:
architecture: 'linux/arm64,linux/arm/v7'
mailu_version: ${{needs.derive-variables.outputs.MAILU_VERSION}}-arm
pinned_mailu_version: ${{needs.derive-variables.outputs.PINNED_MAILU_VERSION}}-arm
docker_org: ${{needs.derive-variables.outputs.DOCKER_ORG}}
branch: ${{needs.derive-variables.outputs.BRANCH}}
deploy: ${{needs.derive-variables.outputs.DEPLOY}}
release: ${{needs.derive-variables.outputs.RELEASE}}
secrets: inherit
################################################
# Code block that is used as one liner for the step:
# Derive PINNED_MAILU_VERSION and DEPLOY/RELEASE for normal release x.y
##!/bin/bash
#version=$( git tag --sort=version:refname --list "{{ env.MAILU_VERSION }}.*" | tail -1 )
#root_version=${version%.*}
#patch_version=${version##*.}
#if [ "$patch_version" == "" ]
#then
# pinned_version={{ env.MAILU_VERSION }}.0
#else
# pinned_version=$root_version.$(expr $patch_version + 1)
#fi
#echo "PINNED_MAILU_VERSION=$pinned_version" >> $GITHUB_ENV

@ -1,11 +1,23 @@
###############################################
# REQUIRED secrets
# ${{ secrets.Docker_Login }}
# Username of docker login for logging in docker for pulling images (higher pull rate limit)
# ${{ secrets.Docker_Password }}
# Password of docker login for logging in docker for pulling images (higher pull rate limit)
# ${{ secrets.Docker_Login2 }}
# Second Username of docker login for logging in docker for pulling images (higher pull rate limit)
# ${{ secrets.Docker_Password2 }}
# Second Password of docker login for logging in docker for pulling images (higher pull rate limit)
################################################
name: build-test-deploy
on:
workflow_call:
inputs:
architecture:
description: 'The architecture of the images that will be build.'
description: 'The architecture(s) of the images that will be build. linux/amd64 or linux/arm64/v8,linux/arm/v7 or linux/amd64,linux/arm64/v8,linux/arm/v7'
required: false
default: 'linux/amd64'
default: 'linux/amd64,linux/arm64/v8,linux/arm/v7'
type: string
mailu_version:
description: 'The main version that is build. E.g. master or x.y.'
@ -16,7 +28,7 @@ on:
required: true
type: string
docker_org:
description: 'The docker organisation where the images are pushed to.'
description: 'The docker organisation where the images are pushed to. E.g. ghcr.io/mailu'
required: true
type: string
branch:
@ -24,7 +36,7 @@ on:
required: true
type: string
deploy:
description: Deploy to docker hub. Happens for all branches but staging. Use string true or false.
description: Deploy to container registry. Happens for all branches but staging. Use string true or false.
default: true
required: false
type: string
@ -37,9 +49,9 @@ on:
workflow_dispatch:
inputs:
architecture:
description: 'The architecture of the images that will be build.'
description: 'The architecture(s) of the images that will be build. linux/amd64 or linux/arm64/v8,linux/arm/v7 or linux/amd64,linux/arm64/v8,linux/arm/v7'
required: false
default: 'linux/amd64'
default: 'linux/amd64,linux/arm64/v8,linux/arm/v7'
type: string
mailu_version:
description: 'The main version that is build. E.g. master or x.y.'
@ -50,7 +62,7 @@ on:
required: true
type: string
docker_org:
description: 'The docker organisation where the images are pushed to.'
description: 'The docker organisation where the images are pushed to. E.g. ghcr.io/mailu'
required: true
type: string
branch:
@ -58,15 +70,15 @@ on:
required: true
type: string
deploy:
description: Deploy to docker hub. Happens for all branches but staging
description: Deploy to container registry. Happens for all branches but staging. Use string true or false.
default: true
required: false
type: boolean
type: string
release:
description: 'Tag and create the github release. Only happens for branch x.y (release branch)'
description: Tag and create the github release. Use string true or false.
default: false
required: false
type: boolean
type: string
env:
HCL_FILE: ./tests/build.hcl
@ -84,18 +96,156 @@ jobs:
- name: Create matrix
id: targets
run: |
echo ::set-output name=matrix::$(docker buildx bake -f ${{env.HCL_FILE}} --print | jq -cr '.group.default.targets')
echo matrix=$(docker buildx bake -f ${{env.HCL_FILE}} --print | jq -cr '.group.default.targets') >> $GITHUB_OUTPUT
- name: Show matrix
run: |
echo ${{ steps.targets.outputs.matrix }}
## This job builds the base image. The base image is used by all other images.
build-base-image-x64:
name: Build base image x64
if: contains(inputs.architecture, 'linux/amd64')
needs:
- targets
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Retrieve global variables
shell: bash
run: |
echo "BRANCH=${{ inputs.branch }}" >> $GITHUB_ENV
echo "MAILU_VERSION=${{ inputs.mailu_version }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- uses: crazy-max/ghaction-github-runtime@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Get uuid
id: uuid
run: |
echo uuid=$RANDOM >> $GITHUB_OUTPUT
- name: Build docker base image with retry
env:
DOCKER_ORG: ghcr.io/${{ steps.string.outputs.lowercase }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
LABEL_VERSION: ${{ env.MAILU_VERSION }}
PINNED_LABEL_VERSION: ${{ env.PINNED_MAILU_VERSION }}
ARCH: 'linux/amd64'
BUILDER: ${{ steps.uuid.outputs.uuid }}
DOCKER_LOGIN: ${{ secrets.Docker_Login }}
DOCKER_PASSW: ${{ secrets.Docker_Password }}
BUILDX_NO_DEFAULT_ATTESTATIONS: 1
uses: nick-fields/retry@v2
with:
timeout_minutes: 20
retry_wait_seconds: 30
max_attempts: 3
shell: bash
command: |
set -euxo pipefail \
; /usr/bin/docker info \
; echo "${{ github.token }}" | docker login --username "${{ github.repository_owner }}" --password-stdin ghcr.io \
; echo "$DOCKER_PASSW" | docker login --username "$DOCKER_LOGIN" --password-stdin \
; /usr/bin/docker buildx rm builder-${{ env.BUILDER }} \
|| echo "builder does not exist" \
; /usr/bin/docker buildx create --name builder-${{ env.BUILDER }} --driver docker-container --use \
; /usr/bin/docker buildx bake --file ./tests/build.hcl --set *.cache-from=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/base:${{ hashFiles('core/base/Dockerfile','core/base/requirements-prod.txt') }} --set *.cache-to=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/base:${{ hashFiles('core/base/Dockerfile','core/base/requirements-prod.txt') }},mode=max --set *.platform=${{ env.ARCH }} base \
; /usr/bin/docker buildx rm builder-${{ env.BUILDER }}
- name: cleanup docker buildx instance after failure of build step
if: ${{ failure() }}
shell: bash
env:
BUILDER: ${{ steps.uuid.outputs.uuid }}
run: |
/usr/bin/docker buildx rm builder-${{ env.BUILDER }}
## This job builds the base image. The base image is used by all other images.
build-base-image-arm:
name: Build base image arm
if: contains(inputs.architecture, 'linux/arm64/v8,linux/arm/v7')
needs:
- targets
runs-on: self-hosted
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Retrieve global variables
shell: bash
run: |
echo "BRANCH=${{ inputs.branch }}" >> $GITHUB_ENV
echo "MAILU_VERSION=${{ inputs.mailu_version }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- uses: crazy-max/ghaction-github-runtime@v2
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Get uuid
id: uuid
run: |
echo uuid=$RANDOM >> $GITHUB_OUTPUT
- name: Build docker base image with retry
env:
DOCKER_ORG: ghcr.io/${{ steps.string.outputs.lowercase }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}-arm
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}-arm
LABEL_VERSION: ${{ env.MAILU_VERSION }}
PINNED_LABEL_VERSION: ${{ env.PINNED_MAILU_VERSION }}
ARCH: linux/arm64/v8,linux/arm/v7
BUILDER: ${{ steps.uuid.outputs.uuid }}
DOCKER_LOGIN2: ${{ secrets.Docker_Login2 }}
DOCKER_PASSW2: ${{ secrets.Docker_Password2 }}
BUILDX_NO_DEFAULT_ATTESTATIONS: 1
uses: nick-fields/retry@v2
with:
timeout_minutes: 30
retry_wait_seconds: 30
max_attempts: 10
shell: bash
command: |
set -euxo pipefail \
; /usr/bin/docker info \
; echo "${{ github.token }}" | docker login --username "${{ github.repository_owner }}" --password-stdin ghcr.io \
; echo "$DOCKER_PASSW2" | docker login --username "$DOCKER_LOGIN2" --password-stdin \
; /usr/bin/docker buildx rm builder-${{ env.BUILDER }} \
|| echo "builder does not exist" \
; /usr/bin/docker buildx create --name builder-${{ env.BUILDER }} --driver docker-container --use \
; /usr/bin/docker buildx bake --file ./tests/build.hcl --set *.cache-from=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/base:${{ hashFiles('core/base/Dockerfile','core/base/requirements-prod.txt') }}-arm --set *.cache-to=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/base:${{ hashFiles('core/base/Dockerfile','core/base/requirements-prod.txt') }}-arm,mode=max --set *.platform=${{ env.ARCH }} base \
; /usr/bin/docker buildx rm builder-${{ env.BUILDER }}
- name: cleanup docker buildx instance after failure of build step
if: ${{ failure() }}
shell: bash
env:
BUILDER: ${{ steps.uuid.outputs.uuid }}
run: |
/usr/bin/docker buildx rm builder-${{ env.BUILDER }}
# This job builds all the images. The build cache is stored in the github actions cache.
# In further jobs, this cache is used to quickly rebuild the images.
build:
name: Build images for linux/amd64
if: inputs.architecture == 'linux/amd64'
if: contains(inputs.architecture, 'linux/amd64')
needs:
- targets
- build-base-image-x64
strategy:
fail-fast: false
matrix:
@ -113,46 +263,64 @@ jobs:
echo "MAILU_VERSION=${{ inputs.mailu_version }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV
- name: Configure actions/cache@v3 action for storing build cache in the ${{ runner.temp }}/cache folder
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/${{ matrix.target }}
key: ${{ github.ref }}-${{ inputs.mailu_version }}-${{ matrix.target }}-${{ github.run_id }}
restore-keys: |
${{ github.ref }}-${{ inputs.mailu_version }}-${{ matrix.target }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- uses: crazy-max/ghaction-github-runtime@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v5
with:
username: ${{ secrets.Docker_Login }}
password: ${{ secrets.Docker_Password }}
- name: Build all docker images
string: ${{ github.repository_owner }}
- name: Get uuid
id: uuid
run: |
echo uuid=$RANDOM >> $GITHUB_OUTPUT
- name: Build docker image with retry
env:
DOCKER_ORG: ${{ env.DOCKER_ORG }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
uses: docker/bake-action@v2
DOCKER_ORG: ghcr.io/${{ steps.string.outputs.lowercase }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}-build
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}-build
LABEL_VERSION: ${{ env.MAILU_VERSION }}
PINNED_LABEL_VERSION: ${{ env.PINNED_MAILU_VERSION }}
ARCH: 'linux/amd64'
BUILDER: ${{ steps.uuid.outputs.uuid }}
DOCKER_LOGIN: ${{ secrets.Docker_Login }}
DOCKER_PASSW: ${{ secrets.Docker_Password }}
BUILDX_NO_DEFAULT_ATTESTATIONS: 1
uses: nick-fields/retry@v2
with:
files: ${{env.HCL_FILE}}
targets: ${{ matrix.target }}
load: false
push: false
set: |
*.cache-from=type=local,src=${{ runner.temp }}/cache/${{ matrix.target }}
*.cache-to=type=local,dest=${{ runner.temp }}/cache/${{ matrix.target }},mode=max
*.platform=${{ inputs.architecture }}
timeout_minutes: 20
retry_wait_seconds: 30
max_attempts: 3
shell: bash
command: |
set -euxo pipefail \
; /usr/bin/docker info \
; echo "${{ github.token }}" | docker login --username "${{ github.repository_owner }}" --password-stdin ghcr.io \
; echo "$DOCKER_PASSW" | docker login --username "$DOCKER_LOGIN" --password-stdin \
; /usr/bin/docker buildx rm builder-${{ env.BUILDER }} \
|| echo "builder does not exist" \
; /usr/bin/docker buildx create --name builder-${{ env.BUILDER }} --driver docker-container --use \
; /usr/bin/docker buildx bake --push --file ./tests/build.hcl --set *.cache-from=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/${{ matrix.target }}:buildcache --set *.cache-to=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/${{ matrix.target }}:buildcache,mode=max --set *.cache-from=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/base:${{ hashFiles('core/base/Dockerfile','core/base/requirements-prod.txt') }} --set *.platform=${{ env.ARCH }} ${{ matrix.target }} \
; /usr/bin/docker buildx rm builder-${{ env.BUILDER }}
- name: cleanup docker buildx instance after failure of build step
if: ${{ failure() }}
shell: bash
env:
BUILDER: ${{ steps.uuid.outputs.uuid }}
run: |
/usr/bin/docker buildx rm builder-${{ env.BUILDER }}
# This job builds all the images. The build cache is stored in the github actions cache.
# In further jobs, this cache is used to quickly rebuild the images.
build-arm:
name: Build images for ARM64 & ARM/V7
if: inputs.architecture != 'linux/amd64'
if: contains(inputs.architecture, 'linux/arm64/v8,linux/arm/v7')
needs:
- targets
- build-base-image-arm
strategy:
fail-fast: false
matrix:
@ -170,54 +338,69 @@ jobs:
echo "MAILU_VERSION=${{ inputs.mailu_version }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV
- name: Configure actions/cache@v3 action for storing build cache in the ${{ runner.temp }}/cache folder
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/${{ matrix.target }}
key: ${{ github.ref }}-${{ inputs.mailu_version }}-${{ matrix.target }}-${{ github.run_id }}
restore-keys: |
${{ github.ref }}-${{ inputs.mailu_version }}-${{ matrix.target }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- uses: crazy-max/ghaction-github-runtime@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v5
with:
username: ${{ secrets.Docker_Login }}
password: ${{ secrets.Docker_Password }}
- name: Build all docker images
string: ${{ github.repository_owner }}
#This is to prevent to shared runners from generating the same uuid
- name: Get unique random number
id: uuid
run: |
echo uuid=$RANDOM >> $GITHUB_OUTPUT
- name: Build docker image with retry
env:
DOCKER_ORG: ${{ env.DOCKER_ORG }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
uses: docker/bake-action@v2
DOCKER_ORG: ghcr.io/${{ steps.string.outputs.lowercase }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}-build-arm
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}-build-arm
LABEL_VERSION: ${{ env.MAILU_VERSION }}
PINNED_LABEL_VERSION: ${{ env.PINNED_MAILU_VERSION }}
ARCH: linux/arm64/v8,linux/arm/v7
BUILDER: ${{ steps.uuid.outputs.uuid }}
DOCKER_LOGIN2: ${{ secrets.Docker_Login2 }}
DOCKER_PASSW2: ${{ secrets.Docker_Password2 }}
BUILDX_NO_DEFAULT_ATTESTATIONS: 1
uses: nick-fields/retry@v2
with:
files: ${{env.HCL_FILE}}
targets: ${{ matrix.target }}
load: false
push: false
set: |
*.cache-from=type=local,src=${{ runner.temp }}/cache/${{ matrix.target }}
*.cache-to=type=local,dest=${{ runner.temp }}/cache/${{ matrix.target }},mode=max
*.platform=${{ inputs.architecture }}
timeout_minutes: 30
retry_wait_seconds: 30
max_attempts: 10
shell: bash
command: |
set -euxo pipefail \
; /usr/bin/docker info \
; echo "${{ github.token }}" | docker login --username "${{ github.repository_owner }}" --password-stdin ghcr.io \
; echo "$DOCKER_PASSW2" | docker login --username "$DOCKER_LOGIN2" --password-stdin \
; /usr/bin/docker buildx rm builder-${{ env.BUILDER }} \
|| echo "builder does not exist" \
; /usr/bin/docker buildx create --name builder-${{ env.BUILDER }} --driver docker-container --use \
; /usr/bin/docker buildx bake --push --file ./tests/build.hcl --set *.cache-from=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/base:${{ hashFiles('core/base/Dockerfile','core/base/requirements-prod.txt') }}-arm --set *.cache-to=type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/base:${{ hashFiles('core/base/Dockerfile','core/base/requirements-prod.txt') }}-arm,mode=max --set *.platform=${{ env.ARCH }} ${{ matrix.target }} \
; /usr/bin/docker buildx rm builder-${{ env.BUILDER }}
- name: cleanup docker buildx instance after failure of build step
if: ${{ failure() }}
shell: bash
env:
BUILDER: ${{ steps.uuid.outputs.uuid }}
run: |
/usr/bin/docker buildx rm builder-${{ env.BUILDER }}
# This job runs all the tests.
tests:
name: tests
if: inputs.architecture == 'linux/amd64'
if: contains(inputs.architecture, 'linux/amd64')
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
needs:
- targets
- build
strategy:
fail-fast: false
matrix:
target: ["core", "fetchmail", "filters", "snappymail", "roundcube", "webdav"]
target: ["core", "fetchmail", "filters", "webmail", "webdav"]
time: ["2"]
include:
- target: "filters"
@ -234,112 +417,22 @@ jobs:
echo "MAILU_VERSION=${{ inputs.mailu_version }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV
- name: Configure /cache for image docs
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/docs
key: ${{ github.ref }}-${{ inputs.mailu_version }}-docs-${{ github.run_id }}
- name: Configure /cache for image setup
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/setup
key: ${{ github.ref }}-${{ inputs.mailu_version }}-setup-${{ github.run_id }}
- name: Configure /cache for image admin
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/admin
key: ${{ github.ref }}-${{ inputs.mailu_version }}-admin-${{ github.run_id }}
- name: Configure /cache for image antispam
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/antispam
key: ${{ github.ref }}-${{ inputs.mailu_version }}-antispam-${{ github.run_id }}
- name: Configure /cache for image front
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/front
key: ${{ github.ref }}-${{ inputs.mailu_version }}-front-${{ github.run_id }}
- name: Configure /cache for image imap
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/imap
key: ${{ github.ref }}-${{ inputs.mailu_version }}-imap-${{ github.run_id }}
- name: Configure /cache for image smtp
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/smtp
key: ${{ github.ref }}-${{ inputs.mailu_version }}-smtp-${{ github.run_id }}
- name: Configure /cache for image snappymail
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/snappymail
key: ${{ github.ref }}-${{ inputs.mailu_version }}-snappymail-${{ github.run_id }}
- name: Configure /cache for image roundcube
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/roundcube
key: ${{ github.ref }}-${{ inputs.mailu_version }}-roundcube-${{ github.run_id }}
- name: Configure /cache for image antivirus
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/antivirus
key: ${{ github.ref }}-${{ inputs.mailu_version }}-antivirus-${{ github.run_id }}
- name: Configure /cache for image fetchmail
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/fetchmail
key: ${{ github.ref }}-${{ inputs.mailu_version }}-fetchmail-${{ github.run_id }}
- name: Configure /cache for image resolver
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/resolver
key: ${{ github.ref }}-${{ inputs.mailu_version }}-resolver-${{ github.run_id }}
- name: Configure /cache for image traefik-certdumper
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/traefik-certdumper
key: ${{ github.ref }}-${{ inputs.mailu_version }}-traefik-certdumper-${{ github.run_id }}
- name: Configure /cache for image webdav
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/webdav
key: ${{ github.ref }}-${{ inputs.mailu_version }}-webdav-${{ github.run_id }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- uses: crazy-max/ghaction-github-runtime@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
username: ${{ secrets.Docker_Login }}
password: ${{ secrets.Docker_Password }}
- name: Build docker images for testing from cache
env:
DOCKER_ORG: ${{ env.DOCKER_ORG }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
uses: docker/bake-action@v2
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v5
with:
files: ${{env.HCL_FILE}}
load: true
push: false
set: |
*.cache-from=type=local,src=${{ runner.temp }}/cache/docs
*.cache-from=type=local,src=${{ runner.temp }}/cache/setup
*.cache-from=type=local,src=${{ runner.temp }}/cache/admin
*.cache-from=type=local,src=${{ runner.temp }}/cache/antispam
*.cache-from=type=local,src=${{ runner.temp }}/cache/front
*.cache-from=type=local,src=${{ runner.temp }}/cache/imap
*.cache-from=type=local,src=${{ runner.temp }}/cache/smtp
*.cache-from=type=local,src=${{ runner.temp }}/cache/snappymail
*.cache-from=type=local,src=${{ runner.temp }}/cache/roundcube
*.cache-from=type=local,src=${{ runner.temp }}/cache/antivirus
*.cache-from=type=local,src=${{ runner.temp }}/cache/fetchmail
*.cache-from=type=local,src=${{ runner.temp }}/cache/resolver
*.cache-from=type=local,src=${{ runner.temp }}/cache/traefik-certdumper
*.cache-from=type=local,src=${{ runner.temp }}/cache/webdav
*.platform=${{ inputs.architecture }}
string: ${{ github.repository_owner }}
- name: Install python packages
run: python3 -m pip install -r tests/requirements.txt
- name: Copy all certs
@ -347,12 +440,10 @@ jobs:
- name: Test ${{ matrix.target }}
run: python tests/compose/test.py ${{ matrix.target }} ${{ matrix.time }}
env:
DOCKER_ORG: ${{ env.DOCKER_ORG }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
DOCKER_ORG: ghcr.io/${{ steps.string.outputs.lowercase }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}-build
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}-build
# This job deploys the docker images to the docker repository. The build.hcl file contains logic that determines what tags are pushed.
# E.g. for master only the :master and :latest tags are pushed.
deploy:
name: Deploy images
# Deploying is not required for staging
@ -360,129 +451,12 @@ jobs:
runs-on: ubuntu-latest
needs:
- build
- tests
steps:
- uses: actions/checkout@v3
- name: Retrieve global variables
shell: bash
run: |
echo "BRANCH=${{ inputs.branch }}" >> $GITHUB_ENV
echo "MAILU_VERSION=${{ inputs.mailu_version }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV
- name: Configure /cache for image docs
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/docs
key: ${{ github.ref }}-${{ inputs.mailu_version }}-docs-${{ github.run_id }}
- name: Configure /cache for image setup
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/setup
key: ${{ github.ref }}-${{ inputs.mailu_version }}-setup-${{ github.run_id }}
- name: Configure /cache for image admin
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/admin
key: ${{ github.ref }}-${{ inputs.mailu_version }}-admin-${{ github.run_id }}
- name: Configure /cache for image antispam
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/antispam
key: ${{ github.ref }}-${{ inputs.mailu_version }}-antispam-${{ github.run_id }}
- name: Configure /cache for image front
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/front
key: ${{ github.ref }}-${{ inputs.mailu_version }}-front-${{ github.run_id }}
- name: Configure /cache for image imap
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/imap
key: ${{ github.ref }}-${{ inputs.mailu_version }}-imap-${{ github.run_id }}
- name: Configure /cache for image smtp
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/smtp
key: ${{ github.ref }}-${{ inputs.mailu_version }}-smtp-${{ github.run_id }}
- name: Configure /cache for image snappymail
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/snappymail
key: ${{ github.ref }}-${{ inputs.mailu_version }}-snappymail-${{ github.run_id }}
- name: Configure /cache for image roundcube
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/roundcube
key: ${{ github.ref }}-${{ inputs.mailu_version }}-roundcube-${{ github.run_id }}
- name: Configure /cache for image antivirus
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/antivirus
key: ${{ github.ref }}-${{ inputs.mailu_version }}-antivirus-${{ github.run_id }}
- name: Configure /cache for image fetchmail
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/fetchmail
key: ${{ github.ref }}-${{ inputs.mailu_version }}-fetchmail-${{ github.run_id }}
- name: Configure /cache for image resolver
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/resolver
key: ${{ github.ref }}-${{ inputs.mailu_version }}-resolver-${{ github.run_id }}
- name: Configure /cache for image traefik-certdumper
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/traefik-certdumper
key: ${{ github.ref }}-${{ inputs.mailu_version }}-traefik-certdumper-${{ github.run_id }}
- name: Configure /cache for image webdav
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/webdav
key: ${{ github.ref }}-${{ inputs.mailu_version }}-webdav-${{ github.run_id }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- uses: crazy-max/ghaction-github-runtime@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.Docker_Login }}
password: ${{ secrets.Docker_Password }}
- name: Deploy images to docker hub. Build.hcl contains the logic for the tags that are pushed.
env:
DOCKER_ORG: ${{ env.DOCKER_ORG }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
uses: docker/bake-action@v2
with:
files: ${{env.HCL_FILE}}
push: true
set: |
*.cache-from=type=local,src=${{ runner.temp }}/cache/docs
*.cache-from=type=local,src=${{ runner.temp }}/cache/setup
*.cache-from=type=local,src=${{ runner.temp }}/cache/admin
*.cache-from=type=local,src=${{ runner.temp }}/cache/antispam
*.cache-from=type=local,src=${{ runner.temp }}/cache/front
*.cache-from=type=local,src=${{ runner.temp }}/cache/imap
*.cache-from=type=local,src=${{ runner.temp }}/cache/smtp
*.cache-from=type=local,src=${{ runner.temp }}/cache/snappymail
*.cache-from=type=local,src=${{ runner.temp }}/cache/roundcube
*.cache-from=type=local,src=${{ runner.temp }}/cache/antivirus
*.cache-from=type=local,src=${{ runner.temp }}/cache/fetchmail
*.cache-from=type=local,src=${{ runner.temp }}/cache/resolver
*.cache-from=type=local,src=${{ runner.temp }}/cache/traefik-certdumper
*.cache-from=type=local,src=${{ runner.temp }}/cache/webdav
*.platform=${{ inputs.architecture }}
deploy-arm:
name: Deploy images for arm
# Deploying is not required for staging
if: inputs.deploy == 'true' && inputs.architecture != 'linux/amd64'
runs-on: self-hosted
needs:
- build-arm
- tests
strategy:
fail-fast: false
matrix:
target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "oletools", "postfix", "dovecot", "unbound", "nginx"]
steps:
- uses: actions/checkout@v3
- name: Retrieve global variables
@ -492,111 +466,53 @@ jobs:
echo "MAILU_VERSION=${{ inputs.mailu_version }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV
- name: Configure /cache for image docs
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/docs
key: ${{ github.ref }}-${{ inputs.mailu_version }}-docs-${{ github.run_id }}
- name: Configure /cache for image setup
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/setup
key: ${{ github.ref }}-${{ inputs.mailu_version }}-setup-${{ github.run_id }}
- name: Configure /cache for image admin
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/admin
key: ${{ github.ref }}-${{ inputs.mailu_version }}-admin-${{ github.run_id }}
- name: Configure /cache for image antispam
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/antispam
key: ${{ github.ref }}-${{ inputs.mailu_version }}-antispam-${{ github.run_id }}
- name: Configure /cache for image front
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/front
key: ${{ github.ref }}-${{ inputs.mailu_version }}-front-${{ github.run_id }}
- name: Configure /cache for image imap
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/imap
key: ${{ github.ref }}-${{ inputs.mailu_version }}-imap-${{ github.run_id }}
- name: Configure /cache for image smtp
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/smtp
key: ${{ github.ref }}-${{ inputs.mailu_version }}-smtp-${{ github.run_id }}
- name: Configure /cache for image snappymail
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/snappymail
key: ${{ github.ref }}-${{ inputs.mailu_version }}-snappymail-${{ github.run_id }}
- name: Configure /cache for image roundcube
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/roundcube
key: ${{ github.ref }}-${{ inputs.mailu_version }}-roundcube-${{ github.run_id }}
- name: Configure /cache for image antivirus
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/antivirus
key: ${{ github.ref }}-${{ inputs.mailu_version }}-antivirus-${{ github.run_id }}
- name: Configure /cache for image fetchmail
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/fetchmail
key: ${{ github.ref }}-${{ inputs.mailu_version }}-fetchmail-${{ github.run_id }}
- name: Configure /cache for image resolver
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/resolver
key: ${{ github.ref }}-${{ inputs.mailu_version }}-resolver-${{ github.run_id }}
- name: Configure /cache for image traefik-certdumper
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/traefik-certdumper
key: ${{ github.ref }}-${{ inputs.mailu_version }}-traefik-certdumper-${{ github.run_id }}
- name: Configure /cache for image webdav
uses: actions/cache@v3
with:
path: ${{ runner.temp }}/cache/webdav
key: ${{ github.ref }}-${{ inputs.mailu_version }}-webdav-${{ github.run_id }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- uses: crazy-max/ghaction-github-runtime@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
username: ${{ secrets.Docker_Login }}
password: ${{ secrets.Docker_Password }}
- name: Deploy images to docker hub. Build.hcl contains the logic for the tags that are pushed.
env:
DOCKER_ORG: ${{ env.DOCKER_ORG }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
uses: docker/bake-action@v2
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v5
with:
files: ${{env.HCL_FILE}}
push: true
set: |
*.cache-from=type=local,src=${{ runner.temp }}/cache/docs
*.cache-from=type=local,src=${{ runner.temp }}/cache/setup
*.cache-from=type=local,src=${{ runner.temp }}/cache/admin
*.cache-from=type=local,src=${{ runner.temp }}/cache/antispam
*.cache-from=type=local,src=${{ runner.temp }}/cache/front
*.cache-from=type=local,src=${{ runner.temp }}/cache/imap
*.cache-from=type=local,src=${{ runner.temp }}/cache/smtp
*.cache-from=type=local,src=${{ runner.temp }}/cache/snappymail
*.cache-from=type=local,src=${{ runner.temp }}/cache/roundcube
*.cache-from=type=local,src=${{ runner.temp }}/cache/antivirus
*.cache-from=type=local,src=${{ runner.temp }}/cache/fetchmail
*.cache-from=type=local,src=${{ runner.temp }}/cache/resolver
*.cache-from=type=local,src=${{ runner.temp }}/cache/traefik-certdumper
*.cache-from=type=local,src=${{ runner.temp }}/cache/webdav
*.platform=${{ inputs.architecture }}
string: ${{ github.repository_owner }}
- name: Push multiarch image to Github (ghcr.io)
if: contains(inputs.architecture, 'linux/amd64') && contains(inputs.architecture, 'linux/arm64/v8,linux/arm/v7')
shell: bash
run: |
if [ '${{ env.MAILU_VERSION }}' == 'master' ]; then pinned_mailu_version='master'; else pinned_mailu_version=${{ env.PINNED_MAILU_VERSION}}; fi;
docker buildx imagetools create \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:${{ env.MAILU_VERSION }} \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:$pinned_mailu_version \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:latest \
ghcr.io/${{ steps.string.outputs.lowercase }}/${{ matrix.target }}:${{ env.MAILU_VERSION }}-build \
ghcr.io/${{ steps.string.outputs.lowercase }}/${{ matrix.target }}:${{ env.MAILU_VERSION }}-build-arm
- name: Push x64 image to Github (ghcr.io)
if: contains(inputs.architecture, 'linux/amd64') && !contains(inputs.architecture, 'linux/arm64/v8,linux/arm/v7')
shell: bash
run: |
if [ '${{ env.MAILU_VERSION }}' == 'master' ]; then pinned_mailu_version='master'; else pinned_mailu_version=${{ env.PINNED_MAILU_VERSION}}; fi;
docker buildx imagetools create \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:${{ env.MAILU_VERSION }} \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:$pinned_mailu_version \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:latest \
ghcr.io/${{ steps.string.outputs.lowercase }}/${{ matrix.target }}:${{ env.MAILU_VERSION }}-build
- name: Push arm image to Github (ghcr.io)
if: contains(inputs.architecture, 'linux/arm64/v8,linux/arm/v7') && !contains(inputs.architecture, 'linux/amd64')
shell: bash
run: |
if [ '${{ env.MAILU_VERSION }}' == 'master' ]; then pinned_mailu_version='master'; else pinned_mailu_version=${{ env.PINNED_MAILU_VERSION}}; fi;
docker buildx imagetools create \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:${{ env.MAILU_VERSION }} \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:$pinned_mailu_version \
--tag ${{ inputs.docker_org }}/${{ matrix.target }}:latest \
ghcr.io/${{ steps.string.outputs.lowercase }}/${{ matrix.target }}:${{ env.MAILU_VERSION }}-build-arm
#This job creates a tagged release. A tag is created for the pinned version x.y.z. The GH release refers to this tag.
tag-release:
@ -609,22 +525,57 @@ jobs:
with:
# fetch-depth 0 is required to also retrieve all tags.
fetch-depth: 0
# A bug in actions/checkout@v3 results in all files having mtime of the job running.
- name: Restore Timestamps
uses: chetan/git-restore-mtime-action@v1
- name: Retrieve global variables
shell: bash
run: |
echo "BRANCH=${{ inputs.branch }}" >> $GITHUB_ENV
echo "MAILU_VERSION=${{ inputs.mailu_version }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV
echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV
- name: Create tag for branch x.y.
shell: bash
run: |
echo git tag ${{ env.PINNED_MAILU_VERSION }} $(/usr/bin/git rev-parse HEAD)
git tag ${{ env.PINNED_MAILU_VERSION }} $(/usr/bin/git rev-parse HEAD)
git push origin ${{ env.PINNED_MAILU_VERSION }}
- name: Show list of changelog files (we pick the newest)
shell: bash
run: |
ls -Artl towncrier/newsfragments
- name: Get latest changelog
id: changelog
shell: bash
run: |
pushd . && cd towncrier/newsfragments && ls -Art | tail -n 1 | cut -d. -f1 | xargs -0I % echo "issue=%" >> $GITHUB_OUTPUT && popd
pushd . && cd towncrier/newsfragments && ls -Art | tail -n 1 | xargs cat | xargs -0I % echo "content=%" >> $GITHUB_OUTPUT && popd
- name: Construct message for release
shell: bash
env:
issue: "${{ steps.changelog.outputs.issue }}"
changelog: "${{ steps.changelog.outputs.content }}"
run: |
message="Changelog :mailbox:
---------
+ ${{ env.changelog }}
+ This release was triggered by PR/Issue [${{ env.issue }}](https://github.com/Mailu/Mailu/issues/${{ env.issue }}).
+ The release notes of the original main release can be accessed via menu item 'Release notes' on [mailu.io](https://mailu.io/).
Update
------
The main version X.Y (e.g. 1.9) will always reflect the latest version of the branch. To update your Mailu installation simply pull the latest images \`docker compose pull && docker compose up -d\`.
The pinned version X.Y.Z (e.g. 1.9.1) is not updated. It is pinned to the commit that was used for creating this release. You can use a pinned version to make sure your Mailu installation is not suddenly updated when recreating containers. The pinned version allows the user to manually update. It also allows to go back to a previous pinned version.
" && echo "$message" >> release_note.md
- name: Show release note
shell: bash
run: |
cat release_note.md
- name: Create release for tag x.y.z.
uses: ncipollo/release-action@v1
with:
bodyFile: "RELEASE_TEMPLATE.md"
bodyFile: "release_note.md"
tag: ${{ env.PINNED_MAILU_VERSION }}
token: ${{ secrets.GITHUB_TOKEN }}

@ -1,4 +1,4 @@
name: start-linux-amd64
name: start-linux-multiarch
on:
push:
branches:
@ -6,23 +6,14 @@ on:
- staging
- '1.9'
- master
- test-*
concurrency: ci-x64-${{ github.ref }}
concurrency: ci-multiarch-${{ github.ref }}
###############################################
# REQUIRED secrets
# ${{ secrets.Docker_Login }}
# Username of docker login for pushing the images to repo env.DOCKER_ORG and env.DOCKER_ORG_TESTS
# ${{ secrets.Docker_Password }}
# Password of docker login for pushing the images to repo env.DOCKER_ORG and env.DOCKER_ORG_TESTS
# Add the above secrets to your github repo to determine where the images will be pushed.
################################################
# REQUIRED global variables
# DOCKER_ORG, docker org used for pushing release images (branch x.y and master)
# DOCKER_ORG_TEST, docker org used for pushing images for testing (branch testing).
# DOCKER_ORG, docker org used for pushing images.
env:
DOCKER_ORG: mailu
DOCKER_ORG_TEST: mailuci
DOCKER_ORG: ghcr.io/mailu
jobs:
# This job calculates all global job variables that are required by all the subsequent jobs.
@ -55,7 +46,7 @@ jobs:
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
echo "DOCKER_ORG_DERIVED=${{ env.DOCKER_ORG_TEST }}" >> $GITHUB_ENV
echo "DOCKER_ORG_DERIVED=${{ env.DOCKER_ORG }}" >> $GITHUB_ENV
echo "DEPLOY=true" >> $GITHUB_ENV
echo "RELEASE=false" >> $GITHUB_ENV
- name: Derive MAILU_VERSION and DEPLOY/RELEASE for other branches than testing
@ -67,7 +58,7 @@ jobs:
echo "DEPLOY=true" >> $GITHUB_ENV
echo "RELEASE=false" >> $GITHUB_ENV
- name: Derive PINNED_MAILU_VERSION and DEPLOY/RELEASE for normal release x.y
if: env.BRANCH != 'testing' && env.BRANCH != 'staging' && env.BRANCH != 'master'
if: env.BRANCH != 'testing' && env.BRANCH != 'staging' && env.BRANCH != 'master' && !(contains(env.BRANCH, 'test-'))
shell: bash
run: |
version=$( git tag --sort=version:refname --list "${{ env.MAILU_VERSION }}.*" | tail -1 );root_version=${version%.*};patch_version=${version##*.};if [ "$patch_version" == "" ]; then pinned_version=${{ env.MAILU_VERSION }}.0; else pinned_version=$root_version.$(expr $patch_version + 1); fi;echo "PINNED_MAILU_VERSION=$pinned_version" >> $GITHUB_ENV
@ -82,7 +73,7 @@ jobs:
echo "PINNED_MAILU_VERSION=staging" >> $GITHUB_ENV
echo "DEPLOY=false" >> $GITHUB_ENV
echo "RELEASE=false" >> $GITHUB_ENV
- name: Derive PINNED_MAILU_VERSION for staging for master
- name: Derive PINNED_MAILU_VERSION for master
if: env.BRANCH == 'master'
shell: bash
env:
@ -91,13 +82,21 @@ jobs:
echo "PINNED_MAILU_VERSION=$GITHUB_SHA" >> $GITHUB_ENV
echo "DEPLOY=true" >> $GITHUB_ENV
echo "RELEASE=false" >> $GITHUB_ENV
- name: Derive for branch test-*
if: contains(env.BRANCH, 'test-')
run: |
echo "MAILU_VERSION=${{ env.BRANCH }}" >> $GITHUB_ENV
echo "PINNED_MAILU_VERSION=${{ env.BRANCH }}" >> $GITHUB_ENV
echo "DOCKER_ORG_DERIVED=${{ env.DOCKER_ORG }}" >> $GITHUB_ENV
echo "DEPLOY=true" >> $GITHUB_ENV
echo "RELEASE=false" >> $GITHUB_ENV
build-test-deploy:
needs:
- derive-variables
uses: ./.github/workflows/build_test_deploy.yml
with:
architecture: 'linux/amd64'
architecture: 'linux/amd64,linux/arm64/v8,linux/arm/v7'
mailu_version: ${{needs.derive-variables.outputs.MAILU_VERSION}}
pinned_mailu_version: ${{needs.derive-variables.outputs.PINNED_MAILU_VERSION}}
docker_org: ${{needs.derive-variables.outputs.DOCKER_ORG}}
@ -131,4 +130,4 @@ jobs:
#else
# pinned_version=$root_version.$(expr $patch_version + 1)
#fi
#echo "PINNED_MAILU_VERSION=$pinned_version" >> $GITHUB_ENV
#echo "PINNED_MAILU_VERSION=$pinned_version" >> $GITHUB_ENV

5
.gitignore vendored

@ -1,5 +1,5 @@
*.pyc
*.mo
**/*.pyc
**/*.mo
__pycache__
pip-selfcheck.json
/core/admin/lib*
@ -10,6 +10,7 @@ pip-selfcheck.json
/docs/include
/docs/_build
/.env
/.venv
/docker-compose.yml
/.idea
/.vscode

@ -10,7 +10,7 @@ These settings tell Mailu that the HTTP header with the remote client IP address
For more information see the [configuration reference](https://mailu.io/1.9/configuration.html#advanced-settings).
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).
This is handled when you regenerate the docker-compose file. A fixed hostname is required to retain rspamd history.
This is handled when you regenerate the docker compose file. A fixed hostname is required to retain rspamd history.
After changing mailu.env, it is required to recreate all containers for the changes to be propagated.
@ -314,8 +314,8 @@ v1.6.0 - 2019-01-18
- Enhancement: Reverse proxy - Real ip header and mail-letsencrypt ([#358](https://github.com/Mailu/Mailu/issues/358))
- Enhancement: Parametrize hosts ([#373](https://github.com/Mailu/Mailu/issues/373))
- Enhancement: Expose ports in dockerfiles ([#392](https://github.com/Mailu/Mailu/issues/392))
- Enhancement: Added webmail-imap dependency in docker-compose ([#403](https://github.com/Mailu/Mailu/issues/403))
- Enhancement: Add environment variables to allow running outside of docker-compose ([#429](https://github.com/Mailu/Mailu/issues/429))
- Enhancement: Added webmail-imap dependency in docker compose ([#403](https://github.com/Mailu/Mailu/issues/403))
- Enhancement: Add environment variables to allow running outside of docker compose ([#429](https://github.com/Mailu/Mailu/issues/429))
- Enhancement: Add original Delivered-To header to received messages ([#433](https://github.com/Mailu/Mailu/issues/433))
- Enhancement: Use HOST_ADMIN in "Forwarding authentication server" ([#436](https://github.com/Mailu/Mailu/issues/436), [#437](https://github.com/Mailu/Mailu/issues/437))
- Enhancement: Use POD_ADDRESS_RANGE for Dovecot ([#448](https://github.com/Mailu/Mailu/issues/448))

@ -4,4 +4,4 @@ This project is open source, and your contributions are all welcome. There are m
2. contribute code and/or configuration to the repository (see [the development guidelines](https://mailu.io/master/contributors/workflow.html) for details);
3. contribute localization to your native language (see [the localization docs](https://mailu.io/master/contributors/localization.html) for details);
Either way, keep in mind that the code you write or the translation you produce muts be licensed under the same conditions as the project itself. Additionally, all contributors are considered equal co-authors of the project.
Either way, keep in mind that the code you write or the translation you produce must be licensed under the same conditions as the project itself. Additionally, all contributors are considered equal co-authors of the project.

@ -1,44 +1,71 @@
<!--
Thank you for opening an issue with Mailu. Please understand that issues are meant for bugs and enhancement-requests.
For **user-support questions**, reach out to us on [matrix](https://matrix.to/#/#mailu:tedomum.net).
To be able to help you best, we need some more information.
## Before you open your issue
- [ ] Check if no issue or pull-request for this already exists.
- [ ] Check [documentation](https://mailu.io/master/) and [FAQ](https://mailu.io/master/faq.html). (Tip, use the search function on the documentation page)
- [ ] You understand `Mailu` is made by volunteers in their **free time** — be conscise, civil and accept that delays can occur.
- [ ] The title of the issue should be short and simple. It should contain specific terms related to the actual issue. Be specific while writing the title.
Before you open your issue
- Check if no issue or pull-request for this already exists.
- Check [documentation](https://mailu.io/master/) and [FAQ](https://mailu.io/master/faq.html). (Tip, use the search function on the documentation page)
- You understand `Mailu` is made by volunteers in their **free time** — be concise, civil and accept that delays can occur.
- The title of the issue should be short and simple. It should contain specific terms related to the actual issue. Be specific while writing the title.
Please put your text outside of the comment blocks to be visible. You can use the button "Preview" above to check.
-->
## Environment & Version
## Environment & Versions
### Environment
- [ ] docker-compose
- [ ] kubernetes
- [ ] docker swarm
### Versions
- [ ] docker compose
- [ ] kubernetes
- [ ] docker swarm
### Version
- Version: `master`
<!--
To find your version, get the image name of a mailu container and read the version from the tag (example for version 1.7).
```
$> docker ps -a | grep mailu
140b09d4b09c mailu/roundcube:1.7 "docker-php-entrypoi…" 2 weeks ago Up 2 days (healthy) 80/tcp
$> grep MAILU_VERSION docker-compose.yml mailu.env
```
-->
## Description
<!--
Further explain the bug in a few words. It should be clear what the unexpected behaviour is. Share it in an easy-to-understand language.
-->
## Replication Steps
<!--
Steps for replicating your issue
-->
## Observed behaviour
<!--
Explain or paste the result you received.
-->
## Expected behaviour
Explain what results you expected - be as specific as possible. Just saying "it doesnt work as expected" is not useful. It's also helpful to describe what you actually experienced.
<!--
Explain what results you expected - be as specific as possible.
Just saying "it doesnt work as expected" is not useful. It's also helpful to describe what you actually experienced.
-->
## Logs
Often it is very useful to include log fragments of the involved component. You can get the logs via `docker logs <container name> --tail 1000`. For example for the admin container:
`docker logs mailu_admin_1 --tail 1000`
or using docker-compose `docker-compose -f /mailu/docker-compose.yml logs --tail 1000 admin`
<!--
Often it is very useful to include log fragments of the involved component.
You can get the logs via `docker logs <container name> --tail 1000`.
For example for the admin container: `docker logs mailu_admin_1 --tail 1000`
or using docker compose `docker compose -f /mailu/docker-compose.yml logs --tail 1000 admin`
If you can find the relevant section, please share only the parts that seem relevant. If you have any logs, please enclose them in code tags, like so:
````markdown
```
Your logs here!
```
````
-->

@ -22,8 +22,8 @@ Main features include:
- **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner
- **Antispam**, auto-learn, greylisting, DMARC and SPF
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner, [Snuffleupagus](https://github.com/jvoisin/snuffleupagus/), block malicious attachments
- **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing
- **Freedom**, all FOSS components, no tracker included
![Domains](docs/assets/screenshots/domains.png)

@ -1,5 +1,5 @@
This is a new automatic release of Mailu. The new version can be seen in the tag name.
The main version X.Y (e.g. 1.9) will always reflect the latest version of the branch. To update your Mailu installation simply pull the latest images `docker-compose pull && docker-compose up -d`.
The main version X.Y (e.g. 1.9) will always reflect the latest version of the branch. To update your Mailu installation simply pull the latest images `docker compose pull && docker compose up -d`.
The pinned version X.Y.Z (e.g. 1.9.1) is not updated. It is pinned to the commit that was used for creating this release. You can use a pinned version to make sure your Mailu installation is not suddenly updated when recreating containers. The pinned version allows the user to manually update. It also allows to go back to a previous pinned version.
To check what was changed:

@ -2,3 +2,4 @@
lib64
.vscode
tags
dev

@ -1,61 +1,31 @@
# First stage to build assets
ARG DISTRO=alpine:3.14.5
# syntax=docker/dockerfile-upstream:1.4.3
FROM node:16-alpine3.16 as assets
COPY package.json ./
RUN set -eu \
&& npm config set update-notifier false \
&& npm install --no-fund
COPY webpack.config.js ./
COPY assets ./assets
RUN set -eu \
&& 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
FROM $DISTRO
ARG VERSION
ENV TZ Etc/UTC
# admin image
FROM base
ARG VERSION=local
LABEL version=$VERSION
# python3 shared with most images
RUN set -eu \
&& apk add --no-cache python3 py3-pip py3-wheel git bash tzdata \
&& pip3 install --upgrade pip
RUN set -euxo pipefail \
; apk add --no-cache libressl mariadb-connector-c postgresql-libs
RUN mkdir -p /app
WORKDIR /app
COPY --from=assets /work/static/ ./mailu/static/
COPY requirements-prod.txt requirements.txt
RUN set -eu \
&& apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \
&& pip install --no-cache-dir -r requirements.txt --only-binary=:all: --no-binary=Flask-bootstrap,PyYAML,SQLAlchemy \
|| ( apk add --no-cache --virtual build-dep libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \
&& pip install --upgrade pip \
&& pip install -r requirements.txt \
&& apk del --no-cache build-dep )
COPY audit.py /
COPY start.py /
COPY --from=assets static ./mailu/static
COPY mailu ./mailu
COPY migrations ./migrations
COPY start.py /start.py
COPY audit.py /audit.py
COPY migrations/ ./migrations/
RUN pybabel compile -d mailu/translations
COPY mailu/ ./mailu/
RUN set -euxo pipefail \
; venv/bin/pybabel compile -d mailu/translations
RUN echo $VERSION >/version
#EXPOSE 80/tcp
HEALTHCHECK CMD curl -skfLo /dev/null http://localhost/ping
EXPOSE 80/tcp
VOLUME ["/data","/dkim"]
ENV FLASK_APP mailu
ENV FLASK_APP=mailu
CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/sso/login?next=ui.index || exit 1
RUN echo $VERSION >> /version

@ -0,0 +1,22 @@
# syntax=docker/dockerfile-upstream:1.4.3
FROM node:16-alpine3.16
WORKDIR /work
COPY package.json ./
RUN set -euxo pipefail \
; npm config set update-notifier false \
; npm install --no-audit --no-fund \
; sed -i 's/#007bff/#55a5d9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \
; mkdir assets \
; 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
COPY assets/ ./assets/
COPY webpack.config.js ./
RUN set -euxo pipefail \
; node_modules/.bin/webpack-cli --color

@ -1,79 +0,0 @@
require('./app.css');
import logo from './mailu.png';
import modules from "./*.json";
// 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,
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);
}
});

@ -57,3 +57,9 @@ fieldset:disabled .form-control:disabled {
.input-group-text {
margin-right: 1em;
}
/* version string */
.mailu-version {
font-size: 60%;
line-height: 0;
}

@ -0,0 +1,139 @@
// Inspired from https://github.com/mehdibo/hibp-js/blob/master/hibp.js
function sha1(string) {
var buffer = new TextEncoder("utf-8").encode(string);
return crypto.subtle.digest("SHA-1", buffer).then(function (buffer) {
// Get the hex code
var hexCodes = [];
var view = new DataView(buffer);
for (var i = 0; i < view.byteLength; i += 4) {
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
var value = view.getUint32(i);
// toString(16) will give the hex representation of the number without padding
var stringValue = value.toString(16);
// We use concatenation and slice for padding
var padding = '00000000';
var paddedValue = (padding + stringValue).slice(-padding.length);
hexCodes.push(paddedValue);
}
// Join all the hex strings into one
return hexCodes.join("");
});
}
function hibpCheck(pwd) {
// We hash the pwd first
sha1(pwd).then(function(hash){
// We send the first 5 chars of the hash to hibp's API
const req = new XMLHttpRequest();
req.open('GET', 'https://api.pwnedpasswords.com/range/'+hash.substr(0, 5));
req.setRequestHeader('Add-Padding', 'true');
req.addEventListener("load", function(){
// When we get back a response from the server
// We create an array of lines and loop through them
const lines = this.responseText.split("\n");
const hashSub = hash.slice(5).toUpperCase();
for (var i in lines){
// Check if the line matches the rest of the hash
if (lines[i].substring(0, 35) == hashSub){
const val = parseInt(lines[i].trimEnd("\r").split(":")[1]);
if (val > 0) {
$("#pwned").val(val);
}
return; // If found no need to continue the loop
}
}
$("#pwned").val(0);
});
req.send();
});
}
// 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 unit = $(this).data('unit');
if (typeof unit === 'undefined' || unit === false) {
unit=1;
}
$(this).on('input', function() {
var num = (infinity && this.value == 0) ? '∞' : (this.value/unit).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,
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);
}
if (window.isSecureContext) {
$("#pw").on("change paste", function(){
hibpCheck($(this).val());
return true;
});
$("#pw").closest("form").submit(function(event){
if (parseInt($("#pwned").val()) < 0) {
hibpCheck($("#pw").val());
}
});
}
});

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

@ -1,5 +1,5 @@
// AdminLTE
import 'admin-lte/plugins/jquery/jquery.min.js';
window.$ = window.jQuery = require('admin-lte/plugins/jquery/jquery.min.js');
import 'admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js';
import 'admin-lte/build/scss/adminlte.scss';
import 'admin-lte/build/js/AdminLTE.js';
@ -18,7 +18,7 @@ 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';
import modules from "./*.json";
// clipboard.js
import 'clipboard/dist/clipboard.min.js';
window.ClipboardJS = require('clipboard/dist/clipboard.min.js');

@ -9,7 +9,7 @@ module.exports = {
mode: 'production',
entry: {
app: {
import: './assets/app.js',
import: ['./assets/app.css', './assets/mailu.png', './assets/app.js'],
dependOn: 'vendor',
},
vendor: './assets/vendor.js',

@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python3
import sys
import tabulate

@ -5,9 +5,24 @@ import flask
import flask_bootstrap
from mailu import utils, debug, models, manage, configuration
from gunicorn import glogging
import logging
import hmac
class NoPingFilter(logging.Filter):
def filter(self, record):
if not (record.args['{host}i'] == 'localhost' and record.args['r'] == 'GET /ping HTTP/1.1'):
return True
class Logger(glogging.Logger):
def setup(self, cfg):
super().setup(cfg)
# Add filters to Gunicorn logger
logger = logging.getLogger("gunicorn.access")
logger.addFilter(NoPingFilter())
def create_app_from_config(config):
""" Create a new application based on the given configuration
"""
@ -44,8 +59,10 @@ def create_app_from_config(config):
# Initialize debugging tools
if app.config.get("DEBUG"):
debug.toolbar.init_app(app)
# TODO: add a specific configuration variable for profiling
# debug.profiler.init_app(app)
if app.config.get("DEBUG_PROFILER"):
debug.profiler.init_app(app)
if assets := app.config.get('DEBUG_ASSETS'):
app.static_folder = assets
# Inject the default variables in the Jinja parser
# TODO: move this to blueprints when needed
@ -55,6 +72,7 @@ def create_app_from_config(config):
return dict(
signup_domains= signup_domains,
config = app.config,
get_locale = utils.get_locale,
)
# Jinja filters
@ -66,11 +84,16 @@ def create_app_from_config(config):
def format_datetime(value):
return utils.flask_babel.format_datetime(value) if value else ''
def ping():
return ''
app.route('/ping')(ping)
# Import views
from mailu import ui, internal, sso
from mailu import ui, internal, sso, api
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')
api.register(app, web_api_root=app.config.get('WEB_API'))
return app

@ -0,0 +1,32 @@
from flask import redirect, url_for, Blueprint
from flask_restx import apidoc
from . import v1 as APIv1
def register(app, web_api_root):
APIv1.app = app
# register api bluprint(s)
apidoc.apidoc.url_prefix = f'{web_api_root}/v{int(APIv1.VERSION)}'
APIv1.api_token = app.config['API_TOKEN']
if app.config['API_TOKEN'] != '':
app.register_blueprint(APIv1.blueprint, url_prefix=f'{web_api_root}/v{int(APIv1.VERSION)}')
# add redirect to current api version
redirect_api = Blueprint('redirect_api', __name__)
@redirect_api.route('/')
def redir():
return redirect(url_for(f'{APIv1.blueprint.name}.root'))
app.register_blueprint(redirect_api, url_prefix=f'{web_api_root}')
# swagger ui config
app.config.SWAGGER_UI_DOC_EXPANSION = 'list'
app.config.SWAGGER_UI_OPERATION_ID = True
app.config.SWAGGER_UI_REQUEST_DURATION = True
app.config.RESTX_MASK_SWAGGER = False
else:
api = Blueprint('api', __name__)
@api.route('/', defaults={'path': ''})
@api.route('/<path:path>')
def api_token_missing(path):
return "<p>Error: API_TOKEN is not configured</p>", 500
app.register_blueprint(api, url_prefix=f'{web_api_root}')

@ -0,0 +1,42 @@
from .. import models, utils
from . import v1
from flask import request
import flask
import hmac
from functools import wraps
from flask_restx import abort
from sqlalchemy.sql.expression import label
def fqdn_in_use(name):
d = models.db.session.query(label('name', models.Domain.name))
a = models.db.session.query(label('name', models.Alternative.name))
r = models.db.session.query(label('name', models.Relay.name))
u = d.union_all(a).union_all(r).filter_by(name=name)
if models.db.session.query(u.exists()).scalar():
return True
return False
""" Decorator for validating api token for authentication """
def api_token_authorization(func):
@wraps(func)
def decorated_function(*args, **kwds):
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
if utils.limiter.should_rate_limit_ip(client_ip):
abort(429, 'Too many attempts from your IP (rate-limit)' )
if not request.headers.get('Authorization'):
abort(401, 'A valid Bearer token is expected which is provided as request header')
#Client provides 'Authentication: Bearer <token>'
if (' ' in request.headers.get('Authorization')
and not hmac.compare_digest(request.headers.get('Authorization'), 'Bearer ' + v1.api_token)):
utils.limiter.rate_limit_ip(client_ip)
flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.')
abort(403, 'A valid Bearer token is expected which is provided as request header')
#Client provides 'Authentication: <token>'
elif (' ' not in request.headers.get('Authorization')
and not hmac.compare_digest(request.headers.get('Authorization'), v1.api_token)):
utils.limiter.rate_limit_ip(client_ip)
flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.')
abort(403, 'A valid Bearer token is expected which is provided as request header')
flask.current_app.logger.info(f'Valid API token provided by {client_ip}.')
return func(*args, **kwds)
return decorated_function

@ -0,0 +1,43 @@
from flask import Blueprint
from flask_restx import Api, fields
VERSION = 1.0
api_token = None
blueprint = Blueprint(f'api_v{int(VERSION)}', __name__)
authorization = {
'Bearer': {
'type': 'apiKey',
'in': 'header',
'name': 'Authorization'
}
}
api = Api(
blueprint, version=f'{VERSION:.1f}',
title='Mailu API', default_label='Mailu',
validate=True,
authorizations=authorization,
security='Bearer',
doc='/'
)
response_fields = api.model('Response', {
'code': fields.Integer,
'message': fields.String,
})
error_fields = api.model('Error', {
'errors': fields.Nested(api.model('Error_Key', {
'key': fields.String,
'message':fields.String
})),
'message': fields.String,
})
from . import domains
from . import alias
from . import relay
from . import user

@ -0,0 +1,126 @@
from flask_restx import Resource, fields, marshal
from . import api, response_fields
from .. import common
from ... import models
db = models.db
alias = api.namespace('alias', description='Alias operations')
alias_fields_update = alias.model('AliasUpdate', {
'comment': fields.String(description='a comment'),
'destination': fields.List(fields.String(description='alias email address', example='user@example.com')),
'wildcard': fields.Boolean(description='enable SQL Like wildcard syntax')
})
alias_fields = alias.inherit('Alias',alias_fields_update, {
'email': fields.String(description='the alias email address', example='user@example.com', required=True),
'destination': fields.List(fields.String(description='alias email address', example='user@example.com', required=True)),
})
@alias.route('')
class Aliases(Resource):
@alias.doc('list_alias')
@alias.marshal_with(alias_fields, as_list=True, skip_none=True, mask=None)
@alias.doc(security='Bearer')
@common.api_token_authorization
def get(self):
""" List aliases """
return models.Alias.query.all()
@alias.doc('create_alias')
@alias.expect(alias_fields)
@alias.response(200, 'Success', response_fields)
@alias.response(400, 'Input validation exception', response_fields)
@alias.response(409, 'Duplicate alias', response_fields)
@alias.doc(security='Bearer')
@common.api_token_authorization
def post(self):
""" Create a new alias """
data = api.payload
alias_found = models.Alias.query.filter_by(email = data['email']).first()
if alias_found:
return { 'code': 409, 'message': f'Duplicate alias {data["email"]}'}, 409
alias_model = models.Alias(email=data["email"],destination=data['destination'])
if 'comment' in data:
alias_model.comment = data['comment']
if 'wildcard' in data:
alias_model.wildcard = data['wildcard']
db.session.add(alias_model)
db.session.commit()
return {'code': 200, 'message': f'Alias {data["email"]} to destination {data["destination"]} has been created'}, 200
@alias.route('/<string:alias>')
class Alias(Resource):
@alias.doc('find_alias')
@alias.response(200, 'Success', alias_fields)
@alias.response(404, 'Alias not found', response_fields)
@alias.doc(security='Bearer')
@common.api_token_authorization
def get(self, alias):
""" Find alias """
alias_found = models.Alias.query.filter_by(email = alias).first()
if alias_found is None:
return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404
else:
return marshal(alias_found,alias_fields), 200
@alias.doc('update_alias')
@alias.expect(alias_fields_update)
@alias.response(200, 'Success', response_fields)
@alias.response(404, 'Alias not found', response_fields)
@alias.response(400, 'Input validation exception', response_fields)
@alias.doc(security='Bearer')
@common.api_token_authorization
def patch(self, alias):
""" Update alias """
data = api.payload
alias_found = models.Alias.query.filter_by(email = alias).first()
if alias_found is None:
return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404
if 'comment' in data:
alias_found.comment = data['comment']
if 'destination' in data:
alias_found.destination = data['destination']
if 'wildcard' in data:
alias_found.wildcard = data['wildcard']
db.session.add(alias_found)
db.session.commit()
return {'code': 200, 'message': f'Alias {alias} has been updated'}
@alias.doc('delete_alias')
@alias.response(200, 'Success', response_fields)
@alias.response(404, 'Alias not found', response_fields)
@alias.doc(security='Bearer')
@common.api_token_authorization
def delete(self, alias):
""" Delete alias """
alias_found = models.Alias.query.filter_by(email = alias).first()
if alias_found is None:
return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404
db.session.delete(alias_found)
db.session.commit()
return {'code': 200, 'message': f'Alias {alias} has been deleted'}, 200
@alias.route('/destination/<string:domain>')
class AliasWithDest(Resource):
@alias.doc('find_alias_filter_domain')
@alias.response(200, 'Success', alias_fields)
@alias.response(404, 'Alias or domain not found', response_fields)
@alias.doc(security='Bearer')
@common.api_token_authorization
def get(self, domain):
""" Find aliases of domain """
domain_found = models.Domain.query.filter_by(name=domain).first()
if domain_found is None:
return { 'code': 404, 'message': f'Domain {domain} cannot be found'}, 404
aliases_found = domain_found.aliases
if aliases_found.count == 0:
return { 'code': 404, 'message': f'No alias can be found for domain {domain}'}, 404
else:
return marshal(aliases_found, alias_fields), 200

@ -0,0 +1,410 @@
import validators
from flask_restx import Resource, fields, marshal
from . import api, response_fields, user
from .. import common
from ... import models
db = models.db
dom = api.namespace('domain', description='Domain operations')
alt = api.namespace('alternative', description='Alternative operations')
domain_fields = api.model('Domain', {
'name': fields.String(description='FQDN (e.g. example.com)', example='example.com', required=True),
'comment': fields.String(description='a comment'),
'max_users': fields.Integer(description='maximum number of users', min=-1, default=-1),
'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1),
'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0),
'signup_enabled': fields.Boolean(description='allow signup'),
'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')),
})
domain_fields_update = api.model('DomainUpdate', {
'comment': fields.String(description='a comment'),
'max_users': fields.Integer(description='maximum number of users', min=-1, default=-1),
'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1),
'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0),
'signup_enabled': fields.Boolean(description='allow signup'),
'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')),
})
domain_fields_get = api.model('DomainGet', {
'name': fields.String(description='FQDN (e.g. example.com)', example='example.com', required=True),
'comment': fields.String(description='a comment'),
'max_users': fields.Integer(description='maximum number of users', min=-1, default=-1),
'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1),
'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0),
'signup_enabled': fields.Boolean(description='allow signup'),
'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')),
'dns_autoconfig': fields.List(fields.String(description='DNS client auto-configuration entry')),
'dns_mx': fields.String(Description='MX record for domain'),
'dns_spf': fields.String(Description='SPF record for domain'),
'dns_dkim': fields.String(Description='DKIM record for domain'),
'dns_dmarc': fields.String(Description='DMARC record for domain'),
'dns_dmarc_report': fields.String(Description='DMARC report record for domain'),
'dns_tlsa': fields.String(Description='TLSA record for domain'),
})
domain_fields_dns = api.model('DomainDNS', {
'dns_autoconfig': fields.List(fields.String(description='DNS client auto-configuration entry')),
'dns_mx': fields.String(Description='MX record for domain'),
'dns_spf': fields.String(Description='SPF record for domain'),
'dns_dkim': fields.String(Description='DKIM record for domain'),
'dns_dmarc': fields.String(Description='DMARC record for domain'),
'dns_dmarc_report': fields.String(Description='DMARC report record for domain'),
'dns_tlsa': fields.String(Description='TLSA record for domain'),
})
manager_fields = api.model('Manager', {
'domain_name': fields.String(description='domain managed by manager'),
'user_email': fields.String(description='email address of manager'),
})
manager_fields_create = api.model('ManagerCreate', {
'user_email': fields.String(description='email address of manager', required=True),
})
alternative_fields_update = api.model('AlternativeDomainUpdate', {
'domain': fields.String(description='domain FQDN', example='example.com', required=False),
})
alternative_fields = api.model('AlternativeDomain', {
'name': fields.String(description='alternative FQDN', example='example2.com', required=True),
'domain': fields.String(description='domain FQDN', example='example.com', required=True),
})
@dom.route('')
class Domains(Resource):
@dom.doc('list_domain')
@dom.marshal_with(domain_fields_get, as_list=True, skip_none=True, mask=None)
@dom.doc(security='Bearer')
@common.api_token_authorization
def get(self):
""" List domains """
return models.Domain.query.all()
@dom.doc('create_domain')
@dom.expect(domain_fields)
@dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields)
@dom.response(409, 'Duplicate domain/alternative name', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def post(self):
""" Create a new domain """
data = api.payload
if not validators.domain(data['name']):
return { 'code': 400, 'message': f'Domain {data["name"]} is not a valid domain'}, 400
if common.fqdn_in_use(data['name']):
return { 'code': 409, 'message': f'Duplicate domain name {data["name"]}'}, 409
if 'alternatives' in data:
#check if duplicate alternatives are supplied
if [x for x in data['alternatives'] if data['alternatives'].count(x) >= 2]:
return { 'code': 409, 'message': f'Duplicate alternative domain names in request' }, 409
for item in data['alternatives']:
if common.fqdn_in_use(item):
return { 'code': 409, 'message': f'Duplicate alternative domain name {item}' }, 409
if not validators.domain(item):
return { 'code': 400, 'message': f'Alternative domain {item} is not a valid domain'}, 400
for item in data['alternatives']:
alternative = models.Alternative(name=item, domain_name=data['name'])
models.db.session.add(alternative)
domain_new = models.Domain(name=data['name'])
if 'comment' in data:
domain_new.comment = data['comment']
if 'max_users' in data:
domain_new.comment = data['max_users']
if 'max_aliases' in data:
domain_new.comment = data['max_aliases']
if 'max_quota_bytes' in data:
domain_new.comment = data['max_quota_bytes']
if 'signup_enabled' in data:
domain_new.comment = data['signup_enabled']
models.db.session.add(domain_new)
#apply the changes
db.session.commit()
return {'code': 200, 'message': f'Domain {data["name"]} has been created'}, 200
@dom.route('/<domain>')
class Domain(Resource):
@dom.doc('find_domain')
@dom.response(200, 'Success', domain_fields)
@dom.response(404, 'Domain not found', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def get(self, domain):
""" Find domain by name """
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
domain_found = models.Domain.query.get(domain)
if not domain_found:
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
return marshal(domain_found, domain_fields_get), 200
@dom.doc('update_domain')
@dom.expect(domain_fields_update)
@dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Domain not found', response_fields)
@dom.response(409, 'Duplicate domain/alternative name', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def patch(self, domain):
""" Update an existing domain """
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
domain_found = models.Domain.query.get(domain)
if not domain:
return { 'code': 404, 'message': f'Domain {data["name"]} does not exist'}, 404
data = api.payload
if 'alternatives' in data:
#check if duplicate alternatives are supplied
if [x for x in data['alternatives'] if data['alternatives'].count(x) >= 2]:
return { 'code': 409, 'message': f'Duplicate alternative domain names in request' }, 409
for item in data['alternatives']:
if common.fqdn_in_use(item):
return { 'code': 409, 'message': f'Duplicate alternative domain name {item}' }, 409
if not validators.domain(item):
return { 'code': 400, 'message': f'Alternative domain {item} is not a valid domain'}, 400
for item in data['alternatives']:
alternative = models.Alternative(name=item, domain_name=data['name'])
models.db.session.add(alternative)
if 'comment' in data:
domain_found.comment = data['comment']
if 'max_users' in data:
domain_found.comment = data['max_users']
if 'max_aliases' in data:
domain_found.comment = data['max_aliases']
if 'max_quota_bytes' in data:
domain_found.comment = data['max_quota_bytes']
if 'signup_enabled' in data:
domain_found.comment = data['signup_enabled']
models.db.session.add(domain_found)
#apply the changes
db.session.commit()
return {'code': 200, 'message': f'Domain {domain} has been updated'}, 200
@dom.doc('delete_domain')
@dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Domain not found', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def delete(self, domain):
""" Delete domain """
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
domain_found = models.Domain.query.get(domain)
if not domain:
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
db.session.delete(domain_found)
db.session.commit()
return {'code': 200, 'message': f'Domain {domain} has been deleted'}, 200
@dom.route('/<domain>/dkim')
class Domain(Resource):
@dom.doc('generate_dkim')
@dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Domain not found', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def post(self, domain):
""" Generate new DKIM/DMARC keys for domain """
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
domain_found = models.Domain.query.get(domain)
if not domain_found:
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
domain_found.generate_dkim_key()
domain_found.save_dkim_key()
return {'code': 200, 'message': f'DKIM/DMARC keys have been generated for domain {domain}'}, 200
@dom.route('/<domain>/manager')
class Manager(Resource):
@dom.doc('list_managers')
@dom.marshal_with(manager_fields, as_list=True, skip_none=True, mask=None)
@dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'domain not found', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def get(self, domain):
""" List managers of domain """
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
if not domain:
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
domain = models.Domain.query.filter_by(name=domain)
return domain.managers
@dom.doc('create_manager')
@dom.expect(manager_fields_create)
@dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'User or domain not found', response_fields)
@dom.response(409, 'Duplicate domain manager', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def post(self, domain):
""" Create a new domain manager """
data = api.payload
if not validators.email(data['user_email']):
return {'code': 400, 'message': f'Invalid email address {data["user_email"]}'}, 400
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
domain = models.Domain.query.get(domain)
if not domain:
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
user = models.User.query.get(data['user_email'])
if not user:
return { 'code': 404, 'message': f'User {data["user_email"]} does not exist'}, 404
if user in domain.managers:
return {'code': 409, 'message': f'User {data["user_email"]} is already a manager of the domain {domain} '}, 409
domain.managers.append(user)
models.db.session.commit()
return {'code': 200, 'message': f'User {data["user_email"]} has been added as manager of the domain {domain} '},200
@dom.route('/<domain>/manager/<email>')
class Domain(Resource):
@dom.doc('find_manager')
@dom.response(200, 'Success', manager_fields)
@dom.response(404, 'Manager not found', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def get(self, domain, email):
""" Find manager by email address """
if not validators.email(email):
return {'code': 400, 'message': f'Invalid email address {email}'}, 400
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
domain = models.Domain.query.get(domain)
if not domain:
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
user = models.User.query.get(email)
if not user:
return { 'code': 404, 'message': f'User {email} does not exist'}, 404
if user in domain.managers:
for manager in domain.managers:
if manager.email == email:
return marshal(manager, manager_fields),200
else:
return { 'code': 404, 'message': f'User {email} is not a manager of the domain {domain}'}, 404
@dom.doc('delete_manager')
@dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Manager not found', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def delete(self, domain, email):
if not validators.email(email):
return {'code': 400, 'message': f'Invalid email address {email}'}, 400
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
domain = models.Domain.query.get(domain)
if not domain:
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
user = models.User.query.get(email)
if not user:
return { 'code': 404, 'message': f'User {email} does not exist'}, 404
if user in domain.managers:
domain.managers.remove(user)
models.db.session.commit()
return {'code': 200, 'message': f'User {email} has been removed as a manager of the domain {domain} '},200
else:
return { 'code': 404, 'message': f'User {email} is not a manager of the domain {domain}'}, 404
@dom.route('/<domain>/users')
class User(Resource):
@dom.doc('list_user_domain')
@dom.marshal_with(user.user_fields_get, as_list=True, skip_none=True, mask=None)
@dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Domain not found', response_fields)
@dom.doc(security='Bearer')
@common.api_token_authorization
def get(self, domain):
""" List users from domain """
if not validators.domain(domain):
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
domain_found = models.Domain.query.get(domain)
if not domain_found:
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
return models.User.query.filter_by(domain=domain_found).all()
@alt.route('')
class Alternatives(Resource):
@alt.doc('list_alternative')
@alt.marshal_with(alternative_fields, as_list=True, skip_none=True, mask=None)
@alt.doc(security='Bearer')
@common.api_token_authorization
def get(self):
""" List alternatives """
return models.Alternative.query.all()
@alt.doc('create_alternative')
@alt.expect(alternative_fields)
@alt.response(200, 'Success', response_fields)
@alt.response(400, 'Input validation exception', response_fields)
@alt.response(404, 'Domain not found or missing', response_fields)
@alt.response(409, 'Duplicate alternative domain name', response_fields)
@alt.doc(security='Bearer')
@common.api_token_authorization
def post(self):
""" Create new alternative (for domain) """
data = api.payload
if not validators.domain(data['name']):
return { 'code': 400, 'message': f'Alternative domain {data["name"]} is not a valid domain'}, 400
if not validators.domain(data['domain']):
return { 'code': 400, 'message': f'Domain {data["domain"]} is not a valid domain'}, 400
domain = models.Domain.query.get(data['domain'])
if not domain:
return { 'code': 404, 'message': f'Domain {data["domain"]} does not exist'}, 404
if common.fqdn_in_use(data['name']):
return { 'code': 409, 'message': f'Duplicate alternative domain name {data["name"]}'}, 409
alternative = models.Alternative(name=data['name'], domain_name=data['domain'])
models.db.session.add(alternative)
db.session.commit()
return {'code': 200, 'message': f'Alternative {data["name"]} for domain {data["domain"]} has been created'}, 200
@alt.route('/<string:alt>')
class Alternative(Resource):
@alt.doc('find_alternative')
@alt.doc(security='Bearer')
@common.api_token_authorization
def get(self, alt):
""" Find alternative (of domain) """
if not validators.domain(alt):
return { 'code': 400, 'message': f'Alternative domain {alt} is not a valid domain'}, 400
alternative = models.Alternative.query.filter_by(name=alt).first()
if not alternative:
return{ 'code': 404, 'message': f'Alternative domain {alt} does not exist'}, 404
return marshal(alternative, alternative_fields), 200
@alt.doc('delete_alternative')
@alt.response(200, 'Success', response_fields)
@alt.response(400, 'Input validation exception', response_fields)
@alt.response(404, 'Alternative/Domain not found or missing', response_fields)
@alt.response(409, 'Duplicate domain name', response_fields)
@alt.doc(security='Bearer')
@common.api_token_authorization
def delete(self, alt):
""" Delete alternative (for domain) """
if not validators.domain(alt):
return { 'code': 400, 'message': f'Alternative domain {alt} is not a valid domain'}, 400
alternative = models.Alternative.query.filter_by(name=alt).scalar()
if not alternative:
return { 'code': 404, 'message': f'Alternative domain {alt} does not exist'}, 404
domain = alternative.domain_name
db.session.delete(alternative)
db.session.commit()
return {'code': 200, 'message': f'Alternative {alt} for domain {domain} has been deleted'}, 200

@ -0,0 +1,118 @@
from flask_restx import Resource, fields, marshal
import validators
from . import api, response_fields
from .. import common
from ... import models
db = models.db
relay = api.namespace('relay', description='Relay operations')
relay_fields = api.model('Relay', {
'name': fields.String(description='relayed domain name', example='example.com', required=True),
'smtp': fields.String(description='remote host', example='example.com', required=False),
'comment': fields.String(description='a comment', required=False)
})
relay_fields_update = api.model('RelayUpdate', {
'smtp': fields.String(description='remote host', example='example.com', required=False),
'comment': fields.String(description='a comment', required=False)
})
@relay.route('')
class Relays(Resource):
@relay.doc('list_relays')
@relay.marshal_with(relay_fields, as_list=True, skip_none=True, mask=None)
@relay.doc(security='Bearer')
@common.api_token_authorization
def get(self):
"List relays"
return models.Relay.query.all()
@relay.doc('create_relay')
@relay.expect(relay_fields)
@relay.response(200, 'Success', response_fields)
@relay.response(400, 'Input validation exception')
@relay.response(409, 'Duplicate relay', response_fields)
@relay.doc(security='Bearer')
@common.api_token_authorization
def post(self):
""" Create relay """
data = api.payload
if not validators.domain(name):
return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
if common.fqdn_in_use(data['name']):
return { 'code': 409, 'message': f'Duplicate domain {data["name"]}'}, 409
relay_model = models.Relay(name=data['name'])
if 'smtp' in data:
relay_model.smtp = data['smtp']
if 'comment' in data:
relay_model.comment = data['comment']
db.session.add(relay_model)
db.session.commit()
return {'code': 200, 'message': f'Relayed domain {data["name"]} has been created'}, 200
@relay.route('/<string:name>')
class Relay(Resource):
@relay.doc('find_relay')
@relay.response(400, 'Input validation exception', response_fields)
@relay.response(404, 'Relay not found', response_fields)
@relay.doc(security='Bearer')
@common.api_token_authorization
def get(self, name):
""" Find relay """
if not validators.domain(name):
return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
relay_found = models.Relay.query.filter_by(name=name).first()
if relay_found is None:
return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
return marshal(relay_found, relay_fields), 200
@relay.doc('update_relay')
@relay.expect(relay_fields_update)
@relay.response(200, 'Success', response_fields)
@relay.response(400, 'Input validation exception', response_fields)
@relay.response(404, 'Relay not found', response_fields)
@relay.response(409, 'Duplicate relay', response_fields)
@relay.doc(security='Bearer')
@common.api_token_authorization
def patch(self, name):
""" Update relay """
data = api.payload
if not validators.domain(name):
return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
relay_found = models.Relay.query.filter_by(name=name).first()
if relay_found is None:
return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
if 'smtp' in data:
relay_found.smtp = data['smtp']
if 'comment' in data:
relay_found.comment = data['comment']
db.session.add(relay_found)
db.session.commit()
return { 'code': 200, 'message': f'Relayed domain {name} has been updated'}, 200
@relay.doc('delete_relay')
@relay.response(200, 'Success', response_fields)
@relay.response(400, 'Input validation exception', response_fields)
@relay.response(404, 'Relay not found', response_fields)
@relay.doc(security='Bearer')
@common.api_token_authorization
def delete(self, name):
""" Delete relay """
if not validators.domain(name):
return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
relay_found = models.Relay.query.filter_by(name=name).first()
if relay_found is None:
return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
db.session.delete(relay_found)
db.session.commit()
return { 'code': 200, 'message': f'Relayed domain {name} has been deleted'}, 200

@ -0,0 +1,262 @@
from flask_restx import Resource, fields, marshal
import validators, datetime
from . import api, response_fields
from .. import common
from ... import models
db = models.db
user = api.namespace('user', description='User operations')
user_fields_get = api.model('UserGet', {
'email': fields.String(description='The email address of the user', example='John.Doe@example.com', attribute='_email'),
'password': fields.String(description="Hash of the user's password; Example='$bcrypt-sha256$v=2,t=2b,r=12$fmsAdJbYAD1gGQIE5nfJq.$zLkQUEs2XZfTpAEpcix/1k5UTNPm0jO'"),
'comment': fields.String(description='A description for the user. This description is shown on the Users page', example='my comment'),
'quota_bytes': fields.Integer(description='The maximum quota for the users email box in bytes', example='1000000000'),
'global_admin': fields.Boolean(description='Make the user a global administrator'),
'enabled': fields.Boolean(description='Enable the user. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail'),
'enable_imap': fields.Boolean(description='Allow email retrieval via IMAP'),
'enable_pop': fields.Boolean(description='Allow email retrieval via POP3'),
'allow_spoofing': fields.Boolean(description='Allow the user to spoof the sender (send email as anyone)'),
'forward_enabled': fields.Boolean(description='Enable auto forwarding'),
'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='Other@example.com'),
'forward_keep': fields.Boolean(description='Keep a copy of the forwarded email in the inbox'),
'reply_enabled': fields.Boolean(description='Enable automatic replies. This is also known as out of office (ooo) or out of facility (oof) replies'),
'reply_subject': fields.String(description='Optional subject for the automatic reply', example='Out of office'),
'reply_body': fields.String(description='The body of the automatic reply email', example='Hello, I am out of office. I will respond when I am back.'),
'reply_startdate': fields.Date(description='Start date for automatic replies in YYYY-MM-DD format.', example='2022-02-10'),
'reply_enddate': fields.Date(description='End date for automatic replies in YYYY-MM-DD format.', example='2022-02-22'),
'displayed_name': fields.String(description='The display name of the user within the Admin GUI', example='John Doe'),
'spam_enabled': fields.Boolean(description='Enable the spam filter'),
'spam_mark_as_read': fields.Boolean(description='Enable marking spam mails as read'),
'spam_threshold': fields.Integer(description='The user defined spam filter tolerance', example='80'),
})
user_fields_post = api.model('UserCreate', {
'email': fields.String(description='The email address of the user', example='John.Doe@example.com', attribute='_email', required=True),
'raw_password': fields.String(description='The raw (plain text) password of the user. Mailu will hash the password using BCRYPT-SHA256', example='secret', required=True),
'comment': fields.String(description='A description for the user. This description is shown on the Users page', example='my comment'),
'quota_bytes': fields.Integer(description='The maximum quota for the users email box in bytes', example='1000000000'),
'global_admin': fields.Boolean(description='Make the user a global administrator'),
'enabled': fields.Boolean(description='Enable the user. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail'),
'enable_imap': fields.Boolean(description='Allow email retrieval via IMAP'),
'enable_pop': fields.Boolean(description='Allow email retrieval via POP3'),
'allow_spoofing': fields.Boolean(description='Allow the user to spoof the sender (send email as anyone)'),
'forward_enabled': fields.Boolean(description='Enable auto forwarding'),
'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='Other@example.com'),
'forward_keep': fields.Boolean(description='Keep a copy of the forwarded email in the inbox'),
'reply_enabled': fields.Boolean(description='Enable automatic replies. This is also known as out of office (ooo) or out of facility (oof) replies'),
'reply_subject': fields.String(description='Optional subject for the automatic reply', example='Out of office'),
'reply_body': fields.String(description='The body of the automatic reply email', example='Hello, I am out of office. I will respond when I am back.'),
'reply_startdate': fields.Date(description='Start date for automatic replies in YYYY-MM-DD format.', example='2022-02-10'),
'reply_enddate': fields.Date(description='End date for automatic replies in YYYY-MM-DD format.', example='2022-02-22'),
'displayed_name': fields.String(description='The display name of the user within the Admin GUI', example='John Doe'),
'spam_enabled': fields.Boolean(description='Enable the spam filter'),
'spam_mark_as_read': fields.Boolean(description='Enable marking spam mails as read'),
'spam_threshold': fields.Integer(description='The user defined spam filter tolerance', example='80'),
})
user_fields_put = api.model('UserUpdate', {
'raw_password': fields.String(description='The raw (plain text) password of the user. Mailu will hash the password using BCRYPT-SHA256', example='secret'),
'comment': fields.String(description='A description for the user. This description is shown on the Users page', example='my comment'),
'quota_bytes': fields.Integer(description='The maximum quota for the users email box in bytes', example='1000000000'),
'global_admin': fields.Boolean(description='Make the user a global administrator'),
'enabled': fields.Boolean(description='Enable the user. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail'),
'enable_imap': fields.Boolean(description='Allow email retrieval via IMAP'),
'enable_pop': fields.Boolean(description='Allow email retrieval via POP3'),
'allow_spoofing': fields.Boolean(description='Allow the user to spoof the sender (send email as anyone)'),
'forward_enabled': fields.Boolean(description='Enable auto forwarding'),
'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='Other@example.com'),
'forward_keep': fields.Boolean(description='Keep a copy of the forwarded email in the inbox'),
'reply_enabled': fields.Boolean(description='Enable automatic replies. This is also known as out of office (ooo) or out of facility (oof) replies'),
'reply_subject': fields.String(description='Optional subject for the automatic reply', example='Out of office'),
'reply_body': fields.String(description='The body of the automatic reply email', example='Hello, I am out of office. I will respond when I am back.'),
'reply_startdate': fields.Date(description='Start date for automatic replies in YYYY-MM-DD format.', example='2022-02-10'),
'reply_enddate': fields.Date(description='End date for automatic replies in YYYY-MM-DD format.', example='2022-02-22'),
'displayed_name': fields.String(description='The display name of the user within the Admin GUI', example='John Doe'),
'spam_enabled': fields.Boolean(description='Enable the spam filter'),
'spam_mark_as_read': fields.Boolean(description='Enable marking spam mails as read'),
'spam_threshold': fields.Integer(description='The user defined spam filter tolerance', example='80'),
})
@user.route('')
class Users(Resource):
@user.doc('list_users')
@user.marshal_with(user_fields_get, as_list=True, skip_none=True, mask=None)
@user.doc(security='Bearer')
@common.api_token_authorization
def get(self):
"List users"
return models.User.query.all()
@user.doc('create_user')
@user.expect(user_fields_post)
@user.response(200, 'Success', response_fields)
@user.response(400, 'Input validation exception')
@user.response(409, 'Duplicate user', response_fields)
@user.doc(security='Bearer')
@common.api_token_authorization
def post(self):
""" Create user """
data = api.payload
if not validators.email(data['email']):
return { 'code': 400, 'message': f'Provided email address {data["email"]} is not a valid email address'}, 400
localpart, domain_name = data['email'].lower().rsplit('@', 1)
domain_found = models.Domain.query.get(domain_name)
if not domain_found:
return { 'code': 404, 'message': f'Domain {domain_name} does not exist'}, 404
user_new = models.User(email=data['email'])
if 'raw_password' in data:
user_new.set_password(data['raw_password'])
if 'comment' in data:
user_new.comment = data['comment']
if 'quota_bytes' in data:
user_new.quota_bytes = data['quota_bytes']
if 'global_admin' in data:
user_new.global_admin = data['global_admin']
if 'enabled' in data:
user_new.enabled = data['enabled']
if 'enable_imap' in data:
user_new.enable_imap = data['enable_imap']
if 'enable_pop' in data:
user_new.enable_pop = data['enable_pop']
if 'allow_spoofing' in data:
user_new.allow_spoofing = data['allow_spoofing']
if 'forward_enabled' in data:
user_new.forward_enabled = data['forward_enabled']
if 'forward_destination' in data:
user_new.forward_destination = data['forward_destination']
if 'forward_keep' in data:
user_new.forward_keep = data['forward_keep']
if 'reply_enabled' in data:
user_new.reply_enabled = data['reply_enabled']
if 'reply_subject' in data:
user_new.reply_subject = data['reply_subject']
if 'reply_body' in data:
user_new.reply_body = data['reply_body']
if 'reply_startdate' in data:
year, month, day = data['reply_startdate'].split('-')
date = datetime.datetime(int(year), int(month), int(day))
user_new.reply_startdate = date
if 'reply_enddate' in data:
year, month, day = data['reply_enddate'].split('-')
date = datetime.datetime(int(year), int(month), int(day))
user_new.reply_enddate = date
if 'displayed_name' in data:
user_new.displayed_name = data['displayed_name']
if 'spam_enabled' in data:
user_new.spam_enabled = data['spam_enabled']
if 'spam_mark_as_read' in data:
user_new.spam_mark_as_read = data['spam_mark_as_read']
if 'spam_threshold' in data:
user_new.spam_threshold = data['spam_threshold']
db.session.add(user_new)
db.session.commit()
return {'code': 200,'message': f'User {data["email"]} has been created'}, 200
@user.route('/<string:email>')
class User(Resource):
@user.doc('find_user')
@user.response(400, 'Input validation exception', response_fields)
@user.response(404, 'User not found', response_fields)
@user.doc(security='Bearer')
@common.api_token_authorization
def get(self, email):
""" Find user """
if not validators.email(email):
return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400
email_found = models.User.query.filter_by(email=email).first()
if email_found is None:
return { 'code': 404, 'message': f'User {email} cannot be found'}, 404
return marshal(email_found, user_fields_get), 200
@user.doc('update_user')
@user.expect(user_fields_put)
@user.response(200, 'Success', response_fields)
@user.response(400, 'Input validation exception', response_fields)
@user.response(404, 'User not found', response_fields)
@user.response(409, 'Duplicate user', response_fields)
@user.doc(security='Bearer')
@common.api_token_authorization
def patch(self, email):
""" Update user """
data = api.payload
if not validators.email(email):
return { 'code': 400, 'message': f'Provided email address {data["email"]} is not a valid email address'}, 400
user_found = models.User.query.get(email)
if not user_found:
return {'code': 404, 'message': f'User {email} cannot be found'}, 404
if 'raw_password' in data:
user_found.set_password(data['raw_password'])
if 'comment' in data:
user_found.comment = data['comment']
if 'quota_bytes' in data:
user_found.quota_bytes = data['quota_bytes']
if 'global_admin' in data:
user_found.global_admin = data['global_admin']
if 'enabled' in data:
user_found.enabled = data['enabled']
if 'enable_imap' in data:
user_found.enable_imap = data['enable_imap']
if 'enable_pop' in data:
user_found.enable_pop = data['enable_pop']
if 'allow_spoofing' in data:
user_found.allow_spoofing = data['allow_spoofing']
if 'forward_enabled' in data:
user_found.forward_enabled = data['forward_enabled']
if 'forward_destination' in data:
user_found.forward_destination = data['forward_destination']
if 'forward_keep' in data:
user_found.forward_keep = data['forward_keep']
if 'reply_enabled' in data:
user_found.reply_enabled = data['reply_enabled']
if 'reply_subject' in data:
user_found.reply_subject = data['reply_subject']
if 'reply_body' in data:
user_found.reply_body = data['reply_body']
if 'reply_startdate' in data:
year, month, day = data['reply_startdate'].split('-')
date = datetime.datetime(int(year), int(month), int(day))
user_found.reply_startdate = date
if 'reply_enddate' in data:
year, month, day = data['reply_enddate'].split('-')
date = datetime.datetime(int(year), int(month), int(day))
user_found.reply_enddate = date
if 'displayed_name' in data:
user_found.displayed_name = data['displayed_name']
if 'spam_enabled' in data:
user_found.spam_enabled = data['spam_enabled']
if 'spam_mark_as_read' in data:
user_found.spam_mark_as_read = data['spam_mark_as_read']
if 'spam_threshold' in data:
user_found.spam_threshold = data['spam_threshold']
db.session.add(user_found)
db.session.commit()
return {'code': 200,'message': f'User {email} has been updated'}, 200
@user.doc('delete_user')
@user.response(200, 'Success', response_fields)
@user.response(400, 'Input validation exception', response_fields)
@user.response(404, 'User not found', response_fields)
@user.doc(security='Bearer')
@common.api_token_authorization
def delete(self, email):
""" Delete user """
if not validators.email(email):
return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400
email_found = models.User.query.filter_by(email=email).first()
if email_found is None:
return { 'code': 404, 'message': f'User {email} cannot be found'}, 404
db.session.delete(email_found)
db.session.commit()
return { 'code': 200, 'message': f'User {email} has been deleted'}, 200

@ -1,7 +1,6 @@
import os
from datetime import timedelta
from socrate import system
import ipaddress
DEFAULT_CONFIG = {
@ -11,19 +10,24 @@ DEFAULT_CONFIG = {
'BABEL_DEFAULT_TIMEZONE': 'UTC',
'BOOTSTRAP_SERVE_LOCAL': True,
'RATELIMIT_STORAGE_URL': '',
'QUOTA_STORAGE_URL': '',
'DEBUG': False,
'DEBUG_PROFILER': False,
'DEBUG_TB_INTERCEPT_REDIRECTS': False,
'DEBUG_ASSETS': '',
'DOMAIN_REGISTRATION': False,
'TEMPLATES_AUTO_RELOAD': True,
'MEMORY_SESSIONS': False,
'FETCHMAIL_ENABLED': True,
'MAILU_VERSION': 'unknown',
# Database settings
'DB_FLAVOR': None,
'DB_USER': 'mailu',
'DB_PW': None,
'DB_HOST': 'database',
'DB_NAME': 'mailu',
'SQLITE_DATABASE_FILE':'data/main.db',
'SQLITE_DATABASE_FILE': 'data/main.db',
'SQLALCHEMY_DATABASE_URI': 'sqlite:////data/main.db',
'SQLALCHEMY_DATABASE_URI_ROUNDCUBE': 'sqlite:////data/roundcube.db',
'SQLALCHEMY_TRACK_MODIFICATIONS': False,
# Statistics management
'INSTANCE_ID_PATH': '/data/instance',
@ -37,9 +41,9 @@ DEFAULT_CONFIG = {
'TLS_FLAVOR': 'cert',
'INBOUND_TLS_ENFORCE': False,
'DEFER_ON_TLS_ERROR': True,
'AUTH_RATELIMIT_IP': '60/hour',
'AUTH_RATELIMIT_IP': '5/hour',
'AUTH_RATELIMIT_IP_V4_MASK': 24,
'AUTH_RATELIMIT_IP_V6_MASK': 56,
'AUTH_RATELIMIT_IP_V6_MASK': 48,
'AUTH_RATELIMIT_USER': '100/day',
'AUTH_RATELIMIT_EXEMPTION': '',
'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400,
@ -59,7 +63,7 @@ DEFAULT_CONFIG = {
# Web settings
'SITENAME': 'Mailu',
'WEBSITE': 'https://mailu.io',
'ADMIN' : 'none',
'ADMIN': 'none',
'WEB_ADMIN': '/admin',
'WEB_WEBMAIL': '/webmail',
'WEBMAIL': 'none',
@ -68,27 +72,24 @@ DEFAULT_CONFIG = {
'LOGO_URL': None,
'LOGO_BACKGROUND': None,
# Advanced settings
'API': False,
'WEB_API': '/api',
'API_TOKEN': None,
'LOG_LEVEL': 'WARNING',
'SESSION_KEY_BITS': 128,
'SESSION_TIMEOUT': 3600,
'PERMANENT_SESSION_LIFETIME': 30*24*3600,
'SESSION_COOKIE_SECURE': True,
'SESSION_COOKIE_SECURE': None,
'CREDENTIAL_ROUNDS': 12,
'TLS_PERMISSIVE': True,
'TZ': 'Etc/UTC',
# Host settings
'HOST_IMAP': 'imap',
'HOST_LMTP': 'imap:2525',
'HOST_POP3': 'imap',
'HOST_SMTP': 'smtp',
'HOST_AUTHSMTP': 'smtp',
'HOST_ADMIN': 'admin',
'HOST_WEBMAIL': 'webmail',
'HOST_WEBDAV': 'webdav:5232',
'HOST_REDIS': 'redis',
'HOST_FRONT': 'front',
'DEFAULT_SPAM_THRESHOLD': 80,
'PROXY_AUTH_WHITELIST': '',
'PROXY_AUTH_HEADER': 'X-Auth-Email',
'PROXY_AUTH_CREATE': False,
'PROXY_AUTH_LOGOUT_URL': None,
'SUBNET': '192.168.203.0/24',
'SUBNET6': None
'SUBNET6': None,
}
class ConfigManager:
@ -98,25 +99,12 @@ class ConfigManager:
DB_TEMPLATES = {
'sqlite': 'sqlite:////{SQLITE_DATABASE_FILE}',
'postgresql': 'postgresql://{DB_USER}:{DB_PW}@{DB_HOST}/{DB_NAME}',
'mysql': 'mysql+mysqlconnector://{DB_USER}:{DB_PW}@{DB_HOST}/{DB_NAME}'
'mysql': 'mysql+mysqlconnector://{DB_USER}:{DB_PW}@{DB_HOST}/{DB_NAME}',
}
def __init__(self):
self.config = dict()
def get_host_address(self, name):
# if MYSERVICE_ADDRESS is defined, use this
if f'{name}_ADDRESS' in os.environ:
return os.environ.get(f'{name}_ADDRESS')
# otherwise use the host name and resolve it
return system.resolve_address(self.config[f'HOST_{name}'])
def resolve_hosts(self):
for key in ['IMAP', 'POP3', 'AUTHSMTP', 'SMTP', 'REDIS']:
self.config[f'{key}_ADDRESS'] = self.get_host_address(key)
if self.config['WEBMAIL'] != 'none':
self.config['WEBMAIL_ADDRESS'] = self.get_host_address('WEBMAIL')
def __get_env(self, key, value):
key_file = key + "_FILE"
if key_file in os.environ:
@ -137,33 +125,45 @@ class ConfigManager:
# get current app config
self.config.update(app.config)
# get environment variables
for key in os.environ:
if key.endswith('_ADDRESS'):
self.config[key] = os.environ[key]
self.config.update({
key: self.__coerce_value(self.__get_env(key, value))
for key, value in DEFAULT_CONFIG.items()
})
self.resolve_hosts()
# automatically set the sqlalchemy string
if self.config['DB_FLAVOR']:
template = self.DB_TEMPLATES[self.config['DB_FLAVOR']]
self.config['SQLALCHEMY_DATABASE_URI'] = template.format(**self.config)
self.config['RATELIMIT_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/2'
self.config['QUOTA_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/1'
if not self.config.get('RATELIMIT_STORAGE_URL'):
self.config['RATELIMIT_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/2'
self.config['SESSION_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/3'
self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
self.config['SESSION_COOKIE_HTTPONLY'] = True
if self.config['SESSION_COOKIE_SECURE'] is None:
self.config['SESSION_COOKIE_SECURE'] = self.config['TLS_FLAVOR'] != 'notls'
self.config['SESSION_PERMANENT'] = True
self.config['SESSION_TIMEOUT'] = int(self.config['SESSION_TIMEOUT'])
self.config['SESSION_KEY_BITS'] = int(self.config['SESSION_KEY_BITS'])
self.config['PERMANENT_SESSION_LIFETIME'] = int(self.config['PERMANENT_SESSION_LIFETIME'])
self.config['AUTH_RATELIMIT_IP_V4_MASK'] = int(self.config['AUTH_RATELIMIT_IP_V4_MASK'])
self.config['AUTH_RATELIMIT_IP_V6_MASK'] = int(self.config['AUTH_RATELIMIT_IP_V6_MASK'])
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])
hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')]
self.config['HOSTNAMES'] = ','.join(hostnames)
self.config['HOSTNAME'] = hostnames[0]
self.config['DEFAULT_SPAM_THRESHOLD'] = int(self.config['DEFAULT_SPAM_THRESHOLD'])
self.config['PROXY_AUTH_WHITELIST'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['PROXY_AUTH_WHITELIST'].split(',')) if cidr)
try:
self.config['MAILU_VERSION'] = open('/version', 'r').read()
except FileNotFoundError:
pass
# update the app config
app.config.update(self.config)

@ -2,20 +2,20 @@
They are thus represented as ASCII armored PEM.
"""
from OpenSSL import crypto
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
def gen_key(key_type=crypto.TYPE_RSA, bits=2048):
def gen_key(bits=2048):
""" Generate and return a new RSA key.
"""
key = crypto.PKey()
key.generate_key(key_type, bits)
return crypto.dump_privatekey(crypto.FILETYPE_PEM, key)
k = rsa.generate_private_key(public_exponent=65537, key_size=bits)
return k.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())
def strip_key(pem):
""" Return only the b64 part of the ASCII armored PEM.
"""
key = crypto.load_privatekey(crypto.FILETYPE_PEM, pem)
public_pem = crypto.dump_publickey(crypto.FILETYPE_PEM, key)
priv_key = serialization.load_pem_private_key(pem, password=None)
public_pem = priv_key.public_key().public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)
return public_pem.replace(b"\n", b"").split(b"-----")[2]

@ -1,12 +1,10 @@
from mailu import models, utils
from flask import current_app as app
from socrate import system
import re
import urllib
import ipaddress
import socket
import sqlalchemy.exc
import tenacity
SUPPORTED_AUTH_METHODS = ["none", "plain"]
@ -27,12 +25,14 @@ STATUSES = {
}),
}
WEBMAIL_PORTS = ['10143', '10025']
def check_credentials(user, password, ip, protocol=None, auth_port=None):
if not user or not user.enabled or (protocol == "imap" and not user.enable_imap) or (protocol == "pop3" and not user.enable_pop):
if not user or not user.enabled or (protocol == "imap" and not user.enable_imap and not auth_port in WEBMAIL_PORTS) or (protocol == "pop3" and not user.enable_pop):
return False
is_ok = False
# webmails
if auth_port in ['10143', '10025'] and password.startswith('token-'):
if auth_port in WEBMAIL_PORTS and password.startswith('token-'):
if utils.verify_temp_token(user.get_id(), password):
is_ok = True
# All tokens are 32 characters hex lowercase
@ -127,32 +127,20 @@ def get_status(protocol, status):
status, codes = STATUSES[status]
return status, codes[protocol]
def extract_host_port(host_and_port, default_port):
host, _, port = re.match('^(.*?)(:([0-9]*))?$', host_and_port).groups()
return host, int(port) if port else default_port
def get_server(protocol, authenticated=False):
if protocol == "imap":
hostname, port = extract_host_port(app.config['IMAP_ADDRESS'], 143)
hostname, port = app.config['IMAP_ADDRESS'], 143
elif protocol == "pop3":
hostname, port = extract_host_port(app.config['POP3_ADDRESS'], 110)
hostname, port = app.config['IMAP_ADDRESS'], 110
elif protocol == "smtp":
if authenticated:
hostname, port = extract_host_port(app.config['AUTHSMTP_ADDRESS'], 10025)
hostname, port = app.config['SMTP_ADDRESS'], 10025
else:
hostname, port = extract_host_port(app.config['SMTP_ADDRESS'], 25)
hostname, port = app.config['SMTP_ADDRESS'], 25
try:
# test if hostname is already resolved to an ip adddress
# test if hostname is already resolved to an ip address
ipaddress.ip_address(hostname)
except:
# hostname is not an ip address - so we need to resolve it
hostname = resolve_hostname(hostname)
hostname = system.resolve_hostname(hostname)
return hostname, port
@tenacity.retry(stop=tenacity.stop_after_attempt(100),
wait=tenacity.wait_random(min=2, max=5))
def resolve_hostname(hostname):
""" This function uses system DNS to resolve a hostname.
It is capable of retrying in case the host is not immediately available
"""
return socket.gethostbyname(hostname)

@ -33,8 +33,8 @@ def nginx_authentication():
for key, value in headers.items():
response.headers[key] = str(value)
is_valid_user = False
username = response.headers.get('Auth-User', None)
if response.headers.get("Auth-User-Exists") == "True":
username = response.headers["Auth-User"]
if utils.limiter.should_rate_limit_user(username, client_ip):
# FIXME could be done before handle_authentication()
status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
@ -50,7 +50,7 @@ def nginx_authentication():
elif is_valid_user:
utils.limiter.rate_limit_user(username, client_ip)
elif not is_from_webmail:
utils.limiter.rate_limit_ip(client_ip)
utils.limiter.rate_limit_ip(client_ip, username)
return response
@internal.route("/auth/admin")
@ -109,7 +109,7 @@ def basic_authentication():
utils.limiter.exempt_ip_from_ratelimits(client_ip)
return response
# We failed check_credentials
utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip, user_email)
response = flask.Response(status=401)
response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
return response

@ -5,6 +5,7 @@ from flask import current_app as app
import flask
import socket
import os
import sqlalchemy.exc
@internal.route("/dovecot/passdb/<path:user_email>")
def dovecot_passdb_dict(user_email):
@ -16,15 +17,23 @@ def dovecot_passdb_dict(user_email):
return flask.jsonify({
"password": None,
"nopassword": "Y",
"allow_nets": ",".join(allow_nets)
"allow_real_nets": ",".join(allow_nets)
})
@internal.route("/dovecot/userdb/")
def dovecot_userdb_dict_list():
return flask.jsonify([
user[0] for user in models.User.query.filter(models.User.enabled.is_(True)).with_entities(models.User.email).all()
])
@internal.route("/dovecot/userdb/<path:user_email>")
def dovecot_userdb_dict(user_email):
user = models.User.query.get(user_email) or flask.abort(404)
try:
quota = models.User.query.filter(models.User.email==user_email).with_entities(models.User.quota_bytes).one_or_none() or flask.abort(404)
except sqlalchemy.exc.StatementError as exc:
flask.abort(404)
return flask.jsonify({
"quota_rule": "*:bytes={}".format(user.quota_bytes)
"quota_rule": f"*:bytes={quota[0]}"
})
@ -33,6 +42,7 @@ def dovecot_quota(ns, user_email):
user = models.User.query.get(user_email) or flask.abort(404)
if ns == "storage":
user.quota_bytes_used = flask.request.get_json()
user.dont_change_updated_at()
models.db.session.commit()
return flask.jsonify(None)

@ -12,10 +12,12 @@ def fetch_list():
"id": fetch.id,
"tls": fetch.tls,
"keep": fetch.keep,
"scan": fetch.scan,
"user_email": fetch.user_email,
"protocol": fetch.protocol,
"host": fetch.host,
"port": fetch.port,
"folders": fetch.folders,
"username": fetch.username,
"password": fetch.password
} for fetch in models.Fetch.query.all()
@ -27,6 +29,7 @@ def fetch_done(fetch_id):
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
fetch.last_check = datetime.datetime.now()
fetch.error_message = str(flask.request.get_json())
fetch.dont_change_updated_at()
models.db.session.add(fetch)
models.db.session.commit()
return ""

@ -143,8 +143,9 @@ def postfix_sender_login(sender):
if localpart is None:
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)]
destinations = models.Email.resolve_destination(localpart, domain_name, True) or []
destinations.extend(wildcard_senders)
destinations = set(models.Email.resolve_destination(localpart, domain_name, True) or [])
destinations.update(wildcard_senders)
destinations.update(i[0] for i in models.User.query.filter_by(allow_spoofing=True).with_entities(models.User.email).all())
if destinations:
return flask.jsonify(",".join(idna_encode(destinations)))
return flask.abort(404)
@ -158,21 +159,6 @@ def postfix_sender_rate(sender):
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>")
def postfix_sender_access(sender):
""" Simply reject any sender that pretends to be from a local domain
"""
if '@' in sender:
if sender.startswith('<') and sender.endswith('>'):
sender = sender[1:-1]
try:
localpart, domain_name = models.Email.resolve_domain(sender)
if models.Domain.query.get(domain_name):
return flask.jsonify("REJECT")
except sqlalchemy.exc.StatementError:
pass
return flask.abort(404)
# idna encode domain part of each address in list of addresses
def idna_encode(addresses):
return [

@ -25,3 +25,7 @@ def rspamd_dkim_key(domain_name):
}
)
return flask.jsonify({'data': {'selectors': selectors}})
@internal.route("/rspamd/local_domains", methods=['GET'])
def rspamd_local_domains():
return '\n'.join(domain[0] for domain in models.Domain.query.with_entities(models.Domain.name).all() + models.Alternative.query.with_entities(models.Alternative.name).all())

@ -52,10 +52,13 @@ class LimitWraperFactory(object):
app.logger.warn(f'Authentication attempt from {ip} has been rate-limited.')
return is_rate_limited
def rate_limit_ip(self, ip):
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip')
def rate_limit_ip(self, ip, username=None):
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):
if username and self.storage.get(f'dedup-{client_network}-{username}') > 0:
return
self.storage.incr(f'dedup-{client_network}-{username}', limits.parse(app.config['AUTH_RATELIMIT_IP']).GRANULARITY.seconds, True)
limiter.hit(client_network)
def should_rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None):

@ -304,6 +304,7 @@ def config_update(verbose=False, delete_objects=False):
if verbose:
print(f'Deleting domain: {domain.name}')
db.session.delete(domain)
db.session.commit()
@ -351,7 +352,7 @@ def config_import(verbose=0, secrets=False, debug=False, quiet=False, color=Fals
raise click.ClickException(msg) from exc
raise
# don't commit when running dry
# do not commit when running dry
if dry_run:
log.changes('Dry run. Not committing changes.')
db.session.rollback()
@ -385,6 +386,7 @@ def config_export(full=False, secrets=False, color=False, dns=False, output=None
'dns': dns,
}
old_umask = os.umask(0o077)
try:
schema = MailuSchema(only=only, context=context)
if as_json:
@ -396,17 +398,22 @@ def config_export(full=False, secrets=False, color=False, dns=False, output=None
if msg := log.format_exception(exc):
raise click.ClickException(msg) from exc
raise
finally:
os.umask(old_umask)
@mailu.command()
@click.argument('email')
@click.option('-r', '--really', is_flag=True)
@with_appcontext
def user_delete(email):
"""delete user"""
user = models.User.query.get(email)
if user:
db.session.delete(user)
db.session.commit()
def user_delete(email, really=False):
"""disable or delete user"""
if user := models.User.query.get(email):
if really:
db.session.delete(user)
else:
user.enabled = False
db.session.commit()
@mailu.command()
@ -414,10 +421,9 @@ def user_delete(email):
@with_appcontext
def alias_delete(email):
"""delete alias"""
alias = models.Alias.query.get(email)
if alias:
if alias := models.Alias.query.get(email):
db.session.delete(alias)
db.session.commit()
db.session.commit()
@mailu.command()

@ -2,7 +2,6 @@
"""
import os
import smtplib
import json
from datetime import date
@ -25,6 +24,7 @@ from flask import current_app as app
from sqlalchemy.ext import declarative
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.inspection import inspect
from sqlalchemy.orm.attributes import flag_modified
from werkzeug.utils import cached_property
from mailu import dkim, utils
@ -75,7 +75,7 @@ class CommaSeparatedList(db.TypeDecorator):
""" Stores a list as a comma-separated string, compatible with Postfix.
"""
impl = db.String
impl = db.String(255)
cache_ok = True
python_type = list
@ -96,7 +96,7 @@ class JSONEncoded(db.TypeDecorator):
""" Represents an immutable structure as a json-encoded string.
"""
impl = db.String
impl = db.String(255)
cache_ok = True
python_type = str
@ -154,6 +154,10 @@ class Base(db.Model):
self.__hashed = id(self) if primary is None else hash(primary)
return self.__hashed
def dont_change_updated_at(self):
""" Mark updated_at as modified, but keep the old date when updating the model"""
flag_modified(self, 'updated_at')
# Many-to-many association table for domain managers
managers = db.Table('manager', Base.metadata,
@ -415,14 +419,18 @@ class Email(object):
def sendmail(self, subject, body):
""" send an email to the address """
f_addr = f'{app.config["POSTMASTER"]}@{idna.encode(app.config["DOMAIN"]).decode("ascii")}'
with smtplib.SMTP(app.config['HOST_AUTHSMTP'], port=10025) as smtp:
to_address = f'{self.localpart}@{idna.encode(self.domain_name).decode("ascii")}'
msg = text.MIMEText(body)
msg['Subject'] = subject
msg['From'] = f_addr
msg['To'] = to_address
smtp.sendmail(f_addr, [to_address], msg.as_string())
try:
f_addr = f'{app.config["POSTMASTER"]}@{idna.encode(app.config["DOMAIN"]).decode("ascii")}'
with smtplib.LMTP(host=app.config['IMAP_ADDRESS'], port=2525) as lmtp:
to_address = f'{self.localpart}@{idna.encode(self.domain_name).decode("ascii")}'
msg = text.MIMEText(body)
msg['Subject'] = subject
msg['From'] = f_addr
msg['To'] = to_address
lmtp.sendmail(f_addr, [to_address], msg.as_string())
return True
except smtplib.SMTPException:
return False
@classmethod
def resolve_domain(cls, email):
@ -496,6 +504,7 @@ class User(Base, Email):
# Features
enable_imap = db.Column(db.Boolean, nullable=False, default=True)
enable_pop = db.Column(db.Boolean, nullable=False, default=True)
allow_spoofing = db.Column(db.Boolean, nullable=False, default=False)
# Filters
forward_enabled = db.Column(db.Boolean, nullable=False, default=False)
@ -513,7 +522,7 @@ class User(Base, Email):
displayed_name = db.Column(db.String(160), nullable=False, default='')
spam_enabled = db.Column(db.Boolean, nullable=False, default=True)
spam_mark_as_read = db.Column(db.Boolean, nullable=False, default=True)
spam_threshold = db.Column(db.Integer, nullable=False, default=80)
spam_threshold = db.Column(db.Integer, nullable=False, default=lambda:int(app.config.get("DEFAULT_SPAM_THRESHOLD", 80)))
# Flask-login attributes
is_authenticated = True
@ -541,8 +550,8 @@ class User(Base, Email):
now = date.today()
return (
self.reply_enabled and
self.reply_startdate < now and
self.reply_enddate > now
self.reply_startdate <= now and
self.reply_enddate >= now
)
@property
@ -766,6 +775,8 @@ class Fetch(Base):
username = db.Column(db.String(255), nullable=False)
password = db.Column(db.String(255), nullable=False)
keep = db.Column(db.Boolean, nullable=False, default=False)
scan = db.Column(db.Boolean, nullable=False, default=False)
folders = db.Column(CommaSeparatedList, nullable=True, default=list)
last_check = db.Column(db.DateTime, nullable=True)
error = db.Column(db.String(1023), nullable=True)

@ -5,6 +5,7 @@ from copy import deepcopy
from collections import Counter
from datetime import timezone
import inspect
import json
import logging
import yaml
@ -19,7 +20,7 @@ from marshmallow_sqlalchemy.fields import RelatedList
from flask_marshmallow import Marshmallow
from OpenSSL import crypto
from cryptography.hazmat.primitives import serialization
from pygments import highlight
from pygments.token import Token
@ -609,8 +610,8 @@ class DkimKeyField(fields.String):
# check key validity
try:
crypto.load_privatekey(crypto.FILETYPE_PEM, value)
except crypto.Error as exc:
serialization.load_pem_private_key(bytes(value, "ascii"), password=None)
except (UnicodeEncodeError, ValueError) as exc:
raise ValidationError(f'invalid dkim key {bad_key!r}') from exc
else:
return value
@ -669,20 +670,15 @@ class Storage:
context = {}
def _bind(self, key, bind):
if bind is True:
return (self.__class__, key)
if isinstance(bind, str):
return (get_schema(self.recall(bind).__class__), key)
return (bind, key)
def store(self, key, value, bind=None):
def store(self, key, value):
""" store value under key """
self.context.setdefault('_track', {})[self._bind(key, bind)]= value
key = f'{self.__class__.__name__}.{key}'
self.context.setdefault('_track', {})[key] = value
def recall(self, key, bind=None):
def recall(self, key):
""" recall value from key """
return self.context['_track'][self._bind(key, bind)]
key = f'{self.__class__.__name__}.{key}'
return self.context['_track'][key]
class BaseOpts(SQLAlchemyAutoSchemaOpts):
""" Option class with sqla session
@ -790,10 +786,16 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
for key, value in data.items()
}
def _call_and_store(self, *args, **kwargs):
""" track current parent field for pruning """
self.store('field', kwargs['field_name'], True)
return super()._call_and_store(*args, **kwargs)
def get_parent(self):
""" helper to determine parent of current object """
for x in inspect.stack():
loc = x[0].f_locals
if 'ret_d' in loc:
if isinstance(loc['self'], MailuSchema):
return self.context.get('config'), loc['attr_name']
else:
return loc['self'].get_instance(loc['ret_d']), loc['attr_name']
return None, None
# this is only needed to work around the declared attr "email" primary key in model
def get_instance(self, data):
@ -803,9 +805,13 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
if keys := getattr(self.Meta, 'primary_keys', None):
filters = {key: data.get(key) for key in keys}
if None not in filters.values():
res= self.session.query(self.opts.model).filter_by(**filters).first()
return res
res= super().get_instance(data)
try:
res = self.session.query(self.opts.model).filter_by(**filters).first()
except sqlalchemy.exc.StatementError as exc:
raise ValidationError(f'Invalid {keys[0]}: {data.get(keys[0])!r}', data.get(keys[0])) from exc
else:
return res
res = super().get_instance(data)
return res
@pre_load(pass_many=True)
@ -829,6 +835,10 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
want_prune = []
def patch(count, data):
# we only process objects here
if type(data) is not dict:
raise ValidationError(f'Invalid item. {self.Meta.model.__tablename__.title()} needs to be an object.', f'{data!r}')
# don't allow __delete__ coming from input
if '__delete__' in data:
raise ValidationError('Unknown field.', f'{count}.__delete__')
@ -882,10 +892,10 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
]
# remember if prune was requested for _prune_items@post_load
self.store('prune', bool(want_prune), True)
self.store('prune', bool(want_prune))
# remember original items to stabilize password-changes in _add_instance@post_load
self.store('original', items, True)
self.store('original', items)
return items
@ -909,13 +919,15 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
# stabilize import of auto-increment primary keys (not required),
# by matching import data to existing items and setting primary key
if not self._primary in data:
for item in getattr(self.recall('parent'), self.recall('field', 'parent')):
existing = self.dump(item, many=False)
this = existing.pop(self._primary)
if data == existing:
instance = item
data[self._primary] = this
break
parent, field = self.get_parent()
if parent is not None:
for item in getattr(parent, field):
existing = self.dump(item, many=False)
this = existing.pop(self._primary)
if data == existing:
self.instance = item
data[self._primary] = this
break
# try to load instance
instance = self.instance or self.get_instance(data)
@ -931,9 +943,6 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
else:
if self.context.get('update'):
# remember instance as parent for pruning siblings
if not self.Meta.sibling:
self.store('parent', instance)
# delete instance from session when marked
if '__delete__' in data:
self.opts.sqla_session.delete(instance)
@ -968,7 +977,7 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
) from exc
# sort list of new values
data[key] = sorted(new_value)
# log backref modification not catched by modify hook
# log backref modification not caught by modify hook
if isinstance(self.fields[key], RelatedList):
if callback := self.context.get('callback'):
before = {str(v) for v in getattr(instance, key)}
@ -997,7 +1006,7 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
return items
# get prune flag from _patch_many@pre_load
want_prune = self.recall('prune', True)
want_prune = self.recall('prune')
# prune: determine if existing items in db need to be added or marked for deletion
add_items = False
@ -1014,14 +1023,17 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
del_items = True
if add_items or del_items:
existing = {item[self._primary] for item in items if self._primary in item}
for item in getattr(self.recall('parent'), self.recall('field', 'parent')):
key = getattr(item, self._primary)
if key not in existing:
if add_items:
items.append({self._primary: key})
else:
items.append({self._primary: key, '__delete__': '?'})
parent, field = self.get_parent()
if parent is not None:
existing = {item[self._primary] for item in items if self._primary in item}
for item in getattr(parent, field):
key = getattr(item, self._primary)
if key not in existing:
if add_items:
items.append({self._primary: key})
else:
if self.context.get('update'):
self.opts.sqla_session.delete(self.instance or self.get_instance({self._primary: key}))
return items
@ -1042,7 +1054,7 @@ class BaseSchema(ma.SQLAlchemyAutoSchema, Storage):
# did we hash a new plaintext password?
original = None
pkey = getattr(item, self._primary)
for data in self.recall('original', True):
for data in self.recall('original'):
if 'hash_password' in data and data.get(self._primary) == pkey:
original = data['password']
break
@ -1238,12 +1250,6 @@ class MailuSchema(Schema, Storage):
if field in fieldlist:
fieldlist[field] = fieldlist.pop(field)
def _call_and_store(self, *args, **kwargs):
""" track current parent and field for pruning """
self.store('field', kwargs['field_name'], True)
self.store('parent', self.context.get('config'))
return super()._call_and_store(*args, **kwargs)
@pre_load
def _clear_config(self, data, many, **kwargs): # pylint: disable=unused-argument
""" create config object in context if missing

@ -5,7 +5,8 @@ import flask_wtf
class LoginForm(flask_wtf.FlaskForm):
class Meta:
csrf = False
email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()])
email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()], render_kw={'autofocus': True})
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pwned = fields.HiddenField(label='', default=-1)
submitWebmail = fields.SubmitField(_('Sign in'))
submitAdmin = fields.SubmitField(_('Sign in'))

@ -1,7 +1,7 @@
{%- import "macros.html" as macros %}
{%- import "bootstrap/utils.html" as utils %}
<!doctype html>
<html lang="{{ session['language'] }}" data-static="/static/">
<html lang="{{ get_locale() }}" data-static="/static/">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -34,8 +34,8 @@
<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>
<i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">{% trans %}change language{% endtrans %}</span>
<span class="badge badge-primary navbar-badge">{{ get_locale() }}</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>

@ -3,6 +3,7 @@
{%- block content %}
{%- call macros.card() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.email) }}
{{ macros.form_field(form.pw) }}
{{ macros.form_fields(fields, label=False, class="btn btn-default") }}

@ -36,6 +36,12 @@
</a>
</li>
{%- endif %}
<li class="nav-item" role="none">
<a href="{{ url_for('sso.login') }}" class="nav-link" role="menuitem">
<i class="nav-icon fas fa-sign-in-alt"></i>
<p>{% trans %}Sign in{% endtrans %}</p>
</a>
</li>
{#-
User self-registration is only available when
- Admin is available

@ -6,26 +6,40 @@ from mailu.ui import access
from flask import current_app as app
import flask
import flask_login
import secrets
import ipaddress
from urllib.parse import urlparse, urljoin
from werkzeug.urls import url_unquote
@sso.route('/login', methods=['GET', 'POST'])
def login():
if flask.request.headers.get(app.config['PROXY_AUTH_HEADER']) and not 'noproxyauth' in flask.request.url:
return _proxy()
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["WEBMAIL"]).upper() != "NONE":
fields.append(form.submitWebmail)
if str(app.config["ADMIN"]).upper() != "FALSE":
if 'url' in flask.request.args and not 'homepage' in flask.request.url:
fields.append(form.submitAdmin)
else:
form.submitAdmin.label.text = form.submitAdmin.label.text + ' Admin'
form.submitWebmail.label.text = form.submitWebmail.label.text + ' Webmail'
if str(app.config["WEBMAIL"]).upper() != "NONE":
fields.append(form.submitWebmail)
if str(app.config["ADMIN"]).upper() != "FALSE":
fields.append(form.submitAdmin)
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']
if destination := _has_usable_redirect():
pass
else:
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):
@ -40,10 +54,12 @@ def login():
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}.')
flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip} pwned={form.pwned.data}.')
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
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)
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, username)
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)
@ -53,5 +69,65 @@ def login():
def logout():
flask_login.logout_user()
flask.session.destroy()
return flask.redirect(flask.url_for('.login'))
response = flask.redirect(app.config['PROXY_AUTH_LOGOUT_URL'] or flask.url_for('.login'))
for cookie in ['roundcube_sessauth', 'roundcube_sessid', 'smsession']:
response.set_cookie(cookie, 'empty', expires=0)
return response
"""
Redirect to the url passed in parameter if any; Ensure that this is not an open-redirect too...
https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
"""
def _has_usable_redirect(is_proxied=False):
if 'homepage' in flask.request.url and not is_proxied:
return None
if url := flask.request.args.get('url'):
url = url_unquote(url)
target = urlparse(urljoin(flask.request.url, url))
if target.netloc == urlparse(flask.request.url).netloc:
return target.geturl()
return None
"""
https://mailu.io/master/configuration.html#header-authentication-using-an-external-proxy
"""
def _proxy():
proxy_ip = flask.request.headers.get('X-Forwarded-By', flask.request.remote_addr)
ip = ipaddress.ip_address(proxy_ip)
if not any(ip in cidr for cidr in app.config['PROXY_AUTH_WHITELIST']):
return flask.abort(500, '%s is not on PROXY_AUTH_WHITELIST' % proxy_ip)
email = flask.request.headers.get(app.config['PROXY_AUTH_HEADER'])
if not email:
return flask.abort(500, 'No %s header' % app.config['PROXY_AUTH_HEADER'])
url = _has_usable_redirect(True) or app.config['WEB_ADMIN']
user = models.User.get(email)
if user:
flask.session.regenerate()
flask_login.login_user(user)
return flask.redirect(url)
if not app.config['PROXY_AUTH_CREATE']:
return flask.abort(500, 'You don\'t exist. Go away! (%s)' % email)
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
try:
localpart, desireddomain = email.rsplit('@')
except Exception as e:
flask.current_app.logger.error('Error creating a new user via proxy for %s from %s: %s' % (email, client_ip, str(e)), e)
return flask.abort(500, 'You don\'t exist. Go away! (%s)' % email)
domain = models.Domain.query.get(desireddomain) or flask.abort(500, 'You don\'t exist. Go away! (domain=%s)' % desireddomain)
if not domain.max_users == -1 and len(domain.users) >= domain.max_users:
flask.current_app.logger.warning('Too many users for domain %s' % domain)
return flask.abort(500, 'Too many users in (domain=%s)' % domain)
user = models.User(localpart=localpart, domain=domain)
user.set_password(secrets.token_urlsafe())
models.db.session.add(user)
models.db.session.commit()
flask.session.regenerate()
flask_login.login_user(user)
user.send_welcome()
flask.current_app.logger.info(f'Login succeeded by proxy created user: {user} from {client_ip} through {flask.request.remote_addr}.')
return flask.redirect(url)

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

@ -660,7 +660,7 @@ msgid "New relay domain"
msgstr "Nou domini llegat (relayed)"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Editeu domini llegat (relayed)"
#: mailu/ui/templates/relay/list.html:4

@ -0,0 +1,733 @@
# Czech translations for Mailu.io.
# Copyright (C) 2023 S474N
# This file is distributed under the same license as the PROJECT project.
# S474N <translate@s474n.com>, 2023.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: translate@s474n.com\n"
"POT-Creation-Date: 2022-05-22 18:47+0200\n"
"PO-Revision-Date: 2023-02-21 16:14+0100\n"
"Last-Translator: S474N <translate@s474n.com>\n"
"Language-Team: Czech\n"
"Language: cs_CZ\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);\n"
"Generated-By: Babel 2.3.4\n"
"X-Generator: Poedit 3.2.2\n"
#: mailu/sso/forms.py:8 mailu/ui/forms.py:79
msgid "E-mail"
msgstr "E-mail"
#: mailu/sso/forms.py:9 mailu/ui/forms.py:80 mailu/ui/forms.py:93
#: mailu/ui/forms.py:112 mailu/ui/forms.py:166
#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:57
msgid "Password"
msgstr "Heslo"
#: mailu/sso/forms.py:10 mailu/sso/forms.py:11 mailu/sso/templates/login.html:4
#: mailu/ui/templates/sidebar.html:142
msgid "Sign in"
msgstr "Přihlásit se"
#: mailu/sso/templates/base_sso.html:8 mailu/ui/templates/base.html:8
msgid "Admin page for"
msgstr "Admin stránka pro"
#: mailu/sso/templates/base_sso.html:19 mailu/ui/templates/base.html:19
msgid "toggle sidebar"
msgstr "přepnout postranní panel"
#: mailu/sso/templates/base_sso.html:37 mailu/ui/templates/base.html:37
msgid "change language"
msgstr "změnit jazyk"
#: mailu/sso/templates/sidebar_sso.html:4 mailu/ui/templates/sidebar.html:94
msgid "Go to"
msgstr "Jít"
#: mailu/sso/templates/sidebar_sso.html:9 mailu/ui/templates/client.html:4
#: mailu/ui/templates/sidebar.html:50 mailu/ui/templates/sidebar.html:107
msgid "Client setup"
msgstr "Nastavení klienta"
#: mailu/sso/templates/sidebar_sso.html:16 mailu/ui/templates/sidebar.html:114
msgid "Website"
msgstr "Webová stránka"
#: mailu/sso/templates/sidebar_sso.html:22 mailu/ui/templates/sidebar.html:120
msgid "Help"
msgstr "Pomoc"
#: mailu/sso/templates/sidebar_sso.html:35
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:127
msgid "Register a domain"
msgstr "Registrovat doménu"
#: mailu/sso/templates/sidebar_sso.html:49 mailu/ui/forms.py:95
#: mailu/ui/templates/sidebar.html:149 mailu/ui/templates/user/signup.html:4
#: mailu/ui/templates/user/signup_domain.html:4
msgid "Sign up"
msgstr "Registrovat se"
#: mailu/ui/forms.py:33 mailu/ui/forms.py:36
msgid "Invalid email address."
msgstr "Špatná mailová adresa."
#: mailu/ui/forms.py:45
msgid "Confirm"
msgstr "Potvrdit"
#: mailu/ui/forms.py:48 mailu/ui/forms.py:58
#: mailu/ui/templates/domain/details.html:26
#: mailu/ui/templates/domain/list.html:19 mailu/ui/templates/relay/list.html:18
msgid "Domain name"
msgstr "Název domény"
#: mailu/ui/forms.py:49
msgid "Maximum user count"
msgstr "Maximální počet uživatelů"
#: mailu/ui/forms.py:50
msgid "Maximum alias count"
msgstr "Maximální počet aliasů"
#: mailu/ui/forms.py:51
msgid "Maximum user quota"
msgstr "Maximální uživatelská kvóta"
#: mailu/ui/forms.py:52
msgid "Enable sign-up"
msgstr "Povolit registraci"
#: mailu/ui/forms.py:53 mailu/ui/forms.py:74 mailu/ui/forms.py:86
#: mailu/ui/forms.py:132 mailu/ui/forms.py:144
#: mailu/ui/templates/alias/list.html:22 mailu/ui/templates/domain/list.html:22
#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:20
#: mailu/ui/templates/user/list.html:24
msgid "Comment"
msgstr "Komentář"
#: mailu/ui/forms.py:54 mailu/ui/forms.py:68 mailu/ui/forms.py:75
#: mailu/ui/forms.py:88 mailu/ui/forms.py:136 mailu/ui/forms.py:145
msgid "Save"
msgstr "Uložit"
#: mailu/ui/forms.py:59
msgid "Initial admin"
msgstr "Hlavní admin"
#: mailu/ui/forms.py:60
msgid "Admin password"
msgstr "Heslo admina"
#: mailu/ui/forms.py:61 mailu/ui/forms.py:81 mailu/ui/forms.py:94
msgid "Confirm password"
msgstr "Potvrdit heslo"
#: mailu/ui/forms.py:63
msgid "Create"
msgstr "Vytvořit"
#: mailu/ui/forms.py:67
msgid "Alternative name"
msgstr "Alternativní jméno"
#: mailu/ui/forms.py:72
msgid "Relayed domain name"
msgstr "Seznam předávaných domén"
#: mailu/ui/forms.py:73 mailu/ui/templates/relay/list.html:19
msgid "Remote host"
msgstr "Vzdálený hostitel"
#: mailu/ui/forms.py:82 mailu/ui/templates/user/list.html:23
#: mailu/ui/templates/user/signup_domain.html:16
msgid "Quota"
msgstr "Kvóta"
#: mailu/ui/forms.py:83
msgid "Allow IMAP access"
msgstr "Povolit přístup IMAP"
#: mailu/ui/forms.py:84
msgid "Allow POP3 access"
msgstr "Povolit přístup POP3"
#: mailu/ui/forms.py:85 mailu/ui/forms.py:101
#: mailu/ui/templates/user/settings.html:15
msgid "Displayed name"
msgstr "Zobrazené jméno"
#: mailu/ui/forms.py:87
msgid "Enabled"
msgstr "Povoleno"
#: mailu/ui/forms.py:92
msgid "Email address"
msgstr "Emailová adresa"
#: mailu/ui/forms.py:102
msgid "Enable spam filter"
msgstr "Povolit filtr spamu"
#: mailu/ui/forms.py:103
msgid "Enable marking spam mails as read"
msgstr "Povolit označování spamových e-mailů jako přečtených"
#: mailu/ui/forms.py:104
msgid "Spam filter tolerance"
msgstr "Tolerance spamového filtru"
#: mailu/ui/forms.py:105
msgid "Enable forwarding"
msgstr "Povolit přeposílání"
#: mailu/ui/forms.py:106
msgid "Keep a copy of the emails"
msgstr "Zachovat kopii e-mailů"
#: mailu/ui/forms.py:107 mailu/ui/forms.py:143
#: mailu/ui/templates/alias/list.html:21
msgid "Destination"
msgstr "Cíl"
#: mailu/ui/forms.py:108
msgid "Save settings"
msgstr "Uložit nastavení"
#: mailu/ui/forms.py:113
msgid "Password check"
msgstr "Kontrola hesla"
#: mailu/ui/forms.py:114 mailu/ui/templates/sidebar.html:25
msgid "Update password"
msgstr "Aktualizovat heslo"
#: mailu/ui/forms.py:118
msgid "Enable automatic reply"
msgstr "Povolit automatickou odpověď"
#: mailu/ui/forms.py:119
msgid "Reply subject"
msgstr "Předmět odpovědi"
#: mailu/ui/forms.py:120
msgid "Reply body"
msgstr "Tělo odpovědi"
#: mailu/ui/forms.py:122
msgid "Start of vacation"
msgstr "Začátek dovolené"
#: mailu/ui/forms.py:123
msgid "End of vacation"
msgstr "Konec dovolené"
#: mailu/ui/forms.py:124
msgid "Update"
msgstr "Aktualizovat"
#: mailu/ui/forms.py:129
msgid "Your token (write it down, as it will never be displayed again)"
msgstr "Váš token (zapište si ho, protože se již nikdy nezobrazí)"
#: mailu/ui/forms.py:134 mailu/ui/templates/token/list.html:21
msgid "Authorized IP"
msgstr "Autorizovaná IP"
#: mailu/ui/forms.py:140
msgid "Alias"
msgstr "Alias"
#: mailu/ui/forms.py:142
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
msgstr "Použít syntaxi jako SQL (např. pro doménové koše)"
#: mailu/ui/forms.py:149
msgid "Admin email"
msgstr "Email admina"
#: mailu/ui/forms.py:150 mailu/ui/forms.py:155 mailu/ui/forms.py:168
msgid "Submit"
msgstr "Poslat"
#: mailu/ui/forms.py:154
msgid "Manager email"
msgstr "E-mail manažera"
#: mailu/ui/forms.py:159
msgid "Protocol"
msgstr "Protokol"
#: mailu/ui/forms.py:162
msgid "Hostname or IP"
msgstr "Hostitel nebo IP"
#: mailu/ui/forms.py:163 mailu/ui/templates/client.html:20
#: mailu/ui/templates/client.html:45
msgid "TCP port"
msgstr "TCP port"
#: mailu/ui/forms.py:164
msgid "Enable TLS"
msgstr "Povolit TLS"
#: mailu/ui/forms.py:165 mailu/ui/templates/client.html:28
#: mailu/ui/templates/client.html:53 mailu/ui/templates/fetch/list.html:21
msgid "Username"
msgstr "Uživatelské jméno"
#: mailu/ui/forms.py:167
msgid "Keep emails on the server"
msgstr "Zachovat e-maily na serveru"
#: mailu/ui/forms.py:172
msgid "Announcement subject"
msgstr "Předmět oznámení"
#: mailu/ui/forms.py:174
msgid "Announcement body"
msgstr "Tělo oznámení"
#: mailu/ui/forms.py:176
msgid "Send"
msgstr "Poslat"
#: mailu/ui/templates/announcement.html:4
msgid "Public announcement"
msgstr "Veřejné oznámení"
#: mailu/ui/templates/antispam.html:4 mailu/ui/templates/sidebar.html:80
#: mailu/ui/templates/user/settings.html:19
msgid "Antispam"
msgstr "Antispam"
#: mailu/ui/templates/antispam.html:8
msgid "RSPAMD status page"
msgstr "Stavová stránka RSPAMD"
#: mailu/ui/templates/client.html:8
msgid "configure your email client"
msgstr "nakonfigurovat e-mailového klienta"
#: mailu/ui/templates/client.html:13
msgid "Incoming mail"
msgstr "Příchozí mail"
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:41
msgid "Mail protocol"
msgstr "Poštovní protokol"
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:49
msgid "Server name"
msgstr "Název serveru"
#: mailu/ui/templates/client.html:38
msgid "Outgoing mail"
msgstr "Odchozí pošta"
#: mailu/ui/templates/confirm.html:4
msgid "Confirm action"
msgstr "Potvrdit akci"
#: mailu/ui/templates/confirm.html:13
#, python-format
msgid "You are about to %(action)s. Please confirm your action."
msgstr "Chystáte se %(action)s. Potvrďte prosím vaši akci."
#: mailu/ui/templates/docker-error.html:4
msgid "Docker error"
msgstr "Chyba Dockeru"
#: mailu/ui/templates/docker-error.html:12
msgid "An error occurred while talking to the Docker server."
msgstr "Při komunikaci se serverem Docker došlo k chybě."
#: mailu/ui/templates/macros.html:129
msgid "copy to clipboard"
msgstr "zkopírovat do schránky"
#: mailu/ui/templates/sidebar.html:15
msgid "My account"
msgstr "Můj účet"
#: mailu/ui/templates/sidebar.html:19 mailu/ui/templates/user/list.html:37
msgid "Settings"
msgstr "Nastavení"
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/user/list.html:38
msgid "Auto-reply"
msgstr "Automatická odpověď"
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:37
#: mailu/ui/templates/user/list.html:39
msgid "Fetched accounts"
msgstr "Fetched účty"
#: mailu/ui/templates/sidebar.html:43 mailu/ui/templates/token/list.html:4
msgid "Authentication tokens"
msgstr "Autentizační tokeny"
#: mailu/ui/templates/sidebar.html:56
msgid "Administration"
msgstr "Administrace"
#: mailu/ui/templates/sidebar.html:62
msgid "Announcement"
msgstr "Oznámení"
#: mailu/ui/templates/sidebar.html:68
msgid "Administrators"
msgstr "Administrátoři"
#: mailu/ui/templates/sidebar.html:74
msgid "Relayed domains"
msgstr "Relayované domény"
#: mailu/ui/templates/sidebar.html:88
msgid "Mail domains"
msgstr "Poštovní domény"
#: mailu/ui/templates/sidebar.html:99
msgid "Webmail"
msgstr "Webmail"
#: mailu/ui/templates/sidebar.html:135
msgid "Sign out"
msgstr "Odhlásit se"
#: mailu/ui/templates/working.html:4
msgid "We are still working on this feature!"
msgstr "Na této funkci stále pracujeme!"
#: mailu/ui/templates/admin/create.html:4
msgid "Add a global administrator"
msgstr "Přidat globálního administrátora"
#: mailu/ui/templates/admin/list.html:4
msgid "Global administrators"
msgstr "Globální administrátor"
#: mailu/ui/templates/admin/list.html:9
msgid "Add administrator"
msgstr "Přidat administrátora"
#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19
#: mailu/ui/templates/alternative/list.html:19
#: mailu/ui/templates/domain/list.html:17 mailu/ui/templates/fetch/list.html:19
#: mailu/ui/templates/manager/list.html:19
#: mailu/ui/templates/relay/list.html:17 mailu/ui/templates/token/list.html:19
#: mailu/ui/templates/user/list.html:19
msgid "Actions"
msgstr "Akce"
#: mailu/ui/templates/admin/list.html:18 mailu/ui/templates/alias/list.html:20
#: mailu/ui/templates/manager/list.html:20 mailu/ui/templates/user/list.html:21
msgid "Email"
msgstr "Email"
#: mailu/ui/templates/admin/list.html:25 mailu/ui/templates/alias/list.html:32
#: mailu/ui/templates/alternative/list.html:29
#: mailu/ui/templates/domain/list.html:34 mailu/ui/templates/fetch/list.html:34
#: mailu/ui/templates/manager/list.html:27
#: mailu/ui/templates/relay/list.html:30 mailu/ui/templates/token/list.html:30
#: mailu/ui/templates/user/list.html:34
msgid "Delete"
msgstr "Vymazat"
#: mailu/ui/templates/alias/create.html:4
msgid "Create alias"
msgstr "Vytvořit alias"
#: mailu/ui/templates/alias/edit.html:4
msgid "Edit alias"
msgstr "Upravit alias"
#: mailu/ui/templates/alias/list.html:4
msgid "Alias list"
msgstr "Seznam aliasů"
#: mailu/ui/templates/alias/list.html:12
msgid "Add alias"
msgstr "Přidat alias"
#: mailu/ui/templates/alias/list.html:23
#: mailu/ui/templates/alternative/list.html:21
#: mailu/ui/templates/domain/list.html:23 mailu/ui/templates/fetch/list.html:25
#: mailu/ui/templates/relay/list.html:21 mailu/ui/templates/token/list.html:22
#: mailu/ui/templates/user/list.html:25
msgid "Created"
msgstr "Vytvořeno"
#: mailu/ui/templates/alias/list.html:24
#: mailu/ui/templates/alternative/list.html:22
#: mailu/ui/templates/domain/list.html:24 mailu/ui/templates/fetch/list.html:26
#: mailu/ui/templates/relay/list.html:22 mailu/ui/templates/token/list.html:23
#: mailu/ui/templates/user/list.html:26
msgid "Last edit"
msgstr "Poslední úprava"
#: mailu/ui/templates/alias/list.html:31 mailu/ui/templates/domain/list.html:33
#: mailu/ui/templates/fetch/list.html:33 mailu/ui/templates/relay/list.html:29
#: mailu/ui/templates/user/list.html:33
msgid "Edit"
msgstr "Upravit"
#: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain"
msgstr "Vytvořit alternativní doménu"
#: mailu/ui/templates/alternative/list.html:4
msgid "Alternative domain list"
msgstr "Seznam alternativních domén"
#: mailu/ui/templates/alternative/list.html:12
msgid "Add alternative"
msgstr "Přidat alternativu"
#: mailu/ui/templates/alternative/list.html:20
msgid "Name"
msgstr "Jméno"
#: mailu/ui/templates/domain/create.html:4
#: mailu/ui/templates/domain/list.html:9
msgid "New domain"
msgstr "Nová doména"
#: mailu/ui/templates/domain/details.html:4
msgid "Domain details"
msgstr "Podrobnosti o doméně"
#: mailu/ui/templates/domain/details.html:15
msgid "Regenerate keys"
msgstr "Obnovit klíče"
#: mailu/ui/templates/domain/details.html:17
msgid "Generate keys"
msgstr "Generovat klíče"
#: mailu/ui/templates/domain/details.html:30
msgid "DNS MX entry"
msgstr "Záznam DNS MX"
#: mailu/ui/templates/domain/details.html:34
msgid "DNS SPF entries"
msgstr "Záznamy DNS SPF"
#: mailu/ui/templates/domain/details.html:40
msgid "DKIM public key"
msgstr "Veřejný klíč DKIM"
#: mailu/ui/templates/domain/details.html:44
msgid "DNS DKIM entry"
msgstr "Záznam DNS DKIM"
#: mailu/ui/templates/domain/details.html:48
msgid "DNS DMARC entry"
msgstr "Záznam DNS DMARC"
#: mailu/ui/templates/domain/details.html:58
msgid "DNS TLSA entry"
msgstr "Záznam DNS TLSA"
#: mailu/ui/templates/domain/details.html:63
msgid "DNS client auto-configuration entries"
msgstr "Položky automatické konfigurace klienta DNS"
#: mailu/ui/templates/domain/edit.html:4
msgid "Edit domain"
msgstr "Upravit doménu"
#: mailu/ui/templates/domain/list.html:4
msgid "Domain list"
msgstr "Seznam domén"
#: mailu/ui/templates/domain/list.html:18
msgid "Manage"
msgstr "Spravovat"
#: mailu/ui/templates/domain/list.html:20
msgid "Mailbox count"
msgstr "Počet poštovních schránek"
#: mailu/ui/templates/domain/list.html:21
msgid "Alias count"
msgstr "Počet aliasů"
#: mailu/ui/templates/domain/list.html:31
msgid "Details"
msgstr "Podrobnosti"
#: mailu/ui/templates/domain/list.html:38
msgid "Users"
msgstr "Uživatelů"
#: mailu/ui/templates/domain/list.html:39
msgid "Aliases"
msgstr "Aliasů"
#: mailu/ui/templates/domain/list.html:40
msgid "Managers"
msgstr "Manažerů"
#: mailu/ui/templates/domain/list.html:42
msgid "Alternatives"
msgstr "Alternativ"
#: 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 ""
"Chcete-li zaregistrovat novou doménu, musíte nejprve nastavit\n"
" zónu domény tak, aby doménový <code>MX</code> záznam ukazovala na tento "
"server"
#: 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 ""
"Pokud nevíte, jak nastavit <code>MX</code> záznam pro zónu DNS,\n"
" kontaktujte svého poskytovatele DNS nebo správce. Také prosím počkejte "
"a\n"
" několik minut po <code>MX</code> tak, aby vypršela v mezipaměti "
"místního\n"
" serveru."
#: mailu/ui/templates/fetch/create.html:4
msgid "Add a fetched account"
msgstr "Přidejte fetched účet"
#: mailu/ui/templates/fetch/edit.html:4
msgid "Update a fetched account"
msgstr "Aktualizujte fetched účet"
#: mailu/ui/templates/fetch/list.html:12
msgid "Add an account"
msgstr "Přidat účet"
#: mailu/ui/templates/fetch/list.html:20
msgid "Endpoint"
msgstr "Koncový bod"
#: mailu/ui/templates/fetch/list.html:22
msgid "Keep emails"
msgstr "Zachovat emaily"
#: mailu/ui/templates/fetch/list.html:23
msgid "Last check"
msgstr "Poslední kontrola"
#: mailu/ui/templates/fetch/list.html:24
msgid "Status"
msgstr "Status"
#: mailu/ui/templates/fetch/list.html:38
msgid "yes"
msgstr "ano"
#: mailu/ui/templates/fetch/list.html:38
msgid "no"
msgstr "ne"
#: mailu/ui/templates/manager/create.html:4
msgid "Add a manager"
msgstr "Přidat manažera"
#: mailu/ui/templates/manager/list.html:4
msgid "Manager list"
msgstr "Seznam manažerů"
#: mailu/ui/templates/manager/list.html:12
msgid "Add manager"
msgstr "Přidat manažera"
#: mailu/ui/templates/relay/create.html:4
msgid "New relay domain"
msgstr "Nová relay doména"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayed domain"
msgstr "Upravit relay doménu"
#: mailu/ui/templates/relay/list.html:4
msgid "Relayed domain list"
msgstr "Seznam relay domén"
#: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain"
msgstr "Nová relay doména"
#: mailu/ui/templates/token/create.html:4
msgid "Create an authentication token"
msgstr "Vytvořit ověřovací token"
#: mailu/ui/templates/token/list.html:12
msgid "New token"
msgstr "Nový token"
#: mailu/ui/templates/user/create.html:4
msgid "New user"
msgstr "Nový uživatel"
#: mailu/ui/templates/user/create.html:15
msgid "General"
msgstr "Všeobecné"
#: mailu/ui/templates/user/create.html:23
msgid "Features and quotas"
msgstr "Funkce a kvóty"
#: mailu/ui/templates/user/edit.html:4
msgid "Edit user"
msgstr "Upravit uživatele"
#: mailu/ui/templates/user/list.html:4
msgid "User list"
msgstr "Seznam uživatelů"
#: mailu/ui/templates/user/list.html:12
msgid "Add user"
msgstr "Přidat uživatele"
#: mailu/ui/templates/user/list.html:20 mailu/ui/templates/user/settings.html:4
msgid "User settings"
msgstr "Uživatelské nastavení"
#: mailu/ui/templates/user/list.html:22
msgid "Features"
msgstr "Funkce"
#: mailu/ui/templates/user/password.html:4
msgid "Password update"
msgstr "Aktualizace hesla"
#: mailu/ui/templates/user/reply.html:4
msgid "Automatic reply"
msgstr "Automatická odpověď"
#: mailu/ui/templates/user/settings.html:27
msgid "Auto-forward"
msgstr "Automatické přeposlání"
#: mailu/ui/templates/user/signup_domain.html:8
msgid "pick a domain for the new account"
msgstr "vybrat doménu pro nový účet"
#: mailu/ui/templates/user/signup_domain.html:14
msgid "Domain"
msgstr "Doména"
#: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots"
msgstr "Dostupných slotů"

@ -649,7 +649,7 @@ msgid "New relay domain"
msgstr ""
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr ""
#: mailu/ui/templates/relay/list.html:4

@ -655,7 +655,7 @@ msgid "New relay domain"
msgstr "Neue Relay-Domain"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Relay-Domain bearbeiten"
#: mailu/ui/templates/relay/list.html:4

@ -649,7 +649,7 @@ msgid "New relay domain"
msgstr "New relay domain"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr ""
#: mailu/ui/templates/relay/list.html:4

@ -651,7 +651,7 @@ msgid "New relay domain"
msgstr "Nuevo dominio externo (relay)"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Editar dominio externo (relay)"
#: mailu/ui/templates/relay/list.html:4

@ -649,7 +649,7 @@ msgid "New relay domain"
msgstr "Igorritako domeinu berria"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Editatu igorritako domeinua"
#: mailu/ui/templates/relay/list.html:4

@ -653,7 +653,7 @@ msgid "New relay domain"
msgstr "Nouveau domaine relayé"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Modifier le domaine relayé"
#: mailu/ui/templates/relay/list.html:4

@ -658,7 +658,7 @@ msgid "New relay domain"
msgstr "שם תחום מועבר"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "עריכת שמות תחום מועברים"
#: mailu/ui/templates/relay/list.html:4

@ -652,7 +652,7 @@ msgid "New relay domain"
msgstr "Új továbbító tartomány"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Továbbított tartomány szerkesztése"
#: mailu/ui/templates/relay/list.html:4

@ -648,7 +648,7 @@ msgid "New relay domain"
msgstr ""
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr ""
#: mailu/ui/templates/relay/list.html:4

@ -655,7 +655,7 @@ msgid "New relay domain"
msgstr "Nuovo dominio affidato"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Editare dominio affidato"
#: mailu/ui/templates/relay/list.html:4

@ -649,7 +649,7 @@ msgid "New relay domain"
msgstr "リレードメイン名を追加"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "リレードメイン名を編集"
#: mailu/ui/templates/relay/list.html:4

@ -657,7 +657,7 @@ msgid "New relay domain"
msgstr "Nytt videresendt domene"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Endre videresendt domene"
#: mailu/ui/templates/relay/list.html:4

@ -650,7 +650,7 @@ msgid "New relay domain"
msgstr "Nieuw relay domein"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Bewerk relay domein"
#: mailu/ui/templates/relay/list.html:4

@ -659,7 +659,7 @@ msgstr "Nowa domena do przekierowania"
#: mailu/ui/templates/relay/edit.html:4
#, fuzzy
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Edycja domeny"
#: mailu/ui/templates/relay/list.html:4

@ -650,7 +650,7 @@ msgid "New relay domain"
msgstr "Novo domínio para encaminhamento"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Editar domínio de encaminhamento"
#: mailu/ui/templates/relay/list.html:4

@ -656,7 +656,7 @@ msgid "New relay domain"
msgstr "Новый релейный домен"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Изменить релейный домен"
#: mailu/ui/templates/relay/list.html:4

@ -645,7 +645,7 @@ msgid "New relay domain"
msgstr "Ny relä-domän"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "Redigera reläade domäner"
#: mailu/ui/templates/relay/list.html:4

@ -646,7 +646,7 @@ msgid "New relay domain"
msgstr "新的中继域"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr "编辑中继域"
#: mailu/ui/templates/relay/list.html:4

@ -37,7 +37,17 @@ class MultipleEmailAddressesVerify(object):
self.message = message
def __call__(self, form, field):
pattern = re.compile(r'^([_a-z0-9\-]+)(\.[_a-z0-9\-]+)*@([a-z0-9\-]{1,}\.)*([a-z]{1,})(,([_a-z0-9\-]+)(\.[_a-z0-9\-]+)*@([a-z0-9\-]{1,}\.)*([a-z]{2,}))*$')
pattern = re.compile(r'^([_a-z0-9\-\+]+)(\.[_a-z0-9\-\+]+)*@([a-z0-9\-]{1,}\.)*([a-z]{1,})(,([_a-z0-9\-\+]+)(\.[_a-z0-9\-\+]+)*@([a-z0-9\-]{1,}\.)*([a-z]{2,}))*$')
if not pattern.match(field.data.replace(" ", "")):
raise validators.ValidationError(self.message)
class MultipleFoldersVerify(object):
""" Ensure that we have CSV formated data """
def __init__(self,message=_('Invalid list of folders.')):
self.message = message
def __call__(self, form, field):
pattern = re.compile(r'^[^,]+(,[^,]+)*$')
if not pattern.match(field.data.replace(" ", "")):
raise validators.ValidationError(self.message)
@ -59,6 +69,7 @@ class DomainSignupForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Initial admin'), [validators.DataRequired()])
pw = fields.PasswordField(_('Admin password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
pwned = fields.HiddenField(label='', default=-1)
captcha = flask_wtf.RecaptchaField()
submit = fields.SubmitField(_('Create'))
@ -79,9 +90,11 @@ class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password'))
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
pwned = fields.HiddenField(label='', default=-1)
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=10**9)
enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True)
allow_spoofing = fields.BooleanField(_('Allow the user to spoof the sender (send email as anyone)'), default=False)
displayed_name = fields.StringField(_('Displayed name'))
comment = fields.StringField(_('Comment'))
enabled = fields.BooleanField(_('Enabled'), default=True)
@ -92,6 +105,7 @@ class UserSignupForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Email address'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
pwned = fields.HiddenField(label='', default=-1)
submit = fields.SubmitField(_('Sign up'))
class UserSignupFormCaptcha(UserSignupForm):
@ -111,6 +125,7 @@ class UserSettingsForm(flask_wtf.FlaskForm):
class UserPasswordForm(flask_wtf.FlaskForm):
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Password check'), [validators.DataRequired()])
pwned = fields.HiddenField(label='', default=-1)
submit = fields.SubmitField(_('Update password'))
@ -119,8 +134,8 @@ class UserReplyForm(flask_wtf.FlaskForm):
reply_subject = fields.StringField(_('Reply subject'))
reply_body = fields.StringField(_('Reply body'),
widget=widgets.TextArea())
reply_startdate = fields.html5.DateField(_('Start of vacation'))
reply_enddate = fields.html5.DateField(_('End of vacation'))
reply_startdate = fields.DateField(_('Start of vacation'))
reply_enddate = fields.DateField(_('End of vacation'))
submit = fields.SubmitField(_('Update'))
@ -160,11 +175,13 @@ class FetchForm(flask_wtf.FlaskForm):
('imap', 'IMAP'), ('pop3', 'POP3')
])
host = fields.StringField(_('Hostname or IP'), [validators.DataRequired()])
port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)])
tls = fields.BooleanField(_('Enable TLS'))
port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)], default=993)
tls = fields.BooleanField(_('Enable TLS'), default=True)
username = fields.StringField(_('Username'), [validators.DataRequired()])
password = fields.PasswordField(_('Password'))
keep = fields.BooleanField(_('Keep emails on the server'))
scan = fields.BooleanField(_('Rescan emails locally'))
folders = fields.StringField(_('Folders to fetch on the server'), [validators.Optional(), MultipleFoldersVerify()], default='INBOX,Junk')
submit = fields.SubmitField(_('Submit'))

@ -11,10 +11,10 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
</tr>
</thead>
@ -22,6 +22,7 @@
{%- for admin in admins %}
<tr>
<td>
<a href="{{ url_for('.user_edit', user_email=admin.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp;
<a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ admin }}</td>

@ -13,10 +13,10 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Destination{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
@ -34,8 +34,8 @@
<td>{{ alias }}</td>
<td>{{ alias.destination|join(', ') or '-' }}</td>
<td>{{ alias.comment or '' }}</td>
<td>{{ alias.created_at | format_date }}</td>
<td>{{ alias.updated_at | format_date }}</td>
<td data-sort="{{ alias.created_at or '0000-00-00' }}">{{ alias.created_at | format_date }}</td>
<td data-sort="{{ alias.updated_at or '0000-00-00' }}">{{ alias.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -13,10 +13,10 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
@ -29,8 +29,8 @@
<a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ alternative }}</td>
<td>{{ alternative.created_at | format_date }}</td>
<td>{{ alternative.updated_at | format_date }}</td>
<td data-sort="{{ alternative.created_at or '0000-00-00' }}">{{ alternative.created_at | format_date }}</td>
<td data-sort="{{ alternative.updated_at or '0000-00-00' }}">{{ alternative.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -1,7 +1,7 @@
{%- import "macros.html" as macros %}
{%- import "bootstrap/utils.html" as utils %}
<!doctype html>
<html lang="{{ session['language'] }}" data-static="/static/">
<html lang="{{ get_locale() }}" data-static="/static/">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -34,8 +34,8 @@
<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>
<i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">{% trans %}change language{% endtrans %}</span>
<span class="badge badge-primary navbar-badge">{{ get_locale() }}</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>
@ -78,6 +78,11 @@
<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>
<div class="mailu-version">
<span class="fa-pull-right">
{{ config["MAILU_VERSION"] }}
</span>
</div>
</footer>
</div>
<script src="{{ url_for('static', filename='vendor.js') }}"></script>

@ -9,7 +9,6 @@
{%- endblock %}
{%- block content %}
<div>If you use an Apple device, <a href="/apple.mobileconfig">click here to autoconfigure it.</a></div>
{%- call macros.table(title=_("Incoming mail"), datatable=False) %}
<tbody>
<tr>
@ -22,7 +21,7 @@
</tr>
<tr>
<th>{% trans %}Server name{% endtrans %}</th>
<td><pre class="pre-config border bg-light">{{ config["HOSTNAMES"] }}</pre></td>
<td><pre class="pre-config border bg-light">{{ config["HOSTNAME"] }}</pre></td>
</tr>
<tr>
<th>{% trans %}Username{% endtrans %}</th>
@ -47,7 +46,7 @@
</tr>
<tr>
<th>{% trans %}Server name{% endtrans %}</th>
<td><pre class="pre-config border bg-light">{{ config["HOSTNAMES"] }}</pre></td>
<td><pre class="pre-config border bg-light">{{ config["HOSTNAME"] }}</pre></td>
</tr>
<tr>
<th>{% trans %}Username{% endtrans %}</th>
@ -59,4 +58,8 @@
</tr>
</tbody>
{%- endcall %}
<blockquote>
{% trans %}If you use an Apple device,{% endtrans %}
<a href="/apple.mobileconfig">{% trans %}click here to autoconfigure it.{% endtrans %}</a>
</blockquote>
{%- endblock %}

@ -10,7 +10,7 @@
{{ form.hidden_tag() }}
{{ macros.form_field(form.name) }}
{{ macros.form_fields((form.max_users, form.max_aliases)) }}
{{ macros.form_field(form.max_quota_bytes, step=10**9, max=50*10**9, data_infinity="true",
{{ macros.form_field(form.max_quota_bytes, step=50*10**6, max=50*10**9, data_infinity="true", data_unit=10**9,
prepend='<span class="input-group-text"><span id="max_quota_bytes_value"></span>&nbsp;GB</span>') }}
{{ macros.form_field(form.signup_enabled) }}
{{ macros.form_field(form.comment) }}

@ -11,15 +11,17 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[2,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Manage{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}Manage{% endtrans %}</th>
<th>{% trans %}Domain name{% endtrans %}</th>
<th>{% trans %}Mailbox count{% endtrans %}</th>
<th>{% trans %}Alias count{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Enable sign-up{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
@ -43,11 +45,13 @@
{%- endif %}
</td>
<td>{{ domain.name }}</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 data-order="{{ domain.users | count }}">{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td>
<td data-order="{{ domain.aliases | count }}">{{ domain.aliases | count }} / {{ '∞' if domain.max_aliases == -1 else domain.max_aliases }}</td>
<td data-sort="{{ domain.max_quota_bytes }}">{{ (domain.max_quota_bytes | filesizeformat) if domain.max_quota_bytes else '∞' }}</td>
<td>{{ domain.comment or '' }}</td>
<td>{{ domain.created_at | format_date }}</td>
<td>{{ domain.updated_at | format_date }}</td>
<td data-sort="{{ domain.signup_enabled }}">{% if domain.signup_enabled %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td data-order="{{ domain.created_at or '0000-00-00' }}">{{ domain.created_at | format_date }}</td>
<td data-order="{{ domain.updated_at or '0000-00-00' }}">{{ domain.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -24,6 +24,8 @@
{%- call macros.card(title="Settings") %}
{{ macros.form_field(form.keep) }}
{{ macros.form_field(form.scan) }}
{{ macros.form_field(form.folders) }}
{%- endcall %}
{{ macros.form_field(form.submit) }}

@ -13,13 +13,15 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Endpoint{% endtrans %}</th>
<th>{% trans %}Username{% endtrans %}</th>
<th>{% trans %}Keep emails{% endtrans %}</th>
<th>{% trans %}Rescan emails{% endtrans %}</th>
<th>{% trans %}Folders{% endtrans %}</th>
<th>{% trans %}Last check{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
@ -35,11 +37,13 @@
</td>
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
<td>{{ fetch.username }}</td>
<td>{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td data-sort="{{ fetch.keep }}">{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td data-sort="{{ fetch.scan }}">{% if fetch.scan %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td>{{ fetch.folders | join(',') }}</td>
<td>{{ fetch.last_check | format_datetime or '-' }}</td>
<td>{{ fetch.error or '-' }}</td>
<td>{{ fetch.created_at | format_date }}</td>
<td>{{ fetch.updated_at | format_date }}</td>
<td data-sort="{{ fetch.created_at or '0000-00-00' }}">{{ fetch.created_at | format_date }}</td>
<td data-sort="{{ fetch.updated_at or '0000-00-00' }}">{{ fetch.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -3,7 +3,7 @@
{%- for fieldname, errors in form.errors.items() %}
{%- if bootstrap_is_hidden_field(form[fieldname]) %}
{%- for error in errors %}
<p class="error">{{error}}</p>
<p class="form-text text-danger">{{error}}</p>
{%- endfor %}
{%- endif %}
{%- endfor %}
@ -13,7 +13,7 @@
{%- macro form_field_errors(field) %}
{%- if field.errors %}
{%- for error in field.errors %}
<p class="help-block inline">{{ error }}</p>
<p class="form-text text-danger">{{ error }}</p>
{%- endfor %}
{%- endif %}
{%- endmacro %}
@ -23,7 +23,7 @@
<div class="form-group">
<div class="row">
{%- 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">
{%- if field.__class__.__name__ == 'list' %}
{%- for subfield in field %}
{{ form_individual_field(subfield, prepend=prepend, append=append, label=label, **kwargs) }}
@ -38,12 +38,13 @@
{%- endmacro %}
{%- macro form_individual_field(field, prepend='', append='', label=True, class_="") %}
{%- set fieldclass=" ".join(["form-control"] + ([class_] if class_ else []) + (["is-invalid"] if field.errors else [])) %}
{%- if field.type == "BooleanField" %}
{{ field(**kwargs) }}<span>&nbsp;&nbsp;</span>{{ field.label if label else '' }}
{%- else %}
{{ field.label if label else '' }}{{ form_field_errors(field) }}
{%- if prepend %}<div class="input-group-prepend">{%- elif append %}<div class="input-group-append">{%- endif %}
{{ prepend|safe }}{{ field(class_=("form-control " + class_) if class_ else "form-control", **kwargs) }}{{ append|safe }}
{{ prepend|safe }}{{ field(class_=fieldclass, **kwargs) }}{{ append|safe }}
{%- if prepend or append %}</div>{%- endif %}
{%- endif %}
{%- endmacro %}
@ -60,9 +61,7 @@
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{%- for field in form %}
{%- if bootstrap_is_hidden_field(field) %}
{{ field() }}
{%- else %}
{%- if not bootstrap_is_hidden_field(field) %}
{{ form_field(field) }}
{%- endif %}
{%- endfor %}
@ -88,7 +87,7 @@
</div>
{%- endmacro %}
{%- macro table(title=None, theme="primary", datatable=True) %}
{%- macro table(title=None, theme="primary", datatable=True, order=None) %}
<div class="row">
<div class="col-lg-12">
<div class="card card-outline card-{{ theme }}">
@ -98,7 +97,7 @@
</div>
{%- endif %}
<div class="card-body">
<table class="table table-bordered{% if datatable %} dataTable{% endif %}">
<table class="table table-bordered{% if datatable %} dataTable{% endif %}" data-order="{{ order or '[]' | e }}">
{{- caller() }}
</table>
</div>

@ -13,10 +13,10 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
</tr>
</thead>
@ -24,6 +24,7 @@
{%- for manager in domain.managers %}
<tr>
<td>
<a href="{{ url_for('.user_edit', user_email=manager.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp;
<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>{{ manager }}</td>

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

@ -11,10 +11,10 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Domain name{% endtrans %}</th>
<th>{% trans %}Remote host{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
@ -32,8 +32,8 @@
<td>{{ relay.name }}</td>
<td>{{ relay.smtp or '-' }}</td>
<td>{{ relay.comment or '' }}</td>
<td>{{ relay.created_at | format_date }}</td>
<td>{{ relay.updated_at | format_date }}</td>
<td data-sort="{{ relay.created_at or '0000-00-00' }}">{{ relay.created_at | format_date }}</td>
<td data-sort="{{ relay.updated_at or '0000-00-00' }}">{{ relay.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -31,12 +31,14 @@
<p>{% trans %}Auto-reply{% endtrans %}</p>
</a>
</li>
{%- if config["FETCHMAIL_ENABLED"] %}
<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>
{%- endif %}
<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>
@ -94,9 +96,9 @@
<li class="nav-header text-uppercase text-primary" role="none">{% trans %}Go to{% endtrans %}</li>
{%- if config["WEBMAIL"] != "none" and current_user.is_authenticated %}
<li class="nav-item" role="none">
<a href="{{ config["WEB_WEBMAIL"] }}" target="_blank" class="nav-link" role="menuitem">
<a href="{{ config["WEB_WEBMAIL"] }}" class="nav-link" role="menuitem">
<i class="nav-icon far fa-envelope"></i>
<p>{% trans %}Webmail{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
<p>{% trans %}Webmail{% endtrans %}</p>
</a>
</li>
{%- endif %}

@ -13,10 +13,10 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Authorized IP{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
@ -31,8 +31,8 @@
</td>
<td>{{ token.comment }}</td>
<td>{{ token.ip or "any" }}</td>
<td>{{ token.created_at | format_date }}</td>
<td>{{ token.updated_at | format_date }}</td>
<td data-sort="{{ token.created_at or '0000-00-00' }}">{{ token.created_at | format_date }}</td>
<td data-sort="{{ token.updated_at or '0000-00-00' }}">{{ token.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -21,10 +21,11 @@
{%- endcall %}
{%- 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 50*10**9), data_infinity="true",
{{ macros.form_field(form.quota_bytes, step=50*10**6, max=(max_quota_bytes or domain.max_quota_bytes or 50*10**9), data_infinity="true", data_unit=10**9,
prepend='<span class="input-group-text"><span id="quota_bytes_value"></span>&nbsp;GB</span>') }}
{{ macros.form_field(form.enable_imap) }}
{{ macros.form_field(form.enable_pop) }}
{{ macros.form_field(form.allow_spoofing) }}
{%- endcall %}
{{ macros.form_field(form.submit) }}

@ -13,11 +13,11 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
{%- call macros.table(order='[[2,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}User settings{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th data-orderable="false">{% trans %}User settings{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Features{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th>
@ -31,22 +31,24 @@
<tr{% if not user.enabled %} class="warning"{% endif %}>
<td>
<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>
</td>
<td>
<a href="{{ url_for('.user_settings', user_email=user.email) }}" title="{% trans %}Settings{% endtrans %}"><i class="fa fa-wrench"></i></a>&nbsp;
<a href="{{ url_for('.user_reply', user_email=user.email) }}" title="{% trans %}Auto-reply{% endtrans %}"><i class="fa fa-plane"></i></a>&nbsp;
{%- if config["FETCHMAIL_ENABLED"] -%}
<a href="{{ url_for('.fetch_list', user_email=user.email) }}" title="{% trans %}Fetched accounts{% endtrans %}"><i class="fa fa-download"></i></a>&nbsp;
{%- endif -%}
</td>
<td>{{ user }}</td>
<td>
{% if user.enable_imap %}<span class="badge bg-info">imap</span>{% endif %}
{% if user.enable_pop %}<span class="badge bg-info">pop3</span>{% endif %}
<td data-sort="{{ user.allow_spoofing*4 + user.enable_imap*2 + user.enable_pop }}">
{% if user.enable_imap %}<span class="badge bg-primary">imap</span>{% endif %}
{% if user.enable_pop %}<span class="badge bg-secondary">pop3</span>{% endif %}
{% if user.allow_spoofing %}<span class="badge bg-danger">allow-spoofing</span>{% endif %}
</td>
<td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td>
<td data-sort="{{ user.quota_bytes_used }}">{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td>
<td>{{ user.comment or '-' }}</td>
<td>{{ user.created_at | format_date }}</td>
<td>{{ user.updated_at | format_date }}</td>
<td data-sort="{{ user.created_at or '0000-00-00' }}">{{ user.created_at | format_date }}</td>
<td data-sort="{{ user.updated_at or '0000-00-00' }}">{{ user.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -9,18 +9,22 @@
{%- endblock %}
{%- block content %}
{%- call macros.table() %}
<tr>
<th>{% trans %}Domain{% endtrans %}</th>
<th>{% trans %}Available slots{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th>
</tr>
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}Domain{% endtrans %}</th>
<th>{% trans %}Available slots{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th>
</tr>
</thead>
<tbody>
{%- for domain_name, domain in available_domains.items() %}
<tr>
<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>{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }}</td>
<td data-sort="{{ -1 if domain.max_users == -1 else domain.max_users - (domain.users | count)}}">{{ '∞' if domain.max_users == -1 else domain.max_users - (domain.users | count)}}</td>
<td data-sort="{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] }}">{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }}</td>
</tr>
{%- endfor %}
</tbody>
{%- endcall %}
{%- endblock %}

@ -21,8 +21,9 @@ def announcement():
form = forms.AnnouncementForm()
if form.validate_on_submit():
for user in models.User.query.all():
user.sendmail(form.announcement_subject.data,
form.announcement_body.data)
if not user.sendmail(form.announcement_subject.data,
form.announcement_body.data):
flask.flash('Failed to send to %s' % user.email, 'error')
# Force-empty the form
form.announcement_subject.data = ''
form.announcement_body.data = ''

@ -1,4 +1,4 @@
from mailu import models
from mailu import models, utils
from mailu.ui import ui, forms, access
from flask import current_app as app
@ -93,6 +93,9 @@ def domain_signup(domain_name=None):
del form.pw
del form.pw2
if form.validate_on_submit():
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
return flask.render_template('domain/signup.html', form=form)
conflicting_domain = models.Domain.query.get(form.name.data)
conflicting_alternative = models.Alternative.query.get(form.name.data)
conflicting_relay = models.Relay.query.get(form.name.data)

@ -1,5 +1,6 @@
from mailu import models
from mailu import models, utils
from mailu.ui import ui, forms, access
from flask import current_app as app
import flask
import flask_login
@ -10,6 +11,8 @@ import wtforms
@ui.route('/fetch/list/<path:user_email>', methods=['GET'])
@access.owner(models.User, 'user_email')
def fetch_list(user_email):
if not app.config['FETCHMAIL_ENABLED']:
flask.abort(404)
user_email = user_email or flask_login.current_user.email
user = models.User.query.get(user_email) or flask.abort(404)
return flask.render_template('fetch/list.html', user=user)
@ -19,13 +22,18 @@ def fetch_list(user_email):
@ui.route('/fetch/create/<path:user_email>', methods=['GET', 'POST'])
@access.owner(models.User, 'user_email')
def fetch_create(user_email):
if not app.config['FETCHMAIL_ENABLED']:
flask.abort(404)
user_email = user_email or flask_login.current_user.email
user = models.User.query.get(user_email) or flask.abort(404)
form = forms.FetchForm()
form.password.validators = [wtforms.validators.DataRequired()]
utils.formatCSVField(form.folders)
if form.validate_on_submit():
fetch = models.Fetch(user=user)
form.populate_obj(fetch)
if form.folders.data:
fetch.folders = form.folders.data.replace(' ','').split(',')
models.db.session.add(fetch)
models.db.session.commit()
flask.flash('Fetch configuration created')
@ -37,12 +45,17 @@ def fetch_create(user_email):
@ui.route('/fetch/edit/<fetch_id>', methods=['GET', 'POST'])
@access.owner(models.Fetch, 'fetch_id')
def fetch_edit(fetch_id):
if not app.config['FETCHMAIL_ENABLED']:
flask.abort(404)
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
form = forms.FetchForm(obj=fetch)
utils.formatCSVField(form.folders)
if form.validate_on_submit():
if not form.password.data:
form.password.data = fetch.password
form.populate_obj(fetch)
if form.folders.data:
fetch.folders = form.folders.data.replace(' ','').split(',')
models.db.session.commit()
flask.flash('Fetch configuration updated')
return flask.redirect(
@ -55,6 +68,8 @@ def fetch_edit(fetch_id):
@access.confirmation_required("delete a fetched account")
@access.owner(models.Fetch, 'fetch_id')
def fetch_delete(fetch_id):
if not app.config['FETCHMAIL_ENABLED']:
flask.abort(404)
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
user = fetch.user
models.db.session.delete(fetch)

@ -1,4 +1,4 @@
from mailu import models
from mailu import models, utils
from mailu.ui import ui, access, forms
from flask import current_app as app
@ -28,6 +28,10 @@ def user_create(domain_name):
form.quota_bytes.validators = [
wtforms.validators.NumberRange(max=domain.max_quota_bytes)]
if form.validate_on_submit():
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
return flask.render_template('user/create.html',
domain=domain, form=form)
if domain.has_email(form.localpart.data):
flask.flash('Email is already used', 'error')
else:
@ -60,6 +64,11 @@ def user_edit(user_email):
form.quota_bytes.validators = [
wtforms.validators.NumberRange(max=max_quota_bytes)]
if form.validate_on_submit():
if form.pw.data:
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
return flask.render_template('user/edit.html', form=form, user=user,
domain=user.domain, max_quota_bytes=max_quota_bytes)
form.populate_obj(user)
if form.pw.data:
user.set_password(form.pw.data)
@ -71,19 +80,6 @@ def user_edit(user_email):
domain=user.domain, max_quota_bytes=max_quota_bytes)
@ui.route('/user/delete/<path:user_email>', methods=['GET', 'POST'])
@access.domain_admin(models.User, 'user_email')
@access.confirmation_required("delete {user_email}")
def user_delete(user_email):
user = models.User.query.get(user_email) or flask.abort(404)
domain = user.domain
models.db.session.delete(user)
models.db.session.commit()
flask.flash('User %s deleted' % user)
return flask.redirect(
flask.url_for('.user_list', domain_name=domain.name))
@ui.route('/user/settings', methods=['GET', 'POST'], defaults={'user_email': None})
@ui.route('/user/usersettings/<path:user_email>', methods=['GET', 'POST'])
@access.owner(models.User, 'user_email')
@ -91,11 +87,7 @@ def user_settings(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.UserSettingsForm(obj=user)
if isinstance(form.forward_destination.data,str):
data = form.forward_destination.data.replace(" ","").split(",")
else:
data = form.forward_destination.data
form.forward_destination.data = ", ".join(data)
utils.formatCSVField(form.forward_destination)
if form.validate_on_submit():
form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",")
form.populate_obj(user)
@ -119,6 +111,9 @@ def user_password(user_email):
if form.pw.data != form.pw2.data:
flask.flash('Passwords do not match', 'error')
else:
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
return flask.render_template('user/password.html', form=form, user=user)
flask.session.regenerate()
user.set_password(form.pw.data)
models.db.session.commit()
@ -170,6 +165,9 @@ def user_signup(domain_name=None):
if domain.has_email(form.localpart.data) or models.Alias.resolve(form.localpart.data, domain_name):
flask.flash('Email is already used', 'error')
else:
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
return flask.render_template('user/signup.html', domain=domain, form=form)
flask.session.regenerate()
user = models.User(domain=domain)
form.populate_obj(user)

@ -42,7 +42,7 @@ login.login_view = "sso.login"
def handle_needs_login():
""" redirect unauthorized requests to login page """
return flask.redirect(
flask.url_for('sso.login')
flask.url_for('sso.login', url=flask.request.url)
)
# DNS stub configured to do DNSSEC enabled queries
@ -472,7 +472,7 @@ class MailuSessionExtension:
redis.StrictRedis().from_url(app.config['SESSION_STORAGE_URL'])
)
# clean expired sessions oonce on first use in case lifetime was changed
# clean expired sessions once on first use in case lifetime was changed
def cleaner():
with cleaned.get_lock():
if not cleaned.value:
@ -507,3 +507,21 @@ def gen_temp_token(email, session):
app.config['PERMANENT_SESSION_LIFETIME'],
)
return token
def isBadOrPwned(form):
try:
if len(form.pw.data) < 8:
return "This password is too short."
breaches = int(form.pwned.data)
except ValueError:
breaches = -1
if breaches > 0:
return f"This password appears in {breaches} data breaches! It is not unique; please change it."
return None
def formatCSVField(field):
if isinstance(field.data,str):
data = field.data.replace(" ","").split(",")
else:
data = field.data
field.data = ", ".join(data)

@ -647,7 +647,7 @@ msgid "New relay domain"
msgstr ""
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgid "Edit relayed domain"
msgstr ""
#: mailu/ui/templates/relay/list.html:4

@ -0,0 +1,22 @@
""" Add user.allow_spoofing
Revision ID: 7ac252f2bbbf
Revises: 8f9ea78776f4
Create Date: 2022-11-20 08:57:16.879152
"""
# revision identifiers, used by Alembic.
revision = '7ac252f2bbbf'
down_revision = 'f4f0f89e0047'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('user', sa.Column('allow_spoofing', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()))
def downgrade():
op.drop_column('user', 'allow_spoofing')

@ -0,0 +1,25 @@
""" Add fetch.scan and fetch.folders
Revision ID: f4f0f89e0047
Revises: 8f9ea78776f4
Create Date: 2022-11-13 16:29:01.246509
"""
# revision identifiers, used by Alembic.
revision = 'f4f0f89e0047'
down_revision = '8f9ea78776f4'
from alembic import op
import sqlalchemy as sa
import mailu
def upgrade():
with op.batch_alter_table('fetch') as batch:
batch.add_column(sa.Column('scan', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()))
batch.add_column(sa.Column('folders', mailu.models.CommaSeparatedList(), nullable=True))
def downgrade():
with op.batch_alter_table('fetch') as batch:
batch.drop_column('folders')
batch.drop_column('scan')

@ -1,78 +0,0 @@
alembic==1.7.4
appdirs==1.4.4
Babel==2.9.1
bcrypt==3.2.0
blinker==1.4
CacheControl==0.12.9
certifi==2021.10.8
# cffi==1.15.0
chardet==4.0.0
click==8.0.3
colorama==0.4.4
contextlib2==21.6.0
cryptography==35.0.0
decorator==5.1.0
# distlib==0.3.1
# distro==1.5.0
dnspython==2.1.0
dominate==2.6.0
email-validator==1.1.3
Flask==2.0.2
Flask-Babel==2.0.0
Flask-Bootstrap==3.3.7.1
Flask-DebugToolbar==0.11.0
Flask-Limiter==1.4
Flask-Login==0.5.0
flask-marshmallow==0.14.0
Flask-Migrate==3.1.0
Flask-Script==2.0.6
Flask-SQLAlchemy==2.5.1
Flask-WTF==0.15.1
greenlet==1.1.2
gunicorn==20.1.0
html5lib==1.1
idna==3.3
infinity==1.5
intervals==0.9.2
itsdangerous==2.0.1
Jinja2==3.0.2
limits==1.5.1
lockfile==0.12.2
Mako==1.1.5
MarkupSafe==2.0.1
marshmallow==3.14.0
marshmallow-sqlalchemy==0.26.1
msgpack==1.0.2
# mysqlclient==2.0.3
mysql-connector-python==8.0.25
ordered-set==4.0.2
# packaging==20.9
passlib==1.7.4
# pep517==0.10.0
progress==1.6
#psycopg2==2.9.1
psycopg2-binary==2.9.3
pycparser==2.20
Pygments==2.10.0
pyOpenSSL==21.0.0
pyparsing==3.0.4
pytz==2021.3
PyYAML==6.0
redis==3.5.3
requests==2.26.0
retrying==1.3.3
# six==1.15.0
socrate==0.2.0
SQLAlchemy==1.4.26
srslib==0.1.4
tabulate==0.8.9
tenacity==8.0.1
toml==0.10.2
urllib3==1.26.7
validators==0.18.2
visitor==0.1.3
webencodings==0.5.1
Werkzeug==2.0.2
WTForms==2.3.3
WTForms-Components==0.10.5
xmltodict==0.12.0

@ -1,28 +0,0 @@
Flask
Flask-Login
Flask-SQLAlchemy
Flask-bootstrap
Flask-Babel
Flask-migrate
Flask-script
Flask-wtf
Flask-debugtoolbar
limits
redis
WTForms-Components
socrate
passlib
gunicorn
tabulate
PyYAML
PyOpenSSL
Pygments
dnspython
tenacity
mysql-connector-python
idna
srslib
marshmallow
flask-marshmallow
marshmallow-sqlalchemy
xmltodict

@ -0,0 +1,159 @@
#!/usr/bin/env bash
set -euo pipefail
### CONFIG
DEV_NAME="${DEV_NAME:-mailu-dev}"
DEV_DB="${DEV_DB:-}"
DEV_PROFILER="${DEV_PROFILER:-false}"
DEV_LISTEN="${DEV_LISTEN:-127.0.0.1:8080}"
[[ "${DEV_LISTEN}" == *:* ]] || DEV_LISTEN="127.0.0.1:${DEV_LISTEN}"
DEV_ADMIN="${DEV_ADMIN:-admin@example.com}"
DEV_PASSWORD="${DEV_PASSWORD:-letmein}"
DEV_ARGS=( "$@" )
### MAIN
[[ -n "${DEV_DB}" ]] && {
[[ -f "${DEV_DB}" ]] || {
echo "Sorry, can't find DEV_DB: '${DEV_DB}'"
exit 1
}
DEV_DB="$(realpath "${DEV_DB}")"
}
docker="$(command -v podman || command -v docker || echo false)"
[[ "${docker}" == "false" ]] && {
echo "Sorry, you'll need podman or docker to run this."
exit 1
}
tmp="$(mktemp -d)"
[[ -n "${tmp}" && -d "${tmp}" ]] || {
echo "Sorry, can't create temporary folder."
exit 1
}
trap "rm -rf '${tmp}'" INT TERM EXIT
admin="$(realpath "$(pwd)/${0%/*}")"
base="${admin}/../base"
assets="${admin}/assets"
cd "${tmp}"
# base
cp "${base}"/requirements-* .
cp -r "${base}"/libs .
sed -E '/^#/d;s:^FROM system$:FROM system AS base:' "${base}/Dockerfile" >Dockerfile
# assets
cp "${assets}/package.json" .
cp -r "${assets}/assets" ./assets
awk '/new compress/{f=1}!f{print}/}),/{f=0}' <"${assets}/webpack.config.js" >webpack.config.js
sed -E '/^#/d;s:^(FROM [^ ]+$):\1 AS assets:' "${assets}/Dockerfile" >>Dockerfile
# admin
sed -E '/^#/d;/^(COPY|EXPOSE|HEALTHCHECK|VOLUME|CMD) /d; s:^(.* )[^ ]*pybabel[^\\]*(.*):\1true \2:' "${admin}/Dockerfile" >>Dockerfile
# development
cat >>Dockerfile <<EOF
COPY --from=assets /work/static/ ./static/
RUN set -euxo pipefail \
; mkdir /data \
; ln -s /app/audit.py / \
; ln -s /app/start.py /
ENV \
FLASK_DEBUG="true" \
MEMORY_SESSIONS="true" \
RATELIMIT_STORAGE_URL="memory://" \
SESSION_COOKIE_SECURE="false" \
\
DEBUG="true" \
DEBUG_PROFILER="${DEV_PROFILER}" \
DEBUG_ASSETS="/app/static" \
DEBUG_TB_INTERCEPT_REDIRECTS=False \
\
ADMIN_ADDRESS="127.0.0.1" \
FRONT_ADDRESS="127.0.0.1" \
SMTP_ADDRESS="127.0.0.1" \
IMAP_ADDRESS="127.0.0.1" \
REDIS_ADDRESS="127.0.0.1" \
ANTIVIRUS_ADDRESS="127.0.0.1" \
ANTISPAM_ADDRESS="127.0.0.1" \
WEBMAIL_ADDRESS="127.0.0.1" \
WEBDAV_ADDRESS="127.0.0.1"
CMD ["/bin/bash", "-c", "flask db upgrade &>/dev/null && flask mailu admin '${DEV_ADMIN/@*}' '${DEV_ADMIN#*@}' '${DEV_PASSWORD}' --mode ifmissing >/dev/null; flask --debug run --host=0.0.0.0 --port=8080"]
EOF
# build
chmod -R u+rwX,go+rX .
echo Running: "${docker/*\/}" build --tag "${DEV_NAME}:latest" "${DEV_ARGS[@]}" .
"${docker}" build --tag "${DEV_NAME}:latest" "${DEV_ARGS[@]}" .
# gather volumes to map into container
volumes=()
[[ -n "${DEV_DB}" ]] && volumes+=( --volume "${DEV_DB}:/data/main.db" )
for vol in audit.py start.py mailu/ migrations/; do
volumes+=( --volume "${admin}/${vol}:/app/${vol}" )
done
for file in "${assets}/assets"/*; do
[[ ! -f "${file}" || "${file}" == */vendor.js ]] && continue
volumes+=( --volume "${file}:/app/static/${file/*\//}" )
done
# show configuration
cat <<EOF
=============================================================================
The "${DEV_NAME}" container was built using this configuration:
DEV_NAME="${DEV_NAME}"
DEV_DB="${DEV_DB}"
DEV_PROFILER="${DEV_PROFILER}"
DEV_LISTEN="${DEV_LISTEN}"
DEV_ADMIN="${DEV_ADMIN}"
DEV_PASSWORD="${DEV_PASSWORD}"
DEV_ARGS=( ${DEV_ARGS[*]} )
=============================================================================
You can start the container later using this command:
${docker/*\/} run --rm -it --name "${DEV_NAME}" --publish ${DEV_LISTEN}:8080$(printf " %q" "${volumes[@]}") "${DEV_NAME}"
=============================================================================
Enter the running container using this command:
${docker/*\/} exec -it "${DEV_NAME}" /bin/bash
=============================================================================
To update requirements-prod.txt you can build (and test) using:
${docker/*\/} build --tag "${DEV_NAME}:latest" --build-arg MAILU_DEPS=dev .
And then fetch the new dependencies with:
${docker/*\/} exec "${DEV_NAME}" pip freeze >$(realpath "${base}")/requirements-new.txt
=============================================================================
The Mailu UI can be found here: http://${DEV_LISTEN}/sso/login
EOF
[[ -z "${DEV_DB}" ]] && echo "You can log in with user ${DEV_ADMIN} and password ${DEV_PASSWORD}"
cat <<EOF
=============================================================================
Starting mailu dev environment...
EOF
# run
"${docker}" run --rm -it --name "${DEV_NAME}" --publish "${DEV_LISTEN}:8080" "${volumes[@]}" "${DEV_NAME}"

@ -1,10 +1,16 @@
#!/usr/bin/python3
#!/usr/bin/env python3
import os
import logging as log
import sys
from socrate import system
os.system("chown mailu:mailu -R /dkim")
os.system("find /data | grep -v /fetchmail | xargs -n1 chown mailu:mailu")
system.drop_privs_to('mailu')
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "INFO"))
system.set_env(['SECRET'])
os.system("flask mailu advertise")
os.system("flask db upgrade")
@ -15,7 +21,7 @@ password = os.environ.get("INITIAL_ADMIN_PW")
if account is not None and domain is not None and password is not None:
mode = os.environ.get("INITIAL_ADMIN_MODE", default="ifmissing")
log.info("Creating initial admin accout %s@%s with mode %s",account,domain,mode)
log.info("Creating initial admin account %s@%s with mode %s", account, domain, mode)
os.system("flask mailu admin %s %s '%s' --mode %s" % (account, domain, password, mode))
def test_DNS():
@ -37,7 +43,7 @@ def test_DNS():
try:
result = resolver.resolve('example.org', dns.rdatatype.A, dns.rdataclass.IN, lifetime=10)
except Exception as e:
log.critical("Your DNS resolver at %s is not working (%s). Please see https://mailu.io/master/faq.html#the-admin-container-won-t-start-and-its-log-says-critical-your-dns-resolver-isn-t-doing-dnssec-validation", ns, e);
log.critical("Your DNS resolver at %s is not working (%s). Please see https://mailu.io/master/faq.html#the-admin-container-won-t-start-and-its-log-says-critical-your-dns-resolver-isn-t-doing-dnssec-validation", ns, e)
else:
if result.response.flags & dns.flags.AD:
break
@ -46,12 +52,21 @@ def test_DNS():
test_DNS()
start_command="".join([
"gunicorn --threads ", str(os.cpu_count()),
" -b :80 ",
"--access-logfile - " if (log.root.level<=log.INFO) else "",
"--error-logfile - ",
"--preload ",
"'mailu:create_app()'"])
cmdline = [
"gunicorn",
"--threads", f"{os.cpu_count()}",
# If SUBNET6 is defined, gunicorn must listen on IPv6 as well as IPv4
"-b", f"{'[::]' if os.environ.get('SUBNET6') else ''}:80",
"--logger-class mailu.Logger",
"--worker-tmp-dir /dev/shm",
"--error-logfile", "-",
"--preload"
]
os.system(start_command)
# logging
if log.root.level <= log.INFO:
cmdline.extend(["--access-logfile", "-"])
cmdline.append("'mailu:create_app()'")
os.system(" ".join(cmdline))

@ -0,0 +1,91 @@
# syntax=docker/dockerfile-upstream:1.4.3
# base system image (intermediate)
ARG DISTRO=alpine:3.17.2
FROM $DISTRO as system
ENV TZ=Etc/UTC LANG=C.UTF-8
ARG MAILU_UID=1000
ARG MAILU_GID=1000
RUN set -euxo pipefail \
; addgroup -Sg ${MAILU_GID} mailu \
; adduser -Sg ${MAILU_UID} -G mailu -h /app -g "mailu app" -s /bin/bash mailu \
; apk add --no-cache bash ca-certificates curl python3 tzdata libcap \
; ! [[ "$(uname -m)" == x86_64 ]] \
|| apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing hardened-malloc==11-r0
WORKDIR /app
CMD /bin/bash
# build virtual env (intermediate)
FROM system as build
ARG MAILU_DEPS=prod
ARG SNUFFLEUPAGUS_VERSION=0.9.0
ENV VIRTUAL_ENV=/app/venv
COPY requirements-build.txt ./
RUN set -euxo pipefail \
; apk add --no-cache py3-pip \
; python3 -m venv ${VIRTUAL_ENV} \
; ${VIRTUAL_ENV}/bin/pip install --no-cache-dir -r requirements-build.txt \
; apk del -r py3-pip \
; rm -f /tmp/*.pem
COPY requirements-${MAILU_DEPS}.txt ./
COPY libs/ libs/
ENV \
PATH="${VIRTUAL_ENV}/bin:${PATH}" \
CXXFLAGS="-g -O2 -fdebug-prefix-map=/app=. -fstack-protector-strong -Wformat -Werror=format-security -fstack-clash-protection -fexceptions" \
CFLAGS="-g -O2 -fdebug-prefix-map=/app=. -fstack-protector-strong -Wformat -Werror=format-security -fstack-clash-protection -fexceptions" \
CPPFLAGS="-Wdate-time -D_FORTIFY_SOURCE=2" \
LDFLAGS="-Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now" \
SNUFFLEUPAGUS_URL="https://github.com/jvoisin/snuffleupagus/archive/refs/tags/v${SNUFFLEUPAGUS_VERSION}.tar.gz"
RUN set -euxo pipefail \
; machine="$(uname -m)" \
; deps="build-base gcc libffi-dev python3-dev" \
; [[ "${machine}" != x86_64 ]] && \
deps="${deps} cargo git libretls-dev mariadb-connector-c-dev postgresql-dev" \
; apk add --virtual .build-deps ${deps} \
; [[ "${machine}" == armv7* ]] && \
mkdir -p /root/.cargo/registry/index && \
git clone --bare https://github.com/rust-lang/crates.io-index.git /root/.cargo/registry/index/github.com-1285ae84e5963aae \
; pip install -r requirements-${MAILU_DEPS}.txt \
; curl -sL ${SNUFFLEUPAGUS_URL} | tar xz \
; cd snuffleupagus-${SNUFFLEUPAGUS_VERSION} \
; rm -rf src/tests/*php7*/ src/tests/*session*/ src/tests/broken_configuration/ src/tests/*cookie* src/tests/upload_validation/ \
; apk add --virtual .build-deps php81-dev php81-cgi php81-simplexml php81-xml pcre-dev build-base php81-pear php81-openssl re2c \
; pecl install vld-beta \
; make -j $(grep -c processor /proc/cpuinfo) release \
; cp src/.libs/snuffleupagus.so /app \
; rm -rf /root/.cargo /tmp/*.pem /root/.cache
# base mailu image
FROM system
COPY --from=build /app/venv/ /app/venv/
COPY --chown=root:root --from=build /app/snuffleupagus.so /usr/lib/php81/modules/
RUN setcap 'cap_net_bind_service=+ep' /app/venv/bin/gunicorn 'cap_net_bind_service=+ep' /usr/bin/python3.10
ENV \
VIRTUAL_ENV=/app/venv \
PATH="/app/venv/bin:${PATH}" \
LD_PRELOAD="/usr/lib/libhardened_malloc.so" \
ADMIN_ADDRESS="admin" \
FRONT_ADDRESS="front" \
SMTP_ADDRESS="smtp" \
IMAP_ADDRESS="imap" \
OLETOOLS_ADDRESS="oletools" \
REDIS_ADDRESS="redis" \
ANTIVIRUS_ADDRESS="antivirus" \
ANTISPAM_ADDRESS="antispam" \
WEBMAIL_ADDRESS="webmail" \
WEBDAV_ADDRESS="webdav"

@ -0,0 +1,20 @@
.DS_Store
.idea
tmp
*.bak
*~
.*.swp
__pycache__/
*.pyc
*.pyo
*.egg-info/
.build
.env*
.venv
*.code-workspace
build/

@ -0,0 +1,7 @@
This project is open source, and your contributions are all welcome. There are mostly three different ways one can contribute to the project:
1. use Podop, either on test or on production servers, and report meaningful bugs when you find some;
2. write and publish, or contribute to mail distributions based on Podop, like Mailu;
2. contribute code and/or configuration to the repository (see [the development guidelines](https://mailu.io/contributors/guide.html) for details);
Either way, keep in mind that the code you write must be licensed under the same conditions as the project itself. Additionally, all contributors are considered equal co-authors of the project.

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

Loading…
Cancel
Save