Compare commits

...

54 Commits

Author SHA1 Message Date
Patrick Schult
cb0b0235f0 Merge pull request #5623 from mailcow/staging
🛷 🐄 Moocember 2023 Update Revision A | Postfix CVE-2023-51764 Security Update
2023-12-29 20:35:20 +01:00
FreddleSpl0it
6ff6f7a28d [Postfix] set smtpd_forbid_bare_newline = yes 2023-12-29 20:19:26 +01:00
milkmaker
0b628fb22d Translations update from Weblate (#5622)
* [Web] Updated lang.zh-tw.json

Co-authored-by: BallBill <xxx@billtang.ddns.net>

* [Web] Updated lang.pt-br.json

Co-authored-by: Abner Santana <abnerss@outlook.com>

---------

Co-authored-by: BallBill <xxx@billtang.ddns.net>
Co-authored-by: Abner Santana <abnerss@outlook.com>
2023-12-29 19:22:19 +01:00
Patrick Schult
acf9d5480c Merge pull request #5504 from FELDSAM-INC/feldsam/do-not-remove-x-mailer
[Postfix] Do not remove X-Mailer header
2023-12-27 18:40:19 +01:00
milkmaker
a1cb7fd778 [Web] Updated lang.zh-tw.json (#5617)
Co-authored-by: BallBill <xxx@billtang.ddns.net>
2023-12-27 18:03:24 +01:00
Kristian Feldsam
100e8ab00d [Postfix] Do not remove X-Mailer header
some providers, like seznam.cz use X-Mailer in DKIM signatures

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-12-27 16:32:50 +01:00
renovate[bot]
7bd27b920a chore(deps): update dependency nextcloud/server to v28.0.1 (#5614)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-24 18:24:01 +01:00
Patrick Schult
121f0120f0 Merge pull request #5604 from mailcow/staging
🛷 🐄 Moocember 2023 Update | Netfilter NFTables Support and Banlist Endpoint
2023-12-19 10:59:37 +01:00
Niklas Meyer
515b85bb2f Merge pull request #5603 from mailcow/renovate/alpine-3.x
chore(deps): update alpine docker tag to v3.19
2023-12-19 10:06:21 +01:00
renovate[bot]
f27e41d19c chore(deps): update alpine docker tag to v3.19
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-12-19 08:48:40 +00:00
Niklas Meyer
603d451fc9 Merge pull request #5602 from mailcow/feat/bug-reporting-changes
Guideline Improvement + Issue Template adjusting
2023-12-19 09:48:21 +01:00
DerLinkman
89adaabb64 contributing.md: Updated guidelines 2023-12-19 09:47:12 +01:00
DerLinkman
987ca68ca6 issue_templates: corrected links + added premium support link 2023-12-18 16:02:59 +01:00
FreddleSpl0it
71defbf2f9 escapeHtml in qhandler.js 2023-12-18 14:02:05 +01:00
FreddleSpl0it
5c35b42844 Update Netfilter and Watchdog Image 2023-12-18 11:53:30 +01:00
milkmaker
904b37c4be [Web] Updated lang.pt-br.json (#5598)
Co-authored-by: Abner Santana <abnerss@outlook.com>
2023-12-16 19:23:27 +01:00
milkmaker
4e252f8243 [Web] Updated lang.pt-br.json (#5591)
Co-authored-by: Abner Santana <abnerss@outlook.com>
2023-12-13 17:50:13 +01:00
Niklas Meyer
dc3e52a900 Merge pull request #5589 from mailcow/renovate/nextcloud-server-28.x
Update dependency nextcloud/server to v28
2023-12-13 10:56:05 +01:00
milkmaker
06ad5f6652 Translations update from Weblate (#5590)
* [Web] Updated lang.ru-ru.json

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>

* [Web] Updated lang.uk-ua.json

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>

---------

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
2023-12-12 17:49:29 +01:00
renovate[bot]
c3b5474cbf Update dependency nextcloud/server to v28
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-12-12 13:30:18 +00:00
Patrick Schult
69e3b830ed Merge pull request #5453 from smarsching/watchdog-no-notify-on-startup
Allow suppressing watchdog start notification
2023-12-12 11:16:37 +01:00
Patrick Schult
96a5891ce7 Merge branch 'staging' into watchdog-no-notify-on-startup 2023-12-12 11:14:29 +01:00
FreddleSpl0it
66b9245b28 fix WATCHDOG_NOTIFY_WEBHOOK env vars 2023-12-12 11:10:10 +01:00
DerLinkman
f38ec68695 [SOGo] Update to 5.9.1 2023-12-12 11:00:16 +01:00
Patrick Schult
996772a27d Merge pull request #4968 from felixoi/staging
Watchdog: Allow sending notifications via webhooks
2023-12-11 16:29:52 +01:00
Patrick Schult
7f4e9c1ad4 Merge branch 'staging' into staging 2023-12-11 16:28:05 +01:00
FreddleSpl0it
218ba69501 [Watchdog] add curl verbose & use | as sed delimiter 2023-12-11 15:44:11 +01:00
Patrick Schult
c2e5dfd933 Merge pull request #5313 from mailcow/feat/f2b-banlist
[Web] add f2b_banlist endpoint
2023-12-11 12:36:06 +01:00
FreddleSpl0it
3e40bbc603 Merge remote-tracking branch 'origin/staging' into feat/f2b-banlist 2023-12-11 12:27:14 +01:00
Patrick Schult
3498d4b9c5 Merge pull request #5585 from mailcow/feat/nftables
[Netfilter] add nftables support
2023-12-11 11:54:01 +01:00
FreddleSpl0it
f4b838cad8 [Netfilter] update image & delete old server.py 2023-12-11 11:51:28 +01:00
FreddleSpl0it
86fa8634ee [Netfilter] do not ignore RETRY_WINDOW 2023-12-11 11:38:48 +01:00
milkmaker
8882006700 Translations update from Weblate (#5583)
* [Web] Updated lang.cs-cz.json

Co-authored-by: Kristian Feldsam <feldsam@gmail.com>

* [Web] Updated lang.de-de.json

Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.sk-sk.json

Co-authored-by: Kristian Feldsam <feldsam@gmail.com>

* [Web] Updated lang.pt-br.json

[Web] Updated lang.pt-br.json

Co-authored-by: Abner Santana <abnerss@outlook.com>
Co-authored-by: xmacaba <lixo@macaba.com.br>

---------

Co-authored-by: Kristian Feldsam <feldsam@gmail.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Abner Santana <abnerss@outlook.com>
Co-authored-by: xmacaba <lixo@macaba.com.br>
2023-12-10 18:07:28 +01:00
renovate[bot]
0257736c64 Update actions/stale action to v9 (#5579)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-07 15:57:53 +01:00
FreddleSpl0it
340980bdd0 [Netfilter] set image back to mailcow/netfilter:1.52 2023-11-27 17:32:41 +01:00
FreddleSpl0it
f39005b72d [Netfilter] add nftables support 2023-10-30 11:54:14 +01:00
Sebastian Marsching
5425cca47e Allow suppressing watchdog start notification.
The default behavior is still the old one (send a notifcation when the
watchdog is started), but this notification can now be suppressed by
setting WATCHDOG_NOTIFY_START=n.
2023-10-12 18:34:55 +02:00
FreddleSpl0it
db2759b7d1 [Web] fix wrong content type + add more http 500 responses 2023-07-12 16:46:32 +02:00
DerLinkman
3c3b9575a2 [Netfilter] Update Compose File to 1.53 2023-07-12 09:42:17 +02:00
FreddleSpl0it
987cfd5dae [Web] f2b banlist - add http status codes 2023-07-11 10:31:25 +02:00
FreddleSpl0it
1537fb39c0 [Web] add manage f2b external option 2023-07-11 10:19:32 +02:00
FreddleSpl0it
65cbc478b8 [Web] add manage f2b external option 2023-07-11 10:13:00 +02:00
FreddleSpl0it
e2e8fbe313 [Web] add f2b_banlist endpoint 2023-07-10 13:54:23 +02:00
Felix Kleinekathöfer
a3c5f785e9 Added new env vars to docker compose 2023-02-20 22:34:53 +01:00
Felix Kleinekathöfer
7877215d59 mailcow should be lowercase 2023-01-08 20:02:46 +01:00
Felix Kleinekathöfer
e4347792b8 mailcow should be llow 2023-01-08 20:02:18 +01:00
Felix Kleinekathöfer
50fde60899 Added webhook variables to update script 2023-01-07 16:29:43 +01:00
Felix Kleinekathöfer
38f5e293b0 Webhook variables in config generation 2023-01-07 16:21:11 +01:00
Felix Kleinekathöfer
b6b399a590 Fixed POST to webhook 2023-01-07 16:00:17 +01:00
Felix Kleinekathöfer
b83841d253 Replace placeholders with sed 2023-01-07 15:44:29 +01:00
Felix Kleinekathöfer
3e69304f0f Send webhook 2023-01-06 16:25:18 +01:00
Felix Kleinekathöfer
fe8131f743 Only sent mail if enabled 2023-01-06 15:52:36 +01:00
Felix Kleinekathöfer
9ef14a20d1 Centralized checking of enabled notifications 2023-01-06 15:43:43 +01:00
Felix Kleinekathöfer
5897b97065 Renamed mail notification method for watchdog to be more general 2023-01-06 15:35:06 +01:00
34 changed files with 1467 additions and 399 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -1,6 +1,8 @@
FROM alpine:3.17
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
WORKDIR /app
ENV XTABLES_LIBDIR /usr/lib/xtables
ENV PYTHON_IPTABLES_XTABLES_VERSION 12
ENV IPTABLES_LIBDIR /usr/lib
@@ -14,10 +16,13 @@ RUN apk add --virtual .build-deps \
iptables \
ip6tables \
xtables-addons \
nftables \
tzdata \
py3-pip \
py3-nftables \
musl-dev \
&& pip3 install --ignore-installed --upgrade pip \
jsonschema \
python-iptables \
redis \
ipaddress \
@@ -26,5 +31,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"]

View 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

View File

@@ -13,10 +13,15 @@ from threading import Thread
from threading import Lock
import redis
import json
import iptc
import dns.resolver
import dns.exception
import uuid
from modules.Logger import Logger
from modules.IPTables import IPTables
from modules.NFTables import NFTables
# connect to redis
while True:
try:
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
@@ -31,34 +36,33 @@ while True:
time.sleep(3)
else:
break
pubsub = r.pubsub()
# rename fail2ban to netfilter
if r.exists('F2B_LOG'):
r.rename('F2B_LOG', 'NETFILTER_LOG')
# globals
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)
# init Logger
logger = Logger(r)
# init backend
backend = sys.argv[1]
if backend == "nftables":
logger.logInfo('Using NFTables backend')
tables = NFTables("MAILCOW", logger)
else:
logger.logInfo('Using IPTables backend')
tables = IPTables("MAILCOW", logger)
def logCrit(message):
log('crit', message)
def logInfo(message):
log('info', message)
def refreshF2boptions():
global f2boptions
@@ -79,7 +83,7 @@ def refreshF2boptions():
try:
f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError:
print('Error loading F2B options: F2B_OPTIONS is not json')
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
quit_now = True
exit_code = 2
@@ -94,6 +98,8 @@ def verifyF2boptions(f2boptions):
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
@@ -120,43 +126,23 @@ def refreshF2bregex():
f2bregex = {}
f2bregex = json.loads(r.get('F2B_REGEX'))
except ValueError:
print('Error loading F2B options: F2B_REGEX is not json')
logger.logCrit('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 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()
BAN_TIME = int(f2boptions['ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
@@ -165,23 +151,18 @@ def ban(address):
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
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):
logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
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)
@@ -190,60 +171,44 @@ def ban(address):
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'] = time.time()
bans[net]['last_attempt'] = current_attempt
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:
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:
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:
tables.banIPv4(net)
elif int(f2boptions['manage_external']) != 1:
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)
tables.banIPv6(net)
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))
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:
logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
logger.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)
logger.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)
tables.unbanIPv4(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 in chain.rules:
chain.delete_rule(rule)
tables.unbanIPv6(net)
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans:
@@ -251,74 +216,46 @@ def unban(net):
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:
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)
if unban:
is_unbanned = tables.unbanIPv4(net)
elif int(f2boptions['manage_external']) != 1:
is_banned = tables.banIPv4(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)
if unban:
is_unbanned = tables.unbanIPv6(net)
elif int(f2boptions['manage_external']) != 1:
is_banned = tables.banIPv6(net)
def quit(signum, frame):
global quit_now
quit_now = True
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
logInfo('Clearing all bans')
logger.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
tables.clearIPv4Table()
tables.clearIPv6Table()
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
pubsub.unsubscribe()
def watch():
logInfo('Watching Redis channel F2B_CHANNEL')
logger.logInfo('Watching Redis channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL')
global quit_now
@@ -339,10 +276,10 @@ def watch():
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']))
logger.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)
logger.logWarn('Error reading log line from pubsub: %s' % ex)
quit_now = True
exit_code = 2
@@ -350,87 +287,19 @@ 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...')
tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24')
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...')
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
def autopurge():
while not quit_now:
@@ -451,6 +320,17 @@ def autopurge():
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_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 isIpNetwork(address):
try:
ipaddress.ip_network(address, False)
@@ -458,7 +338,6 @@ def isIpNetwork(address):
return False
return True
def genNetworkList(list):
resolver = dns.resolver.Resolver()
hostnames = []
@@ -474,12 +353,12 @@ def genNetworkList(list):
try:
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
except dns.exception.Timeout:
logInfo('Hostname %s timedout on resolve' % hostname)
logger.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)
logger.logInfo('%s' % dnsexception)
continue
for rdata in answer:
hostname_ips.append(rdata.to_text())
@@ -499,7 +378,7 @@ def whitelistUpdate():
with lock:
if Counter(new_whitelist) != Counter(WHITELIST):
WHITELIST = new_whitelist
logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def blacklistUpdate():
@@ -515,7 +394,7 @@ def blacklistUpdate():
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))
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
if addban:
for net in addban:
permBan(net=net)
@@ -524,40 +403,20 @@ def blacklistUpdate():
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)
def quit(signum, frame):
global quit_now
quit_now = True
if __name__ == '__main__':
refreshF2boptions()
# In case a previous session was killed without cleanup
clear()
# Reinit MAILCOW chain
initChain()
# Is called before threads start, no locking
logger.logInfo("Initializing mailcow netfilter chain")
tables.initChainIPv4()
tables.initChainIPv6()
watch_thread = Thread(target=watch)
watch_thread.daemon = True

