mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-02-19 15:46:23 +00:00
Compare commits
279 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
a80b5b7dd0 | ||
|
|
392967d664 | ||
|
|
d4dd1e37ce | ||
|
|
a8dfa95126 | ||
|
|
3b3c2b7141 | ||
|
|
f55c3c0887 | ||
|
|
f423ad77f3 | ||
|
|
8ba1e1ba9e | ||
|
|
55576084fc | ||
|
|
03311b06c9 | ||
|
|
b5c3d01834 | ||
|
|
f398ecbe39 | ||
|
|
8f1ae0f099 | ||
|
|
c8bee57732 | ||
|
|
85641794c3 | ||
|
|
849decaa59 | ||
|
|
6e88550f92 | ||
|
|
7c52483887 | ||
|
|
0aa520c030 | ||
|
|
548999f163 | ||
|
|
63df547306 | ||
|
|
547d2ca308 | ||
|
|
46b995f9e3 | ||
|
|
4f109c1a94 | ||
|
|
1fdf704cb4 | ||
|
|
5ec9c4c750 | ||
|
|
28cec99699 | ||
|
|
3e194c7906 | ||
|
|
afed94cc0e | ||
|
|
6f48c5ace0 | ||
|
|
9a7e1c2b5a | ||
|
|
2ef7539d55 | ||
|
|
4e52542e33 | ||
|
|
a1895ad924 | ||
|
|
d5a2c96887 | ||
|
|
3f30fe3113 | ||
|
|
d89f24a1a3 | ||
|
|
413354ff29 | ||
|
|
a28ba5bebb | ||
|
|
b93375b671 | ||
|
|
f39005b72d | ||
|
|
b568a33581 | ||
|
|
b05ef8edac | ||
|
|
015f9b663f | ||
|
|
b6167257c9 | ||
|
|
687fe044b2 | ||
|
|
cfa47eb873 | ||
|
|
7079000ee0 | ||
|
|
f60c4f39ee | ||
|
|
473713219f | ||
|
|
03ed81dc3f | ||
|
|
53543ccf26 | ||
|
|
3b183933e3 | ||
|
|
6c6fde8e2e | ||
|
|
61e23b6b81 | ||
|
|
6c649debc9 | ||
|
|
87b0683f77 | ||
|
|
59c1e7a18a | ||
|
|
4f9dad5dd3 | ||
|
|
adc6a0054c | ||
|
|
5425cca47e | ||
|
|
8a70cdb48b | ||
|
|
bb4bc11383 | ||
|
|
a366494c34 | ||
|
|
99de302ec9 | ||
|
|
907912046f | ||
|
|
2c0d379dc5 | ||
|
|
db2759b7d1 | ||
|
|
3c3b9575a2 | ||
|
|
987cfd5dae | ||
|
|
1537fb39c0 | ||
|
|
65cbc478b8 | ||
|
|
e2e8fbe313 | ||
|
|
a3c5f785e9 | ||
|
|
7877215d59 | ||
|
|
e4347792b8 | ||
|
|
50fde60899 | ||
|
|
38f5e293b0 | ||
|
|
b6b399a590 | ||
|
|
b83841d253 | ||
|
|
3e69304f0f | ||
|
|
fe8131f743 | ||
|
|
9ef14a20d1 | ||
|
|
5897b97065 |
10
.github/ISSUE_TEMPLATE/Bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/Bug_report.yml
vendored
@@ -62,6 +62,16 @@ body:
|
||||
- nightly
|
||||
validations:
|
||||
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
|
||||
attributes:
|
||||
label: "Operating System:"
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ❓ Community-driven support
|
||||
- name: ❓ Community-driven support (Free)
|
||||
url: https://docs.mailcow.email/#get-support
|
||||
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
|
||||
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.
|
||||
|
||||
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@@ -12,7 +12,7 @@
|
||||
"baseBranches": ["staging"],
|
||||
"enabledManagers": ["github-actions", "regex", "docker-compose"],
|
||||
"ignorePaths": [
|
||||
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**"
|
||||
"data\/web\/inc\/lib\/vendor\/**"
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
|
||||
37
.github/workflows/check_if_support_labeled.yml
vendored
Normal file
37
.github/workflows/check_if_support_labeled.yml
vendored
Normal file
@@ -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
|
||||
steps:
|
||||
- name: Send message
|
||||
uses: thollander/actions-comment-pull-request@v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@v2.5.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
||||
message: |
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||
uses: actions/stale@v8.0.0
|
||||
uses: actions/stale@v9.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||
days-before-stale: 60
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
bash helper-scripts/update_postscreen_whitelist.sh
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||
commit-message: update postscreen_access.cidr
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@ data/conf/dovecot/acl_anyone
|
||||
data/conf/dovecot/dovecot-master.passwd
|
||||
data/conf/dovecot/dovecot-master.userdb
|
||||
data/conf/dovecot/extra.conf
|
||||
data/conf/dovecot/mail_replica.conf
|
||||
data/conf/dovecot/global_sieve_*
|
||||
data/conf/dovecot/last_login
|
||||
data/conf/dovecot/lua
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
@@ -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.
|
||||
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).
|
||||
|
||||
## 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://twitter.com/mailcow_email)
|
||||

