mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-02-17 14:53:56 +00:00
Compare commits
145 Commits
dragoangel
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0be3347f8 | ||
|
|
caaa4a414d | ||
|
|
4f08c4ed7d | ||
|
|
c3d841340c | ||
|
|
b8cd00111f | ||
|
|
81cda80651 | ||
|
|
c1d4f04c22 | ||
|
|
82276cd1ca | ||
|
|
56ea4302ed | ||
|
|
c06112b26e | ||
|
|
aa5a4f0998 | ||
|
|
bf4f471cfd | ||
|
|
978bff9dbc | ||
|
|
869d9af7dd | ||
|
|
af10499ecb | ||
|
|
a1a4d8ff98 | ||
|
|
95d61e8aa2 | ||
|
|
ec8dd1a54f | ||
|
|
382ee34d0e | ||
|
|
0999c9e9ab | ||
|
|
0e76396f01 | ||
|
|
9bbac9f171 | ||
|
|
c485968e7f | ||
|
|
e727620bd3 | ||
|
|
71fa3ecebc | ||
|
|
70101d1187 | ||
|
|
c060c205d3 | ||
|
|
038b2efb75 | ||
|
|
1fe4cd03e9 | ||
|
|
12e02e67ff | ||
|
|
b6f57dfb78 | ||
|
|
3ebf2c2d2d | ||
|
|
1bac6f1ee7 | ||
|
|
67e7acd6bd | ||
|
|
910ce573d6 | ||
|
|
689336b3e1 | ||
|
|
01cf72cdef | ||
|
|
4cdb97c699 | ||
|
|
1bd795a9c6 | ||
|
|
39f29e6c30 | ||
|
|
1ab6af21e3 | ||
|
|
5d95c48e0d | ||
|
|
dbb9e474b0 | ||
|
|
f8eed8c786 | ||
|
|
ef010aa39c | ||
|
|
79171ea6f5 | ||
|
|
4e3294b273 | ||
|
|
32a6ecddb6 | ||
|
|
f3d9833ecf | ||
|
|
930ca76ea7 | ||
|
|
9a2887cf46 | ||
|
|
9950914086 | ||
|
|
470cfb0026 | ||
|
|
6c106b4e4d | ||
|
|
3d6253a2b2 | ||
|
|
b873812588 | ||
|
|
514fefd2ed | ||
|
|
6f9ee2d151 | ||
|
|
9832006141 | ||
|
|
45666d2c4e | ||
|
|
95e0608749 | ||
|
|
e6f83853ae | ||
|
|
586b3a2ed1 | ||
|
|
6bfa58611e | ||
|
|
e31b6d9a07 | ||
|
|
24c62b2f09 | ||
|
|
7da088c931 | ||
|
|
bb3c2fb4fe | ||
|
|
ef0f366d1c | ||
|
|
eb84847a5b | ||
|
|
4db1569c93 | ||
|
|
7ce3b0faed | ||
|
|
b1c088a57f | ||
|
|
0cfcde673c | ||
|
|
ed5be5d7dc | ||
|
|
ac90ecaf4f | ||
|
|
fed3fc9514 | ||
|
|
35b9940db4 | ||
|
|
ece940b000 | ||
|
|
4b5fd0b50a | ||
|
|
5aa9498f65 | ||
|
|
690d511e54 | ||
|
|
e2a2b42139 | ||
|
|
4bbda8006d | ||
|
|
a281746958 | ||
|
|
cec51b6162 | ||
|
|
107c5d2e7d | ||
|
|
00c025f31a | ||
|
|
9b6388d0d0 | ||
|
|
2f25fcad77 | ||
|
|
7067e2c714 | ||
|
|
9f3cdfa713 | ||
|
|
529acf5ff6 | ||
|
|
0371edcf5e | ||
|
|
d20254d4ee | ||
|
|
befecfc31d | ||
|
|
004fcf092b | ||
|
|
a487fcd0bd | ||
|
|
17e38a05f0 | ||
|
|
c503abfe40 | ||
|
|
73929db796 | ||
|
|
fb0685fa71 | ||
|
|
df36670c7c | ||
|
|
3f9215678d | ||
|
|
0ac0e5c252 | ||
|
|
af61c82077 | ||
|
|
c066273c79 | ||
|
|
527f27d249 | ||
|
|
0c3e53e3a9 | ||
|
|
5ca10d1cde | ||
|
|
7907d43af7 | ||
|
|
d198f1d3f8 | ||
|
|
102226723e | ||
|
|
2efaccf038 | ||
|
|
aa7b6fa4a9 | ||
|
|
714727a129 | ||
|
|
4e5e264e3e | ||
|
|
267c81b42e | ||
|
|
f2f3fbe497 | ||
|
|
6ba650820f | ||
|
|
baa6286471 | ||
|
|
be8537d165 | ||
|
|
737fced7be | ||
|
|
5a532df8ce | ||
|
|
f8ce7a71e6 | ||
|
|
2e876bda9a | ||
|
|
d2e5926cce | ||
|
|
1994b9895b | ||
|
|
798e6a4c00 | ||
|
|
e3b576be67 | ||
|
|
3f493e043d | ||
|
|
0f7e359686 | ||
|
|
b9a0b2db6d | ||
|
|
93b876c473 | ||
|
|
2c47145dee | ||
|
|
92c2aa2023 | ||
|
|
9351cf24fe | ||
|
|
c3c68360dc | ||
|
|
a632980871 | ||
|
|
2d1ef41d32 | ||
|
|
120366fec7 | ||
|
|
244d4b8c4c | ||
|
|
f92ddd86c5 | ||
|
|
ba0349a911 | ||
|
|
8caf09cd80 |
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||
uses: actions/stale@v10.1.0
|
||||
uses: actions/stale@v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||
days-before-stale: 60
|
||||
|
||||
4
.github/workflows/image_builds.yml
vendored
4
.github/workflows/image_builds.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
images:
|
||||
- "acme-mailcow"
|
||||
- "clamd-mailcow"
|
||||
- "dockerapi-mailcow"
|
||||
- "controller-mailcow"
|
||||
- "dovecot-mailcow"
|
||||
- "netfilter-mailcow"
|
||||
- "olefy-mailcow"
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- "watchdog-mailcow"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Docker
|
||||
run: |
|
||||
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
||||
|
||||
2
.github/workflows/pr_to_nightly.yml
vendored
2
.github/workflows/pr_to_nightly.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run the Action
|
||||
|
||||
2
.github/workflows/rebuild_backup_image.yml
vendored
2
.github/workflows/rebuild_backup_image.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -15,14 +15,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Generate postscreen_access.cidr
|
||||
run: |
|
||||
bash helper-scripts/update_postscreen_whitelist.sh
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||
commit-message: update postscreen_access.cidr
|
||||
|
||||
@@ -5,7 +5,7 @@ First of all, thank you for wanting to provide a bugfix or a new feature for the
|
||||
|
||||
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 FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
||||
**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.
|
||||
|
||||
## Topics
|
||||
|
||||
|
||||
@@ -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." -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
|
||||
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.\e[0m"
|
||||
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
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 -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
|
||||
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() {
|
||||
|
||||
@@ -48,11 +48,11 @@ if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
exec $(readlink -f "$0")
|
||||
fi
|
||||
|
||||
log_f "Waiting for Docker API..."
|
||||
until ping dockerapi -c1 > /dev/null; do
|
||||
log_f "Waiting for Controller .."
|
||||
until ping controller -c1 > /dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
log_f "Docker API OK"
|
||||
log_f "Controller OK"
|
||||
|
||||
log_f "Waiting for Postfix..."
|
||||
until ping postfix -c1 > /dev/null; do
|
||||
@@ -246,6 +246,25 @@ 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
|
||||
|
||||
@@ -2,32 +2,32 @@
|
||||
|
||||
# Reading container IDs
|
||||
# Wrapping as array to ensure trimmed content when calling $NGINX etc.
|
||||
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" " "))
|
||||
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" " "))
|
||||
|
||||
reload_nginx(){
|
||||
echo "Reloading Nginx..."
|
||||
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=$(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} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; }
|
||||
}
|
||||
|
||||
reload_dovecot(){
|
||||
echo "Reloading Dovecot..."
|
||||
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=$(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} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; }
|
||||
}
|
||||
|
||||
reload_postfix(){
|
||||
echo "Reloading Postfix..."
|
||||
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=$(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} != '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://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg')
|
||||
C_REST_OUT=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg')
|
||||
echo "${C_REST_OUT}"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -6,22 +6,29 @@ 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
|
||||
RUN mkdir /app/modules
|
||||
|
||||
COPY mailcow-adm/ /app/mailcow-adm/
|
||||
RUN pip3 install -r /app/mailcow-adm/requirements.txt
|
||||
|
||||
COPY api/ /app/api/
|
||||
|
||||
COPY docker-entrypoint.sh /app/
|
||||
COPY main.py /app/main.py
|
||||
COPY modules/ /app/modules/
|
||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
||||
CMD ["python", "main.py"]
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
@@ -254,8 +254,8 @@ if __name__ == '__main__':
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=443,
|
||||
ssl_certfile="/app/dockerapi_cert.pem",
|
||||
ssl_keyfile="/app/dockerapi_key.pem",
|
||||
ssl_certfile="/app/controller_cert.pem",
|
||||
ssl_keyfile="/app/controller_key.pem",
|
||||
log_level="info",
|
||||
loop="none"
|
||||
)
|
||||
9
data/Dockerfiles/controller/docker-entrypoint.sh
Executable file
9
data/Dockerfiles/controller/docker-entrypoint.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/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 "$@"
|
||||
61
data/Dockerfiles/controller/mailcow-adm/mailcow-adm.py
Executable file
61
data/Dockerfiles/controller/mailcow-adm/mailcow-adm.py
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,140 @@
|
||||
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)")
|
||||
|
||||
107
data/Dockerfiles/controller/mailcow-adm/models/AliasModel.py
Normal file
107
data/Dockerfiles/controller/mailcow-adm/models/AliasModel.py
Normal file
@@ -0,0 +1,107 @@
|
||||
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)")
|
||||
|
||||
35
data/Dockerfiles/controller/mailcow-adm/models/BaseModel.py
Normal file
35
data/Dockerfiles/controller/mailcow-adm/models/BaseModel.py
Normal file
@@ -0,0 +1,35 @@
|
||||
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
|
||||
111
data/Dockerfiles/controller/mailcow-adm/models/CalendarModel.py
Normal file
111
data/Dockerfiles/controller/mailcow-adm/models/CalendarModel.py
Normal file
@@ -0,0 +1,111 @@
|
||||
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")
|
||||
162
data/Dockerfiles/controller/mailcow-adm/models/DomainModel.py
Normal file
162
data/Dockerfiles/controller/mailcow-adm/models/DomainModel.py
Normal file
@@ -0,0 +1,162 @@
|
||||
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")
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
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")
|
||||
|
||||
164
data/Dockerfiles/controller/mailcow-adm/models/MailboxModel.py
Normal file
164
data/Dockerfiles/controller/mailcow-adm/models/MailboxModel.py
Normal file
@@ -0,0 +1,164 @@
|
||||
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")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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")
|
||||
@@ -0,0 +1,62 @@
|
||||
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)")
|
||||
@@ -0,0 +1,45 @@
|
||||
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")
|
||||
221
data/Dockerfiles/controller/mailcow-adm/models/SyncjobModel.py
Normal file
221
data/Dockerfiles/controller/mailcow-adm/models/SyncjobModel.py
Normal file
@@ -0,0 +1,221 @@
|
||||
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")
|
||||
|
||||
128
data/Dockerfiles/controller/mailcow-adm/modules/Docker.py
Normal file
128
data/Dockerfiles/controller/mailcow-adm/modules/Docker.py
Normal file
@@ -0,0 +1,128 @@
|
||||
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)
|
||||
}
|
||||
206
data/Dockerfiles/controller/mailcow-adm/modules/Dovecot.py
Normal file
206
data/Dockerfiles/controller/mailcow-adm/modules/Dovecot.py
Normal file
@@ -0,0 +1,206 @@
|
||||
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"
|
||||
457
data/Dockerfiles/controller/mailcow-adm/modules/Mailcow.py
Normal file
457
data/Dockerfiles/controller/mailcow-adm/modules/Mailcow.py
Normal file
@@ -0,0 +1,457 @@
|
||||
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."
|
||||
64
data/Dockerfiles/controller/mailcow-adm/modules/Mailer.py
Normal file
64
data/Dockerfiles/controller/mailcow-adm/modules/Mailer.py
Normal file
@@ -0,0 +1,64 @@
|
||||
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()
|
||||
51
data/Dockerfiles/controller/mailcow-adm/modules/Reader.py
Normal file
51
data/Dockerfiles/controller/mailcow-adm/modules/Reader.py
Normal file
@@ -0,0 +1,51 @@
|
||||
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
|
||||
512
data/Dockerfiles/controller/mailcow-adm/modules/Sogo.py
Normal file
512
data/Dockerfiles/controller/mailcow-adm/modules/Sogo.py
Normal file
@@ -0,0 +1,512 @@
|
||||
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
|
||||
|
||||
37
data/Dockerfiles/controller/mailcow-adm/modules/Utils.py
Normal file
37
data/Dockerfiles/controller/mailcow-adm/modules/Utils.py
Normal file
@@ -0,0 +1,37 @@
|
||||
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)
|
||||
4
data/Dockerfiles/controller/mailcow-adm/requirements.txt
Normal file
4
data/Dockerfiles/controller/mailcow-adm/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
jinja2
|
||||
requests
|
||||
mysql-connector-python
|
||||
pytest
|
||||
@@ -0,0 +1,94 @@
|
||||
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!")
|
||||
@@ -0,0 +1,71 @@
|
||||
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
|
||||
@@ -0,0 +1,74 @@
|
||||
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!")
|
||||
@@ -0,0 +1,89 @@
|
||||
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!")
|
||||
@@ -0,0 +1,89 @@
|
||||
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!")
|
||||
@@ -0,0 +1,39 @@
|
||||
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!")
|
||||
@@ -0,0 +1,106 @@
|
||||
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!")
|
||||
8
data/Dockerfiles/controller/stop-supervisor.sh
Executable file
8
data/Dockerfiles/controller/stop-supervisor.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
printf "READY\n";
|
||||
|
||||
while read line; do
|
||||
echo "Processing Event: $line" >&2;
|
||||
kill -3 $(cat "/var/run/supervisord.pid")
|
||||
done < /dev/stdin
|
||||
17
data/Dockerfiles/controller/supervisord.conf
Normal file
17
data/Dockerfiles/controller/supervisord.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
[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
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/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 "$@"
|
||||
@@ -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.17
|
||||
ARG GOSU_VERSION=1.19
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
@@ -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://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \
|
||||
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(\"${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://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
|
||||
curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -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.27
|
||||
ARG APCU_PECL_VERSION=5.1.28
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG IMAGICK_PECL_VERSION=3.8.0
|
||||
ARG IMAGICK_PECL_VERSION=3.8.1
|
||||
# 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.3.0
|
||||
ARG MEMCACHED_PECL_VERSION=3.4.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG REDIS_PECL_VERSION=6.2.0
|
||||
ARG REDIS_PECL_VERSION=6.3.0
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG COMPOSER_VERSION=2.8.6
|
||||
|
||||
|
||||
@@ -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://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)
|
||||
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)
|
||||
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://dockerapi.${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://controller.${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://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)
|
||||
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)
|
||||
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://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
|
||||
curl -X POST --silent --insecure https://controller.${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://dockerapi.${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://controller.${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
|
||||
|
||||
@@ -4,7 +4,7 @@ WORKDIR /src
|
||||
ENV CGO_ENABLED=0 \
|
||||
GO111MODULE=on \
|
||||
NOOPT=1 \
|
||||
VERSION=1.8.14
|
||||
VERSION=1.8.22
|
||||
|
||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||
cd /src/postfix-tlspol && \
|
||||
|
||||
@@ -18,15 +18,11 @@ done
|
||||
|
||||
# Do not attempt to write to slave
|
||||
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||
export REDIS_SERVER="${REDIS_SLAVEOF_IP}"
|
||||
export REDIS_PORT="${REDIS_SLAVEOF_PORT}"
|
||||
export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
|
||||
else
|
||||
export REDIS_SERVER="redis"
|
||||
export REDIS_PORT="6379"
|
||||
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
|
||||
fi
|
||||
|
||||
export REDIS_CMDLINE="redis-cli -h ${REDIS_SERVER} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning"
|
||||
|
||||
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
||||
echo "Waiting for Redis..."
|
||||
sleep 2
|
||||
@@ -41,13 +37,16 @@ echo "Postfix OK"
|
||||
cat <<EOF > /etc/postfix-tlspol/config.yaml
|
||||
server:
|
||||
address: 0.0.0.0:8642
|
||||
|
||||
log-level: ${LOGLVL}
|
||||
|
||||
prefetch: true
|
||||
|
||||
cache-file: /var/lib/postfix-tlspol/cache.db
|
||||
|
||||
dns:
|
||||
# must support DNSSEC
|
||||
address: 127.0.0.11:53
|
||||
redis:
|
||||
address: ${REDIS_SERVER}:${REDIS_PORT}
|
||||
db: 2
|
||||
EOF
|
||||
|
||||
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml
|
||||
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml
|
||||
@@ -329,14 +329,17 @@ 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'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG RSPAMD_VER=rspamd_3.13.2-1~8bf602278
|
||||
ARG CODENAME=bookworm
|
||||
ARG RSPAMD_VER=rspamd_3.14.2-82~90302bc
|
||||
ARG CODENAME=trixie
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -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.17
|
||||
ARG GOSU_VERSION=1.19
|
||||
ENV LC_ALL=C
|
||||
|
||||
# Prerequisites
|
||||
|
||||
@@ -200,12 +200,12 @@ get_container_ip() {
|
||||
else
|
||||
sleep 0.5
|
||||
# get long container id for exact match
|
||||
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"))
|
||||
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"))
|
||||
# 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://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
|
||||
CONTAINER_IPS=($(curl --silent --insecure https://controller.${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 dockerapi
|
||||
# Monitor controller
|
||||
(
|
||||
while true; do
|
||||
while nc -z dockerapi 443; do
|
||||
while nc -z controller 443; do
|
||||
sleep 3
|
||||
done
|
||||
log_msg "Cannot find dockerapi-mailcow, waiting to recover..."
|
||||
log_msg "Cannot find controller-mailcow, waiting to recover..."
|
||||
kill -STOP ${BACKGROUND_TASKS[*]}
|
||||
until nc -z dockerapi 443; do
|
||||
until nc -z controller 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://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")
|
||||
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")
|
||||
if [[ ! -z ${CONTAINER_ID} ]]; then
|
||||
if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
|
||||
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)
|
||||
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)
|
||||
fi
|
||||
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)))
|
||||
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)))
|
||||
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://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
|
||||
curl --silent --insecure -XPOST https://controller.${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
|
||||
|
||||
@@ -80,14 +80,21 @@ if ($isSOGoRequest) {
|
||||
}
|
||||
if ($result === false){
|
||||
// If it's a SOGo Request, don't check for protocol access
|
||||
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
|
||||
$result = apppass_login($post['username'], $post['password'], $service, array(
|
||||
if ($isSOGoRequest) {
|
||||
$service = 'SOGO';
|
||||
$post['service'] = 'NONE';
|
||||
} else {
|
||||
$service = $post['service'];
|
||||
}
|
||||
|
||||
$result = apppass_login($post['username'], $post['password'], array(
|
||||
'service' => $post['service'],
|
||||
'is_internal' => true,
|
||||
'remote_addr' => $post['real_rip']
|
||||
));
|
||||
if ($result) {
|
||||
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']);
|
||||
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);
|
||||
}
|
||||
}
|
||||
if ($result === false){
|
||||
|
||||
@@ -185,6 +185,7 @@ 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";
|
||||
@@ -210,6 +211,7 @@ 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";
|
||||
@@ -232,6 +234,7 @@ 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";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Whitelist generated by Postwhite v3.4 on Sat Nov 1 00:21:43 UTC 2025
|
||||
# Whitelist generated by Postwhite v3.4 on Thu Jan 1 00:24:01 UTC 2026
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 2161 total rules
|
||||
# 2105 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a01:111:f400::/48 permit
|
||||
2a01:111:f403:2800::/53 permit
|
||||
@@ -29,7 +29,9 @@
|
||||
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,8 +54,8 @@
|
||||
8.36.116.0/24 permit
|
||||
8.39.144.0/24 permit
|
||||
12.130.86.238 permit
|
||||
13.107.213.69 permit
|
||||
13.107.246.69 permit
|
||||
13.107.213.38 permit
|
||||
13.107.246.38 permit
|
||||
13.108.16.0/20 permit
|
||||
13.110.208.0/21 permit
|
||||
13.110.209.0/24 permit
|
||||
@@ -293,14 +295,6 @@
|
||||
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.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
|
||||
@@ -394,27 +388,8 @@
|
||||
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
|
||||
@@ -534,7 +509,6 @@
|
||||
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
|
||||
@@ -631,7 +605,6 @@
|
||||
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
|
||||
@@ -727,8 +700,6 @@
|
||||
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
|
||||
@@ -1221,7 +1192,6 @@
|
||||
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.151.192.0/23 permit
|
||||
@@ -1367,11 +1337,6 @@
|
||||
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
|
||||
@@ -1433,6 +1398,7 @@
|
||||
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
|
||||
@@ -1471,6 +1437,8 @@
|
||||
139.138.58.119 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
|
||||
@@ -1516,6 +1484,7 @@
|
||||
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
|
||||
@@ -1525,20 +1494,7 @@
|
||||
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
|
||||
@@ -1588,6 +1544,9 @@
|
||||
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
|
||||
@@ -1620,14 +1579,12 @@
|
||||
168.245.12.252 permit
|
||||
168.245.46.9 permit
|
||||
168.245.127.231 permit
|
||||
170.9.232.254 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/21 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
|
||||
@@ -1771,6 +1728,7 @@
|
||||
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
|
||||
@@ -1784,6 +1742,7 @@
|
||||
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
|
||||
@@ -1858,7 +1817,6 @@
|
||||
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
|
||||
@@ -1884,24 +1842,13 @@
|
||||
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
|
||||
@@ -2056,8 +2003,6 @@
|
||||
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
|
||||
@@ -2085,7 +2030,6 @@
|
||||
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
|
||||
|
||||
@@ -146,8 +146,171 @@ 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)
|
||||
ip_check_table = {from_ip_string}
|
||||
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
|
||||
|
||||
local maxbits = 128
|
||||
local minbits = 32
|
||||
@@ -155,10 +318,18 @@ rspamd_config:register_symbol({
|
||||
maxbits = 32
|
||||
minbits = 8
|
||||
end
|
||||
|
||||
-- Add all CIDR notations with variants
|
||||
for i=maxbits,minbits,-1 do
|
||||
local nip = ip:apply_mask(i):to_string() .. "/" .. i
|
||||
table.insert(ip_check_table, nip)
|
||||
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
|
||||
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)
|
||||
@@ -166,12 +337,15 @@ 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 in keep_spam map, setting pre-result")
|
||||
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])
|
||||
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
|
||||
@@ -210,6 +384,7 @@ 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"
|
||||
@@ -218,9 +393,6 @@ 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
|
||||
@@ -231,101 +403,149 @@ rspamd_config:register_symbol({
|
||||
return true
|
||||
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)
|
||||
-- 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 action ~= 'no action' and action ~= 'greylist' then
|
||||
rspamd_logger.infox("skipping tag handler for action: %s", action)
|
||||
remove_moo_tag()
|
||||
return true
|
||||
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)
|
||||
end
|
||||
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)
|
||||
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 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)
|
||||
-- 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_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 action = task:get_metric_action('default')
|
||||
rspamd_logger.infox("TAG_MOO: metric action: %s", action)
|
||||
|
||||
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")
|
||||
-- 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
|
||||
|
||||
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
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
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")
|
||||
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")
|
||||
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,
|
||||
@@ -335,6 +555,7 @@ 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"
|
||||
@@ -363,11 +584,13 @@ 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 = task:get_from(stp)[1].addr,
|
||||
from = from_addr,
|
||||
recipients = bcc_dest,
|
||||
helo = 'bcc',
|
||||
timeout = 20,
|
||||
@@ -397,27 +620,41 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
|
||||
local action = task:get_metric_action('default')
|
||||
rspamd_logger.infox("metric action now: %s", action)
|
||||
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
|
||||
|
||||
local function rcpt_callback(err_message, code, body, headers)
|
||||
if err_message == nil and code == 201 and body ~= nil then
|
||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
||||
send_mail(task, body)
|
||||
end
|
||||
rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
|
||||
send_mail(task, body)
|
||||
end
|
||||
end
|
||||
|
||||
local function from_callback(err_message, code, body, headers)
|
||||
if err_message == nil and code == 201 and body ~= nil then
|
||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
||||
send_mail(task, body)
|
||||
end
|
||||
rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
|
||||
send_mail(task, body)
|
||||
end
|
||||
end
|
||||
|
||||
if rcpt_table then
|
||||
for _,e in ipairs(rcpt_table) do
|
||||
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
|
||||
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/bcc.php',
|
||||
@@ -430,7 +667,7 @@ rspamd_config:register_symbol({
|
||||
|
||||
if from_table then
|
||||
for _,e in ipairs(from_table) do
|
||||
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
|
||||
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/bcc.php',
|
||||
@@ -441,7 +678,7 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
-- Don't return true to avoid symbol being logged
|
||||
end,
|
||||
priority = 20
|
||||
})
|
||||
@@ -708,4 +945,4 @@ rspamd_config:register_symbol({
|
||||
return true
|
||||
end,
|
||||
priority = 1
|
||||
})
|
||||
})
|
||||
@@ -5352,9 +5352,9 @@ paths:
|
||||
started_at: "2019-12-22T21:00:01.622856172Z"
|
||||
state: running
|
||||
type: info
|
||||
dockerapi-mailcow:
|
||||
container: dockerapi-mailcow
|
||||
image: "mailcow/dockerapi:1.36"
|
||||
controller-mailcow:
|
||||
container: controller-mailcow
|
||||
image: "mailcow/controller:1.36"
|
||||
started_at: "2019-12-22T20:59:59.984797808Z"
|
||||
state: running
|
||||
type: info
|
||||
|
||||
@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="<?=$mailcow_hostname; ?>">
|
||||
<domain>%EMAILDOMAIN%</domain>
|
||||
<displayName>A mailcow mail server</displayName>
|
||||
<displayShortName>mail server</displayShortName>
|
||||
<displayName><?=$autodiscover_config['displayName']; ?></displayName>
|
||||
<displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
|
||||
|
||||
<incomingServer type="imap">
|
||||
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
||||
|
||||
@@ -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('eas' => TRUE));
|
||||
$login_role = check_login($login_user, $login_pass, array('service' => 'EAS'));
|
||||
|
||||
if ($login_role === "user") {
|
||||
header("Content-Type: application/xml");
|
||||
|
||||
@@ -129,7 +129,16 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
);
|
||||
}
|
||||
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
// 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);
|
||||
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
|
||||
if (!in_array($domain, $alias_domains)) {
|
||||
$records[] = array(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
|
||||
function check_login($user, $pass, $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") {
|
||||
@@ -25,34 +26,20 @@ function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
|
||||
|
||||
// Try validate app password
|
||||
if (!isset($role) || $role == "app") {
|
||||
$result = apppass_login($user, $pass, $app_passwd_data);
|
||||
$result = apppass_login($user, $pass, $extra);
|
||||
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, $service, $pass);
|
||||
set_sasl_log($user, $real_rip, $extra['service'], $pass);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try validate user
|
||||
if (!isset($role) || $role == "user") {
|
||||
$result = user_login($user, $pass);
|
||||
$result = user_login($user, $pass, $extra);
|
||||
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, $service);
|
||||
set_sasl_log($user, $real_rip, $extra['service']);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -193,7 +180,7 @@ function user_login($user, $pass, $extra = null){
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$service = $extra['service'];
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
@@ -236,10 +223,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) if service is set
|
||||
// check if user has access to service (imap, smtp, pop3, sieve, dav, eas) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if ($extra['service'] != 'NONE') {
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
@@ -253,8 +240,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 (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if ($extra['service'] != 'NONE') {
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
@@ -408,7 +395,7 @@ function user_login($user, $pass, $extra = null){
|
||||
|
||||
return false;
|
||||
}
|
||||
function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
function apppass_login($user, $pass, $extra = null){
|
||||
global $pdo;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
@@ -424,20 +411,8 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
return false;
|
||||
}
|
||||
|
||||
$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) {
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
if (!$is_internal && $extra['service'] == 'NONE') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -458,7 +433,7 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ($protocol && $row[$protocol . '_access'] != '1'){
|
||||
if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 dockerapi, the names will not match, the certs are trusted anyway
|
||||
// We are using our mail certificates for controller, 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://dockerapi:443/containers/json');
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://controller: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://dockerapi:443/containers/json');
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://controller: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://dockerapi:443/containers/json');
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://controller: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://dockerapi:443/containers/' . $container_id . '/json');
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://controller: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://dockerapi:443/containers/' . $container_id . '/' . $attr1);
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://controller: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://dockerapi:443/container/' . $container_id . '/stats/update');
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://controller: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://dockerapi:443/host/stats');
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://controller:443/host/stats');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
|
||||
@@ -205,6 +205,42 @@ 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');
|
||||
|
||||
@@ -488,6 +524,16 @@ 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
|
||||
@@ -814,6 +860,32 @@ 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);
|
||||
|
||||
|
||||
@@ -475,10 +475,11 @@ 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)
|
||||
'msg' => array('mailbox_modified', $username, $id)
|
||||
);
|
||||
break;
|
||||
case 'domain':
|
||||
@@ -695,6 +696,7 @@ 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']);
|
||||
@@ -850,8 +852,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`, `active`)
|
||||
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
|
||||
$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)");
|
||||
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
|
||||
$stmt->execute(array(
|
||||
':address' => '@'.$domain,
|
||||
@@ -862,6 +864,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => $domain,
|
||||
':sogo_visible' => $sogo_visible,
|
||||
':internal' => $internal,
|
||||
':sender_allowed' => $sender_allowed,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
@@ -874,6 +877,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => $domain,
|
||||
':sogo_visible' => $sogo_visible,
|
||||
':internal' => $internal,
|
||||
':sender_allowed' => $sender_allowed,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
@@ -1075,6 +1079,8 @@ 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']);
|
||||
@@ -1085,6 +1091,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$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']);
|
||||
@@ -1103,6 +1111,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'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']),
|
||||
@@ -1721,12 +1731,16 @@ 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'];
|
||||
@@ -2501,6 +2515,7 @@ 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;
|
||||
@@ -2686,6 +2701,7 @@ 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(
|
||||
@@ -2696,6 +2712,7 @@ 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']
|
||||
));
|
||||
@@ -3043,6 +3060,8 @@ 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'];
|
||||
@@ -3052,6 +3071,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
(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'];
|
||||
@@ -3185,9 +3206,10 @@ 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(
|
||||
mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'],
|
||||
mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro']
|
||||
$current_sender_acls['sender_acl_domains']['ro'],
|
||||
$current_sender_acls['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
|
||||
@@ -3275,16 +3297,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases'];
|
||||
$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'];
|
||||
|
||||
foreach ($sender_acl_merged as $sender_acl) {
|
||||
$domain = ltrim($sender_acl, '@');
|
||||
if (is_valid_domain_name($domain)) {
|
||||
$sender_acl = '@' . $domain;
|
||||
}
|
||||
// Don't add if allowed by alias
|
||||
if (in_array($sender_acl, $fixed_sender_aliases)) {
|
||||
|
||||
// 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
|
||||
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(
|
||||
@@ -3335,6 +3366,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`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");
|
||||
@@ -3349,6 +3382,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':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,
|
||||
@@ -3731,6 +3766,8 @@ 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){
|
||||
@@ -4160,13 +4197,22 @@ 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
|
||||
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
|
||||
// Fixed addresses - split by sender_allowed status
|
||||
$stmt = $pdo->prepare("SELECT `address`, `sender_allowed` 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`
|
||||
@@ -4726,6 +4772,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`internal`,
|
||||
`active`,
|
||||
`sogo_visible`,
|
||||
`sender_allowed`,
|
||||
`created`,
|
||||
`modified`
|
||||
FROM `alias`
|
||||
@@ -4759,6 +4806,7 @@ 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'])) {
|
||||
|
||||
@@ -4,7 +4,7 @@ function init_db_schema()
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "10312025_0525";
|
||||
$db_version = "28012026_1000";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -185,6 +185,7 @@ 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(
|
||||
@@ -1394,6 +1395,8 @@ function init_db_schema()
|
||||
$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;");
|
||||
|
||||
@@ -105,11 +105,11 @@ http_response_code(500);
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
// Stop when dockerapi is not available
|
||||
if (fsockopen("tcp://dockerapi", 443, $errno, $errstr) === false) {
|
||||
// Stop when controller is not available
|
||||
if (fsockopen("tcp://controller", 443, $errno, $errstr) === false) {
|
||||
http_response_code(500);
|
||||
?>
|
||||
<center style='font-family:sans-serif;'>Connection to dockerapi container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
|
||||
<center style='font-family:sans-serif;'>Connection to controller 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) == 'user') {
|
||||
if (check_login($username, $password, array("role" => "user", "service" => "NONE")) == 'user') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -165,14 +165,6 @@ 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';
|
||||
|
||||
@@ -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 " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for API_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 " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
|
||||
http_response_code(401);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
@@ -92,6 +92,16 @@ 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"])) {
|
||||
|
||||
@@ -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"], false, array("role" => "admin"));
|
||||
$as = check_login($login_user, $_POST["pass_user"], array("role" => "admin", "service" => "MAILCOWUI"));
|
||||
|
||||
if ($as == "admin") {
|
||||
session_regenerate_id(true);
|
||||
|
||||
@@ -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"], false, array("role" => "domain_admin"));
|
||||
$as = check_login($login_user, $_POST["pass_user"], array("role" => "domain_admin", "service" => "MAILCOWUI"));
|
||||
|
||||
if ($as == "domainadmin") {
|
||||
session_regenerate_id(true);
|
||||
|
||||
@@ -119,7 +119,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"], false, array("role" => "user"));
|
||||
$as = check_login($login_user, $_POST["pass_user"], array("role" => "user", "service" => "MAILCOWUI"));
|
||||
|
||||
if ($as == "user") {
|
||||
set_user_loggedin_session($login_user);
|
||||
|
||||
@@ -33,6 +33,8 @@ 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.
|
||||
@@ -215,6 +217,12 @@ $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
|
||||
|
||||
@@ -27,6 +27,12 @@ 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'];
|
||||
|
||||
@@ -54,7 +54,16 @@ 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) {
|
||||
|
||||
@@ -352,6 +352,12 @@ $(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 = [];
|
||||
@@ -933,6 +939,8 @@ 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') {
|
||||
@@ -1096,6 +1104,18 @@ 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',
|
||||
@@ -1209,6 +1229,8 @@ 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>';
|
||||
if (item.attributes.quarantine_notification === 'never') {
|
||||
item.attributes.quarantine_notification = lang.never;
|
||||
@@ -1317,6 +1339,16 @@ jQuery(function($){
|
||||
data: 'attributes.sieve_access',
|
||||
defaultContent: '',
|
||||
},
|
||||
{
|
||||
title: 'EAS',
|
||||
data: 'attributes.eas_access',
|
||||
defaultContent: '',
|
||||
},
|
||||
{
|
||||
title: 'DAV',
|
||||
data: 'attributes.dav_access',
|
||||
defaultContent: '',
|
||||
},
|
||||
{
|
||||
title: 'SOGO',
|
||||
data: 'attributes.sogo_access',
|
||||
|
||||
@@ -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' => 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],
|
||||
['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],
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
|
||||
@@ -1992,6 +1992,7 @@ 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;
|
||||
|
||||
@@ -19,7 +19,16 @@
|
||||
"spam_alias": "Àlies temporals",
|
||||
"spam_score": "Puntuació de correu brossa",
|
||||
"tls_policy": "Política TLS",
|
||||
"unlimited_quota": "Quota ilimitada per bústies de correo"
|
||||
"unlimited_quota": "Quota ilimitada per bústies de correo",
|
||||
"delimiter_action": "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"
|
||||
},
|
||||
"add": {
|
||||
"activate_filter_warn": "All other filters will be deactivated, when active is checked.",
|
||||
@@ -73,7 +82,25 @@
|
||||
"validate": "Validar",
|
||||
"validation_success": "Validated successfully",
|
||||
"app_name": "Nom de l'aplicació",
|
||||
"app_password": "Afegir contrasenya a 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"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Accés",
|
||||
|
||||
@@ -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": "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.",
|
||||
"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ů.",
|
||||
"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ě/hostitelé na blacklistu",
|
||||
"f2b_blacklist": "Sítě či hostitelé na seznamu zákazů",
|
||||
"f2b_filter": "Regex filtre",
|
||||
"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_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_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ě/hostitelé na whitelistu",
|
||||
"f2b_whitelist": "Sítě či hostitelé na seznamu povolení",
|
||||
"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 jiné globální black- a whitelisty.",
|
||||
"rspamd_global_filters_info": "Mapa globálních filtrů obsahuje různé seznamy povolených a zakázaných serverů",
|
||||
"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'",
|
||||
@@ -733,7 +733,7 @@
|
||||
"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 whitelistu/blacklistu",
|
||||
"spam_policy": "Přidat nebo odebrat položky seznamu",
|
||||
"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": "<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_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_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é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.",
|
||||
"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.",
|
||||
"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": "Dočasné e-mailové aliasy",
|
||||
"spam_aliases": "Spam aliasy",
|
||||
"spam_score_reset": "Obnovit výchozí nastavení serveru",
|
||||
"spamfilter": "Filtr spamu",
|
||||
"spamfilter_behavior": "Hodnocení",
|
||||
"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_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_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ých adres (whitelist)",
|
||||
"spamfilter_wl": "Seznam povolení",
|
||||
"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,7 +1407,10 @@
|
||||
"authentication": "Autentifikace",
|
||||
"overview": "Přehled",
|
||||
"protocols": "Protokoly",
|
||||
"value": "Hodnota"
|
||||
"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."
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Nelze smazat právě přihlášeného uživatele",
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"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)",
|
||||
@@ -694,6 +695,8 @@
|
||||
"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)",
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"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)",
|
||||
@@ -694,6 +695,8 @@
|
||||
"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)",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"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 destinataire",
|
||||
"recipient_maps": "Cartes des destinataires",
|
||||
"smtp_ip_access": "Changer les hôtes autorisés pour SMTP",
|
||||
"sogo_access": "Autoriser la gestion des accès à SOGo",
|
||||
"sogo_profile_reset": "Réinitialiser le profil SOGo",
|
||||
@@ -109,7 +109,9 @@
|
||||
"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"
|
||||
"dry": "Simuler la synchronisation",
|
||||
"internal": "Interne",
|
||||
"internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine."
|
||||
},
|
||||
"admin": {
|
||||
"access": "Accès",
|
||||
@@ -407,7 +409,9 @@
|
||||
"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"
|
||||
"filter": "Filtrer",
|
||||
"needs_restart": "nécessite un redémarrage",
|
||||
"iam": "Fournisseur d'identité"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Accès refusé ou données de formulaire non valides",
|
||||
@@ -441,7 +445,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 l’ID 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 l’image",
|
||||
@@ -548,7 +552,11 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"debug": {
|
||||
"chart_this_server": "Graphique (ce serveur)",
|
||||
@@ -693,7 +701,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 à l’hôte distant",
|
||||
"timeout2": "Délai de connexion à l’hôte local",
|
||||
@@ -734,7 +742,20 @@
|
||||
"mailbox_rename_alias": "Créer un alias automatiquement",
|
||||
"sogo_access": "Redirection directe vers SOGo",
|
||||
"pushover": "Pushover",
|
||||
"pushover_sound": "Son"
|
||||
"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)."
|
||||
},
|
||||
"footer": {
|
||||
"cancel": "Annuler",
|
||||
@@ -789,7 +810,8 @@
|
||||
"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"
|
||||
"login_admintext": "Se connecter en tant qu'administrateur",
|
||||
"email": "Adresse de courriel"
|
||||
},
|
||||
"mailbox": {
|
||||
"action": "Action",
|
||||
@@ -896,7 +918,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 bénéficiaires",
|
||||
"recipient_maps": "Cartes des destinataires",
|
||||
"relay_all": "Relayer tous les destinataires",
|
||||
"remove": "Supprimer",
|
||||
"resources": "Ressources",
|
||||
@@ -965,7 +987,8 @@
|
||||
"syncjob_check_log": "Vérifier le journal",
|
||||
"recipient": "Destinataire",
|
||||
"open_logs": "Afficher les journaux",
|
||||
"iam": "Fournisseur d'identité"
|
||||
"iam": "Fournisseur d'identité",
|
||||
"internal": "Interne"
|
||||
},
|
||||
"oauth2": {
|
||||
"access_denied": "Veuillez vous connecter en tant que propriétaire de la boîte de réception pour accorder l’accès via Oauth2.",
|
||||
@@ -1222,7 +1245,7 @@
|
||||
"email_and_dav": "Courriel, calendriers et contacts",
|
||||
"encryption": "Chiffrement",
|
||||
"excludes": "Exclus",
|
||||
"expire_in": "Expire dans",
|
||||
"expire_in": "Expirer 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",
|
||||
@@ -1243,7 +1266,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 courant (confirmer les changements)",
|
||||
"password_now": "Mot de passe actuel (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 s’appliqueront à tout le courrier propre (non spam) livré à <b>%s</b> y compris les alias (partagés, non partagés, étiquetés).",
|
||||
@@ -1350,7 +1373,12 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Impossible de supprimer l’utilisateur connecté",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"weeks": "Εβδομάδες",
|
||||
"with_app_password": "με κωδικό εφαρμογής",
|
||||
"year": "χρόνος",
|
||||
"years": "χρόνια"
|
||||
"years": "χρόνια",
|
||||
"value": "Τιμή"
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Αδυναμία διαγραφής συνδεδεμένου χρήστη",
|
||||
@@ -16,5 +17,170 @@
|
||||
"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": "Επιλογές αναμετάδοσης"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,9 @@
|
||||
"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": "✓",
|
||||
"success": "Siker"
|
||||
"success": "Siker",
|
||||
"login_page": "Belépő oldal",
|
||||
"needs_restart": "újraindítást igényel"
|
||||
},
|
||||
"edit": {
|
||||
"active": "Aktív",
|
||||
@@ -1070,7 +1072,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.<domain>, autodiscover.<domain>).<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": "Kedves",
|
||||
"kind": "Típus",
|
||||
"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",
|
||||
@@ -1092,9 +1094,9 @@
|
||||
"exclude": "Objektumok kizárása (regex)",
|
||||
"full_name": "Teljes név",
|
||||
"gal": "Globális címlista",
|
||||
"goto_ham": "Tanulj <span class=\"text-success\"><b>sonkaként</b></span>",
|
||||
"goto_ham": "Tanítás <span class=\"text-success\"><b>valódi</b></span> levélként",
|
||||
"goto_null": "Leveleket csendben eldobni",
|
||||
"goto_spam": "Tanuld <span class=\"text-danger\"><b>spamként</b></span>",
|
||||
"goto_spam": "Tanítás <span class=\"text-danger\"><b>spam</b></span>ként",
|
||||
"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>",
|
||||
@@ -1102,7 +1104,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": "Házigazda",
|
||||
"hostname": "Hoszt",
|
||||
"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",
|
||||
@@ -1140,6 +1142,109 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
"delete2": "Usuń wiadomości w miejscu docelowym, które nie znajdują się w źródle",
|
||||
"destination": "Miejsce docelowe",
|
||||
"disable_login": "Nie pozwalaj na logowanie(poczta przychodząca jest nadal akceptowana)",
|
||||
"domain_matches_hostname": "Domena %s pasuje do nazwy hosta",
|
||||
"domain_matches_hostname": "Domena pasuje do nazwy hosta",
|
||||
"dry": "Symulacja synchronizacji",
|
||||
"gal": "Globalna lista adresów",
|
||||
"gal_info": "GAL zawiera wszystkie obiekty domeny i nie może być edytowany przez żadnego użytkownika. Wolne/zajęte logi w SOGo bedą nidostępne, jeśli są wyłączone! <b>Uruchom ponownie SOGo, aby zastosować zmiany.</b>",
|
||||
@@ -139,7 +139,7 @@
|
||||
"f2b_max_attempts": "Max. ilość prób",
|
||||
"f2b_parameters": "Parametry Fail2ban",
|
||||
"f2b_retry_window": "Spróbuj ponownie (s) dla max. ilości prób",
|
||||
"f2b_whitelist": "Biała lista sieci/hosty",
|
||||
"f2b_whitelist": "Dozwolone sieci/hosty",
|
||||
"filter_table": "Tabela filtru",
|
||||
"forwarding_hosts": "Hosty przekazujące",
|
||||
"forwarding_hosts_add_hint": "Możesz albo wyszczególnić adresy IPv4/IPv6, sieci w notacji CIDR, nazwy hostów (które zostaną rozłożone na adresy IP), albo nazwy domen (które zostaną rozłożone na adresy IP poprzez sprawdzanie rekordów SPF lub, w razie ich braku, rekordów MX).",
|
||||
@@ -173,7 +173,7 @@
|
||||
"active_rspamd_settings_map": "Aktywne ustawienia mapy",
|
||||
"add_admin": "Dodaj administratora",
|
||||
"add_relayhost": "Dodaj transport zależny od nadawcy",
|
||||
"add_relayhost_hint": "Miej na uwadze, że dane uwierzytelniające, jeśli takie istnieją, będą przechowywane w postaci zwykłego tekstu",
|
||||
"add_relayhost_hint": "Miej na uwadze, że dane uwierzytelniające, jeśli takie istnieją, będą przechowywane w postaci zwykłego tekstu.",
|
||||
"add_row": "Dodaj wiersz",
|
||||
"add_settings_rule": "Dodaj regułę ustawień",
|
||||
"add_transport": "Dodaj transport",
|
||||
@@ -205,7 +205,7 @@
|
||||
"convert_html_to_text": "Konwertuj HTML na zwykły tekst",
|
||||
"copy_to_clipboard": "Tekst skopiowany do schowka!",
|
||||
"cors_settings": "Ustawienia CORS",
|
||||
"credentials_transport_warning": "Ostrzeżenie</b>: Dodanie nowego wpisu mapy transportu spowoduje aktualizację danych uwierzytelniających dla wszystkich wpisów z pasującą kolumną hop.",
|
||||
"credentials_transport_warning": "<b>Ostrzeżenie</b>: Dodanie nowego wpisu mapy transportu spowoduje aktualizację danych uwierzytelniających dla wszystkich wpisów z pasującą kolumną hop.",
|
||||
"customer_id": "Identyfikator klienta",
|
||||
"customize": "Dostosuj",
|
||||
"login_page": "Strona logowania",
|
||||
@@ -226,7 +226,7 @@
|
||||
"f2b_ban_time_increment": "Czas zakazu jest zwiększany z każdym zakazem",
|
||||
"f2b_blacklist": "Lista odrzuconych sieci/hostów",
|
||||
"f2b_filter": "Filtry Regex",
|
||||
"f2b_list_info": "Odrzucony host lub sieć zawsze będzie przewyższać jednostkę zezwalającą.</b>Zastosowanie aktualizacji listy zajmie kilka sekund.</b><b>",
|
||||
"f2b_list_info": "Odrzucony host lub sieć zawsze będzie przewyższać jednostkę zezwalającą.<b>Zastosowanie aktualizacji listy zajmie kilka sekund.</b>",
|
||||
"f2b_manage_external": "Zarządzaj Fail2Ban zewnętrznie",
|
||||
"f2b_manage_external_info": "Fail2ban nadal będzie utrzymywać listę banów, ale nie będzie aktywnie ustalać zasad blokowania ruchu. Użyj wygenerowane listy banów poniżej, aby zewnętrznie zablokować ruch.",
|
||||
"f2b_max_ban_time": "Max. czas bana (s)",
|
||||
@@ -240,7 +240,7 @@
|
||||
"generate": "Generuj",
|
||||
"guid": "GUID - unikalny identyfikator instancji",
|
||||
"guid_and_license": "GUID & licencja",
|
||||
"hash_remove_info": "Usunięcie hasha z limitem współczynnika (jeśli nadal istnieje) spowoduje całkowite zresetowanie jego licznika.<br>\n\nKażdy hash jest oznaczony indywidualnym kolorem.",
|
||||
"hash_remove_info": "Usunięcie hasha z limitem współczynnika (jeśli nadal istnieje) spowoduje całkowite zresetowanie jego licznika.<br> Każdy hash jest oznaczony indywidualnym kolorem.",
|
||||
"help_text": "Zastąp tekst pomocy poniżej maski logowania (dozwolone HTML)",
|
||||
"html": "HTML",
|
||||
"iam": "Dostawca tożsamości",
|
||||
@@ -266,7 +266,7 @@
|
||||
"iam_port": "Port",
|
||||
"iam_realm": "Realm",
|
||||
"iam_redirect_url": "Przekierowanie Url",
|
||||
"iam_rest_flow": "Mailpassword Flow",
|
||||
"iam_rest_flow": "Procedura uwierzytelniania hasła pocztowego",
|
||||
"iam_server_url": "Adres URL serwera",
|
||||
"iam_sso": "Pojedyncze logowanie",
|
||||
"iam_sync_interval": "Interwał synchronizacji / importu (min)",
|
||||
@@ -310,7 +310,7 @@
|
||||
"oauth2_add_client": "Dodaj klienta OAuth2",
|
||||
"oauth2_client_id": "ID klienta",
|
||||
"oauth2_client_secret": "Sekret klienta",
|
||||
"oauth2_info": "Implementacja OAuth2 obsługuje typ grantu \"Kod autoryzacji\" i wydaje tokeny odświeżania.<br>\nSerwer automatycznie wydaje również nowe tokeny odświeżania, po użyciu tokena odświeżania.<br><br>\n• Domyślnym zakresem jest <i>profil</i>. Tylko użytkownicy skrzynek pocztowych mogą być uwierzytelniani w OAuth2. Jeśli parametr zakresu zostanie pominięty, wraca do <i>profil</i>.<br>\n\nParametr <i>state</i> musi zostać wysłany przez klienta w ramach żądania autoryzacji.<br><br>>\nŚcieżki zapytań do API OAuth2: <br>\n<ul>\nPunkt końcowy autoryzacji: <kod>/autoryzuj</koduj</koduj</li>\nPunkt końcowy tokena: <kod>/autoth/token</kod></li>\nStrona zasobów: <kod>/prawo/profil</kod>>\n</ul>\nRegeneracja tajemnicy klienta nie spowoduje wygaśnięcia istniejących kodów autoryzacyjnych, ale nie odnowi ich tokena.<br><br>\nOdwołanie tokenów klienta spowoduje natychmiastowe zakończenie wszystkich aktywnych sesji. Wszyscy klienci muszą się ponownie uwierzytelnić.",
|
||||
"oauth2_info": "Implementacja OAuth2 obsługuje typ grantu \"Kod autoryzacji\" i wydaje tokeny odświeżania.<br>\nSerwer automatycznie wydaje również nowe tokeny odświeżania, po użyciu tokena odświeżania.<br><br>\n• Domyślnym zakresem jest <i>profil</i>. Tylko użytkownicy skrzynek pocztowych mogą być uwierzytelniani w OAuth2. Jeśli parametr zakresu zostanie pominięty, wraca do <i>profil</i>.<br>\n\nParametr <i>state</i> musi zostać wysłany przez klienta w ramach żądania autoryzacji.<br><br>>\nŚcieżki zapytań do API OAuth2: <br>\n<ul>\nPunkt końcowy autoryzacji\n<code>/oauth/authorize</code></li> <li>Token endpoint: <code>/oauth/token</code></li> <li>Resource page: <code>/oauth/profile</code></li>\n</ul>\nRegeneracja tajemnicy klienta nie spowoduje wygaśnięcia istniejących kodów autoryzacyjnych, ale nie odnowi ich tokena.<br><br>\nOdwołanie tokenów klienta spowoduje natychmiastowe zakończenie wszystkich aktywnych sesji. Wszyscy klienci muszą się ponownie uwierzytelnić.",
|
||||
"oauth2_redirect_uri": "Przekieruj URI",
|
||||
"oauth2_renew_secret": "Wygeneruj sekret nowego klienta",
|
||||
"oauth2_revoke_tokens": "Unieważnij wszystkie tokeny klienta",
|
||||
@@ -362,13 +362,13 @@
|
||||
"reset_limit": "Usuń hash",
|
||||
"reset_password_vars": "<code>{{link}}</code> Wygenerowany link do resetu hasła <br><code>{{username}}</code> Nazwa skrzynki pocztowej użytkownika, który poprosił o zresetowanie hasła<br><code>{{username2}}</code> Nazwa skrzynki pocztowej do odzyskiwania <br><code>{{date}}</code> Data złożenia żądania resetowania hasła<br><code>{{token_lifetime}}</code>Żywotność tokena w minutach<br><code>{{hostname}}</code> Nazwa hosta mailcow",
|
||||
"restore_template": "Pozostaw puste, aby przywrócić szablon domyślny.",
|
||||
"routing": "Routing",
|
||||
"routing": "Routowanie",
|
||||
"rsetting_add_rule": "Dodaj regułę",
|
||||
"rsetting_content": "Zawartość reguły",
|
||||
"rsetting_desc": "Krótki opis",
|
||||
"rsetting_no_selection": "Proszę zaznaczyć regułę",
|
||||
"rsetting_none": "Brak dostępnych reguł",
|
||||
"rsettings_insert_preset": "Wstaw przykład presetowany \"%s\"",
|
||||
"rsettings_insert_preset": "Wstaw przykładowy preset",
|
||||
"rsettings_preset_1": "Wyłącz wszystkie z wyjątkiem DKIM i limitu stawek dla uwierzytelnionych użytkowników",
|
||||
"rsettings_preset_2": "Administratorzy poczty pozwalają na spam",
|
||||
"rsettings_preset_3": "Zezwalaj tylko określonym nadawcom na skrzynkę pocztową (tj. używaj tylko jako wewnętrznej skrzynki pocztowej)",
|
||||
@@ -390,7 +390,7 @@
|
||||
"title": "Tytuł",
|
||||
"title_name": "Tytuł strony internetowej \"mailcow UI\"",
|
||||
"to_top": "Powrót na górę",
|
||||
"transport_dest_format": "Regex lub syntax: example.org, .example.org, *, box@example.org\n(wiele wartości można rozdzielić przecinkami)",
|
||||
"transport_dest_format": "Regex lub syntax: example.org, .example.org, *, box@example.org(wiele wartości można rozdzielić przecinkami)",
|
||||
"transport_maps": "Mapy transportu",
|
||||
"transport_test_rcpt_info": "• Użyj null@hosted.mailcow.de aby przetestować przekazywanie do nieznanego miejsca docelowego.",
|
||||
"transports_hint": "• Wpis mapy transportowej <b>overrules</b> mapa transportowa zależna od nadawcy</b>.<br>\nNajlepiej stosować transporty oparte na MX •.<br>\nUstawienia zasad Outbound TLS dotyczące użytkownika są ignorowane i mogą być egzekwowane tylko przez wpisy mapy zasad TLS\n• Usługa transportu dla zdefiniowanych transportów jest zawsze \"smtp:\" i dlatego uruchomi TLS, gdy będzie oferowana. Wrapped TLS (SMTPS) nie jest obsługiwany.<br>\nAdresy pasujące do \"/localhost$/\" będą zawsze transportowane przez \"lokalne:\", dlatego miejsce docelowe \"*\" nie będzie miało zastosowania do tych adresów.<br>\n• Aby określić poświadczenia dla przykładowego następnego hopa \"[host]:25\", Postfix <b>zawsze</b> kolejkuje zapytania o \"host\" przed wyszukaniem \"[host]:25\". Takie zachowanie uniemożliwia jednoczesne użycie \"host\" i \"[host]:25.",
|
||||
@@ -418,21 +418,21 @@
|
||||
"alias_domain_invalid": "Alias domeny nieprawidłowy",
|
||||
"alias_empty": "Alias nie może być pusty",
|
||||
"alias_goto_identical": "Alias i Idź do nie mogą być identyczne",
|
||||
"alias_invalid": "Alias nieprawidłowy",
|
||||
"alias_invalid": "Adres aliasu jest nieprawidłowy",
|
||||
"aliasd_targetd_identical": "Alias domeny nie może być identyczny z domeną docelową",
|
||||
"aliases_in_use": "Maks. liczba aliasów musi być większa od lub równa %d",
|
||||
"description_invalid": "Nieprawidłowy opis źródła",
|
||||
"dkim_domain_or_sel_invalid": "Nieprawidłowa domena lub selektor DKIM",
|
||||
"domain_exists": "Domena %s już istnieje",
|
||||
"domain_exists": "Domena już istnieje",
|
||||
"domain_invalid": "Nazwa domeny jest pusta lub nieprawidłowa",
|
||||
"domain_not_empty": "Nie można usunąć niepustej domeny",
|
||||
"domain_not_found": "Nie znaleziono domeny %s",
|
||||
"domain_quota_m_in_use": "Limit domeny musi być większy lub równy do %s MiB",
|
||||
"domain_not_found": "Nie znaleziono domeny",
|
||||
"domain_quota_m_in_use": "Limit domeny musi być większy lub równy do MiB",
|
||||
"goto_empty": "Adres alias musi zawierać co najmniej jeden prawidłowy adres goto",
|
||||
"goto_invalid": "Adres Idź do jest nieprawidłowy",
|
||||
"is_alias": "%s został już podany jako alias",
|
||||
"is_alias_or_mailbox": "%s podano wcześniej jako alias lub skrzynkę",
|
||||
"is_spam_alias": "%s podano wcześniej jako alias dla spam",
|
||||
"is_alias": "został już podany jako alias",
|
||||
"is_alias_or_mailbox": "jest już znany jako alias, skrzynka pocztowa lub adres aliasu rozwinięty z domeny aliasu.",
|
||||
"is_spam_alias": "jest już znany jako tymczasowy adres aliasu (adres aliasu antyspamowego)",
|
||||
"last_key": "Nie można usunąć ostatniego klucza, zamiast tego należy wyłączyć TFA.",
|
||||
"login_failed": "Logowanie nie powiodło się",
|
||||
"mailbox_invalid": "Nieprawidłowa nazwa skrzynki",
|
||||
@@ -456,34 +456,34 @@
|
||||
"sender_acl_invalid": "ACL Nadawcy jest nieprawidłowy",
|
||||
"target_domain_invalid": "Domena Idź do jest nieprawidłowa",
|
||||
"targetd_not_found": "Nie znaleziono domeny docelowej",
|
||||
"username_invalid": "Nie można użyć nazwy użytkownika",
|
||||
"username_invalid": "Nie można użyć tej nazwy użytkownika",
|
||||
"validity_missing": "Proszę wyznaczyć termin ważności",
|
||||
"to_invalid": "Pole odbiorca nie może być puste",
|
||||
"app_name_empty": "Nazwa aplikacji nie może być pusta",
|
||||
"app_passwd_id_invalid": "Identyfikator hasła aplikacji %s jest niepoprawny",
|
||||
"app_passwd_id_invalid": "Identyfikator hasła aplikacji jest niepoprawny",
|
||||
"authsource_in_use": "Nie można zmienić ani usunąć dostawcy tożsamości, ponieważ jest używany przez co najmniej jednego użytkownika.",
|
||||
"bcc_empty": "Miejsce docelowe BCC nie może być puste",
|
||||
"bcc_exists": "Mapa BCC %s istnieje dla typu %s",
|
||||
"bcc_must_be_email": "Miejsce docelowe BCC %s nie jest prawidłowym adresem e-mail",
|
||||
"comment_too_long": "Komentarz zbyt długi, maksymalnie 160 dozwolonych znaków.",
|
||||
"bcc_exists": "Mapa BCC istnieje już dla tego typu",
|
||||
"bcc_must_be_email": "Miejsce docelowe BCC nie jest prawidłowym adresem e-mail",
|
||||
"comment_too_long": "Komentarz zbyt długi, maksymalnie 160 dozwolonych znaków",
|
||||
"cors_invalid_method": "Podano nieprawidłową metodę Allow-Method",
|
||||
"cors_invalid_origin": "Podano nieprawidłową wartość Allow-Origin",
|
||||
"defquota_empty": "Domyślny limit skrzynki pocztowej nie może wynosić 0",
|
||||
"defquota_empty": "Domyślny limit skrzynki pocztowej nie może wynosić 0.",
|
||||
"demo_mode_enabled": "Tryb demo jest włączony",
|
||||
"dkim_domain_or_sel_exists": "Klucz DKIM dla \"%s\" istnieje i nie zostanie nadpisany",
|
||||
"dkim_domain_or_sel_exists": "Klucz DKIM już istnieje i nie zostanie nadpisany",
|
||||
"domain_cannot_match_hostname": "Domena nie może być taka sama jak nazwa hosta",
|
||||
"extended_sender_acl_denied": "brak ACL do ustawiania adresów nadawcy zewnętrznego",
|
||||
"extra_acl_invalid": "Adres nadawcy zewnętrznego \"%s\" jest nieważny",
|
||||
"extra_acl_invalid_domain": "Nadawca zewnętrzny \"%s\" używa nieprawidłowej domeny",
|
||||
"fido2_verification_failed": "Weryfikacja FIDO2 nie powiodła się: %s",
|
||||
"extra_acl_invalid": "Adres nadawcy zewnętrznego jest nieprawidłowy",
|
||||
"extra_acl_invalid_domain": "Nadawca zewnętrzny używa nieprawidłowej domeny",
|
||||
"fido2_verification_failed": "Weryfikacja FIDO2 nie powiodła się",
|
||||
"file_open_error": "Nie można otworzyć pliku do zapisu",
|
||||
"filter_type": "Niewłaściwy typ filtra",
|
||||
"from_invalid": "Nadawca nie może być pusty",
|
||||
"generic_server_error": "Wystąpił nieoczekiwany błąd serwera. Skontaktuj się z administratorem.",
|
||||
"global_filter_write_error": "Nie można zapisać pliku filtra: %s",
|
||||
"global_map_invalid": "Id mapy globalnej %s nieprawidłowe",
|
||||
"global_map_write_error": "Nie można napisać globalnej mapy ID %s: %s",
|
||||
"ham_learn_error": "Błąd uczenia ham: %s",
|
||||
"global_filter_write_error": "Nie można zapisać pliku filtra",
|
||||
"global_map_invalid": "Id mapy globalnej nieprawidłowe",
|
||||
"global_map_write_error": "Nie można zapisać globalnej mapy ID",
|
||||
"ham_learn_error": "Błąd uczenia ham",
|
||||
"iam_test_connection": "Połączenie nie powiodło się",
|
||||
"imagick_exception": "Błąd: wyjątek Imagick podczas odczytu obrazu",
|
||||
"img_dimensions_exceeded": "Obraz przekracza maksymalny rozmiar obrazu",
|
||||
@@ -491,25 +491,25 @@
|
||||
"img_size_exceeded": "Obraz przekracza maksymalny rozmiar pliku",
|
||||
"img_tmp_missing": "Nie można zweryfikować pliku obrazu: Nie znaleziono pliku tymczasowego",
|
||||
"invalid_bcc_map_type": "Nieprawidłowy typ mapy BCC",
|
||||
"invalid_destination": "Format docelowy \"%s\" jest nieprawidłowy",
|
||||
"invalid_destination": "Format docelowy jest nieprawidłowy",
|
||||
"invalid_filter_type": "Nieprawidłowy typ filtra",
|
||||
"invalid_host": "Nieprawidłowy określony host: %s",
|
||||
"invalid_host": "Nieprawidłowy określony host",
|
||||
"invalid_mime_type": "Niepoprawny typ mime",
|
||||
"invalid_nexthop": "Następny format hop jest nieprawidłowy",
|
||||
"invalid_nexthop_authenticated": "Następny hop już istnieje z innymi danymi logowania — zaktualizuj najpierw istniejące dane uwierzytelniające dla tego hopa",
|
||||
"invalid_recipient_map_new": "Określony nieprawidłowy nowy odbiorca: %s",
|
||||
"invalid_recipient_map_old": "Określony nieprawidłowy pierwotny odbiorca: %s",
|
||||
"invalid_nexthop_authenticated": "Następny hop już istnieje z innymi danymi logowania, zaktualizuj najpierw istniejące dane uwierzytelniające dla tego hopa.",
|
||||
"invalid_recipient_map_new": "Określony nieprawidłowy nowy odbiorca",
|
||||
"invalid_recipient_map_old": "Określony nieprawidłowy pierwotny odbiorca",
|
||||
"invalid_reset_token": "Nieprawidłowy token resetu",
|
||||
"ip_list_empty": "Lista dozwolonych adresów IP nie może być pusta",
|
||||
"mailbox_defquota_exceeds_mailbox_maxquota": "Domyślny limit skrzynki pocztowej przekracza maksymalny dozwolony limit",
|
||||
"malformed_username": "Nieprawidłowy format nazwy użytkownika.",
|
||||
"malformed_username": "Nieprawidłowy format nazwy użytkownika",
|
||||
"map_content_empty": "Zawartość mapy nie może być pusta",
|
||||
"max_age_invalid": "Nieprawidłowa wartość maksymalnego wieku: %s",
|
||||
"mode_invalid": "Tryb %s jest nieprawidłowy",
|
||||
"mx_invalid": "Rekord MX %s jest nieprawidłowy",
|
||||
"mysql_error": "Błąd MySQL: %s",
|
||||
"network_host_invalid": "Nieprawidłowa sieć lub host: %s",
|
||||
"next_hop_interferes": "%s powoduje konflikt z nexthop %s.",
|
||||
"next_hop_interferes": "%s powoduje konflikt z nexthopem %s",
|
||||
"next_hop_interferes_any": "Istniejący next hop koliduje z %s.”",
|
||||
"nginx_reload_failed": "Nie udało się przeładować Nginx: %s",
|
||||
"no_user_defined": "Brak zdefiniowanego użytkownika",
|
||||
@@ -523,17 +523,17 @@
|
||||
"recovery_email_failed": "Nie można wysłać e-maila odzyskiwania. Skontaktuj się ze swoim administratorem.",
|
||||
"redis_error": "Błąd Redis: %s",
|
||||
"relayhost_invalid": "Wpis mapy %s jest niepoprawny",
|
||||
"release_send_failed": "Nie udało się zwolnić wiadomości: %s.",
|
||||
"release_send_failed": "Nie udało się zwolnić wiadomości: %s",
|
||||
"required_data_missing": "Brakuje wymaganych danych %s",
|
||||
"reset_f2b_regex": "Filtr Regex nie mógł zostać zresetowany na czas, spróbuj ponownie lub poczekaj jeszcze kilka sekund i przeładuj stronę.",
|
||||
"reset_token_limit_exceeded": "Limit tokenów Reset został przekroczony. Spróbuj ponownie później",
|
||||
"reset_token_limit_exceeded": "Limit tokenów Reset został przekroczony. Spróbuj ponownie później.",
|
||||
"rl_timeframe": "Nieprawidłowo ustawiony przedział czasu limitu",
|
||||
"rspamd_ui_pw_length": "Hasło do Rspamd UI powinno mieć co najmniej 6 znaków długości",
|
||||
"script_empty": "Skrypt nie może być pusty",
|
||||
"set_acl_failed": "Nie udało się ustawić ACL",
|
||||
"settings_map_invalid": "Ustawienia id mapy %s są nieprawidłowe",
|
||||
"sieve_error": "Błąd podczas analizy skryptu Sieve: %s",
|
||||
"spam_learn_error": "Błąd uczenia spamu: %s.",
|
||||
"spam_learn_error": "Błąd uczenia spamu: %s",
|
||||
"subject_empty": "Temat nie może być pusty",
|
||||
"targetd_relay_domain": "Domena docelowa %s jest skonfigurowana jako domena przekazująca relay",
|
||||
"template_exists": "Szablon %s już istnieje",
|
||||
@@ -589,7 +589,7 @@
|
||||
"previous": "Poprzednia strona",
|
||||
"quota_mb": "Limit wielkośći (MiB)",
|
||||
"relay_all": "Przekaż wszystkim odbiorcom",
|
||||
"relay_all_info": "<small>Jeśli decydujesz się <b>nie</b> przekazywać wszystkim odbiorcom, musisz dodać (\"ślepą\")skrzynkę dla każdego poszczególnego odbiorcy, któremu należy przekazać.</small>",
|
||||
"relay_all_info": "↪ Jeśli zdecydujesz się <b>nie</b> przekazywać wszystkim odbiorcom, musisz dodać (\"ślepą\")skrzynkę dla każdego poszczególnego odbiorcy, któremu należy przekazać.",
|
||||
"relay_domain": "Domena przekaźnikowa",
|
||||
"remove": "Usuń",
|
||||
"resource": "Zasób",
|
||||
@@ -608,7 +608,7 @@
|
||||
"advanced_settings": "Ustawienia zaawansowane",
|
||||
"allow_from_smtp": "Zezwalaj tylko tym adresom IP na używanie <b>SMTP</b>",
|
||||
"allow_from_smtp_info": "Pozostaw puste, aby zezwolić na wszystkich nadawców.<br>adresy IPv4/IPv6 i sieci.",
|
||||
"allowed_protocols": "Dozwolone protokoły dla bezpośredniego dostępu użytkownika (nie wpływa na protokoły haseł aplikacji).",
|
||||
"allowed_protocols": "Dozwolone protokoły dla bezpośredniego dostępu użytkownika (nie wpływa na protokoły haseł aplikacji)",
|
||||
"app_name": "Nazwa aplikacji",
|
||||
"app_passwd": "Hasło do aplikacji",
|
||||
"app_passwd_protocols": "Dozwolone protokoły dla hasła aplikacji",
|
||||
@@ -616,10 +616,10 @@
|
||||
"bcc_dest_format": "Miejscem docelowym BCC musi być jeden prawidłowy adres e-mail.<br>Jeśli chcesz wysłać kopię na wiele adresów, utwórz alias i użyj go tutaj.",
|
||||
"client_id": "Id klienta",
|
||||
"client_secret": "Tajny klucz klienta(sekret)",
|
||||
"comment_info": "Komentarz prywatny nie jest widoczny dla użytkownika, natomiast komentarz publiczny jest wyświetlany jako podpowiedź po najechaniu kursorem w widoku użytkownika.",
|
||||
"comment_info": "Komentarz prywatny nie jest widoczny dla użytkownika, natomiast komentarz publiczny jest wyświetlany jako podpowiedź po najechaniu kursorem w widoku użytkownika",
|
||||
"created_on": "Stworzony na",
|
||||
"custom_attributes": "Niestandardowe atrybuty",
|
||||
"delete2": "Usunąć wiadomości na koncie docelowym, które nie występują na koncie źródłowym?",
|
||||
"delete2": "Usuń wiadomości na koncie docelowym, które nie występują na koncie źródłowym",
|
||||
"disable_login": "Zablokuj logowanie (przychodząca poczta nadal będzie przyjmowana)",
|
||||
"domain_footer": "Stopka dla całej domeny",
|
||||
"domain_footer_html": "Stopka HTML",
|
||||
@@ -627,17 +627,17 @@
|
||||
"domain_footer_info_vars": {
|
||||
"auth_user": "{= auth_user =} - Uwierzytelniona nazwa użytkownika określona przez MTA",
|
||||
"from_user": "{= from_user =} – część użytkownika (local-part) adresu nadawcy; np. dla „moo@mailcow.tld\n” zwróci „moo”",
|
||||
"from_name": "{= from_name =} – nazwa nadawcy (From name) z nagłówka wiadomości; np. dla „Mailcow <moo@mailcow.tld\n>” zwróci „Mailcow”.",
|
||||
"from_name": "{= from_name =} – nazwa nadawcy (From name) z nagłówka wiadomości; np. dla \"Mailcow <moo@mailcow.tld>\" zwróci \"Mailcow\"",
|
||||
"from_addr": "{= from_addr =} – pełny adres nadawcy (z części envelope)",
|
||||
"from_domain": "{= from_domain =} – część domenowa adresu nadawcy (z envelope).",
|
||||
"custom": "{= foo =} - jeśli skrzynka pocztowa ma niestandardowy atrybut „foo” o wartości „bar”, zwraca „bar”."
|
||||
"from_domain": "{= from_domain =} – część domenowa adresu nadawcy (z envelope)",
|
||||
"custom": "{= foo =} - jeśli skrzynka pocztowa ma niestandardowy atrybut „foo” o wartości „bar”, zwraca „bar”"
|
||||
},
|
||||
"domain_footer_plain": "Stopka w formacie tekstowym(PLAIN)",
|
||||
"domain_footer_skip_replies": "Nie dodawaj stopki do odpowiedzi na e-maile",
|
||||
"extended_sender_acl": "Adresy nadawców zewnętrznych",
|
||||
"extended_sender_acl_info": "Klucz DKIM dla domeny powinien zostać zaimportowany, jeśli jest dostępny.\nPamiętaj, aby dodać ten serwer do odpowiadającego rekordu SPF typu TXT.\nKiedy domena lub domena aliasu zostanie dodana do tego serwera i pokrywa się z zewnętrznym adresem, zewnętrzny adres zostanie usunięty.",
|
||||
"force_pw_update": "Wymuszenie aktualizacji hasła przy następnym logowaniu",
|
||||
"force_pw_update_info": "Ten użytkownik będzie mógł logować się wyłącznie do %s. Hasła aplikacyjne pozostają aktywne.",
|
||||
"force_pw_update_info": "Ten użytkownik będzie mógł logować się wyłącznie tutaj %s. Hasła aplikacyjne pozostają aktywne.",
|
||||
"footer_exclude": "Wyklucz ze stopki",
|
||||
"gal": "Globalna lista adresowa",
|
||||
"gal_info": "GAL zawiera wszystkie obiekty domeny i nie może być edytowana przez żadnego użytkownika. Informacje o dostępności (free/busy) w SOGo są niedostępne, jeśli funkcja jest wyłączona!<b>Uruchom ponownie SOGo, aby zastosować zmiany.</b>",
|
||||
@@ -646,7 +646,7 @@
|
||||
"internal": "Wewnętrzny",
|
||||
"internal_info": "Aliasów wewnętrznych można używać tylko w obrębie własnej domeny lub domen aliasów.",
|
||||
"last_modified": "Ostatnio modyfikowany",
|
||||
"lookup_mx": "Destination to wyrażenie regularne dopasowujące nazwę serwera MX (np. <code>.*.google.com</code> — aby kierować całą pocztę wysyłaną do MX kończących się na google.com przez ten hop).",
|
||||
"lookup_mx": "Destination to wyrażenie regularne dopasowujące nazwę serwera MX (np. <code>.*.google.com</code> — aby kierować całą pocztę wysyłaną do MX kończących się na google.com przez ten hop)",
|
||||
"pushover_title": "Tytuł powiadomienia",
|
||||
"pushover_sound": "Dźwięk powiadomienia",
|
||||
"pushover_vars": "Gdy nie zdefiniowano żadnego filtra nadawcy, brane będą pod uwagę wszystkie wiadomości. Filtry oparte na wyrażeniach regularnych (regex) oraz dokładne dopasowania nadawców można definiować indywidualnie — będą one przetwarzane kolejno, niezależnie od siebie. Dostępne zmienne do użycia w treści i tytule (należy pamiętać o zasadach ochrony danych osobowych).",
|
||||
@@ -674,7 +674,39 @@
|
||||
"spam_score": "Ustaw własny poziom punktacji spamu",
|
||||
"timeout1": "Limit czasu połączenia z serwerem zdalnym",
|
||||
"timeout2": "Limit czasu połączenia z serwerem lokalnym",
|
||||
"validate_save": "Zatwierdź i zapisz"
|
||||
"validate_save": "Zatwierdź i zapisz",
|
||||
"pushover_info": "Ustawienia powiadomień push będą miały zastosowanie do wszystkich czystych (niespamowych) wiadomości dostarczanych do <b>%s</b> w tym aliasów (współdzielonych, niewspółdzielonych, oznaczonych).",
|
||||
"mailbox_quota_def": "Domyślny limit skrzynki pocztowej",
|
||||
"mailbox_relayhost_info": "Dotyczy wyłącznie skrzynki pocztowej i bezpośrednich aliasów, nadpisuje ustawienie serwera pośredniczącego (relayhost) dla domeny.",
|
||||
"maxbytespersecond": "Max. Ilość bajtów na sekundę <br><small>(0 = unlimited)</small>",
|
||||
"mailbox_rename": "Zmień nazwę skrzynki pocztowej",
|
||||
"mailbox_rename_agree": "Stworzyłem kopię zapasową.",
|
||||
"mailbox_rename_warning": "WAŻNE! Utwórz kopię zapasową przed zmianą nazwy skrzynki pocztowej.",
|
||||
"mailbox_rename_alias": "Tworzenie aliasów automatycznie",
|
||||
"mailbox_rename_title": "Nowa nazwa lokalnej skrzynki pocztowej",
|
||||
"mbox_rl_info": "Ten limit szybkości dotyczy nazwy logowania SASL i odpowiada dowolnemu adresowi „from” używanemu przez zalogowanego użytkownika. Limit szybkości dla skrzynki pocztowej nadpisuje limit szybkości dla całej domeny.",
|
||||
"nexthop": "Następny hop",
|
||||
"private_comment": "Prywatny komentarz",
|
||||
"public_comment": "Komentarz publiczny",
|
||||
"mta_sts": "Konfiguruj MTA-STS",
|
||||
"mta_sts_info": "<a\n\nhref='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> to standard wymuszający dostarczanie poczty elektronicznej pomiędzy serwerami pocztowymi z użyciem TLS oraz ważnych certyfikatów.\n\nJest stosowany wtedy, gdy użycie DANE nie jest możliwe z powodu braku lub nieobsługiwanego DNSSEC.\n\n<br><b>Uwaga</b>: Jeżeli domena odbiorcza obsługuje DANE z DNSSEC, DANE jest <b>zawsze</b> preferowane — MTA-STS działa wyłącznie jako mechanizm zapasowy.",
|
||||
"mta_sts_version": "Wersja.",
|
||||
"mta_sts_version_info": "Określa wersję standardu MTA-STS — obecnie jedyną prawidłową wartością jest <code>STSv1</code>..",
|
||||
"mta_sts_mode": "Tryb.",
|
||||
"mta_sts_mode_info": "Dostępne są trzy tryby do wyboru:\n<ul> <li><em>testing</em> – polityka jest wyłącznie monitorowana, a naruszenia nie mają wpływu na dostarczanie poczty.</li> <li><em>enforce</em> – polityka jest ściśle egzekwowana; połączenia bez ważnego TLS są odrzucane.</li> <li><em>none</em> – polityka jest publikowana, lecz nie jest stosowana.</li> </ul>.",
|
||||
"mta_sts_max_age": "Maksymalny czas obowiązywania.",
|
||||
"mta_sts_max_age_info": "Czas (w sekundach) przechowywania polityki w cache przez serwery odbierające..",
|
||||
"mta_sts_mx": "serwer MX.",
|
||||
"mta_sts_mx_info": "Umożliwia wysyłanie poczty wyłącznie do jawnie wymienionych nazw hostów serwerów pocztowych; wysyłający MTA sprawdza, czy nazwa hosta MX w DNS odpowiada liście z polityki, i zezwala na dostarczenie tylko przy użyciu ważnego certyfikatu TLS (ochrona przed atakami MITM)..",
|
||||
"mta_sts_mx_notice": "Dopuszcza się podanie wielu serwerów MX, rozdzielonych przecinkami..",
|
||||
"none_inherit": "Brak /Dziedzicz",
|
||||
"password_recovery_email": "Email do odzyskiwania hasła",
|
||||
"pushover": "Pushover",
|
||||
"pushover_evaluate_x_prio": "Eskaluj wiadomości o wysokim priorytecie [<code>X-Priority: 1</code>]",
|
||||
"pushover_only_x_prio": "Uwzględniaj wyłącznie wiadomości o wysokim priorytecie [<code>X-Priority: 1</code>]",
|
||||
"pushover_sender_array": "Uwzględniaj wyłącznie następujące adresy e-mail nadawców <small>(oddzielone przecinkami)</small>",
|
||||
"pushover_sender_regex": "Bierz pod uwagę następujący regex nadawcy",
|
||||
"pushover_text": "Tekst powiadomienia"
|
||||
},
|
||||
"footer": {
|
||||
"cancel": "Anuluj",
|
||||
@@ -721,7 +753,7 @@
|
||||
"other_logins": "lub zaloguj za pomocą",
|
||||
"email": "Adres e-mail",
|
||||
"back_to_mailcow": "Wróć do mailcow",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn Login",
|
||||
"fido2_webauthn": "Logowanie FIDO2/WebAuthn",
|
||||
"invalid_pass_reset_token": "Token resetowania hasła jest nieprawidłowy lub wygasł.<br>Proszę poprosić o nowy link do resetowania hasła.",
|
||||
"login_user": "Logowanie użytkownika",
|
||||
"login_dadmin": "Logowanie administratora domeny",
|
||||
@@ -831,7 +863,14 @@
|
||||
"template": "Szablon",
|
||||
"tls_map_dest": "Miejsce docelowe",
|
||||
"tls_map_dest_info": "Przykłady: example.org, .example.org, [mail.example.org]:25",
|
||||
"tls_map_parameters": "Parametry"
|
||||
"tls_map_parameters": "Parametry",
|
||||
"add_recipient_map_entry": "Dodaj mapę odbiorców",
|
||||
"add_template": "Dodaj szablon",
|
||||
"add_tls_policy_map": "Dodaj mapę polityk TLS",
|
||||
"address_rewriting": "Przepisywanie adresów",
|
||||
"alias_domain_alias_hint": "Aliasy <b>nie</b> są automatycznie stosowane do aliasów domen. Adres aliasu <code>my-alias@domain</code> <b>nie</b> obejmuje adresu <code>my-alias@alias-domain</code> (gdzie „alias-domain” jest przykładową domeną aliasową dla „domain”).\n<br> Aby przekierować pocztę do zewnętrznej skrzynki, użyj filtra Sieve (zob. kartę „Filtry” lub SOGo → Przekazywanie). Skorzystaj z opcji „Rozszerz alias na domeny aliasowe”, aby automatycznie dodać brakujące aliasy.",
|
||||
"alias_domain_backupmx": "Domena aliasowa nieaktywna dla domeny przekaźnikowej",
|
||||
"all_domains": "Wszystkie domeny"
|
||||
},
|
||||
"quarantine": {
|
||||
"action": "Działanie",
|
||||
@@ -847,12 +886,32 @@
|
||||
"rejected": "Odrzucony",
|
||||
"release": "Zwolnij",
|
||||
"release_body": "Dołączyliśmy Twoją wiadomość jako plik eml do tej wiadomości.",
|
||||
"release_subject": "Potencjalnie szkodliwa pozycja kwarantanny %s"
|
||||
"release_subject": "Potencjalnie szkodliwa pozycja kwarantanny %s",
|
||||
"show_item": "Pokaż element",
|
||||
"spam": "Spam",
|
||||
"spam_score": "Wskaźnik",
|
||||
"subj": "Temat",
|
||||
"table_size": "Rozmiar tabeli",
|
||||
"table_size_show_n": "Pokaż %s elementy",
|
||||
"text_from_html_content": "Zawartość (przekonwertowany HTML)",
|
||||
"text_plain_content": "Zawartość (tekst zwykły)",
|
||||
"type": "Typ"
|
||||
},
|
||||
"queue": {
|
||||
"queue_manager": "Menedżer kolejki",
|
||||
"delete": "Usuń wszystko",
|
||||
"ays": "Potwierdź, że chcesz usunąć wszystkie elementy z bieżącej kolejki."
|
||||
"ays": "Potwierdź, że chcesz usunąć wszystkie elementy z bieżącej kolejki.",
|
||||
"flush": "Opróżnij kolejkę wiadomości",
|
||||
"info": "Kolejka poczty zawiera wszystkie wiadomości e-mail oczekujące na dostarczenie.\nJeśli wiadomość e-mail pozostaje w kolejce przez dłuższy czas, system automatycznie ją usuwa.<br>\nKomunikat błędu dla danej wiadomości zawiera informacje o przyczynie, dla której nie mogła zostać dostarczona.",
|
||||
"legend": "Funkcje zarządzania kolejką poczty:",
|
||||
"deliver_mail": "Dostarcz",
|
||||
"deliver_mail_legend": "Próby ponownego dostarczenia wybranych wiadomości.",
|
||||
"hold_mail": "Wstrzymane",
|
||||
"hold_mail_legend": "Wstrzymuję wybrane maile. (Zapobiega dalszym próbom dostarczenia)",
|
||||
"show_message": "Pokaż wiadomość",
|
||||
"unban": "Usuń zablokowanie w kolejce",
|
||||
"unhold_mail": "Zwolnij wiadomość",
|
||||
"unhold_mail_legend": "Zwalnia wybrane wiadomości do dostarczenia (wymaga wcześniejszego wstrzymania)."
|
||||
},
|
||||
"start": {
|
||||
"help": "Pokaż/Ukryj panel pomocy",
|
||||
@@ -878,7 +937,7 @@
|
||||
"f2b_modified": "Zmiany w Fail2ban zostały zapisane",
|
||||
"forwarding_host_added": "Dodano hosta przekazującego %s",
|
||||
"forwarding_host_removed": "Usunięto hosta przekazującego %s",
|
||||
"item_deleted": "",
|
||||
"item_deleted": "Element %s skutecznie usunięte",
|
||||
"items_deleted": "Elementy %s skutecznie usunięte",
|
||||
"mailbox_added": "Dodano skrzynkę %s",
|
||||
"mailbox_modified": "Zapisano zmiany w skrzynce %s",
|
||||
@@ -895,7 +954,59 @@
|
||||
"verified_fido2_login": "Zweryfikowany login FIDO2",
|
||||
"verified_totp_login": "Zweryfikowany login TOTP",
|
||||
"verified_webauthn_login": "Zweryfikowany login WebAuthn",
|
||||
"verified_yotp_login": "Zweryfikowany login Yubico OTP"
|
||||
"verified_yotp_login": "Zweryfikowany login Yubico OTP",
|
||||
"acl_saved": "ACL dla obiektu %s zapisany",
|
||||
"admin_added": "Administrator %s został dodany",
|
||||
"admin_api_modified": "Zmiany w API zostały zapisane",
|
||||
"admin_removed": "Administrator %s został usunięty",
|
||||
"app_links": "Zapisane zmiany w linkach aplikacji",
|
||||
"app_passwd_added": "Dodano nowe hasło aplikacji",
|
||||
"app_passwd_removed": "Usunięto ID hasła aplikacji %s",
|
||||
"bcc_deleted": "Wpisy map BCC usunięte: %s",
|
||||
"bcc_edited": "Wpis mapy BCC %s edytowany",
|
||||
"bcc_saved": "Wpis mapy BCC zapisany",
|
||||
"cors_headers_edited": "Ustawienia CORS zostały zapisane",
|
||||
"custom_login_modified": "Dostosowanie logowania zostało pomyślnie zapisane",
|
||||
"db_init_complete": "Inicjalizacja bazy danych zakończona",
|
||||
"delete_filter": "Filtry %s ID usunięte",
|
||||
"delete_filters": "Usunięte filtry: %s",
|
||||
"deleted_syncjob": "Usunięte syncjob ID %s",
|
||||
"deleted_syncjobs": "Usunięte syncjobs: %s”.",
|
||||
"domain_add_dkim_available": "Klucz DKIM już istniał",
|
||||
"dkim_duplicated": "Klucz DKIM dla domeny %s został skopiowany do %s",
|
||||
"domain_footer_modified": "Zmiany w stopce domeny %s zostały zapisane",
|
||||
"dovecot_restart_success": "Dovecot został pomyślnie zrestartowany",
|
||||
"f2b_banlist_refreshed": "ID listy banów został pomyślnie odświeżony.",
|
||||
"global_filter_written": "Filtr został pomyślnie zapisany do pliku",
|
||||
"hash_deleted": "Hash usunięty",
|
||||
"iam_test_connection": "Połączenie powiodło się",
|
||||
"ip_check_opt_in_modified": "Sprawdzenie adresu IP zostało pomyślnie zapisane",
|
||||
"item_released": "Pozycja %s zwolniona",
|
||||
"items_released": "Wybrane elementy zostały zwolnione",
|
||||
"learned_ham": "Pomyślnie nauczono ID %s jako ham",
|
||||
"license_modified": "Zmiany w licencji zostały zapisane",
|
||||
"logged_in_as": "Zalogowany jako %s",
|
||||
"mailbox_renamed": "Nazwa skrzynki pocztowej została zmieniona z %s na %s",
|
||||
"nginx_reloaded": "Nginx został przeładowany",
|
||||
"password_policy_saved": "Polityka haseł została pomyślnie zapisana",
|
||||
"password_changed_success": "Hasło zostało pomyślnie zmienione",
|
||||
"pushover_settings_edited": "Ustawienia Pushover pomyślnie ustawione, proszę zweryfikować dane uwierzytelniające.",
|
||||
"qlearn_spam": "Identyfikator wiadomości %s został nauczony jako spam i usunięty",
|
||||
"queue_command_success": "Polecenie kolejki zostało pomyślnie wykonane",
|
||||
"recipient_map_entry_deleted": "Id mapy odbiorcy %s został usunięty",
|
||||
"recipient_map_entry_saved": "Wpis mapy odbiorcy \"%s\" został zapisany",
|
||||
"recovery_email_sent": "E-mail do odzyskiwania wysłany do %s",
|
||||
"relayhost_added": "Wpis mapy %s został dodany",
|
||||
"relayhost_removed": "Wpis mapy %s został usunięty",
|
||||
"reset_main_logo": "Reset do domyślnego logo",
|
||||
"rl_saved": "Limit szybkości dla obiektu %s został zapisany",
|
||||
"rspamd_ui_pw_set": "Hasło do Rspamd UI pomyślnie ustawione",
|
||||
"saved_settings": "Zapisane ustawienia",
|
||||
"settings_map_added": "Dodano ustawienia (wpis mapy)",
|
||||
"settings_map_removed": "Usunięte ustawienia mapy ID %s",
|
||||
"sogo_profile_reset": "Profil SOGo dla użytkownika/ów został zresetowany",
|
||||
"template_added": "Dodano szablon",
|
||||
"template_modified": "Zmiany w szablonie/ach zostały zapisane"
|
||||
},
|
||||
"tfa": {
|
||||
"api_register": "%s używa Yubico Cloud API. Proszę pobrać klucz API dla Twojego klucza <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">tutaj</a>",
|
||||
@@ -994,7 +1105,7 @@
|
||||
"spamfilter_table_remove": "Usuń",
|
||||
"spamfilter_table_rule": "Zasada",
|
||||
"spamfilter_wl": "Biała lista",
|
||||
"spamfilter_wl_desc": "Adresy e-mail znajdujące się na liście dozwolonych (allowlist) są zaprogramowane tak, aby <b> nigdy nie </b> były klasyfikowane jako spam.\nMożna używać symboli wieloznacznych (wildcardów).\nFiltr jest stosowany wyłącznie do bezpośrednich aliasów (aliasów wskazujących na jedną skrzynkę pocztową), z wyłączeniem aliasów typu „catch-all” oraz samej skrzynki pocztowej",
|
||||
"spamfilter_wl_desc": "Adresy e-mail znajdujące się na liście dozwolonych (allowlist) są zaprogramowane tak, aby <b> nigdy nie </b> były klasyfikowane jako spam. Można używać symboli wieloznacznych (wildcardów).Filtr jest stosowany wyłącznie do bezpośrednich aliasów (aliasów wskazujących na jedną skrzynkę pocztową), z wyłączeniem aliasów typu „catch-all” oraz samej skrzynki pocztowej",
|
||||
"spamfilter_yellow": "Żółty: ta wiadomość może być spamem, zostanie oznaczona jako spam i przeniesiona do folderu spam",
|
||||
"sync_jobs": "Zadania synchronizacji",
|
||||
"tag_handling": "Ustaw obsługę znaczników pocztowych",
|
||||
@@ -1034,7 +1145,7 @@
|
||||
"email_and_dav": "E-maile, kalendarze i kontakty",
|
||||
"empty": "Brak wyników",
|
||||
"expire_in": "wygasa w",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn (standard uwierzytelniania)",
|
||||
"from": "od",
|
||||
"generate": "generuj",
|
||||
"last_mail_login": "Ostatni login na skrzynkę pocztową",
|
||||
@@ -1087,14 +1198,15 @@
|
||||
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nie można połączyć się ze zdalnym serwerem",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Niewłaściwa nazwa użytkownika lub hasło",
|
||||
"text": "Tekst",
|
||||
"tfa_info": "Uwierzytelnianie dwuskładnikowe pomaga chronić Twoje konto.\nJeśli je włączysz, będziesz potrzebować haseł aplikacji, aby logować się do programów lub usług, które nie obsługują uwierzytelniania dwuskładnikowego (np. klientów poczty).",
|
||||
"tfa_info": "Uwierzytelnianie dwuskładnikowe pomaga chronić Twoje konto. Jeśli je włączysz, będziesz potrzebować haseł aplikacji, aby logować się do programów lub usług, które nie obsługują uwierzytelniania dwuskładnikowego (np. klientów poczty).",
|
||||
"title": "Tytuł",
|
||||
"value": "Wartość",
|
||||
"verify": "Zweryfikuj",
|
||||
"waiting": "Oczekuje",
|
||||
"with_app_password": "z hasłem aplikacji",
|
||||
"year": "rok",
|
||||
"years": "lata"
|
||||
"years": "lata",
|
||||
"spam_aliases_info": "Alias antyspamowy to tymczasowy adres e-mail, który może być używany do ochrony właściwych adresów pocztowych. <br>Opcjonalnie można ustawić czas wygaśnięcia, po którym alias zostanie automatycznie dezaktywowany, co pozwala skutecznie pozbyć się nadużywanych lub ujawnionych adresów."
|
||||
},
|
||||
"warning": {
|
||||
"session_ua": "Nieprawidłowy token formularza: Błąd walidacji User-Agent",
|
||||
@@ -1114,7 +1226,7 @@
|
||||
"decimal": ".",
|
||||
"emptyTable": "Brak danych w tabeli",
|
||||
"expand_all": "Rozszerz wszystko",
|
||||
"info": "Wyświetlanie od START do END z TOTAL wpisów.",
|
||||
"info": "Wyświetlanie od START do END z TOTAL wpisów",
|
||||
"infoEmpty": "Wyświetlanie od 0 do 0 z 0 wpisów",
|
||||
"infoFiltered": "(filtrowane z _MAX_ suma wpisów)",
|
||||
"thousands": ",",
|
||||
@@ -1145,7 +1257,7 @@
|
||||
"current_time": "Czas systemowy",
|
||||
"disk_usage": "Użycie dysku",
|
||||
"docs": "Dokumentacja",
|
||||
"error_show_ip": "Nie można ustalić publicznych adresów IP.",
|
||||
"error_show_ip": "Nie można ustalić publicznych adresów IP",
|
||||
"external_logs": "Logi zewnętrzne",
|
||||
"history_all_servers": "Historia (wszystkie serwery)",
|
||||
"in_memory_logs": "Logi w pamięci",
|
||||
@@ -1177,7 +1289,7 @@
|
||||
"dns_records": "Rekordy DNS",
|
||||
"dns_records_24hours": "Pamiętaj, że zmiany wprowadzone w DNS mogą zająć nawet do 24 godzin, zanim ich aktualny stan zostanie poprawnie odzwierciedlony na tej stronie.\nTa sekcja ma na celu ułatwienie Ci konfiguracji rekordów DNS oraz sprawdzenie, czy wszystkie rekordy zostały prawidłowo zapisane w DNS.",
|
||||
"dns_records_data": "Poprawne dane",
|
||||
"dns_records_docs": "Proszę skonsultuj również <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">dokumnetację</a>",
|
||||
"dns_records_docs": "Proszę skonsultuj również <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">dokumnetację</a>.",
|
||||
"dns_records_name": "Nazwa",
|
||||
"dns_records_status": "Aktualny stan",
|
||||
"dns_records_type": "Typ",
|
||||
@@ -1197,5 +1309,12 @@
|
||||
"set_fido2_touchid": "Zarejestruj Touch ID w Apple M1",
|
||||
"set_fn": "Ustaw przyjazną nazwę w FIDO",
|
||||
"start_fido2_validation": "Rozpocznij walidację FIDO2"
|
||||
},
|
||||
"ratelimit": {
|
||||
"disabled": "Wyłączone",
|
||||
"second": "wiadomości / sekundę",
|
||||
"minute": "wiadomości / minutę",
|
||||
"hour": "wiadomości / godzinę",
|
||||
"day": "wiadomości/ dzień"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +340,8 @@
|
||||
"tls_policy": "Política de TLS",
|
||||
"quarantine_attachments": "Anexos de quarentena",
|
||||
"filters": "Filtros",
|
||||
"smtp_ip_access": "Mudar anfitriões permitidos para SMTP"
|
||||
"smtp_ip_access": "Mudar anfitriões permitidos para SMTP",
|
||||
"app_passwds": "Gerenciar senhas de aplicativos"
|
||||
},
|
||||
"warning": {
|
||||
"no_active_admin": "Não é possível desactivar o último administrador activo"
|
||||
|
||||
@@ -109,7 +109,9 @@
|
||||
"timeout2": "Тайм-аут для подключения к локальному хосту",
|
||||
"username": "Имя пользователя",
|
||||
"validate": "Проверить",
|
||||
"validation_success": "Проверка прошла успешно"
|
||||
"validation_success": "Проверка прошла успешно",
|
||||
"internal": "Внутренний",
|
||||
"internal_info": "Внутренние псевдонимы доступны только из самого домена или доменов-псевдонимов."
|
||||
},
|
||||
"admin": {
|
||||
"access": "Настройки доступа",
|
||||
@@ -550,7 +552,11 @@
|
||||
"generic_server_error": "На сервере произошла непредвиденная ошибка. Пожалуйста, свяжитесь с вашим администратором.",
|
||||
"authsource_in_use": "Поставщик идентификационных данных не может быть изменен или удален, так как в данный момент он используется одним или несколькими пользователями.",
|
||||
"iam_test_connection": "Ошибка соединения",
|
||||
"required_data_missing": "Отсутствуют необходимые данные %s"
|
||||
"required_data_missing": "Отсутствуют необходимые данные %s",
|
||||
"max_age_invalid": "Максимальный возраст %s недействителен",
|
||||
"mode_invalid": "Режим %s недействителен",
|
||||
"mx_invalid": "Запись MX %s недействительна",
|
||||
"version_invalid": "Версия %s недействительна"
|
||||
},
|
||||
"datatables": {
|
||||
"collapse_all": "Свернуть все",
|
||||
@@ -762,7 +768,20 @@
|
||||
"title": "Изменение объекта",
|
||||
"unchanged_if_empty": "Если без изменений - оставьте пустым",
|
||||
"username": "Имя пользователя",
|
||||
"validate_save": "Подтвердить и сохранить"
|
||||
"validate_save": "Подтвердить и сохранить",
|
||||
"internal": "Внутренний",
|
||||
"internal_info": "Внутренние псевдонимы доступны только из самого домена или доменов-псевдонимов.",
|
||||
"mta_sts": "MTA-STS",
|
||||
"mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> — это стандарт, который обязывает почтовые серверы использовать TLS с подлинными сертификатами для доставки электронной почты.<br>Он используется, когда <a target='_blank' href='https://ru.wikipedia.org/wiki/DANE'>DANE</a> невозможен из-за неиспользуемого или неподдерживаемого DNSSEC.<br><b>Примечание</b>: если принимающий домен поддерживает DANE с DNSSEC, <b>всегда</b> предпочитается DANE — MTA-STS действует только как резервный вариант.",
|
||||
"mta_sts_version": "Версия",
|
||||
"mta_sts_version_info": "Определяет версию стандарта MTA-STS – на данный момент существует только <code>STSv1</code>.",
|
||||
"mta_sts_mode": "Режим",
|
||||
"mta_sts_mode_info": "Есть три режима на выбор:<ul><li><em>testing</em> – политика только наблюдается, нарушения не имеют последствий.</li><li><em>enforce</em> – политика соблюдается строго, соединения без подлинного TLS отклоняются.</li><li><em>none</em> – политика опубликована, но не применяется.</li></ul>",
|
||||
"mta_sts_max_age": "Максимальный возраст",
|
||||
"mta_sts_max_age_info": "Время в секундах, в течение которого принимающие почтовые серверы могут кэшировать эту политику перед повторной загрузкой.",
|
||||
"mta_sts_mx": "Сервер MX",
|
||||
"mta_sts_mx_info": "Разрешает отправку только на явно указанные имена хостов почтовых серверов; отправляющий MTA проверяет, соответствует ли DNS-имя MX-хоста списку политик, и разрешает доставку только с подлинным TLS-сертификатом (защита от MITM).",
|
||||
"mta_sts_mx_notice": "Можно указать несколько MX-серверов (через запятую)."
|
||||
},
|
||||
"fido2": {
|
||||
"confirm": "Подтвердить",
|
||||
@@ -832,7 +851,8 @@
|
||||
"login_admintext": "Войти как администратор",
|
||||
"login_user": "Вход для пользователей",
|
||||
"login_dadmin": "Вход для администраторов домена",
|
||||
"login_admin": "Вход для администраторов"
|
||||
"login_admin": "Вход для администраторов",
|
||||
"email": "Email-адрес"
|
||||
},
|
||||
"mailbox": {
|
||||
"action": "Действия",
|
||||
@@ -1008,7 +1028,8 @@
|
||||
"waiting": "В ожидании",
|
||||
"weekly": "Раз в неделю",
|
||||
"yes": "✓",
|
||||
"iam": "Поставщик идентификационных данных"
|
||||
"iam": "Поставщик идентификационных данных",
|
||||
"internal": "Внутренний"
|
||||
},
|
||||
"oauth2": {
|
||||
"access_denied": "Пожалуйста, войдите в систему как владелец почтового аккаунта, чтобы получить доступ через OAuth2.",
|
||||
@@ -1331,7 +1352,7 @@
|
||||
"sogo_profile_reset": "Сбросить профиль SOGo",
|
||||
"sogo_profile_reset_help": "<b>Внимание:</b> это удалит настройки профиля SOGo вместе с <b>всеми контактами, календарями и фильтрами безвозвратно</b>.",
|
||||
"sogo_profile_reset_now": "Сбросить профиль сейчас",
|
||||
"spam_aliases": "Временные псевдонимы электронной почты",
|
||||
"spam_aliases": "Псевдонимы для спама",
|
||||
"spam_score_reset": "Сброс на настройки по умолчанию",
|
||||
"spamfilter": "Спам фильтр",
|
||||
"spamfilter_behavior": "Фильтрация спама",
|
||||
@@ -1387,7 +1408,10 @@
|
||||
"authentication": "Аутентификация",
|
||||
"tfa_info": "Двухфакторная аутентификация помогает защитить вашу учетную запись. Если вы включите эту функцию, вам понадобятся пароли приложений для входа в приложения или службы, которые не поддерживают двухфакторную аутентификацию (например, почтовые клиенты).",
|
||||
"protocols": "Протоколы",
|
||||
"overview": "Обзор"
|
||||
"overview": "Обзор",
|
||||
"expire_never": "Никогда не истекает",
|
||||
"forever": "Навсегда",
|
||||
"spam_aliases_info": "Псевдоним для спама — это временный адрес электронной почты, который можно использовать для защиты реальных адресов.<br>При желании можно установить срок действия, по истечении которого псевдоним будет автоматически деактивирован, что позволяет эффективно избавляться от адресов, которые были использованы не по назначению или стали доступны посторонним лицам."
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Вы не можете удалить сами себя",
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
"forwarding_hosts_add_hint": "Lahko vpišete IPv4/IPv6 naslove, mreže v CIDR obliki, imena gostiteljev (kateri se prevedejo v IP naslove) ali imena domen (katera se prevedejo v IP naslove glede na poizvedbo po SPF zapisih, v primeru manjkajočih zapisov pa MX zapisih).",
|
||||
"forwarding_hosts_hint": "Dohodna sporočila so brezpogojno sprejeta od katerih koli gostiteljev v tem seznamu. Ti gostitelji se ne bodo preverjali po DNSBL seznamih in ne bodo dodani v listo sivih. Prejeta neželena pošta s teh gostiteljev ni nikoli zavrnjena, opcijsko pa se lahko premakne v mapo neželene pošte. Najpogostejša uporaba za to je navedba poštnih strežnikov, iz katerih ste nastavili pravilo za posredovanje pošte na vaš mailcow strežnik.",
|
||||
"license_info": "Licenca ni zahtevana, a pomaga pri nadaljnjem razvoju. <br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"Naročilo SAL\">Registrirajte svoj GUID tukaj</a> ali <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Naročilo podpore\">Kupite podporo za svojo namestitev Mailcow.</a>",
|
||||
"lookup_mx": "Cilj je regularni izraz, ki se ujema z imenom MX (<code>.*\\.google\\.com</code> za usmerjanje vse pošte, usmerjene na MX, ki se konča na google.com, prek tega skoka).",
|
||||
"lookup_mx": "Cilj je regularni izraz, ki se ujema z imenom MX (<code>.*\\.google\\.com</code> za usmerjanje vse pošte, usmerjene na MX, ki se konča na google.com, prek tega skoka)",
|
||||
"main_name": "Naziv \"mailcow UI\"",
|
||||
"merged_vars_hint": "Sive vrstice so združene iz <code>vars.(local.)inc.php</code> in jih ni mogoče spremeniti.",
|
||||
"oauth2_info": "Implementacija OAuth2 podpira vrsto odobritve »Avtorizacijska koda« in izda osvežilne žetone.<br>\nStrežnik samodejno izda tudi nove osvežilne žetone, ko je žeton za osvežitev uporabljen.<br><br>\n• Privzeti obseg je <i>profile</i>. Prek OAuth2 je mogoče overiti samo uporabnike poštnega predala. Če parameter obsega izpustite, se vrne na <i>profile</i>.<br>\n• Parameter <i>state</i> mora odjemalec poslati kot del zahteve za avtorizacijo.<br><br>\nPoti za zahteve do API-ja OAuth2: <br>\n<ul>\n<li>Končna točka avtorizacije: <code>/oauth/authorize</code></li>\n<li>Končna točka žetona: <code>/oauth/token</code></li>\n<li>Stran z viri: <code>/oauth/profile</code></li>\n</ul>\nPonovno ustvarjanje skrivnosti odjemalca ne bo poteklo obstoječih kod za avtorizacijo, vendar ne bo obnovilo žetona.<br><br>\nPreklic žetonov odjemalca bo povzročil takojšnjo prekinitev vseh aktivnih sej. Vse stranke se morajo ponovno overiti.",
|
||||
@@ -414,7 +414,7 @@
|
||||
"needs_restart": "potreben je ponovni zagon"
|
||||
},
|
||||
"danger": {
|
||||
"alias_goto_identical": "Vzdevek in ciljni naslov se ne smeta ujemati.",
|
||||
"alias_goto_identical": "Vzdevek in ciljni naslov se ne smeta ujemati",
|
||||
"aliasd_targetd_identical": "Vzdevek domene ne sme biti enak ciljni domeni: %s",
|
||||
"bcc_exists": "Za tip %s obstaja BCC preslikava %s",
|
||||
"dkim_domain_or_sel_exists": "DKIM ključ za \"%s\" obstaja in ne bo prepisan",
|
||||
@@ -651,7 +651,7 @@
|
||||
"pushover_title": "Naslov obvestila",
|
||||
"domains": "Domene",
|
||||
"extended_sender_acl_info": "Uvoziti je treba ključ domene DKIM, če je na voljo.<br>\n Ne pozabite dodati tega strežnika v ustrezni zapis SPF TXT.<br>\n Kadar koli je temu strežniku dodana domena ali vzdevek domene, ki se prekriva z zunanjim naslovom, se zunanji naslov odstrani.<br>\n Uporabite @domain.tld, da omogočite pošiljanje kot *@domain.tld.",
|
||||
"lookup_mx": "Cilj je regularni izraz, ki se ujema z imenom MX (<code>.*\\.google\\.com</code> za usmerjanje vse pošte, usmerjene na MX, ki se konča na google.com, prek tega skoka).",
|
||||
"lookup_mx": "Cilj je regularni izraz, ki se ujema z imenom MX (<code>.*\\.google\\.com</code> za usmerjanje vse pošte, usmerjene na MX, ki se konča na google.com, prek tega skoka)",
|
||||
"maxbytespersecond": "Največ bajtov na sekundo <br><small>(0 = neomejeno)</small>",
|
||||
"pushover_sender_array": "Upoštevaj samo sledeče e-poštne naslove pošiljateljev <small>(ločeni z vejico)</small>",
|
||||
"mbox_rl_info": "Ta omejitev se uporabi za prijavno ime SASL in se ujema z naslovom \"od\", ki ga uporablja prijavljeni uporabnik. Omejitev poštnega nabiralnika preglasi omejitev za celotno domeno.",
|
||||
@@ -1359,7 +1359,7 @@
|
||||
"sogo_profile_reset": "Ponastavi profil SOGo",
|
||||
"sogo_profile_reset_help": "S tem boste uničili uporabnikov profil SOGo in <b>nepovratno izbrisali vse stike in podatke koledarja</b>.",
|
||||
"sogo_profile_reset_now": "Ponastavi profil zdaj",
|
||||
"spam_aliases": "Začasni vzdevki e-pošte",
|
||||
"spam_aliases": "Vzdevki neželene e-pošte",
|
||||
"spam_score_reset": "Ponastavi na privzete nastavitve strežnika",
|
||||
"spamfilter": "Filter neželene pošte",
|
||||
"spamfilter_behavior": "Ocena",
|
||||
@@ -1407,7 +1407,10 @@
|
||||
"years": "leta",
|
||||
"waiting": "Čakanje",
|
||||
"q_all": "Vse kategorije",
|
||||
"syncjob_EX_OK": "Uspeh"
|
||||
"syncjob_EX_OK": "Uspeh",
|
||||
"expire_never": "Nikoli ne poteče",
|
||||
"forever": "Za vedno",
|
||||
"spam_aliases_info": "Vzdevek za neželeno pošto je začasni e-poštni naslov, ki ga je mogoče uporabiti za zaščito pravih e-poštnih naslovov. <br>Po želji je mogoče nastaviti čas poteka veljavnosti, tako da se vzdevek po določenem obdobju samodejno deaktivira, s čimer se učinkovito znebite zlorabljenih ali razkritih naslovov."
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Prijavljenega uporabnika ni mogoče izbrisati",
|
||||
|
||||
@@ -582,13 +582,13 @@
|
||||
"username": "用户名",
|
||||
"container_disabled": "容器已被停止或禁用",
|
||||
"container_running": "运行中",
|
||||
"cores": "核心数",
|
||||
"cores": "核",
|
||||
"memory": "内存",
|
||||
"error_show_ip": "无法解析公网IP地址",
|
||||
"show_ip": "显示公网IP",
|
||||
"update_available": "有可用更新",
|
||||
"update_failed": "无法检查更新",
|
||||
"architecture": "结构",
|
||||
"architecture": "架构",
|
||||
"container_stopped": "已停止",
|
||||
"current_time": "系统时间",
|
||||
"timezone": "时区",
|
||||
@@ -1321,7 +1321,7 @@
|
||||
"sogo_profile_reset": "重置 SOGo 个人资料",
|
||||
"sogo_profile_reset_help": "此操作会不可恢复地删除用户的 SOGo 个人资料并<b>删除所有联系人和日历数据</b>。",
|
||||
"sogo_profile_reset_now": "立即重置个人资料",
|
||||
"spam_aliases": "临时邮箱别名",
|
||||
"spam_aliases": "垃圾邮件别名",
|
||||
"spam_score_reset": "重置为服务器默认值",
|
||||
"spamfilter": "垃圾邮件过滤器",
|
||||
"spamfilter_behavior": "分数",
|
||||
@@ -1381,7 +1381,10 @@
|
||||
"protocols": "协议",
|
||||
"authentication": "认证",
|
||||
"tfa_info": "两步验证有助于保护您的账户安全。启用后,对于不支持两步验证的应用程序或服务(例如邮件客户端),需要使用应用专用密码进行登录。",
|
||||
"overview": "概览"
|
||||
"overview": "概览",
|
||||
"expire_never": "永不过期",
|
||||
"forever": "永久",
|
||||
"spam_aliases_info": "垃圾邮件别名是一种临时电子邮件地址,可用于保护真实电子邮件地址。<br>还可以选择设置过期时间,以便在设定的时间后自动停用别名,从而有效地销毁被滥用或泄露的地址。"
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "不能删除已登录的用户",
|
||||
|
||||
@@ -34,15 +34,15 @@ catch(PDOException $e) {
|
||||
|
||||
if (isset($_GET['only_email'])) {
|
||||
$onlyEmailAccount = true;
|
||||
$description = 'IMAP';
|
||||
$description = 'IMAP';
|
||||
} else {
|
||||
$onlyEmailAccount = false;
|
||||
$description = 'IMAP, CalDAV, CardDAV';
|
||||
$description = 'IMAP, CalDAV, CardDAV';
|
||||
}
|
||||
if (isset($_GET['app_password'])) {
|
||||
$app_password = true;
|
||||
$description .= ' with application password';
|
||||
|
||||
|
||||
if (strpos($_SERVER['HTTP_USER_AGENT'], 'iPad') !== FALSE)
|
||||
$platform = 'iPad';
|
||||
elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== FALSE)
|
||||
@@ -51,8 +51,9 @@ if (isset($_GET['app_password'])) {
|
||||
$platform = 'Mac';
|
||||
else
|
||||
$platform = $_SERVER['HTTP_USER_AGENT'];
|
||||
|
||||
$password = bin2hex(openssl_random_pseudo_bytes(16));
|
||||
|
||||
$password = password_generate();
|
||||
|
||||
$attr = array(
|
||||
'app_name' => $platform,
|
||||
'app_passwd' => $password,
|
||||
|
||||
@@ -7,7 +7,30 @@ if (!isset($_SERVER['HTTP_HOST']) || strpos($_SERVER['HTTP_HOST'], 'mta-sts.') !
|
||||
}
|
||||
|
||||
$host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']);
|
||||
$domain = str_replace('mta-sts.', '', $host);
|
||||
$domain = idn_to_ascii(strtolower(str_replace('mta-sts.', '', $host)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
|
||||
// Validate domain or return 404 on error
|
||||
if ($domain === false || empty($domain)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if domain is an alias domain and resolve to target domain
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain");
|
||||
$stmt->execute(array(':domain' => $domain));
|
||||
$alias_row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($alias_row !== false && !empty($alias_row['target_domain'])) {
|
||||
// This is an alias domain, use the target domain for MTA-STS lookup
|
||||
$domain = $alias_row['target_domain'];
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
// On database error, return 404
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
|
||||
if (count($mta_sts) == 0 ||
|
||||
|
||||
@@ -12,18 +12,29 @@ $session_var_pass = 'sogo-sso-pass';
|
||||
if (isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
// load prerequisites only when required
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
|
||||
$username = $_SERVER['PHP_AUTH_USER'];
|
||||
$password = $_SERVER['PHP_AUTH_PW'];
|
||||
$is_eas = false;
|
||||
$is_dav = false;
|
||||
|
||||
// Determine service type for protocol access check
|
||||
$service = 'NONE';
|
||||
$original_uri = isset($_SERVER['HTTP_X_ORIGINAL_URI']) ? $_SERVER['HTTP_X_ORIGINAL_URI'] : '';
|
||||
if (preg_match('/^(\/SOGo|)\/dav.*/', $original_uri) === 1) {
|
||||
$is_dav = true;
|
||||
$service = 'DAV';
|
||||
}
|
||||
elseif (preg_match('/^(\/SOGo|)\/Microsoft-Server-ActiveSync.*/', $original_uri) === 1) {
|
||||
$is_eas = true;
|
||||
$service = 'EAS';
|
||||
}
|
||||
if (empty($password)) {
|
||||
$remote = get_remote_ip();
|
||||
$docker_ipv4_network = getenv('IPV4_NETWORK');
|
||||
if ($remote == "{$docker_ipv4_network}.246") {
|
||||
$login_check = 'user';
|
||||
$password = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
|
||||
}
|
||||
} else {
|
||||
$login_check = check_login($username, $password, array('service' => $service));
|
||||
}
|
||||
$login_check = check_login($username, $password, array('dav' => $is_dav, 'eas' => $is_eas));
|
||||
if ($login_check === 'user') {
|
||||
header("X-User: $username");
|
||||
header("X-Auth: Basic ".base64_encode("$username:$password"));
|
||||
@@ -57,7 +68,6 @@ elseif (isset($_GET['login'])) {
|
||||
$_SESSION['mailcow_cc_role'] = "user";
|
||||
}
|
||||
// update sasl logs
|
||||
$service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
|
||||
$stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES ('SSO', 0, :username, :remote_addr)");
|
||||
$stmt->execute(array(
|
||||
':username' => $login,
|
||||
@@ -79,6 +89,7 @@ elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HT
|
||||
if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/inc/vars.local.inc.php')) {
|
||||
include_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.local.inc.php';
|
||||
}
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
|
||||
|
||||
$email_list = array(
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
|
||||
<form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>
|
||||
|
||||
{% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri %}
|
||||
{% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri and mailcow_cc_username %}
|
||||
<div class="container mt-4">
|
||||
<div class="alert alert-{{ ui_texts.ui_announcement_type }}">{{ ui_texts.ui_announcement_text }}</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<form class="form-horizontal" data-id="editalias" role="form" method="post">
|
||||
<input type="hidden" value="0" name="active">
|
||||
<input type="hidden" value="0" name="internal">
|
||||
<input type="hidden" value="0" name="sender_allowed">
|
||||
{% if not skip_sogo %}
|
||||
<input type="hidden" value="0" name="sogo_visible">
|
||||
{% endif %}
|
||||
@@ -39,7 +40,11 @@
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="internal"{% if result.internal == '1' %} checked{% endif %}> {{ lang.edit.internal }}</label>
|
||||
</div>
|
||||
<small class="text-muted d-block">{{ lang.edit.internal_info }}</small>
|
||||
<small class="text-muted d-block mb-2">{{ lang.edit.internal_info }}</small>
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="sender_allowed"{% if result.sender_allowed == '1' %} checked{% endif %}> {{ lang.edit.sender_allowed }}</label>
|
||||
</div>
|
||||
<small class="text-muted d-block">{{ lang.edit.sender_allowed_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
@@ -108,6 +108,8 @@
|
||||
<option value="pop3"{% if template.attributes.pop3_access == '1' %} selected{% endif %}>POP3</option>
|
||||
<option value="smtp"{% if template.attributes.smtp_access == '1' %} selected{% endif %}>SMTP</option>
|
||||
<option value="sieve"{% if template.attributes.sieve_access == '1' %} selected{% endif %}>Sieve</option>
|
||||
<option value="eas"{% if template.attributes.eas_access == '1' %} selected{% endif %}>ActiveSync</option>
|
||||
<option value="dav"{% if template.attributes.dav_access == '1' %} selected{% endif %}>CalDAV/CardDAV</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,14 +85,6 @@
|
||||
{{ lang.edit.dont_check_sender_acl|format(domain) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% for alias in sender_acl_handles.sender_acl_addresses.ro %}
|
||||
<option data-subtext="Admin" disabled selected>
|
||||
{{ alias }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% for alias in sender_acl_handles.fixed_sender_aliases %}
|
||||
<option data-subtext="Alias" disabled selected>{{ alias }}</option>
|
||||
{% endfor %}
|
||||
{% for domain in sender_acl_handles.sender_acl_domains.rw %}
|
||||
<option value="{{ domain }}" selected>
|
||||
{{ lang.edit.dont_check_sender_acl|format(domain) }}
|
||||
@@ -104,11 +96,25 @@
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% for address in sender_acl_handles.sender_acl_addresses.rw %}
|
||||
<option selected>{{ address }}</option>
|
||||
{% if address in sender_acl_handles.fixed_sender_aliases_allowed or address in sender_acl_handles.fixed_sender_aliases_blocked %}
|
||||
<option data-subtext="Alias" selected>{{ address }}</option>
|
||||
{% else %}
|
||||
<option selected>{{ address }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for address in sender_acl_handles.sender_acl_addresses.selectable %}
|
||||
<option>{{ address }}</option>
|
||||
{% endfor %}
|
||||
{% for alias in sender_acl_handles.fixed_sender_aliases_allowed %}
|
||||
{% if alias not in sender_acl_handles.sender_acl_addresses.rw %}
|
||||
<option data-subtext="Alias (allowed)" value="{{ alias }}" selected>{{ alias }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for alias in sender_acl_handles.fixed_sender_aliases_blocked %}
|
||||
{% if alias not in sender_acl_handles.sender_acl_addresses.rw %}
|
||||
<option data-subtext="Alias (blocked)" value="{{ alias }}">{{ alias }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div id="sender_acl_disabled"><i class="bi bi-shield-exclamation"></i> {{ lang.edit.sender_acl_disabled|raw }}</div>
|
||||
<small class="text-muted d-block">{{ lang.edit.sender_acl_info|raw }}</small>
|
||||
@@ -281,6 +287,8 @@
|
||||
<option value="pop3"{% if result.attributes.pop3_access == '1' %} selected{% endif %}>POP3</option>
|
||||
<option value="smtp"{% if result.attributes.smtp_access == '1' %} selected{% endif %}>SMTP</option>
|
||||
<option value="sieve"{% if result.attributes.sieve_access == '1' %} selected{% endif %}>Sieve</option>
|
||||
<option value="eas"{% if result.attributes.eas_access == '1' %} selected{% endif %}>ActiveSync</option>
|
||||
<option value="dav"{% if result.attributes.dav_access == '1' %} selected{% endif %}>CalDAV/CardDAV</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -148,6 +148,8 @@
|
||||
<option value="pop3">POP3</option>
|
||||
<option value="smtp">SMTP</option>
|
||||
<option value="sieve">Sieve</option>
|
||||
<option value="eas">ActiveSync</option>
|
||||
<option value="dav">CalDAV/CardDAV</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,6 +337,8 @@
|
||||
<option value="pop3" selected>POP3</option>
|
||||
<option value="smtp" selected>SMTP</option>
|
||||
<option value="sieve" selected>Sieve</option>
|
||||
<option value="activesync" selected>ActiveSync</option>
|
||||
<option value="dav" selected>CalDAV/CardDAV</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -778,6 +782,7 @@
|
||||
<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias">
|
||||
<input type="hidden" value="0" name="active">
|
||||
<input type="hidden" value="0" name="internal">
|
||||
<input type="hidden" value="0" name="sender_allowed">
|
||||
<div class="row mb-2">
|
||||
<label class="control-label col-sm-2 text-sm-end" for="address">{{ lang.add.alias_address }}</label>
|
||||
<div class="col-sm-10">
|
||||
@@ -809,7 +814,11 @@
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="internal"> {{ lang.add.internal }}</label>
|
||||
</div>
|
||||
<small class="text-muted d-block">{{ lang.edit.internal_info }}</small>
|
||||
<small class="text-muted d-block mb-2">{{ lang.edit.internal_info }}</small>
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="sender_allowed" checked> {{ lang.add.sender_allowed }}</label>
|
||||
</div>
|
||||
<small class="text-muted d-block">{{ lang.edit.sender_allowed_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
{% if mailboxdata.attributes.smtp_access == 1 %}<div class="badge fs-6 bg-success m-2">SMTP <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">SMTP <i class="bi bi-x-lg"></i></div>{% endif %}
|
||||
{% if mailboxdata.attributes.sieve_access == 1 %}<div class="badge fs-6 bg-success m-2">Sieve <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">Sieve <i class="bi bi-x-lg"></i></div>{% endif %}
|
||||
{% if mailboxdata.attributes.pop3_access == 1 %}<div class="badge fs-6 bg-success m-2">POP3 <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">POP3 <i class="bi bi-x-lg"></i></div>{% endif %}
|
||||
{% if mailboxdata.attributes.eas_access == 1 %}<div class="badge fs-6 bg-success m-2">ActiveSync <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">ActiveSync <i class="bi bi-x-lg"></i></div>{% endif %}
|
||||
{% if mailboxdata.attributes.dav_access == 1 %}<div class="badge fs-6 bg-success m-2">CalDAV/CardDAV <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">CalDAV/CardDAV <i class="bi bi-x-lg"></i></div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user