Compare commits

..

1 Commits

Author SHA1 Message Date
FreddleSpl0it
aaa23d2dc1 [Web] Add User ACL to manage SOGo access 2025-10-16 13:42:39 +02:00
135 changed files with 885 additions and 6957 deletions

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Mark/Close Stale Issues and Pull Requests 🗑️
uses: actions/stale@v10.1.1
uses: actions/stale@v10.1.0
with:
repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60

View File

@@ -15,7 +15,7 @@ jobs:
images:
- "acme-mailcow"
- "clamd-mailcow"
- "controller-mailcow"
- "dockerapi-mailcow"
- "dovecot-mailcow"
- "netfilter-mailcow"
- "olefy-mailcow"
@@ -27,7 +27,7 @@ jobs:
- "watchdog-mailcow"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Setup Docker
run: |
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh

View File

@@ -8,11 +8,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Run the Action
uses: devops-infra/action-pull-request@v1.0.2
uses: devops-infra/action-pull-request@v0.6.1
with:
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}

View File

@@ -13,7 +13,7 @@ jobs:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

View File

@@ -15,14 +15,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Generate postscreen_access.cidr
run: |
bash helper-scripts/update_postscreen_whitelist.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
commit-message: update postscreen_access.cidr

View File

@@ -1,11 +1,11 @@
# Contribution Guidelines
**_Last modified on 12th November 2025_**
**_Last modified on 15th August 2024_**
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!
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
**PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULFILL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
## Topics
@@ -27,18 +27,14 @@ However, please note the following regarding pull requests:
6. 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.*
7. 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.
8. 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!
9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme.
---
## Issue Reporting
**_Last modified on 12th November 2025_**
**_Last modified on 15th August 2024_**
If you plan to report a issue within mailcow please read and understand the following rules:
### Security disclosures / Security-related fixes
- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure.
### Issue Reporting Guidelines
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).

View File

@@ -38,45 +38,45 @@ get_docker_version(){
}
get_compose_type(){
if docker compose > /dev/null 2>&1; then
if docker compose version --short | grep -e "^[2-9]\." -e "^v[2-9]\." -e "^[1-9][0-9]\." -e "^v[1-9][0-9]\." > /dev/null 2>&1; then
COMPOSE_VERSION=native
COMPOSE_COMMAND="docker compose"
if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
fi
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
sleep 2
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
if docker compose > /dev/null 2>&1; then
if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then
COMPOSE_VERSION=native
COMPOSE_COMMAND="docker compose"
if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
fi
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
sleep 2
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
elif docker-compose > /dev/null 2>&1; then
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
COMPOSE_VERSION=standalone
COMPOSE_COMMAND="docker-compose"
if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
fi
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
sleep 2
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
fi
elif docker-compose > /dev/null 2>&1; then
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
if docker-compose version --short | grep -e "^[2-9]\." -e "^[1-9][0-9]\." > /dev/null 2>&1; then
COMPOSE_VERSION=standalone
COMPOSE_COMMAND="docker-compose"
if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
fi
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
sleep 2
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
fi
else
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
}
detect_bad_asn() {

View File

@@ -48,11 +48,11 @@ if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
exec $(readlink -f "$0")
fi
log_f "Waiting for Controller .."
until ping controller -c1 > /dev/null; do
log_f "Waiting for Docker API..."
until ping dockerapi -c1 > /dev/null; do
sleep 1
done
log_f "Controller OK"
log_f "Docker API OK"
log_f "Waiting for Postfix..."
until ping postfix -c1 > /dev/null; do
@@ -246,25 +246,6 @@ while true; do
done
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
done
# Fetch alias domains where target domain has MTA-STS enabled
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
if [[ $? -eq 0 ]]; then
while read alias_domain; do
if [[ -z "${alias_domain}" ]]; then
# ignore empty lines
continue
fi
# Only add mta-sts subdomain for alias domains
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
if check_domain "mta-sts.${alias_domain}"; then
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
fi
fi
done <<< "${SQL_ALIAS_DOMAINS}"
fi
fi
fi
if check_domain ${MAILCOW_HOSTNAME}; then

View File

@@ -2,32 +2,32 @@
# Reading container IDs
# Wrapping as array to ensure trimmed content when calling $NGINX etc.
NGINX=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
DOVECOT=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
POSTFIX=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
NGINX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
DOVECOT=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
POSTFIX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
reload_nginx(){
echo "Reloading Nginx..."
NGINX_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type)
NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; }
}
reload_dovecot(){
echo "Reloading Dovecot..."
DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type)
DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; }
}
reload_postfix(){
echo "Reloading Postfix..."
POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type)
POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; }
}
restart_container(){
for container in $*; do
echo "Restarting ${container}..."
C_REST_OUT=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg')
C_REST_OUT=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg')
echo "${C_REST_OUT}"
done
}

View File

@@ -1,3 +1,3 @@
FROM debian:trixie-slim
FROM debian:bookworm-slim
RUN apt update && apt install pigz zstd -y --no-install-recommends
RUN apt update && apt install pigz -y --no-install-recommends

View File

@@ -1,9 +0,0 @@
#!/bin/bash
`openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
-keyout /app/controller_key.pem \
-out /app/controller_cert.pem \
-subj /CN=controller/O=mailcow \
-addext subjectAltName=DNS:controller`
exec "$@"

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import sys
from models.AliasModel import AliasModel
from models.MailboxModel import MailboxModel
from models.SyncjobModel import SyncjobModel
from models.CalendarModel import CalendarModel
from models.MailerModel import MailerModel
from models.AddressbookModel import AddressbookModel
from models.MaildirModel import MaildirModel
from models.DomainModel import DomainModel
from models.DomainadminModel import DomainadminModel
from models.StatusModel import StatusModel
from modules.Utils import Utils
def main():
utils = Utils()
model_map = {
MailboxModel.parser_command: MailboxModel,
AliasModel.parser_command: AliasModel,
SyncjobModel.parser_command: SyncjobModel,
CalendarModel.parser_command: CalendarModel,
AddressbookModel.parser_command: AddressbookModel,
MailerModel.parser_command: MailerModel,
MaildirModel.parser_command: MaildirModel,
DomainModel.parser_command: DomainModel,
DomainadminModel.parser_command: DomainadminModel,
StatusModel.parser_command: StatusModel
}
parser = argparse.ArgumentParser(description="mailcow Admin Tool")
subparsers = parser.add_subparsers(dest="command", required=True)
for model in model_map.values():
model.add_parser(subparsers)
args = parser.parse_args()
for cmd, model_cls in model_map.items():
if args.command == cmd and model_cls.has_required_args(args):
instance = model_cls(**vars(args))
action = getattr(instance, args.object, None)
if callable(action):
res = action()
utils.pprint(res)
sys.exit(0)
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -1,140 +0,0 @@
from modules.Sogo import Sogo
from models.BaseModel import BaseModel
class AddressbookModel(BaseModel):
parser_command = "addressbook"
required_args = {
"add": [["username", "name"]],
"delete": [["username", "name"]],
"get": [["username", "name"]],
"set_acl": [["username", "name", "sharee_email", "acl"]],
"get_acl": [["username", "name"]],
"delete_acl": [["username", "name", "sharee_email"]],
"add_contact": [["username", "name", "contact_name", "contact_email", "type"]],
"delete_contact": [["username", "name", "contact_name"]],
}
def __init__(
self,
username=None,
name=None,
sharee_email=None,
acl=None,
subscribe=None,
ics=None,
contact_name=None,
contact_email=None,
type=None,
**kwargs
):
self.sogo = Sogo(username)
self.name = name
self.acl = acl
self.sharee_email = sharee_email
self.subscribe = subscribe
self.ics = ics
self.contact_name = contact_name
self.contact_email = contact_email
self.type = type
def add(self):
"""
Add a new addressbook.
:return: Response from SOGo API.
"""
return self.sogo.addAddressbook(self.name)
def set_acl(self):
"""
Set ACL for the addressbook.
:return: Response from SOGo API.
"""
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
if not addressbook_id:
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.setAddressbookACL(addressbook_id, self.sharee_email, self.acl, self.subscribe)
def delete_acl(self):
"""
Delete the addressbook ACL.
:return: Response from SOGo API.
"""
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
if not addressbook_id:
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.deleteAddressbookACL(addressbook_id, self.sharee_email)
def get_acl(self):
"""
Get the ACL for the addressbook.
:return: Response from SOGo API.
"""
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
if not addressbook_id:
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.getAddressbookACL(addressbook_id)
def add_contact(self):
"""
Add a new contact to the addressbook.
:return: Response from SOGo API.
"""
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
if not addressbook_id:
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
return None
if self.type == "card":
return self.sogo.addAddressbookContact(addressbook_id, self.contact_name, self.contact_email)
elif self.type == "list":
return self.sogo.addAddressbookContactList(addressbook_id, self.contact_name, self.contact_email)
def delete_contact(self):
"""
Delete a contact or contactlist from the addressbook.
:return: Response from SOGo API.
"""
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
if not addressbook_id:
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.deleteAddressbookItem(addressbook_id, self.contact_name)
def get(self):
"""
Retrieve addressbooks list.
:return: Response from SOGo API.
"""
return self.sogo.getAddressbookList()
def delete(self):
"""
Delete the addressbook.
:return: Response from SOGo API.
"""
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
if not addressbook_id:
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.deleteAddressbook(addressbook_id)
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Manage addressbooks (add, delete, get, set_acl, get_acl, delete_acl, add_contact, delete_contact)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, set_acl, get_acl, delete_acl, add_contact, delete_contact")
parser.add_argument("--username", required=True, help="Username of the addressbook owner (e.g. user@example.com)")
parser.add_argument("--name", help="Addressbook name")
parser.add_argument("--sharee-email", help="Email address to share the addressbook with")
parser.add_argument("--acl", help="ACL rights for the sharee (e.g. r, w, rw)")
parser.add_argument("--subscribe", action='store_true', help="Subscribe the sharee to the addressbook")
parser.add_argument("--contact-name", help="Name of the contact or contactlist to add or delete")
parser.add_argument("--contact-email", help="Email address of the contact to add")
parser.add_argument("--type", choices=["card", "list"], help="Type of contact to add: card (single contact) or list (distribution list)")

View File

@@ -1,107 +0,0 @@
from modules.Mailcow import Mailcow
from models.BaseModel import BaseModel
class AliasModel(BaseModel):
parser_command = "alias"
required_args = {
"add": [["address", "goto"]],
"delete": [["id"]],
"get": [["id"]],
"edit": [["id"]]
}
def __init__(
self,
id=None,
address=None,
goto=None,
active=None,
sogo_visible=None,
**kwargs
):
self.mailcow = Mailcow()
self.id = id
self.address = address
self.goto = goto
self.active = active
self.sogo_visible = sogo_visible
@classmethod
def from_dict(cls, data):
return cls(
address=data.get("address"),
goto=data.get("goto"),
active=data.get("active", None),
sogo_visible=data.get("sogo_visible", None)
)
def getAdd(self):
"""
Get the alias details as a dictionary for adding, sets default values.
:return: Dictionary containing alias details.
"""
alias = {
"address": self.address,
"goto": self.goto,
"active": self.active if self.active is not None else 1,
"sogo_visible": self.sogo_visible if self.sogo_visible is not None else 0
}
return {key: value for key, value in alias.items() if value is not None}
def getEdit(self):
"""
Get the alias details as a dictionary for editing, sets no default values.
:return: Dictionary containing mailbox details.
"""
alias = {
"address": self.address,
"goto": self.goto,
"active": self.active,
"sogo_visible": self.sogo_visible
}
return {key: value for key, value in alias.items() if value is not None}
def get(self):
"""
Get the mailbox details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.getAlias(self.id)
def delete(self):
"""
Get the mailbox details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.deleteAlias(self.id)
def add(self):
"""
Get the mailbox details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.addAlias(self.getAdd())
def edit(self):
"""
Get the mailbox details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.editAlias(self.id, self.getEdit())
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Manage aliases (add, delete, get, edit)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
parser.add_argument("--id", help="Alias object ID (required for get, edit, delete)")
parser.add_argument("--address", help="Alias email address (e.g. alias@example.com)")
parser.add_argument("--goto", help="Destination address(es), comma-separated (e.g. user1@example.com,user2@example.com)")
parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the alias")
parser.add_argument("--sogo-visible", choices=["1", "0"], help="Show alias in SOGo addressbook (1 = yes, 0 = no)")

View File

@@ -1,35 +0,0 @@
class BaseModel:
parser_command = ""
required_args = {}
@classmethod
def has_required_args(cls, args):
"""
Validate that all required arguments are present.
"""
object_name = args.object if hasattr(args, "object") else args.get("object")
required_lists = cls.required_args.get(object_name, False)
if not required_lists:
return False
for required_set in required_lists:
result = True
for required_args in required_set:
if isinstance(args, dict):
if not args.get(required_args):
result = False
break
elif not hasattr(args, required_args):
result = False
break
if result:
break
if not result:
print(f"Required arguments for '{object_name}': {required_lists}")
return result
@classmethod
def add_parser(cls, subparsers):
pass

View File