View File

@@ -0,0 +1,213 @@
import iptc
import time
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

View File

@@ -0,0 +1,23 @@
import time
import json
class Logger:
def __init__(self, redis):
self.r = redis
def log(self, priority, message):
tolog = {}
tolog['time'] = int(round(time.time()))
tolog['priority'] = priority
tolog['message'] = message
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
print(message)
def logWarn(self, message):
self.log('warn', message)
def logCrit(self, message):
self.log('crit', message)
def logInfo(self, message):
self.log('info', message)

View File

@@ -0,0 +1,495 @@
import nftables
import ipaddress
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:
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 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)
target_net = ipaddress.ip_network(snat_target)
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))
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len))
current_target_net = ipaddress.ip_network(target_ip)
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):
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"] == "mailcow":
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)
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"]
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)
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

View File

@@ -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|")
# 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
@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'])
];

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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();
}

View File

@@ -503,6 +503,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'])) {
@@ -1324,6 +1334,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);
@@ -1943,7 +1957,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));

View File

@@ -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",
@@ -218,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",
@@ -343,7 +344,9 @@
"verify": "Ověřit",
"yes": "&#10003;",
"f2b_ban_time_increment": "Délka banu je prodlužována s každým dalším banem",
"f2b_max_ban_time": "Maximální délka banu (s)"
"f2b_max_ban_time": "Maximální délka banu (s)",
"cors_settings": "Nastavení CORS",
"queue_unban": "zrušit ban"
},
"danger": {
"access_denied": "Přístup odepřen nebo jsou neplatná data ve formuláři",
@@ -465,7 +468,14 @@
"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",
@@ -488,7 +498,9 @@
"processing": "Zpracovávání...",
"search": "Vyhledávání:",
"decimal": ",",
"thousands": " "
"thousands": " ",
"collapse_all": "Sbalit vše",
"expand_all": "Rozbalit vše"
},
"debug": {
"chart_this_server": "Graf (tento server)",
@@ -515,7 +527,20 @@
"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.",
@@ -640,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 &lt;moo@mailcow.tld&gt;\" 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",
@@ -870,7 +907,8 @@
"username": "Uživatelské jméno",
"waiting": "Čekání",
"weekly": "Každý týden",
"yes": "&#10003;"
"yes": "&#10003;",
"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.",
@@ -935,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",
@@ -1029,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>",
@@ -1215,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",

View File

@@ -148,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",
@@ -181,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)",
@@ -346,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",
@@ -675,7 +680,11 @@
"unchanged_if_empty": "Unverändert, wenn leer",
"username": "Benutzername",
"validate_save": "Validieren und speichern",
"pushover_sound": "Ton"
"pushover_sound": "Ton",
"domain_footer_info_vars": {
"auth_user": "{= auth_user =} - Angemeldeter Benutzername vom MTA",
"from_user": "{= from_user =} - Von Teil des Benutzers z.B. \"moo@mailcow.tld\" wird \"moo\" zurückgeben."
}
},
"fido2": {
"confirm": "Bestätigen",
@@ -1029,6 +1038,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",

View File

@@ -154,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",
@@ -187,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)",
@@ -1046,6 +1049,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",