|
||||
|
||||
|
||||
## Want to support mailcow?
|
||||
|
||||
@@ -25,7 +27,9 @@ Please see [the official documentation](https://docs.mailcow.email/) for install
|
||||
|
||||
[Telegram mailcow Off-Topic channel](https://t.me/mailcowOfftopic)
|
||||
|
||||
[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.
|
||||
|
||||
@@ -38,4 +42,4 @@ mailcow is a registered word mark of The Infrastructure Company GmbH, Parkstr. 4
|
||||
|
||||
The project is managed and maintained by The Infrastructure Company GmbH.
|
||||
|
||||
Originated from @andryyy (André)
|
||||
Originated from @andryyy (André)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
FROM alpine:3.17
|
||||
FROM alpine:3.18
|
||||
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
bash \
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
FROM clamav/clamav:1.0.3_base
|
||||
FROM alpine:3.19
|
||||
|
||||
LABEL maintainer "André Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
rsync \
|
||||
clamav \
|
||||
bind-tools \
|
||||
bash
|
||||
bash \
|
||||
tini
|
||||
|
||||
# init
|
||||
COPY clamd.sh /clamd.sh
|
||||
@@ -14,7 +16,9 @@ RUN chmod +x /sbin/tini
|
||||
|
||||
# healthcheck
|
||||
COPY healthcheck.sh /healthcheck.sh
|
||||
COPY clamdcheck.sh /usr/local/bin
|
||||
RUN chmod +x /healthcheck.sh
|
||||
RUN chmod +x /usr/local/bin/clamdcheck.sh
|
||||
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
|
||||
|
||||
ENTRYPOINT []
|
||||
|
||||
14
data/Dockerfiles/clamd/clamdcheck.sh
Normal file
14
data/Dockerfiles/clamd/clamdcheck.sh
Normal file
@@ -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.19
|
||||
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --update --no-cache python3 \
|
||||
@@ -9,12 +10,13 @@ RUN apk add --update --no-cache python3 \
|
||||
openssl \
|
||||
tzdata \
|
||||
py3-psutil \
|
||||
py3-redis \
|
||||
py3-async-timeout \
|
||||
&& pip3 install --upgrade pip \
|
||||
fastapi \
|
||||
uvicorn \
|
||||
aiodocker \
|
||||
docker \
|
||||
aioredis
|
||||
docker
|
||||
RUN mkdir /app/modules
|
||||
|
||||
COPY docker-entrypoint.sh /app/
|
||||
|
||||
@@ -5,16 +5,63 @@ import json
|
||||
import uuid
|
||||
import async_timeout
|
||||
import asyncio
|
||||
import aioredis
|
||||
import aiodocker
|
||||
import docker
|
||||
import logging
|
||||
from logging.config import dictConfig
|
||||
from fastapi import FastAPI, Response, Request
|
||||
from modules.DockerApi import DockerApi
|
||||
from redis import asyncio as aioredis
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
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
|
||||
@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'))
|
||||
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
|
||||
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
||||
|
||||
@@ -1,119 +1,115 @@
|
||||
FROM debian:bullseye-slim
|
||||
FROM alpine:3.19
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG DOVECOT=2.3.21
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
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
|
||||
RUN groupadd -g 5000 vmail \
|
||||
&& groupadd -g 401 dovecot \
|
||||
&& groupadd -g 402 dovenull \
|
||||
&& groupadd -g 999 sogo \
|
||||
&& usermod -a -G sogo nobody \
|
||||
&& useradd -g vmail -u 5000 vmail -d /var/vmail \
|
||||
&& useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
|
||||
&& useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \
|
||||
&& touch /etc/default/locale \
|
||||
&& apt-get update \
|
||||
&& apt-get -y --no-install-recommends install \
|
||||
build-essential \
|
||||
apt-transport-https \
|
||||
RUN addgroup -g 5000 vmail \
|
||||
&& addgroup -g 401 dovecot \
|
||||
&& addgroup -g 402 dovenull \
|
||||
&& sed -i "s/999/99/" /etc/group \
|
||||
&& addgroup -g 999 sogo \
|
||||
&& addgroup nobody sogo \
|
||||
&& adduser -D -u 5000 -G vmail -h /var/vmail vmail \
|
||||
&& adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \
|
||||
&& adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \
|
||||
&& apk add --no-cache --update \
|
||||
bash \
|
||||
bind-tools \
|
||||
findutils \
|
||||
envsubst \
|
||||
ca-certificates \
|
||||
cpanminus \
|
||||
curl \
|
||||
dnsutils \
|
||||
dirmngr \
|
||||
gettext \
|
||||
gnupg2 \
|
||||
jq \
|
||||
libauthen-ntlm-perl \
|
||||
libcgi-pm-perl \
|
||||
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 \
|
||||
lua-cjson \
|
||||
lua-socket \
|
||||
lua-sql-mysql \
|
||||
lua5.3-sql-mysql \
|
||||
icu-data-full \
|
||||
mariadb-connector-c \
|
||||
gcompat \
|
||||
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 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ \
|
||||
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 \
|
||||
python3-pip \
|
||||
redis-server \
|
||||
supervisor \
|
||||
python3 \
|
||||
py3-mysqlclient \
|
||||
py3-html2text \
|
||||
py3-jinja2 \
|
||||
py3-redis \
|
||||
redis \
|
||||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
syslog-ng-redis \
|
||||
syslog-ng-json \
|
||||
supervisor \
|
||||
tzdata \
|
||||
wget \
|
||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||
&& 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 \
|
||||
dovecot-dev \
|
||||
dovecot-lmtpd \
|
||||
dovecot-lua \
|
||||
dovecot-ldap \
|
||||
dovecot-mysql \
|
||||
dovecot-core \
|
||||
dovecot-sql \
|
||||
dovecot-submissiond \
|
||||
dovecot-pigeonhole-plugin \
|
||||
dovecot-pop3d \
|
||||
dovecot-imapd \
|
||||
dovecot-solr \
|
||||
&& pip3 install mysql-connector-python html2text jinja2 redis \
|
||||
&& apt-get autoremove --purge -y \
|
||||
&& apt-get autoclean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /tmp/* /var/tmp/* /root/.cache/
|
||||
# imapsync dependencies
|
||||
RUN cpan Crypt::OpenSSL::PKCS12
|
||||
dovecot-fts-solr \
|
||||
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
|
||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
|
||||
&& chmod +x /usr/local/bin/gosu \
|
||||
&& gosu nobody true
|
||||
|
||||
# RUN cpan LockFile::Simple
|
||||
|
||||
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
|
||||
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
|
||||
|
||||
@@ -335,6 +335,15 @@ sys.exit()
|
||||
EOF
|
||||
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
|
||||
if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
|
||||
openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
|
||||
@@ -432,4 +441,8 @@ done
|
||||
# May be related to something inside Docker, I seriously don't know
|
||||
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 "$@"
|
||||
|
||||
@@ -75,7 +75,8 @@ my $sth = $dbh->prepare("SELECT id,
|
||||
custom_params,
|
||||
subscribeall,
|
||||
timeout1,
|
||||
timeout2
|
||||
timeout2,
|
||||
dry
|
||||
FROM imapsync
|
||||
WHERE active = 1
|
||||
AND is_running = 0
|
||||
@@ -111,13 +112,16 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
$subscribeall = @$row[18];
|
||||
$timeout1 = @$row[19];
|
||||
$timeout2 = @$row[20];
|
||||
$dry = @$row[21];
|
||||
|
||||
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
|
||||
|
||||
my $template = $run_dir . '/imapsync.XXXXXXX';
|
||||
my $passfile1 = File::Temp->new(TEMPLATE => $template);
|
||||
my $passfile2 = File::Temp->new(TEMPLATE => $template);
|
||||
|
||||
|
||||
binmode( $passfile1, ":utf8" );
|
||||
|
||||
print $passfile1 "$password1\n";
|
||||
print $passfile2 trim($master_pass) . "\n";
|
||||
|
||||
@@ -148,6 +152,7 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
"--host2", "localhost",
|
||||
"--user2", $user2 . '*' . trim($master_user),
|
||||
"--passfile2", $passfile2->filename,
|
||||
($dry eq "1" ? ('--dry') : ()),
|
||||
'--no-modulesversion',
|
||||
'--noreleasecheck'];
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import smtplib
|
||||
import os
|
||||
import sys
|
||||
import mysql.connector
|
||||
import MySQLdb
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import COMMASPACE, formatdate
|
||||
import cgi
|
||||
import jinja2
|
||||
from jinja2 import Template
|
||||
import json
|
||||
@@ -50,7 +49,7 @@ try:
|
||||
def query_mysql(query, headers = True, update = False):
|
||||
while True:
|
||||
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:
|
||||
print('%s - trying again...' % (ex))
|
||||
time.sleep(3)
|
||||
|
||||
@@ -55,7 +55,7 @@ try:
|
||||
msg.attach(text_part)
|
||||
msg.attach(html_part)
|
||||
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'))
|
||||
|
||||
domain = username.split("@")[-1]
|
||||
|
||||
@@ -11,7 +11,7 @@ fi
|
||||
|
||||
# Is replication active?
|
||||
# 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
|
||||
exit
|
||||
fi
|
||||
|
||||
@@ -13,6 +13,10 @@ autostart=true
|
||||
|
||||
[program:dovecot]
|
||||
command=/usr/sbin/dovecot -F
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autorestart=true
|
||||
|
||||
[eventlistener:processes]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.28
|
||||
@version: 4.5
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -6,11 +6,12 @@ options {
|
||||
use_dns(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
keep_timestamp(no);
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
unix-stream("/dev/log");
|
||||
source s_dgram {
|
||||
unix-dgram("/dev/log");
|
||||
internal();
|
||||
};
|
||||
destination d_stdout { pipe("/dev/stdout"); };
|
||||
@@ -36,7 +37,7 @@ filter f_replica {
|
||||
not match("Error: sync: Unknown user in remote" value("MESSAGE"));
|
||||
};
|
||||
log {
|
||||
source(s_src);
|
||||
source(s_dgram);
|
||||
filter(f_replica);
|
||||
destination(d_stdout);
|
||||
filter(f_mail);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.28
|
||||
@version: 4.5
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -6,11 +6,12 @@ options {
|
||||
use_dns(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
keep_timestamp(no);
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
unix-stream("/dev/log");
|
||||
source s_dgram {
|
||||
unix-dgram("/dev/log");
|
||||
internal();
|
||||
};
|
||||
destination d_stdout { pipe("/dev/stdout"); };
|
||||
@@ -36,7 +37,7 @@ filter f_replica {
|
||||
not match("Error: sync: Unknown user in remote" value("MESSAGE"));
|
||||
};
|
||||
log {
|
||||
source(s_src);
|
||||
source(s_dgram);
|
||||
filter(f_replica);
|
||||
destination(d_stdout);
|
||||
filter(f_mail);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
FROM alpine:3.17
|
||||
FROM alpine:3.19
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
ENV XTABLES_LIBDIR /usr/lib/xtables
|
||||
ENV PYTHON_IPTABLES_XTABLES_VERSION 12
|
||||
ENV IPTABLES_LIBDIR /usr/lib
|
||||
@@ -12,12 +15,16 @@ RUN apk add --virtual .build-deps \
|
||||
openssl-dev \
|
||||
&& apk add -U python3 \
|
||||
iptables \
|
||||
iptables-dev \
|
||||
ip6tables \
|
||||
xtables-addons \
|
||||
nftables \
|
||||
tzdata \
|
||||
py3-pip \
|
||||
py3-nftables \
|
||||
musl-dev \
|
||||
&& pip3 install --ignore-installed --upgrade pip \
|
||||
jsonschema \
|
||||
python-iptables \
|
||||
redis \
|
||||
ipaddress \
|
||||
@@ -26,5 +33,10 @@ RUN apk add --virtual .build-deps \
|
||||
|
||||
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
|
||||
|
||||
COPY server.py /
|
||||
CMD ["python3", "-u", "/server.py"]
|
||||
COPY modules /app/modules
|
||||
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
data/Dockerfiles/netfilter/docker-entrypoint.sh
Executable file
29
data/Dockerfiles/netfilter/docker-entrypoint.sh
Executable file
@@ -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
|
||||
503
data/Dockerfiles/netfilter/main.py
Normal file
503
data/Dockerfiles/netfilter/main.py
Normal file
@@ -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] = '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:
|
||||
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)
|
||||
252
data/Dockerfiles/netfilter/modules/IPTables.py
Normal file
252
data/Dockerfiles/netfilter/modules/IPTables.py
Normal file
@@ -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
|
||||
30
data/Dockerfiles/netfilter/modules/Logger.py
Normal file
30
data/Dockerfiles/netfilter/modules/Logger.py
Normal file
@@ -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)
|
||||
659
data/Dockerfiles/netfilter/modules/NFTables.py
Normal file
659
data/Dockerfiles/netfilter/modules/NFTables.py
Normal file
@@ -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
|
||||
0
data/Dockerfiles/netfilter/modules/__init__.py
Normal file
0
data/Dockerfiles/netfilter/modules/__init__.py
Normal file
@@ -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.19
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
WORKDIR /app
|
||||
|
||||
#RUN addgroup -S olefy && adduser -S olefy -G olefy \
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
FROM php:8.2-fpm-alpine3.17
|
||||
FROM php:8.2-fpm-alpine3.18
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG APCU_PECL_VERSION=5.1.22
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG APCU_PECL_VERSION=5.1.23
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG IMAGICK_PECL_VERSION=3.7.0
|
||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MAILPARSE_PECL_VERSION=3.1.6
|
||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MEMCACHED_PECL_VERSION=3.2.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG REDIS_PECL_VERSION=6.0.1
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG COMPOSER_VERSION=2.6.5
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG REDIS_PECL_VERSION=6.0.2
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG COMPOSER_VERSION=2.6.6
|
||||
|
||||
RUN apk add -U --no-cache autoconf \
|
||||
aspell-dev \
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN apt-get update && apt-get install -y \
|
||||
dnsutils \
|
||||
netcat \
|
||||
&& apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
|
||||
&& echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
|
||||
&& echo "deb https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
|
||||
&& apt-get update \
|
||||
&& apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
@@ -79,6 +79,9 @@ EOF
|
||||
redis-cli -h redis-mailcow SLAVEOF NO ONE
|
||||
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 \
|
||||
/etc/rspamd/local.d \
|
||||
/etc/rspamd/override.d \
|
||||
|
||||
@@ -2,9 +2,10 @@ FROM debian:bullseye-slim
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.16
|
||||
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>.*)$
|
||||
ARG GOSU_VERSION=1.17
|
||||
ENV LC_ALL C
|
||||
|
||||
# Prerequisites
|
||||
@@ -21,7 +22,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
dirmngr \
|
||||
netcat \
|
||||
netcat-traditional \
|
||||
psmisc \
|
||||
wget \
|
||||
patch \
|
||||
@@ -32,7 +33,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
&& mkdir /usr/share/doc/sogo \
|
||||
&& touch /usr/share/doc/sogo/empty.sh \
|
||||
&& 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 \
|
||||
sogo \
|
||||
sogo-activesync \
|
||||
|
||||
@@ -150,6 +150,8 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
<string>YES</string>
|
||||
<key>SOGoEncryptionKey</key>
|
||||
<string>${RAND_PASS}</string>
|
||||
<key>OCSAdminURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
||||
<key>OCSCacheFolderURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
|
||||
<key>OCSEMailAlarmsFolderURL</key>
|
||||
|
||||
@@ -2,8 +2,8 @@ FROM solr:7.7-slim
|
||||
|
||||
USER root
|
||||
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.16
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.17
|
||||
|
||||
COPY solr.sh /
|
||||
COPY solr-config-7.7.0.xml /
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
FROM alpine:3.17
|
||||
FROM alpine:3.18
|
||||
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
curl \
|
||||
bind-tools \
|
||||
unbound \
|
||||
bash \
|
||||
openssl \
|
||||
@@ -18,10 +19,10 @@ EXPOSE 53/udp 53/tcp
|
||||
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
# healthcheck (nslookup)
|
||||
# healthcheck (dig, ping)
|
||||
COPY healthcheck.sh /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"]
|
||||
|
||||
|
||||
@@ -1,12 +1,72 @@
|
||||
#!/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
|
||||
echo "DNS resolution is working!"
|
||||
# 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
|
||||
else
|
||||
echo "DNS resolution is not working correctly..."
|
||||
echo "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
|
||||
fi
|
||||
|
||||
# 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
|
||||
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
|
||||
LABEL maintainer "André Peters <andre.peters@servercow.de>"
|
||||
FROM alpine:3.18
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# Installation
|
||||
RUN apk add --update \
|
||||
|
||||
@@ -19,9 +19,11 @@ fi
|
||||
|
||||
if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
SMTP_VERBOSE="--verbose"
|
||||
CURL_VERBOSE="--verbose"
|
||||
set -xv
|
||||
else
|
||||
SMTP_VERBOSE=""
|
||||
CURL_VERBOSE=""
|
||||
exec 2>/dev/null
|
||||
fi
|
||||
|
||||
@@ -97,7 +99,9 @@ log_msg() {
|
||||
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=
|
||||
[[ -z ${1} ]] && return 1
|
||||
# If exists, body will be the content of "/tmp/${1}", even if ${2} is set
|
||||
@@ -122,37 +126,57 @@ function mail_error() {
|
||||
else
|
||||
SUBJECT="${WATCHDOG_SUBJECT}: ${1}"
|
||||
fi
|
||||
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
|
||||
for rcpt in "${MAIL_RCPTS[@]}"; do
|
||||
RCPT_DOMAIN=
|
||||
RCPT_MX=
|
||||
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..."
|
||||
|
||||
# Send mail notification if enabled
|
||||
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
|
||||
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
|
||||
for rcpt in "${MAIL_RCPTS[@]}"; do
|
||||
RCPT_DOMAIN=
|
||||
RCPT_MX=
|
||||
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
|
||||
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
|
||||
|
||||
# Replace subject and body placeholders
|
||||
WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s/\$SUBJECT\|\${SUBJECT}/$SUBJECT/g" | sed "s/\$BODY\|\${BODY}/$BODY/g")
|
||||
|
||||
# POST to webhook
|
||||
curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK}
|
||||
|
||||
log_msg "Sent notification using webhook"
|
||||
fi
|
||||
}
|
||||
|
||||
get_container_ip() {
|
||||
@@ -197,7 +221,7 @@ get_container_ip() {
|
||||
# One-time check
|
||||
if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; 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
|
||||
|
||||
@@ -692,8 +716,8 @@ rspamd_checks() {
|
||||
From: watchdog@localhost
|
||||
|
||||
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)
|
||||
if [[ ${SCORE} != "9999" ]]; then
|
||||
' | 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} -ne 9999 ]]; then
|
||||
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
|
||||
err_count=$(( ${err_count} + 1))
|
||||
else
|
||||
@@ -746,8 +770,8 @@ olefy_checks() {
|
||||
}
|
||||
|
||||
# Notify about start
|
||||
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
|
||||
mail_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
|
||||
if [[ ${WATCHDOG_NOTIFY_START} =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
notify_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
|
||||
fi
|
||||
|
||||
# Create watchdog agents
|
||||
@@ -1029,33 +1053,33 @@ while true; do
|
||||
fi
|
||||
if [[ ${com_pipe_answer} == "ratelimit" ]]; then
|
||||
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
|
||||
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
|
||||
log_msg "Your mailcow is an open relay!"
|
||||
# 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
|
||||
log_msg "MySQL replication is not working properly"
|
||||
# Define $2 to override message text, else print service was restarted at ...
|
||||
# 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
|
||||
log_msg "Dovecot replication is not working properly"
|
||||
# Define $2 to override message text, else print service was restarted at ...
|
||||
# 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
|
||||
log_msg "Certificates are about to expire"
|
||||
# Define $2 to override message text, else print service was restarted at ...
|
||||
# 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
|
||||
log_msg "acme-mailcow did not complete successfully"
|
||||
# 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
|
||||
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
|
||||
if [[ ! -z "${F2B_RES}" ]]; then
|
||||
@@ -1065,7 +1089,7 @@ while true; do
|
||||
log_msg "Banned ${host}"
|
||||
rm /tmp/fail2ban 2> /dev/null
|
||||
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
|
||||
fi
|
||||
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
|
||||
@@ -1085,7 +1109,7 @@ while true; do
|
||||
else
|
||||
log_msg "Sending restart command to ${CONTAINER_ID}..."
|
||||
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..."
|
||||
sleep 35
|
||||
fi
|
||||
@@ -1095,3 +1119,4 @@ while true; do
|
||||
kill -USR1 ${BACKGROUND_TASKS[*]}
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ server {
|
||||
deny all;
|
||||
}
|
||||
|
||||
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) {
|
||||
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+)\.php(?:$|\/) {
|
||||
fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
|
||||
set $path_info $fastcgi_path_info;
|
||||
try_files $fastcgi_script_name =404;
|
||||
@@ -105,7 +105,7 @@ server {
|
||||
fastcgi_read_timeout 1200;
|
||||
}
|
||||
|
||||
location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) {
|
||||
location ~ ^\/(?:updater|ocs-provider)(?:$|\/) {
|
||||
try_files $uri/ =404;
|
||||
index index.php;
|
||||
}
|
||||
|
||||
@@ -247,6 +247,9 @@ plugin {
|
||||
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
||||
mail_log_fields = uid box msgid size
|
||||
mail_log_cached_only = yes
|
||||
|
||||
# Try set mail_replica
|
||||
!include_try /etc/dovecot/mail_replica.conf
|
||||
}
|
||||
service quota-warning {
|
||||
executable = script /usr/local/bin/quota_notify.py
|
||||
|
||||
@@ -12,7 +12,8 @@ if /^\s*Received: from.* \(.*rspamd-mailcow.*mailcow-network.*\).*\(Postcow\)/
|
||||
REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5
|
||||
endif
|
||||
/^\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-Forward/ IGNORE
|
||||
# Not removing UA by default, might be signed
|
||||
|
||||
@@ -11,6 +11,7 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
|
||||
smtpd_relay_restrictions = permit_mynetworks,
|
||||
permit_sasl_authenticated,
|
||||
defer_unauth_destination
|
||||
smtpd_forbid_bare_newline = yes
|
||||
# alias maps are auto-generated in postfix.sh on startup
|
||||
alias_maps = 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_delay_reject = yes
|
||||
smtpd_error_sleep_time = 10s
|
||||
smtpd_forbid_bare_newline = yes
|
||||
smtpd_hard_error_limit = ${stress?1}${stress:5}
|
||||
smtpd_helo_required = yes
|
||||
smtpd_proxy_timeout = 600s
|
||||
@@ -160,7 +162,8 @@ 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_transport_maps.cf
|
||||
smtp_sasl_auth_soft_bounce = no
|
||||
postscreen_discard_ehlo_keywords = silent-discard, dsn
|
||||
postscreen_discard_ehlo_keywords = silent-discard, dsn, chunking
|
||||
smtpd_discard_ehlo_keywords = chunking
|
||||
compatibility_level = 2
|
||||
smtputf8_enable = no
|
||||
# Define protocols for SMTPS and submission service
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
# Whitelist generated by Postwhite v3.4 on Sun Oct 1 00:14:59 UTC 2023
|
||||
# Whitelist generated by Postwhite v3.4 on Mon Apr 1 00:15:02 UTC 2024
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 2019 total rules
|
||||
# 2009 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a01:111:f400::/48 permit
|
||||
2a01:111:f403:8000::/50 permit
|
||||
2a01:111:f403:8000::/51 permit
|
||||
2a01:111:f403::/49 permit
|
||||
2a01:111:f403:c000::/51 permit
|
||||
2a01:111:f403:f000::/52 permit
|
||||
2a02:a60:0:5::/64 permit
|
||||
2c0f:fb50:4000::/36 permit
|
||||
2.207.151.53 permit
|
||||
3.14.230.16 permit
|
||||
3.70.123.177 permit
|
||||
3.93.157.0/24 permit
|
||||
3.129.120.190 permit
|
||||
3.137.16.58 permit
|
||||
3.210.190.0/24 permit
|
||||
8.20.114.31 permit
|
||||
8.25.194.0/23 permit
|
||||
@@ -43,9 +44,10 @@
|
||||
18.198.96.88 permit
|
||||
18.208.124.128/25 permit
|
||||
18.216.232.154 permit
|
||||
18.234.1.244 permit
|
||||
18.236.40.242 permit
|
||||
18.236.56.161 permit
|
||||
20.51.6.32/30 permit
|
||||
20.51.98.61 permit
|
||||
20.52.52.2 permit
|
||||
20.52.128.133 permit
|
||||
20.59.80.4/30 permit
|
||||
@@ -90,23 +92,22 @@
|
||||
27.123.204.172 permit
|
||||
27.123.204.188/30 permit
|
||||
27.123.204.192 permit
|
||||
27.123.206.0/24 permit
|
||||
27.123.206.50/31 permit
|
||||
27.123.206.56/29 permit
|
||||
27.123.206.76/30 permit
|
||||
27.123.206.80/28 permit
|
||||
31.25.48.222 permit
|
||||
34.195.217.107 permit
|
||||
34.202.239.6 permit
|
||||
34.212.163.75 permit
|
||||
34.215.104.144 permit
|
||||
34.218.116.3 permit
|
||||
34.225.212.172 permit
|
||||
34.247.168.44 permit
|
||||
35.161.32.253 permit
|
||||
35.167.93.243 permit
|
||||
35.176.132.251 permit
|
||||
35.190.247.0/24 permit
|
||||
35.191.0.0/16 permit
|
||||
35.242.169.159 permit
|
||||
37.218.248.47 permit
|
||||
37.218.249.47 permit
|
||||
37.218.251.62 permit
|
||||
@@ -116,13 +117,12 @@
|
||||
40.92.0.0/16 permit
|
||||
40.107.0.0/16 permit
|
||||
40.112.65.63 permit
|
||||
40.117.80.0/24 permit
|
||||
43.228.184.0/22 permit
|
||||
44.206.138.57 permit
|
||||
44.209.42.157 permit
|
||||
44.236.56.93 permit
|
||||
44.238.220.251 permit
|
||||
46.19.168.0/23 permit
|
||||
46.19.170.16 permit
|
||||
46.226.48.0/21 permit
|
||||
46.228.36.37 permit
|
||||
46.228.36.38/31 permit
|
||||
@@ -162,7 +162,6 @@
|
||||
46.228.38.144/29 permit
|
||||
46.228.38.152/31 permit
|
||||
46.228.38.154 permit
|
||||
46.228.39.0/24 permit
|
||||
46.228.39.64/27 permit
|
||||
46.228.39.96/30 permit
|
||||
46.228.39.100/30 permit
|
||||
@@ -183,11 +182,12 @@
|
||||
50.18.125.237 permit
|
||||
50.18.126.162 permit
|
||||
50.31.32.0/19 permit
|
||||
50.31.156.96/27 permit
|
||||
50.31.205.0/24 permit
|
||||
50.56.130.220 permit
|
||||
50.56.130.221 permit
|
||||
51.137.58.21 permit
|
||||
51.140.75.55 permit
|
||||
51.144.100.179 permit
|
||||
52.1.14.157 permit
|
||||
52.5.230.59 permit
|
||||
52.27.5.72 permit
|
||||
52.27.28.47 permit
|
||||
@@ -206,11 +206,11 @@
|
||||
52.95.49.88/29 permit
|
||||
52.96.91.34 permit
|
||||
52.96.111.82 permit
|
||||
52.96.172.98 permit
|
||||
52.96.214.50 permit
|
||||
52.96.222.194 permit
|
||||
52.96.222.226 permit
|
||||
52.96.223.2 permit
|
||||
52.96.228.130 permit
|
||||
52.96.229.242 permit
|
||||
52.100.0.0/14 permit
|
||||
52.103.0.0/17 permit
|
||||
@@ -242,22 +242,17 @@
|
||||
54.174.63.0/24 permit
|
||||
54.186.193.102 permit
|
||||
54.191.223.56 permit
|
||||
54.194.61.95 permit
|
||||
54.195.113.45 permit
|
||||
54.213.20.246 permit
|
||||
54.214.39.184 permit
|
||||
54.216.77.168 permit
|
||||
54.221.227.204 permit
|
||||
54.240.0.0/18 permit
|
||||
54.240.64.0/19 permit
|
||||
54.240.96.0/19 permit
|
||||
54.241.16.209 permit
|
||||
54.244.54.130 permit
|
||||
54.244.242.0/24 permit
|
||||
54.246.232.180 permit
|
||||
54.255.61.23 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.136.0/22 permit
|
||||
62.13.140.0/22 permit
|
||||
@@ -265,12 +260,12 @@
|
||||
62.13.148.0/23 permit
|
||||
62.13.150.0/23 permit
|
||||
62.13.152.0/23 permit
|
||||
62.13.159.196 permit
|
||||
62.17.146.128/26 permit
|
||||
62.179.121.0/24 permit
|
||||
62.201.172.0/27 permit
|
||||
62.201.172.32/27 permit
|
||||
62.253.227.114 permit
|
||||
63.32.13.159 permit
|
||||
63.80.14.0/23 permit
|
||||
63.111.28.137 permit
|
||||
63.128.21.0/24 permit
|
||||
@@ -285,10 +280,6 @@
|
||||
64.79.155.193 permit
|
||||
64.79.155.205 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.132.88.0/23 permit
|
||||
64.132.92.0/24 permit
|
||||
@@ -303,22 +294,31 @@
|
||||
64.147.123.27 permit
|
||||
64.147.123.28 permit
|
||||
64.147.123.29 permit
|
||||
64.147.123.128/27 permit
|
||||
64.207.219.7 permit
|
||||
64.207.219.8 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.14 permit
|
||||
64.207.219.15 permit
|
||||
64.207.219.71 permit
|
||||
64.207.219.72 permit
|
||||
64.207.219.73 permit
|
||||
64.207.219.74 permit
|
||||
64.207.219.75 permit
|
||||
64.207.219.76 permit
|
||||
64.207.219.77 permit
|
||||
64.207.219.78 permit
|
||||
64.207.219.79 permit
|
||||
64.207.219.135 permit
|
||||
64.207.219.136 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.142 permit
|
||||
64.207.219.143 permit
|
||||
@@ -367,7 +367,6 @@
|
||||
66.135.215.0/24 permit
|
||||
66.135.222.1 permit
|
||||
66.162.193.226/31 permit
|
||||
66.163.184.0/21 permit
|
||||
66.163.184.0/24 permit
|
||||
66.163.185.0/24 permit
|
||||
66.163.186.0/24 permit
|
||||
@@ -380,7 +379,6 @@
|
||||
66.196.80.112/28 permit
|
||||
66.196.80.144/29 permit
|
||||
66.196.80.193 permit
|
||||
66.196.81.0/24 permit
|
||||
66.196.81.104/29 permit
|
||||
66.196.81.112/29 permit
|
||||
66.196.81.120 permit
|
||||
@@ -395,8 +393,6 @@
|
||||
66.196.81.228/30 permit
|
||||
66.196.81.232/31 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.184.0/23 permit
|
||||
66.218.74.64/30 permit
|
||||
@@ -425,12 +421,10 @@
|
||||
66.249.80.0/20 permit
|
||||
67.23.31.6 permit
|
||||
67.72.99.26 permit
|
||||
67.195.22.0/24 permit
|
||||
67.195.22.113 permit
|
||||
67.195.22.116/30 permit
|
||||
67.195.23.144/30 permit
|
||||
67.195.23.148 permit
|
||||
67.195.60.0/24 permit
|
||||
67.195.60.45 permit
|
||||
67.195.60.46/31 permit
|
||||
67.195.60.48/31 permit
|
||||
@@ -438,7 +432,6 @@
|
||||
67.195.60.146 permit
|
||||
67.195.60.155 permit
|
||||
67.195.60.156 permit
|
||||
67.195.87.0/24 permit
|
||||
67.195.87.64 permit
|
||||
67.195.87.81 permit
|
||||
67.195.87.82/31 permit
|
||||
@@ -463,7 +456,6 @@
|
||||
67.228.37.4/30 permit
|
||||
67.231.145.42 permit
|
||||
67.231.153.30 permit
|
||||
68.142.230.0/24 permit
|
||||
68.142.230.64/31 permit
|
||||
68.142.230.69 permit
|
||||
68.142.230.70/31 permit
|
||||
@@ -479,10 +471,6 @@
|
||||
69.65.42.195 permit
|
||||
69.65.49.192/29 permit
|
||||
69.72.32.0/20 permit
|
||||
69.72.40.93 permit
|
||||
69.72.40.94/31 permit
|
||||
69.72.40.96/30 permit
|
||||
69.72.47.205 permit
|
||||
69.147.84.227 permit
|
||||
69.162.98.0/24 permit
|
||||
69.169.224.0/20 permit
|
||||
@@ -522,7 +510,6 @@
|
||||
72.30.237.180/30 permit
|
||||
72.30.237.184/31 permit
|
||||
72.30.237.204/30 permit
|
||||
72.30.238.0/23 permit
|
||||
72.30.238.116/30 permit
|
||||
72.30.238.120/31 permit
|
||||
72.30.238.128 permit
|
||||
@@ -553,12 +540,7 @@
|
||||
72.30.239.228/31 permit
|
||||
72.30.239.244/30 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
|
||||
74.6.128.0/21 permit
|
||||
74.6.128.0/24 permit
|
||||
74.6.129.0/24 permit
|
||||
74.6.130.0/24 permit
|
||||
@@ -585,13 +567,14 @@
|
||||
74.112.67.243 permit
|
||||
74.125.0.0/16 permit
|
||||
74.202.227.40 permit
|
||||
74.208.4.192/26 permit
|
||||
74.208.5.64/26 permit
|
||||
74.208.122.0/26 permit
|
||||
74.208.4.200 permit
|
||||
74.208.4.201 permit
|
||||
74.208.4.220 permit
|
||||
74.208.4.221 permit
|
||||
74.209.250.0/24 permit
|
||||
75.2.70.75 permit
|
||||
76.223.128.0/19 permit
|
||||
76.223.176.0/20 permit
|
||||
77.238.176.0/22 permit
|
||||
77.238.176.0/24 permit
|
||||
77.238.177.0/24 permit
|
||||
77.238.178.0/24 permit
|
||||
@@ -614,17 +597,25 @@
|
||||
77.238.189.148/30 permit
|
||||
81.7.169.128/25 permit
|
||||
81.223.46.0/27 permit
|
||||
82.165.159.0/24 permit
|
||||
82.165.159.0/26 permit
|
||||
82.165.229.31 permit
|
||||
82.165.229.130 permit
|
||||
82.165.230.21 permit
|
||||
82.165.230.22 permit
|
||||
82.165.159.2 permit
|
||||
82.165.159.3 permit
|
||||
82.165.159.4 permit
|
||||
82.165.159.12 permit
|
||||
82.165.159.13 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.50.0/23 permit
|
||||
85.158.136.0/21 permit
|
||||
86.61.88.25 permit
|
||||
87.198.219.130 permit
|
||||
87.198.219.153 permit
|
||||
87.238.80.0/21 permit
|
||||
87.248.103.12 permit
|
||||
87.248.103.21 permit
|
||||
@@ -664,6 +655,7 @@
|
||||
87.253.232.0/21 permit
|
||||
89.22.108.0/24 permit
|
||||
91.211.240.0/22 permit
|
||||
94.169.2.0/23 permit
|
||||
94.245.112.0/27 permit
|
||||
94.245.112.10/31 permit
|
||||
95.131.104.0/21 permit
|
||||
@@ -677,7 +669,6 @@
|
||||
98.136.44.181 permit
|
||||
98.136.44.182/31 permit
|
||||
98.136.44.184 permit
|
||||
98.136.164.0/24 permit
|
||||
98.136.164.36/31 permit
|
||||
98.136.164.64/29 permit
|
||||
98.136.164.72/30 permit
|
||||
@@ -685,7 +676,6 @@
|
||||
98.136.164.78 permit
|
||||
98.136.172.32/30 permit
|
||||
98.136.172.36/31 permit
|
||||
98.136.185.0/24 permit
|
||||
98.136.185.29 permit
|
||||
98.136.185.42/31 permit
|
||||
98.136.185.46 permit
|
||||
@@ -720,7 +710,6 @@
|
||||
98.136.215.184 permit
|
||||
98.136.215.208/30 permit
|
||||
98.136.215.212/31 permit
|
||||
98.136.217.0/24 permit
|
||||
98.136.217.1 permit
|
||||
98.136.217.2 permit
|
||||
98.136.217.3 permit
|
||||
@@ -730,14 +719,12 @@
|
||||
98.136.217.12/30 permit
|
||||
98.136.217.16/30 permit
|
||||
98.136.217.20/30 permit
|
||||
98.136.218.0/24 permit
|
||||
98.136.218.39 permit
|
||||
98.136.218.40/29 permit
|
||||
98.136.218.48/28 permit
|
||||
98.136.218.67 permit
|
||||
98.136.218.68/30 permit
|
||||
98.136.218.72/30 permit
|
||||
98.137.12.0/24 permit
|
||||
98.137.12.48/30 permit
|
||||
98.137.12.52/31 permit
|
||||
98.137.12.54 permit
|
||||
@@ -775,7 +762,6 @@
|
||||
98.137.13.132 permit
|
||||
98.137.13.137 permit
|
||||
98.137.13.138 permit
|
||||
98.137.64.0/21 permit
|
||||
98.137.64.0/24 permit
|
||||
98.137.65.0/24 permit
|
||||
98.137.66.0/24 permit
|
||||
@@ -799,7 +785,6 @@
|
||||
98.138.83.176/31 permit
|
||||
98.138.83.179 permit
|
||||
98.138.83.180/31 permit
|
||||
98.138.84.0/22 permit
|
||||
98.138.84.37 permit
|
||||
98.138.84.38/31 permit
|
||||
98.138.84.40/29 permit
|
||||
@@ -840,7 +825,6 @@
|
||||
98.138.87.148/31 permit
|
||||
98.138.87.192/30 permit
|
||||
98.138.87.196/31 permit
|
||||
98.138.88.0/22 permit
|
||||
98.138.88.105 permit
|
||||
98.138.88.106 permit
|
||||
98.138.88.128/30 permit
|
||||
@@ -880,7 +864,6 @@
|
||||
98.138.91.2/31 permit
|
||||
98.138.91.4/31 permit
|
||||
98.138.91.6 permit
|
||||
98.138.100.0/23 permit
|
||||
98.138.100.220/30 permit
|
||||
98.138.100.224/30 permit
|
||||
98.138.100.228/31 permit
|
||||
@@ -890,7 +873,6 @@
|
||||
98.138.104.100 permit
|
||||
98.138.104.112/30 permit
|
||||
98.138.104.116 permit
|
||||
98.138.120.0/24 permit
|
||||
98.138.120.36/30 permit
|
||||
98.138.120.48/28 permit
|
||||
98.138.197.46/31 permit
|
||||
@@ -983,12 +965,10 @@
|
||||
98.138.213.238/31 permit
|
||||
98.138.213.240/31 permit
|
||||
98.138.213.242 permit
|
||||
98.138.215.0/24 permit
|
||||
98.138.215.12/30 permit
|
||||
98.138.215.16/28 permit
|
||||
98.138.217.216/30 permit
|
||||
98.138.217.220/31 permit
|
||||
98.138.226.0/24 permit
|
||||
98.138.226.30/31 permit
|
||||
98.138.226.56/29 permit
|
||||
98.138.226.64/30 permit
|
||||
@@ -1014,21 +994,18 @@
|
||||
98.138.227.108/31 permit
|
||||
98.138.227.128/30 permit
|
||||
98.138.227.132/31 permit
|
||||
98.138.229.0/24 permit
|
||||
98.138.229.24/29 permit
|
||||
98.138.229.32/31 permit
|
||||
98.138.229.122/31 permit
|
||||
98.138.229.138/31 permit
|
||||
98.138.229.154/31 permit
|
||||
98.138.229.170/31 permit
|
||||
98.139.164.0/24 permit
|
||||
98.139.164.96/30 permit
|
||||
98.139.164.100/30 permit
|
||||
98.139.164.104/29 permit
|
||||
98.139.164.112/30 permit
|
||||
98.139.172.112/30 permit
|
||||
98.139.172.116/31 permit
|
||||
98.139.175.0/24 permit
|
||||
98.139.175.65 permit
|
||||
98.139.175.66/31 permit
|
||||
98.139.175.68/30 permit
|
||||
@@ -1053,10 +1030,8 @@
|
||||
98.139.210.196/31 permit
|
||||
98.139.210.202/31 permit
|
||||
98.139.210.204/30 permit
|
||||
98.139.211.0/24 permit
|
||||
98.139.211.160/30 permit
|
||||
98.139.211.192/28 permit
|
||||
98.139.212.0/23 permit
|
||||
98.139.212.160/28 permit
|
||||
98.139.212.176/29 permit
|
||||
98.139.212.184/30 permit
|
||||
@@ -1071,7 +1046,6 @@
|
||||
98.139.214.155 permit
|
||||
98.139.214.156/30 permit
|
||||
98.139.214.221 permit
|
||||
98.139.215.0/24 permit
|
||||
98.139.215.228/31 permit
|
||||
98.139.215.230 permit
|
||||
98.139.215.248/30 permit
|
||||
@@ -1130,14 +1104,12 @@
|
||||
98.139.220.243 permit
|
||||
98.139.220.245 permit
|
||||
98.139.220.253 permit
|
||||
98.139.221.0/24 permit
|
||||
98.139.221.43 permit
|
||||
98.139.221.60/30 permit
|
||||
98.139.221.156/30 permit
|
||||
98.139.221.232/30 permit
|
||||
98.139.221.236/31 permit
|
||||
98.139.221.250 permit
|
||||
98.139.244.0/24 permit
|
||||
98.139.244.47 permit
|
||||
98.139.244.49 permit
|
||||
98.139.244.50/31 permit
|
||||
@@ -1177,6 +1149,7 @@
|
||||
98.139.245.208/30 permit
|
||||
98.139.245.212/31 permit
|
||||
99.78.197.208/28 permit
|
||||
99.83.190.102 permit
|
||||
103.2.140.0/22 permit
|
||||
103.9.96.0/22 permit
|
||||
103.28.42.0/24 permit
|
||||
@@ -1192,7 +1165,6 @@
|
||||
104.130.96.0/28 permit
|
||||
104.130.122.0/23 permit
|
||||
104.214.25.77 permit
|
||||
104.245.209.192/26 permit
|
||||
106.10.144.64/27 permit
|
||||
106.10.144.100/31 permit
|
||||
106.10.144.103 permit
|
||||
@@ -1218,7 +1190,6 @@
|
||||
106.10.146.52/31 permit
|
||||
106.10.146.224/30 permit
|
||||
106.10.146.228/31 permit
|
||||
106.10.148.0/24 permit
|
||||
106.10.148.48/30 permit
|
||||
106.10.148.52/31 permit
|
||||
106.10.148.68/30 permit
|
||||
@@ -1231,7 +1202,6 @@
|
||||
106.10.149.30 permit
|
||||
106.10.149.160/30 permit
|
||||
106.10.149.164/31 permit
|
||||
106.10.150.0/23 permit
|
||||
106.10.150.23 permit
|
||||
106.10.150.24/30 permit
|
||||
106.10.150.28/31 permit
|
||||
@@ -1270,7 +1240,6 @@
|
||||
106.10.151.250/31 permit
|
||||
106.10.151.252/31 permit
|
||||
106.10.151.254 permit
|
||||
106.10.167.0/24 permit
|
||||
106.10.167.72 permit
|
||||
106.10.167.128/27 permit
|
||||
106.10.167.160/28 permit
|
||||
@@ -1292,7 +1261,6 @@
|
||||
106.10.174.120/30 permit
|
||||
106.10.174.154/31 permit
|
||||
106.10.174.156/30 permit
|
||||
106.10.176.0/24 permit
|
||||
106.10.176.32/29 permit
|
||||
106.10.176.48 permit
|
||||
106.10.176.112 permit
|
||||
@@ -1311,7 +1279,6 @@
|
||||
106.10.196.43 permit
|
||||
106.10.196.44/30 permit
|
||||
106.10.196.48 permit
|
||||
106.10.240.0/22 permit
|
||||
106.10.240.0/24 permit
|
||||
106.10.241.0/24 permit
|
||||
106.10.242.0/24 permit
|
||||
@@ -1319,6 +1286,7 @@
|
||||
106.10.244.0/24 permit
|
||||
106.39.212.64/29 permit
|
||||
106.50.16.0/28 permit
|
||||
107.20.210.250 permit
|
||||
108.174.0.0/24 permit
|
||||
108.174.0.215 permit
|
||||
108.174.3.0/24 permit
|
||||
@@ -1337,7 +1305,6 @@
|
||||
111.221.112.0/21 permit
|
||||
112.19.199.64/29 permit
|
||||
112.19.242.64/29 permit
|
||||
116.214.12.0/24 permit
|
||||
116.214.12.47 permit
|
||||
116.214.12.48/31 permit
|
||||
116.214.12.56/31 permit
|
||||
@@ -1356,7 +1323,6 @@
|
||||
121.244.91.48 permit
|
||||
122.15.156.182 permit
|
||||
123.126.78.64/29 permit
|
||||
124.108.96.0/24 permit
|
||||
124.108.96.24/31 permit
|
||||
124.108.96.28/31 permit
|
||||
124.108.96.70/31 permit
|
||||
@@ -1369,6 +1335,8 @@
|
||||
128.245.0.0/20 permit
|
||||
128.245.64.0/20 permit
|
||||
128.245.176.0/20 permit
|
||||
128.245.240.0/24 permit
|
||||
128.245.241.0/24 permit
|
||||
128.245.242.0/24 permit
|
||||
128.245.242.16 permit
|
||||
128.245.242.17 permit
|
||||
@@ -1378,6 +1346,7 @@
|
||||
128.245.245.0/24 permit
|
||||
128.245.246.0/24 permit
|
||||
128.245.247.0/24 permit
|
||||
128.245.248.0/21 permit
|
||||
129.41.77.70 permit
|
||||
129.41.169.249 permit
|
||||
129.80.5.164 permit
|
||||
@@ -1415,9 +1384,11 @@
|
||||
135.84.216.0/22 permit
|
||||
136.143.160.0/24 permit
|
||||
136.143.161.0/24 permit
|
||||
136.143.178.49 permit
|
||||
136.143.182.0/23 permit
|
||||
136.143.184.0/24 permit
|
||||
136.143.188.0/24 permit
|
||||
136.143.190.0/23 permit
|
||||
136.147.128.0/20 permit
|
||||
136.147.135.0/24 permit
|
||||
136.147.176.0/20 permit
|
||||
@@ -1448,13 +1419,14 @@
|
||||
144.178.38.0/24 permit
|
||||
145.253.228.160/29 permit
|
||||
145.253.239.128/29 permit
|
||||
146.20.14.105 permit
|
||||
146.20.14.107 permit
|
||||
146.20.112.0/26 permit
|
||||
146.20.113.0/24 permit
|
||||
146.20.191.0/24 permit
|
||||
146.20.215.0/24 permit
|
||||
146.20.215.182 permit
|
||||
146.88.28.0/24 permit
|
||||
147.160.158.0/24 permit
|
||||
147.243.1.47 permit
|
||||
147.243.1.48 permit
|
||||
147.243.1.153 permit
|
||||
@@ -1463,6 +1435,7 @@
|
||||
148.105.0.0/16 permit
|
||||
148.105.8.0/21 permit
|
||||
149.72.0.0/16 permit
|
||||
149.72.223.204 permit
|
||||
149.72.248.236 permit
|
||||
149.97.173.180 permit
|
||||
150.230.98.160 permit
|
||||
@@ -1492,6 +1465,8 @@
|
||||
158.101.211.207 permit
|
||||
158.120.80.0/21 permit
|
||||
158.247.16.0/20 permit
|
||||
159.92.154.0/24 permit
|
||||
159.92.155.0/24 permit
|
||||
159.92.157.0/24 permit
|
||||
159.92.157.16 permit
|
||||
159.92.157.17 permit
|
||||
@@ -1501,6 +1476,9 @@
|
||||
159.92.160.0/24 permit
|
||||
159.92.161.0/24 permit
|
||||
159.92.162.0/24 permit
|
||||
159.92.163.0/24 permit
|
||||
159.92.164.0/22 permit
|
||||
159.92.168.0/21 permit
|
||||
159.112.240.0/20 permit
|
||||
159.112.242.162 permit
|
||||
159.135.132.128/25 permit
|
||||
@@ -1515,9 +1493,12 @@
|
||||
161.71.64.0/20 permit
|
||||
162.247.216.0/22 permit
|
||||
163.47.180.0/22 permit
|
||||
163.47.180.0/23 permit
|
||||
163.114.130.16 permit
|
||||
163.114.132.120 permit
|
||||
164.177.132.168 permit
|
||||
164.177.132.169 permit
|
||||
164.177.132.170 permit
|
||||
164.177.132.171 permit
|
||||
165.173.128.0/24 permit
|
||||
166.78.68.0/22 permit
|
||||
166.78.68.221 permit
|
||||
@@ -1538,8 +1519,6 @@
|
||||
167.89.75.164 permit
|
||||
167.89.101.2 permit
|
||||
167.89.101.192/28 permit
|
||||
167.216.129.210 permit
|
||||
167.216.131.180 permit
|
||||
167.220.67.232/29 permit
|
||||
168.138.5.36 permit
|
||||
168.138.73.51 permit
|
||||
@@ -1549,6 +1528,9 @@
|
||||
168.245.127.231 permit
|
||||
169.148.129.0/24 permit
|
||||
169.148.131.0/24 permit
|
||||
169.148.142.10 permit
|
||||
169.148.144.0/25 permit
|
||||
169.148.144.10 permit
|
||||
170.10.68.0/22 permit
|
||||
170.10.128.0/24 permit
|
||||
170.10.129.0/24 permit
|
||||
@@ -1586,8 +1568,7 @@
|
||||
182.50.76.0/22 permit
|
||||
182.50.78.64/28 permit
|
||||
183.240.219.64/29 permit
|
||||
185.4.120.0/23 permit
|
||||
185.4.122.0/24 permit
|
||||
185.4.120.0/22 permit
|
||||
185.12.80.0/22 permit
|
||||
185.58.84.93 permit
|
||||
185.80.93.204 permit
|
||||
@@ -1597,9 +1578,6 @@
|
||||
185.189.236.0/22 permit
|
||||
185.211.120.0/22 permit
|
||||
185.250.236.0/22 permit
|
||||
185.250.239.148 permit
|
||||
185.250.239.168 permit
|
||||
185.250.239.190 permit
|
||||
188.125.68.132 permit
|
||||
188.125.68.152/31 permit
|
||||
188.125.68.156 permit
|
||||
@@ -1610,7 +1588,6 @@
|
||||
188.125.68.184 permit
|
||||
188.125.68.186 permit
|
||||
188.125.68.192 permit
|
||||
188.125.69.0/24 permit
|
||||
188.125.69.105 permit
|
||||
188.125.69.110 permit
|
||||
188.125.69.112 permit
|
||||
@@ -1653,9 +1630,6 @@
|
||||
192.0.64.0/18 permit
|
||||
192.18.139.154 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.162.87.0/24 permit
|
||||
192.237.158.0/23 permit
|
||||
@@ -1693,20 +1667,21 @@
|
||||
198.37.144.0/20 permit
|
||||
198.37.152.186 permit
|
||||
198.61.254.0/23 permit
|
||||
198.61.254.21 permit
|
||||
198.61.254.231 permit
|
||||
198.178.234.57 permit
|
||||
198.244.48.0/20 permit
|
||||
198.244.60.0/22 permit
|
||||
198.245.80.0/20 permit
|
||||
198.245.81.0/24 permit
|
||||
199.15.176.173 permit
|
||||
199.15.213.187 permit
|
||||
199.15.226.37 permit
|
||||
199.16.156.0/22 permit
|
||||
199.33.145.1 permit
|
||||
199.33.145.32 permit
|
||||
199.34.22.36 permit
|
||||
199.59.148.0/22 permit
|
||||
199.67.80.2 permit
|
||||
199.67.82.2 permit
|
||||
199.67.84.0/24 permit
|
||||
199.67.86.0/24 permit
|
||||
199.67.88.0/24 permit
|
||||
@@ -1734,7 +1709,6 @@
|
||||
203.188.194.251 permit
|
||||
203.188.195.240/30 permit
|
||||
203.188.195.244/31 permit
|
||||
203.188.197.0/24 permit
|
||||
203.188.197.193 permit
|
||||
203.188.197.194/31 permit
|
||||
203.188.197.196/30 permit
|
||||
@@ -1744,7 +1718,6 @@
|
||||
203.188.197.216/29 permit
|
||||
203.188.197.232/29 permit
|
||||
203.188.197.240/29 permit
|
||||
203.188.200.0/24 permit
|
||||
203.188.200.56/31 permit
|
||||
203.188.200.58 permit
|
||||
203.188.200.60/30 permit
|
||||
@@ -1764,12 +1737,12 @@
|
||||
204.14.232.0/21 permit
|
||||
204.14.232.64/28 permit
|
||||
204.14.234.64/28 permit
|
||||
204.29.186.0/23 permit
|
||||
204.75.142.0/24 permit
|
||||
204.79.197.212 permit
|
||||
204.92.114.187 permit
|
||||
204.92.114.203 permit
|
||||
204.92.114.204/31 permit
|
||||
204.132.224.66 permit
|
||||
204.141.32.0/23 permit
|
||||
204.141.42.0/23 permit
|
||||
204.220.160.0/20 permit
|
||||
@@ -1813,6 +1786,8 @@
|
||||
207.67.98.192/27 permit
|
||||
207.68.176.0/26 permit
|
||||
207.68.176.96/27 permit
|
||||
207.97.204.96 permit
|
||||
207.97.204.97 permit
|
||||
207.126.144.0/20 permit
|
||||
207.171.160.0/19 permit
|
||||
207.211.30.64/26 permit
|
||||
@@ -1826,11 +1801,7 @@
|
||||
208.43.21.28/30 permit
|
||||
208.43.21.64/29 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.71.40.0/24 permit
|
||||
208.71.40.63 permit
|
||||
208.71.40.64/31 permit
|
||||
208.71.40.174/31 permit
|
||||
@@ -1849,7 +1820,6 @@
|
||||
208.71.41.172/31 permit
|
||||
208.71.41.188/30 permit
|
||||
208.71.41.192/31 permit
|
||||
208.71.42.0/24 permit
|
||||
208.71.42.190/31 permit
|
||||
208.71.42.192/28 permit
|
||||
208.71.42.208/30 permit
|
||||
@@ -1857,10 +1827,9 @@
|
||||
208.71.42.214 permit
|
||||
208.72.249.240/29 permit
|
||||
208.74.204.0/22 permit
|
||||
208.74.204.5 permit
|
||||
208.74.204.9 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.104/31 permit
|
||||
208.82.238.96/29 permit
|
||||
@@ -1873,16 +1842,11 @@
|
||||
209.46.117.168 permit
|
||||
209.46.117.179 permit
|
||||
209.61.151.0/24 permit
|
||||
209.61.151.236 permit
|
||||
209.61.151.249 permit
|
||||
209.61.151.251 permit
|
||||
209.67.98.46 permit
|
||||
209.67.98.59 permit
|
||||
209.85.128.0/17 permit
|
||||
212.82.96.0/24 permit
|
||||
212.82.96.32/27 permit
|
||||
212.82.96.64/29 permit
|
||||
212.82.98.0/24 permit
|
||||
212.82.98.32/29 permit
|
||||
212.82.98.64/27 permit
|
||||
212.82.98.96/30 permit
|
||||
@@ -1919,12 +1883,41 @@
|
||||
212.82.111.228/31 permit
|
||||
212.82.111.230 permit
|
||||
212.123.28.40 permit
|
||||
212.227.15.0/24 permit
|
||||
212.227.15.0/25 permit
|
||||
212.227.17.0/27 permit
|
||||
212.227.126.128/25 permit
|
||||
212.227.15.3 permit
|
||||
212.227.15.4 permit
|
||||
212.227.15.5 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.165.64.0/23 permit
|
||||
213.199.128.139 permit
|
||||
213.199.128.145 permit
|
||||
213.199.138.181 permit
|
||||
@@ -1935,7 +1928,6 @@
|
||||
216.17.150.251 permit
|
||||
216.22.15.224/27 permit
|
||||
216.24.224.0/20 permit
|
||||
216.39.60.0/23 permit
|
||||
216.39.60.154/31 permit
|
||||
216.39.60.156/30 permit
|
||||
216.39.60.160/30 permit
|
||||
@@ -1953,7 +1945,6 @@
|
||||
216.39.61.170 permit
|
||||
216.39.61.175 permit
|
||||
216.39.61.238/31 permit
|
||||
216.39.62.0/24 permit
|
||||
216.39.62.32/28 permit
|
||||
216.39.62.48/29 permit
|
||||
216.39.62.56/30 permit
|
||||
@@ -1973,7 +1964,6 @@
|
||||
216.98.158.0/24 permit
|
||||
216.99.5.67 permit
|
||||
216.99.5.68 permit
|
||||
216.109.114.0/24 permit
|
||||
216.109.114.32/27 permit
|
||||
216.109.114.64/29 permit
|
||||
216.113.160.0/24 permit
|
||||
@@ -1988,10 +1978,10 @@
|
||||
216.203.30.55 permit
|
||||
216.203.33.178/31 permit
|
||||
216.205.24.0/24 permit
|
||||
216.221.160.0/19 permit
|
||||
216.239.32.0/19 permit
|
||||
217.72.192.64/26 permit
|
||||
217.72.192.248/29 permit
|
||||
217.72.207.0/27 permit
|
||||
217.72.192.77 permit
|
||||
217.72.192.78 permit
|
||||
217.77.141.52 permit
|
||||
217.77.141.59 permit
|
||||
217.175.194.0/24 permit
|
||||
|
||||
113
data/conf/rspamd/dynmaps/footer.php
Normal file
113
data/conf/rspamd/dynmaps/footer.php
Normal file
@@ -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
|
||||
# instead of silently accepting the message without further processing.
|
||||
soft_reject_on_timeout = true;
|
||||
local_addrs = /etc/rspamd/custom/mailcow_networks.map;
|
||||
|
||||
9
data/conf/rspamd/local.d/ratelimit.conf
Normal file
9
data/conf/rspamd/local.d/ratelimit.conf
Normal file
@@ -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";
|
||||
# }
|
||||
@@ -527,20 +527,21 @@ rspamd_config:register_symbol({
|
||||
name = 'MOO_FOOTER',
|
||||
type = 'prefilter',
|
||||
callback = function(task)
|
||||
local cjson = require "cjson"
|
||||
local lua_mime = require "lua_mime"
|
||||
local lua_util = require "lua_util"
|
||||
local rspamd_logger = require "rspamd_logger"
|
||||
local rspamd_redis = require "rspamd_redis"
|
||||
local ucl = require "ucl"
|
||||
local redis_params = rspamd_parse_redis_server('footer')
|
||||
local rspamd_http = require "rspamd_http"
|
||||
local envfrom = task:get_from(1)
|
||||
local uname = task:get_user()
|
||||
if not envfrom or not uname then
|
||||
return false
|
||||
end
|
||||
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 t = task:get_newlines_type()
|
||||
|
||||
@@ -552,20 +553,27 @@ rspamd_config:register_symbol({
|
||||
|
||||
return '\r\n'
|
||||
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
|
||||
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
|
||||
|
||||
-- parse json string
|
||||
local parser = ucl.parser()
|
||||
local res,err = parser:parse_string(data)
|
||||
if not res then
|
||||
local footer = cjson.decode(data)
|
||||
if not footer 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)
|
||||
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
|
||||
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 and type(footer) == "table" and (footer.html or footer.plain) then
|
||||
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain)
|
||||
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 from_name = ""
|
||||
@@ -575,6 +583,7 @@ rspamd_config:register_symbol({
|
||||
from_name = envfrom[1].name
|
||||
end
|
||||
|
||||
-- default replacements
|
||||
local replacements = {
|
||||
auth_user = uname,
|
||||
from_user = envfrom[1].user,
|
||||
@@ -582,10 +591,20 @@ rspamd_config:register_symbol({
|
||||
from_addr = envfrom[1].addr,
|
||||
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)
|
||||
end
|
||||
if footer.plain then
|
||||
if footer.plain and footer.plain ~= "" then
|
||||
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
|
||||
end
|
||||
|
||||
@@ -602,10 +621,24 @@ rspamd_config:register_symbol({
|
||||
local nct = string.format('%s: %s/%s; charset=utf-8',
|
||||
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
|
||||
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
|
||||
elseif name:lower() == 'content-transfer-encoding' then
|
||||
out[#out + 1] = string.format('%s: %s',
|
||||
'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
|
||||
return
|
||||
end
|
||||
@@ -631,15 +664,19 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
local out_parts = {}
|
||||
for _,o in ipairs(out) do
|
||||
if type(o) ~= 'table' then
|
||||
out_parts[#out_parts + 1] = o
|
||||
out_parts[#out_parts + 1] = newline_s
|
||||
else
|
||||
out_parts[#out_parts + 1] = o[1]
|
||||
if o[2] then
|
||||
out_parts[#out_parts + 1] = newline_s
|
||||
end
|
||||
end
|
||||
if type(o) ~= 'table' then
|
||||
out_parts[#out_parts + 1] = o
|
||||
out_parts[#out_parts + 1] = newline_s
|
||||
else
|
||||
local removePrefix = "--\x0D\x0AContent-Type"
|
||||
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
|
||||
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
|
||||
end
|
||||
out_parts[#out_parts + 1] = o[1]
|
||||
if o[2] then
|
||||
out_parts[#out_parts + 1] = newline_s
|
||||
end
|
||||
end
|
||||
end
|
||||
task:set_message(out_parts)
|
||||
else
|
||||
@@ -649,17 +686,14 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
end
|
||||
|
||||
local redis_ret_footer = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
env_from_domain, -- hash key
|
||||
false, -- is write
|
||||
redis_cb_footer, --callback
|
||||
'HGET', -- command
|
||||
{"DOMAIN_WIDE_FOOTER", env_from_domain} -- arguments
|
||||
)
|
||||
if not redis_ret_footer then
|
||||
rspamd_logger.infox(rspamd_config, "cannot make request to load footer for domain")
|
||||
end
|
||||
-- fetch footer
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/footer.php',
|
||||
body='',
|
||||
callback=footer_cb,
|
||||
headers={Domain=env_from_domain,Username=uname,From=env_from_addr},
|
||||
})
|
||||
|
||||
return true
|
||||
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";
|
||||
max_rcpt = 25;
|
||||
custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
|
||||
|
||||
@@ -12,9 +12,13 @@
|
||||
SOGoJunkFolderName= "Junk";
|
||||
SOGoMailDomain = "sogo.local";
|
||||
SOGoEnableEMailAlarms = YES;
|
||||
SOGoMailHideInlineAttachments = YES;
|
||||
SOGoFoldersSendEMailNotifications = YES;
|
||||
SOGoForwardEnabled = YES;
|
||||
|
||||
// Option to set Users as admin to globally manage calendar permissions etc. Disabled by default
|
||||
// SOGoSuperUsernames = ("moo@example.com");
|
||||
|
||||
SOGoUIAdditionalJSFiles = (
|
||||
js/theme.js,
|
||||
js/custom-sogo.js
|
||||
@@ -37,6 +41,7 @@
|
||||
|
||||
SOGoLanguage = English;
|
||||
SOGoMailAuxiliaryUserAccountsEnabled = YES;
|
||||
// SOGoCreateIdentitiesDisabled = NO;
|
||||
SOGoMailCustomFromEnabled = YES;
|
||||
SOGoMailingMechanism = smtp;
|
||||
SOGoSMTPAuthenticationType = plain;
|
||||
|
||||
@@ -85,6 +85,8 @@ $cors_settings = cors('get');
|
||||
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
||||
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||
|
||||
$f2b_data = fail2ban('get');
|
||||
|
||||
$template = 'admin.twig';
|
||||
$template_data = [
|
||||
'tfa_data' => $tfa_data,
|
||||
@@ -101,7 +103,8 @@ $template_data = [
|
||||
'domains' => $domains,
|
||||
'all_domains' => $all_domains,
|
||||
'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'),
|
||||
'qn_data' => quota_notification('get'),
|
||||
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
|
||||
@@ -113,6 +116,7 @@ $template_data = [
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
'lang_datatables' => json_encode($lang['datatables'])
|
||||
];
|
||||
|
||||
@@ -3137,6 +3137,86 @@ paths:
|
||||
type: string
|
||||
type: object
|
||||
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:
|
||||
post:
|
||||
responses:
|
||||
@@ -3336,6 +3416,86 @@ paths:
|
||||
type: object
|
||||
type: object
|
||||
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:
|
||||
post:
|
||||
responses:
|
||||
@@ -5581,6 +5741,7 @@ paths:
|
||||
sogo_access: "1"
|
||||
tls_enforce_in: "0"
|
||||
tls_enforce_out: "0"
|
||||
custom_attributes: {}
|
||||
domain: domain3.tld
|
||||
is_relayed: 0
|
||||
local_part: info
|
||||
@@ -5646,6 +5807,40 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
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:
|
||||
- name: Domains
|
||||
|
||||
@@ -228,8 +228,8 @@ legend {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.slave-info {
|
||||
padding: 15px 0px 15px 15px;
|
||||
font-weight: bold;
|
||||
color: orange;
|
||||
}
|
||||
.alert-hr {
|
||||
margin:3px 0px;
|
||||
|
||||
@@ -175,6 +175,9 @@ pre {
|
||||
background-color: #282828;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
.form-control {
|
||||
background-color: transparent;
|
||||
}
|
||||
input.form-control, textarea.form-control {
|
||||
color: #e2e2e2 !important;
|
||||
background-color: #424242 !important;
|
||||
|
||||
@@ -39,9 +39,13 @@ foreach ($containers as $container => $container_info) {
|
||||
$StartedAt['month'],
|
||||
$StartedAt['day'],
|
||||
$StartedAt['year']));
|
||||
$user_tz = new DateTimeZone(getenv('TZ'));
|
||||
$date->setTimezone($user_tz);
|
||||
$started = $date->format('r');
|
||||
try {
|
||||
$user_tz = new DateTimeZone(getenv('TZ'));
|
||||
$date->setTimezone($user_tz);
|
||||
$started = $date->format('r');
|
||||
} catch(Exception $e) {
|
||||
$started = '?';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$started = '?';
|
||||
|
||||
@@ -58,6 +58,9 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'dkim' => dkim('details', $domain),
|
||||
'domain_details' => $result,
|
||||
'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['return_to'] = $_SESSION['return_to'];
|
||||
$template_data['lang_user'] = json_encode($lang['user']);
|
||||
$template_data['lang_admin'] = json_encode($lang['admin']);
|
||||
$template_data['lang_datatables'] = json_encode($lang['datatables']);
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
|
||||
@@ -12,7 +12,8 @@ $alertbox_log_parser = alertbox_log_parser($_SESSION);
|
||||
$alerts = [];
|
||||
if (is_array($alertbox_log_parser)) {
|
||||
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, '"');
|
||||
}
|
||||
$alert = array_filter(array_unique($alerts));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
function customize($_action, $_item, $_data = null) {
|
||||
global $redis;
|
||||
global $lang;
|
||||
global $LOGO_LIMITS;
|
||||
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
@@ -35,6 +36,23 @@ function customize($_action, $_item, $_data = null) {
|
||||
);
|
||||
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']);
|
||||
if ($image->valid() !== true) {
|
||||
$_SESSION['return'][] = array(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
function fail2ban($_action, $_data = null) {
|
||||
function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
global $redis;
|
||||
$_data_log = $_data;
|
||||
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']);
|
||||
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
|
||||
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
|
||||
$manage_external = (isset($_data['manage_external'])) ? intval($_data['manage_external']) : 0;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -266,6 +267,8 @@ function fail2ban($_action, $_data = null) {
|
||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
||||
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
||||
$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 {
|
||||
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||
$redis->Del('F2B_WHITELIST');
|
||||
@@ -329,5 +332,71 @@ function fail2ban($_action, $_data = null) {
|
||||
'msg' => 'f2b_modified'
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2246,6 +2246,21 @@ function cors($action, $data = null) {
|
||||
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) {
|
||||
if ($lines === false) {
|
||||
|
||||
@@ -325,6 +325,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$timeout2 = intval($_data['timeout2']);
|
||||
$skipcrossduplicates = intval($_data['skipcrossduplicates']);
|
||||
$automap = intval($_data['automap']);
|
||||
$dry = intval($_data['dry']);
|
||||
$port1 = $_data['port1'];
|
||||
$host1 = strtolower($_data['host1']);
|
||||
$password1 = $_data['password1'];
|
||||
@@ -435,8 +436,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
|
||||
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
|
||||
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `dry`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
|
||||
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :dry, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
|
||||
$stmt->execute(array(
|
||||
':user2' => $username,
|
||||
':custom_params' => $custom_params,
|
||||
@@ -450,6 +451,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':skipcrossduplicates' => $skipcrossduplicates,
|
||||
':maxbytespersecond' => $maxbytespersecond,
|
||||
':subscribeall' => $subscribeall,
|
||||
':dry' => $dry,
|
||||
':subfolder2' => $subfolder2,
|
||||
':host1' => $host1,
|
||||
':authmech1' => 'PLAIN',
|
||||
@@ -476,16 +478,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
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);
|
||||
$description = $_data['description'];
|
||||
if (empty($description)) $description = $domain;
|
||||
$tags = (array)$_data['tags'];
|
||||
$aliases = (int)$_data['aliases'];
|
||||
$mailboxes = (int)$_data['mailboxes'];
|
||||
$defquota = (int)$_data['defquota'];
|
||||
$maxquota = (int)$_data['maxquota'];
|
||||
$tags = (isset($_data['tags'])) ? (array)$_data['tags'] : $DOMAIN_DEFAULT_ATTRIBUTES['tags'];
|
||||
$aliases = (isset($_data['aliases'])) ? (int)$_data['aliases'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_aliases_for_domain'];
|
||||
$mailboxes = (isset($_data['mailboxes'])) ? (int)$_data['mailboxes'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_mboxes_for_domain'];
|
||||
$defquota = (isset($_data['defquota'])) ? (int)$_data['defquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['def_quota_for_mbox'] / 1024 ** 2;
|
||||
$maxquota = (isset($_data['maxquota'])) ? (int)$_data['maxquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_mbox'] / 1024 ** 2;
|
||||
$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) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -518,11 +528,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$active = intval($_data['active']);
|
||||
$relay_all_recipients = intval($_data['relay_all_recipients']);
|
||||
$relay_unknown_only = intval($_data['relay_unknown_only']);
|
||||
$backupmx = intval($_data['backupmx']);
|
||||
$gal = intval($_data['gal']);
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $DOMAIN_DEFAULT_ATTRIBUTES['active'];
|
||||
$relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_all_recipients'];
|
||||
$relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_unknown_only'];
|
||||
$backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $DOMAIN_DEFAULT_ATTRIBUTES['backupmx'];
|
||||
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $DOMAIN_DEFAULT_ATTRIBUTES['gal'];
|
||||
if ($relay_all_recipients == 1) {
|
||||
$backupmx = '1';
|
||||
}
|
||||
@@ -623,9 +633,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
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));
|
||||
}
|
||||
$_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($redis->hGet('DKIM_SELECTORS', $domain))) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -1004,11 +1018,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
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'];
|
||||
$password2 = $_data['password2'];
|
||||
$name = ltrim(rtrim($_data['name'], '>'), '<');
|
||||
$tags = $_data['tags'];
|
||||
$quota_m = intval($_data['quota']);
|
||||
$tags = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags'];
|
||||
$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) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -1017,9 +1043,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($name)) {
|
||||
$name = $local_part;
|
||||
}
|
||||
|
||||
if (isset($_data['protocol_access'])) {
|
||||
$_data['protocol_access'] = (array)$_data['protocol_access'];
|
||||
$_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
|
||||
@@ -1027,7 +1051,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$_data['smtp_access'] = (in_array('smtp', $_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']);
|
||||
$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']);
|
||||
@@ -1225,12 +1249,29 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$_data['quarantine_notification'] = (in_array('quarantine_notification', $_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;
|
||||
} 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`
|
||||
(`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,
|
||||
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
|
||||
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':spam_alias' => $_data['spam_alias'],
|
||||
@@ -1249,31 +1290,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':app_passwds' => $_data['app_passwds']
|
||||
));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("INSERT INTO `user_acl`
|
||||
(`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`)
|
||||
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) ");
|
||||
$stmt->execute(array(
|
||||
':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
|
||||
));
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$_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'])){
|
||||
ratelimit('edit', 'mailbox', array(
|
||||
'object' => $username,
|
||||
@@ -1522,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']);
|
||||
if (isset($_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['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
||||
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
||||
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
||||
$attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||
}
|
||||
else {
|
||||
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
|
||||
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
||||
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
||||
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
||||
}
|
||||
}
|
||||
if (isset($_data['acl'])) {
|
||||
$_data['acl'] = (array)$_data['acl'];
|
||||
$attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
|
||||
@@ -2031,6 +2058,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$success = (isset($_data['success'])) ? NULL : $is_now['success'];
|
||||
$delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
|
||||
$subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall'];
|
||||
$dry = (isset($_data['dry'])) ? intval($_data['dry']) : $is_now['dry'];
|
||||
$delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
|
||||
$delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
|
||||
$automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap'];
|
||||
@@ -2164,6 +2192,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`timeout1` = :timeout1,
|
||||
`timeout2` = :timeout2,
|
||||
`subscribeall` = :subscribeall,
|
||||
`dry` = :dry,
|
||||
`active` = :active
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
@@ -2189,6 +2218,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':timeout1' => $timeout1,
|
||||
':timeout2' => $timeout2,
|
||||
':subscribeall' => $subscribeall,
|
||||
':dry' => $dry,
|
||||
':active' => $active,
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -3259,6 +3289,62 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
return true;
|
||||
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':
|
||||
if (!is_array($_data['name'])) {
|
||||
$names = array();
|
||||
@@ -3338,44 +3424,116 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'domain_wide_footer':
|
||||
$domain = idn_to_ascii(strtolower(trim($_data['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;
|
||||
case 'domain_wide_footer':
|
||||
if (!is_array($_data['domains'])) {
|
||||
$domains = array();
|
||||
$domains[] = $_data['domains'];
|
||||
}
|
||||
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;
|
||||
else {
|
||||
$domains = $_data['domains'];
|
||||
}
|
||||
|
||||
$footers = array();
|
||||
$footers['html'] = isset($_data['footer_html']) ? $_data['footer_html'] : '';
|
||||
$footers['plain'] = isset($_data['footer_plain']) ? $_data['footer_plain'] : '';
|
||||
try {
|
||||
$redis->hSet('DOMAIN_WIDE_FOOTER', $domain, json_encode($footers));
|
||||
$footers['html'] = isset($_data['html']) ? $_data['html'] : '';
|
||||
$footers['plain'] = isset($_data['plain']) ? $_data['plain'] : '';
|
||||
$footers['skip_replies'] = isset($_data['skip_replies']) ? (int)$_data['skip_replies'] : 0;
|
||||
$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(
|
||||
'type' => 'danger',
|
||||
'type' => 'success',
|
||||
'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;
|
||||
@@ -3929,13 +4087,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||
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(
|
||||
':domain' => $_data,
|
||||
));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$aliases[] = $row['id'];
|
||||
if ($_extra == "address"){
|
||||
$aliases[] = $row['address'];
|
||||
} else {
|
||||
$aliases[] = $row['id'];
|
||||
}
|
||||
}
|
||||
return $aliases;
|
||||
break;
|
||||
@@ -4179,6 +4341,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
|
||||
$domaindata['mboxes_left'] = $row['mailboxes'] - $MailboxDataDomain['count'];
|
||||
$domaindata['domain_name'] = $row['domain'];
|
||||
$domaindata['domain_h_name'] = idn_to_utf8($row['domain']);
|
||||
$domaindata['description'] = $row['description'];
|
||||
$domaindata['max_num_aliases_for_domain'] = $row['aliases'];
|
||||
$domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
|
||||
@@ -4287,6 +4450,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`mailbox`.`modified`,
|
||||
`quota2`.`bytes`,
|
||||
`attributes`,
|
||||
`custom_attributes`,
|
||||
`quota2`.`messages`
|
||||
FROM `mailbox`, `quota2`, `domain`
|
||||
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
||||
@@ -4307,6 +4471,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`mailbox`.`modified`,
|
||||
`quota2replica`.`bytes`,
|
||||
`attributes`,
|
||||
`custom_attributes`,
|
||||
`quota2replica`.`messages`
|
||||
FROM `mailbox`, `quota2replica`, `domain`
|
||||
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
||||
@@ -4323,12 +4488,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$mailboxdata['active'] = $row['active'];
|
||||
$mailboxdata['active_int'] = $row['active'];
|
||||
$mailboxdata['domain'] = $row['domain'];
|
||||
$mailboxdata['relayhost'] = $row['relayhost'];
|
||||
$mailboxdata['name'] = $row['name'];
|
||||
$mailboxdata['local_part'] = $row['local_part'];
|
||||
$mailboxdata['quota'] = $row['quota'];
|
||||
$mailboxdata['messages'] = $row['messages'];
|
||||
$mailboxdata['attributes'] = json_decode($row['attributes'], true);
|
||||
$mailboxdata['custom_attributes'] = json_decode($row['custom_attributes'], true);
|
||||
$mailboxdata['quota_used'] = intval($row['bytes']);
|
||||
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
|
||||
$mailboxdata['created'] = $row['created'];
|
||||
@@ -4509,19 +4674,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
|
||||
try {
|
||||
$footers = $redis->hGet('DOMAIN_WIDE_FOOTER', $domain);
|
||||
$footers = json_decode($footers, true);
|
||||
$stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer`
|
||||
WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain
|
||||
));
|
||||
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('redis_error', $e)
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $footers;
|
||||
return $footer;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -143,6 +143,7 @@ function rspamd_maps($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
$maps = (array)$_data['map'];
|
||||
$valid_maps = array();
|
||||
foreach ($maps as $map) {
|
||||
foreach ($RSPAMD_MAPS as $rspamd_map_type) {
|
||||
if (!in_array($map, $rspamd_map_type)) {
|
||||
@@ -151,9 +152,12 @@ function rspamd_maps($_action, $_data = null) {
|
||||
'log' => array(__FUNCTION__, $_action, '-'),
|
||||
'msg' => array('global_map_invalid', $map)
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
array_push($valid_maps, $map);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($valid_maps as $map) {
|
||||
try {
|
||||
if (file_exists('/rspamd_custom_maps/' . $map)) {
|
||||
$map_content = trim($_data['rspamd_map_data']);
|
||||
|
||||
@@ -3,7 +3,7 @@ function init_db_schema() {
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "14022023_1000";
|
||||
$db_version = "26022024_1433";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$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"
|
||||
),
|
||||
"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(
|
||||
"cols" => array(
|
||||
"tag_name" => "VARCHAR(255) NOT NULL",
|
||||
@@ -344,6 +360,7 @@ function init_db_schema() {
|
||||
"local_part" => "VARCHAR(255) NOT NULL",
|
||||
"domain" => "VARCHAR(255) NOT NULL",
|
||||
"attributes" => "JSON",
|
||||
"custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
|
||||
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
|
||||
"multiple_bookings" => "INT NOT NULL DEFAULT -1",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
@@ -704,6 +721,7 @@ function init_db_schema() {
|
||||
"timeout1" => "SMALLINT NOT NULL DEFAULT '600'",
|
||||
"timeout2" => "SMALLINT NOT NULL DEFAULT '600'",
|
||||
"subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"dry" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"returned_text" => "LONGTEXT",
|
||||
"last_run" => "TIMESTAMP NULL DEFAULT NULL",
|
||||
@@ -961,6 +979,18 @@ function init_db_schema() {
|
||||
),
|
||||
"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(
|
||||
"cols" => array(
|
||||
"username" => "VARCHAR(255) NOT NULL",
|
||||
|
||||
622
data/web/inc/lib/ssp.class.php
Normal file
622
data/web/inc/lib/ssp.class.php
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
|
||||
@@ -52,10 +52,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
dependency-version: [prefer-lowest, prefer-stable]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer update --no-progress --ignore-platform-reqs
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Install PHP with extensions"
|
||||
uses: shivammathur/setup-php@v2
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Install PHP with extensions"
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Set-up PHP"
|
||||
uses: shivammathur/setup-php@v2
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.composercache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Run DOCtor-RST"
|
||||
uses: docker://oskarstark/doctor-rst
|
||||
|
||||
@@ -70,6 +70,8 @@ try {
|
||||
}
|
||||
}
|
||||
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>
|
||||
<?php
|
||||
@@ -98,6 +100,7 @@ try {
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
// 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>
|
||||
<?php
|
||||
@@ -105,6 +108,7 @@ exit;
|
||||
}
|
||||
// Stop when dockerapi is not available
|
||||
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>
|
||||
<?php
|
||||
|
||||
18
data/web/inc/presets/sieve/sieve_8.yml
Normal file
18
data/web/inc/presets/sieve/sieve_8.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
headline: lang.sieve_preset_8
|
||||
content: |
|
||||
require "fileinto";
|
||||
require "mailbox";
|
||||
require "variables";
|
||||
require "subaddress";
|
||||
require "envelope";
|
||||
require "duplicate";
|
||||
require "imap4flags";
|
||||
if header :matches "To" "*mail@domain.tld*" {
|
||||
redirect "anothermail@anotherdomain.tld";
|
||||
setflag "\\seen"; /* Mark mail as read */
|
||||
fileInto "INBOX/SubFolder"; /* Move mail on subfolder after */
|
||||
} else {
|
||||
# The rest goes into INBOX
|
||||
# default is "implicit keep", we do it explicitly here
|
||||
keep;
|
||||
}
|
||||
@@ -95,8 +95,11 @@ $AVAILABLE_LANGUAGES = array(
|
||||
'it-it' => 'Italiano (Italian)',
|
||||
'ko-kr' => '한국어 (Korean)',
|
||||
'lv-lv' => 'latviešu (Latvian)',
|
||||
'lt-lt' => 'Lietuvių (Lithuanian)',
|
||||
'nb-no' => 'Norsk (Norwegian)',
|
||||
'nl-nl' => 'Nederlands (Dutch)',
|
||||
'pl-pl' => 'Język Polski (Polish)',
|
||||
'pt-br' => 'Português brasileiro (Brazilian Portuguese)',
|
||||
'pt-pt' => 'Português (Portuguese)',
|
||||
'ro-ro' => 'Română (Romanian)',
|
||||
'ru-ru' => 'Pусский (Russian)',
|
||||
@@ -125,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
|
||||
$PAGINATION_SIZE = 25;
|
||||
|
||||
@@ -235,118 +247,120 @@ $RSPAMD_MAPS = array(
|
||||
|
||||
$IMAPSYNC_OPTIONS = array(
|
||||
'whitelist' => array(
|
||||
'abort',
|
||||
'authmd51',
|
||||
'authmd52',
|
||||
'authmech1',
|
||||
'authmech2',
|
||||
'authuser1',
|
||||
'authuser2',
|
||||
'debugcontent',
|
||||
'disarmreadreceipts',
|
||||
'logdir',
|
||||
'debugcrossduplicates',
|
||||
'maxsize',
|
||||
'minsize',
|
||||
'minage',
|
||||
'search',
|
||||
'noabletosearch',
|
||||
'pidfile',
|
||||
'pidfilelocking',
|
||||
'search1',
|
||||
'search2',
|
||||
'sslargs1',
|
||||
'sslargs2',
|
||||
'syncduplicates',
|
||||
'usecache',
|
||||
'synclabels',
|
||||
'truncmess',
|
||||
'domino2',
|
||||
'expunge1',
|
||||
'filterbuggyflags',
|
||||
'justconnect',
|
||||
'justfolders',
|
||||
'maxlinelength',
|
||||
'useheader',
|
||||
'noabletosearch1',
|
||||
'nolog',
|
||||
'prefix1',
|
||||
'prefix2',
|
||||
'sep1',
|
||||
'sep2',
|
||||
'nofoldersizesatend',
|
||||
'justfoldersizes',
|
||||
'proxyauth1',
|
||||
'skipemptyfolders',
|
||||
'include',
|
||||
'subfolder1',
|
||||
'subscribed',
|
||||
'subscribe',
|
||||
'debug',
|
||||
'debugcontent',
|
||||
'debugcrossduplicates',
|
||||
'debugflags',
|
||||
'debugfolders',
|
||||
'debugimap',
|
||||
'debugimap1',
|
||||
'debugimap2',
|
||||
'debugmemory',
|
||||
'debugssl',
|
||||
'delete1emptyfolders',
|
||||
'delete2folders',
|
||||
'disarmreadreceipts',
|
||||
'domain1',
|
||||
'domain2',
|
||||
'domino1',
|
||||
'domino2',
|
||||
'dry',
|
||||
'errorsmax',
|
||||
'exchange1',
|
||||
'exchange2',
|
||||
'exitwhenover',
|
||||
'expunge1',
|
||||
'f1f2',
|
||||
'filterbuggyflags',
|
||||
'folder',
|
||||
'folderfirst',
|
||||
'folderlast',
|
||||
'folderrec',
|
||||
'gmail1',
|
||||
'gmail2',
|
||||
'idatefromheader',
|
||||
'include',
|
||||
'inet4',
|
||||
'inet6',
|
||||
'justconnect',
|
||||
'justfolders',
|
||||
'justfoldersizes',
|
||||
'justlogin',
|
||||
'keepalive1',
|
||||
'keepalive2',
|
||||
'log',
|
||||
'logdir',
|
||||
'logfile',
|
||||
'maxbytesafter',
|
||||
'maxlinelength',
|
||||
'maxmessagespersecond',
|
||||
'maxsize',
|
||||
'maxsleep',
|
||||
'minage',
|
||||
'minsize',
|
||||
'noabletosearch',
|
||||
'noabletosearch1',
|
||||
'noabletosearch2',
|
||||
'noexpunge1',
|
||||
'noexpunge2',
|
||||
'nofoldersizesatend',
|
||||
'noid',
|
||||
'nolog',
|
||||
'nomixfolders',
|
||||
'noresyncflags',
|
||||
'nossl1',
|
||||
'nouidexpunge2',
|
||||
'syncinternaldates',
|
||||
'idatefromheader',
|
||||
'useuid',
|
||||
'debugflags',
|
||||
'debugimap',
|
||||
'delete1emptyfolders',
|
||||
'delete2folders',
|
||||
'gmail2',
|
||||
'office1',
|
||||
'testslive6',
|
||||
'debugimap1',
|
||||
'errorsmax',
|
||||
'tests',
|
||||
'gmail1',
|
||||
'maxmessagespersecond',
|
||||
'maxbytesafter',
|
||||
'maxsleep',
|
||||
'abort',
|
||||
'resyncflags',
|
||||
'resynclabels',
|
||||
'syncacls',
|
||||
'nossl2',
|
||||
'nosyncacls',
|
||||
'notls1',
|
||||
'notls2',
|
||||
'nouidexpunge2',
|
||||
'nousecache',
|
||||
'office2',
|
||||
'testslive',
|
||||
'debugmemory',
|
||||
'exitwhenover',
|
||||
'noid',
|
||||
'noexpunge1',
|
||||
'authmd51',
|
||||
'logfile',
|
||||
'proxyauth2',
|
||||
'domain1',
|
||||
'domain2',
|
||||
'oauthaccesstoken1',
|
||||
'oauthaccesstoken2',
|
||||
'oauthdirect1',
|
||||
'oauthdirect2',
|
||||
'folder',
|
||||
'folderrec',
|
||||
'folderfirst',
|
||||
'folderlast',
|
||||
'nomixfolders',
|
||||
'authmd52',
|
||||
'debugfolders',
|
||||
'nossl2',
|
||||
'office1',
|
||||
'office2',
|
||||
'pidfile',
|
||||
'pidfilelocking',
|
||||
'prefix1',
|
||||
'prefix2',
|
||||
'proxyauth1',
|
||||
'proxyauth2',
|
||||
'resyncflags',
|
||||
'resynclabels',
|
||||
'search',
|
||||
'search1',
|
||||
'search2',
|
||||
'sep1',
|
||||
'sep2',
|
||||
'showpasswords',
|
||||
'skipemptyfolders',
|
||||
'ssl2',
|
||||
'sslargs1',
|
||||
'sslargs2',
|
||||
'subfolder1',
|
||||
'subscribe',
|
||||
'subscribed',
|
||||
'syncacls',
|
||||
'syncduplicates',
|
||||
'syncinternaldates',
|
||||
'synclabels',
|
||||
'tests',
|
||||
'testslive',
|
||||
'testslive6',
|
||||
'tls2',
|
||||
'notls2',
|
||||
'debugssl',
|
||||
'notls1',
|
||||
'inet4',
|
||||
'inet6',
|
||||
'log',
|
||||
'showpasswords'
|
||||
'truncmess',
|
||||
'usecache',
|
||||
'useheader',
|
||||
'useuid'
|
||||
),
|
||||
'blacklist' => array(
|
||||
'skipmess',
|
||||
|
||||
@@ -391,3 +391,11 @@ function addTag(tagAddElem, tag = null){
|
||||
$(tagValuesElem).val(JSON.stringify(value_tags));
|
||||
$(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");
|
||||
}
|
||||
@@ -1684,7 +1684,7 @@ function showVersionModal(title, version){
|
||||
function parseGithubMarkdownLinks(inputText) {
|
||||
var replacedText, replacePattern1;
|
||||
|
||||
replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
|
||||
replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])(?![^<]*>)/gim;
|
||||
replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
|
||||
if (matched.includes('github.com')){
|
||||
// return short link if it's github link
|
||||
|
||||
@@ -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
|
||||
function onVisible(element, callback) {
|
||||
|
||||
@@ -435,7 +435,7 @@ jQuery(function($){
|
||||
var table = $('#domain_table').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||
@@ -447,10 +447,14 @@ jQuery(function($){
|
||||
},
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/domain/all",
|
||||
url: "/api/v1/get/domain/datatables",
|
||||
dataSrc: function(json){
|
||||
$.each(json, function(i, item) {
|
||||
$.each(json.data, function(i, item) {
|
||||
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.mailboxes = item.mboxes_in_domain + " / " + item.max_num_mboxes_for_domain;
|
||||
@@ -489,16 +493,16 @@ jQuery(function($){
|
||||
|
||||
if (item.backupmx == 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) {
|
||||
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 {
|
||||
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: [
|
||||
@@ -521,24 +525,27 @@ jQuery(function($){
|
||||
},
|
||||
{
|
||||
title: lang.domain,
|
||||
data: 'domain_name',
|
||||
data: 'domain_h_name',
|
||||
responsivePriority: 3,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.aliases,
|
||||
data: 'aliases',
|
||||
searchable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.mailboxes,
|
||||
data: 'mailboxes',
|
||||
searchable: false,
|
||||
responsivePriority: 4,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.domain_quota,
|
||||
data: 'quota',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
render: function (data, type) {
|
||||
data = data.split("/");
|
||||
@@ -548,6 +555,7 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.stats,
|
||||
data: 'stats',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
render: function (data, type) {
|
||||
data = data.split("/");
|
||||
@@ -557,53 +565,67 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.mailbox_defquota,
|
||||
data: 'def_quota_for_mbox',
|
||||
searchable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.mailbox_quota,
|
||||
data: 'max_quota_for_mbox',
|
||||
searchable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'RL',
|
||||
data: 'rl',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.backup_mx,
|
||||
data: 'backupmx',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
redner: function (data, type){
|
||||
return 1==value ? '<i class="bi bi-check-lg"></i>' : 0==value && '<i class="bi bi-x-lg"></i>';
|
||||
render: function (data, type){
|
||||
return 1==data ? '<i class="bi bi-check-lg"></i>' : 0==data && '<i class="bi bi-x-lg"></i>';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.domain_admins,
|
||||
data: 'domain_admins',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: '',
|
||||
className: 'none'
|
||||
},
|
||||
{
|
||||
title: lang.created_on,
|
||||
data: 'created',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: '',
|
||||
className: 'none'
|
||||
},
|
||||
{
|
||||
title: lang.last_modified,
|
||||
data: 'modified',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: '',
|
||||
className: 'none'
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
data: 'tags',
|
||||
searchable: true,
|
||||
orderable: false,
|
||||
defaultContent: '',
|
||||
className: 'none'
|
||||
},
|
||||
{
|
||||
title: lang.active,
|
||||
data: 'active',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
responsivePriority: 6,
|
||||
render: function (data, type) {
|
||||
@@ -613,6 +635,8 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.action,
|
||||
data: 'action',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
|
||||
responsivePriority: 5,
|
||||
defaultContent: ''
|
||||
@@ -844,7 +868,7 @@ jQuery(function($){
|
||||
var table = $('#mailbox_table').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
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,
|
||||
initComplete: function(settings, json){
|
||||
hideTableExpandCollapseBtn('#tab-mailboxes', '#mailbox_table');
|
||||
filterByDomain(json, 8, table);
|
||||
},
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/mailbox/reduced",
|
||||
url: "/api/v1/get/mailbox/datatables",
|
||||
dataSrc: function(json){
|
||||
$.each(json, function (i, item) {
|
||||
$.each(json.data, function (i, item) {
|
||||
item.quota = {
|
||||
sortBy: item.quota_used,
|
||||
value: item.quota
|
||||
@@ -945,7 +968,7 @@ jQuery(function($){
|
||||
}
|
||||
});
|
||||
|
||||
return json;
|
||||
return json.data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
@@ -975,13 +998,14 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.domain_quota,
|
||||
data: 'quota.value',
|
||||
searchable: false,
|
||||
responsivePriority: 8,
|
||||
defaultContent: '',
|
||||
orderData: 23
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.last_mail_login,
|
||||
data: 'last_mail_login',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
responsivePriority: 7,
|
||||
render: function (data, type) {
|
||||
@@ -994,15 +1018,16 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.last_pw_change,
|
||||
data: 'last_pw_change',
|
||||
searchable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.in_use,
|
||||
data: 'in_use.value',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
responsivePriority: 9,
|
||||
className: 'dt-data-w100',
|
||||
orderData: 24
|
||||
className: 'dt-data-w100'
|
||||
},
|
||||
{
|
||||
title: lang.fname,
|
||||
@@ -1067,6 +1092,7 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.msg_num,
|
||||
data: 'messages',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
responsivePriority: 5
|
||||
},
|
||||
@@ -1085,12 +1111,14 @@ jQuery(function($){
|
||||
{
|
||||
title: 'Tags',
|
||||
data: 'tags',
|
||||
searchable: true,
|
||||
defaultContent: '',
|
||||
className: 'none'
|
||||
},
|
||||
{
|
||||
title: lang.active,
|
||||
data: 'active',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
responsivePriority: 4,
|
||||
render: function (data, type) {
|
||||
@@ -1100,22 +1128,12 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.action,
|
||||
data: 'action',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
|
||||
responsivePriority: 6,
|
||||
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';
|
||||
else if (value.score < 0) highlightClass = 'positive';
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ jQuery(function($){
|
||||
if (value.score > 0) highlightClass = 'negative';
|
||||
else if (value.score < 0) highlightClass = 'positive';
|
||||
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();
|
||||
}
|
||||
@@ -295,3 +295,7 @@ jQuery(function($){
|
||||
$(".table_collapse_option").hide();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ function api_log($_data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = json_decode($value, true);
|
||||
$value = json_decode($value, true);
|
||||
if ($value) {
|
||||
if (is_array($value)) unset($value["csrf_token"]);
|
||||
foreach ($value as $key => &$val) {
|
||||
@@ -23,7 +23,7 @@ function api_log($_data) {
|
||||
$val = '*';
|
||||
}
|
||||
}
|
||||
$value = json_encode($value);
|
||||
$value = json_encode($value);
|
||||
}
|
||||
$data_var[] = $data . "='" . $value . "'";
|
||||
}
|
||||
@@ -44,7 +44,13 @@ function api_log($_data) {
|
||||
'msg' => 'Redis: '.$e
|
||||
);
|
||||
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'])) {
|
||||
@@ -178,12 +184,12 @@ if (isset($_GET['query'])) {
|
||||
// parse post data
|
||||
$post = trim(file_get_contents('php://input'));
|
||||
if ($post) $post = json_decode($post);
|
||||
|
||||
|
||||
// process registration data from authenticator
|
||||
try {
|
||||
// decode base64 strings
|
||||
$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)
|
||||
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true);
|
||||
@@ -250,7 +256,7 @@ if (isset($_GET['query'])) {
|
||||
default:
|
||||
process_add_return(mailbox('add', 'domain', $attr));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "resource":
|
||||
process_add_return(mailbox('add', 'resource', $attr));
|
||||
@@ -470,7 +476,7 @@ if (isset($_GET['query'])) {
|
||||
// false, if only internal 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);
|
||||
|
||||
|
||||
print(json_encode($createArgs));
|
||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||
return;
|
||||
@@ -503,6 +509,16 @@ if (isset($_GET['query'])) {
|
||||
print(json_encode($getArgs));
|
||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||
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;
|
||||
}
|
||||
if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
@@ -523,9 +539,50 @@ if (isset($_GET['query'])) {
|
||||
|
||||
case "domain":
|
||||
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":
|
||||
$tags = null;
|
||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||
$tags = explode(',', $_GET['tags']);
|
||||
|
||||
$domains = mailbox('get', 'domains', null, $tags);
|
||||
@@ -1011,10 +1068,49 @@ if (isset($_GET['query'])) {
|
||||
break;
|
||||
case "mailbox":
|
||||
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 "reduced":
|
||||
$tags = null;
|
||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||
$tags = explode(',', $_GET['tags']);
|
||||
|
||||
if (empty($extra)) $domains = mailbox('get', 'domains');
|
||||
@@ -1048,7 +1144,7 @@ if (isset($_GET['query'])) {
|
||||
break;
|
||||
default:
|
||||
$tags = null;
|
||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||
if (isset($_GET['tags']) && $_GET['tags'] != '')
|
||||
$tags = explode(',', $_GET['tags']);
|
||||
|
||||
if ($tags === null) {
|
||||
@@ -1058,7 +1154,7 @@ if (isset($_GET['query'])) {
|
||||
$mailboxes = mailbox('get', 'mailboxes', $object, $tags);
|
||||
if (is_array($mailboxes)) {
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
if ($details = mailbox('get', 'mailbox_details', $mailbox))
|
||||
if ($details = mailbox('get', 'mailbox_details', $mailbox))
|
||||
$data[] = $details;
|
||||
}
|
||||
}
|
||||
@@ -1324,6 +1420,10 @@ if (isset($_GET['query'])) {
|
||||
break;
|
||||
case "fail2ban":
|
||||
switch ($object) {
|
||||
case 'banlist':
|
||||
header('Content-Type: text/plain');
|
||||
echo fail2ban('banlist', 'get', $extra);
|
||||
break;
|
||||
default:
|
||||
$data = fail2ban('get');
|
||||
process_get_return($data);
|
||||
@@ -1557,15 +1657,15 @@ if (isset($_GET['query'])) {
|
||||
'solr_size' => $solr_size,
|
||||
'solr_documents' => $solr_documents
|
||||
));
|
||||
break;
|
||||
break;
|
||||
case "host":
|
||||
if (!$extra){
|
||||
$stats = docker("host_stats");
|
||||
echo json_encode($stats);
|
||||
}
|
||||
}
|
||||
else if ($extra == "ip") {
|
||||
// get public ips
|
||||
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
@@ -1591,6 +1691,12 @@ if (isset($_GET['query'])) {
|
||||
}
|
||||
}
|
||||
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;
|
||||
// return no route found if no case is matched
|
||||
default:
|
||||
@@ -1867,8 +1973,6 @@ if (isset($_GET['query'])) {
|
||||
case "quota_notification_bcc":
|
||||
process_edit_return(quota_notification_bcc('edit', $attr));
|
||||
break;
|
||||
case "domain-wide-footer":
|
||||
process_edit_return(mailbox('edit', 'domain_wide_footer', $attr));
|
||||
break;
|
||||
case "mailq":
|
||||
process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr)));
|
||||
@@ -1881,6 +1985,9 @@ if (isset($_GET['query'])) {
|
||||
case "template":
|
||||
process_edit_return(mailbox('edit', 'mailbox_templates', array_merge(array('ids' => $items), $attr)));
|
||||
break;
|
||||
case "custom-attribute":
|
||||
process_edit_return(mailbox('edit', 'mailbox_custom_attribute', array_merge(array('mailboxes' => $items), $attr)));
|
||||
break;
|
||||
default:
|
||||
process_edit_return(mailbox('edit', 'mailbox', array_merge(array('username' => $items), $attr)));
|
||||
break;
|
||||
@@ -1900,6 +2007,9 @@ if (isset($_GET['query'])) {
|
||||
case "template":
|
||||
process_edit_return(mailbox('edit', 'domain_templates', array_merge(array('ids' => $items), $attr)));
|
||||
break;
|
||||
case "footer":
|
||||
process_edit_return(mailbox('edit', 'domain_wide_footer', array_merge(array('domains' => $items), $attr)));
|
||||
break;
|
||||
default:
|
||||
process_edit_return(mailbox('edit', 'domain', array_merge(array('domain' => $items), $attr)));
|
||||
break;
|
||||
@@ -1933,7 +2043,14 @@ if (isset($_GET['query'])) {
|
||||
process_edit_return(fwdhost('edit', array_merge(array('fwdhost' => $items), $attr)));
|
||||
break;
|
||||
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;
|
||||
case "ui_texts":
|
||||
process_edit_return(customize('edit', 'ui_texts', $attr));
|
||||
@@ -1972,7 +2089,7 @@ if (isset($_GET['query'])) {
|
||||
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) {
|
||||
unset($_SESSION['return']);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
"username": "Uživatelské jméno",
|
||||
"validate": "Ověřit",
|
||||
"validation_success": "Úspěšně ověřeno",
|
||||
"tags": "Štítky"
|
||||
"tags": "Štítky",
|
||||
"dry": "Simulovat synchronizaci"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Přístupy",
|
||||
@@ -147,6 +148,8 @@
|
||||
"ays": "Opravdu chcete pokračovat?",
|
||||
"ban_list_info": "Seznam blokovaných IP adres je zobrazen níže: <b>síť (zbývající čas blokování) - [akce]</b>.<br />IP adresy zařazené pro odblokování budou z aktivního seznamu odebrány během několika sekund.<br />Červeně označené položky jsou pernamentní bloky z blacklistu.",
|
||||
"change_logo": "Změnit logo",
|
||||
"logo_normal_label": "Normální",
|
||||
"logo_dark_label": "Inverzní pro tmavý režim",
|
||||
"configuration": "Nastavení",
|
||||
"convert_html_to_text": "Převést HTML do prostého textu",
|
||||
"credentials_transport_warning": "<b>Upozornění</b>: Přidání položky do transportní mapy aktualizuje také přihlašovací údaje všech záznamů s odpovídajícím skokem.",
|
||||
@@ -206,6 +209,9 @@
|
||||
"include_exclude": "Zahrnout/Vyloučit",
|
||||
"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",
|
||||
"ip_check": "Kontrola IP",
|
||||
"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.",
|
||||
"is_mx_based": "Na základě MX",
|
||||
"last_applied": "Naposledy použité",
|
||||
"license_info": "Licence není povinná, pomůžete však dalšímu vývoji.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Registrujte si své GUID</a>, nebo si <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">zaplaťte podporu pro svou instalaci mailcow.</a>",
|
||||
@@ -213,7 +219,7 @@
|
||||
"loading": "Prosím čekejte...",
|
||||
"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.",
|
||||
"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\")",
|
||||
"merged_vars_hint": "Šedé řádky byly přidány z <code>vars.(local.)inc.php</code> a zde je nelze upravit.",
|
||||
"message": "Zpráva",
|
||||
@@ -232,6 +238,7 @@
|
||||
"oauth2_renew_secret": "Vytvořit nový tajný klíč",
|
||||
"oauth2_revoke_tokens": "Odvolat všechny klientské tokeny",
|
||||
"optional": "volitelné",
|
||||
"options": "Možnosti",
|
||||
"password": "Heslo",
|
||||
"password_length": "Délka hesla",
|
||||
"password_policy": "Politika hesel",
|
||||
@@ -338,8 +345,8 @@
|
||||
"yes": "✓",
|
||||
"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)",
|
||||
"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>"
|
||||
"cors_settings": "Nastavení CORS",
|
||||
"queue_unban": "zrušit ban"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Přístup odepřen nebo jsou neplatná data ve formuláři",
|
||||
@@ -443,6 +450,9 @@
|
||||
"target_domain_invalid": "Cílová doména %s je neplatná",
|
||||
"targetd_not_found": "Cílová doména %s nenalezena",
|
||||
"targetd_relay_domain": "Cílová doména %s je předávaná",
|
||||
"template_exists": "Šablona %s již existuje",
|
||||
"template_id_invalid": "Šablona ID %s je neplatná",
|
||||
"template_name_invalid": "Název šablony je neplatný",
|
||||
"temp_error": "Dočasná chyba",
|
||||
"text_empty": "Text nesmí být prázdný",
|
||||
"tfa_token_invalid": "Neplatný TFA token",
|
||||
@@ -458,30 +468,39 @@
|
||||
"username_invalid": "Uživatelské jméno %s nelze použít",
|
||||
"validity_missing": "Zdejte dobu platnosti",
|
||||
"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ý"
|
||||
},
|
||||
"datatables": {
|
||||
"emptyTable": "Tabulka neobsahuje žádná data",
|
||||
"info": "Zobrazuji _START_ až _END_ z celkem _TOTAL_ záznamů",
|
||||
"infoEmpty": "Zobrazuji 0 až 0 z 0 záznamů",
|
||||
"infoFiltered": "(filtrováno z celkem _MAX_ záznamů)",
|
||||
"loadingRecords": "Načítám...",
|
||||
"zeroRecords": "Žádné záznamy nebyly nalezeny",
|
||||
"paginate": {
|
||||
"first": "První",
|
||||
"last": "Poslední",
|
||||
"next": "Další",
|
||||
"previous": "Předchozí"
|
||||
},
|
||||
"aria": {
|
||||
"sortAscending": ": aktivujte pro seřazení vzestupně",
|
||||
"sortDescending": ": aktivujte pro seřazení sestupně"
|
||||
},
|
||||
"lengthMenu": "Zobrazit _MENU_ výsledků",
|
||||
"processing": "Zpracovávání...",
|
||||
"search": "Vyhledávání:",
|
||||
"decimal": ",",
|
||||
"thousands": " "
|
||||
"emptyTable": "Tabulka neobsahuje žádná data",
|
||||
"info": "Zobrazuji _START_ až _END_ z celkem _TOTAL_ záznamů",
|
||||
"infoEmpty": "Zobrazuji 0 až 0 z 0 záznamů",
|
||||
"infoFiltered": "(filtrováno z celkem _MAX_ záznamů)",
|
||||
"loadingRecords": "Načítám...",
|
||||
"zeroRecords": "Žádné záznamy nebyly nalezeny",
|
||||
"paginate": {
|
||||
"first": "První",
|
||||
"last": "Poslední",
|
||||
"next": "Další",
|
||||
"previous": "Předchozí"
|
||||
},
|
||||
"aria": {
|
||||
"sortAscending": ": aktivujte pro seřazení vzestupně",
|
||||
"sortDescending": ": aktivujte pro seřazení sestupně"
|
||||
},
|
||||
"lengthMenu": "Zobrazit _MENU_ výsledků",
|
||||
"processing": "Zpracovávání...",
|
||||
"search": "Vyhledávání:",
|
||||
"decimal": ",",
|
||||
"thousands": " ",
|
||||
"collapse_all": "Sbalit vše",
|
||||
"expand_all": "Rozbalit vše"
|
||||
},
|
||||
"debug": {
|
||||
"chart_this_server": "Graf (tento server)",
|
||||
@@ -508,14 +527,27 @@
|
||||
"success": "Úspěch",
|
||||
"system_containers": "Systém a kontejnery",
|
||||
"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"
|
||||
},
|
||||
"diagnostics": {
|
||||
"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_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_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_status": "Současný stav",
|
||||
"dns_records_type": "Typ",
|
||||
@@ -633,7 +665,19 @@
|
||||
"title": "Úprava objektu",
|
||||
"unchanged_if_empty": "Pokud se nemění, ponechte prázdné",
|
||||
"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"
|
||||
},
|
||||
"domain_footer": "Patička pro celou doménu",
|
||||
"domain_footer_html": "HTML text",
|
||||
"domain_footer_plain": "Prostý text",
|
||||
"pushover_sound": "Zvukové upozornění"
|
||||
},
|
||||
"fido2": {
|
||||
"confirm": "Potvrdit",
|
||||
@@ -670,6 +714,7 @@
|
||||
"apps": "Aplikace",
|
||||
"debug": "Systémové informace",
|
||||
"email": "E-Mail",
|
||||
"mailcow_system": "Systém",
|
||||
"mailcow_config": "Nastavení",
|
||||
"quarantine": "Karanténa",
|
||||
"restart_netfilter": "Restartovat netfilter",
|
||||
@@ -705,6 +750,7 @@
|
||||
"add_mailbox": "Přidat mailovou schránku",
|
||||
"add_recipient_map_entry": "Přidat mapu příjemce",
|
||||
"add_resource": "Přidat zdroj",
|
||||
"add_template": "Přidat šablonu",
|
||||
"add_tls_policy_map": "Přidat mapu TLS pravidel",
|
||||
"address_rewriting": "Přepisování adres",
|
||||
"alias": "Alias",
|
||||
@@ -747,6 +793,7 @@
|
||||
"domain": "Doména",
|
||||
"domain_admins": "Správci domén",
|
||||
"domain_aliases": "Doménové aliasy",
|
||||
"domain_templates": "Šablony domén",
|
||||
"domain_quota": "Kvóta",
|
||||
"domain_quota_total": "Celková kvóta domény",
|
||||
"domains": "Domény",
|
||||
@@ -775,6 +822,7 @@
|
||||
"mailbox_defaults": "Výchozí nastavení",
|
||||
"mailbox_defaults_info": "Definuje výchozí nastavení pro nové schránky",
|
||||
"mailbox_defquota": "Výchozí velikost schránky",
|
||||
"mailbox_templates": "Šablony schránek",
|
||||
"mailbox_quota": "Max. velikost schránky",
|
||||
"mailboxes": "Mailové schránky",
|
||||
"max_aliases": "Max. počet aliasů",
|
||||
@@ -842,6 +890,8 @@
|
||||
"table_size_show_n": "Zobrazit %s položek",
|
||||
"target_address": "Cílová adresa",
|
||||
"target_domain": "Cílová doména",
|
||||
"templates": "Šablony",
|
||||
"template": "Šablona",
|
||||
"tls_enforce_in": "Vynutit TLS pro příchozí",
|
||||
"tls_enforce_out": "Vynutit TLS pro odchozí",
|
||||
"tls_map_dest": "Cíl",
|
||||
@@ -857,7 +907,8 @@
|
||||
"username": "Uživatelské jméno",
|
||||
"waiting": "Čekání",
|
||||
"weekly": "Každý týden",
|
||||
"yes": "✓"
|
||||
"yes": "✓",
|
||||
"relay_unknown": "Předávání neexistujících schránek"
|
||||
},
|
||||
"oauth2": {
|
||||
"access_denied": "K udělení přístupu se přihlašte jako vlastník mailové schránky.",
|
||||
@@ -922,7 +973,19 @@
|
||||
"type": "Typ"
|
||||
},
|
||||
"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í)"
|
||||
},
|
||||
"ratelimit": {
|
||||
"disabled": "Vypnuto",
|
||||
@@ -1006,6 +1069,9 @@
|
||||
"settings_map_added": "Přidána položka mapování nastavení",
|
||||
"settings_map_removed": "Položka mapování nastavení: %s smazána",
|
||||
"sogo_profile_reset": "SOGo profil uživatele %s vyresetován",
|
||||
"template_added": "Přidána šablona %s",
|
||||
"template_modified": "Změny šablony %s byly uloženy",
|
||||
"template_removed": "Šablona ID %s byla odstraněna",
|
||||
"tls_policy_map_entry_deleted": "Položka mapy TLS pravidel ID %s smazána",
|
||||
"tls_policy_map_entry_saved": "Položka mapy TLS pravidel \"%s\" uložena",
|
||||
"ui_texts": "Změny UI textů uloženy",
|
||||
@@ -1013,7 +1079,9 @@
|
||||
"verified_fido2_login": "Ověřené FIDO2 přihlášení",
|
||||
"verified_totp_login": "TOTP 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"
|
||||
},
|
||||
"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>",
|
||||
@@ -1199,7 +1267,8 @@
|
||||
"weeks": "týdny",
|
||||
"with_app_password": "s heslem aplikace",
|
||||
"year": "rok",
|
||||
"years": "let"
|
||||
"years": "let",
|
||||
"pushover_sound": "Zvukové upozornění"
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Nelze smazat právě přihlášeného uživatele",
|
||||
|
||||
@@ -459,7 +459,7 @@
|
||||
"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_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_name": "Navn",
|
||||
"dns_records_status": "Nuværende tilstand",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"domain": "Domain",
|
||||
"domain_matches_hostname": "Domain %s darf nicht dem Hostnamen entsprechen",
|
||||
"domain_quota_m": "Domain-Speicherplatz gesamt (MiB)",
|
||||
"dry": "Synchronisation simulieren",
|
||||
"enc_method": "Verschlüsselung",
|
||||
"exclude": "Elemente ausschließen (Regex)",
|
||||
"full_name": "Vor- und Nachname",
|
||||
@@ -147,6 +148,7 @@
|
||||
"change_logo": "Logo ändern",
|
||||
"configuration": "Konfiguration",
|
||||
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
|
||||
"copy_to_clipboard": "Text wurde in die Zwischenablage kopiert!",
|
||||
"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.",
|
||||
"customer_id": "Kunde",
|
||||
@@ -180,6 +182,8 @@
|
||||
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
|
||||
"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_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_ban_time": "Maximale Bannzeit in Sekunden",
|
||||
"f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
|
||||
@@ -345,7 +349,9 @@
|
||||
"oauth2_apps": "OAuth2 Apps",
|
||||
"queue_unban": "entsperren",
|
||||
"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": {
|
||||
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
|
||||
@@ -388,7 +394,9 @@
|
||||
"goto_invalid": "Ziel-Adresse %s ist ungültig",
|
||||
"ham_learn_error": "Ham Lernfehler: %s",
|
||||
"imagick_exception": "Fataler Bildverarbeitungsfehler",
|
||||
"img_dimensions_exceeded": "Grafik überschreitet die maximale Bildgröße",
|
||||
"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.",
|
||||
"invalid_bcc_map_type": "Ungültiger BCC-Map-Typ",
|
||||
"invalid_destination": "Ziel-Format \"%s\" ist ungültig",
|
||||
@@ -548,7 +556,7 @@
|
||||
"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_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_status": "Aktueller Status",
|
||||
"dns_records_type": "Typ",
|
||||
@@ -573,6 +581,7 @@
|
||||
"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.",
|
||||
"created_on": "Erstellt am",
|
||||
"custom_attributes": "benutzerdefinierte Attribute",
|
||||
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server",
|
||||
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind",
|
||||
"delete2duplicates": "Lösche Duplikate im Ziel",
|
||||
@@ -581,10 +590,19 @@
|
||||
"disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
|
||||
"domain": "Domain bearbeiten",
|
||||
"domain_admin": "Domain-Administrator bearbeiten",
|
||||
"domain_footer": "Domain wide footer",
|
||||
"domain_footer_html": "HTML footer",
|
||||
"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_plain": "PLAIN footer",
|
||||
"domain_footer": "Domänenweite Fußzeile",
|
||||
"domain_footer_html": "Fußzeile im HTML Format",
|
||||
"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_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)",
|
||||
"domains": "Domains",
|
||||
"dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen",
|
||||
@@ -595,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",
|
||||
"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.",
|
||||
"footer_exclude": "von Fußzeile ausschließen",
|
||||
"full_name": "Voller Name",
|
||||
"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>",
|
||||
@@ -857,7 +876,7 @@
|
||||
"sieve_preset_5": "Auto-Responder (Vacation, Urlaub)",
|
||||
"sieve_preset_6": "E-Mails mit Nachricht abweisen",
|
||||
"sieve_preset_7": "Weiterleiten und behalten oder verwerfen",
|
||||
"sieve_preset_8": "Nachricht verwerfen, wenn Absender und Alias-Ziel identisch sind.",
|
||||
"sieve_preset_8": "E-Mail eines bestimmten Absenders umleiten, als gelesen markieren und in Unterordner sortieren",
|
||||
"sieve_preset_header": "Beispielinhalte zur Einsicht stehen nachstehend bereit. Siehe auch <a href=\"https://de.wikipedia.org/wiki/Sieve\" target=\"_blank\">Wikipedia</a>.",
|
||||
"sogo_visible": "Alias Sichtbarkeit in SOGo",
|
||||
"sogo_visible_n": "Alias in SOGo verbergen",
|
||||
@@ -1026,6 +1045,7 @@
|
||||
"domain_removed": "Domain %s wurde entfernt",
|
||||
"dovecot_restart_success": "Dovecot wurde erfolgreich neu gestartet",
|
||||
"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",
|
||||
"forwarding_host_added": "Weiterleitungs-Host %s wurde hinzugefügt",
|
||||
"forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
|
||||
@@ -1075,6 +1095,7 @@
|
||||
"verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert"
|
||||
},
|
||||
"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.",
|
||||
"confirm": "Bestätigen",
|
||||
"confirm_totp_token": "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens",
|
||||
@@ -1124,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_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.",
|
||||
"attribute": "Attribut",
|
||||
"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.",
|
||||
"clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen",
|
||||
@@ -1243,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.",
|
||||
"user_settings": "Benutzereinstellungen",
|
||||
"username": "Benutzername",
|
||||
"value": "Wert",
|
||||
"verify": "Verifizieren",
|
||||
"waiting": "Warte auf Ausführung",
|
||||
"week": "Woche",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"domain": "Domain",
|
||||
"domain_matches_hostname": "Domain %s matches hostname",
|
||||
"domain_quota_m": "Total domain quota (MiB)",
|
||||
"dry": "Simulate synchronization",
|
||||
"enc_method": "Encryption method",
|
||||
"exclude": "Exclude objects (regex)",
|
||||
"full_name": "Full name",
|
||||
@@ -153,6 +154,7 @@
|
||||
"logo_dark_label": "Inverted for dark mode",
|
||||
"configuration": "Configuration",
|
||||
"convert_html_to_text": "Convert HTML to plain text",
|
||||
"copy_to_clipboard": "Text copied to clipboard!",
|
||||
"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.",
|
||||
"customer_id": "Customer ID",
|
||||
@@ -186,6 +188,8 @@
|
||||
"f2b_blacklist": "Blacklisted networks/hosts",
|
||||
"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_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_ban_time": "Max. ban time (s)",
|
||||
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
|
||||
@@ -390,7 +394,9 @@
|
||||
"goto_invalid": "Goto address %s is invalid",
|
||||
"ham_learn_error": "Ham learn error: %s",
|
||||
"imagick_exception": "Error: Imagick exception while reading image",
|
||||
"img_dimensions_exceeded": "Image exceeds the maximum image size",
|
||||
"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",
|
||||
"invalid_bcc_map_type": "Invalid BCC map type",
|
||||
"invalid_destination": "Destination format \"%s\" is invalid",
|
||||
@@ -550,7 +556,7 @@
|
||||
"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_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_status": "Current State",
|
||||
"dns_records_type": "Type",
|
||||
@@ -575,6 +581,7 @@
|
||||
"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",
|
||||
"created_on": "Created on",
|
||||
"custom_attributes": "Custom attributes",
|
||||
"delete1": "Delete from source when completed",
|
||||
"delete2": "Delete messages on destination that are not on source",
|
||||
"delete2duplicates": "Delete duplicates on destination",
|
||||
@@ -591,9 +598,11 @@
|
||||
"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_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_skip_replies": "Ignore footer on reply e-mails",
|
||||
"domain_quota": "Domain quota",
|
||||
"domains": "Domains",
|
||||
"dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)",
|
||||
@@ -604,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.",
|
||||
"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.",
|
||||
"footer_exclude": "Exclude from footer",
|
||||
"full_name": "Full name",
|
||||
"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>",
|
||||
@@ -873,7 +883,7 @@
|
||||
"sieve_preset_5": "Auto responder (vacation)",
|
||||
"sieve_preset_6": "Reject mail with reponse",
|
||||
"sieve_preset_7": "Redirect and keep/drop",
|
||||
"sieve_preset_8": "Discard message sent to an alias address the sender is part of",
|
||||
"sieve_preset_8": "Redirect e-mail from a specific sender, mark as read and sort into subfolder",
|
||||
"sieve_preset_header": "Please see the example presets below. For more details see <a href=\"https://en.wikipedia.org/wiki/Sieve_(mail_filtering_language)\" target=\"_blank\">Wikipedia</a>.",
|
||||
"sogo_visible": "Alias is visible in SOGo",
|
||||
"sogo_visible_n": "Hide alias in SOGo",
|
||||
@@ -1042,6 +1052,7 @@
|
||||
"domain_removed": "Domain %s has been removed",
|
||||
"dovecot_restart_success": "Dovecot was restarted successfully",
|
||||
"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",
|
||||
"forwarding_host_added": "Forwarding host %s has been added",
|
||||
"forwarding_host_removed": "Forwarding host %s has been removed",
|
||||
@@ -1091,6 +1102,7 @@
|
||||
"verified_yotp_login": "Verified Yubico OTP login"
|
||||
},
|
||||
"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>",
|
||||
"confirm": "Confirm",
|
||||
"confirm_totp_token": "Please confirm your changes by entering the generated token",
|
||||
@@ -1140,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_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.",
|
||||
"attribute": "Attribute",
|
||||
"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.",
|
||||
"clear_recent_successful_connections": "Clear seen successful connections",
|
||||
@@ -1270,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.",
|
||||
"user_settings": "User settings",
|
||||
"username": "Username",
|
||||
"value": "Value",
|
||||
"verify": "Verify",
|
||||
"waiting": "Waiting",
|
||||
"week": "week",
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
"app_passwds": "Gestionar las contraseñas de aplicaciones",
|
||||
"domain_desc": "Cambiar descripción del dominio",
|
||||
"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"
|
||||
},
|
||||
"add": {
|
||||
"activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.",
|
||||
|
||||
@@ -891,5 +891,17 @@
|
||||
"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_ua": "Lomakkeen tunnus sanoma ei kelpaa: käyttäjä agentin tarkistus virhe"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"delete2duplicates": "Supprimer les doubles à destination",
|
||||
"description": "Description",
|
||||
"destination": "Destination",
|
||||
"disable_login": "Désactiver l'authentification (les mails entrants resteront acceptés)",
|
||||
"disable_login": "Désactiver l'authentification (les e-mails entrants resteront acceptés)",
|
||||
"domain": "domaine",
|
||||
"domain_matches_hostname": "Le domaine %s correspond à la machine (hostname)",
|
||||
"domain_quota_m": "Quota total du domaine (Mo)",
|
||||
@@ -89,7 +89,7 @@
|
||||
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Info</div> Vous pouvez définir des cartes de transport vers une destination personnalisée pour ce domaine. sinon, une recherche MX sera effectuée.",
|
||||
"relay_unknown_only": "Relayer uniquement les boîtes inexistantes. Les boîtes existantes seront livrées localement.",
|
||||
"relayhost_wrapped_tls_info": "Veuillez <b>ne pas</b> utiliser des ports TLS wrappés (généralement utilisés sur le port 465).<br>\r\nUtilisez n'importe quel port non encapsulé et lancez STARTTLS. Une politique TLS pour appliquer TLS peut être créée dans \"Cartes de politique TLS\".",
|
||||
"select": "Veuillez sélectionner...",
|
||||
"select": "Veuillez sélectionner…",
|
||||
"select_domain": "Sélectionner d'abord un domaine",
|
||||
"sieve_desc": "Description courte",
|
||||
"sieve_type": "Type de filtre",
|
||||
@@ -107,7 +107,8 @@
|
||||
"validation_success": "Validation réussie",
|
||||
"bcc_dest_format": "La destination Cci doit être une seule adresse e-mail valide.<br>Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici.",
|
||||
"tags": "Etiquettes",
|
||||
"app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application"
|
||||
"app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application",
|
||||
"dry": "Simuler la synchronisation"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Accès",
|
||||
@@ -169,7 +170,7 @@
|
||||
"domain_s": "Domaine(s)",
|
||||
"duplicate": "Dupliquer",
|
||||
"duplicate_dkim": "Dupliquer l'enregistrement DKIM",
|
||||
"edit": "Editer",
|
||||
"edit": "Éditer",
|
||||
"empty": "Aucun résultat",
|
||||
"excludes": "Exclure ces destinataires",
|
||||
"f2b_ban_time": "Durée du bannissement (s)",
|
||||
@@ -207,9 +208,9 @@
|
||||
"last_applied": "Dernière application",
|
||||
"license_info": "Une licence n’est pas requise, mais contribue au développement.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Enregistrer votre GUID ici</a> or <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">acheter le support pour votre intallation Mailcow.</a>",
|
||||
"link": "Lien",
|
||||
"loading": "Veuillez patienter...",
|
||||
"loading": "Veuillez patienter…",
|
||||
"logo_info": "Votre image sera redimensionnée à une hauteur de 40 pixels pour la barre de navigation du haut et à un maximum de 250 pixels en largeur pour la page d'accueil. Un graphique extensible est fortement recommandé.",
|
||||
"lookup_mx": "Faire correspondre la destination à MX (.outlook.com pour acheminer tous les messages ciblés vers un MX * .outlook.com sur ce tronçon)",
|
||||
"lookup_mx": "Faire correspondre la destination à MX (<code>.*\\.google\\.com</code> pour acheminer tous les messages ciblés vers un MX se terminant par google.com sur ce tronçon)",
|
||||
"main_name": "\"mailcow UI\" nom",
|
||||
"merged_vars_hint": "Les lignes grisées ont été importées depuis <code>vars.(local.)inc.php</code> et ne peuvent pas être modifiées.",
|
||||
"message": "Message",
|
||||
@@ -326,7 +327,18 @@
|
||||
"password_policy_lowerupper": "Doit contenir des caractères minuscules et majuscules",
|
||||
"password_policy_numbers": "Doit contenir au moins un chiffre",
|
||||
"ip_check": "Vérification IP",
|
||||
"ip_check_disabled": "La vérification IP est désactivée. Vous pouvez l'activer sous<br> <strong>Système > Configuration > Options > Personnaliser</strong>"
|
||||
"ip_check_disabled": "La vérification IP est désactivée. Vous pouvez l'activer sous<br> <strong>Système > Configuration > Options > Personnaliser</strong>",
|
||||
"logo_normal_label": "Normal",
|
||||
"logo_dark_label": "Inversé pour le mode sombre",
|
||||
"allowed_methods": "Access-Control-Allow-Methods",
|
||||
"allowed_origins": "Access-Control-Allow-Origin",
|
||||
"copy_to_clipboard": "Texte copié dans le presse-papier !",
|
||||
"password_policy_special_chars": "Doit contenir des caractères spéciaux",
|
||||
"rsettings_preset_4": "Désactiver Rspamd pour un domaine",
|
||||
"oauth2_apps": "Applications OAuth2",
|
||||
"password_length": "Longueur des mots de passe",
|
||||
"password_policy_chars": "Doit contenir au moins une lettre",
|
||||
"password_policy_length": "La longueur minimale du mot de passe est de %d"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Accès refusé ou données de formulaire non valides",
|
||||
@@ -340,7 +352,7 @@
|
||||
"app_passwd_id_invalid": "Le mot de passe ID %s de l'application est non valide",
|
||||
"bcc_empty": "La destination BCC destination ne peut pas être vide",
|
||||
"bcc_exists": "Une carte de transport BCC %s existe pour le type %s",
|
||||
"bcc_must_be_email": "Le destination BCC %s n'est pas une adresse mail valide",
|
||||
"bcc_must_be_email": "Le destination BCC %s n'est pas une adresse e-mail valide",
|
||||
"comment_too_long": "Le commentaire est trop long, 160 caractère max sont permis",
|
||||
"defquota_empty": "Le quota par défaut par boîte ne doit pas être 0.",
|
||||
"description_invalid": "La description des ressources pour %s est non valide",
|
||||
@@ -450,7 +462,9 @@
|
||||
"demo_mode_enabled": "Le mode de démonstration est activé",
|
||||
"template_exists": "La template %s existe déja",
|
||||
"template_id_invalid": "Le numéro de template %s est invalide",
|
||||
"template_name_invalid": "Le nom de la template est invalide"
|
||||
"template_name_invalid": "Le nom de la template est invalide",
|
||||
"img_dimensions_exceeded": "L'image dépasse les dimensions maximales",
|
||||
"img_size_exceeded": "L'image dépasse la taille maximale de fichier"
|
||||
},
|
||||
"debug": {
|
||||
"chart_this_server": "Graphique (ce serveur)",
|
||||
@@ -479,7 +493,7 @@
|
||||
"cname_from_a": "Valeur dérivée de l’enregistrement A/AAAA. Ceci est supporté tant que l’enregistrement indique la bonne ressource.",
|
||||
"dns_records": "Enregistrements DNS",
|
||||
"dns_records_24hours": "Veuillez noter que les modifications apportées au DNS peuvent prendre jusqu’à 24 heures pour que leurs états actuels soient correctement reflétés sur cette page. Il est conçu comme un moyen pour vous de voir facilement comment configurer vos enregistrements DNS et de vérifier si tous vos enregistrements sont correctement stockés dans les DNS.",
|
||||
"dns_records_docs": "Veuillez également consulter <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">la documentation</a>.",
|
||||
"dns_records_docs": "Veuillez également consulter <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">la documentation</a>.",
|
||||
"dns_records_data": "Données correcte",
|
||||
"dns_records_name": "Nom",
|
||||
"dns_records_status": "Etat courant",
|
||||
@@ -489,7 +503,7 @@
|
||||
"edit": {
|
||||
"active": "Actif",
|
||||
"advanced_settings": "Réglages avancés",
|
||||
"alias": "Editer les alias",
|
||||
"alias": "Éditer les alias",
|
||||
"allow_from_smtp": "Restreindre l'utilisation de <b>SMTP</b> à ces adresses IP",
|
||||
"allow_from_smtp_info": "Laissez vide pour autoriser tous les expéditeurs.<br>Adresses IPv4/IPv6 et réseaux.",
|
||||
"allowed_protocols": "Protocoles autorisés",
|
||||
@@ -507,12 +521,12 @@
|
||||
"delete_ays": "Veuillez confirmer le processus de suppression.",
|
||||
"description": "Description",
|
||||
"disable_login": "Refuser l’ouverture de session (le courrier entrant est toujours accepté)",
|
||||
"domain": "Edition du domaine",
|
||||
"domain_admin": "Edition de l'administrateur du domaine",
|
||||
"domain": "Édition du domaine",
|
||||
"domain_admin": "Édition de l'administrateur du domaine",
|
||||
"domain_quota": "Quota du domaine",
|
||||
"domains": "Domaines",
|
||||
"dont_check_sender_acl": "Désactiver la vérification de l’expéditeur pour le domaine %s (+ alias de domaines)",
|
||||
"edit_alias_domain": "Edition des alias de domaine",
|
||||
"edit_alias_domain": "Édition des alias de domaine",
|
||||
"encryption": "Cryptage",
|
||||
"exclude": "Exclure des objets (regex)",
|
||||
"extended_sender_acl": "Adresses de l’expéditeur externe",
|
||||
@@ -528,7 +542,7 @@
|
||||
"inactive": "Inactif",
|
||||
"kind": "Type",
|
||||
"last_modified": "Dernière modification",
|
||||
"mailbox": "Edition de la boîte mail",
|
||||
"mailbox": "Édition de la boîte mail",
|
||||
"mailbox_quota_def": "Quota par défaut de la boîte",
|
||||
"max_aliases": "Nombre max. d'alias",
|
||||
"max_mailboxes": "Nombre max. de boîtes possibles",
|
||||
@@ -584,7 +598,7 @@
|
||||
"target_domain": "Domaine cible",
|
||||
"timeout1": "Délai de connexion à l’hôte distant",
|
||||
"timeout2": "Délai de connexion à l’hôte local",
|
||||
"title": "Editer l'objet",
|
||||
"title": "Éditer l'objet",
|
||||
"unchanged_if_empty": "Si non modifié, laisser en blanc",
|
||||
"username": "Nom d'utilisateur",
|
||||
"validate_save": "Valider et sauver",
|
||||
@@ -598,9 +612,9 @@
|
||||
"delete_these_items": "Veuillez confirmer les modifications apportées à l’identifiant d’objet suivant",
|
||||
"hibp_nok": "Trouvé ! Il s’agit d’un mot de passe potentiellement dangereux !",
|
||||
"hibp_ok": "Aucune correspondance trouvée.",
|
||||
"loading": "Veuillez patienter...",
|
||||
"loading": "Veuillez patienter…",
|
||||
"restart_container": "Redémarrer le conteneur",
|
||||
"restart_container_info": "<b>Important:</b> Un redémarrage en douceur peut prendre un certain temps, veuillez attendre qu’il soit terminé..",
|
||||
"restart_container_info": "<b>Important:</b> Un redémarrage en douceur peut prendre un certain temps, veuillez attendre qu’il soit terminé.",
|
||||
"restart_now": "Redémarrer maintenant",
|
||||
"restarting_container": "Redémarrage du conteneur, cela peut prendre un certain temps"
|
||||
},
|
||||
@@ -685,7 +699,7 @@
|
||||
"domain_quota": "Quota",
|
||||
"domain_quota_total": "Quota total du domaine",
|
||||
"domains": "Domaines",
|
||||
"edit": "Editer",
|
||||
"edit": "Éditer",
|
||||
"empty": "Pas de résulats",
|
||||
"enable_x": "Activer",
|
||||
"excludes": "Exclut",
|
||||
@@ -935,7 +949,7 @@
|
||||
"disable_tfa": "Désactiver TFA jusqu’à la prochaine ouverture de session réussie",
|
||||
"enter_qr_code": "Votre code TOTP si votre appareil ne peut pas scanner les codes QR",
|
||||
"error_code": "Code d'erreur",
|
||||
"init_webauthn": "Initialisation, veuillez patienter...",
|
||||
"init_webauthn": "Initialisation, veuillez patienter…",
|
||||
"key_id": "Un identifiant pour votre Périphérique",
|
||||
"key_id_totp": "Un identifiant pour votre clé",
|
||||
"none": "Désactiver",
|
||||
@@ -948,8 +962,8 @@
|
||||
"tfa_token_invalid": "Token TFA invalide",
|
||||
"totp": "OTP (One Time Password = Mot de passe à usage unique : Google Authenticator, Authy, etc.)",
|
||||
"webauthn": "Authentification WebAuthn",
|
||||
"waiting_usb_auth": "<i>En attente d’un périphérique USB...</i><br><br>S’il vous plaît appuyez maintenant sur le bouton de votre périphérique USB WebAuthn.",
|
||||
"waiting_usb_register": "<i>En attente d’un périphérique USB...</i><br><br>Veuillez entrer votre mot de passe ci-dessus et confirmer votre inscription WebAuthn en appuyant sur le bouton de votre périphérique USB WebAuthn.",
|
||||
"waiting_usb_auth": "<i>En attente d’un périphérique USB…</i><br><br>S’il vous plaît appuyez maintenant sur le bouton de votre périphérique USB WebAuthn.",
|
||||
"waiting_usb_register": "<i>En attente d’un périphérique USB…</i><br><br>Veuillez entrer votre mot de passe ci-dessus et confirmer votre inscription WebAuthn en appuyant sur le bouton de votre périphérique USB WebAuthn.",
|
||||
"yubi_otp": "Authentification OTP Yubico"
|
||||
},
|
||||
"fido2": {
|
||||
@@ -999,9 +1013,9 @@
|
||||
"eas_reset": "Réinitialiser le cache de l’appareil Activesync",
|
||||
"eas_reset_help": "Dans de nombreux cas, une réinitialisation du cache de l’appareil aidera à récupérer un profil Activesync cassé.<br><b>Attention :</b> Tous les éléments seront à nouveau téléchargés !",
|
||||
"eas_reset_now": "Réinitialiser maintenant",
|
||||
"edit": "Editer",
|
||||
"email": "Email",
|
||||
"email_and_dav": "Email, calendriers et contacts",
|
||||
"edit": "Éditer",
|
||||
"email": "E-mail",
|
||||
"email_and_dav": "E-mail, calendriers et contacts",
|
||||
"encryption": "Cryptage",
|
||||
"excludes": "Exclut",
|
||||
"expire_in": "Expire dans",
|
||||
@@ -1015,7 +1029,7 @@
|
||||
"is_catch_all": "Attrape-tout pour le domaine(s)",
|
||||
"last_mail_login": "Dernière connexion mail",
|
||||
"last_run": "Dernière exécution",
|
||||
"loading": "Chargement...",
|
||||
"loading": "Chargement…",
|
||||
"mailbox_details": "Détails de la boîte",
|
||||
"messages": "messages",
|
||||
"never": "jamais",
|
||||
@@ -1051,7 +1065,7 @@
|
||||
"shared_aliases": "Adresses alias partagées",
|
||||
"shared_aliases_desc": "Les alias partagés ne sont pas affectés par les paramètres spécifiques à l’utilisateur tels que le filtre anti-spam ou la politique de chiffrement. Les filtres anti-spam correspondants ne peuvent être effectués que par un administrateur en tant que politique de domaine.",
|
||||
"show_sieve_filters": "Afficher le filtre de tamis actif de l’utilisateur",
|
||||
"sogo_profile_reset": "Remise é zéro du profil SOGo",
|
||||
"sogo_profile_reset": "Remise à zéro du profil SOGo",
|
||||
"sogo_profile_reset_help": "Ceci détruira un profil Sogo des utilisateurs et <b>supprimera toutes les données de contact et de calendrier irrécupérables</b>.",
|
||||
"sogo_profile_reset_now": "Remise à zéro du profil maintenant",
|
||||
"spam_aliases": "Alias de courriel temporaire",
|
||||
@@ -1077,7 +1091,7 @@
|
||||
"sync_jobs": "Jobs de synchronisation",
|
||||
"tag_handling": "Régler la manipulation du courrier étiqueté",
|
||||
"tag_help_example": "Exemple pour une adresse e-mail étiquetée : me<b>+Facebook</b>@example.org",
|
||||
"tag_help_explain": "Dans un sous-dossier : un nouveau sous-dossier nommé selon l'étiquette sera créé sous INBOX (\"INBOX/Facebook\").<br>\nDans le sujet : le nom des balises sera ajouté au début du sujet du mail, exemple : \"[Facebook] My News\".",
|
||||
"tag_help_explain": "Dans un sous-dossier : un nouveau sous-dossier nommé selon l'étiquette sera créé sous INBOX (\"INBOX/Facebook\").<br>\nDans le sujet : le nom des balises sera ajouté au début du sujet de l'e-mail, exemple : \"[Facebook] My News\".",
|
||||
"tag_in_none": "Ne rien faire",
|
||||
"tag_in_subfolder": "Dans un sous dossier",
|
||||
"tag_in_subject": "Dans le sujet",
|
||||
|
||||
@@ -394,7 +394,12 @@
|
||||
"filters": "Szűrők",
|
||||
"login_as": "Bejelentkezés mint",
|
||||
"quarantine": "Karantén műveletek",
|
||||
"bcc_maps": "BCC címek"
|
||||
"bcc_maps": "BCC címek",
|
||||
"domain_relayhost": "Relayhost megváltoztatása egy domainhez",
|
||||
"protocol_access": "Protokoll-hozzáférés módosítása",
|
||||
"quarantine_attachments": "Karantén mellékletek",
|
||||
"quarantine_category": "Karantén értesítési kategória módosítása",
|
||||
"quarantine_notification": "Karantén értesítések módosítása"
|
||||
},
|
||||
"diagnostics": {
|
||||
"dns_records": "DNS bejegyzések"
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
"validation_success": "Convalidato con successo",
|
||||
"bcc_dest_format": "Il destinatario in copia nascosta deve essere un singolo indirizzo email.<br>Se si vuole spedire una copia del messaggio a più destinatari, bisogna creare un alias ed utilizzarlo per questa opzione.",
|
||||
"app_passwd_protocols": "Protocolli consentiti per la password dell'app",
|
||||
"tags": "Tag"
|
||||
"tags": "Tag",
|
||||
"dry": "Simula sincronizzazione"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Accedi",
|
||||
@@ -339,7 +340,8 @@
|
||||
"oauth2_add_client": "Aggiungere il client OAuth2",
|
||||
"rsettings_preset_4": "Disattivare Rspamd per un dominio",
|
||||
"options": "Opzioni",
|
||||
"cors_settings": "Impostazioni CORS"
|
||||
"cors_settings": "Impostazioni CORS",
|
||||
"copy_to_clipboard": "Testo copiato negli appunti!"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Accesso negato o form di login non corretto",
|
||||
@@ -462,7 +464,9 @@
|
||||
"demo_mode_enabled": "La modalità demo è abilitata",
|
||||
"template_name_invalid": "Nome template non valido",
|
||||
"template_exists": "Il template %s esiste già",
|
||||
"template_id_invalid": "Il template con ID %s non è valido"
|
||||
"template_id_invalid": "Il template con ID %s non è valido",
|
||||
"img_dimensions_exceeded": "L'immagine supera la dimensione massima consentita",
|
||||
"img_size_exceeded": "L'immagine supera la dimensione massima del file"
|
||||
},
|
||||
"debug": {
|
||||
"chart_this_server": "Grafico (questo server)",
|
||||
@@ -506,7 +510,7 @@
|
||||
"dns_records": "Record DNS",
|
||||
"dns_records_24hours": "Tieni presente che le modifiche apportate ai record DNS potrebbero richiedere fino a 24 ore per poter essere visualizzate correttamente in questa pagina. Tutto ciò è da intendersi come un modo per voi di vedere come configurare i record DNS e per controllare se tutti i record DNS sono stati inseriti correttamente.",
|
||||
"dns_records_data": "Dati corretti",
|
||||
"dns_records_docs": "Si prega di consultare anche <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">la documentazione</a>.",
|
||||
"dns_records_docs": "Si prega di consultare anche <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">la documentazione</a>.",
|
||||
"dns_records_name": "Nome",
|
||||
"dns_records_status": "Stato attuale",
|
||||
"dns_records_type": "Tipo",
|
||||
@@ -626,7 +630,9 @@
|
||||
"acl": "ACL (autorizzazione)",
|
||||
"app_passwd_protocols": "Protocolli consentiti per la password dell'app",
|
||||
"last_modified": "Ultima modifica",
|
||||
"pushover_sound": "Suono"
|
||||
"pushover_sound": "Suono",
|
||||
"custom_attributes": "Attributi personalizzati",
|
||||
"domain_footer_skip_replies": "Ignora il piè di pagina nelle e-mail di risposta"
|
||||
},
|
||||
"fido2": {
|
||||
"confirm": "Conferma",
|
||||
@@ -1021,7 +1027,8 @@
|
||||
"domain_add_dkim_available": "Esisteva già una chiave DKIM",
|
||||
"template_added": "Aggiunto template %s",
|
||||
"template_modified": "Le modifiche al template %s sono state salvate",
|
||||
"template_removed": "Il template con ID %s è stato cancellato"
|
||||
"template_removed": "Il template con ID %s è stato cancellato",
|
||||
"f2b_banlist_refreshed": "L'ID della lista blocchi è stato aggiornato con successo."
|
||||
},
|
||||
"tfa": {
|
||||
"api_register": "%s usa le API Yubico Cloud. Richiedi una chiave API <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">qui</a>",
|
||||
@@ -1048,7 +1055,8 @@
|
||||
"yubi_otp": "Autenticazione Yubico OTP",
|
||||
"tfa_token_invalid": "Token TFA non valido",
|
||||
"u2f_deprecated": "Sembra che la tua chiave sia stata registrata utilizzando il metodo U2F deprecato. Disattiveremo Two-Factor-Authenticaiton per te e cancelleremo la tua chiave.",
|
||||
"u2f_deprecated_important": "Registra la tua chiave nel pannello di amministrazione con il nuovo metodo WebAuthn."
|
||||
"u2f_deprecated_important": "Registra la tua chiave nel pannello di amministrazione con il nuovo metodo WebAuthn.",
|
||||
"authenticators": "Autenticatori"
|
||||
},
|
||||
"user": {
|
||||
"action": "Azione",
|
||||
@@ -1210,7 +1218,9 @@
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome utente o password errati",
|
||||
"with_app_password": "con password dell'app",
|
||||
"direct_protocol_access": "Questo utente della mailbox ha <b>accesso diretto ed esterno</b> ai seguenti protocolli e applicazioni. Questa impostazione è controllata dal tuo amministratore. Le password delle applicazioni possono essere create per garantire l'accesso ai singoli protocolli e applicazioni.<br>Il pulsante \"Accedi alla webmail\" fornisce un singolo accesso a SOGo ed è sempre disponibile.",
|
||||
"pushover_sound": "Suono"
|
||||
"pushover_sound": "Suono",
|
||||
"attribute": "Attributo",
|
||||
"value": "Valore"
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Cannot delete logged in user",
|
||||
|
||||
354
data/web/lang/lang.lt-lt.json
Normal file
354
data/web/lang/lang.lt-lt.json
Normal file
@@ -0,0 +1,354 @@
|
||||
{
|
||||
"acl": {
|
||||
"app_passwds": "Tvarkyti programėlių slaptažodžius",
|
||||
"bcc_maps": "BCC žemėlapiai",
|
||||
"domain_desc": "Keisti domeno aprašymą",
|
||||
"eas_reset": "Išvalyti EAS įrenginius",
|
||||
"filters": "Filtrai",
|
||||
"login_as": "Prisijungti kaip elektroninio pašto naudotojas",
|
||||
"protocol_access": "Keisti protokolų prieigą",
|
||||
"quarantine": "Karantino valdymas",
|
||||
"quarantine_category": "Keisti karantino pranešimo katrgoriją",
|
||||
"quarantine_notification": "Keisti karantino pranešimus",
|
||||
"ratelimit": "Prieigos limitas",
|
||||
"recipient_maps": "Gavėjų sąsajos",
|
||||
"smtp_ip_access": "Pakeisti prieinamuosius SMTP serverius",
|
||||
"sogo_access": "Leisti SOGo prieigos valdymą",
|
||||
"spam_policy": "Juodasis/Baltasis sąrašas",
|
||||
"spam_score": "Šlamsto balas",
|
||||
"tls_policy": "TLS politika",
|
||||
"unlimited_quota": "Neribota elektroninio pašto dėžudžių kvota",
|
||||
"quarantine_attachments": "Karantino priedai",
|
||||
"sogo_profile_reset": "Nustatyti SOGo profilį iš naujo",
|
||||
"alias_domains": "Pridėti pakaitinius domenus",
|
||||
"delimiter_action": "Delimitatoriaus veiksmas",
|
||||
"domain_relayhost": "Pakeisti peradresavimo serverį domenui",
|
||||
"extend_sender_acl": "Leisti išplėsti siuntėjo ACL išoriniais adresais",
|
||||
"mailbox_relayhost": "Pakeisti siuntimo serverį pašto dėžutei",
|
||||
"prohibited": "Draudžiama pagal ACL",
|
||||
"spam_alias": "Laikini slapyvardžiai",
|
||||
"syncjobs": "Sinchronizuoti darbus"
|
||||
},
|
||||
"add": {
|
||||
"active": "Aktyvus",
|
||||
"add": "Pridėti",
|
||||
"add_domain_only": "Pridėti tik domeną",
|
||||
"add_domain_restart": "Pridėti domeną ir paleisti SOGo iš naujo",
|
||||
"alias_domain_info": "<small>Tik tinkami domenų vardai (išskirta kableliais).</small>",
|
||||
"app_name": "Programėlės pavadinimas",
|
||||
"app_password": "Pridėti programėlės slaptažodį",
|
||||
"app_passwd_protocols": "Prieinamieji programėlės slaptažodžio protokolai",
|
||||
"custom_params": "Pasirinktiniai parametrai",
|
||||
"delete1": "Ištrinti iš šaltinio, kai baigta",
|
||||
"activate_filter_warn": "Visi kiti filtrai bus deaktivuoti, kai „aktyvus“ yra pražymėtas.",
|
||||
"custom_params_hint": "Teisingai: --param=xy, neteisingai: --param xy",
|
||||
"description": "Aprašymas",
|
||||
"destination": "Gavėjas",
|
||||
"domain": "Domenas",
|
||||
"domain_quota_m": "Viso domeno kvota (MiB)",
|
||||
"dry": "Simuliuoti sinchronizaciją",
|
||||
"enc_method": "Šifravimo metodas",
|
||||
"full_name": "Prilnas vardas",
|
||||
"generate": "generuoti",
|
||||
"goto_null": "Tyliai atmesti žinutes",
|
||||
"goto_spam": "Išmokti kaip <span class=\"text-danger\"><b>šlamštas</b></span>",
|
||||
"password": "Slaptažodis",
|
||||
"select": "Pasirinkite...",
|
||||
"select_domain": "Prima, pasirinkite domeną",
|
||||
"sieve_desc": "Trumpas aprašymas",
|
||||
"sieve_type": "Filtro tipas",
|
||||
"mailbox_quota_def": "Numatytoji elelktroninio pašto dėžutės kvota",
|
||||
"password_repeat": "Slaptažodžio patvirtininas (pakartoti)",
|
||||
"disable_login": "Uždrausti prisijungimą (ateinančios žinutės vistiek bus gaunamos)",
|
||||
"inactive": "Neaktyvus",
|
||||
"alias_address": "Pakaitiniai/is adresai/as",
|
||||
"alias_address_info": "<small>Pilnas el. pašto adresas (-ai) arba @example.com, kad gautumėte visus pranešimus iš domeno (atskirtus kableliais). <b>Tik \"mailcow\" domenai</b>.</small>",
|
||||
"alias_domain": "Pakaitinis domenas",
|
||||
"automap": "Stenktis automatiškai susieti aplankus (\"Išsiųsti\", \"Išsiųsti\" => \"išsiųsti\" ir tt.)",
|
||||
"backup_mx_options": "Perdavimo nustatymai",
|
||||
"bcc_dest_format": "BCC gavimo adresas turi būti vienas galiojantis el. pašto adresas.<br>Jei turite siųsti kopiją į kelis adresus, sukurkite slapyvardį ir naudokite jį čia.",
|
||||
"delete2": "Ištrinti žinutes gavimo vietoje, kurios nėra šaltinyje.",
|
||||
"delete2duplicates": "Ištrinti dublikatus gavimo vietoje",
|
||||
"domain_matches_hostname": "Domenas %s atitinka serverio vardą",
|
||||
"exclude": "Išskirti objektus (regex)",
|
||||
"gal": "Visuotinis adresų sąrašas",
|
||||
"gal_info": "GAL (Global Address List) apima visus domeno objektus ir jų negali redaguoti joks vartotojas. SOGo trūksta laisvosios/užimtosios informacijos, jei tai išjungta! <b>Perkraukite SOGo, kad pritaikytumėte pakeitimus.</b>"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Prieiga",
|
||||
"active": "Aktyvus",
|
||||
"add": "Pridėti",
|
||||
"add_admin": "Pridėti administratorių",
|
||||
"add_domain_admin": "Pridėti domeno administratorių",
|
||||
"add_settings_rule": "Pridėti nustatymų taisyklę",
|
||||
"admin": "Administratorius",
|
||||
"admins": "Administratoriai",
|
||||
"admins_ldap": "LDAP administratoriai",
|
||||
"api_key": "API prieigos raktas",
|
||||
"copy_to_clipboard": "Tekstas nukopijuotas!",
|
||||
"cors_settings": "CORS nustatymai",
|
||||
"dkim_from": "Iš",
|
||||
"dkim_key_length": "DKIM rakto ilgis (bitai)",
|
||||
"dkim_key_valid": "Raktas tinkamas",
|
||||
"dkim_keys": "ARC/DKIM raktai",
|
||||
"dkim_private_key": "Slaptasis raktas",
|
||||
"dkim_to": "Į",
|
||||
"domain": "Domenas",
|
||||
"domain_admin": "Domeno administratorius",
|
||||
"domain_admins": "Doneno administratoriai",
|
||||
"domain_s": "Domenas/ai",
|
||||
"duplicate": "Dublikatas",
|
||||
"duplicate_dkim": "Dublikuotas DKIM įrašas",
|
||||
"edit": "Redaguoti",
|
||||
"empty": "Jokių rezultatų",
|
||||
"f2b_filter": "Regex filtrai",
|
||||
"filter_table": "Filtrų lentelė",
|
||||
"from": "Nuo",
|
||||
"generate": "generuoti",
|
||||
"html": "HTML",
|
||||
"import": "Importuoti",
|
||||
"inactive": "Neaktyvus",
|
||||
"message": "Žinutė",
|
||||
"message_size": "Žinutės dydis",
|
||||
"nexthop": "Kitas šuolis",
|
||||
"no": "✕",
|
||||
"no_record": "Nėra įrašo",
|
||||
"oauth2_apps": "OAuth2 Programėlės",
|
||||
"oauth2_add_client": "Pridėti OAuth2 klientą",
|
||||
"oauth2_client_id": "Kliento ID",
|
||||
"optional": "pasirinktinas",
|
||||
"options": "Nustatymai",
|
||||
"password": "Slaptažodis",
|
||||
"password_length": "Slaptažodžio ilgis",
|
||||
"password_policy_chars": "Privalo turėti bent vieną numerį ar skaičių",
|
||||
"password_policy_length": "Mažiausias slaptažodžio ilgis - %d",
|
||||
"password_policy_numbers": "Privalo turėti bent vieną skaičių",
|
||||
"password_policy_special_chars": "Privalo turėti specialiųjų ženklų",
|
||||
"password_repeat": "Slaptažodžio patvirtinimas (pakartoti)",
|
||||
"quarantine": "Karantinas",
|
||||
"quarantine_notification_sender": "Pranešimų e-pašto siuntėjas",
|
||||
"quarantine_release_format_att": "Kaip priedą",
|
||||
"quarantine_release_format_raw": "Nepakeitas originalas",
|
||||
"quota_notifications": "Kvotos pranešimai",
|
||||
"r_active": "Aktyvūs apribojimai",
|
||||
"r_inactive": "Neaktyvus apribojimai",
|
||||
"refresh": "Perkrauti",
|
||||
"regen_api_key": "Regeneruoti API raktą",
|
||||
"relay_rcpt": "„Kam:“ adresas",
|
||||
"relay_run": "Paleisti testą",
|
||||
"remove": "Ištrinti",
|
||||
"rsetting_add_rule": "Pridėti taisyklę",
|
||||
"rsetting_desc": "Trumpas aprašymas",
|
||||
"rspamd_global_filters_agree": "Aš busiu atsargus/i!",
|
||||
"save": "Išsaugoti pakeitimus",
|
||||
"search_domain_da": "Ieškoti domenus",
|
||||
"send": "Siųsti",
|
||||
"sender": "Siuntėjas",
|
||||
"source": "Šaltinis",
|
||||
"spamfilter": "Šlamšto filtras",
|
||||
"text": "Tekstas",
|
||||
"time": "Laikas",
|
||||
"to_top": "Atgal į viršų",
|
||||
"ui_header_announcement_content": "Tekstas (HTML leistina)",
|
||||
"ui_header_announcement_select": "Pasirinkite pranešimo tipą",
|
||||
"ui_header_announcement_type": "Tipas",
|
||||
"ui_header_announcement_type_danger": "Labai svarbus",
|
||||
"ui_header_announcement_type_info": "Info",
|
||||
"upload": "Įkelti",
|
||||
"username": "Naudotojo vardas",
|
||||
"verify": "Patikrinti",
|
||||
"yes": "✓",
|
||||
"customer_id": "Kliento ID",
|
||||
"f2b_parameters": "„Fail2Ban“ parametrai",
|
||||
"import_private_key": "Importuoti slaptąjį raktą",
|
||||
"action": "Veiksmas",
|
||||
"convert_html_to_text": "Konvertuoti HTML į paprastą tekstą",
|
||||
"oauth2_renew_secret": "Geberuoti naują kliento slapuką",
|
||||
"private_key": "Slaptasis raktas",
|
||||
"sal_level": "Moo lygis",
|
||||
"ui_header_announcement_type_warning": "Svarbus",
|
||||
"admin_details": "Redaguoti administratoriaus duomenis",
|
||||
"login_time": "Prisijungino laikas",
|
||||
"ui_header_announcement": "Pranešimai",
|
||||
"dkim_overwrite_key": "Perrašyti egzistuojantį DKIK raktą",
|
||||
"f2b_whitelist": "Tinklai baltajama sąraše",
|
||||
"f2b_blacklist": "Tinklai juodajame saraše",
|
||||
"loading": "Prašau palaukite...",
|
||||
"password_policy_lowerupper": "Privalo turėti mažuosius ir didžiuosius ženklus/raides",
|
||||
"relay_from": "„Nuo:“ adresas"
|
||||
},
|
||||
"danger": {
|
||||
"demo_mode_enabled": "Demo Režimas įjungtas",
|
||||
"description_invalid": "Resurso %s aprašymas yra netinkamas",
|
||||
"domain_exists": "Domenas %s jau egzistuoja",
|
||||
"domain_invalid": "Domeno vardas tuščias arba netinkamas",
|
||||
"domain_not_found": "Domenas %s nerastas",
|
||||
"domain_quota_m_in_use": "Domeno kvota turi būti daygiau arba lygiai %s MiB",
|
||||
"file_open_error": "Failas negali būti atidarytas įrašymui",
|
||||
"filter_type": "Netinkamas filtro tipas",
|
||||
"invalid_mime_type": "Netinkamas MIME tipas",
|
||||
"login_failed": "Prisijungimas nepavyko",
|
||||
"mailbox_defquota_exceeds_mailbox_maxquota": "Numatytoji kvota didesnė nei maksimalus kvotos limitas",
|
||||
"mysql_error": "MySQL problema: %s",
|
||||
"img_invalid": "Negalimas nuotraukos patikrinimas",
|
||||
"mailbox_quota_left_exceeded": "Per mažai vietos diske (liko %d MiB)",
|
||||
"comment_too_long": "Komentaeas per ilgas, maksimalus leistinas simbolių skaičius yra 160",
|
||||
"nginx_reload_failed": "Nginx perkrovimas nepavyko: %s",
|
||||
"invalid_filter_type": "Netinkamas filtro tipas"
|
||||
},
|
||||
"edit": {
|
||||
"validate_save": "Patikrinti ir išsaugoti",
|
||||
"unchanged_if_empty": "Jei nepakeista, palikite tuščią",
|
||||
"username": "Naudotojo vardas"
|
||||
},
|
||||
"fido2": {
|
||||
"confirm": "Patvirtinti",
|
||||
"fido2_success": "Įrenginys sėkmingai užregistruotas",
|
||||
"fido2_validation_failed": "Patikrinimas nepavyko",
|
||||
"fn": "Draugiškasis vardas",
|
||||
"known_ids": "Žinomi ID",
|
||||
"none": "Neįgalintas",
|
||||
"register_status": "Registracijos statusas",
|
||||
"rename": "Pervadyti",
|
||||
"set_fido2_touchid": "Registruokite Touch ID su Apple M1",
|
||||
"set_fn": "Nustatyti draugiškąjį vardą",
|
||||
"start_fido2_validation": "Pradėti FIDO2 patikrinimą",
|
||||
"fido2_auth": "Prisijungti su FIDO2",
|
||||
"set_fido2": "Registruoti FIDO2 įrengenį"
|
||||
},
|
||||
"footer": {
|
||||
"cancel": "Atšaukti",
|
||||
"confirm_delete": "Patvirtinti trynimą",
|
||||
"delete_now": "Ištrinti dabar",
|
||||
"hibp_check": "Patikrinti naudodami haveibeenpwned.com",
|
||||
"hibp_nok": "Atitikta! Tai yra galimai pavojingas slaptažodis!",
|
||||
"hibp_ok": "Nerasta jokių atitikmenų.",
|
||||
"loading": "Prašome palaukti...",
|
||||
"nothing_selected": "Niekas nepasirinkta",
|
||||
"restart_container": "Paleiskite konteinerį iš naujo",
|
||||
"restart_now": "Perkrauti dabar",
|
||||
"restarting_container": "Perkraunamas konteineris, tai gali užtrukti.",
|
||||
"delete_these_items": "Prašome patvirtinti savo pakeitimus šiam objekto ID",
|
||||
"restart_container_info": "<b>Svarbu:</b> Sklandus paleidimas iš naujo gali užtrukti, prašome palaukti, kol jis baigsis."
|
||||
},
|
||||
"header": {
|
||||
"administration": "Konfigūracija ir detalės",
|
||||
"apps": "Programėlės",
|
||||
"debug": "Informacija",
|
||||
"email": "El. paštas",
|
||||
"mailcow_system": "Sistema",
|
||||
"mailcow_config": "Konfiguracija",
|
||||
"quarantine": "Karantinas",
|
||||
"restart_netfilter": "Paleisti „netfilter“ iš naujo",
|
||||
"restart_sogo": "Paleisti SOGo iš naujo",
|
||||
"user_settings": "Naudotojo Nustatymai"
|
||||
},
|
||||
"info": {
|
||||
"no_action": "Nėra taikomų veiksmų",
|
||||
"session_expires": "Jūsų sesija pasibaigs už maždaug 15 sekundžių.",
|
||||
"awaiting_tfa_confirmation": "Laukiama DFA patvirtinimo"
|
||||
},
|
||||
"login": {
|
||||
"fido2_webauthn": "FIDO2/WebAuthn Prisijungimas",
|
||||
"login": "Prisijungti",
|
||||
"other_logins": "Prisijungimas raktu",
|
||||
"password": "Slaptažodis",
|
||||
"username": "Naudotojo vardas",
|
||||
"mobileconfig_info": "Prašome prisijungti kaip pašto dėžutės vartotojui, kad galėtumėte atsisiųsti pageidaujamą „Apple“ ryšio profilį."
|
||||
},
|
||||
"mailbox": {
|
||||
"action": "Veiksmas",
|
||||
"activate": "Aktivuoti",
|
||||
"active": "Aktyvus",
|
||||
"add": "Pridėti",
|
||||
"add_alias": "Pridėti pseudonimą",
|
||||
"add_alias_expand": "Išplėsti pseudonimą per pseudonimų domenus",
|
||||
"add_bcc_entry": "Pridėti BCC žemėlapį",
|
||||
"add_domain": "Pridėti domeną",
|
||||
"add_domain_alias": "Pridėti pakaitinį domeną",
|
||||
"add_domain_record_first": "Prašome pridėti domeną pirmiausia",
|
||||
"add_filter": "Pridėti filtrą",
|
||||
"add_mailbox": "Pridėti pašto dėžutę",
|
||||
"add_recipient_map_entry": "Pridėti gavėjų žemėlapį",
|
||||
"add_resource": "Pridėti resursą",
|
||||
"add_template": "Pridėti Šabloną",
|
||||
"add_tls_policy_map": "Pridėti TLS politikos schemą",
|
||||
"address_rewriting": "Adresų perrašymas",
|
||||
"alias": "Slapyvardis",
|
||||
"alias_domain_backupmx": "Pakaitinis domenas nėra aktyvuotas peradresavimo domenui",
|
||||
"all_domains": "Visi Domenai",
|
||||
"allow_from_smtp_info": "Palikite tuščią, norėdami leisti visiems siuntėjams.<br>IPv4/IPv6 adresai ir tinklai.",
|
||||
"backup_mx": "Peradresavimo domenas",
|
||||
"bcc": "BCC",
|
||||
"bcc_destination": "BCC gavimo vieta",
|
||||
"bcc_destinations": "BCC gavimo vieta",
|
||||
"bcc_local_dest": "Vietinė gavimo vieta",
|
||||
"bcc_map": "BCC žemėlapis",
|
||||
"bcc_map_type": "BCC tipas",
|
||||
"bcc_maps": "BCC žemėlapiai",
|
||||
"bcc_type": "BCC tipas",
|
||||
"booking_null": "Visada rodyti kaip laisvą",
|
||||
"booking_0_short": "Visada laisvas",
|
||||
"booking_custom": "Nekintamasis apribojimas pagal individualią užsakymų sumą",
|
||||
"booking_custom_short": "Nekintamasis ribojimas",
|
||||
"booking_ltnull": "Neribotas, bet rodyti kaip užimtas, kai užrezervuota",
|
||||
"booking_lt0_short": "Kintamasis apribojimas",
|
||||
"catch_all": "Viską sugriebiantis",
|
||||
"daily": "Kasdien",
|
||||
"description": "Aprašymas",
|
||||
"disable_login": "Uždrausti prisijungimą (gaunami el. laiškai vis dar priimami)",
|
||||
"disable_x": "Išjungti",
|
||||
"dkim_key_length": "DKIM rakto ilgis (bitai)",
|
||||
"domain": "Domenas",
|
||||
"domain_aliases": "Pakaitomieji domenai",
|
||||
"domain_templates": "Domenų Šablonai",
|
||||
"domain_quota": "Kvota",
|
||||
"domain_quota_total": "Bendroji domeno kvota",
|
||||
"domains": "Domenai",
|
||||
"edit": "Redaguoti",
|
||||
"filters": "Filtrai",
|
||||
"fname": "Pilnas vardas",
|
||||
"gal": "Visuotinis adresų sąrašas",
|
||||
"goto_spam": "Išmokti kaip <b>šlamštas</b>",
|
||||
"hourly": "Kas valandą",
|
||||
"in_use": "Naudojimas (%)",
|
||||
"inactive": "Neaktyvus",
|
||||
"allowed_protocols": "Leidžiami protokolai tiesioginiam vartotojo prieigai (neturi įtakos programėlių slaptažodžių protokolams)",
|
||||
"goto_ham": "Išmokti kaip <b>ham</b>",
|
||||
"aliases": "Pseudonimai",
|
||||
"allow_from_smtp": "Leiskite tik šiems IP naudotis <b>SMTP</b>",
|
||||
"alias_domain_alias_hint": "Pseudonimai <b>nėra</b> taikomi domeno pseudonimams automatiškai. Pseudonimo adresas <code>my-alias@domain</code> <b>neapima</b> adreso <code>my-alias@alias-domain</code> (kur \"alias-domain\" yra įsivaizduojamas \"domain\" pseudonimas).<br>Prašome naudoti sieve filtrą, kad nukreiptumėte laiškus į išorinę pašto dėžutę (žr. skirtuką \"Filtrai\" arba naudokite SOGo -> Peradresavimas). Naudokite \"Išplėsti pseudonimus per pseudonimų sritis\" kad automatiškai pridėti trūkstamus pseudonimus.",
|
||||
"deactivate": "Deaktivuoti",
|
||||
"domain_admins": "Domeno administratoriai",
|
||||
"enable_x": "Įjungti",
|
||||
"last_run_reset": "Suplanuoti kitą",
|
||||
"mailbox": "Pašto dėžutė",
|
||||
"mailbox_defaults": "Numatytieji nustatymai",
|
||||
"mailbox_defaults_info": "Nustatyti numatytuosius nustatymus naujoms pašto dėžutėms.",
|
||||
"mailbox_defquota": "Numatytasis pašto dėžutės dydis",
|
||||
"mailbox_templates": "Pašto dėžučių šablonai",
|
||||
"mailbox_quota": "Maks. pašto dėžutės dydis",
|
||||
"mailboxes": "Pašto dėžutės",
|
||||
"max_aliases": "Maks. slapyvardžiai",
|
||||
"max_mailboxes": "Maks. galimų pašto dėžučių",
|
||||
"max_quota": "Maks. kvota kiekvienai pašto dėžutei",
|
||||
"mins_interval": "Intervalas (min)",
|
||||
"msg_num": "Žinutė #"
|
||||
},
|
||||
"quarantine": {
|
||||
"atts": "Priedaj",
|
||||
"check_hash": "Ieškoti failo Hash'o @ VT",
|
||||
"confirm": "Patvirtinti",
|
||||
"confirm_delete": "Patvirtinti šio elemento trynimą",
|
||||
"danger": "Pavojus"
|
||||
},
|
||||
"success": {
|
||||
"domain_admin_added": "Pridėtas domeno administractorius %s",
|
||||
"domain_admin_removed": "Domeno administratorius %s ištrintas",
|
||||
"domain_modified": "Domeno %s pakitimai išsaugoti",
|
||||
"domain_removed": "Domenas %s ištrintas",
|
||||
"dovecot_restart_success": "„Doveccot“ perkrautas",
|
||||
"domain_added": "Pridėtas domenas %s",
|
||||
"domain_admin_modified": "Pakeitimai domeno adminustratoriui %s išsaugoti"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,17 @@
|
||||
"filters": "Filtri",
|
||||
"recipient_maps": "Saņēmēja kartes",
|
||||
"syncjobs": "Sinhronizācijas uzdevumi",
|
||||
"spam_score": "Mēstules novērtējums"
|
||||
"spam_score": "Mēstules novērtējums",
|
||||
"alias_domains": "Pievienot aizstājdomēnus",
|
||||
"spam_alias": "Pagaidu aizstājvārdi",
|
||||
"app_passwds": "Lietotņu paroļu pārvaldība",
|
||||
"delimiter_action": "Atdalītāja darbība",
|
||||
"domain_desc": "Mainīt domēna aprakstu",
|
||||
"domain_relayhost": "Mainīt domēna relayhost",
|
||||
"eas_reset": "EAS ierīču atiestatīšana",
|
||||
"extend_sender_acl": "Ļauj paplašināt sūtītāja ACL ar ārējām adresēm",
|
||||
"login_as": "Pieteikšanās kā pastkastes lietotājam",
|
||||
"mailbox_relayhost": "Pasta kastītes relayhost maiņa"
|
||||
},
|
||||
"add": {
|
||||
"activate_filter_warn": "Visi pārējie filtri tiks deaktivizēti, kad aktīvs ir atzīmēts.",
|
||||
@@ -12,9 +22,9 @@
|
||||
"add": "Pievienot",
|
||||
"add_domain_only": "Tikai pievienot domēnu",
|
||||
"add_domain_restart": "Pievienot domēnu un restartēt SOGo",
|
||||
"alias_address": "Alias addrese/s",
|
||||
"alias_address": "Aizstājaddrese/s",
|
||||
"alias_address_info": "<small>Pilna epasta addrese/s vai @piemērs.com, lai notvertu visas domēna ziņas (komatu atdalītas). <b>tikai mailcow domēni</b>.</small>",
|
||||
"alias_domain": "Alias domēni",
|
||||
"alias_domain": "Aizstājdomēni",
|
||||
"alias_domain_info": "<small>Tikai derīgi domēna vārdi (komatu atdalīti).</small>",
|
||||
"automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" etc.)",
|
||||
"backup_mx_options": "Dublējuma MX iespējas",
|
||||
@@ -32,14 +42,14 @@
|
||||
"kind": "Veids",
|
||||
"mailbox_quota_m": "Maks. kvota pastkastei (MiB)",
|
||||
"mailbox_username": "Lietotājvārds (kriesā daļa no epasta adreses)",
|
||||
"max_aliases": "Maks. iespejamās aliases",
|
||||
"max_aliases": "Lielākais pieļaujamo aizstājvārdu skaits",
|
||||
"max_mailboxes": "Maks. iespējamās pastkastes",
|
||||
"mins_interval": "Aptauajs intervāls (minūtes)",
|
||||
"multiple_bookings": "Vairākas rezervācijas",
|
||||
"password": "Parole",
|
||||
"password_repeat": "Apstiprinājuma parole (atkārtot)",
|
||||
"port": "Ports",
|
||||
"post_domain_add": "Jums būs nepieciešams atsāknēt SOGo servisa konteineru pēc jauna domēna pievienošanas!",
|
||||
"post_domain_add": "SOGO konteineru \"sogo-mailcow\" ir nepieciešams pārsāknēt pēc jauna domēna pievienošanas.<br><br>Papildus vajadzētu pārskatīt domēnu DNS konfigurāciju. Tiklīdz DNS konfigurācija ir apstiprināta, jāpārsāknē \"acme-mailcow\", lai automātiski izveidotu sertifikātus jaunajam domēnam (autoconfig.<domain>, autodiscover.<domain>).<br>Šis solis ir izvēles un tiks atkārtots ik pēc 24 stundām.",
|
||||
"quota_mb": "Kvota (MiB)",
|
||||
"relay_all": "Pārsūtīt visus saņēmējus",
|
||||
"relay_all_info": "<small>Ja izvēlies <b>nepārsūtīt</b> visus saņēmējus, tad Tev būs nepieciešams pievienot (\"tukšu\") pastkasti katram saņēmējam, kas būtu jāpārsūta.</small>",
|
||||
@@ -56,7 +66,8 @@
|
||||
"target_domain": "Mērķa domēns",
|
||||
"username": "Lietotājvārds",
|
||||
"validate": "Apstiprināt",
|
||||
"validation_success": "Apstiprināts veiksmīgi"
|
||||
"validation_success": "Apstiprināts veiksmīgi",
|
||||
"bcc_dest_format": "BCC galamērķim ir jābūt vienai derīgai e-pasta adresei.<br>Ja ir nepieciešams nosūtīt kopiju vairākām adresēm, jāizveido aizstājvārds un jāizmanto tas šeit."
|
||||
},
|
||||
"admin": {
|
||||
"access": "Pieeja",
|
||||
@@ -118,7 +129,7 @@
|
||||
"password_repeat": "Apstiprināt paroli (atkārtot)",
|
||||
"private_key": "Privāta atslēga",
|
||||
"quarantine": "Karantīna",
|
||||
"quarantine_exclude_domains": "Izslēgt domēnus un alias-domēnus:",
|
||||
"quarantine_exclude_domains": "Neņemt vērā domēnus un aizstājdomēnus",
|
||||
"quarantine_max_size": "Maks. izmērs MiB (lielāki vienumi ir atbrīvoti):",
|
||||
"quarantine_retention_size": "Atlikumi pastkastēs:",
|
||||
"r_active": "Aktīvie ierobežojumi",
|
||||
@@ -129,7 +140,7 @@
|
||||
"regen_api_key": "Reģenerēt API atslēgu",
|
||||
"relay_from": "\"No:\" addrese",
|
||||
"relay_run": "Palaist testu",
|
||||
"relayhosts_hint": "Definējiet relejhostus šeit, lai tos varētu izvēlēties domēna konfigurācijas logā.",
|
||||
"relayhosts_hint": "Norādīt no sūtītāja atkarīgas piegādes, lai varētu tos atlasīt domēnu konfigurācijas uzvednē.<br>\n Piegādes pakalpojums vienmēr ir \"smtp\", tādējādi tiks mēģināts TLS, kad piedāvāts. Iekļautais TLS (SMTPS) netiek atbalstīts. Tiek ņemts vērā lietotāja atsevišķais izejošā TLS nosacījuma iestatījums.<br>\n Ietekmē atlasītos domēnus, tajā skaitā aizstājdomēnus.",
|
||||
"remove": "Noņemt",
|
||||
"remove_row": "Noņemt rindu",
|
||||
"reset_default": "Atiestatīt uz noklusējumu",
|
||||
@@ -148,16 +159,19 @@
|
||||
"username": "Lietotājvārds",
|
||||
"generate": "izveidot",
|
||||
"message": "Ziņojums",
|
||||
"last_applied": "Pēdējoreiz pielietots"
|
||||
"last_applied": "Pēdējoreiz pielietots",
|
||||
"f2b_regex_info": "Vērā ņemtie žurnāli: SOGO, Postfix, Dovecot, PHP-FPM",
|
||||
"sys_mails": "Sistēmas pasts",
|
||||
"ip_check_disabled": "IP pārbaude ir atspējota. To var iespējot<br><strong>Sistēma > Konfigurācija > iespējas > Pielāgot</strong>"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Piekļuve liegta, vai nepareizi dati",
|
||||
"alias_domain_invalid": "Alias domēns ir nepareizs",
|
||||
"alias_empty": "Alias adrese nevar būt tukša",
|
||||
"alias_goto_identical": "Alias un domēnvārds nevar būt identisks",
|
||||
"alias_invalid": "Alias adrese nepareiza",
|
||||
"aliasd_targetd_identical": "Alias domēns nevar būt vienāds ar mērķa domēnu",
|
||||
"aliases_in_use": "Maks. aliases jabūt lielākām vai vienādām ar %d",
|
||||
"alias_domain_invalid": "Aizstājdomēns %s ir nederīgs",
|
||||
"alias_empty": "Aizstājadrese nedrīkst būt tukša",
|
||||
"alias_goto_identical": "Aizstājvārds un mērķa adrese nedrīkst būt vienādi",
|
||||
"alias_invalid": "Aizstājadrese %s ir nederīga",
|
||||
"aliasd_targetd_identical": "Aizstājdomēns nedrīkst būt vienāds ar mērķa domēnu: %s",
|
||||
"aliases_in_use": "Pieļaujamajam aizstājvārdu skaitam jābūt lielākam vai vienādam ar %d",
|
||||
"description_invalid": "Resursa apraksts ir nederīgs",
|
||||
"dkim_domain_or_sel_invalid": "DKIM domēns vai selektors nepareizs",
|
||||
"domain_exists": "Domēns %s jau pastāv",
|
||||
@@ -165,15 +179,15 @@
|
||||
"domain_not_empty": "Nevar noņemt neaizpildītu domēnu",
|
||||
"domain_not_found": "Domēns %s nav atrasts",
|
||||
"domain_quota_m_in_use": "Domēna kvotai jābūt lielākai vai vienādai ar %s MiB",
|
||||
"goto_empty": "Goto adrese nevar būt tukša",
|
||||
"goto_empty": "Aizstājādresei jāsatur vismaz viena derīga mērķa adrese",
|
||||
"goto_invalid": "Goto adrese nepareiza",
|
||||
"imagick_exception": "Kļūda: Imagick izņēmums, lasot attēlu",
|
||||
"img_invalid": "Nevar apstiprināt attēla failu",
|
||||
"img_tmp_missing": "Nevar apstiprināt attēla failu: pagaidu failu nav atrasts",
|
||||
"invalid_mime_type": "Nederīgs mime tips",
|
||||
"is_alias": "%s jau ir zināms alias",
|
||||
"is_alias_or_mailbox": "%s jau ir zināms alias, pastkastes vai alias addrese izvērsta no alias domēna.",
|
||||
"is_spam_alias": "%s ir jau zināms spam alias",
|
||||
"is_alias": "%s jau ir zināma kā aizstājadrese",
|
||||
"is_alias_or_mailbox": "%s jau ir zināms kā aizstājvārds, pastkaste vai aizstājadrese, kas ir izvērsta no aizstājdomēna.",
|
||||
"is_spam_alias": "%s jau ir zināma kā pagaidu aizstājadrese (aizstājadrese mēstulēm)",
|
||||
"last_key": "Pēdējo atslēgu nevar izdzēst, tā vietā jāatspējo divpakāpju pārbaude.",
|
||||
"login_failed": "Ielogošanās neveiksmīga",
|
||||
"mailbox_invalid": "Pastkastes vārds ir nederīgs",
|
||||
@@ -181,7 +195,7 @@
|
||||
"mailbox_quota_exceeds_domain_quota": "Pastkastes izmērs pārsniedz maksimāli pieļaujamo",
|
||||
"mailbox_quota_left_exceeded": "Nav pietiekami daudz vietas (atlikusī vieta: %d MiB)",
|
||||
"mailboxes_in_use": "Maks. pastkastēm jābūt lielākām vai vienādām ar %d",
|
||||
"max_alias_exceeded": "Visas aliases izmantotas",
|
||||
"max_alias_exceeded": "Pārsniegts pieļaujamo aizstājvārdu skaits",
|
||||
"max_mailbox_exceeded": "Maksimālais pastkastšu skaits sasniegts (%d of %d)",
|
||||
"max_quota_in_use": "Pastkastes kvotai jābūt lielākai vai vienādai %d MiB",
|
||||
"maxquota_empty": "Maksimālais pieļaujamais izmērs nevar būt 0.",
|
||||
@@ -213,7 +227,7 @@
|
||||
},
|
||||
"edit": {
|
||||
"active": "Aktīvs",
|
||||
"alias": "Rediģēt alias",
|
||||
"alias": "Labot aizstājvārdu",
|
||||
"automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" utt.)",
|
||||
"backup_mx_options": "Dublēt MX iespējas",
|
||||
"delete1": "Dzēst no avota, kad pabeigts",
|
||||
@@ -224,8 +238,8 @@
|
||||
"domain_admin": "Labot domēna administratoru",
|
||||
"domain_quota": "Domēna kvota",
|
||||
"domains": "Domēni",
|
||||
"dont_check_sender_acl": "Atspējot domēna sūtītāju pārbaudi %s + alias domēni",
|
||||
"edit_alias_domain": "Rediģēt alias domēnu",
|
||||
"dont_check_sender_acl": "Atspējot sūtītāju pārbaudi domēnam %s (+ aizstājdomēni)",
|
||||
"edit_alias_domain": "Labot aizstājdomēnu",
|
||||
"encryption": "Šifrēšana",
|
||||
"exclude": "Neiekļaut objektus (regex)",
|
||||
"force_pw_update": "Piespiedu paroles atjaunošana pie nākošās pieslēgšanās",
|
||||
@@ -235,7 +249,7 @@
|
||||
"inactive": "Neaktīvs",
|
||||
"kind": "Veids",
|
||||
"mailbox": "Rediģēt pastkasti",
|
||||
"max_aliases": "Maks. aliases",
|
||||
"max_aliases": "Lielākais aizstājvārdu skaits",
|
||||
"max_mailboxes": "Maks. iespējamās pastkastes",
|
||||
"max_quota": "Maks. kvota uz pastkasti (MiB)",
|
||||
"maxage": "Maximum age of messages in days that will be polled from remote<br><small>(0 = ignore age)</small>",
|
||||
@@ -267,7 +281,15 @@
|
||||
"unchanged_if_empty": "Ja neizmainīts atstājiet tukšu",
|
||||
"username": "Lietotājvārds",
|
||||
"validate_save": "Apstiprināt un saglabāt",
|
||||
"last_modified": "Pēdējoreiz mainīts"
|
||||
"last_modified": "Pēdējoreiz mainīts",
|
||||
"domain_footer_skip_replies": "Neņemt vērā kājeni atbildes e-pastos",
|
||||
"bcc_dest_format": "BCC galamērķim jābūt vienai derīgai e-pasta adresei.<br>Ja ir nepieciešams nosūtīt kopiju uz vairākām adresēm, jāizveido aizstājvārds, un tas ir jāizmanto šeit.",
|
||||
"extended_sender_acl_info": "Vajadzētu ievietot DKIM domēna atslēgu, ja tā ir pieejama.<br>\n Jāatceras pievienot šo serveri attiecīgajam SPF TXT ierakstam.<br>\n Kad vien šim serverim tiek pievienots domēns vai aizstājdomēns, kas pārklājas ar ārēju adresi, ārējā adrese tiek noņemta.<br>\n Jāizmanto @domain.tld, lai ļautu nosūtīt kā *@domain.tld.",
|
||||
"pushover_info": "Pašpiegādes paziņojumu iestatījumi tiks piemēroti visiem tīrajiem (ne surogātpasta) sūtījumiem, kas piegādāti <b>%s</b>, tostarp aizstājvārdiem (kopīgotiem, nekopīgotiem, ar birkām).",
|
||||
"mailbox_relayhost_info": "Tiek piemērots tikai pastkastei un tiešajiem aizstājvārdiem, aizstāj domēna retranslācijas saimniekdatoru.",
|
||||
"sender_acl_info": "Ja pastkastes lietotājam A ir ļauts sūtīt kā pastkastes lietotājam B, sūtītāja adrese SOGo netiek automātiski parādīta kā atlasāms lauks \"no\".<br>.\n Pastkastes lietotājam B SOGo ir jāizveido pilnvarojums, lai pastkastes lietotājs A varētu izvēlēties tā adresi kā sūtītāja. Lai SOGo pilnvarotu pastkasti, pasta skatā jāizmanto izvēlne (trīs punkti) pa labi no pastkastes nosaukuma augšējā kreisajā pusē. Šī darbība neattiecas uz aizstājadresēm.",
|
||||
"sogo_visible": "Aizstājvārds ir redzams SOGo",
|
||||
"sogo_visible_info": "Šī iespēja ietekmē tikai tos objektus, kurus var parādīt SOGo (koplietojamās vai nekoplietojamās aizstājadreses, kas norāda uz vismaz vienu vietējo pastkasti). Ja paslēpts, netiks parādīts SOGo kā atlasāms sūtītājs."
|
||||
},
|
||||
"footer": {
|
||||
"cancel": "Atcelt",
|
||||
@@ -282,11 +304,12 @@
|
||||
"header": {
|
||||
"administration": "Administrēšana",
|
||||
"debug": "Atkļūdošana",
|
||||
"email": "E-Mail",
|
||||
"mailcow_config": "Configurācija",
|
||||
"email": "E-pasts",
|
||||
"mailcow_config": "Konfigurācija",
|
||||
"quarantine": "Karantīna",
|
||||
"restart_sogo": "Restartēt SOGo",
|
||||
"user_settings": "Lietotāja uzstādījumi"
|
||||
"user_settings": "Lietotāja uzstādījumi",
|
||||
"mailcow_system": "Sistēma"
|
||||
},
|
||||
"info": {
|
||||
"no_action": "No action applicable"
|
||||
@@ -385,7 +408,10 @@
|
||||
"daily": "Ik dienu",
|
||||
"hourly": "Ik stundu",
|
||||
"last_mail_login": "Pēdējā pieteikšanās pastkastē",
|
||||
"mailbox": "Pastkaste"
|
||||
"mailbox": "Pastkaste",
|
||||
"sieve_preset_2": "Vienmēr atzīmēt noteikta sūtītāja e-pastu kā izlasītu",
|
||||
"open_logs": "Atvērt žurnālus",
|
||||
"sieve_preset_8": "Pārvirzīt noteikta sūtītāja e-pastu, atzīmēt kā izlasītu un ievietot to apakšmapē"
|
||||
},
|
||||
"quarantine": {
|
||||
"action": "Darbības",
|
||||
@@ -408,10 +434,14 @@
|
||||
"subj": "Priekšmets",
|
||||
"text_from_html_content": "Saturs (konvertēts html)",
|
||||
"text_plain_content": "Saturs (teksts/vienkāršs)",
|
||||
"toggle_all": "Pārslēgt visu"
|
||||
"toggle_all": "Pārslēgt visu",
|
||||
"disabled_by_config": "Pašreizējā sistēmas konfigurācija atspējo karantīnu. Lūgums iestatīt \"saglabāšanu katrai pastkastītei\" un \"lielākais pieļaujamais lielums\" karantīnas vienumiem.",
|
||||
"qhandler_success": "Pieprasījums veiksmīgi nosūtīts sistēmai. Tagad var aizvērt logu.",
|
||||
"qinfo": "Karantīnas sistēma datubāzē saglabās noraidīto pastu (sūtītājam <em>netiks</em> radīts iespaids par piegādātu pastu), kā arī pastu, kas tiek piegādāts kā kopija pastkastes mēstuļu mapē.\n <br>\"Apgūt kā surogātpastu un izdzēst\" apgūs ziņojumu kā surogātpastu ar Bajesa teorēmu un aprēķinās arī nestriktas jaucējvērtības, lai nākotnē noraidītu līdzīgus ziņojumus.\n <br>Lūgums apzināties, ka vairāku ziņojumu apgūšana var būt laikietilpīga atkarībā no sistēmas.<br>Melnā saraksta vienumi karantīnā netiek iekļauti."
|
||||
},
|
||||
"queue": {
|
||||
"queue_manager": "Queue Manager"
|
||||
"queue_manager": "Queue Manager",
|
||||
"info": "Pasta rinda satur visus e-pastus, kas gaida piegādi. Ja e-pasts ir iestrēdzis pasta rindā ilgu laiku, sistēma to automātiski izdzēš.<br>Attiecīgā pasta kļūdas ziņojums sniedz informāciju par to, kāpēc pastu neizdevās piegādāt."
|
||||
},
|
||||
"start": {
|
||||
"help": "Rādīt/Paslēp palīdzības paneli",
|
||||
@@ -552,12 +582,13 @@
|
||||
"tls_enforce_in": "Piespiest TLS ienākošajiem",
|
||||
"tls_enforce_out": "Piespiest TLS izejošajiem",
|
||||
"tls_policy": "Šifrēšanas politika",
|
||||
"tls_policy_warning": "<strong>Uzmanību:</strong> Ja jūs izlemjat aktivizēt e-pastu šifrēšanu, Jūs varat pazaudēt vēstules.<br>Vēstules kas neatbilst politikai atleks atpakaļ no sistēmas ar kļūdu.<br>Šī opcija attiecas uz Jūsu primāro e-pasta adresi, visām adresēm, kas atvasinātas no alias domēniem, kā arī aliasi <b>,kas saistīti tikai ar šo pastkasti</b> kā mērķis.",
|
||||
"tls_policy_warning": "<strong>Brīdinājums:</strong> Ja tiek izlemts ieviest šifrēta pasta nosūtīšanu, var tikt pazaudēti e-pasti.<br>Ziņojumi, kas neatbilst nosacījumiem, pasta sistēma atmetīs ar kļūdu.<br>Šī iespēja attiecas uz galveno e-pasta adresi (pieteikšanās vārdu), visām adresēm, kas atvasinātas no aizsājdomēniem, kā arī aizstājadreses, <b>kas norāda tikai uz šo pastkasti</b>.",
|
||||
"user_settings": "Lietotāja iestatījumi",
|
||||
"username": "Lietotājvārds",
|
||||
"waiting": "Waiting",
|
||||
"week": "Nedēļa",
|
||||
"weeks": "Nedēļas"
|
||||
"weeks": "Nedēļas",
|
||||
"open_logs": "Atvērt žurnālus"
|
||||
},
|
||||
"datatables": {
|
||||
"paginate": {
|
||||
@@ -566,6 +597,17 @@
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"last_modified": "Pēdējoreiz mainīts"
|
||||
"last_modified": "Pēdējoreiz mainīts",
|
||||
"static_logs": "Nemainīgie žurnāli",
|
||||
"no_update_available": "Sistēma izmanto jaunāko versiju",
|
||||
"in_memory_logs": "Atmiņā esošie žurnāli",
|
||||
"system_containers": "Sistēma un konteineri",
|
||||
"current_time": "Sistēmas laiks",
|
||||
"external_logs": "Ārējie žurnāli",
|
||||
"logs": "Žurnāli"
|
||||
},
|
||||
"warning": {
|
||||
"domain_added_sogo_failed": "Domēns pievienots, bet neizdevās pārsāknēt SOGO. Lūgums pārbaudīt servera žurnālus.",
|
||||
"dovecot_restart_failed": "Dovecot neizdevās pārsāknēties. Lūgums pārbaudīt žurnālus."
|
||||
}
|
||||
}
|
||||
|
||||
326
data/web/lang/lang.nb-no.json
Normal file
326
data/web/lang/lang.nb-no.json
Normal file
@@ -0,0 +1,326 @@
|
||||
{
|
||||
"admin": {
|
||||
"api_read_only": "Kun lesetilgang",
|
||||
"advanced_settings": "Avanserte innstillinger",
|
||||
"admin_details": "Endre administratordetaljer",
|
||||
"add_row": "Legg til rad",
|
||||
"active_rspamd_settings_map": "Aktivt innstillingskart",
|
||||
"admins": "Administratorer",
|
||||
"api_info": "APIet er under utvikling. Dokumentasjonen finnes på <a href=\"/api\">/api</a>",
|
||||
"admin_domains": "Domenetilordninger",
|
||||
"add": "Legg til",
|
||||
"active": "Aktiv",
|
||||
"add_transport": "Legg til transport",
|
||||
"api_key": "API-nøkkel",
|
||||
"add_admin": "Legg til administrator",
|
||||
"admin": "Administrator",
|
||||
"allowed_methods": "Tilgangskontroll-Tillatte-Metoder",
|
||||
"add_transports_hint": "Vær oppmerksom på at eventuelle autentiseringsdata vil bli lagret i klartekst.",
|
||||
"add_relayhost": "Legg til sender-avhengig transport",
|
||||
"additional_rows": " ekstra rader ble lagt til",
|
||||
"admins_ldap": "LDAP-administratorer",
|
||||
"action": "Handling",
|
||||
"add_settings_rule": "Legg til innstillingsregel",
|
||||
"allowed_origins": "Tilgangskontroll-Tillat-Opphav",
|
||||
"api_allow_from": "Tillat API-tilgang fra disse IP/CIDR-nettverkene",
|
||||
"activate_send": "Aktivér sendeknapp",
|
||||
"access": "Tilgang",
|
||||
"add_domain_admin": "Legg til domeneadministrator",
|
||||
"add_forwarding_host": "Legg til videresendingsvert",
|
||||
"activate_api": "Aktivér API",
|
||||
"add_relayhost_hint": "Vennligst vær oppmerksom på at autentiseringsdata, om noe, vil bli lagret i klartekst.",
|
||||
"app_links": "App-lenker",
|
||||
"app_name": "App-navn",
|
||||
"apps_name": "\"mailcow Apps\"-navn",
|
||||
"arrival_time": "Ankomsttid (servertid)",
|
||||
"authed_user": "Autorisert bruker",
|
||||
"ays": "Er du sikker på at du vil fortsette?",
|
||||
"change_logo": "Endre logo",
|
||||
"logo_normal_label": "Normal",
|
||||
"logo_dark_label": "Omvendt for mørk modus",
|
||||
"configuration": "Konfigurasjon",
|
||||
"convert_html_to_text": "Konvertér HTML til ren tekst",
|
||||
"copy_to_clipboard": "Tekst kopiert til utklippstavle!",
|
||||
"cors_settings": "CORS-innstillinger",
|
||||
"credentials_transport_warning": "<b>Advarsel</b>: Å legge til en ny transport-mapping vil oppdatere innloggingsinformasjon for alle oppføringer med en tilsvarende \"next hop\"-kolonne.",
|
||||
"customer_id": "Kunde-ID",
|
||||
"customize": "Tilpass",
|
||||
"destination": "Mål",
|
||||
"dkim_add_key": "Legg til ARC/DKIM-nøkkel",
|
||||
"dkim_domains_selector": "Velger",
|
||||
"dkim_domains_wo_keys": "Velg domener med manglende nøkler",
|
||||
"dkim_from": "Fra",
|
||||
"dkim_key_length": "DKIM-nøkkellengde (bits)",
|
||||
"dkim_key_missing": "Nøkkel mangler",
|
||||
"dkim_key_unused": "Nøkkel ikke i bruk",
|
||||
"dkim_key_valid": "Nøkkel gyldig",
|
||||
"dkim_keys": "ARC/DKIM-nøkler",
|
||||
"dkim_overwrite_key": "Overskriv eksisterende DKIM-nøkkel",
|
||||
"dkim_private_key": "Privat nøkkel",
|
||||
"dkim_to": "Til",
|
||||
"domain_admins": "Domeneadministratorer",
|
||||
"domain_s": "Domene/r",
|
||||
"duplicate": "Duplikat",
|
||||
"duplicate_dkim": "Duplisert DKIM-oppføring",
|
||||
"edit": "Rediger",
|
||||
"empty": "Ingen resultater",
|
||||
"excludes": "Ekspluderer disse mottakerne",
|
||||
"f2b_ban_time": "Utestengingstid (sek)",
|
||||
"f2b_blacklist": "Svartelistede nettverk/verter",
|
||||
"f2b_filter": "Regex-filtre",
|
||||
"f2b_manage_external": "Betjene Fail2Ban eksternt",
|
||||
"f2b_manage_external_info": "Fail2Ban vil fortsette å vedlikeholde utestengingslisten, men den vil ikke aktivt legge til regler for å blokkere trafikk. Bruk den genererte blokkeringslisten under for å blokkere trafikk eksternt.",
|
||||
"f2b_max_attempts": "Maks antall forsøk",
|
||||
"f2b_max_ban_time": "Maks utestengingstid (sek)",
|
||||
"f2b_netban_ipv4": "IPv4-subnetstørrelse å aktivere blokkering for (8-32)",
|
||||
"f2b_netban_ipv6": "IPv6-subnetstørrelse å aktivere blokkering for (8-128)",
|
||||
"f2b_parameters": "Fail2Ban-parametre",
|
||||
"f2b_retry_window": "Tidsrom (sek) for maks antall forsøk",
|
||||
"f2b_whitelist": "Hvitelistede nettverk/verter",
|
||||
"filter_table": "Filtreringstabell",
|
||||
"forwarding_hosts": "Videresendingsverter",
|
||||
"api_skip_ip_check": "Hopp over IP-sjekk for API",
|
||||
"ban_list_info": "Se liste over utestengte IPer under: <b>nettverk (gjenværende utestengingstid) - [handlinger]</b>.<br />IPer i kø for å bli tillatt igjen vil bli fjernet fra den aktive utestengingslisten i løpet av noen sekunder.<br />Røde etiketter indikerer aktive permanente utestenginger via svartelisting.",
|
||||
"dkim_from_title": "Kildedomene å kopiere data fra",
|
||||
"domain": "Domene",
|
||||
"f2b_ban_time_increment": "Utestengingstid økes for hver ny utestenging",
|
||||
"api_read_write": "Lese- og skrivetilgang",
|
||||
"f2b_list_info": "En svartelistet vert eller nettverk vil alltid ha større vekt enn en hvitelistet oppføring.<b>Oppdateringer av listen tar noen sekunder før de vises.</b>",
|
||||
"f2b_regex_info": "Logger tatt i betraktning: SOGo, Postfix, Dovecot, PHP-FHM.",
|
||||
"dkim_to_title": "Måldomene/r - vil bli overskrevet",
|
||||
"domain_admin": "Domeneadministrator",
|
||||
"r_active": "Aktive begrensninger",
|
||||
"queue_unban": "fjern blokkering",
|
||||
"r_inactive": "Inaktive begrensninger",
|
||||
"r_info": "Grå/deaktivere elementer i listen over aktive restriksjoner er ikke kjente som gyldige restriksjoner i mailcow og kan ikke flyttes. Ukjente begrensninger vil bli aktivert i den rekkefølgen de opptrer uansett.<br>Du kan legge til nye elementer i <code>inc/vars.local.inc.php</code> for å kunne slå dem av og på.",
|
||||
"relayhosts_hint": "Definer sender-avhengige transportmetoder for å kunne velge dem i domenekonfigurasjonsdialogen<br>\n Transportmetoden er alltid \"smtp:\" og vil derfor forsøke TLS når tilbudt. Innpakket TLS (SMTPS) er ikke støttet. En brukers individuelle utgående TLS-policyinnstilling tas til etterretning.<br>\n Gjelder for valgte domener inkludert alias-domener.",
|
||||
"rspamd_global_filters_regex": "Navnene forklarer deres mening. Alt innhold må inneholde gyldige regulære utstrykk etter formatet \"/mønster/alternativer\" (f.eks. <code>/.+@domain\\.tld/i</code>).<br>\n Selv om grove sjekkrutiner kjøres for hver linje regex, kan funksjonaliteten i Rspamd bli ødelagt dersom den ikke klarer å lese syntakset riktig.<br>\n Rspamd vil forsøke å lese innholdet når det endres. Hvis du opplever problemer, <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">start Rspamd på nytt</a> for å tvinge ny innlesing av mappingene.<br>Svartelistede elementer er ekskluderte fra karantene.",
|
||||
"transport_test_rcpt_info": "• Bruk null@hosted.mailcow.de for å teste videresending til en ekstern destinasjon.",
|
||||
"transports_hint": "• En transportmapping <b>overstyrer</b> en sender-avhengig transportmapping</b>.<br>\n• MX-baserte transportmappinger er foretrukket brukt.<br>\n• Utgående TLS-policyinnstillinger per-bruker blir ignorert og kan kun tvinges via TLS-policymappinger.<br>\n• Transporttjenesten for definerte transportmetoder er alltid \"smtp:\" og vil derfor forsøke TLS når tilbudt. Innpakket TLS (SMTPS) støttes ikke.<br>\n• Adresser som matcher \"/localhost$/\" vil alltid sendes via \"local:\", derfor vil en \"*\"-destinasjon ikke gjelde disse adressene.<br>\n• For å bestemme identifikasjon for et eksempelvis neste hopp \"[host]:25\", vil Postfix <b>alltid</b> sende en forespørsel for \"host\" før den søker etter \"[host]:25\". Denne oppførselen gjør det umulig å bruke både \"host\" og \"[host]:25\" samtidig.",
|
||||
"regex_maps": "Regex-mappinger",
|
||||
"relay_from": "\"Fra:\"-adresse",
|
||||
"relay_rcpt": "\"Til:\"-adresse",
|
||||
"relay_run": "Kjør test",
|
||||
"relayhosts": "Sender-avhengige transportmetoder",
|
||||
"remove": "Fjern",
|
||||
"remove_row": "Fjern rad",
|
||||
"reset_default": "Tilbakestill til standardinnstillinger",
|
||||
"reset_limit": "Fjern hash",
|
||||
"routing": "Ruting",
|
||||
"rsetting_add_rule": "Legg til regel",
|
||||
"rsetting_content": "Regelinnhold",
|
||||
"rsetting_desc": "Kort beskrivelse",
|
||||
"rsetting_no_selection": "Vennligst velg en regel",
|
||||
"rsetting_none": "Ingen regler tilgjengelig",
|
||||
"rsettings_insert_preset": "Legg inn eksempel-forvalg \"%s\"",
|
||||
"rsettings_preset_1": "Deaktiver alt unntatt DKIM og mengdebegrensning for autentiserte brukere",
|
||||
"rsettings_preset_2": "Postmastere vil ha spam",
|
||||
"rsettings_preset_3": "Bare tillat spesifikke sendere for en postboks (f.eks. kun bruk som intern postboks)",
|
||||
"rsettings_preset_4": "Deaktiver Rspamd for et domene",
|
||||
"rspamd_com_settings": "Et navn for innstillingen vil bli autogenerert, vennligst se eksempelforvalgene under. For ytterligere detaljer se <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">Rspamd-dokumentasjonen</a>",
|
||||
"rspamd_global_filters": "Globale filtreringsmappinger",
|
||||
"rspamd_global_filters_agree": "Jeg skal være forsiktig!",
|
||||
"rspamd_global_filters_info": "Globale filtermappinger inneholder forskjellige typer globale svarte- og hvitelister.",
|
||||
"rspamd_settings_map": "Rspamd innstillingsmapping",
|
||||
"sal_level": "Moo-nivå",
|
||||
"save": "Lagre endringer",
|
||||
"search_domain_da": "Søk domener",
|
||||
"send": "Send",
|
||||
"sender": "Avsender",
|
||||
"service": "Tjeneste",
|
||||
"service_id": "TjenesteID",
|
||||
"source": "Kilde",
|
||||
"spamfilter": "Spamfilter",
|
||||
"subject": "Emne",
|
||||
"success": "Suksess",
|
||||
"sys_mails": "Systemeposter",
|
||||
"text": "Tekst",
|
||||
"time": "Tid",
|
||||
"title": "Tittel",
|
||||
"title_name": "\"mailcow UI\" nettstedstittel",
|
||||
"to_top": "Tilbake til toppen",
|
||||
"transport_dest_format": "Regex eller syntaks: example.org, .example.org, *, box@example.org (flere verdier kan separeres via komma)",
|
||||
"transport_maps": "Transportmappinger",
|
||||
"ui_footer": "Bunntekst (HTML tillatt)",
|
||||
"ui_header_announcement": "Annonseringer",
|
||||
"ui_header_announcement_active": "Gjør annonsering aktiv",
|
||||
"ui_header_announcement_content": "Tekst (HTML tillatt)",
|
||||
"ui_header_announcement_help": "Annonseringen er synlig for alle innloggede brukere og på innloggingsskjermen for grensesnittet.",
|
||||
"ui_header_announcement_select": "Velg annonseringstype",
|
||||
"ui_header_announcement_type": "Type",
|
||||
"ui_header_announcement_type_danger": "Veldig viktig",
|
||||
"ui_header_announcement_type_info": "Info",
|
||||
"ui_header_announcement_type_warning": "Viktig",
|
||||
"ui_texts": "Grensesnittmerkelapper og tekster",
|
||||
"unban_pending": "fjerning av utestenging avventes",
|
||||
"unchanged_if_empty": "Hvis uendret, la være tomt",
|
||||
"upload": "Last opp",
|
||||
"username": "Brukernavn",
|
||||
"validate_license_now": "Validér GUID mot lisenstjener",
|
||||
"verify": "Validér",
|
||||
"yes": "✓"
|
||||
},
|
||||
"acl": {
|
||||
"ratelimit": "Nivågrense",
|
||||
"sogo_profile_reset": "Nullstill SOGo-profil",
|
||||
"smtp_ip_access": "Endre tillatte verter for SMTP",
|
||||
"tls_policy": "TLS-policy",
|
||||
"spam_alias": "Midlertidige alias",
|
||||
"spam_policy": "Svarteliste/Hvitliste",
|
||||
"unlimited_quota": "Ubegrenset kvote for postkasser",
|
||||
"sogo_access": "Tillat administrasjon av SOGo-tilgang",
|
||||
"syncjobs": "Synkroniser jobber",
|
||||
"spam_score": "Spam-resultat",
|
||||
"recipient_maps": "Mottakerkart",
|
||||
"alias_domains": "Legg til alias-domene",
|
||||
"app_passwds": "Behandle app-passord",
|
||||
"bcc_maps": "BCC-mappinger",
|
||||
"delimiter_action": "Skilletegn-handling",
|
||||
"domain_relayhost": "Endre videresendingsvert for et domene",
|
||||
"eas_reset": "Nullstill EAS-enheter",
|
||||
"filters": "Filtre",
|
||||
"login_as": "Logg inn som postkassebruker",
|
||||
"mailbox_relayhost": "Endre videresendingsvert for en postkasse",
|
||||
"prohibited": "Forbudt av ACL",
|
||||
"protocol_access": "Endre protokolltilgang",
|
||||
"pushover": "Pushover",
|
||||
"quarantine": "Karantenehandlinger",
|
||||
"quarantine_attachments": "Sett vedlegg i karantene",
|
||||
"quarantine_category": "Endre varslingskategori for karantene",
|
||||
"quarantine_notification": "Endre karantenevarslinger",
|
||||
"domain_desc": "Endre domenebeskrivelse",
|
||||
"extend_sender_acl": "Tillat utvidelse av sender-ACL fra eksterne adresser"
|
||||
},
|
||||
"add": {
|
||||
"app_passwd_protocols": "Tillatte protokoller for app-passord",
|
||||
"hostname": "Vert",
|
||||
"goto_ham": "Lær som <span class=\"text-success\"><b>ham</b></span>",
|
||||
"custom_params": "Tilpassede parametre",
|
||||
"target_address": "Gå-til-adresse",
|
||||
"delete2": "Slett meldinger på måldestinasjonen som ikke finnes på kilden",
|
||||
"syncjob": "Legg til synkroniseringsjobb",
|
||||
"domain": "Domene",
|
||||
"timeout1": "Tidsavbrudd for tilkobling til ekstern vert",
|
||||
"goto_null": "Slett epost uten varsling",
|
||||
"custom_params_hint": "Riktig: --param=xy, feil: --param xy",
|
||||
"multiple_bookings": "Flere reservasjoner",
|
||||
"alias_domain": "Aliasdomene",
|
||||
"exclude": "Ekskludér objekter (regex)",
|
||||
"validate": "Validér",
|
||||
"dry": "Simulér synkronisering",
|
||||
"relay_all_info": "↪ Hvis du velger å <b>ikke</b> videresende for alle mottakere, så må du legge til en (\"blind\") postkasse for hver eneste mottaker som det skal videresendes for.",
|
||||
"description": "Beskrivelse",
|
||||
"destination": "Måldestinasjon",
|
||||
"alias_domain_info": "<small>Kun gyldige domenenavn (kommaseparerte).</small>",
|
||||
"mailbox_username": "Brukernavn (venstre del av en epostadresse)",
|
||||
"username": "Brukernavn",
|
||||
"mins_interval": "Sjekkintervall (minutter)",
|
||||
"disable_login": "Ikke tillat innlogging (innkommende epost blir fremdeles mottatt)",
|
||||
"delete2duplicates": "Slett duplikater på måldestinasjonen",
|
||||
"post_domain_add": "SOGo-kontaineren, \"sogo-mailcow\", må startes på nytt etter at man har lagt til et nytt domene!<br><br>I tillegg må domenets DNS-oppsett ses gjennom. Når DNS-oppsettet er godkjent, start \"acme-mailcow\" på nytt for å automatisk opprette sertifikater for det nye domenet (autoconfig.<domain>, autodiscover.<domain>).<br>Dette steget er valgfritt og vil bli forsøkt utført hver 24. time.",
|
||||
"select": "Vennligst velg...",
|
||||
"full_name": "Fullt navn",
|
||||
"alias_address": "Aliasadresse/r",
|
||||
"activate_filter_warn": "Alle andre filtre vil deaktiveres når aktive er valgt.",
|
||||
"quota_mb": "Kvote (MiB)",
|
||||
"gal_info": "Den globale adresselisten inneholder alle objekter i et domene og kan ikke redigeres av noen bruker. Ledig/opptatt-informasjon i SOGo vil mangle, hvis deaktivert! <b>Start SOGo på nytt for å aktivere endringene.</b>",
|
||||
"alias_address_info": "<small>Komplett/e epostadresse/r eller @example.com, for å fange opp alle meldinger til et domene (kommaseparert).<b>Kun mailcow-domener</b>.</small>",
|
||||
"bcc_dest_format": "BCC-mål må være én enkelt og gyldig epostadresse.<br>Hvis du trenger å sende en kopi til flere adresser, opprett et alias og bruk det her.",
|
||||
"add": "Legg til",
|
||||
"add_domain_only": "Legg til kun domene",
|
||||
"kind": "Type",
|
||||
"relay_domain": "Vidersend for dette domenet",
|
||||
"add_domain_restart": "Legg til domene og start SOGo på nytt",
|
||||
"enc_method": "Krypteringsmetode",
|
||||
"app_name": "App-navn",
|
||||
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Info</div> Du kan definere transport-kart for en egendefinert destinasjon for dette domenet. Hvis denne ikke er satt, vil det bli foretatt et MX-oppslag.",
|
||||
"max_aliases": "Maks. antall mulige alias",
|
||||
"comment_info": "En privat kommentar er ikke synlig for brukeren, mens en offentlig kommentar vises som et verktøytips når man holder muspekeren over i en brukers oversikt.",
|
||||
"inactive": "Inaktiv",
|
||||
"domain_quota_m": "Total domenekvote (MiB)",
|
||||
"password": "Passord",
|
||||
"delete1": "Slett fra kilde når fullført",
|
||||
"tags": "Emneknagger",
|
||||
"port": "Port",
|
||||
"backup_mx_options": "Alternativer for videresending",
|
||||
"gal": "Global Adresseliste",
|
||||
"private_comment": "Privat kommentar",
|
||||
"goto_spam": "Lær som <span class=\"text-danger\"><b>spam</b></span>",
|
||||
"target_domain": "Måldomene",
|
||||
"public_comment": "Offentlig kommentar",
|
||||
"timeout2": "Tidsavbrudd for tilkobling til lokal vert",
|
||||
"mailbox_quota_def": "Standard kvote for postkasser",
|
||||
"skipcrossduplicates": "Hopp over duplikatmeldinger på tvers av mapper (førstemann til mølla)",
|
||||
"sieve_desc": "Kort beskrivelse",
|
||||
"mailbox_quota_m": "Maks. kvote pr. postkasse (MiB)",
|
||||
"automap": "Forsøk automatisk mappe-tilordning (\"Sendte elementer\", \"Sendt\" => \"Sendt\" etc.)",
|
||||
"validation_success": "Validering fullført",
|
||||
"relay_all": "Videresend for alle mottakere",
|
||||
"subscribeall": "Abonnér på alle mapper",
|
||||
"domain_matches_hostname": "Domene %s er det samme som vertsnavn",
|
||||
"generate": "opprett",
|
||||
"app_password": "Legg til app-passord",
|
||||
"max_mailboxes": "Maks. antall mulige postkasser",
|
||||
"syncjob_hint": "Vær oppmerksom på at passord må lagres i klartekst!",
|
||||
"relayhost_wrapped_tls_info": "Vennligst <b>ikke</b> bruk TLS-aktiverte porter (stort sett bare brukt på port 465).<br>\nBruk en annen ikke-TLS-aktivert port og send STARTTLS. En TLS-policy for å tvinge TLS kan opprettes i \"TLS-policy-mappinger\".",
|
||||
"relay_unknown_only": "Videresend kun for ikke-eksisterende postkasser. Eksisterende postkasser vil bli levert lokalt.",
|
||||
"sieve_type": "Filtertype",
|
||||
"password_repeat": "Passordbekreftelse (gjenta)",
|
||||
"nexthop": "Neste hopp",
|
||||
"select_domain": "Vennligst velg et domene først",
|
||||
"active": "Aktiv",
|
||||
"target_address_info": "<small>Komplett/e epostadresse/r (kommaseparert).</small>"
|
||||
},
|
||||
"danger": {
|
||||
"dkim_domain_or_sel_exists": "En DKIM-nøkkel for \"%s\" eksisterer og vil ikke bli overskrevet",
|
||||
"from_invalid": "Avsender kan ikke være tom",
|
||||
"bcc_must_be_email": "BCC-destinasjon %s er ikke en gyldig epostadresse",
|
||||
"domain_quota_m_in_use": "Domenekvote må være større enn eller lik %s MiB",
|
||||
"access_denied": "Tilgang nektet eller ugyldige skjemadata",
|
||||
"alias_domain_invalid": "Aliasdomene %s er ugyldig",
|
||||
"alias_empty": "Aliasadresse kan ikke være tom",
|
||||
"alias_goto_identical": "Alias og gå-til-adresse kan ikke være identiske",
|
||||
"alias_invalid": "Aliasadresse %s er ugyldig",
|
||||
"aliasd_targetd_identical": "Aliasdomene kan ikke være likt som måldomene: %s",
|
||||
"aliases_in_use": "Max. aliaser må være større enn eller det samme som %d",
|
||||
"app_name_empty": "App-navn kan ikke være tomt",
|
||||
"app_passwd_id_invalid": "App-passord ID %s ugyldig",
|
||||
"bcc_empty": "BCC-destinasjon kan ikke være tom",
|
||||
"bcc_exists": "En BCC-mapping %s eksisterer for type %s",
|
||||
"comment_too_long": "Kommentar for lang, maks 160 tegn tillatt",
|
||||
"cors_invalid_method": "Ugyldig Allow-Method spesifisert",
|
||||
"cors_invalid_origin": "Ugyldig Allow-Origin angitt",
|
||||
"defquota_empty": "Standardkvote pr. postboks kan ikke være 0.",
|
||||
"demo_mode_enabled": "Demomodus er aktivert",
|
||||
"description_invalid": "Ressursbeskrivelse for %s er ugyldig",
|
||||
"dkim_domain_or_sel_invalid": "DKIM-domene eller selector ugyldig: %s",
|
||||
"domain_cannot_match_hostname": "Domenet kan ikke være det samme som vertsnavnet",
|
||||
"domain_exists": "Domenet %s eksisterer allerede",
|
||||
"domain_invalid": "Domenenavn er tomt eller ugyldig",
|
||||
"domain_not_empty": "Kan ikke fjerne ikke-tomt domene %s",
|
||||
"domain_not_found": "Domene %s ikke funnet",
|
||||
"extended_sender_acl_denied": "manglende ACL for å angi ekstern avsenderadresse",
|
||||
"extra_acl_invalid": "Ekstern avsenderadresse \"%s\" er ugyldig",
|
||||
"extra_acl_invalid_domain": "Ekstern avsenderadresse \"%s\" bruker et ugyldig domene",
|
||||
"fido2_verification_failed": "FIDO2-verifisering feilet: %s",
|
||||
"file_open_error": "Fil kan ikke åpnes for skriving",
|
||||
"filter_type": "Feil filtertype",
|
||||
"global_filter_write_error": "Kunne ikke skrive filterfil: %s",
|
||||
"global_map_invalid": "Global mapping-ID %s ugyldig",
|
||||
"global_map_write_error": "Kunne ikke skrive global mapping-ID %s: %s",
|
||||
"goto_empty": "En aliasadresse må inneholde minst en gyldig gå-til-adresse",
|
||||
"goto_invalid": "Gå-til-adresse %s er ugyldig",
|
||||
"ham_learn_error": "Ham-læringsfeil: %s",
|
||||
"imagick_exception": "Feil: Imagick-avvik under lesing av bilde",
|
||||
"img_invalid": "Kan ikke validere bildefil",
|
||||
"img_tmp_missing": "Kan ikke validere bildefil: Midlertidig fil ikke funnet",
|
||||
"invalid_bcc_map_type": "Ugyldig BCC-mappingtype",
|
||||
"invalid_destination": "Målformat \"%s\" er ugyldig",
|
||||
"invalid_filter_type": "Ugyldig filtertype",
|
||||
"invalid_host": "Ugyldig vert angitt: %s",
|
||||
"invalid_mime_type": "Ugyldig mime-type",
|
||||
"invalid_nexthop": "\"Next hop\"-format er ugyldig",
|
||||
"img_dimensions_exceeded": "Bildet overskriver maksimal bildestørrelse",
|
||||
"img_size_exceeded": "Bildet overskrider maksimal filstørrelse"
|
||||
}
|
||||
}
|
||||
@@ -510,7 +510,7 @@
|
||||
"cname_from_a": "Waarde afgeleid van een A- of AAAA-vermelding.",
|
||||
"dns_records": "DNS-configuratie",
|
||||
"dns_records_24hours": "Houd er rekening mee dat wijzigingen aan DNS tot wel 24 uur in beslag kunnen nemen voordat ze op deze pagina worden weergegeven. Deze informatie is bedoeld om gemakkelijk te bekijken of de DNS-configuratie aan de eisen voldoet.",
|
||||
"dns_records_docs": "Raadpleeg ook <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">de documentatie</a>.",
|
||||
"dns_records_docs": "Raadpleeg ook <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">de documentatie</a>.",
|
||||
"dns_records_data": "Correcte gegevens",
|
||||
"dns_records_name": "Naam",
|
||||
"dns_records_status": "Huidige staat",
|
||||
|
||||
1309
data/web/lang/lang.pt-br.json
Normal file
1309
data/web/lang/lang.pt-br.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -504,7 +504,7 @@
|
||||
"cname_from_a": "Valoare derivată din înregistrarea A/AAAA. Acest lucru este acceptat atâta timp cât înregistrarea indică resursele corecte.",
|
||||
"dns_records": "Înregistrări DNS",
|
||||
"dns_records_24hours": "Rețineți că modificările aduse DNS-ului pot dura până la 24 de ore pentru a reflecta corect starea lor curentă pe această pagină. Acest mecanism este conceput ca o modalitate să vezi ușor cum să îți configurezi înregistrările DNS și să verifici dacă toate înregistrările sunt stocate corect în DNS.",
|
||||
"dns_records_docs": "Vă rugăm să consultați și <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">documentația</a>.",
|
||||
"dns_records_docs": "Vă rugăm să consultați și <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">documentația</a>.",
|
||||
"dns_records_data": "Date corecte",
|
||||
"dns_records_name": "Nume",
|
||||
"dns_records_status": "Stare curentă",
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
"validate": "Проверить",
|
||||
"validation_success": "Проверка прошла успешно",
|
||||
"tags": "Теги",
|
||||
"app_passwd_protocols": "Разрешенные протоколы для пароля приложения"
|
||||
"app_passwd_protocols": "Разрешенные протоколы для пароля приложения",
|
||||
"dry": "Имитировать синхронизацию"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Настройки доступа",
|
||||
@@ -344,7 +345,10 @@
|
||||
"allowed_methods": "Access-Control-Allow-Methods",
|
||||
"ip_check": "Проверить IP",
|
||||
"ip_check_disabled": "Проверка IP отключена. Вы можете включить его в разделе <br> <strong>Система > Конфигурация > Параметры > Настроить</strong>.",
|
||||
"ip_check_opt_in": "Согласие на использование сторонних служб <strong>ipv4.mailcow.email</strong> и <strong>ipv6.mailcow.email</strong> для разрешения внешних IP-адресов."
|
||||
"ip_check_opt_in": "Согласие на использование сторонних служб <strong>ipv4.mailcow.email</strong> и <strong>ipv6.mailcow.email</strong> для разрешения внешних IP-адресов.",
|
||||
"f2b_manage_external": "Внешнее управление Fail2Ban",
|
||||
"f2b_manage_external_info": "Fail2ban по-прежнему будет вести банлист, но не будет активно устанавливать правила для блокировки трафика. Используйте сгенерированный ниже банлист для внешнего блокирования трафика.",
|
||||
"copy_to_clipboard": "Текст скопирован в буфер обмена!"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Доступ запрещён, или указаны неверные данные",
|
||||
@@ -500,7 +504,7 @@
|
||||
"dns_records": "Записи DNS",
|
||||
"dns_records_24hours": "Обратите внимание, что для внесения изменений в DNS может потребоваться до 24 часов, чтобы правильно отобразить их текущее состояние на этой странице. Эта страница предназначен для того, чтобы вы могли легко увидеть, как настроить записи DNS и проверить, все ли записи правильно занесены в DNS.",
|
||||
"dns_records_data": "Значение",
|
||||
"dns_records_docs": "Пожалуйста, ознакомьтесь с <a target=\"_blank\" href=\"https://docs.mailcow.email/prerequisite/prerequisite-dns/\">документацией</a>.",
|
||||
"dns_records_docs": "Пожалуйста, ознакомьтесь с <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">документацией</a>.",
|
||||
"dns_records_name": "Название",
|
||||
"dns_records_status": "Статус",
|
||||
"dns_records_type": "Тип",
|
||||
@@ -542,6 +546,7 @@
|
||||
"extended_sender_acl_info": "Для внешних доменов должен быть импортирован или сгенерирован доменный ключ DKIM с соответствующей записью TXT в домене, если внешний домен использует DMARC.<br>\r\n Не забудьте добавить этот сервер к соответствующей записи SPF TXT внешнего домена.<br>\r\n Добавление домена из списка внешних адресов в mailcow автоматически удалит соответствующие записи из внешних адресов пользователей.<br>\r\n Чтобы разрешить пользователю отправку от имени *@domain.tld, укажите @domain.tld.",
|
||||
"force_pw_update": "Требовать смены пароля при следующем входе в систему",
|
||||
"force_pw_update_info": "Пользователь должен будет войти в %s и сменить свой пароль. mailcow OAuth2, SOGo, EAS, IMAP/POP3 и SMTP будут не доступны до смены пароля.",
|
||||
"footer_exclude": "Исключить из нижнего колонтитула",
|
||||
"full_name": "Полное имя",
|
||||
"gal": "GAL - Глобальная адресная книга",
|
||||
"gal_info": "GAL содержит все объекты домена и не подлежит редактированию. Информация о занятости в SOGo будет отсутствовать для домена, если данная функция будет отключена! <b>Требуется перезапустить SOGo, чтобы применить изменения.</b>",
|
||||
@@ -625,11 +630,13 @@
|
||||
"auth_user": "{= auth_user =} - Аутентифицированное имя пользователя, указанное MTA",
|
||||
"from_user": "{= from_user =} - Из пользовательской части envelope, например, для \"moo@mailcow.tld\" возвращается \"moo\"",
|
||||
"from_addr": "{= from_addr =} - Из адресной части envelope",
|
||||
"from_domain": "{= from_domain =} - из доменной части envelope"
|
||||
"from_domain": "{= from_domain =} - из доменной части envelope",
|
||||
"custom": "{= foo =} - Если почтовый ящик имеет пользовательский атрибут \"foo\" со значением \"bar\", он возвращает \"bar\"."
|
||||
},
|
||||
"domain_footer": "Нижний колонтитул домена",
|
||||
"domain_footer_html": "HTML нижний колонтитул",
|
||||
"domain_footer_plain": "ПРОСТОЙ нижний колонтитул"
|
||||
"domain_footer_plain": "ПРОСТОЙ нижний колонтитул",
|
||||
"custom_attributes": "Пользовательские атрибуты"
|
||||
},
|
||||
"fido2": {
|
||||
"confirm": "Подтвердить",
|
||||
@@ -1011,7 +1018,8 @@
|
||||
"verified_webauthn_login": "Авторизация WebAuthn пройдена",
|
||||
"verified_yotp_login": "Авторизация Yubico OTP пройдена",
|
||||
"cors_headers_edited": "Настройки CORS сохранены",
|
||||
"domain_footer_modified": "Изменения в нижнем колонтитуле домена %s сохранены"
|
||||
"domain_footer_modified": "Изменения в нижнем колонтитуле домена %s сохранены",
|
||||
"f2b_banlist_refreshed": "Идентификатор банлиста был успешно обновлен."
|
||||
},
|
||||
"tfa": {
|
||||
"api_register": "%s использует Yubico Cloud API. Пожалуйста, получите ключ API для вашего ключа <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">здесь</a>",
|
||||
@@ -1198,7 +1206,9 @@
|
||||
"apple_connection_profile_with_app_password": "Новый пароль приложения генерируется и добавляется в профиль, поэтому при настройке устройства не требуется вводить пароль. Не предоставляйте доступ к файлу, поскольку он предоставляет полный доступ к вашему почтовому ящику.",
|
||||
"direct_protocol_access": "Этот пользователь почтового ящика имеет <b>прямой, внешний доступ</b> к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.<br> Кнопка \"Вход в веб-почту\" обеспечивает единый вход в SOGo и всегда доступна.",
|
||||
"with_app_password": "с паролем приложения",
|
||||
"change_password_hint_app_passwords": "В вашей учетной записи есть {{number_of_app_passwords}} паролей приложений, которые не будут изменены. Чтобы управлять ими, перейдите на вкладку \"Пароли приложений\"."
|
||||
"change_password_hint_app_passwords": "В вашей учетной записи есть {{number_of_app_passwords}} паролей приложений, которые не будут изменены. Чтобы управлять ими, перейдите на вкладку \"Пароли приложений\".",
|
||||
"attribute": "Атрибут",
|
||||
"value": "Значение"
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Вы не можете удалить сами себя",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user