@@ -1,111 +0,0 @@
from modules.Sogo import Sogo
from models.BaseModel import BaseModel
class CalendarModel(BaseModel):
parser_command = "calendar"
required_args = {
"add": [["username", "name"]],
"delete": [["username", "name"]],
"get": [["username"]],
"import_ics": [["username", "name", "ics"]],
"set_acl": [["username", "name", "sharee_email", "acl"]],
"get_acl": [["username", "name"]],
"delete_acl": [["username", "name", "sharee_email"]],
}
def __init__(
self,
username=None,
name=None,
sharee_email=None,
acl=None,
subscribe=None,
ics=None,
**kwargs
):
self.sogo = Sogo(username)
self.name = name
self.acl = acl
self.sharee_email = sharee_email
self.subscribe = subscribe
self.ics = ics
def add(self):
"""
Add a new calendar.
:return: Response from SOGo API.
"""
return self.sogo.addCalendar(self.name)
def delete(self):
"""
Delete a calendar.
:return: Response from SOGo API.
"""
calendar_id = self.sogo.getCalendarIdByName(self.name)
if not calendar_id:
print(f"Calendar '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.deleteCalendar(calendar_id)
def get(self):
"""
Get the calendar details.
:return: Response from SOGo API.
"""
return self.sogo.getCalendar()
def set_acl(self):
"""
Set ACL for the calendar.
:return: Response from SOGo API.
"""
calendar_id = self.sogo.getCalendarIdByName(self.name)
if not calendar_id:
print(f"Calendar '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.setCalendarACL(calendar_id, self.sharee_email, self.acl, self.subscribe)
def delete_acl(self):
"""
Delete the calendar ACL.
:return: Response from SOGo API.
"""
calendar_id = self.sogo.getCalendarIdByName(self.name)
if not calendar_id:
print(f"Calendar '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.deleteCalendarACL(calendar_id, self.sharee_email)
def get_acl(self):
"""
Get the ACL for the calendar.
:return: Response from SOGo API.
"""
calendar_id = self.sogo.getCalendarIdByName(self.name)
if not calendar_id:
print(f"Calendar '{self.name}' not found for user '{self.username}'.")
return None
return self.sogo.getCalendarACL(calendar_id)
def import_ics(self):
"""
Import a calendar from an ICS file.
:return: Response from SOGo API.
"""
return self.sogo.importCalendar(self.name, self.ics)
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Manage calendars (add, delete, get, import_ics, set_acl, get_acl, delete_acl)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, import_ics, set_acl, get_acl, delete_acl")
parser.add_argument("--username", required=True, help="Username of the calendar owner (e.g. user@example.com)")
parser.add_argument("--name", help="Calendar name")
parser.add_argument("--ics", help="Path to ICS file for import")
parser.add_argument("--sharee-email", help="Email address to share the calendar with")
parser.add_argument("--acl", help="ACL rights for the sharee (e.g. r, w, rw)")
parser.add_argument("--subscribe", action='store_true', help="Subscribe the sharee to the calendar")

View File

@@ -1,162 +0,0 @@
from modules.Mailcow import Mailcow
from models.BaseModel import BaseModel
class DomainModel(BaseModel):
parser_command = "domain"
required_args = {
"add": [["domain"]],
"delete": [["domain"]],
"get": [["domain"]],
"edit": [["domain"]]
}
def __init__(
self,
domain=None,
active=None,
aliases=None,
backupmx=None,
defquota=None,
description=None,
mailboxes=None,
maxquota=None,
quota=None,
relay_all_recipients=None,
rl_frame=None,
rl_value=None,
restart_sogo=None,
tags=None,
**kwargs
):
self.mailcow = Mailcow()
self.domain = domain
self.active = active
self.aliases = aliases
self.backupmx = backupmx
self.defquota = defquota
self.description = description
self.mailboxes = mailboxes
self.maxquota = maxquota
self.quota = quota
self.relay_all_recipients = relay_all_recipients
self.rl_frame = rl_frame
self.rl_value = rl_value
self.restart_sogo = restart_sogo
self.tags = tags
@classmethod
def from_dict(cls, data):
return cls(
domain=data.get("domain"),
active=data.get("active", None),
aliases=data.get("aliases", None),
backupmx=data.get("backupmx", None),
defquota=data.get("defquota", None),
description=data.get("description", None),
mailboxes=data.get("mailboxes", None),
maxquota=data.get("maxquota", None),
quota=data.get("quota", None),
relay_all_recipients=data.get("relay_all_recipients", None),
rl_frame=data.get("rl_frame", None),
rl_value=data.get("rl_value", None),
restart_sogo=data.get("restart_sogo", None),
tags=data.get("tags", None)
)
def getAdd(self):
"""
Get the domain details as a dictionary for adding, sets default values.
:return: Dictionary containing domain details.
"""
domain = {
"domain": self.domain,
"active": self.active if self.active is not None else 1,
"aliases": self.aliases if self.aliases is not None else 400,
"backupmx": self.backupmx if self.backupmx is not None else 0,
"defquota": self.defquota if self.defquota is not None else 3072,
"description": self.description if self.description is not None else "",
"mailboxes": self.mailboxes if self.mailboxes is not None else 10,
"maxquota": self.maxquota if self.maxquota is not None else 10240,
"quota": self.quota if self.quota is not None else 10240,
"relay_all_recipients": self.relay_all_recipients if self.relay_all_recipients is not None else 0,
"rl_frame": self.rl_frame,
"rl_value": self.rl_value,
"restart_sogo": self.restart_sogo if self.restart_sogo is not None else 0,
"tags": self.tags if self.tags is not None else []
}
return {key: value for key, value in domain.items() if value is not None}
def getEdit(self):
"""
Get the domain details as a dictionary for editing, sets no default values.
:return: Dictionary containing domain details.
"""
domain = {
"domain": self.domain,
"active": self.active,
"aliases": self.aliases,
"backupmx": self.backupmx,
"defquota": self.defquota,
"description": self.description,
"mailboxes": self.mailboxes,
"maxquota": self.maxquota,
"quota": self.quota,
"relay_all_recipients": self.relay_all_recipients,
"rl_frame": self.rl_frame,
"rl_value": self.rl_value,
"restart_sogo": self.restart_sogo,
"tags": self.tags
}
return {key: value for key, value in domain.items() if value is not None}
def get(self):
"""
Get the domain details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.getDomain(self.domain)
def delete(self):
"""
Delete the domain from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.deleteDomain(self.domain)
def add(self):
"""
Add the domain to the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.addDomain(self.getAdd())
def edit(self):
"""
Edit the domain in the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.editDomain(self.domain, self.getEdit())
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Manage domains (add, delete, get, edit)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
parser.add_argument("--domain", required=True, help="Domain name (e.g. domain.tld)")
parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the domain")
parser.add_argument("--aliases", help="Number of aliases allowed for the domain")
parser.add_argument("--backupmx", choices=["1", "0"], help="Enable (1) or disable (0) backup MX")
parser.add_argument("--defquota", help="Default quota for mailboxes in MB")
parser.add_argument("--description", help="Description of the domain")
parser.add_argument("--mailboxes", help="Number of mailboxes allowed for the domain")
parser.add_argument("--maxquota", help="Maximum quota for the domain in MB")
parser.add_argument("--quota", help="Quota used by the domain in MB")
parser.add_argument("--relay-all-recipients", choices=["1", "0"], help="Relay all recipients (1 = yes, 0 = no)")
parser.add_argument("--rl-frame", help="Rate limit frame (e.g., s, m, h)")
parser.add_argument("--rl-value", help="Rate limit value")
parser.add_argument("--restart-sogo", help="Restart SOGo after changes (1 = yes, 0 = no)")
parser.add_argument("--tags", nargs="*", help="Tags for the domain")

View File

@@ -1,106 +0,0 @@
from modules.Mailcow import Mailcow
from models.BaseModel import BaseModel
class DomainadminModel(BaseModel):
parser_command = "domainadmin"
required_args = {
"add": [["username", "domains", "password"]],
"delete": [["username"]],
"get": [["username"]],
"edit": [["username"]]
}
def __init__(
self,
username=None,
domains=None,
password=None,
active=None,
**kwargs
):
self.mailcow = Mailcow()
self.username = username
self.domains = domains
self.password = password
self.password2 = password
self.active = active
@classmethod
def from_dict(cls, data):
return cls(
username=data.get("username"),
domains=data.get("domains"),
password=data.get("password"),
password2=data.get("password"),
active=data.get("active", None),
)
def getAdd(self):
"""
Get the domain admin details as a dictionary for adding, sets default values.
:return: Dictionary containing domain admin details.
"""
domainadmin = {
"username": self.username,
"domains": self.domains,
"password": self.password,
"password2": self.password2,
"active": self.active if self.active is not None else "1"
}
return {key: value for key, value in domainadmin.items() if value is not None}
def getEdit(self):
"""
Get the domain admin details as a dictionary for editing, sets no default values.
:return: Dictionary containing domain admin details.
"""
domainadmin = {
"username": self.username,
"domains": self.domains,
"password": self.password,
"password2": self.password2,
"active": self.active
}
return {key: value for key, value in domainadmin.items() if value is not None}
def get(self):
"""
Get the domain admin details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.getDomainadmin(self.username)
def delete(self):
"""
Delete the domain admin from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.deleteDomainadmin(self.username)
def add(self):
"""
Add the domain admin to the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.addDomainadmin(self.getAdd())
def edit(self):
"""
Edit the domain admin in the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.editDomainadmin(self.username, self.getEdit())
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Manage domain admins (add, delete, get, edit)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
parser.add_argument("--username", help="Username for the domain admin")
parser.add_argument("--domains", help="Comma-separated list of domains")
parser.add_argument("--password", help="Password for the domain admin")
parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the domain admin")

View File

@@ -1,164 +0,0 @@
from modules.Mailcow import Mailcow
from models.BaseModel import BaseModel
class MailboxModel(BaseModel):
parser_command = "mailbox"
required_args = {
"add": [["username", "password"]],
"delete": [["username"]],
"get": [["username"]],
"edit": [["username"]]
}
def __init__(
self,
password=None,
username=None,
domain=None,
local_part=None,
active=None,
sogo_access=None,
name=None,
authsource=None,
quota=None,
force_pw_update=None,
tls_enforce_in=None,
tls_enforce_out=None,
tags=None,
sender_acl=None,
**kwargs
):
self.mailcow = Mailcow()
if username is not None and "@" in username:
self.username = username
self.local_part, self.domain = username.split("@")
else:
self.username = f"{local_part}@{domain}"
self.local_part = local_part
self.domain = domain
self.password = password
self.password2 = password
self.active = active
self.sogo_access = sogo_access
self.name = name
self.authsource = authsource
self.quota = quota
self.force_pw_update = force_pw_update
self.tls_enforce_in = tls_enforce_in
self.tls_enforce_out = tls_enforce_out
self.tags = tags
self.sender_acl = sender_acl
@classmethod
def from_dict(cls, data):
return cls(
domain=data.get("domain"),
local_part=data.get("local_part"),
password=data.get("password"),
password2=data.get("password"),
active=data.get("active", None),
sogo_access=data.get("sogo_access", None),
name=data.get("name", None),
authsource=data.get("authsource", None),
quota=data.get("quota", None),
force_pw_update=data.get("force_pw_update", None),
tls_enforce_in=data.get("tls_enforce_in", None),
tls_enforce_out=data.get("tls_enforce_out", None),
tags=data.get("tags", None),
sender_acl=data.get("sender_acl", None)
)
def getAdd(self):
"""
Get the mailbox details as a dictionary for adding, sets default values.
:return: Dictionary containing mailbox details.
"""
mailbox = {
"domain": self.domain,
"local_part": self.local_part,
"password": self.password,
"password2": self.password2,
"active": self.active if self.active is not None else 1,
"name": self.name if self.name is not None else "",
"authsource": self.authsource if self.authsource is not None else "mailcow",
"quota": self.quota if self.quota is not None else 0,
"force_pw_update": self.force_pw_update if self.force_pw_update is not None else 0,
"tls_enforce_in": self.tls_enforce_in if self.tls_enforce_in is not None else 0,
"tls_enforce_out": self.tls_enforce_out if self.tls_enforce_out is not None else 0,
"tags": self.tags if self.tags is not None else []
}
return {key: value for key, value in mailbox.items() if value is not None}
def getEdit(self):
"""
Get the mailbox details as a dictionary for editing, sets no default values.
:return: Dictionary containing mailbox details.
"""
mailbox = {
"domain": self.domain,
"local_part": self.local_part,
"password": self.password,
"password2": self.password2,
"active": self.active,
"name": self.name,
"authsource": self.authsource,
"quota": self.quota,
"force_pw_update": self.force_pw_update,
"tls_enforce_in": self.tls_enforce_in,
"tls_enforce_out": self.tls_enforce_out,
"tags": self.tags
}
return {key: value for key, value in mailbox.items() if value is not None}
def get(self):
"""
Get the mailbox details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.getMailbox(self.username)
def delete(self):
"""
Get the mailbox details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.deleteMailbox(self.username)
def add(self):
"""
Get the mailbox details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.addMailbox(self.getAdd())
def edit(self):
"""
Get the mailbox details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.editMailbox(self.username, self.getEdit())
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Manage mailboxes (add, delete, get, edit)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
parser.add_argument("--username", help="Full email address of the mailbox (e.g. user@example.com)")
parser.add_argument("--password", help="Password for the mailbox (required for add)")
parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the mailbox")
parser.add_argument("--sogo-access", choices=["1", "0"], help="Redirect mailbox to SOGo after web login (1 = yes, 0 = no)")
parser.add_argument("--name", help="Display name of the mailbox owner")
parser.add_argument("--authsource", help="Authentication source (default: mailcow)")
parser.add_argument("--quota", help="Mailbox quota in bytes (0 = unlimited)")
parser.add_argument("--force-pw-update", choices=["1", "0"], help="Force password update on next login (1 = yes, 0 = no)")
parser.add_argument("--tls-enforce-in", choices=["1", "0"], help="Enforce TLS for incoming emails (1 = yes, 0 = no)")
parser.add_argument("--tls-enforce-out", choices=["1", "0"], help="Enforce TLS for outgoing emails (1 = yes, 0 = no)")
parser.add_argument("--tags", help="Comma-separated list of tags for the mailbox")
parser.add_argument("--sender-acl", help="Comma-separated list of allowed sender addresses for this mailbox")

View File

@@ -1,67 +0,0 @@
from modules.Dovecot import Dovecot
from models.BaseModel import BaseModel
class MaildirModel(BaseModel):
parser_command = "maildir"
required_args = {
"encrypt": [],
"decrypt": [],
"restore": [["username", "item"], ["list"]]
}
def __init__(
self,
username=None,
source=None,
item=None,
overwrite=None,
list=None,
**kwargs
):
self.dovecot = Dovecot()
for key, value in kwargs.items():
setattr(self, key, value)
self.username = username
self.source = source
self.item = item
self.overwrite = overwrite
self.list = list
def encrypt(self):
"""
Encrypt the maildir for the specified user or all.
:return: Response from Dovecot.
"""
return self.dovecot.encryptMaildir(self.source_dir, self.output_dir)
def decrypt(self):
"""
Decrypt the maildir for the specified user or all.
:return: Response from Dovecot.
"""
return self.dovecot.decryptMaildir(self.source_dir, self.output_dir)
def restore(self):
"""
Restore or List maildir data for the specified user.
:return: Response from Dovecot.
"""
if self.list:
return self.dovecot.listDeletedMaildirs()
return self.dovecot.restoreMaildir(self.username, self.item)
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Manage maildir (encrypt, decrypt, restore)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: encrypt, decrypt, restore")
parser.add_argument("--item", help="Item to restore")
parser.add_argument("--username", help="Username to restore the item to")
parser.add_argument("--list", action="store_true", help="List items to restore")
parser.add_argument("--source-dir", help="Path to the source maildir to import/encrypt/decrypt")
parser.add_argument("--output-dir", help="Directory to store encrypted/decrypted files inside the Dovecot container")

View File

@@ -1,62 +0,0 @@
import json
from models.BaseModel import BaseModel
from modules.Mailer import Mailer
class MailerModel(BaseModel):
parser_command = "mail"
required_args = {
"send": [["sender", "recipient", "subject", "body"]]
}
def __init__(
self,
sender=None,
recipient=None,
subject=None,
body=None,
context=None,
**kwargs
):
self.sender = sender
self.recipient = recipient
self.subject = subject
self.body = body
self.context = context
def send(self):
if self.context is not None:
try:
self.context = json.loads(self.context)
except json.JSONDecodeError as e:
return f"Invalid context JSON: {e}"
else:
self.context = {}
mailer = Mailer(
smtp_host="postfix-mailcow",
smtp_port=25,
username=self.sender,
password="",
use_tls=True
)
res = mailer.send_mail(
subject=self.subject,
from_addr=self.sender,
to_addrs=self.recipient.split(","),
template=self.body,
context=self.context
)
return res
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Send emails via SMTP"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: send")
parser.add_argument("--sender", required=True, help="Email sender address")
parser.add_argument("--recipient", required=True, help="Email recipient address (comma-separated for multiple)")
parser.add_argument("--subject", required=True, help="Email subject")
parser.add_argument("--body", required=True, help="Email body (Jinja2 template supported)")
parser.add_argument("--context", help="Context for Jinja2 template rendering (JSON format)")

View File

@@ -1,45 +0,0 @@
from modules.Mailcow import Mailcow
from models.BaseModel import BaseModel
class StatusModel(BaseModel):
parser_command = "status"
required_args = {
"version": [[]],
"vmail": [[]],
"containers": [[]]
}
def __init__(
self,
**kwargs
):
self.mailcow = Mailcow()
def version(self):
"""
Get the version of the mailcow instance.
:return: Response from the mailcow API.
"""
return self.mailcow.getStatusVersion()
def vmail(self):
"""
Get the vmail details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.getStatusVmail()
def containers(self):
"""
Get the status of containers in the mailcow instance.
:return: Response from the mailcow API.
"""
return self.mailcow.getStatusContainers()
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Get information about mailcow (version, vmail, containers)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: version, vmail, containers")

View File

@@ -1,221 +0,0 @@
from modules.Mailcow import Mailcow
from models.BaseModel import BaseModel
class SyncjobModel(BaseModel):
parser_command = "syncjob"
required_args = {
"add": [["username", "host1", "port1", "user1", "password1", "enc1"]],
"delete": [["id"]],
"get": [["username"]],
"edit": [["id"]],
"run": [["id"]]
}
def __init__(
self,
id=None,
username=None,
host1=None,
port1=None,
user1=None,
password1=None,
enc1=None,
mins_interval=None,
subfolder2=None,
maxage=None,
maxbytespersecond=None,
timeout1=None,
timeout2=None,
exclude=None,
custom_parameters=None,
delete2duplicates=None,
delete1=None,
delete2=None,
automap=None,
skipcrossduplicates=None,
subscribeall=None,
active=None,
force=None,
**kwargs
):
self.mailcow = Mailcow()
for key, value in kwargs.items():
setattr(self, key, value)
self.id = id
self.username = username
self.host1 = host1
self.port1 = port1
self.user1 = user1
self.password1 = password1
self.enc1 = enc1
self.mins_interval = mins_interval
self.subfolder2 = subfolder2
self.maxage = maxage
self.maxbytespersecond = maxbytespersecond
self.timeout1 = timeout1
self.timeout2 = timeout2
self.exclude = exclude
self.custom_parameters = custom_parameters
self.delete2duplicates = delete2duplicates
self.delete1 = delete1
self.delete2 = delete2
self.automap = automap
self.skipcrossduplicates = skipcrossduplicates
self.subscribeall = subscribeall
self.active = active
self.force = force
@classmethod
def from_dict(cls, data):
return cls(
username=data.get("username"),
host1=data.get("host1"),
port1=data.get("port1"),
user1=data.get("user1"),
password1=data.get("password1"),
enc1=data.get("enc1"),
mins_interval=data.get("mins_interval", None),
subfolder2=data.get("subfolder2", None),
maxage=data.get("maxage", None),
maxbytespersecond=data.get("maxbytespersecond", None),
timeout1=data.get("timeout1", None),
timeout2=data.get("timeout2", None),
exclude=data.get("exclude", None),
custom_parameters=data.get("custom_parameters", None),
delete2duplicates=data.get("delete2duplicates", None),
delete1=data.get("delete1", None),
delete2=data.get("delete2", None),
automap=data.get("automap", None),
skipcrossduplicates=data.get("skipcrossduplicates", None),
subscribeall=data.get("subscribeall", None),
active=data.get("active", None),
)
def getAdd(self):
"""
Get the sync job details as a dictionary for adding, sets default values.
:return: Dictionary containing sync job details.
"""
syncjob = {
"username": self.username,
"host1": self.host1,
"port1": self.port1,
"user1": self.user1,
"password1": self.password1,
"enc1": self.enc1,
"mins_interval": self.mins_interval if self.mins_interval is not None else 20,
"subfolder2": self.subfolder2 if self.subfolder2 is not None else "",
"maxage": self.maxage if self.maxage is not None else 0,
"maxbytespersecond": self.maxbytespersecond if self.maxbytespersecond is not None else 0,
"timeout1": self.timeout1 if self.timeout1 is not None else 600,
"timeout2": self.timeout2 if self.timeout2 is not None else 600,
"exclude": self.exclude if self.exclude is not None else "(?i)spam|(?i)junk",
"custom_parameters": self.custom_parameters if self.custom_parameters is not None else "",
"delete2duplicates": 1 if self.delete2duplicates else 0,
"delete1": 1 if self.delete1 else 0,
"delete2": 1 if self.delete2 else 0,
"automap": 1 if self.automap else 0,
"skipcrossduplicates": 1 if self.skipcrossduplicates else 0,
"subscribeall": 1 if self.subscribeall else 0,
"active": 1 if self.active else 0
}
return {key: value for key, value in syncjob.items() if value is not None}
def getEdit(self):
"""
Get the sync job details as a dictionary for editing, sets no default values.
:return: Dictionary containing sync job details.
"""
syncjob = {
"username": self.username,
"host1": self.host1,
"port1": self.port1,
"user1": self.user1,
"password1": self.password1,
"enc1": self.enc1,
"mins_interval": self.mins_interval,
"subfolder2": self.subfolder2,
"maxage": self.maxage,
"maxbytespersecond": self.maxbytespersecond,
"timeout1": self.timeout1,
"timeout2": self.timeout2,
"exclude": self.exclude,
"custom_parameters": self.custom_parameters,
"delete2duplicates": self.delete2duplicates,
"delete1": self.delete1,
"delete2": self.delete2,
"automap": self.automap,
"skipcrossduplicates": self.skipcrossduplicates,
"subscribeall": self.subscribeall,
"active": self.active
}
return {key: value for key, value in syncjob.items() if value is not None}
def get(self):
"""
Get the sync job details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.getSyncjob(self.username)
def delete(self):
"""
Get the sync job details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.deleteSyncjob(self.id)
def add(self):
"""
Get the sync job details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.addSyncjob(self.getAdd())
def edit(self):
"""
Get the sync job details from the mailcow API.
:return: Response from the mailcow API.
"""
return self.mailcow.editSyncjob(self.id, self.getEdit())
def run(self):
"""
Run the sync job.
:return: Response from the mailcow API.
"""
return self.mailcow.runSyncjob(self.id, force=self.force)
@classmethod
def add_parser(cls, subparsers):
parser = subparsers.add_parser(
cls.parser_command,
help="Manage sync jobs (add, delete, get, edit)"
)
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
parser.add_argument("--id", help="Syncjob object ID (required for edit, delete, run)")
parser.add_argument("--username", help="Target mailbox username (e.g. user@example.com)")
parser.add_argument("--host1", help="Source IMAP server hostname")
parser.add_argument("--port1", help="Source IMAP server port")
parser.add_argument("--user1", help="Source IMAP account username")
parser.add_argument("--password1", help="Source IMAP account password")
parser.add_argument("--enc1", choices=["PLAIN", "SSL", "TLS"], help="Encryption for source server connection")
parser.add_argument("--mins-interval", help="Sync interval in minutes (default: 20)")
parser.add_argument("--subfolder2", help="Destination subfolder (default: empty)")
parser.add_argument("--maxage", help="Maximum mail age in days (default: 0 = unlimited)")
parser.add_argument("--maxbytespersecond", help="Maximum bandwidth in bytes/sec (default: 0 = unlimited)")
parser.add_argument("--timeout1", help="Timeout for source server in seconds (default: 600)")
parser.add_argument("--timeout2", help="Timeout for destination server in seconds (default: 600)")
parser.add_argument("--exclude", help="Regex pattern to exclude folders (default: (?i)spam|(?i)junk)")
parser.add_argument("--custom-parameters", help="Additional imapsync parameters")
parser.add_argument("--delete2duplicates", choices=["1", "0"], help="Delete duplicates on destination (1 = yes, 0 = no)")
parser.add_argument("--del1", choices=["1", "0"], help="Delete mails on source after sync (1 = yes, 0 = no)")
parser.add_argument("--del2", choices=["1", "0"], help="Delete mails on destination after sync (1 = yes, 0 = no)")
parser.add_argument("--automap", choices=["1", "0"], help="Enable folder automapping (1 = yes, 0 = no)")
parser.add_argument("--skipcrossduplicates", choices=["1", "0"], help="Skip cross-account duplicates (1 = yes, 0 = no)")
parser.add_argument("--subscribeall", choices=["1", "0"], help="Subscribe to all folders (1 = yes, 0 = no)")
parser.add_argument("--active", choices=["1", "0"], help="Activate syncjob (1 = yes, 0 = no)")
parser.add_argument("--force", action="store_true", help="Force the syncjob to run even if it is not active")

View File

@@ -1,128 +0,0 @@
import docker
from docker.errors import APIError
class Docker:
def __init__(self):
self.client = docker.from_env()
def exec_command(self, container_name, cmd, user=None):
"""
Execute a command in a container by its container name.
:param container_name: The name of the container.
:param cmd: The command to execute as a list (e.g., ["ls", "-la"]).
:param user: The user to execute the command as (optional).
:return: A standardized response with status, output, and exit_code.
"""
filters = {"name": container_name}
try:
for container in self.client.containers.list(filters=filters):
exec_result = container.exec_run(cmd, user=user)
return {
"status": "success",
"exit_code": exec_result.exit_code,
"output": exec_result.output.decode("utf-8")
}
except APIError as e:
return {
"status": "error",
"exit_code": "APIError",
"output": str(e)
}
except Exception as e:
return {
"status": "error",
"exit_code": "Exception",
"output": str(e)
}
def start_container(self, container_name):
"""
Start a container by its container name.
:param container_name: The name of the container.
:return: A standardized response with status, output, and exit_code.
"""
filters = {"name": container_name}
try:
for container in self.client.containers.list(filters=filters):
container.start()
return {
"status": "success",
"exit_code": "0",
"output": f"Container '{container_name}' started successfully."
}
except APIError as e:
return {
"status": "error",
"exit_code": "APIError",
"output": str(e)
}
except Exception as e:
return {
"status": "error",
"error_type": "Exception",
"output": str(e)
}
def stop_container(self, container_name):
"""
Stop a container by its container name.
:param container_name: The name of the container.
:return: A standardized response with status, output, and exit_code.
"""
filters = {"name": container_name}
try:
for container in self.client.containers.list(filters=filters):
container.stop()
return {
"status": "success",
"exit_code": "0",
"output": f"Container '{container_name}' stopped successfully."
}
except APIError as e:
return {
"status": "error",
"exit_code": "APIError",
"output": str(e)
}
except Exception as e:
return {
"status": "error",
"exit_code": "Exception",
"output": str(e)
}
def restart_container(self, container_name):
"""
Restart a container by its container name.
:param container_name: The name of the container.
:return: A standardized response with status, output, and exit_code.
"""
filters = {"name": container_name}
try:
for container in self.client.containers.list(filters=filters):
container.restart()
return {
"status": "success",
"exit_code": "0",
"output": f"Container '{container_name}' restarted successfully."
}
except APIError as e:
return {
"status": "error",
"exit_code": "APIError",
"output": str(e)
}
except Exception as e:
return {
"status": "error",
"exit_code": "Exception",
"output": str(e)
}

View File

@@ -1,206 +0,0 @@
import os
from modules.Docker import Docker
class Dovecot:
def __init__(self):
self.docker = Docker()
def decryptMaildir(self, source_dir="/var/vmail/", output_dir=None):
"""
Decrypt files in /var/vmail using doveadm if they are encrypted.
:param output_dir: Directory inside the Dovecot container to store decrypted files, Default overwrite.
"""
private_key = "/mail_crypt/ecprivkey.pem"
public_key = "/mail_crypt/ecpubkey.pem"
if output_dir:
# Ensure the output directory exists inside the container
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c 'mkdir -p {output_dir} && chown vmail:vmail {output_dir}'")
if mkdir_result.get("status") != "success":
print(f"Error creating output directory: {mkdir_result.get('output')}")
return
find_command = [
"find", source_dir, "-type", "f", "-regextype", "egrep", "-regex", ".*S=.*W=.*"
]
try:
find_result = self.docker.exec_command("dovecot-mailcow", " ".join(find_command))
if find_result.get("status") != "success":
print(f"Error finding files: {find_result.get('output')}")
return
files = find_result.get("output", "").splitlines()
for file in files:
head_command = f"head -c7 {file}"
head_result = self.docker.exec_command("dovecot-mailcow", head_command)
if head_result.get("status") == "success" and head_result.get("output", "").strip() == "CRYPTED":
if output_dir:
# Preserve the directory structure in the output directory
relative_path = os.path.relpath(file, source_dir)
output_file = os.path.join(output_dir, relative_path)
current_path = output_dir
for part in os.path.dirname(relative_path).split(os.sep):
current_path = os.path.join(current_path, part)
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c '[ ! -d {current_path} ] && mkdir {current_path} && chown vmail:vmail {current_path}'")
if mkdir_result.get("status") != "success":
print(f"Error creating directory {current_path}: {mkdir_result.get('output')}")
continue
else:
# Overwrite the original file
output_file = file
decrypt_command = (
f"bash -c 'doveadm fs get compress lz4:1:crypt:private_key_path={private_key}:public_key_path={public_key}:posix:prefix=/ {file} > {output_file}'"
)
decrypt_result = self.docker.exec_command("dovecot-mailcow", decrypt_command)
if decrypt_result.get("status") == "success":
print(f"Decrypted {file}")
# Verify the file size and set permissions
size_check_command = f"bash -c '[ -s {output_file} ] && chmod 600 {output_file} && chown vmail:vmail {output_file} || rm -f {output_file}'"
size_check_result = self.docker.exec_command("dovecot-mailcow", size_check_command)
if size_check_result.get("status") != "success":
print(f"Error setting permissions for {output_file}: {size_check_result.get('output')}\n")
except Exception as e:
print(f"Error during decryption: {e}")
return "Done"
def encryptMaildir(self, source_dir="/var/vmail/", output_dir=None):
"""
Encrypt files in /var/vmail using doveadm if they are not already encrypted.
:param source_dir: Directory inside the Dovecot container to encrypt files.
:param output_dir: Directory inside the Dovecot container to store encrypted files, Default overwrite.
"""
private_key = "/mail_crypt/ecprivkey.pem"
public_key = "/mail_crypt/ecpubkey.pem"
if output_dir:
# Ensure the output directory exists inside the container
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"mkdir -p {output_dir}")
if mkdir_result.get("status") != "success":
print(f"Error creating output directory: {mkdir_result.get('output')}")
return
find_command = [
"find", source_dir, "-type", "f", "-regextype", "egrep", "-regex", ".*S=.*W=.*"
]
try:
find_result = self.docker.exec_command("dovecot-mailcow", " ".join(find_command))
if find_result.get("status") != "success":
print(f"Error finding files: {find_result.get('output')}")
return
files = find_result.get("output", "").splitlines()
for file in files:
head_command = f"head -c7 {file}"
head_result = self.docker.exec_command("dovecot-mailcow", head_command)
if head_result.get("status") == "success" and head_result.get("output", "").strip() != "CRYPTED":
if output_dir:
# Preserve the directory structure in the output directory
relative_path = os.path.relpath(file, source_dir)
output_file = os.path.join(output_dir, relative_path)
current_path = output_dir
for part in os.path.dirname(relative_path).split(os.sep):
current_path = os.path.join(current_path, part)
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c '[ ! -d {current_path} ] && mkdir {current_path} && chown vmail:vmail {current_path}'")
if mkdir_result.get("status") != "success":
print(f"Error creating directory {current_path}: {mkdir_result.get('output')}")
continue
else:
# Overwrite the original file
output_file = file
encrypt_command = (
f"bash -c 'doveadm fs put crypt private_key_path={private_key}:public_key_path={public_key}:posix:prefix=/ {file} {output_file}'"
)
encrypt_result = self.docker.exec_command("dovecot-mailcow", encrypt_command)
if encrypt_result.get("status") == "success":
print(f"Encrypted {file}")
# Set permissions
permissions_command = f"bash -c 'chmod 600 {output_file} && chown 5000:5000 {output_file}'"
permissions_result = self.docker.exec_command("dovecot-mailcow", permissions_command)
if permissions_result.get("status") != "success":
print(f"Error setting permissions for {output_file}: {permissions_result.get('output')}\n")
except Exception as e:
print(f"Error during encryption: {e}")
return "Done"
def listDeletedMaildirs(self, source_dir="/var/vmail/_garbage"):
"""
List deleted maildirs in the specified garbage directory.
:param source_dir: Directory to search for deleted maildirs.
:return: List of maildirs.
"""
list_command = ["bash", "-c", f"ls -la {source_dir}"]
try:
result = self.docker.exec_command("dovecot-mailcow", list_command)
if result.get("status") != "success":
print(f"Error listing deleted maildirs: {result.get('output')}")
return []
lines = result.get("output", "").splitlines()
maildirs = {}
for idx, line in enumerate(lines):
parts = line.split()
if "_" in line:
folder_name = parts[-1]
time, maildir = folder_name.split("_", 1)
if maildir.endswith("_index"):
main_item = maildir[:-6]
if main_item in maildirs:
maildirs[main_item]["has_index"] = True
else:
maildirs[maildir] = {"item": idx, "time": time, "name": maildir, "has_index": False}
return list(maildirs.values())
except Exception as e:
print(f"Error during listing deleted maildirs: {e}")
return []
def restoreMaildir(self, username, item, source_dir="/var/vmail/_garbage"):
"""
Restore a maildir item for a specific user from the deleted maildirs.
:param username: Username to restore the item to.
:param item: Item to restore (e.g., mailbox, folder).
:param source_dir: Directory containing deleted maildirs.
:return: Response from Dovecot.
"""
username_splitted = username.split("@")
maildirs = self.listDeletedMaildirs()
maildir = None
for mdir in maildirs:
if mdir["item"] == int(item):
maildir = mdir
break
if not maildir:
return {"status": "error", "message": "Maildir not found."}
restore_command = f"mv {source_dir}/{maildir['time']}_{maildir['name']} /var/vmail/{username_splitted[1]}/{username_splitted[0]}"
restore_index_command = f"mv {source_dir}/{maildir['time']}_{maildir['name']}_index /var/vmail_index/{username}"
result = self.docker.exec_command("dovecot-mailcow", ["bash", "-c", restore_command])
if result.get("status") != "success":
return {"status": "error", "message": "Failed to restore maildir."}
result = self.docker.exec_command("dovecot-mailcow", ["bash", "-c", restore_index_command])
if result.get("status") != "success":
return {"status": "error", "message": "Failed to restore maildir index."}
return "Done"

View File

@@ -1,457 +0,0 @@
import requests
import urllib3
import sys
import os
import subprocess
import tempfile
import mysql.connector
from contextlib import contextmanager
from datetime import datetime
from modules.Docker import Docker
class Mailcow:
def __init__(self):
self.apiUrl = "/api/v1"
self.ignore_ssl_errors = True
self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247:{os.getenv('HTTPS_PORT', '443')}"
self.host = os.getenv("MAILCOW_HOSTNAME", "")
self.apiKey = ""
if self.ignore_ssl_errors:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
self.db_config = {
'user': os.getenv('DBUSER'),
'password': os.getenv('DBPASS'),
'database': os.getenv('DBNAME'),
'unix_socket': '/var/run/mysqld/mysqld.sock',
}
self.docker = Docker()
# API Functions
def addDomain(self, domain):
"""
Add a domain to the mailcow instance.
:param domain: Dictionary containing domain details.
:return: Response from the mailcow API.
"""
return self.post('/add/domain', domain)
def addMailbox(self, mailbox):
"""
Add a mailbox to the mailcow instance.
:param mailbox: Dictionary containing mailbox details.
:return: Response from the mailcow API.
"""
return self.post('/add/mailbox', mailbox)
def addAlias(self, alias):
"""
Add an alias to the mailcow instance.
:param alias: Dictionary containing alias details.
:return: Response from the mailcow API.
"""
return self.post('/add/alias', alias)
def addSyncjob(self, syncjob):
"""
Add a sync job to the mailcow instance.
:param syncjob: Dictionary containing sync job details.
:return: Response from the mailcow API.
"""
return self.post('/add/syncjob', syncjob)
def addDomainadmin(self, domainadmin):
"""
Add a domain admin to the mailcow instance.
:param domainadmin: Dictionary containing domain admin details.
:return: Response from the mailcow API.
"""
return self.post('/add/domain-admin', domainadmin)
def deleteDomain(self, domain):
"""
Delete a domain from the mailcow instance.
:param domain: Name of the domain to delete.
:return: Response from the mailcow API.
"""
items = [domain]
return self.post('/delete/domain', items)
def deleteAlias(self, id):
"""
Delete an alias from the mailcow instance.
:param id: ID of the alias to delete.
:return: Response from the mailcow API.
"""
items = [id]
return self.post('/delete/alias', items)
def deleteSyncjob(self, id):
"""
Delete a sync job from the mailcow instance.
:param id: ID of the sync job to delete.
:return: Response from the mailcow API.
"""
items = [id]
return self.post('/delete/syncjob', items)
def deleteMailbox(self, mailbox):
"""
Delete a mailbox from the mailcow instance.
:param mailbox: Name of the mailbox to delete.
:return: Response from the mailcow API.
"""
items = [mailbox]
return self.post('/delete/mailbox', items)
def deleteDomainadmin(self, username):
"""
Delete a domain admin from the mailcow instance.
:param username: Username of the domain admin to delete.
:return: Response from the mailcow API.
"""
items = [username]
return self.post('/delete/domain-admin', items)
def post(self, endpoint, data):
"""
Make a POST request to the mailcow API.
:param endpoint: The API endpoint to post to.
:param data: Data to be sent in the POST request.
:return: Response from the mailcow API.
"""
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
headers = {
"Content-Type": "application/json",
"Host": self.host
}
if self.apiKey:
headers["X-Api-Key"] = self.apiKey
response = requests.post(
url,
json=data,
headers=headers,
verify=not self.ignore_ssl_errors
)
response.raise_for_status()
return response.json()
def getDomain(self, domain):
"""
Get a domain from the mailcow instance.
:param domain: Name of the domain to get.
:return: Response from the mailcow API.
"""
return self.get(f'/get/domain/{domain}')
def getMailbox(self, username):
"""
Get a mailbox from the mailcow instance.
:param mailbox: Dictionary containing mailbox details (e.g. {"username": "user@example.com"})
:return: Response from the mailcow API.
"""
return self.get(f'/get/mailbox/{username}')
def getAlias(self, id):
"""
Get an alias from the mailcow instance.
:param alias: Dictionary containing alias details (e.g. {"address": "alias@example.com"})
:return: Response from the mailcow API.
"""
return self.get(f'/get/alias/{id}')
def getSyncjob(self, id):
"""
Get a sync job from the mailcow instance.
:param syncjob: Dictionary containing sync job details (e.g. {"id": "123"})
:return: Response from the mailcow API.
"""
return self.get(f'/get/syncjobs/{id}')
def getDomainadmin(self, username):
"""
Get a domain admin from the mailcow instance.
:param username: Username of the domain admin to get.
:return: Response from the mailcow API.
"""
return self.get(f'/get/domain-admin/{username}')
def getStatusVersion(self):
"""
Get the version of the mailcow instance.
:return: Response from the mailcow API.
"""
return self.get('/get/status/version')
def getStatusVmail(self):
"""
Get the vmail status from the mailcow instance.
:return: Response from the mailcow API.
"""
return self.get('/get/status/vmail')
def getStatusContainers(self):
"""
Get the status of containers from the mailcow instance.
:return: Response from the mailcow API.
"""
return self.get('/get/status/containers')
def get(self, endpoint, params=None):
"""
Make a GET request to the mailcow API.
:param endpoint: The API endpoint to get from.
:param params: Parameters to be sent in the GET request.
:return: Response from the mailcow API.
"""
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
headers = {
"Content-Type": "application/json",
"Host": self.host
}
if self.apiKey:
headers["X-Api-Key"] = self.apiKey
response = requests.get(
url,
params=params,
headers=headers,
verify=not self.ignore_ssl_errors
)
response.raise_for_status()
return response.json()
def editDomain(self, domain, attributes):
"""
Edit an existing domain in the mailcow instance.
:param domain: Name of the domain to edit
:param attributes: Dictionary containing the new domain attributes.
"""
items = [domain]
return self.edit('/edit/domain', items, attributes)
def editMailbox(self, mailbox, attributes):
"""
Edit an existing mailbox in the mailcow instance.
:param mailbox: Name of the mailbox to edit
:param attributes: Dictionary containing the new mailbox attributes.
"""
items = [mailbox]
return self.edit('/edit/mailbox', items, attributes)
def editAlias(self, alias, attributes):
"""
Edit an existing alias in the mailcow instance.
:param alias: Name of the alias to edit
:param attributes: Dictionary containing the new alias attributes.
"""
items = [alias]
return self.edit('/edit/alias', items, attributes)
def editSyncjob(self, syncjob, attributes):
"""
Edit an existing sync job in the mailcow instance.
:param syncjob: Name of the sync job to edit
:param attributes: Dictionary containing the new sync job attributes.
"""
items = [syncjob]
return self.edit('/edit/syncjob', items, attributes)
def editDomainadmin(self, username, attributes):
"""
Edit an existing domain admin in the mailcow instance.
:param username: Username of the domain admin to edit
:param attributes: Dictionary containing the new domain admin attributes.
"""
items = [username]
return self.edit('/edit/domain-admin', items, attributes)
def edit(self, endpoint, items, attributes):
"""
Make a POST request to edit items in the mailcow API.
:param items: List of items to edit.
:param attributes: Dictionary containing the new attributes for the items.
:return: Response from the mailcow API.
"""
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
headers = {
"Content-Type": "application/json",
"Host": self.host
}
if self.apiKey:
headers["X-Api-Key"] = self.apiKey
data = {
"items": items,
"attr": attributes
}
response = requests.post(
url,
json=data,
headers=headers,
verify=not self.ignore_ssl_errors
)
response.raise_for_status()
return response.json()
# System Functions
def runSyncjob(self, id, force=False):
"""
Run a sync job.
:param id: ID of the sync job to run.
:return: Response from the imapsync script.
"""
creds_path = "/app/sieve.creds"
conn = mysql.connector.connect(**self.db_config)
cursor = conn.cursor(dictionary=True)
with open(creds_path, 'r') as file:
master_user, master_pass = file.read().strip().split(':')
query = ("SELECT * FROM imapsync WHERE id = %s")
cursor.execute(query, (id,))
success = False
syncjob = cursor.fetchone()
if not syncjob:
cursor.close()
conn.close()
return f"Sync job with ID {id} not found."
if syncjob['active'] == 0 and not force:
cursor.close()
conn.close()
return f"Sync job with ID {id} is not active."
enc1_flag = "--tls1" if syncjob['enc1'] == "TLS" else "--ssl1" if syncjob['enc1'] == "SSL" else None
passfile1_path = f"/tmp/passfile1_{id}.txt"
passfile2_path = f"/tmp/passfile2_{id}.txt"
passfile1_cmd = [
"sh", "-c",
f"echo {syncjob['password1']} > {passfile1_path}"
]
passfile2_cmd = [
"sh", "-c",
f"echo {master_pass} > {passfile2_path}"
]
self.docker.exec_command("dovecot-mailcow", passfile1_cmd)
self.docker.exec_command("dovecot-mailcow", passfile2_cmd)
imapsync_cmd = [
"/usr/local/bin/imapsync",
"--tmpdir", "/tmp",
"--nofoldersizes",
"--addheader"
]
if int(syncjob['timeout1']) > 0:
imapsync_cmd.extend(['--timeout1', str(syncjob['timeout1'])])
if int(syncjob['timeout2']) > 0:
imapsync_cmd.extend(['--timeout2', str(syncjob['timeout2'])])
if syncjob['exclude']:
imapsync_cmd.extend(['--exclude', syncjob['exclude']])
if syncjob['subfolder2']:
imapsync_cmd.extend(['--subfolder2', syncjob['subfolder2']])
if int(syncjob['maxage']) > 0:
imapsync_cmd.extend(['--maxage', str(syncjob['maxage'])])
if int(syncjob['maxbytespersecond']) > 0:
imapsync_cmd.extend(['--maxbytespersecond', str(syncjob['maxbytespersecond'])])
if int(syncjob['delete2duplicates']) == 1:
imapsync_cmd.append("--delete2duplicates")
if int(syncjob['subscribeall']) == 1:
imapsync_cmd.append("--subscribeall")
if int(syncjob['delete1']) == 1:
imapsync_cmd.append("--delete")
if int(syncjob['delete2']) == 1:
imapsync_cmd.append("--delete2")
if int(syncjob['automap']) == 1:
imapsync_cmd.append("--automap")
if int(syncjob['skipcrossduplicates']) == 1:
imapsync_cmd.append("--skipcrossduplicates")
if enc1_flag:
imapsync_cmd.append(enc1_flag)
imapsync_cmd.extend([
"--host1", syncjob['host1'],
"--user1", syncjob['user1'],
"--passfile1", passfile1_path,
"--port1", str(syncjob['port1']),
"--host2", "localhost",
"--user2", f"{syncjob['user2']}*{master_user}",
"--passfile2", passfile2_path
])
if syncjob['dry'] == 1:
imapsync_cmd.append("--dry")
imapsync_cmd.extend([
"--no-modulesversion",
"--noreleasecheck"
])
try:
cursor.execute("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = %s", (id,))
conn.commit()
result = self.docker.exec_command("dovecot-mailcow", imapsync_cmd)
print(result)
success = result['status'] == "success" and result['exit_code'] == 0
cursor.execute(
"UPDATE imapsync SET returned_text = %s, success = %s, exit_status = %s WHERE id = %s",
(result['output'], int(success), result['exit_code'], id)
)
conn.commit()
except Exception as e:
cursor.execute(
"UPDATE imapsync SET returned_text = %s, success = 0 WHERE id = %s",
(str(e), id)
)
conn.commit()
finally:
cursor.execute("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = %s", (id,))
conn.commit()
delete_passfile1_cmd = [
"sh", "-c",
f"rm -f {passfile1_path}"
]
delete_passfile2_cmd = [
"sh", "-c",
f"rm -f {passfile2_path}"
]
self.docker.exec_command("dovecot-mailcow", delete_passfile1_cmd)
self.docker.exec_command("dovecot-mailcow", delete_passfile2_cmd)
cursor.close()
conn.close()
return "Sync job completed successfully." if success else "Sync job failed."

View File

@@ -1,64 +0,0 @@
import smtplib
import json
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from jinja2 import Environment, BaseLoader
class Mailer:
def __init__(self, smtp_host, smtp_port, username, password, use_tls=True):
self.smtp_host = smtp_host
self.smtp_port = smtp_port
self.username = username
self.password = password
self.use_tls = use_tls
self.server = None
self.env = Environment(loader=BaseLoader())
def connect(self):
print("Connecting to the SMTP server...")
self.server = smtplib.SMTP(self.smtp_host, self.smtp_port)
if self.use_tls:
self.server.starttls()
print("TLS activated!")
if self.username and self.password:
self.server.login(self.username, self.password)
print("Authenticated!")
def disconnect(self):
if self.server:
try:
if self.server.sock:
self.server.quit()
except smtplib.SMTPServerDisconnected:
pass
finally:
self.server = None
def render_inline_template(self, template_string, context):
template = self.env.from_string(template_string)
return template.render(context)
def send_mail(self, subject, from_addr, to_addrs, template, context = {}):
try:
if template == "":
print("Cannot send email, template is empty!")
return "Failed: Template is empty."
body = self.render_inline_template(template, context)
msg = MIMEMultipart()
msg['From'] = from_addr
msg['To'] = ', '.join(to_addrs) if isinstance(to_addrs, list) else to_addrs
msg['Subject'] = subject
msg.attach(MIMEText(body, 'html'))
self.connect()
self.server.sendmail(from_addr, to_addrs, msg.as_string())
self.disconnect()
return f"Success: Email sent to {msg['To']}"
except Exception as e:
print(f"Error during send_mail: {type(e).__name__}: {e}")
return f"Failed: {type(e).__name__}: {e}"
finally:
self.disconnect()

View File

@@ -1,51 +0,0 @@
from jinja2 import Environment, Template
import csv
def split_at(value, sep, idx):
try:
return value.split(sep)[idx]
except Exception:
return ''
class Reader:
"""
Reader class to handle reading and processing of CSV and JSON files for mailcow.
"""
def __init__(self):
pass
def read_csv(self, file_path, delimiter=',', encoding='iso-8859-1'):
"""
Read a CSV file and return a list of dictionaries.
Each dictionary represents a row in the CSV file.
:param file_path: Path to the CSV file.
:param delimiter: Delimiter used in the CSV file (default: ',').
"""
with open(file_path, mode='r', encoding=encoding) as file:
reader = csv.DictReader(file, delimiter=delimiter)
reader.fieldnames = [h.replace(" ", "_") if h else h for h in reader.fieldnames]
return [row for row in reader]
def map_csv_data(self, data, mapping_file_path, encoding='iso-8859-1'):
"""
Map CSV data to a specific structure based on the provided Jinja2 template file.
:param data: List of dictionaries representing CSV rows.
:param mapping_file_path: Path to the Jinja2 template file.
:return: List of dictionaries with mapped data.
"""
with open(mapping_file_path, 'r', encoding=encoding) as tpl_file:
template_content = tpl_file.read()
env = Environment()
env.filters['split_at'] = split_at
template = env.from_string(template_content)
mapped_data = []
for row in data:
rendered = template.render(**row)
try:
mapped_row = eval(rendered)
except Exception:
mapped_row = rendered
mapped_data.append(mapped_row)
return mapped_data

View File

@@ -1,512 +0,0 @@
import requests
import urllib3
import os
from uuid import uuid4
from collections import defaultdict
class Sogo:
def __init__(self, username, password=""):
self.apiUrl = "/SOGo/so"
self.davUrl = "/SOGo/dav"
self.ignore_ssl_errors = True
self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247:{os.getenv('HTTPS_PORT', '443')}"
self.host = os.getenv("MAILCOW_HOSTNAME", "")
if self.ignore_ssl_errors:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
self.username = username
self.password = password
def addCalendar(self, calendar_name):
"""
Add a new calendar to the sogo instance.
:param calendar_name: Name of the calendar to be created
:return: Response from the sogo API.
"""
res = self.post(f"/{self.username}/Calendar/createFolder", {
"name": calendar_name
})
try:
return res.json()
except ValueError:
return res.text
def getCalendarIdByName(self, calendar_name):
"""
Get the calendar ID by its name.
:param calendar_name: Name of the calendar to find
:return: Calendar ID if found, otherwise None.
"""
res = self.get(f"/{self.username}/Calendar/calendarslist")
try:
for calendar in res.json()["calendars"]:
if calendar['name'] == calendar_name:
return calendar['id']
except ValueError:
return None
return None
def getCalendar(self):
"""
Get calendar list.
:return: Response from SOGo API.
"""
res = self.get(f"/{self.username}/Calendar/calendarslist")
try:
return res.json()
except ValueError:
return res.text
def deleteCalendar(self, calendar_id):
"""
Delete a calendar.
:param calendar_id: ID of the calendar to be deleted
:return: Response from SOGo API.
"""
res = self.get(f"/{self.username}/Calendar/{calendar_id}/delete")
return res.status_code == 204
def importCalendar(self, calendar_name, ics_file):
"""
Import a calendar from an ICS file.
:param calendar_name: Name of the calendar to import into
:param ics_file: Path to the ICS file to import
:return: Response from SOGo API.
"""
try:
with open(ics_file, "rb") as f:
pass
except Exception as e:
print(f"Could not open ICS file '{ics_file}': {e}")
return {"status": "error", "message": str(e)}
new_calendar = self.addCalendar(calendar_name)
selected_calendar = new_calendar.json()["id"]
url = f"{self.baseUrl}{self.apiUrl}/{self.username}/Calendar/{selected_calendar}/import"
auth = (self.username, self.password)
with open(ics_file, "rb") as f:
files = {'icsFile': (ics_file, f, 'text/calendar')}
res = requests.post(
url,
files=files,
auth=auth,
verify=not self.ignore_ssl_errors
)
try:
return res.json()
except ValueError:
return res.text
return None
def setCalendarACL(self, calendar_id, sharee_email, acl="r", subscribe=False):
"""
Set CalDAV calendar permissions for a user (sharee).
:param calendar_id: ID of the calendar to share
:param sharee_email: Email of the user to share with
:param acl: "w" for write, "r" for read-only or combination "rw" for read-write
:param subscribe: True will scubscribe the sharee to the calendar
:return: None
"""
# Access rights
if acl == "" or len(acl) > 2:
return "Invalid acl level specified. Use 'w', 'r' or combinations like 'rw'."
rights = [{
"c_email": sharee_email,
"uid": sharee_email,
"userClass": "normal-user",
"rights": {
"Public": "None",
"Private": "None",
"Confidential": "None",
"canCreateObjects": 0,
"canEraseObjects": 0
}
}]
if "w" in acl:
rights[0]["rights"]["canCreateObjects"] = 1
rights[0]["rights"]["canEraseObjects"] = 1
if "r" in acl:
rights[0]["rights"]["Public"] = "Viewer"
rights[0]["rights"]["Private"] = "Viewer"
rights[0]["rights"]["Confidential"] = "Viewer"
r_add = self.get(f"/{self.username}/Calendar/{calendar_id}/addUserInAcls?uid={sharee_email}")
if r_add.status_code < 200 or r_add.status_code > 299:
try:
return r_add.json()
except ValueError:
return r_add.text
r_save = self.post(f"/{self.username}/Calendar/{calendar_id}/saveUserRights", rights)
if r_save.status_code < 200 or r_save.status_code > 299:
try:
return r_save.json()
except ValueError:
return r_save.text
if subscribe:
r_subscribe = self.get(f"/{self.username}/Calendar/{calendar_id}/subscribeUsers?uids={sharee_email}")
if r_subscribe.status_code < 200 or r_subscribe.status_code > 299:
try:
return r_subscribe.json()
except ValueError:
return r_subscribe.text
return r_save.status_code == 200
def getCalendarACL(self, calendar_id):
"""
Get CalDAV calendar permissions for a user (sharee).
:param calendar_id: ID of the calendar to get ACL from
:return: Response from SOGo API.
"""
res = self.get(f"/{self.username}/Calendar/{calendar_id}/acls")
try:
return res.json()
except ValueError:
return res.text
def deleteCalendarACL(self, calendar_id, sharee_email):
"""
Delete a calendar ACL for a user (sharee).
:param calendar_id: ID of the calendar to delete ACL from
:param sharee_email: Email of the user whose ACL to delete
:return: Response from SOGo API.
"""
res = self.get(f"/{self.username}/Calendar/{calendar_id}/removeUserFromAcls?uid={sharee_email}")
return res.status_code == 204
def addAddressbook(self, addressbook_name):
"""
Add a new addressbook to the sogo instance.
:param addressbook_name: Name of the addressbook to be created
:return: Response from the sogo API.
"""
res = self.post(f"/{self.username}/Contacts/createFolder", {
"name": addressbook_name
})
try:
return res.json()
except ValueError:
return res.text
def getAddressbookIdByName(self, addressbook_name):
"""
Get the addressbook ID by its name.
:param addressbook_name: Name of the addressbook to find
:return: Addressbook ID if found, otherwise None.
"""
res = self.get(f"/{self.username}/Contacts/addressbooksList")
try:
for addressbook in res.json()["addressbooks"]:
if addressbook['name'] == addressbook_name:
return addressbook['id']
except ValueError:
return None
return None
def deleteAddressbook(self, addressbook_id):
"""
Delete an addressbook.
:param addressbook_id: ID of the addressbook to be deleted
:return: Response from SOGo API.
"""
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/delete")
return res.status_code == 204
def getAddressbookList(self):
"""
Get addressbook list.
:return: Response from SOGo API.
"""
res = self.get(f"/{self.username}/Contacts/addressbooksList")
try:
return res.json()
except ValueError:
return res.text
def setAddressbookACL(self, addressbook_id, sharee_email, acl="r", subscribe=False):
"""
Set CalDAV addressbook permissions for a user (sharee).
:param addressbook_id: ID of the addressbook to share
:param sharee_email: Email of the user to share with
:param acl: "w" for write, "r" for read-only or combination "rw" for read-write
:param subscribe: True will subscribe the sharee to the addressbook
:return: None
"""
# Access rights
if acl == "" or len(acl) > 2:
print("Invalid acl level specified. Use 's', 'w', 'r' or combinations like 'rws'.")
return "Invalid acl level specified. Use 'w', 'r' or combinations like 'rw'."
rights = [{
"c_email": sharee_email,
"uid": sharee_email,
"userClass": "normal-user",
"rights": {
"canCreateObjects": 0,
"canEditObjects": 0,
"canEraseObjects": 0,
"canViewObjects": 0,
}
}]
if "w" in acl:
rights[0]["rights"]["canCreateObjects"] = 1
rights[0]["rights"]["canEditObjects"] = 1
rights[0]["rights"]["canEraseObjects"] = 1
if "r" in acl:
rights[0]["rights"]["canViewObjects"] = 1
r_add = self.get(f"/{self.username}/Contacts/{addressbook_id}/addUserInAcls?uid={sharee_email}")
if r_add.status_code < 200 or r_add.status_code > 299:
try:
return r_add.json()
except ValueError:
return r_add.text
r_save = self.post(f"/{self.username}/Contacts/{addressbook_id}/saveUserRights", rights)
if r_save.status_code < 200 or r_save.status_code > 299:
try:
return r_save.json()
except ValueError:
return r_save.text
if subscribe:
r_subscribe = self.get(f"/{self.username}/Contacts/{addressbook_id}/subscribeUsers?uids={sharee_email}")
if r_subscribe.status_code < 200 or r_subscribe.status_code > 299:
try:
return r_subscribe.json()
except ValueError:
return r_subscribe.text
return r_save.status_code == 200
def getAddressbookACL(self, addressbook_id):
"""
Get CalDAV addressbook permissions for a user (sharee).
:param addressbook_id: ID of the addressbook to get ACL from
:return: Response from SOGo API.
"""
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/acls")
try:
return res.json()
except ValueError:
return res.text
def deleteAddressbookACL(self, addressbook_id, sharee_email):
"""
Delete an addressbook ACL for a user (sharee).
:param addressbook_id: ID of the addressbook to delete ACL from
:param sharee_email: Email of the user whose ACL to delete
:return: Response from SOGo API.
"""
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/removeUserFromAcls?uid={sharee_email}")
return res.status_code == 204
def getAddressbookNewGuid(self, addressbook_id):
"""
Request a new GUID for a SOGo addressbook.
:param addressbook_id: ID of the addressbook
:return: JSON response from SOGo or None if not found
"""
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/newguid")
try:
return res.json()
except ValueError:
return res.text
def addAddressbookContact(self, addressbook_id, contact_name, contact_email):
"""
Save a vCard as a contact in the specified addressbook.
:param addressbook_id: ID of the addressbook
:param contact_name: Name of the contact
:param contact_email: Email of the contact
:return: JSON response from SOGo or None if not found
"""
vcard_id = self.getAddressbookNewGuid(addressbook_id)
contact_data = {
"id": vcard_id["id"],
"pid": vcard_id["pid"],
"c_cn": contact_name,
"emails": [{
"type": "pref",
"value": contact_email
}],
"isNew": True,
"c_component": "vcard",
}
endpoint = f"/{self.username}/Contacts/{addressbook_id}/{vcard_id['id']}/saveAsContact"
res = self.post(endpoint, contact_data)
try:
return res.json()
except ValueError:
return res.text
def getAddressbookContacts(self, addressbook_id, contact_email=None):
"""
Get all contacts from the specified addressbook.
:param addressbook_id: ID of the addressbook
:return: JSON response with contacts or None if not found
"""
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/view")
try:
res_json = res.json()
headers = res_json.get("headers", [])
if not headers or len(headers) < 2:
return []
field_names = headers[0]
contacts = []
for row in headers[1:]:
contact = dict(zip(field_names, row))
contacts.append(contact)
if contact_email:
contact = {}
for c in contacts:
if c["c_mail"] == contact_email or c["c_cn"] == contact_email:
contact = c
break
return contact
return contacts
except ValueError:
return res.text
def addAddressbookContactList(self, addressbook_id, contact_name, contact_email=None):
"""
Add a new contact list to the addressbook.
:param addressbook_id: ID of the addressbook
:param contact_name: Name of the contact list
:param contact_email: Comma-separated emails to include in the list
:return: Response from SOGo API.
"""
gal_domain = self.username.split("@")[-1]
vlist_id = self.getAddressbookNewGuid(addressbook_id)
contact_emails = contact_email.split(",") if contact_email else []
contacts = self.getAddressbookContacts(addressbook_id)
refs = []
for contact in contacts:
if contact['c_mail'] in contact_emails:
refs.append({
"refs": [],
"categories": [],
"c_screenname": contact.get("c_screenname", ""),
"pid": contact.get("pid", vlist_id["pid"]),
"id": contact.get("id", ""),
"notes": [""],
"empty": " ",
"hasphoto": contact.get("hasphoto", 0),
"c_cn": contact.get("c_cn", ""),
"c_uid": contact.get("c_uid", None),
"containername": contact.get("containername", f"GAL {gal_domain}"), # or your addressbook name
"sourceid": contact.get("sourceid", gal_domain),
"c_component": contact.get("c_component", "vcard"),
"c_sn": contact.get("c_sn", ""),
"c_givenname": contact.get("c_givenname", ""),
"c_name": contact.get("c_name", contact.get("id", "")),
"c_telephonenumber": contact.get("c_telephonenumber", ""),
"fn": contact.get("fn", ""),
"c_mail": contact.get("c_mail", ""),
"emails": contact.get("emails", []),
"c_o": contact.get("c_o", ""),
"reference": contact.get("id", ""),
"birthday": contact.get("birthday", "")
})
contact_data = {
"refs": refs,
"categories": [],
"c_screenname": None,
"pid": vlist_id["pid"],
"c_component": "vlist",
"notes": [""],
"empty": " ",
"isNew": True,
"id": vlist_id["id"],
"c_cn": contact_name,
"birthday": ""
}
endpoint = f"/{self.username}/Contacts/{addressbook_id}/{vlist_id['id']}/saveAsList"
res = self.post(endpoint, contact_data)
try:
return res.json()
except ValueError:
return res.text
def deleteAddressbookItem(self, addressbook_id, contact_name):
"""
Delete an addressbook item by its ID.
:param addressbook_id: ID of the addressbook item to delete
:param contact_name: Name of the contact to delete
:return: Response from SOGo API.
"""
res = self.getAddressbookContacts(addressbook_id, contact_name)
if "id" not in res:
print(f"Contact '{contact_name}' not found in addressbook '{addressbook_id}'.")
return None
res = self.post(f"/{self.username}/Contacts/{addressbook_id}/batchDelete", {
"uids": [res["id"]],
})
return res.status_code == 204
def get(self, endpoint, params=None):
"""
Make a GET request to the mailcow API.
:param endpoint: The API endpoint to get.
:param params: Optional parameters for the GET request.
:return: Response from the mailcow API.
"""
url = f"{self.baseUrl}{self.apiUrl}{endpoint}"
auth = (self.username, self.password)
headers = {"Host": self.host}
response = requests.get(
url,
params=params,
auth=auth,
headers=headers,
verify=not self.ignore_ssl_errors
)
return response
def post(self, endpoint, data):
"""
Make a POST request to the mailcow API.
:param endpoint: The API endpoint to post to.
:param data: Data to be sent in the POST request.
:return: Response from the mailcow API.
"""
url = f"{self.baseUrl}{self.apiUrl}{endpoint}"
auth = (self.username, self.password)
headers = {"Host": self.host}
response = requests.post(
url,
json=data,
auth=auth,
headers=headers,
verify=not self.ignore_ssl_errors
)
return response

View File

@@ -1,37 +0,0 @@
import json
import random
import string
class Utils:
def __init(self):
pass
def normalize_email(self, email):
replacements = {
"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss",
"Ä": "Ae", "Ö": "Oe", "Ü": "Ue"
}
for orig, repl in replacements.items():
email = email.replace(orig, repl)
return email
def generate_password(self, length=8):
chars = string.ascii_letters + string.digits
return ''.join(random.choices(chars, k=length))
def pprint(self, data=""):
"""
Pretty print a dictionary, list, or text.
If data is a text containing JSON, it will be printed in a formatted way.
"""
if isinstance(data, (dict, list)):
print(json.dumps(data, indent=2, ensure_ascii=False))
elif isinstance(data, str):
try:
json_data = json.loads(data)
print(json.dumps(json_data, indent=2, ensure_ascii=False))
except json.JSONDecodeError:
print(data)
else:
print(data)

View File

@@ -1,4 +0,0 @@
jinja2
requests
mysql-connector-python
pytest

View File

@@ -1,94 +0,0 @@
import pytest
import json
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from models.DomainModel import DomainModel
from models.AliasModel import AliasModel
def test_model():
# Generate random alias
random_alias = f"alias_test{os.urandom(4).hex()}@mailcow.local"
# Create an instance of AliasModel
model = AliasModel(
address=random_alias,
goto="test@mailcow.local,test2@mailcow.local"
)
# Test the parser_command attribute
assert model.parser_command == "alias", "Parser command should be 'alias'"
# add Domain for testing
domain_model = DomainModel(domain="mailcow.local")
domain_model.add()
# 1. Alias add tests, should success
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[0]['msg'][0] == "alias_added", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'alias_added'\n{json.dumps(r_add, indent=2)}"
# Assign created alias ID for further tests
model.id = r_add[0]['msg'][2]
# 2. Alias add tests, should fail because the alias already exists
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[0]['msg'][0] == "is_alias_or_mailbox", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'is_alias_or_mailbox'\n{json.dumps(r_add, indent=2)}"
# 3. Alias get tests
r_get = model.get()
assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}"
assert "domain" in r_get, f"'domain' key missing in response: {json.dumps(r_get, indent=2)}"
assert "goto" in r_get, f"'goto' key missing in response: {json.dumps(r_get, indent=2)}"
assert "address" in r_get, f"'address' key missing in response: {json.dumps(r_get, indent=2)}"
assert r_get['domain'] == model.address.split("@")[1], f"Wrong 'domain' received: {r_get['domain']}, expected: {model.address.split('@')[1]}\n{json.dumps(r_get, indent=2)}"
assert r_get['goto'] == model.goto, f"Wrong 'goto' received: {r_get['goto']}, expected: {model.goto}\n{json.dumps(r_get, indent=2)}"
assert r_get['address'] == model.address, f"Wrong 'address' received: {r_get['address']}, expected: {model.address}\n{json.dumps(r_get, indent=2)}"
# 4. Alias edit tests
model.goto = "test@mailcow.local"
model.active = 0
r_edit = model.edit()
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['msg'][0] == "alias_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'alias_modified'\n{json.dumps(r_edit, indent=2)}"
# 5. Alias delete tests
r_delete = model.delete()
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['msg'][0] == "alias_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'alias_removed'\n{json.dumps(r_delete, indent=2)}"
# delete testing Domain
domain_model.delete()
if __name__ == "__main__":
print("Running AliasModel tests...")
test_model()
print("All tests passed!")