View File

@@ -9,8 +9,8 @@
"eas_reset": "Redefinir dispositivos EAS",
"extend_sender_acl": "Permitir estender a ACL do remetente por endereços externos",
"filters": "Filtros",
"login_as": "Faça login como usuário da caixa de correio",
"mailbox_relayhost": "Alterar relayhost para uma caixa de correio",
"login_as": "Faça login como usuário da mailbox",
"mailbox_relayhost": "Alterar relayhost para uma mailbox",
"prohibited": "Proibido pela ACL",
"protocol_access": "Alterar o acesso ao protocolo",
"pushover": "Pushover",
@@ -28,7 +28,7 @@
"spam_score": "Pontuação de spam",
"syncjobs": "Trabalhos de sincronização",
"tls_policy": "Política de TLS",
"unlimited_quota": "Cota ilimitada para caixas de correio"
"unlimited_quota": "Cota ilimitada para mailbox"
},
"add": {
"activate_filter_warn": "Todos os outros filtros serão desativados quando a opção ativa estiver marcada.",
@@ -37,7 +37,7 @@
"add_domain_only": "Adicionar somente domínio",
"add_domain_restart": "Adicionar domínio e reiniciar o SoGo",
"alias_address": "Endereço (s) de alias",
"alias_address_info": "<small>Endereço de e-mail completo/es ou @example .com, para capturar todas as mensagens de um domínio (separadas por vírgula). somente <b>domínios mailcow</b></small>.",
"alias_address_info": "<small>Endereço/s de e-mail completo ou @example .com, para capturar todas as mensagens de um domínio (separadas por vírgula). <b> somente domínios mailcow</b>.</small>",
"alias_domain": "Domínio de alias",
"alias_domain_info": "<small>Somente nomes de domínio válidos (separados por vírgula).</small>",
"app_name": "Nome do aplicativo",
@@ -70,11 +70,11 @@
"hostname": "Anfitrião",
"inactive": "Inativo",
"kind": "Gentil",
"mailbox_quota_def": "Cota de caixa de correio padrão",
"mailbox_quota_m": "Cota máxima por caixa de correio (MiB)",
"mailbox_quota_def": "Cota de caixa de mailbox",
"mailbox_quota_m": "Cota máxima por mailbox (MiB)",
"mailbox_username": "Nome de usuário (parte esquerda de um endereço de e-mail)",
"max_aliases": "Máximo de aliases possíveis",
"max_mailboxes": "Número máximo de caixas de correio possíveis",
"max_mailboxes": "Número máximo de mailboxes possíveis",
"mins_interval": "Intervalo de votação (minutos)",
"multiple_bookings": "Várias reservas",
"nexthop": "Próximo salto",
@@ -86,10 +86,10 @@
"public_comment": "Comentário público",
"quota_mb": "Cota (MiB)",
"relay_all": "Retransmita todos os destinatários",
"relay_all_info": "↪ Se você optar por <b>não</b> retransmitir todos os destinatários, precisará adicionar uma caixa de correio (“cega”) para cada destinatário que deve ser retransmitido.",
"relay_all_info": "↪ Se você optar por <b>não</b> retransmitir todos os destinatários, precisará adicionar uma mailbox (“cega”) para cada destinatário que deve ser retransmitido.",
"relay_domain": "Retransmitir este domínio",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Informações</div> Você pode definir mapas de transporte para um destino personalizado para esse domínio. Se não for definido, uma pesquisa MX será feita.",
"relay_unknown_only": "Retransmita somente caixas de correio não existentes. As caixas de correio existentes serão entregues localmente.",
"relay_unknown_only": "Retransmita somente mailboxes não existentes. As mailboxes existentes serão entregues localmente.",
"relayhost_wrapped_tls_info": "Por favor, <b>não</b> use portas com cobertura TLS (usadas principalmente na porta 465). <br>\r\nUse qualquer porta não encapsulada e emita STARTTLS. Uma política de TLS para impor o TLS pode ser criada em “mapas de políticas de TLS”.",
"select": "Selecione...",
"select_domain": "Selecione primeiro um domínio",
@@ -212,14 +212,14 @@
"in_use_by": "Em uso por",
"inactive": "Inativo",
"include_exclude": "Incluir/Excluir",
"include_exclude_info": "Por padrão - sem seleção - <b>todas as caixas de correio são endereçadas</b>",
"include_exclude_info": "Por padrão - sem seleção - <b>todas as mailboxes são endereçadas</b>",
"includes": "Inclua esses destinatários",
"ip_check": "Verificação de IP",
"ip_check_disabled": "A verificação de IP está desativada. Você pode ativá-lo em <br><strong>Sistema > Configuração > Opções > Personalizar</strong>",
"ip_check_opt_in": "<strong>Opte por usar o serviço de terceiros <strong>ipv4.mailcow.email e ipv6.mailcow.email</strong> para resolver endereços IP externos.</strong>",
"ip_check_opt_in": "Opte por usar o serviço de terceiros <strong>ipv4.mailcow.email.</strong> e <strong>ipv6.mailcow.email</strong> para resolver endereços IP externos.",
"is_mx_based": "Baseado em MX",
"last_applied": "Aplicado pela última vez",
"license_info": "Uma licença não é necessária, mas ajuda no desenvolvimento futuro. <br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Registre seu GUID aqui</a> ou <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">compre suporte para a instalação do mailcow</a>.",
"license_info": "Uma licença não é necessária, mas ajuda no desenvolvimento.<br><a href=\"https://www.servercow.de/mailcow? Lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Registre seu GUID aqui</a> ou <a href=\"https://www.servercow.de/mailcow? Lang=en#support\" target=\"_blank\" alt=\"Support order\">comprar suporte para sua instalação de mailcow.</a>",
"link": "Link",
"loading": "Por favor, espere...",
"login_time": "Hora do login",
@@ -238,7 +238,7 @@
"oauth2_add_client": "Adicionar cliente OAuth2",
"oauth2_client_id": "ID do cliente",
"oauth2_client_secret": "Segredo do cliente",
"oauth2_info": "A implementação do OAuth2 suporta o tipo de concessão Código de Autorização e emite tokens de atualização. <br>\r\nO servidor também emite automaticamente novos tokens de atualização, após o uso de um token de atualização. <br><br>\r\n• O escopo padrão é <i>perfil</i>. Somente usuários de caixas de correio podem ser autenticados no OAuth2. <i>Se o parâmetro do escopo for omitido, ele retornará ao perfil.</i> <br>\r\n• O parâmetro <i>state</i> deve ser enviado pelo cliente como parte da solicitação de autorização. <br><br>\r\nCaminhos para solicitações para a API OAuth2: <br>\r\n<ul>\r\n <li><code>Ponto final de autorização: /oauth/authorize</code></li>\r\n <li><code>Ponto final do token: /oauth/token</code></li>\r\n <li>Página de recursos: <code>/oauth/profile</code></li></ul>\r\nA regeneração do segredo do cliente não expirará os códigos de autorização existentes, mas eles falharão na renovação do token. <br><br>\r\nA revogação dos tokens do cliente causará o encerramento imediato de todas as sessões ativas. Todos os clientes precisam se autenticar novamente.",
"oauth2_info": "A implementação OAuth2 suporta o tipo de concessão \"Código de Autorização\" e emite tokens de atualização.<br>\nO servidor também emite automaticamente novos tokens de atualização, depois que um token de atualização foi usado.<br><br>\n&#8226; O escopo padrão é <i>perfil</i>. Somente usuários com caixa de e-mail podem ser autenticados contra o OAuth2. Se o parâmetro de escopo for omitido, ele voltará para <i>perfil</i>.<br>\nCaminhos para solicitações OAuth2 API: <br>\n<ul>\n<li>Endpoint de autorização: <code>/oauth/authorize</code></li>\n<li>Endpoint token: <code>/oauth/token</code></li>\n<li>Página de recursos: <code>/oauth/profile</code></li>\n</ul>\nRegenerar o segredo do cliente não expirará os códigos de autorização existentes, mas eles não renovarão seu token.<br><br>\nA revogação dos tokens do cliente causará o término imediato de todas as sessões ativas. Todos os clientes precisam se autenticar novamente.",
"oauth2_redirect_uri": "URI de redirecionamento",
"oauth2_renew_secret": "Gere um novo segredo de cliente",
"oauth2_revoke_tokens": "Revogar todos os tokens do cliente",
@@ -256,25 +256,25 @@
"priority": "Prioridade",
"private_key": "Chave privada",
"quarantine": "Quarentena",
"quarantine_bcc": "Envie uma cópia de todas as notificações (BCC) para esse destinatário: <br><small>deixe em branco para desativar. <b>Correio não assinado e não verificado. Deve ser entregue somente internamente</b></small>.",
"quarantine_bcc": "Envie uma cópia de todas as notificações (BCC) para este destinatário:<br><small>Deixe em branco para desativar. <b>E-mail não assinado e não verificado. Deve ser entregue apenas internamente.</b></small>",
"quarantine_exclude_domains": "Excluir domínios e domínios de alias",
"quarantine_max_age": "Idade máxima em dias <br><small>O valor deve ser igual ou superior a 1 dia.</small>",
"quarantine_max_score": "Descarte a notificação se a pontuação de spam de um e-mail for maior que esse valor: O <br><small>padrão</small> é 9999,0",
"quarantine_max_size": "Tamanho máximo em MiB (elementos maiores são descartados): <br><small>0 <b>não</b> indica ilimitado</small>.",
"quarantine_max_size": "Tamanho máximo em MiB (elementos maiores são descartados): <br><small>0 <b>não</b> indica ilimitado.</small>",
"quarantine_notification_html": "Modelo de e-mail de notificação: <br><small>deixe em branco para restaurar o modelo padrão.</small>",
"quarantine_notification_sender": "Remetente do e-mail de notificação",
"quarantine_notification_subject": "Assunto do e-mail de notificação",
"quarantine_redirect": "<b>Redirecione todas as notificações</b> para esse destinatário: <br><small>deixe em branco para desativar. <b>Correio não assinado e não verificado. Deve ser entregue somente internamente</b></small>.",
"quarantine_redirect": "<b>Redirecione todas as notificações</b> para esse destinatário: <br><small>deixe em branco para desativar. <b>E-mail não assinado e não verificado. Deve ser entregue somente internamente.</b></small>",
"quarantine_release_format": "Formato dos itens lançados",
"quarantine_release_format_att": "Como anexo",
"quarantine_release_format_raw": "Original não modificado",
"quarantine_retention_size": "<b>Retenções por caixa de correio: <small>0</small> indica inativo.</b> <br>",
"quarantine_retention_size": "Retenções por mailbox: <br><small>0 indica <b>inativo</b>.</small>",
"quota_notification_html": "Modelo de e-mail de notificação: <br><small>deixe em branco para restaurar o modelo padrão.</small>",
"quota_notification_sender": "Remetente do e-mail de notificação",
"quota_notification_subject": "Assunto do e-mail de notificação",
"quota_notifications": "Notificações de cotas",
"quota_notifications_info": "As notificações de cota são enviadas aos usuários uma vez ao ultrapassar 80% e uma vez ao ultrapassar 95% de uso.",
"quota_notifications_vars": "{{percent}} é igual à cota atual do usuário <br>{{username}} é o nome da caixa de correio",
"quota_notifications_vars": "{{percent}} é igual à cota atual do usuário <br>{{username}} é o nome da mailbox",
"queue_unban": "não banido",
"r_active": "Restrições ativas",
"r_inactive": "Restrições inativas",
@@ -302,7 +302,7 @@
"rsettings_insert_preset": "Inserir exemplo de predefinição “%s”",
"rsettings_preset_1": "Desative tudo, exceto o DKIM e o limite de taxa para usuários autenticados",
"rsettings_preset_2": "Postmasters querem spam",
"rsettings_preset_3": "Permitir somente remetentes específicos para uma caixa de correio (ou seja, uso somente como caixa de correio interna)",
"rsettings_preset_3": "Permitir somente remetentes específicos para uma mailbox (ou seja, uso somente como mailbox interna)",
"rsettings_preset_4": "Desativar Rspamd para um domínio",
"rspamd_com_settings": "Um nome de configuração será gerado automaticamente, veja os exemplos de predefinições abaixo. Para obter mais detalhes, consulte a documentação <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">do Rspamd</a>",
"rspamd_global_filters": "Mapas de filtro globais",
@@ -348,7 +348,10 @@
"username": "Nome de usuário",
"validate_license_now": "Valide o GUID em relação ao servidor de licenças",
"verify": "Verificar",
"yes": "✓"
"yes": "✓",
"copy_to_clipboard": "Texto copiado para a área de transferência!",
"f2b_manage_external": "Gerenciar Fail2Ban externamente",
"f2b_manage_external_info": "O Fail2ban ainda manterá a lista de banimentos, mas não definirá ativamente regras para bloquear o tráfego. Use a lista de banimento gerada abaixo para bloquear externamente o tráfego."
},
"danger": {
"access_denied": "Acesso negado ou dados de formulário inválidos",
@@ -366,7 +369,7 @@
"comment_too_long": "Comentário muito longo, máximo de 160 caracteres permitidos",
"cors_invalid_method": "Método de permissão inválido especificado",
"cors_invalid_origin": "Origem de permissão inválida especificada",
"defquota_empty": "A cota padrão por caixa de correio não deve ser 0.",
"defquota_empty": "A cota padrão por mailbox não deve ser 0.",
"demo_mode_enabled": "O modo de demonstração está ativado",
"description_invalid": "A descrição do recurso para %s é inválida",
"dkim_domain_or_sel_exists": "Existe uma chave DKIM para “%s” e não será substituída",
@@ -404,22 +407,22 @@
"invalid_recipient_map_old": "Destinatário original inválido especificado: %s",
"ip_list_empty": "A lista de IPs permitidos não pode estar vazia",
"is_alias": "%s já é conhecido como endereço de alias",
"is_alias_or_mailbox": "%s já é conhecido como alias, caixa de correio ou endereço de alias expandido a partir de um domínio de alias.",
"is_alias_or_mailbox": "%s já é conhecido como alias, mailbox ou alias de endereço expandido a partir de um domínio de alias.",
"is_spam_alias": "%s já é conhecido como endereço de alias temporário (endereço de alias de spam)",
"last_key": "A última chave não pode ser excluída. Em vez disso, desative o TFA.",
"login_failed": "Falha no login",
"mailbox_defquota_exceeds_mailbox_maxquota": "A cota padrão excede o limite máximo da cota",
"mailbox_invalid": "O nome da caixa de correio é inválido",
"mailbox_invalid": "O nome da mailbox é inválido",
"mailbox_quota_exceeded": "A cota excede o limite do domínio (máx. %d MiB)",
"mailbox_quota_exceeds_domain_quota": "A cota máxima excede o limite da cota do domínio",
"mailbox_quota_left_exceeded": "Não há espaço restante (espaço restante: %d MiB)",
"mailboxes_in_use": "O máximo de caixas de correio deve ser maior ou igual a %d",
"mailboxes_in_use": "O máximo de mailboxes deve ser maior ou igual a %d",
"malformed_username": "Nome de usuário malformado",
"map_content_empty": "O conteúdo do mapa não pode estar vazio",
"max_alias_exceeded": "Número máximo de aliases excedido",
"max_mailbox_exceeded": "Número máximo de caixas de correio excedido (%d de %d)",
"max_quota_in_use": "A cota da caixa de correio deve ser maior ou igual a %d MiB",
"maxquota_empty": "A cota máxima por caixa de correio não deve ser 0.",
"max_mailbox_exceeded": "Número máximo de mailboxes excedido (%d de %d)",
"max_quota_in_use": "A cota da mailbox deve ser maior ou igual a %d MiB",
"maxquota_empty": "A cota máxima por mailbox não deve ser 0.",
"mysql_error": "Erro do MySQL: %s",
"network_host_invalid": "Rede ou host inválidos: %s",
"next_hop_interferes": "%s interfere com o nexthop %s",
@@ -489,27 +492,27 @@
"infoFiltered": "(filtrado do total de entradas _MAX_)",
"infoPostFix": "",
"thousands": ",",
"lengthMenu": "Show _MENU_ entries",
"loadingRecords": "Loading...",
"processing": "Please wait...",
"search": "Search:",
"zeroRecords": "No matching records found",
"lengthMenu": "Mostrar _ MENU_ entradas",
"loadingRecords": "Carregando...",
"processing": "Por favor, aguarde...",
"search": "Pesquisa:",
"zeroRecords": "Nenhum registro correspondente encontrado",
"paginate": {
"first": "First",
"last": "Last",
"first": "Primeiro",
"last": "Última",
"next": "Next",
"previous": "Previous"
"previous": "Anterior"
},
"aria": {
"sortAscending": ": activate to sort column ascending",
"sortDescending": ": activate to sort column descending"
"sortAscending": ": Ative para classificar a coluna ascendente",
"sortDescending": ": Ative para classificar a coluna decrescente"
}
},
"debug": {
"architecture": "Arquitetura",
"chart_this_server": "Gráfico (este servidor)",
"containers_info": "Informações do contêiner",
"container_running": "Correndo",
"container_running": "Executando",
"container_disabled": "Contêiner parado ou desativado",
"container_stopped": "Parado",
"cores": "Núcleos",
@@ -572,7 +575,7 @@
"automap": "Tente mapear pastas automaticamente (“Itens enviados”, “Enviados” => “Enviados” etc.)",
"backup_mx_options": "Opções de relé",
"bcc_dest_format": "O destino do BCC deve ser um único endereço de e-mail válido. <br>Se precisar enviar uma cópia para vários endereços, crie um alias e use-o aqui.",
"client_id": "ID do cliente",
"client_id": "ID Cliente",
"client_secret": "Segredo do cliente",
"comment_info": "Um comentário privado não é visível para o usuário, enquanto um comentário público é mostrado como dica de ferramenta ao passar o mouse sobre ele na visão geral do usuário",
"created_on": "Criado em",
@@ -592,7 +595,8 @@
"from_user": "{= from_user =} - Da parte do envelope do usuário, por exemplo, para \"moo@mailcow.tld\", ele retorna “moo”",
"from_name": "{= from_name =} - Do nome do envelope, por exemplo, para “Mailcow < moo@mailcow.tld >”, ele retorna “Mailcow”",
"from_addr": "{= from_addr =} - Do endereço, parte do envelope",
"from_domain": "{= from_domain =} - Da parte do domínio do envelope"
"from_domain": "{= from_domain =} - Da parte do domínio do envelope",
"custom": "{= foo =} - Se o mailbox tiver o atributo personalizado \"foo\" com valor \"bar\", retornará \"bar\""
},
"domain_footer_plain": "Rodapé simples",
"domain_quota": "Cota de domínio",
@@ -615,11 +619,11 @@
"kind": "Gentil",
"last_modified": "Última modificação",
"lookup_mx": "Destination é uma expressão regular que corresponde ao nome MX (<code>.*\\ .google\\ .com</code> para rotear todos os e-mails direcionados a um MX que termina em google.com nesse salto)",
"mailbox": "Editar caixa de correio",
"mailbox_quota_def": "Cota de caixa de correio padrão",
"mailbox": "Editar mailbox",
"mailbox_quota_def": "Cota mailbox padrão",
"mailbox_relayhost_info": "Aplicado somente à caixa de correio e aos aliases diretos, substitui um host de retransmissão de domínio.",
"max_aliases": "Máximo de aliases",
"max_mailboxes": "Número máximo de caixas de correio possíveis",
"max_mailboxes": "Número máximo de mailboxes possíveis",
"max_quota": "Cota máxima por caixa de correio (MiB)",
"maxage": "Duração máxima das mensagens em dias que serão pesquisadas remotamente <br><small>(0 = ignorar a idade</small>)",
"maxbytespersecond": "Máximo de bytes por segundo <br><small>(0 = ilimitado</small>)",
@@ -653,7 +657,7 @@
"relay_all_info": "↪ Se você optar por <b>não</b> retransmitir todos os destinatários, precisará adicionar uma caixa de correio (“cega”) para cada destinatário que deve ser retransmitido.",
"relay_domain": "Retransmitir este domínio",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Informações</div> Você pode definir mapas de transporte para um destino personalizado para esse domínio. Se não for definido, uma pesquisa MX será feita.",
"relay_unknown_only": "Retransmita somente caixas de correio não existentes. As caixas de correio existentes serão entregues localmente.",
"relay_unknown_only": "Retransmita somente mailboxes não existentes. As caixas de mailboxes serão entregues localmente.",
"relayhost": "Transportes dependentes do remetente",
"remove": "Remover",
"resource": "Recurso",
@@ -684,7 +688,7 @@
"username": "Nome de usuário",
"validate_save": "Valide e salve",
"custom_attributes": "Atributos personalizados",
"mbox_exclude": "Excluir caixas de email"
"mbox_exclude": "Excluir mailboxes"
},
"fido2": {
"confirm": "Confirme",
@@ -827,13 +831,13 @@
"last_run_reset": "Programe a seguir",
"mailbox": "Caixa de correio",
"mailbox_defaults": "Configurações padrão",
"mailbox_defaults_info": "Defina as configurações padrão para novas caixas de correio.",
"mailbox_defaults_info": "Defina as configurações padrão para novas mailboxes.",
"mailbox_defquota": "Tamanho padrão da caixa de correio",
"mailbox_templates": "Modelos de caixa de correio",
"mailbox_quota": "Tamanho máximo de uma caixa de correio",
"mailboxes": "Caixas de correio",
"mailboxes": "mailboxes",
"max_aliases": "Máximo de aliases",
"max_mailboxes": "Número máximo de caixas de correio possíveis",
"max_mailboxes": "Número máximo de mailboxes possíveis",
"max_quota": "Cota máxima por caixa de correio",
"mins_interval": "Intervalo (min)",
"msg_num": "Mensagem #",
@@ -861,10 +865,10 @@
"recipient_map_old_info": "O destino original do mapa de um destinatário deve ser um endereço de e-mail válido ou um nome de domínio.",
"recipient_maps": "Mapas de destinatários",
"relay_all": "Retransmita todos os destinatários",
"relay_unknown": "Retransmitir caixas de correio desconhecidas",
"relay_unknown": "Retransmitir mailboxes desconhecidas",
"remove": "Remover",
"resources": "Recursos",
"running": "Correndo",
"running": "Executando",
"sender": "Remetente",
"set_postfilter": "Marcar como postfilter",
"set_prefilter": "Marcar como pré-filtro",
@@ -908,7 +912,7 @@
"tls_map_parameters_info": "Vazio ou parâmetros, por exemplo: protocols=! Cifras SSLv2 = média, exclusão = 3DES",
"tls_map_policy": "Política",
"tls_policy_maps": "Mapas de políticas de TLS",
"tls_policy_maps_enforced_tls": "Essas políticas também substituirão o comportamento dos usuários de caixas de correio que impõem conexões TLS de saída. <code>Se nenhuma política existir abaixo, esses usuários aplicarão os valores padrão especificados como <code>smtp_tls_mandatory_protocols e smtp_tls_mandatory_ciphers</code>.</code>",
"tls_policy_maps_enforced_tls": "Essas políticas também substituirão o comportamento das caixas de e-mail dos usuários, que impõem conexões TLS de saída. Se não houver nenhuma política abaixo, esses usuários aplicarão os valores padrão especificados como <code>smtp_tls_mandatory_protocols</code> e <code>smtp_tls_mandatory_ciphers</code>.",
"tls_policy_maps_info": "Esse mapa de políticas substitui as regras de transporte TLS de saída, independentemente das configurações de política de TLS do usuário. <br>\r\n Consulte <a href=\"http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps\" target=\"_blank\">a documentação do “smtp_tls_policy_maps” para obter mais informações</a>.",
"tls_policy_maps_long": "Substituições do mapa de políticas de TLS de saída",
"toggle_all": "Alternar tudo",
@@ -1006,7 +1010,7 @@
"help": "Mostrar/ocultar painel de ajuda",
"imap_smtp_server_auth_info": "Use seu endereço de e-mail completo e o mecanismo de autenticação PLAIN. <br>\r\nSeus dados de login serão criptografados pela criptografia obrigatória do lado do servidor.",
"mailcow_apps_detail": "Use um aplicativo mailcow para acessar seus e-mails, calendário, contatos e muito mais.",
"mailcow_panel_detail": "<b>Os administradores de domínio</b> criam, modificam ou excluem caixas de correio e aliases, alteram domínios e leem mais informações sobre seus domínios atribuídos. <br>\r\n<b>Os usuários de caixas de correio</b> podem criar aliases com limite de tempo (aliases de spam), alterar suas configurações de senha e filtro de spam."
"mailcow_panel_detail": "<b>Os administradores de domínio</b> criam, modificam ou excluem mailboxes e aliases, alteram domínios e leem mais informações sobre seus domínios atribuídos. <br>\n<b>Os usuários de caixas de correio</b> podem criar aliases com limite de tempo (aliases de spam), alterar suas configurações de senha e filtro de spam."
},
"success": {
"acl_saved": "ACL para o objeto %s salvo",
@@ -1091,7 +1095,8 @@
"verified_fido2_login": "Login FIDO2 verificado",
"verified_totp_login": "Login TOTP verificado",
"verified_webauthn_login": "Login verificado do WebAuthn",
"verified_yotp_login": "Login OTP verificado do Yubico"
"verified_yotp_login": "Login OTP verificado do Yubico",
"f2b_banlist_refreshed": "O Banlist ID foi atualizado com sucesso."
},
"tfa": {
"api_register": "%s usa a API Yubico Cloud. Obtenha uma chave de API para sua chave <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">aqui</a>",
@@ -1219,7 +1224,7 @@
"quarantine_notification_info": "Depois que uma notificação for enviada, os itens serão marcados como “notificados” e nenhuma outra notificação será enviada para esse item específico.",
"recent_successful_connections": "Conexões bem-sucedidas vistas",
"remove": "Remover",
"running": "Correndo",
"running": "Executando",
"save": "Salvar alterações",
"save_changes": "Salvar alterações",
"sender_acl_disabled": "<span class=\"badge fs-6 bg-danger\">A verificação do remetente está desativada</span>",
@@ -1293,7 +1298,7 @@
"ip_invalid": "IP inválido ignorado: %s",
"is_not_primary_alias": "Alias não primário ignorado %s",
"no_active_admin": "Não é possível desativar o último administrador ativo",
"quota_exceeded_scope": "Cota de domínio excedida: somente caixas de correio ilimitadas podem ser criadas nesse escopo de domínio.",
"quota_exceeded_scope": "Cota de domínio excedida: somente mailboxes ilimitadas podem ser criadas nesse escopo de domínio.",
"session_token": "Token de formulário inválido: incompatibilidade de token",
"session_ua": "Token de formulário inválido: erro de validação do agente de usuário"
}

