mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-06-14 10:30:27 +00:00
Compare commits
270 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c814cc182 | |||
| cf6594220c | |||
| 2cf952eb36 | |||
| 6fc86dd7d3 | |||
| bf13af9691 | |||
| 1af9c21a50 | |||
| 443941e687 | |||
| 527577b438 | |||
| 9daf2d80c0 | |||
| 38b0641742 | |||
| f675af5bb0 | |||
| 533c4e7956 | |||
| 1b2c2c0037 | |||
| 97768494e1 | |||
| 4a052da289 | |||
| 18d7a55b15 | |||
| 9ca2fb7ccf | |||
| b4e8355827 | |||
| e0bde1c459 | |||
| 27c007ebd3 | |||
| 8f3ea09732 | |||
| af626d98d3 | |||
| 34b0574e56 | |||
| 49d738809b | |||
| 2fa3a22eca | |||
| dc5eb6f92e | |||
| ba8902f0b1 | |||
| 11e9a77840 | |||
| 64cd7e74c5 | |||
| cac65d081e | |||
| e5ada994be | |||
| 6ba2459645 | |||
| 58f63aad08 | |||
| 8a8687a63c | |||
| f7f93c360d | |||
| c160e1f68e | |||
| 47c08ab8d2 | |||
| cd83ffbaa2 | |||
| e12981a821 | |||
| 47fd1bb894 | |||
| 20582b6353 | |||
| c8ff5387c0 | |||
| 7cb138d515 | |||
| 3dd4c45fab | |||
| 549539bec9 | |||
| e449cac464 | |||
| 62e458f39b | |||
| b37caaf9e5 | |||
| 7660ca89ae | |||
| 36b5cccd18 | |||
| 9decfa9c31 | |||
| 3aee2b6cf5 | |||
| 17d797cee4 | |||
| 75550eeea3 | |||
| 0d09c86c12 | |||
| 2db8f482db | |||
| 00d4b32a1b | |||
| 8a82bab1f3 | |||
| 237a25e6b0 | |||
| 5dc836671d | |||
| 26be1cb602 | |||
| dc7a48cbf9 | |||
| 52455be815 | |||
| bbbdcfb625 | |||
| b054a57e16 | |||
| fd73b3ad88 | |||
| 8c0637b556 | |||
| d92ffe8fc7 | |||
| e0eb3a4f13 | |||
| 1fb0060a73 | |||
| d7430bf516 | |||
| 35f039a119 | |||
| 79432a40d7 | |||
| 98cdb95bc0 | |||
| 02a55ce9db | |||
| 6f4720e1ea | |||
| 6a807b7799 | |||
| 8d4ef147d2 | |||
| 8ed6217d1c | |||
| 7dae4a976d | |||
| 3b83949ba3 | |||
| d8baadb991 | |||
| 7d3f9fa407 | |||
| 705d144a85 | |||
| ff05cff36c | |||
| 861fa7b145 | |||
| d65a0bba44 | |||
| dac1bd88dc | |||
| 288dbfa37c | |||
| a0e55cb9b1 | |||
| 86ba019ca0 | |||
| 3cb9c2ece5 | |||
| 1787c53d98 | |||
| 8ae762a8c8 | |||
| 63426c3cd0 | |||
| e184713c67 | |||
| 1926625297 | |||
| 63bb8e8cef | |||
| 583c5b48a0 | |||
| d08ccbce78 | |||
| 5a9702771c | |||
| eb91d9905b | |||
| 38cc85fa4c | |||
| 77e6ef218c | |||
| 464b6f2e93 | |||
| 20c90642f9 | |||
| 57e67ea8f7 | |||
| c9e9628383 | |||
| 909f07939e | |||
| a310493485 | |||
| 1e09df20b6 | |||
| 087481ac12 | |||
| c941e802d4 | |||
| 39589bd441 | |||
| 2e57325dde | |||
| 2072301d89 | |||
| b236fd3ac6 | |||
| b968695e31 | |||
| 694f1d1623 | |||
| 93e4d58606 | |||
| cc77caad67 | |||
| f74573f5d0 | |||
| deb6f0babc | |||
| cb978136bd | |||
| 1159450cc4 | |||
| a0613e4b10 | |||
| 68989f0a45 | |||
| 7da5e3697e | |||
| 6e7a0eb662 | |||
| b25ac855ca | |||
| 3e02dcbb95 | |||
| 53be119e39 | |||
| 25bdc4c9ed | |||
| 9d4055fc4d | |||
| d2edf359ac | |||
| aa1d92dfbb | |||
| b89d71e6e4 | |||
| ed493f9c3a | |||
| 76f8a5b7de | |||
| cb3bc207b9 | |||
| b5db5dd0b4 | |||
| 90a7cff2c9 | |||
| cc3adbe78c | |||
| bd6a7210b7 | |||
| 905a202873 | |||
| accedf0280 | |||
| 99d9a2eacd | |||
| ac4f131fa8 | |||
| 7f6f7e0e9f | |||
| 43bb26f28c | |||
| b29dc37991 | |||
| cf9f02adbb | |||
| b5a1a18b04 | |||
| b4eeb0ffae | |||
| 48549ead7f | |||
| 01b0ad0fd9 | |||
| 2b21501450 | |||
| b491f6af9b | |||
| 942ef7c254 | |||
| 1ee3bb42f3 | |||
| 25007b1963 | |||
| f442378377 | |||
| 333b7ebc0c | |||
| 5896766fc3 | |||
| 89540aec28 | |||
| b960143045 | |||
| 6ab45cf668 | |||
| fd206a7ef6 | |||
| 1c7347d38d | |||
| 7f58c422f2 | |||
| 0a0e2b5e93 | |||
| de00c424f4 | |||
| a249e2028d | |||
| 68036eeccf | |||
| cb0b0235f0 | |||
| 6ff6f7a28d | |||
| 0b628fb22d | |||
| b4bb11320f | |||
| c61938db23 | |||
| acf9d5480c | |||
| a1cb7fd778 | |||
| c24543fea0 | |||
| 100e8ab00d | |||
| 38497b04ac | |||
| 7bd27b920a | |||
| efab11720d | |||
| 121f0120f0 | |||
| 515b85bb2f | |||
| f27e41d19c | |||
| 603d451fc9 | |||
| 89adaabb64 | |||
| 987ca68ca6 | |||
| 71defbf2f9 | |||
| 5c35b42844 | |||
| 904b37c4be | |||
| 4e252f8243 | |||
| dc3e52a900 | |||
| 06ad5f6652 | |||
| c3b5474cbf | |||
| 69e3b830ed | |||
| 96a5891ce7 | |||
| 66b9245b28 | |||
| f38ec68695 | |||
| 996772a27d | |||
| 7f4e9c1ad4 | |||
| 218ba69501 | |||
| c2e5dfd933 | |||
| 3e40bbc603 | |||
| 3498d4b9c5 | |||
| f4b838cad8 | |||
| 86fa8634ee | |||
| 8882006700 | |||
| 40fdf99a55 | |||
| 0257736c64 | |||
| 2024cda560 | |||
| 03aaf4ad76 | |||
| 550b88861f | |||
| 02ae5fa007 | |||
| d81f105ed7 | |||
| d3ed225675 | |||
| efcca61f5a | |||
| 4dad0002cd | |||
| 9ffc83f0f6 | |||
| 981c7d5974 | |||
| 5da089ccd7 | |||
| 91e00f7d97 | |||
| 3a675fb541 | |||
| 9a5d8d2d22 | |||
| de812221ef | |||
| 340980bdd0 | |||
| f68a28fa2b | |||
| 7b7798e8c4 | |||
| b3ac94115e | |||
| b1a172cad9 | |||
| f2e21c68d0 | |||
| 8b784c0eb1 | |||
| bc59f32b96 | |||
| a4fa8a4fae | |||
| f730192c98 | |||
| f994501296 | |||
| 9c3e73606c | |||
| 5619e16b70 | |||
| d2e3867893 | |||
| 979f5475c3 | |||
| 5a10f2dd7c | |||
| d4dd1e37ce | |||
| a8dfa95126 | |||
| 4f109c1a94 | |||
| 28cec99699 | |||
| 3e194c7906 | |||
| f39005b72d | |||
| 3b183933e3 | |||
| 5425cca47e | |||
| db2759b7d1 | |||
| 3c3b9575a2 | |||
| 987cfd5dae | |||
| 1537fb39c0 | |||
| 65cbc478b8 | |||
| e2e8fbe313 | |||
| a3c5f785e9 | |||
| 7877215d59 | |||
| e4347792b8 | |||
| 50fde60899 | |||
| 38f5e293b0 | |||
| b6b399a590 | |||
| b83841d253 | |||
| 3e69304f0f | |||
| fe8131f743 | |||
| 9ef14a20d1 | |||
| 5897b97065 |
@@ -62,6 +62,16 @@ body:
|
|||||||
- nightly
|
- nightly
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: "Which architecture are you using?"
|
||||||
|
description: "#### `uname -m`"
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- x86
|
||||||
|
- ARM64 (aarch64)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: "Operating System:"
|
label: "Operating System:"
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ❓ Community-driven support
|
- name: ❓ Community-driven support (Free)
|
||||||
url: https://docs.mailcow.email/#get-support
|
url: https://docs.mailcow.email/#get-support
|
||||||
about: Please use the community forum for questions or assistance
|
about: Please use the community forum for questions or assistance
|
||||||
|
- name: 🔥 Premium Support (Paid)
|
||||||
|
url: https://www.servercow.de/mailcow?lang=en#support
|
||||||
|
about: Buy a support subscription for any critical issues and get assisted by the mailcow Team. See conditions!
|
||||||
- name: 🚨 Report a security vulnerability
|
- name: 🚨 Report a security vulnerability
|
||||||
url: https://www.servercow.de/anfrage?lang=en
|
url: "mailto:info@servercow.de?subject=mailcow: dockerized Security Vulnerability"
|
||||||
about: Please give us appropriate time to verify, respond and fix before disclosure.
|
about: Please give us appropriate time to verify, respond and fix before disclosure.
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Check if labeled support, if so send message and close issue
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
jobs:
|
||||||
|
add-comment:
|
||||||
|
if: github.event.label.name == 'support'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Add comment
|
||||||
|
run: gh issue comment "$NUMBER" --body "$BODY"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.SUPPORTISSUES_ACTION_PAT }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
NUMBER: ${{ github.event.issue.number }}
|
||||||
|
BODY: |
|
||||||
|
**THIS IS A AUTOMATED MESSAGE!**
|
||||||
|
|
||||||
|
It seems your issue is not a bug.
|
||||||
|
Therefore we highly advise you to get support!
|
||||||
|
|
||||||
|
You can get support either by:
|
||||||
|
- ordering a paid [support contract at Servercow](https://www.servercow.de/mailcow?lang=en#support/) (Directly from the developers) or
|
||||||
|
- using the [community forum](https://community.mailcow.email) (**Based on volunteers! NO guaranteed answer**) or
|
||||||
|
- using the [Telegram support channel](https://t.me/mailcow) (**Based on volunteers! NO guaranteed answer**)
|
||||||
|
|
||||||
|
This issue will be closed. If you think your reported issue is not a support case feel free to comment above and if so the issue will reopened.
|
||||||
|
|
||||||
|
- name: Close issue
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.SUPPORTISSUES_ACTION_PAT }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
NUMBER: ${{ github.event.issue.number }}
|
||||||
|
run: gh issue close "$NUMBER" -r "not planned"
|
||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
|
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
|
||||||
steps:
|
steps:
|
||||||
- name: Send message
|
- name: Send message
|
||||||
uses: thollander/actions-comment-pull-request@v2.4.3
|
uses: thollander/actions-comment-pull-request@v2.5.0
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
||||||
message: |
|
message: |
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||||
uses: actions/stale@v8.0.0
|
uses: actions/stale@v9.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
|
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
bash helper-scripts/update_postscreen_whitelist.sh
|
bash helper-scripts/update_postscreen_whitelist.sh
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v5
|
uses: peter-evans/create-pull-request@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||||
commit-message: update postscreen_access.cidr
|
commit-message: update postscreen_access.cidr
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ data/conf/dovecot/acl_anyone
|
|||||||
data/conf/dovecot/dovecot-master.passwd
|
data/conf/dovecot/dovecot-master.passwd
|
||||||
data/conf/dovecot/dovecot-master.userdb
|
data/conf/dovecot/dovecot-master.userdb
|
||||||
data/conf/dovecot/extra.conf
|
data/conf/dovecot/extra.conf
|
||||||
|
data/conf/dovecot/mail_replica.conf
|
||||||
data/conf/dovecot/global_sieve_*
|
data/conf/dovecot/global_sieve_*
|
||||||
data/conf/dovecot/last_login
|
data/conf/dovecot/last_login
|
||||||
data/conf/dovecot/lua
|
data/conf/dovecot/lua
|
||||||
|
|||||||
+31
-1
@@ -1,5 +1,33 @@
|
|||||||
When a problem occurs, then always for a reason! What you want to do in such a case is:
|
# Contribution Guidelines (Last modified on 18th December 2023)
|
||||||
|
|
||||||
|
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
|
||||||
|
|
||||||
|
## Pull Requests (Last modified on 18th December 2023)
|
||||||
|
|
||||||
|
However, please note the following regarding pull requests:
|
||||||
|
|
||||||
|
1. **ALWAYS** create your PR using the staging branch of your locally cloned mailcow instance, as the pull request will end up in said staging branch of mailcow once approved. Ideally, you should simply create a new branch for your pull request that is named after the type of your PR (e.g. `feat/` for function updates or `fix/` for bug fixes) and the actual content (e.g. `sogo-6.0.0` for an update from SOGo to version 6 or `html-escape` for a fix that includes escaping HTML in mailcow).
|
||||||
|
2. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case.
|
||||||
|
3. **Test your changes before you commit them as a pull request.** <ins>If possible</ins>, write a small **test log** or demonstrate the functionality with a **screenshot or GIF**. *We will of course also test your pull request ourselves, but proof from you will save us the question of whether you have tested your own changes yourself.*
|
||||||
|
4. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
|
||||||
|
5. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
|
||||||
|
6. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Reporting (Last modified on 18th December 2023)
|
||||||
|
|
||||||
|
If you plan to report a issue within mailcow please read and understand the following rules:
|
||||||
|
|
||||||
|
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
|
||||||
|
2. **ONLY** report an error if you have the **necessary know-how (at least the basics)** for the administration of an e-mail server and the usage of Docker. mailcow is a complex and fully-fledged e-mail server including groupware components on a Docker basement and it requires a bit of technical know-how for debugging and operating.
|
||||||
|
3. **ONLY** report bugs that are contained in the latest mailcow release series. *The definition of the latest release series includes the last major patch (e.g. 2023-12) and all minor patches (revisions) below it (e.g. 2023-12a, b, c etc.).* New issue reports published starting from January 1, 2024 must meet this criterion, as versions below the latest releases are no longer supported by us.
|
||||||
|
4. When reporting a problem, please be as detailed as possible and include even the smallest changes to your mailcow installation. Simply fill out the corresponding bug report form in detail and accurately to minimize possible questions.
|
||||||
|
5. **Before you open an issue/feature request**, please first check whether a similar request already exists in the mailcow tracker on GitHub. If so, please include yourself in this request.
|
||||||
|
6. When you create a issue/feature request: Please note that the creation does <ins>**not guarantee an instant implementation or fix by the mailcow team or the community**</ins>.
|
||||||
|
7. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it.
|
||||||
|
|
||||||
|
### Quick guide to reporting problems:
|
||||||
1. Read your logs; follow them to see what the reason for your problem is.
|
1. Read your logs; follow them to see what the reason for your problem is.
|
||||||
2. Follow the leads given to you in your logfiles and start investigating.
|
2. Follow the leads given to you in your logfiles and start investigating.
|
||||||
3. Restarting the troubled service or the whole stack to see if the problem persists.
|
3. Restarting the troubled service or the whole stack to see if the problem persists.
|
||||||
@@ -7,3 +35,5 @@ When a problem occurs, then always for a reason! What you want to do in such a c
|
|||||||
5. Search our [issues](https://github.com/mailcow/mailcow-dockerized/issues) for your problem.
|
5. Search our [issues](https://github.com/mailcow/mailcow-dockerized/issues) for your problem.
|
||||||
6. [Create an issue](https://github.com/mailcow/mailcow-dockerized/issues/new/choose) over at our GitHub repository if you think your problem might be a bug or a missing feature you badly need. But please make sure, that you include **all the logs** and a full description to your problem.
|
6. [Create an issue](https://github.com/mailcow/mailcow-dockerized/issues/new/choose) over at our GitHub repository if you think your problem might be a bug or a missing feature you badly need. But please make sure, that you include **all the logs** and a full description to your problem.
|
||||||
7. Ask your questions in our community-driven [support channels](https://docs.mailcow.email/#community-support-and-chat).
|
7. Ask your questions in our community-driven [support channels](https://docs.mailcow.email/#community-support-and-chat).
|
||||||
|
|
||||||
|
## When creating an issue/feature request or a pull request, you will be asked to confirm these guidelines.
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
[](https://translate.mailcow.email/engage/mailcow-dockerized/)
|
[](https://translate.mailcow.email/engage/mailcow-dockerized/)
|
||||||
[](https://twitter.com/mailcow_email)
|
[](https://twitter.com/mailcow_email)
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Want to support mailcow?
|
## Want to support mailcow?
|
||||||
|
|
||||||
@@ -27,6 +29,8 @@ Please see [the official documentation](https://docs.mailcow.email/) for install
|
|||||||
|
|
||||||
[Official 𝕏 (Twitter) Account](https://twitter.com/mailcow_email)
|
[Official 𝕏 (Twitter) Account](https://twitter.com/mailcow_email)
|
||||||
|
|
||||||
|
[Official Mastodon Account](https://mailcow.social/@doncow)
|
||||||
|
|
||||||
Telegram desktop clients are available for [multiple platforms](https://desktop.telegram.org). You can search the groups history for keywords.
|
Telegram desktop clients are available for [multiple platforms](https://desktop.telegram.org). You can search the groups history for keywords.
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.20
|
||||||
|
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|
||||||
RUN apk upgrade --no-cache \
|
RUN apk upgrade --no-cache \
|
||||||
&& apk add --update --no-cache \
|
&& apk add --update --no-cache \
|
||||||
bash \
|
bash \
|
||||||
@@ -14,9 +15,7 @@ RUN apk upgrade --no-cache \
|
|||||||
tini \
|
tini \
|
||||||
tzdata \
|
tzdata \
|
||||||
python3 \
|
python3 \
|
||||||
py3-pip \
|
acme-tiny --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
|
||||||
&& pip3 install --upgrade pip \
|
|
||||||
&& pip3 install acme-tiny
|
|
||||||
|
|
||||||
COPY acme.sh /srv/acme.sh
|
COPY acme.sh /srv/acme.sh
|
||||||
COPY functions.sh /srv/functions.sh
|
COPY functions.sh /srv/functions.sh
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ if [[ "${ONLY_MAILCOW_HOSTNAME}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
|||||||
ONLY_MAILCOW_HOSTNAME=y
|
ONLY_MAILCOW_HOSTNAME=y
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${AUTODISCOVER_SAN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
AUTODISCOVER_SAN=y
|
||||||
|
fi
|
||||||
|
|
||||||
# Request individual certificate for every domain
|
# Request individual certificate for every domain
|
||||||
if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
ENABLE_SSL_SNI=y
|
ENABLE_SSL_SNI=y
|
||||||
@@ -211,7 +215,11 @@ while true; do
|
|||||||
ADDITIONAL_SAN_ARR+=($i)
|
ADDITIONAL_SAN_ARR+=($i)
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||||
|
# Fetch certs for autoconfig and autodiscover subdomains
|
||||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
||||||
# Start IP detection
|
# Start IP detection
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
FROM debian:bullseye-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
RUN apt update && apt install pigz
|
RUN apt update && apt install pigz
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
FROM clamav/clamav:1.0.3_base
|
FROM alpine:3.20
|
||||||
|
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
RUN apk upgrade --no-cache \
|
RUN apk upgrade --no-cache \
|
||||||
&& apk add --update --no-cache \
|
&& apk add --update --no-cache \
|
||||||
rsync \
|
rsync \
|
||||||
|
clamav \
|
||||||
bind-tools \
|
bind-tools \
|
||||||
bash
|
bash \
|
||||||
|
tini
|
||||||
|
|
||||||
# init
|
# init
|
||||||
COPY clamd.sh /clamd.sh
|
COPY clamd.sh /clamd.sh
|
||||||
@@ -14,7 +16,9 @@ RUN chmod +x /sbin/tini
|
|||||||
|
|
||||||
# healthcheck
|
# healthcheck
|
||||||
COPY healthcheck.sh /healthcheck.sh
|
COPY healthcheck.sh /healthcheck.sh
|
||||||
|
COPY clamdcheck.sh /usr/local/bin
|
||||||
RUN chmod +x /healthcheck.sh
|
RUN chmod +x /healthcheck.sh
|
||||||
|
RUN chmod +x /usr/local/bin/clamdcheck.sh
|
||||||
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
|
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
|
||||||
|
|
||||||
ENTRYPOINT []
|
ENTRYPOINT []
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "${CLAMAV_NO_CLAMD:-}" != "false" ]; then
|
||||||
|
if [ "$(echo "PING" | nc localhost 3310)" != "PONG" ]; then
|
||||||
|
echo "ERROR: Unable to contact server"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Clamd is up"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.20
|
||||||
|
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --update --no-cache python3 \
|
RUN apk add --update --no-cache python3 \
|
||||||
@@ -9,12 +10,13 @@ RUN apk add --update --no-cache python3 \
|
|||||||
openssl \
|
openssl \
|
||||||
tzdata \
|
tzdata \
|
||||||
py3-psutil \
|
py3-psutil \
|
||||||
|
py3-redis \
|
||||||
|
py3-async-timeout \
|
||||||
&& pip3 install --upgrade pip \
|
&& pip3 install --upgrade pip \
|
||||||
fastapi \
|
fastapi \
|
||||||
uvicorn \
|
uvicorn \
|
||||||
aiodocker \
|
aiodocker \
|
||||||
docker \
|
docker
|
||||||
aioredis
|
|
||||||
RUN mkdir /app/modules
|
RUN mkdir /app/modules
|
||||||
|
|
||||||
COPY docker-entrypoint.sh /app/
|
COPY docker-entrypoint.sh /app/
|
||||||
|
|||||||
@@ -5,16 +5,63 @@ import json
|
|||||||
import uuid
|
import uuid
|
||||||
import async_timeout
|
import async_timeout
|
||||||
import asyncio
|
import asyncio
|
||||||
import aioredis
|
|
||||||
import aiodocker
|
import aiodocker
|
||||||
import docker
|
import docker
|
||||||
import logging
|
import logging
|
||||||
from logging.config import dictConfig
|
from logging.config import dictConfig
|
||||||
from fastapi import FastAPI, Response, Request
|
from fastapi import FastAPI, Response, Request
|
||||||
from modules.DockerApi import DockerApi
|
from modules.DockerApi import DockerApi
|
||||||
|
from redis import asyncio as aioredis
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
dockerapi = None
|
dockerapi = None
|
||||||
app = FastAPI()
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
# Initialize a custom logger
|
||||||
|
logger = logging.getLogger("dockerapi")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
# Configure the logger to output logs to the terminal
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setLevel(logging.INFO)
|
||||||
|
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
logger.info("Init APP")
|
||||||
|
|
||||||
|
# Init redis client
|
||||||
|
if os.environ['REDIS_SLAVEOF_IP'] != "":
|
||||||
|
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
|
||||||
|
else:
|
||||||
|
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
|
||||||
|
|
||||||
|
# Init docker clients
|
||||||
|
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
||||||
|
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
|
||||||
|
|
||||||
|
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
|
||||||
|
|
||||||
|
logger.info("Subscribe to redis channel")
|
||||||
|
# Subscribe to redis channel
|
||||||
|
dockerapi.pubsub = redis.pubsub()
|
||||||
|
await dockerapi.pubsub.subscribe("MC_CHANNEL")
|
||||||
|
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
|
||||||
|
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Close docker connections
|
||||||
|
dockerapi.sync_docker_client.close()
|
||||||
|
await dockerapi.async_docker_client.close()
|
||||||
|
|
||||||
|
# Close redis
|
||||||
|
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
|
||||||
|
await dockerapi.redis_client.close()
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
# Define Routes
|
# Define Routes
|
||||||
@app.get("/host/stats")
|
@app.get("/host/stats")
|
||||||
@@ -144,53 +191,7 @@ async def post_container_update_stats(container_id : str):
|
|||||||
|
|
||||||
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
|
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
|
||||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||||
|
|
||||||
# Events
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup_event():
|
|
||||||
global dockerapi
|
|
||||||
|
|
||||||
# Initialize a custom logger
|
|
||||||
logger = logging.getLogger("dockerapi")
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
# Configure the logger to output logs to the terminal
|
|
||||||
handler = logging.StreamHandler()
|
|
||||||
handler.setLevel(logging.INFO)
|
|
||||||
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
logger.info("Init APP")
|
|
||||||
|
|
||||||
# Init redis client
|
|
||||||
if os.environ['REDIS_SLAVEOF_IP'] != "":
|
|
||||||
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
|
|
||||||
else:
|
|
||||||
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
|
|
||||||
|
|
||||||
# Init docker clients
|
|
||||||
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
|
||||||
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
|
|
||||||
|
|
||||||
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
|
|
||||||
|
|
||||||
logger.info("Subscribe to redis channel")
|
|
||||||
# Subscribe to redis channel
|
|
||||||
dockerapi.pubsub = redis.pubsub()
|
|
||||||
await dockerapi.pubsub.subscribe("MC_CHANNEL")
|
|
||||||
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
|
||||||
async def shutdown_event():
|
|
||||||
global dockerapi
|
|
||||||
|
|
||||||
# Close docker connections
|
|
||||||
dockerapi.sync_docker_client.close()
|
|
||||||
await dockerapi.async_docker_client.close()
|
|
||||||
|
|
||||||
# Close redis
|
|
||||||
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
|
|
||||||
await dockerapi.redis_client.close()
|
|
||||||
|
|
||||||
# PubSub Handler
|
# PubSub Handler
|
||||||
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
||||||
|
|||||||
@@ -358,8 +358,8 @@ class DockerApi:
|
|||||||
for line in cmd_response.split("\n"):
|
for line in cmd_response.split("\n"):
|
||||||
if '$2$' in line:
|
if '$2$' in line:
|
||||||
hash = line.strip()
|
hash = line.strip()
|
||||||
hash_out = re.search('\$2\$.+$', hash).group(0)
|
hash_out = re.search(r'\$2\$.+$', hash).group(0)
|
||||||
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
|
rspamd_passphrase_hash = re.sub(r'[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
|
||||||
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
|
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
|
||||||
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
|
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
|
||||||
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||||
|
|||||||
@@ -1,119 +1,115 @@
|
|||||||
FROM debian:bullseye-slim
|
FROM alpine:3.20
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced extractVersion=(?<version>.*)$
|
|
||||||
ARG DOVECOT=2.3.21
|
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
|
|
||||||
ARG GOSU_VERSION=1.16
|
ARG GOSU_VERSION=1.16
|
||||||
ENV LC_ALL C
|
|
||||||
|
|
||||||
|
ENV LANG C.UTF-8
|
||||||
|
ENV LC_ALL C.UTF-8
|
||||||
|
|
||||||
# Add groups and users before installing Dovecot to not break compatibility
|
# Add groups and users before installing Dovecot to not break compatibility
|
||||||
RUN groupadd -g 5000 vmail \
|
RUN addgroup -g 5000 vmail \
|
||||||
&& groupadd -g 401 dovecot \
|
&& addgroup -g 401 dovecot \
|
||||||
&& groupadd -g 402 dovenull \
|
&& addgroup -g 402 dovenull \
|
||||||
&& groupadd -g 999 sogo \
|
&& sed -i "s/999/99/" /etc/group \
|
||||||
&& usermod -a -G sogo nobody \
|
&& addgroup -g 999 sogo \
|
||||||
&& useradd -g vmail -u 5000 vmail -d /var/vmail \
|
&& addgroup nobody sogo \
|
||||||
&& useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
|
&& adduser -D -u 5000 -G vmail -h /var/vmail vmail \
|
||||||
&& useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \
|
&& adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \
|
||||||
&& touch /etc/default/locale \
|
&& adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \
|
||||||
&& apt-get update \
|
&& apk add --no-cache --update \
|
||||||
&& apt-get -y --no-install-recommends install \
|
bash \
|
||||||
build-essential \
|
bind-tools \
|
||||||
apt-transport-https \
|
findutils \
|
||||||
|
envsubst \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
cpanminus \
|
|
||||||
curl \
|
curl \
|
||||||
dnsutils \
|
coreutils \
|
||||||
dirmngr \
|
|
||||||
gettext \
|
|
||||||
gnupg2 \
|
|
||||||
jq \
|
jq \
|
||||||
libauthen-ntlm-perl \
|
lua \
|
||||||
libcgi-pm-perl \
|
lua-cjson \
|
||||||
libcrypt-openssl-rsa-perl \
|
|
||||||
libcrypt-ssleay-perl \
|
|
||||||
libdata-uniqid-perl \
|
|
||||||
libdbd-mysql-perl \
|
|
||||||
libdbi-perl \
|
|
||||||
libdigest-hmac-perl \
|
|
||||||
libdist-checkconflicts-perl \
|
|
||||||
libencode-imaputf7-perl \
|
|
||||||
libfile-copy-recursive-perl \
|
|
||||||
libfile-tail-perl \
|
|
||||||
libhtml-parser-perl \
|
|
||||||
libio-compress-perl \
|
|
||||||
libio-socket-inet6-perl \
|
|
||||||
libio-socket-ssl-perl \
|
|
||||||
libio-tee-perl \
|
|
||||||
libipc-run-perl \
|
|
||||||
libjson-webtoken-perl \
|
|
||||||
liblockfile-simple-perl \
|
|
||||||
libmail-imapclient-perl \
|
|
||||||
libmodule-implementation-perl \
|
|
||||||
libmodule-scandeps-perl \
|
|
||||||
libnet-ssleay-perl \
|
|
||||||
libpackage-stash-perl \
|
|
||||||
libpackage-stash-xs-perl \
|
|
||||||
libpar-packer-perl \
|
|
||||||
libparse-recdescent-perl \
|
|
||||||
libproc-processtable-perl \
|
|
||||||
libreadonly-perl \
|
|
||||||
libregexp-common-perl \
|
|
||||||
libssl-dev \
|
|
||||||
libsys-meminfo-perl \
|
|
||||||
libterm-readkey-perl \
|
|
||||||
libtest-deep-perl \
|
|
||||||
libtest-fatal-perl \
|
|
||||||
libtest-mock-guard-perl \
|
|
||||||
libtest-mockobject-perl \
|
|
||||||
libtest-nowarnings-perl \
|
|
||||||
libtest-pod-perl \
|
|
||||||
libtest-requires-perl \
|
|
||||||
libtest-simple-perl \
|
|
||||||
libtest-warn-perl \
|
|
||||||
libtry-tiny-perl \
|
|
||||||
libunicode-string-perl \
|
|
||||||
liburi-perl \
|
|
||||||
libwww-perl \
|
|
||||||
lua-sql-mysql \
|
|
||||||
lua-socket \
|
lua-socket \
|
||||||
|
lua-sql-mysql \
|
||||||
|
lua5.3-sql-mysql \
|
||||||
|
icu-data-full \
|
||||||
|
mariadb-connector-c \
|
||||||
|
gcompat \
|
||||||
mariadb-client \
|
mariadb-client \
|
||||||
|
perl \
|
||||||
|
perl-ntlm \
|
||||||
|
perl-cgi \
|
||||||
|
perl-crypt-openssl-rsa \
|
||||||
|
perl-utils \
|
||||||
|
perl-crypt-ssleay \
|
||||||
|
perl-data-uniqid \
|
||||||
|
perl-dbd-mysql \
|
||||||
|
perl-dbi \
|
||||||
|
perl-digest-hmac \
|
||||||
|
perl-dist-checkconflicts \
|
||||||
|
perl-encode-imaputf7 \
|
||||||
|
perl-file-copy-recursive \
|
||||||
|
perl-file-tail \
|
||||||
|
perl-io-socket-inet6 \
|
||||||
|
perl-io-gzip \
|
||||||
|
perl-io-socket-ssl \
|
||||||
|
perl-io-tee \
|
||||||
|
perl-ipc-run \
|
||||||
|
perl-json-webtoken \
|
||||||
|
perl-mail-imapclient \
|
||||||
|
perl-module-implementation \
|
||||||
|
perl-module-scandeps \
|
||||||
|
perl-net-ssleay \
|
||||||
|
perl-package-stash \
|
||||||
|
perl-package-stash-xs \
|
||||||
|
perl-par-packer \
|
||||||
|
perl-parse-recdescent \
|
||||||
|
perl-lockfile-simple \
|
||||||
|
libproc \
|
||||||
|
perl-readonly \
|
||||||
|
perl-regexp-common \
|
||||||
|
perl-sys-meminfo \
|
||||||
|
perl-term-readkey \
|
||||||
|
perl-test-deep \
|
||||||
|
perl-test-fatal \
|
||||||
|
perl-test-mockobject \
|
||||||
|
perl-test-mock-guard \
|
||||||
|
perl-test-pod \
|
||||||
|
perl-test-requires \
|
||||||
|
perl-test-simple \
|
||||||
|
perl-test-warn \
|
||||||
|
perl-try-tiny \
|
||||||
|
perl-unicode-string \
|
||||||
|
perl-proc-processtable \
|
||||||
|
perl-app-cpanminus \
|
||||||
procps \
|
procps \
|
||||||
python3-pip \
|
python3 \
|
||||||
redis-server \
|
py3-mysqlclient \
|
||||||
supervisor \
|
py3-html2text \
|
||||||
|
py3-jinja2 \
|
||||||
|
py3-redis \
|
||||||
|
redis \
|
||||||
syslog-ng \
|
syslog-ng \
|
||||||
syslog-ng-core \
|
syslog-ng-redis \
|
||||||
syslog-ng-mod-redis \
|
syslog-ng-json \
|
||||||
|
supervisor \
|
||||||
|
tzdata \
|
||||||
wget \
|
wget \
|
||||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
dovecot \
|
||||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
dovecot-dev \
|
||||||
&& chmod +x /usr/local/bin/gosu \
|
|
||||||
&& gosu nobody true \
|
|
||||||
&& apt-key adv --fetch-keys https://repo.dovecot.org/DOVECOT-REPO-GPG \
|
|
||||||
&& echo "deb https://repo.dovecot.org/ce-${DOVECOT}/debian/bullseye bullseye main" > /etc/apt/sources.list.d/dovecot.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get -y --no-install-recommends install \
|
|
||||||
dovecot-lua \
|
|
||||||
dovecot-managesieved \
|
|
||||||
dovecot-sieve \
|
|
||||||
dovecot-lmtpd \
|
dovecot-lmtpd \
|
||||||
|
dovecot-lua \
|
||||||
dovecot-ldap \
|
dovecot-ldap \
|
||||||
dovecot-mysql \
|
dovecot-mysql \
|
||||||
dovecot-core \
|
dovecot-sql \
|
||||||
|
dovecot-submissiond \
|
||||||
|
dovecot-pigeonhole-plugin \
|
||||||
dovecot-pop3d \
|
dovecot-pop3d \
|
||||||
dovecot-imapd \
|
dovecot-fts-solr \
|
||||||
dovecot-solr \
|
dovecot-fts-flatcurve \
|
||||||
&& pip3 install mysql-connector-python html2text jinja2 redis \
|
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
|
||||||
&& apt-get autoremove --purge -y \
|
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
|
||||||
&& apt-get autoclean \
|
&& chmod +x /usr/local/bin/gosu \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& gosu nobody true
|
||||||
&& rm -rf /tmp/* /var/tmp/* /root/.cache/
|
|
||||||
# imapsync dependencies
|
|
||||||
RUN cpan Crypt::OpenSSL::PKCS12
|
|
||||||
|
|
||||||
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
|
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
|
||||||
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
|
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
|
||||||
@@ -133,6 +129,7 @@ COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
|||||||
COPY quarantine_notify.py /usr/local/bin/quarantine_notify.py
|
COPY quarantine_notify.py /usr/local/bin/quarantine_notify.py
|
||||||
COPY quota_notify.py /usr/local/bin/quota_notify.py
|
COPY quota_notify.py /usr/local/bin/quota_notify.py
|
||||||
COPY repl_health.sh /usr/local/bin/repl_health.sh
|
COPY repl_health.sh /usr/local/bin/repl_health.sh
|
||||||
|
COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh
|
||||||
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
|
|||||||
# Create missing directories
|
# Create missing directories
|
||||||
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
|
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
|
||||||
[[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/
|
[[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/
|
||||||
|
[[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/
|
||||||
[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
|
[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
|
||||||
[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
|
[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
|
||||||
[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
|
[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
|
||||||
@@ -109,7 +110,14 @@ EOF
|
|||||||
|
|
||||||
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
|
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
|
||||||
|
|
||||||
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY]) ]]; then
|
||||||
|
echo -e "\e[33mActivating Flatcurve as FTS Backend...\e[0m"
|
||||||
|
echo -e "\e[33mDepending on your previous setup a full reindex might be needed... \e[0m"
|
||||||
|
echo -e "\e[34mVisit https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-fts/#fts-related-dovecot-commands to learn how to reindex\e[0m"
|
||||||
|
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins
|
||||||
|
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
|
||||||
|
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
||||||
|
elif [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication' > /etc/dovecot/mail_plugins
|
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication' > /etc/dovecot/mail_plugins
|
||||||
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
|
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
|
||||||
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
||||||
@@ -239,6 +247,47 @@ function script_deinit()
|
|||||||
end
|
end
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Temporarily set FTS depending on user choice inside mailcow.conf. Will be removed as soon as Solr is dropped
|
||||||
|
if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])$ ]]; then
|
||||||
|
cat <<EOF > /etc/dovecot/conf.d/fts.conf
|
||||||
|
# Autogenerated by mailcow
|
||||||
|
plugin {
|
||||||
|
fts_autoindex = yes
|
||||||
|
fts_autoindex_exclude = \Junk
|
||||||
|
fts_autoindex_exclude2 = \Trash
|
||||||
|
fts = flatcurve
|
||||||
|
|
||||||
|
# These are not flatcurve settings, but required for Dovecot FTS. See
|
||||||
|
# Dovecot FTS Configuration link above for further information.
|
||||||
|
fts_languages = en es de
|
||||||
|
fts_tokenizer_generic = algorithm=simple
|
||||||
|
fts_tokenizers = generic email-address
|
||||||
|
|
||||||
|
# OPTIONAL: Recommended default FTS core configuration
|
||||||
|
fts_filters = normalizer-icu snowball stopwords
|
||||||
|
fts_filters_en = lowercase snowball english-possessive stopwords
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
elif [[ ! "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])$ ]]; then
|
||||||
|
cat <<EOF > /etc/dovecot/conf.d/fts.conf
|
||||||
|
# Autogenerated by mailcow
|
||||||
|
plugin {
|
||||||
|
fts = solr
|
||||||
|
fts_autoindex = yes
|
||||||
|
fts_autoindex_exclude = \Junk
|
||||||
|
fts_autoindex_exclude2 = \Trash
|
||||||
|
fts_solr = url=http://solr:8983/solr/dovecot-fts/
|
||||||
|
|
||||||
|
fts_tokenizers = generic email-address
|
||||||
|
fts_tokenizer_generic = algorithm=simple
|
||||||
|
|
||||||
|
fts_filters = normalizer-icu snowball stopwords
|
||||||
|
fts_filters_en = lowercase snowball english-possessive stopwords
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
# Replace patterns in app-passdb.lua
|
# Replace patterns in app-passdb.lua
|
||||||
sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
|
sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||||
sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
|
sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||||
@@ -335,6 +384,14 @@ sys.exit()
|
|||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Set mail_replica for HA setups
|
||||||
|
if [[ -n ${MAILCOW_REPLICA_IP} && -n ${DOVEADM_REPLICA_PORT} ]]; then
|
||||||
|
cat <<EOF > /etc/dovecot/mail_replica.conf
|
||||||
|
# Autogenerated by mailcow
|
||||||
|
mail_replica = tcp:${MAILCOW_REPLICA_IP}:${DOVEADM_REPLICA_PORT}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
# 401 is user dovecot
|
# 401 is user dovecot
|
||||||
if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
|
if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
|
||||||
openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
|
openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
|
||||||
@@ -378,7 +435,8 @@ chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
|
|||||||
/usr/local/bin/maildir_gc.sh \
|
/usr/local/bin/maildir_gc.sh \
|
||||||
/usr/local/sbin/stop-supervisor.sh \
|
/usr/local/sbin/stop-supervisor.sh \
|
||||||
/usr/local/bin/quota_notify.py \
|
/usr/local/bin/quota_notify.py \
|
||||||
/usr/local/bin/repl_health.sh
|
/usr/local/bin/repl_health.sh \
|
||||||
|
/usr/local/bin/optimize-fts.sh
|
||||||
|
|
||||||
# Prepare environment file for cronjobs
|
# Prepare environment file for cronjobs
|
||||||
printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
|
printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
|
||||||
@@ -432,4 +490,8 @@ done
|
|||||||
# May be related to something inside Docker, I seriously don't know
|
# May be related to something inside Docker, I seriously don't know
|
||||||
touch /etc/dovecot/lua/passwd-verify.lua
|
touch /etc/dovecot/lua/passwd-verify.lua
|
||||||
|
|
||||||
|
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||||
|
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||||
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ && ! "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
doveadm fts optimize -A
|
||||||
|
fi
|
||||||
@@ -3,11 +3,10 @@
|
|||||||
import smtplib
|
import smtplib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import mysql.connector
|
import MySQLdb
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.utils import COMMASPACE, formatdate
|
from email.utils import COMMASPACE, formatdate
|
||||||
import cgi
|
|
||||||
import jinja2
|
import jinja2
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
import json
|
import json
|
||||||
@@ -50,7 +49,7 @@ try:
|
|||||||
def query_mysql(query, headers = True, update = False):
|
def query_mysql(query, headers = True, update = False):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
|
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print('%s - trying again...' % (ex))
|
print('%s - trying again...' % (ex))
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ try:
|
|||||||
msg.attach(text_part)
|
msg.attach(text_part)
|
||||||
msg.attach(html_part)
|
msg.attach(html_part)
|
||||||
msg['To'] = username
|
msg['To'] = username
|
||||||
p = Popen(['/usr/lib/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
|
p = Popen(['/usr/libexec/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
|
||||||
p.communicate(input=bytes(msg.as_string(), 'utf-8'))
|
p.communicate(input=bytes(msg.as_string(), 'utf-8'))
|
||||||
|
|
||||||
domain = username.split("@")[-1]
|
domain = username.split("@")[-1]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ fi
|
|||||||
|
|
||||||
# Is replication active?
|
# Is replication active?
|
||||||
# grep on file is less expensive than doveconf
|
# grep on file is less expensive than doveconf
|
||||||
if ! grep -qi mail_replica /etc/dovecot/dovecot.conf; then
|
if [ -n ${MAILCOW_REPLICA_IP} ]; then
|
||||||
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
|
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Deploy
|
# Deploy
|
||||||
curl --connect-timeout 15 --retry 10 --max-time 30 http://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"' | tr -dc '0-9').tar.gz --output /tmp/sa-rules-heinlein.tar.gz
|
curl --connect-timeout 15 --retry 10 --max-time 30 https://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"' | tr -dc '0-9').tar.gz --output /tmp/sa-rules-heinlein.tar.gz
|
||||||
if gzip -t /tmp/sa-rules-heinlein.tar.gz; then
|
if gzip -t /tmp/sa-rules-heinlein.tar.gz; then
|
||||||
tar xfvz /tmp/sa-rules-heinlein.tar.gz -C /tmp/sa-rules-heinlein
|
tar xfvz /tmp/sa-rules-heinlein.tar.gz -C /tmp/sa-rules-heinlein
|
||||||
cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules
|
cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ autostart=true
|
|||||||
|
|
||||||
[program:dovecot]
|
[program:dovecot]
|
||||||
command=/usr/sbin/dovecot -F
|
command=/usr/sbin/dovecot -F
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
autorestart=true
|
autorestart=true
|
||||||
|
|
||||||
[eventlistener:processes]
|
[eventlistener:processes]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.28
|
@version: 4.5
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
@@ -6,11 +6,12 @@ options {
|
|||||||
use_dns(no);
|
use_dns(no);
|
||||||
use_fqdn(no);
|
use_fqdn(no);
|
||||||
owner("root"); group("adm"); perm(0640);
|
owner("root"); group("adm"); perm(0640);
|
||||||
stats_freq(0);
|
stats(freq(0));
|
||||||
|
keep_timestamp(no);
|
||||||
bad_hostname("^gconfd$");
|
bad_hostname("^gconfd$");
|
||||||
};
|
};
|
||||||
source s_src {
|
source s_dgram {
|
||||||
unix-stream("/dev/log");
|
unix-dgram("/dev/log");
|
||||||
internal();
|
internal();
|
||||||
};
|
};
|
||||||
destination d_stdout { pipe("/dev/stdout"); };
|
destination d_stdout { pipe("/dev/stdout"); };
|
||||||
@@ -36,7 +37,7 @@ filter f_replica {
|
|||||||
not match("Error: sync: Unknown user in remote" value("MESSAGE"));
|
not match("Error: sync: Unknown user in remote" value("MESSAGE"));
|
||||||
};
|
};
|
||||||
log {
|
log {
|
||||||
source(s_src);
|
source(s_dgram);
|
||||||
filter(f_replica);
|
filter(f_replica);
|
||||||
destination(d_stdout);
|
destination(d_stdout);
|
||||||
filter(f_mail);
|
filter(f_mail);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.28
|
@version: 4.5
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
@@ -6,11 +6,12 @@ options {
|
|||||||
use_dns(no);
|
use_dns(no);
|
||||||
use_fqdn(no);
|
use_fqdn(no);
|
||||||
owner("root"); group("adm"); perm(0640);
|
owner("root"); group("adm"); perm(0640);
|
||||||
stats_freq(0);
|
stats(freq(0));
|
||||||
|
keep_timestamp(no);
|
||||||
bad_hostname("^gconfd$");
|
bad_hostname("^gconfd$");
|
||||||
};
|
};
|
||||||
source s_src {
|
source s_dgram {
|
||||||
unix-stream("/dev/log");
|
unix-dgram("/dev/log");
|
||||||
internal();
|
internal();
|
||||||
};
|
};
|
||||||
destination d_stdout { pipe("/dev/stdout"); };
|
destination d_stdout { pipe("/dev/stdout"); };
|
||||||
@@ -36,7 +37,7 @@ filter f_replica {
|
|||||||
not match("Error: sync: Unknown user in remote" value("MESSAGE"));
|
not match("Error: sync: Unknown user in remote" value("MESSAGE"));
|
||||||
};
|
};
|
||||||
log {
|
log {
|
||||||
source(s_src);
|
source(s_dgram);
|
||||||
filter(f_replica);
|
filter(f_replica);
|
||||||
destination(d_stdout);
|
destination(d_stdout);
|
||||||
filter(f_mail);
|
filter(f_mail);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.20
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
ENV XTABLES_LIBDIR /usr/lib/xtables
|
ENV XTABLES_LIBDIR /usr/lib/xtables
|
||||||
ENV PYTHON_IPTABLES_XTABLES_VERSION 12
|
ENV PYTHON_IPTABLES_XTABLES_VERSION 12
|
||||||
ENV IPTABLES_LIBDIR /usr/lib
|
ENV IPTABLES_LIBDIR /usr/lib
|
||||||
@@ -12,12 +15,16 @@ RUN apk add --virtual .build-deps \
|
|||||||
openssl-dev \
|
openssl-dev \
|
||||||
&& apk add -U python3 \
|
&& apk add -U python3 \
|
||||||
iptables \
|
iptables \
|
||||||
|
iptables-dev \
|
||||||
ip6tables \
|
ip6tables \
|
||||||
xtables-addons \
|
xtables-addons \
|
||||||
|
nftables \
|
||||||
tzdata \
|
tzdata \
|
||||||
py3-pip \
|
py3-pip \
|
||||||
|
py3-nftables \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
&& pip3 install --ignore-installed --upgrade pip \
|
&& pip3 install --ignore-installed --upgrade pip \
|
||||||
|
jsonschema \
|
||||||
python-iptables \
|
python-iptables \
|
||||||
redis \
|
redis \
|
||||||
ipaddress \
|
ipaddress \
|
||||||
@@ -26,5 +33,10 @@ RUN apk add --virtual .build-deps \
|
|||||||
|
|
||||||
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
|
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
|
||||||
|
|
||||||
COPY server.py /
|
COPY modules /app/modules
|
||||||
CMD ["python3", "-u", "/server.py"]
|
COPY main.py /app/
|
||||||
|
COPY ./docker-entrypoint.sh /app/
|
||||||
|
|
||||||
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
|
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
backend=iptables
|
||||||
|
|
||||||
|
nft list table ip filter &>/dev/null
|
||||||
|
nftables_found=$?
|
||||||
|
|
||||||
|
iptables -L -n &>/dev/null
|
||||||
|
iptables_found=$?
|
||||||
|
|
||||||
|
if [ $nftables_found -lt $iptables_found ]; then
|
||||||
|
backend=nftables
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $nftables_found -gt $iptables_found ]; then
|
||||||
|
backend=iptables
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $nftables_found -eq 0 ] && [ $nftables_found -eq $iptables_found ]; then
|
||||||
|
nftables_lines=$(nft list ruleset | wc -l)
|
||||||
|
iptables_lines=$(iptables-save | wc -l)
|
||||||
|
if [ $nftables_lines -gt $iptables_lines ]; then
|
||||||
|
backend=nftables
|
||||||
|
else
|
||||||
|
backend=iptables
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec python -u /app/main.py $backend
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import atexit
|
||||||
|
import signal
|
||||||
|
import ipaddress
|
||||||
|
from collections import Counter
|
||||||
|
from random import randint
|
||||||
|
from threading import Thread
|
||||||
|
from threading import Lock
|
||||||
|
import redis
|
||||||
|
import json
|
||||||
|
import dns.resolver
|
||||||
|
import dns.exception
|
||||||
|
import uuid
|
||||||
|
from modules.Logger import Logger
|
||||||
|
from modules.IPTables import IPTables
|
||||||
|
from modules.NFTables import NFTables
|
||||||
|
|
||||||
|
|
||||||
|
# globals
|
||||||
|
WHITELIST = []
|
||||||
|
BLACKLIST= []
|
||||||
|
bans = {}
|
||||||
|
quit_now = False
|
||||||
|
exit_code = 0
|
||||||
|
lock = Lock()
|
||||||
|
chain_name = "MAILCOW"
|
||||||
|
r = None
|
||||||
|
pubsub = None
|
||||||
|
clear_before_quit = False
|
||||||
|
|
||||||
|
|
||||||
|
def refreshF2boptions():
|
||||||
|
global f2boptions
|
||||||
|
global quit_now
|
||||||
|
global exit_code
|
||||||
|
|
||||||
|
f2boptions = {}
|
||||||
|
|
||||||
|
if not r.get('F2B_OPTIONS'):
|
||||||
|
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
|
||||||
|
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
|
||||||
|
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
|
||||||
|
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
|
||||||
|
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
|
||||||
|
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
|
||||||
|
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
||||||
|
except ValueError:
|
||||||
|
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
|
||||||
|
quit_now = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
verifyF2boptions(f2boptions)
|
||||||
|
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||||
|
|
||||||
|
def verifyF2boptions(f2boptions):
|
||||||
|
verifyF2boption(f2boptions,'ban_time', 1800)
|
||||||
|
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
||||||
|
verifyF2boption(f2boptions,'ban_time_increment', True)
|
||||||
|
verifyF2boption(f2boptions,'max_attempts', 10)
|
||||||
|
verifyF2boption(f2boptions,'retry_window', 600)
|
||||||
|
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
||||||
|
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
||||||
|
verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4()))
|
||||||
|
verifyF2boption(f2boptions,'manage_external', 0)
|
||||||
|
|
||||||
|
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||||
|
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||||
|
|
||||||
|
def refreshF2bregex():
|
||||||
|
global f2bregex
|
||||||
|
global quit_now
|
||||||
|
global exit_code
|
||||||
|
if not r.get('F2B_REGEX'):
|
||||||
|
f2bregex = {}
|
||||||
|
f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
|
||||||
|
f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
|
||||||
|
f2bregex[3] = r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
|
||||||
|
f2bregex[4] = r'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
|
||||||
|
f2bregex[5] = r'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
|
||||||
|
f2bregex[6] = r'-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
|
||||||
|
f2bregex[7] = r'-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
||||||
|
f2bregex[8] = r'-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
||||||
|
f2bregex[9] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
|
||||||
|
f2bregex[10] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
|
||||||
|
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
f2bregex = {}
|
||||||
|
f2bregex = json.loads(r.get('F2B_REGEX'))
|
||||||
|
except ValueError:
|
||||||
|
logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
|
||||||
|
quit_now = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
def get_ip(address):
|
||||||
|
ip = ipaddress.ip_address(address)
|
||||||
|
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
|
||||||
|
ip = ip.ipv4_mapped
|
||||||
|
if ip.is_private or ip.is_loopback:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def ban(address):
|
||||||
|
global f2boptions
|
||||||
|
global lock
|
||||||
|
|
||||||
|
refreshF2boptions()
|
||||||
|
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||||
|
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||||
|
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
||||||
|
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
||||||
|
|
||||||
|
ip = get_ip(address)
|
||||||
|
if not ip: return
|
||||||
|
address = str(ip)
|
||||||
|
self_network = ipaddress.ip_network(address)
|
||||||
|
|
||||||
|
with lock:
|
||||||
|
temp_whitelist = set(WHITELIST)
|
||||||
|
if temp_whitelist:
|
||||||
|
for wl_key in temp_whitelist:
|
||||||
|
wl_net = ipaddress.ip_network(wl_key, False)
|
||||||
|
if wl_net.overlaps(self_network):
|
||||||
|
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
||||||
|
return
|
||||||
|
|
||||||
|
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||||
|
net = str(net)
|
||||||
|
|
||||||
|
if not net in bans:
|
||||||
|
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||||
|
|
||||||
|
current_attempt = time.time()
|
||||||
|
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||||
|
bans[net]['attempts'] = 0
|
||||||
|
|
||||||
|
bans[net]['attempts'] += 1
|
||||||
|
bans[net]['last_attempt'] = current_attempt
|
||||||
|
|
||||||
|
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||||
|
cur_time = int(round(time.time()))
|
||||||
|
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
|
||||||
|
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||||
|
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
|
||||||
|
with lock:
|
||||||
|
tables.banIPv4(net)
|
||||||
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
|
with lock:
|
||||||
|
tables.banIPv6(net)
|
||||||
|
|
||||||
|
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||||
|
else:
|
||||||
|
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||||
|
|
||||||
|
def unban(net):
|
||||||
|
global lock
|
||||||
|
|
||||||
|
if not net in bans:
|
||||||
|
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||||
|
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.logInfo('Unbanning %s' % net)
|
||||||
|
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
||||||
|
with lock:
|
||||||
|
tables.unbanIPv4(net)
|
||||||
|
else:
|
||||||
|
with lock:
|
||||||
|
tables.unbanIPv6(net)
|
||||||
|
|
||||||
|
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||||
|
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||||
|
if net in bans:
|
||||||
|
bans[net]['attempts'] = 0
|
||||||
|
bans[net]['ban_counter'] += 1
|
||||||
|
|
||||||
|
def permBan(net, unban=False):
|
||||||
|
global f2boptions
|
||||||
|
global lock
|
||||||
|
|
||||||
|
is_unbanned = False
|
||||||
|
is_banned = False
|
||||||
|
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
|
||||||
|
with lock:
|
||||||
|
if unban:
|
||||||
|
is_unbanned = tables.unbanIPv4(net)
|
||||||
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
|
is_banned = tables.banIPv4(net)
|
||||||
|
else:
|
||||||
|
with lock:
|
||||||
|
if unban:
|
||||||
|
is_unbanned = tables.unbanIPv6(net)
|
||||||
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
|
is_banned = tables.banIPv6(net)
|
||||||
|
|
||||||
|
|
||||||
|
if is_unbanned:
|
||||||
|
r.hdel('F2B_PERM_BANS', '%s' % net)
|
||||||
|
logger.logCrit('Removed host/network %s from blacklist' % net)
|
||||||
|
elif is_banned:
|
||||||
|
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||||
|
logger.logCrit('Added host/network %s to blacklist' % net)
|
||||||
|
|
||||||
|
def clear():
|
||||||
|
global lock
|
||||||
|
logger.logInfo('Clearing all bans')
|
||||||
|
for net in bans.copy():
|
||||||
|
unban(net)
|
||||||
|
with lock:
|
||||||
|
tables.clearIPv4Table()
|
||||||
|
tables.clearIPv6Table()
|
||||||
|
try:
|
||||||
|
if r is not None:
|
||||||
|
r.delete('F2B_ACTIVE_BANS')
|
||||||
|
r.delete('F2B_PERM_BANS')
|
||||||
|
except Exception as ex:
|
||||||
|
logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex)
|
||||||
|
|
||||||
|
def watch():
|
||||||
|
global pubsub
|
||||||
|
global quit_now
|
||||||
|
global exit_code
|
||||||
|
|
||||||
|
logger.logInfo('Watching Redis channel F2B_CHANNEL')
|
||||||
|
pubsub.subscribe('F2B_CHANNEL')
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
try:
|
||||||
|
for item in pubsub.listen():
|
||||||
|
refreshF2bregex()
|
||||||
|
for rule_id, rule_regex in f2bregex.items():
|
||||||
|
if item['data'] and item['type'] == 'message':
|
||||||
|
try:
|
||||||
|
result = re.search(rule_regex, item['data'])
|
||||||
|
except re.error:
|
||||||
|
result = False
|
||||||
|
if result:
|
||||||
|
addr = result.group(1)
|
||||||
|
ip = ipaddress.ip_address(addr)
|
||||||
|
if ip.is_private or ip.is_loopback:
|
||||||
|
continue
|
||||||
|
logger.logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
||||||
|
ban(addr)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.logWarn('Error reading log line from pubsub: %s' % ex)
|
||||||
|
pubsub = None
|
||||||
|
quit_now = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
def snat4(snat_target):
|
||||||
|
global lock
|
||||||
|
global quit_now
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
|
with lock:
|
||||||
|
tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24')
|
||||||
|
|
||||||
|
def snat6(snat_target):
|
||||||
|
global lock
|
||||||
|
global quit_now
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
|
with lock:
|
||||||
|
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
|
||||||
|
|
||||||
|
def autopurge():
|
||||||
|
global f2boptions
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
|
refreshF2boptions()
|
||||||
|
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||||
|
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
||||||
|
if QUEUE_UNBAN:
|
||||||
|
for net in QUEUE_UNBAN:
|
||||||
|
unban(str(net))
|
||||||
|
for net in bans.copy():
|
||||||
|
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||||
|
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
|
||||||
|
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
||||||
|
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME:
|
||||||
|
unban(net)
|
||||||
|
|
||||||
|
def mailcowChainOrder():
|
||||||
|
global lock
|
||||||
|
global quit_now
|
||||||
|
global exit_code
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
|
with lock:
|
||||||
|
quit_now, exit_code = tables.checkIPv4ChainOrder()
|
||||||
|
if quit_now: return
|
||||||
|
quit_now, exit_code = tables.checkIPv6ChainOrder()
|
||||||
|
|
||||||
|
def calcNetBanTime(ban_counter):
|
||||||
|
global f2boptions
|
||||||
|
|
||||||
|
BAN_TIME = int(f2boptions['ban_time'])
|
||||||
|
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
|
||||||
|
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||||
|
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** ban_counter
|
||||||
|
NET_BAN_TIME = max([BAN_TIME, min([NET_BAN_TIME, MAX_BAN_TIME])])
|
||||||
|
return NET_BAN_TIME
|
||||||
|
|
||||||
|
def isIpNetwork(address):
|
||||||
|
try:
|
||||||
|
ipaddress.ip_network(address, False)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def genNetworkList(list):
|
||||||
|
resolver = dns.resolver.Resolver()
|
||||||
|
hostnames = []
|
||||||
|
networks = []
|
||||||
|
for key in list:
|
||||||
|
if isIpNetwork(key):
|
||||||
|
networks.append(key)
|
||||||
|
else:
|
||||||
|
hostnames.append(key)
|
||||||
|
for hostname in hostnames:
|
||||||
|
hostname_ips = []
|
||||||
|
for rdtype in ['A', 'AAAA']:
|
||||||
|
try:
|
||||||
|
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
|
||||||
|
except dns.exception.Timeout:
|
||||||
|
logger.logInfo('Hostname %s timedout on resolve' % hostname)
|
||||||
|
break
|
||||||
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||||
|
continue
|
||||||
|
except dns.exception.DNSException as dnsexception:
|
||||||
|
logger.logInfo('%s' % dnsexception)
|
||||||
|
continue
|
||||||
|
for rdata in answer:
|
||||||
|
hostname_ips.append(rdata.to_text())
|
||||||
|
networks.extend(hostname_ips)
|
||||||
|
return set(networks)
|
||||||
|
|
||||||
|
def whitelistUpdate():
|
||||||
|
global lock
|
||||||
|
global quit_now
|
||||||
|
global WHITELIST
|
||||||
|
while not quit_now:
|
||||||
|
start_time = time.time()
|
||||||
|
list = r.hgetall('F2B_WHITELIST')
|
||||||
|
new_whitelist = []
|
||||||
|
if list:
|
||||||
|
new_whitelist = genNetworkList(list)
|
||||||
|
with lock:
|
||||||
|
if Counter(new_whitelist) != Counter(WHITELIST):
|
||||||
|
WHITELIST = new_whitelist
|
||||||
|
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
||||||
|
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||||
|
|
||||||
|
def blacklistUpdate():
|
||||||
|
global quit_now
|
||||||
|
global BLACKLIST
|
||||||
|
while not quit_now:
|
||||||
|
start_time = time.time()
|
||||||
|
list = r.hgetall('F2B_BLACKLIST')
|
||||||
|
new_blacklist = []
|
||||||
|
if list:
|
||||||
|
new_blacklist = genNetworkList(list)
|
||||||
|
if Counter(new_blacklist) != Counter(BLACKLIST):
|
||||||
|
addban = set(new_blacklist).difference(BLACKLIST)
|
||||||
|
delban = set(BLACKLIST).difference(new_blacklist)
|
||||||
|
BLACKLIST = new_blacklist
|
||||||
|
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
||||||
|
if addban:
|
||||||
|
for net in addban:
|
||||||
|
permBan(net=net)
|
||||||
|
if delban:
|
||||||
|
for net in delban:
|
||||||
|
permBan(net=net, unban=True)
|
||||||
|
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||||
|
|
||||||
|
def sigterm_quit(signum, frame):
|
||||||
|
global clear_before_quit
|
||||||
|
clear_before_quit = True
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
def berfore_quit():
|
||||||
|
if clear_before_quit:
|
||||||
|
clear()
|
||||||
|
if pubsub is not None:
|
||||||
|
pubsub.unsubscribe()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
atexit.register(berfore_quit)
|
||||||
|
signal.signal(signal.SIGTERM, sigterm_quit)
|
||||||
|
|
||||||
|
# init Logger
|
||||||
|
logger = Logger()
|
||||||
|
|
||||||
|
# init backend
|
||||||
|
backend = sys.argv[1]
|
||||||
|
if backend == "nftables":
|
||||||
|
logger.logInfo('Using NFTables backend')
|
||||||
|
tables = NFTables(chain_name, logger)
|
||||||
|
else:
|
||||||
|
logger.logInfo('Using IPTables backend')
|
||||||
|
tables = IPTables(chain_name, logger)
|
||||||
|
|
||||||
|
# In case a previous session was killed without cleanup
|
||||||
|
clear()
|
||||||
|
|
||||||
|
# Reinit MAILCOW chain
|
||||||
|
# Is called before threads start, no locking
|
||||||
|
logger.logInfo("Initializing mailcow netfilter chain")
|
||||||
|
tables.initChainIPv4()
|
||||||
|
tables.initChainIPv6()
|
||||||
|
|
||||||
|
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"):
|
||||||
|
logger.logInfo(f"Skipping {chain_name} isolation")
|
||||||
|
else:
|
||||||
|
logger.logInfo(f"Setting {chain_name} isolation")
|
||||||
|
tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP"))
|
||||||
|
|
||||||
|
# connect to redis
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
|
||||||
|
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
|
||||||
|
if "".__eq__(redis_slaveof_ip):
|
||||||
|
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
|
||||||
|
else:
|
||||||
|
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
|
||||||
|
r.ping()
|
||||||
|
pubsub = r.pubsub()
|
||||||
|
except Exception as ex:
|
||||||
|
print('%s - trying again in 3 seconds' % (ex))
|
||||||
|
time.sleep(3)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
logger.set_redis(r)
|
||||||
|
|
||||||
|
# rename fail2ban to netfilter
|
||||||
|
if r.exists('F2B_LOG'):
|
||||||
|
r.rename('F2B_LOG', 'NETFILTER_LOG')
|
||||||
|
# clear bans in redis
|
||||||
|
r.delete('F2B_ACTIVE_BANS')
|
||||||
|
r.delete('F2B_PERM_BANS')
|
||||||
|
|
||||||
|
refreshF2boptions()
|
||||||
|
|
||||||
|
watch_thread = Thread(target=watch)
|
||||||
|
watch_thread.daemon = True
|
||||||
|
watch_thread.start()
|
||||||
|
|
||||||
|
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
|
||||||
|
try:
|
||||||
|
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
||||||
|
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||||
|
if type(snat_ipo) is ipaddress.IPv4Address:
|
||||||
|
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
||||||
|
snat4_thread.daemon = True
|
||||||
|
snat4_thread.start()
|
||||||
|
except ValueError:
|
||||||
|
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
|
||||||
|
|
||||||
|
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
|
||||||
|
try:
|
||||||
|
snat_ip = os.getenv('SNAT6_TO_SOURCE')
|
||||||
|
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||||
|
if type(snat_ipo) is ipaddress.IPv6Address:
|
||||||
|
snat6_thread = Thread(target=snat6,args=(snat_ip,))
|
||||||
|
snat6_thread.daemon = True
|
||||||
|
snat6_thread.start()
|
||||||
|
except ValueError:
|
||||||
|
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
|
||||||
|
|
||||||
|
autopurge_thread = Thread(target=autopurge)
|
||||||
|
autopurge_thread.daemon = True
|
||||||
|
autopurge_thread.start()
|
||||||
|
|
||||||
|
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
|
||||||
|
mailcowchainwatch_thread.daemon = True
|
||||||
|
mailcowchainwatch_thread.start()
|
||||||
|
|
||||||
|
blacklistupdate_thread = Thread(target=blacklistUpdate)
|
||||||
|
blacklistupdate_thread.daemon = True
|
||||||
|
blacklistupdate_thread.start()
|
||||||
|
|
||||||
|
whitelistupdate_thread = Thread(target=whitelistUpdate)
|
||||||
|
whitelistupdate_thread.daemon = True
|
||||||
|
whitelistupdate_thread.start()
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import iptc
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
class IPTables:
|
||||||
|
def __init__(self, chain_name, logger):
|
||||||
|
self.chain_name = chain_name
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def initChainIPv4(self):
|
||||||
|
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) in iptc.Table(iptc.Table.FILTER).chains:
|
||||||
|
iptc.Table(iptc.Table.FILTER).create_chain(self.chain_name)
|
||||||
|
for c in ['FORWARD', 'INPUT']:
|
||||||
|
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = '0.0.0.0/0'
|
||||||
|
rule.dst = '0.0.0.0/0'
|
||||||
|
target = iptc.Target(rule, self.chain_name)
|
||||||
|
rule.target = target
|
||||||
|
if rule not in chain.rules:
|
||||||
|
chain.insert_rule(rule)
|
||||||
|
|
||||||
|
def initChainIPv6(self):
|
||||||
|
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) in iptc.Table6(iptc.Table6.FILTER).chains:
|
||||||
|
iptc.Table6(iptc.Table6.FILTER).create_chain(self.chain_name)
|
||||||
|
for c in ['FORWARD', 'INPUT']:
|
||||||
|
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
|
||||||
|
rule = iptc.Rule6()
|
||||||
|
rule.src = '::/0'
|
||||||
|
rule.dst = '::/0'
|
||||||
|
target = iptc.Target(rule, self.chain_name)
|
||||||
|
rule.target = target
|
||||||
|
if rule not in chain.rules:
|
||||||
|
chain.insert_rule(rule)
|
||||||
|
|
||||||
|
def checkIPv4ChainOrder(self):
|
||||||
|
filter_table = iptc.Table(iptc.Table.FILTER)
|
||||||
|
filter_table.refresh()
|
||||||
|
return self.checkChainOrder(filter_table)
|
||||||
|
|
||||||
|
def checkIPv6ChainOrder(self):
|
||||||
|
filter_table = iptc.Table6(iptc.Table6.FILTER)
|
||||||
|
filter_table.refresh()
|
||||||
|
return self.checkChainOrder(filter_table)
|
||||||
|
|
||||||
|
def checkChainOrder(self, filter_table):
|
||||||
|
err = False
|
||||||
|
exit_code = None
|
||||||
|
|
||||||
|
forward_chain = iptc.Chain(filter_table, 'FORWARD')
|
||||||
|
input_chain = iptc.Chain(filter_table, 'INPUT')
|
||||||
|
for chain in [forward_chain, input_chain]:
|
||||||
|
target_found = False
|
||||||
|
for position, item in enumerate(chain.rules):
|
||||||
|
if item.target.name == self.chain_name:
|
||||||
|
target_found = True
|
||||||
|
if position > 2:
|
||||||
|
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
|
||||||
|
err = True
|
||||||
|
exit_code = 2
|
||||||
|
if not target_found:
|
||||||
|
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
|
||||||
|
err = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
return err, exit_code
|
||||||
|
|
||||||
|
def clearIPv4Table(self):
|
||||||
|
self.clearTable(iptc.Table(iptc.Table.FILTER))
|
||||||
|
|
||||||
|
def clearIPv6Table(self):
|
||||||
|
self.clearTable(iptc.Table6(iptc.Table6.FILTER))
|
||||||
|
|
||||||
|
def clearTable(self, filter_table):
|
||||||
|
filter_table.autocommit = False
|
||||||
|
forward_chain = iptc.Chain(filter_table, "FORWARD")
|
||||||
|
input_chain = iptc.Chain(filter_table, "INPUT")
|
||||||
|
mailcow_chain = iptc.Chain(filter_table, self.chain_name)
|
||||||
|
if mailcow_chain in filter_table.chains:
|
||||||
|
for rule in mailcow_chain.rules:
|
||||||
|
mailcow_chain.delete_rule(rule)
|
||||||
|
for rule in forward_chain.rules:
|
||||||
|
if rule.target.name == self.chain_name:
|
||||||
|
forward_chain.delete_rule(rule)
|
||||||
|
for rule in input_chain.rules:
|
||||||
|
if rule.target.name == self.chain_name:
|
||||||
|
input_chain.delete_rule(rule)
|
||||||
|
filter_table.delete_chain(self.chain_name)
|
||||||
|
filter_table.commit()
|
||||||
|
filter_table.refresh()
|
||||||
|
filter_table.autocommit = True
|
||||||
|
|
||||||
|
def banIPv4(self, source):
|
||||||
|
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = source
|
||||||
|
target = iptc.Target(rule, "REJECT")
|
||||||
|
rule.target = target
|
||||||
|
if rule in chain.rules:
|
||||||
|
return False
|
||||||
|
chain.insert_rule(rule)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def banIPv6(self, source):
|
||||||
|
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name)
|
||||||
|
rule = iptc.Rule6()
|
||||||
|
rule.src = source
|
||||||
|
target = iptc.Target(rule, "REJECT")
|
||||||
|
rule.target = target
|
||||||
|
if rule in chain.rules:
|
||||||
|
return False
|
||||||
|
chain.insert_rule(rule)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unbanIPv4(self, source):
|
||||||
|
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = source
|
||||||
|
target = iptc.Target(rule, "REJECT")
|
||||||
|
rule.target = target
|
||||||
|
if rule not in chain.rules:
|
||||||
|
return False
|
||||||
|
chain.delete_rule(rule)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unbanIPv6(self, source):
|
||||||
|
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name)
|
||||||
|
rule = iptc.Rule6()
|
||||||
|
rule.src = source
|
||||||
|
target = iptc.Target(rule, "REJECT")
|
||||||
|
rule.target = target
|
||||||
|
if rule not in chain.rules:
|
||||||
|
return False
|
||||||
|
chain.delete_rule(rule)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def snat4(self, snat_target, source):
|
||||||
|
try:
|
||||||
|
table = iptc.Table('nat')
|
||||||
|
table.refresh()
|
||||||
|
chain = iptc.Chain(table, 'POSTROUTING')
|
||||||
|
table.autocommit = False
|
||||||
|
new_rule = self.getSnat4Rule(snat_target, source)
|
||||||
|
|
||||||
|
if not chain.rules:
|
||||||
|
# if there are no rules in the chain, insert the new rule directly
|
||||||
|
self.logger.logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||||
|
chain.insert_rule(new_rule)
|
||||||
|
else:
|
||||||
|
for position, rule in enumerate(chain.rules):
|
||||||
|
if not hasattr(rule.target, 'parameter'):
|
||||||
|
continue
|
||||||
|
match = all((
|
||||||
|
new_rule.get_src() == rule.get_src(),
|
||||||
|
new_rule.get_dst() == rule.get_dst(),
|
||||||
|
new_rule.target.parameters == rule.target.parameters,
|
||||||
|
new_rule.target.name == rule.target.name
|
||||||
|
))
|
||||||
|
if position == 0:
|
||||||
|
if not match:
|
||||||
|
self.logger.logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||||
|
chain.insert_rule(new_rule)
|
||||||
|
else:
|
||||||
|
if match:
|
||||||
|
self.logger.logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
|
||||||
|
chain.delete_rule(rule)
|
||||||
|
|
||||||
|
table.commit()
|
||||||
|
table.autocommit = True
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
self.logger.logCrit('Error running SNAT4, retrying...')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def snat6(self, snat_target, source):
|
||||||
|
try:
|
||||||
|
table = iptc.Table6('nat')
|
||||||
|
table.refresh()
|
||||||
|
chain = iptc.Chain(table, 'POSTROUTING')
|
||||||
|
table.autocommit = False
|
||||||
|
new_rule = self.getSnat6Rule(snat_target, source)
|
||||||
|
|
||||||
|
if new_rule not in chain.rules:
|
||||||
|
self.logger.logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (new_rule.src, snat_target))
|
||||||
|
chain.insert_rule(new_rule)
|
||||||
|
else:
|
||||||
|
for position, item in enumerate(chain.rules):
|
||||||
|
if item == new_rule:
|
||||||
|
if position != 0:
|
||||||
|
chain.delete_rule(new_rule)
|
||||||
|
|
||||||
|
table.commit()
|
||||||
|
table.autocommit = True
|
||||||
|
except:
|
||||||
|
self.logger.logCrit('Error running SNAT6, retrying...')
|
||||||
|
|
||||||
|
|
||||||
|
def getSnat4Rule(self, snat_target, source):
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = source
|
||||||
|
rule.dst = '!' + rule.src
|
||||||
|
target = rule.create_target("SNAT")
|
||||||
|
target.to_source = snat_target
|
||||||
|
match = rule.create_match("comment")
|
||||||
|
match.comment = f'{int(round(time.time()))}'
|
||||||
|
return rule
|
||||||
|
|
||||||
|
def getSnat6Rule(self, snat_target, source):
|
||||||
|
rule = iptc.Rule6()
|
||||||
|
rule.src = source
|
||||||
|
rule.dst = '!' + rule.src
|
||||||
|
target = rule.create_target("SNAT")
|
||||||
|
target.to_source = snat_target
|
||||||
|
return rule
|
||||||
|
|
||||||
|
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
|
||||||
|
try:
|
||||||
|
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
|
||||||
|
|
||||||
|
# insert mailcow isolation rule
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.in_interface = f'!{_interface}'
|
||||||
|
rule.out_interface = _interface
|
||||||
|
rule.protocol = 'tcp'
|
||||||
|
rule.create_target("DROP")
|
||||||
|
match = rule.create_match("multiport")
|
||||||
|
match.dports = ','.join(map(str, _dports))
|
||||||
|
|
||||||
|
if rule in chain.rules:
|
||||||
|
chain.delete_rule(rule)
|
||||||
|
chain.insert_rule(rule, position=0)
|
||||||
|
|
||||||
|
# insert mailcow isolation exception rule
|
||||||
|
if _allow != "":
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = _allow
|
||||||
|
rule.in_interface = f'!{_interface}'
|
||||||
|
rule.out_interface = _interface
|
||||||
|
rule.protocol = 'tcp'
|
||||||
|
rule.create_target("ACCEPT")
|
||||||
|
match = rule.create_match("multiport")
|
||||||
|
match.dports = ','.join(map(str, _dports))
|
||||||
|
|
||||||
|
if rule in chain.rules:
|
||||||
|
chain.delete_rule(rule)
|
||||||
|
chain.insert_rule(rule, position=0)
|
||||||
|
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.logCrit(f"Error adding {self.chain_name} isolation: {e}")
|
||||||
|
return False
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
def __init__(self):
|
||||||
|
self.r = None
|
||||||
|
|
||||||
|
def set_redis(self, redis):
|
||||||
|
self.r = redis
|
||||||
|
|
||||||
|
def log(self, priority, message):
|
||||||
|
tolog = {}
|
||||||
|
tolog['time'] = int(round(time.time()))
|
||||||
|
tolog['priority'] = priority
|
||||||
|
tolog['message'] = message
|
||||||
|
print(message)
|
||||||
|
if self.r is not None:
|
||||||
|
try:
|
||||||
|
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
||||||
|
except Exception as ex:
|
||||||
|
print('Failed logging to redis: %s' % (ex))
|
||||||
|
|
||||||
|
def logWarn(self, message):
|
||||||
|
self.log('warn', message)
|
||||||
|
|
||||||
|
def logCrit(self, message):
|
||||||
|
self.log('crit', message)
|
||||||
|
|
||||||
|
def logInfo(self, message):
|
||||||
|
self.log('info', message)
|
||||||
@@ -0,0 +1,659 @@
|
|||||||
|
import nftables
|
||||||
|
import ipaddress
|
||||||
|
import os
|
||||||
|
|
||||||
|
class NFTables:
|
||||||
|
def __init__(self, chain_name, logger):
|
||||||
|
self.chain_name = chain_name
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
self.nft = nftables.Nftables()
|
||||||
|
self.nft.set_json_output(True)
|
||||||
|
self.nft.set_handle_output(True)
|
||||||
|
self.nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} },
|
||||||
|
'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } }
|
||||||
|
|
||||||
|
self.search_current_chains()
|
||||||
|
|
||||||
|
def initChainIPv4(self):
|
||||||
|
self.insert_mailcow_chains("ip")
|
||||||
|
|
||||||
|
def initChainIPv6(self):
|
||||||
|
self.insert_mailcow_chains("ip6")
|
||||||
|
|
||||||
|
def checkIPv4ChainOrder(self):
|
||||||
|
return self.checkChainOrder("ip")
|
||||||
|
|
||||||
|
def checkIPv6ChainOrder(self):
|
||||||
|
return self.checkChainOrder("ip6")
|
||||||
|
|
||||||
|
def checkChainOrder(self, filter_table):
|
||||||
|
err = False
|
||||||
|
exit_code = None
|
||||||
|
|
||||||
|
for chain in ['input', 'forward']:
|
||||||
|
chain_position = self.check_mailcow_chains(filter_table, chain)
|
||||||
|
if chain_position is None: continue
|
||||||
|
|
||||||
|
if chain_position is False:
|
||||||
|
self.logger.logCrit(f'MAILCOW target not found in {filter_table} {chain} table, restarting container to fix it...')
|
||||||
|
err = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
if chain_position > 0:
|
||||||
|
chain_position += 1
|
||||||
|
self.logger.logCrit(f'MAILCOW target is in position {chain_position} in the {filter_table} {chain} table, restarting container to fix it...')
|
||||||
|
err = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
return err, exit_code
|
||||||
|
|
||||||
|
def clearIPv4Table(self):
|
||||||
|
self.clearTable("ip")
|
||||||
|
|
||||||
|
def clearIPv6Table(self):
|
||||||
|
self.clearTable("ip6")
|
||||||
|
|
||||||
|
def clearTable(self, _family):
|
||||||
|
is_empty_dict = True
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
chain_handle = self.get_chain_handle(_family, "filter", self.chain_name)
|
||||||
|
# if no handle, the chain doesn't exists
|
||||||
|
if chain_handle is not None:
|
||||||
|
is_empty_dict = False
|
||||||
|
# flush chain
|
||||||
|
mailcow_chain = {'family': _family, 'table': 'filter', 'name': self.chain_name}
|
||||||
|
flush_chain = {'flush': {'chain': mailcow_chain}}
|
||||||
|
json_command["nftables"].append(flush_chain)
|
||||||
|
|
||||||
|
# remove rule in forward chain
|
||||||
|
# remove rule in input chain
|
||||||
|
chains_family = [self.nft_chain_names[_family]['filter']['input'],
|
||||||
|
self.nft_chain_names[_family]['filter']['forward'] ]
|
||||||
|
|
||||||
|
for chain_base in chains_family:
|
||||||
|
if not chain_base: continue
|
||||||
|
|
||||||
|
rules_handle = self.get_rules_handle(_family, "filter", chain_base)
|
||||||
|
if rules_handle is not None:
|
||||||
|
for r_handle in rules_handle:
|
||||||
|
is_empty_dict = False
|
||||||
|
mailcow_rule = {'family':_family,
|
||||||
|
'table': 'filter',
|
||||||
|
'chain': chain_base,
|
||||||
|
'handle': r_handle }
|
||||||
|
delete_rules = {'delete': {'rule': mailcow_rule} }
|
||||||
|
json_command["nftables"].append(delete_rules)
|
||||||
|
|
||||||
|
# remove chain
|
||||||
|
# after delete all rules referencing this chain
|
||||||
|
if chain_handle is not None:
|
||||||
|
mc_chain_handle = {'family':_family,
|
||||||
|
'table': 'filter',
|
||||||
|
'name': self.chain_name,
|
||||||
|
'handle': chain_handle }
|
||||||
|
delete_chain = {'delete': {'chain': mc_chain_handle} }
|
||||||
|
json_command["nftables"].append(delete_chain)
|
||||||
|
|
||||||
|
if is_empty_dict == False:
|
||||||
|
if self.nft_exec_dict(json_command):
|
||||||
|
self.logger.logInfo(f"Clear completed: {_family}")
|
||||||
|
|
||||||
|
def banIPv4(self, source):
|
||||||
|
ban_dict = self.get_ban_ip_dict(source, "ip")
|
||||||
|
return self.nft_exec_dict(ban_dict)
|
||||||
|
|
||||||
|
def banIPv6(self, source):
|
||||||
|
ban_dict = self.get_ban_ip_dict(source, "ip6")
|
||||||
|
return self.nft_exec_dict(ban_dict)
|
||||||
|
|
||||||
|
def unbanIPv4(self, source):
|
||||||
|
unban_dict = self.get_unban_ip_dict(source, "ip")
|
||||||
|
if not unban_dict:
|
||||||
|
return False
|
||||||
|
return self.nft_exec_dict(unban_dict)
|
||||||
|
|
||||||
|
def unbanIPv6(self, source):
|
||||||
|
unban_dict = self.get_unban_ip_dict(source, "ip6")
|
||||||
|
if not unban_dict:
|
||||||
|
return False
|
||||||
|
return self.nft_exec_dict(unban_dict)
|
||||||
|
|
||||||
|
def snat4(self, snat_target, source):
|
||||||
|
self.snat_rule("ip", snat_target, source)
|
||||||
|
|
||||||
|
def snat6(self, snat_target, source):
|
||||||
|
self.snat_rule("ip6", snat_target, source)
|
||||||
|
|
||||||
|
|
||||||
|
def nft_exec_dict(self, query: dict):
|
||||||
|
if not query: return False
|
||||||
|
|
||||||
|
rc, output, error = self.nft.json_cmd(query)
|
||||||
|
if rc != 0:
|
||||||
|
#self.logger.logCrit(f"Nftables Error: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Prevent returning False or empty string on commands that do not produce output
|
||||||
|
if rc == 0 and len(output) == 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def get_base_dict(self):
|
||||||
|
return {'nftables': [{ 'metainfo': { 'json_schema_version': 1} } ] }
|
||||||
|
|
||||||
|
def search_current_chains(self):
|
||||||
|
nft_chain_priority = {'ip': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} },
|
||||||
|
'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } }
|
||||||
|
|
||||||
|
# Command: 'nft list chains'
|
||||||
|
_list = {'list' : {'chains': 'null'} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset['nftables']:
|
||||||
|
chain = _object.get("chain")
|
||||||
|
if not chain: continue
|
||||||
|
|
||||||
|
_family = chain['family']
|
||||||
|
_table = chain['table']
|
||||||
|
_hook = chain.get("hook")
|
||||||
|
_priority = chain.get("prio")
|
||||||
|
_name = chain['name']
|
||||||
|
|
||||||
|
if _family not in self.nft_chain_names: continue
|
||||||
|
if _table not in self.nft_chain_names[_family]: continue
|
||||||
|
if _hook not in self.nft_chain_names[_family][_table]: continue
|
||||||
|
if _priority is None: continue
|
||||||
|
|
||||||
|
_saved_priority = nft_chain_priority[_family][_table][_hook]
|
||||||
|
if _saved_priority is None or _priority < _saved_priority:
|
||||||
|
# at this point, we know the chain has:
|
||||||
|
# hook and priority set
|
||||||
|
# and it has the lowest priority
|
||||||
|
nft_chain_priority[_family][_table][_hook] = _priority
|
||||||
|
self.nft_chain_names[_family][_table][_hook] = _name
|
||||||
|
|
||||||
|
def search_for_chain(self, kernel_ruleset: dict, chain_name: str):
|
||||||
|
found = False
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
chain = _object.get("chain")
|
||||||
|
if not chain:
|
||||||
|
continue
|
||||||
|
ch_name = chain.get("name")
|
||||||
|
if ch_name == chain_name:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
return found
|
||||||
|
|
||||||
|
def get_chain_dict(self, _family: str, _name: str):
|
||||||
|
# nft (add | create) chain [<family>] <table> <name>
|
||||||
|
_chain_opts = {'family': _family, 'table': 'filter', 'name': _name }
|
||||||
|
_add = {'add': {'chain': _chain_opts} }
|
||||||
|
final_chain = self.get_base_dict()
|
||||||
|
final_chain["nftables"].append(_add)
|
||||||
|
return final_chain
|
||||||
|
|
||||||
|
def get_mailcow_jump_rule_dict(self, _family: str, _chain: str):
|
||||||
|
_jump_rule = self.get_base_dict()
|
||||||
|
_expr_opt=[]
|
||||||
|
_expr_counter = {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0}
|
||||||
|
_counter_dict = {'counter': _expr_counter}
|
||||||
|
_expr_opt.append(_counter_dict)
|
||||||
|
|
||||||
|
_jump_opts = {'jump': {'target': self.chain_name} }
|
||||||
|
|
||||||
|
_expr_opt.append(_jump_opts)
|
||||||
|
|
||||||
|
_rule_params = {'family': _family,
|
||||||
|
'table': 'filter',
|
||||||
|
'chain': _chain,
|
||||||
|
'expr': _expr_opt,
|
||||||
|
'comment': "mailcow" }
|
||||||
|
|
||||||
|
_add_rule = {'insert': {'rule': _rule_params} }
|
||||||
|
|
||||||
|
_jump_rule["nftables"].append(_add_rule)
|
||||||
|
|
||||||
|
return _jump_rule
|
||||||
|
|
||||||
|
def insert_mailcow_chains(self, _family: str):
|
||||||
|
nft_input_chain = self.nft_chain_names[_family]['filter']['input']
|
||||||
|
nft_forward_chain = self.nft_chain_names[_family]['filter']['forward']
|
||||||
|
# Command: 'nft list table <family> filter'
|
||||||
|
_table_opts = {'family': _family, 'name': 'filter'}
|
||||||
|
_list = {'list': {'table': _table_opts} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
# chain
|
||||||
|
if not self.search_for_chain(kernel_ruleset, self.chain_name):
|
||||||
|
cadena = self.get_chain_dict(_family, self.chain_name)
|
||||||
|
if self.nft_exec_dict(cadena):
|
||||||
|
self.logger.logInfo(f"MAILCOW {_family} chain created successfully.")
|
||||||
|
|
||||||
|
input_jump_found, forward_jump_found = False, False
|
||||||
|
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule = _object["rule"]
|
||||||
|
if nft_input_chain and rule["chain"] == nft_input_chain:
|
||||||
|
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||||
|
input_jump_found = True
|
||||||
|
if nft_forward_chain and rule["chain"] == nft_forward_chain:
|
||||||
|
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||||
|
forward_jump_found = True
|
||||||
|
|
||||||
|
if not input_jump_found:
|
||||||
|
command = self.get_mailcow_jump_rule_dict(_family, nft_input_chain)
|
||||||
|
self.nft_exec_dict(command)
|
||||||
|
|
||||||
|
if not forward_jump_found:
|
||||||
|
command = self.get_mailcow_jump_rule_dict(_family, nft_forward_chain)
|
||||||
|
self.nft_exec_dict(command)
|
||||||
|
|
||||||
|
def delete_nat_rule(self, _family:str, _chain: str, _handle:str):
|
||||||
|
delete_command = self.get_base_dict()
|
||||||
|
_rule_opts = {'family': _family,
|
||||||
|
'table': 'nat',
|
||||||
|
'chain': _chain,
|
||||||
|
'handle': _handle }
|
||||||
|
_delete = {'delete': {'rule': _rule_opts} }
|
||||||
|
delete_command["nftables"].append(_delete)
|
||||||
|
|
||||||
|
return self.nft_exec_dict(delete_command)
|
||||||
|
|
||||||
|
def delete_filter_rule(self, _family:str, _chain: str, _handle:str):
|
||||||
|
delete_command = self.get_base_dict()
|
||||||
|
_rule_opts = {'family': _family,
|
||||||
|
'table': 'filter',
|
||||||
|
'chain': _chain,
|
||||||
|
'handle': _handle }
|
||||||
|
_delete = {'delete': {'rule': _rule_opts} }
|
||||||
|
delete_command["nftables"].append(_delete)
|
||||||
|
|
||||||
|
return self.nft_exec_dict(delete_command)
|
||||||
|
|
||||||
|
def snat_rule(self, _family: str, snat_target: str, source_address: str):
|
||||||
|
chain_name = self.nft_chain_names[_family]['nat']['postrouting']
|
||||||
|
|
||||||
|
# no postrouting chain, may occur if docker has ipv6 disabled.
|
||||||
|
if not chain_name: return
|
||||||
|
|
||||||
|
# Command: nft list chain <family> nat <chain_name>
|
||||||
|
_chain_opts = {'family': _family, 'table': 'nat', 'name': chain_name}
|
||||||
|
_list = {'list':{'chain': _chain_opts} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if not kernel_ruleset:
|
||||||
|
return
|
||||||
|
|
||||||
|
rule_position = 0
|
||||||
|
rule_handle = None
|
||||||
|
rule_found = False
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule = _object["rule"]
|
||||||
|
if not rule.get("comment") or not rule["comment"] == "mailcow":
|
||||||
|
rule_position +=1
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule_found = True
|
||||||
|
rule_handle = rule["handle"]
|
||||||
|
break
|
||||||
|
|
||||||
|
dest_net = ipaddress.ip_network(source_address, strict=False)
|
||||||
|
target_net = ipaddress.ip_network(snat_target, strict=False)
|
||||||
|
|
||||||
|
if rule_found:
|
||||||
|
saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"]
|
||||||
|
saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"])
|
||||||
|
|
||||||
|
daddr_ip = rule["expr"][1]["match"]["right"]["prefix"]["addr"]
|
||||||
|
daddr_len = int(rule["expr"][1]["match"]["right"]["prefix"]["len"])
|
||||||
|
|
||||||
|
target_ip = rule["expr"][3]["snat"]["addr"]
|
||||||
|
|
||||||
|
saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len), strict=False)
|
||||||
|
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len), strict=False)
|
||||||
|
current_target_net = ipaddress.ip_network(target_ip, strict=False)
|
||||||
|
|
||||||
|
match = all((
|
||||||
|
dest_net == saddr_net,
|
||||||
|
dest_net == daddr_net,
|
||||||
|
target_net == current_target_net
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
if rule_position == 0:
|
||||||
|
if not match:
|
||||||
|
# Position 0 , it is a mailcow rule , but it does not have the same parameters
|
||||||
|
if self.delete_nat_rule(_family, chain_name, rule_handle):
|
||||||
|
self.logger.logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule does not match configured parameters')
|
||||||
|
else:
|
||||||
|
# Position > 0 and is mailcow rule
|
||||||
|
if self.delete_nat_rule(_family, chain_name, rule_handle):
|
||||||
|
self.logger.logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule is at position {rule_position}')
|
||||||
|
except:
|
||||||
|
self.logger.logCrit(f"Error running SNAT on {_family}, retrying..." )
|
||||||
|
else:
|
||||||
|
# rule not found
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
try:
|
||||||
|
snat_dict = {'snat': {'addr': str(target_net.network_address)} }
|
||||||
|
|
||||||
|
expr_counter = {'family': _family, 'table': 'nat', 'packets': 0, 'bytes': 0}
|
||||||
|
counter_dict = {'counter': expr_counter}
|
||||||
|
|
||||||
|
prefix_dict = {'prefix': {'addr': str(dest_net.network_address), 'len': int(dest_net.prefixlen)} }
|
||||||
|
payload_dict = {'payload': {'protocol': _family, 'field': "saddr"} }
|
||||||
|
match_dict1 = {'match': {'op': '==', 'left': payload_dict, 'right': prefix_dict} }
|
||||||
|
|
||||||
|
payload_dict2 = {'payload': {'protocol': _family, 'field': "daddr"} }
|
||||||
|
match_dict2 = {'match': {'op': '!=', 'left': payload_dict2, 'right': prefix_dict } }
|
||||||
|
expr_list = [
|
||||||
|
match_dict1,
|
||||||
|
match_dict2,
|
||||||
|
counter_dict,
|
||||||
|
snat_dict
|
||||||
|
]
|
||||||
|
rule_fields = {'family': _family,
|
||||||
|
'table': 'nat',
|
||||||
|
'chain': chain_name,
|
||||||
|
'comment': "mailcow",
|
||||||
|
'expr': expr_list }
|
||||||
|
|
||||||
|
insert_dict = {'insert': {'rule': rule_fields} }
|
||||||
|
json_command["nftables"].append(insert_dict)
|
||||||
|
if self.nft_exec_dict(json_command):
|
||||||
|
self.logger.logInfo(f'Added {_family} nat {chain_name} rule for source network {dest_net} to {target_net}')
|
||||||
|
except:
|
||||||
|
self.logger.logCrit(f"Error running SNAT on {_family}, retrying...")
|
||||||
|
|
||||||
|
def get_chain_handle(self, _family: str, _table: str, chain_name: str):
|
||||||
|
chain_handle = None
|
||||||
|
# Command: 'nft list chains {family}'
|
||||||
|
_list = {'list': {'chains': {'family': _family} } }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("chain"):
|
||||||
|
continue
|
||||||
|
chain = _object["chain"]
|
||||||
|
if chain["family"] == _family and chain["table"] == _table and chain["name"] == chain_name:
|
||||||
|
chain_handle = chain["handle"]
|
||||||
|
break
|
||||||
|
return chain_handle
|
||||||
|
|
||||||
|
def get_rules_handle(self, _family: str, _table: str, chain_name: str, _comment_filter = "mailcow"):
|
||||||
|
rule_handle = []
|
||||||
|
# Command: 'nft list chain {family} {table} {chain_name}'
|
||||||
|
_chain_opts = {'family': _family, 'table': _table, 'name': chain_name}
|
||||||
|
_list = {'list': {'chain': _chain_opts} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule = _object["rule"]
|
||||||
|
if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name:
|
||||||
|
if rule.get("comment") and rule["comment"] == _comment_filter:
|
||||||
|
rule_handle.append(rule["handle"])
|
||||||
|
return rule_handle
|
||||||
|
|
||||||
|
def get_ban_ip_dict(self, ipaddr: str, _family: str):
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
|
||||||
|
expr_opt = []
|
||||||
|
ipaddr_net = ipaddress.ip_network(ipaddr, strict=False)
|
||||||
|
right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } }
|
||||||
|
|
||||||
|
left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} }
|
||||||
|
match_dict = {'op': '==', 'left': left_dict, 'right': right_dict }
|
||||||
|
expr_opt.append({'match': match_dict})
|
||||||
|
|
||||||
|
counter_dict = {'counter': {'family': _family, 'table': "filter", 'packets': 0, 'bytes': 0} }
|
||||||
|
expr_opt.append(counter_dict)
|
||||||
|
|
||||||
|
expr_opt.append({'drop': "null"})
|
||||||
|
|
||||||
|
rule_dict = {'family': _family, 'table': "filter", 'chain': self.chain_name, 'expr': expr_opt}
|
||||||
|
|
||||||
|
base_dict = {'insert': {'rule': rule_dict} }
|
||||||
|
json_command["nftables"].append(base_dict)
|
||||||
|
|
||||||
|
return json_command
|
||||||
|
|
||||||
|
def get_unban_ip_dict(self, ipaddr:str, _family: str):
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
# Command: 'nft list chain {s_family} filter MAILCOW'
|
||||||
|
_chain_opts = {'family': _family, 'table': 'filter', 'name': self.chain_name}
|
||||||
|
_list = {'list': {'chain': _chain_opts} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
rule_handle = None
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule = _object["rule"]["expr"][0]["match"]
|
||||||
|
if not "payload" in rule["left"]:
|
||||||
|
continue
|
||||||
|
left_opt = rule["left"]["payload"]
|
||||||
|
if not left_opt["protocol"] == _family:
|
||||||
|
continue
|
||||||
|
if not left_opt["field"] =="saddr":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ip currently banned
|
||||||
|
rule_right = rule["right"]
|
||||||
|
if isinstance(rule_right, dict):
|
||||||
|
current_rule_ip = rule_right["prefix"]["addr"] + '/' + str(rule_right["prefix"]["len"])
|
||||||
|
else:
|
||||||
|
current_rule_ip = rule_right
|
||||||
|
current_rule_net = ipaddress.ip_network(current_rule_ip)
|
||||||
|
|
||||||
|
# ip to ban
|
||||||
|
candidate_net = ipaddress.ip_network(ipaddr, strict=False)
|
||||||
|
|
||||||
|
if current_rule_net == candidate_net:
|
||||||
|
rule_handle = _object["rule"]["handle"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if rule_handle is not None:
|
||||||
|
mailcow_rule = {'family': _family, 'table': 'filter', 'chain': self.chain_name, 'handle': rule_handle}
|
||||||
|
delete_rule = {'delete': {'rule': mailcow_rule} }
|
||||||
|
json_command["nftables"].append(delete_rule)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return json_command
|
||||||
|
|
||||||
|
def check_mailcow_chains(self, family: str, chain: str):
|
||||||
|
position = 0
|
||||||
|
rule_found = False
|
||||||
|
chain_name = self.nft_chain_names[family]['filter'][chain]
|
||||||
|
|
||||||
|
if not chain_name: return None
|
||||||
|
|
||||||
|
_chain_opts = {'family': family, 'table': 'filter', 'name': chain_name}
|
||||||
|
_list = {'list': {'chain': _chain_opts}}
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
rule = _object["rule"]
|
||||||
|
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||||
|
rule_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
position+=1
|
||||||
|
|
||||||
|
return position if rule_found else False
|
||||||
|
|
||||||
|
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
|
||||||
|
family = "ip"
|
||||||
|
table = "filter"
|
||||||
|
comment_filter_drop = "mailcow isolation"
|
||||||
|
comment_filter_allow = "mailcow isolation allow"
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
|
||||||
|
# Delete old mailcow isolation rules
|
||||||
|
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_drop)
|
||||||
|
for handle in handles:
|
||||||
|
self.delete_filter_rule(family, self.chain_name, handle)
|
||||||
|
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_allow)
|
||||||
|
for handle in handles:
|
||||||
|
self.delete_filter_rule(family, self.chain_name, handle)
|
||||||
|
|
||||||
|
# insert mailcow isolation rule
|
||||||
|
_match_dict_drop = [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"op": "!=",
|
||||||
|
"left": {
|
||||||
|
"meta": {
|
||||||
|
"key": "iifname"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"right": _interface
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"op": "==",
|
||||||
|
"left": {
|
||||||
|
"meta": {
|
||||||
|
"key": "oifname"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"right": _interface
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"op": "==",
|
||||||
|
"left": {
|
||||||
|
"payload": {
|
||||||
|
"protocol": "tcp",
|
||||||
|
"field": "dport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"set": _dports
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"counter": {
|
||||||
|
"packets": 0,
|
||||||
|
"bytes": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"drop": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
rule_drop = { "insert": { "rule": {
|
||||||
|
"family": family,
|
||||||
|
"table": table,
|
||||||
|
"chain": self.chain_name,
|
||||||
|
"comment": comment_filter_drop,
|
||||||
|
"expr": _match_dict_drop
|
||||||
|
}}}
|
||||||
|
json_command["nftables"].append(rule_drop)
|
||||||
|
|
||||||
|
# insert mailcow isolation allow rule
|
||||||
|
if _allow != "":
|
||||||
|
_match_dict_allow = [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"op": "==",
|
||||||
|
"left": {
|
||||||
|
"payload": {
|
||||||
|
"protocol": "ip",
|
||||||
|
"field": "saddr"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"right": _allow
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"op": "!=",
|
||||||
|
"left": {
|
||||||
|
"meta": {
|
||||||
|
"key": "iifname"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"right": _interface
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"op": "==",
|
||||||
|
"left": {
|
||||||
|
"meta": {
|
||||||
|
"key": "oifname"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"right": _interface
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"op": "==",
|
||||||
|
"left": {
|
||||||
|
"payload": {
|
||||||
|
"protocol": "tcp",
|
||||||
|
"field": "dport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"set": _dports
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"counter": {
|
||||||
|
"packets": 0,
|
||||||
|
"bytes": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accept": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
rule_allow = { "insert": { "rule": {
|
||||||
|
"family": family,
|
||||||
|
"table": table,
|
||||||
|
"chain": self.chain_name,
|
||||||
|
"comment": comment_filter_allow,
|
||||||
|
"expr": _match_dict_allow
|
||||||
|
}}}
|
||||||
|
json_command["nftables"].append(rule_allow)
|
||||||
|
|
||||||
|
success = self.nft_exec_dict(json_command)
|
||||||
|
if success == False:
|
||||||
|
self.logger.logCrit(f"Error adding {self.chain_name} isolation")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -1,610 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import atexit
|
|
||||||
import signal
|
|
||||||
import ipaddress
|
|
||||||
from collections import Counter
|
|
||||||
from random import randint
|
|
||||||
from threading import Thread
|
|
||||||
from threading import Lock
|
|
||||||
import redis
|
|
||||||
import json
|
|
||||||
import iptc
|
|
||||||
import dns.resolver
|
|
||||||
import dns.exception
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
|
|
||||||
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
|
|
||||||
if "".__eq__(redis_slaveof_ip):
|
|
||||||
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
|
|
||||||
else:
|
|
||||||
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
|
|
||||||
r.ping()
|
|
||||||
except Exception as ex:
|
|
||||||
print('%s - trying again in 3 seconds' % (ex))
|
|
||||||
time.sleep(3)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
pubsub = r.pubsub()
|
|
||||||
|
|
||||||
WHITELIST = []
|
|
||||||
BLACKLIST= []
|
|
||||||
|
|
||||||
bans = {}
|
|
||||||
|
|
||||||
quit_now = False
|
|
||||||
exit_code = 0
|
|
||||||
lock = Lock()
|
|
||||||
|
|
||||||
def log(priority, message):
|
|
||||||
tolog = {}
|
|
||||||
tolog['time'] = int(round(time.time()))
|
|
||||||
tolog['priority'] = priority
|
|
||||||
tolog['message'] = message
|
|
||||||
r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
|
||||||
print(message)
|
|
||||||
|
|
||||||
def logWarn(message):
|
|
||||||
log('warn', message)
|
|
||||||
|
|
||||||
def logCrit(message):
|
|
||||||
log('crit', message)
|
|
||||||
|
|
||||||
def logInfo(message):
|
|
||||||
log('info', message)
|
|
||||||
|
|
||||||
def refreshF2boptions():
|
|
||||||
global f2boptions
|
|
||||||
global quit_now
|
|
||||||
global exit_code
|
|
||||||
|
|
||||||
f2boptions = {}
|
|
||||||
|
|
||||||
if not r.get('F2B_OPTIONS'):
|
|
||||||
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
|
|
||||||
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
|
|
||||||
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
|
|
||||||
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
|
|
||||||
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
|
|
||||||
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
|
|
||||||
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
|
||||||
except ValueError:
|
|
||||||
print('Error loading F2B options: F2B_OPTIONS is not json')
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
|
|
||||||
verifyF2boptions(f2boptions)
|
|
||||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
|
||||||
|
|
||||||
def verifyF2boptions(f2boptions):
|
|
||||||
verifyF2boption(f2boptions,'ban_time', 1800)
|
|
||||||
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
|
||||||
verifyF2boption(f2boptions,'ban_time_increment', True)
|
|
||||||
verifyF2boption(f2boptions,'max_attempts', 10)
|
|
||||||
verifyF2boption(f2boptions,'retry_window', 600)
|
|
||||||
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
|
||||||
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
|
||||||
|
|
||||||
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
|
||||||
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
|
||||||
|
|
||||||
def refreshF2bregex():
|
|
||||||
global f2bregex
|
|
||||||
global quit_now
|
|
||||||
global exit_code
|
|
||||||
if not r.get('F2B_REGEX'):
|
|
||||||
f2bregex = {}
|
|
||||||
f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
|
|
||||||
f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
|
|
||||||
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
|
|
||||||
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
|
|
||||||
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
|
|
||||||
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
|
|
||||||
f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
|
||||||
f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
|
||||||
f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
|
|
||||||
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
|
|
||||||
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
f2bregex = {}
|
|
||||||
f2bregex = json.loads(r.get('F2B_REGEX'))
|
|
||||||
except ValueError:
|
|
||||||
print('Error loading F2B options: F2B_REGEX is not json')
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
|
|
||||||
if r.exists('F2B_LOG'):
|
|
||||||
r.rename('F2B_LOG', 'NETFILTER_LOG')
|
|
||||||
|
|
||||||
def mailcowChainOrder():
|
|
||||||
global lock
|
|
||||||
global quit_now
|
|
||||||
global exit_code
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
|
||||||
with lock:
|
|
||||||
filter4_table = iptc.Table(iptc.Table.FILTER)
|
|
||||||
filter6_table = iptc.Table6(iptc.Table6.FILTER)
|
|
||||||
filter4_table.refresh()
|
|
||||||
filter6_table.refresh()
|
|
||||||
for f in [filter4_table, filter6_table]:
|
|
||||||
forward_chain = iptc.Chain(f, 'FORWARD')
|
|
||||||
input_chain = iptc.Chain(f, 'INPUT')
|
|
||||||
for chain in [forward_chain, input_chain]:
|
|
||||||
target_found = False
|
|
||||||
for position, item in enumerate(chain.rules):
|
|
||||||
if item.target.name == 'MAILCOW':
|
|
||||||
target_found = True
|
|
||||||
if position > 2:
|
|
||||||
logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position))
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
if not target_found:
|
|
||||||
logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
|
|
||||||
def ban(address):
|
|
||||||
global lock
|
|
||||||
refreshF2boptions()
|
|
||||||
BAN_TIME = int(f2boptions['ban_time'])
|
|
||||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
|
||||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
|
||||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
|
||||||
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
|
||||||
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
|
||||||
|
|
||||||
ip = ipaddress.ip_address(address)
|
|
||||||
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
|
|
||||||
ip = ip.ipv4_mapped
|
|
||||||
address = str(ip)
|
|
||||||
if ip.is_private or ip.is_loopback:
|
|
||||||
return
|
|
||||||
|
|
||||||
self_network = ipaddress.ip_network(address)
|
|
||||||
|
|
||||||
with lock:
|
|
||||||
temp_whitelist = set(WHITELIST)
|
|
||||||
|
|
||||||
if temp_whitelist:
|
|
||||||
for wl_key in temp_whitelist:
|
|
||||||
wl_net = ipaddress.ip_network(wl_key, False)
|
|
||||||
if wl_net.overlaps(self_network):
|
|
||||||
logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
|
||||||
return
|
|
||||||
|
|
||||||
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
|
||||||
net = str(net)
|
|
||||||
|
|
||||||
if not net in bans:
|
|
||||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
|
||||||
|
|
||||||
bans[net]['attempts'] += 1
|
|
||||||
bans[net]['last_attempt'] = time.time()
|
|
||||||
|
|
||||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
|
||||||
cur_time = int(round(time.time()))
|
|
||||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
|
||||||
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
|
||||||
if type(ip) is ipaddress.IPv4Address:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules:
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
else:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules:
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
|
||||||
else:
|
|
||||||
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
|
||||||
|
|
||||||
def unban(net):
|
|
||||||
global lock
|
|
||||||
if not net in bans:
|
|
||||||
logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
|
||||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
|
||||||
return
|
|
||||||
logInfo('Unbanning %s' % net)
|
|
||||||
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule in chain.rules:
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
else:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule in chain.rules:
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
|
||||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
|
||||||
if net in bans:
|
|
||||||
bans[net]['attempts'] = 0
|
|
||||||
bans[net]['ban_counter'] += 1
|
|
||||||
|
|
||||||
def permBan(net, unban=False):
|
|
||||||
global lock
|
|
||||||
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules and not unban:
|
|
||||||
logCrit('Add host/network %s to blacklist' % net)
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
|
||||||
elif rule in chain.rules and unban:
|
|
||||||
logCrit('Remove host/network %s from blacklist' % net)
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
r.hdel('F2B_PERM_BANS', '%s' % net)
|
|
||||||
else:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules and not unban:
|
|
||||||
logCrit('Add host/network %s to blacklist' % net)
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
|
||||||
elif rule in chain.rules and unban:
|
|
||||||
logCrit('Remove host/network %s from blacklist' % net)
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
r.hdel('F2B_PERM_BANS', '%s' % net)
|
|
||||||
|
|
||||||
def quit(signum, frame):
|
|
||||||
global quit_now
|
|
||||||
quit_now = True
|
|
||||||
|
|
||||||
def clear():
|
|
||||||
global lock
|
|
||||||
logInfo('Clearing all bans')
|
|
||||||
for net in bans.copy():
|
|
||||||
unban(net)
|
|
||||||
with lock:
|
|
||||||
filter4_table = iptc.Table(iptc.Table.FILTER)
|
|
||||||
filter6_table = iptc.Table6(iptc.Table6.FILTER)
|
|
||||||
for filter_table in [filter4_table, filter6_table]:
|
|
||||||
filter_table.autocommit = False
|
|
||||||
forward_chain = iptc.Chain(filter_table, "FORWARD")
|
|
||||||
input_chain = iptc.Chain(filter_table, "INPUT")
|
|
||||||
mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
|
|
||||||
if mailcow_chain in filter_table.chains:
|
|
||||||
for rule in mailcow_chain.rules:
|
|
||||||
mailcow_chain.delete_rule(rule)
|
|
||||||
for rule in forward_chain.rules:
|
|
||||||
if rule.target.name == 'MAILCOW':
|
|
||||||
forward_chain.delete_rule(rule)
|
|
||||||
for rule in input_chain.rules:
|
|
||||||
if rule.target.name == 'MAILCOW':
|
|
||||||
input_chain.delete_rule(rule)
|
|
||||||
filter_table.delete_chain("MAILCOW")
|
|
||||||
filter_table.commit()
|
|
||||||
filter_table.refresh()
|
|
||||||
filter_table.autocommit = True
|
|
||||||
r.delete('F2B_ACTIVE_BANS')
|
|
||||||
r.delete('F2B_PERM_BANS')
|
|
||||||
pubsub.unsubscribe()
|
|
||||||
|
|
||||||
def watch():
|
|
||||||
logInfo('Watching Redis channel F2B_CHANNEL')
|
|
||||||
pubsub.subscribe('F2B_CHANNEL')
|
|
||||||
|
|
||||||
global quit_now
|
|
||||||
global exit_code
|
|
||||||
|
|
||||||
while not quit_now:
|
|
||||||
try:
|
|
||||||
for item in pubsub.listen():
|
|
||||||
refreshF2bregex()
|
|
||||||
for rule_id, rule_regex in f2bregex.items():
|
|
||||||
if item['data'] and item['type'] == 'message':
|
|
||||||
try:
|
|
||||||
result = re.search(rule_regex, item['data'])
|
|
||||||
except re.error:
|
|
||||||
result = False
|
|
||||||
if result:
|
|
||||||
addr = result.group(1)
|
|
||||||
ip = ipaddress.ip_address(addr)
|
|
||||||
if ip.is_private or ip.is_loopback:
|
|
||||||
continue
|
|
||||||
logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
|
||||||
ban(addr)
|
|
||||||
except Exception as ex:
|
|
||||||
logWarn('Error reading log line from pubsub: %s' % ex)
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
|
|
||||||
def snat4(snat_target):
|
|
||||||
global lock
|
|
||||||
global quit_now
|
|
||||||
|
|
||||||
def get_snat4_rule():
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
|
|
||||||
rule.dst = '!' + rule.src
|
|
||||||
target = rule.create_target("SNAT")
|
|
||||||
target.to_source = snat_target
|
|
||||||
match = rule.create_match("comment")
|
|
||||||
match.comment = f'{int(round(time.time()))}'
|
|
||||||
return rule
|
|
||||||
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
|
||||||
with lock:
|
|
||||||
try:
|
|
||||||
table = iptc.Table('nat')
|
|
||||||
table.refresh()
|
|
||||||
chain = iptc.Chain(table, 'POSTROUTING')
|
|
||||||
table.autocommit = False
|
|
||||||
new_rule = get_snat4_rule()
|
|
||||||
|
|
||||||
if not chain.rules:
|
|
||||||
# if there are no rules in the chain, insert the new rule directly
|
|
||||||
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
|
||||||
chain.insert_rule(new_rule)
|
|
||||||
else:
|
|
||||||
for position, rule in enumerate(chain.rules):
|
|
||||||
if not hasattr(rule.target, 'parameter'):
|
|
||||||
continue
|
|
||||||
match = all((
|
|
||||||
new_rule.get_src() == rule.get_src(),
|
|
||||||
new_rule.get_dst() == rule.get_dst(),
|
|
||||||
new_rule.target.parameters == rule.target.parameters,
|
|
||||||
new_rule.target.name == rule.target.name
|
|
||||||
))
|
|
||||||
if position == 0:
|
|
||||||
if not match:
|
|
||||||
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
|
||||||
chain.insert_rule(new_rule)
|
|
||||||
else:
|
|
||||||
if match:
|
|
||||||
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
|
|
||||||
table.commit()
|
|
||||||
table.autocommit = True
|
|
||||||
except:
|
|
||||||
print('Error running SNAT4, retrying...')
|
|
||||||
|
|
||||||
def snat6(snat_target):
|
|
||||||
global lock
|
|
||||||
global quit_now
|
|
||||||
|
|
||||||
def get_snat6_rule():
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
|
|
||||||
rule.dst = '!' + rule.src
|
|
||||||
target = rule.create_target("SNAT")
|
|
||||||
target.to_source = snat_target
|
|
||||||
return rule
|
|
||||||
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
|
||||||
with lock:
|
|
||||||
try:
|
|
||||||
table = iptc.Table6('nat')
|
|
||||||
table.refresh()
|
|
||||||
chain = iptc.Chain(table, 'POSTROUTING')
|
|
||||||
table.autocommit = False
|
|
||||||
if get_snat6_rule() not in chain.rules:
|
|
||||||
logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))
|
|
||||||
chain.insert_rule(get_snat6_rule())
|
|
||||||
table.commit()
|
|
||||||
else:
|
|
||||||
for position, item in enumerate(chain.rules):
|
|
||||||
if item == get_snat6_rule():
|
|
||||||
if position != 0:
|
|
||||||
chain.delete_rule(get_snat6_rule())
|
|
||||||
table.commit()
|
|
||||||
table.autocommit = True
|
|
||||||
except:
|
|
||||||
print('Error running SNAT6, retrying...')
|
|
||||||
|
|
||||||
def autopurge():
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
|
||||||
refreshF2boptions()
|
|
||||||
BAN_TIME = int(f2boptions['ban_time'])
|
|
||||||
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
|
|
||||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
|
||||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
|
||||||
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
|
||||||
if QUEUE_UNBAN:
|
|
||||||
for net in QUEUE_UNBAN:
|
|
||||||
unban(str(net))
|
|
||||||
for net in bans.copy():
|
|
||||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
|
||||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
|
||||||
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
|
||||||
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
|
|
||||||
unban(net)
|
|
||||||
|
|
||||||
def isIpNetwork(address):
|
|
||||||
try:
|
|
||||||
ipaddress.ip_network(address, False)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def genNetworkList(list):
|
|
||||||
resolver = dns.resolver.Resolver()
|
|
||||||
hostnames = []
|
|
||||||
networks = []
|
|
||||||
for key in list:
|
|
||||||
if isIpNetwork(key):
|
|
||||||
networks.append(key)
|
|
||||||
else:
|
|
||||||
hostnames.append(key)
|
|
||||||
for hostname in hostnames:
|
|
||||||
hostname_ips = []
|
|
||||||
for rdtype in ['A', 'AAAA']:
|
|
||||||
try:
|
|
||||||
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
|
|
||||||
except dns.exception.Timeout:
|
|
||||||
logInfo('Hostname %s timedout on resolve' % hostname)
|
|
||||||
break
|
|
||||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
|
||||||
continue
|
|
||||||
except dns.exception.DNSException as dnsexception:
|
|
||||||
logInfo('%s' % dnsexception)
|
|
||||||
continue
|
|
||||||
for rdata in answer:
|
|
||||||
hostname_ips.append(rdata.to_text())
|
|
||||||
networks.extend(hostname_ips)
|
|
||||||
return set(networks)
|
|
||||||
|
|
||||||
def whitelistUpdate():
|
|
||||||
global lock
|
|
||||||
global quit_now
|
|
||||||
global WHITELIST
|
|
||||||
while not quit_now:
|
|
||||||
start_time = time.time()
|
|
||||||
list = r.hgetall('F2B_WHITELIST')
|
|
||||||
new_whitelist = []
|
|
||||||
if list:
|
|
||||||
new_whitelist = genNetworkList(list)
|
|
||||||
with lock:
|
|
||||||
if Counter(new_whitelist) != Counter(WHITELIST):
|
|
||||||
WHITELIST = new_whitelist
|
|
||||||
logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
|
||||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
|
||||||
|
|
||||||
def blacklistUpdate():
|
|
||||||
global quit_now
|
|
||||||
global BLACKLIST
|
|
||||||
while not quit_now:
|
|
||||||
start_time = time.time()
|
|
||||||
list = r.hgetall('F2B_BLACKLIST')
|
|
||||||
new_blacklist = []
|
|
||||||
if list:
|
|
||||||
new_blacklist = genNetworkList(list)
|
|
||||||
if Counter(new_blacklist) != Counter(BLACKLIST):
|
|
||||||
addban = set(new_blacklist).difference(BLACKLIST)
|
|
||||||
delban = set(BLACKLIST).difference(new_blacklist)
|
|
||||||
BLACKLIST = new_blacklist
|
|
||||||
logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
|
||||||
if addban:
|
|
||||||
for net in addban:
|
|
||||||
permBan(net=net)
|
|
||||||
if delban:
|
|
||||||
for net in delban:
|
|
||||||
permBan(net=net, unban=True)
|
|
||||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
|
||||||
|
|
||||||
def initChain():
|
|
||||||
# Is called before threads start, no locking
|
|
||||||
print("Initializing mailcow netfilter chain")
|
|
||||||
# IPv4
|
|
||||||
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
|
|
||||||
iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
|
|
||||||
for c in ['FORWARD', 'INPUT']:
|
|
||||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = '0.0.0.0/0'
|
|
||||||
rule.dst = '0.0.0.0/0'
|
|
||||||
target = iptc.Target(rule, "MAILCOW")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules:
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
# IPv6
|
|
||||||
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
|
|
||||||
iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
|
|
||||||
for c in ['FORWARD', 'INPUT']:
|
|
||||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = '::/0'
|
|
||||||
rule.dst = '::/0'
|
|
||||||
target = iptc.Target(rule, "MAILCOW")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules:
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
# In case a previous session was killed without cleanup
|
|
||||||
clear()
|
|
||||||
# Reinit MAILCOW chain
|
|
||||||
initChain()
|
|
||||||
|
|
||||||
watch_thread = Thread(target=watch)
|
|
||||||
watch_thread.daemon = True
|
|
||||||
watch_thread.start()
|
|
||||||
|
|
||||||
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
|
|
||||||
try:
|
|
||||||
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
|
||||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
|
||||||
if type(snat_ipo) is ipaddress.IPv4Address:
|
|
||||||
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
|
||||||
snat4_thread.daemon = True
|
|
||||||
snat4_thread.start()
|
|
||||||
except ValueError:
|
|
||||||
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
|
|
||||||
|
|
||||||
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
|
|
||||||
try:
|
|
||||||
snat_ip = os.getenv('SNAT6_TO_SOURCE')
|
|
||||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
|
||||||
if type(snat_ipo) is ipaddress.IPv6Address:
|
|
||||||
snat6_thread = Thread(target=snat6,args=(snat_ip,))
|
|
||||||
snat6_thread.daemon = True
|
|
||||||
snat6_thread.start()
|
|
||||||
except ValueError:
|
|
||||||
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
|
|
||||||
|
|
||||||
autopurge_thread = Thread(target=autopurge)
|
|
||||||
autopurge_thread.daemon = True
|
|
||||||
autopurge_thread.start()
|
|
||||||
|
|
||||||
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
|
|
||||||
mailcowchainwatch_thread.daemon = True
|
|
||||||
mailcowchainwatch_thread.start()
|
|
||||||
|
|
||||||
blacklistupdate_thread = Thread(target=blacklistUpdate)
|
|
||||||
blacklistupdate_thread.daemon = True
|
|
||||||
blacklistupdate_thread.start()
|
|
||||||
|
|
||||||
whitelistupdate_thread = Thread(target=whitelistUpdate)
|
|
||||||
whitelistupdate_thread.daemon = True
|
|
||||||
whitelistupdate_thread.start()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, quit)
|
|
||||||
atexit.register(clear)
|
|
||||||
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.20
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
#RUN addgroup -S olefy && adduser -S olefy -G olefy \
|
#RUN addgroup -S olefy && adduser -S olefy -G olefy \
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM php:8.2-fpm-alpine3.17
|
FROM php:8.2-fpm-alpine3.20
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG APCU_PECL_VERSION=5.1.22
|
ARG APCU_PECL_VERSION=5.1.23
|
||||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG IMAGICK_PECL_VERSION=3.7.0
|
ARG IMAGICK_PECL_VERSION=3.7.0
|
||||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
@@ -10,9 +10,9 @@ ARG MAILPARSE_PECL_VERSION=3.1.6
|
|||||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG MEMCACHED_PECL_VERSION=3.2.0
|
ARG MEMCACHED_PECL_VERSION=3.2.0
|
||||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG REDIS_PECL_VERSION=6.0.1
|
ARG REDIS_PECL_VERSION=6.0.2
|
||||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG COMPOSER_VERSION=2.6.5
|
ARG COMPOSER_VERSION=2.6.6
|
||||||
|
|
||||||
RUN apk add -U --no-cache autoconf \
|
RUN apk add -U --no-cache autoconf \
|
||||||
aspell-dev \
|
aspell-dev \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM debian:bullseye-slim
|
FROM debian:bookworm-slim
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|||||||
@@ -415,12 +415,6 @@ postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
|
|||||||
b.barracudacentral.org=127.0.0.2*7
|
b.barracudacentral.org=127.0.0.2*7
|
||||||
bl.mailspike.net=127.0.0.2*5
|
bl.mailspike.net=127.0.0.2*5
|
||||||
bl.mailspike.net=127.0.0.[10;11;12]*4
|
bl.mailspike.net=127.0.0.[10;11;12]*4
|
||||||
dnsbl.sorbs.net=127.0.0.10*8
|
|
||||||
dnsbl.sorbs.net=127.0.0.5*6
|
|
||||||
dnsbl.sorbs.net=127.0.0.7*3
|
|
||||||
dnsbl.sorbs.net=127.0.0.8*2
|
|
||||||
dnsbl.sorbs.net=127.0.0.6*2
|
|
||||||
dnsbl.sorbs.net=127.0.0.9*2
|
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
|
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.28
|
@version: 3.38
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.28
|
@version: 3.38
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ FROM debian:bullseye-slim
|
|||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG RSPAMD_VER=rspamd_3.7.5-2~8c86c1676
|
||||||
ARG CODENAME=bullseye
|
ARG CODENAME=bullseye
|
||||||
ENV LC_ALL C
|
ENV LC_ALL C
|
||||||
|
|
||||||
@@ -12,11 +13,14 @@ RUN apt-get update && apt-get install -y \
|
|||||||
apt-transport-https \
|
apt-transport-https \
|
||||||
dnsutils \
|
dnsutils \
|
||||||
netcat \
|
netcat \
|
||||||
&& apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
|
wget \
|
||||||
&& echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
|
redis-tools \
|
||||||
&& apt-get update \
|
procps \
|
||||||
&& apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
|
nano \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
|
||||||
|
&& wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\
|
||||||
|
&& apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/*\
|
||||||
&& apt-get autoremove --purge \
|
&& apt-get autoremove --purge \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& mkdir -p /run/rspamd \
|
&& mkdir -p /run/rspamd \
|
||||||
@@ -25,7 +29,6 @@ RUN apt-get update && apt-get install -y \
|
|||||||
&& sed -i 's/#analysis_keyword_table > 0/analysis_cat_table.macro_exist == "M"/g' /usr/share/rspamd/lualib/lua_scanners/oletools.lua
|
&& sed -i 's/#analysis_keyword_table > 0/analysis_cat_table.macro_exist == "M"/g' /usr/share/rspamd/lualib/lua_scanners/oletools.lua
|
||||||
|
|
||||||
COPY settings.conf /etc/rspamd/settings.conf
|
COPY settings.conf /etc/rspamd/settings.conf
|
||||||
COPY metadata_exporter.lua /usr/share/rspamd/plugins/metadata_exporter.lua
|
|
||||||
COPY set_worker_password.sh /set_worker_password.sh
|
COPY set_worker_password.sh /set_worker_password.sh
|
||||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ EOF
|
|||||||
redis-cli -h redis-mailcow SLAVEOF NO ONE
|
redis-cli -h redis-mailcow SLAVEOF NO ONE
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Provide additional lua modules
|
||||||
|
ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so
|
||||||
|
|
||||||
chown -R _rspamd:_rspamd /var/lib/rspamd \
|
chown -R _rspamd:_rspamd /var/lib/rspamd \
|
||||||
/etc/rspamd/local.d \
|
/etc/rspamd/local.d \
|
||||||
/etc/rspamd/override.d \
|
/etc/rspamd/override.d \
|
||||||
|
|||||||
@@ -1,632 +0,0 @@
|
|||||||
--[[
|
|
||||||
Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
|
|
||||||
Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
]]--
|
|
||||||
|
|
||||||
if confighelp then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A plugin that pushes metadata (or whole messages) to external services
|
|
||||||
|
|
||||||
local redis_params
|
|
||||||
local lua_util = require "lua_util"
|
|
||||||
local rspamd_http = require "rspamd_http"
|
|
||||||
local rspamd_util = require "rspamd_util"
|
|
||||||
local rspamd_logger = require "rspamd_logger"
|
|
||||||
local ucl = require "ucl"
|
|
||||||
local E = {}
|
|
||||||
local N = 'metadata_exporter'
|
|
||||||
|
|
||||||
local settings = {
|
|
||||||
pusher_enabled = {},
|
|
||||||
pusher_format = {},
|
|
||||||
pusher_select = {},
|
|
||||||
mime_type = 'text/plain',
|
|
||||||
defer = false,
|
|
||||||
mail_from = '',
|
|
||||||
mail_to = 'postmaster@localhost',
|
|
||||||
helo = 'rspamd',
|
|
||||||
email_template = [[From: "Rspamd" <$mail_from>
|
|
||||||
To: $mail_to
|
|
||||||
Subject: Spam alert
|
|
||||||
Date: $date
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Message-ID: <$our_message_id>
|
|
||||||
Content-type: text/plain; charset=utf-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
|
|
||||||
Authenticated username: $user
|
|
||||||
IP: $ip
|
|
||||||
Queue ID: $qid
|
|
||||||
SMTP FROM: $from
|
|
||||||
SMTP RCPT: $rcpt
|
|
||||||
MIME From: $header_from
|
|
||||||
MIME To: $header_to
|
|
||||||
MIME Date: $header_date
|
|
||||||
Subject: $header_subject
|
|
||||||
Message-ID: $message_id
|
|
||||||
Action: $action
|
|
||||||
Score: $score
|
|
||||||
Symbols: $symbols]],
|
|
||||||
}
|
|
||||||
|
|
||||||
local function get_general_metadata(task, flatten, no_content)
|
|
||||||
local r = {}
|
|
||||||
local ip = task:get_from_ip()
|
|
||||||
if ip and ip:is_valid() then
|
|
||||||
r.ip = tostring(ip)
|
|
||||||
else
|
|
||||||
r.ip = 'unknown'
|
|
||||||
end
|
|
||||||
r.user = task:get_user() or 'unknown'
|
|
||||||
r.qid = task:get_queue_id() or 'unknown'
|
|
||||||
r.subject = task:get_subject() or 'unknown'
|
|
||||||
r.action = task:get_metric_action('default')
|
|
||||||
|
|
||||||
local s = task:get_metric_score('default')[1]
|
|
||||||
r.score = flatten and string.format('%.2f', s) or s
|
|
||||||
|
|
||||||
local fuzzy = task:get_mempool():get_variable("fuzzy_hashes", "fstrings")
|
|
||||||
if fuzzy and #fuzzy > 0 then
|
|
||||||
local fz = {}
|
|
||||||
for _,h in ipairs(fuzzy) do
|
|
||||||
table.insert(fz, h)
|
|
||||||
end
|
|
||||||
if not flatten then
|
|
||||||
r.fuzzy = fz
|
|
||||||
else
|
|
||||||
r.fuzzy = table.concat(fz, ', ')
|
|
||||||
end
|
|
||||||
else
|
|
||||||
r.fuzzy = 'unknown'
|
|
||||||
end
|
|
||||||
|
|
||||||
local rcpt = task:get_recipients('smtp')
|
|
||||||
if rcpt then
|
|
||||||
local l = {}
|
|
||||||
for _, a in ipairs(rcpt) do
|
|
||||||
table.insert(l, a['addr'])
|
|
||||||
end
|
|
||||||
if not flatten then
|
|
||||||
r.rcpt = l
|
|
||||||
else
|
|
||||||
r.rcpt = table.concat(l, ', ')
|
|
||||||
end
|
|
||||||
else
|
|
||||||
r.rcpt = 'unknown'
|
|
||||||
end
|
|
||||||
local from = task:get_from('smtp')
|
|
||||||
if ((from or E)[1] or E).addr then
|
|
||||||
r.from = from[1].addr
|
|
||||||
else
|
|
||||||
r.from = 'unknown'
|
|
||||||
end
|
|
||||||
local syminf = task:get_symbols_all()
|
|
||||||
if flatten then
|
|
||||||
local l = {}
|
|
||||||
for _, sym in ipairs(syminf) do
|
|
||||||
local txt
|
|
||||||
if sym.options then
|
|
||||||
local topt = table.concat(sym.options, ', ')
|
|
||||||
txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')' .. ' [' .. topt .. ']'
|
|
||||||
else
|
|
||||||
txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')'
|
|
||||||
end
|
|
||||||
table.insert(l, txt)
|
|
||||||
end
|
|
||||||
r.symbols = table.concat(l, '\n\t')
|
|
||||||
else
|
|
||||||
r.symbols = syminf
|
|
||||||
end
|
|
||||||
local function process_header(name)
|
|
||||||
local hdr = task:get_header_full(name)
|
|
||||||
if hdr then
|
|
||||||
local l = {}
|
|
||||||
for _, h in ipairs(hdr) do
|
|
||||||
table.insert(l, h.decoded)
|
|
||||||
end
|
|
||||||
if not flatten then
|
|
||||||
return l
|
|
||||||
else
|
|
||||||
return table.concat(l, '\n')
|
|
||||||
end
|
|
||||||
else
|
|
||||||
return 'unknown'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not no_content then
|
|
||||||
r.header_from = process_header('from')
|
|
||||||
r.header_to = process_header('to')
|
|
||||||
r.header_subject = process_header('subject')
|
|
||||||
r.header_date = process_header('date')
|
|
||||||
r.message_id = task:get_message_id()
|
|
||||||
end
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
|
|
||||||
local formatters = {
|
|
||||||
default = function(task)
|
|
||||||
return task:get_content(), {}
|
|
||||||
end,
|
|
||||||
email_alert = function(task, rule, extra)
|
|
||||||
local meta = get_general_metadata(task, true)
|
|
||||||
local display_emails = {}
|
|
||||||
local mail_targets = {}
|
|
||||||
meta.mail_from = rule.mail_from or settings.mail_from
|
|
||||||
local mail_rcpt = rule.mail_to or settings.mail_to
|
|
||||||
if type(mail_rcpt) ~= 'table' then
|
|
||||||
table.insert(display_emails, string.format('<%s>', mail_rcpt))
|
|
||||||
table.insert(mail_targets, mail_rcpt)
|
|
||||||
else
|
|
||||||
for _, e in ipairs(mail_rcpt) do
|
|
||||||
table.insert(display_emails, string.format('<%s>', e))
|
|
||||||
table.insert(mail_targets, mail_rcpt)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if rule.email_alert_sender then
|
|
||||||
local x = task:get_from('smtp')
|
|
||||||
if x and string.len(x[1].addr) > 0 then
|
|
||||||
table.insert(mail_targets, x)
|
|
||||||
table.insert(display_emails, string.format('<%s>', x[1].addr))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if rule.email_alert_user then
|
|
||||||
local x = task:get_user()
|
|
||||||
if x then
|
|
||||||
table.insert(mail_targets, x)
|
|
||||||
table.insert(display_emails, string.format('<%s>', x))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if rule.email_alert_recipients then
|
|
||||||
local x = task:get_recipients('smtp')
|
|
||||||
if x then
|
|
||||||
for _, e in ipairs(x) do
|
|
||||||
if string.len(e.addr) > 0 then
|
|
||||||
table.insert(mail_targets, e.addr)
|
|
||||||
table.insert(display_emails, string.format('<%s>', e.addr))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
meta.mail_to = table.concat(display_emails, ', ')
|
|
||||||
meta.our_message_id = rspamd_util.random_hex(12) .. '@rspamd'
|
|
||||||
meta.date = rspamd_util.time_to_string(rspamd_util.get_time())
|
|
||||||
return lua_util.template(rule.email_template or settings.email_template, meta), { mail_targets = mail_targets}
|
|
||||||
end,
|
|
||||||
json = function(task)
|
|
||||||
return ucl.to_format(get_general_metadata(task), 'json-compact')
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
local function is_spam(action)
|
|
||||||
return (action == 'reject' or action == 'add header' or action == 'rewrite subject')
|
|
||||||
end
|
|
||||||
|
|
||||||
local selectors = {
|
|
||||||
default = function(task)
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
is_spam = function(task)
|
|
||||||
local action = task:get_metric_action('default')
|
|
||||||
return is_spam(action)
|
|
||||||
end,
|
|
||||||
is_spam_authed = function(task)
|
|
||||||
if not task:get_user() then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
local action = task:get_metric_action('default')
|
|
||||||
return is_spam(action)
|
|
||||||
end,
|
|
||||||
is_reject = function(task)
|
|
||||||
local action = task:get_metric_action('default')
|
|
||||||
return (action == 'reject')
|
|
||||||
end,
|
|
||||||
is_reject_authed = function(task)
|
|
||||||
if not task:get_user() then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
local action = task:get_metric_action('default')
|
|
||||||
return (action == 'reject')
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
local function maybe_defer(task, rule)
|
|
||||||
if rule.defer then
|
|
||||||
rspamd_logger.warnx(task, 'deferring message')
|
|
||||||
task:set_pre_result('soft reject', 'deferred', N)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local pushers = {
|
|
||||||
redis_pubsub = function(task, formatted, rule)
|
|
||||||
local _,ret,upstream
|
|
||||||
local function redis_pub_cb(err)
|
|
||||||
if err then
|
|
||||||
rspamd_logger.errx(task, 'got error %s when publishing on server %s',
|
|
||||||
err, upstream:get_addr())
|
|
||||||
return maybe_defer(task, rule)
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
ret,_,upstream = rspamd_redis_make_request(task,
|
|
||||||
redis_params, -- connect params
|
|
||||||
nil, -- hash key
|
|
||||||
true, -- is write
|
|
||||||
redis_pub_cb, --callback
|
|
||||||
'PUBLISH', -- command
|
|
||||||
{rule.channel, formatted} -- arguments
|
|
||||||
)
|
|
||||||
if not ret then
|
|
||||||
rspamd_logger.errx(task, 'error connecting to redis')
|
|
||||||
maybe_defer(task, rule)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
http = function(task, formatted, rule)
|
|
||||||
local function http_callback(err, code)
|
|
||||||
if err then
|
|
||||||
rspamd_logger.errx(task, 'got error %s in http callback', err)
|
|
||||||
return maybe_defer(task, rule)
|
|
||||||
end
|
|
||||||
if code ~= 200 then
|
|
||||||
rspamd_logger.errx(task, 'got unexpected http status: %s', code)
|
|
||||||
return maybe_defer(task, rule)
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
local hdrs = {}
|
|
||||||
if rule.meta_headers then
|
|
||||||
local gm = get_general_metadata(task, false, true)
|
|
||||||
local pfx = rule.meta_header_prefix or 'X-Rspamd-'
|
|
||||||
for k, v in pairs(gm) do
|
|
||||||
if type(v) == 'table' then
|
|
||||||
hdrs[pfx .. k] = ucl.to_format(v, 'json-compact')
|
|
||||||
else
|
|
||||||
hdrs[pfx .. k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rspamd_http.request({
|
|
||||||
task=task,
|
|
||||||
url=rule.url,
|
|
||||||
body=formatted,
|
|
||||||
callback=http_callback,
|
|
||||||
mime_type=rule.mime_type or settings.mime_type,
|
|
||||||
headers=hdrs,
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
send_mail = function(task, formatted, rule, extra)
|
|
||||||
local lua_smtp = require "lua_smtp"
|
|
||||||
local function sendmail_cb(ret, err)
|
|
||||||
if not ret then
|
|
||||||
rspamd_logger.errx(task, 'SMTP export error: %s', err)
|
|
||||||
maybe_defer(task, rule)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
lua_smtp.sendmail({
|
|
||||||
task = task,
|
|
||||||
host = rule.smtp,
|
|
||||||
port = rule.smtp_port or settings.smtp_port or 25,
|
|
||||||
from = rule.mail_from or settings.mail_from,
|
|
||||||
recipients = extra.mail_targets or rule.mail_to or settings.mail_to,
|
|
||||||
helo = rule.helo or settings.helo,
|
|
||||||
timeout = rule.timeout or settings.timeout,
|
|
||||||
}, formatted, sendmail_cb)
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
local opts = rspamd_config:get_all_opt(N)
|
|
||||||
if not opts then return end
|
|
||||||
local process_settings = {
|
|
||||||
select = function(val)
|
|
||||||
selectors.custom = assert(load(val))()
|
|
||||||
end,
|
|
||||||
format = function(val)
|
|
||||||
formatters.custom = assert(load(val))()
|
|
||||||
end,
|
|
||||||
push = function(val)
|
|
||||||
pushers.custom = assert(load(val))()
|
|
||||||
end,
|
|
||||||
custom_push = function(val)
|
|
||||||
if type(val) == 'table' then
|
|
||||||
for k, v in pairs(val) do
|
|
||||||
pushers[k] = assert(load(v))()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
custom_select = function(val)
|
|
||||||
if type(val) == 'table' then
|
|
||||||
for k, v in pairs(val) do
|
|
||||||
selectors[k] = assert(load(v))()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
custom_format = function(val)
|
|
||||||
if type(val) == 'table' then
|
|
||||||
for k, v in pairs(val) do
|
|
||||||
formatters[k] = assert(load(v))()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
pusher_enabled = function(val)
|
|
||||||
if type(val) == 'string' then
|
|
||||||
if pushers[val] then
|
|
||||||
settings.pusher_enabled[val] = true
|
|
||||||
else
|
|
||||||
rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
|
|
||||||
end
|
|
||||||
elseif type(val) == 'table' then
|
|
||||||
for _, v in ipairs(val) do
|
|
||||||
if pushers[v] then
|
|
||||||
settings.pusher_enabled[v] = true
|
|
||||||
else
|
|
||||||
rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
for k, v in pairs(opts) do
|
|
||||||
local f = process_settings[k]
|
|
||||||
if f then
|
|
||||||
f(opts[k])
|
|
||||||
else
|
|
||||||
settings[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if type(settings.rules) ~= 'table' then
|
|
||||||
-- Legacy config
|
|
||||||
settings.rules = {}
|
|
||||||
if not next(settings.pusher_enabled) then
|
|
||||||
if pushers.custom then
|
|
||||||
rspamd_logger.infox(rspamd_config, 'Custom pusher implicitly enabled')
|
|
||||||
settings.pusher_enabled.custom = true
|
|
||||||
else
|
|
||||||
-- Check legacy options
|
|
||||||
if settings.url then
|
|
||||||
rspamd_logger.warnx(rspamd_config, 'HTTP pusher implicitly enabled')
|
|
||||||
settings.pusher_enabled.http = true
|
|
||||||
end
|
|
||||||
if settings.channel then
|
|
||||||
rspamd_logger.warnx(rspamd_config, 'Redis Pubsub pusher implicitly enabled')
|
|
||||||
settings.pusher_enabled.redis_pubsub = true
|
|
||||||
end
|
|
||||||
if settings.smtp and settings.mail_to then
|
|
||||||
rspamd_logger.warnx(rspamd_config, 'SMTP pusher implicitly enabled')
|
|
||||||
settings.pusher_enabled.send_mail = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not next(settings.pusher_enabled) then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No push backend enabled')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if settings.formatter then
|
|
||||||
settings.format = formatters[settings.formatter]
|
|
||||||
if not settings.format then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No such formatter: %s', settings.formatter)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if settings.selector then
|
|
||||||
settings.select = selectors[settings.selector]
|
|
||||||
if not settings.select then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No such selector: %s', settings.selector)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for k in pairs(settings.pusher_enabled) do
|
|
||||||
local formatter = settings.pusher_format[k]
|
|
||||||
local selector = settings.pusher_select[k]
|
|
||||||
if not formatter then
|
|
||||||
settings.pusher_format[k] = settings.formatter or 'default'
|
|
||||||
rspamd_logger.infox(rspamd_config, 'Using default formatter for %s pusher', k)
|
|
||||||
else
|
|
||||||
if not formatters[formatter] then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No such formatter: %s - disabling %s', formatter, k)
|
|
||||||
settings.pusher_enabled.k = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not selector then
|
|
||||||
settings.pusher_select[k] = settings.selector or 'default'
|
|
||||||
rspamd_logger.infox(rspamd_config, 'Using default selector for %s pusher', k)
|
|
||||||
else
|
|
||||||
if not selectors[selector] then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No such selector: %s - disabling %s', selector, k)
|
|
||||||
settings.pusher_enabled.k = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if settings.pusher_enabled.redis_pubsub then
|
|
||||||
redis_params = rspamd_parse_redis_server(N)
|
|
||||||
if not redis_params then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
|
|
||||||
settings.pusher_enabled.redis_pubsub = nil
|
|
||||||
else
|
|
||||||
local r = {}
|
|
||||||
r.backend = 'redis_pubsub'
|
|
||||||
r.channel = settings.channel
|
|
||||||
r.defer = settings.defer
|
|
||||||
r.selector = settings.pusher_select.redis_pubsub
|
|
||||||
r.formatter = settings.pusher_format.redis_pubsub
|
|
||||||
settings.rules[r.backend:upper()] = r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if settings.pusher_enabled.http then
|
|
||||||
if not settings.url then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No URL is specified')
|
|
||||||
settings.pusher_enabled.http = nil
|
|
||||||
else
|
|
||||||
local r = {}
|
|
||||||
r.backend = 'http'
|
|
||||||
r.url = settings.url
|
|
||||||
r.mime_type = settings.mime_type
|
|
||||||
r.defer = settings.defer
|
|
||||||
r.selector = settings.pusher_select.http
|
|
||||||
r.formatter = settings.pusher_format.http
|
|
||||||
settings.rules[r.backend:upper()] = r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if settings.pusher_enabled.send_mail then
|
|
||||||
if not (settings.mail_to and settings.smtp) then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No mail_to and/or smtp setting is specified')
|
|
||||||
settings.pusher_enabled.send_mail = nil
|
|
||||||
else
|
|
||||||
local r = {}
|
|
||||||
r.backend = 'send_mail'
|
|
||||||
r.mail_to = settings.mail_to
|
|
||||||
r.mail_from = settings.mail_from
|
|
||||||
r.helo = settings.hello
|
|
||||||
r.smtp = settings.smtp
|
|
||||||
r.smtp_port = settings.smtp_port
|
|
||||||
r.email_template = settings.email_template
|
|
||||||
r.defer = settings.defer
|
|
||||||
r.selector = settings.pusher_select.send_mail
|
|
||||||
r.formatter = settings.pusher_format.send_mail
|
|
||||||
settings.rules[r.backend:upper()] = r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not next(settings.pusher_enabled) then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No push backend enabled')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
elseif not next(settings.rules) then
|
|
||||||
lua_util.debugm(N, rspamd_config, 'No rules enabled')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not settings.rules or not next(settings.rules) then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No rules enabled')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local backend_required_elements = {
|
|
||||||
http = {
|
|
||||||
'url',
|
|
||||||
},
|
|
||||||
smtp = {
|
|
||||||
'mail_to',
|
|
||||||
'smtp',
|
|
||||||
},
|
|
||||||
redis_pubsub = {
|
|
||||||
'channel',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
local check_element = {
|
|
||||||
selector = function(k, v)
|
|
||||||
if not selectors[v] then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid selector %s', k, v)
|
|
||||||
return false
|
|
||||||
else
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
formatter = function(k, v)
|
|
||||||
if not formatters[v] then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid formatter %s', k, v)
|
|
||||||
return false
|
|
||||||
else
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
local backend_check = {
|
|
||||||
default = function(k, rule)
|
|
||||||
local reqset = backend_required_elements[rule.backend]
|
|
||||||
if reqset then
|
|
||||||
for _, e in ipairs(reqset) do
|
|
||||||
if not rule[e] then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'Rule %s misses required setting %s', k, e)
|
|
||||||
settings.rules[k] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for sett, v in pairs(rule) do
|
|
||||||
local f = check_element[sett]
|
|
||||||
if f then
|
|
||||||
if not f(sett, v) then
|
|
||||||
settings.rules[k] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
backend_check.redis_pubsub = function(k, rule)
|
|
||||||
if not redis_params then
|
|
||||||
redis_params = rspamd_parse_redis_server(N)
|
|
||||||
end
|
|
||||||
if not redis_params then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
|
|
||||||
settings.rules[k] = nil
|
|
||||||
else
|
|
||||||
backend_check.default(k, rule)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
setmetatable(backend_check, {
|
|
||||||
__index = function()
|
|
||||||
return backend_check.default
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
for k, v in pairs(settings.rules) do
|
|
||||||
if type(v) == 'table' then
|
|
||||||
local backend = v.backend
|
|
||||||
if not backend then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'Rule %s has no backend', k)
|
|
||||||
settings.rules[k] = nil
|
|
||||||
elseif not pushers[backend] then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid backend %s', k, backend)
|
|
||||||
settings.rules[k] = nil
|
|
||||||
else
|
|
||||||
local f = backend_check[backend]
|
|
||||||
f(k, v)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
rspamd_logger.errx(rspamd_config, 'Rule %s has bad type: %s', k, type(v))
|
|
||||||
settings.rules[k] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function gen_exporter(rule)
|
|
||||||
return function (task)
|
|
||||||
if task:has_flag('skip') then return end
|
|
||||||
local selector = rule.selector or 'default'
|
|
||||||
local selected = selectors[selector](task)
|
|
||||||
if selected then
|
|
||||||
lua_util.debugm(N, task, 'Message selected for processing')
|
|
||||||
local formatter = rule.formatter or 'default'
|
|
||||||
local formatted, extra = formatters[formatter](task, rule)
|
|
||||||
if formatted then
|
|
||||||
pushers[rule.backend](task, formatted, rule, extra)
|
|
||||||
else
|
|
||||||
lua_util.debugm(N, task, 'Formatter [%s] returned non-truthy value [%s]', formatter, formatted)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
lua_util.debugm(N, task, 'Selector [%s] returned non-truthy value [%s]', selector, selected)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not next(settings.rules) then
|
|
||||||
rspamd_logger.errx(rspamd_config, 'No rules enabled')
|
|
||||||
lua_util.disable_module(N, "config")
|
|
||||||
end
|
|
||||||
for k, r in pairs(settings.rules) do
|
|
||||||
rspamd_config:register_symbol({
|
|
||||||
name = 'EXPORT_METADATA_' .. k,
|
|
||||||
type = 'idempotent',
|
|
||||||
callback = gen_exporter(r),
|
|
||||||
priority = 10,
|
|
||||||
flags = 'empty,explicit_disable,ignore_passthrough',
|
|
||||||
})
|
|
||||||
end
|
|
||||||
@@ -2,9 +2,10 @@ FROM debian:bullseye-slim
|
|||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
|
ARG DEBIAN_VERSION=bullseye
|
||||||
|
ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
ARG GOSU_VERSION=1.16
|
ARG GOSU_VERSION=1.17
|
||||||
ENV LC_ALL C
|
ENV LC_ALL C
|
||||||
|
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
@@ -21,7 +22,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
|||||||
syslog-ng-core \
|
syslog-ng-core \
|
||||||
syslog-ng-mod-redis \
|
syslog-ng-mod-redis \
|
||||||
dirmngr \
|
dirmngr \
|
||||||
netcat \
|
netcat-traditional \
|
||||||
psmisc \
|
psmisc \
|
||||||
wget \
|
wget \
|
||||||
patch \
|
patch \
|
||||||
@@ -32,7 +33,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
|||||||
&& mkdir /usr/share/doc/sogo \
|
&& mkdir /usr/share/doc/sogo \
|
||||||
&& touch /usr/share/doc/sogo/empty.sh \
|
&& touch /usr/share/doc/sogo/empty.sh \
|
||||||
&& apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \
|
&& apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \
|
||||||
&& echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \
|
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} sogo-v5" > /etc/apt/sources.list.d/sogo.list \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||||
sogo \
|
sogo \
|
||||||
sogo-activesync \
|
sogo-activesync \
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
|||||||
<string>YES</string>
|
<string>YES</string>
|
||||||
<key>SOGoEncryptionKey</key>
|
<key>SOGoEncryptionKey</key>
|
||||||
<string>${RAND_PASS}</string>
|
<string>${RAND_PASS}</string>
|
||||||
|
<key>OCSAdminURL</key>
|
||||||
|
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
||||||
<key>OCSCacheFolderURL</key>
|
<key>OCSCacheFolderURL</key>
|
||||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
|
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
|
||||||
<key>OCSEMailAlarmsFolderURL</key>
|
<key>OCSEMailAlarmsFolderURL</key>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ FROM solr:7.7-slim
|
|||||||
USER root
|
USER root
|
||||||
|
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG GOSU_VERSION=1.16
|
ARG GOSU_VERSION=1.17
|
||||||
|
|
||||||
COPY solr.sh /
|
COPY solr.sh /
|
||||||
COPY solr-config-7.7.0.xml /
|
COPY solr-config-7.7.0.xml /
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
echo "FLATCURVE_EXPERIMENTAL=y, skipping Solr but enabling Flatcurve as FTS for Dovecot!"
|
||||||
|
echo "Solr will be removed in the future!"
|
||||||
|
sleep 365d
|
||||||
|
exit 0
|
||||||
|
elif [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
echo "SKIP_SOLR=y, skipping Solr..."
|
echo "SKIP_SOLR=y, skipping Solr..."
|
||||||
|
echo "HINT: You could try the newer FTS Backend Flatcurve, which is currently in experimental state..."
|
||||||
|
echo "Simply set FLATCURVE_EXPERIMENTAL=y inside your mailcow.conf and restart the stack afterwards!"
|
||||||
|
echo "Solr will be removed in the future!"
|
||||||
sleep 365d
|
sleep 365d
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -57,5 +65,11 @@ if [[ "${1}" == "--bootstrap" ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Starting up Solr..."
|
||||||
|
echo -e "\e[31mSolr is deprecated! You can try the new FTS System now by enabling FLATCURVE_EXPERIMENTAL=y inside mailcow.conf and restarting the stack\e[0m"
|
||||||
|
echo -e "\e[31mSolr will be removed completely soon!\e[0m"
|
||||||
|
|
||||||
|
sleep 15
|
||||||
|
|
||||||
exec gosu solr solr-foreground
|
exec gosu solr solr-foreground
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.20
|
||||||
|
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
RUN apk add --update --no-cache \
|
RUN apk add --update --no-cache \
|
||||||
curl \
|
curl \
|
||||||
|
bind-tools \
|
||||||
unbound \
|
unbound \
|
||||||
bash \
|
bash \
|
||||||
openssl \
|
openssl \
|
||||||
@@ -18,10 +19,10 @@ EXPOSE 53/udp 53/tcp
|
|||||||
|
|
||||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
|
||||||
# healthcheck (nslookup)
|
# healthcheck (dig, ping)
|
||||||
COPY healthcheck.sh /healthcheck.sh
|
COPY healthcheck.sh /healthcheck.sh
|
||||||
RUN chmod +x /healthcheck.sh
|
RUN chmod +x /healthcheck.sh
|
||||||
HEALTHCHECK --interval=30s --timeout=10s CMD [ "/healthcheck.sh" ]
|
HEALTHCHECK --interval=30s --timeout=30s CMD [ "/healthcheck.sh" ]
|
||||||
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,76 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
nslookup mailcow.email 127.0.0.1 1> /dev/null
|
# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!)
|
||||||
|
if [[ "${SKIP_UNBOUND_HEALTHCHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
SKIP_UNBOUND_HEALTHCHECK=y
|
||||||
|
fi
|
||||||
|
|
||||||
if [ $? == 0 ]; then
|
# Reset logfile
|
||||||
echo "DNS resolution is working!"
|
echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check - logs can be found in /var/log/healthcheck.log"
|
||||||
|
echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check" > /var/log/healthcheck.log
|
||||||
|
|
||||||
|
# Declare log function for logfile inside container
|
||||||
|
function log_to_file() {
|
||||||
|
echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" >> /var/log/healthcheck.log
|
||||||
|
}
|
||||||
|
|
||||||
|
# General Ping function to check general pingability
|
||||||
|
function check_ping() {
|
||||||
|
declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9")
|
||||||
|
|
||||||
|
for ip in "${ipstoping[@]}" ; do
|
||||||
|
ping -q -c 3 -w 5 "$ip"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_to_file "Healthcheck: Couldn't ping $ip for 5 seconds... Gave up!"
|
||||||
|
log_to_file "Please check your internet connection or firewall rules to fix this error, because a simple ping test should always go through from the unbound container!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log_to_file "Healthcheck: Ping Checks WORKING properly!"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# General DNS Resolve Check against Unbound Resolver himself
|
||||||
|
function check_dns() {
|
||||||
|
declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
|
||||||
|
|
||||||
|
for domain in "${domains[@]}" ; do
|
||||||
|
for ((i=1; i<=3; i++)); do
|
||||||
|
dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 > /dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_to_file "Healthcheck: DNS Resolution Failed on $i attempt! Trying again..."
|
||||||
|
if [ $i -eq 3 ]; then
|
||||||
|
log_to_file "Healthcheck: DNS Resolution not possible after $i attempts... Gave up!"
|
||||||
|
log_to_file "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
log_to_file "Healthcheck: DNS Resolver WORKING properly!"
|
||||||
|
return 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then
|
||||||
|
log_to_file "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
fi
|
||||||
echo "DNS resolution is not working correctly..."
|
|
||||||
echo "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
|
# run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy)
|
||||||
|
check_ping
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
check_dns
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_to_file "Healthcheck: ALL CHECKS WERE SUCCESSFUL! Unbound is healthy!"
|
||||||
|
exit 0
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.20
|
||||||
LABEL maintainer "André Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
RUN apk add --update \
|
RUN apk add --update \
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ fi
|
|||||||
|
|
||||||
if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
SMTP_VERBOSE="--verbose"
|
SMTP_VERBOSE="--verbose"
|
||||||
|
CURL_VERBOSE="--verbose"
|
||||||
set -xv
|
set -xv
|
||||||
else
|
else
|
||||||
SMTP_VERBOSE=""
|
SMTP_VERBOSE=""
|
||||||
|
CURL_VERBOSE=""
|
||||||
exec 2>/dev/null
|
exec 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -97,7 +99,9 @@ log_msg() {
|
|||||||
echo $(date) $(printf '%s\n' "${1}")
|
echo $(date) $(printf '%s\n' "${1}")
|
||||||
}
|
}
|
||||||
|
|
||||||
function mail_error() {
|
function notify_error() {
|
||||||
|
# Check if one of the notification options is enabled
|
||||||
|
[[ -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ -z ${WATCHDOG_NOTIFY_WEBHOOK} ]] && return 0
|
||||||
THROTTLE=
|
THROTTLE=
|
||||||
[[ -z ${1} ]] && return 1
|
[[ -z ${1} ]] && return 1
|
||||||
# If exists, body will be the content of "/tmp/${1}", even if ${2} is set
|
# If exists, body will be the content of "/tmp/${1}", even if ${2} is set
|
||||||
@@ -122,37 +126,57 @@ function mail_error() {
|
|||||||
else
|
else
|
||||||
SUBJECT="${WATCHDOG_SUBJECT}: ${1}"
|
SUBJECT="${WATCHDOG_SUBJECT}: ${1}"
|
||||||
fi
|
fi
|
||||||
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
|
|
||||||
for rcpt in "${MAIL_RCPTS[@]}"; do
|
# Send mail notification if enabled
|
||||||
RCPT_DOMAIN=
|
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
|
||||||
RCPT_MX=
|
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
|
||||||
RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'})
|
for rcpt in "${MAIL_RCPTS[@]}"; do
|
||||||
CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx)
|
RCPT_DOMAIN=
|
||||||
if [[ -z ${CHECK_FOR_VALID_MX} ]]; then
|
RCPT_MX=
|
||||||
log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
|
RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'})
|
||||||
|
CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx)
|
||||||
|
if [[ -z ${CHECK_FOR_VALID_MX} ]]; then
|
||||||
|
log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
[ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
|
||||||
|
timeout 10s ./smtp-cli --missing-modules-ok \
|
||||||
|
"${SMTP_VERBOSE}" \
|
||||||
|
--charset=UTF-8 \
|
||||||
|
--subject="${SUBJECT}" \
|
||||||
|
--body-plain="${BODY}" \
|
||||||
|
--add-header="X-Priority: 1" \
|
||||||
|
--to=${rcpt} \
|
||||||
|
--from="watchdog@${MAILCOW_HOSTNAME}" \
|
||||||
|
--hello-host=${MAILCOW_HOSTNAME} \
|
||||||
|
--ipv4
|
||||||
|
if [[ $? -eq 1 ]]; then # exit code 1 is fine
|
||||||
|
log_msg "Sent notification email to ${rcpt}"
|
||||||
|
else
|
||||||
|
if [[ "${SMTP_VERBOSE}" == "" ]]; then
|
||||||
|
log_msg "Error while sending notification email to ${rcpt}. You can enable verbose logging by setting 'WATCHDOG_VERBOSE=y' in mailcow.conf."
|
||||||
|
else
|
||||||
|
log_msg "Error while sending notification email to ${rcpt}."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Send webhook notification if enabled
|
||||||
|
if [[ ! -z ${WATCHDOG_NOTIFY_WEBHOOK} ]]; then
|
||||||
|
if [[ -z ${WATCHDOG_NOTIFY_WEBHOOK_BODY} ]]; then
|
||||||
|
log_msg "No webhook body set, skipping webhook notification..."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
[ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
|
|
||||||
timeout 10s ./smtp-cli --missing-modules-ok \
|
# Replace subject and body placeholders
|
||||||
"${SMTP_VERBOSE}" \
|
WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s/\$SUBJECT\|\${SUBJECT}/$SUBJECT/g" | sed "s/\$BODY\|\${BODY}/$BODY/g")
|
||||||
--charset=UTF-8 \
|
|
||||||
--subject="${SUBJECT}" \
|
# POST to webhook
|
||||||
--body-plain="${BODY}" \
|
curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK}
|
||||||
--add-header="X-Priority: 1" \
|
|
||||||
--to=${rcpt} \
|
log_msg "Sent notification using webhook"
|
||||||
--from="watchdog@${MAILCOW_HOSTNAME}" \
|
fi
|
||||||
--hello-host=${MAILCOW_HOSTNAME} \
|
|
||||||
--ipv4
|
|
||||||
if [[ $? -eq 1 ]]; then # exit code 1 is fine
|
|
||||||
log_msg "Sent notification email to ${rcpt}"
|
|
||||||
else
|
|
||||||
if [[ "${SMTP_VERBOSE}" == "" ]]; then
|
|
||||||
log_msg "Error while sending notification email to ${rcpt}. You can enable verbose logging by setting 'WATCHDOG_VERBOSE=y' in mailcow.conf."
|
|
||||||
else
|
|
||||||
log_msg "Error while sending notification email to ${rcpt}."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get_container_ip() {
|
get_container_ip() {
|
||||||
@@ -197,7 +221,7 @@ get_container_ip() {
|
|||||||
# One-time check
|
# One-time check
|
||||||
if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then
|
if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then
|
||||||
if [[ -z "$(get_ipv6)" ]]; then
|
if [[ -z "$(get_ipv6)" ]]; then
|
||||||
mail_error "ipv6-config" "enable_ipv6 is true in docker-compose.yml, but an IPv6 link could not be established. Please verify your IPv6 connection."
|
notify_error "ipv6-config" "enable_ipv6 is true in docker-compose.yml, but an IPv6 link could not be established. Please verify your IPv6 connection."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -692,8 +716,8 @@ rspamd_checks() {
|
|||||||
From: watchdog@localhost
|
From: watchdog@localhost
|
||||||
|
|
||||||
Empty
|
Empty
|
||||||
' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score)
|
' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score | sed 's/\..*//' )
|
||||||
if [[ ${SCORE} != "9999" ]]; then
|
if [[ ${SCORE} -ne 9999 ]]; then
|
||||||
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
|
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
|
||||||
err_count=$(( ${err_count} + 1))
|
err_count=$(( ${err_count} + 1))
|
||||||
else
|
else
|
||||||
@@ -746,8 +770,8 @@ olefy_checks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Notify about start
|
# Notify about start
|
||||||
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
|
if [[ ${WATCHDOG_NOTIFY_START} =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
mail_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
|
notify_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create watchdog agents
|
# Create watchdog agents
|
||||||
@@ -1029,33 +1053,33 @@ while true; do
|
|||||||
fi
|
fi
|
||||||
if [[ ${com_pipe_answer} == "ratelimit" ]]; then
|
if [[ ${com_pipe_answer} == "ratelimit" ]]; then
|
||||||
log_msg "At least one ratelimit was applied"
|
log_msg "At least one ratelimit was applied"
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
|
notify_error "${com_pipe_answer}"
|
||||||
elif [[ ${com_pipe_answer} == "mail_queue_status" ]]; then
|
elif [[ ${com_pipe_answer} == "mail_queue_status" ]]; then
|
||||||
log_msg "Mail queue status is critical"
|
log_msg "Mail queue status is critical"
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
|
notify_error "${com_pipe_answer}"
|
||||||
elif [[ ${com_pipe_answer} == "external_checks" ]]; then
|
elif [[ ${com_pipe_answer} == "external_checks" ]]; then
|
||||||
log_msg "Your mailcow is an open relay!"
|
log_msg "Your mailcow is an open relay!"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please stop mailcow now and check your network configuration!"
|
notify_error "${com_pipe_answer}" "Please stop mailcow now and check your network configuration!"
|
||||||
elif [[ ${com_pipe_answer} == "mysql_repl_checks" ]]; then
|
elif [[ ${com_pipe_answer} == "mysql_repl_checks" ]]; then
|
||||||
log_msg "MySQL replication is not working properly"
|
log_msg "MySQL replication is not working properly"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
# Once mail per 10 minutes
|
# Once mail per 10 minutes
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check the SQL replication status" 600
|
notify_error "${com_pipe_answer}" "Please check the SQL replication status" 600
|
||||||
elif [[ ${com_pipe_answer} == "dovecot_repl_checks" ]]; then
|
elif [[ ${com_pipe_answer} == "dovecot_repl_checks" ]]; then
|
||||||
log_msg "Dovecot replication is not working properly"
|
log_msg "Dovecot replication is not working properly"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
# Once mail per 10 minutes
|
# Once mail per 10 minutes
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check the Dovecot replicator status" 600
|
notify_error "${com_pipe_answer}" "Please check the Dovecot replicator status" 600
|
||||||
elif [[ ${com_pipe_answer} == "certcheck" ]]; then
|
elif [[ ${com_pipe_answer} == "certcheck" ]]; then
|
||||||
log_msg "Certificates are about to expire"
|
log_msg "Certificates are about to expire"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
# Only mail once a day
|
# Only mail once a day
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please renew your certificate" 86400
|
notify_error "${com_pipe_answer}" "Please renew your certificate" 86400
|
||||||
elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then
|
elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then
|
||||||
log_msg "acme-mailcow did not complete successfully"
|
log_msg "acme-mailcow did not complete successfully"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
|
notify_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
|
||||||
elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
|
elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
|
||||||
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
|
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
|
||||||
if [[ ! -z "${F2B_RES}" ]]; then
|
if [[ ! -z "${F2B_RES}" ]]; then
|
||||||
@@ -1065,7 +1089,7 @@ while true; do
|
|||||||
log_msg "Banned ${host}"
|
log_msg "Banned ${host}"
|
||||||
rm /tmp/fail2ban 2> /dev/null
|
rm /tmp/fail2ban 2> /dev/null
|
||||||
timeout 2s whois "${host}" > /tmp/fail2ban
|
timeout 2s whois "${host}" > /tmp/fail2ban
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && mail_error "${com_pipe_answer}" "IP ban: ${host}"
|
[[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && notify_error "${com_pipe_answer}" "IP ban: ${host}"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
|
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
|
||||||
@@ -1085,7 +1109,7 @@ while true; do
|
|||||||
else
|
else
|
||||||
log_msg "Sending restart command to ${CONTAINER_ID}..."
|
log_msg "Sending restart command to ${CONTAINER_ID}..."
|
||||||
curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart
|
curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
|
notify_error "${com_pipe_answer}"
|
||||||
log_msg "Wait for restarted container to settle and continue watching..."
|
log_msg "Wait for restarted container to settle and continue watching..."
|
||||||
sleep 35
|
sleep 35
|
||||||
fi
|
fi
|
||||||
@@ -1095,3 +1119,4 @@ while true; do
|
|||||||
kill -USR1 ${BACKGROUND_TASKS[*]}
|
kill -USR1 ${BACKGROUND_TASKS[*]}
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
auth_mechanisms = plain login
|
auth_mechanisms = plain login
|
||||||
#mail_debug = yes
|
#mail_debug = yes
|
||||||
#auth_debug = yes
|
#auth_debug = yes
|
||||||
|
#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
|
||||||
log_path = syslog
|
log_path = syslog
|
||||||
disable_plaintext_auth = yes
|
disable_plaintext_auth = yes
|
||||||
# Uncomment on NFS share
|
# Uncomment on NFS share
|
||||||
@@ -194,9 +195,6 @@ plugin {
|
|||||||
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
|
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
|
||||||
acl = vfile
|
acl = vfile
|
||||||
acl_user = %u
|
acl_user = %u
|
||||||
fts = solr
|
|
||||||
fts_autoindex = yes
|
|
||||||
fts_solr = url=http://solr:8983/solr/dovecot-fts/
|
|
||||||
quota = dict:Userquota::proxy::sqlquota
|
quota = dict:Userquota::proxy::sqlquota
|
||||||
quota_rule2 = Trash:storage=+100%%
|
quota_rule2 = Trash:storage=+100%%
|
||||||
sieve = /var/vmail/sieve/%u.sieve
|
sieve = /var/vmail/sieve/%u.sieve
|
||||||
@@ -247,6 +245,9 @@ plugin {
|
|||||||
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
||||||
mail_log_fields = uid box msgid size
|
mail_log_fields = uid box msgid size
|
||||||
mail_log_cached_only = yes
|
mail_log_cached_only = yes
|
||||||
|
|
||||||
|
# Try set mail_replica
|
||||||
|
!include_try /etc/dovecot/mail_replica.conf
|
||||||
}
|
}
|
||||||
service quota-warning {
|
service quota-warning {
|
||||||
executable = script /usr/local/bin/quota_notify.py
|
executable = script /usr/local/bin/quota_notify.py
|
||||||
@@ -302,6 +303,7 @@ replication_dsync_parameters = -d -l 30 -U -n INBOX
|
|||||||
!include_try /etc/dovecot/extra.conf
|
!include_try /etc/dovecot/extra.conf
|
||||||
!include_try /etc/dovecot/sogo-sso.conf
|
!include_try /etc/dovecot/sogo-sso.conf
|
||||||
!include_try /etc/dovecot/shared_namespace.conf
|
!include_try /etc/dovecot/shared_namespace.conf
|
||||||
|
!include_try /etc/dovecot/conf.d/fts.conf
|
||||||
# </Includes>
|
# </Includes>
|
||||||
default_client_limit = 10400
|
default_client_limit = 10400
|
||||||
default_vsz_limit = 1024 M
|
default_vsz_limit = 1024 M
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
if /^\s*Received:.*Authenticated sender.*\(Postcow\)/
|
if /^\s*Received:.*Authenticated sender.*\(Postcow\)/
|
||||||
#/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/
|
#/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/
|
||||||
/^Received: from .*? \([\w-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/
|
/^Received: from .*? \([\w\-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/
|
||||||
REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3
|
REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3
|
||||||
endif
|
endif
|
||||||
if /^\s*Received: from.* \(.*dovecot-mailcow.*mailcow-network.*\).*\(Postcow\)/
|
if /^\s*Received: from.* \(.*dovecot-mailcow.*mailcow-network.*\).*\(Postcow\)/
|
||||||
@@ -12,7 +12,8 @@ if /^\s*Received: from.* \(.*rspamd-mailcow.*mailcow-network.*\).*\(Postcow\)/
|
|||||||
REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5
|
REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5
|
||||||
endif
|
endif
|
||||||
/^\s*X-Enigmail/ IGNORE
|
/^\s*X-Enigmail/ IGNORE
|
||||||
/^\s*X-Mailer/ IGNORE
|
# Not removing Mailer by default, might be signed
|
||||||
|
#/^\s*X-Mailer/ IGNORE
|
||||||
/^\s*X-Originating-IP/ IGNORE
|
/^\s*X-Originating-IP/ IGNORE
|
||||||
/^\s*X-Forward/ IGNORE
|
/^\s*X-Forward/ IGNORE
|
||||||
# Not removing UA by default, might be signed
|
# Not removing UA by default, might be signed
|
||||||
|
|||||||
+13
-10
@@ -11,6 +11,7 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
|
|||||||
smtpd_relay_restrictions = permit_mynetworks,
|
smtpd_relay_restrictions = permit_mynetworks,
|
||||||
permit_sasl_authenticated,
|
permit_sasl_authenticated,
|
||||||
defer_unauth_destination
|
defer_unauth_destination
|
||||||
|
smtpd_forbid_bare_newline = yes
|
||||||
# alias maps are auto-generated in postfix.sh on startup
|
# alias maps are auto-generated in postfix.sh on startup
|
||||||
alias_maps = hash:/etc/aliases
|
alias_maps = hash:/etc/aliases
|
||||||
alias_database = hash:/etc/aliases
|
alias_database = hash:/etc/aliases
|
||||||
@@ -84,6 +85,7 @@ smtp_tls_security_level = dane
|
|||||||
smtpd_data_restrictions = reject_unauth_pipelining, permit
|
smtpd_data_restrictions = reject_unauth_pipelining, permit
|
||||||
smtpd_delay_reject = yes
|
smtpd_delay_reject = yes
|
||||||
smtpd_error_sleep_time = 10s
|
smtpd_error_sleep_time = 10s
|
||||||
|
smtpd_forbid_bare_newline = yes
|
||||||
smtpd_hard_error_limit = ${stress?1}${stress:5}
|
smtpd_hard_error_limit = ${stress?1}${stress:5}
|
||||||
smtpd_helo_required = yes
|
smtpd_helo_required = yes
|
||||||
smtpd_proxy_timeout = 600s
|
smtpd_proxy_timeout = 600s
|
||||||
@@ -112,14 +114,14 @@ smtpd_tls_loglevel = 1
|
|||||||
|
|
||||||
# Mandatory protocols and ciphers are used when a connections is enforced to use TLS
|
# Mandatory protocols and ciphers are used when a connections is enforced to use TLS
|
||||||
# Does _not_ apply to enforced incoming TLS settings per mailbox
|
# Does _not_ apply to enforced incoming TLS settings per mailbox
|
||||||
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
smtp_tls_mandatory_protocols = >=TLSv1.2
|
||||||
lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
lmtp_tls_mandatory_protocols = >=TLSv1.2
|
||||||
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
smtpd_tls_mandatory_protocols = >=TLSv1.2
|
||||||
smtpd_tls_mandatory_ciphers = high
|
smtpd_tls_mandatory_ciphers = high
|
||||||
|
|
||||||
smtp_tls_protocols = !SSLv2, !SSLv3
|
smtp_tls_protocols = >=TLSv1.2
|
||||||
lmtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
lmtp_tls_protocols = >=TLSv1.2
|
||||||
smtpd_tls_protocols = !SSLv2, !SSLv3
|
smtpd_tls_protocols = >=TLSv1.2
|
||||||
|
|
||||||
smtpd_tls_security_level = may
|
smtpd_tls_security_level = may
|
||||||
tls_preempt_cipherlist = yes
|
tls_preempt_cipherlist = yes
|
||||||
@@ -160,12 +162,13 @@ transport_maps = pcre:/opt/postfix/conf/custom_transport.pcre,
|
|||||||
proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf,
|
proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf,
|
||||||
proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
|
proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
|
||||||
smtp_sasl_auth_soft_bounce = no
|
smtp_sasl_auth_soft_bounce = no
|
||||||
postscreen_discard_ehlo_keywords = silent-discard, dsn
|
postscreen_discard_ehlo_keywords = silent-discard, dsn, chunking
|
||||||
compatibility_level = 2
|
smtpd_discard_ehlo_keywords = chunking, silent-discard
|
||||||
|
compatibility_level = 3.7
|
||||||
smtputf8_enable = no
|
smtputf8_enable = no
|
||||||
# Define protocols for SMTPS and submission service
|
# Define protocols for SMTPS and submission service
|
||||||
submission_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
submission_smtpd_tls_mandatory_protocols = >=TLSv1.2
|
||||||
smtps_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
smtps_smtpd_tls_mandatory_protocols = >=TLSv1.2
|
||||||
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
|
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
|
||||||
|
|
||||||
# DO NOT EDIT ANYTHING BELOW #
|
# DO NOT EDIT ANYTHING BELOW #
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
# Whitelist generated by Postwhite v3.4 on Wed Nov 1 00:14:24 UTC 2023
|
# Whitelist generated by Postwhite v3.4 on Sat Jun 1 00:15:02 UTC 2024
|
||||||
# https://github.com/stevejenkins/postwhite/
|
# https://github.com/stevejenkins/postwhite/
|
||||||
# 2030 total rules
|
# 2000 total rules
|
||||||
2a00:1450:4000::/36 permit
|
2a00:1450:4000::/36 permit
|
||||||
2a01:111:f400::/48 permit
|
2a01:111:f400::/48 permit
|
||||||
2a01:111:f403:8000::/50 permit
|
2a01:111:f403:8000::/50 permit
|
||||||
|
2a01:111:f403:8000::/51 permit
|
||||||
2a01:111:f403::/49 permit
|
2a01:111:f403::/49 permit
|
||||||
2a01:111:f403:c000::/51 permit
|
2a01:111:f403:c000::/51 permit
|
||||||
2a01:111:f403:f000::/52 permit
|
2a01:111:f403:f000::/52 permit
|
||||||
2a02:a60:0:5::/64 permit
|
2a02:a60:0:5::/64 permit
|
||||||
2c0f:fb50:4000::/36 permit
|
2c0f:fb50:4000::/36 permit
|
||||||
2.207.151.53 permit
|
2.207.151.53 permit
|
||||||
3.14.230.16 permit
|
|
||||||
3.70.123.177 permit
|
3.70.123.177 permit
|
||||||
3.93.157.0/24 permit
|
3.93.157.0/24 permit
|
||||||
|
3.94.40.108 permit
|
||||||
3.129.120.190 permit
|
3.129.120.190 permit
|
||||||
3.210.190.0/24 permit
|
3.210.190.0/24 permit
|
||||||
8.20.114.31 permit
|
8.20.114.31 permit
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
8.25.196.0/23 permit
|
8.25.196.0/23 permit
|
||||||
8.39.54.0/23 permit
|
8.39.54.0/23 permit
|
||||||
8.40.222.0/23 permit
|
8.40.222.0/23 permit
|
||||||
|
10.162.0.0/16 permit
|
||||||
12.130.86.238 permit
|
12.130.86.238 permit
|
||||||
13.70.32.43 permit
|
13.70.32.43 permit
|
||||||
13.72.50.45 permit
|
13.72.50.45 permit
|
||||||
@@ -31,21 +33,25 @@
|
|||||||
13.110.216.0/22 permit
|
13.110.216.0/22 permit
|
||||||
13.110.224.0/20 permit
|
13.110.224.0/20 permit
|
||||||
13.111.0.0/16 permit
|
13.111.0.0/16 permit
|
||||||
|
13.111.191.0/24 permit
|
||||||
15.200.21.50 permit
|
15.200.21.50 permit
|
||||||
15.200.44.248 permit
|
15.200.44.248 permit
|
||||||
15.200.201.185 permit
|
15.200.201.185 permit
|
||||||
|
17.41.0.0/16 permit
|
||||||
17.57.155.0/24 permit
|
17.57.155.0/24 permit
|
||||||
17.57.156.0/24 permit
|
17.57.156.0/24 permit
|
||||||
17.58.0.0/16 permit
|
17.58.0.0/16 permit
|
||||||
|
17.142.0.0/15 permit
|
||||||
18.156.89.250 permit
|
18.156.89.250 permit
|
||||||
18.157.243.190 permit
|
18.157.243.190 permit
|
||||||
18.194.95.56 permit
|
18.194.95.56 permit
|
||||||
18.198.96.88 permit
|
18.198.96.88 permit
|
||||||
18.208.124.128/25 permit
|
18.208.124.128/25 permit
|
||||||
18.216.232.154 permit
|
18.216.232.154 permit
|
||||||
18.234.1.244 permit
|
|
||||||
18.236.40.242 permit
|
18.236.40.242 permit
|
||||||
|
18.236.56.161 permit
|
||||||
20.51.6.32/30 permit
|
20.51.6.32/30 permit
|
||||||
|
20.51.98.61 permit
|
||||||
20.52.52.2 permit
|
20.52.52.2 permit
|
||||||
20.52.128.133 permit
|
20.52.128.133 permit
|
||||||
20.59.80.4/30 permit
|
20.59.80.4/30 permit
|
||||||
@@ -63,10 +69,9 @@
|
|||||||
20.107.239.64/30 permit
|
20.107.239.64/30 permit
|
||||||
20.112.250.133 permit
|
20.112.250.133 permit
|
||||||
20.118.139.208/30 permit
|
20.118.139.208/30 permit
|
||||||
20.185.213.160/27 permit
|
20.141.10.196 permit
|
||||||
20.185.213.224/27 permit
|
20.185.213.0/24 permit
|
||||||
20.185.214.0/27 permit
|
20.185.214.0/27 permit
|
||||||
20.185.214.2 permit
|
|
||||||
20.185.214.32/27 permit
|
20.185.214.32/27 permit
|
||||||
20.185.214.64/27 permit
|
20.185.214.64/27 permit
|
||||||
20.231.239.246 permit
|
20.231.239.246 permit
|
||||||
@@ -90,23 +95,22 @@
|
|||||||
27.123.204.172 permit
|
27.123.204.172 permit
|
||||||
27.123.204.188/30 permit
|
27.123.204.188/30 permit
|
||||||
27.123.204.192 permit
|
27.123.204.192 permit
|
||||||
27.123.206.0/24 permit
|
|
||||||
27.123.206.50/31 permit
|
27.123.206.50/31 permit
|
||||||
27.123.206.56/29 permit
|
27.123.206.56/29 permit
|
||||||
27.123.206.76/30 permit
|
27.123.206.76/30 permit
|
||||||
27.123.206.80/28 permit
|
27.123.206.80/28 permit
|
||||||
31.25.48.222 permit
|
31.25.48.222 permit
|
||||||
34.195.217.107 permit
|
34.195.217.107 permit
|
||||||
34.202.239.6 permit
|
|
||||||
34.212.163.75 permit
|
34.212.163.75 permit
|
||||||
34.215.104.144 permit
|
34.215.104.144 permit
|
||||||
|
34.218.116.3 permit
|
||||||
34.225.212.172 permit
|
34.225.212.172 permit
|
||||||
34.247.168.44 permit
|
|
||||||
35.161.32.253 permit
|
35.161.32.253 permit
|
||||||
35.167.93.243 permit
|
35.167.93.243 permit
|
||||||
35.176.132.251 permit
|
35.176.132.251 permit
|
||||||
35.190.247.0/24 permit
|
35.190.247.0/24 permit
|
||||||
35.191.0.0/16 permit
|
35.191.0.0/16 permit
|
||||||
|
35.242.169.159 permit
|
||||||
37.218.248.47 permit
|
37.218.248.47 permit
|
||||||
37.218.249.47 permit
|
37.218.249.47 permit
|
||||||
37.218.251.62 permit
|
37.218.251.62 permit
|
||||||
@@ -116,13 +120,11 @@
|
|||||||
40.92.0.0/16 permit
|
40.92.0.0/16 permit
|
||||||
40.107.0.0/16 permit
|
40.107.0.0/16 permit
|
||||||
40.112.65.63 permit
|
40.112.65.63 permit
|
||||||
40.117.80.0/24 permit
|
|
||||||
43.228.184.0/22 permit
|
43.228.184.0/22 permit
|
||||||
44.206.138.57 permit
|
44.206.138.57 permit
|
||||||
44.209.42.157 permit
|
|
||||||
44.236.56.93 permit
|
44.236.56.93 permit
|
||||||
44.238.220.251 permit
|
44.238.220.251 permit
|
||||||
46.19.168.0/23 permit
|
46.19.170.16 permit
|
||||||
46.226.48.0/21 permit
|
46.226.48.0/21 permit
|
||||||
46.228.36.37 permit
|
46.228.36.37 permit
|
||||||
46.228.36.38/31 permit
|
46.228.36.38/31 permit
|
||||||
@@ -162,7 +164,6 @@
|
|||||||
46.228.38.144/29 permit
|
46.228.38.144/29 permit
|
||||||
46.228.38.152/31 permit
|
46.228.38.152/31 permit
|
||||||
46.228.38.154 permit
|
46.228.38.154 permit
|
||||||
46.228.39.0/24 permit
|
|
||||||
46.228.39.64/27 permit
|
46.228.39.64/27 permit
|
||||||
46.228.39.96/30 permit
|
46.228.39.96/30 permit
|
||||||
46.228.39.100/30 permit
|
46.228.39.100/30 permit
|
||||||
@@ -183,24 +184,23 @@
|
|||||||
50.18.125.237 permit
|
50.18.125.237 permit
|
||||||
50.18.126.162 permit
|
50.18.126.162 permit
|
||||||
50.31.32.0/19 permit
|
50.31.32.0/19 permit
|
||||||
50.31.156.96/27 permit
|
50.56.130.220/30 permit
|
||||||
50.31.205.0/24 permit
|
|
||||||
51.137.58.21 permit
|
51.137.58.21 permit
|
||||||
51.140.75.55 permit
|
51.140.75.55 permit
|
||||||
51.144.100.179 permit
|
52.1.14.157 permit
|
||||||
52.5.230.59 permit
|
52.5.230.59 permit
|
||||||
52.27.5.72 permit
|
52.27.5.72 permit
|
||||||
52.27.28.47 permit
|
52.27.28.47 permit
|
||||||
52.28.63.81 permit
|
52.28.63.81 permit
|
||||||
52.36.138.31 permit
|
52.36.138.31 permit
|
||||||
52.37.142.146 permit
|
52.37.142.146 permit
|
||||||
|
52.50.24.208 permit
|
||||||
52.58.216.183 permit
|
52.58.216.183 permit
|
||||||
52.59.143.3 permit
|
52.59.143.3 permit
|
||||||
52.60.41.5 permit
|
52.60.41.5 permit
|
||||||
52.60.115.116 permit
|
52.60.115.116 permit
|
||||||
52.61.91.9 permit
|
52.61.91.9 permit
|
||||||
52.71.0.205 permit
|
52.71.0.205 permit
|
||||||
52.82.172.0/22 permit
|
|
||||||
52.94.124.0/28 permit
|
52.94.124.0/28 permit
|
||||||
52.95.48.152/29 permit
|
52.95.48.152/29 permit
|
||||||
52.95.49.88/29 permit
|
52.95.49.88/29 permit
|
||||||
@@ -216,7 +216,6 @@
|
|||||||
52.100.0.0/14 permit
|
52.100.0.0/14 permit
|
||||||
52.103.0.0/17 permit
|
52.103.0.0/17 permit
|
||||||
52.119.213.144/28 permit
|
52.119.213.144/28 permit
|
||||||
52.160.39.140 permit
|
|
||||||
52.165.175.144 permit
|
52.165.175.144 permit
|
||||||
52.185.106.240/28 permit
|
52.185.106.240/28 permit
|
||||||
52.200.59.0/24 permit
|
52.200.59.0/24 permit
|
||||||
@@ -228,37 +227,33 @@
|
|||||||
52.222.75.85 permit
|
52.222.75.85 permit
|
||||||
52.222.89.228 permit
|
52.222.89.228 permit
|
||||||
52.234.172.96/28 permit
|
52.234.172.96/28 permit
|
||||||
|
52.235.253.128 permit
|
||||||
52.236.28.240/28 permit
|
52.236.28.240/28 permit
|
||||||
52.244.206.214 permit
|
52.244.206.214 permit
|
||||||
52.247.53.144 permit
|
52.247.53.144 permit
|
||||||
52.250.107.196 permit
|
52.250.107.196 permit
|
||||||
52.250.126.174 permit
|
52.250.126.174 permit
|
||||||
54.90.148.255 permit
|
54.90.148.255 permit
|
||||||
|
54.165.19.38 permit
|
||||||
54.172.97.247 permit
|
54.172.97.247 permit
|
||||||
54.174.52.0/24 permit
|
54.174.52.0/24 permit
|
||||||
54.174.53.128/30 permit
|
|
||||||
54.174.57.0/24 permit
|
54.174.57.0/24 permit
|
||||||
54.174.59.0/24 permit
|
54.174.59.0/24 permit
|
||||||
54.174.60.0/23 permit
|
54.174.60.0/23 permit
|
||||||
54.174.63.0/24 permit
|
54.174.63.0/24 permit
|
||||||
54.186.193.102 permit
|
54.186.193.102 permit
|
||||||
54.191.223.56 permit
|
54.191.223.56 permit
|
||||||
54.194.61.95 permit
|
|
||||||
54.195.113.45 permit
|
|
||||||
54.213.20.246 permit
|
54.213.20.246 permit
|
||||||
54.214.39.184 permit
|
54.214.39.184 permit
|
||||||
54.216.77.168 permit
|
|
||||||
54.221.227.204 permit
|
|
||||||
54.240.0.0/18 permit
|
54.240.0.0/18 permit
|
||||||
54.240.64.0/19 permit
|
54.240.64.0/19 permit
|
||||||
54.240.96.0/19 permit
|
54.240.96.0/19 permit
|
||||||
54.241.16.209 permit
|
54.241.16.209 permit
|
||||||
54.244.54.130 permit
|
54.244.54.130 permit
|
||||||
54.244.242.0/24 permit
|
54.244.242.0/24 permit
|
||||||
54.246.232.180 permit
|
|
||||||
54.255.61.23 permit
|
54.255.61.23 permit
|
||||||
62.13.128.0/24 permit
|
62.13.128.0/24 permit
|
||||||
62.13.128.150 permit
|
62.13.128.196 permit
|
||||||
62.13.129.128/25 permit
|
62.13.129.128/25 permit
|
||||||
62.13.136.0/22 permit
|
62.13.136.0/22 permit
|
||||||
62.13.140.0/22 permit
|
62.13.140.0/22 permit
|
||||||
@@ -266,14 +261,13 @@
|
|||||||
62.13.148.0/23 permit
|
62.13.148.0/23 permit
|
||||||
62.13.150.0/23 permit
|
62.13.150.0/23 permit
|
||||||
62.13.152.0/23 permit
|
62.13.152.0/23 permit
|
||||||
|
62.13.159.196 permit
|
||||||
62.17.146.128/26 permit
|
62.17.146.128/26 permit
|
||||||
62.179.121.0/24 permit
|
62.179.121.0/24 permit
|
||||||
62.201.172.0/27 permit
|
62.201.172.0/27 permit
|
||||||
62.201.172.32/27 permit
|
62.201.172.32/27 permit
|
||||||
62.253.227.114 permit
|
62.253.227.114 permit
|
||||||
63.32.13.159 permit
|
|
||||||
63.80.14.0/23 permit
|
63.80.14.0/23 permit
|
||||||
63.111.28.137 permit
|
|
||||||
63.128.21.0/24 permit
|
63.128.21.0/24 permit
|
||||||
63.143.57.128/25 permit
|
63.143.57.128/25 permit
|
||||||
63.143.59.128/25 permit
|
63.143.59.128/25 permit
|
||||||
@@ -286,40 +280,34 @@
|
|||||||
64.79.155.193 permit
|
64.79.155.193 permit
|
||||||
64.79.155.205 permit
|
64.79.155.205 permit
|
||||||
64.79.155.206 permit
|
64.79.155.206 permit
|
||||||
64.89.44.85 permit
|
|
||||||
64.89.45.80 permit
|
|
||||||
64.89.45.194 permit
|
|
||||||
64.89.45.196 permit
|
|
||||||
64.127.115.252 permit
|
64.127.115.252 permit
|
||||||
64.132.88.0/23 permit
|
64.132.88.0/23 permit
|
||||||
64.132.92.0/24 permit
|
64.132.92.0/24 permit
|
||||||
64.147.123.17 permit
|
64.147.123.128/27 permit
|
||||||
64.147.123.18 permit
|
|
||||||
64.147.123.19 permit
|
|
||||||
64.147.123.20 permit
|
|
||||||
64.147.123.21 permit
|
|
||||||
64.147.123.24 permit
|
|
||||||
64.147.123.25 permit
|
|
||||||
64.147.123.26 permit
|
|
||||||
64.147.123.27 permit
|
|
||||||
64.147.123.28 permit
|
|
||||||
64.147.123.29 permit
|
|
||||||
64.207.219.7 permit
|
64.207.219.7 permit
|
||||||
64.207.219.8 permit
|
64.207.219.8 permit
|
||||||
64.207.219.9 permit
|
64.207.219.9 permit
|
||||||
|
64.207.219.10 permit
|
||||||
|
64.207.219.11 permit
|
||||||
|
64.207.219.12 permit
|
||||||
64.207.219.13 permit
|
64.207.219.13 permit
|
||||||
64.207.219.14 permit
|
64.207.219.14 permit
|
||||||
64.207.219.15 permit
|
64.207.219.15 permit
|
||||||
64.207.219.71 permit
|
64.207.219.71 permit
|
||||||
64.207.219.72 permit
|
64.207.219.72 permit
|
||||||
64.207.219.73 permit
|
64.207.219.73 permit
|
||||||
|
64.207.219.74 permit
|
||||||
64.207.219.75 permit
|
64.207.219.75 permit
|
||||||
|
64.207.219.76 permit
|
||||||
64.207.219.77 permit
|
64.207.219.77 permit
|
||||||
64.207.219.78 permit
|
64.207.219.78 permit
|
||||||
64.207.219.79 permit
|
64.207.219.79 permit
|
||||||
64.207.219.135 permit
|
64.207.219.135 permit
|
||||||
64.207.219.136 permit
|
64.207.219.136 permit
|
||||||
64.207.219.137 permit
|
64.207.219.137 permit
|
||||||
|
64.207.219.138 permit
|
||||||
|
64.207.219.139 permit
|
||||||
|
64.207.219.140 permit
|
||||||
64.207.219.141 permit
|
64.207.219.141 permit
|
||||||
64.207.219.142 permit
|
64.207.219.142 permit
|
||||||
64.207.219.143 permit
|
64.207.219.143 permit
|
||||||
@@ -352,23 +340,8 @@
|
|||||||
65.154.166.0/24 permit
|
65.154.166.0/24 permit
|
||||||
65.212.180.36 permit
|
65.212.180.36 permit
|
||||||
66.102.0.0/20 permit
|
66.102.0.0/20 permit
|
||||||
66.111.4.25 permit
|
|
||||||
66.111.4.26 permit
|
|
||||||
66.111.4.27 permit
|
|
||||||
66.111.4.28 permit
|
|
||||||
66.111.4.29 permit
|
|
||||||
66.111.4.221 permit
|
|
||||||
66.111.4.222 permit
|
|
||||||
66.111.4.224 permit
|
|
||||||
66.111.4.225 permit
|
|
||||||
66.111.4.229 permit
|
|
||||||
66.111.4.230 permit
|
|
||||||
66.119.150.192/26 permit
|
66.119.150.192/26 permit
|
||||||
66.135.202.0/27 permit
|
|
||||||
66.135.215.0/24 permit
|
|
||||||
66.135.222.1 permit
|
|
||||||
66.162.193.226/31 permit
|
66.162.193.226/31 permit
|
||||||
66.163.184.0/21 permit
|
|
||||||
66.163.184.0/24 permit
|
66.163.184.0/24 permit
|
||||||
66.163.185.0/24 permit
|
66.163.185.0/24 permit
|
||||||
66.163.186.0/24 permit
|
66.163.186.0/24 permit
|
||||||
@@ -381,7 +354,6 @@
|
|||||||
66.196.80.112/28 permit
|
66.196.80.112/28 permit
|
||||||
66.196.80.144/29 permit
|
66.196.80.144/29 permit
|
||||||
66.196.80.193 permit
|
66.196.80.193 permit
|
||||||
66.196.81.0/24 permit
|
|
||||||
66.196.81.104/29 permit
|
66.196.81.104/29 permit
|
||||||
66.196.81.112/29 permit
|
66.196.81.112/29 permit
|
||||||
66.196.81.120 permit
|
66.196.81.120 permit
|
||||||
@@ -396,8 +368,6 @@
|
|||||||
66.196.81.228/30 permit
|
66.196.81.228/30 permit
|
||||||
66.196.81.232/31 permit
|
66.196.81.232/31 permit
|
||||||
66.196.81.234 permit
|
66.196.81.234 permit
|
||||||
66.211.168.230/31 permit
|
|
||||||
66.211.170.86/31 permit
|
|
||||||
66.211.170.88/29 permit
|
66.211.170.88/29 permit
|
||||||
66.211.184.0/23 permit
|
66.211.184.0/23 permit
|
||||||
66.218.74.64/30 permit
|
66.218.74.64/30 permit
|
||||||
@@ -426,12 +396,10 @@
|
|||||||
66.249.80.0/20 permit
|
66.249.80.0/20 permit
|
||||||
67.23.31.6 permit
|
67.23.31.6 permit
|
||||||
67.72.99.26 permit
|
67.72.99.26 permit
|
||||||
67.195.22.0/24 permit
|
|
||||||
67.195.22.113 permit
|
67.195.22.113 permit
|
||||||
67.195.22.116/30 permit
|
67.195.22.116/30 permit
|
||||||
67.195.23.144/30 permit
|
67.195.23.144/30 permit
|
||||||
67.195.23.148 permit
|
67.195.23.148 permit
|
||||||
67.195.60.0/24 permit
|
|
||||||
67.195.60.45 permit
|
67.195.60.45 permit
|
||||||
67.195.60.46/31 permit
|
67.195.60.46/31 permit
|
||||||
67.195.60.48/31 permit
|
67.195.60.48/31 permit
|
||||||
@@ -439,7 +407,6 @@
|
|||||||
67.195.60.146 permit
|
67.195.60.146 permit
|
||||||
67.195.60.155 permit
|
67.195.60.155 permit
|
||||||
67.195.60.156 permit
|
67.195.60.156 permit
|
||||||
67.195.87.0/24 permit
|
|
||||||
67.195.87.64 permit
|
67.195.87.64 permit
|
||||||
67.195.87.81 permit
|
67.195.87.81 permit
|
||||||
67.195.87.82/31 permit
|
67.195.87.82/31 permit
|
||||||
@@ -464,7 +431,6 @@
|
|||||||
67.228.37.4/30 permit
|
67.228.37.4/30 permit
|
||||||
67.231.145.42 permit
|
67.231.145.42 permit
|
||||||
67.231.153.30 permit
|
67.231.153.30 permit
|
||||||
68.142.230.0/24 permit
|
|
||||||
68.142.230.64/31 permit
|
68.142.230.64/31 permit
|
||||||
68.142.230.69 permit
|
68.142.230.69 permit
|
||||||
68.142.230.70/31 permit
|
68.142.230.70/31 permit
|
||||||
@@ -492,7 +458,6 @@
|
|||||||
70.37.151.128/25 permit
|
70.37.151.128/25 permit
|
||||||
70.42.149.0/24 permit
|
70.42.149.0/24 permit
|
||||||
70.42.149.35 permit
|
70.42.149.35 permit
|
||||||
72.3.237.64/28 permit
|
|
||||||
72.14.192.0/18 permit
|
72.14.192.0/18 permit
|
||||||
72.21.192.0/19 permit
|
72.21.192.0/19 permit
|
||||||
72.21.217.142 permit
|
72.21.217.142 permit
|
||||||
@@ -523,7 +488,6 @@
|
|||||||
72.30.237.180/30 permit
|
72.30.237.180/30 permit
|
||||||
72.30.237.184/31 permit
|
72.30.237.184/31 permit
|
||||||
72.30.237.204/30 permit
|
72.30.237.204/30 permit
|
||||||
72.30.238.0/23 permit
|
|
||||||
72.30.238.116/30 permit
|
72.30.238.116/30 permit
|
||||||
72.30.238.120/31 permit
|
72.30.238.120/31 permit
|
||||||
72.30.238.128 permit
|
72.30.238.128 permit
|
||||||
@@ -554,12 +518,7 @@
|
|||||||
72.30.239.228/31 permit
|
72.30.239.228/31 permit
|
||||||
72.30.239.244/30 permit
|
72.30.239.244/30 permit
|
||||||
72.30.239.248/31 permit
|
72.30.239.248/31 permit
|
||||||
72.34.168.76 permit
|
|
||||||
72.34.168.80 permit
|
|
||||||
72.34.168.85 permit
|
|
||||||
72.34.168.86 permit
|
|
||||||
72.52.72.32/28 permit
|
72.52.72.32/28 permit
|
||||||
74.6.128.0/21 permit
|
|
||||||
74.6.128.0/24 permit
|
74.6.128.0/24 permit
|
||||||
74.6.129.0/24 permit
|
74.6.129.0/24 permit
|
||||||
74.6.130.0/24 permit
|
74.6.130.0/24 permit
|
||||||
@@ -586,13 +545,14 @@
|
|||||||
74.112.67.243 permit
|
74.112.67.243 permit
|
||||||
74.125.0.0/16 permit
|
74.125.0.0/16 permit
|
||||||
74.202.227.40 permit
|
74.202.227.40 permit
|
||||||
74.208.4.192/26 permit
|
74.208.4.200 permit
|
||||||
74.208.5.64/26 permit
|
74.208.4.201 permit
|
||||||
74.208.122.0/26 permit
|
74.208.4.220 permit
|
||||||
|
74.208.4.221 permit
|
||||||
74.209.250.0/24 permit
|
74.209.250.0/24 permit
|
||||||
|
75.2.70.75 permit
|
||||||
76.223.128.0/19 permit
|
76.223.128.0/19 permit
|
||||||
76.223.176.0/20 permit
|
76.223.176.0/20 permit
|
||||||
77.238.176.0/22 permit
|
|
||||||
77.238.176.0/24 permit
|
77.238.176.0/24 permit
|
||||||
77.238.177.0/24 permit
|
77.238.177.0/24 permit
|
||||||
77.238.178.0/24 permit
|
77.238.178.0/24 permit
|
||||||
@@ -615,17 +575,25 @@
|
|||||||
77.238.189.148/30 permit
|
77.238.189.148/30 permit
|
||||||
81.7.169.128/25 permit
|
81.7.169.128/25 permit
|
||||||
81.223.46.0/27 permit
|
81.223.46.0/27 permit
|
||||||
82.165.159.0/24 permit
|
82.165.159.2 permit
|
||||||
82.165.159.0/26 permit
|
82.165.159.3 permit
|
||||||
82.165.229.31 permit
|
82.165.159.4 permit
|
||||||
82.165.229.130 permit
|
82.165.159.12 permit
|
||||||
82.165.230.21 permit
|
82.165.159.13 permit
|
||||||
82.165.230.22 permit
|
82.165.159.14 permit
|
||||||
|
82.165.159.34 permit
|
||||||
|
82.165.159.35 permit
|
||||||
|
82.165.159.40 permit
|
||||||
|
82.165.159.41 permit
|
||||||
|
82.165.159.42 permit
|
||||||
|
82.165.159.45 permit
|
||||||
|
82.165.159.130 permit
|
||||||
|
82.165.159.131 permit
|
||||||
|
84.116.6.0/23 permit
|
||||||
84.116.36.0/24 permit
|
84.116.36.0/24 permit
|
||||||
|
84.116.50.0/23 permit
|
||||||
85.158.136.0/21 permit
|
85.158.136.0/21 permit
|
||||||
86.61.88.25 permit
|
86.61.88.25 permit
|
||||||
87.198.219.130 permit
|
|
||||||
87.198.219.153 permit
|
|
||||||
87.238.80.0/21 permit
|
87.238.80.0/21 permit
|
||||||
87.248.103.12 permit
|
87.248.103.12 permit
|
||||||
87.248.103.21 permit
|
87.248.103.21 permit
|
||||||
@@ -665,6 +633,7 @@
|
|||||||
87.253.232.0/21 permit
|
87.253.232.0/21 permit
|
||||||
89.22.108.0/24 permit
|
89.22.108.0/24 permit
|
||||||
91.211.240.0/22 permit
|
91.211.240.0/22 permit
|
||||||
|
94.169.2.0/23 permit
|
||||||
94.245.112.0/27 permit
|
94.245.112.0/27 permit
|
||||||
94.245.112.10/31 permit
|
94.245.112.10/31 permit
|
||||||
95.131.104.0/21 permit
|
95.131.104.0/21 permit
|
||||||
@@ -678,7 +647,6 @@
|
|||||||
98.136.44.181 permit
|
98.136.44.181 permit
|
||||||
98.136.44.182/31 permit
|
98.136.44.182/31 permit
|
||||||
98.136.44.184 permit
|
98.136.44.184 permit
|
||||||
98.136.164.0/24 permit
|
|
||||||
98.136.164.36/31 permit
|
98.136.164.36/31 permit
|
||||||
98.136.164.64/29 permit
|
98.136.164.64/29 permit
|
||||||
98.136.164.72/30 permit
|
98.136.164.72/30 permit
|
||||||
@@ -686,7 +654,6 @@
|
|||||||
98.136.164.78 permit
|
98.136.164.78 permit
|
||||||
98.136.172.32/30 permit
|
98.136.172.32/30 permit
|
||||||
98.136.172.36/31 permit
|
98.136.172.36/31 permit
|
||||||
98.136.185.0/24 permit
|
|
||||||
98.136.185.29 permit
|
98.136.185.29 permit
|
||||||
98.136.185.42/31 permit
|
98.136.185.42/31 permit
|
||||||
98.136.185.46 permit
|
98.136.185.46 permit
|
||||||
@@ -721,7 +688,6 @@
|
|||||||
98.136.215.184 permit
|
98.136.215.184 permit
|
||||||
98.136.215.208/30 permit
|
98.136.215.208/30 permit
|
||||||
98.136.215.212/31 permit
|
98.136.215.212/31 permit
|
||||||
98.136.217.0/24 permit
|
|
||||||
98.136.217.1 permit
|
98.136.217.1 permit
|
||||||
98.136.217.2 permit
|
98.136.217.2 permit
|
||||||
98.136.217.3 permit
|
98.136.217.3 permit
|
||||||
@@ -731,14 +697,12 @@
|
|||||||
98.136.217.12/30 permit
|
98.136.217.12/30 permit
|
||||||
98.136.217.16/30 permit
|
98.136.217.16/30 permit
|
||||||
98.136.217.20/30 permit
|
98.136.217.20/30 permit
|
||||||
98.136.218.0/24 permit
|
|
||||||
98.136.218.39 permit
|
98.136.218.39 permit
|
||||||
98.136.218.40/29 permit
|
98.136.218.40/29 permit
|
||||||
98.136.218.48/28 permit
|
98.136.218.48/28 permit
|
||||||
98.136.218.67 permit
|
98.136.218.67 permit
|
||||||
98.136.218.68/30 permit
|
98.136.218.68/30 permit
|
||||||
98.136.218.72/30 permit
|
98.136.218.72/30 permit
|
||||||
98.137.12.0/24 permit
|
|
||||||
98.137.12.48/30 permit
|
98.137.12.48/30 permit
|
||||||
98.137.12.52/31 permit
|
98.137.12.52/31 permit
|
||||||
98.137.12.54 permit
|
98.137.12.54 permit
|
||||||
@@ -776,7 +740,6 @@
|
|||||||
98.137.13.132 permit
|
98.137.13.132 permit
|
||||||
98.137.13.137 permit
|
98.137.13.137 permit
|
||||||
98.137.13.138 permit
|
98.137.13.138 permit
|
||||||
98.137.64.0/21 permit
|
|
||||||
98.137.64.0/24 permit
|
98.137.64.0/24 permit
|
||||||
98.137.65.0/24 permit
|
98.137.65.0/24 permit
|
||||||
98.137.66.0/24 permit
|
98.137.66.0/24 permit
|
||||||
@@ -800,7 +763,6 @@
|
|||||||
98.138.83.176/31 permit
|
98.138.83.176/31 permit
|
||||||
98.138.83.179 permit
|
98.138.83.179 permit
|
||||||
98.138.83.180/31 permit
|
98.138.83.180/31 permit
|
||||||
98.138.84.0/22 permit
|
|
||||||
98.138.84.37 permit
|
98.138.84.37 permit
|
||||||
98.138.84.38/31 permit
|
98.138.84.38/31 permit
|
||||||
98.138.84.40/29 permit
|
98.138.84.40/29 permit
|
||||||
@@ -841,7 +803,6 @@
|
|||||||
98.138.87.148/31 permit
|
98.138.87.148/31 permit
|
||||||
98.138.87.192/30 permit
|
98.138.87.192/30 permit
|
||||||
98.138.87.196/31 permit
|
98.138.87.196/31 permit
|
||||||
98.138.88.0/22 permit
|
|
||||||
98.138.88.105 permit
|
98.138.88.105 permit
|
||||||
98.138.88.106 permit
|
98.138.88.106 permit
|
||||||
98.138.88.128/30 permit
|
98.138.88.128/30 permit
|
||||||
@@ -881,7 +842,6 @@
|
|||||||
98.138.91.2/31 permit
|
98.138.91.2/31 permit
|
||||||
98.138.91.4/31 permit
|
98.138.91.4/31 permit
|
||||||
98.138.91.6 permit
|
98.138.91.6 permit
|
||||||
98.138.100.0/23 permit
|
|
||||||
98.138.100.220/30 permit
|
98.138.100.220/30 permit
|
||||||
98.138.100.224/30 permit
|
98.138.100.224/30 permit
|
||||||
98.138.100.228/31 permit
|
98.138.100.228/31 permit
|
||||||
@@ -891,7 +851,6 @@
|
|||||||
98.138.104.100 permit
|
98.138.104.100 permit
|
||||||
98.138.104.112/30 permit
|
98.138.104.112/30 permit
|
||||||
98.138.104.116 permit
|
98.138.104.116 permit
|
||||||
98.138.120.0/24 permit
|
|
||||||
98.138.120.36/30 permit
|
98.138.120.36/30 permit
|
||||||
98.138.120.48/28 permit
|
98.138.120.48/28 permit
|
||||||
98.138.197.46/31 permit
|
98.138.197.46/31 permit
|
||||||
@@ -984,12 +943,10 @@
|
|||||||
98.138.213.238/31 permit
|
98.138.213.238/31 permit
|
||||||
98.138.213.240/31 permit
|
98.138.213.240/31 permit
|
||||||
98.138.213.242 permit
|
98.138.213.242 permit
|
||||||
98.138.215.0/24 permit
|
|
||||||
98.138.215.12/30 permit
|
98.138.215.12/30 permit
|
||||||
98.138.215.16/28 permit
|
98.138.215.16/28 permit
|
||||||
98.138.217.216/30 permit
|
98.138.217.216/30 permit
|
||||||
98.138.217.220/31 permit
|
98.138.217.220/31 permit
|
||||||
98.138.226.0/24 permit
|
|
||||||
98.138.226.30/31 permit
|
98.138.226.30/31 permit
|
||||||
98.138.226.56/29 permit
|
98.138.226.56/29 permit
|
||||||
98.138.226.64/30 permit
|
98.138.226.64/30 permit
|
||||||
@@ -1015,21 +972,18 @@
|
|||||||
98.138.227.108/31 permit
|
98.138.227.108/31 permit
|
||||||
98.138.227.128/30 permit
|
98.138.227.128/30 permit
|
||||||
98.138.227.132/31 permit
|
98.138.227.132/31 permit
|
||||||
98.138.229.0/24 permit
|
|
||||||
98.138.229.24/29 permit
|
98.138.229.24/29 permit
|
||||||
98.138.229.32/31 permit
|
98.138.229.32/31 permit
|
||||||
98.138.229.122/31 permit
|
98.138.229.122/31 permit
|
||||||
98.138.229.138/31 permit
|
98.138.229.138/31 permit
|
||||||
98.138.229.154/31 permit
|
98.138.229.154/31 permit
|
||||||
98.138.229.170/31 permit
|
98.138.229.170/31 permit
|
||||||
98.139.164.0/24 permit
|
|
||||||
98.139.164.96/30 permit
|
98.139.164.96/30 permit
|
||||||
98.139.164.100/30 permit
|
98.139.164.100/30 permit
|
||||||
98.139.164.104/29 permit
|
98.139.164.104/29 permit
|
||||||
98.139.164.112/30 permit
|
98.139.164.112/30 permit
|
||||||
98.139.172.112/30 permit
|
98.139.172.112/30 permit
|
||||||
98.139.172.116/31 permit
|
98.139.172.116/31 permit
|
||||||
98.139.175.0/24 permit
|
|
||||||
98.139.175.65 permit
|
98.139.175.65 permit
|
||||||
98.139.175.66/31 permit
|
98.139.175.66/31 permit
|
||||||
98.139.175.68/30 permit
|
98.139.175.68/30 permit
|
||||||
@@ -1054,10 +1008,8 @@
|
|||||||
98.139.210.196/31 permit
|
98.139.210.196/31 permit
|
||||||
98.139.210.202/31 permit
|
98.139.210.202/31 permit
|
||||||
98.139.210.204/30 permit
|
98.139.210.204/30 permit
|
||||||
98.139.211.0/24 permit
|
|
||||||
98.139.211.160/30 permit
|
98.139.211.160/30 permit
|
||||||
98.139.211.192/28 permit
|
98.139.211.192/28 permit
|
||||||
98.139.212.0/23 permit
|
|
||||||
98.139.212.160/28 permit
|
98.139.212.160/28 permit
|
||||||
98.139.212.176/29 permit
|
98.139.212.176/29 permit
|
||||||
98.139.212.184/30 permit
|
98.139.212.184/30 permit
|
||||||
@@ -1072,7 +1024,6 @@
|
|||||||
98.139.214.155 permit
|
98.139.214.155 permit
|
||||||
98.139.214.156/30 permit
|
98.139.214.156/30 permit
|
||||||
98.139.214.221 permit
|
98.139.214.221 permit
|
||||||
98.139.215.0/24 permit
|
|
||||||
98.139.215.228/31 permit
|
98.139.215.228/31 permit
|
||||||
98.139.215.230 permit
|
98.139.215.230 permit
|
||||||
98.139.215.248/30 permit
|
98.139.215.248/30 permit
|
||||||
@@ -1131,14 +1082,12 @@
|
|||||||
98.139.220.243 permit
|
98.139.220.243 permit
|
||||||
98.139.220.245 permit
|
98.139.220.245 permit
|
||||||
98.139.220.253 permit
|
98.139.220.253 permit
|
||||||
98.139.221.0/24 permit
|
|
||||||
98.139.221.43 permit
|
98.139.221.43 permit
|
||||||
98.139.221.60/30 permit
|
98.139.221.60/30 permit
|
||||||
98.139.221.156/30 permit
|
98.139.221.156/30 permit
|
||||||
98.139.221.232/30 permit
|
98.139.221.232/30 permit
|
||||||
98.139.221.236/31 permit
|
98.139.221.236/31 permit
|
||||||
98.139.221.250 permit
|
98.139.221.250 permit
|
||||||
98.139.244.0/24 permit
|
|
||||||
98.139.244.47 permit
|
98.139.244.47 permit
|
||||||
98.139.244.49 permit
|
98.139.244.49 permit
|
||||||
98.139.244.50/31 permit
|
98.139.244.50/31 permit
|
||||||
@@ -1178,6 +1127,7 @@
|
|||||||
98.139.245.208/30 permit
|
98.139.245.208/30 permit
|
||||||
98.139.245.212/31 permit
|
98.139.245.212/31 permit
|
||||||
99.78.197.208/28 permit
|
99.78.197.208/28 permit
|
||||||
|
99.83.190.102 permit
|
||||||
103.2.140.0/22 permit
|
103.2.140.0/22 permit
|
||||||
103.9.96.0/22 permit
|
103.9.96.0/22 permit
|
||||||
103.28.42.0/24 permit
|
103.28.42.0/24 permit
|
||||||
@@ -1193,7 +1143,6 @@
|
|||||||
104.130.96.0/28 permit
|
104.130.96.0/28 permit
|
||||||
104.130.122.0/23 permit
|
104.130.122.0/23 permit
|
||||||
104.214.25.77 permit
|
104.214.25.77 permit
|
||||||
104.245.209.192/26 permit
|
|
||||||
106.10.144.64/27 permit
|
106.10.144.64/27 permit
|
||||||
106.10.144.100/31 permit
|
106.10.144.100/31 permit
|
||||||
106.10.144.103 permit
|
106.10.144.103 permit
|
||||||
@@ -1219,7 +1168,6 @@
|
|||||||
106.10.146.52/31 permit
|
106.10.146.52/31 permit
|
||||||
106.10.146.224/30 permit
|
106.10.146.224/30 permit
|
||||||
106.10.146.228/31 permit
|
106.10.146.228/31 permit
|
||||||
106.10.148.0/24 permit
|
|
||||||
106.10.148.48/30 permit
|
106.10.148.48/30 permit
|
||||||
106.10.148.52/31 permit
|
106.10.148.52/31 permit
|
||||||
106.10.148.68/30 permit
|
106.10.148.68/30 permit
|
||||||
@@ -1232,7 +1180,6 @@
|
|||||||
106.10.149.30 permit
|
106.10.149.30 permit
|
||||||
106.10.149.160/30 permit
|
106.10.149.160/30 permit
|
||||||
106.10.149.164/31 permit
|
106.10.149.164/31 permit
|
||||||
106.10.150.0/23 permit
|
|
||||||
106.10.150.23 permit
|
106.10.150.23 permit
|
||||||
106.10.150.24/30 permit
|
106.10.150.24/30 permit
|
||||||
106.10.150.28/31 permit
|
106.10.150.28/31 permit
|
||||||
@@ -1271,7 +1218,6 @@
|
|||||||
106.10.151.250/31 permit
|
106.10.151.250/31 permit
|
||||||
106.10.151.252/31 permit
|
106.10.151.252/31 permit
|
||||||
106.10.151.254 permit
|
106.10.151.254 permit
|
||||||
106.10.167.0/24 permit
|
|
||||||
106.10.167.72 permit
|
106.10.167.72 permit
|
||||||
106.10.167.128/27 permit
|
106.10.167.128/27 permit
|
||||||
106.10.167.160/28 permit
|
106.10.167.160/28 permit
|
||||||
@@ -1293,7 +1239,6 @@
|
|||||||
106.10.174.120/30 permit
|
106.10.174.120/30 permit
|
||||||
106.10.174.154/31 permit
|
106.10.174.154/31 permit
|
||||||
106.10.174.156/30 permit
|
106.10.174.156/30 permit
|
||||||
106.10.176.0/24 permit
|
|
||||||
106.10.176.32/29 permit
|
106.10.176.32/29 permit
|
||||||
106.10.176.48 permit
|
106.10.176.48 permit
|
||||||
106.10.176.112 permit
|
106.10.176.112 permit
|
||||||
@@ -1312,7 +1257,6 @@
|
|||||||
106.10.196.43 permit
|
106.10.196.43 permit
|
||||||
106.10.196.44/30 permit
|
106.10.196.44/30 permit
|
||||||
106.10.196.48 permit
|
106.10.196.48 permit
|
||||||
106.10.240.0/22 permit
|
|
||||||
106.10.240.0/24 permit
|
106.10.240.0/24 permit
|
||||||
106.10.241.0/24 permit
|
106.10.241.0/24 permit
|
||||||
106.10.242.0/24 permit
|
106.10.242.0/24 permit
|
||||||
@@ -1320,6 +1264,7 @@
|
|||||||
106.10.244.0/24 permit
|
106.10.244.0/24 permit
|
||||||
106.39.212.64/29 permit
|
106.39.212.64/29 permit
|
||||||
106.50.16.0/28 permit
|
106.50.16.0/28 permit
|
||||||
|
107.20.210.250 permit
|
||||||
108.174.0.0/24 permit
|
108.174.0.0/24 permit
|
||||||
108.174.0.215 permit
|
108.174.0.215 permit
|
||||||
108.174.3.0/24 permit
|
108.174.3.0/24 permit
|
||||||
@@ -1330,6 +1275,7 @@
|
|||||||
108.175.30.45 permit
|
108.175.30.45 permit
|
||||||
108.177.8.0/21 permit
|
108.177.8.0/21 permit
|
||||||
108.177.96.0/19 permit
|
108.177.96.0/19 permit
|
||||||
|
108.179.144.0/20 permit
|
||||||
109.237.142.0/24 permit
|
109.237.142.0/24 permit
|
||||||
111.221.23.128/25 permit
|
111.221.23.128/25 permit
|
||||||
111.221.26.0/27 permit
|
111.221.26.0/27 permit
|
||||||
@@ -1338,7 +1284,6 @@
|
|||||||
111.221.112.0/21 permit
|
111.221.112.0/21 permit
|
||||||
112.19.199.64/29 permit
|
112.19.199.64/29 permit
|
||||||
112.19.242.64/29 permit
|
112.19.242.64/29 permit
|
||||||
116.214.12.0/24 permit
|
|
||||||
116.214.12.47 permit
|
116.214.12.47 permit
|
||||||
116.214.12.48/31 permit
|
116.214.12.48/31 permit
|
||||||
116.214.12.56/31 permit
|
116.214.12.56/31 permit
|
||||||
@@ -1357,7 +1302,6 @@
|
|||||||
121.244.91.48 permit
|
121.244.91.48 permit
|
||||||
122.15.156.182 permit
|
122.15.156.182 permit
|
||||||
123.126.78.64/29 permit
|
123.126.78.64/29 permit
|
||||||
124.108.96.0/24 permit
|
|
||||||
124.108.96.24/31 permit
|
124.108.96.24/31 permit
|
||||||
124.108.96.28/31 permit
|
124.108.96.28/31 permit
|
||||||
124.108.96.70/31 permit
|
124.108.96.70/31 permit
|
||||||
@@ -1419,9 +1363,11 @@
|
|||||||
135.84.216.0/22 permit
|
135.84.216.0/22 permit
|
||||||
136.143.160.0/24 permit
|
136.143.160.0/24 permit
|
||||||
136.143.161.0/24 permit
|
136.143.161.0/24 permit
|
||||||
|
136.143.178.49 permit
|
||||||
136.143.182.0/23 permit
|
136.143.182.0/23 permit
|
||||||
136.143.184.0/24 permit
|
136.143.184.0/24 permit
|
||||||
136.143.188.0/24 permit
|
136.143.188.0/24 permit
|
||||||
|
136.143.190.0/23 permit
|
||||||
136.147.128.0/20 permit
|
136.147.128.0/20 permit
|
||||||
136.147.135.0/24 permit
|
136.147.135.0/24 permit
|
||||||
136.147.176.0/20 permit
|
136.147.176.0/20 permit
|
||||||
@@ -1439,6 +1385,12 @@
|
|||||||
139.180.17.0/24 permit
|
139.180.17.0/24 permit
|
||||||
141.148.159.229 permit
|
141.148.159.229 permit
|
||||||
141.193.32.0/23 permit
|
141.193.32.0/23 permit
|
||||||
|
141.193.184.32/27 permit
|
||||||
|
141.193.184.64/26 permit
|
||||||
|
141.193.184.128/25 permit
|
||||||
|
141.193.185.32/27 permit
|
||||||
|
141.193.185.64/26 permit
|
||||||
|
141.193.185.128/25 permit
|
||||||
143.55.224.0/21 permit
|
143.55.224.0/21 permit
|
||||||
143.55.232.0/22 permit
|
143.55.232.0/22 permit
|
||||||
143.55.236.0/22 permit
|
143.55.236.0/22 permit
|
||||||
@@ -1452,13 +1404,13 @@
|
|||||||
144.178.38.0/24 permit
|
144.178.38.0/24 permit
|
||||||
145.253.228.160/29 permit
|
145.253.228.160/29 permit
|
||||||
145.253.239.128/29 permit
|
145.253.239.128/29 permit
|
||||||
|
146.20.14.104/30 permit
|
||||||
146.20.112.0/26 permit
|
146.20.112.0/26 permit
|
||||||
146.20.113.0/24 permit
|
146.20.113.0/24 permit
|
||||||
146.20.191.0/24 permit
|
146.20.191.0/24 permit
|
||||||
146.20.215.0/24 permit
|
146.20.215.0/24 permit
|
||||||
146.20.215.182 permit
|
146.20.215.182 permit
|
||||||
146.88.28.0/24 permit
|
146.88.28.0/24 permit
|
||||||
147.160.158.0/24 permit
|
|
||||||
147.243.1.47 permit
|
147.243.1.47 permit
|
||||||
147.243.1.48 permit
|
147.243.1.48 permit
|
||||||
147.243.1.153 permit
|
147.243.1.153 permit
|
||||||
@@ -1467,6 +1419,7 @@
|
|||||||
148.105.0.0/16 permit
|
148.105.0.0/16 permit
|
||||||
148.105.8.0/21 permit
|
148.105.8.0/21 permit
|
||||||
149.72.0.0/16 permit
|
149.72.0.0/16 permit
|
||||||
|
149.72.223.204 permit
|
||||||
149.72.248.236 permit
|
149.72.248.236 permit
|
||||||
149.97.173.180 permit
|
149.97.173.180 permit
|
||||||
150.230.98.160 permit
|
150.230.98.160 permit
|
||||||
@@ -1524,9 +1477,11 @@
|
|||||||
161.71.64.0/20 permit
|
161.71.64.0/20 permit
|
||||||
162.247.216.0/22 permit
|
162.247.216.0/22 permit
|
||||||
163.47.180.0/22 permit
|
163.47.180.0/22 permit
|
||||||
163.47.180.0/23 permit
|
|
||||||
163.114.130.16 permit
|
163.114.130.16 permit
|
||||||
163.114.132.120 permit
|
163.114.132.120 permit
|
||||||
|
163.114.134.16 permit
|
||||||
|
163.114.135.16 permit
|
||||||
|
164.177.132.168/30 permit
|
||||||
165.173.128.0/24 permit
|
165.173.128.0/24 permit
|
||||||
166.78.68.0/22 permit
|
166.78.68.0/22 permit
|
||||||
166.78.68.221 permit
|
166.78.68.221 permit
|
||||||
@@ -1547,8 +1502,6 @@
|
|||||||
167.89.75.164 permit
|
167.89.75.164 permit
|
||||||
167.89.101.2 permit
|
167.89.101.2 permit
|
||||||
167.89.101.192/28 permit
|
167.89.101.192/28 permit
|
||||||
167.216.129.210 permit
|
|
||||||
167.216.131.180 permit
|
|
||||||
167.220.67.232/29 permit
|
167.220.67.232/29 permit
|
||||||
168.138.5.36 permit
|
168.138.5.36 permit
|
||||||
168.138.73.51 permit
|
168.138.73.51 permit
|
||||||
@@ -1558,7 +1511,9 @@
|
|||||||
168.245.127.231 permit
|
168.245.127.231 permit
|
||||||
169.148.129.0/24 permit
|
169.148.129.0/24 permit
|
||||||
169.148.131.0/24 permit
|
169.148.131.0/24 permit
|
||||||
|
169.148.142.10 permit
|
||||||
169.148.144.0/25 permit
|
169.148.144.0/25 permit
|
||||||
|
169.148.144.10 permit
|
||||||
170.10.68.0/22 permit
|
170.10.68.0/22 permit
|
||||||
170.10.128.0/24 permit
|
170.10.128.0/24 permit
|
||||||
170.10.129.0/24 permit
|
170.10.129.0/24 permit
|
||||||
@@ -1596,14 +1551,14 @@
|
|||||||
182.50.76.0/22 permit
|
182.50.76.0/22 permit
|
||||||
182.50.78.64/28 permit
|
182.50.78.64/28 permit
|
||||||
183.240.219.64/29 permit
|
183.240.219.64/29 permit
|
||||||
185.4.120.0/23 permit
|
185.4.120.0/22 permit
|
||||||
185.4.122.0/24 permit
|
|
||||||
185.12.80.0/22 permit
|
185.12.80.0/22 permit
|
||||||
185.58.84.93 permit
|
185.58.84.93 permit
|
||||||
185.80.93.204 permit
|
185.80.93.204 permit
|
||||||
185.80.93.227 permit
|
185.80.93.227 permit
|
||||||
185.80.95.31 permit
|
185.80.95.31 permit
|
||||||
185.90.20.0/22 permit
|
185.90.20.0/22 permit
|
||||||
|
185.138.56.128/25 permit
|
||||||
185.189.236.0/22 permit
|
185.189.236.0/22 permit
|
||||||
185.211.120.0/22 permit
|
185.211.120.0/22 permit
|
||||||
185.250.236.0/22 permit
|
185.250.236.0/22 permit
|
||||||
@@ -1620,7 +1575,6 @@
|
|||||||
188.125.68.184 permit
|
188.125.68.184 permit
|
||||||
188.125.68.186 permit
|
188.125.68.186 permit
|
||||||
188.125.68.192 permit
|
188.125.68.192 permit
|
||||||
188.125.69.0/24 permit
|
|
||||||
188.125.69.105 permit
|
188.125.69.105 permit
|
||||||
188.125.69.110 permit
|
188.125.69.110 permit
|
||||||
188.125.69.112 permit
|
188.125.69.112 permit
|
||||||
@@ -1663,9 +1617,6 @@
|
|||||||
192.0.64.0/18 permit
|
192.0.64.0/18 permit
|
||||||
192.18.139.154 permit
|
192.18.139.154 permit
|
||||||
192.30.252.0/22 permit
|
192.30.252.0/22 permit
|
||||||
192.64.236.0/24 permit
|
|
||||||
192.64.237.0/24 permit
|
|
||||||
192.64.238.0/24 permit
|
|
||||||
192.161.144.0/20 permit
|
192.161.144.0/20 permit
|
||||||
192.162.87.0/24 permit
|
192.162.87.0/24 permit
|
||||||
192.237.158.0/23 permit
|
192.237.158.0/23 permit
|
||||||
@@ -1681,7 +1632,6 @@
|
|||||||
193.122.128.100 permit
|
193.122.128.100 permit
|
||||||
193.123.56.63 permit
|
193.123.56.63 permit
|
||||||
194.19.134.0/25 permit
|
194.19.134.0/25 permit
|
||||||
194.64.234.128/27 permit
|
|
||||||
194.64.234.129 permit
|
194.64.234.129 permit
|
||||||
194.106.220.0/23 permit
|
194.106.220.0/23 permit
|
||||||
194.113.24.0/22 permit
|
194.113.24.0/22 permit
|
||||||
@@ -1707,10 +1657,12 @@
|
|||||||
198.61.254.231 permit
|
198.61.254.231 permit
|
||||||
198.178.234.57 permit
|
198.178.234.57 permit
|
||||||
198.244.48.0/20 permit
|
198.244.48.0/20 permit
|
||||||
|
198.244.59.30 permit
|
||||||
|
198.244.59.33 permit
|
||||||
|
198.244.59.35 permit
|
||||||
198.244.60.0/22 permit
|
198.244.60.0/22 permit
|
||||||
198.245.80.0/20 permit
|
198.245.80.0/20 permit
|
||||||
198.245.81.0/24 permit
|
198.245.81.0/24 permit
|
||||||
199.15.176.173 permit
|
|
||||||
199.15.213.187 permit
|
199.15.213.187 permit
|
||||||
199.15.226.37 permit
|
199.15.226.37 permit
|
||||||
199.16.156.0/22 permit
|
199.16.156.0/22 permit
|
||||||
@@ -1718,6 +1670,10 @@
|
|||||||
199.33.145.32 permit
|
199.33.145.32 permit
|
||||||
199.34.22.36 permit
|
199.34.22.36 permit
|
||||||
199.59.148.0/22 permit
|
199.59.148.0/22 permit
|
||||||
|
199.67.80.2 permit
|
||||||
|
199.67.80.20 permit
|
||||||
|
199.67.82.2 permit
|
||||||
|
199.67.82.20 permit
|
||||||
199.67.84.0/24 permit
|
199.67.84.0/24 permit
|
||||||
199.67.86.0/24 permit
|
199.67.86.0/24 permit
|
||||||
199.67.88.0/24 permit
|
199.67.88.0/24 permit
|
||||||
@@ -1745,7 +1701,6 @@
|
|||||||
203.188.194.251 permit
|
203.188.194.251 permit
|
||||||
203.188.195.240/30 permit
|
203.188.195.240/30 permit
|
||||||
203.188.195.244/31 permit
|
203.188.195.244/31 permit
|
||||||
203.188.197.0/24 permit
|
|
||||||
203.188.197.193 permit
|
203.188.197.193 permit
|
||||||
203.188.197.194/31 permit
|
203.188.197.194/31 permit
|
||||||
203.188.197.196/30 permit
|
203.188.197.196/30 permit
|
||||||
@@ -1755,7 +1710,6 @@
|
|||||||
203.188.197.216/29 permit
|
203.188.197.216/29 permit
|
||||||
203.188.197.232/29 permit
|
203.188.197.232/29 permit
|
||||||
203.188.197.240/29 permit
|
203.188.197.240/29 permit
|
||||||
203.188.200.0/24 permit
|
|
||||||
203.188.200.56/31 permit
|
203.188.200.56/31 permit
|
||||||
203.188.200.58 permit
|
203.188.200.58 permit
|
||||||
203.188.200.60/30 permit
|
203.188.200.60/30 permit
|
||||||
@@ -1771,11 +1725,9 @@
|
|||||||
203.209.230.76/31 permit
|
203.209.230.76/31 permit
|
||||||
204.11.168.0/21 permit
|
204.11.168.0/21 permit
|
||||||
204.13.11.48/29 permit
|
204.13.11.48/29 permit
|
||||||
204.13.11.48/30 permit
|
|
||||||
204.14.232.0/21 permit
|
204.14.232.0/21 permit
|
||||||
204.14.232.64/28 permit
|
204.14.232.64/28 permit
|
||||||
204.14.234.64/28 permit
|
204.14.234.64/28 permit
|
||||||
204.29.186.0/23 permit
|
|
||||||
204.75.142.0/24 permit
|
204.75.142.0/24 permit
|
||||||
204.79.197.212 permit
|
204.79.197.212 permit
|
||||||
204.92.114.187 permit
|
204.92.114.187 permit
|
||||||
@@ -1824,6 +1776,7 @@
|
|||||||
207.67.98.192/27 permit
|
207.67.98.192/27 permit
|
||||||
207.68.176.0/26 permit
|
207.68.176.0/26 permit
|
||||||
207.68.176.96/27 permit
|
207.68.176.96/27 permit
|
||||||
|
207.97.204.96/29 permit
|
||||||
207.126.144.0/20 permit
|
207.126.144.0/20 permit
|
||||||
207.171.160.0/19 permit
|
207.171.160.0/19 permit
|
||||||
207.211.30.64/26 permit
|
207.211.30.64/26 permit
|
||||||
@@ -1837,11 +1790,7 @@
|
|||||||
208.43.21.28/30 permit
|
208.43.21.28/30 permit
|
||||||
208.43.21.64/29 permit
|
208.43.21.64/29 permit
|
||||||
208.43.21.72/30 permit
|
208.43.21.72/30 permit
|
||||||
208.46.212.80 permit
|
|
||||||
208.46.212.208/31 permit
|
|
||||||
208.46.212.210 permit
|
|
||||||
208.64.132.0/22 permit
|
208.64.132.0/22 permit
|
||||||
208.71.40.0/24 permit
|
|
||||||
208.71.40.63 permit
|
208.71.40.63 permit
|
||||||
208.71.40.64/31 permit
|
208.71.40.64/31 permit
|
||||||
208.71.40.174/31 permit
|
208.71.40.174/31 permit
|
||||||
@@ -1860,18 +1809,15 @@
|
|||||||
208.71.41.172/31 permit
|
208.71.41.172/31 permit
|
||||||
208.71.41.188/30 permit
|
208.71.41.188/30 permit
|
||||||
208.71.41.192/31 permit
|
208.71.41.192/31 permit
|
||||||
208.71.42.0/24 permit
|
|
||||||
208.71.42.190/31 permit
|
208.71.42.190/31 permit
|
||||||
208.71.42.192/28 permit
|
208.71.42.192/28 permit
|
||||||
208.71.42.208/30 permit
|
208.71.42.208/30 permit
|
||||||
208.71.42.212/31 permit
|
208.71.42.212/31 permit
|
||||||
208.71.42.214 permit
|
208.71.42.214 permit
|
||||||
208.72.249.240/29 permit
|
208.72.249.240/29 permit
|
||||||
208.74.204.0/22 permit
|
208.74.204.5 permit
|
||||||
208.74.204.9 permit
|
208.74.204.9 permit
|
||||||
208.75.120.0/22 permit
|
208.75.120.0/22 permit
|
||||||
208.75.121.246 permit
|
|
||||||
208.75.122.246 permit
|
|
||||||
208.82.237.96/29 permit
|
208.82.237.96/29 permit
|
||||||
208.82.237.104/31 permit
|
208.82.237.104/31 permit
|
||||||
208.82.238.96/29 permit
|
208.82.238.96/29 permit
|
||||||
@@ -1890,10 +1836,8 @@
|
|||||||
209.67.98.46 permit
|
209.67.98.46 permit
|
||||||
209.67.98.59 permit
|
209.67.98.59 permit
|
||||||
209.85.128.0/17 permit
|
209.85.128.0/17 permit
|
||||||
212.82.96.0/24 permit
|
|
||||||
212.82.96.32/27 permit
|
212.82.96.32/27 permit
|
||||||
212.82.96.64/29 permit
|
212.82.96.64/29 permit
|
||||||
212.82.98.0/24 permit
|
|
||||||
212.82.98.32/29 permit
|
212.82.98.32/29 permit
|
||||||
212.82.98.64/27 permit
|
212.82.98.64/27 permit
|
||||||
212.82.98.96/30 permit
|
212.82.98.96/30 permit
|
||||||
@@ -1930,12 +1874,41 @@
|
|||||||
212.82.111.228/31 permit
|
212.82.111.228/31 permit
|
||||||
212.82.111.230 permit
|
212.82.111.230 permit
|
||||||
212.123.28.40 permit
|
212.123.28.40 permit
|
||||||
212.227.15.0/24 permit
|
212.227.15.3 permit
|
||||||
212.227.15.0/25 permit
|
212.227.15.4 permit
|
||||||
212.227.17.0/27 permit
|
212.227.15.5 permit
|
||||||
212.227.126.128/25 permit
|
212.227.15.6 permit
|
||||||
|
212.227.15.14 permit
|
||||||
|
212.227.15.15 permit
|
||||||
|
212.227.15.18 permit
|
||||||
|
212.227.15.19 permit
|
||||||
|
212.227.15.25 permit
|
||||||
|
212.227.15.26 permit
|
||||||
|
212.227.15.29 permit
|
||||||
|
212.227.15.44 permit
|
||||||
|
212.227.15.45 permit
|
||||||
|
212.227.15.46 permit
|
||||||
|
212.227.15.47 permit
|
||||||
|
212.227.15.50 permit
|
||||||
|
212.227.15.52 permit
|
||||||
|
212.227.15.53 permit
|
||||||
|
212.227.15.54 permit
|
||||||
|
212.227.15.55 permit
|
||||||
|
212.227.17.11 permit
|
||||||
|
212.227.17.12 permit
|
||||||
|
212.227.17.18 permit
|
||||||
|
212.227.17.19 permit
|
||||||
|
212.227.17.20 permit
|
||||||
|
212.227.17.21 permit
|
||||||
|
212.227.17.22 permit
|
||||||
|
212.227.17.26 permit
|
||||||
|
212.227.17.28 permit
|
||||||
|
212.227.17.29 permit
|
||||||
|
212.227.126.224 permit
|
||||||
|
212.227.126.225 permit
|
||||||
|
212.227.126.226 permit
|
||||||
|
212.227.126.227 permit
|
||||||
213.46.255.0/24 permit
|
213.46.255.0/24 permit
|
||||||
213.165.64.0/23 permit
|
|
||||||
213.199.128.139 permit
|
213.199.128.139 permit
|
||||||
213.199.128.145 permit
|
213.199.128.145 permit
|
||||||
213.199.138.181 permit
|
213.199.138.181 permit
|
||||||
@@ -1946,7 +1919,6 @@
|
|||||||
216.17.150.251 permit
|
216.17.150.251 permit
|
||||||
216.22.15.224/27 permit
|
216.22.15.224/27 permit
|
||||||
216.24.224.0/20 permit
|
216.24.224.0/20 permit
|
||||||
216.39.60.0/23 permit
|
|
||||||
216.39.60.154/31 permit
|
216.39.60.154/31 permit
|
||||||
216.39.60.156/30 permit
|
216.39.60.156/30 permit
|
||||||
216.39.60.160/30 permit
|
216.39.60.160/30 permit
|
||||||
@@ -1964,14 +1936,12 @@
|
|||||||
216.39.61.170 permit
|
216.39.61.170 permit
|
||||||
216.39.61.175 permit
|
216.39.61.175 permit
|
||||||
216.39.61.238/31 permit
|
216.39.61.238/31 permit
|
||||||
216.39.62.0/24 permit
|
|
||||||
216.39.62.32/28 permit
|
216.39.62.32/28 permit
|
||||||
216.39.62.48/29 permit
|
216.39.62.48/29 permit
|
||||||
216.39.62.56/30 permit
|
216.39.62.56/30 permit
|
||||||
216.39.62.60/31 permit
|
216.39.62.60/31 permit
|
||||||
216.39.62.136/29 permit
|
216.39.62.136/29 permit
|
||||||
216.39.62.144/31 permit
|
216.39.62.144/31 permit
|
||||||
216.46.168.0/24 permit
|
|
||||||
216.58.192.0/19 permit
|
216.58.192.0/19 permit
|
||||||
216.66.217.240/29 permit
|
216.66.217.240/29 permit
|
||||||
216.71.138.33 permit
|
216.71.138.33 permit
|
||||||
@@ -1984,12 +1954,8 @@
|
|||||||
216.98.158.0/24 permit
|
216.98.158.0/24 permit
|
||||||
216.99.5.67 permit
|
216.99.5.67 permit
|
||||||
216.99.5.68 permit
|
216.99.5.68 permit
|
||||||
216.109.114.0/24 permit
|
|
||||||
216.109.114.32/27 permit
|
216.109.114.32/27 permit
|
||||||
216.109.114.64/29 permit
|
216.109.114.64/29 permit
|
||||||
216.113.160.0/24 permit
|
|
||||||
216.113.172.0/25 permit
|
|
||||||
216.113.175.0/24 permit
|
|
||||||
216.128.126.97 permit
|
216.128.126.97 permit
|
||||||
216.136.162.65 permit
|
216.136.162.65 permit
|
||||||
216.136.162.120/29 permit
|
216.136.162.120/29 permit
|
||||||
@@ -1999,10 +1965,10 @@
|
|||||||
216.203.30.55 permit
|
216.203.30.55 permit
|
||||||
216.203.33.178/31 permit
|
216.203.33.178/31 permit
|
||||||
216.205.24.0/24 permit
|
216.205.24.0/24 permit
|
||||||
|
216.221.160.0/19 permit
|
||||||
216.239.32.0/19 permit
|
216.239.32.0/19 permit
|
||||||
217.72.192.64/26 permit
|
217.72.192.77 permit
|
||||||
217.72.192.248/29 permit
|
217.72.192.78 permit
|
||||||
217.72.207.0/27 permit
|
|
||||||
217.77.141.52 permit
|
217.77.141.52 permit
|
||||||
217.77.141.59 permit
|
217.77.141.59 permit
|
||||||
217.175.194.0/24 permit
|
217.175.194.0/24 permit
|
||||||
@@ -2020,6 +1986,8 @@
|
|||||||
2603:1030:20e:3::23c permit
|
2603:1030:20e:3::23c permit
|
||||||
2603:1030:b:3::152 permit
|
2603:1030:b:3::152 permit
|
||||||
2603:1030:c02:8::14 permit
|
2603:1030:c02:8::14 permit
|
||||||
|
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||||
|
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||||
2607:f8b0:4000::/36 permit
|
2607:f8b0:4000::/36 permit
|
||||||
2620:109:c003:104::/64 permit
|
2620:109:c003:104::/64 permit
|
||||||
2620:109:c003:104::215 permit
|
2620:109:c003:104::215 permit
|
||||||
@@ -2028,6 +1996,8 @@
|
|||||||
2620:109:c00d:104::/64 permit
|
2620:109:c00d:104::/64 permit
|
||||||
2620:10d:c090:400::8:1 permit
|
2620:10d:c090:400::8:1 permit
|
||||||
2620:10d:c091:400::8:1 permit
|
2620:10d:c091:400::8:1 permit
|
||||||
|
2620:10d:c09b:400::8:1 permit
|
||||||
|
2620:10d:c09c:400::8:1 permit
|
||||||
2620:119:50c0:207::/64 permit
|
2620:119:50c0:207::/64 permit
|
||||||
2620:119:50c0:207::215 permit
|
2620:119:50c0:207::215 permit
|
||||||
2800:3f0:4000::/36 permit
|
2800:3f0:4000::/36 permit
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
// File size is limited by Nginx site to 10M
|
||||||
|
// To speed things up, we do not include prerequisites
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Do not show errors, we log to using error_log
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
// Init database
|
||||||
|
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||||
|
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||||
|
$opt = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
error_log("FOOTER: " . $e . PHP_EOL);
|
||||||
|
http_response_code(501);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('getallheaders')) {
|
||||||
|
function getallheaders() {
|
||||||
|
if (!is_array($_SERVER)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
$headers = array();
|
||||||
|
foreach ($_SERVER as $name => $value) {
|
||||||
|
if (substr($name, 0, 5) == 'HTTP_') {
|
||||||
|
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read headers
|
||||||
|
$headers = getallheaders();
|
||||||
|
// Get Domain
|
||||||
|
$domain = $headers['Domain'];
|
||||||
|
// Get Username
|
||||||
|
$username = $headers['Username'];
|
||||||
|
// Get From
|
||||||
|
$from = $headers['From'];
|
||||||
|
// define empty footer
|
||||||
|
$empty_footer = json_encode(array(
|
||||||
|
'html' => '',
|
||||||
|
'plain' => '',
|
||||||
|
'skip_replies' => 0,
|
||||||
|
'vars' => array()
|
||||||
|
));
|
||||||
|
|
||||||
|
error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// try get $target_domain if $domain is an alias_domain
|
||||||
|
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
|
||||||
|
WHERE `alias_domain` = :alias_domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':alias_domain' => $domain
|
||||||
|
));
|
||||||
|
$alias_domain = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$alias_domain) {
|
||||||
|
$target_domain = $domain;
|
||||||
|
} else {
|
||||||
|
$target_domain = $alias_domain['target_domain'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// get footer associated with the domain
|
||||||
|
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer`
|
||||||
|
WHERE `domain` = :domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $target_domain
|
||||||
|
));
|
||||||
|
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// check if the sender is excluded
|
||||||
|
if (in_array($from, json_decode($footer['mbox_exclude']))){
|
||||||
|
$footer = false;
|
||||||
|
}
|
||||||
|
if (in_array($domain, json_decode($footer['alias_domain_exclude']))){
|
||||||
|
$footer = false;
|
||||||
|
}
|
||||||
|
if (empty($footer)){
|
||||||
|
echo $empty_footer;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
error_log("FOOTER: " . json_encode($footer) . PHP_EOL);
|
||||||
|
|
||||||
|
// footer will be applied
|
||||||
|
// get custom mailbox attributes to insert into the footer
|
||||||
|
$stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':username' => $username
|
||||||
|
));
|
||||||
|
$custom_attributes = $stmt->fetch(PDO::FETCH_ASSOC)['custom_attributes'];
|
||||||
|
if (empty($custom_attributes)){
|
||||||
|
$custom_attributes = (object)array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
error_log("FOOTER: " . $e->getMessage() . PHP_EOL);
|
||||||
|
http_response_code(502);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// return footer
|
||||||
|
$footer["vars"] = $custom_attributes;
|
||||||
|
echo json_encode($footer);
|
||||||
@@ -6,3 +6,4 @@ disable_monitoring = true;
|
|||||||
# In case a task times out (like DNS lookup), soft reject the message
|
# In case a task times out (like DNS lookup), soft reject the message
|
||||||
# instead of silently accepting the message without further processing.
|
# instead of silently accepting the message without further processing.
|
||||||
soft_reject_on_timeout = true;
|
soft_reject_on_timeout = true;
|
||||||
|
local_addrs = /etc/rspamd/custom/mailcow_networks.map;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Uncomment below to apply the ratelimits globally. Use Ratelimits inside mailcow UI to overwrite them for a specific domain/mailbox.
|
||||||
|
# rates {
|
||||||
|
# # Format: "1 / 1h" or "20 / 1m" etc.
|
||||||
|
# to = "100 / 1s";
|
||||||
|
# to_ip = "100 / 1s";
|
||||||
|
# to_ip_from = "100 / 1s";
|
||||||
|
# bounce_to = "100 / 1h";
|
||||||
|
# bounce_to_ip = "7 / 1m";
|
||||||
|
# }
|
||||||
@@ -1,20 +1,4 @@
|
|||||||
rbls {
|
rbls {
|
||||||
sorbs {
|
|
||||||
symbol = "RBL_SORBS";
|
|
||||||
rbl = "dnsbl.sorbs.net";
|
|
||||||
returncodes {
|
|
||||||
# http:// www.sorbs.net/general/using.shtml
|
|
||||||
RBL_SORBS_HTTP = "127.0.0.2";
|
|
||||||
RBL_SORBS_SOCKS = "127.0.0.3";
|
|
||||||
RBL_SORBS_MISC = "127.0.0.4";
|
|
||||||
RBL_SORBS_SMTP = "127.0.0.5";
|
|
||||||
RBL_SORBS_RECENT = "127.0.0.6";
|
|
||||||
RBL_SORBS_WEB = "127.0.0.7";
|
|
||||||
RBL_SORBS_DUL = "127.0.0.10";
|
|
||||||
RBL_SORBS_BLOCK = "127.0.0.8";
|
|
||||||
RBL_SORBS_ZOMBIE = "127.0.0.9";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interserver_ip {
|
interserver_ip {
|
||||||
symbol = "RBL_INTERSERVER_IP";
|
symbol = "RBL_INTERSERVER_IP";
|
||||||
rbl = "rbl.interserver.net";
|
rbl = "rbl.interserver.net";
|
||||||
|
|||||||
@@ -5,46 +5,6 @@ symbols = {
|
|||||||
"RBL_UCEPROTECT_LEVEL2" {
|
"RBL_UCEPROTECT_LEVEL2" {
|
||||||
score = 1.5;
|
score = 1.5;
|
||||||
}
|
}
|
||||||
"RBL_SORBS" {
|
|
||||||
score = 0.0;
|
|
||||||
description = "Unrecognised result from SORBS RBL";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_HTTP" {
|
|
||||||
score = 2.5;
|
|
||||||
description = "List of Open HTTP Proxy Servers.";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_SOCKS" {
|
|
||||||
score = 2.5;
|
|
||||||
description = "List of Open SOCKS Proxy Servers.";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_MISC" {
|
|
||||||
score = 1.0;
|
|
||||||
description = "List of open Proxy Servers not listed in the SOCKS or HTTP lists.";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_SMTP" {
|
|
||||||
score = 4.0;
|
|
||||||
description = "List of Open SMTP relay servers.";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_RECENT" {
|
|
||||||
score = 2.0;
|
|
||||||
description = "List of hosts that have been noted as sending spam/UCE/UBE to the admins of SORBS within the last 28 days (includes new.spam.dnsbl.sorbs.net).";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_WEB" {
|
|
||||||
score = 2.0;
|
|
||||||
description = "List of web (WWW) servers which have spammer abusable vulnerabilities (e.g. FormMail scripts)";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_DUL" {
|
|
||||||
score = 2.0;
|
|
||||||
description = "Dynamic IP Address ranges (NOT a Dial Up list!)";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_BLOCK" {
|
|
||||||
score = 0.5;
|
|
||||||
description = "List of hosts demanding that they never be tested by SORBS.";
|
|
||||||
}
|
|
||||||
"RBL_SORBS_ZOMBIE" {
|
|
||||||
score = 2.0;
|
|
||||||
description = "List of networks hijacked from their original owners, some of which have already used for spamming.";
|
|
||||||
}
|
|
||||||
"RECEIVED_SPAMHAUS_XBL" {
|
"RECEIVED_SPAMHAUS_XBL" {
|
||||||
weight = 0.0;
|
weight = 0.0;
|
||||||
description = "Received address is listed in ZEN XBL";
|
description = "Received address is listed in ZEN XBL";
|
||||||
|
|||||||
@@ -527,20 +527,21 @@ rspamd_config:register_symbol({
|
|||||||
name = 'MOO_FOOTER',
|
name = 'MOO_FOOTER',
|
||||||
type = 'prefilter',
|
type = 'prefilter',
|
||||||
callback = function(task)
|
callback = function(task)
|
||||||
|
local cjson = require "cjson"
|
||||||
local lua_mime = require "lua_mime"
|
local lua_mime = require "lua_mime"
|
||||||
local lua_util = require "lua_util"
|
local lua_util = require "lua_util"
|
||||||
local rspamd_logger = require "rspamd_logger"
|
local rspamd_logger = require "rspamd_logger"
|
||||||
local rspamd_redis = require "rspamd_redis"
|
local rspamd_http = require "rspamd_http"
|
||||||
local ucl = require "ucl"
|
|
||||||
local redis_params = rspamd_parse_redis_server('footer')
|
|
||||||
local envfrom = task:get_from(1)
|
local envfrom = task:get_from(1)
|
||||||
local uname = task:get_user()
|
local uname = task:get_user()
|
||||||
if not envfrom or not uname then
|
if not envfrom or not uname then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local uname = uname:lower()
|
local uname = uname:lower()
|
||||||
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
|
local env_from_domain = envfrom[1].domain:lower()
|
||||||
|
local env_from_addr = envfrom[1].addr:lower()
|
||||||
|
|
||||||
|
-- determine newline type
|
||||||
local function newline(task)
|
local function newline(task)
|
||||||
local t = task:get_newlines_type()
|
local t = task:get_newlines_type()
|
||||||
|
|
||||||
@@ -552,20 +553,27 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
return '\r\n'
|
return '\r\n'
|
||||||
end
|
end
|
||||||
local function redis_cb_footer(err, data)
|
-- retrieve footer
|
||||||
|
local function footer_cb(err_message, code, data, headers)
|
||||||
if err or type(data) ~= 'string' then
|
if err or type(data) ~= 'string' then
|
||||||
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
||||||
else
|
else
|
||||||
|
|
||||||
-- parse json string
|
-- parse json string
|
||||||
local parser = ucl.parser()
|
local footer = cjson.decode(data)
|
||||||
local res,err = parser:parse_string(data)
|
if not footer then
|
||||||
if not res then
|
|
||||||
rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
||||||
else
|
else
|
||||||
local footer = parser:get_object()
|
|
||||||
|
|
||||||
if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then
|
if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then
|
||||||
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain)
|
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars)
|
||||||
|
|
||||||
|
if footer.skip_replies ~= 0 then
|
||||||
|
in_reply_to = task:get_header_raw('in-reply-to')
|
||||||
|
if in_reply_to then
|
||||||
|
rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local envfrom_mime = task:get_from(2)
|
local envfrom_mime = task:get_from(2)
|
||||||
local from_name = ""
|
local from_name = ""
|
||||||
@@ -575,6 +583,7 @@ rspamd_config:register_symbol({
|
|||||||
from_name = envfrom[1].name
|
from_name = envfrom[1].name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- default replacements
|
||||||
local replacements = {
|
local replacements = {
|
||||||
auth_user = uname,
|
auth_user = uname,
|
||||||
from_user = envfrom[1].user,
|
from_user = envfrom[1].user,
|
||||||
@@ -582,10 +591,20 @@ rspamd_config:register_symbol({
|
|||||||
from_addr = envfrom[1].addr,
|
from_addr = envfrom[1].addr,
|
||||||
from_domain = envfrom[1].domain:lower()
|
from_domain = envfrom[1].domain:lower()
|
||||||
}
|
}
|
||||||
if footer.html then
|
-- add custom mailbox attributes
|
||||||
|
if footer.vars and type(footer.vars) == "string" then
|
||||||
|
local footer_vars = cjson.decode(footer.vars)
|
||||||
|
|
||||||
|
if type(footer_vars) == "table" then
|
||||||
|
for key, value in pairs(footer_vars) do
|
||||||
|
replacements[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if footer.html and footer.html ~= "" then
|
||||||
footer.html = lua_util.jinja_template(footer.html, replacements, true)
|
footer.html = lua_util.jinja_template(footer.html, replacements, true)
|
||||||
end
|
end
|
||||||
if footer.plain then
|
if footer.plain and footer.plain ~= "" then
|
||||||
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
|
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -602,10 +621,24 @@ rspamd_config:register_symbol({
|
|||||||
local nct = string.format('%s: %s/%s; charset=utf-8',
|
local nct = string.format('%s: %s/%s; charset=utf-8',
|
||||||
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
|
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
|
||||||
out[#out + 1] = nct
|
out[#out + 1] = nct
|
||||||
|
-- update Content-Type header
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {['Content-Type'] = 0},
|
||||||
|
})
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
|
||||||
|
})
|
||||||
return
|
return
|
||||||
elseif name:lower() == 'content-transfer-encoding' then
|
elseif name:lower() == 'content-transfer-encoding' then
|
||||||
out[#out + 1] = string.format('%s: %s',
|
out[#out + 1] = string.format('%s: %s',
|
||||||
'Content-Transfer-Encoding', 'quoted-printable')
|
'Content-Transfer-Encoding', 'quoted-printable')
|
||||||
|
-- update Content-Transfer-Encoding header
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {['Content-Transfer-Encoding'] = 0},
|
||||||
|
})
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
|
||||||
|
})
|
||||||
seen_cte = true
|
seen_cte = true
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -653,17 +686,14 @@ rspamd_config:register_symbol({
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local redis_ret_footer = rspamd_redis_make_request(task,
|
-- fetch footer
|
||||||
redis_params, -- connect params
|
rspamd_http.request({
|
||||||
env_from_domain, -- hash key
|
task=task,
|
||||||
false, -- is write
|
url='http://nginx:8081/footer.php',
|
||||||
redis_cb_footer, --callback
|
body='',
|
||||||
'HGET', -- command
|
callback=footer_cb,
|
||||||
{"DOMAIN_WIDE_FOOTER", env_from_domain} -- arguments
|
headers={Domain=env_from_domain,Username=uname,From=env_from_addr},
|
||||||
)
|
})
|
||||||
if not redis_ret_footer then
|
|
||||||
rspamd_logger.infox(rspamd_config, "cannot make request to load footer for domain")
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
rates {
|
|
||||||
# Format: "1 / 1h" or "20 / 1m" etc. - global ratelimits are disabled by default
|
|
||||||
to = "100 / 1s";
|
|
||||||
to_ip = "100 / 1s";
|
|
||||||
to_ip_from = "100 / 1s";
|
|
||||||
bounce_to = "100 / 1h";
|
|
||||||
bounce_to_ip = "7 / 1m";
|
|
||||||
}
|
|
||||||
whitelisted_rcpts = "postmaster,mailer-daemon";
|
whitelisted_rcpts = "postmaster,mailer-daemon";
|
||||||
max_rcpt = 25;
|
max_rcpt = 25;
|
||||||
custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
|
custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
|
||||||
|
|||||||
@@ -12,9 +12,13 @@
|
|||||||
SOGoJunkFolderName= "Junk";
|
SOGoJunkFolderName= "Junk";
|
||||||
SOGoMailDomain = "sogo.local";
|
SOGoMailDomain = "sogo.local";
|
||||||
SOGoEnableEMailAlarms = YES;
|
SOGoEnableEMailAlarms = YES;
|
||||||
|
SOGoMailHideInlineAttachments = YES;
|
||||||
SOGoFoldersSendEMailNotifications = YES;
|
SOGoFoldersSendEMailNotifications = YES;
|
||||||
SOGoForwardEnabled = YES;
|
SOGoForwardEnabled = YES;
|
||||||
|
|
||||||
|
// Option to set Users as admin to globally manage calendar permissions etc. Disabled by default
|
||||||
|
// SOGoSuperUsernames = ("moo@example.com");
|
||||||
|
|
||||||
SOGoUIAdditionalJSFiles = (
|
SOGoUIAdditionalJSFiles = (
|
||||||
js/theme.js,
|
js/theme.js,
|
||||||
js/custom-sogo.js
|
js/custom-sogo.js
|
||||||
@@ -37,6 +41,7 @@
|
|||||||
|
|
||||||
SOGoLanguage = English;
|
SOGoLanguage = English;
|
||||||
SOGoMailAuxiliaryUserAccountsEnabled = YES;
|
SOGoMailAuxiliaryUserAccountsEnabled = YES;
|
||||||
|
// SOGoCreateIdentitiesDisabled = NO;
|
||||||
SOGoMailCustomFromEnabled = YES;
|
SOGoMailCustomFromEnabled = YES;
|
||||||
SOGoMailingMechanism = smtp;
|
SOGoMailingMechanism = smtp;
|
||||||
SOGoSMTPAuthenticationType = plain;
|
SOGoSMTPAuthenticationType = plain;
|
||||||
|
|||||||
+5
-1
@@ -85,6 +85,8 @@ $cors_settings = cors('get');
|
|||||||
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
||||||
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||||
|
|
||||||
|
$f2b_data = fail2ban('get');
|
||||||
|
|
||||||
$template = 'admin.twig';
|
$template = 'admin.twig';
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'tfa_data' => $tfa_data,
|
'tfa_data' => $tfa_data,
|
||||||
@@ -101,7 +103,8 @@ $template_data = [
|
|||||||
'domains' => $domains,
|
'domains' => $domains,
|
||||||
'all_domains' => $all_domains,
|
'all_domains' => $all_domains,
|
||||||
'mailboxes' => $mailboxes,
|
'mailboxes' => $mailboxes,
|
||||||
'f2b_data' => fail2ban('get'),
|
'f2b_data' => $f2b_data,
|
||||||
|
'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'],
|
||||||
'q_data' => quarantine('settings'),
|
'q_data' => quarantine('settings'),
|
||||||
'qn_data' => quota_notification('get'),
|
'qn_data' => quota_notification('get'),
|
||||||
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
|
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
|
||||||
@@ -113,6 +116,7 @@ $template_data = [
|
|||||||
'password_complexity' => password_complexity('get'),
|
'password_complexity' => password_complexity('get'),
|
||||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||||
'cors_settings' => $cors_settings,
|
'cors_settings' => $cors_settings,
|
||||||
|
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||||
'lang_admin' => json_encode($lang['admin']),
|
'lang_admin' => json_encode($lang['admin']),
|
||||||
'lang_datatables' => json_encode($lang['datatables'])
|
'lang_datatables' => json_encode($lang['datatables'])
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3137,6 +3137,86 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
summary: Update domain
|
summary: Update domain
|
||||||
|
/api/v1/edit/domain/footer:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- mailbox
|
||||||
|
- edit
|
||||||
|
- domain_wide_footer
|
||||||
|
- domains:
|
||||||
|
- mailcow.tld
|
||||||
|
html: "<br>foo {= foo =}"
|
||||||
|
plain: "<foo {= foo =}"
|
||||||
|
mbox_exclude:
|
||||||
|
- moo@mailcow.tld
|
||||||
|
- null
|
||||||
|
msg:
|
||||||
|
- domain_footer_modified
|
||||||
|
- mailcow.tld
|
||||||
|
type: success
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
log:
|
||||||
|
description: contains request object
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
msg:
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- success
|
||||||
|
- danger
|
||||||
|
- error
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Domains
|
||||||
|
description: >-
|
||||||
|
You can update the footer of one or more domains per request.
|
||||||
|
operationId: Update domain wide footer
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
attr:
|
||||||
|
html: "<br>foo {= foo =}"
|
||||||
|
plain: "foo {= foo =}"
|
||||||
|
mbox_exclude:
|
||||||
|
- moo@mailcow.tld
|
||||||
|
items: mailcow.tld
|
||||||
|
properties:
|
||||||
|
attr:
|
||||||
|
properties:
|
||||||
|
html:
|
||||||
|
description: Footer text in HTML format
|
||||||
|
type: string
|
||||||
|
plain:
|
||||||
|
description: Footer text in PLAIN text format
|
||||||
|
type: string
|
||||||
|
mbox_exclude:
|
||||||
|
description: Array of mailboxes to exclude from domain wide footer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
items:
|
||||||
|
description: contains a list of domain names where you want to update the footer
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Update domain wide footer
|
||||||
/api/v1/edit/fail2ban:
|
/api/v1/edit/fail2ban:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -3336,6 +3416,86 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
summary: Update mailbox
|
summary: Update mailbox
|
||||||
|
/api/v1/edit/mailbox/custom-attribute:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- mailbox
|
||||||
|
- edit
|
||||||
|
- mailbox_custom_attribute
|
||||||
|
- mailboxes:
|
||||||
|
- moo@mailcow.tld
|
||||||
|
attribute:
|
||||||
|
- role
|
||||||
|
- foo
|
||||||
|
value:
|
||||||
|
- cow
|
||||||
|
- bar
|
||||||
|
- null
|
||||||
|
msg:
|
||||||
|
- mailbox_modified
|
||||||
|
- moo@mailcow.tld
|
||||||
|
type: success
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
log:
|
||||||
|
description: contains request object
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
msg:
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- success
|
||||||
|
- danger
|
||||||
|
- error
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Mailboxes
|
||||||
|
description: >-
|
||||||
|
You can update custom attributes of one or more mailboxes per request.
|
||||||
|
operationId: Update mailbox custom attributes
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
attr:
|
||||||
|
attribute:
|
||||||
|
- role
|
||||||
|
- foo
|
||||||
|
value:
|
||||||
|
- cow
|
||||||
|
- bar
|
||||||
|
items:
|
||||||
|
- moo@mailcow.tld
|
||||||
|
properties:
|
||||||
|
attr:
|
||||||
|
properties:
|
||||||
|
attribute:
|
||||||
|
description: Array of attribute keys
|
||||||
|
type: object
|
||||||
|
value:
|
||||||
|
description: Array of attribute values
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
items:
|
||||||
|
description: contains list of mailboxes you want update
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
summary: Update mailbox custom attributes
|
||||||
/api/v1/edit/mailq:
|
/api/v1/edit/mailq:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -5581,6 +5741,7 @@ paths:
|
|||||||
sogo_access: "1"
|
sogo_access: "1"
|
||||||
tls_enforce_in: "0"
|
tls_enforce_in: "0"
|
||||||
tls_enforce_out: "0"
|
tls_enforce_out: "0"
|
||||||
|
custom_attributes: {}
|
||||||
domain: domain3.tld
|
domain: domain3.tld
|
||||||
is_relayed: 0
|
is_relayed: 0
|
||||||
local_part: info
|
local_part: info
|
||||||
@@ -5646,6 +5807,40 @@ paths:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
summary: Edit Cross-Origin Resource Sharing (CORS) settings
|
summary: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||||
|
"/api/v1/get/spam-score/{mailbox}":
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: name of mailbox or empty for current user - admin user will retrieve the global spam filter score
|
||||||
|
in: path
|
||||||
|
name: mailbox
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- description: e.g. api-key-string
|
||||||
|
example: api-key-string
|
||||||
|
in: header
|
||||||
|
name: X-API-Key
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
spam_score: "8,15"
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Mailboxes
|
||||||
|
description: >-
|
||||||
|
Using this endpoint you can get the global spam filter score or the spam filter score of a certain mailbox.
|
||||||
|
operationId: Get mailbox or global spam filter score
|
||||||
|
summary: Get mailbox or global spam filter score
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- name: Domains
|
- name: Domains
|
||||||
|
|||||||
@@ -228,8 +228,8 @@ legend {
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
.slave-info {
|
.slave-info {
|
||||||
padding: 15px 0px 15px 15px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: orange;
|
||||||
}
|
}
|
||||||
.alert-hr {
|
.alert-hr {
|
||||||
margin:3px 0px;
|
margin:3px 0px;
|
||||||
|
|||||||
@@ -175,6 +175,9 @@ pre {
|
|||||||
background-color: #282828;
|
background-color: #282828;
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
}
|
}
|
||||||
|
.form-control {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
input.form-control, textarea.form-control {
|
input.form-control, textarea.form-control {
|
||||||
color: #e2e2e2 !important;
|
color: #e2e2e2 !important;
|
||||||
background-color: #424242 !important;
|
background-color: #424242 !important;
|
||||||
|
|||||||
+7
-3
@@ -39,9 +39,13 @@ foreach ($containers as $container => $container_info) {
|
|||||||
$StartedAt['month'],
|
$StartedAt['month'],
|
||||||
$StartedAt['day'],
|
$StartedAt['day'],
|
||||||
$StartedAt['year']));
|
$StartedAt['year']));
|
||||||
$user_tz = new DateTimeZone(getenv('TZ'));
|
try {
|
||||||
$date->setTimezone($user_tz);
|
$user_tz = new DateTimeZone(getenv('TZ'));
|
||||||
$started = $date->format('r');
|
$date->setTimezone($user_tz);
|
||||||
|
$started = $date->format('r');
|
||||||
|
} catch(Exception $e) {
|
||||||
|
$started = '?';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$started = '?';
|
$started = '?';
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
|||||||
'dkim' => dkim('details', $domain),
|
'dkim' => dkim('details', $domain),
|
||||||
'domain_details' => $result,
|
'domain_details' => $result,
|
||||||
'domain_footer' => $domain_footer,
|
'domain_footer' => $domain_footer,
|
||||||
|
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
|
||||||
|
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
|
||||||
|
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +221,7 @@ $js_minifier->add('/web/js/site/pwgen.js');
|
|||||||
$template_data['result'] = $result;
|
$template_data['result'] = $result;
|
||||||
$template_data['return_to'] = $_SESSION['return_to'];
|
$template_data['return_to'] = $_SESSION['return_to'];
|
||||||
$template_data['lang_user'] = json_encode($lang['user']);
|
$template_data['lang_user'] = json_encode($lang['user']);
|
||||||
|
$template_data['lang_admin'] = json_encode($lang['admin']);
|
||||||
$template_data['lang_datatables'] = json_encode($lang['datatables']);
|
$template_data['lang_datatables'] = json_encode($lang['datatables']);
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
// Block requests by checking the 'Sec-Fetch-Dest' header.
|
||||||
|
if (isset($_SERVER['HTTP_SEC_FETCH_DEST']) && $_SERVER['HTTP_SEC_FETCH_DEST'] !== 'empty') {
|
||||||
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
|
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
|
||||||
exit();
|
exit();
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ $alertbox_log_parser = alertbox_log_parser($_SESSION);
|
|||||||
$alerts = [];
|
$alerts = [];
|
||||||
if (is_array($alertbox_log_parser)) {
|
if (is_array($alertbox_log_parser)) {
|
||||||
foreach ($alertbox_log_parser as $log) {
|
foreach ($alertbox_log_parser as $log) {
|
||||||
$message = strtr($log['msg'], ["\n" => '', "\r" => '', "\t" => '<br>']);
|
$message = htmlspecialchars($log['msg'], ENT_QUOTES);
|
||||||
|
$message = strtr($message, ["\n" => '', "\r" => '', "\t" => '<br>']);
|
||||||
$alerts[trim($log['type'], '"')][] = trim($message, '"');
|
$alerts[trim($log['type'], '"')][] = trim($message, '"');
|
||||||
}
|
}
|
||||||
$alert = array_filter(array_unique($alerts));
|
$alert = array_filter(array_unique($alerts));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
function customize($_action, $_item, $_data = null) {
|
function customize($_action, $_item, $_data = null) {
|
||||||
global $redis;
|
global $redis;
|
||||||
global $lang;
|
global $lang;
|
||||||
|
global $LOGO_LIMITS;
|
||||||
|
|
||||||
switch ($_action) {
|
switch ($_action) {
|
||||||
case 'add':
|
case 'add':
|
||||||
@@ -35,6 +36,23 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if ($_data[$_item]['size'] > $LOGO_LIMITS['max_size']) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||||
|
'msg' => 'img_size_exceeded'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
list($width, $height) = getimagesize($_data[$_item]['tmp_name']);
|
||||||
|
if ($width > $LOGO_LIMITS['max_width'] || $height > $LOGO_LIMITS['max_height']) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||||
|
'msg' => 'img_dimensions_exceeded'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$image = new Imagick($_data[$_item]['tmp_name']);
|
$image = new Imagick($_data[$_item]['tmp_name']);
|
||||||
if ($image->valid() !== true) {
|
if ($image->valid() !== true) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
function fail2ban($_action, $_data = null) {
|
function fail2ban($_action, $_data = null, $_extra = null) {
|
||||||
global $redis;
|
global $redis;
|
||||||
$_data_log = $_data;
|
$_data_log = $_data;
|
||||||
switch ($_action) {
|
switch ($_action) {
|
||||||
@@ -247,6 +247,7 @@ function fail2ban($_action, $_data = null) {
|
|||||||
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
|
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
|
||||||
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
|
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
|
||||||
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
|
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
|
||||||
|
$manage_external = (isset($_data['manage_external'])) ? intval($_data['manage_external']) : 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -266,6 +267,8 @@ function fail2ban($_action, $_data = null) {
|
|||||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
||||||
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
||||||
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
||||||
|
$f2b_options['banlist_id'] = $is_now['banlist_id'];
|
||||||
|
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
|
||||||
try {
|
try {
|
||||||
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||||
$redis->Del('F2B_WHITELIST');
|
$redis->Del('F2B_WHITELIST');
|
||||||
@@ -329,5 +332,71 @@ function fail2ban($_action, $_data = null) {
|
|||||||
'msg' => 'f2b_modified'
|
'msg' => 'f2b_modified'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'banlist':
|
||||||
|
try {
|
||||||
|
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
http_response_code(500);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (is_array($_extra)) {
|
||||||
|
$_extra = $_extra[0];
|
||||||
|
}
|
||||||
|
if ($_extra != $f2b_options['banlist_id']){
|
||||||
|
http_response_code(404);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($_data) {
|
||||||
|
case 'get':
|
||||||
|
try {
|
||||||
|
$bl = $redis->hKeys('F2B_BLACKLIST');
|
||||||
|
$active_bans = $redis->hKeys('F2B_ACTIVE_BANS');
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
http_response_code(500);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$banlist = implode("\n", array_merge($bl, $active_bans));
|
||||||
|
return $banlist;
|
||||||
|
break;
|
||||||
|
case 'refresh':
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$f2b_options['banlist_id'] = uuid4();
|
||||||
|
try {
|
||||||
|
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => 'f2b_banlist_refreshed'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,17 +284,17 @@ function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
|
|||||||
}
|
}
|
||||||
if (!$sasl[$k]['location']) {
|
if (!$sasl[$k]['location']) {
|
||||||
$curl = curl_init();
|
$curl = curl_init();
|
||||||
curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/lookup/" . $sasl[$k]['real_rip']);
|
curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/country/" . $sasl[$k]['real_rip']);
|
||||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||||
curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow');
|
curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow');
|
||||||
curl_setopt($curl, CURLOPT_TIMEOUT, 5);
|
curl_setopt($curl, CURLOPT_TIMEOUT, 5);
|
||||||
$ip_data = curl_exec($curl);
|
$ip_data = curl_exec($curl);
|
||||||
if (!curl_errno($curl)) {
|
if (!curl_errno($curl)) {
|
||||||
$ip_data_array = json_decode($ip_data, true);
|
$ip_data_array = json_decode($ip_data, true);
|
||||||
if ($ip_data_array !== false and !empty($ip_data_array['location']['shortcountry'])) {
|
if ($ip_data_array !== false and !empty($ip_data_array['shortcountry'])) {
|
||||||
$sasl[$k]['location'] = $ip_data_array['location']['shortcountry'];
|
$sasl[$k]['location'] = $ip_data_array['shortcountry'];
|
||||||
try {
|
try {
|
||||||
$redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['location']['shortcountry']);
|
$redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['shortcountry']);
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
catch (RedisException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -2246,6 +2246,21 @@ function cors($action, $data = null) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function getBaseURL() {
|
||||||
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||||
|
$host = $_SERVER['HTTP_HOST'];
|
||||||
|
$base_url = $protocol . '://' . $host;
|
||||||
|
|
||||||
|
return $base_url;
|
||||||
|
}
|
||||||
|
function uuid4() {
|
||||||
|
$data = openssl_random_pseudo_bytes(16);
|
||||||
|
|
||||||
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||||
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||||
|
|
||||||
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||||
|
}
|
||||||
|
|
||||||
function get_logs($application, $lines = false) {
|
function get_logs($application, $lines = false) {
|
||||||
if ($lines === false) {
|
if ($lines === false) {
|
||||||
|
|||||||
@@ -478,16 +478,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
$DOMAIN_DEFAULT_ATTRIBUTES = null;
|
||||||
|
if ($_data['template']){
|
||||||
|
$DOMAIN_DEFAULT_ATTRIBUTES = mailbox('get', 'domain_templates', $_data['template'])['attributes'];
|
||||||
|
}
|
||||||
|
if (empty($DOMAIN_DEFAULT_ATTRIBUTES)) {
|
||||||
|
$DOMAIN_DEFAULT_ATTRIBUTES = mailbox('get', 'domain_templates')[0]['attributes'];
|
||||||
|
}
|
||||||
|
|
||||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
||||||
$description = $_data['description'];
|
$description = $_data['description'];
|
||||||
if (empty($description)) $description = $domain;
|
if (empty($description)) $description = $domain;
|
||||||
$tags = (array)$_data['tags'];
|
$tags = (isset($_data['tags'])) ? (array)$_data['tags'] : $DOMAIN_DEFAULT_ATTRIBUTES['tags'];
|
||||||
$aliases = (int)$_data['aliases'];
|
$aliases = (isset($_data['aliases'])) ? (int)$_data['aliases'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_aliases_for_domain'];
|
||||||
$mailboxes = (int)$_data['mailboxes'];
|
$mailboxes = (isset($_data['mailboxes'])) ? (int)$_data['mailboxes'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_mboxes_for_domain'];
|
||||||
$defquota = (int)$_data['defquota'];
|
$defquota = (isset($_data['defquota'])) ? (int)$_data['defquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['def_quota_for_mbox'] / 1024 ** 2;
|
||||||
$maxquota = (int)$_data['maxquota'];
|
$maxquota = (isset($_data['maxquota'])) ? (int)$_data['maxquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_mbox'] / 1024 ** 2;
|
||||||
$restart_sogo = (int)$_data['restart_sogo'];
|
$restart_sogo = (int)$_data['restart_sogo'];
|
||||||
$quota = (int)$_data['quota'];
|
$quota = (isset($_data['quota'])) ? (int)$_data['quota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_domain'] / 1024 ** 2;
|
||||||
if ($defquota > $maxquota) {
|
if ($defquota > $maxquota) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
@@ -520,11 +528,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$active = intval($_data['active']);
|
$active = (isset($_data['active'])) ? intval($_data['active']) : $DOMAIN_DEFAULT_ATTRIBUTES['active'];
|
||||||
$relay_all_recipients = intval($_data['relay_all_recipients']);
|
$relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_all_recipients'];
|
||||||
$relay_unknown_only = intval($_data['relay_unknown_only']);
|
$relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_unknown_only'];
|
||||||
$backupmx = intval($_data['backupmx']);
|
$backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $DOMAIN_DEFAULT_ATTRIBUTES['backupmx'];
|
||||||
$gal = intval($_data['gal']);
|
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $DOMAIN_DEFAULT_ATTRIBUTES['gal'];
|
||||||
if ($relay_all_recipients == 1) {
|
if ($relay_all_recipients == 1) {
|
||||||
$backupmx = '1';
|
$backupmx = '1';
|
||||||
}
|
}
|
||||||
@@ -625,9 +633,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!empty(intval($_data['rl_value']))) {
|
$_data['rl_value'] = (isset($_data['rl_value'])) ? intval($_data['rl_value']) : $DOMAIN_DEFAULT_ATTRIBUTES['rl_value'];
|
||||||
|
$_data['rl_frame'] = (isset($_data['rl_frame'])) ? $_data['rl_frame'] : $DOMAIN_DEFAULT_ATTRIBUTES['rl_frame'];
|
||||||
|
if (!empty($_data['rl_value']) && !empty($_data['rl_frame'])){
|
||||||
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain));
|
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain));
|
||||||
}
|
}
|
||||||
|
$_data['key_size'] = (isset($_data['key_size'])) ? intval($_data['key_size']) : $DOMAIN_DEFAULT_ATTRIBUTES['key_size'];
|
||||||
|
$_data['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : $DOMAIN_DEFAULT_ATTRIBUTES['dkim_selector'];
|
||||||
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
|
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
|
||||||
if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) {
|
if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -1006,11 +1018,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (empty($name)) {
|
||||||
|
$name = $local_part;
|
||||||
|
}
|
||||||
|
$template_attr = null;
|
||||||
|
if ($_data['template']){
|
||||||
|
$template_attr = mailbox('get', 'mailbox_templates', $_data['template'])['attributes'];
|
||||||
|
}
|
||||||
|
if (empty($template_attr)) {
|
||||||
|
$template_attr = mailbox('get', 'mailbox_templates')[0]['attributes'];
|
||||||
|
}
|
||||||
|
$MAILBOX_DEFAULT_ATTRIBUTES = array_merge($MAILBOX_DEFAULT_ATTRIBUTES, $template_attr);
|
||||||
|
|
||||||
$password = $_data['password'];
|
$password = $_data['password'];
|
||||||
$password2 = $_data['password2'];
|
$password2 = $_data['password2'];
|
||||||
$name = ltrim(rtrim($_data['name'], '>'), '<');
|
$name = ltrim(rtrim($_data['name'], '>'), '<');
|
||||||
$tags = $_data['tags'];
|
$tags = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags'];
|
||||||
$quota_m = intval($_data['quota']);
|
$quota_m = (isset($_data['quota'])) ? intval($_data['quota']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['quota']) / 1024 ** 2;
|
||||||
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
|
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
@@ -1019,9 +1043,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (empty($name)) {
|
|
||||||
$name = $local_part;
|
|
||||||
}
|
|
||||||
if (isset($_data['protocol_access'])) {
|
if (isset($_data['protocol_access'])) {
|
||||||
$_data['protocol_access'] = (array)$_data['protocol_access'];
|
$_data['protocol_access'] = (array)$_data['protocol_access'];
|
||||||
$_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
|
$_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
|
||||||
@@ -1029,7 +1051,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||||
}
|
}
|
||||||
$active = intval($_data['active']);
|
$active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
|
||||||
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
||||||
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
||||||
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
|
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
|
||||||
@@ -1227,12 +1249,29 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
|
$_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
|
||||||
$_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
|
$_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
|
||||||
$_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
|
$_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
|
||||||
|
} else {
|
||||||
|
$_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']);
|
||||||
|
$_data['tls_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_tls_policy']);
|
||||||
|
$_data['spam_score'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_score']);
|
||||||
|
$_data['spam_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_policy']);
|
||||||
|
$_data['delimiter_action'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_delimiter_action']);
|
||||||
|
$_data['syncjobs'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_syncjobs']);
|
||||||
|
$_data['eas_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_eas_reset']);
|
||||||
|
$_data['sogo_profile_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_sogo_profile_reset']);
|
||||||
|
$_data['pushover'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pushover']);
|
||||||
|
$_data['quarantine'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine']);
|
||||||
|
$_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']);
|
||||||
|
$_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']);
|
||||||
|
$_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']);
|
||||||
|
$_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$stmt = $pdo->prepare("INSERT INTO `user_acl`
|
$stmt = $pdo->prepare("INSERT INTO `user_acl`
|
||||||
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
|
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
|
||||||
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`)
|
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`)
|
||||||
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
|
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
|
||||||
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
|
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
':spam_alias' => $_data['spam_alias'],
|
':spam_alias' => $_data['spam_alias'],
|
||||||
@@ -1251,31 +1290,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':app_passwds' => $_data['app_passwds']
|
':app_passwds' => $_data['app_passwds']
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
else {
|
catch (PDOException $e) {
|
||||||
$stmt = $pdo->prepare("INSERT INTO `user_acl`
|
$_SESSION['return'][] = array(
|
||||||
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
|
'type' => 'danger',
|
||||||
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`)
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
|
'msg' => $e->getMessage()
|
||||||
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
|
);
|
||||||
$stmt->execute(array(
|
return false;
|
||||||
':username' => $username,
|
|
||||||
':spam_alias' => 0,
|
|
||||||
':tls_policy' => 0,
|
|
||||||
':spam_score' => 0,
|
|
||||||
':spam_policy' => 0,
|
|
||||||
':delimiter_action' => 0,
|
|
||||||
':syncjobs' => 0,
|
|
||||||
':eas_reset' => 0,
|
|
||||||
':sogo_profile_reset' => 0,
|
|
||||||
':pushover' => 0,
|
|
||||||
':quarantine' => 0,
|
|
||||||
':quarantine_attachments' => 0,
|
|
||||||
':quarantine_notification' => 0,
|
|
||||||
':quarantine_category' => 0,
|
|
||||||
':app_passwds' => 0
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$_data['rl_frame'] = (isset($_data['rl_frame'])) ? $_data['rl_frame'] : $MAILBOX_DEFAULT_ATTRIBUTES['rl_frame'];
|
||||||
|
$_data['rl_value'] = (isset($_data['rl_value'])) ? $_data['rl_value'] : $MAILBOX_DEFAULT_ATTRIBUTES['rl_value'];
|
||||||
if (isset($_data['rl_frame']) && isset($_data['rl_value'])){
|
if (isset($_data['rl_frame']) && isset($_data['rl_value'])){
|
||||||
ratelimit('edit', 'mailbox', array(
|
ratelimit('edit', 'mailbox', array(
|
||||||
'object' => $username,
|
'object' => $username,
|
||||||
@@ -1524,17 +1549,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
|
$attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
|
||||||
if (isset($_data['protocol_access'])) {
|
if (isset($_data['protocol_access'])) {
|
||||||
$_data['protocol_access'] = (array)$_data['protocol_access'];
|
$_data['protocol_access'] = (array)$_data['protocol_access'];
|
||||||
$attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
|
$attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
|
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
|
||||||
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
||||||
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
||||||
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
||||||
}
|
}
|
||||||
if (isset($_data['acl'])) {
|
if (isset($_data['acl'])) {
|
||||||
$_data['acl'] = (array)$_data['acl'];
|
$_data['acl'] = (array)$_data['acl'];
|
||||||
$attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
|
$attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
|
||||||
@@ -3264,6 +3289,62 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
break;
|
break;
|
||||||
|
case 'mailbox_custom_attribute':
|
||||||
|
$_data['attribute'] = isset($_data['attribute']) ? $_data['attribute'] : array();
|
||||||
|
$_data['attribute'] = is_array($_data['attribute']) ? $_data['attribute'] : array($_data['attribute']);
|
||||||
|
$_data['attribute'] = array_map(function($value) { return str_replace(' ', '', $value); }, $_data['attribute']);
|
||||||
|
$_data['value'] = isset($_data['value']) ? $_data['value'] : array();
|
||||||
|
$_data['value'] = is_array($_data['value']) ? $_data['value'] : array($_data['value']);
|
||||||
|
$attributes = (object)array_combine($_data['attribute'], $_data['value']);
|
||||||
|
$mailboxes = is_array($_data['mailboxes']) ? $_data['mailboxes'] : array($_data['mailboxes']);
|
||||||
|
|
||||||
|
foreach ($mailboxes as $mailbox) {
|
||||||
|
if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('username_invalid', $mailbox)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$is_now = mailbox('get', 'mailbox_details', $mailbox);
|
||||||
|
if(!empty($is_now)){
|
||||||
|
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE `mailbox`
|
||||||
|
SET `custom_attributes` = :custom_attributes
|
||||||
|
WHERE username = :username");
|
||||||
|
$stmt->execute(array(
|
||||||
|
":username" => $mailbox,
|
||||||
|
":custom_attributes" => json_encode($attributes)
|
||||||
|
));
|
||||||
|
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('mailbox_modified', $mailbox)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
case 'resource':
|
case 'resource':
|
||||||
if (!is_array($_data['name'])) {
|
if (!is_array($_data['name'])) {
|
||||||
$names = array();
|
$names = array();
|
||||||
@@ -3343,44 +3424,116 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'domain_wide_footer':
|
case 'domain_wide_footer':
|
||||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
if (!is_array($_data['domains'])) {
|
||||||
if (!is_valid_domain_name($domain)) {
|
$domains = array();
|
||||||
$_SESSION['return'][] = array(
|
$domains[] = $_data['domains'];
|
||||||
'type' => 'danger',
|
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
|
||||||
'msg' => 'domain_invalid'
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
else {
|
||||||
$_SESSION['return'][] = array(
|
$domains = $_data['domains'];
|
||||||
'type' => 'danger',
|
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
|
||||||
'msg' => 'access_denied'
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$footers = array();
|
$footers = array();
|
||||||
$footers['html'] = isset($_data['footer_html']) ? $_data['footer_html'] : '';
|
$footers['html'] = isset($_data['html']) ? $_data['html'] : '';
|
||||||
$footers['plain'] = isset($_data['footer_plain']) ? $_data['footer_plain'] : '';
|
$footers['plain'] = isset($_data['plain']) ? $_data['plain'] : '';
|
||||||
try {
|
$footers['skip_replies'] = isset($_data['skip_replies']) ? (int)$_data['skip_replies'] : 0;
|
||||||
$redis->hSet('DOMAIN_WIDE_FOOTER', $domain, json_encode($footers));
|
$footers['mbox_exclude'] = array();
|
||||||
|
$footers['alias_domain_exclude'] = array();
|
||||||
|
if (isset($_data["exclude"])){
|
||||||
|
if (!is_array($_data["exclude"])) {
|
||||||
|
$_data["exclude"] = array($_data["exclude"]);
|
||||||
|
}
|
||||||
|
foreach ($_data["exclude"] as $exclude) {
|
||||||
|
if (filter_var($exclude, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `address` = :address
|
||||||
|
UNION
|
||||||
|
SELECT `username` FROM `mailbox` WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':address' => $exclude,
|
||||||
|
':username' => $exclude,
|
||||||
|
));
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if(!$row){
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('username_invalid', $exclude)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
array_push($footers['mbox_exclude'], $exclude);
|
||||||
|
}
|
||||||
|
elseif (is_valid_domain_name($exclude)) {
|
||||||
|
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain` = :alias_domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':alias_domain' => $exclude,
|
||||||
|
));
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if(!$row){
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('username_invalid', $exclude)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
array_push($footers['alias_domain_exclude'], $exclude);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('username_invalid', $exclude)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
foreach ($domains as $domain) {
|
||||||
|
$domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||||
|
if (!is_valid_domain_name($domain)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'domain_invalid'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM `domain_wide_footer` WHERE `domain`= :domain");
|
||||||
|
$stmt->execute(array(':domain' => $domain));
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies`) VALUES (:domain, :html, :plain, :mbox_exclude, :alias_domain_exclude, :skip_replies)");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $domain,
|
||||||
|
':html' => $footers['html'],
|
||||||
|
':plain' => $footers['plain'],
|
||||||
|
':mbox_exclude' => json_encode($footers['mbox_exclude']),
|
||||||
|
':alias_domain_exclude' => json_encode($footers['alias_domain_exclude']),
|
||||||
|
':skip_replies' => $footers['skip_replies'],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => $e->getMessage()
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('redis_error', $e)
|
'msg' => array('domain_footer_modified', htmlspecialchars($domain))
|
||||||
);
|
);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'success',
|
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
|
||||||
'msg' => array('domain_footer_modified', htmlspecialchars($domain))
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3934,13 +4087,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("SELECT `id` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
|
$stmt = $pdo->prepare("SELECT `id`, `address` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':domain' => $_data,
|
':domain' => $_data,
|
||||||
));
|
));
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
while($row = array_shift($rows)) {
|
while($row = array_shift($rows)) {
|
||||||
$aliases[] = $row['id'];
|
if ($_extra == "address"){
|
||||||
|
$aliases[] = $row['address'];
|
||||||
|
} else {
|
||||||
|
$aliases[] = $row['id'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $aliases;
|
return $aliases;
|
||||||
break;
|
break;
|
||||||
@@ -4184,6 +4341,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
|
$domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
|
||||||
$domaindata['mboxes_left'] = $row['mailboxes'] - $MailboxDataDomain['count'];
|
$domaindata['mboxes_left'] = $row['mailboxes'] - $MailboxDataDomain['count'];
|
||||||
$domaindata['domain_name'] = $row['domain'];
|
$domaindata['domain_name'] = $row['domain'];
|
||||||
|
$domaindata['domain_h_name'] = idn_to_utf8($row['domain']);
|
||||||
$domaindata['description'] = $row['description'];
|
$domaindata['description'] = $row['description'];
|
||||||
$domaindata['max_num_aliases_for_domain'] = $row['aliases'];
|
$domaindata['max_num_aliases_for_domain'] = $row['aliases'];
|
||||||
$domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
|
$domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
|
||||||
@@ -4292,6 +4450,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
`mailbox`.`modified`,
|
`mailbox`.`modified`,
|
||||||
`quota2`.`bytes`,
|
`quota2`.`bytes`,
|
||||||
`attributes`,
|
`attributes`,
|
||||||
|
`custom_attributes`,
|
||||||
`quota2`.`messages`
|
`quota2`.`messages`
|
||||||
FROM `mailbox`, `quota2`, `domain`
|
FROM `mailbox`, `quota2`, `domain`
|
||||||
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
||||||
@@ -4312,6 +4471,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
`mailbox`.`modified`,
|
`mailbox`.`modified`,
|
||||||
`quota2replica`.`bytes`,
|
`quota2replica`.`bytes`,
|
||||||
`attributes`,
|
`attributes`,
|
||||||
|
`custom_attributes`,
|
||||||
`quota2replica`.`messages`
|
`quota2replica`.`messages`
|
||||||
FROM `mailbox`, `quota2replica`, `domain`
|
FROM `mailbox`, `quota2replica`, `domain`
|
||||||
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
||||||
@@ -4328,12 +4488,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$mailboxdata['active'] = $row['active'];
|
$mailboxdata['active'] = $row['active'];
|
||||||
$mailboxdata['active_int'] = $row['active'];
|
$mailboxdata['active_int'] = $row['active'];
|
||||||
$mailboxdata['domain'] = $row['domain'];
|
$mailboxdata['domain'] = $row['domain'];
|
||||||
$mailboxdata['relayhost'] = $row['relayhost'];
|
|
||||||
$mailboxdata['name'] = $row['name'];
|
$mailboxdata['name'] = $row['name'];
|
||||||
$mailboxdata['local_part'] = $row['local_part'];
|
$mailboxdata['local_part'] = $row['local_part'];
|
||||||
$mailboxdata['quota'] = $row['quota'];
|
$mailboxdata['quota'] = $row['quota'];
|
||||||
$mailboxdata['messages'] = $row['messages'];
|
$mailboxdata['messages'] = $row['messages'];
|
||||||
$mailboxdata['attributes'] = json_decode($row['attributes'], true);
|
$mailboxdata['attributes'] = json_decode($row['attributes'], true);
|
||||||
|
$mailboxdata['custom_attributes'] = json_decode($row['custom_attributes'], true);
|
||||||
$mailboxdata['quota_used'] = intval($row['bytes']);
|
$mailboxdata['quota_used'] = intval($row['bytes']);
|
||||||
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
|
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
|
||||||
$mailboxdata['created'] = $row['created'];
|
$mailboxdata['created'] = $row['created'];
|
||||||
@@ -4514,19 +4674,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$footers = $redis->hGet('DOMAIN_WIDE_FOOTER', $domain);
|
$stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer`
|
||||||
$footers = json_decode($footers, true);
|
WHERE `domain` = :domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $domain
|
||||||
|
));
|
||||||
|
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
catch (PDOException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('redis_error', $e)
|
'msg' => $e->getMessage()
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $footers;
|
return $footer;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -143,17 +143,26 @@ function rspamd_maps($_action, $_data = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$maps = (array)$_data['map'];
|
$maps = (array)$_data['map'];
|
||||||
|
$valid_maps = array();
|
||||||
foreach ($maps as $map) {
|
foreach ($maps as $map) {
|
||||||
|
$is_valid = false;
|
||||||
foreach ($RSPAMD_MAPS as $rspamd_map_type) {
|
foreach ($RSPAMD_MAPS as $rspamd_map_type) {
|
||||||
if (!in_array($map, $rspamd_map_type)) {
|
if (in_array($map, $rspamd_map_type)) {
|
||||||
$_SESSION['return'][] = array(
|
$is_valid = true;
|
||||||
'type' => 'danger',
|
break;
|
||||||
'log' => array(__FUNCTION__, $_action, '-'),
|
|
||||||
'msg' => array('global_map_invalid', $map)
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($is_valid) {
|
||||||
|
array_push($valid_maps, $map);
|
||||||
|
} else {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, '-'),
|
||||||
|
'msg' => array('global_map_invalid', $map)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($valid_maps as $map) {
|
||||||
try {
|
try {
|
||||||
if (file_exists('/rspamd_custom_maps/' . $map)) {
|
if (file_exists('/rspamd_custom_maps/' . $map)) {
|
||||||
$map_content = trim($_data['rspamd_map_data']);
|
$map_content = trim($_data['rspamd_map_data']);
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ $globalVariables = [
|
|||||||
'app_links' => customize('get', 'app_links'),
|
'app_links' => customize('get', 'app_links'),
|
||||||
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
|
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
|
||||||
'uri' => $_SERVER['REQUEST_URI'],
|
'uri' => $_SERVER['REQUEST_URI'],
|
||||||
'last_login' => last_login('get', $_SESSION['mailcow_cc_username'], 7, 0)['ui']['time']
|
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
|
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ function init_db_schema() {
|
|||||||
try {
|
try {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
|
|
||||||
$db_version = "15112023_1536";
|
$db_version = "26022024_1433";
|
||||||
|
|
||||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
@@ -267,6 +267,22 @@ function init_db_schema() {
|
|||||||
),
|
),
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||||
),
|
),
|
||||||
|
"domain_wide_footer" => array(
|
||||||
|
"cols" => array(
|
||||||
|
"domain" => "VARCHAR(255) NOT NULL",
|
||||||
|
"html" => "LONGTEXT",
|
||||||
|
"plain" => "LONGTEXT",
|
||||||
|
"mbox_exclude" => "JSON NOT NULL DEFAULT ('[]')",
|
||||||
|
"alias_domain_exclude" => "JSON NOT NULL DEFAULT ('[]')",
|
||||||
|
"skip_replies" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
||||||
|
),
|
||||||
|
"keys" => array(
|
||||||
|
"primary" => array(
|
||||||
|
"" => array("domain")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||||
|
),
|
||||||
"tags_domain" => array(
|
"tags_domain" => array(
|
||||||
"cols" => array(
|
"cols" => array(
|
||||||
"tag_name" => "VARCHAR(255) NOT NULL",
|
"tag_name" => "VARCHAR(255) NOT NULL",
|
||||||
@@ -344,6 +360,7 @@ function init_db_schema() {
|
|||||||
"local_part" => "VARCHAR(255) NOT NULL",
|
"local_part" => "VARCHAR(255) NOT NULL",
|
||||||
"domain" => "VARCHAR(255) NOT NULL",
|
"domain" => "VARCHAR(255) NOT NULL",
|
||||||
"attributes" => "JSON",
|
"attributes" => "JSON",
|
||||||
|
"custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
|
||||||
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
|
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
|
||||||
"multiple_bookings" => "INT NOT NULL DEFAULT -1",
|
"multiple_bookings" => "INT NOT NULL DEFAULT -1",
|
||||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||||
@@ -962,6 +979,18 @@ function init_db_schema() {
|
|||||||
),
|
),
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||||
),
|
),
|
||||||
|
"sogo_admin" => array(
|
||||||
|
"cols" => array(
|
||||||
|
"c_key" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||||
|
"c_content" => "mediumtext NOT NULL",
|
||||||
|
),
|
||||||
|
"keys" => array(
|
||||||
|
"primary" => array(
|
||||||
|
"" => array("c_key")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||||
|
),
|
||||||
"pushover" => array(
|
"pushover" => array(
|
||||||
"cols" => array(
|
"cols" => array(
|
||||||
"username" => "VARCHAR(255) NOT NULL",
|
"username" => "VARCHAR(255) NOT NULL",
|
||||||
|
|||||||
@@ -0,0 +1,622 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Helper functions for building a DataTables server-side processing SQL query
|
||||||
|
*
|
||||||
|
* The static functions in this class are just helper functions to help build
|
||||||
|
* the SQL used in the DataTables demo server-side processing scripts. These
|
||||||
|
* functions obviously do not represent all that can be done with server-side
|
||||||
|
* processing, they are intentionally simple to show how it works. More complex
|
||||||
|
* server-side processing operations will likely require a custom script.
|
||||||
|
*
|
||||||
|
* See https://datatables.net/usage/server-side for full details on the server-
|
||||||
|
* side processing requirements of DataTables.
|
||||||
|
*
|
||||||
|
* @license MIT - https://datatables.net/license_mit
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SSP {
|
||||||
|
/**
|
||||||
|
* Create the data output array for the DataTables rows
|
||||||
|
*
|
||||||
|
* @param array $columns Column information array
|
||||||
|
* @param array $data Data from the SQL get
|
||||||
|
* @return array Formatted data in a row based format
|
||||||
|
*/
|
||||||
|
static function data_output ( $columns, $data )
|
||||||
|
{
|
||||||
|
$out = array();
|
||||||
|
|
||||||
|
for ( $i=0, $ien=count($data) ; $i<$ien ; $i++ ) {
|
||||||
|
$row = array();
|
||||||
|
|
||||||
|
for ( $j=0, $jen=count($columns) ; $j<$jen ; $j++ ) {
|
||||||
|
$column = $columns[$j];
|
||||||
|
|
||||||
|
// Is there a formatter?
|
||||||
|
if ( isset( $column['formatter'] ) ) {
|
||||||
|
if(empty($column['db'])){
|
||||||
|
$row[ $column['dt'] ] = $column['formatter']( $data[$i] );
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$row[ $column['dt'] ] = $column['formatter']( $data[$i][ $column['db'] ], $data[$i] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(!empty($column['db']) && (!isset($column['dummy']) || $column['dummy'] !== true)){
|
||||||
|
$row[ $column['dt'] ] = $data[$i][ $columns[$j]['db'] ];
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$row[ $column['dt'] ] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$out[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database connection
|
||||||
|
*
|
||||||
|
* Obtain an PHP PDO connection from a connection details array
|
||||||
|
*
|
||||||
|
* @param array $conn SQL connection details. The array should have
|
||||||
|
* the following properties
|
||||||
|
* * host - host name
|
||||||
|
* * db - database name
|
||||||
|
* * user - user name
|
||||||
|
* * pass - user password
|
||||||
|
* * Optional: `'charset' => 'utf8'` - you might need this depending on your PHP / MySQL config
|
||||||
|
* @return resource PDO connection
|
||||||
|
*/
|
||||||
|
static function db ( $conn )
|
||||||
|
{
|
||||||
|
if ( is_array( $conn ) ) {
|
||||||
|
return self::sql_connect( $conn );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paging
|
||||||
|
*
|
||||||
|
* Construct the LIMIT clause for server-side processing SQL query
|
||||||
|
*
|
||||||
|
* @param array $request Data sent to server by DataTables
|
||||||
|
* @param array $columns Column information array
|
||||||
|
* @return string SQL limit clause
|
||||||
|
*/
|
||||||
|
static function limit ( $request, $columns )
|
||||||
|
{
|
||||||
|
$limit = '';
|
||||||
|
|
||||||
|
if ( isset($request['start']) && $request['length'] != -1 ) {
|
||||||
|
$limit = "LIMIT ".intval($request['start']).", ".intval($request['length']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordering
|
||||||
|
*
|
||||||
|
* Construct the ORDER BY clause for server-side processing SQL query
|
||||||
|
*
|
||||||
|
* @param array $request Data sent to server by DataTables
|
||||||
|
* @param array $columns Column information array
|
||||||
|
* @return string SQL order by clause
|
||||||
|
*/
|
||||||
|
static function order ( $tableAS, $request, $columns )
|
||||||
|
{
|
||||||
|
$select = '';
|
||||||
|
$order = '';
|
||||||
|
|
||||||
|
if ( isset($request['order']) && count($request['order']) ) {
|
||||||
|
$selects = [];
|
||||||
|
$orderBy = [];
|
||||||
|
$dtColumns = self::pluck( $columns, 'dt' );
|
||||||
|
|
||||||
|
for ( $i=0, $ien=count($request['order']) ; $i<$ien ; $i++ ) {
|
||||||
|
// Convert the column index into the column data property
|
||||||
|
$columnIdx = intval($request['order'][$i]['column']);
|
||||||
|
$requestColumn = $request['columns'][$columnIdx];
|
||||||
|
|
||||||
|
$columnIdx = array_search( $columnIdx, $dtColumns );
|
||||||
|
$column = $columns[ $columnIdx ];
|
||||||
|
|
||||||
|
if ( $requestColumn['orderable'] == 'true' ) {
|
||||||
|
$dir = $request['order'][$i]['dir'] === 'asc' ?
|
||||||
|
'ASC' :
|
||||||
|
'DESC';
|
||||||
|
|
||||||
|
if(isset($column['order_subquery'])) {
|
||||||
|
$selects[] = '('.$column['order_subquery'].') AS `'.$column['db'].'_count`';
|
||||||
|
$orderBy[] = '`'.$column['db'].'_count` '.$dir;
|
||||||
|
} else {
|
||||||
|
$orderBy[] = '`'.$tableAS.'`.`'.$column['db'].'` '.$dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count( $selects ) ) {
|
||||||
|
$select = ', '.implode(', ', $selects);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count( $orderBy ) ) {
|
||||||
|
$order = 'ORDER BY '.implode(', ', $orderBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$select, $order];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searching / Filtering
|
||||||
|
*
|
||||||
|
* Construct the WHERE clause for server-side processing SQL query.
|
||||||
|
*
|
||||||
|
* NOTE this does not match the built-in DataTables filtering which does it
|
||||||
|
* word by word on any field. It's possible to do here performance on large
|
||||||
|
* databases would be very poor
|
||||||
|
*
|
||||||
|
* @param array $request Data sent to server by DataTables
|
||||||
|
* @param array $columns Column information array
|
||||||
|
* @param array $bindings Array of values for PDO bindings, used in the
|
||||||
|
* sql_exec() function
|
||||||
|
* @return string SQL where clause
|
||||||
|
*/
|
||||||
|
static function filter ( $tablesAS, $request, $columns, &$bindings )
|
||||||
|
{
|
||||||
|
$globalSearch = array();
|
||||||
|
$columnSearch = array();
|
||||||
|
$joins = array();
|
||||||
|
$dtColumns = self::pluck( $columns, 'dt' );
|
||||||
|
|
||||||
|
if ( isset($request['search']) && $request['search']['value'] != '' ) {
|
||||||
|
$str = $request['search']['value'];
|
||||||
|
|
||||||
|
for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) {
|
||||||
|
$requestColumn = $request['columns'][$i];
|
||||||
|
$columnIdx = array_search( $i, $dtColumns );
|
||||||
|
$column = $columns[ $columnIdx ];
|
||||||
|
|
||||||
|
if ( $requestColumn['searchable'] == 'true' ) {
|
||||||
|
if(!empty($column['db'])){
|
||||||
|
$binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR );
|
||||||
|
|
||||||
|
if(isset($column['search']['join'])) {
|
||||||
|
$joins[] = $column['search']['join'];
|
||||||
|
$globalSearch[] = $column['search']['where_column'].' LIKE '.$binding;
|
||||||
|
} else {
|
||||||
|
$globalSearch[] = "`".$tablesAS."`.`".$column['db']."` LIKE ".$binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual column filtering
|
||||||
|
if ( isset( $request['columns'] ) ) {
|
||||||
|
for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) {
|
||||||
|
$requestColumn = $request['columns'][$i];
|
||||||
|
$columnIdx = array_search( $requestColumn['data'], $dtColumns );
|
||||||
|
$column = $columns[ $columnIdx ];
|
||||||
|
|
||||||
|
$str = $requestColumn['search']['value'];
|
||||||
|
|
||||||
|
if ( $requestColumn['searchable'] == 'true' &&
|
||||||
|
$str != '' ) {
|
||||||
|
if(!empty($column['db'])){
|
||||||
|
$binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR );
|
||||||
|
$columnSearch[] = "`".$tablesAS."`.`".$column['db']."` LIKE ".$binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine the filters into a single string
|
||||||
|
$where = '';
|
||||||
|
|
||||||
|
if ( count( $globalSearch ) ) {
|
||||||
|
$where = '('.implode(' OR ', $globalSearch).')';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count( $columnSearch ) ) {
|
||||||
|
$where = $where === '' ?
|
||||||
|
implode(' AND ', $columnSearch) :
|
||||||
|
$where .' AND '. implode(' AND ', $columnSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
$join = '';
|
||||||
|
if( count($joins) ) {
|
||||||
|
$join = implode(' ', $joins);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $where !== '' ) {
|
||||||
|
$where = 'WHERE '.$where;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$join, $where];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the SQL queries needed for an server-side processing requested,
|
||||||
|
* utilising the helper functions of this class, limit(), order() and
|
||||||
|
* filter() among others. The returned array is ready to be encoded as JSON
|
||||||
|
* in response to an SSP request, or can be modified if needed before
|
||||||
|
* sending back to the client.
|
||||||
|
*
|
||||||
|
* @param array $request Data sent to server by DataTables
|
||||||
|
* @param array|PDO $conn PDO connection resource or connection parameters array
|
||||||
|
* @param string $table SQL table to query
|
||||||
|
* @param string $primaryKey Primary key of the table
|
||||||
|
* @param array $columns Column information array
|
||||||
|
* @return array Server-side processing response array
|
||||||
|
*/
|
||||||
|
static function simple ( $request, $conn, $table, $primaryKey, $columns )
|
||||||
|
{
|
||||||
|
$bindings = array();
|
||||||
|
$db = self::db( $conn );
|
||||||
|
|
||||||
|
// Allow for a JSON string to be passed in
|
||||||
|
if (isset($request['json'])) {
|
||||||
|
$request = json_decode($request['json'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// table AS
|
||||||
|
$tablesAS = null;
|
||||||
|
if(is_array($table)) {
|
||||||
|
$tablesAS = $table[1];
|
||||||
|
$table = $table[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the SQL query string from the request
|
||||||
|
list($select, $order) = self::order( $tablesAS, $request, $columns );
|
||||||
|
$limit = self::limit( $request, $columns );
|
||||||
|
list($join, $where) = self::filter( $tablesAS, $request, $columns, $bindings );
|
||||||
|
|
||||||
|
// Main query to actually get the data
|
||||||
|
$data = self::sql_exec( $db, $bindings,
|
||||||
|
"SELECT `$tablesAS`.`".implode("`, `$tablesAS`.`", self::pluck($columns, 'db'))."`
|
||||||
|
$select
|
||||||
|
FROM `$table` AS `$tablesAS`
|
||||||
|
$join
|
||||||
|
$where
|
||||||
|
GROUP BY `{$tablesAS}`.`{$primaryKey}`
|
||||||
|
$order
|
||||||
|
$limit"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Data set length after filtering
|
||||||
|
$resFilterLength = self::sql_exec( $db, $bindings,
|
||||||
|
"SELECT COUNT(DISTINCT `{$tablesAS}`.`{$primaryKey}`)
|
||||||
|
FROM `$table` AS `$tablesAS`
|
||||||
|
$join
|
||||||
|
$where"
|
||||||
|
);
|
||||||
|
$recordsFiltered = $resFilterLength[0][0];
|
||||||
|
|
||||||
|
// Total data set length
|
||||||
|
$resTotalLength = self::sql_exec( $db,
|
||||||
|
"SELECT COUNT(`{$tablesAS}`.`{$primaryKey}`)
|
||||||
|
FROM `$table` AS `$tablesAS`"
|
||||||
|
);
|
||||||
|
$recordsTotal = $resTotalLength[0][0];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Output
|
||||||
|
*/
|
||||||
|
return array(
|
||||||
|
"draw" => isset ( $request['draw'] ) ?
|
||||||
|
intval( $request['draw'] ) :
|
||||||
|
0,
|
||||||
|
"recordsTotal" => intval( $recordsTotal ),
|
||||||
|
"recordsFiltered" => intval( $recordsFiltered ),
|
||||||
|
"data" => self::data_output( $columns, $data )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The difference between this method and the `simple` one, is that you can
|
||||||
|
* apply additional `where` conditions to the SQL queries. These can be in
|
||||||
|
* one of two forms:
|
||||||
|
*
|
||||||
|
* * 'Result condition' - This is applied to the result set, but not the
|
||||||
|
* overall paging information query - i.e. it will not effect the number
|
||||||
|
* of records that a user sees they can have access to. This should be
|
||||||
|
* used when you want apply a filtering condition that the user has sent.
|
||||||
|
* * 'All condition' - This is applied to all queries that are made and
|
||||||
|
* reduces the number of records that the user can access. This should be
|
||||||
|
* used in conditions where you don't want the user to ever have access to
|
||||||
|
* particular records (for example, restricting by a login id).
|
||||||
|
*
|
||||||
|
* In both cases the extra condition can be added as a simple string, or if
|
||||||
|
* you are using external values, as an assoc. array with `condition` and
|
||||||
|
* `bindings` parameters. The `condition` is a string with the SQL WHERE
|
||||||
|
* condition and `bindings` is an assoc. array of the binding names and
|
||||||
|
* values.
|
||||||
|
*
|
||||||
|
* @param array $request Data sent to server by DataTables
|
||||||
|
* @param array|PDO $conn PDO connection resource or connection parameters array
|
||||||
|
* @param string|array $table SQL table to query, if array second key is AS
|
||||||
|
* @param string $primaryKey Primary key of the table
|
||||||
|
* @param array $columns Column information array
|
||||||
|
* @param string $join JOIN sql string
|
||||||
|
* @param string|array $whereResult WHERE condition to apply to the result set
|
||||||
|
* @return array Server-side processing response array
|
||||||
|
*/
|
||||||
|
static function complex (
|
||||||
|
$request,
|
||||||
|
$conn,
|
||||||
|
$table,
|
||||||
|
$primaryKey,
|
||||||
|
$columns,
|
||||||
|
$join=null,
|
||||||
|
$whereResult=null
|
||||||
|
) {
|
||||||
|
$bindings = array();
|
||||||
|
$db = self::db( $conn );
|
||||||
|
|
||||||
|
// table AS
|
||||||
|
$tablesAS = null;
|
||||||
|
if(is_array($table)) {
|
||||||
|
$tablesAS = $table[1];
|
||||||
|
$table = $table[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the SQL query string from the request
|
||||||
|
list($select, $order) = self::order( $tablesAS, $request, $columns );
|
||||||
|
$limit = self::limit( $request, $columns );
|
||||||
|
list($join_filter, $where) = self::filter( $tablesAS, $request, $columns, $bindings );
|
||||||
|
|
||||||
|
// whereResult can be a simple string, or an assoc. array with a
|
||||||
|
// condition and bindings
|
||||||
|
if ( $whereResult ) {
|
||||||
|
$str = $whereResult;
|
||||||
|
|
||||||
|
if ( is_array($whereResult) ) {
|
||||||
|
$str = $whereResult['condition'];
|
||||||
|
|
||||||
|
if ( isset($whereResult['bindings']) ) {
|
||||||
|
self::add_bindings($bindings, $whereResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = $where ?
|
||||||
|
$where .' AND '.$str :
|
||||||
|
'WHERE '.$str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main query to actually get the data
|
||||||
|
$data = self::sql_exec( $db, $bindings,
|
||||||
|
"SELECT `$tablesAS`.`".implode("`, `$tablesAS`.`", self::pluck($columns, 'db'))."`
|
||||||
|
$select
|
||||||
|
FROM `$table` AS `$tablesAS`
|
||||||
|
$join
|
||||||
|
$join_filter
|
||||||
|
$where
|
||||||
|
GROUP BY `{$tablesAS}`.`{$primaryKey}`
|
||||||
|
$order
|
||||||
|
$limit"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Data set length after filtering
|
||||||
|
$resFilterLength = self::sql_exec( $db, $bindings,
|
||||||
|
"SELECT COUNT(DISTINCT `{$tablesAS}`.`{$primaryKey}`)
|
||||||
|
FROM `$table` AS `$tablesAS`
|
||||||
|
$join
|
||||||
|
$join_filter
|
||||||
|
$where"
|
||||||
|
);
|
||||||
|
$recordsFiltered = (isset($resFilterLength[0])) ? $resFilterLength[0][0] : 0;
|
||||||
|
|
||||||
|
// Total data set length
|
||||||
|
$resTotalLength = self::sql_exec( $db, $bindings,
|
||||||
|
"SELECT COUNT(`{$tablesAS}`.`{$primaryKey}`)
|
||||||
|
FROM `$table` AS `$tablesAS`
|
||||||
|
$join
|
||||||
|
$join_filter
|
||||||
|
$where"
|
||||||
|
);
|
||||||
|
$recordsTotal = (isset($resTotalLength[0])) ? $resTotalLength[0][0] : 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Output
|
||||||
|
*/
|
||||||
|
return array(
|
||||||
|
"draw" => isset ( $request['draw'] ) ?
|
||||||
|
intval( $request['draw'] ) :
|
||||||
|
0,
|
||||||
|
"recordsTotal" => intval( $recordsTotal ),
|
||||||
|
"recordsFiltered" => intval( $recordsFiltered ),
|
||||||
|
"data" => self::data_output( $columns, $data )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the database
|
||||||
|
*
|
||||||
|
* @param array $sql_details SQL server connection details array, with the
|
||||||
|
* properties:
|
||||||
|
* * host - host name
|
||||||
|
* * db - database name
|
||||||
|
* * user - user name
|
||||||
|
* * pass - user password
|
||||||
|
* @return resource Database connection handle
|
||||||
|
*/
|
||||||
|
static function sql_connect ( $sql_details )
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = @new PDO(
|
||||||
|
"mysql:host={$sql_details['host']};dbname={$sql_details['db']}",
|
||||||
|
$sql_details['user'],
|
||||||
|
$sql_details['pass'],
|
||||||
|
array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
self::fatal(
|
||||||
|
"An error occurred while connecting to the database. ".
|
||||||
|
"The error reported by the server was: ".$e->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an SQL query on the database
|
||||||
|
*
|
||||||
|
* @param resource $db Database handler
|
||||||
|
* @param array $bindings Array of PDO binding values from bind() to be
|
||||||
|
* used for safely escaping strings. Note that this can be given as the
|
||||||
|
* SQL query string if no bindings are required.
|
||||||
|
* @param string $sql SQL query to execute.
|
||||||
|
* @return array Result from the query (all rows)
|
||||||
|
*/
|
||||||
|
static function sql_exec ( $db, $bindings, $sql=null )
|
||||||
|
{
|
||||||
|
// Argument shifting
|
||||||
|
if ( $sql === null ) {
|
||||||
|
$sql = $bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare( $sql );
|
||||||
|
|
||||||
|
// Bind parameters
|
||||||
|
if ( is_array( $bindings ) ) {
|
||||||
|
for ( $i=0, $ien=count($bindings) ; $i<$ien ; $i++ ) {
|
||||||
|
$binding = $bindings[$i];
|
||||||
|
$stmt->bindValue( $binding['key'], $binding['val'], $binding['type'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
try {
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
self::fatal( "An SQL error occurred: ".$e->getMessage() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all
|
||||||
|
return $stmt->fetchAll( PDO::FETCH_BOTH );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
* Internal methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw a fatal error.
|
||||||
|
*
|
||||||
|
* This writes out an error message in a JSON string which DataTables will
|
||||||
|
* see and show to the user in the browser.
|
||||||
|
*
|
||||||
|
* @param string $msg Message to send to the client
|
||||||
|
*/
|
||||||
|
static function fatal ( $msg )
|
||||||
|
{
|
||||||
|
echo json_encode( array(
|
||||||
|
"error" => $msg
|
||||||
|
) );
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a PDO binding key which can be used for escaping variables safely
|
||||||
|
* when executing a query with sql_exec()
|
||||||
|
*
|
||||||
|
* @param array &$a Array of bindings
|
||||||
|
* @param * $val Value to bind
|
||||||
|
* @param int $type PDO field type
|
||||||
|
* @return string Bound key to be used in the SQL where this parameter
|
||||||
|
* would be used.
|
||||||
|
*/
|
||||||
|
static function bind ( &$a, $val, $type )
|
||||||
|
{
|
||||||
|
$key = ':binding_'.count( $a );
|
||||||
|
|
||||||
|
$a[] = array(
|
||||||
|
'key' => $key,
|
||||||
|
'val' => $val,
|
||||||
|
'type' => $type
|
||||||
|
);
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function add_bindings(&$bindings, $vals)
|
||||||
|
{
|
||||||
|
foreach($vals['bindings'] as $key => $value) {
|
||||||
|
$bindings[] = array(
|
||||||
|
'key' => $key,
|
||||||
|
'val' => $value,
|
||||||
|
'type' => PDO::PARAM_STR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull a particular property from each assoc. array in a numeric array,
|
||||||
|
* returning and array of the property values from each item.
|
||||||
|
*
|
||||||
|
* @param array $a Array to get data from
|
||||||
|
* @param string $prop Property to read
|
||||||
|
* @return array Array of property values
|
||||||
|
*/
|
||||||
|
static function pluck ( $a, $prop )
|
||||||
|
{
|
||||||
|
$out = array();
|
||||||
|
|
||||||
|
for ( $i=0, $len=count($a) ; $i<$len ; $i++ ) {
|
||||||
|
if ( empty($a[$i][$prop]) && $a[$i][$prop] !== 0 ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ( $prop == 'db' && isset($a[$i]['dummy']) && $a[$i]['dummy'] === true ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//removing the $out array index confuses the filter method in doing proper binding,
|
||||||
|
//adding it ensures that the array data are mapped correctly
|
||||||
|
$out[$i] = $a[$i][$prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a string from an array or a string
|
||||||
|
*
|
||||||
|
* @param array|string $a Array to join
|
||||||
|
* @param string $join Glue for the concatenation
|
||||||
|
* @return string Joined string
|
||||||
|
*/
|
||||||
|
static function _flatten ( $a, $join = ' AND ' )
|
||||||
|
{
|
||||||
|
if ( ! $a ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
else if ( $a && is_array($a) ) {
|
||||||
|
return implode( $join, $a );
|
||||||
|
}
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -70,6 +70,8 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception $e) {
|
catch (Exception $e) {
|
||||||
|
// Stop when redis is not available
|
||||||
|
http_response_code(500);
|
||||||
?>
|
?>
|
||||||
<center style='font-family:sans-serif;'>Connection to Redis failed.<br /><br />The following error was reported:<br/><?=$e->getMessage();?></center>
|
<center style='font-family:sans-serif;'>Connection to Redis failed.<br /><br />The following error was reported:<br/><?=$e->getMessage();?></center>
|
||||||
<?php
|
<?php
|
||||||
@@ -98,6 +100,7 @@ try {
|
|||||||
}
|
}
|
||||||
catch (PDOException $e) {
|
catch (PDOException $e) {
|
||||||
// Stop when SQL connection fails
|
// Stop when SQL connection fails
|
||||||
|
http_response_code(500);
|
||||||
?>
|
?>
|
||||||
<center style='font-family:sans-serif;'>Connection to database failed.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center>
|
<center style='font-family:sans-serif;'>Connection to database failed.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center>
|
||||||
<?php
|
<?php
|
||||||
@@ -105,6 +108,7 @@ exit;
|
|||||||
}
|
}
|
||||||
// Stop when dockerapi is not available
|
// Stop when dockerapi is not available
|
||||||
if (fsockopen("tcp://dockerapi", 443, $errno, $errstr) === false) {
|
if (fsockopen("tcp://dockerapi", 443, $errno, $errstr) === false) {
|
||||||
|
http_response_code(500);
|
||||||
?>
|
?>
|
||||||
<center style='font-family:sans-serif;'>Connection to dockerapi container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
|
<center style='font-family:sans-serif;'>Connection to dockerapi container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
headline: lang.sieve_preset_1
|
headline: lang.sieve_preset_1
|
||||||
content: |
|
content: |
|
||||||
require ["reject","body","regex"];
|
require ["reject","body","regex"];
|
||||||
if anyof (body :raw :regex ["filename=.*\.doc","filename=.*\.exe","filename=.*\.moo"]) {
|
if anyof (body :raw :regex ["filename=\".*\\.(doc|exe|moo)\""]) {
|
||||||
reject text:
|
reject text:
|
||||||
doc, exe and moo are dangerous file extensions.
|
doc, exe and moo are dangerous file extensions.
|
||||||
Why would you do that? I am a sad cow.
|
Why would you do that? I am a sad cow.
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ $AVAILABLE_LANGUAGES = array(
|
|||||||
'it-it' => 'Italiano (Italian)',
|
'it-it' => 'Italiano (Italian)',
|
||||||
'ko-kr' => '한국어 (Korean)',
|
'ko-kr' => '한국어 (Korean)',
|
||||||
'lv-lv' => 'latviešu (Latvian)',
|
'lv-lv' => 'latviešu (Latvian)',
|
||||||
|
'lt-lt' => 'Lietuvių (Lithuanian)',
|
||||||
|
'nb-no' => 'Norsk (Norwegian)',
|
||||||
'nl-nl' => 'Nederlands (Dutch)',
|
'nl-nl' => 'Nederlands (Dutch)',
|
||||||
'pl-pl' => 'Język Polski (Polish)',
|
'pl-pl' => 'Język Polski (Polish)',
|
||||||
'pt-br' => 'Português brasileiro (Brazilian Portuguese)',
|
'pt-br' => 'Português brasileiro (Brazilian Portuguese)',
|
||||||
@@ -126,6 +128,15 @@ $MAILCOW_APPS = array(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Logo max file size in bytes
|
||||||
|
$LOGO_LIMITS['max_size'] = 15 * 1024 * 1024; // 15MB
|
||||||
|
|
||||||
|
// Logo max width in pixels
|
||||||
|
$LOGO_LIMITS['max_width'] = 1920;
|
||||||
|
|
||||||
|
// Logo max height in pixels
|
||||||
|
$LOGO_LIMITS['max_height'] = 1920;
|
||||||
|
|
||||||
// Rows until pagination begins
|
// Rows until pagination begins
|
||||||
$PAGINATION_SIZE = 25;
|
$PAGINATION_SIZE = 25;
|
||||||
|
|
||||||
|
|||||||
@@ -391,3 +391,11 @@ function addTag(tagAddElem, tag = null){
|
|||||||
$(tagValuesElem).val(JSON.stringify(value_tags));
|
$(tagValuesElem).val(JSON.stringify(value_tags));
|
||||||
$(tagInputElem).val('');
|
$(tagInputElem).val('');
|
||||||
}
|
}
|
||||||
|
function copyToClipboard(id) {
|
||||||
|
var copyText = document.getElementById(id);
|
||||||
|
copyText.select();
|
||||||
|
copyText.setSelectionRange(0, 99999);
|
||||||
|
// only works with https connections
|
||||||
|
navigator.clipboard.writeText(copyText.value);
|
||||||
|
mailcow_alert_box(lang.copy_to_clipboard, "success");
|
||||||
|
}
|
||||||
@@ -199,6 +199,23 @@ jQuery(function($){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function add_table_row(table_id, type) {
|
||||||
|
var row = $('<tr />');
|
||||||
|
if (type == "mbox_attr") {
|
||||||
|
cols = '<td><input class="input-sm input-xs-lg form-control" data-id="mbox_attr" type="text" name="attribute" required></td>';
|
||||||
|
cols += '<td><input class="input-sm input-xs-lg form-control" data-id="mbox_attr" type="text" name="value" required></td>';
|
||||||
|
cols += '<td><a href="#" role="button" class="btn btn-sm btn-xs-lg btn-secondary h-100 w-100" type="button">' + lang_admin.remove_row + '</a></td>';
|
||||||
|
}
|
||||||
|
row.append(cols);
|
||||||
|
table_id.append(row);
|
||||||
|
}
|
||||||
|
$('#mbox_attr_table').on('click', 'tr a', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).parents('tr').remove();
|
||||||
|
});
|
||||||
|
$('#add_mbox_attr_row').click(function() {
|
||||||
|
add_table_row($('#mbox_attr_table'), "mbox_attr");
|
||||||
|
});
|
||||||
|
|
||||||
// detect element visibility changes
|
// detect element visibility changes
|
||||||
function onVisible(element, callback) {
|
function onVisible(element, callback) {
|
||||||
|
|||||||
+50
-32
@@ -435,7 +435,7 @@ jQuery(function($){
|
|||||||
var table = $('#domain_table').DataTable({
|
var table = $('#domain_table').DataTable({
|
||||||
responsive: true,
|
responsive: true,
|
||||||
processing: true,
|
processing: true,
|
||||||
serverSide: false,
|
serverSide: true,
|
||||||
stateSave: true,
|
stateSave: true,
|
||||||
pageLength: pagination_size,
|
pageLength: pagination_size,
|
||||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||||
@@ -447,10 +447,14 @@ jQuery(function($){
|
|||||||
},
|
},
|
||||||
ajax: {
|
ajax: {
|
||||||
type: "GET",
|
type: "GET",
|
||||||
url: "/api/v1/get/domain/all",
|
url: "/api/v1/get/domain/datatables",
|
||||||
dataSrc: function(json){
|
dataSrc: function(json){
|
||||||
$.each(json, function(i, item) {
|
$.each(json.data, function(i, item) {
|
||||||
item.domain_name = escapeHtml(item.domain_name);
|
item.domain_name = escapeHtml(item.domain_name);
|
||||||
|
item.domain_h_name = escapeHtml(item.domain_h_name);
|
||||||
|
if (item.domain_name != item.domain_h_name){
|
||||||
|
item.domain_h_name = item.domain_h_name + '<small class="d-block">' + item.domain_name + '</small>';
|
||||||
|
}
|
||||||
|
|
||||||
item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain;
|
item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain;
|
||||||
item.mailboxes = item.mboxes_in_domain + " / " + item.max_num_mboxes_for_domain;
|
item.mailboxes = item.mboxes_in_domain + " / " + item.max_num_mboxes_for_domain;
|
||||||
@@ -489,16 +493,16 @@ jQuery(function($){
|
|||||||
|
|
||||||
if (item.backupmx == 1) {
|
if (item.backupmx == 1) {
|
||||||
if (item.relay_unknown_only == 1) {
|
if (item.relay_unknown_only == 1) {
|
||||||
item.domain_name = '<div class="badge fs-6 bg-info">Relay Non-Local</div> ' + item.domain_name;
|
item.domain_h_name = '<div class="badge fs-7 bg-info">Relay Non-Local</div> ' + item.domain_h_name;
|
||||||
} else if (item.relay_all_recipients == 1) {
|
} else if (item.relay_all_recipients == 1) {
|
||||||
item.domain_name = '<div class="badge fs-6 bg-info">Relay All</div> ' + item.domain_name;
|
item.domain_h_name = '<div class="badge fs-7 bg-info">Relay All</div> ' + item.domain_h_name;
|
||||||
} else {
|
} else {
|
||||||
item.domain_name = '<div class="badge fs-6 bg-info">Relay</div> ' + item.domain_name;
|
item.domain_h_name = '<div class="badge fs-7 bg-info">Relay</div> ' + item.domain_h_name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return json;
|
return json.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
@@ -521,24 +525,27 @@ jQuery(function($){
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.domain,
|
title: lang.domain,
|
||||||
data: 'domain_name',
|
data: 'domain_h_name',
|
||||||
responsivePriority: 3,
|
responsivePriority: 3,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.aliases,
|
title: lang.aliases,
|
||||||
data: 'aliases',
|
data: 'aliases',
|
||||||
|
searchable: false,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.mailboxes,
|
title: lang.mailboxes,
|
||||||
data: 'mailboxes',
|
data: 'mailboxes',
|
||||||
|
searchable: false,
|
||||||
responsivePriority: 4,
|
responsivePriority: 4,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.domain_quota,
|
title: lang.domain_quota,
|
||||||
data: 'quota',
|
data: 'quota',
|
||||||
|
searchable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
render: function (data, type) {
|
render: function (data, type) {
|
||||||
data = data.split("/");
|
data = data.split("/");
|
||||||
@@ -548,6 +555,7 @@ jQuery(function($){
|
|||||||
{
|
{
|
||||||
title: lang.stats,
|
title: lang.stats,
|
||||||
data: 'stats',
|
data: 'stats',
|
||||||
|
searchable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
render: function (data, type) {
|
render: function (data, type) {
|
||||||
data = data.split("/");
|
data = data.split("/");
|
||||||
@@ -557,53 +565,67 @@ jQuery(function($){
|
|||||||
{
|
{
|
||||||
title: lang.mailbox_defquota,
|
title: lang.mailbox_defquota,
|
||||||
data: 'def_quota_for_mbox',
|
data: 'def_quota_for_mbox',
|
||||||
|
searchable: false,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.mailbox_quota,
|
title: lang.mailbox_quota,
|
||||||
data: 'max_quota_for_mbox',
|
data: 'max_quota_for_mbox',
|
||||||
|
searchable: false,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'RL',
|
title: 'RL',
|
||||||
data: 'rl',
|
data: 'rl',
|
||||||
|
searchable: false,
|
||||||
|
orderable: false,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.backup_mx,
|
title: lang.backup_mx,
|
||||||
data: 'backupmx',
|
data: 'backupmx',
|
||||||
|
searchable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
redner: function (data, type){
|
render: function (data, type){
|
||||||
return 1==value ? '<i class="bi bi-check-lg"></i>' : 0==value && '<i class="bi bi-x-lg"></i>';
|
return 1==data ? '<i class="bi bi-check-lg"></i>' : 0==data && '<i class="bi bi-x-lg"></i>';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.domain_admins,
|
title: lang.domain_admins,
|
||||||
data: 'domain_admins',
|
data: 'domain_admins',
|
||||||
|
searchable: false,
|
||||||
|
orderable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
className: 'none'
|
className: 'none'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.created_on,
|
title: lang.created_on,
|
||||||
data: 'created',
|
data: 'created',
|
||||||
|
searchable: false,
|
||||||
|
orderable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
className: 'none'
|
className: 'none'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.last_modified,
|
title: lang.last_modified,
|
||||||
data: 'modified',
|
data: 'modified',
|
||||||
|
searchable: false,
|
||||||
|
orderable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
className: 'none'
|
className: 'none'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
data: 'tags',
|
data: 'tags',
|
||||||
|
searchable: true,
|
||||||
|
orderable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
className: 'none'
|
className: 'none'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.active,
|
title: lang.active,
|
||||||
data: 'active',
|
data: 'active',
|
||||||
|
searchable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
responsivePriority: 6,
|
responsivePriority: 6,
|
||||||
render: function (data, type) {
|
render: function (data, type) {
|
||||||
@@ -613,6 +635,8 @@ jQuery(function($){
|
|||||||
{
|
{
|
||||||
title: lang.action,
|
title: lang.action,
|
||||||
data: 'action',
|
data: 'action',
|
||||||
|
searchable: false,
|
||||||
|
orderable: false,
|
||||||
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
|
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
|
||||||
responsivePriority: 5,
|
responsivePriority: 5,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
@@ -844,7 +868,7 @@ jQuery(function($){
|
|||||||
var table = $('#mailbox_table').DataTable({
|
var table = $('#mailbox_table').DataTable({
|
||||||
responsive: true,
|
responsive: true,
|
||||||
processing: true,
|
processing: true,
|
||||||
serverSide: false,
|
serverSide: true,
|
||||||
stateSave: true,
|
stateSave: true,
|
||||||
pageLength: pagination_size,
|
pageLength: pagination_size,
|
||||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||||
@@ -853,13 +877,12 @@ jQuery(function($){
|
|||||||
language: lang_datatables,
|
language: lang_datatables,
|
||||||
initComplete: function(settings, json){
|
initComplete: function(settings, json){
|
||||||
hideTableExpandCollapseBtn('#tab-mailboxes', '#mailbox_table');
|
hideTableExpandCollapseBtn('#tab-mailboxes', '#mailbox_table');
|
||||||
filterByDomain(json, 8, table);
|
|
||||||
},
|
},
|
||||||
ajax: {
|
ajax: {
|
||||||
type: "GET",
|
type: "GET",
|
||||||
url: "/api/v1/get/mailbox/reduced",
|
url: "/api/v1/get/mailbox/datatables",
|
||||||
dataSrc: function(json){
|
dataSrc: function(json){
|
||||||
$.each(json, function (i, item) {
|
$.each(json.data, function (i, item) {
|
||||||
item.quota = {
|
item.quota = {
|
||||||
sortBy: item.quota_used,
|
sortBy: item.quota_used,
|
||||||
value: item.quota
|
value: item.quota
|
||||||
@@ -945,7 +968,7 @@ jQuery(function($){
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return json;
|
return json.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
@@ -975,13 +998,14 @@ jQuery(function($){
|
|||||||
{
|
{
|
||||||
title: lang.domain_quota,
|
title: lang.domain_quota,
|
||||||
data: 'quota.value',
|
data: 'quota.value',
|
||||||
|
searchable: false,
|
||||||
responsivePriority: 8,
|
responsivePriority: 8,
|
||||||
defaultContent: '',
|
defaultContent: ''
|
||||||
orderData: 23
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.last_mail_login,
|
title: lang.last_mail_login,
|
||||||
data: 'last_mail_login',
|
data: 'last_mail_login',
|
||||||
|
searchable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
responsivePriority: 7,
|
responsivePriority: 7,
|
||||||
render: function (data, type) {
|
render: function (data, type) {
|
||||||
@@ -994,15 +1018,16 @@ jQuery(function($){
|
|||||||
{
|
{
|
||||||
title: lang.last_pw_change,
|
title: lang.last_pw_change,
|
||||||
data: 'last_pw_change',
|
data: 'last_pw_change',
|
||||||
|
searchable: false,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.in_use,
|
title: lang.in_use,
|
||||||
data: 'in_use.value',
|
data: 'in_use.value',
|
||||||
|
searchable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
responsivePriority: 9,
|
responsivePriority: 9,
|
||||||
className: 'dt-data-w100',
|
className: 'dt-data-w100'
|
||||||
orderData: 24
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.fname,
|
title: lang.fname,
|
||||||
@@ -1067,6 +1092,7 @@ jQuery(function($){
|
|||||||
{
|
{
|
||||||
title: lang.msg_num,
|
title: lang.msg_num,
|
||||||
data: 'messages',
|
data: 'messages',
|
||||||
|
searchable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
responsivePriority: 5
|
responsivePriority: 5
|
||||||
},
|
},
|
||||||
@@ -1085,12 +1111,14 @@ jQuery(function($){
|
|||||||
{
|
{
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
data: 'tags',
|
data: 'tags',
|
||||||
|
searchable: true,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
className: 'none'
|
className: 'none'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: lang.active,
|
title: lang.active,
|
||||||
data: 'active',
|
data: 'active',
|
||||||
|
searchable: false,
|
||||||
defaultContent: '',
|
defaultContent: '',
|
||||||
responsivePriority: 4,
|
responsivePriority: 4,
|
||||||
render: function (data, type) {
|
render: function (data, type) {
|
||||||
@@ -1100,22 +1128,12 @@ jQuery(function($){
|
|||||||
{
|
{
|
||||||
title: lang.action,
|
title: lang.action,
|
||||||
data: 'action',
|
data: 'action',
|
||||||
|
searchable: false,
|
||||||
|
orderable: false,
|
||||||
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
|
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
|
||||||
responsivePriority: 6,
|
responsivePriority: 6,
|
||||||
defaultContent: ''
|
defaultContent: ''
|
||||||
},
|
}
|
||||||
{
|
|
||||||
title: "",
|
|
||||||
data: 'quota.sortBy',
|
|
||||||
defaultContent: '',
|
|
||||||
className: "d-none"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "",
|
|
||||||
data: 'in_use.sortBy',
|
|
||||||
defaultContent: '',
|
|
||||||
className: "d-none"
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jQuery(function($){
|
|||||||
if (value.score > 0) highlightClass = 'negative';
|
if (value.score > 0) highlightClass = 'negative';
|
||||||
else if (value.score < 0) highlightClass = 'positive';
|
else if (value.score < 0) highlightClass = 'positive';
|
||||||
else highlightClass = 'neutral';
|
else highlightClass = 'neutral';
|
||||||
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
|
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? escapeHtml(value.options.join(', ')) : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
|
||||||
});
|
});
|
||||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -676,5 +676,5 @@ jQuery(function($){
|
|||||||
onVisible("[id^=wl_policy_mailbox_table]", () => draw_wl_policy_mailbox_table());
|
onVisible("[id^=wl_policy_mailbox_table]", () => draw_wl_policy_mailbox_table());
|
||||||
onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
|
onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
|
||||||
onVisible("[id^=app_passwd_table]", () => draw_app_passwd_table());
|
onVisible("[id^=app_passwd_table]", () => draw_app_passwd_table());
|
||||||
last_logins('get');
|
onVisible("[id^=recent-logins]", () => last_logins('get'));
|
||||||
});
|
});
|
||||||
|
|||||||
+135
-18
@@ -15,7 +15,7 @@ function api_log($_data) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = json_decode($value, true);
|
$value = json_decode($value, true);
|
||||||
if ($value) {
|
if ($value) {
|
||||||
if (is_array($value)) unset($value["csrf_token"]);
|
if (is_array($value)) unset($value["csrf_token"]);
|
||||||
foreach ($value as $key => &$val) {
|
foreach ($value as $key => &$val) {
|
||||||
@@ -23,7 +23,7 @@ function api_log($_data) {
|
|||||||
$val = '*';
|
$val = '*';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$value = json_encode($value);
|
$value = json_encode($value);
|
||||||
}
|
}
|
||||||
$data_var[] = $data . "='" . $value . "'";
|
$data_var[] = $data . "='" . $value . "'";
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,13 @@ function api_log($_data) {
|
|||||||
'msg' => 'Redis: '.$e
|
'msg' => 'Redis: '.$e
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block requests not intended for direct API use by checking the 'Sec-Fetch-Dest' header.
|
||||||
|
if (isset($_SERVER['HTTP_SEC_FETCH_DEST']) && $_SERVER['HTTP_SEC_FETCH_DEST'] !== 'empty') {
|
||||||
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_GET['query'])) {
|
if (isset($_GET['query'])) {
|
||||||
@@ -178,12 +184,12 @@ if (isset($_GET['query'])) {
|
|||||||
// parse post data
|
// parse post data
|
||||||
$post = trim(file_get_contents('php://input'));
|
$post = trim(file_get_contents('php://input'));
|
||||||
if ($post) $post = json_decode($post);
|
if ($post) $post = json_decode($post);
|
||||||
|
|
||||||
// process registration data from authenticator
|
// process registration data from authenticator
|
||||||
try {
|
try {
|
||||||
// decode base64 strings
|
// decode base64 strings
|
||||||
$clientDataJSON = base64_decode($post->clientDataJSON);
|
$clientDataJSON = base64_decode($post->clientDataJSON);
|
||||||
$attestationObject = base64_decode($post->attestationObject);
|
$attestationObject = base64_decode($post->attestationObject);
|
||||||
|
|
||||||
// processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
|
// processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
|
||||||
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true);
|
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true);
|
||||||
@@ -250,7 +256,7 @@ if (isset($_GET['query'])) {
|
|||||||
default:
|
default:
|
||||||
process_add_return(mailbox('add', 'domain', $attr));
|
process_add_return(mailbox('add', 'domain', $attr));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "resource":
|
case "resource":
|
||||||
process_add_return(mailbox('add', 'resource', $attr));
|
process_add_return(mailbox('add', 'resource', $attr));
|
||||||
@@ -470,7 +476,7 @@ if (isset($_GET['query'])) {
|
|||||||
// false, if only internal is allowed
|
// false, if only internal is allowed
|
||||||
// null, if internal and cross-platform is allowed
|
// null, if internal and cross-platform is allowed
|
||||||
$createArgs = $WebAuthn->getCreateArgs($_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], 30, false, $GLOBALS['WEBAUTHN_UV_FLAG_REGISTER'], null, $excludeCredentialIds);
|
$createArgs = $WebAuthn->getCreateArgs($_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], 30, false, $GLOBALS['WEBAUTHN_UV_FLAG_REGISTER'], null, $excludeCredentialIds);
|
||||||
|
|
||||||
print(json_encode($createArgs));
|
print(json_encode($createArgs));
|
||||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||||
return;
|
return;
|
||||||
@@ -503,6 +509,16 @@ if (isset($_GET['query'])) {
|
|||||||
print(json_encode($getArgs));
|
print(json_encode($getArgs));
|
||||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||||
return;
|
return;
|
||||||
|
break;
|
||||||
|
case "fail2ban":
|
||||||
|
if (!isset($_SESSION['mailcow_cc_role'])){
|
||||||
|
switch ($object) {
|
||||||
|
case 'banlist':
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
echo fail2ban('banlist', 'get', $extra);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (isset($_SESSION['mailcow_cc_role'])) {
|
if (isset($_SESSION['mailcow_cc_role'])) {
|
||||||
@@ -523,9 +539,50 @@ if (isset($_GET['query'])) {
|
|||||||
|
|
||||||
case "domain":
|
case "domain":
|
||||||
switch ($object) {
|
switch ($object) {
|
||||||
|
case "datatables":
|
||||||
|
$table = ['domain', 'd'];
|
||||||
|
$primaryKey = 'domain';
|
||||||
|
$columns = [
|
||||||
|
['db' => 'domain', 'dt' => 2],
|
||||||
|
['db' => 'aliases', 'dt' => 3, 'order_subquery' => "SELECT COUNT(*) FROM `alias` WHERE (`domain`= `d`.`domain` OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = `d`.`domain`)) AND `address` NOT IN (SELECT `username` FROM `mailbox`)"],
|
||||||
|
['db' => 'mailboxes', 'dt' => 4, 'order_subquery' => "SELECT COUNT(*) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
|
||||||
|
['db' => 'quota', 'dt' => 5, 'order_subquery' => "SELECT COALESCE(SUM(`mailbox`.`quota`), 0) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
|
||||||
|
['db' => 'stats', 'dt' => 6, 'dummy' => true, 'order_subquery' => "SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` IN (SELECT `username` FROM `mailbox` WHERE `domain` = `d`.`domain`)"],
|
||||||
|
['db' => 'defquota', 'dt' => 7],
|
||||||
|
['db' => 'maxquota', 'dt' => 8],
|
||||||
|
['db' => 'backupmx', 'dt' => 10],
|
||||||
|
['db' => 'tags', 'dt' => 14, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_domain` AS `td` ON `td`.`domain` = `d`.`domain`', 'where_column' => '`td`.`tag_name`']],
|
||||||
|
['db' => 'active', 'dt' => 15],
|
||||||
|
];
|
||||||
|
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
|
||||||
|
global $pdo;
|
||||||
|
if($_SESSION['mailcow_cc_role'] === 'admin') {
|
||||||
|
$data = SSP::simple($_GET, $pdo, $table, $primaryKey, $columns);
|
||||||
|
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
|
||||||
|
$data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns,
|
||||||
|
'INNER JOIN domain_admins as da ON da.domain = d.domain',
|
||||||
|
[
|
||||||
|
'condition' => 'da.active = 1 and da.username = :username',
|
||||||
|
'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['data'])) {
|
||||||
|
$domainsData = [];
|
||||||
|
foreach ($data['data'] as $domain) {
|
||||||
|
if ($details = mailbox('get', 'domain_details', $domain[2])) {
|
||||||
|
$domainsData[] = $details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data['data'] = $domainsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
process_get_return($data);
|
||||||
|
break;
|
||||||
case "all":
|
case "all":
|
||||||
$tags = null;
|
$tags = null;
|
||||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||||
$tags = explode(',', $_GET['tags']);
|
$tags = explode(',', $_GET['tags']);
|
||||||
|
|
||||||
$domains = mailbox('get', 'domains', null, $tags);
|
$domains = mailbox('get', 'domains', null, $tags);
|
||||||
@@ -1011,10 +1068,49 @@ if (isset($_GET['query'])) {
|
|||||||
break;
|
break;
|
||||||
case "mailbox":
|
case "mailbox":
|
||||||
switch ($object) {
|
switch ($object) {
|
||||||
|
case "datatables":
|
||||||
|
$table = ['mailbox', 'm'];
|
||||||
|
$primaryKey = 'username';
|
||||||
|
$columns = [
|
||||||
|
['db' => 'username', 'dt' => 2],
|
||||||
|
['db' => 'quota', 'dt' => 3],
|
||||||
|
['db' => 'last_mail_login', 'dt' => 4, 'dummy' => true, 'order_subquery' => "SELECT MAX(`datetime`) FROM `sasl_log` WHERE `service` != 'SSO' AND `username` = `m`.`username`"],
|
||||||
|
['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"],
|
||||||
|
['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"],
|
||||||
|
['db' => 'messages', 'dt' => 17, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
|
||||||
|
['db' => 'tags', 'dt' => 20, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
|
||||||
|
['db' => 'active', 'dt' => 21]
|
||||||
|
];
|
||||||
|
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
|
||||||
|
global $pdo;
|
||||||
|
if($_SESSION['mailcow_cc_role'] === 'admin') {
|
||||||
|
$data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns, null, "(`m`.`kind` = '' OR `m`.`kind` = NULL)");
|
||||||
|
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
|
||||||
|
$data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns,
|
||||||
|
'INNER JOIN domain_admins as da ON da.domain = m.domain',
|
||||||
|
[
|
||||||
|
'condition' => "(`m`.`kind` = '' OR `m`.`kind` = NULL) AND `da`.`active` = 1 AND `da`.`username` = :username",
|
||||||
|
'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['data'])) {
|
||||||
|
$mailboxData = [];
|
||||||
|
foreach ($data['data'] as $mailbox) {
|
||||||
|
if ($details = mailbox('get', 'mailbox_details', $mailbox[2])) {
|
||||||
|
$mailboxData[] = $details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data['data'] = $mailboxData;
|
||||||
|
}
|
||||||
|
|
||||||
|
process_get_return($data);
|
||||||
|
break;
|
||||||
case "all":
|
case "all":
|
||||||
case "reduced":
|
case "reduced":
|
||||||
$tags = null;
|
$tags = null;
|
||||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||||
$tags = explode(',', $_GET['tags']);
|
$tags = explode(',', $_GET['tags']);
|
||||||
|
|
||||||
if (empty($extra)) $domains = mailbox('get', 'domains');
|
if (empty($extra)) $domains = mailbox('get', 'domains');
|
||||||
@@ -1048,7 +1144,7 @@ if (isset($_GET['query'])) {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$tags = null;
|
$tags = null;
|
||||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||||
$tags = explode(',', $_GET['tags']);
|
$tags = explode(',', $_GET['tags']);
|
||||||
|
|
||||||
if ($tags === null) {
|
if ($tags === null) {
|
||||||
@@ -1058,7 +1154,7 @@ if (isset($_GET['query'])) {
|
|||||||
$mailboxes = mailbox('get', 'mailboxes', $object, $tags);
|
$mailboxes = mailbox('get', 'mailboxes', $object, $tags);
|
||||||
if (is_array($mailboxes)) {
|
if (is_array($mailboxes)) {
|
||||||
foreach ($mailboxes as $mailbox) {
|
foreach ($mailboxes as $mailbox) {
|
||||||
if ($details = mailbox('get', 'mailbox_details', $mailbox))
|
if ($details = mailbox('get', 'mailbox_details', $mailbox))
|
||||||
$data[] = $details;
|
$data[] = $details;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1324,6 +1420,10 @@ if (isset($_GET['query'])) {
|
|||||||
break;
|
break;
|
||||||
case "fail2ban":
|
case "fail2ban":
|
||||||
switch ($object) {
|
switch ($object) {
|
||||||
|
case 'banlist':
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
echo fail2ban('banlist', 'get', $extra);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$data = fail2ban('get');
|
$data = fail2ban('get');
|
||||||
process_get_return($data);
|
process_get_return($data);
|
||||||
@@ -1557,15 +1657,15 @@ if (isset($_GET['query'])) {
|
|||||||
'solr_size' => $solr_size,
|
'solr_size' => $solr_size,
|
||||||
'solr_documents' => $solr_documents
|
'solr_documents' => $solr_documents
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
case "host":
|
case "host":
|
||||||
if (!$extra){
|
if (!$extra){
|
||||||
$stats = docker("host_stats");
|
$stats = docker("host_stats");
|
||||||
echo json_encode($stats);
|
echo json_encode($stats);
|
||||||
}
|
}
|
||||||
else if ($extra == "ip") {
|
else if ($extra == "ip") {
|
||||||
// get public ips
|
// get public ips
|
||||||
|
|
||||||
$curl = curl_init();
|
$curl = curl_init();
|
||||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||||
curl_setopt($curl, CURLOPT_POST, 0);
|
curl_setopt($curl, CURLOPT_POST, 0);
|
||||||
@@ -1591,6 +1691,12 @@ if (isset($_GET['query'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "spam-score":
|
||||||
|
$score = mailbox('get', 'spam_score', $object);
|
||||||
|
if ($score)
|
||||||
|
$score = array("score" => preg_replace("/\s+/", "", $score));
|
||||||
|
process_get_return($score);
|
||||||
|
break;
|
||||||
break;
|
break;
|
||||||
// return no route found if no case is matched
|
// return no route found if no case is matched
|
||||||
default:
|
default:
|
||||||
@@ -1867,8 +1973,6 @@ if (isset($_GET['query'])) {
|
|||||||
case "quota_notification_bcc":
|
case "quota_notification_bcc":
|
||||||
process_edit_return(quota_notification_bcc('edit', $attr));
|
process_edit_return(quota_notification_bcc('edit', $attr));
|
||||||
break;
|
break;
|
||||||
case "domain-wide-footer":
|
|
||||||
process_edit_return(mailbox('edit', 'domain_wide_footer', $attr));
|
|
||||||
break;
|
break;
|
||||||
case "mailq":
|
case "mailq":
|
||||||
process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr)));
|
process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr)));
|
||||||
@@ -1881,6 +1985,9 @@ if (isset($_GET['query'])) {
|
|||||||
case "template":
|
case "template":
|
||||||
process_edit_return(mailbox('edit', 'mailbox_templates', array_merge(array('ids' => $items), $attr)));
|
process_edit_return(mailbox('edit', 'mailbox_templates', array_merge(array('ids' => $items), $attr)));
|
||||||
break;
|
break;
|
||||||
|
case "custom-attribute":
|
||||||
|
process_edit_return(mailbox('edit', 'mailbox_custom_attribute', array_merge(array('mailboxes' => $items), $attr)));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
process_edit_return(mailbox('edit', 'mailbox', array_merge(array('username' => $items), $attr)));
|
process_edit_return(mailbox('edit', 'mailbox', array_merge(array('username' => $items), $attr)));
|
||||||
break;
|
break;
|
||||||
@@ -1900,6 +2007,9 @@ if (isset($_GET['query'])) {
|
|||||||
case "template":
|
case "template":
|
||||||
process_edit_return(mailbox('edit', 'domain_templates', array_merge(array('ids' => $items), $attr)));
|
process_edit_return(mailbox('edit', 'domain_templates', array_merge(array('ids' => $items), $attr)));
|
||||||
break;
|
break;
|
||||||
|
case "footer":
|
||||||
|
process_edit_return(mailbox('edit', 'domain_wide_footer', array_merge(array('domains' => $items), $attr)));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
process_edit_return(mailbox('edit', 'domain', array_merge(array('domain' => $items), $attr)));
|
process_edit_return(mailbox('edit', 'domain', array_merge(array('domain' => $items), $attr)));
|
||||||
break;
|
break;
|
||||||
@@ -1933,7 +2043,14 @@ if (isset($_GET['query'])) {
|
|||||||
process_edit_return(fwdhost('edit', array_merge(array('fwdhost' => $items), $attr)));
|
process_edit_return(fwdhost('edit', array_merge(array('fwdhost' => $items), $attr)));
|
||||||
break;
|
break;
|
||||||
case "fail2ban":
|
case "fail2ban":
|
||||||
process_edit_return(fail2ban('edit', array_merge(array('network' => $items), $attr)));
|
switch ($object) {
|
||||||
|
case 'banlist':
|
||||||
|
process_edit_return(fail2ban('banlist', 'refresh', $items));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
process_edit_return(fail2ban('edit', array_merge(array('network' => $items), $attr)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "ui_texts":
|
case "ui_texts":
|
||||||
process_edit_return(customize('edit', 'ui_texts', $attr));
|
process_edit_return(customize('edit', 'ui_texts', $attr));
|
||||||
@@ -1972,7 +2089,7 @@ if (isset($_GET['query'])) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($_SESSION['mailcow_cc_api'] === true) {
|
if (array_key_exists('mailcow_cc_api', $_SESSION) && $_SESSION['mailcow_cc_api'] === true) {
|
||||||
if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
|
if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
|
||||||
unset($_SESSION['return']);
|
unset($_SESSION['return']);
|
||||||
}
|
}
|
||||||
|
|||||||
+765
-17
@@ -19,7 +19,16 @@
|
|||||||
"spam_alias": "Àlies temporals",
|
"spam_alias": "Àlies temporals",
|
||||||
"spam_score": "Puntuació de correu brossa",
|
"spam_score": "Puntuació de correu brossa",
|
||||||
"tls_policy": "Política TLS",
|
"tls_policy": "Política TLS",
|
||||||
"unlimited_quota": "Quota ilimitada per bústies de correo"
|
"unlimited_quota": "Quota ilimitada per bústies de correo",
|
||||||
|
"delimiter_action": "",
|
||||||
|
"ratelimit": "",
|
||||||
|
"smtp_ip_access": "",
|
||||||
|
"sogo_access": "",
|
||||||
|
"domain_relayhost": "",
|
||||||
|
"extend_sender_acl": "",
|
||||||
|
"mailbox_relayhost": "",
|
||||||
|
"pushover": "",
|
||||||
|
"spam_policy": ""
|
||||||
},
|
},
|
||||||
"add": {
|
"add": {
|
||||||
"activate_filter_warn": "All other filters will be deactivated, when active is checked.",
|
"activate_filter_warn": "All other filters will be deactivated, when active is checked.",
|
||||||
@@ -73,7 +82,33 @@
|
|||||||
"validate": "Validar",
|
"validate": "Validar",
|
||||||
"validation_success": "Validated successfully",
|
"validation_success": "Validated successfully",
|
||||||
"app_name": "Nom de l'aplicació",
|
"app_name": "Nom de l'aplicació",
|
||||||
"app_password": "Afegir contrasenya a l'aplicació"
|
"app_password": "Afegir contrasenya a l'aplicació",
|
||||||
|
"dry": "",
|
||||||
|
"inactive": "",
|
||||||
|
"disable_login": "",
|
||||||
|
"goto_ham": "",
|
||||||
|
"goto_spam": "",
|
||||||
|
"mailbox_quota_def": "",
|
||||||
|
"private_comment": "",
|
||||||
|
"public_comment": "",
|
||||||
|
"relay_unknown_only": "",
|
||||||
|
"relayhost_wrapped_tls_info": "",
|
||||||
|
"subscribeall": "",
|
||||||
|
"tags": "",
|
||||||
|
"timeout1": "",
|
||||||
|
"timeout2": "",
|
||||||
|
"relay_transport_info": "",
|
||||||
|
"domain_matches_hostname": "",
|
||||||
|
"gal": "",
|
||||||
|
"gal_info": "",
|
||||||
|
"generate": "",
|
||||||
|
"nexthop": "",
|
||||||
|
"app_passwd_protocols": "",
|
||||||
|
"bcc_dest_format": "",
|
||||||
|
"comment_info": "",
|
||||||
|
"custom_params": "",
|
||||||
|
"custom_params_hint": "",
|
||||||
|
"destination": ""
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"access": "Accés",
|
"access": "Accés",
|
||||||
@@ -156,7 +191,167 @@
|
|||||||
"ui_texts": "Etiquetes i textos de la UI",
|
"ui_texts": "Etiquetes i textos de la UI",
|
||||||
"unchanged_if_empty": "Si no hi ha canvis, deixa'l en blanc",
|
"unchanged_if_empty": "Si no hi ha canvis, deixa'l en blanc",
|
||||||
"upload": "Pujar",
|
"upload": "Pujar",
|
||||||
"username": "Nom d'usuari"
|
"username": "Nom d'usuari",
|
||||||
|
"lookup_mx": "",
|
||||||
|
"logo_dark_label": "",
|
||||||
|
"optional": "",
|
||||||
|
"copy_to_clipboard": "",
|
||||||
|
"f2b_manage_external": "",
|
||||||
|
"f2b_manage_external_info": "",
|
||||||
|
"license_info": "",
|
||||||
|
"message": "",
|
||||||
|
"message_size": "",
|
||||||
|
"nexthop": "",
|
||||||
|
"no": "",
|
||||||
|
"no_active_bans": "",
|
||||||
|
"oauth2_apps": "",
|
||||||
|
"oauth2_client_secret": "",
|
||||||
|
"oauth2_info": "",
|
||||||
|
"password_policy_chars": "",
|
||||||
|
"password_policy_numbers": "",
|
||||||
|
"password_policy_special_chars": "",
|
||||||
|
"priority": "",
|
||||||
|
"quarantine_bcc": "",
|
||||||
|
"quarantine_max_size": "",
|
||||||
|
"quarantine_notification_html": "",
|
||||||
|
"quarantine_redirect": "",
|
||||||
|
"quarantine_release_format_att": "",
|
||||||
|
"quarantine_retention_size": "",
|
||||||
|
"quota_notification_html": "",
|
||||||
|
"quota_notification_subject": "",
|
||||||
|
"quota_notifications": "",
|
||||||
|
"relay_rcpt": "",
|
||||||
|
"relay_run": "",
|
||||||
|
"routing": "",
|
||||||
|
"rspamd_global_filters_regex": "",
|
||||||
|
"title": "",
|
||||||
|
"transport_maps": "",
|
||||||
|
"transport_test_rcpt_info": "",
|
||||||
|
"transports_hint": "",
|
||||||
|
"ui_header_announcement_active": "",
|
||||||
|
"validate_license_now": "",
|
||||||
|
"verify": "",
|
||||||
|
"yes": "",
|
||||||
|
"password_length": "",
|
||||||
|
"password_policy": "",
|
||||||
|
"logo_normal_label": "",
|
||||||
|
"f2b_max_ban_time": "",
|
||||||
|
"f2b_ban_time_increment": "",
|
||||||
|
"add_relayhost_hint": "",
|
||||||
|
"add_transport": "",
|
||||||
|
"add_transports_hint": "",
|
||||||
|
"admins": "",
|
||||||
|
"quarantine_exclude_domains": "",
|
||||||
|
"quarantine_max_age": "",
|
||||||
|
"quarantine_release_format_raw": "",
|
||||||
|
"quota_notification_sender": "",
|
||||||
|
"quota_notifications_info": "",
|
||||||
|
"quota_notifications_vars": "",
|
||||||
|
"r_active": "",
|
||||||
|
"r_inactive": "",
|
||||||
|
"rate_name": "",
|
||||||
|
"regex_maps": "",
|
||||||
|
"ui_header_announcement": "",
|
||||||
|
"cors_settings": "",
|
||||||
|
"allowed_methods": "",
|
||||||
|
"allowed_origins": "",
|
||||||
|
"oauth2_renew_secret": "",
|
||||||
|
"oauth2_revoke_tokens": "",
|
||||||
|
"send": "",
|
||||||
|
"success": "",
|
||||||
|
"sys_mails": "",
|
||||||
|
"text": "",
|
||||||
|
"time": "",
|
||||||
|
"unban_pending": "",
|
||||||
|
"ip_check": "",
|
||||||
|
"quarantine_notification_sender": "",
|
||||||
|
"dkim_domains_wo_keys": "",
|
||||||
|
"dkim_to": "",
|
||||||
|
"duplicate": "",
|
||||||
|
"excludes": "",
|
||||||
|
"f2b_blacklist": "",
|
||||||
|
"f2b_filter": "",
|
||||||
|
"guid": "",
|
||||||
|
"oauth2_add_client": "",
|
||||||
|
"rsetting_content": "",
|
||||||
|
"queue_unban": "",
|
||||||
|
"ip_check_disabled": "",
|
||||||
|
"ip_check_opt_in": "",
|
||||||
|
"options": "",
|
||||||
|
"relayhosts": "",
|
||||||
|
"reset_limit": "",
|
||||||
|
"rsetting_add_rule": "",
|
||||||
|
"rspamd_com_settings": "",
|
||||||
|
"ui_footer": "",
|
||||||
|
"dkim_from": "",
|
||||||
|
"admins_ldap": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"api_info": "",
|
||||||
|
"api_read_only": "",
|
||||||
|
"api_read_write": "",
|
||||||
|
"api_skip_ip_check": "",
|
||||||
|
"arrival_time": "",
|
||||||
|
"authed_user": "",
|
||||||
|
"ays": "",
|
||||||
|
"activate_send": "",
|
||||||
|
"active_rspamd_settings_map": "",
|
||||||
|
"add_admin": "",
|
||||||
|
"add_settings_rule": "",
|
||||||
|
"oauth2_redirect_uri": "",
|
||||||
|
"domain_admin": "",
|
||||||
|
"password_policy_length": "",
|
||||||
|
"password_policy_lowerupper": "",
|
||||||
|
"quarantine_release_format": "",
|
||||||
|
"rsetting_none": "",
|
||||||
|
"rsettings_insert_preset": "",
|
||||||
|
"transport_dest_format": "",
|
||||||
|
"ban_list_info": "",
|
||||||
|
"convert_html_to_text": "",
|
||||||
|
"credentials_transport_warning": "",
|
||||||
|
"customer_id": "",
|
||||||
|
"destination": "",
|
||||||
|
"dkim_domains_selector": "",
|
||||||
|
"dkim_from_title": "",
|
||||||
|
"dkim_overwrite_key": "",
|
||||||
|
"dkim_to_title": "",
|
||||||
|
"domain_s": "",
|
||||||
|
"f2b_list_info": "",
|
||||||
|
"f2b_regex_info": "",
|
||||||
|
"from": "",
|
||||||
|
"generate": "",
|
||||||
|
"guid_and_license": "",
|
||||||
|
"hash_remove_info": "",
|
||||||
|
"html": "",
|
||||||
|
"include_exclude": "",
|
||||||
|
"include_exclude_info": "",
|
||||||
|
"includes": "",
|
||||||
|
"is_mx_based": "",
|
||||||
|
"last_applied": "",
|
||||||
|
"login_time": "",
|
||||||
|
"oauth2_client_id": "",
|
||||||
|
"quarantine_max_score": "",
|
||||||
|
"quarantine_notification_subject": "",
|
||||||
|
"rsetting_no_selection": "",
|
||||||
|
"rsettings_preset_1": "",
|
||||||
|
"rsettings_preset_2": "",
|
||||||
|
"rsettings_preset_3": "",
|
||||||
|
"rsettings_preset_4": "",
|
||||||
|
"rspamd_global_filters": "",
|
||||||
|
"rspamd_global_filters_agree": "",
|
||||||
|
"rspamd_global_filters_info": "",
|
||||||
|
"rspamd_settings_map": "",
|
||||||
|
"sal_level": "",
|
||||||
|
"service": "",
|
||||||
|
"service_id": "",
|
||||||
|
"ui_header_announcement_content": "",
|
||||||
|
"ui_header_announcement_help": "",
|
||||||
|
"ui_header_announcement_select": "",
|
||||||
|
"ui_header_announcement_type": "",
|
||||||
|
"ui_header_announcement_type_danger": "",
|
||||||
|
"ui_header_announcement_type_info": "",
|
||||||
|
"ui_header_announcement_type_warning": "",
|
||||||
|
"duplicate_dkim": "",
|
||||||
|
"r_info": ""
|
||||||
},
|
},
|
||||||
"danger": {
|
"danger": {
|
||||||
"access_denied": "Accés denegat o dades incorrectes",
|
"access_denied": "Accés denegat o dades incorrectes",
|
||||||
@@ -207,7 +402,87 @@
|
|||||||
"target_domain_invalid": "El domini \"goto\" no és vàlid",
|
"target_domain_invalid": "El domini \"goto\" no és vàlid",
|
||||||
"targetd_not_found": "No s'ha trobat el domini destí",
|
"targetd_not_found": "No s'ha trobat el domini destí",
|
||||||
"username_invalid": "El nom d'usuari no es pot fer servir",
|
"username_invalid": "El nom d'usuari no es pot fer servir",
|
||||||
"validity_missing": "Si et plau posa un període de validesa"
|
"validity_missing": "Si et plau posa un període de validesa",
|
||||||
|
"webauthn_authenticator_failed": "",
|
||||||
|
"webauthn_publickey_failed": "",
|
||||||
|
"value_missing": "",
|
||||||
|
"unknown": "",
|
||||||
|
"app_name_empty": "",
|
||||||
|
"app_passwd_id_invalid": "",
|
||||||
|
"bcc_empty": "",
|
||||||
|
"invalid_bcc_map_type": "",
|
||||||
|
"invalid_filter_type": "",
|
||||||
|
"invalid_host": "",
|
||||||
|
"bcc_exists": "",
|
||||||
|
"bcc_must_be_email": "",
|
||||||
|
"comment_too_long": "",
|
||||||
|
"cors_invalid_method": "",
|
||||||
|
"cors_invalid_origin": "",
|
||||||
|
"defquota_empty": "",
|
||||||
|
"extended_sender_acl_denied": "",
|
||||||
|
"extra_acl_invalid": "",
|
||||||
|
"extra_acl_invalid_domain": "",
|
||||||
|
"fido2_verification_failed": "",
|
||||||
|
"file_open_error": "",
|
||||||
|
"filter_type": "",
|
||||||
|
"from_invalid": "",
|
||||||
|
"global_filter_write_error": "",
|
||||||
|
"global_map_invalid": "",
|
||||||
|
"global_map_write_error": "",
|
||||||
|
"ham_learn_error": "",
|
||||||
|
"invalid_nexthop": "",
|
||||||
|
"invalid_recipient_map_new": "",
|
||||||
|
"invalid_destination": "",
|
||||||
|
"tfa_token_invalid": "",
|
||||||
|
"tls_policy_map_dest_invalid": "",
|
||||||
|
"rl_timeframe": "",
|
||||||
|
"img_dimensions_exceeded": "",
|
||||||
|
"img_size_exceeded": "",
|
||||||
|
"invalid_nexthop_authenticated": "",
|
||||||
|
"invalid_recipient_map_old": "",
|
||||||
|
"private_key_error": "",
|
||||||
|
"pushover_credentials_missing": "",
|
||||||
|
"pushover_key": "",
|
||||||
|
"pushover_token": "",
|
||||||
|
"recipient_map_entry_exists": "",
|
||||||
|
"redis_error": "",
|
||||||
|
"relayhost_invalid": "",
|
||||||
|
"reset_f2b_regex": "",
|
||||||
|
"rspamd_ui_pw_length": "",
|
||||||
|
"script_empty": "",
|
||||||
|
"set_acl_failed": "",
|
||||||
|
"targetd_relay_domain": "",
|
||||||
|
"template_exists": "",
|
||||||
|
"template_id_invalid": "",
|
||||||
|
"template_name_invalid": "",
|
||||||
|
"temp_error": "",
|
||||||
|
"text_empty": "",
|
||||||
|
"tls_policy_map_entry_exists": "",
|
||||||
|
"tls_policy_map_parameter_invalid": "",
|
||||||
|
"totp_verification_failed": "",
|
||||||
|
"transport_dest_exists": "",
|
||||||
|
"webauthn_verification_failed": "",
|
||||||
|
"webauthn_username_failed": "",
|
||||||
|
"unknown_tfa_method": "",
|
||||||
|
"unlimited_quota_acl": "",
|
||||||
|
"yotp_verification_failed": "",
|
||||||
|
"no_user_defined": "",
|
||||||
|
"demo_mode_enabled": "",
|
||||||
|
"dkim_domain_or_sel_exists": "",
|
||||||
|
"domain_cannot_match_hostname": "",
|
||||||
|
"ip_list_empty": "",
|
||||||
|
"settings_map_invalid": "",
|
||||||
|
"sieve_error": "",
|
||||||
|
"spam_learn_error": "",
|
||||||
|
"subject_empty": "",
|
||||||
|
"mailbox_defquota_exceeds_mailbox_maxquota": "",
|
||||||
|
"malformed_username": "",
|
||||||
|
"map_content_empty": "",
|
||||||
|
"mysql_error": "",
|
||||||
|
"network_host_invalid": "",
|
||||||
|
"next_hop_interferes": "",
|
||||||
|
"next_hop_interferes_any": "",
|
||||||
|
"nginx_reload_failed": ""
|
||||||
},
|
},
|
||||||
"diagnostics": {
|
"diagnostics": {
|
||||||
"cname_from_a": "Valor derivat de registre A/AAAA. Això és compatible sempre que el registre assenyali el recurs correcte.",
|
"cname_from_a": "Valor derivat de registre A/AAAA. Això és compatible sempre que el registre assenyali el recurs correcte.",
|
||||||
@@ -217,7 +492,8 @@
|
|||||||
"dns_records_name": "Nom",
|
"dns_records_name": "Nom",
|
||||||
"dns_records_status": "Valor actual",
|
"dns_records_status": "Valor actual",
|
||||||
"dns_records_type": "Tipus",
|
"dns_records_type": "Tipus",
|
||||||
"optional": "Aquest registre és opcional."
|
"optional": "Aquest registre és opcional.",
|
||||||
|
"dns_records_docs": ""
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"active": "Actiu",
|
"active": "Actiu",
|
||||||
@@ -273,7 +549,82 @@
|
|||||||
"title": "Editar l'objecte",
|
"title": "Editar l'objecte",
|
||||||
"unchanged_if_empty": "Si no hay cambios dejalo en blanco",
|
"unchanged_if_empty": "Si no hay cambios dejalo en blanco",
|
||||||
"username": "Usuari",
|
"username": "Usuari",
|
||||||
"validate_save": "Validar i desar"
|
"validate_save": "Validar i desar",
|
||||||
|
"lookup_mx": "",
|
||||||
|
"acl": "",
|
||||||
|
"admin": "",
|
||||||
|
"allow_from_smtp": "",
|
||||||
|
"footer_exclude": "",
|
||||||
|
"domain_footer_skip_replies": "",
|
||||||
|
"domain_footer_plain": "",
|
||||||
|
"gal_info": "",
|
||||||
|
"grant_types": "",
|
||||||
|
"pushover": "",
|
||||||
|
"scope": "",
|
||||||
|
"custom_attributes": "",
|
||||||
|
"disable_login": "",
|
||||||
|
"none_inherit": "",
|
||||||
|
"pushover_info": "",
|
||||||
|
"domain_footer": "",
|
||||||
|
"domain_footer_html": "",
|
||||||
|
"domain_footer_info_vars": {
|
||||||
|
"from_name": "",
|
||||||
|
"from_addr": "",
|
||||||
|
"from_domain": "",
|
||||||
|
"auth_user": "",
|
||||||
|
"from_user": "",
|
||||||
|
"custom": ""
|
||||||
|
},
|
||||||
|
"gal": "",
|
||||||
|
"mbox_rl_info": "",
|
||||||
|
"nexthop": "",
|
||||||
|
"private_comment": "",
|
||||||
|
"public_comment": "",
|
||||||
|
"pushover_evaluate_x_prio": "",
|
||||||
|
"pushover_only_x_prio": "",
|
||||||
|
"pushover_sender_array": "",
|
||||||
|
"pushover_sender_regex": "",
|
||||||
|
"sogo_visible": "",
|
||||||
|
"allow_from_smtp_info": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"app_name": "",
|
||||||
|
"app_passwd": "",
|
||||||
|
"bcc_dest_format": "",
|
||||||
|
"client_id": "",
|
||||||
|
"client_secret": "",
|
||||||
|
"comment_info": "",
|
||||||
|
"created_on": "",
|
||||||
|
"delete_ays": "",
|
||||||
|
"domain_footer_info": "",
|
||||||
|
"inactive": "",
|
||||||
|
"extended_sender_acl": "",
|
||||||
|
"extended_sender_acl_info": "",
|
||||||
|
"last_modified": "",
|
||||||
|
"mailbox_quota_def": "",
|
||||||
|
"mailbox_relayhost_info": "",
|
||||||
|
"pushover_text": "",
|
||||||
|
"pushover_title": "",
|
||||||
|
"sender_acl_disabled": "",
|
||||||
|
"sender_acl_info": "",
|
||||||
|
"sogo_access_info": "",
|
||||||
|
"sogo_visible_info": "",
|
||||||
|
"timeout1": "",
|
||||||
|
"timeout2": "",
|
||||||
|
"generate": "",
|
||||||
|
"app_passwd_protocols": "",
|
||||||
|
"ratelimit": "",
|
||||||
|
"relayhost": "",
|
||||||
|
"pushover_sound": "",
|
||||||
|
"pushover_vars": "",
|
||||||
|
"pushover_verify": "",
|
||||||
|
"quota_warning_bcc": "",
|
||||||
|
"quota_warning_bcc_info": "",
|
||||||
|
"relay_transport_info": "",
|
||||||
|
"relay_unknown_only": "",
|
||||||
|
"spam_filter": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"redirect_uri": "",
|
||||||
|
"sogo_access": ""
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"cancel": "Cancel·lar",
|
"cancel": "Cancel·lar",
|
||||||
@@ -282,7 +633,13 @@
|
|||||||
"delete_these_items": "Si et plau confirma els canvis al objecte amb id:",
|
"delete_these_items": "Si et plau confirma els canvis al objecte amb id:",
|
||||||
"loading": "Si et plau espera ...",
|
"loading": "Si et plau espera ...",
|
||||||
"restart_container_info": "<b>Important:</b> Un reinici pot trigar una estona, si et plau espera a que acabi.",
|
"restart_container_info": "<b>Important:</b> Un reinici pot trigar una estona, si et plau espera a que acabi.",
|
||||||
"restart_now": "Reiniciar ara"
|
"restart_now": "Reiniciar ara",
|
||||||
|
"hibp_check": "",
|
||||||
|
"hibp_nok": "",
|
||||||
|
"hibp_ok": "",
|
||||||
|
"nothing_selected": "",
|
||||||
|
"restart_container": "",
|
||||||
|
"restarting_container": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"administration": "Administració",
|
"administration": "Administració",
|
||||||
@@ -291,16 +648,24 @@
|
|||||||
"mailcow_config": "Configuració",
|
"mailcow_config": "Configuració",
|
||||||
"quarantine": "Quarantena",
|
"quarantine": "Quarantena",
|
||||||
"restart_sogo": "Reiniciar SOGo",
|
"restart_sogo": "Reiniciar SOGo",
|
||||||
"user_settings": "Preferències d'usuari"
|
"user_settings": "Preferències d'usuari",
|
||||||
|
"restart_netfilter": "",
|
||||||
|
"apps": "",
|
||||||
|
"mailcow_system": ""
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"no_action": "No hi ha cap acció aplicable"
|
"no_action": "No hi ha cap acció aplicable",
|
||||||
|
"awaiting_tfa_confirmation": "",
|
||||||
|
"session_expires": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"delayed": "Pots iniciar de sessió passats %s segons.",
|
"delayed": "Pots iniciar de sessió passats %s segons.",
|
||||||
"login": "Inici de sessió",
|
"login": "Inici de sessió",
|
||||||
"password": "Contrasenya",
|
"password": "Contrasenya",
|
||||||
"username": "Nom d'usuari"
|
"username": "Nom d'usuari",
|
||||||
|
"fido2_webauthn": "",
|
||||||
|
"mobileconfig_info": "",
|
||||||
|
"other_logins": ""
|
||||||
},
|
},
|
||||||
"mailbox": {
|
"mailbox": {
|
||||||
"action": "Acció",
|
"action": "Acció",
|
||||||
@@ -385,7 +750,97 @@
|
|||||||
"tls_enforce_out": "Forçar TLS al enviar",
|
"tls_enforce_out": "Forçar TLS al enviar",
|
||||||
"toggle_all": "Tots",
|
"toggle_all": "Tots",
|
||||||
"username": "Nom d'usuari",
|
"username": "Nom d'usuari",
|
||||||
"waiting": "Esperant"
|
"waiting": "Esperant",
|
||||||
|
"booking_0_short": "",
|
||||||
|
"booking_null": "",
|
||||||
|
"booking_custom": "",
|
||||||
|
"sieve_preset_8": "",
|
||||||
|
"sieve_preset_header": "",
|
||||||
|
"sogo_visible": "",
|
||||||
|
"stats": "",
|
||||||
|
"tls_policy_maps_long": "",
|
||||||
|
"quarantine_notification": "",
|
||||||
|
"recipient_map_old_info": "",
|
||||||
|
"table_size_show_n": "",
|
||||||
|
"tls_map_dest": "",
|
||||||
|
"tls_map_dest_info": "",
|
||||||
|
"sieve_preset_2": "",
|
||||||
|
"add_tls_policy_map": "",
|
||||||
|
"sieve_preset_3": "",
|
||||||
|
"syncjob_EXIT_OVERQUOTA": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "",
|
||||||
|
"table_size": "",
|
||||||
|
"tls_map_parameters": "",
|
||||||
|
"disable_x": "",
|
||||||
|
"domain_templates": "",
|
||||||
|
"last_pw_change": "",
|
||||||
|
"no": "",
|
||||||
|
"public_comment": "",
|
||||||
|
"q_add_header": "",
|
||||||
|
"q_reject": "",
|
||||||
|
"recipient": "",
|
||||||
|
"relay_unknown": "",
|
||||||
|
"sieve_preset_1": "",
|
||||||
|
"sieve_preset_7": "",
|
||||||
|
"tls_map_parameters_info": "",
|
||||||
|
"tls_map_policy": "",
|
||||||
|
"tls_policy_maps": "",
|
||||||
|
"dkim_domains_selector": "",
|
||||||
|
"templates": "",
|
||||||
|
"never": "",
|
||||||
|
"add_template": "",
|
||||||
|
"all_domains": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"bcc_map": "",
|
||||||
|
"booking_custom_short": "",
|
||||||
|
"booking_ltnull": "",
|
||||||
|
"booking_lt0_short": "",
|
||||||
|
"catch_all": "",
|
||||||
|
"created_on": "",
|
||||||
|
"daily": "",
|
||||||
|
"disable_login": "",
|
||||||
|
"domain_quota_total": "",
|
||||||
|
"enable_x": "",
|
||||||
|
"gal": "",
|
||||||
|
"goto_ham": "",
|
||||||
|
"goto_spam": "",
|
||||||
|
"hourly": "",
|
||||||
|
"insert_preset": "",
|
||||||
|
"last_mail_login": "",
|
||||||
|
"last_modified": "",
|
||||||
|
"mailbox": "",
|
||||||
|
"mailbox_defaults": "",
|
||||||
|
"mailbox_defaults_info": "",
|
||||||
|
"mailbox_defquota": "",
|
||||||
|
"mailbox_templates": "",
|
||||||
|
"open_logs": "",
|
||||||
|
"private_comment": "",
|
||||||
|
"q_all": "",
|
||||||
|
"quarantine_category": "",
|
||||||
|
"recipient_map_new_info": "",
|
||||||
|
"sender": "",
|
||||||
|
"sieve_preset_4": "",
|
||||||
|
"sieve_preset_5": "",
|
||||||
|
"syncjob_last_run_result": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_TLS_FAILURE": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE": "",
|
||||||
|
"template": "",
|
||||||
|
"tls_policy_maps_enforced_tls": "",
|
||||||
|
"tls_policy_maps_info": "",
|
||||||
|
"weekly": "",
|
||||||
|
"yes": "",
|
||||||
|
"sogo_visible_n": "",
|
||||||
|
"add_alias_expand": "",
|
||||||
|
"alias_domain_alias_hint": "",
|
||||||
|
"alias_domain_backupmx": "",
|
||||||
|
"allow_from_smtp": "",
|
||||||
|
"allow_from_smtp_info": "",
|
||||||
|
"sogo_visible_y": "",
|
||||||
|
"syncjob_check_log": "",
|
||||||
|
"syncjob_EX_OK": "",
|
||||||
|
"sieve_preset_6": ""
|
||||||
},
|
},
|
||||||
"quarantine": {
|
"quarantine": {
|
||||||
"action": "Acció",
|
"action": "Acció",
|
||||||
@@ -408,15 +863,59 @@
|
|||||||
"subj": "Assumpte",
|
"subj": "Assumpte",
|
||||||
"text_from_html_content": "Contingut (a partir del HTML)",
|
"text_from_html_content": "Contingut (a partir del HTML)",
|
||||||
"text_plain_content": "Contingut (text/plain)",
|
"text_plain_content": "Contingut (text/plain)",
|
||||||
"toggle_all": "Marcar tots"
|
"toggle_all": "Marcar tots",
|
||||||
|
"deliver_inbox": "",
|
||||||
|
"spam_score": "",
|
||||||
|
"table_size": "",
|
||||||
|
"type": "",
|
||||||
|
"learn_spam_delete": "",
|
||||||
|
"confirm": "",
|
||||||
|
"confirm_delete": "",
|
||||||
|
"danger": "",
|
||||||
|
"disabled_by_config": "",
|
||||||
|
"spam": "",
|
||||||
|
"download_eml": "",
|
||||||
|
"high_danger": "",
|
||||||
|
"info": "",
|
||||||
|
"junk_folder": "",
|
||||||
|
"low_danger": "",
|
||||||
|
"medium_danger": "",
|
||||||
|
"neutral_danger": "",
|
||||||
|
"notified": "",
|
||||||
|
"qhandler_success": "",
|
||||||
|
"qinfo": "",
|
||||||
|
"quick_delete_link": "",
|
||||||
|
"quick_info_link": "",
|
||||||
|
"quick_release_link": "",
|
||||||
|
"rspamd_result": "",
|
||||||
|
"sender_header": "",
|
||||||
|
"settings_info": "",
|
||||||
|
"table_size_show_n": "",
|
||||||
|
"refresh": "",
|
||||||
|
"rejected": "",
|
||||||
|
"rewrite_subject": ""
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"queue_manager": "Queue Manager"
|
"queue_manager": "Queue Manager",
|
||||||
|
"delete": "",
|
||||||
|
"flush": "",
|
||||||
|
"info": "",
|
||||||
|
"legend": "",
|
||||||
|
"ays": "",
|
||||||
|
"hold_mail": "",
|
||||||
|
"hold_mail_legend": "",
|
||||||
|
"unhold_mail": "",
|
||||||
|
"unhold_mail_legend": "",
|
||||||
|
"deliver_mail": "",
|
||||||
|
"deliver_mail_legend": "",
|
||||||
|
"show_message": "",
|
||||||
|
"unban": ""
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"help": "Mostrar/Ocultar panell d'ajuda",
|
"help": "Mostrar/Ocultar panell d'ajuda",
|
||||||
"mailcow_apps_detail": "Tria una aplicació (de moment només SOGo) per a accedir als teus correus, calendari, contactes i més.",
|
"mailcow_apps_detail": "Tria una aplicació (de moment només SOGo) per a accedir als teus correus, calendari, contactes i més.",
|
||||||
"mailcow_panel_detail": "Els <b>administradors del domini</b> poden crear, modificar o esborrar bústies i àlies, configurar i obtenir informació detallada sobre els seus dominis<br>\r\n\tEls <b>usuaris d'e-mail</b> poden crear àlies temporals (spam àlies), canviar la seva contrasenya i la configuració del seu filtre anti-spam."
|
"mailcow_panel_detail": "Els <b>administradors del domini</b> poden crear, modificar o esborrar bústies i àlies, configurar i obtenir informació detallada sobre els seus dominis<br>\r\n\tEls <b>usuaris d'e-mail</b> poden crear àlies temporals (spam àlies), canviar la seva contrasenya i la configuració del seu filtre anti-spam.",
|
||||||
|
"imap_smtp_server_auth_info": ""
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"admin_modified": "Els canvis fets a l'administrador s'han desat",
|
"admin_modified": "Els canvis fets a l'administrador s'han desat",
|
||||||
@@ -453,7 +952,56 @@
|
|||||||
"resource_modified": "S'han desat els canvis fets al recurs %s",
|
"resource_modified": "S'han desat els canvis fets al recurs %s",
|
||||||
"resource_removed": "S'ha esborrat el recurs %s",
|
"resource_removed": "S'ha esborrat el recurs %s",
|
||||||
"ui_texts": "S'han desat els canvis als noms de App",
|
"ui_texts": "S'han desat els canvis als noms de App",
|
||||||
"upload_success": "El fitxer s'ha pujat"
|
"upload_success": "El fitxer s'ha pujat",
|
||||||
|
"template_removed": "",
|
||||||
|
"tls_policy_map_entry_deleted": "",
|
||||||
|
"app_passwd_added": "",
|
||||||
|
"bcc_saved": "",
|
||||||
|
"hash_deleted": "",
|
||||||
|
"ip_check_opt_in_modified": "",
|
||||||
|
"item_released": "",
|
||||||
|
"learned_ham": "",
|
||||||
|
"license_modified": "",
|
||||||
|
"logged_in_as": "",
|
||||||
|
"pushover_settings_edited": "",
|
||||||
|
"qlearn_spam": "",
|
||||||
|
"queue_command_success": "",
|
||||||
|
"recipient_map_entry_deleted": "",
|
||||||
|
"recipient_map_entry_saved": "",
|
||||||
|
"settings_map_removed": "",
|
||||||
|
"verified_fido2_login": "",
|
||||||
|
"global_filter_written": "",
|
||||||
|
"bcc_deleted": "",
|
||||||
|
"cors_headers_edited": "",
|
||||||
|
"db_init_complete": "",
|
||||||
|
"delete_filter": "",
|
||||||
|
"delete_filters": "",
|
||||||
|
"deleted_syncjob": "",
|
||||||
|
"deleted_syncjobs": "",
|
||||||
|
"domain_add_dkim_available": "",
|
||||||
|
"dkim_duplicated": "",
|
||||||
|
"sogo_profile_reset": "",
|
||||||
|
"template_modified": "",
|
||||||
|
"tls_policy_map_entry_saved": "",
|
||||||
|
"verified_webauthn_login": "",
|
||||||
|
"template_added": "",
|
||||||
|
"acl_saved": "",
|
||||||
|
"admin_added": "",
|
||||||
|
"admin_api_modified": "",
|
||||||
|
"admin_removed": "",
|
||||||
|
"app_passwd_removed": "",
|
||||||
|
"bcc_edited": "",
|
||||||
|
"domain_footer_modified": "",
|
||||||
|
"dovecot_restart_success": "",
|
||||||
|
"f2b_banlist_refreshed": "",
|
||||||
|
"rl_saved": "",
|
||||||
|
"rspamd_ui_pw_set": "",
|
||||||
|
"saved_settings": "",
|
||||||
|
"settings_map_added": "",
|
||||||
|
"verified_totp_login": "",
|
||||||
|
"verified_yotp_login": "",
|
||||||
|
"nginx_reloaded": "",
|
||||||
|
"password_policy_saved": ""
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
"api_register": "%s fa servir la Yubico Cloud API. Obté una API key per la teva clau <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">aquí</a>",
|
"api_register": "%s fa servir la Yubico Cloud API. Obté una API key per la teva clau <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">aquí</a>",
|
||||||
@@ -473,7 +1021,15 @@
|
|||||||
"webauthn": "Autenticació WebAuthn",
|
"webauthn": "Autenticació WebAuthn",
|
||||||
"waiting_usb_auth": "<i>Esperant el dispositiu USB...</i><br><br>Apreta el botó del teu dispositiu USB WebAuthn ara.",
|
"waiting_usb_auth": "<i>Esperant el dispositiu USB...</i><br><br>Apreta el botó del teu dispositiu USB WebAuthn ara.",
|
||||||
"waiting_usb_register": "<i>Esperant el dispositiu USB...</i><br><br>Posa el teu password i confirma el registre del teu WebAuthn apretant el botó del teu dispositiiu USB WebAuthn.",
|
"waiting_usb_register": "<i>Esperant el dispositiu USB...</i><br><br>Posa el teu password i confirma el registre del teu WebAuthn apretant el botó del teu dispositiiu USB WebAuthn.",
|
||||||
"yubi_otp": "Autenticació OTP de Yubico"
|
"yubi_otp": "Autenticació OTP de Yubico",
|
||||||
|
"reload_retry": "",
|
||||||
|
"authenticators": "",
|
||||||
|
"error_code": "",
|
||||||
|
"start_webauthn_validation": "",
|
||||||
|
"tfa_token_invalid": "",
|
||||||
|
"init_webauthn": "",
|
||||||
|
"u2f_deprecated": "",
|
||||||
|
"u2f_deprecated_important": ""
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"action": "Acció",
|
"action": "Acció",
|
||||||
@@ -556,6 +1112,198 @@
|
|||||||
"username": "Usuari",
|
"username": "Usuari",
|
||||||
"waiting": "Esperant",
|
"waiting": "Esperant",
|
||||||
"week": "Setmana",
|
"week": "Setmana",
|
||||||
"weeks": "Setmanes"
|
"weeks": "Setmanes",
|
||||||
|
"last_mail_login": "",
|
||||||
|
"last_pw_change": "",
|
||||||
|
"last_ui_login": "",
|
||||||
|
"login_history": "",
|
||||||
|
"open_logs": "",
|
||||||
|
"password": "",
|
||||||
|
"pushover_info": "",
|
||||||
|
"pushover_sender_array": "",
|
||||||
|
"app_hint": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"apple_connection_profile": "",
|
||||||
|
"apple_connection_profile_complete": "",
|
||||||
|
"apple_connection_profile_with_app_password": "",
|
||||||
|
"delete_ays": "",
|
||||||
|
"pushover_sound": "",
|
||||||
|
"years": "",
|
||||||
|
"daily": "",
|
||||||
|
"attribute": "",
|
||||||
|
"hourly": "",
|
||||||
|
"mailbox": "",
|
||||||
|
"mailbox_general": "",
|
||||||
|
"mailbox_settings": "",
|
||||||
|
"pushover_evaluate_x_prio": "",
|
||||||
|
"pushover_only_x_prio": "",
|
||||||
|
"q_add_header": "",
|
||||||
|
"quarantine_notification": "",
|
||||||
|
"sogo_profile_reset": "",
|
||||||
|
"app_passwds": "",
|
||||||
|
"created_on": "",
|
||||||
|
"change_password_hint_app_passwords": "",
|
||||||
|
"clear_recent_successful_connections": "",
|
||||||
|
"create_app_passwd": "",
|
||||||
|
"month": "",
|
||||||
|
"months": "",
|
||||||
|
"open_webmail_sso": "",
|
||||||
|
"password_repeat": "",
|
||||||
|
"pushover_text": "",
|
||||||
|
"quarantine_notification_info": "",
|
||||||
|
"recent_successful_connections": "",
|
||||||
|
"save": "",
|
||||||
|
"sender_acl_disabled": "",
|
||||||
|
"sogo_profile_reset_now": "",
|
||||||
|
"verify": "",
|
||||||
|
"syncjob_last_run_result": "",
|
||||||
|
"syncjob_EX_OK": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE": "",
|
||||||
|
"syncjob_check_log": "",
|
||||||
|
"title": "",
|
||||||
|
"apple_connection_profile_mailonly": "",
|
||||||
|
"expire_in": "",
|
||||||
|
"fido2_webauthn": "",
|
||||||
|
"pushover_sender_regex": "",
|
||||||
|
"pushover_title": "",
|
||||||
|
"pushover_vars": "",
|
||||||
|
"pushover_verify": "",
|
||||||
|
"q_all": "",
|
||||||
|
"quarantine_category": "",
|
||||||
|
"quarantine_category_info": "",
|
||||||
|
"sogo_profile_reset_help": "",
|
||||||
|
"spam_score_reset": "",
|
||||||
|
"syncjob_EXIT_TLS_FAILURE": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_OVERQUOTA": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "",
|
||||||
|
"value": "",
|
||||||
|
"with_app_password": "",
|
||||||
|
"year": "",
|
||||||
|
"from": "",
|
||||||
|
"text": "",
|
||||||
|
"q_reject": "",
|
||||||
|
"generate": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"app_name": "",
|
||||||
|
"direct_protocol_access": "",
|
||||||
|
"email": "",
|
||||||
|
"email_and_dav": "",
|
||||||
|
"empty": "",
|
||||||
|
"never": "",
|
||||||
|
"no_last_login": "",
|
||||||
|
"weekly": ""
|
||||||
|
},
|
||||||
|
"datatables": {
|
||||||
|
"decimal": "",
|
||||||
|
"infoPostFix": "",
|
||||||
|
"collapse_all": "",
|
||||||
|
"emptyTable": "",
|
||||||
|
"expand_all": "",
|
||||||
|
"info": "",
|
||||||
|
"infoEmpty": "",
|
||||||
|
"infoFiltered": "",
|
||||||
|
"lengthMenu": "",
|
||||||
|
"loadingRecords": "",
|
||||||
|
"processing": "",
|
||||||
|
"search": "",
|
||||||
|
"zeroRecords": "",
|
||||||
|
"paginate": {
|
||||||
|
"first": "",
|
||||||
|
"last": "",
|
||||||
|
"next": "",
|
||||||
|
"previous": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"sortAscending": "",
|
||||||
|
"sortDescending": ""
|
||||||
|
},
|
||||||
|
"thousands": ""
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"history_all_servers": "",
|
||||||
|
"architecture": "",
|
||||||
|
"containers_info": "",
|
||||||
|
"container_running": "",
|
||||||
|
"docs": "",
|
||||||
|
"login_time": "",
|
||||||
|
"logs": "",
|
||||||
|
"error_show_ip": "",
|
||||||
|
"external_logs": "",
|
||||||
|
"in_memory_logs": "",
|
||||||
|
"jvm_memory_solr": "",
|
||||||
|
"last_modified": "",
|
||||||
|
"log_info": "",
|
||||||
|
"memory": "",
|
||||||
|
"online_users": "",
|
||||||
|
"restart_container": "",
|
||||||
|
"show_ip": "",
|
||||||
|
"size": "",
|
||||||
|
"solr_status": "",
|
||||||
|
"started_at": "",
|
||||||
|
"started_on": "",
|
||||||
|
"static_logs": "",
|
||||||
|
"success": "",
|
||||||
|
"system_containers": "",
|
||||||
|
"timezone": "",
|
||||||
|
"uptime": "",
|
||||||
|
"update_available": "",
|
||||||
|
"no_update_available": "",
|
||||||
|
"chart_this_server": "",
|
||||||
|
"container_disabled": "",
|
||||||
|
"container_stopped": "",
|
||||||
|
"cores": "",
|
||||||
|
"current_time": "",
|
||||||
|
"disk_usage": "",
|
||||||
|
"solr_dead": "",
|
||||||
|
"update_failed": "",
|
||||||
|
"username": "",
|
||||||
|
"wip": "",
|
||||||
|
"service": ""
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"domain_added_sogo_failed": "",
|
||||||
|
"dovecot_restart_failed": "",
|
||||||
|
"fuzzy_learn_error": "",
|
||||||
|
"hash_not_found": "",
|
||||||
|
"no_active_admin": "",
|
||||||
|
"quota_exceeded_scope": "",
|
||||||
|
"cannot_delete_self": "",
|
||||||
|
"session_token": "",
|
||||||
|
"session_ua": "",
|
||||||
|
"ip_invalid": "",
|
||||||
|
"is_not_primary_alias": ""
|
||||||
|
},
|
||||||
|
"ratelimit": {
|
||||||
|
"hour": "",
|
||||||
|
"day": "",
|
||||||
|
"disabled": "",
|
||||||
|
"second": "",
|
||||||
|
"minute": ""
|
||||||
|
},
|
||||||
|
"oauth2": {
|
||||||
|
"permit": "",
|
||||||
|
"profile": "",
|
||||||
|
"scope_ask_permission": "",
|
||||||
|
"authorize_app": "",
|
||||||
|
"deny": "",
|
||||||
|
"profile_desc": "",
|
||||||
|
"access_denied": ""
|
||||||
|
},
|
||||||
|
"fido2": {
|
||||||
|
"confirm": "",
|
||||||
|
"fido2_auth": "",
|
||||||
|
"fido2_success": "",
|
||||||
|
"fido2_validation_failed": "",
|
||||||
|
"fn": "",
|
||||||
|
"known_ids": "",
|
||||||
|
"register_status": "",
|
||||||
|
"rename": "",
|
||||||
|
"set_fido2": "",
|
||||||
|
"set_fido2_touchid": "",
|
||||||
|
"set_fn": "",
|
||||||
|
"start_fido2_validation": "",
|
||||||
|
"none": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,8 @@
|
|||||||
"username": "Uživatelské jméno",
|
"username": "Uživatelské jméno",
|
||||||
"validate": "Ověřit",
|
"validate": "Ověřit",
|
||||||
"validation_success": "Úspěšně ověřeno",
|
"validation_success": "Úspěšně ověřeno",
|
||||||
"tags": "Štítky"
|
"tags": "Štítky",
|
||||||
|
"dry": "Simulovat synchronizaci"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"access": "Přístupy",
|
"access": "Přístupy",
|
||||||
@@ -209,7 +210,7 @@
|
|||||||
"include_exclude_info": "Ve výchozím nastavení (bez výběru), jsou adresovány <b>všechny mailové schránky</b>",
|
"include_exclude_info": "Ve výchozím nastavení (bez výběru), jsou adresovány <b>všechny mailové schránky</b>",
|
||||||
"includes": "Zahrnout tyto přijemce",
|
"includes": "Zahrnout tyto přijemce",
|
||||||
"ip_check": "Kontrola IP",
|
"ip_check": "Kontrola IP",
|
||||||
"ip_check_disabled": "Kontrola IP je vypnuta. Můžete ji zapnout v <br> <strong>System > Nastavení > Options > Přizpůsobení</strong>",
|
"ip_check_disabled": "Kontrola IP je zakázána. Můžete ji povolit v nabídce<br> <strong>Systém > Nastavení > Možnosti > Přizpůsobení</strong>",
|
||||||
"ip_check_opt_in": "Přihlásit se k používání služby třetí strany <strong>ipv4.mailcow.email</strong> a <strong>ipv6.mailcow.email</strong> pro zjištění externích IP adres.",
|
"ip_check_opt_in": "Přihlásit se k používání služby třetí strany <strong>ipv4.mailcow.email</strong> a <strong>ipv6.mailcow.email</strong> pro zjištění externích IP adres.",
|
||||||
"is_mx_based": "Na základě MX",
|
"is_mx_based": "Na základě MX",
|
||||||
"last_applied": "Naposledy použité",
|
"last_applied": "Naposledy použité",
|
||||||
@@ -218,7 +219,7 @@
|
|||||||
"loading": "Prosím čekejte...",
|
"loading": "Prosím čekejte...",
|
||||||
"login_time": "Čas přihlášení",
|
"login_time": "Čas přihlášení",
|
||||||
"logo_info": "Obrázek bude zmenšen na výšku 40 pixelů pro horní navigační lištu a na max. šířku 250 pixelů pro úvodní stránku.",
|
"logo_info": "Obrázek bude zmenšen na výšku 40 pixelů pro horní navigační lištu a na max. šířku 250 pixelů pro úvodní stránku.",
|
||||||
"lookup_mx": "Ověřit cíl proti MX záznamu (.outlook.com bude směrovat všechnu poštu pro MX *.outlook.com přes tento uzel)",
|
"lookup_mx": "Cíl je regulární výraz, který se porovná s názvem MX (<code>.*\\.google\\.com</code> pro směrování veškeré pošty cílené na MX, který končí na google.com přes tento skok)",
|
||||||
"main_name": "Název webu (\"mailcow UI\")",
|
"main_name": "Název webu (\"mailcow UI\")",
|
||||||
"merged_vars_hint": "Šedé řádky byly přidány z <code>vars.(local.)inc.php</code> a zde je nelze upravit.",
|
"merged_vars_hint": "Šedé řádky byly přidány z <code>vars.(local.)inc.php</code> a zde je nelze upravit.",
|
||||||
"message": "Zpráva",
|
"message": "Zpráva",
|
||||||
@@ -343,7 +344,14 @@
|
|||||||
"verify": "Ověřit",
|
"verify": "Ověřit",
|
||||||
"yes": "✓",
|
"yes": "✓",
|
||||||
"f2b_ban_time_increment": "Délka banu je prodlužována s každým dalším banem",
|
"f2b_ban_time_increment": "Délka banu je prodlužována s každým dalším banem",
|
||||||
"f2b_max_ban_time": "Maximální délka banu (s)"
|
"f2b_max_ban_time": "Maximální délka banu (s)",
|
||||||
|
"cors_settings": "Nastavení CORS",
|
||||||
|
"queue_unban": "zrušit ban",
|
||||||
|
"copy_to_clipboard": "",
|
||||||
|
"f2b_manage_external": "",
|
||||||
|
"f2b_manage_external_info": "",
|
||||||
|
"allowed_methods": "",
|
||||||
|
"allowed_origins": ""
|
||||||
},
|
},
|
||||||
"danger": {
|
"danger": {
|
||||||
"access_denied": "Přístup odepřen nebo jsou neplatná data ve formuláři",
|
"access_denied": "Přístup odepřen nebo jsou neplatná data ve formuláři",
|
||||||
@@ -465,7 +473,16 @@
|
|||||||
"username_invalid": "Uživatelské jméno %s nelze použít",
|
"username_invalid": "Uživatelské jméno %s nelze použít",
|
||||||
"validity_missing": "Zdejte dobu platnosti",
|
"validity_missing": "Zdejte dobu platnosti",
|
||||||
"value_missing": "Prosím, uveďte všechny hodnoty",
|
"value_missing": "Prosím, uveďte všechny hodnoty",
|
||||||
"yotp_verification_failed": "Yubico OTP ověření selhalo: %s"
|
"yotp_verification_failed": "Yubico OTP ověření selhalo: %s",
|
||||||
|
"webauthn_authenticator_failed": "Zvolený ověřovací prostředek nebyl nalezen",
|
||||||
|
"cors_invalid_method": "Zadaná neplatná metoda Allow-Method",
|
||||||
|
"cors_invalid_origin": "Zadán neplatný Allow-Origin",
|
||||||
|
"webauthn_publickey_failed": "Pro vybraný ověřovací prostředek nebyl uložen žádný veřejný klíč",
|
||||||
|
"webauthn_username_failed": "Zvolený ověřovací prostředek patří k jinému účtu",
|
||||||
|
"extended_sender_acl_denied": "chybějící ACL pro nastavení externích adres odesílatele",
|
||||||
|
"demo_mode_enabled": "Demo režim je zapnutý",
|
||||||
|
"img_dimensions_exceeded": "",
|
||||||
|
"img_size_exceeded": ""
|
||||||
},
|
},
|
||||||
"datatables": {
|
"datatables": {
|
||||||
"emptyTable": "Tabulka neobsahuje žádná data",
|
"emptyTable": "Tabulka neobsahuje žádná data",
|
||||||
@@ -488,7 +505,10 @@
|
|||||||
"processing": "Zpracovávání...",
|
"processing": "Zpracovávání...",
|
||||||
"search": "Vyhledávání:",
|
"search": "Vyhledávání:",
|
||||||
"decimal": ",",
|
"decimal": ",",
|
||||||
"thousands": " "
|
"thousands": " ",
|
||||||
|
"collapse_all": "Sbalit vše",
|
||||||
|
"expand_all": "Rozbalit vše",
|
||||||
|
"infoPostFix": ""
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"chart_this_server": "Graf (tento server)",
|
"chart_this_server": "Graf (tento server)",
|
||||||
@@ -515,14 +535,28 @@
|
|||||||
"success": "Úspěch",
|
"success": "Úspěch",
|
||||||
"system_containers": "Systém a kontejnery",
|
"system_containers": "Systém a kontejnery",
|
||||||
"uptime": "Doba běhu",
|
"uptime": "Doba běhu",
|
||||||
"username": "Uživatelské meno"
|
"username": "Uživatelské meno",
|
||||||
|
"architecture": "Architektura",
|
||||||
|
"error_show_ip": "Nepodařilo se přeložit veřejné IP adresy",
|
||||||
|
"show_ip": "Zobrazit veřejné IP adresy",
|
||||||
|
"container_running": "Běží",
|
||||||
|
"container_stopped": "Zastaven",
|
||||||
|
"current_time": "Systémový čas",
|
||||||
|
"timezone": "Časové pásmo",
|
||||||
|
"update_available": "K dispozici je aktualizace",
|
||||||
|
"no_update_available": "Systém je na nejnovější verzi",
|
||||||
|
"update_failed": "Nepodařilo se zkontrolovat aktualizace",
|
||||||
|
"wip": "Nedokončená vývojová verze",
|
||||||
|
"memory": "Paměť",
|
||||||
|
"container_disabled": "Kontejner je zastaven nebo zakázán",
|
||||||
|
"cores": ""
|
||||||
},
|
},
|
||||||
"diagnostics": {
|
"diagnostics": {
|
||||||
"cname_from_a": "Hodnota odvozena z A/AAAA záznamu. Lze použít, pokud záznam ukazuje na správný zdroj.",
|
"cname_from_a": "Hodnota odvozena z A/AAAA záznamu. Lze použít, pokud záznam ukazuje na správný zdroj.",
|
||||||
"dns_records": "DNS záznamy",
|
"dns_records": "DNS záznamy",
|
||||||
"dns_records_24hours": "Upozornění: Změnám v systému DNS může trvat až 24 hodin, než se zde správně zobrazí jejich aktuální stav. Můžete zde snadno zjistit, jak nastavit DNS záznamy a zda jsou všechny záznamy správně uloženy.",
|
"dns_records_24hours": "Upozornění: Změnám v systému DNS může trvat až 24 hodin, než se zde správně zobrazí jejich aktuální stav. Můžete zde snadno zjistit, jak nastavit DNS záznamy a zda jsou všechny záznamy správně uloženy.",
|
||||||
"dns_records_data": "Správný záznam",
|
"dns_records_data": "Správný záznam",
|
||||||
"dns_records_docs": "Přečtěte si prosím <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">dokumentaci</a>.",
|
"dns_records_docs": "Přečtěte si prosím <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">dokumentaci</a>.",
|
||||||
"dns_records_name": "Název",
|
"dns_records_name": "Název",
|
||||||
"dns_records_status": "Současný stav",
|
"dns_records_status": "Současný stav",
|
||||||
"dns_records_type": "Typ",
|
"dns_records_type": "Typ",
|
||||||
@@ -640,7 +674,24 @@
|
|||||||
"title": "Úprava objektu",
|
"title": "Úprava objektu",
|
||||||
"unchanged_if_empty": "Pokud se nemění, ponechte prázdné",
|
"unchanged_if_empty": "Pokud se nemění, ponechte prázdné",
|
||||||
"username": "Uživatelské jméno",
|
"username": "Uživatelské jméno",
|
||||||
"validate_save": "Ověřit a uložit"
|
"validate_save": "Ověřit a uložit",
|
||||||
|
"domain_footer_info": "Patičky pro celou doménu se přidávají ke všem odchozím e-mailům spojeným s adresou v rámci této domény. <br> Pro patičku lze použít následující proměnné:",
|
||||||
|
"domain_footer_info_vars": {
|
||||||
|
"from_name": "{= from_name =} - Jméno odesílatele, např. pro \"Mailcow <moo@mailcow.tld>\" vrátí \"Mailcow\"",
|
||||||
|
"auth_user": "{= auth_user =} - Ověřené uživatelské jméno zadané MTA",
|
||||||
|
"from_user": "{= from_user =} - uživatelská část odesílatele, např. pro \"moo@mailcow.tld\" vrátí \"moo\"",
|
||||||
|
"from_domain": "{= from_domain =} - Doména odesílatele",
|
||||||
|
"from_addr": "{= from_addr =} - E-mailová adresa odesílatele",
|
||||||
|
"custom": ""
|
||||||
|
},
|
||||||
|
"domain_footer": "Patička pro celou doménu",
|
||||||
|
"domain_footer_html": "HTML text",
|
||||||
|
"domain_footer_plain": "Prostý text",
|
||||||
|
"pushover_sound": "Zvukové upozornění",
|
||||||
|
"footer_exclude": "",
|
||||||
|
"custom_attributes": "",
|
||||||
|
"domain_footer_skip_replies": "",
|
||||||
|
"pushover": ""
|
||||||
},
|
},
|
||||||
"fido2": {
|
"fido2": {
|
||||||
"confirm": "Potvrdit",
|
"confirm": "Potvrdit",
|
||||||
@@ -870,7 +921,8 @@
|
|||||||
"username": "Uživatelské jméno",
|
"username": "Uživatelské jméno",
|
||||||
"waiting": "Čekání",
|
"waiting": "Čekání",
|
||||||
"weekly": "Každý týden",
|
"weekly": "Každý týden",
|
||||||
"yes": "✓"
|
"yes": "✓",
|
||||||
|
"relay_unknown": "Předávání neexistujících schránek"
|
||||||
},
|
},
|
||||||
"oauth2": {
|
"oauth2": {
|
||||||
"access_denied": "K udělení přístupu se přihlašte jako vlastník mailové schránky.",
|
"access_denied": "K udělení přístupu se přihlašte jako vlastník mailové schránky.",
|
||||||
@@ -935,7 +987,20 @@
|
|||||||
"type": "Typ"
|
"type": "Typ"
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"queue_manager": "Správce fronty"
|
"queue_manager": "Správce fronty",
|
||||||
|
"delete": "Vymazat vše",
|
||||||
|
"info": "Poštovní fronta obsahuje všechny e-maily, které čekají na doručení. Pokud e-mail uvízne v poštovní frontě na delší dobu, systém jej automaticky odstraní.<br>Chybové hlášení příslušného e-mailu poskytuje informace o tom, proč se e-mail nepodařilo doručit.",
|
||||||
|
"flush": "Vyprázdnit frontu",
|
||||||
|
"legend": "Funkce operací poštovní fronty:",
|
||||||
|
"ays": "Potvrďte, že chcete opravdu odstranit všechny položky z aktuální fronty.",
|
||||||
|
"deliver_mail": "Doručit",
|
||||||
|
"deliver_mail_legend": "Opětovný pokus o doručení vybraných e-mailů.",
|
||||||
|
"hold_mail": "Podržet",
|
||||||
|
"hold_mail_legend": "Podrží vybrané e-maily. (Zabrání dalším pokusům o doručení)",
|
||||||
|
"show_message": "Zobrazit zprávu",
|
||||||
|
"unhold_mail": "Uvolnit",
|
||||||
|
"unhold_mail_legend": "Uvolnit vybrané e-maily k doručení. (Pouze v případě předchozího podržení)",
|
||||||
|
"unban": ""
|
||||||
},
|
},
|
||||||
"ratelimit": {
|
"ratelimit": {
|
||||||
"disabled": "Vypnuto",
|
"disabled": "Vypnuto",
|
||||||
@@ -1029,7 +1094,12 @@
|
|||||||
"verified_fido2_login": "Ověřené FIDO2 přihlášení",
|
"verified_fido2_login": "Ověřené FIDO2 přihlášení",
|
||||||
"verified_totp_login": "TOTP přihlášení ověřeno",
|
"verified_totp_login": "TOTP přihlášení ověřeno",
|
||||||
"verified_webauthn_login": "WebAuthn přihlášení ověřeno",
|
"verified_webauthn_login": "WebAuthn přihlášení ověřeno",
|
||||||
"verified_yotp_login": "Yubico OTP přihlášení ověřeno"
|
"verified_yotp_login": "Yubico OTP přihlášení ověřeno",
|
||||||
|
"cors_headers_edited": "Nastavení CORS byla uložena",
|
||||||
|
"domain_footer_modified": "Změny patičky domény %s byly uloženy",
|
||||||
|
"f2b_banlist_refreshed": "",
|
||||||
|
"domain_add_dkim_available": "",
|
||||||
|
"ip_check_opt_in_modified": ""
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
"api_register": "%s používá Yubico Cloud API. Prosím získejte API klíč pro své Yubico <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">ZDE</a>",
|
"api_register": "%s používá Yubico Cloud API. Prosím získejte API klíč pro své Yubico <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">ZDE</a>",
|
||||||
@@ -1054,7 +1124,10 @@
|
|||||||
"webauthn": "WebAuthn ověření",
|
"webauthn": "WebAuthn ověření",
|
||||||
"waiting_usb_auth": "<i>Čeká se na USB zařízení...</i><br><br>Prosím stiskněte tlačítko na svém WebAuthn USB zařízení.",
|
"waiting_usb_auth": "<i>Čeká se na USB zařízení...</i><br><br>Prosím stiskněte tlačítko na svém WebAuthn USB zařízení.",
|
||||||
"waiting_usb_register": "<i>Čeká se na USB zařízení...</i><br><br>Prosím zadejte své heslo výše a potvrďte WebAuthn registraci stiskem tlačítka na svém WebAuthn USB zařízení.",
|
"waiting_usb_register": "<i>Čeká se na USB zařízení...</i><br><br>Prosím zadejte své heslo výše a potvrďte WebAuthn registraci stiskem tlačítka na svém WebAuthn USB zařízení.",
|
||||||
"yubi_otp": "Yubico OTP ověření"
|
"yubi_otp": "Yubico OTP ověření",
|
||||||
|
"authenticators": "",
|
||||||
|
"u2f_deprecated": "",
|
||||||
|
"u2f_deprecated_important": ""
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"action": "Akce",
|
"action": "Akce",
|
||||||
@@ -1215,7 +1288,10 @@
|
|||||||
"weeks": "týdny",
|
"weeks": "týdny",
|
||||||
"with_app_password": "s heslem aplikace",
|
"with_app_password": "s heslem aplikace",
|
||||||
"year": "rok",
|
"year": "rok",
|
||||||
"years": "let"
|
"years": "let",
|
||||||
|
"pushover_sound": "Zvukové upozornění",
|
||||||
|
"attribute": "",
|
||||||
|
"value": ""
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"cannot_delete_self": "Nelze smazat právě přihlášeného uživatele",
|
"cannot_delete_self": "Nelze smazat právě přihlášeného uživatele",
|
||||||
|
|||||||
+233
-18
@@ -107,7 +107,8 @@
|
|||||||
"validation_success": "Valideret med succes",
|
"validation_success": "Valideret med succes",
|
||||||
"bcc_dest_format": "BCC-destination skal være en enkelt gyldig e-mail-adresse.<br>Hvis du har brug for at sende en kopi til flere adresser, kan du oprette et alias og bruge det her.",
|
"bcc_dest_format": "BCC-destination skal være en enkelt gyldig e-mail-adresse.<br>Hvis du har brug for at sende en kopi til flere adresser, kan du oprette et alias og bruge det her.",
|
||||||
"app_passwd_protocols": "Tilladte protokoller for app adgangskode",
|
"app_passwd_protocols": "Tilladte protokoller for app adgangskode",
|
||||||
"tags": "Tag's"
|
"tags": "Tag's",
|
||||||
|
"dry": ""
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"access": "Adgang",
|
"access": "Adgang",
|
||||||
@@ -317,7 +318,40 @@
|
|||||||
"yes": "✓",
|
"yes": "✓",
|
||||||
"ip_check_opt_in": "Opt-In for brug af tredjepartstjeneste <strong>ipv4.mailcow.email</strong> og <strong>ipv6.mailcow.email</strong> til at finde eksterne IP-adresser.",
|
"ip_check_opt_in": "Opt-In for brug af tredjepartstjeneste <strong>ipv4.mailcow.email</strong> og <strong>ipv6.mailcow.email</strong> til at finde eksterne IP-adresser.",
|
||||||
"queue_unban": "unban",
|
"queue_unban": "unban",
|
||||||
"admins": "Administratorer"
|
"admins": "Administratorer",
|
||||||
|
"copy_to_clipboard": "",
|
||||||
|
"f2b_ban_time_increment": "",
|
||||||
|
"f2b_manage_external": "",
|
||||||
|
"f2b_manage_external_info": "",
|
||||||
|
"logo_normal_label": "",
|
||||||
|
"logo_dark_label": "",
|
||||||
|
"f2b_max_ban_time": "",
|
||||||
|
"cors_settings": "",
|
||||||
|
"allowed_origins": "",
|
||||||
|
"allowed_methods": "",
|
||||||
|
"options": "",
|
||||||
|
"password_length": "",
|
||||||
|
"password_policy": "",
|
||||||
|
"password_policy_chars": "",
|
||||||
|
"password_policy_length": "",
|
||||||
|
"ip_check": "",
|
||||||
|
"login_time": "",
|
||||||
|
"oauth2_apps": "",
|
||||||
|
"oauth2_add_client": "",
|
||||||
|
"password_policy_lowerupper": "",
|
||||||
|
"password_policy_numbers": "",
|
||||||
|
"password_policy_special_chars": "",
|
||||||
|
"relay_rcpt": "",
|
||||||
|
"service": "",
|
||||||
|
"transport_test_rcpt_info": "",
|
||||||
|
"ip_check_disabled": "",
|
||||||
|
"rsettings_preset_4": "",
|
||||||
|
"convert_html_to_text": "",
|
||||||
|
"success": "",
|
||||||
|
"is_mx_based": "",
|
||||||
|
"admins_ldap": "",
|
||||||
|
"api_read_only": "",
|
||||||
|
"api_read_write": ""
|
||||||
},
|
},
|
||||||
"danger": {
|
"danger": {
|
||||||
"access_denied": "Adgang nægtet eller ugyldig formular data",
|
"access_denied": "Adgang nægtet eller ugyldig formular data",
|
||||||
@@ -435,7 +469,20 @@
|
|||||||
"validity_missing": "Tildel venligst en gyldighedsperiode",
|
"validity_missing": "Tildel venligst en gyldighedsperiode",
|
||||||
"value_missing": "Angiv alle værdier",
|
"value_missing": "Angiv alle værdier",
|
||||||
"yotp_verification_failed": "Yubico OTP verifikationen mislykkedes: %s",
|
"yotp_verification_failed": "Yubico OTP verifikationen mislykkedes: %s",
|
||||||
"webauthn_publickey_failed": "Der er ikke gemt nogen offentlig nøgle for den valgte autentifikator"
|
"webauthn_publickey_failed": "Der er ikke gemt nogen offentlig nøgle for den valgte autentifikator",
|
||||||
|
"cors_invalid_method": "",
|
||||||
|
"cors_invalid_origin": "",
|
||||||
|
"img_dimensions_exceeded": "",
|
||||||
|
"img_size_exceeded": "",
|
||||||
|
"webauthn_authenticator_failed": "",
|
||||||
|
"webauthn_username_failed": "",
|
||||||
|
"nginx_reload_failed": "",
|
||||||
|
"template_exists": "",
|
||||||
|
"extended_sender_acl_denied": "",
|
||||||
|
"extra_acl_invalid_domain": "",
|
||||||
|
"template_id_invalid": "",
|
||||||
|
"template_name_invalid": "",
|
||||||
|
"demo_mode_enabled": ""
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"chart_this_server": "Diagram (denne server)",
|
"chart_this_server": "Diagram (denne server)",
|
||||||
@@ -453,13 +500,36 @@
|
|||||||
"started_on": "Startede den",
|
"started_on": "Startede den",
|
||||||
"static_logs": "Statiske logfiler",
|
"static_logs": "Statiske logfiler",
|
||||||
"system_containers": "System og Beholdere",
|
"system_containers": "System og Beholdere",
|
||||||
"error_show_ip": "Kunne ikke finde de offentlige IP-adresser"
|
"error_show_ip": "Kunne ikke finde de offentlige IP-adresser",
|
||||||
|
"update_available": "",
|
||||||
|
"username": "",
|
||||||
|
"timezone": "",
|
||||||
|
"started_at": "",
|
||||||
|
"success": "",
|
||||||
|
"uptime": "",
|
||||||
|
"architecture": "",
|
||||||
|
"container_running": "",
|
||||||
|
"cores": "",
|
||||||
|
"current_time": "",
|
||||||
|
"docs": "",
|
||||||
|
"last_modified": "",
|
||||||
|
"login_time": "",
|
||||||
|
"memory": "",
|
||||||
|
"show_ip": "",
|
||||||
|
"size": "",
|
||||||
|
"no_update_available": "",
|
||||||
|
"update_failed": "",
|
||||||
|
"wip": "",
|
||||||
|
"online_users": "",
|
||||||
|
"service": "",
|
||||||
|
"container_disabled": "",
|
||||||
|
"container_stopped": ""
|
||||||
},
|
},
|
||||||
"diagnostics": {
|
"diagnostics": {
|
||||||
"cname_from_a": "Værdi afledt af A / AAAA-post. Dette understøttes, så længe posten peger på den korrekte ressource.",
|
"cname_from_a": "Værdi afledt af A / AAAA-post. Dette understøttes, så længe posten peger på den korrekte ressource.",
|
||||||
"dns_records": "DNS-poster",
|
"dns_records": "DNS-poster",
|
||||||
"dns_records_24hours": "Bemærk, at ændringer, der foretages i DNS, kan tage op til 24 timer for at få deres aktuelle status korrekt reflekteret på denne side. Det er beregnet som en måde for dig let at se, hvordan du konfigurerer dine DNS-poster og kontrollere, om alle dine poster er korrekt gemt i DNS.",
|
"dns_records_24hours": "Bemærk, at ændringer, der foretages i DNS, kan tage op til 24 timer for at få deres aktuelle status korrekt reflekteret på denne side. Det er beregnet som en måde for dig let at se, hvordan du konfigurerer dine DNS-poster og kontrollere, om alle dine poster er korrekt gemt i DNS.",
|
||||||
"dns_records_docs": "Se også <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">dokumentationen</a>.",
|
"dns_records_docs": "Se også <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">dokumentationen</a>.",
|
||||||
"dns_records_data": "Korrekte data",
|
"dns_records_data": "Korrekte data",
|
||||||
"dns_records_name": "Navn",
|
"dns_records_name": "Navn",
|
||||||
"dns_records_status": "Nuværende tilstand",
|
"dns_records_status": "Nuværende tilstand",
|
||||||
@@ -568,7 +638,34 @@
|
|||||||
"admin": "Rediger administrator",
|
"admin": "Rediger administrator",
|
||||||
"lookup_mx": "Destination er et regulært udtryk, der matcher MX-navnet (<code>.*google\\.dk</code> for at dirigere al e-mail, der er målrettet til en MX, der ender på google.dk, over dette hop)",
|
"lookup_mx": "Destination er et regulært udtryk, der matcher MX-navnet (<code>.*google\\.dk</code> for at dirigere al e-mail, der er målrettet til en MX, der ender på google.dk, over dette hop)",
|
||||||
"mailbox_relayhost_info": "Anvendt på postkassen og kun direkte aliasser, og overskriver et domæne relæ-host.",
|
"mailbox_relayhost_info": "Anvendt på postkassen og kun direkte aliasser, og overskriver et domæne relæ-host.",
|
||||||
"quota_warning_bcc": "Kvoteadvarsel BCC"
|
"quota_warning_bcc": "Kvoteadvarsel BCC",
|
||||||
|
"footer_exclude": "",
|
||||||
|
"custom_attributes": "",
|
||||||
|
"last_modified": "",
|
||||||
|
"app_passwd_protocols": "",
|
||||||
|
"created_on": "",
|
||||||
|
"domain_footer_info_vars": {
|
||||||
|
"custom": "",
|
||||||
|
"auth_user": "",
|
||||||
|
"from_user": "",
|
||||||
|
"from_name": "",
|
||||||
|
"from_addr": "",
|
||||||
|
"from_domain": ""
|
||||||
|
},
|
||||||
|
"domain_footer_skip_replies": "",
|
||||||
|
"spam_filter": "",
|
||||||
|
"domain_footer": "",
|
||||||
|
"domain_footer_html": "",
|
||||||
|
"domain_footer_info": "",
|
||||||
|
"domain_footer_plain": "",
|
||||||
|
"sogo_access_info": "",
|
||||||
|
"acl": "",
|
||||||
|
"none_inherit": "",
|
||||||
|
"pushover": "",
|
||||||
|
"pushover_sound": "",
|
||||||
|
"quota_warning_bcc_info": "",
|
||||||
|
"ratelimit": "",
|
||||||
|
"sogo_access": ""
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"cancel": "Afbestille",
|
"cancel": "Afbestille",
|
||||||
@@ -581,7 +678,9 @@
|
|||||||
"restart_container": "Genstart beholderen",
|
"restart_container": "Genstart beholderen",
|
||||||
"restart_container_info": "<b>Vigtig:</b> Det kan tage et stykke tid at gennemføre en yndefuld genstart. Vent til den er færdig.",
|
"restart_container_info": "<b>Vigtig:</b> Det kan tage et stykke tid at gennemføre en yndefuld genstart. Vent til den er færdig.",
|
||||||
"restart_now": "Genstart nu",
|
"restart_now": "Genstart nu",
|
||||||
"restarting_container": "Genstart af beholder, det kan tage et stykke tid"
|
"restarting_container": "Genstart af beholder, det kan tage et stykke tid",
|
||||||
|
"hibp_check": "",
|
||||||
|
"nothing_selected": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"administration": "Konfiguration og detailer",
|
"administration": "Konfiguration og detailer",
|
||||||
@@ -592,7 +691,8 @@
|
|||||||
"quarantine": "Karantæne",
|
"quarantine": "Karantæne",
|
||||||
"restart_netfilter": "Genstart netfilter",
|
"restart_netfilter": "Genstart netfilter",
|
||||||
"restart_sogo": "Genstart SOGo",
|
"restart_sogo": "Genstart SOGo",
|
||||||
"user_settings": "Brugerindstillinger"
|
"user_settings": "Brugerindstillinger",
|
||||||
|
"mailcow_system": ""
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"awaiting_tfa_confirmation": "Venter på TFA-bekræftelse",
|
"awaiting_tfa_confirmation": "Venter på TFA-bekræftelse",
|
||||||
@@ -757,7 +857,31 @@
|
|||||||
"yes": "✓",
|
"yes": "✓",
|
||||||
"goto_ham": "Lær som <b>ønsket</b>",
|
"goto_ham": "Lær som <b>ønsket</b>",
|
||||||
"catch_all": "Fang-alt",
|
"catch_all": "Fang-alt",
|
||||||
"open_logs": "Åben logfiler"
|
"open_logs": "Åben logfiler",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_OVERQUOTA": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "",
|
||||||
|
"all_domains": "",
|
||||||
|
"domain_templates": "",
|
||||||
|
"last_modified": "",
|
||||||
|
"last_pw_change": "",
|
||||||
|
"mailbox_templates": "",
|
||||||
|
"template": "",
|
||||||
|
"relay_unknown": "",
|
||||||
|
"add_alias_expand": "",
|
||||||
|
"add_template": "",
|
||||||
|
"created_on": "",
|
||||||
|
"goto_spam": "",
|
||||||
|
"recipient": "",
|
||||||
|
"relay_all": "",
|
||||||
|
"sender": "",
|
||||||
|
"syncjob_check_log": "",
|
||||||
|
"syncjob_last_run_result": "",
|
||||||
|
"syncjob_EX_OK": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_TLS_FAILURE": "",
|
||||||
|
"templates": ""
|
||||||
},
|
},
|
||||||
"oauth2": {
|
"oauth2": {
|
||||||
"access_denied": "Log ind som mailboks ejer for at give adgang via OAuth2.",
|
"access_denied": "Log ind som mailboks ejer for at give adgang via OAuth2.",
|
||||||
@@ -818,10 +942,24 @@
|
|||||||
"table_size_show_n": "Vis %s genstande",
|
"table_size_show_n": "Vis %s genstande",
|
||||||
"text_from_html_content": "Indhold (konverterede html)",
|
"text_from_html_content": "Indhold (konverterede html)",
|
||||||
"text_plain_content": "Indhold (text/plain)",
|
"text_plain_content": "Indhold (text/plain)",
|
||||||
"toggle_all": "Skift alt"
|
"toggle_all": "Skift alt",
|
||||||
|
"settings_info": ""
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"queue_manager": "Køadministrator"
|
"queue_manager": "Køadministrator",
|
||||||
|
"deliver_mail": "",
|
||||||
|
"delete": "",
|
||||||
|
"flush": "",
|
||||||
|
"info": "",
|
||||||
|
"legend": "",
|
||||||
|
"ays": "",
|
||||||
|
"deliver_mail_legend": "",
|
||||||
|
"hold_mail": "",
|
||||||
|
"hold_mail_legend": "",
|
||||||
|
"show_message": "",
|
||||||
|
"unban": "",
|
||||||
|
"unhold_mail": "",
|
||||||
|
"unhold_mail_legend": ""
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"help": "Vis / skjul hjælpepanel",
|
"help": "Vis / skjul hjælpepanel",
|
||||||
@@ -903,7 +1041,17 @@
|
|||||||
"verified_totp_login": "Bekræftet TOTP-login",
|
"verified_totp_login": "Bekræftet TOTP-login",
|
||||||
"verified_webauthn_login": "Bekræftet WebAuthn-login",
|
"verified_webauthn_login": "Bekræftet WebAuthn-login",
|
||||||
"verified_fido2_login": "Bekræftet FIDO2-login",
|
"verified_fido2_login": "Bekræftet FIDO2-login",
|
||||||
"verified_yotp_login": "Bekræftet Yubico OTP-login"
|
"verified_yotp_login": "Bekræftet Yubico OTP-login",
|
||||||
|
"template_removed": "",
|
||||||
|
"domain_footer_modified": "",
|
||||||
|
"cors_headers_edited": "",
|
||||||
|
"domain_add_dkim_available": "",
|
||||||
|
"f2b_banlist_refreshed": "",
|
||||||
|
"ip_check_opt_in_modified": "",
|
||||||
|
"nginx_reloaded": "",
|
||||||
|
"password_policy_saved": "",
|
||||||
|
"template_added": "",
|
||||||
|
"template_modified": ""
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
"api_register": "%s bruger Yubico Cloud API. Få en API-nøgle til din nøgle <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",
|
"api_register": "%s bruger Yubico Cloud API. Få en API-nøgle til din nøgle <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",
|
||||||
@@ -928,7 +1076,10 @@
|
|||||||
"webauthn": "WebAuthn godkendelse",
|
"webauthn": "WebAuthn godkendelse",
|
||||||
"waiting_usb_auth": "<i>Venter på USB-enhed...</i><br><br>Tryk let på knappen på din USB-enhed nu.",
|
"waiting_usb_auth": "<i>Venter på USB-enhed...</i><br><br>Tryk let på knappen på din USB-enhed nu.",
|
||||||
"waiting_usb_register": "<i>Venter på USB-enhed...</i><br><br>Indtast din adgangskode ovenfor, og bekræft din registrering ved at trykke på knappen på din USB-enhed.",
|
"waiting_usb_register": "<i>Venter på USB-enhed...</i><br><br>Indtast din adgangskode ovenfor, og bekræft din registrering ved at trykke på knappen på din USB-enhed.",
|
||||||
"yubi_otp": "Yubico OTP godkendelse"
|
"yubi_otp": "Yubico OTP godkendelse",
|
||||||
|
"authenticators": "",
|
||||||
|
"u2f_deprecated": "",
|
||||||
|
"u2f_deprecated_important": ""
|
||||||
},
|
},
|
||||||
"fido2": {
|
"fido2": {
|
||||||
"set_fn": "Set venneligt navn",
|
"set_fn": "Set venneligt navn",
|
||||||
@@ -942,7 +1093,8 @@
|
|||||||
"start_fido2_validation": "Start FIDO2 validering",
|
"start_fido2_validation": "Start FIDO2 validering",
|
||||||
"fido2_auth": "Login med FIDO2",
|
"fido2_auth": "Login med FIDO2",
|
||||||
"fido2_success": "Enheden blev registreret",
|
"fido2_success": "Enheden blev registreret",
|
||||||
"fido2_validation_failed": "Validering mislykkedes"
|
"fido2_validation_failed": "Validering mislykkedes",
|
||||||
|
"set_fido2_touchid": ""
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"action": "Handling",
|
"action": "Handling",
|
||||||
@@ -1071,7 +1223,42 @@
|
|||||||
"waiting": "Venter",
|
"waiting": "Venter",
|
||||||
"week": "uge",
|
"week": "uge",
|
||||||
"weekly": "Ugeligt",
|
"weekly": "Ugeligt",
|
||||||
"weeks": "uger"
|
"weeks": "uger",
|
||||||
|
"empty": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"login_history": "",
|
||||||
|
"mailbox": "",
|
||||||
|
"mailbox_general": "",
|
||||||
|
"syncjob_EXIT_TLS_FAILURE": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_OVERQUOTA": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "",
|
||||||
|
"syncjob_check_log": "",
|
||||||
|
"syncjob_last_run_result": "",
|
||||||
|
"syncjob_EX_OK": "",
|
||||||
|
"apple_connection_profile_with_app_password": "",
|
||||||
|
"attribute": "",
|
||||||
|
"change_password_hint_app_passwords": "",
|
||||||
|
"clear_recent_successful_connections": "",
|
||||||
|
"created_on": "",
|
||||||
|
"direct_protocol_access": "",
|
||||||
|
"fido2_webauthn": "",
|
||||||
|
"from": "",
|
||||||
|
"last_pw_change": "",
|
||||||
|
"last_ui_login": "",
|
||||||
|
"month": "",
|
||||||
|
"months": "",
|
||||||
|
"open_logs": "",
|
||||||
|
"open_webmail_sso": "",
|
||||||
|
"pushover_sound": "",
|
||||||
|
"recent_successful_connections": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE": "",
|
||||||
|
"value": "",
|
||||||
|
"with_app_password": "",
|
||||||
|
"year": "",
|
||||||
|
"years": "",
|
||||||
|
"mailbox_settings": ""
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"cannot_delete_self": "Kan ikke slette en bruger som er logget ind.",
|
"cannot_delete_self": "Kan ikke slette en bruger som er logget ind.",
|
||||||
@@ -1083,12 +1270,40 @@
|
|||||||
"no_active_admin": "Kan ikke deaktivere den sidste administrator",
|
"no_active_admin": "Kan ikke deaktivere den sidste administrator",
|
||||||
"quota_exceeded_scope": "Domænekvote overskredet: Kun ubegrænsede postkasser kan oprettes i dette domæneomfang.",
|
"quota_exceeded_scope": "Domænekvote overskredet: Kun ubegrænsede postkasser kan oprettes i dette domæneomfang.",
|
||||||
"session_token": "Form nøgle ugyldig: Nøgle passer ikke",
|
"session_token": "Form nøgle ugyldig: Nøgle passer ikke",
|
||||||
"session_ua": "Form nøgle ugyldig: Bruger-Agent gyldighedskontrols fejl"
|
"session_ua": "Form nøgle ugyldig: Bruger-Agent gyldighedskontrols fejl",
|
||||||
|
"is_not_primary_alias": ""
|
||||||
},
|
},
|
||||||
"datatables": {
|
"datatables": {
|
||||||
"lengthMenu": "Vis _MENU_ poster",
|
"lengthMenu": "Vis _MENU_ poster",
|
||||||
"paginate": {
|
"paginate": {
|
||||||
"first": "Først"
|
"first": "Først",
|
||||||
}
|
"last": "",
|
||||||
|
"next": "",
|
||||||
|
"previous": ""
|
||||||
|
},
|
||||||
|
"decimal": "",
|
||||||
|
"infoPostFix": "",
|
||||||
|
"loadingRecords": "",
|
||||||
|
"processing": "",
|
||||||
|
"search": "",
|
||||||
|
"zeroRecords": "",
|
||||||
|
"thousands": "",
|
||||||
|
"emptyTable": "",
|
||||||
|
"expand_all": "",
|
||||||
|
"info": "",
|
||||||
|
"infoEmpty": "",
|
||||||
|
"infoFiltered": "",
|
||||||
|
"aria": {
|
||||||
|
"sortAscending": "",
|
||||||
|
"sortDescending": ""
|
||||||
|
},
|
||||||
|
"collapse_all": ""
|
||||||
|
},
|
||||||
|
"ratelimit": {
|
||||||
|
"day": "",
|
||||||
|
"hour": "",
|
||||||
|
"disabled": "",
|
||||||
|
"second": "",
|
||||||
|
"minute": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,7 @@
|
|||||||
"change_logo": "Logo ändern",
|
"change_logo": "Logo ändern",
|
||||||
"configuration": "Konfiguration",
|
"configuration": "Konfiguration",
|
||||||
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
|
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
|
||||||
|
"copy_to_clipboard": "Text wurde in die Zwischenablage kopiert!",
|
||||||
"cors_settings": "CORS Einstellungen",
|
"cors_settings": "CORS Einstellungen",
|
||||||
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
|
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
|
||||||
"customer_id": "Kunde",
|
"customer_id": "Kunde",
|
||||||
@@ -181,6 +182,8 @@
|
|||||||
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
|
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
|
||||||
"f2b_filter": "Regex-Filter",
|
"f2b_filter": "Regex-Filter",
|
||||||
"f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
|
"f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
|
||||||
|
"f2b_manage_external": "Fail2Ban extern verwalten",
|
||||||
|
"f2b_manage_external_info": "Fail2ban wird die Banlist weiterhin pflegen, jedoch werden keine aktiven Regeln zum blockieren gesetzt. Die unten generierte Banlist, kann verwendet werden, um den Datenverkehr extern zu blockieren.",
|
||||||
"f2b_max_attempts": "Max. Versuche",
|
"f2b_max_attempts": "Max. Versuche",
|
||||||
"f2b_max_ban_time": "Maximale Bannzeit in Sekunden",
|
"f2b_max_ban_time": "Maximale Bannzeit in Sekunden",
|
||||||
"f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
|
"f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
|
||||||
@@ -346,7 +349,9 @@
|
|||||||
"oauth2_apps": "OAuth2 Apps",
|
"oauth2_apps": "OAuth2 Apps",
|
||||||
"queue_unban": "entsperren",
|
"queue_unban": "entsperren",
|
||||||
"allowed_methods": "Access-Control-Allow-Methods",
|
"allowed_methods": "Access-Control-Allow-Methods",
|
||||||
"allowed_origins": "Access-Control-Allow-Origin"
|
"allowed_origins": "Access-Control-Allow-Origin",
|
||||||
|
"logo_dark_label": "Invertiert für den Darkmode",
|
||||||
|
"logo_normal_label": "Normal"
|
||||||
},
|
},
|
||||||
"danger": {
|
"danger": {
|
||||||
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
|
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
|
||||||
@@ -389,7 +394,9 @@
|
|||||||
"goto_invalid": "Ziel-Adresse %s ist ungültig",
|
"goto_invalid": "Ziel-Adresse %s ist ungültig",
|
||||||
"ham_learn_error": "Ham Lernfehler: %s",
|
"ham_learn_error": "Ham Lernfehler: %s",
|
||||||
"imagick_exception": "Fataler Bildverarbeitungsfehler",
|
"imagick_exception": "Fataler Bildverarbeitungsfehler",
|
||||||
|
"img_dimensions_exceeded": "Grafik überschreitet die maximale Bildgröße",
|
||||||
"img_invalid": "Grafik konnte nicht validiert werden",
|
"img_invalid": "Grafik konnte nicht validiert werden",
|
||||||
|
"img_size_exceeded": "Grafik überschreitet die maximale Dateigröße",
|
||||||
"img_tmp_missing": "Grafik konnte nicht validiert werden: Erstellung temporärer Datei fehlgeschlagen.",
|
"img_tmp_missing": "Grafik konnte nicht validiert werden: Erstellung temporärer Datei fehlgeschlagen.",
|
||||||
"invalid_bcc_map_type": "Ungültiger BCC-Map-Typ",
|
"invalid_bcc_map_type": "Ungültiger BCC-Map-Typ",
|
||||||
"invalid_destination": "Ziel-Format \"%s\" ist ungültig",
|
"invalid_destination": "Ziel-Format \"%s\" ist ungültig",
|
||||||
@@ -549,7 +556,7 @@
|
|||||||
"dns_records": "DNS-Einträge",
|
"dns_records": "DNS-Einträge",
|
||||||
"dns_records_24hours": "Bitte beachten Sie, dass es bis zu 24 Stunden dauern kann, bis Änderungen an Ihren DNS-Einträgen als aktueller Status auf dieser Seite dargestellt werden. Diese Seite ist nur als Hilfsmittel gedacht, um die korrekten Werte für DNS-Einträge anzuzeigen und zu überprüfen, ob die Daten im DNS hinterlegt sind.",
|
"dns_records_24hours": "Bitte beachten Sie, dass es bis zu 24 Stunden dauern kann, bis Änderungen an Ihren DNS-Einträgen als aktueller Status auf dieser Seite dargestellt werden. Diese Seite ist nur als Hilfsmittel gedacht, um die korrekten Werte für DNS-Einträge anzuzeigen und zu überprüfen, ob die Daten im DNS hinterlegt sind.",
|
||||||
"dns_records_data": "Korrekte Daten",
|
"dns_records_data": "Korrekte Daten",
|
||||||
"dns_records_docs": "Die <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">Online-Dokumentation</a> enthält weitere Informationen zur DNS-Konfiguration.",
|
"dns_records_docs": "Die <a target=\"_blank\" href=\"https://docs.mailcow.email/de/getstarted/prerequisite-dns\">Online-Dokumentation</a> enthält weitere Informationen zur DNS-Konfiguration.",
|
||||||
"dns_records_name": "Name",
|
"dns_records_name": "Name",
|
||||||
"dns_records_status": "Aktueller Status",
|
"dns_records_status": "Aktueller Status",
|
||||||
"dns_records_type": "Typ",
|
"dns_records_type": "Typ",
|
||||||
@@ -574,6 +581,7 @@
|
|||||||
"client_secret": "Client-Secret",
|
"client_secret": "Client-Secret",
|
||||||
"comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.",
|
"comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.",
|
||||||
"created_on": "Erstellt am",
|
"created_on": "Erstellt am",
|
||||||
|
"custom_attributes": "benutzerdefinierte Attribute",
|
||||||
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server",
|
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server",
|
||||||
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind",
|
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind",
|
||||||
"delete2duplicates": "Lösche Duplikate im Ziel",
|
"delete2duplicates": "Lösche Duplikate im Ziel",
|
||||||
@@ -582,10 +590,19 @@
|
|||||||
"disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
|
"disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
|
||||||
"domain": "Domain bearbeiten",
|
"domain": "Domain bearbeiten",
|
||||||
"domain_admin": "Domain-Administrator bearbeiten",
|
"domain_admin": "Domain-Administrator bearbeiten",
|
||||||
"domain_footer": "Domain wide footer",
|
"domain_footer": "Domänenweite Fußzeile",
|
||||||
"domain_footer_html": "HTML footer",
|
"domain_footer_html": "Fußzeile im HTML Format",
|
||||||
"domain_footer_info": "Domain wide footer werden allen ausgehenden E-Mails hinzugefügt, die einer Adresse innerhalb dieser Domain gehört.<br>Die folgenden Variablen können für den Footer benutzt werden:",
|
"domain_footer_info": "Domänenweite Footer (Domain wide footer) werden allen ausgehenden E-Mails hinzugefügt, die einer Adresse innerhalb dieser Domain gehört.<br>Die folgenden Variablen können für die Fußzeile benutzt werden:",
|
||||||
"domain_footer_plain": "PLAIN footer",
|
"domain_footer_info_vars": {
|
||||||
|
"auth_user": "{= auth_user =} - Angemeldeter Benutzername vom MTA",
|
||||||
|
"from_user": "{= from_user =} - Absender Teil der E-Mail z.B. für \"moo@mailcow.tld\" wird \"moo\" zurückgeben.",
|
||||||
|
"from_name": "{= from_name =} - Namen des Absenders z.B. für \"Mailcow <moo@mailcow.tld>\", wird \"Mailcow\" zurückgegeben.",
|
||||||
|
"from_addr": "{= from_addr =} - Adresse des Absenders.",
|
||||||
|
"from_domain": "{= from_domain =} - Domain des Absenders",
|
||||||
|
"custom": "{= foo =} - Wenn die Mailbox das benutzerdefinierte Attribut \"foo\" mit dem Wert \"bar\" hat, wird \"bar\" zurückgegeben."
|
||||||
|
},
|
||||||
|
"domain_footer_plain": "Fußzeile im PLAIN Format",
|
||||||
|
"domain_footer_skip_replies": "Ignoriere Footer bei Antwort E-Mails",
|
||||||
"domain_quota": "Domain Speicherplatz gesamt (MiB)",
|
"domain_quota": "Domain Speicherplatz gesamt (MiB)",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen",
|
"dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen",
|
||||||
@@ -596,6 +613,7 @@
|
|||||||
"extended_sender_acl_info": "Der DKIM-Domainkey der externen Absenderdomain sollte in diesen Server importiert werden, falls vorhanden.<br>\r\n Wird SPF verwendet, muss diesem Server der Versand gestattet werden.<br>\r\n Wird eine Domain oder Alias-Domain zu diesem Server hinzugefügt, die sich mit der externen Absenderadresse überschneidet, wird der externe Absender hier entfernt.<br>\r\n Ein Eintrag @domain.tld erlaubt den Versand als *@domain.tld",
|
"extended_sender_acl_info": "Der DKIM-Domainkey der externen Absenderdomain sollte in diesen Server importiert werden, falls vorhanden.<br>\r\n Wird SPF verwendet, muss diesem Server der Versand gestattet werden.<br>\r\n Wird eine Domain oder Alias-Domain zu diesem Server hinzugefügt, die sich mit der externen Absenderadresse überschneidet, wird der externe Absender hier entfernt.<br>\r\n Ein Eintrag @domain.tld erlaubt den Versand als *@domain.tld",
|
||||||
"force_pw_update": "Erzwinge Passwortänderung bei nächstem Login",
|
"force_pw_update": "Erzwinge Passwortänderung bei nächstem Login",
|
||||||
"force_pw_update_info": "Dem Benutzer wird lediglich der Zugang zur %s ermöglicht, App Passwörter funktionieren weiterhin.",
|
"force_pw_update_info": "Dem Benutzer wird lediglich der Zugang zur %s ermöglicht, App Passwörter funktionieren weiterhin.",
|
||||||
|
"footer_exclude": "von Fußzeile ausschließen",
|
||||||
"full_name": "Voller Name",
|
"full_name": "Voller Name",
|
||||||
"gal": "Globales Adressbuch",
|
"gal": "Globales Adressbuch",
|
||||||
"gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>",
|
"gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>",
|
||||||
@@ -1027,6 +1045,7 @@
|
|||||||
"domain_removed": "Domain %s wurde entfernt",
|
"domain_removed": "Domain %s wurde entfernt",
|
||||||
"dovecot_restart_success": "Dovecot wurde erfolgreich neu gestartet",
|
"dovecot_restart_success": "Dovecot wurde erfolgreich neu gestartet",
|
||||||
"eas_reset": "ActiveSync Gerät des Benutzers %s wurde zurückgesetzt",
|
"eas_reset": "ActiveSync Gerät des Benutzers %s wurde zurückgesetzt",
|
||||||
|
"f2b_banlist_refreshed": "Banlist ID wurde erfolgreich erneuert.",
|
||||||
"f2b_modified": "Änderungen an Fail2ban-Parametern wurden gespeichert",
|
"f2b_modified": "Änderungen an Fail2ban-Parametern wurden gespeichert",
|
||||||
"forwarding_host_added": "Weiterleitungs-Host %s wurde hinzugefügt",
|
"forwarding_host_added": "Weiterleitungs-Host %s wurde hinzugefügt",
|
||||||
"forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
|
"forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
|
||||||
@@ -1076,6 +1095,7 @@
|
|||||||
"verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert"
|
"verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert"
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
|
"authenticators": "Authentikatoren",
|
||||||
"api_register": "%s verwendet die Yubico-Cloud-API. Ein API-Key für den Yubico-Stick kann <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">hier</a> bezogen werden.",
|
"api_register": "%s verwendet die Yubico-Cloud-API. Ein API-Key für den Yubico-Stick kann <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">hier</a> bezogen werden.",
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
"confirm_totp_token": "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens",
|
"confirm_totp_token": "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens",
|
||||||
@@ -1125,6 +1145,7 @@
|
|||||||
"apple_connection_profile_complete": "Dieses Verbindungsprofil beinhaltet neben IMAP- und SMTP-Konfigurationen auch Pfade für die Konfiguration von CalDAV (Kalender) und CardDAV (Adressbücher) für ein Apple-Gerät.",
|
"apple_connection_profile_complete": "Dieses Verbindungsprofil beinhaltet neben IMAP- und SMTP-Konfigurationen auch Pfade für die Konfiguration von CalDAV (Kalender) und CardDAV (Adressbücher) für ein Apple-Gerät.",
|
||||||
"apple_connection_profile_mailonly": "Dieses Verbindungsprofil beinhaltet IMAP- und SMTP-Konfigurationen für ein Apple-Gerät.",
|
"apple_connection_profile_mailonly": "Dieses Verbindungsprofil beinhaltet IMAP- und SMTP-Konfigurationen für ein Apple-Gerät.",
|
||||||
"apple_connection_profile_with_app_password": "Es wird ein neues App-Passwort erzeugt und in das Profil eingefügt, damit bei der Einrichtung kein Passwort eingegeben werden muss. Geben Sie das Profil nicht weiter, da es einen vollständigen Zugriff auf Ihr Postfach ermöglicht.",
|
"apple_connection_profile_with_app_password": "Es wird ein neues App-Passwort erzeugt und in das Profil eingefügt, damit bei der Einrichtung kein Passwort eingegeben werden muss. Geben Sie das Profil nicht weiter, da es einen vollständigen Zugriff auf Ihr Postfach ermöglicht.",
|
||||||
|
"attribute": "Attribut",
|
||||||
"change_password": "Passwort ändern",
|
"change_password": "Passwort ändern",
|
||||||
"change_password_hint_app_passwords": "Ihre Mailbox hat %d App-Passwörter, die nicht geändert werden. Um diese zu verwalten, gehen Sie bitte zum App-Passwörter-Tab.",
|
"change_password_hint_app_passwords": "Ihre Mailbox hat %d App-Passwörter, die nicht geändert werden. Um diese zu verwalten, gehen Sie bitte zum App-Passwörter-Tab.",
|
||||||
"clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen",
|
"clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen",
|
||||||
@@ -1244,6 +1265,7 @@
|
|||||||
"tls_policy_warning": "<strong>Vorsicht:</strong> Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.<br>Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.<br>Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox <b>direkt zugeordnet</b> sind (lediglich eine einzige Ziel-Adresse) und der Adressen, die sich aus Alias-Domains ergeben. Ausgeschlossen sind temporäre Aliasse (\"Spam-Alias-Adressen\"), Catch-All Alias-Adressen sowie Alias-Adressen mit mehreren Zielen.",
|
"tls_policy_warning": "<strong>Vorsicht:</strong> Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.<br>Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.<br>Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox <b>direkt zugeordnet</b> sind (lediglich eine einzige Ziel-Adresse) und der Adressen, die sich aus Alias-Domains ergeben. Ausgeschlossen sind temporäre Aliasse (\"Spam-Alias-Adressen\"), Catch-All Alias-Adressen sowie Alias-Adressen mit mehreren Zielen.",
|
||||||
"user_settings": "Benutzereinstellungen",
|
"user_settings": "Benutzereinstellungen",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
|
"value": "Wert",
|
||||||
"verify": "Verifizieren",
|
"verify": "Verifizieren",
|
||||||
"waiting": "Warte auf Ausführung",
|
"waiting": "Warte auf Ausführung",
|
||||||
"week": "Woche",
|
"week": "Woche",
|
||||||
|
|||||||
@@ -154,6 +154,7 @@
|
|||||||
"logo_dark_label": "Inverted for dark mode",
|
"logo_dark_label": "Inverted for dark mode",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
"convert_html_to_text": "Convert HTML to plain text",
|
"convert_html_to_text": "Convert HTML to plain text",
|
||||||
|
"copy_to_clipboard": "Text copied to clipboard!",
|
||||||
"cors_settings": "CORS Settings",
|
"cors_settings": "CORS Settings",
|
||||||
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
|
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
|
||||||
"customer_id": "Customer ID",
|
"customer_id": "Customer ID",
|
||||||
@@ -187,6 +188,8 @@
|
|||||||
"f2b_blacklist": "Blacklisted networks/hosts",
|
"f2b_blacklist": "Blacklisted networks/hosts",
|
||||||
"f2b_filter": "Regex filters",
|
"f2b_filter": "Regex filters",
|
||||||
"f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. <b>List updates will take a few seconds to be applied.</b>",
|
"f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. <b>List updates will take a few seconds to be applied.</b>",
|
||||||
|
"f2b_manage_external": "Manage Fail2Ban externally",
|
||||||
|
"f2b_manage_external_info": "Fail2ban will still maintain the banlist, but it will not actively set rules to block traffic. Use the generated banlist below to externally block the traffic.",
|
||||||
"f2b_max_attempts": "Max. attempts",
|
"f2b_max_attempts": "Max. attempts",
|
||||||
"f2b_max_ban_time": "Max. ban time (s)",
|
"f2b_max_ban_time": "Max. ban time (s)",
|
||||||
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
|
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
|
||||||
@@ -391,7 +394,9 @@
|
|||||||
"goto_invalid": "Goto address %s is invalid",
|
"goto_invalid": "Goto address %s is invalid",
|
||||||
"ham_learn_error": "Ham learn error: %s",
|
"ham_learn_error": "Ham learn error: %s",
|
||||||
"imagick_exception": "Error: Imagick exception while reading image",
|
"imagick_exception": "Error: Imagick exception while reading image",
|
||||||
|
"img_dimensions_exceeded": "Image exceeds the maximum image size",
|
||||||
"img_invalid": "Cannot validate image file",
|
"img_invalid": "Cannot validate image file",
|
||||||
|
"img_size_exceeded": "Image exceeds the maximum file size",
|
||||||
"img_tmp_missing": "Cannot validate image file: Temporary file not found",
|
"img_tmp_missing": "Cannot validate image file: Temporary file not found",
|
||||||
"invalid_bcc_map_type": "Invalid BCC map type",
|
"invalid_bcc_map_type": "Invalid BCC map type",
|
||||||
"invalid_destination": "Destination format \"%s\" is invalid",
|
"invalid_destination": "Destination format \"%s\" is invalid",
|
||||||
@@ -551,7 +556,7 @@
|
|||||||
"dns_records": "DNS Records",
|
"dns_records": "DNS Records",
|
||||||
"dns_records_24hours": "Please note that changes made to DNS may take up to 24 hours to correctly have their current state reflected on this page. It is intended as a way for you to easily see how to configure your DNS records and to check whether all your records are correctly stored in DNS.",
|
"dns_records_24hours": "Please note that changes made to DNS may take up to 24 hours to correctly have their current state reflected on this page. It is intended as a way for you to easily see how to configure your DNS records and to check whether all your records are correctly stored in DNS.",
|
||||||
"dns_records_data": "Correct Data",
|
"dns_records_data": "Correct Data",
|
||||||
"dns_records_docs": "Please also consult <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">the documentation</a>.",
|
"dns_records_docs": "Please also consult <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">the documentation</a>.",
|
||||||
"dns_records_name": "Name",
|
"dns_records_name": "Name",
|
||||||
"dns_records_status": "Current State",
|
"dns_records_status": "Current State",
|
||||||
"dns_records_type": "Type",
|
"dns_records_type": "Type",
|
||||||
@@ -576,6 +581,7 @@
|
|||||||
"client_secret": "Client secret",
|
"client_secret": "Client secret",
|
||||||
"comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview",
|
"comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview",
|
||||||
"created_on": "Created on",
|
"created_on": "Created on",
|
||||||
|
"custom_attributes": "Custom attributes",
|
||||||
"delete1": "Delete from source when completed",
|
"delete1": "Delete from source when completed",
|
||||||
"delete2": "Delete messages on destination that are not on source",
|
"delete2": "Delete messages on destination that are not on source",
|
||||||
"delete2duplicates": "Delete duplicates on destination",
|
"delete2duplicates": "Delete duplicates on destination",
|
||||||
@@ -592,9 +598,11 @@
|
|||||||
"from_user": "{= from_user =} - From user part of envelope, e.g for \"moo@mailcow.tld\" it returns \"moo\"",
|
"from_user": "{= from_user =} - From user part of envelope, e.g for \"moo@mailcow.tld\" it returns \"moo\"",
|
||||||
"from_name": "{= from_name =} - From name of envelope, e.g for \"Mailcow <moo@mailcow.tld>\" it returns \"Mailcow\"",
|
"from_name": "{= from_name =} - From name of envelope, e.g for \"Mailcow <moo@mailcow.tld>\" it returns \"Mailcow\"",
|
||||||
"from_addr": "{= from_addr =} - From address part of envelope",
|
"from_addr": "{= from_addr =} - From address part of envelope",
|
||||||
"from_domain": "{= from_domain =} - From domain part of envelope"
|
"from_domain": "{= from_domain =} - From domain part of envelope",
|
||||||
|
"custom": "{= foo =} - If mailbox has the custom attribute \"foo\" with value \"bar\" it returns \"bar\""
|
||||||
},
|
},
|
||||||
"domain_footer_plain": "PLAIN footer",
|
"domain_footer_plain": "PLAIN footer",
|
||||||
|
"domain_footer_skip_replies": "Ignore footer on reply e-mails",
|
||||||
"domain_quota": "Domain quota",
|
"domain_quota": "Domain quota",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)",
|
"dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)",
|
||||||
@@ -605,6 +613,7 @@
|
|||||||
"extended_sender_acl_info": "A DKIM domain key should be imported, if available.<br>\r\n Remember to add this server to the corresponding SPF TXT record.<br>\r\n Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.<br>\r\n Use @domain.tld to allow to send as *@domain.tld.",
|
"extended_sender_acl_info": "A DKIM domain key should be imported, if available.<br>\r\n Remember to add this server to the corresponding SPF TXT record.<br>\r\n Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.<br>\r\n Use @domain.tld to allow to send as *@domain.tld.",
|
||||||
"force_pw_update": "Force password update at next login",
|
"force_pw_update": "Force password update at next login",
|
||||||
"force_pw_update_info": "This user will only be able to login to %s. App passwords remain useable.",
|
"force_pw_update_info": "This user will only be able to login to %s. App passwords remain useable.",
|
||||||
|
"footer_exclude": "Exclude from footer",
|
||||||
"full_name": "Full name",
|
"full_name": "Full name",
|
||||||
"gal": "Global Address List",
|
"gal": "Global Address List",
|
||||||
"gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
|
"gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
|
||||||
@@ -1043,6 +1052,7 @@
|
|||||||
"domain_removed": "Domain %s has been removed",
|
"domain_removed": "Domain %s has been removed",
|
||||||
"dovecot_restart_success": "Dovecot was restarted successfully",
|
"dovecot_restart_success": "Dovecot was restarted successfully",
|
||||||
"eas_reset": "ActiveSync devices for user %s were reset",
|
"eas_reset": "ActiveSync devices for user %s were reset",
|
||||||
|
"f2b_banlist_refreshed": "Banlist ID has been successfully refreshed.",
|
||||||
"f2b_modified": "Changes to Fail2ban parameters have been saved",
|
"f2b_modified": "Changes to Fail2ban parameters have been saved",
|
||||||
"forwarding_host_added": "Forwarding host %s has been added",
|
"forwarding_host_added": "Forwarding host %s has been added",
|
||||||
"forwarding_host_removed": "Forwarding host %s has been removed",
|
"forwarding_host_removed": "Forwarding host %s has been removed",
|
||||||
@@ -1092,6 +1102,7 @@
|
|||||||
"verified_yotp_login": "Verified Yubico OTP login"
|
"verified_yotp_login": "Verified Yubico OTP login"
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
|
"authenticators": "Authenticators",
|
||||||
"api_register": "%s uses the Yubico Cloud API. Please get an API key for your key <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",
|
"api_register": "%s uses the Yubico Cloud API. Please get an API key for your key <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"confirm_totp_token": "Please confirm your changes by entering the generated token",
|
"confirm_totp_token": "Please confirm your changes by entering the generated token",
|
||||||
@@ -1141,6 +1152,7 @@
|
|||||||
"apple_connection_profile_complete": "This connection profile includes IMAP and SMTP parameters as well as CalDAV (calendars) and CardDAV (contacts) paths for an Apple device.",
|
"apple_connection_profile_complete": "This connection profile includes IMAP and SMTP parameters as well as CalDAV (calendars) and CardDAV (contacts) paths for an Apple device.",
|
||||||
"apple_connection_profile_mailonly": "This connection profile includes IMAP and SMTP configuration parameters for an Apple device.",
|
"apple_connection_profile_mailonly": "This connection profile includes IMAP and SMTP configuration parameters for an Apple device.",
|
||||||
"apple_connection_profile_with_app_password": "A new app password is generated and added to the profile so that no password needs to be entered when setting up your device. Please do not share the file as it grants full access to your mailbox.",
|
"apple_connection_profile_with_app_password": "A new app password is generated and added to the profile so that no password needs to be entered when setting up your device. Please do not share the file as it grants full access to your mailbox.",
|
||||||
|
"attribute": "Attribute",
|
||||||
"change_password": "Change password",
|
"change_password": "Change password",
|
||||||
"change_password_hint_app_passwords": "Your account has %d app passwords that will not be changed. To manage these, go to the App passwords tab.",
|
"change_password_hint_app_passwords": "Your account has %d app passwords that will not be changed. To manage these, go to the App passwords tab.",
|
||||||
"clear_recent_successful_connections": "Clear seen successful connections",
|
"clear_recent_successful_connections": "Clear seen successful connections",
|
||||||
@@ -1271,6 +1283,7 @@
|
|||||||
"tls_policy_warning": "<strong>Warning:</strong> If you decide to enforce encrypted mail transfer, you may lose emails.<br>Messages to not satisfy the policy will be bounced with a hard fail by the mail system.<br>This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses <b>with only this single mailbox</b> as target.",
|
"tls_policy_warning": "<strong>Warning:</strong> If you decide to enforce encrypted mail transfer, you may lose emails.<br>Messages to not satisfy the policy will be bounced with a hard fail by the mail system.<br>This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses <b>with only this single mailbox</b> as target.",
|
||||||
"user_settings": "User settings",
|
"user_settings": "User settings",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
"value": "Value",
|
||||||
"verify": "Verify",
|
"verify": "Verify",
|
||||||
"waiting": "Waiting",
|
"waiting": "Waiting",
|
||||||
"week": "week",
|
"week": "week",
|
||||||
|
|||||||
+548
-18
@@ -22,7 +22,13 @@
|
|||||||
"app_passwds": "Gestionar las contraseñas de aplicaciones",
|
"app_passwds": "Gestionar las contraseñas de aplicaciones",
|
||||||
"domain_desc": "Cambiar descripción del dominio",
|
"domain_desc": "Cambiar descripción del dominio",
|
||||||
"protocol_access": "Cambiar protocolo de acceso",
|
"protocol_access": "Cambiar protocolo de acceso",
|
||||||
"quarantine_category": "Cambiar categoría de las notificaciones de cuarentena"
|
"quarantine_category": "Cambiar categoría de las notificaciones de cuarentena",
|
||||||
|
"domain_relayhost": "Cambiar relayhost por un dominio",
|
||||||
|
"extend_sender_acl": "Permitir extender la ACL del remitente por direcciones externas",
|
||||||
|
"smtp_ip_access": "",
|
||||||
|
"sogo_access": "",
|
||||||
|
"mailbox_relayhost": "",
|
||||||
|
"pushover": ""
|
||||||
},
|
},
|
||||||
"add": {
|
"add": {
|
||||||
"activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.",
|
"activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.",
|
||||||
@@ -93,7 +99,16 @@
|
|||||||
"app_password": "Añadir contraseña para la app",
|
"app_password": "Añadir contraseña para la app",
|
||||||
"public_comment": "Comentarios públicos",
|
"public_comment": "Comentarios públicos",
|
||||||
"disable_login": "Desactivar login (el correo entrante seguirá activo)",
|
"disable_login": "Desactivar login (el correo entrante seguirá activo)",
|
||||||
"comment_info": "Los comentarios privados no son visibles al usuario, mientras que los comentarios públicos aparecerán sobre la información general del usuario"
|
"comment_info": "Los comentarios privados no son visibles al usuario, mientras que los comentarios públicos aparecerán sobre la información general del usuario",
|
||||||
|
"dry": "",
|
||||||
|
"private_comment": "",
|
||||||
|
"relay_transport_info": "",
|
||||||
|
"domain_matches_hostname": "",
|
||||||
|
"relay_unknown_only": "",
|
||||||
|
"relayhost_wrapped_tls_info": "",
|
||||||
|
"app_passwd_protocols": "",
|
||||||
|
"bcc_dest_format": "",
|
||||||
|
"tags": ""
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"access": "Acceso",
|
"access": "Acceso",
|
||||||
@@ -246,7 +261,97 @@
|
|||||||
"unban_pending": "Desbloqueo pendiente",
|
"unban_pending": "Desbloqueo pendiente",
|
||||||
"unchanged_if_empty": "Si no hay cambios déjalo en blanco",
|
"unchanged_if_empty": "Si no hay cambios déjalo en blanco",
|
||||||
"upload": "Cargar",
|
"upload": "Cargar",
|
||||||
"username": "Nombre de usuario"
|
"username": "Nombre de usuario",
|
||||||
|
"lookup_mx": "",
|
||||||
|
"license_info": "",
|
||||||
|
"message": "",
|
||||||
|
"oauth2_apps": "",
|
||||||
|
"transport_dest_format": "",
|
||||||
|
"ui_footer": "",
|
||||||
|
"copy_to_clipboard": "",
|
||||||
|
"f2b_manage_external": "",
|
||||||
|
"f2b_manage_external_info": "",
|
||||||
|
"no": "",
|
||||||
|
"quarantine_max_score": "",
|
||||||
|
"queue_unban": "",
|
||||||
|
"rate_name": "",
|
||||||
|
"regex_maps": "",
|
||||||
|
"relay_rcpt": "",
|
||||||
|
"rsetting_no_selection": "",
|
||||||
|
"rsettings_preset_3": "",
|
||||||
|
"rsettings_preset_4": "",
|
||||||
|
"title": "",
|
||||||
|
"transport_test_rcpt_info": "",
|
||||||
|
"oauth2_add_client": "",
|
||||||
|
"oauth2_revoke_tokens": "",
|
||||||
|
"options": "",
|
||||||
|
"ui_header_announcement": "",
|
||||||
|
"logo_normal_label": "",
|
||||||
|
"logo_dark_label": "",
|
||||||
|
"quarantine_redirect": "",
|
||||||
|
"time": "",
|
||||||
|
"ui_header_announcement_active": "",
|
||||||
|
"ui_header_announcement_type_info": "",
|
||||||
|
"ui_header_announcement_type_warning": "",
|
||||||
|
"validate_license_now": "",
|
||||||
|
"reset_limit": "",
|
||||||
|
"success": "",
|
||||||
|
"allowed_methods": "",
|
||||||
|
"allowed_origins": "",
|
||||||
|
"cors_settings": "",
|
||||||
|
"login_time": "",
|
||||||
|
"yes": "",
|
||||||
|
"f2b_filter": "",
|
||||||
|
"html": "",
|
||||||
|
"oauth2_redirect_uri": "",
|
||||||
|
"oauth2_renew_secret": "",
|
||||||
|
"admins": "",
|
||||||
|
"admins_ldap": "",
|
||||||
|
"guid": "",
|
||||||
|
"guid_and_license": "",
|
||||||
|
"hash_remove_info": "",
|
||||||
|
"is_mx_based": "",
|
||||||
|
"last_applied": "",
|
||||||
|
"ip_check": "",
|
||||||
|
"ip_check_disabled": "",
|
||||||
|
"ip_check_opt_in": "",
|
||||||
|
"rspamd_global_filters_regex": "",
|
||||||
|
"sal_level": "",
|
||||||
|
"service": "",
|
||||||
|
"service_id": "",
|
||||||
|
"oauth2_info": "",
|
||||||
|
"optional": "",
|
||||||
|
"password_length": "",
|
||||||
|
"password_policy": "",
|
||||||
|
"password_policy_chars": "",
|
||||||
|
"password_policy_length": "",
|
||||||
|
"password_policy_lowerupper": "",
|
||||||
|
"password_policy_numbers": "",
|
||||||
|
"priority": "",
|
||||||
|
"quarantine_bcc": "",
|
||||||
|
"rspamd_global_filters": "",
|
||||||
|
"rspamd_global_filters_agree": "",
|
||||||
|
"rspamd_global_filters_info": "",
|
||||||
|
"ui_header_announcement_content": "",
|
||||||
|
"ui_header_announcement_help": "",
|
||||||
|
"ui_header_announcement_select": "",
|
||||||
|
"ui_header_announcement_type": "",
|
||||||
|
"ui_header_announcement_type_danger": "",
|
||||||
|
"verify": "",
|
||||||
|
"customer_id": "",
|
||||||
|
"dkim_overwrite_key": "",
|
||||||
|
"domain_admin": "",
|
||||||
|
"domain_s": "",
|
||||||
|
"f2b_regex_info": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"api_info": "",
|
||||||
|
"api_read_only": "",
|
||||||
|
"api_read_write": "",
|
||||||
|
"api_skip_ip_check": "",
|
||||||
|
"authed_user": "",
|
||||||
|
"ays": "",
|
||||||
|
"convert_html_to_text": "",
|
||||||
|
"password_policy_special_chars": ""
|
||||||
},
|
},
|
||||||
"danger": {
|
"danger": {
|
||||||
"access_denied": "Acceso denegado o datos del formulario inválidos",
|
"access_denied": "Acceso denegado o datos del formulario inválidos",
|
||||||
@@ -334,7 +439,50 @@
|
|||||||
"username_invalid": "Nombre de usuario no se puede utilizar",
|
"username_invalid": "Nombre de usuario no se puede utilizar",
|
||||||
"validity_missing": "Por favor asigna un periodo de validez",
|
"validity_missing": "Por favor asigna un periodo de validez",
|
||||||
"value_missing": "Por favor proporcione todos los valores",
|
"value_missing": "Por favor proporcione todos los valores",
|
||||||
"yotp_verification_failed": "Verificación Yubico OTP fallida: %s"
|
"yotp_verification_failed": "Verificación Yubico OTP fallida: %s",
|
||||||
|
"invalid_mime_type": "",
|
||||||
|
"description_invalid": "",
|
||||||
|
"dkim_domain_or_sel_exists": "",
|
||||||
|
"extended_sender_acl_denied": "",
|
||||||
|
"extra_acl_invalid": "",
|
||||||
|
"last_key": "",
|
||||||
|
"webauthn_authenticator_failed": "",
|
||||||
|
"webauthn_publickey_failed": "",
|
||||||
|
"webauthn_username_failed": "",
|
||||||
|
"app_name_empty": "",
|
||||||
|
"app_passwd_id_invalid": "",
|
||||||
|
"comment_too_long": "",
|
||||||
|
"cors_invalid_method": "",
|
||||||
|
"cors_invalid_origin": "",
|
||||||
|
"demo_mode_enabled": "",
|
||||||
|
"global_filter_write_error": "",
|
||||||
|
"pushover_credentials_missing": "",
|
||||||
|
"pushover_key": "",
|
||||||
|
"pushover_token": "",
|
||||||
|
"img_dimensions_exceeded": "",
|
||||||
|
"img_size_exceeded": "",
|
||||||
|
"maxquota_empty": "",
|
||||||
|
"nginx_reload_failed": "",
|
||||||
|
"targetd_relay_domain": "",
|
||||||
|
"template_exists": "",
|
||||||
|
"template_id_invalid": "",
|
||||||
|
"temp_error": "",
|
||||||
|
"tfa_token_invalid": "",
|
||||||
|
"tls_policy_map_dest_invalid": "",
|
||||||
|
"extra_acl_invalid_domain": "",
|
||||||
|
"fido2_verification_failed": "",
|
||||||
|
"template_name_invalid": "",
|
||||||
|
"resource_invalid": "",
|
||||||
|
"file_open_error": "",
|
||||||
|
"img_tmp_missing": "",
|
||||||
|
"invalid_filter_type": "",
|
||||||
|
"global_map_invalid": "",
|
||||||
|
"global_map_write_error": "",
|
||||||
|
"ham_learn_error": "",
|
||||||
|
"img_invalid": "",
|
||||||
|
"imagick_exception": "",
|
||||||
|
"max_alias_exceeded": "",
|
||||||
|
"reset_f2b_regex": ""
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"containers_info": "Información de los contenedores",
|
"containers_info": "Información de los contenedores",
|
||||||
@@ -352,7 +500,30 @@
|
|||||||
"solr_status": "Solr status",
|
"solr_status": "Solr status",
|
||||||
"uptime": "Uptime",
|
"uptime": "Uptime",
|
||||||
"static_logs": "Logs estáticos",
|
"static_logs": "Logs estáticos",
|
||||||
"system_containers": "Sistema y Contenedores"
|
"system_containers": "Sistema y Contenedores",
|
||||||
|
"container_disabled": "",
|
||||||
|
"architecture": "",
|
||||||
|
"update_failed": "",
|
||||||
|
"wip": "",
|
||||||
|
"chart_this_server": "",
|
||||||
|
"container_running": "",
|
||||||
|
"container_stopped": "",
|
||||||
|
"cores": "",
|
||||||
|
"current_time": "",
|
||||||
|
"error_show_ip": "",
|
||||||
|
"history_all_servers": "",
|
||||||
|
"jvm_memory_solr": "",
|
||||||
|
"memory": "",
|
||||||
|
"online_users": "",
|
||||||
|
"service": "",
|
||||||
|
"show_ip": "",
|
||||||
|
"started_on": "",
|
||||||
|
"success": "",
|
||||||
|
"timezone": "",
|
||||||
|
"update_available": "",
|
||||||
|
"no_update_available": "",
|
||||||
|
"username": "",
|
||||||
|
"login_time": ""
|
||||||
},
|
},
|
||||||
"diagnostics": {
|
"diagnostics": {
|
||||||
"cname_from_a": "Valor derivado del registro A / AAAA. Esto es permitido siempre que el registro apunte al recurso correcto.",
|
"cname_from_a": "Valor derivado del registro A / AAAA. Esto es permitido siempre que el registro apunte al recurso correcto.",
|
||||||
@@ -362,7 +533,8 @@
|
|||||||
"dns_records_name": "Nombre",
|
"dns_records_name": "Nombre",
|
||||||
"dns_records_status": "Información actual",
|
"dns_records_status": "Información actual",
|
||||||
"dns_records_type": "Tipo",
|
"dns_records_type": "Tipo",
|
||||||
"optional": "Este récord es opcional."
|
"optional": "Este récord es opcional.",
|
||||||
|
"dns_records_docs": ""
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"active": "Activo",
|
"active": "Activo",
|
||||||
@@ -430,13 +602,85 @@
|
|||||||
"title": "Editar objeto",
|
"title": "Editar objeto",
|
||||||
"unchanged_if_empty": "Si no hay cambios dejalo en blanco",
|
"unchanged_if_empty": "Si no hay cambios dejalo en blanco",
|
||||||
"username": "Nombre de usuario",
|
"username": "Nombre de usuario",
|
||||||
"validate_save": "Validar y guardar"
|
"validate_save": "Validar y guardar",
|
||||||
|
"lookup_mx": "",
|
||||||
|
"footer_exclude": "",
|
||||||
|
"custom_attributes": "",
|
||||||
|
"domain_footer_info_vars": {
|
||||||
|
"from_user": "",
|
||||||
|
"from_name": "",
|
||||||
|
"from_domain": "",
|
||||||
|
"custom": "",
|
||||||
|
"auth_user": "",
|
||||||
|
"from_addr": ""
|
||||||
|
},
|
||||||
|
"domain_footer_plain": "",
|
||||||
|
"domain_footer_skip_replies": "",
|
||||||
|
"sender_acl_info": "",
|
||||||
|
"domain_footer": "",
|
||||||
|
"domain_footer_html": "",
|
||||||
|
"spam_score": "",
|
||||||
|
"domain_footer_info": "",
|
||||||
|
"acl": "",
|
||||||
|
"relay_transport_info": "",
|
||||||
|
"sender_acl_disabled": "",
|
||||||
|
"sogo_access": "",
|
||||||
|
"sogo_access_info": "",
|
||||||
|
"sogo_visible": "",
|
||||||
|
"sogo_visible_info": "",
|
||||||
|
"spam_alias": "",
|
||||||
|
"spam_filter": "",
|
||||||
|
"spam_policy": "",
|
||||||
|
"generate": "",
|
||||||
|
"private_comment": "",
|
||||||
|
"public_comment": "",
|
||||||
|
"pushover": "",
|
||||||
|
"pushover_evaluate_x_prio": "",
|
||||||
|
"pushover_text": "",
|
||||||
|
"admin": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"allow_from_smtp": "",
|
||||||
|
"allow_from_smtp_info": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"app_passwd": "",
|
||||||
|
"app_passwd_protocols": "",
|
||||||
|
"comment_info": "",
|
||||||
|
"created_on": "",
|
||||||
|
"delete_ays": "",
|
||||||
|
"extended_sender_acl": "",
|
||||||
|
"extended_sender_acl_info": "",
|
||||||
|
"mbox_rl_info": "",
|
||||||
|
"none_inherit": "",
|
||||||
|
"pushover_only_x_prio": "",
|
||||||
|
"pushover_sender_array": "",
|
||||||
|
"pushover_title": "",
|
||||||
|
"pushover_sound": "",
|
||||||
|
"pushover_vars": "",
|
||||||
|
"mailbox_relayhost_info": "",
|
||||||
|
"pushover_verify": "",
|
||||||
|
"quota_warning_bcc": "",
|
||||||
|
"quota_warning_bcc_info": "",
|
||||||
|
"ratelimit": "",
|
||||||
|
"relay_unknown_only": "",
|
||||||
|
"pushover_info": "",
|
||||||
|
"pushover_sender_regex": "",
|
||||||
|
"app_name": "",
|
||||||
|
"disable_login": ""
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"hibp_nok": "¡Se encontró coincidencia - esta es una contraseña <b>no segura</b>, selecciona otra!",
|
"hibp_nok": "¡Se encontró coincidencia - esta es una contraseña <b>no segura</b>, selecciona otra!",
|
||||||
"hibp_ok": "No se encontraron coincidencias",
|
"hibp_ok": "No se encontraron coincidencias",
|
||||||
"loading": "Espera por favor...",
|
"loading": "Espera por favor...",
|
||||||
"restart_now": "Reiniciar ahora"
|
"restart_now": "Reiniciar ahora",
|
||||||
|
"cancel": "",
|
||||||
|
"confirm_delete": "",
|
||||||
|
"delete_now": "",
|
||||||
|
"hibp_check": "",
|
||||||
|
"nothing_selected": "",
|
||||||
|
"delete_these_items": "",
|
||||||
|
"restart_container": "",
|
||||||
|
"restarting_container": "",
|
||||||
|
"restart_container_info": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"administration": "Administración",
|
"administration": "Administración",
|
||||||
@@ -445,17 +689,24 @@
|
|||||||
"mailcow_config": "Configuración",
|
"mailcow_config": "Configuración",
|
||||||
"quarantine": "Cuarentena",
|
"quarantine": "Cuarentena",
|
||||||
"restart_sogo": "Reiniciar SOGo",
|
"restart_sogo": "Reiniciar SOGo",
|
||||||
"user_settings": "Configuraciones de usuario"
|
"user_settings": "Configuraciones de usuario",
|
||||||
|
"apps": "",
|
||||||
|
"mailcow_system": "",
|
||||||
|
"restart_netfilter": ""
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"awaiting_tfa_confirmation": "En espera de confirmación de TFA",
|
"awaiting_tfa_confirmation": "En espera de confirmación de TFA",
|
||||||
"no_action": "No hay acción aplicable"
|
"no_action": "No hay acción aplicable",
|
||||||
|
"session_expires": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"delayed": "El inicio de sesión ha sido retrasado %s segundos.",
|
"delayed": "El inicio de sesión ha sido retrasado %s segundos.",
|
||||||
"login": "Inicio de sesión",
|
"login": "Inicio de sesión",
|
||||||
"password": "Contraseña",
|
"password": "Contraseña",
|
||||||
"username": "Nombre de usuario"
|
"username": "Nombre de usuario",
|
||||||
|
"fido2_webauthn": "",
|
||||||
|
"mobileconfig_info": "",
|
||||||
|
"other_logins": ""
|
||||||
},
|
},
|
||||||
"mailbox": {
|
"mailbox": {
|
||||||
"action": "Acción",
|
"action": "Acción",
|
||||||
@@ -567,7 +818,70 @@
|
|||||||
"toggle_all": "Selecionar todo",
|
"toggle_all": "Selecionar todo",
|
||||||
"username": "Nombre de usuario",
|
"username": "Nombre de usuario",
|
||||||
"waiting": "Esperando",
|
"waiting": "Esperando",
|
||||||
"weekly": "Cada semana"
|
"weekly": "Cada semana",
|
||||||
|
"disable_login": "",
|
||||||
|
"templates": "",
|
||||||
|
"public_comment": "",
|
||||||
|
"q_add_header": "",
|
||||||
|
"q_reject": "",
|
||||||
|
"table_size": "",
|
||||||
|
"domain_templates": "",
|
||||||
|
"catch_all": "",
|
||||||
|
"created_on": "",
|
||||||
|
"goto_ham": "",
|
||||||
|
"add_alias_expand": "",
|
||||||
|
"add_template": "",
|
||||||
|
"alias_domain_alias_hint": "",
|
||||||
|
"alias_domain_backupmx": "",
|
||||||
|
"all_domains": "",
|
||||||
|
"allow_from_smtp": "",
|
||||||
|
"allow_from_smtp_info": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"goto_spam": "",
|
||||||
|
"insert_preset": "",
|
||||||
|
"last_mail_login": "",
|
||||||
|
"last_pw_change": "",
|
||||||
|
"last_run_reset": "",
|
||||||
|
"mailbox": "",
|
||||||
|
"mailbox_defaults": "",
|
||||||
|
"no": "",
|
||||||
|
"open_logs": "",
|
||||||
|
"owner": "",
|
||||||
|
"private_comment": "",
|
||||||
|
"q_all": "",
|
||||||
|
"quarantine_category": "",
|
||||||
|
"recipient": "",
|
||||||
|
"relay_unknown": "",
|
||||||
|
"sender": "",
|
||||||
|
"sieve_preset_1": "",
|
||||||
|
"sieve_preset_5": "",
|
||||||
|
"syncjob_EXIT_TLS_FAILURE": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "",
|
||||||
|
"template": "",
|
||||||
|
"tls_policy_maps_enforced_tls": "",
|
||||||
|
"yes": "",
|
||||||
|
"table_size_show_n": "",
|
||||||
|
"mailbox_defaults_info": "",
|
||||||
|
"sieve_preset_header": "",
|
||||||
|
"mailbox_templates": "",
|
||||||
|
"sieve_preset_2": "",
|
||||||
|
"sieve_preset_3": "",
|
||||||
|
"sieve_preset_8": "",
|
||||||
|
"sogo_visible": "",
|
||||||
|
"sogo_visible_n": "",
|
||||||
|
"sogo_visible_y": "",
|
||||||
|
"spam_aliases": "",
|
||||||
|
"stats": "",
|
||||||
|
"syncjob_check_log": "",
|
||||||
|
"syncjob_last_run_result": "",
|
||||||
|
"syncjob_EX_OK": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_OVERQUOTA": "",
|
||||||
|
"sieve_preset_4": "",
|
||||||
|
"sieve_preset_6": "",
|
||||||
|
"sieve_preset_7": ""
|
||||||
},
|
},
|
||||||
"oauth2": {
|
"oauth2": {
|
||||||
"access_denied": "Inicie sesión como propietario del buzón para otorgar acceso a través de OAuth2.",
|
"access_denied": "Inicie sesión como propietario del buzón para otorgar acceso a través de OAuth2.",
|
||||||
@@ -609,10 +923,43 @@
|
|||||||
"subj": "Asunto",
|
"subj": "Asunto",
|
||||||
"text_from_html_content": "Contenido (html convertido)",
|
"text_from_html_content": "Contenido (html convertido)",
|
||||||
"text_plain_content": "Contenido (text/plain)",
|
"text_plain_content": "Contenido (text/plain)",
|
||||||
"toggle_all": "Seleccionar todos"
|
"toggle_all": "Seleccionar todos",
|
||||||
|
"spam": "",
|
||||||
|
"confirm": "",
|
||||||
|
"download_eml": "",
|
||||||
|
"info": "",
|
||||||
|
"deliver_inbox": "",
|
||||||
|
"junk_folder": "",
|
||||||
|
"notified": "",
|
||||||
|
"qinfo": "",
|
||||||
|
"quick_delete_link": "",
|
||||||
|
"quick_info_link": "",
|
||||||
|
"quick_release_link": "",
|
||||||
|
"refresh": "",
|
||||||
|
"rejected": "",
|
||||||
|
"rewrite_subject": "",
|
||||||
|
"rspamd_result": "",
|
||||||
|
"sender_header": "",
|
||||||
|
"settings_info": "",
|
||||||
|
"table_size": "",
|
||||||
|
"table_size_show_n": "",
|
||||||
|
"type": ""
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"queue_manager": "Administrador de cola"
|
"queue_manager": "Administrador de cola",
|
||||||
|
"legend": "",
|
||||||
|
"ays": "",
|
||||||
|
"deliver_mail": "",
|
||||||
|
"unhold_mail": "",
|
||||||
|
"unhold_mail_legend": "",
|
||||||
|
"delete": "",
|
||||||
|
"flush": "",
|
||||||
|
"info": "",
|
||||||
|
"deliver_mail_legend": "",
|
||||||
|
"hold_mail": "",
|
||||||
|
"hold_mail_legend": "",
|
||||||
|
"show_message": "",
|
||||||
|
"unban": ""
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"help": "Mostrar/Ocultar panel de ayuda",
|
"help": "Mostrar/Ocultar panel de ayuda",
|
||||||
@@ -668,7 +1015,43 @@
|
|||||||
"tls_policy_map_entry_saved": "Regla de póliza de TLS \"%s\" ha sido guardada",
|
"tls_policy_map_entry_saved": "Regla de póliza de TLS \"%s\" ha sido guardada",
|
||||||
"verified_totp_login": "Inicio de sesión TOTP verificado",
|
"verified_totp_login": "Inicio de sesión TOTP verificado",
|
||||||
"verified_webauthn_login": "Inicio de sesión WebAuthn verificado",
|
"verified_webauthn_login": "Inicio de sesión WebAuthn verificado",
|
||||||
"verified_yotp_login": "Inicio de sesión Yubico OTP verificado"
|
"verified_yotp_login": "Inicio de sesión Yubico OTP verificado",
|
||||||
|
"sogo_profile_reset": "",
|
||||||
|
"template_added": "",
|
||||||
|
"template_removed": "",
|
||||||
|
"ui_texts": "",
|
||||||
|
"upload_success": "",
|
||||||
|
"verified_fido2_login": "",
|
||||||
|
"admin_added": "",
|
||||||
|
"admin_api_modified": "",
|
||||||
|
"admin_removed": "",
|
||||||
|
"app_links": "",
|
||||||
|
"app_passwd_added": "",
|
||||||
|
"app_passwd_removed": "",
|
||||||
|
"cors_headers_edited": "",
|
||||||
|
"domain_add_dkim_available": "",
|
||||||
|
"dkim_duplicated": "",
|
||||||
|
"domain_footer_modified": "",
|
||||||
|
"dovecot_restart_success": "",
|
||||||
|
"eas_reset": "",
|
||||||
|
"f2b_banlist_refreshed": "",
|
||||||
|
"f2b_modified": "",
|
||||||
|
"global_filter_written": "",
|
||||||
|
"hash_deleted": "",
|
||||||
|
"ip_check_opt_in_modified": "",
|
||||||
|
"item_deleted": "",
|
||||||
|
"item_released": "",
|
||||||
|
"items_deleted": "",
|
||||||
|
"items_released": "",
|
||||||
|
"learned_ham": "",
|
||||||
|
"license_modified": "",
|
||||||
|
"nginx_reloaded": "",
|
||||||
|
"object_modified": "",
|
||||||
|
"password_policy_saved": "",
|
||||||
|
"pushover_settings_edited": "",
|
||||||
|
"reset_main_logo": "",
|
||||||
|
"resource_removed": "",
|
||||||
|
"template_modified": ""
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
"api_register": "%s utiliza la API de la nube de Yubico. Por favor, obtén una clave API para tu llave <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">aquí</a>.",
|
"api_register": "%s utiliza la API de la nube de Yubico. Por favor, obtén una clave API para tu llave <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">aquí</a>.",
|
||||||
@@ -688,7 +1071,15 @@
|
|||||||
"webauthn": "Autenticación WebAuthn",
|
"webauthn": "Autenticación WebAuthn",
|
||||||
"waiting_usb_auth": "<i>Esperando al dispositivo USB...</i><br><br>Toque el botón en su dispositivo USB WebAuthn ahora.",
|
"waiting_usb_auth": "<i>Esperando al dispositivo USB...</i><br><br>Toque el botón en su dispositivo USB WebAuthn ahora.",
|
||||||
"waiting_usb_register": "<i>Esperando al dispositivo USB....</i><br><br>Ingrese su contraseña arriba y confirme su registro WebAuthn tocando el botón en su dispositivo USB WebAuthn.",
|
"waiting_usb_register": "<i>Esperando al dispositivo USB....</i><br><br>Ingrese su contraseña arriba y confirme su registro WebAuthn tocando el botón en su dispositivo USB WebAuthn.",
|
||||||
"yubi_otp": "Yubico OTP"
|
"yubi_otp": "Yubico OTP",
|
||||||
|
"authenticators": "",
|
||||||
|
"u2f_deprecated": "",
|
||||||
|
"u2f_deprecated_important": "",
|
||||||
|
"error_code": "",
|
||||||
|
"init_webauthn": "",
|
||||||
|
"reload_retry": "",
|
||||||
|
"start_webauthn_validation": "",
|
||||||
|
"tfa_token_invalid": ""
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"action": "Acción",
|
"action": "Acción",
|
||||||
@@ -769,11 +1160,150 @@
|
|||||||
"waiting": "Esperando",
|
"waiting": "Esperando",
|
||||||
"week": "Semana",
|
"week": "Semana",
|
||||||
"weekly": "Cada semana",
|
"weekly": "Cada semana",
|
||||||
"weeks": "Semanas"
|
"weeks": "Semanas",
|
||||||
|
"fido2_webauthn": "",
|
||||||
|
"last_pw_change": "",
|
||||||
|
"save": "",
|
||||||
|
"pushover_info": "",
|
||||||
|
"client_configuration": "",
|
||||||
|
"created_on": "",
|
||||||
|
"q_add_header": "",
|
||||||
|
"active_sieve": "",
|
||||||
|
"apple_connection_profile_mailonly": "",
|
||||||
|
"attribute": "",
|
||||||
|
"email": "",
|
||||||
|
"expire_in": "",
|
||||||
|
"last_ui_login": "",
|
||||||
|
"loading": "",
|
||||||
|
"mailbox_general": "",
|
||||||
|
"mailbox_settings": "",
|
||||||
|
"messages": "",
|
||||||
|
"month": "",
|
||||||
|
"months": "",
|
||||||
|
"no_active_filter": "",
|
||||||
|
"password_repeat": "",
|
||||||
|
"pushover_evaluate_x_prio": "",
|
||||||
|
"pushover_only_x_prio": "",
|
||||||
|
"pushover_sender_array": "",
|
||||||
|
"pushover_sender_regex": "",
|
||||||
|
"pushover_text": "",
|
||||||
|
"pushover_title": "",
|
||||||
|
"pushover_sound": "",
|
||||||
|
"pushover_vars": "",
|
||||||
|
"q_reject": "",
|
||||||
|
"quarantine_category": "",
|
||||||
|
"recent_successful_connections": "",
|
||||||
|
"sogo_profile_reset_help": "",
|
||||||
|
"sogo_profile_reset_now": "",
|
||||||
|
"spam_score_reset": "",
|
||||||
|
"spamfilter_table_domain_policy": "",
|
||||||
|
"syncjob_last_run_result": "",
|
||||||
|
"syncjob_EX_OK": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_TLS_FAILURE": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_OVERQUOTA": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "",
|
||||||
|
"last_mail_login": "",
|
||||||
|
"pushover_verify": "",
|
||||||
|
"q_all": "",
|
||||||
|
"quarantine_category_info": "",
|
||||||
|
"create_app_passwd": "",
|
||||||
|
"no_last_login": "",
|
||||||
|
"sender_acl_disabled": "",
|
||||||
|
"show_sieve_filters": "",
|
||||||
|
"email_and_dav": "",
|
||||||
|
"empty": "",
|
||||||
|
"syncjob_check_log": "",
|
||||||
|
"login_history": "",
|
||||||
|
"mailbox": "",
|
||||||
|
"text": "",
|
||||||
|
"apple_connection_profile_with_app_password": "",
|
||||||
|
"change_password_hint_app_passwords": "",
|
||||||
|
"clear_recent_successful_connections": "",
|
||||||
|
"delete_ays": "",
|
||||||
|
"direct_protocol_access": "",
|
||||||
|
"force_pw_update": "",
|
||||||
|
"from": "",
|
||||||
|
"generate": "",
|
||||||
|
"in_use": "",
|
||||||
|
"open_logs": "",
|
||||||
|
"open_webmail_sso": "",
|
||||||
|
"title": "",
|
||||||
|
"value": "",
|
||||||
|
"verify": "",
|
||||||
|
"with_app_password": "",
|
||||||
|
"year": "",
|
||||||
|
"years": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"app_hint": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"app_name": "",
|
||||||
|
"app_passwds": "",
|
||||||
|
"apple_connection_profile": "",
|
||||||
|
"apple_connection_profile_complete": "",
|
||||||
|
"password": ""
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"domain_added_sogo_failed": "Se agregó el dominio pero no se pudo reiniciar SOGo, revisa los logs del servidor.",
|
"domain_added_sogo_failed": "Se agregó el dominio pero no se pudo reiniciar SOGo, revisa los logs del servidor.",
|
||||||
"fuzzy_learn_error": "Error aprendiendo hash: %s",
|
"fuzzy_learn_error": "Error aprendiendo hash: %s",
|
||||||
"ip_invalid": "IP inválida omitida: %s"
|
"ip_invalid": "IP inválida omitida: %s",
|
||||||
|
"session_token": "",
|
||||||
|
"session_ua": "",
|
||||||
|
"cannot_delete_self": "",
|
||||||
|
"dovecot_restart_failed": "",
|
||||||
|
"hash_not_found": "",
|
||||||
|
"is_not_primary_alias": "",
|
||||||
|
"no_active_admin": "",
|
||||||
|
"quota_exceeded_scope": ""
|
||||||
|
},
|
||||||
|
"datatables": {
|
||||||
|
"decimal": "",
|
||||||
|
"infoPostFix": "",
|
||||||
|
"paginate": {
|
||||||
|
"last": "",
|
||||||
|
"first": "",
|
||||||
|
"next": "",
|
||||||
|
"previous": ""
|
||||||
|
},
|
||||||
|
"collapse_all": "",
|
||||||
|
"emptyTable": "",
|
||||||
|
"expand_all": "",
|
||||||
|
"info": "",
|
||||||
|
"infoEmpty": "",
|
||||||
|
"infoFiltered": "",
|
||||||
|
"thousands": "",
|
||||||
|
"lengthMenu": "",
|
||||||
|
"loadingRecords": "",
|
||||||
|
"processing": "",
|
||||||
|
"search": "",
|
||||||
|
"zeroRecords": "",
|
||||||
|
"aria": {
|
||||||
|
"sortAscending": "",
|
||||||
|
"sortDescending": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fido2": {
|
||||||
|
"set_fn": "",
|
||||||
|
"start_fido2_validation": "",
|
||||||
|
"confirm": "",
|
||||||
|
"fido2_auth": "",
|
||||||
|
"fido2_success": "",
|
||||||
|
"fido2_validation_failed": "",
|
||||||
|
"fn": "",
|
||||||
|
"known_ids": "",
|
||||||
|
"none": "",
|
||||||
|
"register_status": "",
|
||||||
|
"rename": "",
|
||||||
|
"set_fido2": "",
|
||||||
|
"set_fido2_touchid": ""
|
||||||
|
},
|
||||||
|
"ratelimit": {
|
||||||
|
"disabled": "",
|
||||||
|
"second": "",
|
||||||
|
"minute": "",
|
||||||
|
"hour": "",
|
||||||
|
"day": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+431
-17
@@ -20,7 +20,15 @@
|
|||||||
"spam_score": "Roskapostitulos",
|
"spam_score": "Roskapostitulos",
|
||||||
"syncjobs": "Synkronoi työt",
|
"syncjobs": "Synkronoi työt",
|
||||||
"tls_policy": "TLS-käytäntö",
|
"tls_policy": "TLS-käytäntö",
|
||||||
"unlimited_quota": "Rajoittamaton kiintiö sähkö postilaatikoille"
|
"unlimited_quota": "Rajoittamaton kiintiö sähkö postilaatikoille",
|
||||||
|
"app_passwds": "",
|
||||||
|
"domain_desc": "",
|
||||||
|
"domain_relayhost": "",
|
||||||
|
"mailbox_relayhost": "",
|
||||||
|
"protocol_access": "",
|
||||||
|
"pushover": "",
|
||||||
|
"quarantine_category": "",
|
||||||
|
"smtp_ip_access": ""
|
||||||
},
|
},
|
||||||
"add": {
|
"add": {
|
||||||
"activate_filter_warn": "Kaikki muut suodattimet deaktivoidaan, kun aktiivinen on valittu.",
|
"activate_filter_warn": "Kaikki muut suodattimet deaktivoidaan, kun aktiivinen on valittu.",
|
||||||
@@ -90,7 +98,17 @@
|
|||||||
"timeout2": "Aikakatkaisu yhteyden muodostamiseen paikalliseen isäntään",
|
"timeout2": "Aikakatkaisu yhteyden muodostamiseen paikalliseen isäntään",
|
||||||
"username": "Käyttäjätunnus",
|
"username": "Käyttäjätunnus",
|
||||||
"validate": "Vahvista",
|
"validate": "Vahvista",
|
||||||
"validation_success": "Vahvistettu onnistuneesti"
|
"validation_success": "Vahvistettu onnistuneesti",
|
||||||
|
"dry": "",
|
||||||
|
"tags": "",
|
||||||
|
"inactive": "",
|
||||||
|
"relay_transport_info": "",
|
||||||
|
"app_name": "",
|
||||||
|
"app_password": "",
|
||||||
|
"app_passwd_protocols": "",
|
||||||
|
"bcc_dest_format": "",
|
||||||
|
"disable_login": "",
|
||||||
|
"relay_unknown_only": ""
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"access": "Hallinta",
|
"access": "Hallinta",
|
||||||
@@ -267,7 +285,73 @@
|
|||||||
"upload": "Lataa",
|
"upload": "Lataa",
|
||||||
"username": "Käyttäjätunnus",
|
"username": "Käyttäjätunnus",
|
||||||
"validate_license_now": "Vahvista GUID-tunnus lisenssi palvelinta vastaan",
|
"validate_license_now": "Vahvista GUID-tunnus lisenssi palvelinta vastaan",
|
||||||
"yes": "✓"
|
"yes": "✓",
|
||||||
|
"logo_normal_label": "",
|
||||||
|
"logo_dark_label": "",
|
||||||
|
"copy_to_clipboard": "",
|
||||||
|
"f2b_ban_time_increment": "",
|
||||||
|
"f2b_manage_external": "",
|
||||||
|
"f2b_manage_external_info": "",
|
||||||
|
"f2b_max_ban_time": "",
|
||||||
|
"allowed_methods": "",
|
||||||
|
"allowed_origins": "",
|
||||||
|
"cors_settings": "",
|
||||||
|
"ip_check": "",
|
||||||
|
"rspamd_global_filters": "",
|
||||||
|
"service": "",
|
||||||
|
"password_policy_lowerupper": "",
|
||||||
|
"password_policy_numbers": "",
|
||||||
|
"quarantine_max_score": "",
|
||||||
|
"quarantine_redirect": "",
|
||||||
|
"domain_admin": "",
|
||||||
|
"f2b_filter": "",
|
||||||
|
"f2b_regex_info": "",
|
||||||
|
"html": "",
|
||||||
|
"oauth2_apps": "",
|
||||||
|
"oauth2_add_client": "",
|
||||||
|
"optional": "",
|
||||||
|
"options": "",
|
||||||
|
"password_length": "",
|
||||||
|
"password_policy_chars": "",
|
||||||
|
"rspamd_global_filters_agree": "",
|
||||||
|
"rspamd_global_filters_info": "",
|
||||||
|
"sal_level": "",
|
||||||
|
"success": "",
|
||||||
|
"title": "",
|
||||||
|
"transport_test_rcpt_info": "",
|
||||||
|
"ui_header_announcement": "",
|
||||||
|
"ui_header_announcement_active": "",
|
||||||
|
"ui_header_announcement_content": "",
|
||||||
|
"password_policy": "",
|
||||||
|
"password_policy_length": "",
|
||||||
|
"queue_unban": "",
|
||||||
|
"relay_rcpt": "",
|
||||||
|
"rsettings_preset_3": "",
|
||||||
|
"rsettings_preset_4": "",
|
||||||
|
"ui_header_announcement_help": "",
|
||||||
|
"ui_header_announcement_select": "",
|
||||||
|
"ui_header_announcement_type": "",
|
||||||
|
"ui_header_announcement_type_danger": "",
|
||||||
|
"ui_header_announcement_type_info": "",
|
||||||
|
"ui_header_announcement_type_warning": "",
|
||||||
|
"verify": "",
|
||||||
|
"ip_check_disabled": "",
|
||||||
|
"ip_check_opt_in": "",
|
||||||
|
"is_mx_based": "",
|
||||||
|
"login_time": "",
|
||||||
|
"rspamd_global_filters_regex": "",
|
||||||
|
"password_policy_special_chars": "",
|
||||||
|
"quarantine_bcc": "",
|
||||||
|
"regex_maps": "",
|
||||||
|
"dkim_overwrite_key": "",
|
||||||
|
"admins": "",
|
||||||
|
"admins_ldap": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"api_read_only": "",
|
||||||
|
"api_read_write": "",
|
||||||
|
"api_skip_ip_check": "",
|
||||||
|
"ays": "",
|
||||||
|
"convert_html_to_text": ""
|
||||||
},
|
},
|
||||||
"danger": {
|
"danger": {
|
||||||
"access_denied": "Käyttö estetty tai lomake tiedot eivät kelpaa",
|
"access_denied": "Käyttö estetty tai lomake tiedot eivät kelpaa",
|
||||||
@@ -369,7 +453,36 @@
|
|||||||
"username_invalid": "Käyttäjätunnusta %s ei voi käyttää",
|
"username_invalid": "Käyttäjätunnusta %s ei voi käyttää",
|
||||||
"validity_missing": "Anna voimassaolo aika",
|
"validity_missing": "Anna voimassaolo aika",
|
||||||
"value_missing": "Anna kaikki arvot",
|
"value_missing": "Anna kaikki arvot",
|
||||||
"yotp_verification_failed": "Yubico OTP todentaminen epäonnistui: %s"
|
"yotp_verification_failed": "Yubico OTP todentaminen epäonnistui: %s",
|
||||||
|
"webauthn_authenticator_failed": "",
|
||||||
|
"webauthn_publickey_failed": "",
|
||||||
|
"cors_invalid_origin": "",
|
||||||
|
"file_open_error": "",
|
||||||
|
"global_filter_write_error": "",
|
||||||
|
"global_map_invalid": "",
|
||||||
|
"global_map_write_error": "",
|
||||||
|
"ham_learn_error": "",
|
||||||
|
"invalid_filter_type": "",
|
||||||
|
"cors_invalid_method": "",
|
||||||
|
"pushover_key": "",
|
||||||
|
"img_dimensions_exceeded": "",
|
||||||
|
"img_size_exceeded": "",
|
||||||
|
"nginx_reload_failed": "",
|
||||||
|
"pushover_credentials_missing": "",
|
||||||
|
"pushover_token": "",
|
||||||
|
"template_exists": "",
|
||||||
|
"template_id_invalid": "",
|
||||||
|
"template_name_invalid": "",
|
||||||
|
"tls_policy_map_dest_invalid": "",
|
||||||
|
"webauthn_username_failed": "",
|
||||||
|
"app_name_empty": "",
|
||||||
|
"app_passwd_id_invalid": "",
|
||||||
|
"demo_mode_enabled": "",
|
||||||
|
"dkim_domain_or_sel_exists": "",
|
||||||
|
"extended_sender_acl_denied": "",
|
||||||
|
"fido2_verification_failed": "",
|
||||||
|
"reset_f2b_regex": "",
|
||||||
|
"tfa_token_invalid": ""
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"containers_info": "Säilön tiedot",
|
"containers_info": "Säilön tiedot",
|
||||||
@@ -389,7 +502,28 @@
|
|||||||
"uptime": "Päällä",
|
"uptime": "Päällä",
|
||||||
"started_on": "Aloitettiin",
|
"started_on": "Aloitettiin",
|
||||||
"static_logs": "Staattiset lokit",
|
"static_logs": "Staattiset lokit",
|
||||||
"system_containers": "Systeemi & Säiliöt"
|
"system_containers": "Systeemi & Säiliöt",
|
||||||
|
"memory": "",
|
||||||
|
"architecture": "",
|
||||||
|
"online_users": "",
|
||||||
|
"error_show_ip": "",
|
||||||
|
"success": "",
|
||||||
|
"wip": "",
|
||||||
|
"container_disabled": "",
|
||||||
|
"history_all_servers": "",
|
||||||
|
"service": "",
|
||||||
|
"timezone": "",
|
||||||
|
"update_available": "",
|
||||||
|
"no_update_available": "",
|
||||||
|
"update_failed": "",
|
||||||
|
"username": "",
|
||||||
|
"chart_this_server": "",
|
||||||
|
"container_running": "",
|
||||||
|
"container_stopped": "",
|
||||||
|
"cores": "",
|
||||||
|
"login_time": "",
|
||||||
|
"current_time": "",
|
||||||
|
"show_ip": ""
|
||||||
},
|
},
|
||||||
"diagnostics": {
|
"diagnostics": {
|
||||||
"cname_from_a": "Arvo johdettu A / AAAA-tietueesta. Tätä tuetaan niin kauan kuin tietue osoittaa oikealle resurssille.",
|
"cname_from_a": "Arvo johdettu A / AAAA-tietueesta. Tätä tuetaan niin kauan kuin tietue osoittaa oikealle resurssille.",
|
||||||
@@ -399,7 +533,8 @@
|
|||||||
"dns_records_name": "Nimi",
|
"dns_records_name": "Nimi",
|
||||||
"dns_records_status": "Nykyinen tila",
|
"dns_records_status": "Nykyinen tila",
|
||||||
"dns_records_type": "Tyyppi",
|
"dns_records_type": "Tyyppi",
|
||||||
"optional": "Tämä tietue on valinnainen."
|
"optional": "Tämä tietue on valinnainen.",
|
||||||
|
"dns_records_docs": ""
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"active": "Aktiivinen",
|
"active": "Aktiivinen",
|
||||||
@@ -480,7 +615,57 @@
|
|||||||
"title": "Muokkaa objektia",
|
"title": "Muokkaa objektia",
|
||||||
"unchanged_if_empty": "Jos muuttumaton jätä tyhjäksi",
|
"unchanged_if_empty": "Jos muuttumaton jätä tyhjäksi",
|
||||||
"username": "Käyttäjätunnus",
|
"username": "Käyttäjätunnus",
|
||||||
"validate_save": "Vahvista ja tallenna"
|
"validate_save": "Vahvista ja tallenna",
|
||||||
|
"lookup_mx": "",
|
||||||
|
"footer_exclude": "",
|
||||||
|
"custom_attributes": "",
|
||||||
|
"domain_footer_info_vars": {
|
||||||
|
"from_addr": "",
|
||||||
|
"from_domain": "",
|
||||||
|
"auth_user": "",
|
||||||
|
"from_user": "",
|
||||||
|
"from_name": "",
|
||||||
|
"custom": ""
|
||||||
|
},
|
||||||
|
"allow_from_smtp_info": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"app_name": "",
|
||||||
|
"app_passwd": "",
|
||||||
|
"app_passwd_protocols": "",
|
||||||
|
"created_on": "",
|
||||||
|
"sogo_access": "",
|
||||||
|
"sogo_access_info": "",
|
||||||
|
"domain_footer_skip_replies": "",
|
||||||
|
"domain_footer_plain": "",
|
||||||
|
"domain_footer": "",
|
||||||
|
"domain_footer_html": "",
|
||||||
|
"ratelimit": "",
|
||||||
|
"acl": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"allow_from_smtp": "",
|
||||||
|
"domain_footer_info": "",
|
||||||
|
"none_inherit": "",
|
||||||
|
"pushover_only_x_prio": "",
|
||||||
|
"pushover_sender_regex": "",
|
||||||
|
"pushover_verify": "",
|
||||||
|
"quota_warning_bcc_info": "",
|
||||||
|
"relay_transport_info": "",
|
||||||
|
"relay_unknown_only": "",
|
||||||
|
"spam_filter": "",
|
||||||
|
"pushover_text": "",
|
||||||
|
"admin": "",
|
||||||
|
"generate": "",
|
||||||
|
"delete_ays": "",
|
||||||
|
"disable_login": "",
|
||||||
|
"pushover": "",
|
||||||
|
"pushover_info": "",
|
||||||
|
"pushover_sender_array": "",
|
||||||
|
"pushover_title": "",
|
||||||
|
"pushover_sound": "",
|
||||||
|
"mailbox_relayhost_info": "",
|
||||||
|
"pushover_vars": "",
|
||||||
|
"pushover_evaluate_x_prio": "",
|
||||||
|
"quota_warning_bcc": ""
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"cancel": "Peruuta",
|
"cancel": "Peruuta",
|
||||||
@@ -493,7 +678,9 @@
|
|||||||
"restart_container": "Uudelleen käynnistä moottori",
|
"restart_container": "Uudelleen käynnistä moottori",
|
||||||
"restart_container_info": "<b>Tärkeää:</b> Uudelleenkäynnistys voi kestää jonkin aikaa, odota, kunnes se päättyy.",
|
"restart_container_info": "<b>Tärkeää:</b> Uudelleenkäynnistys voi kestää jonkin aikaa, odota, kunnes se päättyy.",
|
||||||
"restart_now": "Käynnistä uudelleen nyt",
|
"restart_now": "Käynnistä uudelleen nyt",
|
||||||
"restarting_container": "Uudelleen käynnistä container, tämä saattaa kestää jonkin aikaa..."
|
"restarting_container": "Uudelleen käynnistä container, tämä saattaa kestää jonkin aikaa...",
|
||||||
|
"hibp_check": "",
|
||||||
|
"nothing_selected": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"administration": "Kokoonpanon & tiedot",
|
"administration": "Kokoonpanon & tiedot",
|
||||||
@@ -504,7 +691,8 @@
|
|||||||
"quarantine": "Karanteeni",
|
"quarantine": "Karanteeni",
|
||||||
"restart_netfilter": "Uudelleen käynnistä netfilter",
|
"restart_netfilter": "Uudelleen käynnistä netfilter",
|
||||||
"restart_sogo": "Uudelleen käynnistä SOGo",
|
"restart_sogo": "Uudelleen käynnistä SOGo",
|
||||||
"user_settings": "Käyttäjän asetukset"
|
"user_settings": "Käyttäjän asetukset",
|
||||||
|
"mailcow_system": ""
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"awaiting_tfa_confirmation": "Odotetaan TFA-vahvistusta",
|
"awaiting_tfa_confirmation": "Odotetaan TFA-vahvistusta",
|
||||||
@@ -515,7 +703,10 @@
|
|||||||
"delayed": "Kirjautuminen viivästyi %s sekunttia.",
|
"delayed": "Kirjautuminen viivästyi %s sekunttia.",
|
||||||
"login": "Kirjaudu",
|
"login": "Kirjaudu",
|
||||||
"password": "Salasana",
|
"password": "Salasana",
|
||||||
"username": "Käyttäjätunnus"
|
"username": "Käyttäjätunnus",
|
||||||
|
"fido2_webauthn": "",
|
||||||
|
"mobileconfig_info": "",
|
||||||
|
"other_logins": ""
|
||||||
},
|
},
|
||||||
"mailbox": {
|
"mailbox": {
|
||||||
"action": "Toiminnot",
|
"action": "Toiminnot",
|
||||||
@@ -639,7 +830,58 @@
|
|||||||
"username": "Käyttäjätunnus",
|
"username": "Käyttäjätunnus",
|
||||||
"waiting": "Odotetaan..",
|
"waiting": "Odotetaan..",
|
||||||
"weekly": "Viikoittain",
|
"weekly": "Viikoittain",
|
||||||
"yes": "✓"
|
"yes": "✓",
|
||||||
|
"templates": "",
|
||||||
|
"catch_all": "",
|
||||||
|
"created_on": "",
|
||||||
|
"disable_login": "",
|
||||||
|
"domain_templates": "",
|
||||||
|
"goto_ham": "",
|
||||||
|
"goto_spam": "",
|
||||||
|
"template": "",
|
||||||
|
"sieve_preset_8": "",
|
||||||
|
"allow_from_smtp_info": "",
|
||||||
|
"sieve_preset_header": "",
|
||||||
|
"sender": "",
|
||||||
|
"sieve_preset_6": "",
|
||||||
|
"sieve_preset_7": "",
|
||||||
|
"insert_preset": "",
|
||||||
|
"last_mail_login": "",
|
||||||
|
"add_template": "",
|
||||||
|
"alias_domain_alias_hint": "",
|
||||||
|
"all_domains": "",
|
||||||
|
"allow_from_smtp": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"q_reject": "",
|
||||||
|
"quarantine_category": "",
|
||||||
|
"recipient": "",
|
||||||
|
"relay_unknown": "",
|
||||||
|
"stats": "",
|
||||||
|
"syncjob_check_log": "",
|
||||||
|
"syncjob_last_run_result": "",
|
||||||
|
"syncjob_EX_OK": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_TLS_FAILURE": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_OVERQUOTA": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "",
|
||||||
|
"table_size": "",
|
||||||
|
"table_size_show_n": "",
|
||||||
|
"tls_policy_maps_enforced_tls": "",
|
||||||
|
"last_pw_change": "",
|
||||||
|
"open_logs": "",
|
||||||
|
"q_add_header": "",
|
||||||
|
"q_all": "",
|
||||||
|
"mailbox_defaults": "",
|
||||||
|
"mailbox_defaults_info": "",
|
||||||
|
"mailbox_templates": "",
|
||||||
|
"sieve_preset_1": "",
|
||||||
|
"sieve_preset_2": "",
|
||||||
|
"sieve_preset_3": "",
|
||||||
|
"sieve_preset_4": "",
|
||||||
|
"sieve_preset_5": "",
|
||||||
|
"add_alias_expand": ""
|
||||||
},
|
},
|
||||||
"oauth2": {
|
"oauth2": {
|
||||||
"access_denied": "Kirjaudu sisään postilaatikon omistajana myöntääksesi käyttöoikeuden OAuth2: n kautta.",
|
"access_denied": "Kirjaudu sisään postilaatikon omistajana myöntääksesi käyttöoikeuden OAuth2: n kautta.",
|
||||||
@@ -683,10 +925,41 @@
|
|||||||
"subj": "Aihe",
|
"subj": "Aihe",
|
||||||
"text_from_html_content": "Sisältö (muunnettu html)",
|
"text_from_html_content": "Sisältö (muunnettu html)",
|
||||||
"text_plain_content": "Sisältö (teksti / tavallinen)",
|
"text_plain_content": "Sisältö (teksti / tavallinen)",
|
||||||
"toggle_all": "Valitse kaikki"
|
"toggle_all": "Valitse kaikki",
|
||||||
|
"spam": "",
|
||||||
|
"deliver_inbox": "",
|
||||||
|
"info": "",
|
||||||
|
"type": "",
|
||||||
|
"quick_info_link": "",
|
||||||
|
"table_size": "",
|
||||||
|
"table_size_show_n": "",
|
||||||
|
"confirm": "",
|
||||||
|
"qinfo": "",
|
||||||
|
"sender_header": "",
|
||||||
|
"settings_info": "",
|
||||||
|
"rewrite_subject": "",
|
||||||
|
"junk_folder": "",
|
||||||
|
"notified": "",
|
||||||
|
"quick_release_link": "",
|
||||||
|
"rejected": "",
|
||||||
|
"quick_delete_link": "",
|
||||||
|
"refresh": ""
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"queue_manager": "Jonon hallinta"
|
"queue_manager": "Jonon hallinta",
|
||||||
|
"deliver_mail": "",
|
||||||
|
"deliver_mail_legend": "",
|
||||||
|
"delete": "",
|
||||||
|
"flush": "",
|
||||||
|
"info": "",
|
||||||
|
"legend": "",
|
||||||
|
"ays": "",
|
||||||
|
"hold_mail": "",
|
||||||
|
"hold_mail_legend": "",
|
||||||
|
"show_message": "",
|
||||||
|
"unban": "",
|
||||||
|
"unhold_mail": "",
|
||||||
|
"unhold_mail_legend": ""
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"help": "Näytä/Piilota help paneeli",
|
"help": "Näytä/Piilota help paneeli",
|
||||||
@@ -761,7 +1034,24 @@
|
|||||||
"upload_success": "Tiedosto ladattu onnistuneesti",
|
"upload_success": "Tiedosto ladattu onnistuneesti",
|
||||||
"verified_totp_login": "Vahvistettu TOTP-kirjautuminen",
|
"verified_totp_login": "Vahvistettu TOTP-kirjautuminen",
|
||||||
"verified_webauthn_login": "Vahvistettu WebAuthn kirjautuminen",
|
"verified_webauthn_login": "Vahvistettu WebAuthn kirjautuminen",
|
||||||
"verified_yotp_login": "Vahvistettu Yubico OTP kirjautuminen"
|
"verified_yotp_login": "Vahvistettu Yubico OTP kirjautuminen",
|
||||||
|
"app_passwd_added": "",
|
||||||
|
"global_filter_written": "",
|
||||||
|
"template_added": "",
|
||||||
|
"template_modified": "",
|
||||||
|
"app_passwd_removed": "",
|
||||||
|
"cors_headers_edited": "",
|
||||||
|
"domain_add_dkim_available": "",
|
||||||
|
"domain_footer_modified": "",
|
||||||
|
"dovecot_restart_success": "",
|
||||||
|
"f2b_banlist_refreshed": "",
|
||||||
|
"ip_check_opt_in_modified": "",
|
||||||
|
"learned_ham": "",
|
||||||
|
"nginx_reloaded": "",
|
||||||
|
"password_policy_saved": "",
|
||||||
|
"pushover_settings_edited": "",
|
||||||
|
"template_removed": "",
|
||||||
|
"verified_fido2_login": ""
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
"api_register": "%s käyttää Yubico Cloud API. Saat avaimesi API-avaimen <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">täältä</a>",
|
"api_register": "%s käyttää Yubico Cloud API. Saat avaimesi API-avaimen <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">täältä</a>",
|
||||||
@@ -785,7 +1075,11 @@
|
|||||||
"webauthn": "WebAuthn todennus",
|
"webauthn": "WebAuthn todennus",
|
||||||
"waiting_usb_auth": "<i>Odotetaan USB-laitetta...</i><br><br>Napauta painiketta WebAuthn USB-laitteessa nyt",
|
"waiting_usb_auth": "<i>Odotetaan USB-laitetta...</i><br><br>Napauta painiketta WebAuthn USB-laitteessa nyt",
|
||||||
"waiting_usb_register": "<i>Odotetaan USB-laitetta...</i><br><br>Anna salasanasi yltä ja vahvista WebAuthn-rekisteröinti napauttamalla painiketta WebAuthn USB-laitteessa.",
|
"waiting_usb_register": "<i>Odotetaan USB-laitetta...</i><br><br>Anna salasanasi yltä ja vahvista WebAuthn-rekisteröinti napauttamalla painiketta WebAuthn USB-laitteessa.",
|
||||||
"yubi_otp": "Yubico OTP-todennus"
|
"yubi_otp": "Yubico OTP-todennus",
|
||||||
|
"authenticators": "",
|
||||||
|
"tfa_token_invalid": "",
|
||||||
|
"u2f_deprecated": "",
|
||||||
|
"u2f_deprecated_important": ""
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"action": "Toiminnot",
|
"action": "Toiminnot",
|
||||||
@@ -880,7 +1174,76 @@
|
|||||||
"waiting": "Odottaa",
|
"waiting": "Odottaa",
|
||||||
"week": "Viikko",
|
"week": "Viikko",
|
||||||
"weekly": "Viikoittain",
|
"weekly": "Viikoittain",
|
||||||
"weeks": "Viikkoa"
|
"weeks": "Viikkoa",
|
||||||
|
"attribute": "",
|
||||||
|
"last_ui_login": "",
|
||||||
|
"allowed_protocols": "",
|
||||||
|
"delete_ays": "",
|
||||||
|
"email_and_dav": "",
|
||||||
|
"empty": "",
|
||||||
|
"fido2_webauthn": "",
|
||||||
|
"text": "",
|
||||||
|
"generate": "",
|
||||||
|
"last_mail_login": "",
|
||||||
|
"last_pw_change": "",
|
||||||
|
"login_history": "",
|
||||||
|
"mailbox_general": "",
|
||||||
|
"mailbox_settings": "",
|
||||||
|
"month": "",
|
||||||
|
"months": "",
|
||||||
|
"no_last_login": "",
|
||||||
|
"open_logs": "",
|
||||||
|
"open_webmail_sso": "",
|
||||||
|
"password": "",
|
||||||
|
"password_repeat": "",
|
||||||
|
"pushover_evaluate_x_prio": "",
|
||||||
|
"pushover_info": "",
|
||||||
|
"pushover_sender_array": "",
|
||||||
|
"recent_successful_connections": "",
|
||||||
|
"value": "",
|
||||||
|
"mailbox": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_TLS_FAILURE": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE": "",
|
||||||
|
"syncjob_EXIT_OVERQUOTA": "",
|
||||||
|
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "",
|
||||||
|
"syncjob_last_run_result": "",
|
||||||
|
"syncjob_EX_OK": "",
|
||||||
|
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "",
|
||||||
|
"title": "",
|
||||||
|
"verify": "",
|
||||||
|
"with_app_password": "",
|
||||||
|
"year": "",
|
||||||
|
"years": "",
|
||||||
|
"advanced_settings": "",
|
||||||
|
"app_hint": "",
|
||||||
|
"app_name": "",
|
||||||
|
"app_passwds": "",
|
||||||
|
"apple_connection_profile": "",
|
||||||
|
"apple_connection_profile_mailonly": "",
|
||||||
|
"apple_connection_profile_with_app_password": "",
|
||||||
|
"change_password_hint_app_passwords": "",
|
||||||
|
"clear_recent_successful_connections": "",
|
||||||
|
"create_app_passwd": "",
|
||||||
|
"created_on": "",
|
||||||
|
"direct_protocol_access": "",
|
||||||
|
"email": "",
|
||||||
|
"apple_connection_profile_complete": "",
|
||||||
|
"pushover_sender_regex": "",
|
||||||
|
"pushover_text": "",
|
||||||
|
"pushover_title": "",
|
||||||
|
"pushover_sound": "",
|
||||||
|
"pushover_vars": "",
|
||||||
|
"pushover_verify": "",
|
||||||
|
"q_add_header": "",
|
||||||
|
"q_all": "",
|
||||||
|
"q_reject": "",
|
||||||
|
"quarantine_category": "",
|
||||||
|
"quarantine_category_info": "",
|
||||||
|
"pushover_only_x_prio": "",
|
||||||
|
"from": "",
|
||||||
|
"save": "",
|
||||||
|
"syncjob_check_log": ""
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"cannot_delete_self": "Kirjautuneen käyttäjän poistaminen ei onnistu",
|
"cannot_delete_self": "Kirjautuneen käyttäjän poistaminen ei onnistu",
|
||||||
@@ -890,6 +1253,57 @@
|
|||||||
"ip_invalid": "Ohitettu virheellinen IP-osoite: %s",
|
"ip_invalid": "Ohitettu virheellinen IP-osoite: %s",
|
||||||
"no_active_admin": "Viimeistä aktiivista järjestelmänvalvojaa ei voi poistaa käytöstä",
|
"no_active_admin": "Viimeistä aktiivista järjestelmänvalvojaa ei voi poistaa käytöstä",
|
||||||
"session_token": "Lomakkeen tunnus sanoma ei kelpaa: tunnus sanoman risti riita",
|
"session_token": "Lomakkeen tunnus sanoma ei kelpaa: tunnus sanoman risti riita",
|
||||||
"session_ua": "Lomakkeen tunnus sanoma ei kelpaa: käyttäjä agentin tarkistus virhe"
|
"session_ua": "Lomakkeen tunnus sanoma ei kelpaa: käyttäjä agentin tarkistus virhe",
|
||||||
|
"is_not_primary_alias": "",
|
||||||
|
"quota_exceeded_scope": "",
|
||||||
|
"dovecot_restart_failed": ""
|
||||||
|
},
|
||||||
|
"datatables": {
|
||||||
|
"emptyTable": "Tietoja ei ole saatavilla taulukossa",
|
||||||
|
"expand_all": "Laajenna kaikki",
|
||||||
|
"lengthMenu": "Näytä menu merkinnät",
|
||||||
|
"loadingRecords": "Ladataan...",
|
||||||
|
"processing": "Ole hyvä ja odota...",
|
||||||
|
"search": "Etsi:",
|
||||||
|
"paginate": {
|
||||||
|
"first": "Ensimmäinen",
|
||||||
|
"last": "Edellinen",
|
||||||
|
"next": "",
|
||||||
|
"previous": ""
|
||||||
|
},
|
||||||
|
"infoPostFix": "",
|
||||||
|
"decimal": "",
|
||||||
|
"collapse_all": "",
|
||||||
|
"info": "",
|
||||||
|
"infoEmpty": "",
|
||||||
|
"infoFiltered": "",
|
||||||
|
"thousands": "",
|
||||||
|
"zeroRecords": "",
|
||||||
|
"aria": {
|
||||||
|
"sortAscending": "",
|
||||||
|
"sortDescending": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fido2": {
|
||||||
|
"confirm": "",
|
||||||
|
"fido2_auth": "",
|
||||||
|
"fido2_validation_failed": "",
|
||||||
|
"fn": "",
|
||||||
|
"known_ids": "",
|
||||||
|
"register_status": "",
|
||||||
|
"rename": "",
|
||||||
|
"set_fido2": "",
|
||||||
|
"fido2_success": "",
|
||||||
|
"none": "",
|
||||||
|
"set_fido2_touchid": "",
|
||||||
|
"set_fn": "",
|
||||||
|
"start_fido2_validation": ""
|
||||||
|
},
|
||||||
|
"ratelimit": {
|
||||||
|
"disabled": "",
|
||||||
|
"second": "",
|
||||||
|
"minute": "",
|
||||||
|
"hour": "",
|
||||||
|
"day": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+361
-170
File diff suppressed because it is too large
Load Diff
+1291
-2
File diff suppressed because it is too large
Load Diff
+913
-15
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user