View File

@@ -1,71 +0,0 @@
import pytest
from models.BaseModel import BaseModel
class Args:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def test_has_required_args():
BaseModel.required_args = {
"test_object": [["arg1"], ["arg2", "arg3"]],
}
# Test cases with Args object
args = Args(object="non_existent_object")
assert BaseModel.has_required_args(args) == False
args = Args(object="test_object")
assert BaseModel.has_required_args(args) == False
args = Args(object="test_object", arg1="value")
assert BaseModel.has_required_args(args) == True
args = Args(object="test_object", arg2="value")
assert BaseModel.has_required_args(args) == False
args = Args(object="test_object", arg3="value")
assert BaseModel.has_required_args(args) == False
args = Args(object="test_object", arg2="value", arg3="value")
assert BaseModel.has_required_args(args) == True
# Test cases with dict object
args = {"object": "non_existent_object"}
assert BaseModel.has_required_args(args) == False
args = {"object": "test_object"}
assert BaseModel.has_required_args(args) == False
args = {"object": "test_object", "arg1": "value"}
assert BaseModel.has_required_args(args) == True
args = {"object": "test_object", "arg2": "value"}
assert BaseModel.has_required_args(args) == False
args = {"object": "test_object", "arg3": "value"}
assert BaseModel.has_required_args(args) == False
args = {"object": "test_object", "arg2": "value", "arg3": "value"}
assert BaseModel.has_required_args(args) == True
BaseModel.required_args = {
"test_object": [[]],
}
# Test cases with Args object
args = Args(object="non_existent_object")
assert BaseModel.has_required_args(args) == False
args = Args(object="test_object")
assert BaseModel.has_required_args(args) == True
# Test cases with dict object
args = {"object": "non_existent_object"}
assert BaseModel.has_required_args(args) == False
args = {"object": "test_object"}
assert BaseModel.has_required_args(args) == True

View File

@@ -1,74 +0,0 @@
import pytest
import json
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from models.DomainModel import DomainModel
def test_model():
# Create an instance of DomainModel
model = DomainModel(
domain="mailcow.local",
)
# Test the parser_command attribute
assert model.parser_command == "domain", "Parser command should be 'domain'"
# 1. Domain add tests, should success
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0 and len(r_add) >= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[1], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[1]['type'] == "success", f"Wrong 'type' received: {r_add[1]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[1], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[1]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[1]['msg']) > 0 and len(r_add[1]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[1]['msg'][0] == "domain_added", f"Wrong 'msg' received: {r_add[1]['msg'][0]}, expected: 'domain_added'\n{json.dumps(r_add, indent=2)}"
# 2. Domain add tests, should fail because the domain already exists
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[0]['msg'][0] == "domain_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'domain_exists'\n{json.dumps(r_add, indent=2)}"
# 3. Domain get tests
r_get = model.get()
assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}"
assert "domain_name" in r_get, f"'domain_name' key missing in response: {json.dumps(r_get, indent=2)}"
assert r_get['domain_name'] == model.domain, f"Wrong 'domain_name' received: {r_get['domain_name']}, expected: {model.domain}\n{json.dumps(r_get, indent=2)}"
# 4. Domain edit tests
model.active = 0
r_edit = model.edit()
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['msg'][0] == "domain_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'domain_modified'\n{json.dumps(r_edit, indent=2)}"
# 5. Domain delete tests
r_delete = model.delete()
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['msg'][0] == "domain_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'domain_removed'\n{json.dumps(r_delete, indent=2)}"
if __name__ == "__main__":
print("Running DomainModel tests...")
test_model()
print("All tests passed!")

View File

@@ -1,89 +0,0 @@
import pytest
import json
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from models.DomainModel import DomainModel
from models.DomainadminModel import DomainadminModel
def test_model():
# Generate random domainadmin
random_username = f"dadmin_test{os.urandom(4).hex()}"
random_password = f"{os.urandom(4).hex()}"
# Create an instance of DomainadminModel
model = DomainadminModel(
username=random_username,
password=random_password,
domains="mailcow.local",
)
# Test the parser_command attribute
assert model.parser_command == "domainadmin", "Parser command should be 'domainadmin'"
# add Domain for testing
domain_model = DomainModel(domain="mailcow.local")
domain_model.add()
# 1. Domainadmin add tests, should success
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[0]['msg'][0] == "domain_admin_added", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'domain_admin_added'\n{json.dumps(r_add, indent=2)}"
# 2. Domainadmin add tests, should fail because the domainadmin already exists
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}"
# 3. Domainadmin get tests
r_get = model.get()
assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}"
assert "selected_domains" in r_get, f"'selected_domains' key missing in response: {json.dumps(r_get, indent=2)}"
assert "username" in r_get, f"'username' key missing in response: {json.dumps(r_get, indent=2)}"
assert set(model.domains.replace(" ", "").split(",")) == set(r_get['selected_domains']), f"Wrong 'selected_domains' received: {r_get['selected_domains']}, expected: {model.domains}\n{json.dumps(r_get, indent=2)}"
assert r_get['username'] == model.username, f"Wrong 'username' received: {r_get['username']}, expected: {model.username}\n{json.dumps(r_get, indent=2)}"
# 4. Domainadmin edit tests
model.active = 0
r_edit = model.edit()
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['msg'][0] == "domain_admin_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'domain_admin_modified'\n{json.dumps(r_edit, indent=2)}"
# 5. Domainadmin delete tests
r_delete = model.delete()
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['msg'][0] == "domain_admin_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'domain_admin_removed'\n{json.dumps(r_delete, indent=2)}"
# delete testing Domain
domain_model.delete()
if __name__ == "__main__":
print("Running DomainadminModel tests...")
test_model()
print("All tests passed!")

View File

@@ -1,89 +0,0 @@
import pytest
import json
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from models.DomainModel import DomainModel
from models.MailboxModel import MailboxModel
def test_model():
# Generate random mailbox
random_username = f"mbox_test{os.urandom(4).hex()}@mailcow.local"
random_password = f"{os.urandom(4).hex()}"
# Create an instance of MailboxModel
model = MailboxModel(
username=random_username,
password=random_password
)
# Test the parser_command attribute
assert model.parser_command == "mailbox", "Parser command should be 'mailbox'"
# add Domain for testing
domain_model = DomainModel(domain="mailcow.local")
domain_model.add()
# 1. Mailbox add tests, should success
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0 and len(r_add) <= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[1], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[1]['type'] == "success", f"Wrong 'type' received: {r_add[1]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[1], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[1]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[1]['msg']) > 0 and len(r_add[1]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[1]['msg'][0] == "mailbox_added", f"Wrong 'msg' received: {r_add[1]['msg'][0]}, expected: 'mailbox_added'\n{json.dumps(r_add, indent=2)}"
# 2. Mailbox add tests, should fail because the mailbox already exists
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}"
# 3. Mailbox get tests
r_get = model.get()
assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}"
assert "domain" in r_get, f"'domain' key missing in response: {json.dumps(r_get, indent=2)}"
assert "local_part" in r_get, f"'local_part' key missing in response: {json.dumps(r_get, indent=2)}"
assert r_get['domain'] == model.domain, f"Wrong 'domain' received: {r_get['domain']}, expected: {model.domain}\n{json.dumps(r_get, indent=2)}"
assert r_get['local_part'] == model.local_part, f"Wrong 'local_part' received: {r_get['local_part']}, expected: {model.local_part}\n{json.dumps(r_get, indent=2)}"
# 4. Mailbox edit tests
model.active = 0
r_edit = model.edit()
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_edit, indent=2)}"
# 5. Mailbox delete tests
r_delete = model.delete()
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['msg'][0] == "mailbox_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'mailbox_removed'\n{json.dumps(r_delete, indent=2)}"
# delete testing Domain
domain_model.delete()
if __name__ == "__main__":
print("Running MailboxModel tests...")
test_model()
print("All tests passed!")