View File

@@ -345,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": "Доступ запрещён, или указаны неверные данные",
@@ -1015,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>",

View File

@@ -107,7 +107,8 @@
"username": "Používateľské meno",
"validate": "Overiť",
"validation_success": "Úspešne overené",
"tags": "Štítky"
"tags": "Štítky",
"dry": "Simulovať synchronizáciu"
},
"admin": {
"access": "Prístup",
@@ -486,7 +487,9 @@
},
"emptyTable": "Nie sú k dispozícii žiadne dáta.",
"decimal": ",",
"thousands": " "
"thousands": " ",
"collapse_all": "Zbaliť všetko",
"expand_all": "Rozbaliť všetko"
},
"debug": {
"chart_this_server": "Graf (tento server)",
@@ -639,7 +642,18 @@
"title": "Upraviť objekt",
"unchanged_if_empty": "Ak nemeníte, nechajte prázdne",
"username": "Používateľské meno",
"validate_save": "Validovať a uložiť"
"validate_save": "Validovať a uložiť",
"domain_footer_info_vars": {
"from_addr": "{= from_addr =} - E-mailová adresa odosielateľa",
"from_domain": "{= from_domain =} - Doména odosielateľa",
"auth_user": "{= auth_user =} - Prihlasovacie meno odosielateľa",
"from_user": "{= from_user =} - Používateľská časť e-mailovej adresy odosielateľa, napr. pre \"moo@mailcow.tld\" vráti \"moo\"",
"from_name": "{= from_name =} - Meno odosielateľa, napr. pre \"Mailcow &lt;moo@mailcow.tld&gt;\" vráti \"Mailcow\""
},
"domain_footer": "Pätička pre celú doménu",
"domain_footer_html": "HTML text",
"domain_footer_info": "Pätička pre celú doménu sa pridáva do všetkých odchádzajúcich e-mailov spojených s adresou v rámci tejto domény. <br> Pre pätičku je možné použiť nasledujúce premenné:",
"domain_footer_plain": "Obyčajný text"
},
"fido2": {
"confirm": "Potvrdiť",
@@ -934,7 +948,19 @@
"type": "Typ"
},
"queue": {
"queue_manager": "Správca fronty"
"queue_manager": "Správca fronty",
"delete": "Vymazať všetko",
"flush": "Vyprázdnit frontu",
"info": "Poštová fronta obsahuje všetky e-maily, ktoré čakajú na doručenie. Ak e-mail uviazne v poštovej fronte na dlhší čas, systém ho automaticky vymaže.<br>Chybové hlásenie príslušného e-mailu poskytuje informácie o tom, prečo sa e-mail nepodarilo doručiť.",
"legend": "Možnosti akcií nad poštovou frontou:",
"ays": "Potvrďte, že chcete naozaj odstrániť všetky položky z aktuálnej fronty.",
"deliver_mail": "Doručiť",
"deliver_mail_legend": "Pokus o opätovné doručenie vybraných e-mailov.",
"show_message": "Zobraziť správu",
"unhold_mail": "Uvoľniť",
"unhold_mail_legend": "Uvoľniť vybrané e-maily na doručenie. (Len v prípade predchádzajúceho podržania)",
"hold_mail": "Podržať",
"hold_mail_legend": "Podržať vybrané e-maily. (Zabráni ďalším pokusom o doručenie)"
},
"ratelimit": {
"disabled": "Vypnuté",
@@ -1028,7 +1054,8 @@
"verified_fido2_login": "Overené FIDO2 prihlásenie",
"verified_totp_login": "Overené TOTP prihlásenie",
"verified_webauthn_login": "Overené WebAuthn prihlásenie",
"verified_yotp_login": "Overené Yubico OTP prihlásenie"
"verified_yotp_login": "Overené Yubico OTP prihlásenie",
"domain_footer_modified": "Zmeny v pätičke domény %s boli uložené"
},
"tfa": {
"api_register": "%s využíva Yubico Cloud API. Prosím, zaobstarajte si API kľúč pre váš kľúč <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">tu</a>",

View File

@@ -346,7 +346,10 @@
"ip_check_disabled": "Перевірка IP вимкнена. Ви можете ввімкнути його в меню<br> <strong>Система > Конфігурація > Параметри > Налаштувати</strong>",
"ip_check_opt_in": "Згода на використання сторонніх служб <strong>ipv4.mailcow.email</strong> і <strong>ipv6.mailcow.email</strong> для визначення зовнішніх IP-адрес.",
"options": "Параметри",
"queue_unban": "розблокувати"
"queue_unban": "розблокувати",
"f2b_manage_external": "Керування Fail2Ban ззовні",
"f2b_manage_external_info": "Fail2ban буде підтримувати список заборонених, але не буде активно встановлювати правила для блокування трафіку. Використовуйте згенерований список заборон нижче для зовнішнього блокування трафіку.",
"copy_to_clipboard": "Текст скопійовано в буфер обміну!"
},
"danger": {
"alias_domain_invalid": "Неприпустимий псевдонім домену: %s",
@@ -1063,7 +1066,8 @@
"template_modified": "Зміни до шаблону %s збережено",
"cors_headers_edited": "Налаштування CORS збережено",
"ip_check_opt_in_modified": "Перевірка IP-адреси успішно збережено",
"template_removed": "Шаблону із ID %s видалено"
"template_removed": "Шаблону із ID %s видалено",
"f2b_banlist_refreshed": "Ідентифікатор списку заборонених успішно оновлено."
},
"tfa": {
"confirm": "Підтвердьте",

View File

@@ -107,7 +107,8 @@
"timeout2": "本地主機連線逾時時間",
"username": "使用者名稱",
"validate": "驗證",
"validation_success": "驗證成功"
"validation_success": "驗證成功",
"dry": "模擬同步"
},
"admin": {
"access": "存取",
@@ -335,7 +336,22 @@
"username": "使用者名稱",
"validate_license_now": "與證書伺服器驗證 GUID",
"verify": "驗證",
"yes": "&#10003;"
"yes": "&#10003;",
"f2b_manage_external_info": "Fail2ban仍會維護禁令列表但不會主動設定規則來阻止流量。 使用下面產生的禁止清單從外部阻止流量。",
"allowed_origins": "存取控制允許來源",
"logo_dark_label": "深色模式",
"logo_normal_label": "標準",
"f2b_ban_time_increment": "禁令時間會隨著每次禁令增加",
"copy_to_clipboard": "文字已複製到剪貼簿!",
"cors_settings": "CORS 設定",
"f2b_manage_external": "外部管理 Fail2Ban",
"f2b_max_ban_time": "最大限度。 禁止時間(s)",
"allowed_methods": "存取控制允許方法",
"ip_check": "IP檢查",
"ip_check_opt_in": "選擇使用第三方服務 <strong>ipv4.mailcow.email</strong> 和 <strong>ipv6.mailcow.email</strong> 來解析外部 IP 位址。",
"ip_check_disabled": "IP 檢查已停用。 您可以在<br> <strong>系統 > 配置 > 選項 > 自訂</strong>下啟用它",
"options": "選項",
"queue_unban": "解除禁令"
},
"danger": {
"access_denied": "存取拒絕或表單資料有誤",
@@ -454,7 +470,17 @@
"username_invalid": "使用者名稱 %s 無法使用",
"validity_missing": "請設定有效期",
"value_missing": "請填入所有欄位",
"yotp_verification_failed": "Yubico OTP 認證失敗: %s"
"yotp_verification_failed": "Yubico OTP 認證失敗: %s",
"webauthn_authenticator_failed": "找不到所選的驗證器",
"webauthn_publickey_failed": "沒有為選定的身份驗證器儲存公鑰",
"webauthn_username_failed": "所選驗證器屬於另一個帳戶",
"cors_invalid_method": "指定的允許方法無效",
"cors_invalid_origin": "指定的允許來源無效",
"demo_mode_enabled": "演示模式已啟用",
"extended_sender_acl_denied": "缺少設定外部寄件者地址的 ACL",
"template_exists": "模板 %s 已存在",
"template_id_invalid": "範本 ID %s 無效",
"template_name_invalid": "模板名稱無效"
},
"debug": {
"chart_this_server": "圖表 (此伺服器)",
@@ -473,7 +499,7 @@
"restart_container": "重新啟動",
"service": "服務",
"size": "大小",
"solr_dead": "Solr 正在啟動停用或已停止運行",
"solr_dead": "Solr 正在啟動,停用或已停止運行.",
"solr_status": "Solr 狀態",
"started_at": "啟動於",
"started_on": "啟動於",
@@ -481,7 +507,19 @@
"success": "成功",
"system_containers": "系統和容器",
"uptime": "運行時間",
"username": "使用者名稱"
"username": "使用者名稱",
"architecture": "結構",
"current_time": "系統時間",
"container_running": "正在執行",
"memory": "記憶",
"container_disabled": "容器停止或停用",
"container_stopped": "已停止",
"cores": "核心",
"error_show_ip": "無法解析公用IP位址",
"show_ip": "顯示公網IP",
"update_available": "有可用更新",
"no_update_available": "系統已經是最新版本",
"update_failed": "無法檢查更新"
},
"diagnostics": {
"cname_from_a": "由 A/AAAA 紀錄獲取。只要紀錄指向正確的資源,此功能就會持續運作。",
@@ -607,7 +645,11 @@
"title": "編輯物件",
"unchanged_if_empty": "如果不更改則留空",
"username": "使用者名稱",
"validate_save": "驗證並儲存"
"validate_save": "驗證並儲存",
"domain_footer_info": "網域範圍的頁尾將會新增至與該網域內的位址關聯的所有外發電子郵件。 <br> 以下變數可用於頁尾:",
"custom_attributes": "自訂屬性",
"mbox_exclude": "排除信箱",
"pushover_sound": "聲音"
},
"fido2": {
"confirm": "確認",
@@ -646,7 +688,10 @@
"quarantine": "隔離",
"restart_netfilter": "重新啟動 netfilter",
"restart_sogo": "重新啟動 SOGo",
"user_settings": "使用者設定"
"user_settings": "使用者設定",
"email": "電子郵件",
"mailcow_system": "系統",
"mailcow_config": "配置"
},
"info": {
"awaiting_tfa_confirmation": "等待 TFA 確認",
@@ -829,7 +874,13 @@
"username": "使用者名稱",
"waiting": "等待中",
"weekly": "每週",
"yes": "&#10003;"
"yes": "&#10003;",
"templates": "範本",
"domain_templates": "域模板",
"add_template": "新增模板",
"mailbox_templates": "信箱模板",
"relay_unknown": "轉發未知信箱",
"template": "範本"
},
"oauth2": {
"access_denied": "請使用信箱使用者登入來進行 OAuth2 授權",
@@ -929,7 +980,7 @@
"delete_filters": "過濾器 %s 已刪除",
"deleted_syncjob": "同步任務 ID %s 已刪除",
"deleted_syncjobs": "同步任務 %s 已刪除",
"dkim_added": " DKIM 金鑰 %s 已儲存",
"dkim_added": "DKIM 金鑰 %s 已儲存",
"domain_add_dkim_available": "DKIM 金鑰已存在",
"dkim_duplicated": "域名的 DKIM 金鑰 %s 已複製到 %s",
"dkim_removed": " DKIM 金鑰 %s 已刪除",
@@ -976,14 +1027,21 @@
"settings_map_added": "設定規則已新增",
"settings_map_removed": "設定規則 ID %s 已刪除",
"sogo_profile_reset": "使用者 %s 的 SOGo 個人頁面已重設",
"tls_policy_map_entry_deleted": " TLS 規則 ID %s 已刪除",
"tls_policy_map_entry_deleted": "TLS 規則 ID %s 已刪除",
"tls_policy_map_entry_saved": "TLS 規則 \"%s\" 已儲存",
"ui_texts": "UI 內文更改已儲存",
"upload_success": "檔案已成功上傳",
"verified_fido2_login": "FIDO2 登入驗證成功",
"verified_totp_login": "TOTP 登入驗證成功",
"verified_webauthn_login": "WebAuthn 登入驗證成功",
"verified_yotp_login": "Yubico OTP 登入驗證成功"
"verified_yotp_login": "Yubico OTP 登入驗證成功",
"template_removed": "模板 ID %s 已刪除",
"template_added": "新增了模板 %s",
"template_modified": "模板 %s 的變更已儲存",
"cors_headers_edited": "CORS 設定已儲存",
"domain_footer_modified": "網域頁尾 %s 的變更已儲存",
"f2b_banlist_refreshed": "禁止清單 ID 已成功刷新。",
"ip_check_opt_in_modified": "IP檢查已成功儲存"
},
"tfa": {
"api_register": "%s 使用 Yubico Cloud API請在<a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">這裡</a>為這把金鑰獲取 API 金鑰",
@@ -1171,7 +1229,9 @@
"weeks": "週",
"with_app_password": "使用應用程式密碼",
"year": "年",
"years": "年"
"years": "年",
"attribute": "屬性",
"pushover_sound": "聲音"
},
"warning": {
"cannot_delete_self": "不能刪除已登入的使用者",
@@ -1185,5 +1245,43 @@
"quota_exceeded_scope": "域名容量配額已滿: 此域名現在只能創建無限容量的信箱。",
"session_token": "表單驗證失敗: 驗證碼錯誤",
"session_ua": "表單驗證失敗: User-Agent 校驗錯誤"
},
"datatables": {
"infoEmpty": "顯示 0 到 0 個條目,共 0 個條目",
"infoFiltered": "從_MAX_個總條目中過濾",
"lengthMenu": "顯示_選單_條目",
"loadingRecords": "載入中...",
"processing": "請稍等...",
"search": "搜尋:",
"zeroRecords": "未找到符合的記錄",
"paginate": {
"first": "第一的",
"last": "最後的",
"next": "下一個",
"previous": "上一個"
},
"aria": {
"sortAscending": ":啟動以升序對列進行排序",
"sortDescending": ":啟動以降序對列進行排序"
},
"expand_all": "展開全部",
"info": "顯示 _START_ 到 _END_ 條,共 _TOTAL_ 條",
"collapse_all": "全部折疊",
"emptyTable": "表中沒有可用數據",
"thousands": ",",
"decimal": "."
},
"queue": {
"deliver_mail_legend": "嘗試重新投遞選定的郵件。",
"show_message": "顯示訊息",
"unban": "解除禁令隊列",
"legend": "郵件隊列操作功能:",
"delete": "刪除所有",
"flush": "刷新隊列",
"info": "郵件隊列包含所有等待投遞的電子郵件。 如果電子郵件長時間滯留在郵件隊列中,系統會自動將其刪除。<br>對應郵件的錯誤訊息會提供有關郵件無法送達的原因的資訊。",
"ays": "請確認您要刪除目前隊列中的所有項目。",
"deliver_mail": "遞送",
"queue_manager": "隊列管理器",
"unhold_mail_legend": "釋放選定的郵件以供投遞。 (需事先持有)"
}
}