View File

@@ -1,39 +0,0 @@
import pytest
import json
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from models.StatusModel import StatusModel
def test_model():
# Create an instance of StatusModel
model = StatusModel()
# Test the parser_command attribute
assert model.parser_command == "status", "Parser command should be 'status'"
# 1. Status version tests
r_version = model.version()
assert isinstance(r_version, dict), f"Expected a dict but received: {json.dumps(r_version, indent=2)}"
assert "version" in r_version, f"'version' key missing in response: {json.dumps(r_version, indent=2)}"
# 2. Status vmail tests
r_vmail = model.vmail()
assert isinstance(r_vmail, dict), f"Expected a dict but received: {json.dumps(r_vmail, indent=2)}"
assert "type" in r_vmail, f"'type' key missing in response: {json.dumps(r_vmail, indent=2)}"
assert "disk" in r_vmail, f"'disk' key missing in response: {json.dumps(r_vmail, indent=2)}"
assert "used" in r_vmail, f"'used' key missing in response: {json.dumps(r_vmail, indent=2)}"
assert "total" in r_vmail, f"'total' key missing in response: {json.dumps(r_vmail, indent=2)}"
assert "used_percent" in r_vmail, f"'used_percent' key missing in response: {json.dumps(r_vmail, indent=2)}"
# 3. Status containers tests
r_containers = model.containers()
assert isinstance(r_containers, dict), f"Expected a dict but received: {json.dumps(r_containers, indent=2)}"
if __name__ == "__main__":
print("Running StatusModel tests...")
test_model()
print("All tests passed!")

View File

@@ -1,106 +0,0 @@
import pytest
import json
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from models.DomainModel import DomainModel
from models.MailboxModel import MailboxModel
from models.SyncjobModel import SyncjobModel
def test_model():
# Generate random Mailbox
random_username = f"mbox_test@mailcow.local"
random_password = f"{os.urandom(4).hex()}"
# Create an instance of SyncjobModel
model = SyncjobModel(
username=random_username,
host1="mailcow.local",
port1=993,
user1="testuser@mailcow.local",
password1="testpassword",
enc1="SSL",
)
# Test the parser_command attribute
assert model.parser_command == "syncjob", "Parser command should be 'syncjob'"
# add Domain and Mailbox for testing
domain_model = DomainModel(domain="mailcow.local")
domain_model.add()
mbox_model = MailboxModel(username=random_username, password=random_password)
mbox_model.add()
# 1. Syncjob add tests, should success
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0 and len(r_add) <= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_add, indent=2)}"
# Assign created syncjob ID for further tests
model.id = r_add[0]['msg'][2]
# 2. Syncjob add tests, should fail because the syncjob already exists
r_add = model.add()
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}"
# 3. Syncjob get tests
r_get = model.get()
assert isinstance(r_get, list), f"Expected a list but received: {json.dumps(r_get, indent=2)}"
assert "user2" in r_get[0], f"'user2' key missing in response: {json.dumps(r_get, indent=2)}"
assert "host1" in r_get[0], f"'host1' key missing in response: {json.dumps(r_get, indent=2)}"
assert "port1" in r_get[0], f"'port1' key missing in response: {json.dumps(r_get, indent=2)}"
assert "user1" in r_get[0], f"'user1' key missing in response: {json.dumps(r_get, indent=2)}"
assert "enc1" in r_get[0], f"'enc1' key missing in response: {json.dumps(r_get, indent=2)}"
assert r_get[0]['user2'] == model.username, f"Wrong 'user2' received: {r_get[0]['user2']}, expected: {model.username}\n{json.dumps(r_get, indent=2)}"
assert r_get[0]['host1'] == model.host1, f"Wrong 'host1' received: {r_get[0]['host1']}, expected: {model.host1}\n{json.dumps(r_get, indent=2)}"
assert r_get[0]['port1'] == model.port1, f"Wrong 'port1' received: {r_get[0]['port1']}, expected: {model.port1}\n{json.dumps(r_get, indent=2)}"
assert r_get[0]['user1'] == model.user1, f"Wrong 'user1' received: {r_get[0]['user1']}, expected: {model.user1}\n{json.dumps(r_get, indent=2)}"
assert r_get[0]['enc1'] == model.enc1, f"Wrong 'enc1' received: {r_get[0]['enc1']}, expected: {model.enc1}\n{json.dumps(r_get, indent=2)}"
# 4. Syncjob edit tests
model.active = 1
r_edit = model.edit()
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
assert r_edit[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_edit, indent=2)}"
# 5. Syncjob delete tests
r_delete = model.delete()
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
assert r_delete[0]['msg'][0] == "deleted_syncjob", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'deleted_syncjob'\n{json.dumps(r_delete, indent=2)}"
# delete testing Domain and Mailbox
mbox_model.delete()
domain_model.delete()
if __name__ == "__main__":
print("Running SyncjobModel tests...")
test_model()
print("All tests passed!")

View File

@@ -1,8 +0,0 @@
#!/bin/bash
printf "READY\n";
while read line; do
echo "Processing Event: $line" >&2;
kill -3 $(cat "/var/run/supervisord.pid")
done < /dev/stdin

View File

@@ -1,17 +0,0 @@
[supervisord]
nodaemon=true
user=root
pidfile=/var/run/supervisord.pid
[program:api]
command=python /app/api/main.py
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
[eventlistener:processes]
command=/usr/local/sbin/stop-supervisor.sh
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL

View File

@@ -6,29 +6,22 @@ ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app
RUN apk add --update --no-cache python3 \
bash \
py3-pip \
openssl \
tzdata \
py3-psutil \
py3-redis \
py3-async-timeout \
supervisor \
curl \
&& pip3 install --upgrade pip \
fastapi \
uvicorn \
aiodocker \
docker
COPY mailcow-adm/ /app/mailcow-adm/
RUN pip3 install -r /app/mailcow-adm/requirements.txt
COPY api/ /app/api/
RUN mkdir /app/modules
COPY docker-entrypoint.sh /app/
COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
COPY main.py /app/main.py
COPY modules/ /app/modules/
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
CMD ["python", "main.py"]

View File

@@ -0,0 +1,9 @@
#!/bin/bash
`openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
-keyout /app/dockerapi_key.pem \
-out /app/dockerapi_cert.pem \
-subj /CN=dockerapi/O=mailcow \
-addext subjectAltName=DNS:dockerapi`
exec "$@"

View File

@@ -254,8 +254,8 @@ if __name__ == '__main__':
app,
host="0.0.0.0",
port=443,
ssl_certfile="/app/controller_cert.pem",
ssl_keyfile="/app/controller_key.pem",
ssl_certfile="/app/dockerapi_cert.pem",
ssl_keyfile="/app/dockerapi_key.pem",
log_level="info",
loop="none"
)

View File

@@ -3,7 +3,7 @@ FROM alpine:3.21
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.19
ARG GOSU_VERSION=1.17
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8

View File

@@ -204,17 +204,16 @@ EOF
# Create random master Password for SOGo SSO
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
# Creating additional creds file for SOGo notify crons (calendars, etc)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
cat <<EOF > /etc/dovecot/sogo-sso.conf
# Autogenerated by mailcow
passdb {
driver = static
args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
}
EOF
# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
cat <<'EOF' > /usr/local/bin/quota_notify.py

View File

@@ -25,11 +25,11 @@ sed -i -e 's/\([^\\]\)\$\([^\/]\)/\1\\$\2/g' /etc/rspamd/custom/sa-rules
if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then
CONTAINER_NAME=rspamd-mailcow
CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \
CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \
jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \
jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
if [[ ! -z ${CONTAINER_ID} ]]; then
curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
fi
fi

View File

@@ -3,15 +3,15 @@ FROM php:8.2-fpm-alpine3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.28
ARG APCU_PECL_VERSION=5.1.27
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.8.1
ARG IMAGICK_PECL_VERSION=3.8.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.9
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.4.0
ARG MEMCACHED_PECL_VERSION=3.3.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.3.0
ARG REDIS_PECL_VERSION=6.2.0
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.8.6

View File

@@ -32,7 +32,7 @@ session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'"
# Check mysql_upgrade (master and slave)
CONTAINER_ID=
until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do
CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
echo "Could not get mysql-mailcow container id... trying again"
sleep 2
done
@@ -44,7 +44,7 @@ until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do
echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)"
break
fi
SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json')
SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json')
SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type)
SQL_LOOP_C=$((SQL_LOOP_C+1))
echo "SQL upgrade iteration #${SQL_LOOP_C}"
@@ -69,12 +69,12 @@ done
# doing post-installation stuff, if SQL was upgraded (master and slave)
if [ ${SQL_CHANGED} -eq 1 ]; then
POSTFIX=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then
echo "Could not determine Postfix container ID, skipping Postfix restart."
else
echo "Restarting Postfix"
curl -X POST --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
echo "Sleeping 5 seconds..."
sleep 5
fi
@@ -83,7 +83,7 @@ fi
# Check mysql tz import (master and slave)
TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
echo "MySQL mysql_tzinfo_to_sql - debug output:"
echo ${SQL_FULL_TZINFO_IMPORT_RETURN}
fi
@@ -167,7 +167,7 @@ DELIMITER //
CREATE EVENT clean_spamalias
ON SCHEDULE EVERY 1 DAY DO
BEGIN
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
END;
//
DELIMITER ;

View File

@@ -4,7 +4,7 @@ WORKDIR /src
ENV CGO_ENABLED=0 \
GO111MODULE=on \
NOOPT=1 \
VERSION=1.8.22
VERSION=1.8.14
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
cd /src/postfix-tlspol && \

View File