View File

@@ -42,6 +42,13 @@
<input type="number" class="form-control" id="f2b_netban_ipv6" name="netban_ipv6" value="{{ f2b_data.netban_ipv6 }}" required>
</div>
</div>
<div class="mb-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="f2b_manage_external" value="1" name="manage_external" {% if f2b_data.manage_external == 1 %}checked{% endif %}>
<label class="form-check-label" for="f2b_manage_external">{{ lang.admin.f2b_manage_external }}</label>
</div>
<p class="text-muted">{{ lang.admin.f2b_manage_external_info }}</p>
</div>
<hr>
<p class="text-muted">{{ lang.admin.f2b_list_info|raw }}</p>
<div class="mb-2">
@@ -90,6 +97,15 @@
{% if not f2b_data.active_bans and not f2b_data.perm_bans %}
<i>{{ lang.admin.no_active_bans }}</i>
{% endif %}
<form class="form-inline" data-id="f2b_banlist" role="form" method="post">
<div class="input-group mb-3">
<input type="text" class="form-control" aria-label="Banlist url" value="{{ f2b_banlist_url}}" id="banlist_url">
{% if is_https %}
<button class="btn btn-secondary" type="button" onclick="copyToClipboard('banlist_url')"><i class="bi bi-clipboard"></i></button>
{% endif %}
<button class="btn btn-secondary" type="button" data-action="edit_selected" data-item="{{ f2b_data.banlist_id }}" data-id="f2b_banlist" data-api-url='edit/fail2ban/banlist' data-api-attr='{}'><i class="bi bi-arrow-clockwise"></i></button>
</div>
</form>
{% for active_ban in f2b_data.active_bans %}
<p>
<span class="badge fs-7 bg-info d-block d-sm-inline-block">