@@ -329,17 +329,14 @@ query = SELECT goto FROM alias
SELECT id FROM alias
WHERE address='%s'
AND (active='1' OR active='2')
AND sender_allowed='1'
), (
SELECT id FROM alias
WHERE address='@%d'
AND (active='1' OR active='2')
AND sender_allowed='1'
)
)
)
AND active='1'
AND sender_allowed='1'
AND (domain IN
(SELECT domain FROM domain
WHERE domain='%d'
@@ -393,7 +390,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
dbname = ${DBNAME}
query = SELECT goto FROM spamalias
WHERE address='%s'
AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
AND validity >= UNIX_TIMESTAMP()
EOF
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
@@ -527,4 +524,4 @@ if [[ $? != 0 ]]; then
else
postfix -c /opt/postfix/conf start
sleep 126144000
fi
fi

View File

@@ -1,9 +1,9 @@
FROM debian:trixie-slim
FROM debian:bookworm-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.14.2-82~90302bc
ARG CODENAME=trixie
ARG RSPAMD_VER=rspamd_3.13.2-1~8bf602278
ARG CODENAME=bookworm
ENV LC_ALL=C
RUN apt-get update && apt-get install -y --no-install-recommends \

View File

@@ -6,7 +6,7 @@ ARG DEBIAN_FRONTEND=noninteractive
ARG DEBIAN_VERSION=bookworm
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.19
ARG GOSU_VERSION=1.17
ENV LC_ALL=C
# Prerequisites

View File

@@ -50,6 +50,10 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>YES</string>
<key>SOGoEncryptionKey</key>
<string>${RAND_PASS}</string>
<key>SOGoURLEncryptionEnabled</key>
<string>YES</string>
<key>SOGoURLEncryptionPassphrase</key>
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
<key>OCSAdminURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
<key>OCSCacheFolderURL</key>

View File

@@ -200,12 +200,12 @@ get_container_ip() {
else
sleep 0.5
# get long container id for exact match
CONTAINER_ID=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id"))
CONTAINER_ID=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id"))
# returned id can have multiple elements (if scaled), shuffle for random test
CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
if [[ ! -z ${CONTAINER_ID} ]]; then
for matched_container in "${CONTAINER_ID[@]}"; do
CONTAINER_IPS=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
CONTAINER_IPS=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
for ip_match in "${CONTAINER_IPS[@]}"; do
# grep will do nothing if one of these vars is empty
[[ -z ${ip_match} ]] && continue
@@ -1075,15 +1075,15 @@ while true; do
done
) &
# Monitor controller
# Monitor dockerapi
(
while true; do
while nc -z controller 443; do
while nc -z dockerapi 443; do
sleep 3
done
log_msg "Cannot find controller-mailcow, waiting to recover..."
log_msg "Cannot find dockerapi-mailcow, waiting to recover..."
kill -STOP ${BACKGROUND_TASKS[*]}
until nc -z controller 443; do
until nc -z dockerapi 443; do
sleep 3
done
kill -CONT ${BACKGROUND_TASKS[*]}
@@ -1143,12 +1143,12 @@ while true; do
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
kill -STOP ${BACKGROUND_TASKS[*]}
sleep 10
CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
if [[ ! -z ${CONTAINER_ID} ]]; then
if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
HAS_INITDB=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
fi
S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
if [ ${S_RUNNING} -lt 360 ]; then
log_msg "Container is running for less than 360 seconds, skipping action..."
elif [[ ! -z ${HAS_INITDB} ]]; then
@@ -1156,7 +1156,7 @@ while true; do
sleep 60
else
log_msg "Sending restart command to ${CONTAINER_ID}..."
curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
notify_error "${com_pipe_answer}"
log_msg "Wait for restarted container to settle and continue watching..."
sleep 35

View File

@@ -80,21 +80,14 @@ if ($isSOGoRequest) {
}
if ($result === false){
// If it's a SOGo Request, don't check for protocol access
if ($isSOGoRequest) {
$service = 'SOGO';
$post['service'] = 'NONE';
} else {
$service = $post['service'];
}
$result = apppass_login($post['username'], $post['password'], array(
'service' => $post['service'],
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
$result = apppass_login($post['username'], $post['password'], $service, array(
'is_internal' => true,
'remote_addr' => $post['real_rip']
));
if ($result) {
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']);
set_sasl_log($post['username'], $post['real_rip'], $service);
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
}
}
if ($result === false){

View File

@@ -13,7 +13,6 @@ events {
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '

View File

@@ -14,6 +14,7 @@ ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=15768000;";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header X-Download-Options noopen;
add_header X-Frame-Options "SAMEORIGIN" always;
@@ -185,7 +186,6 @@ location ^~ /Microsoft-Server-ActiveSync {
auth_request_set $user $upstream_http_x_user;
auth_request_set $auth $upstream_http_x_auth;
auth_request_set $auth_type $upstream_http_x_auth_type;
auth_request_set $real_ip $remote_addr;
proxy_set_header x-webobjects-remote-user "$user";
proxy_set_header Authorization "$auth";
proxy_set_header x-webobjects-auth-type "$auth_type";
@@ -211,7 +211,6 @@ location ^~ /SOGo {
auth_request_set $user $upstream_http_x_user;
auth_request_set $auth $upstream_http_x_auth;
auth_request_set $auth_type $upstream_http_x_auth_type;
auth_request_set $real_ip $remote_addr;
proxy_set_header x-webobjects-remote-user "$user";
proxy_set_header Authorization "$auth";
proxy_set_header x-webobjects-auth-type "$auth_type";
@@ -234,7 +233,6 @@ location ^~ /SOGo {
auth_request_set $user $upstream_http_x_user;
auth_request_set $auth $upstream_http_x_auth;
auth_request_set $auth_type $upstream_http_x_auth_type;
auth_request_set $real_ip $remote_addr;
proxy_set_header x-webobjects-remote-user "$user";
proxy_set_header Authorization "$auth";
proxy_set_header x-webobjects-auth-type "$auth_type";

View File

@@ -66,7 +66,7 @@ $_SESSION['acl']['tls_policy'] = "1";
$_SESSION['acl']['quarantine_notification'] = "1";
$_SESSION['acl']['quarantine_category'] = "1";
$_SESSION['acl']['ratelimit'] = "1";
$_SESSION['acl']['sogo_access'] = "1";
$_SESSION['acl']['sogo_redirection'] = "1";
$_SESSION['acl']['protocol_access'] = "1";
$_SESSION['acl']['mailbox_relayhost'] = "1";
$_SESSION['acl']['unlimited_quota'] = "1";

View File

@@ -66,7 +66,7 @@ $_SESSION['acl']['tls_policy'] = "1";
$_SESSION['acl']['quarantine_notification'] = "1";
$_SESSION['acl']['quarantine_category'] = "1";
$_SESSION['acl']['ratelimit'] = "1";
$_SESSION['acl']['sogo_access'] = "1";
$_SESSION['acl']['sogo_redirection'] = "1";
$_SESSION['acl']['protocol_access'] = "1";
$_SESSION['acl']['mailbox_relayhost'] = "1";
$_SESSION['acl']['unlimited_quota'] = "1";

View File

@@ -7,10 +7,9 @@ opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.memory_consumption=128
opcache.save_comments=1
opcache.revalidate_freq=120
opcache.validate_timestamps=0
; JIT
; Disabled for now due to some PHP segmentation faults observed
; in certain environments. Possibly some PHP or PHP extension bug.
opcache.jit=disable
opcache.jit_buffer_size=0
opcache.jit=1255
opcache.jit_buffer_size=8M

View File

@@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Thu Jan 1 00:24:01 UTC 2026
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
# https://github.com/stevejenkins/postwhite/
# 2105 total rules
# 2216 total rules
2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit
2a01:111:f403:2800::/53 permit
@@ -29,9 +29,7 @@
2a01:b747:3005:200::/56 permit
2a01:b747:3006:200::/56 permit
2a02:a60:0:5::/64 permit
2a0f:f640::/56 permit
2c0f:fb50:4000::/36 permit
2.207.151.53 permit
2.207.217.30 permit
3.64.237.68 permit
3.65.3.180 permit
@@ -52,11 +50,14 @@
8.25.194.0/23 permit
8.25.196.0/23 permit
8.36.116.0/24 permit
8.39.54.0/23 permit
8.39.54.250/31 permit
8.39.144.0/24 permit
8.40.222.0/23 permit
8.40.222.250/31 permit
12.130.86.238 permit
13.107.213.38 permit
13.107.246.38 permit
13.108.16.0/20 permit
13.107.213.41 permit
13.107.246.41 permit
13.110.208.0/21 permit
13.110.209.0/24 permit
13.110.216.0/22 permit
@@ -168,6 +169,7 @@
34.215.104.144 permit
34.218.115.239 permit
34.225.212.172 permit
34.241.242.183 permit
35.83.148.184 permit
35.155.198.111 permit
35.158.23.94 permit
@@ -191,6 +193,7 @@
40.233.64.216 permit
40.233.83.78 permit
40.233.88.28 permit
43.239.212.33 permit
44.206.138.57 permit
44.210.169.44 permit
44.217.45.156 permit
@@ -272,6 +275,7 @@
50.112.246.219 permit
52.1.14.157 permit
52.5.230.59 permit
52.6.74.205 permit
52.12.53.23 permit
52.13.214.179 permit
52.26.1.71 permit
@@ -295,6 +299,15 @@
52.94.124.0/28 permit
52.95.48.152/29 permit
52.95.49.88/29 permit
52.96.91.34 permit
52.96.111.82 permit
52.96.172.98 permit
52.96.214.50 permit
52.96.222.194 permit
52.96.222.226 permit
52.96.223.2 permit
52.96.228.130 permit
52.96.229.242 permit
52.100.0.0/15 permit
52.102.0.0/16 permit
52.103.0.0/17 permit
@@ -328,6 +341,7 @@
54.244.54.130 permit
54.244.242.0/24 permit
54.255.61.23 permit
56.124.6.228 permit
57.103.64.0/18 permit
57.129.93.249 permit
62.13.128.0/24 permit
@@ -388,11 +402,31 @@
64.207.219.143 permit
64.233.160.0/19 permit
65.52.80.137 permit
65.54.51.64/26 permit
65.54.61.64/26 permit
65.54.121.120/29 permit
65.54.190.0/24 permit
65.54.241.0/24 permit
65.55.29.77 permit
65.55.33.64/28 permit
65.55.34.0/24 permit
65.55.42.224/28 permit
65.55.52.224/27 permit
65.55.78.128/25 permit
65.55.81.48/28 permit
65.55.90.0/24 permit
65.55.94.0/25 permit
65.55.111.0/24 permit
65.55.113.64/26 permit
65.55.116.0/25 permit
65.55.126.0/25 permit
65.55.174.0/25 permit
65.55.178.128/27 permit
65.55.234.192/26 permit
65.110.161.77 permit
65.123.29.213 permit
65.123.29.220 permit
65.154.166.0/24 permit
65.212.180.36 permit
66.102.0.0/20 permit
66.119.150.192/26 permit
@@ -509,6 +543,7 @@
69.169.224.0/20 permit
69.171.232.0/24 permit
69.171.244.0/23 permit
70.37.151.128/25 permit
70.42.149.35 permit
72.3.185.0/24 permit
72.14.192.0/18 permit
@@ -605,6 +640,7 @@
74.208.4.220 permit
74.208.4.221 permit
74.209.250.0/24 permit
75.2.70.75 permit
76.223.128.0/19 permit
76.223.176.0/20 permit
77.238.176.0/24 permit
@@ -700,6 +736,8 @@
91.198.2.0/24 permit
91.211.240.0/22 permit
94.236.119.0/26 permit
94.245.112.0/27 permit
94.245.112.10/31 permit
95.131.104.0/21 permit
95.217.114.154 permit
96.43.144.0/20 permit
@@ -1192,8 +1230,11 @@
98.139.245.208/30 permit
98.139.245.212/31 permit
99.78.197.208/28 permit
99.83.190.102 permit
103.9.96.0/22 permit
103.28.42.0/24 permit
103.84.217.238 permit
103.89.75.238 permit
103.151.192.0/23 permit
103.168.172.128/27 permit
103.237.104.0/22 permit
@@ -1337,6 +1378,11 @@
108.179.144.0/20 permit
109.224.244.0/24 permit
109.237.142.0/24 permit
111.221.23.128/25 permit
111.221.26.0/27 permit
111.221.66.0/25 permit
111.221.69.128/25 permit
111.221.112.0/21 permit
112.19.199.64/29 permit
112.19.242.64/29 permit
116.214.12.47 permit
@@ -1354,6 +1400,9 @@
117.120.16.0/21 permit
119.42.242.52/31 permit
119.42.242.156 permit
121.244.91.48 permit
121.244.91.52 permit
122.15.156.182 permit
123.126.78.64/29 permit
124.108.96.24/31 permit
124.108.96.28/31 permit
@@ -1381,7 +1430,6 @@
128.245.248.0/21 permit
129.41.77.70 permit
129.41.169.249 permit
129.77.16.0/20 permit
129.80.5.164 permit
129.80.64.36 permit
129.80.67.121 permit
@@ -1398,7 +1446,6 @@
129.153.194.228 permit
129.154.255.129 permit
129.158.56.255 permit
129.158.62.153 permit
129.159.22.159 permit
129.159.87.137 permit
129.213.195.191 permit
@@ -1419,8 +1466,21 @@
134.170.141.64/26 permit
134.170.143.0/24 permit
134.170.174.0/24 permit
135.84.80.0/24 permit
135.84.81.0/24 permit
135.84.82.0/24 permit
135.84.83.0/24 permit
135.84.216.0/22 permit
136.146.128.0/20 permit
136.143.160.0/24 permit
136.143.161.0/24 permit
136.143.162.0/24 permit
136.143.176.0/24 permit
136.143.177.0/24 permit
136.143.178.49 permit
136.143.182.0/23 permit
136.143.184.0/24 permit
136.143.188.0/24 permit
136.143.190.0/23 permit
136.147.128.0/20 permit
136.147.135.0/24 permit
136.147.176.0/20 permit
@@ -1435,10 +1495,9 @@
139.138.46.219 permit
139.138.57.55 permit
139.138.58.119 permit
139.167.79.86 permit
139.180.17.0/24 permit
140.238.148.191 permit
141.148.55.217 permit
141.148.91.244 permit
141.148.159.229 permit
141.193.32.0/23 permit
141.193.184.32/27 permit
@@ -1484,7 +1543,6 @@
149.72.234.184 permit
149.72.248.236 permit
149.97.173.180 permit
150.136.21.199 permit
150.230.98.160 permit
151.145.38.14 permit
152.67.105.195 permit
@@ -1494,7 +1552,20 @@
155.248.220.138 permit
155.248.234.149 permit
155.248.237.141 permit
157.55.0.192/26 permit
157.55.1.128/26 permit
157.55.2.0/25 permit
157.55.9.128/25 permit
157.55.11.0/25 permit
157.55.49.0/25 permit
157.55.61.0/24 permit
157.55.157.128/25 permit
157.55.225.0/25 permit
157.56.24.0/25 permit
157.56.120.128/26 permit
157.56.232.0/21 permit
157.56.240.0/20 permit
157.56.248.0/21 permit
157.58.30.128/25 permit
157.58.196.96/29 permit
157.58.249.3 permit
@@ -1544,12 +1615,12 @@
163.114.135.16 permit
163.116.128.0/17 permit
163.192.116.87 permit
163.192.125.176 permit
163.192.196.146 permit
163.192.204.161 permit
164.152.23.32 permit
164.152.25.241 permit
164.177.132.168/30 permit
165.173.128.0/24 permit
165.173.180.250/31 permit
165.173.182.250/31 permit
166.78.68.0/22 permit
166.78.68.221 permit
166.78.69.169 permit
@@ -1579,12 +1650,25 @@
168.245.12.252 permit
168.245.46.9 permit
168.245.127.231 permit
170.9.232.254 permit
169.148.129.0/24 permit
169.148.131.0/24 permit
169.148.138.0/24 permit
169.148.142.10 permit
169.148.142.33 permit
169.148.144.0/25 permit
169.148.144.10 permit
169.148.146.0/23 permit
169.148.175.3 permit
169.148.188.0/24 permit
169.148.188.182 permit
170.10.128.0/24 permit
170.10.129.0/24 permit
170.10.132.56/29 permit
170.10.132.64/29 permit
170.10.133.0/24 permit
172.217.32.0/20 permit
172.253.56.0/21 permit
172.253.112.0/20 permit
173.0.84.0/29 permit
173.0.84.224/27 permit
173.0.94.244/30 permit
@@ -1728,7 +1812,6 @@
194.97.212.12 permit
194.106.220.0/23 permit
194.113.24.0/22 permit
194.113.42.0/26 permit
194.154.193.192/27 permit
195.4.92.0/23 permit
195.54.172.0/23 permit
@@ -1742,7 +1825,6 @@
198.61.254.21 permit
198.61.254.231 permit
198.178.234.57 permit
198.202.211.1 permit
198.244.48.0/20 permit
198.244.56.107 permit
198.244.56.108 permit
@@ -1764,7 +1846,16 @@
199.16.156.0/22 permit
199.33.145.1 permit
199.33.145.32 permit
199.34.22.36 permit
199.59.148.0/22 permit
199.67.80.2 permit
199.67.80.20 permit
199.67.82.2 permit
199.67.82.20 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
199.67.90.0/24 permit
199.101.161.130 permit
199.101.162.0/25 permit
199.122.120.0/21 permit
@@ -1817,9 +1908,12 @@
204.14.232.64/28 permit
204.14.234.64/28 permit
204.75.142.0/24 permit
204.79.197.212 permit
204.92.114.187 permit
204.92.114.203 permit
204.92.114.204/31 permit
204.141.32.0/23 permit
204.141.42.0/23 permit
204.216.164.202 permit
204.220.160.0/21 permit
204.220.168.0/21 permit
@@ -1842,13 +1936,24 @@
206.165.246.80/29 permit
206.191.224.0/19 permit
206.246.157.1 permit
207.46.4.128/25 permit
207.46.22.35 permit
207.46.50.72 permit
207.46.50.82 permit
207.46.50.192/26 permit
207.46.50.224 permit
207.46.52.71 permit
207.46.52.79 permit
207.46.58.128/25 permit
207.46.116.128/29 permit
207.46.117.0/24 permit
207.46.132.128/27 permit
207.46.198.0/25 permit
207.46.200.0/27 permit
207.67.38.0/24 permit
207.67.98.192/27 permit
207.68.176.0/26 permit
207.68.176.96/27 permit
207.97.204.96/29 permit
207.126.144.0/20 permit
207.171.160.0/19 permit
@@ -2003,6 +2108,8 @@
213.199.128.145 permit
213.199.138.181 permit
213.199.138.191 permit
213.199.161.128/27 permit
213.199.177.0/26 permit
216.17.150.242 permit
216.17.150.251 permit
216.24.224.0/20 permit
@@ -2030,6 +2137,7 @@
216.39.62.60/31 permit
216.39.62.136/29 permit
216.39.62.144/31 permit
216.58.192.0/19 permit
216.66.217.240/29 permit
216.71.138.33 permit
216.71.152.207 permit
@@ -2093,6 +2201,9 @@
2603:1030:20e:3::23c permit
2603:1030:b:3::152 permit
2603:1030:c02:8::14 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
2607:f8b0:4000::/36 permit
2620:109:c003:104::/64 permit
2620:109:c003:104::215 permit

View File

@@ -146,171 +146,8 @@ rspamd_config:register_symbol({
return false
end
-- Helper function to parse IPv6 into 8 segments
local function ipv6_to_segments(ip_str)
-- Remove zone identifier if present (e.g., %eth0)
ip_str = ip_str:gsub("%%.*$", "")
local segments = {}
-- Handle :: compression
if ip_str:find('::') then
local before, after = ip_str:match('^(.*)::(.*)$')
before = before or ''
after = after or ''
local before_parts = {}
local after_parts = {}
if before ~= '' then
for seg in before:gmatch('[^:]+') do
table.insert(before_parts, tonumber(seg, 16) or 0)
end
end
if after ~= '' then
for seg in after:gmatch('[^:]+') do
table.insert(after_parts, tonumber(seg, 16) or 0)
end
end
-- Add before segments
for _, seg in ipairs(before_parts) do
table.insert(segments, seg)
end
-- Add compressed zeros
local zeros_needed = 8 - #before_parts - #after_parts
for i = 1, zeros_needed do
table.insert(segments, 0)
end
-- Add after segments
for _, seg in ipairs(after_parts) do
table.insert(segments, seg)
end
else
-- No compression
for seg in ip_str:gmatch('[^:]+') do
table.insert(segments, tonumber(seg, 16) or 0)
end
end
-- Ensure we have exactly 8 segments
while #segments < 8 do
table.insert(segments, 0)
end
return segments
end
-- Generate all common IPv6 notations
local function get_ipv6_variants(ip_str)
local variants = {}
local seen = {}
local function add_variant(v)
if v and not seen[v] then
table.insert(variants, v)
seen[v] = true
end
end
-- For IPv4, just return the original
if not ip_str:find(':') then
add_variant(ip_str)
return variants
end
local segments = ipv6_to_segments(ip_str)
-- 1. Fully expanded form (all zeros shown as 0000)
local expanded_parts = {}
for _, seg in ipairs(segments) do
table.insert(expanded_parts, string.format('%04x', seg))
end
add_variant(table.concat(expanded_parts, ':'))
-- 2. Standard form (no leading zeros, but all segments present)
local standard_parts = {}
for _, seg in ipairs(segments) do
table.insert(standard_parts, string.format('%x', seg))
end
add_variant(table.concat(standard_parts, ':'))
-- 3. Find all possible :: compressions
-- RFC 5952: compress the longest run of consecutive zeros
-- But we need to check all possibilities since Redis might have any form
-- Find all zero runs
local zero_runs = {}
local in_run = false
local run_start = 0
local run_length = 0
for i = 1, 8 do
if segments[i] == 0 then
if not in_run then
in_run = true
run_start = i
run_length = 1
else
run_length = run_length + 1
end
else
if in_run then
if run_length >= 1 then -- Allow single zero compression too
table.insert(zero_runs, {start = run_start, length = run_length})
end
in_run = false
end
end
end
-- Don't forget the last run
if in_run and run_length >= 1 then
table.insert(zero_runs, {start = run_start, length = run_length})
end
-- Generate variant for each zero run compression
for _, run in ipairs(zero_runs) do
local parts = {}
-- Before compression
for i = 1, run.start - 1 do
table.insert(parts, string.format('%x', segments[i]))
end
-- The compression
if run.start == 1 then
table.insert(parts, '')
table.insert(parts, '')
elseif run.start + run.length - 1 == 8 then
table.insert(parts, '')
table.insert(parts, '')
else
table.insert(parts, '')
end
-- After compression
for i = run.start + run.length, 8 do
table.insert(parts, string.format('%x', segments[i]))
end
local compressed = table.concat(parts, ':'):gsub('::+', '::')
add_variant(compressed)
end
return variants
end
local from_ip_string = tostring(ip)
local ip_check_table = {}
-- Add all variants of the exact IP
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
table.insert(ip_check_table, variant)
end
ip_check_table = {from_ip_string}
local maxbits = 128
local minbits = 32
@@ -318,18 +155,10 @@ rspamd_config:register_symbol({
maxbits = 32
minbits = 8
end
-- Add all CIDR notations with variants
for i=maxbits,minbits,-1 do
local masked_ip = ip:apply_mask(i)
local cidr_base = masked_ip:to_string()
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
local cidr = variant .. "/" .. i
table.insert(ip_check_table, cidr)
end
local nip = ip:apply_mask(i):to_string() .. "/" .. i
table.insert(ip_check_table, nip)
end
local function keep_spam_cb(err, data)
if err then
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
@@ -337,15 +166,12 @@ rspamd_config:register_symbol({
else
for k,v in pairs(data) do
if (v and v ~= userdata and v == '1') then
rspamd_logger.infox(rspamd_config, "found ip %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k])
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
task:set_flag('no_stat')
return
end
end
end
end
table.insert(ip_check_table, 1, 'KEEP_SPAM')
local redis_ret_user = rspamd_redis_make_request(task,
redis_params, -- connect params
@@ -384,7 +210,6 @@ rspamd_config:register_symbol({
rspamd_config:register_symbol({
name = 'TAG_MOO',
type = 'postfilter',
flags = 'ignore_passthrough',
callback = function(task)
local util = require("rspamd_util")
local rspamd_logger = require "rspamd_logger"
@@ -393,6 +218,9 @@ rspamd_config:register_symbol({
local rcpts = task:get_recipients('smtp')
local lua_util = require "lua_util"
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
local function remove_moo_tag()
local moo_tag_header = task:get_header('X-Moo-Tag', false)
if moo_tag_header then
@@ -403,149 +231,101 @@ rspamd_config:register_symbol({
return true
end
-- Check if we have exactly one recipient
if not (rcpts and #rcpts == 1) then
rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0)
remove_moo_tag()
return
end
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
local tag = tagged_rcpt[1].options[1]
rspamd_logger.infox("found tag: %s", tag)
local action = task:get_metric_action('default')
rspamd_logger.infox("metric action now: %s", action)
local rcpt_addr = rcpts[1]['addr']
local rcpt_user = rcpts[1]['user']
local rcpt_domain = rcpts[1]['domain']
-- Check if recipient has a tag (contains '+')
local tag = nil
if rcpt_user:find('%+') then
local base_user, tag_part = rcpt_user:match('^(.-)%+(.+)$')
if base_user and tag_part then
tag = tag_part
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
if action ~= 'no action' and action ~= 'greylist' then
rspamd_logger.infox("skipping tag handler for action: %s", action)
remove_moo_tag()
return true
end
end
if not tag then
rspamd_logger.infox("TAG_MOO: no tag found in recipient %s, removing moo tag", rcpt_addr)
remove_moo_tag()
return
end
local function http_callback(err_message, code, body, headers)
if body ~= nil and body ~= "" then
rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
-- Optional: Check if domain is a mailcow domain
-- When KEEP_SPAM is active, RCPT_MAILCOW_DOMAIN might not be set
-- If the mail is being delivered, we can assume it's valid
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
if not mailcow_domain then
rspamd_logger.infox("TAG_MOO: RCPT_MAILCOW_DOMAIN not set (possibly due to pre-result), proceeding anyway for domain %s", rcpt_domain)
end
local function tag_callback_subject(err, data)
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
local action = task:get_metric_action('default')
rspamd_logger.infox("TAG_MOO: metric action: %s", action)
local function tag_callback_subfolder(err, data)
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
remove_moo_tag()
else
rspamd_logger.infox("Add X-Moo-Tag header")
task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end
end
local redis_ret_subfolder = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subfolder, --callback
'HGET', -- command
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
)
if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
else
rspamd_logger.infox("user wants subject modified for tagged mail")
local sbj = task:get_header('Subject')
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end
end
local redis_ret_subject = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subject, --callback
'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
)
if not redis_ret_subject then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
-- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
local allow_processing = false
if task.has_pre_result then
local has_pre, pre_action = task:has_pre_result()
if has_pre then
rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
if pre_action == 'accept' then
allow_processing = true
rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
end
end
end
-- Allow processing for mild actions or when we have pre-result accept
if not allow_processing and action ~= 'no action' and action ~= 'greylist' then
rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action)
remove_moo_tag()
return true
end
rspamd_logger.infox("TAG_MOO: processing allowed")
local function http_callback(err_message, code, body, headers)
if body ~= nil and body ~= "" then
rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
local function tag_callback_subject(err, data)
if err or type(data) ~= 'string' or data == '' then
rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
local function tag_callback_subfolder(err, data)
if err or type(data) ~= 'string' or data == '' then
rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
remove_moo_tag()
else
rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end
end
local redis_ret_subfolder = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subfolder, --callback
'HGET', -- command
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
)
if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
if rcpts and #rcpts == 1 then
for _,rcpt in ipairs(rcpts) do
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
if #rcpt_split == 2 then
if rcpt_split[1] == 'postmaster' then
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
remove_moo_tag()
else
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt['addr']},
})
end
else
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
local sbj = task:get_header('Subject') or ''
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end
end
local redis_ret_subject = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subject, --callback
'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
)
if not redis_ret_subject then
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
else
rspamd_logger.infox("TAG_MOO: alias expansion returned empty body")
remove_moo_tag()
end
end
local rcpt_split = rspamd_str_split(rcpt_addr, '@')
if #rcpt_split == 2 then
if rcpt_split[1]:match('^postmaster') then
rspamd_logger.infox(rspamd_config, "TAG_MOO: not expanding postmaster alias")
remove_moo_tag()
else
rspamd_logger.infox("TAG_MOO: requesting alias expansion for %s", rcpt_addr)
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt_addr},
})
end
else
rspamd_logger.infox("TAG_MOO: invalid rcpt format")
remove_moo_tag()
end
end,
@@ -555,7 +335,6 @@ rspamd_config:register_symbol({
rspamd_config:register_symbol({
name = 'BCC',
type = 'postfilter',
flags = 'ignore_passthrough',
callback = function(task)
local util = require("rspamd_util")
local rspamd_http = require "rspamd_http"
@@ -584,13 +363,11 @@ rspamd_config:register_symbol({
local email_content = tostring(task:get_content())
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
-- send mail
local from_smtp = task:get_from('smtp')
local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
lua_smtp.sendmail({
task = task,
host = os.getenv("IPV4_NETWORK") .. '.253',
port = 591,
from = from_addr,
from = task:get_from(stp)[1].addr,
recipients = bcc_dest,
helo = 'bcc',
timeout = 20,
@@ -620,41 +397,27 @@ rspamd_config:register_symbol({
end
local action = task:get_metric_action('default')
rspamd_logger.infox("BCC: metric action: %s", action)
-- Check for pre-result accept (e.g., from KEEP_SPAM)
local allow_bcc = false
if task.has_pre_result then
local has_pre, pre_action = task:has_pre_result()
if has_pre and pre_action == 'accept' then
allow_bcc = true
rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
end
end
-- Allow BCC for mild actions or when we have pre-result accept
if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
rspamd_logger.infox("BCC: skipping for action: %s", action)
return
end
rspamd_logger.infox("metric action now: %s", action)
local function rcpt_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then
rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
send_mail(task, body)
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
send_mail(task, body)
end
end
end
local function from_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then
rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
send_mail(task, body)
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
send_mail(task, body)
end
end
end
if rcpt_table then
for _,e in ipairs(rcpt_table) do
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
rspamd_http.request({
task=task,
url='http://nginx:8081/bcc.php',
@@ -667,7 +430,7 @@ rspamd_config:register_symbol({
if from_table then
for _,e in ipairs(from_table) do
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
rspamd_http.request({
task=task,
url='http://nginx:8081/bcc.php',
@@ -678,7 +441,7 @@ rspamd_config:register_symbol({
end
end
-- Don't return true to avoid symbol being logged
return true
end,
priority = 20
})
@@ -945,4 +708,4 @@ rspamd_config:register_symbol({
return true
end,
priority = 1
})
})

View File

@@ -86,12 +86,6 @@
SOGoMaximumFailedLoginInterval = 900;
SOGoFailedLoginBlockInterval = 900;
// Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it.
//SOGoURLEncryptionEnabled = NO;
// Set a 16 character encryption key for SOGo URL Description, change this to your own value
//SOGoURLPathEncryptionKey = "SOGoSuperSecret0";
GCSChannelCollectionTimer = 60;
GCSChannelExpireAge = 60;

View File

@@ -754,7 +754,7 @@ paths:
- syncjobs
- quarantine
- login_as
- sogo_access
- sogo_redirection
- app_passwds
- bcc_maps
- pushover
@@ -807,7 +807,7 @@ paths:
- syncjobs
- quarantine
- login_as
- sogo_access
- sogo_redirection
- app_passwds
- bcc_maps
- pushover
@@ -3339,7 +3339,7 @@ paths:
- info@domain2.tld
- domain3.tld
- "*"
sogo_access: "1"
sogo_redirection: "1"
username:
- info@domain.tld
tags: ["tag3", "tag4"]
@@ -3390,7 +3390,7 @@ paths:
- info@domain2.tld
- domain3.tld
- "*"
sogo_access: "1"
sogo_redirection: "1"
tags: ["tag3", "tag4"]
items:
- info@domain.tld
@@ -3422,8 +3422,8 @@ paths:
sender_acl:
description: list of allowed send from addresses
type: object
sogo_access:
description: is access to SOGo webmail active or not
sogo_redirection:
description: is redirection to SOGo webmail active or not
type: boolean
type: object
items:
@@ -4799,7 +4799,7 @@ paths:
force_pw_update: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_access: "1"
sogo_redirection: "1"
tls_enforce_in: "0"
tls_enforce_out: "0"
domain: doman3.tld
@@ -5352,9 +5352,9 @@ paths:
started_at: "2019-12-22T21:00:01.622856172Z"
state: running
type: info
controller-mailcow:
container: controller-mailcow
image: "mailcow/controller:1.36"
dockerapi-mailcow:
container: dockerapi-mailcow
image: "mailcow/dockerapi:1.36"
started_at: "2019-12-22T20:59:59.984797808Z"
state: running
type: info
@@ -5723,7 +5723,7 @@ paths:
force_pw_update: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_access: "1"
sogo_redirection: "1"
tls_enforce_in: "0"
tls_enforce_out: "0"
custom_attributes: {}

View File

@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
<clientConfig version="1.1">
<emailProvider id="<?=$mailcow_hostname; ?>">
<domain>%EMAILDOMAIN%</domain>
<displayName><?=$autodiscover_config['displayName']; ?></displayName>
<displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
<displayName>A mailcow mail server</displayName>
<displayShortName>mail server</displayShortName>
<incomingServer type="imap">
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>

View File

@@ -79,7 +79,7 @@ if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
exit(0);
}
$login_role = check_login($login_user, $login_pass, array('service' => 'EAS'));
$login_role = check_login($login_user, $login_pass, array('eas' => TRUE));
if ($login_role === "user") {
header("Content-Type: application/xml");

View File

@@ -129,16 +129,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
);
}
// Check if domain is an alias domain and get target domain's MTA-STS
$alias_domain_details = mailbox('get', 'alias_domain_details', $domain);
$mta_sts_domain = $domain;
if ($alias_domain_details !== false && !empty($alias_domain_details['target_domain'])) {
// This is an alias domain, check target domain for MTA-STS
$mta_sts_domain = $alias_domain_details['target_domain'];
}
$mta_sts = mailbox('get', 'mta_sts', $mta_sts_domain);
$mta_sts = mailbox('get', 'mta_sts', $domain);
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
if (!in_array($domain, $alias_domains)) {
$records[] = array(

View File

@@ -1,11 +1,10 @@
<?php
function check_login($user, $pass, $extra = null) {
function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
global $pdo;
global $redis;
$is_internal = $extra['is_internal'];
$role = $extra['role'];
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
// Try validate admin
if (!isset($role) || $role == "admin") {
@@ -26,20 +25,34 @@ function check_login($user, $pass, $extra = null) {
// Try validate app password
if (!isset($role) || $role == "app") {
$result = apppass_login($user, $pass, $extra);
$result = apppass_login($user, $pass, $app_passwd_data);
if ($result !== false) {
if ($app_passwd_data['eas'] === true) {
$service = 'EAS';
} elseif ($app_passwd_data['dav'] === true) {
$service = 'DAV';
} else {
$service = 'NONE';
}
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $extra['service'], $pass);
set_sasl_log($user, $real_rip, $service, $pass);
return $result;
}
}
// Try validate user
if (!isset($role) || $role == "user") {
$result = user_login($user, $pass, $extra);
$result = user_login($user, $pass);
if ($result !== false) {
if ($app_passwd_data['eas'] === true) {
$service = 'EAS';
} elseif ($app_passwd_data['dav'] === true) {
$service = 'DAV';
} else {
$service = 'MAILCOWUI';
}
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $extra['service']);
set_sasl_log($user, $real_rip, $service);
return $result;
}
}
@@ -180,7 +193,7 @@ function user_login($user, $pass, $extra = null){
global $iam_settings;
$is_internal = $extra['is_internal'];
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
$service = $extra['service'];
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
if (!$is_internal){
@@ -223,10 +236,10 @@ function user_login($user, $pass, $extra = null){
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($row)) {
// check if user has access to service (imap, smtp, pop3, sieve, dav, eas) if service is set
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true);
if ($extra['service'] != 'NONE') {
$key = strtolower($extra['service']) . "_access";
if (isset($service)) {
$key = strtolower($service) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false;
}
@@ -240,8 +253,8 @@ function user_login($user, $pass, $extra = null){
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true);
if ($extra['service'] != 'NONE') {
$key = strtolower($extra['service']) . "_access";
if (isset($service)) {
$key = strtolower($service) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false;
}
@@ -395,7 +408,7 @@ function user_login($user, $pass, $extra = null){
return false;
}
function apppass_login($user, $pass, $extra = null){
function apppass_login($user, $pass, $app_passwd_data, $extra = null){
global $pdo;
$is_internal = $extra['is_internal'];
@@ -411,8 +424,20 @@ function apppass_login($user, $pass, $extra = null){
return false;
}
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
if (!$is_internal && $extra['service'] == 'NONE') {
$protocol = false;
if ($app_passwd_data['eas']){
$protocol = 'eas';
} else if ($app_passwd_data['dav']){
$protocol = 'dav';
} else if ($app_passwd_data['smtp']){
$protocol = 'smtp';
} else if ($app_passwd_data['imap']){
$protocol = 'imap';
} else if ($app_passwd_data['sieve']){
$protocol = 'sieve';
} else if ($app_passwd_data['pop3']){
$protocol = 'pop3';
} else if (!$is_internal) {
return false;
}
@@ -433,7 +458,7 @@ function apppass_login($user, $pass, $extra = null){
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
if ($protocol && $row[$protocol . '_access'] != '1'){
continue;
}

View File

@@ -4,12 +4,12 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
global $redis;
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: application/json' ));
// We are using our mail certificates for controller, the names will not match, the certs are trusted anyway
// We are using our mail certificates for dockerapi, the names will not match, the certs are trusted anyway
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
switch($action) {
case 'get_id':
curl_setopt($curl, CURLOPT_URL, 'https://controller:443/containers/json');
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -35,7 +35,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
return false;
break;
case 'containers':
curl_setopt($curl, CURLOPT_URL, 'https://controller:443/containers/json');
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -63,7 +63,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
break;
case 'info':
if (empty($service_name)) {
curl_setopt($curl, CURLOPT_URL, 'https://controller:443/containers/json');
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -71,7 +71,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
else {
$container_id = docker('get_id', $service_name);
if (ctype_xdigit($container_id)) {
curl_setopt($curl, CURLOPT_URL, 'https://controller:443/containers/' . $container_id . '/json');
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/' . $container_id . '/json');
}
else {
return false;
@@ -102,7 +102,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
}
}
else {
if (isset($decoded_response['Config']['Labels']['com.docker.compose.project'])
if (isset($decoded_response['Config']['Labels']['com.docker.compose.project'])
&& strtolower($decoded_response['Config']['Labels']['com.docker.compose.project']) == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
unset($container['Config']['Env']);
$out[$decoded_response['Config']['Labels']['com.docker.compose.service']]['State'] = $decoded_response['State'];
@@ -123,7 +123,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
if (!empty($attr1)) {
$container_id = docker('get_id', $service_name);
if (ctype_xdigit($container_id) && ctype_alnum($attr1)) {
curl_setopt($curl, CURLOPT_URL, 'https://controller:443/containers/' . $container_id . '/' . $attr1);
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/' . $container_id . '/' . $attr1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
if (!empty($attr2)) {
@@ -157,7 +157,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
}
$container_id = $service_name;
curl_setopt($curl, CURLOPT_URL, 'https://controller:443/container/' . $container_id . '/stats/update');
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/container/' . $container_id . '/stats/update');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -175,7 +175,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
return false;
break;
case 'host_stats':
curl_setopt($curl, CURLOPT_URL, 'https://controller:443/host/stats');
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/host/stats');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);

View File

@@ -205,42 +205,6 @@ function password_complexity($_action, $_data = null) {
break;
}
}
function password_generate(){
$password_complexity = password_complexity('get');
$min_length = max(16, intval($password_complexity['length']));
$lowercase = range('a', 'z');
$uppercase = range('A', 'Z');
$digits = range(0, 9);
$special_chars = str_split('!@#$%^&*()?=');
$password = [
$lowercase[random_int(0, count($lowercase) - 1)],
$uppercase[random_int(0, count($uppercase) - 1)],
$digits[random_int(0, count($digits) - 1)],
$special_chars[random_int(0, count($special_chars) - 1)],
];
$all = array_merge($lowercase, $uppercase, $digits, $special_chars);
while (count($password) < $min_length) {
$password[] = $all[random_int(0, count($all) - 1)];
}
// Cryptographically secure shuffle using Fisher-Yates algorithm
$count = count($password);
for ($i = $count - 1; $i > 0; $i--) {
$j = random_int(0, $i);
$temp = $password[$i];
$password[$i] = $password[$j];
$password[$j] = $temp;
}
return implode('', $password);
}
function password_check($password1, $password2) {
$password_complexity = password_complexity('get');
@@ -524,16 +488,6 @@ function sys_mail($_data) {
'msg' => 'Mass mail job completed, sent ' . count($rcpts) . ' mails'
);
}
function get_remote_ip($use_x_real_ip = true) {
$remote = $_SERVER['REMOTE_ADDR'];
if ($use_x_real_ip && !empty($_SERVER['HTTP_X_REAL_IP'])) {
$remote = $_SERVER['HTTP_X_REAL_IP'];
}
if (filter_var($remote, FILTER_VALIDATE_IP) === false) {
$remote = '0.0.0.0';
}
return $remote;
}
function logger($_data = false) {
/*
logger() will be called as last function
@@ -860,32 +814,6 @@ function verify_hash($hash, $password) {
$hash = $components[4];
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
case "PBKDF2-SHA512":
// Handle FreeIPA-style hash: {PBKDF2-SHA512}10000$<base64_salt>$<base64_hash>
$components = explode('$', $hash);
if (count($components) !== 3) return false;
// 1st part: iteration count (integer)
$iterations = intval($components[0]);
if ($iterations <= 0) return false;
// 2nd part: salt (base64-encoded)
$salt = $components[1];
// 3rd part: hash (base64-encoded)
$stored_hash_b64 = $components[2];
// Decode salt and hash from base64
$salt_bin = base64_decode($salt, true);
$hash_bin = base64_decode($stored_hash_b64, true);
if ($salt_bin === false || $hash_bin === false) return false;
// Get length of hash in bytes
$hash_len = strlen($hash_bin);
if ($hash_len === 0) return false;
// Calculate PBKDF2-SHA512 hash for provided password
$test_hash = hash_pbkdf2('sha512', $password, $salt_bin, $iterations, $hash_len, true);
return hash_equals($hash_bin, $test_hash);
case "PLAIN-MD4":
return hash_equals(hash('md4', $password), $hash);
@@ -3443,9 +3371,14 @@ function set_user_loggedin_session($user) {
session_regenerate_id(true);
$_SESSION['mailcow_cc_username'] = $user;
$_SESSION['mailcow_cc_role'] = 'user';
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
$_SESSION['sogo-sso-user-allowed'][] = $user;
$_SESSION['sogo-sso-pass'] = $sogo_sso_pass;
acl('to_session');
if (hasACLAccess("sogo_access")) {
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
$_SESSION['sogo-sso-user-allowed'][] = $user;
$_SESSION['sogo-sso-pass'] = $sogo_sso_pass;
}
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']);

View File

@@ -49,12 +49,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// Default to 1 yr
$_data["validity"] = 8760;
}
if (isset($_data["permanent"]) && filter_var($_data["permanent"], FILTER_VALIDATE_BOOL)) {
$permanent = 1;
}
else {
$permanent = 0;
}
$domain = $_data['domain'];
$description = $_data['description'];
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
@@ -71,14 +65,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return false;
}
$validity = strtotime("+" . $_data["validity"] . " hour");
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`, `permanent`) VALUES
(:address, :description, :goto, :validity, :permanent)");
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES
(:address, :description, :goto, :validity)");
$stmt->execute(array(
':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain,
':description' => $description,
':goto' => $username,
':validity' => $validity,
':permanent' => $permanent
':validity' => $validity
));
$_SESSION['return'][] = array(
'type' => 'success',
@@ -475,11 +468,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':delete2duplicates' => $delete2duplicates,
':active' => $active,
));
$id = $pdo->lastInsertId();
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username, $id)
'msg' => array('mailbox_modified', $username)
);
break;
case 'domain':
@@ -696,7 +688,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
$internal = intval($_data['internal']);
$active = intval($_data['active']);
$sender_allowed = intval($_data['sender_allowed']);
$sogo_visible = intval($_data['sogo_visible']);
$goto_null = intval($_data['goto_null']);
$goto_spam = intval($_data['goto_spam']);
@@ -852,8 +843,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
continue;
}
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `sender_allowed`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :sender_allowed, :active)");
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
$stmt->execute(array(
':address' => '@'.$domain,
@@ -864,7 +855,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain,
':sogo_visible' => $sogo_visible,
':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active
));
}
@@ -877,7 +867,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain,
':sogo_visible' => $sogo_visible,
':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active
));
}
@@ -1079,20 +1068,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
}
$active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
$sogo_redirection = (isset($_data['sogo_redirection'])) ? intval($_data['sogo_redirection']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_redirection']);
$imap_access = (isset($_data['imap_access'])) ? intval($_data['imap_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$eas_access = (isset($_data['eas_access'])) ? intval($_data['eas_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
$dav_access = (isset($_data['dav_access'])) ? intval($_data['dav_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
@@ -1106,13 +1091,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'force_pw_update' => strval($force_pw_update),
'tls_enforce_in' => strval($tls_enforce_in),
'tls_enforce_out' => strval($tls_enforce_out),
'sogo_access' => strval($sogo_access),
'sogo_redirection' => strval($sogo_redirection),
'imap_access' => strval($imap_access),
'pop3_access' => strval($pop3_access),
'smtp_access' => strval($smtp_access),
'sieve_access' => strval($sieve_access),
'eas_access' => strval($eas_access),
'dav_access' => strval($dav_access),
'relayhost' => strval($relayhost),
'passwd_update' => time(),
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
@@ -1297,6 +1280,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
$_data['eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
$_data['sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
$_data['sogo_access'] = (in_array('sogo_access', $_data['acl'])) ? 1 : 0;
$_data['pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
$_data['quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
$_data['quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
@@ -1313,6 +1297,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['syncjobs'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_syncjobs']);
$_data['eas_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_eas_reset']);
$_data['sogo_profile_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_sogo_profile_reset']);
$_data['sogo_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_sogo_access']);
$_data['pushover'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pushover']);
$_data['quarantine'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine']);
$_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']);
@@ -1721,7 +1706,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
$attr["sogo_redirection"] = isset($_data['sogo_redirection']) ? intval($_data['sogo_redirection']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_redirection']);
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1;
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
@@ -1731,16 +1716,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
}
else {
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$attr['eas_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
$attr['dav_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
}
if (isset($_data['acl'])) {
$_data['acl'] = (array)$_data['acl'];
@@ -1752,6 +1733,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
$attr['acl_eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
$attr['acl_sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
$attr['acl_sogo_access'] = (in_array('sogo_access', $_data['acl'])) ? 1 : 0;
$attr['acl_pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
@@ -1769,6 +1751,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_syncjobs'] = 0;
$attr['acl_eas_reset'] = 0;
$attr['acl_sogo_profile_reset'] = 0;
$attr['acl_sogo_access'] = 0;
$attr['acl_pushover'] = 0;
$attr['acl_quarantine'] = 0;
$attr['acl_quarantine_attachments'] = 0;
@@ -2124,23 +2107,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
continue;
}
if (empty($_data['validity']) && empty($_data['permanent'])) {
if (empty($_data['validity'])) {
continue;
}
if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) {
$permanent = 1;
$validity = 0;
}
else if (isset($_data['validity'])) {
$permanent = 0;
$validity = round((int)time() + ($_data['validity'] * 3600));
}
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity, `permanent` = :permanent WHERE
$validity = round((int)time() + ($_data['validity'] * 3600));
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE
`address` = :address");
$stmt->execute(array(
':address' => $address,
':validity' => $validity,
':permanent' => $permanent
':validity' => $validity
));
$_SESSION['return'][] = array(
'type' => 'success',
@@ -2515,7 +2490,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if (!empty($is_now)) {
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$sender_allowed = (isset($_data['sender_allowed'])) ? intval($_data['sender_allowed']) : $is_now['sender_allowed'];
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
$goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
@@ -2701,7 +2675,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`goto` = :goto,
`sogo_visible`= :sogo_visible,
`internal`= :internal,
`sender_allowed`= :sender_allowed,
`active`= :active
WHERE `id` = :id");
$stmt->execute(array(
@@ -2712,7 +2685,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':goto' => $goto,
':sogo_visible' => $sogo_visible,
':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active,
':id' => $is_now['id']
));
@@ -3060,29 +3032,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
}
if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
(int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
(int)$eas_access = (isset($_data['eas_access']) && hasACLAccess("protocol_access")) ? intval($_data['eas_access']) : intval($is_now['attributes']['eas_access']);
(int)$dav_access = (isset($_data['dav_access']) && hasACLAccess("protocol_access")) ? intval($_data['dav_access']) : intval($is_now['attributes']['dav_access']);
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
$domain = $is_now['domain'];
$quota_b = $quota_m * 1048576;
$password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
$tags = (is_array($_data['tags']) ? $_data['tags'] : array());
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
$authsource = $is_now['authsource'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
(int)$sogo_redirection = (isset($_data['sogo_redirection'])) ? intval($_data['sogo_redirection']) : intval($is_now['attributes']['sogo_redirection']);
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
$domain = $is_now['domain'];
$quota_b = $quota_m * 1048576;
$password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
$tags = (is_array($_data['tags']) ? $_data['tags'] : array());
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
$authsource = $is_now['authsource'];
if ($_data['authsource'] == "mailcow" ||
in_array($_data['authsource'], array('keycloak', 'generic-oidc', 'ldap')) && $iam_settings['authsource'] == $_data['authsource']){
$authsource = $_data['authsource'];
@@ -3206,10 +3174,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
if (isset($_data['sender_acl'])) {
// Get sender_acl items set by admin
$current_sender_acls = mailbox('get', 'sender_acl_handles', $username);
$sender_acl_admin = array_merge(
$current_sender_acls['sender_acl_domains']['ro'],
$current_sender_acls['sender_acl_addresses']['ro']
mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'],
mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro']
);
// Get sender_acl items from POST array
// Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
@@ -3297,25 +3264,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $username
));
$sender_acl_handles = mailbox('get', 'sender_acl_handles', $username);
$fixed_sender_aliases_allowed = $sender_acl_handles['fixed_sender_aliases_allowed'];
$fixed_sender_aliases_blocked = $sender_acl_handles['fixed_sender_aliases_blocked'];
$fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases'];
foreach ($sender_acl_merged as $sender_acl) {
$domain = ltrim($sender_acl, '@');
if (is_valid_domain_name($domain)) {
$sender_acl = '@' . $domain;
}
// Always add to sender_acl table to create explicit permission
// Skip only if it's in allowed list (would be redundant)
// But DO add if it's in blocked list (creates override)
if (in_array($sender_acl, $fixed_sender_aliases_allowed)) {
// Skip: already allowed by sender_allowed=1, no need for sender_acl entry
// Don't add if allowed by alias
if (in_array($sender_acl, $fixed_sender_aliases)) {
continue;
}
// Add to sender_acl (either override for blocked aliases, or grant for selectable ones)
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
VALUES (:sender_acl, :username)");
$stmt->execute(array(
@@ -3360,14 +3318,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`quota` = :quota_b,
`authsource` = :authsource,
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
`attributes` = JSON_SET(`attributes`, '$.sogo_redirection', :sogo_redirection),
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
`attributes` = JSON_SET(`attributes`, '$.eas_access', :eas_access),
`attributes` = JSON_SET(`attributes`, '$.dav_access', :dav_access),
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
WHERE `username` = :username");
@@ -3377,13 +3333,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':quota_b' => $quota_b,
':attribute_hash' => $attribute_hash,
':force_pw_update' => $force_pw_update,
':sogo_access' => $sogo_access,
':sogo_redirection' => $sogo_redirection,
':imap_access' => $imap_access,
':pop3_access' => $pop3_access,
':sieve_access' => $sieve_access,
':smtp_access' => $smtp_access,
':eas_access' => $eas_access,
':dav_access' => $dav_access,
':recovery_email' => $pw_recovery_email,
':relayhost' => $relayhost,
':username' => $username,
@@ -3756,7 +3710,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
$attr["sogo_redirection"] = isset($_data['sogo_redirection']) ? intval($_data['sogo_redirection']) : $is_now['sogo_redirection'];
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
$attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : $is_now['tls_enforce_out'];
@@ -3766,8 +3720,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
}
else {
foreach ($is_now as $key => $value){
@@ -3784,6 +3736,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
$attr['acl_eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
$attr['acl_sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
$attr["acl_sogo_access"] = (in_array('sogo_access', $_data['acl'])) ? 1 : 0;
$attr['acl_pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
@@ -4197,22 +4150,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$data['sender_acl_addresses']['rw'] = array();
$data['sender_acl_addresses']['selectable'] = array();
$data['fixed_sender_aliases'] = array();
$data['fixed_sender_aliases_allowed'] = array();
$data['fixed_sender_aliases_blocked'] = array();
$data['external_sender_aliases'] = array();
// Fixed addresses - split by sender_allowed status
$stmt = $pdo->prepare("SELECT `address`, `sender_allowed` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
// Fixed addresses
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
// Keep old array for backward compatibility
$data['fixed_sender_aliases'][] = $row['address'];
// Split into allowed/blocked for proper display
if ($row['sender_allowed'] == '1') {
$data['fixed_sender_aliases_allowed'][] = $row['address'];
} else {
$data['fixed_sender_aliases_blocked'][] = $row['address'];
}
}
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
@@ -4645,12 +4589,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`description`,
`validity`,
`created`,
`modified`,
`permanent`
`modified`
FROM `spamalias`
WHERE `goto` = :username
AND (`validity` >= :unixnow
OR `permanent` != 0)");
AND `validity` >= :unixnow");
$stmt->execute(array(':username' => $_data, ':unixnow' => time()));
$tladata = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $tladata;
@@ -4772,7 +4714,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`internal`,
`active`,
`sogo_visible`,
`sender_allowed`,
`created`,
`modified`
FROM `alias`
@@ -4806,7 +4747,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$aliasdata['active_int'] = $row['active'];
$aliasdata['sogo_visible'] = $row['sogo_visible'];
$aliasdata['sogo_visible_int'] = $row['sogo_visible'];
$aliasdata['sender_allowed'] = $row['sender_allowed'];
$aliasdata['created'] = $row['created'];
$aliasdata['modified'] = $row['modified'];
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
@@ -5227,7 +5167,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username");
$stmt->execute(array(':domain' => $row['domain'], ':username' => $_data));
$MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND (`validity` >= :unixnow OR `permanent` != 0)");
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow");
$stmt->execute(array(':address' => $_data, ':unixnow' => time()));
$SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];

View File

@@ -4,7 +4,7 @@ function init_db_schema()
try {
global $pdo;
$db_version = "28012026_1000";
$db_version = "16102025_1340";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -185,7 +185,6 @@ function init_db_schema()
"public_comment" => "TEXT",
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
"sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '1'",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
@@ -505,6 +504,7 @@ function init_db_schema()
"syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'",
"eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
"sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'",
"sogo_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
"pushover" => "TINYINT(1) NOT NULL DEFAULT '1'",
// quarantine is for quarantine actions, todo: rename
"quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
@@ -555,8 +555,7 @@ function init_db_schema()
"description" => "TEXT NOT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
"validity" => "INT(11)",
"permanent" => "TINYINT(1) NOT NULL DEFAULT '0'"
"validity" => "INT(11)"
),
"keys" => array(
"primary" => array(
@@ -704,7 +703,7 @@ function init_db_schema()
"syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
"quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
"login_as" => "TINYINT(1) NOT NULL DEFAULT '1'",
"sogo_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
"sogo_redirection" => "TINYINT(1) NOT NULL DEFAULT '1'",
"app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'",
"bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'",
"pushover" => "TINYINT(1) NOT NULL DEFAULT '0'",
@@ -1391,12 +1390,11 @@ function init_db_schema()
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.relayhost', \"0\") WHERE JSON_VALUE(`attributes`, '$.relayhost') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_redirection', JSON_VALUE(`attributes`, '$.sogo_access')) WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NOT NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_REMOVE(`attributes`, '$.sogo_access') WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NOT NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.eas_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.eas_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.dav_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.dav_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;");
@@ -1449,7 +1447,7 @@ function init_db_schema()
"rl_frame" => "s",
"rl_value" => "",
"force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']),
"sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']),
"sogo_redirection" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_redirection']),
"active" => 1,
"tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']),
"tls_enforce_out" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_out']),
@@ -1465,6 +1463,7 @@ function init_db_schema()
"acl_syncjobs" => 0,
"acl_eas_reset" => 1,
"acl_sogo_profile_reset" => 0,
"acl_sogo_access" => 1,
"acl_pushover" => 1,
"acl_quarantine" => 1,
"acl_quarantine_attachments" => 1,
@@ -1503,6 +1502,14 @@ function init_db_schema()
":attributes" => json_encode($default_mailbox_template["attributes"])
));
}
$pdo->query("UPDATE `templates`
SET `attributes` = JSON_SET(`attributes`, '$.sogo_redirection', JSON_VALUE(`attributes`, '$.sogo_access'))
WHERE `type` = 'mailbox' AND JSON_VALUE(`attributes`, '$.sogo_access') IS NOT NULL;
");
$pdo->query("UPDATE `templates`
SET `attributes` = JSON_REMOVE(`attributes`, '$.sogo_access')
WHERE `type` = 'mailbox' AND JSON_VALUE(`attributes`, '$.sogo_access') IS NOT NULL;
");
// remove old sogo views and triggers
$pdo->query("DROP TRIGGER IF EXISTS sogo_update_password");

View File

@@ -105,11 +105,11 @@ http_response_code(500);
<?php
exit;
}
// Stop when controller is not available
if (fsockopen("tcp://controller", 443, $errno, $errstr) === false) {
// 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 controller container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
<center style='font-family:sans-serif;'>Connection to dockerapi container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
<?php
exit;
}
@@ -121,7 +121,7 @@ class mailcowPdo extends OAuth2\Storage\Pdo {
$this->config['user_table'] = 'mailbox';
}
public function checkUserCredentials($username, $password) {
if (check_login($username, $password, array("role" => "user", "service" => "NONE")) == 'user') {
if (check_login($username, $password) == 'user') {
return true;
}
return false;
@@ -165,6 +165,14 @@ if(!$DEV_MODE) {
set_exception_handler('exception_handler');
}
// TODO: Move function
function get_remote_ip() {
$remote = $_SERVER['REMOTE_ADDR'];
if (filter_var($remote, FILTER_VALIDATE_IP) === false) {
return '0.0.0.0';
}
return $remote;
}
// Load core functions first
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';

View File

@@ -70,7 +70,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) {
}
else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401);
echo json_encode(array(
'type' => 'error',
@@ -82,7 +82,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) {
}
else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401);
echo json_encode(array(
'type' => 'error',
@@ -92,16 +92,6 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) {
exit();
}
}
else {
$remote = get_remote_ip(false);
$docker_ipv4_network = getenv('IPV4_NETWORK');
if ($remote == "{$docker_ipv4_network}.246") {
$_SESSION['mailcow_cc_username'] = 'Controller';
$_SESSION['mailcow_cc_role'] = 'admin';
$_SESSION['mailcow_cc_api'] = true;
$_SESSION['mailcow_cc_api_access'] = 'rw';
}
}
// Handle logouts
if (isset($_POST["logout"])) {

View File

@@ -44,7 +44,7 @@ if (isset($_GET["cancel_tfa_login"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"], array("role" => "admin", "service" => "MAILCOWUI"));
$as = check_login($login_user, $_POST["pass_user"], false, array("role" => "admin"));
if ($as == "admin") {
session_regenerate_id(true);

View File

@@ -55,7 +55,7 @@ if (isset($_GET["cancel_tfa_login"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"], array("role" => "domain_admin", "service" => "MAILCOWUI"));
$as = check_login($login_user, $_POST["pass_user"], false, array("role" => "domain_admin"));
if ($as == "domainadmin") {
session_regenerate_id(true);

View File

@@ -76,8 +76,9 @@ if (isset($_POST["verify_tfa_login"])) {
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 &&
if (intval($user_details['attributes']['sogo_redirection']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
hasACLAccess('sogo_access') &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/");
@@ -119,7 +120,7 @@ if (isset($_GET["cancel_tfa_login"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"], array("role" => "user", "service" => "MAILCOWUI"));
$as = check_login($login_user, $_POST["pass_user"], false, array("role" => "user"));
if ($as == "user") {
set_user_loggedin_session($login_user);
@@ -142,8 +143,9 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$user_details = mailbox("get", "mailbox_details", $login_user);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 &&
if (intval($user_details['attributes']['sogo_redirection']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
hasACLAccess('sogo_access') &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/");

View File

@@ -33,8 +33,6 @@ if ($https_port === FALSE) {
//$https_port = 1234;
// Other settings =>
$autodiscover_config = array(
'displayName' => 'A mailcow mail server',
'displayShortName' => 'mail server',
// General autodiscover service type: "activesync" or "imap"
// emClient uses autodiscover, but does not support ActiveSync. mailcow excludes emClient from ActiveSync.
// With SOGo disabled, the type will always fallback to imap. CalDAV and CardDAV will be excluded, too.
@@ -87,7 +85,7 @@ $AVAILABLE_LANGUAGES = array(
// 'ca-es' => 'Català (Catalan)',
'bg-bg' => 'Български (Bulgarian)',
'cs-cz' => 'Čeština (Czech)',
'da-dk' => 'Dansk (Danish)',
'da-dk' => 'Danish (Dansk)',
'de-de' => 'Deutsch (German)',
'en-gb' => 'English',
'es-es' => 'Español (Spanish)',
@@ -112,7 +110,6 @@ $AVAILABLE_LANGUAGES = array(
'sv-se' => 'Svenska (Swedish)',
'tr-tr' => 'Türkçe (Turkish)',
'uk-ua' => 'Українська (Ukrainian)',
'vi-vn' => 'Tiếng Việt (Vietnamese)',
'zh-cn' => '简体中文 (Simplified Chinese)',
'zh-tw' => '繁體中文 (Traditional Chinese)',
);
@@ -193,8 +190,8 @@ $MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'] = false;
// Force password change on next login (only allows login to mailcow UI)
$MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;
// Enable SOGo access - Users will be redirected to SOGo after login (set to false to disable redirect by default)
$MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
// Enable SOGo redirection - Users will be redirected to SOGo after login (set to false to disable redirect by default)
$MAILBOX_DEFAULT_ATTRIBUTES['sogo_redirection'] = true;
// How to handle tagged emails
// none - No special handling
@@ -217,12 +214,6 @@ $MAILBOX_DEFAULT_ATTRIBUTES['smtp_access'] = true;
// Mailbox has sieve access by default
$MAILBOX_DEFAULT_ATTRIBUTES['sieve_access'] = true;
// Mailbox has ActiveSync/EAS access by default
$MAILBOX_DEFAULT_ATTRIBUTES['eas_access'] = true;
// Mailbox has CalDAV/CardDAV (DAV) access by default
$MAILBOX_DEFAULT_ATTRIBUTES['dav_access'] = true;
// Mailbox receives notifications about...
// "add_header" - mail that was put into the Junk folder
// "reject" - mail that was rejected

View File

@@ -11,7 +11,10 @@ if (isset($_SESSION['mailcow_cc_role']) && isset($_SESSION['oauth2_request'])) {
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") {
if (intval($user_details['attributes']['sogo_redirection']) == 1 &&
hasACLAccess('sogo_access') &&
!$is_dual &&
getenv('SKIP_SOGO') != "y") {
header("Location: /SOGo/so/");
} else {
header("Location: /user");
@@ -27,12 +30,6 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
exit();
}
$host = strtolower($_SERVER['HTTP_HOST'] ?? '');
if (str_starts_with($host, 'autodiscover.') || str_starts_with($host, 'autoconfig.')) {
http_response_code(404);
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];

View File

@@ -54,16 +54,7 @@ jQuery(function($){
$.get("/inc/ajax/show_rspamd_global_filters.php");
$("#confirm_show_rspamd_global_filters").hide();
$("#rspamd_global_filters").removeClass("d-none");
localStorage.setItem('rspamd_global_filters_confirmed', 'true');
});
$(document).ready(function() {
if (localStorage.getItem('rspamd_global_filters_confirmed') === 'true') {
$("#confirm_show_rspamd_global_filters").hide();
$("#rspamd_global_filters").removeClass("d-none");
}
});
$("#super_delete").click(function() { return confirm(lang.queue_ays); });
$(".refresh_table").on('click', function(e) {

View File

@@ -352,12 +352,6 @@ $(document).ready(function() {
if (template.sieve_access == 1){
protocol_access.push("sieve");
}
if (template.eas_access == 1){
protocol_access.push("eas");
}
if (template.dav_access == 1){
protocol_access.push("dav");
}
$('#protocol_access').selectpicker('val', protocol_access);
var acl = [];
@@ -385,6 +379,9 @@ $(document).ready(function() {
if (template.acl_sogo_profile_reset == 1){
acl.push("sogo_profile_reset");
}
if (template.acl_sogo_access == 1){
acl.push("sogo_access");
}
if (template.acl_pushover == 1){
acl.push("pushover");
}
@@ -424,10 +421,10 @@ $(document).ready(function() {
} else {
$('#force_pw_update').prop('checked', false);
}
if (template.sogo_access == 1){
$('#sogo_access').prop('checked', true);
if (template.sogo_redirection == 1){
$('#sogo_redirection').prop('checked', true);
} else {
$('#sogo_access').prop('checked', false);
$('#sogo_redirection').prop('checked', false);
}
// load tags
@@ -939,8 +936,6 @@ jQuery(function($){
item.imap_access = '<i class="text-' + (item.attributes.imap_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.imap_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
item.smtp_access = '<i class="text-' + (item.attributes.smtp_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.smtp_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
item.sieve_access = '<i class="text-' + (item.attributes.sieve_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sieve_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
item.eas_access = '<i class="text-' + (item.attributes.eas_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.eas_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
item.dav_access = '<i class="text-' + (item.attributes.dav_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.dav_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
if (item.attributes.quarantine_notification === 'never') {
item.quarantine_notification = lang.never;
} else if (item.attributes.quarantine_notification === 'hourly') {
@@ -1104,18 +1099,6 @@ jQuery(function($){
defaultContent: '',
className: 'none'
},
{
title: 'EAS',
data: 'eas_access',
defaultContent: '',
className: 'none'
},
{
title: 'DAV',
data: 'dav_access',
defaultContent: '',
className: 'none'
},
{
title: lang.quarantine_notification,
data: 'quarantine_notification',
@@ -1229,9 +1212,7 @@ jQuery(function($){
item.attributes.imap_access = '<i class="text-' + (item.attributes.imap_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.imap_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.imap_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.smtp_access = '<i class="text-' + (item.attributes.smtp_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.smtp_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.smtp_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.sieve_access = '<i class="text-' + (item.attributes.sieve_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sieve_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sieve_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.eas_access = '<i class="text-' + (item.attributes.eas_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.eas_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.eas_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.dav_access = '<i class="text-' + (item.attributes.dav_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.dav_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.dav_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.sogo_access = '<i class="text-' + (item.attributes.sogo_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.sogo_redirection = '<i class="text-' + (item.attributes.sogo_redirection == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_redirection == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_redirection == 1 ? '1' : '0') + '</span></i>';
if (item.attributes.quarantine_notification === 'never') {
item.attributes.quarantine_notification = lang.never;
} else if (item.attributes.quarantine_notification === 'hourly') {
@@ -1340,18 +1321,8 @@ jQuery(function($){
defaultContent: '',
},
{
title: 'EAS',
data: 'attributes.eas_access',
defaultContent: '',
},
{
title: 'DAV',
data: 'attributes.dav_access',
defaultContent: '',
},
{
title: 'SOGO',
data: 'attributes.sogo_access',
title: 'SOGO redirection',
data: 'attributes.sogo_redirection',
defaultContent: '',
},
{

View File

@@ -175,10 +175,6 @@ jQuery(function($){
'</div>';
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="tla" name="multi_select" value="' + encodeURIComponent(item.address) + '" />';
item.address = escapeHtml(item.address);
item.validity = {
value: item.validity,
permanent: item.permanent
};
}
else {
item.chkbox = '<input type="checkbox" class="form-check-input" disabled />';
@@ -222,21 +218,9 @@ jQuery(function($){
title: lang.alias_valid_until,
data: 'validity',
defaultContent: '',
render: function (data, type) {
var date = new Date(data.value ? data.value * 1000 : 0);
switch (type) {
case "sort":
if (data.permanent) {
return 0;
}
return date.getTime();
default:
if (data.permanent) {
return lang.forever;
}
return date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
}
},
createdCell: function(td, cellData) {
createSortableDate(td, cellData)
}
},
{
title: lang.created_on,

View File

@@ -1007,9 +1007,9 @@ if (isset($_GET['query'])) {
['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"],
['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"],
['db' => 'name', 'dt' => 7],
['db' => 'messages', 'dt' => 20, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
['db' => 'tags', 'dt' => 23, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
['db' => 'active', 'dt' => 24],
['db' => 'messages', 'dt' => 18, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
['db' => 'tags', 'dt' => 20, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
['db' => 'active', 'dt' => 21],
];
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
@@ -1992,7 +1992,6 @@ if (isset($_GET['query'])) {
break;
case "cors":
process_edit_return(cors('edit', $attr));
break;
case "identity-provider":
process_edit_return(identity_provider('edit', $attr));
break;

View File

@@ -22,7 +22,7 @@
"ratelimit": "Ограничение на скоростта",
"recipient_maps": "Карти на получатели",
"smtp_ip_access": "Промяна на разрешените хостове за SMTP",
"sogo_access": "Разрешаване на управление на достъпа до SOGo",
"sogo_redirection": "Разрешаване на управление на достъпа до SOGo",
"sogo_profile_reset": "Нулиране на профила на SOGo",
"spam_alias": "Временни псевдоними",
"spam_policy": "Черен/Бял списък",
@@ -736,8 +736,8 @@
"sieve_desc": "Кратко описание",
"sieve_type": "Тип на филтър",
"skipcrossduplicates": "Пропускане на дублирани съобщения между папки (първи дошъл, първи обслужен)",
"sogo_access": "Директно препращане към SOGo",
"sogo_access_info": "След влизане, потребителят се пренасочва автоматично към SOGo.",
"sogo_redirection": "Директно препращане към SOGo",
"sogo_redirection_info": "След влизане, потребителят се пренасочва автоматично към SOGo.",
"sogo_visible": "Псевдонимът е видим в SOGo",
"sogo_visible_info": "Тази опция засяга само обекти, които могат да бъдат показани в SOGo (споделени или несподелени адреси на псевдоними, сочещи поне една локална пощенска кутия). Ако е скрит, псевдонимът няма да се появи като избираем адрес на изпращач в SOGo.",
"spam_alias": "Създаване или промяна на временни псевдоними",

View File

@@ -19,16 +19,7 @@
"spam_alias": "Àlies temporals",
"spam_score": "Puntuació de correu brossa",
"tls_policy": "Política TLS",
"unlimited_quota": "Quota ilimitada per bústies de correo",
"delimiter_action": "Acció delimitadora",
"domain_relayhost": "Canviar relayhost per un domini",
"extend_sender_acl": "Permetre extendre l'ACL del remitent per adreces externes",
"mailbox_relayhost": "Canvia el host de reenviament per una bústia",
"pushover": "Pushover",
"pw_reset": "Permetre el restabliment de la contrasenya de l'usuari mailcow",
"ratelimit": "Límit de peticions",
"smtp_ip_access": "Canvia hosts permesos per SMTP",
"sogo_access": "Permetre la gestió d'accés a SOGo"
"unlimited_quota": "Quota ilimitada per bústies de correo"
},
"add": {
"activate_filter_warn": "All other filters will be deactivated, when active is checked.",
@@ -82,25 +73,7 @@
"validate": "Validar",
"validation_success": "Validated successfully",
"app_name": "Nom de l'aplicació",
"app_password": "Afegir contrasenya a l'aplicació",
"app_passwd_protocols": "Protocols autoritzats per la contrasenya de l'aplicació",
"bcc_dest_format": "La destinació c/o ha de ser una única adreça de correu vàlida.<br>Si necessiteu enviar una còpia a diverses adreces, creeu un àlies i utilitzeu-lo aquí.",
"comment_info": "Els comentaris privats no són visibles per l'usuari, mentre que els comentaris públics apareixen com una descripció emergent a la informació de l'usuari",
"custom_params": "Paràmetres personalitzats",
"custom_params_hint": "Correcte: --param=xy, incorrecte: --param xy",
"destination": "Destí",
"disable_login": "No permetre l'inici de sessió (els missatges entrants continuen sent acceptats)",
"domain_matches_hostname": "El domini %s coincideix amb el nom del servidor",
"dry": "Simular la sincronització",
"gal": "Llista d'adreces global",
"generate": "genereu",
"inactive": "Inactiu",
"internal": "Intern",
"internal_info": "Els àlies interns són només accessibles des del mateix domini o els àlies de dominis.",
"mailbox_quota_def": "Quota per defecte de la bústia",
"nexthop": "Següent salt",
"private_comment": "Comentari privat",
"public_comment": "Comentari púlbic"
"app_password": "Afegir contrasenya a l'aplicació"
},
"admin": {
"access": "Accés",

View File

@@ -21,7 +21,7 @@
"ratelimit": "Omezení provozu",
"recipient_maps": "Mapy příjemců",
"smtp_ip_access": "Spravovat povolené hostitele pro SMTP",
"sogo_access": "Správa přístupu do SOGo",
"sogo_redirection": "Správa přístupu do SOGo",
"sogo_profile_reset": "Resetování profilu SOGo",
"spam_alias": "Dočasné aliasy",
"spam_policy": "Denylist/Allowlist",
@@ -149,7 +149,7 @@
"arrival_time": "Čas zařazení do fronty (čas na serveru)",
"authed_user": "Přihlášený uživatel",
"ays": "Opravdu chcete pokračovat?",
"ban_list_info": "Viz seznam zablokovaných IP níže: <b>síť (zbývající doba zablokování) - [akce]</b>.<br />IP adresy zařazené pro odblokování budou z aktivního seznamu odebrány během pár sekund.<br />Červeně označeny jsou položky z trvalých seznamů.",
"ban_list_info": "Seznam blokovaných IP adres je zobrazen níže: <b>síť (zbývající čas blokování) - [akce]</b>.<br />IP adresy zařazené pro odblokování budou z aktivního seznamu odebrány během několika sekund.<br />Červeně označené položky jsou pernamentní bloky z blacklistu.",
"change_logo": "Změnit logo",
"logo_normal_label": "Normální",
"logo_dark_label": "Inverzní pro tmavý režim",
@@ -183,16 +183,16 @@
"empty": "Žádné výsledky",
"excludes": "Vyloučit tyto příjemce",
"f2b_ban_time": "Doba blokování (s)",
"f2b_blacklist": "Sítě či hostitelé na seznamu zákazů",
"f2b_blacklist": "Sítě/hostitelé na blacklistu",
"f2b_filter": "Regex filtre",
"f2b_list_info": "Sítě či hostitelé na seznamu zákazů mají vždy větší váhu než položky na seznamu povolení. <b>Každá úprava seznamu trvá pár sekund.</b>",
"f2b_list_info": "Síť nebo hostitelé na blacklistu mají vždy větší váhu než položky na whitelistu. <b>Každá úprava seznamů trvá pár sekund.</b>",
"f2b_max_attempts": "Max. pokusů",
"f2b_netban_ipv4": "Rozsah IPv4 podsítě k zablokování (8-32)",
"f2b_netban_ipv6": "Rozsah IPv6 podsítě k zablokování (8-128)",
"f2b_parameters": "Parametry automatického firewallu",
"f2b_regex_info": "Záznamy které se berou v úvahu: SOGo, Postfix, Dovecot, PHP-FPM.",
"f2b_retry_window": "Časový horizont pro maximum pokusů (s)",
"f2b_whitelist": "Sítě či hostitelé na seznamu povolení",
"f2b_whitelist": "Sítě/hostitelé na whitelistu",
"filter_table": "Tabulka filtrů",
"forwarding_hosts": "Předávající servery",
"forwarding_hosts_add_hint": "Lze zadat IPv4/IPv6 adresy, sítě ve formátu CIDR, názvy serverů (budou převedeny na IP adresy) nebo názvy domén (budou převedeny na IP pomocí SPF záznamů, příp. MX záznamů).",
@@ -304,7 +304,7 @@
"rspamd_com_settings": "Název nastavení se vygeneruje automaticky, viz ukázky nastavení níže. Více informací viz <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">Rspamd dokumentace</a>",
"rspamd_global_filters": "Mapa globálních filtrů",
"rspamd_global_filters_agree": "Budu opatrný!",
"rspamd_global_filters_info": "Mapa globálních filtrů obsahuje různé seznamy povolených a zakázaných serverů",
"rspamd_global_filters_info": "Mapa globálních filtrů obsahuje jiné globální black- a whitelisty.",
"rspamd_global_filters_regex": "Názvy stačí k vysvětlení. Položky musejí obsahovat jen platné regulární výrazy ve tvaru \"/vyraz/parametry\" (e.g. <code>/.+@domena\\.tld/i</code>).<br>\n Každý výraz bude podroben základní kontrole, přesto je možné Rspamd 'rozbít', nebude-li syntax zcela korektní.<br>\n Rspamd se pokusí po každé změně načíst mapu znovu. V případě potíží <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">restartujte Rspamd</a>, aby se konfigurace načetla explicitně.",
"rspamd_settings_map": "Nastavení Rspamd",
"sal_level": "Úroveň 'Moo'",
@@ -727,13 +727,13 @@
"sieve_desc": "Krátký popis",
"sieve_type": "Typ filtru",
"skipcrossduplicates": "Přeskočit duplicitní zprávy (\"první přijde, první mele\")",
"sogo_access": "Přímé předání na SOGo",
"sogo_access_info": "Po přihlášení je uživatel automaticky přesměrován do služby SOGo.",
"sogo_redirection": "Přímé předání na SOGo",
"sogo_redirection_info": "Po přihlášení je uživatel automaticky přesměrován do služby SOGo.",
"sogo_visible": "Alias dostupný v SOGo",
"sogo_visible_info": "Tato volba určuje objekty, jež lze zobrazit v SOGo (sdílené nebo nesdílené aliasy, jež ukazuje alespoň na jednu schránku).",
"spam_alias": "Vytvořit nebo změnit dočasné aliasy",
"spam_filter": "Spam filtr",
"spam_policy": "Přidat nebo odebrat položky seznamu",
"spam_policy": "Přidat nebo odebrat položky whitelistu/blacklistu",
"spam_score": "Nastavte vlastní skóre spamu",
"subfolder2": "Synchronizace do podsložky v cílovém umístění<br><small>(prázdné = nepoužívat podsložku)</small>",
"syncjob": "Upravit synchronizační úlohu",
@@ -883,7 +883,7 @@
"bcc": "BCC",
"bcc_destination": "Cíl kopie",
"bcc_destinations": "Cíl kopií",
"bcc_info": "Skrytá kopie (mapa BCC) se používá pro tiché předávání kopií všech zpráv na jinou adresu. Mapa příjemců se použije, funguje-li je místní cíl jako adresát zprávy. Totéž platí pro mapy odesílatelů.<br/>\n Místní cíl se nedozví, selže-li doručení na cíl BCC.",
"bcc_info": "<br/>Skrytá kopie (mapa BCC) se používá pro tiché předávání kopií všech zpráv na jinou adresu. Mapa příjemců se použije, funguje-li je místní cíl jako adresát zprávy. Totéž platí pro mapy odesílatelů.<br/>\n Místní cíl se nedozví, selže-li doručení na cíl BCC.",
"bcc_local_dest": "Týká se",
"bcc_map": "Skrytá kopie",
"bcc_map_type": "Typ skryté kopie",
@@ -1060,7 +1060,7 @@
"notified": "Oznámeno",
"qhandler_success": "Požadavek úspěšně přijat. Můžete nyní zavřít okno.",
"qid": "Rspamd QID",
"qinfo": "Karanténa uloží do databáze odmítnutou poštu (odesílatel se <em>nedozví</em>, že pošta byla doručena) jakož i poštu, jež se jako kopie doručuje do složky Nevyžádaná pošta.\n <br>\"Naučit jako spam a smazat\" předá zprávu systému k naučení bayesiánskou analýzou jako spam a současně stanoví fuzzy hashe pro odmítání podobných zpráv v budoucnosti.\n <br> Vezměte na vědomí, že učení více zpráv může být podle výkonnosti systému zabrat více času. <br> Položky na seznamu zákazů jsou z karantény vyloučeny.",
"qinfo": "Karanténní systém uloží odmítnutou poštu do databáze (odesílatel se <em>nedozví</em>, že pošta byla doručena) jakož i pošta, která bude jako kopie doručena do složky Nevyžádaná pošta. \r\n<br>\"Naučit jako spam a smazat\" naučí zprávu jako spam přes Bayesian theorem a současně vypočítá fuzzy hashes pro odmítnutí podobných zpráv v budoucnosti. \r\n<br> Prosím, berte na vědomí, že naučení více zpráv může být - záleží na vašem systému - časově náročné . <br> Položky na černé listině jsou z karantény vyloučeny.",
"qitem": "Položka v karanténě",
"quarantine": "Karanténa",
"quick_actions": "Akce",
@@ -1347,12 +1347,12 @@
"sogo_profile_reset": "Resetovat profil SOGo",
"sogo_profile_reset_help": "Tato volba odstraní uživatelský profil SOGo a <b>nenávratně vymaže všechna data</b>.",
"sogo_profile_reset_now": "Resetovat profil",
"spam_aliases": "Spam aliasy",
"spam_aliases": "Dočasné e-mailové aliasy",
"spam_score_reset": "Obnovit výchozí nastavení serveru",
"spamfilter": "Filtr spamu",
"spamfilter_behavior": "Hodnocení",
"spamfilter_bl": "Seznam zákazů",
"spamfilter_bl_desc": "Zakázané emailové adresy budou <b>vždy</b> klasifikovány jako spam a odmítnuty. Odmítnutá pošta <b>se neukládá</b> do karantény. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou poštovní schránkou), s výjimkou doménových košů a samotné poštovní schránky.",
"spamfilter_bl": "Seznam zakázaných adres (blacklist)",
"spamfilter_bl_desc": "Zakázané emailové adresy budou <b>vždy</b> klasifikovány jako spam a odmítnuty. Odmítnutá pošta <b>nebude</b> uložena do karantény. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou poštovní schránkou), s výjimkou doménových košů a samotné poštovní schránky.",
"spamfilter_default_score": "Výchozí hodnoty",
"spamfilter_green": "Zelená: tato zpráva není spam",
"spamfilter_hint": "První hodnota představuje \"nízké spam skóre\" a druhá \"vysoké spam skóre\".",
@@ -1363,7 +1363,7 @@
"spamfilter_table_empty": "Žádná data k zobrazení",
"spamfilter_table_remove": "smazat",
"spamfilter_table_rule": "Pravidlo",
"spamfilter_wl": "Seznam povolení",
"spamfilter_wl": "Seznam povolených adres (whitelist)",
"spamfilter_wl_desc": "Povolené emailové adresy <b>nebudou nikdy klasifikovány jako spam</b>. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou mailovou schránkou), s výjimkou doménových košů a samotné mailové schránky.",
"spamfilter_yellow": "Žlutá: tato zpráva může být spam, bude označena jako spam a přesunuta do složky nevyžádané pošty",
"status": "Stav",
@@ -1407,10 +1407,7 @@
"authentication": "Autentifikace",
"overview": "Přehled",
"protocols": "Protokoly",
"value": "Hodnota",
"expire_never": "Nikdy nevyprší",
"forever": "Navždy",
"spam_aliases_info": "Spam alias je dočasná adresa, již lze použít k ochraně skutečných adres. <br>Případně lze nastavit také dobu platnosti, po níž je alias automaticky deaktivován, čímž se řeší případy zneužitých či odcizených adres."
"value": "Hodnota"
},
"warning": {
"cannot_delete_self": "Nelze smazat právě přihlášeného uživatele",

View File

@@ -17,7 +17,7 @@
"ratelimit": "Satsgrænse",
"recipient_maps": "Modtagerkort",
"smtp_ip_access": "Skift tilladte værter til SMTP",
"sogo_access": "Tillad styring af SOGo-adgang",
"sogo_redirection": "Tillad styring af SOGo-adgang",
"sogo_profile_reset": "Nulstil SOGo-profil",
"spam_alias": "Midlertidige aliasser",
"spam_policy": "Sortliste/hvidliste",

View File

@@ -22,7 +22,8 @@
"ratelimit": "Rate limit",
"recipient_maps": "Empfängerumschreibungen",
"smtp_ip_access": "Verwalten der erlaubten Hosts für SMTP",
"sogo_access": "Verwalten des SOGo-Zugriffsrechts erlauben",
"sogo_access": "Zugriff auf SOGo erlauben",
"sogo_redirection": "Leite den Benutzer nach dem Login zu SOGo weiter",
"sogo_profile_reset": "SOGo-Profil zurücksetzen",
"spam_alias": "Temporäre E-Mail-Aliasse",
"spam_policy": "Deny/Allowlist",
@@ -73,7 +74,6 @@
"inactive": "Inaktiv",
"internal": "Intern",
"internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.",
"sender_allowed": "Als dieser Alias senden erlauben",
"kind": "Art",
"mailbox_quota_def": "Standard-Quota einer Mailbox",
"mailbox_quota_m": "Max. Speicherplatz pro Mailbox (MiB)",
@@ -695,8 +695,6 @@
"inactive": "Inaktiv",
"internal": "Intern",
"internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.",
"sender_allowed": "Als dieser Alias senden erlauben",
"sender_allowed_info": "Wenn deaktiviert, kann dieser Alias nur E-Mails empfangen. Verwenden Sie Sender-ACL, um bestimmten Postfächern die Berechtigung zum Senden zu erteilen.",
"kind": "Art",
"last_modified": "Zuletzt geändert",
"lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*\\.google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
@@ -766,8 +764,9 @@
"sieve_desc": "Kurze Beschreibung",
"sieve_type": "Filtertyp",
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
"sogo_access": "Direktes weiterleiten an SOGo",
"sogo_access_info": "Nach dem Einloggen wird der Benutzer automatisch an SOGo weitergeleitet.",
"sogo_access": "Erlaube Zugriff auf SOGo",
"sogo_redirection": "Direktes weiterleiten an SOGo",
"sogo_redirection_info": "Nach dem Einloggen wird der Benutzer automatisch an SOGo weitergeleitet.",
"sogo_visible": "Alias in SOGo sichtbar",
"sogo_visible_info": "Diese Option hat lediglich Einfluss auf Objekte, die in SOGo darstellbar sind (geteilte oder nicht-geteilte Alias-Adressen mit dem Ziel mindestens einer lokalen Mailbox).",
"spam_alias": "Anpassen temporärer Alias-Adressen",
@@ -990,7 +989,7 @@
"sogo_visible": "Alias Sichtbarkeit in SOGo",
"sogo_visible_n": "Alias in SOGo verbergen",
"sogo_visible_y": "Alias in SOGo anzeigen",
"spam_aliases": "Spam-Alias",
"spam_aliases": "Temp. Alias",
"stats": "Statistik",
"status": "Status",
"sync_jobs": "Synchronisationen",
@@ -1284,9 +1283,7 @@
"encryption": "Verschlüsselung",
"excludes": "Ausschlüsse",
"expire_in": "Ungültig in",
"expire_never": "Niemals ungültig",
"fido2_webauthn": "FIDO2/WebAuthn",
"forever": "Für immer",
"force_pw_update": "Das Passwort für diesen Benutzer <b>muss</b> geändert werden, damit die Zugriffssperre auf die Groupware-Komponenten wieder freigeschaltet wird.",
"from": "von",
"generate": "generieren",
@@ -1351,8 +1348,7 @@
"sogo_profile_reset": "SOGo-Profil zurücksetzen",
"sogo_profile_reset_help": "Das Profil wird inklusive <b>aller</b> Kalender- und Kontaktdaten <b>unwiederbringlich gelöscht</b>.",
"sogo_profile_reset_now": "Profil jetzt zurücksetzen",
"spam_aliases": "Spam E-Mail-Aliasse",
"spam_aliases_info": "Ein Spam-Alias ist eine temporäre E-Mailadresse, die benutzt werden kann, um eine echte E-Mail Adressen zu schützen. <br>Optional kann eine Ablaufzeit gesetzt werden, sodass der Alias nach dem definierten Zeitraum automatisch deaktiviert wird, was missbrauchte oder geleakte Adressen effektiv entsorgt.",
"spam_aliases": "Temporäre E-Mail-Aliasse",
"spam_score_reset": "Auf Server-Standard zurücksetzen",
"spamfilter": "Spamfilter",
"spamfilter_behavior": "Bewertung",

View File

@@ -22,7 +22,8 @@
"ratelimit": "Rate limit",
"recipient_maps": "Recipient maps",
"smtp_ip_access": "Change allowed hosts for SMTP",
"sogo_access": "Allow management of SOGo access",
"sogo_access": "Allow access to SOGo",
"sogo_redirection": "Redirect User to SOGo after login",
"sogo_profile_reset": "Reset SOGo profile",
"spam_alias": "Temporary aliases",
"spam_policy": "Denylist/Allowlist",
@@ -73,7 +74,6 @@
"inactive": "Inactive",
"internal": "Internal",
"internal_info": "Internal aliases are only accessible from the own domain or alias domains.",
"sender_allowed": "Allow to send as this alias",
"kind": "Kind",
"mailbox_quota_def": "Default mailbox quota",
"mailbox_quota_m": "Max. quota per mailbox (MiB)",
@@ -695,8 +695,6 @@
"inactive": "Inactive",
"internal": "Internal",
"internal_info": "Internal aliases are only accessible from the own domain or alias domains.",
"sender_allowed": "Allow to send as this alias",
"sender_allowed_info": "If disabled, this alias can only receive mail. Use sender ACL to override and grant specific mailboxes permission to send.",
"kind": "Kind",
"last_modified": "Last modified",
"lookup_mx": "Destination is a regular expression to match against MX name (<code>.*\\.google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
@@ -767,8 +765,9 @@
"sieve_desc": "Short description",
"sieve_type": "Filter type",
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
"sogo_access": "Direct forwarding to SOGo",
"sogo_access_info": "After logging in, the user is automatically redirected to SOGo.",
"sogo_access": "Allow SOGo access",
"sogo_redirection": "Direct forwarding to SOGo",
"sogo_redirection_info": "After logging in, the user is automatically redirected to SOGo.",
"sogo_visible": "Alias is visible in SOGo",
"sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.",
"spam_alias": "Create or change time limited alias addresses",
@@ -1291,9 +1290,7 @@
"encryption": "Encryption",
"excludes": "Excludes",
"expire_in": "Expire in",
"expire_never": "Never Expire",
"fido2_webauthn": "FIDO2/WebAuthn",
"forever": "Forever",
"force_pw_update": "You <b>must</b> set a new password to be able to access groupware related services.",
"from": "from",
"generate": "generate",
@@ -1360,8 +1357,7 @@
"sogo_profile_reset": "Reset SOGo profile",
"sogo_profile_reset_help": "This will destroy a user's SOGo profile and <b>delete all contact and calendar data irretrievable</b>.",
"sogo_profile_reset_now": "Reset profile now",
"spam_aliases": "Spam email aliases",
"spam_aliases_info": "A spam alias is a temporary email address that can be used to protect real email addresses. <br>Optionally, an expiration time can be set so that the alias is automatically deactivated after the defined period, effectively disposing of abused or leaked addresses.",
"spam_aliases": "Temporary email aliases",
"spam_score_reset": "Reset to server default",
"spamfilter": "Spam filter",
"spamfilter_behavior": "Rating",

View File

@@ -26,7 +26,7 @@
"domain_relayhost": "Cambiar relayhost por un dominio",
"extend_sender_acl": "Permitir extender la ACL del remitente por direcciones externas",
"pw_reset": "Permitir el restablecimiento de la contraseña del usuario mailcow",
"sogo_access": "Permitir la gestión del acceso a SOGo",
"sogo_redirection": "Permitir la gestión del acceso a SOGo",
"mailbox_relayhost": "Cambiar el host de reenvío para un buzón",
"smtp_ip_access": "Cambiar hosts permitidos para SMTP"
},
@@ -662,10 +662,10 @@
"app_passwd_protocols": "Protocolos permitidos con contraseña de aplicación",
"domain_footer_info": "Los pies de página de dominio se añaden a todos los mensajes salientes remitidos por una dirección de dicho dominio.<br> Están disponibles las siguientes variables para el pie de página:",
"sender_acl_info": "Si el usuario del buzón A tiene permitido enviar como el buzón B, la dirección de remitente no se mostrará automáticamente como seleccionable en el campo \"De\" en SOGo.<br>\n El usuario del buzón B necesitará crear una delegación en SOGo para permitir al usuario A seleccionar su dirección como remitente. Para delegar un buzón en SOGo, utilice el menú (tres puntos) a la derecha del nombre del buzón en la esquina superior izquierda, en la vista de correo. Este comportamiento no se aplica a direcciones alias.",
"sogo_access_info": "Tras iniciar sesión, el usuario será redirigido automáticamente a SOGo.",
"sogo_redirection_info": "Tras iniciar sesión, el usuario será redirigido automáticamente a SOGo.",
"comment_info": "Un comentario privado no es visible para el usuario, mientras que un comentario público se muestra como descripción emergente al pasar el ratón en la vista general del usuario",
"quota_warning_bcc_info": "Los avisos se enviarán como copias separadas a los siguientes destinatarios. Se indicará en el asunto el usuario afectado entre paréntesis, como por ejemplo: <code>Aviso de cuota (usuario@ejemplo.com)</code>.",
"sogo_access": "Redirección directa a SOGo",
"sogo_redirection": "Redirección directa a SOGo",
"sogo_visible_info": "Esta opción solamente afecta a objetos que puedan ser visualizados en SOGo (alias compartidos o no compartidos que apunten al menos a un buzón interno). Si se oculta, el alias no aparecerá como seleccionable en SOGo.",
"extended_sender_acl_info": "Se aconseja importar una clave de dominio DKIM, si está disponible.<br>\n Recuerde añadir este servidor al registro SPF correspondiente.<br>\n Siempre que se añada un dominio o alias a este servidor, que se superponga con una dirección externa, se eliminará la dirección externa.<br>\n Utilice @dominio.tld para permitir enviar como *@dominio.tld.",
"pushover_info": "La configuración de notificaciones push se aplicará a todos los mensajes limpios (no spam) entregados a <b>%s</b> incluyendo alias (compartidos, no compartidos, etiquetados).",
@@ -1084,7 +1084,6 @@
"aliases_send_as_all": "No verificar permisos del remitente para los siguientes dominios (y sus aliases)",
"change_password": "Cambiar contraseña",
"create_syncjob": "Crear nuevo trabajo de sincronización",
"created_on": "Creado",
"daily": "Cada día",
"day": "Día",
"description": "Descripción",
@@ -1096,9 +1095,6 @@
"edit": "Editar",
"encryption": "Cifrado",
"excludes": "Excluye",
"expire_in": "Expirará en",
"expire_never": "Nunca expirará",
"forever": "Siempre",
"hour": "Hora",
"hourly": "Cada hora",
"hours": "Horas",
@@ -1119,8 +1115,7 @@
"shared_aliases": "Alias compartidos",
"shared_aliases_desc": "Los alias compartidos no se ven afectados por la configuración específica del usuario, como el filtro de correo no deseado o la política de cifrado. Los filtros de spam correspondientes solo pueden ser realizados por un administrador como una política de dominio.",
"sogo_profile_reset": "Resetear perfil SOGo",
"spam_aliases": "Alias de email de spam",
"spam_aliases_info": "Un alias de spam es una dirección de correo electrónico temporal que se puede usar para proteger direcciones de correo electrónico reales. <br>Opcionalmente, se puede establecer un tiempo de expiración para que el alias se desactive automáticamente después del período definido, eliminando efectivamente las direcciones abusadas o filtradas.",
"spam_aliases": "Alias de email temporales",
"spamfilter": "Filtro anti-spam",
"spamfilter_behavior": "Clasificación",
"spamfilter_bl": "Lista negra",

View File

@@ -13,7 +13,7 @@
"quarantine_notification": "Muuta karanteeni-ilmoituksia",
"ratelimit": "Määrä raja",
"recipient_maps": "Vastaanottajakartat",
"sogo_access": "Salli SOGo-pääsyn hallintaan",
"sogo_redirection": "Salli SOGo-pääsyn hallintaan",
"sogo_profile_reset": "Nollaa SOGo-profiili",
"spam_alias": "Väliaikaiset aliakset",
"spam_policy": "Musta lista / sallitut lista",

View File

@@ -16,9 +16,9 @@
"quarantine_notification": "Modifier la notification de quarantaine",
"quarantine_category": "Modifier la catégorie de la notification de quarantaine",
"ratelimit": "Limite d'envoi",
"recipient_maps": "Cartes des destinataires",
"recipient_maps": "Cartes destinataire",
"smtp_ip_access": "Changer les hôtes autorisés pour SMTP",
"sogo_access": "Autoriser la gestion des accès à SOGo",
"sogo_redirection": "Autoriser la gestion des accès à SOGo",
"sogo_profile_reset": "Réinitialiser le profil SOGo",
"spam_alias": "Alias temporaires",
"spam_policy": "Liste Noire/Liste Blanche",
@@ -109,9 +109,7 @@
"bcc_dest_format": "La destination Cci doit être une seule adresse de courriel valide.<br>Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici.",
"tags": "Etiquettes",
"app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application",
"dry": "Simuler la synchronisation",
"internal": "Interne",
"internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine."
"dry": "Simuler la synchronisation"
},
"admin": {
"access": "Accès",
@@ -409,9 +407,7 @@
"iam_host": "Hôte",
"iam_host_info": "Saisissez un ou plusieurs hôtes LDAP, séparés par des virgules.",
"iam_import_users": "Importer des utilisateurs",
"filter": "Filtrer",
"needs_restart": "nécessite un redémarrage",
"iam": "Fournisseur d'identité"
"filter": "Filtrer"
},
"danger": {
"access_denied": "Accès refusé ou données de formulaire non valides",
@@ -445,7 +441,7 @@
"global_filter_write_error": "Impossible décrire le fichier de filtre : %s",
"global_map_invalid": "ID de carte globale %s non valide",
"global_map_write_error": "Impossible décrire lID de la carte globale %s : %s",
"goto_empty": "Une adresse alias doit contenir au moins une adresse 'goto' valide",
"goto_empty": "Une adresse alias doit contenir au moins une adresse 'goto'valide",
"goto_invalid": "Adresse Goto %s non valide",
"ham_learn_error": "Erreur d'apprentissage Ham : %s",
"imagick_exception": "Erreur : Exception Imagick lors de la lecture de limage",
@@ -552,11 +548,7 @@
"generic_server_error": "Une erreur de serveur inattendue s'est produite. Veuillez contacter votre administrateur.",
"authsource_in_use": "Le fournisseur d'identité ne peut pas être modifié ou supprimé car il est actuellement utilisé par un ou plusieurs utilisateurs.",
"iam_test_connection": "Échec de la connexion",
"required_data_missing": "La donnée requise %s est manquante",
"max_age_invalid": "L'âge maximum %s est invalide",
"mode_invalid": "Le mode %s est invalide",
"mx_invalid": "L'enregistrement MX %s est invalide",
"version_invalid": "La version %s est invalide"
"required_data_missing": "La donnée requise %s est manquante"
},
"debug": {
"chart_this_server": "Graphique (ce serveur)",
@@ -701,7 +693,7 @@
"spam_score": "Définir un score spam personnalisé",
"subfolder2": "Synchronisation dans le sous-dossier sur la destination<br><small>(vide = ne pas utiliser de sous-dossier)</small>",
"syncjob": "Modifier la tâche de synchronisation",
"target_address": "Adresse(s) Goto <small>(séparé(s) par des virgules)</small>",
"target_address": "Adresse(s) Goto<small>(séparé(s) par des virgules)</small>",
"target_domain": "Domaine cible",
"timeout1": "Délai de connexion à lhôte distant",
"timeout2": "Délai de connexion à lhôte local",
@@ -732,7 +724,7 @@
"none_inherit": "Aucun / Héritage",
"quota_warning_bcc": "Avertissement sur les quotas BCC",
"quota_warning_bcc_info": "Les avertissements seront envoyés en copies séparées aux destinataires suivants. Le sujet sera précédé du nom d'utilisateur correspondant entre parenthèses, par exemple : <code>Avertissement sur les quotas (user@example.com)</code>.",
"sogo_access_info": "Après s'être connecté, l'utilisateur est automatiquement redirigé vers SOGo.",
"sogo_redirection_info": "Après s'être connecté, l'utilisateur est automatiquement redirigé vers SOGo.",
"admin": "Modifier l'administrateur",
"password_recovery_email": "Adresse email de récupération",
"mailbox_rename_title": "Nouveau nom de la partie locale de la boîte de réception",
@@ -740,22 +732,9 @@
"mailbox_rename_agree": "J'ai fait une sauvegarde.",
"mailbox_rename_warning": "IMPORTANT ! Faites une sauvegarde avant de renommer la boîte de réception.",
"mailbox_rename_alias": "Créer un alias automatiquement",
"sogo_access": "Redirection directe vers SOGo",
"sogo_redirection": "Redirection directe vers SOGo",
"pushover": "Pushover",
"pushover_sound": "Son",
"internal": "Interne",
"internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine.",
"mta_sts": "MTA-STS",
"mta_sts_version": "Version",
"mta_sts_version_info": "Défini la version du standard MTA-STS actuellement seul <code>STSv1</code> est valide.",
"mta_sts_mode": "Mode",
"mta_sts_max_age": "Âge maximum",
"mta_sts_mx": "Serveur MX",
"mta_sts_mx_notice": "Plusieurs serveurs MX peuvent être spécifiés (séparés par des virgules).",
"mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> est un standard qui oblige la délivrance des courriels entre les serveurs de courriels à utiliser TLS avec des certificats valides. <br>Il est utilisé quand <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> n'est pas possible à cause d'un manque ou d'un non support de DNSSEC.<br><b>Note</b> : Si le domaine du destinataire supporte DANE avec DNSSEC, DANE est <b>toujours</b> préféré MTA-STS sert seulement en secours.",
"mta_sts_mode_info": "Il y a trois modes parmi lesquels choisir :<ul><li><em>testing</em> la politique est seulement surveillée, les violations n'ont pas d'impact.</li><li><em>enforce</em> la politique est appliquée strictement, les connexions sans TLS valide sont rejetées.</li><li><em>none</em> la politique est publiée mais non appliquée.</li></ul>",
"mta_sts_max_age_info": "Durée en secondes pendant laquelle les serveurs de courriel peuvent mettre en cache cette politique avant de revérifier.",
"mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM)."
"pushover_sound": "Son"
},
"footer": {
"cancel": "Annuler",
@@ -810,8 +789,7 @@
"login_linkstext": "L'identifiant n'est pas correct ?",
"login_usertext": "Se connecter en tant qu'utilisateur",
"login_domainadmintext": "Se connecter en tant qu'administrateur du domaine",
"login_admintext": "Se connecter en tant qu'administrateur",
"email": "Adresse de courriel"
"login_admintext": "Se connecter en tant qu'administrateur"
},
"mailbox": {
"action": "Action",
@@ -918,7 +896,7 @@
"recipient_map_new_info": "La destination de la carte du destinataire doit être une adresse de courriel valide ou un nom de domaine.",
"recipient_map_old": "Destinataire original",
"recipient_map_old_info": "La destination originale des cartes des destinataires doit être une adresse de courriel valide ou un nom de domaine.",
"recipient_maps": "Cartes des destinataires",
"recipient_maps": "Cartes des bénéficiaires",
"relay_all": "Relayer tous les destinataires",
"remove": "Supprimer",
"resources": "Ressources",
@@ -987,8 +965,7 @@
"syncjob_check_log": "Vérifier le journal",
"recipient": "Destinataire",
"open_logs": "Afficher les journaux",
"iam": "Fournisseur d'identité",
"internal": "Interne"
"iam": "Fournisseur d'identité"
},
"oauth2": {
"access_denied": "Veuillez vous connecter en tant que propriétaire de la boîte de réception pour accorder laccès via Oauth2.",
@@ -1245,7 +1222,7 @@
"email_and_dav": "Courriel, calendriers et contacts",
"encryption": "Chiffrement",
"excludes": "Exclus",
"expire_in": "Expirer dans",
"expire_in": "Expire dans",
"force_pw_update": "Vous <b>devez</b> définir un nouveau mot de passe pour pouvoir accéder aux services liés aux logiciels de groupe.",
"generate": "générer",
"hour": "heure",
@@ -1266,7 +1243,7 @@
"no_last_login": "Aucune dernière information de connexion à l'interface",
"no_record": "Pas d'enregistrement",
"password": "Mot de passe",
"password_now": "Mot de passe actuel (confirmer les changements)",
"password_now": "Mot de passe courant (confirmer les changements)",
"password_repeat": "Mot de passe (répéter)",
"pushover_evaluate_x_prio": "Acheminement du courrier hautement prioritaire [<code>X-Priority: 1</code>]",
"pushover_info": "Les paramètres de notification push sappliqueront à tout le courrier propre (non spam) livré à <b>%s</b> y compris les alias (partagés, non partagés, étiquetés).",
@@ -1373,12 +1350,7 @@
"mailbox_general": "Général",
"mailbox_settings": "Paramètres",
"tfa_info": "L'authentification à deux facteurs permet de protéger votre compte. Si vous l'activez, vous aurez besoin de mots de passe d'application pour vous connecter à des applications ou des services qui ne prennent pas en charge l'authentification à deux facteurs (par exemple les clients e-mails).",
"overview": "Vue d'ensemble",
"expire_never": "Ne jamais expirer",
"forever": "Pour toujours",
"spam_aliases_info": "Un alias de spam est une adresse de courriel temporaire qui peut être utilisée pour protéger les véritables adresses de courriel. <br> De manière optionnelle, une durée d'expiration peut être définie afin que l'alias soit automatiquement désactivé après la période définie, éliminant ainsi les adresses étant abusées ou ayant fuité.",
"authentication": "Authentification",
"protocols": "Protocoles"
"overview": "Vue d'ensemble"
},
"warning": {
"cannot_delete_self": "Impossible de supprimer lutilisateur connecté",

View File

@@ -6,8 +6,7 @@
"weeks": "Εβδομάδες",
"with_app_password": "με κωδικό εφαρμογής",
"year": "χρόνος",
"years": "χρόνια",
"value": "Τιμή"
"years": "χρόνια"
},
"warning": {
"cannot_delete_self": "Αδυναμία διαγραφής συνδεδεμένου χρήστη",
@@ -17,170 +16,5 @@
"hash_not_found": "Η κατακερματισμένη τιμή (hash value) δεν βρέθηκε ή έχει είδη διαγραφεί.",
"ip_invalid": "Παραλείφθηκε μη έγκυρη διεύθυνση IP: %s",
"is_not_primary_alias": "Παραλείφθηκε μη πρωτεύον ψευδώνυμο %s"
},
"acl": {
"alias_domains": "Προσθήκη ψευδωνύμων τομέων",
"app_passwds": "Διαχείριση κωδικών εφαρμογής",
"bcc_maps": "χαρτογράφηση BCC",
"delimiter_action": "Ενέργεια οριοθέτη",
"domain_desc": "Αλλαγή περιγραφής τομέα",
"domain_relayhost": "Αλλαγή του διακομιστή αναμετάδοσης για ένα τομέα",
"eas_reset": "Επαναφορά συσκευών EAS",
"extend_sender_acl": "Να επιτρέπεται η επέκταση ACL του αποστολέα με εξωτερικές διευθύνσεις",
"filters": "Φίλτρα",
"login_as": "Είσοδος ως χρήστης e-mail",
"mailbox_relayhost": "Αλλαγή διακομιστή αναμετάδοσης για ένα γραμματοκιβώτιο",
"prohibited": "Απαγορεύεται από την ACL",
"protocol_access": "Αλλαγή πρόσβασης πρωτοκόλλου",
"pushover": "Pushover",
"pw_reset": "Επιτρέψτε την επαναφορά κωδικού πρόσβασης του χρήστη",
"quarantine": "Ενέργειες καραντίνας",
"quarantine_attachments": "Συνημμένα καραντίνας",
"quarantine_category": "Αλλαγή κατηγορίας ειδοποιήσεων καραντίνας",
"quarantine_notification": "Αλλαγή ειδοποιήσεων καραντίνας",
"ratelimit": "Όριο τιμής",
"recipient_maps": "Χάρτες παραληπτών",
"smtp_ip_access": "Αλλαγή επιτρεπόμενων διακομιστών SMTP",
"sogo_access": "Επιτρέψτε τη διαχείριση της πρόσβασης στο SOGo",
"sogo_profile_reset": "Επαναφορά του προφίλ SOGo",
"spam_alias": "Προσωρινά ψευδώνυμα",
"spam_policy": "Λίστα απορρίψεων/Λίστα επιτρεπόμενων",
"spam_score": "Βαθμολογία ανεπιθύμητης αλληλογραφίας",
"syncjobs": "Εργασίες συγχρονισμού",
"tls_policy": "Πολιτική TLS",
"unlimited_quota": "Απεριόριστο όριο για γραμματοκιβώτια"
},
"add": {
"activate_filter_warn": "Όλα τα άλλα φίλτρα θα απενεργοποιηθούν, όταν επιλεγεί η επιλογή \"ενεργό\".",
"active": "Ενεργό",
"add": "Προσθήκη",
"add_domain_only": "Προσθήκη μόνο του τομέα",
"add_domain_restart": "Προσθήκη του τομέα και επανεκκίνηση του SOGo",
"alias_address": "Διευθύνσεις ψευδωνύμων",
"alias_address_info": "<small>Πλήρης διεύθυνση(εις) e-mail ή @example.com, για να λαμβάνετε ΟΛΑ τα μηνύματα ενός τομέα (χωρισμένα με κόμα). <b>μόνο τομείς του mailcow</b>.</small>",
"alias_domain": "Ψευδώνυμο τομέα",
"alias_domain_info": "<small>Μόνο έγκυρα ονόματα τομέα (χωρισμένα με κόμα).</small>",
"app_name": "Όνομα εφαρμογής",
"app_password": "Προσθήκη κωδικού εφαρμογής",
"app_passwd_protocols": "Επιτρεπόμενα πρωτόκολλα για κωδικούς εφαρμογών",
"automap": "Αυτόματη αντιστοίχηση φακέλων (\"Απεσταλμένα μηνύματα\", \"Απεσταλμένα\" => \"Στάλθηκαν\" κ.τ.λ.)",
"backup_mx_options": "Επιλογές αναμετάδοσης",
"bcc_dest_format": "Η BCC διεύθυνση πρέπει να είναι μία και έγκυρη διεύθυνση e-mail.<br>Αν θέλετε να στείλετε αντίγραφα σε πολλούς παραλήπτες, δημιουργήστε ένα ψευδόνυμο για όλους και χρησιμοποιήστε το εδώ.",
"comment_info": "Τα προσωπικά σχόλια δεν είναι ορατά στον χρήστη. Τα δημόσια σχόλια εμφανίζονται ως tooltips.",
"custom_params": "Προσαρμοσμένες παράμετροι",
"custom_params_hint": "Σωστή σύνταξη: --param=xy, λάθος σύνταξη: --param xy",
"delete1": "Διαγραφή όταν ολοκληρωθεί",
"delete2": "Διαγραφή μηνυμάτων στον προορισμό που δεν βρίσκονται στην πηγή",
"delete2duplicates": "Διαγραφή διπλότυπων στον προορισμό",
"description": "Περιγραφή",
"destination": "Προορισμός",
"disable_login": "Απαγόρευση εισόδου (η εισερχόμενη αλληλογραφία εξακολουθεί να γίνεται δεκτή)",
"domain": "Τομέας",
"domain_matches_hostname": "Ο τομέας %s είναι ο ίδιος με το όνομα του διακομιστή",
"domain_quota_m": "Συνολικό όριο τομέα (MiB)",
"dry": "Προσομοίωση συγχρονισμού",
"enc_method": "Μέθοδος κρυπτογράφησης",
"exclude": "Εξαίρεση αντικειμένων (regex)",
"full_name": "Πλήρες όνομα",
"gal": "Κοινόχρηστη λίστα διευθύνσεων"
},
"danger": {
"unknown": "Παρουσιάστηκε κάποιο άγωνστο σφάλμα",
"unknown_tfa_method": "Άγνωστη μέθοδος TFA",
"unlimited_quota_acl": "Το απεριόριστο όριο απαγορεύεται από την ACL",
"username_invalid": "Το όνομα χρήστη %s δεν μπορεί να χρησιμοποιηθεί",
"validity_missing": "Παρακαλώ ορίστε μία περίοδο εγκυρότητας",
"value_missing": "Παρακαλώ συμπληρώστε όλα τα δεδομένα",
"version_invalid": "Η έκδοση %s δεν είναι έγκυρη",
"yotp_verification_failed": "Η επαλήθευση μέσω Yubico OTP απέτυχε: %s"
},
"datatables": {
"collapse_all": "Σύμπτυξη όλων",
"decimal": ".",
"emptyTable": "Δεν υπάρχουν εγγραφές",
"expand_all": "Επέκταση όλων",
"info": "Εμφανίζονται _START_ εώς _END_ από _TOTAL_ εγγραφές",
"infoEmpty": "Εμφανίζονται 0 εώς 0 από 0 εγγραφές",
"infoFiltered": "(φιλτραρισμένες από _MAX_ συνολικές εγγραφές)",
"thousands": ",",
"lengthMenu": "Εμφάνιση _MENU_ εγγραφών",
"loadingRecords": "Γίνεται φόρτωση...",
"processing": "Παρακαλώ περιμένετε...",
"search": "Αναζήτηση:",
"zeroRecords": "Δε βρέθηκαν εγγραφές",
"paginate": {
"first": "Πρώτη",
"last": "Τελευταία",
"next": "Επόμενη",
"previous": "Προηγούμενη"
},
"aria": {
"sortAscending": ": ενεργοποίηση αύξουσας ταξινόμησης",
"sortDescending": ": ενεργοποίηση φθίνουσας ταξινόμησης"
}
},
"debug": {
"architecture": "Αρχιτεκτονική",
"chart_this_server": "Γράφημα (αυτός ο διακομιστής)",
"containers_info": "Πληροφορίες για τον container",
"container_running": "Εκτελείται",
"container_disabled": "Ο container έχει σταματήσει ή απενεργοποιηθεί",
"container_stopped": "Σταματημένος",
"cores": "Πυρήνες",
"current_time": "Ώρα συστήματος",
"disk_usage": "Χρήση αποθ. χώρου",
"docs": "Έγγραφα",
"error_show_ip": "Δεν είναι δυνατή η επίλυση της δημόσιας IP διεύθυνσης",
"external_logs": "Εξωτερικά αρχεία καταγραφής",
"history_all_servers": "Ιστορικό (Όλοι οι διακομιστές)",
"in_memory_logs": "Αρχεία καταγραφής στη μνήμη",
"last_modified": "Τελευταία τροποποίηση",
"log_info": "<p>mailcow <b>in-memory logs</b> are collected in Redis lists and trimmed to LOG_LINES (%d) every minute to reduce hammering.\n <br>In-memory logs are not meant to be persistent. All applications that log in-memory, also log to the Docker daemon and therefore to the default logging driver.\n <br>The in-memory log type should be used for debugging minor issues with containers.</p>\n <p><b>External logs</b> are collected via API of the given application.</p>\n <p><b>Static logs</b> are mostly activity logs, that are not logged to the Dockerd but still need to be persistent (except for API logs).</p>",
"login_time": "Ώρα",
"logs": "Αρχεία καταγραφής",
"memory": "Μνήμη",
"online_users": "Συνδεδεμένοι χρήστες",
"restart_container": "Επανεκκίνηση",
"service": "Υπηρεσία",
"show_ip": "Εμφάνιση δημόσιας IP",
"size": "Μέγεθος",
"started_at": "Ξεκίνησε στις",
"started_on": "Ξεκίνησε στις",
"static_logs": "Στατικά αρχεία καταγραφής",
"success": "Επιτυχία",
"system_containers": "Σύστημα και Containers",
"timezone": "Ζώνη ώρας",
"uptime": "Χρόνος λειτουργίας",
"update_available": "Υπάρχει διαθέσιμη ενημέρωση",
"no_update_available": "Έχετε τη τελευταία έκδοση του συστήματος",
"update_failed": "Δεν ήταν δυνατός ο έλεγχος για ενημερώσεις",
"username": "Όνομα χρήστη",
"wip": "Currently Work in Progress"
},
"diagnostics": {
"cname_from_a": "Value derived from A/AAAA record. This is supported as long as the record points to the correct resource.",
"dns_records": "Εγγραφές DNS",
"dns_records_24hours": "Παρακαλώ σημειώστε ότι οι αλλαγές στο DNS μπορεί να χρειαστούν μέχρι 24 ώρες για να ενημερωθούν σωστά και να εμφανιστούν σε αυτή τη σελίδα. Ο σκοπός της είναι να δείτε πως μπορείτε να ρυθμίσετε σωστά τις εγγραφές DNS και να ελέγξετε αν είναι σωστές.",
"dns_records_data": "Σωστά δεδομένα",
"dns_records_docs": "Παρακαλώ συμβουλευτείτε επίσης <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">την τεκμηρίωση</a>.",
"dns_records_name": "Όνομα",
"dns_records_status": "Τρέχουσα κατάσταση",
"dns_records_type": "Τύπος",
"optional": "Αυτή η εγγραφή είναι προαιρετική."
},
"edit": {
"acl": "ACL (Δικαίωμα)",
"active": "Ενεργό",
"admin": "Επεξεργασία διαχειριστή",
"advanced_settings": "Ρυθμίσεις για προχωρημένους",
"alias": "Επεξεργασία ψευδώνυμου",
"allow_from_smtp": "Επέτρεψε μόνο σε αυτές τις IPs να χρησιμοποιήσουν το <b>SMTP</b>",
"allow_from_smtp_info": "Αφήστε το κενό για να επιτρέψετε όλους τους αποστολείς.<br>IPv4/IPv6 διευθύνσεις και δίκτυα.",
"allowed_protocols": "Επιτρεπόμενα πρωτόκολλα για απ' ευθείας πρόσβαση από τους χρήστες (δεν επηρεάζει τα πρωτόκολλα κωδικών πρόσβασης εφαρμογής)",
"app_name": "Όνομα εφαρμογής",
"app_passwd": "Κωδικός πρόσβασης εφαρμογής",
"app_passwd_protocols": "Επιτρέπομενα πρωτόκολλα για τον κωδικό εφαρμογής",
"automap": "Αυτόματη αντιστοίχηση φακέλων (\"Απεσταλμένα μηνύματα\", \"Απεσταλμένα\" => \"Στάλθηκαν\" κ.τ.λ.)",
"backup_mx_options": "Επιλογές αναμετάδοσης"
}
}

View File

@@ -295,9 +295,7 @@
"user_quicklink": "Gyorshivatkozás elrejtése a Felhasználói bejelentkezési oldalra",
"validate_license_now": "GUID érvényesítése a licenszszerverrel szemben",
"yes": "&#10003;",
"success": "Siker",
"login_page": "Belépő oldal",
"needs_restart": "újraindítást igényel"
"success": "Siker"
},
"edit": {
"active": "Aktív",
@@ -417,8 +415,8 @@
"sieve_desc": "Rövid leírás",
"sieve_type": "Szűrő típusa",
"skipcrossduplicates": "Átugrani a duplikált üzeneteket a mappák között (aki előbb jön, előbb kapja)",
"sogo_access": "Közvetlen továbbítás a SOGo-ra",
"sogo_access_info": "A bejelentkezés után a felhasználó automatikusan átirányításra kerül a SOGo-ra.",
"sogo_redirection": "Közvetlen továbbítás a SOGo-ra",
"sogo_redirection_info": "A bejelentkezés után a felhasználó automatikusan átirányításra kerül a SOGo-ra.",
"sogo_visible": "Alias látható a SOGo-ban",
"sogo_visible_info": "Ez az opció csak azokra az objektumokra vonatkozik, amelyek megjeleníthetők a SOGo-ban (megosztott vagy nem megosztott alias címek, amelyek legalább egy helyi postafiókra mutatnak). Ha el van rejtve, egy alias nem jelenik meg választható feladóként a SOGo-ban.",
"spam_alias": "Időkorlátos alias címek létrehozása vagy módosítása",
@@ -1050,7 +1048,7 @@
"syncjobs": "Szinkronizálási feladatok",
"tls_policy": "TLS szabályzat",
"unlimited_quota": "Korlátlan kvóta a postafiókok számára",
"sogo_access": "A SOGo-hozzáférés kezelésének lehetővé tétele",
"sogo_redirection": "A SOGo-hozzáférés kezelésének lehetővé tétele",
"pw_reset": "Lehetővé teszi a mailcow felhasználói jelszavak visszaállítását"
},
"diagnostics": {
@@ -1072,7 +1070,7 @@
"post_domain_add": "A \"sogo-mailcow\" SOGo konténert újra kell indítani egy új tartomány hozzáadása után!<br><br>Kiegészítésképpen a tartományok DNS-konfigurációját is felül kell vizsgálni. A DNS-konfiguráció jóváhagyása után indítsa újra az \"acme-mailcow\"-t, hogy automatikusan generáljon tanúsítványokat az új tartományhoz (autoconfig.&lt;domain&gt;, autodiscover.&lt;domain&gt;).<br>Ez a lépés opcionális, és 24 óránként megismétlődik.",
"dry": "Szinkronizálás szimulálása",
"inactive": "Inaktív",
"kind": "Típus",
"kind": "Kedves",
"mailbox_quota_m": "Maximális kvóta postafiókonként (MiB)",
"mailbox_username": "Felhasználónév (az e-mail cím bal oldali része)",
"max_aliases": "Max. lehetséges álnevek",
@@ -1094,9 +1092,9 @@
"exclude": "Objektumok kizárása (regex)",
"full_name": "Teljes név",
"gal": "Globális címlista",
"goto_ham": "Tanítás <span class=\"text-success\"><b>valódi</b></span> levélként",
"goto_ham": "Tanulj <span class=\"text-success\"><b>sonkaként</b></span>",
"goto_null": "Leveleket csendben eldobni",
"goto_spam": "Tanítás <span class=\"text-danger\"><b>spam</b></span>ként",
"goto_spam": "Tanuld <span class=\"text-danger\"><b>spamként</b></span>",
"syncjob_hint": "Ne feledje, hogy a jelszavakat egyszerű szöveges formában kell elmenteni!",
"target_address": "Továbbítási címek",
"target_address_info": "<small>Teljes e-mail cím(ek) (vesszővel elválasztva).</small>",
@@ -1104,7 +1102,7 @@
"comment_info": "A privát megjegyzés nem látható a felhasználó számára, míg a nyilvános megjegyzés tooltip-ként jelenik meg, amikor a felhasználó áttekintésében a megjegyzésre mutat.",
"custom_params": "Egyéni paraméterek",
"gal_info": "A GAL tartalmazza a tartomány összes objektumát, és egyetlen felhasználó sem szerkesztheti. A SOGo-ban a Szabad/Elfoglalt információ hiányzik, ha ki van kapcsolva! <b>Indítsa újra a SOGo-t a változások alkalmazásához.</b>",
"hostname": "Hoszt",
"hostname": "Házigazda",
"backup_mx_options": "Továbbítási opciók",
"custom_params_hint": "Megfelelő: --param=xy, Rossz: --param xy",
"delete1": "Törlés a forrásból, ha befejeződött",
@@ -1142,109 +1140,6 @@
"sieve_type": "Szűrő típusa",
"skipcrossduplicates": "Duplikált üzenetek átugrása mappák között (érkezési sorrendben)",
"subscribeall": "Feliratkozás minden mappára",
"syncjob": "Szinkronizálási feladat hozzáadása",
"internal": "Belső",
"internal_info": "Belső álnevek csak a saját domain vagy domain álnév számára elérhető."
},
"danger": {
"access_denied": "Hozzáférés megtagatva vagy nem megfelelő űrlap adat",
"alias_domain_invalid": "Az alias domain %s érvénytelen",
"alias_empty": "Az alias cím nem lehet üres",
"alias_goto_identical": "Az alias és a goto cím nem lehetnek azonosak",
"alias_invalid": "Az alias cím %s érvénytelen",
"aliasd_targetd_identical": "Az alias tartomány nem lehet azonos a céltartománnyal: %s",
"aliases_in_use": "A maximális aliasoknak nagyobbnak vagy egyenlőnek kell lenniük mint %d",
"app_name_empty": "Az alkalmazás neve nem lehet üres",
"app_passwd_id_invalid": "Alkalmazás jelszó ID %s érvénytelen",
"authsource_in_use": "A személyazonosság szolgáltatót nem lehet megváltoztatni vagy törölni, mivel ez jelenleg használatban van legalább 1 felhasználónál.",
"bcc_empty": "BCC cél nem lehet üres",
"bcc_exists": "A %s típushoz létezik egy %s típusú BCC térkép.",
"bcc_must_be_email": "A BCC cél %s nem érvényes e-mail cím",
"comment_too_long": "Túl hosszú megjegyzés, max 160 karakter megengedett",
"cors_invalid_method": "Érvénytelen Allow-Method lett megadva",
"cors_invalid_origin": "Érvénytelen Allow-Origin lett megadva",
"defquota_empty": "A postafiókonkénti alapértelmezett kvóta nem lehet 0.",
"demo_mode_enabled": "Demo mód engedélyezve",
"description_invalid": "A %s erőforrás leírása érvénytelen",
"dkim_domain_or_sel_exists": "A \"%s\" DKIM-kulcs létezik, és nem lesz felülírva",
"dkim_domain_or_sel_invalid": "DKIM tartomány vagy szelektor érvénytelen: %s",
"domain_cannot_match_hostname": "A tartomány nem egyezik a hostnévvel",
"domain_exists": "A %s domain már létezik",
"domain_invalid": "A domain név üres vagy érvénytelen",
"domain_not_empty": "Nem lehet eltávolítani a nem üres domaint %s",
"domain_not_found": "Nem található domain %s",
"domain_quota_m_in_use": "A domain kvótának nagyobbnak vagy egyenlőnek kell lennie %s MiB-nál",
"extended_sender_acl_denied": "hiányzó ACL külső küldő cím beállításához",
"extra_acl_invalid": "A \"%s\" külső feladó címe érvénytelen",
"extra_acl_invalid_domain": "Külső feladó \"%s\" érvénytelen tartományt használ",
"fido2_verification_failed": "FIDO2 ellenőrzés sikertelen: %s",
"file_open_error": "A fájl nem nyitható meg írásra",
"filter_type": "Rossz szűrőtípus",
"from_invalid": "A feladó nem lehet üres",
"generic_server_error": "Váratlan szerver hiba keletkezett. Vedd fel a kapcsolatot az adminisztrátorral.",
"global_filter_write_error": "Nem tudott szűrőfájlt írni: %s",
"global_map_invalid": "Globális térkép azonosítója %s érvénytelen",
"global_map_write_error": "Nem tudott globális térképet írni ID %s: %s",
"goto_empty": "Egy alias címnek legalább egy érvényes goto címet kell tartalmaznia.",
"goto_invalid": "Goto cím %s érvénytelen",
"ham_learn_error": "Ham tanulási hiba: %s",
"iam_test_connection": "Kapcsolódás sikertelen",
"imagick_exception": "Hiba: Kép olvasása közben Imagick hiba keletkezett",
"img_dimensions_exceeded": "A kép meghaladja a maximális méretet",
"img_invalid": "A képfájlt nem lehet érvényesíteni",
"img_size_exceeded": "A kép meghaladja a maximális fájl méretet",
"img_tmp_missing": "A képfájlt nem lehet érvényesíteni: Ideiglenes fájl nem található",
"invalid_bcc_map_type": "Érvénytelen a BCC térkép típusa",
"invalid_destination": "A \"%s\" célállomás formátum érvénytelen",
"invalid_filter_type": "Érvénytelen szűrőtípus",
"invalid_host": "Érvénytelen host megadva: %s",
"invalid_mime_type": "Érvénytelen mime típus",
"invalid_nexthop": "A következő ugrás formátuma érvénytelen",
"invalid_nexthop_authenticated": "A következő ugrás más hitelesítő adatokkal létezik, kérjük, először frissítse a meglévő hitelesítő adatokat ehhez a következő ugráshoz.",
"invalid_recipient_map_new": "Érvénytelen új címzett megadása: %s",
"invalid_recipient_map_old": "Érvénytelen eredeti címzett van megadva: %s",
"invalid_reset_token": "Érvénytelen visszaállító kulcs",
"ip_list_empty": "Az engedélyezett IP-k listája nem lehet üres",
"is_alias": "%s már ismert álnév címként",
"is_alias_or_mailbox": "%s már ismert alias, egy postafiók vagy egy alias tartományból kiterjesztett alias cím.",
"is_spam_alias": "%s már ismert ideiglenes alias cím (spam alias cím)",
"last_key": "Az utolsó kulcs nem törölhető, kérjük, helyette deaktiválja a TFA-t.",
"login_failed": "A bejelentkezés sikertelen",
"mailbox_defquota_exceeds_mailbox_maxquota": "Az alapértelmezett kvóta meghaladja a maximális kvótakorlátot",
"mailbox_invalid": "A postafiók neve érvénytelen",
"mailbox_quota_exceeded": "A kvóta meghaladja a tartományi korlátot (max. %d MiB)",
"mailbox_quota_exceeds_domain_quota": "A maximális kvóta meghaladja a tartományi kvótakorlátot",
"mailbox_quota_left_exceeded": "Nincs elég hely (maradék hely: %d MiB)",
"mailboxes_in_use": "A maximális postafiókoknak nagyobbnak vagy egyenlőnek kell lenniük %d-vel.",
"malformed_username": "Hibás felhasználónév",
"map_content_empty": "A térkép tartalma nem lehet üres",
"max_age_invalid": "Maximális kor %s érvénytelen",
"max_alias_exceeded": "Max. aliasok túllépése",
"max_mailbox_exceeded": "Max. postafiókok túllépése (%d %d-ből %d)",
"max_quota_in_use": "A postafiók kvótának nagyobbnak vagy egyenlőnek kell lennie %d MiB-nél",
"maxquota_empty": "A postafiókonkénti maximális kvóta nem lehet 0.",
"mode_invalid": "%s mód érvénytelen",
"mx_invalid": "%s MX rekord érvénytelen",
"mysql_error": "MySQL hiba: %s",
"network_host_invalid": "Érvénytelen hálózat vagy állomás: %s",
"next_hop_interferes": "%s zavarja a nexthop %s-t",
"next_hop_interferes_any": "Egy meglévő következő ugrás zavarja a %s-t.",
"nginx_reload_failed": "Az Nginx újratöltése sikertelen: %s",
"no_user_defined": "Nincs felhasználó által meghatározott",
"object_exists": "Az objektum %s már létezik",
"object_is_not_numeric": "Az érték %s nem numerikus",
"password_complexity": "A jelszó nem felel meg a szabályzatnak",
"password_empty": "A jelszó nem lehet üres",
"password_mismatch": "A megerősítő jelszó nem egyezik",
"password_reset_invalid_user": "A fiók nem található vagy nem lett megadva visszaállításhoz email cím",
"password_reset_na": "A jelszó visszaállítás jelenleg nem elérhető. Vedd fel a kapcsolatot az adminisztrátorral.",
"policy_list_from_exists": "A megadott nevű rekord létezik",
"policy_list_from_invalid": "A rekord érvénytelen formátumú",
"private_key_error": "Privát kulcs hiba: %s",
"pushover_credentials_missing": "Pushover token és/vagy kulcs hiányzik",
"pushover_key": "A pushover kulcs rossz formátumú",
"pushover_token": "A Pushover token rossz formátumú",
"quota_not_0_not_numeric": "A kvótának numerikusnak és >= 0-nak kell lennie.",
"recipient_map_entry_exists": "Létezik egy \"%s\" címzett-térkép bejegyzés"
"syncjob": "Szinkronizálási feladat hozzáadása"
}
}

View File

@@ -21,7 +21,7 @@
"ratelimit": "Limite di invio",
"recipient_maps": "Mappe dei destinatari",
"smtp_ip_access": "Modifica gli host consentiti per SMTP",
"sogo_access": "Consenti la gestione dell'accesso SOGo",
"sogo_redirection": "Consenti la gestione dell'accesso SOGo",
"sogo_profile_reset": "Ripristina profilo SOGo",
"spam_alias": "Alias temporanei",
"spam_policy": "Blacklist/Whitelist",
@@ -671,8 +671,8 @@
"validate_save": "Convalida e salva",
"pushover": "Pushover",
"none_inherit": "Nessuno / Eredita",
"sogo_access": "Inoltro diretto a SOGo",
"sogo_access_info": "Dopo aver effettuato il login, l'utente viene automaticamente reindirizzato a SOGo.",
"sogo_redirection": "Inoltro diretto a SOGo",
"sogo_redirection_info": "Dopo aver effettuato il login, l'utente viene automaticamente reindirizzato a SOGo.",
"acl": "ACL (autorizzazione)",
"app_passwd_protocols": "Protocolli consentiti per la password dell'app",
"last_modified": "Ultima modifica",

View File

@@ -22,7 +22,7 @@
"ratelimit": "レート制限",
"recipient_maps": "受信者マップ",
"smtp_ip_access": "SMTPで許可されるホストの変更",
"sogo_access": "SOGoアクセス管理を許可",
"sogo_redirection": "SOGoアクセス管理を許可",
"sogo_profile_reset": "SOGoプロファイルをリセット",
"spam_alias": "一時的なエイリアス",
"spam_policy": "ブラックリスト/ホワイトリスト",
@@ -691,8 +691,8 @@
"sieve_desc": "簡単な説明",
"sieve_type": "フィルタータイプ",
"skipcrossduplicates": "フォルダー間で重複するメッセージをスキップ(先着順)",
"sogo_access": "SOGoへの直接ログインアクセスを許可",
"sogo_access_info": "メールUI内からのシングルサインオンは引き続き動作します。この設定は他のすべてのサービスへのアクセスには影響しません。また、ユーザーの既存のSOGoプロファイルを削除または変更するものでもありません。",
"sogo_redirection": "SOGoへの直接ログインアクセスを許可",
"sogo_redirection_info": "メールUI内からのシングルサインオンは引き続き動作します。この設定は他のすべてのサービスへのアクセスには影響しません。また、ユーザーの既存のSOGoプロファイルを削除または変更するものでもありません。",
"sogo_visible": "SOGoにエイリアスが表示される",
"sogo_visible_info": "このオプションは、SOGoで表示可能なオブジェクトローカルメールボックスを指す共有または非共有のエイリアスアドレスにのみ影響します。非表示にすると、SOGoで選択可能な送信者としてエイリアスは表示されません。",
"spam_alias": "時間制限付きエイリアスアドレスを作成または変更",
@@ -1187,7 +1187,6 @@
"created_on": "作成日",
"daily": "毎日",
"day": "日",
"description": "説明",
"delete_ays": "削除プロセスを確認してください。",
"direct_aliases": "直接エイリアスアドレス",
"direct_aliases_desc": "直接エイリアスアドレスは、スパムフィルターおよびTLSポリシー設定の影響を受けます。",
@@ -1202,9 +1201,7 @@
"encryption": "暗号化",
"excludes": "除外",
"expire_in": "有効期限まで",
"expire_never": "有効期限なし",
"fido2_webauthn": "FIDO2/WebAuthn",
"forever": "有効期限なし",
"force_pw_update": "グループウェア関連サービスにアクセスするには、新しいパスワードを<b>必ず</b>設定する必要があります。",
"from": "送信元",
"generate": "生成",

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