View File

@@ -171,7 +171,7 @@ services:
- phpfpm
sogo-mailcow:
image: mailcow/sogo:1.119
image: mailcow/sogo:1.120
environment:
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
@@ -298,7 +298,7 @@ services:
- dovecot
postfix-mailcow:
image: mailcow/postfix:1.72
image: mailcow/postfix:1.73
depends_on:
mysql-mailcow:
condition: service_started
@@ -434,7 +434,7 @@ services:
- acme
netfilter-mailcow:
image: mailcow/netfilter:1.52
image: mailcow/netfilter:1.54
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
@@ -457,7 +457,7 @@ services:
- /lib/modules:/lib/modules:ro
watchdog-mailcow:
image: mailcow/watchdog:1.98
image: mailcow/watchdog:2.00
dns:
- ${IPV4_NETWORK:-172.22.1}.254
tmpfs:
@@ -486,7 +486,10 @@ services:
- USE_WATCHDOG=${USE_WATCHDOG:-n}
- WATCHDOG_NOTIFY_EMAIL=${WATCHDOG_NOTIFY_EMAIL:-}
- WATCHDOG_NOTIFY_BAN=${WATCHDOG_NOTIFY_BAN:-y}
- WATCHDOG_NOTIFY_START=${WATCHDOG_NOTIFY_START:-y}
- WATCHDOG_SUBJECT=${WATCHDOG_SUBJECT:-Watchdog ALERT}
- WATCHDOG_NOTIFY_WEBHOOK=${WATCHDOG_NOTIFY_WEBHOOK:-}
- WATCHDOG_NOTIFY_WEBHOOK_BODY=${WATCHDOG_NOTIFY_WEBHOOK_BODY:-}
- WATCHDOG_EXTERNAL_CHECKS=${WATCHDOG_EXTERNAL_CHECKS:-n}
- WATCHDOG_MYSQL_REPLICATION_CHECKS=${WATCHDOG_MYSQL_REPLICATION_CHECKS:-n}
- WATCHDOG_VERBOSE=${WATCHDOG_VERBOSE:-n}

View File

@@ -398,9 +398,19 @@ USE_WATCHDOG=y
#WATCHDOG_NOTIFY_EMAIL=a@example.com,b@example.com,c@example.com
#WATCHDOG_NOTIFY_EMAIL=
# Send notifications to a webhook URL that receives a POST request with the content type "application/json".
# You can use this to send notifications to services like Discord, Slack and others.
#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# JSON body included in the webhook POST request. Needs to be in single quotes.
# Following variables are available: SUBJECT, BODY
#WATCHDOG_NOTIFY_WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}'
# Notify about banned IP (includes whois lookup)
WATCHDOG_NOTIFY_BAN=n
# Send a notification when the watchdog is started.
WATCHDOG_NOTIFY_START=y
# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.
#WATCHDOG_SUBJECT=

View File

@@ -26,6 +26,6 @@ services:
- /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
mysql-mailcow:
image: alpine:3.18
image: alpine:3.19
command: /bin/true
restart: "no"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?<version>.*)$
NEXTCLOUD_VERSION=27.1.4
NEXTCLOUD_VERSION=28.0.1
echo -ne "Checking prerequisites..."
sleep 1

View File

@@ -441,7 +441,10 @@ CONFIG_ARRAY=(
"SKIP_SOGO"
"USE_WATCHDOG"
"WATCHDOG_NOTIFY_EMAIL"
"WATCHDOG_NOTIFY_WEBHOOK"
"WATCHDOG_NOTIFY_WEBHOOK_BODY"
"WATCHDOG_NOTIFY_BAN"
"WATCHDOG_NOTIFY_START"
"WATCHDOG_EXTERNAL_CHECKS"
"WATCHDOG_SUBJECT"
"SKIP_CLAMD"
@@ -623,12 +626,33 @@ for option in ${CONFIG_ARRAY[@]}; do
echo "#MAILDIR_SUB=Maildir" >> mailcow.conf
echo "MAILDIR_SUB=" >> mailcow.conf
fi
elif [[ ${option} == "WATCHDOG_NOTIFY_WEBHOOK" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf
echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf
echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf
fi
elif [[ ${option} == "WATCHDOG_NOTIFY_WEBHOOK_BODY" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf
echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf
WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}'
echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf
fi
elif [[ ${option} == "WATCHDOG_NOTIFY_BAN" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf
echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf
fi
elif [[ ${option} == "WATCHDOG_NOTIFY_START" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Send a notification when the watchdog is started.' >> mailcow.conf
echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf
fi
elif [[ ${option} == "WATCHDOG_SUBJECT" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"