mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-04-09 23:28:50 +00:00
Compare commits
1 Commits
feat/dovec
...
backup-tri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e8a0c24e7 |
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||
uses: actions/stale@v10.2.0
|
||||
uses: actions/stale@v10.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||
days-before-stale: 60
|
||||
|
||||
2
.github/workflows/image_builds.yml
vendored
2
.github/workflows/image_builds.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
- "watchdog-mailcow"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup Docker
|
||||
run: |
|
||||
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
||||
|
||||
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@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run the Action
|
||||
|
||||
6
.github/workflows/rebuild_backup_image.yml
vendored
6
.github/workflows/rebuild_backup_image.yml
vendored
@@ -13,17 +13,17 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
@@ -15,14 +15,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Generate postscreen_access.cidr
|
||||
run: |
|
||||
bash helper-scripts/update_postscreen_whitelist.sh
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||
commit-message: update postscreen_access.cidr
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,7 +51,6 @@ data/conf/sogo/cron.creds
|
||||
data/conf/sogo/custom-fulllogo.svg
|
||||
data/conf/sogo/custom-shortlogo.svg
|
||||
data/conf/sogo/custom-fulllogo.png
|
||||
data/conf/acme/dns-01.conf
|
||||
data/gitea/
|
||||
data/gogs/
|
||||
data/hooks/dovecot/*
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Contribution Guidelines
|
||||
**_Last modified on 12th November 2025_**
|
||||
**_Last modified on 15th August 2024_**
|
||||
|
||||
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
|
||||
|
||||
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
|
||||
|
||||
**PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULFILL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
||||
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
||||
|
||||
## Topics
|
||||
|
||||
@@ -27,18 +27,14 @@ However, please note the following regarding pull requests:
|
||||
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
|
||||
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
|
||||
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
|
||||
9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme.
|
||||
|
||||
---
|
||||
|
||||
## Issue Reporting
|
||||
**_Last modified on 12th November 2025_**
|
||||
**_Last modified on 15th August 2024_**
|
||||
|
||||
If you plan to report a issue within mailcow please read and understand the following rules:
|
||||
|
||||
### Security disclosures / Security-related fixes
|
||||
- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure.
|
||||
|
||||
### Issue Reporting Guidelines
|
||||
|
||||
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
|
||||
|
||||
@@ -38,45 +38,45 @@ get_docker_version(){
|
||||
}
|
||||
|
||||
get_compose_type(){
|
||||
if docker compose > /dev/null 2>&1; then
|
||||
if docker compose version --short | grep -e "^[2-9]\." -e "^v[2-9]\." -e "^[1-9][0-9]\." -e "^v[1-9][0-9]\." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=native
|
||||
COMPOSE_COMMAND="docker compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
if docker compose > /dev/null 2>&1; then
|
||||
if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=native
|
||||
COMPOSE_COMMAND="docker compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
elif docker-compose > /dev/null 2>&1; then
|
||||
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
|
||||
if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=standalone
|
||||
COMPOSE_COMMAND="docker-compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
elif docker-compose > /dev/null 2>&1; then
|
||||
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
|
||||
if docker-compose version --short | grep -e "^[2-9]\." -e "^[1-9][0-9]\." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=standalone
|
||||
COMPOSE_COMMAND="docker-compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
echo -e "\e[31mCannot find Docker Compose.\e[0m"
|
||||
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose.\e[0m"
|
||||
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detect_bad_asn() {
|
||||
|
||||
@@ -57,9 +57,6 @@ adapt_new_options() {
|
||||
"DISABLE_NETFILTER_ISOLATION_RULE"
|
||||
"HTTP_REDIRECT"
|
||||
"ENABLE_IPV6"
|
||||
"ACME_DNS_CHALLENGE"
|
||||
"ACME_DNS_PROVIDER"
|
||||
"ACME_ACCOUNT_EMAIL"
|
||||
)
|
||||
|
||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||
@@ -295,20 +292,6 @@ adapt_new_options() {
|
||||
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
|
||||
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
|
||||
;;
|
||||
ACME_DNS_CHALLENGE)
|
||||
echo '# Enable DNS-01 challenge for ACME (acme-mailcow) - y/n' >> mailcow.conf
|
||||
echo '# This requires you to set ACME_DNS_PROVIDER and ACME_ACCOUNT_EMAIL below' >> mailcow.conf
|
||||
echo 'ACME_DNS_CHALLENGE=n' >> mailcow.conf
|
||||
;;
|
||||
ACME_DNS_PROVIDER)
|
||||
echo '# DNS provider for DNS-01 challenge (e.g. dns_cf, dns_azure, dns_gd, etc.)' >> mailcow.conf
|
||||
echo '# See the dns-101 provider documentation for more information.' >> mailcow.conf
|
||||
echo 'ACME_DNS_PROVIDER=dns_xxx' >> mailcow.conf
|
||||
;;
|
||||
ACME_ACCOUNT_EMAIL)
|
||||
echo '# Account email for ACME DNS-01 challenge registration' >> mailcow.conf
|
||||
echo 'ACME_ACCOUNT_EMAIL=me@example.com' >> mailcow.conf
|
||||
;;
|
||||
*)
|
||||
echo "${option}=" >> mailcow.conf
|
||||
;;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
@@ -14,22 +14,11 @@ RUN apk upgrade --no-cache \
|
||||
tini \
|
||||
tzdata \
|
||||
python3 \
|
||||
acme-tiny \
|
||||
git \
|
||||
socat \
|
||||
&& git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \
|
||||
&& chmod +x /opt/acme.sh/acme.sh \
|
||||
&& mkdir -p /var/lib/acme/acme-sh
|
||||
|
||||
ENV ACME_SH_BIN=/opt/acme.sh/acme.sh \
|
||||
ACME_SH_HOME=/opt/acme.sh \
|
||||
ACME_SH_CONFIG_HOME=/var/lib/acme/acme-sh
|
||||
acme-tiny
|
||||
|
||||
COPY acme.sh /srv/acme.sh
|
||||
COPY functions.sh /srv/functions.sh
|
||||
COPY obtain-certificate.sh /srv/obtain-certificate.sh
|
||||
COPY obtain-certificate-dns.sh /srv/obtain-certificate-dns.sh
|
||||
COPY load-dns-config.sh /srv/load-dns-config.sh
|
||||
COPY reload-configurations.sh /srv/reload-configurations.sh
|
||||
COPY expand6.sh /srv/expand6.sh
|
||||
|
||||
|
||||
@@ -14,17 +14,6 @@ until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Create DNS-01 configuration template if it doesn't exist
|
||||
if [[ ! -f /etc/acme/dns-01.conf ]]; then
|
||||
mkdir -p /etc/acme
|
||||
cat > /etc/acme/dns-01.conf <<'EOF'
|
||||
# Add here your DNS-01 challenge configuration
|
||||
# For more information, visit the acme.sh documentation:
|
||||
# https://github.com/acmesh-official/acme.sh/wiki/dnsapi
|
||||
EOF
|
||||
echo "Created DNS-01 configuration template at /etc/acme/dns-01.conf"
|
||||
fi
|
||||
|
||||
source /srv/functions.sh
|
||||
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
|
||||
source /srv/expand6.sh
|
||||
@@ -53,10 +42,6 @@ if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
ENABLE_SSL_SNI=y
|
||||
fi
|
||||
|
||||
if [[ "${ACME_DNS_CHALLENGE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
ACME_DNS_CHALLENGE=y
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
|
||||
sleep 365d
|
||||
@@ -261,25 +246,6 @@ while true; do
|
||||
done
|
||||
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
|
||||
done
|
||||
|
||||
# Fetch alias domains where target domain has MTA-STS enabled
|
||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
|
||||
if [[ $? -eq 0 ]]; then
|
||||
while read alias_domain; do
|
||||
if [[ -z "${alias_domain}" ]]; then
|
||||
# ignore empty lines
|
||||
continue
|
||||
fi
|
||||
# Only add mta-sts subdomain for alias domains
|
||||
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
|
||||
if check_domain "mta-sts.${alias_domain}"; then
|
||||
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
|
||||
fi
|
||||
fi
|
||||
done <<< "${SQL_ALIAS_DOMAINS}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if check_domain ${MAILCOW_HOSTNAME}; then
|
||||
|
||||
@@ -80,11 +80,6 @@ check_domain(){
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${ACME_DNS_CHALLENGE} == "y" ]]; then
|
||||
log_f "ACME_DNS_CHALLENGE=y - skipping IP and HTTP validation for ${DOMAIN}"
|
||||
return 0
|
||||
fi
|
||||
# Check if CNAME without v6 enabled target
|
||||
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
|
||||
AAAA_DOMAIN=
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_SOURCE="${BASH_SOURCE[0]:-${0}}"
|
||||
if [[ "${SCRIPT_SOURCE}" == "${0}" ]]; then
|
||||
__dns_loader_standalone=1
|
||||
else
|
||||
__dns_loader_standalone=0
|
||||
fi
|
||||
|
||||
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-101.conf}"
|
||||
|
||||
if [[ ! -f "${CONFIG_PATH}" ]]; then
|
||||
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||
exit 0
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
source /srv/functions.sh
|
||||
|
||||
log_f "Loading DNS-01 configuration from ${CONFIG_PATH}"
|
||||
|
||||
LINE_NO=0
|
||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||||
LINE_NO=$((LINE_NO+1))
|
||||
line="${line%$'\r'}"
|
||||
line_trimmed="$(printf '%s' "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
[[ -z "${line_trimmed}" ]] && continue
|
||||
[[ "${line_trimmed:0:1}" == "#" ]] && continue
|
||||
if [[ "${line_trimmed}" != *=* ]]; then
|
||||
log_f "Skipping invalid DNS config line ${LINE_NO} (missing key=value)"
|
||||
continue
|
||||
fi
|
||||
KEY="${line_trimmed%%=*}"
|
||||
VALUE="${line_trimmed#*=}"
|
||||
KEY="$(printf '%s' "${KEY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
VALUE="$(printf '%s' "${VALUE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
if [[ -z "${KEY}" ]]; then
|
||||
log_f "Skipping invalid DNS config line ${LINE_NO} (empty key)"
|
||||
continue
|
||||
fi
|
||||
if [[ "${VALUE}" =~ ^\".*\"$ ]]; then
|
||||
VALUE="${VALUE:1:-1}"
|
||||
elif [[ "${VALUE}" =~ ^\'.*\'$ ]]; then
|
||||
VALUE="${VALUE:1:-1}"
|
||||
fi
|
||||
export "${KEY}"="${VALUE}"
|
||||
log_f "Exported DNS config key ${KEY}"
|
||||
|
||||
done < "${CONFIG_PATH}"
|
||||
|
||||
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||
exit 0
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
@@ -1,177 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Return values / exit codes
|
||||
# 0 = cert created successfully
|
||||
# 1 = cert renewed successfully
|
||||
# 2 = cert not due for renewal
|
||||
# * = errors
|
||||
|
||||
source /srv/functions.sh
|
||||
|
||||
CERT_DOMAINS=(${DOMAINS[@]})
|
||||
CERT_DOMAIN=${CERT_DOMAINS[0]}
|
||||
ACME_BASE=/var/lib/acme
|
||||
|
||||
# Load optional DNS provider secrets from /etc/acme/dns-101.conf
|
||||
if [[ -f /srv/load-dns-config.sh ]]; then
|
||||
source /srv/load-dns-config.sh
|
||||
if declare -F log_f >/dev/null; then
|
||||
log_f "ACME_DNS_CHALLENGE is enabled, DNS provider secrets loaded"
|
||||
fi
|
||||
fi
|
||||
|
||||
TYPE=${1}
|
||||
PREFIX=""
|
||||
# only support rsa certificates for now
|
||||
if [[ "${TYPE}" != "rsa" ]]; then
|
||||
log_f "Unknown certificate type '${TYPE}' requested"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
if [[ -z "${ACME_DNS_PROVIDER}" ]]; then
|
||||
log_f "ACME_DNS_PROVIDER is required when ACME_DNS_CHALLENGE is enabled"
|
||||
exit 6
|
||||
fi
|
||||
|
||||
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||
KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem
|
||||
CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr
|
||||
|
||||
if [[ -z ${CERT_DOMAINS[*]} ]]; then
|
||||
log_f "Missing CERT_DOMAINS to obtain a certificate"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
if [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||
log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL"
|
||||
fi
|
||||
log_f "Using Let's Encrypt staging servers"
|
||||
ACME_SH_SERVER_ARGS=("--staging")
|
||||
elif [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||
log_f "Using custom directory URL ${DIRECTORY_URL}"
|
||||
ACME_SH_SERVER_ARGS=("--server" "${DIRECTORY_URL}")
|
||||
else
|
||||
log_f "Using Let's Encrypt production servers"
|
||||
ACME_SH_SERVER_ARGS=("--server" "letsencrypt")
|
||||
fi
|
||||
|
||||
if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then
|
||||
if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then
|
||||
log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining"
|
||||
elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then
|
||||
log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing"
|
||||
else
|
||||
log_f "Certificate ${CERT} validation done, neither changed nor due for renewal."
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining"
|
||||
fi
|
||||
|
||||
# Make backup
|
||||
if [[ -f ${CERT} ]]; then
|
||||
DATE=$(date +%Y-%m-%d_%H_%M_%S)
|
||||
BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE}
|
||||
log_f "Creating backups in ${BACKUP_DIR} ..."
|
||||
mkdir -p ${BACKUP_DIR}/
|
||||
[[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/
|
||||
[[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/
|
||||
[[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/
|
||||
[[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/
|
||||
fi
|
||||
|
||||
mkdir -p ${ACME_BASE}/${CERT_DOMAIN}
|
||||
if [[ ! -f ${KEY} ]]; then
|
||||
log_f "Copying shared private key for this certificate..."
|
||||
cp ${SHARED_KEY} ${KEY}
|
||||
chmod 600 ${KEY}
|
||||
fi
|
||||
|
||||
# Generating CSR to keep layout parity with HTTP challenge flow
|
||||
printf "[SAN]\nsubjectAltName=" > /tmp/_SAN
|
||||
printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN
|
||||
sed -i '$s/,$//' /tmp/_SAN
|
||||
openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*\"\(.*\)\"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR}
|
||||
|
||||
log_f "Checking resolver..."
|
||||
until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
log_f "Resolver OK"
|
||||
|
||||
ACME_SH_BIN_PATH=${ACME_SH_BIN:-/opt/acme.sh/acme.sh}
|
||||
ACME_SH_WORK_HOME=${ACME_SH_CONFIG_HOME:-/var/lib/acme/acme-sh}
|
||||
mkdir -p ${ACME_SH_WORK_HOME}
|
||||
|
||||
if [[ ! -x ${ACME_SH_BIN_PATH} ]]; then
|
||||
log_f "acme.sh binary not found at ${ACME_SH_BIN_PATH}"
|
||||
exit 7
|
||||
fi
|
||||
|
||||
if [[ ! -f ${ACME_SH_WORK_HOME}/account.conf ]]; then
|
||||
if [[ -z "${ACME_ACCOUNT_EMAIL}" ]]; then
|
||||
log_f "ACME_ACCOUNT_EMAIL is required to register a new acme.sh account"
|
||||
exit 8
|
||||
fi
|
||||
log_f "Registering acme.sh account for ${ACME_ACCOUNT_EMAIL}"
|
||||
REGISTER_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}" "--register-account" "-m" "${ACME_ACCOUNT_EMAIL}")
|
||||
REGISTER_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||
REGISTER_RESPONSE=$("${REGISTER_CMD[@]}" 2>&1)
|
||||
if [[ $? -ne 0 ]]; then
|
||||
log_f "Failed to register acme.sh account: ${REGISTER_RESPONSE}"
|
||||
exit 9
|
||||
fi
|
||||
fi
|
||||
|
||||
TMP_CERT=$(mktemp /tmp/acme-cert.XXXXXX)
|
||||
TMP_FULLCHAIN=$(mktemp /tmp/acme-fullchain.XXXXXX)
|
||||
|
||||
ACME_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}")
|
||||
ACME_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||
ACME_CMD+=("--issue" "--dns" "${ACME_DNS_PROVIDER}" "--key-file" "${KEY}" "--cert-file" "${TMP_CERT}" "--fullchain-file" "${TMP_FULLCHAIN}" "--force")
|
||||
for domain in "${CERT_DOMAINS[@]}"; do
|
||||
ACME_CMD+=("-d" "${domain}")
|
||||
done
|
||||
|
||||
log_f "Using command ${ACME_CMD[*]}"
|
||||
if [[ -n "${ACME_DNS_PROVIDER}" ]]; then
|
||||
log_f "DNS provider: ${ACME_DNS_PROVIDER}"
|
||||
fi
|
||||
if compgen -A variable | grep -Eq "^DNS_|^ACME_"; then
|
||||
LOG_KEYS=$(env | grep -E "^(DNS_|ACME_)" | cut -d= -f1 | tr '\n' ' ')
|
||||
log_f "Available DNS/ACME env keys: ${LOG_KEYS}" redis_only
|
||||
fi
|
||||
ACME_RESPONSE=$("${ACME_CMD[@]}" 2>&1 | tee /dev/fd/5; exit ${PIPESTATUS[0]})
|
||||
SUCCESS="$?"
|
||||
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
|
||||
log_f "${ACME_RESPONSE_B64}" redis_only b64
|
||||
|
||||
case "$SUCCESS" in
|
||||
0)
|
||||
log_f "Deploying certificate ${CERT}..."
|
||||
if verify_hash_match ${TMP_FULLCHAIN} ${KEY}; then
|
||||
RETURN=0
|
||||
if [[ -f ${CERT} ]]; then
|
||||
RETURN=1
|
||||
fi
|
||||
mv -f ${TMP_FULLCHAIN} ${CERT}
|
||||
rm -f ${TMP_CERT}
|
||||
echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE}
|
||||
log_f "Certificate successfully obtained via DNS challenge"
|
||||
exit ${RETURN}
|
||||
else
|
||||
log_f "Certificate was requested, but key and certificate hashes do not match"
|
||||
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||
exit 4
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}' via DNS challenge"
|
||||
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
|
||||
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||
exit 100${SUCCESS}
|
||||
;;
|
||||
esac
|
||||
@@ -20,10 +20,6 @@ if [[ "${TYPE}" != "rsa" ]]; then
|
||||
log_f "Unknown certificate type '${TYPE}' requested"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
if [[ "${ACME_DNS_CHALLENGE}" == "y" ]]; then
|
||||
exec /srv/obtain-certificate-dns.sh "$@"
|
||||
fi
|
||||
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM debian:trixie-slim
|
||||
|
||||
RUN apt update && apt install pigz zstd -y --no-install-recommends
|
||||
RUN apt update && apt install pigz -y --no-install-recommends
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -110,12 +110,12 @@ async def get_container(container_id : str):
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.get("/containers/json")
|
||||
async def get_containers(all: bool = False):
|
||||
async def get_containers():
|
||||
global dockerapi
|
||||
|
||||
containers = {}
|
||||
try:
|
||||
for container in (await dockerapi.async_docker_client.containers.list(all=all)):
|
||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||
container_info = await container.show()
|
||||
containers.update({container_info['Id']: container_info})
|
||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.19
|
||||
ARG GOSU_VERSION=1.17
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
@@ -44,109 +44,90 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
else
|
||||
QUOTA_TABLE=quota2replica
|
||||
fi
|
||||
|
||||
cat <<EOF > /etc/dovecot/conf.d/12-mysql.conf
|
||||
# Autogenerated by mailcow - DO NOT TOUCH!
|
||||
mysql /var/run/mysqld/mysqld.sock {
|
||||
dbname=${DBNAME}
|
||||
user=${DBUSER}
|
||||
password=${DBPASS}
|
||||
|
||||
ssl = no
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-quota.conf
|
||||
# Autogenerated by mailcow
|
||||
dict_map priv/quota/storage {
|
||||
sql_table = ${QUOTA_TABLE}
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/quota/storage
|
||||
table = ${QUOTA_TABLE}
|
||||
username_field = username
|
||||
value_field bytes {
|
||||
}
|
||||
value_field = bytes
|
||||
}
|
||||
|
||||
dict_map priv/quota/messages {
|
||||
sql_table = ${QUOTA_TABLE}
|
||||
map {
|
||||
pattern = priv/quota/messages
|
||||
table = ${QUOTA_TABLE}
|
||||
username_field = username
|
||||
value_field messages {
|
||||
}
|
||||
value_field = messages
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create dict used for sieve pre and postfilters
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||
# Autogenerated by mailcow
|
||||
|
||||
dict_map priv/sieve/name/\$script_name {
|
||||
sql_table = sieve_before
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/sieve/name/\$script_name
|
||||
table = sieve_before
|
||||
username_field = username
|
||||
value_field id {
|
||||
}
|
||||
|
||||
# The script name field in the table to query
|
||||
key_field script_name {
|
||||
value = \$script_name
|
||||
value_field = id
|
||||
fields {
|
||||
script_name = \$script_name
|
||||
}
|
||||
}
|
||||
|
||||
dict_map priv/sieve/data/\$id {
|
||||
sql_table = sieve_before
|
||||
map {
|
||||
pattern = priv/sieve/data/\$id
|
||||
table = sieve_before
|
||||
username_field = username
|
||||
value_field script_data {
|
||||
}
|
||||
key_field id {
|
||||
value = \$id
|
||||
value_field = script_data
|
||||
fields {
|
||||
id = \$id
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
||||
# Autogenerated by mailcow
|
||||
|
||||
dict_map priv/sieve/name/\$script_name {
|
||||
sql_table = sieve_after
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/sieve/name/\$script_name
|
||||
table = sieve_after
|
||||
username_field = username
|
||||
value_field id {
|
||||
}
|
||||
key_field script_name {
|
||||
value = \$script_name
|
||||
value_field = id
|
||||
fields {
|
||||
script_name = \$script_name
|
||||
}
|
||||
}
|
||||
|
||||
dict_map priv/sieve/data/\$id {
|
||||
sql_table = sieve_after
|
||||
map {
|
||||
pattern = priv/sieve/data/\$id
|
||||
table = sieve_after
|
||||
username_field = username
|
||||
value_field script_data {
|
||||
}
|
||||
key_field id {
|
||||
value = \$id
|
||||
value_field = script_data
|
||||
fields {
|
||||
id = \$id
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
if [[ "${ACL_ANYONE}" == "allow" ]]; then
|
||||
echo -n "yes" > /etc/dovecot/acl_anyone
|
||||
else
|
||||
echo -n "no" > /etc/dovecot/acl_anyone
|
||||
fi
|
||||
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
|
||||
|
||||
if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo -e "\e[33mDetecting SKIP_FTS=y... not enabling Flatcurve (FTS) then...\e[0m"
|
||||
echo -n 'quota quota_clone acl mail_crypt mail_crypt_acl mail_log mail_compress notify lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota quota_clone imap_quota imap_acl acl imap_sieve mail_crypt mail_crypt_acl mail_compress notify mail_log' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota quota_clone sieve acl mail_crypt mail_crypt_acl mail_compress notify' > /etc/dovecot/mail_plugins_lmtp
|
||||
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
||||
else
|
||||
echo -e "\e[32mDetecting SKIP_FTS=n... enabling Flatcurve (FTS)\e[0m"
|
||||
echo -n 'quota quota_clone acl mail_crypt mail_crypt_acl mail_log mail_compress notify fts fts_flatcurve lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota quota_clone imap_quota imap_acl acl imap_sieve mail_crypt mail_crypt_acl mail_compress notify mail_log fts fts_flatcurve' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota quota_clone sieve acl mail_crypt mail_crypt_acl mail_compress fts fts_flatcurve notify' > /etc/dovecot/mail_plugins_lmtp
|
||||
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
||||
fi
|
||||
chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
|
||||
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||
# Autogenerated by mailcow
|
||||
query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%{user | domain }}/%{user | username }/Maildir:VOLATILEDIR=/var/volatile/%{user}:INDEX=/var/vmail_index/%{user}') AS mail, '%{protocol}' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%{user}' AND (active = '1' OR active = '2')
|
||||
driver = mysql
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2')
|
||||
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
|
||||
EOF
|
||||
|
||||
@@ -177,8 +158,8 @@ for cert_dir in /etc/ssl/mail/*/ ; do
|
||||
domains=($(cat ${cert_dir}domains))
|
||||
for domain in ${domains[@]}; do
|
||||
echo 'local_name '${domain}' {' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_server_cert_file = '${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_server_key_file = '${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_cert = <'${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_key = <'${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
|
||||
echo '}' >> /etc/dovecot/sni.conf;
|
||||
done
|
||||
done
|
||||
@@ -202,13 +183,11 @@ else
|
||||
fi
|
||||
cat <<EOF > /etc/dovecot/shared_namespace.conf
|
||||
# Autogenerated by mailcow
|
||||
namespace shared {
|
||||
namespace {
|
||||
type = shared
|
||||
separator = /
|
||||
prefix = Shared/\$user/
|
||||
mail_driver = maildir
|
||||
mail_path = %{owner_home}${MAILDIR_SUB_SHARED}
|
||||
mail_index_private_path = ~${MAILDIR_SUB_SHARED}/Shared/%{owner_user}
|
||||
prefix = Shared/%%u/
|
||||
location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u
|
||||
subscriptions = no
|
||||
list = children
|
||||
}
|
||||
@@ -218,28 +197,23 @@ EOF
|
||||
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
|
||||
# Autogenerated by mailcow
|
||||
remote ${IPV4_NETWORK}.248 {
|
||||
auth_allow_cleartext = yes
|
||||
disable_plaintext_auth = no
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create random master Password for SOGo SSO
|
||||
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
||||
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
||||
# Creating additional creds file for SOGo notify crons (calendars, etc)
|
||||
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
||||
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
||||
# Autogenerated by mailcow
|
||||
passdb static {
|
||||
fields {
|
||||
allow_real_nets=${IPV4_NETWORK}.248/32
|
||||
}
|
||||
|
||||
password={plain}${RAND_PASS}
|
||||
|
||||
passdb {
|
||||
driver = static
|
||||
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password)
|
||||
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
||||
|
||||
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
|
||||
cat <<'EOF' > /usr/local/bin/quota_notify.py
|
||||
@@ -261,9 +235,9 @@ fi
|
||||
if [[ "${SKIP_FTS}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||
echo -e "\e[94mConfiguring FTS Settings...\e[0m"
|
||||
echo -e "\e[94mSetting FTS Memory Limit (per process) to ${FTS_HEAP} MB\e[0m"
|
||||
sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/35-fts.conf
|
||||
sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/fts.conf
|
||||
echo -e "\e[94mSetting FTS Process Limit to ${FTS_PROCS}\e[0m"
|
||||
sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/35-fts.conf
|
||||
sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/fts.conf
|
||||
fi
|
||||
|
||||
# 401 is user dovecot
|
||||
@@ -275,16 +249,16 @@ else
|
||||
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
|
||||
fi
|
||||
|
||||
# # Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
|
||||
# if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
|
||||
# sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
|
||||
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
|
||||
if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
|
||||
sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
|
||||
|
||||
# echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
|
||||
# echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
|
||||
# echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
|
||||
# echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
|
||||
# echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
|
||||
# fi
|
||||
echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
|
||||
echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
|
||||
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
|
||||
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
|
||||
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
|
||||
fi
|
||||
|
||||
# Compile sieve scripts
|
||||
sievec /var/vmail/sieve/global_sieve_before.sieve
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
@@ -40,4 +40,4 @@ COPY ./docker-entrypoint.sh /app/
|
||||
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
||||
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
||||
@@ -3,17 +3,17 @@ FROM php:8.2-fpm-alpine3.21
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG APCU_PECL_VERSION=5.1.28
|
||||
ARG APCU_PECL_VERSION=5.1.27
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG IMAGICK_PECL_VERSION=3.8.1
|
||||
ARG IMAGICK_PECL_VERSION=3.8.0
|
||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MAILPARSE_PECL_VERSION=3.1.9
|
||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MEMCACHED_PECL_VERSION=3.4.0
|
||||
ARG MEMCACHED_PECL_VERSION=3.3.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG REDIS_PECL_VERSION=6.3.0
|
||||
ARG REDIS_PECL_VERSION=6.2.0
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG COMPOSER_VERSION=2.9.5
|
||||
ARG COMPOSER_VERSION=2.8.6
|
||||
|
||||
RUN apk add -U --no-cache autoconf \
|
||||
aspell-dev \
|
||||
|
||||
@@ -167,7 +167,7 @@ DELIMITER //
|
||||
CREATE EVENT clean_spamalias
|
||||
ON SCHEDULE EVERY 1 DAY DO
|
||||
BEGIN
|
||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
|
||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
|
||||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
|
||||
@@ -4,7 +4,7 @@ WORKDIR /src
|
||||
ENV CGO_ENABLED=0 \
|
||||
GO111MODULE=on \
|
||||
NOOPT=1 \
|
||||
VERSION=1.8.22
|
||||
VERSION=1.8.14
|
||||
|
||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||
cd /src/postfix-tlspol && \
|
||||
|
||||
@@ -329,17 +329,14 @@ query = SELECT goto FROM alias
|
||||
SELECT id FROM alias
|
||||
WHERE address='%s'
|
||||
AND (active='1' OR active='2')
|
||||
AND sender_allowed='1'
|
||||
), (
|
||||
SELECT id FROM alias
|
||||
WHERE address='@%d'
|
||||
AND (active='1' OR active='2')
|
||||
AND sender_allowed='1'
|
||||
)
|
||||
)
|
||||
)
|
||||
AND active='1'
|
||||
AND sender_allowed='1'
|
||||
AND (domain IN
|
||||
(SELECT domain FROM domain
|
||||
WHERE domain='%d'
|
||||
@@ -393,7 +390,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT goto FROM spamalias
|
||||
WHERE address='%s'
|
||||
AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
|
||||
AND validity >= UNIX_TIMESTAMP()
|
||||
EOF
|
||||
|
||||
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||
@@ -527,4 +524,4 @@ if [[ $? != 0 ]]; then
|
||||
else
|
||||
postfix -c /opt/postfix/conf start
|
||||
sleep 126144000
|
||||
fi
|
||||
fi
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM debian:trixie-slim
|
||||
FROM debian:bookworm-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG RSPAMD_VER=rspamd_3.14.2-82~90302bc
|
||||
ARG CODENAME=trixie
|
||||
ARG RSPAMD_VER=rspamd_3.13.2-1~8bf602278
|
||||
ARG CODENAME=bookworm
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -1,161 +1,47 @@
|
||||
# SOGo built from source to enable security patch application
|
||||
# Repository: https://github.com/Alinto/sogo
|
||||
# Version: SOGo-5.12.4
|
||||
#
|
||||
# Applied security patches:
|
||||
# - 16ab99e7cf8db2c30b211f0d5e338d7f9e3a9efb: XSS vulnerability in theme parameter
|
||||
#
|
||||
# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes
|
||||
|
||||
FROM debian:bookworm
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG SOGO_VERSION=SOGo-5.12.4
|
||||
ARG SOPE_VERSION=SOPE-5.12.4
|
||||
# Security patches to apply (space-separated commit hashes)
|
||||
ARG SOGO_SECURITY_PATCHES="16ab99e7cf8db2c30b211f0d5e338d7f9e3a9efb"
|
||||
ARG DEBIAN_VERSION=bookworm
|
||||
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.19
|
||||
ARG GOSU_VERSION=1.17
|
||||
ENV LC_ALL=C
|
||||
|
||||
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Build dependencies
|
||||
git \
|
||||
build-essential \
|
||||
gobjc \
|
||||
gnustep-make \
|
||||
gnustep-base-runtime \
|
||||
libgnustep-base-dev \
|
||||
libxml2-dev \
|
||||
libldap2-dev \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libpq-dev \
|
||||
libmariadb-dev-compat \
|
||||
libmemcached-dev \
|
||||
libsodium-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libzip-dev \
|
||||
libytnef0-dev \
|
||||
curl \
|
||||
ca-certificates \
|
||||
# Runtime dependencies
|
||||
apt-transport-https \
|
||||
gettext \
|
||||
gnupg \
|
||||
mariadb-client \
|
||||
rsync \
|
||||
supervisor \
|
||||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
dirmngr \
|
||||
netcat-traditional \
|
||||
psmisc \
|
||||
wget \
|
||||
patch \
|
||||
libobjc4 \
|
||||
libxml2 \
|
||||
libldap-2.5-0 \
|
||||
libssl3 \
|
||||
zlib1g \
|
||||
libmariadb3 \
|
||||
libmemcached11 \
|
||||
libsodium23 \
|
||||
libcurl4 \
|
||||
libzip4 \
|
||||
libytnef0 \
|
||||
# Download gosu
|
||||
# Prerequisites
|
||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
gettext \
|
||||
gnupg \
|
||||
mariadb-client \
|
||||
rsync \
|
||||
supervisor \
|
||||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
dirmngr \
|
||||
netcat-traditional \
|
||||
psmisc \
|
||||
wget \
|
||||
patch \
|
||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||
&& chmod +x /usr/local/bin/gosu \
|
||||
&& gosu nobody true \
|
||||
# Build SOPE
|
||||
&& git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \
|
||||
&& cd /tmp/sope \
|
||||
&& rm -rf .git \
|
||||
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||
&& ./configure --prefix=/usr --disable-debug --disable-strip \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& cd / \
|
||||
&& rm -rf /tmp/sope \
|
||||
# Build SOGo with security patches
|
||||
&& git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \
|
||||
&& cd /tmp/sogo \
|
||||
&& git config user.email "builder@mailcow.local" \
|
||||
&& git config user.name "SOGo Builder" \
|
||||
&& for patch in ${SOGO_SECURITY_PATCHES}; do \
|
||||
echo "Applying security patch: ${patch}"; \
|
||||
git fetch origin ${patch} && git cherry-pick ${patch}; \
|
||||
done \
|
||||
&& rm -rf .git \
|
||||
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||
&& ./configure --disable-debug --disable-strip \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& cd / \
|
||||
&& rm -rf /tmp/sogo \
|
||||
# Strip binaries
|
||||
&& strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \
|
||||
&& strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \
|
||||
&& strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \
|
||||
&& strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \
|
||||
# Remove build dependencies and clean up
|
||||
&& apt-get purge -y --auto-remove \
|
||||
git \
|
||||
build-essential \
|
||||
gobjc \
|
||||
gnustep-make \
|
||||
libgnustep-base-dev \
|
||||
libxml2-dev \
|
||||
libldap2-dev \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libpq-dev \
|
||||
libmariadb-dev-compat \
|
||||
libmemcached-dev \
|
||||
libsodium-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libzip-dev \
|
||||
libytnef0-dev \
|
||||
curl \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /usr/share/doc/* \
|
||||
&& rm -rf /usr/share/man/* \
|
||||
&& rm -rf /var/cache/debconf/* \
|
||||
&& rm -rf /tmp/* \
|
||||
&& rm -rf /root/.cache \
|
||||
&& find /usr/local/lib -name '*.a' -delete \
|
||||
&& find /usr/lib -name '*.a' -delete \
|
||||
&& mkdir -p /usr/share/doc/sogo \
|
||||
&& mkdir /usr/share/doc/sogo \
|
||||
&& touch /usr/share/doc/sogo/empty.sh \
|
||||
&& wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \
|
||||
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
sogo \
|
||||
sogo-activesync \
|
||||
&& apt-get autoclean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& touch /etc/default/locale
|
||||
|
||||
# Configure library paths
|
||||
RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
|
||||
&& echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||
&& echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||
&& ldconfig
|
||||
|
||||
# Create sogo user and group
|
||||
RUN groupadd -r -g 999 sogo \
|
||||
&& useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \
|
||||
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo \
|
||||
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo
|
||||
|
||||
# Create symlinks for SOGo binaries
|
||||
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
|
||||
&& ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \
|
||||
&& ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \
|
||||
&& ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd
|
||||
|
||||
# Copy configuration files and scripts
|
||||
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||
@@ -170,4 +56,4 @@ RUN chmod +x /bootstrap-sogo.sh \
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
@@ -50,6 +50,10 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
<string>YES</string>
|
||||
<key>SOGoEncryptionKey</key>
|
||||
<string>${RAND_PASS}</string>
|
||||
<key>SOGoURLEncryptionEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>SOGoURLEncryptionPassphrase</key>
|
||||
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
|
||||
<key>OCSAdminURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
||||
<key>OCSCacheFolderURL</key>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -19,19 +19,19 @@ if [ -z "$HOST" ]; then
|
||||
fi
|
||||
|
||||
# run dig and measure the time it takes to run
|
||||
START_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
|
||||
START_TIME=$(date +%s%3N)
|
||||
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
|
||||
dig_rc=$?
|
||||
END_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
|
||||
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
|
||||
ELAPSED_TIME=$(perl -e "printf('%.3f', $END_TIME - $START_TIME)")
|
||||
END_TIME=$(date +%s%3N)
|
||||
ELAPSED_TIME=$((END_TIME - START_TIME))
|
||||
|
||||
# validate and perform nagios like output and exit codes
|
||||
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
||||
echo "Domain $HOST was not found by the server"
|
||||
exit 2
|
||||
elif [ $dig_rc -eq 0 ]; then
|
||||
echo "DNS OK: $ELAPSED_TIME seconds response time. $HOST returns $dig_output_ips"
|
||||
echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips"
|
||||
exit 0
|
||||
else
|
||||
echo "Unknown error"
|
||||
|
||||
@@ -80,21 +80,14 @@ if ($isSOGoRequest) {
|
||||
}
|
||||
if ($result === false){
|
||||
// If it's a SOGo Request, don't check for protocol access
|
||||
if ($isSOGoRequest) {
|
||||
$service = 'SOGO';
|
||||
$post['service'] = 'NONE';
|
||||
} else {
|
||||
$service = $post['service'];
|
||||
}
|
||||
|
||||
$result = apppass_login($post['username'], $post['password'], array(
|
||||
'service' => $post['service'],
|
||||
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
|
||||
$result = apppass_login($post['username'], $post['password'], $service, array(
|
||||
'is_internal' => true,
|
||||
'remote_addr' => $post['real_rip']
|
||||
));
|
||||
if ($result) {
|
||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $service);
|
||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
||||
}
|
||||
}
|
||||
if ($result === false){
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
function auth_password_verify(request, password)
|
||||
request.domain = request.auth_user:match("@(.+)") or nil
|
||||
if request.domain == nil then
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||
end
|
||||
@@ -10,10 +9,10 @@ function auth_password_verify(request, password)
|
||||
https.TIMEOUT = 30
|
||||
|
||||
local req = {
|
||||
username = request.auth_user,
|
||||
username = request.user,
|
||||
password = password,
|
||||
real_rip = request.remote_ip,
|
||||
service = request.protocol
|
||||
real_rip = request.real_rip,
|
||||
service = request.service
|
||||
}
|
||||
local req_json = json.encode(req)
|
||||
local res = {}
|
||||
@@ -34,6 +33,7 @@ function auth_password_verify(request, password)
|
||||
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
||||
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
||||
if c ~= 200 and c ~= 401 then
|
||||
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
||||
end
|
||||
|
||||
@@ -46,7 +46,7 @@ function auth_password_verify(request, password)
|
||||
end
|
||||
|
||||
if response_json.success == true then
|
||||
return dovecot.auth.PASSDB_RESULT_OK, { msg = "" }
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
|
||||
@@ -55,7 +55,3 @@ end
|
||||
function auth_passdb_lookup(req)
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
|
||||
end
|
||||
|
||||
function auth_passdb_get_cache_key()
|
||||
return "%{protocol}:%{user | username}\t:%{password}"
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
# /etc/dovecot/conf.d/05-core.conf
|
||||
# Core, single-line settings that don't fit elsewhere.
|
||||
recipient_delimiter = +
|
||||
@@ -1,13 +0,0 @@
|
||||
# /etc/dovecot/conf.d/10-logging.conf
|
||||
# Logging and debug.
|
||||
#mail_debug = yes
|
||||
#auth_debug = yes
|
||||
#log_debug = category=fts-flatcurve
|
||||
log_path = syslog
|
||||
log_timestamp = "%Y-%m-%d %H:%M:%S "
|
||||
login_log_format_elements = "user=<%{user}> method=%{mechanism} rip=%{remote_ip} lip=%{local_ip} mpid=%{mail_pid} %{secured} session=<%{session}>"
|
||||
|
||||
# Mail event logging.
|
||||
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
||||
mail_log_fields = uid box msgid size
|
||||
mail_log_cached_only = yes
|
||||
@@ -1,10 +0,0 @@
|
||||
# /etc/dovecot/conf.d/10-mail.conf
|
||||
# Mail storage paths and core mail settings.
|
||||
mail_home = /var/vmail/%{user | domain }/%{user | username }
|
||||
mail_driver = maildir
|
||||
mail_path = ~/Maildir
|
||||
mail_index_path = /var/vmail_index/%{user}
|
||||
mail_plugins = </etc/dovecot/mail_plugins
|
||||
mail_shared_explicit_inbox = yes
|
||||
mailbox_list_storage_escape_char = "\\"
|
||||
mail_prefetch_count = 30
|
||||
@@ -1,13 +0,0 @@
|
||||
# /etc/dovecot/conf.d/10-ssl.conf
|
||||
# TLS/SSL settings.
|
||||
ssl_min_protocol = TLSv1.2
|
||||
ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
|
||||
ssl_options = no_ticket
|
||||
#ssl_dh_parameters_length = 2048
|
||||
|
||||
ssl_server {
|
||||
prefer_ciphers = server
|
||||
dh_file = /etc/ssl/mail/dhparams.pem
|
||||
cert_file = /etc/ssl/mail/cert.pem
|
||||
key_file = /etc/ssl/mail/key.pem
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# /etc/dovecot/conf.d/11-sql.conf
|
||||
# Default SQL driver used by SQL-based dicts/userdb.
|
||||
sql_driver = mysql
|
||||
@@ -1,8 +0,0 @@
|
||||
# Autogenerated by mailcow - DO NOT TOUCH!
|
||||
mysql /var/run/mysqld/mysqld.sock {
|
||||
dbname=mailcow
|
||||
user=mailcow
|
||||
password=D8O9BIivJc7Pb2VCfpAeLbAzUOZ0
|
||||
|
||||
ssl = no
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# /etc/dovecot/conf.d/12-storage-attachments.conf
|
||||
# External attachment storage.
|
||||
fs mail_ext_attachment {
|
||||
fs_driver = posix
|
||||
mail_ext_attachment_path = /var/attachments
|
||||
mail_ext_attachment_min_size = 128k
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# /etc/dovecot/conf.d/15-performance.conf
|
||||
# Performance and mailbox tuning.
|
||||
# Enable only when you do not manually touch cur/.
|
||||
maildir_very_dirty_syncs = yes
|
||||
|
||||
# NFS examples | Only modify if using NFS!:
|
||||
#mm ap_disable = yes
|
||||
#mail_fsync = always
|
||||
#mail_nfs_index = yes
|
||||
#mail_nfs_storage = yes
|
||||
@@ -1,40 +0,0 @@
|
||||
# /etc/dovecot/conf.d/20-auth.conf
|
||||
# Authentication mechanisms, master/user separation, passdb chain, auth cache.
|
||||
auth_mechanisms = plain login
|
||||
auth_allow_cleartext = yes
|
||||
auth_master_user_separator = *
|
||||
|
||||
auth_cache_verify_password_with_worker = yes
|
||||
auth_cache_negative_ttl = 60s
|
||||
auth_cache_ttl = 300s
|
||||
auth_cache_size = 10M
|
||||
auth_verbose_passwords = sha1:6
|
||||
|
||||
# 1) Lua password verification (blocking, return mapping).
|
||||
passdb lua {
|
||||
driver = lua
|
||||
lua_file = /etc/dovecot/auth/passwd-verify.lua
|
||||
lua_settings {
|
||||
blocking=yes
|
||||
result_success = return-ok
|
||||
result_failure = continue
|
||||
result_internalfail = continue
|
||||
}
|
||||
}
|
||||
|
||||
# 2) Master password for master user logins.
|
||||
passdb master {
|
||||
driver = passwd-file
|
||||
passwd_file_path = /etc/dovecot/dovecot-master.passwd
|
||||
master = yes
|
||||
skip = authenticated
|
||||
}
|
||||
|
||||
# 3) Mandatory return layer: empty Lua (e.g. for forced reset).
|
||||
passdb empty-lua {
|
||||
driver = lua
|
||||
lua_file = /etc/dovecot/auth/passwd-verify.lua
|
||||
lua_settings {
|
||||
blocking = yes
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# /etc/dovecot/conf.d/20-userdb.conf
|
||||
# User database chain.
|
||||
userdb passwd {
|
||||
driver = passwd-file
|
||||
passwd_file_path = /etc/dovecot/dovecot-master.userdb
|
||||
}
|
||||
|
||||
userdb sql {
|
||||
!include /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||
skip = found
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
# /etc/dovecot/conf.d/25-services.conf
|
||||
# All service listeners and workers.
|
||||
|
||||
# doveadm remote admin
|
||||
# Set doveadm_password in extra.conf.
|
||||
service doveadm {
|
||||
inet_listener doveadm {
|
||||
port = 12345
|
||||
}
|
||||
vsz_limit = 2048 MB
|
||||
}
|
||||
|
||||
# dict
|
||||
service dict {
|
||||
unix_listener dict {
|
||||
mode = 0660
|
||||
user = vmail
|
||||
group = vmail
|
||||
}
|
||||
}
|
||||
|
||||
# log
|
||||
service log {
|
||||
user = dovenull
|
||||
}
|
||||
|
||||
# config socket
|
||||
service config {
|
||||
unix_listener config {
|
||||
user = root
|
||||
group = vmail
|
||||
mode = 0660
|
||||
}
|
||||
}
|
||||
|
||||
# anvil socket
|
||||
service anvil {
|
||||
unix_listener anvil {
|
||||
user = vmail
|
||||
group = vmail
|
||||
mode = 0660
|
||||
}
|
||||
}
|
||||
|
||||
# auth sockets and inet
|
||||
service auth {
|
||||
inet_listener auth-inet {
|
||||
port = 10001
|
||||
}
|
||||
unix_listener auth-master {
|
||||
mode = 0600
|
||||
user = vmail
|
||||
}
|
||||
unix_listener auth-userdb {
|
||||
mode = 0600
|
||||
user = vmail
|
||||
}
|
||||
vsz_limit = 2G
|
||||
}
|
||||
|
||||
# managesieve login
|
||||
service managesieve-login {
|
||||
inet_listener sieve {
|
||||
port = 4190
|
||||
}
|
||||
inet_listener sieve_haproxy {
|
||||
port = 14190
|
||||
haproxy = yes
|
||||
}
|
||||
service_restart_request_count = 1
|
||||
process_min_avail = 2
|
||||
vsz_limit = 1G
|
||||
}
|
||||
|
||||
# imap login
|
||||
service imap-login {
|
||||
service_restart_request_count = 1
|
||||
process_min_avail = 2
|
||||
process_limit = 10000
|
||||
vsz_limit = 1G
|
||||
user = dovenull
|
||||
inet_listener imap_haproxy {
|
||||
port = 10143
|
||||
haproxy = yes
|
||||
}
|
||||
inet_listener imaps_haproxy {
|
||||
port = 10993
|
||||
ssl = yes
|
||||
haproxy = yes
|
||||
}
|
||||
}
|
||||
|
||||
# pop3 login
|
||||
service pop3-login {
|
||||
service_restart_request_count = 1
|
||||
process_min_avail = 1
|
||||
vsz_limit = 1G
|
||||
inet_listener pop3_haproxy {
|
||||
port = 10110
|
||||
haproxy = yes
|
||||
}
|
||||
inet_listener pop3s_haproxy {
|
||||
port = 10995
|
||||
ssl = yes
|
||||
haproxy = yes
|
||||
}
|
||||
}
|
||||
|
||||
# imap worker
|
||||
service imap {
|
||||
executable = imap
|
||||
user = vmail
|
||||
vsz_limit = 1G
|
||||
}
|
||||
|
||||
# managesieve worker
|
||||
service managesieve {
|
||||
process_limit = 256
|
||||
}
|
||||
|
||||
# lmtp
|
||||
service lmtp {
|
||||
inet_listener lmtp-inet {
|
||||
port = 24
|
||||
}
|
||||
user = vmail
|
||||
}
|
||||
|
||||
# quota warning hook
|
||||
service quota-warning {
|
||||
executable = script /usr/local/bin/quota_notify.py
|
||||
user = vmail
|
||||
unix_listener quota-warning {
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
|
||||
# stats
|
||||
service stats {
|
||||
unix_listener stats-writer {
|
||||
mode = 0660
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# /etc/dovecot/conf.d/30-protocols.conf
|
||||
# IMAP protocol specifics.
|
||||
protocol imap {
|
||||
mail_plugins = </etc/dovecot/mail_plugins_imap
|
||||
imap_metadata = yes
|
||||
}
|
||||
|
||||
# LMTP protocol specifics.
|
||||
protocol lmtp {
|
||||
mail_plugins = </etc/dovecot/mail_plugins_lmtp
|
||||
auth_socket_path = /var/run/dovecot/auth-master
|
||||
}
|
||||
|
||||
# ManageSieve protocol specifics.
|
||||
protocol sieve {
|
||||
managesieve_logout_format = bytes=%i/%o
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
# mailcow FTS Flatcurve Settings, change them as you like.
|
||||
|
||||
# Maximum term length can be set via the 'maxlen' argument (maxlen is
|
||||
# specified in bytes, not number of UTF-8 characters)
|
||||
language_tokenizer_address_token_maxlen = 100
|
||||
language_tokenizer_generic_algorithm = simple
|
||||
language_tokenizer_generic_token_maxlen = 30
|
||||
|
||||
# These are not flatcurve settings, but required for Dovecot FTS. See
|
||||
# Dovecot FTS Configuration link above for further information.
|
||||
language en {
|
||||
default = yes
|
||||
language_filters = lowercase snowball english-possessive stopwords
|
||||
}
|
||||
|
||||
language de {
|
||||
language_filters = lowercase snowball stopwords
|
||||
}
|
||||
|
||||
language es {
|
||||
language_filters = lowercase snowball stopwords
|
||||
}
|
||||
|
||||
language_tokenizers = generic email-address
|
||||
|
||||
fts_search_timeout = 300s
|
||||
|
||||
fts_autoindex = yes
|
||||
# Tweak this setting if you only want to ensure big and frequent folders are indexed, not all.
|
||||
fts_autoindex_max_recent_msgs = 20
|
||||
fts flatcurve {
|
||||
substring_search = no
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
|
||||
service indexer-worker {
|
||||
# Max amount of simultaniously running indexer jobs.
|
||||
process_limit=1
|
||||
|
||||
# Max amount of RAM used by EACH indexer process.
|
||||
vsz_limit=128 MB
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
@@ -1,12 +0,0 @@
|
||||
# /etc/dovecot/conf.d/40-acl.conf
|
||||
# ACL and shared mailboxes.
|
||||
imap_acl_allow_anyone = </etc/dovecot/acl_anyone
|
||||
|
||||
acl_sharing_map {
|
||||
dict file {
|
||||
path = /var/vmail/shared-mailboxes.db
|
||||
}
|
||||
}
|
||||
|
||||
acl_driver = vfile
|
||||
acl_user = %{user}
|
||||
@@ -1,7 +0,0 @@
|
||||
# /etc/dovecot/conf.d/40-attributes.conf
|
||||
# User/mail attributes.
|
||||
mail_attribute {
|
||||
dict file {
|
||||
path = /etc/dovecot/dovecot-attributes
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# /etc/dovecot/conf.d/50-quota.conf
|
||||
# Quota configuration and notifications.
|
||||
quota "User quota" {
|
||||
driver = count
|
||||
|
||||
warning warn-95 {
|
||||
quota_storage_percentage = 95
|
||||
execute quota-warning {
|
||||
args = 95 %{user}
|
||||
}
|
||||
}
|
||||
|
||||
warning warn-80 {
|
||||
quota_storage_percentage = 80
|
||||
execute quota-warning {
|
||||
args = 80 %{user}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quota_clone {
|
||||
dict proxy {
|
||||
name = mysql_quota
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
# /etc/dovecot/conf.d/60-sieve-pipeline.conf
|
||||
# Complete Sieve pipeline: personal/global scripts, plugins, limits, training.
|
||||
|
||||
# Global before/after (file and dict)
|
||||
sieve_script before {
|
||||
type = before
|
||||
driver = file
|
||||
path = /var/vmail/sieve/global_sieve_before.sieve
|
||||
}
|
||||
|
||||
sieve_script before2 {
|
||||
type = before
|
||||
driver = dict
|
||||
name = active
|
||||
dict proxy {
|
||||
name = sieve_before
|
||||
}
|
||||
bin_path = /var/vmail/sieve_before_bindir/%{user}
|
||||
}
|
||||
|
||||
sieve_script after {
|
||||
type = after
|
||||
driver = file
|
||||
path = /var/vmail/sieve/global_sieve_after.sieve
|
||||
}
|
||||
|
||||
sieve_script after2 {
|
||||
type = after
|
||||
driver = dict
|
||||
name = active
|
||||
dict proxy {
|
||||
name = sieve_after
|
||||
}
|
||||
bin_path = /var/vmail/sieve_after_bindir/%{user}
|
||||
}
|
||||
|
||||
# Personal scripts
|
||||
sieve_script personal {
|
||||
type = personal
|
||||
driver = file
|
||||
path = ~/sieve
|
||||
active_path = ~/.dovecot.sieve
|
||||
}
|
||||
|
||||
# Plugins and behavior
|
||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||
sieve_vacation_send_from_recipient = yes
|
||||
sieve_redirect_envelope_from = recipient
|
||||
|
||||
# IMAPSieve training
|
||||
imapsieve_from Junk {
|
||||
sieve_script ham {
|
||||
type = before
|
||||
cause = copy
|
||||
path = /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
}
|
||||
}
|
||||
mailbox Junk {
|
||||
sieve_script spam {
|
||||
type = before
|
||||
cause = copy
|
||||
path = /usr/lib/dovecot/sieve/report-spam.sieve
|
||||
}
|
||||
}
|
||||
|
||||
# Extprograms and extensions
|
||||
sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
|
||||
sieve_plugins {
|
||||
sieve_extprograms = yes
|
||||
}
|
||||
sieve_global_extensions {
|
||||
vnd.dovecot.pipe = yes
|
||||
vnd.dovecot.execute = yes
|
||||
}
|
||||
|
||||
# Limits and duplicate handling
|
||||
sieve_max_script_size = 1M
|
||||
sieve_max_redirects = 100
|
||||
sieve_max_actions = 101
|
||||
sieve_quota_script_count = 0
|
||||
sieve_quota_storage_size = 0
|
||||
sieve_vacation_min_period = 5s
|
||||
sieve_vacation_max_period = 365d
|
||||
sieve_vacation_default_period = 60s
|
||||
sieve_duplicate_default_period = 1m
|
||||
sieve_duplicate_max_period = 7d
|
||||
|
||||
sieve_extensions {
|
||||
vacation-seconds = yes
|
||||
editheader = yes
|
||||
}
|
||||
|
||||
# pipe sockets in /var/run/dovecot/sieve-pipe
|
||||
sieve_pipe_socket_dir = sieve-pipe
|
||||
|
||||
# execute sockets in /var/run/dovecot/sieve-execute
|
||||
sieve_execute_socket_dir = sieve-execute
|
||||
@@ -1,6 +0,0 @@
|
||||
# /etc/dovecot/conf.d/70-crypto.conf
|
||||
# Global mail-crypt keys.
|
||||
crypt_global_private_key global {
|
||||
crypt_private_key_file = /mail_crypt/ecprivkey.pem
|
||||
}
|
||||
crypt_global_public_key_file = /mail_crypt/ecpubkey.pem
|
||||
@@ -1,3 +0,0 @@
|
||||
# /etc/dovecot/conf.d/80-compress.conf
|
||||
# Compression settings.
|
||||
mail_compress_write_method = lz4
|
||||
@@ -1,18 +0,0 @@
|
||||
# /etc/dovecot/conf.d/90-dict.conf
|
||||
# Dict declarations and SQL bindings.
|
||||
dict_server {
|
||||
dict sieve_after {
|
||||
driver = sql
|
||||
!include /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
||||
}
|
||||
|
||||
dict sieve_before {
|
||||
driver = sql
|
||||
!include /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||
}
|
||||
|
||||
dict mysql_quota {
|
||||
driver = sql
|
||||
!include /etc/dovecot/sql/dovecot-dict-sql-quota.conf
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# /etc/dovecot/conf.d/90-limits.conf
|
||||
# Connection and memory limits; doveadm port.
|
||||
mail_max_userip_connections = 500
|
||||
imap_max_line_length = 2 M
|
||||
default_client_limit = 10400
|
||||
default_vsz_limit = 1024 M
|
||||
doveadm_port = 12345
|
||||
@@ -1,22 +0,0 @@
|
||||
# /etc/dovecot/conf.d/99-includes.conf
|
||||
# Late includes and site-specific bits.
|
||||
|
||||
# Mailbox layout includes (if used)
|
||||
!include /etc/dovecot/dovecot.folders.conf
|
||||
|
||||
# Optional replication
|
||||
!include_try /etc/dovecot/mail_replica.conf
|
||||
|
||||
# Existing includes you already had
|
||||
!include_try /etc/dovecot/sni.conf
|
||||
!include_try /etc/dovecot/sogo_trusted_ip.conf
|
||||
!include_try /etc/dovecot/shared_namespace.conf
|
||||
!include_try /etc/dovecot/conf.d/fts.conf
|
||||
|
||||
# Remote auth override
|
||||
remote 127.0.0.1 {
|
||||
auth_allow_cleartext = yes
|
||||
}
|
||||
|
||||
# Outbound submission target
|
||||
submission_host = postfix:588
|
||||
37
data/conf/dovecot/conf.d/fts.conf
Normal file
37
data/conf/dovecot/conf.d/fts.conf
Normal file
@@ -0,0 +1,37 @@
|
||||
# mailcow FTS Flatcurve Settings, change them as you like.
|
||||
plugin {
|
||||
fts_autoindex = yes
|
||||
fts_autoindex_exclude = \Junk
|
||||
fts_autoindex_exclude2 = \Trash
|
||||
# Tweak this setting if you only want to ensure big and frequent folders are indexed, not all.
|
||||
fts_autoindex_max_recent_msgs = 20
|
||||
fts = flatcurve
|
||||
|
||||
# Maximum term length can be set via the 'maxlen' argument (maxlen is
|
||||
# specified in bytes, not number of UTF-8 characters)
|
||||
fts_tokenizer_email_address = maxlen=100
|
||||
fts_tokenizer_generic = algorithm=simple maxlen=30
|
||||
|
||||
# These are not flatcurve settings, but required for Dovecot FTS. See
|
||||
# Dovecot FTS Configuration link above for further information.
|
||||
fts_languages = en es de
|
||||
fts_tokenizers = generic email-address
|
||||
|
||||
# OPTIONAL: Recommended default FTS core configuration
|
||||
fts_filters = normalizer-icu snowball stopwords
|
||||
fts_filters_en = lowercase snowball english-possessive stopwords
|
||||
|
||||
fts_index_timeout = 300s
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
|
||||
service indexer-worker {
|
||||
# Max amount of simultaniously running indexer jobs.
|
||||
process_limit=1
|
||||
|
||||
# Max amount of RAM used by EACH indexer process.
|
||||
vsz_limit=128 MB
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
@@ -1,34 +1,311 @@
|
||||
# /etc/dovecot/dovecot.conf
|
||||
# Base file kept minimal. All real config lives under conf.d/.
|
||||
dovecot_config_version = 2.4.0
|
||||
dovecot_storage_version = 2.4.0
|
||||
# --------------------------------------------------------------------------
|
||||
# Please create a file "extra.conf" for persistent overrides to dovecot.conf
|
||||
# --------------------------------------------------------------------------
|
||||
# LDAP example:
|
||||
#passdb {
|
||||
# args = /etc/dovecot/ldap/passdb.conf
|
||||
# driver = ldap
|
||||
#}
|
||||
|
||||
listen = *,[::]
|
||||
auth_mechanisms = plain login
|
||||
#mail_debug = yes
|
||||
#auth_debug = yes
|
||||
#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
|
||||
log_path = syslog
|
||||
disable_plaintext_auth = yes
|
||||
# Uncomment on NFS share
|
||||
#mmap_disable = yes
|
||||
#mail_fsync = always
|
||||
#mail_nfs_index = yes
|
||||
#mail_nfs_storage = yes
|
||||
login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
|
||||
mail_home = /var/vmail/%d/%n
|
||||
mail_location = maildir:~/
|
||||
mail_plugins = </etc/dovecot/mail_plugins
|
||||
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
|
||||
mail_attachment_dir = /var/attachments
|
||||
mail_attachment_min_size = 128k
|
||||
# Significantly speeds up very large mailboxes, but is only safe to enable if
|
||||
# you do not manually modify the files in the `cur` directories in
|
||||
# mailcowdockerized_vmail-vol-1.
|
||||
# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
|
||||
maildir_very_dirty_syncs = yes
|
||||
|
||||
# Dovecot 2.2
|
||||
#ssl_protocols = !SSLv3
|
||||
# Dovecot 2.3
|
||||
ssl_min_protocol = TLSv1.2
|
||||
|
||||
ssl_prefer_server_ciphers = yes
|
||||
ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
|
||||
|
||||
# Default in Dovecot 2.3
|
||||
ssl_options = no_compression no_ticket
|
||||
|
||||
# New in Dovecot 2.3
|
||||
ssl_dh = </etc/ssl/mail/dhparams.pem
|
||||
# Dovecot 2.2
|
||||
#ssl_dh_parameters_length = 2048
|
||||
log_timestamp = "%Y-%m-%d %H:%M:%S "
|
||||
recipient_delimiter = +
|
||||
auth_master_user_separator = *
|
||||
mail_shared_explicit_inbox = yes
|
||||
mail_prefetch_count = 30
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
|
||||
result_success = return-ok
|
||||
result_failure = continue
|
||||
result_internalfail = continue
|
||||
}
|
||||
# try a master passwd
|
||||
passdb {
|
||||
driver = passwd-file
|
||||
args = /etc/dovecot/dovecot-master.passwd
|
||||
master = yes
|
||||
skip = authenticated
|
||||
}
|
||||
# check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail
|
||||
# a return of the following passdb is mandatory
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
|
||||
}
|
||||
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
|
||||
service doveadm {
|
||||
inet_listener {
|
||||
port = 12345
|
||||
}
|
||||
vsz_limit=2048 MB
|
||||
}
|
||||
!include /etc/dovecot/dovecot.folders.conf
|
||||
protocols = imap sieve lmtp pop3
|
||||
service dict {
|
||||
unix_listener dict {
|
||||
mode = 0660
|
||||
user = vmail
|
||||
group = vmail
|
||||
}
|
||||
}
|
||||
service log {
|
||||
user = dovenull
|
||||
}
|
||||
service config {
|
||||
unix_listener config {
|
||||
user = root
|
||||
group = vmail
|
||||
mode = 0660
|
||||
}
|
||||
}
|
||||
service auth {
|
||||
inet_listener auth-inet {
|
||||
port = 10001
|
||||
}
|
||||
unix_listener auth-master {
|
||||
mode = 0600
|
||||
user = vmail
|
||||
}
|
||||
unix_listener auth-userdb {
|
||||
mode = 0600
|
||||
user = vmail
|
||||
}
|
||||
vsz_limit = 2G
|
||||
}
|
||||
service managesieve-login {
|
||||
inet_listener sieve {
|
||||
port = 4190
|
||||
}
|
||||
inet_listener sieve_haproxy {
|
||||
port = 14190
|
||||
haproxy = yes
|
||||
}
|
||||
service_count = 1
|
||||
process_min_avail = 2
|
||||
vsz_limit = 1G
|
||||
}
|
||||
service imap-login {
|
||||
service_count = 1
|
||||
process_min_avail = 2
|
||||
process_limit = 10000
|
||||
vsz_limit = 1G
|
||||
user = dovenull
|
||||
inet_listener imap_haproxy {
|
||||
port = 10143
|
||||
haproxy = yes
|
||||
}
|
||||
inet_listener imaps_haproxy {
|
||||
port = 10993
|
||||
ssl = yes
|
||||
haproxy = yes
|
||||
}
|
||||
}
|
||||
service pop3-login {
|
||||
service_count = 1
|
||||
process_min_avail = 1
|
||||
vsz_limit = 1G
|
||||
inet_listener pop3_haproxy {
|
||||
port = 10110
|
||||
haproxy = yes
|
||||
}
|
||||
inet_listener pop3s_haproxy {
|
||||
port = 10995
|
||||
ssl = yes
|
||||
haproxy = yes
|
||||
}
|
||||
}
|
||||
service imap {
|
||||
executable = imap
|
||||
user = vmail
|
||||
vsz_limit = 1G
|
||||
}
|
||||
service managesieve {
|
||||
process_limit = 256
|
||||
}
|
||||
service lmtp {
|
||||
inet_listener lmtp-inet {
|
||||
port = 24
|
||||
}
|
||||
user = vmail
|
||||
}
|
||||
listen = *,[::]
|
||||
ssl_cert = </etc/ssl/mail/cert.pem
|
||||
ssl_key = </etc/ssl/mail/key.pem
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
args = /etc/dovecot/dovecot-master.userdb
|
||||
}
|
||||
userdb {
|
||||
args = /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||
driver = sql
|
||||
skip = found
|
||||
}
|
||||
protocol imap {
|
||||
mail_plugins = </etc/dovecot/mail_plugins_imap
|
||||
imap_metadata = yes
|
||||
}
|
||||
mail_attribute_dict = file:%h/dovecot-attributes
|
||||
protocol lmtp {
|
||||
mail_plugins = </etc/dovecot/mail_plugins_lmtp
|
||||
auth_socket_path = /var/run/dovecot/auth-master
|
||||
}
|
||||
protocol sieve {
|
||||
managesieve_logout_format = bytes=%i/%o
|
||||
}
|
||||
plugin {
|
||||
# Allow "any" or "authenticated" to be used in ACLs
|
||||
acl_anyone = </etc/dovecot/acl_anyone
|
||||
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
|
||||
acl = vfile
|
||||
acl_user = %u
|
||||
quota = dict:Userquota::proxy::sqlquota
|
||||
quota_rule2 = Trash:storage=+100%%
|
||||
sieve = /var/vmail/sieve/%u.sieve
|
||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||
sieve_vacation_send_from_recipient = yes
|
||||
sieve_redirect_envelope_from = recipient
|
||||
# From elsewhere to Spam folder
|
||||
imapsieve_mailbox1_name = Junk
|
||||
imapsieve_mailbox1_causes = COPY
|
||||
imapsieve_mailbox1_before = file:/usr/lib/dovecot/sieve/report-spam.sieve
|
||||
# END
|
||||
# From Spam folder to elsewhere
|
||||
imapsieve_mailbox2_name = *
|
||||
imapsieve_mailbox2_from = Junk
|
||||
imapsieve_mailbox2_causes = COPY
|
||||
imapsieve_mailbox2_before = file:/usr/lib/dovecot/sieve/report-ham.sieve
|
||||
# END
|
||||
master_user = %u
|
||||
quota_warning = storage=95%% quota-warning 95 %u
|
||||
quota_warning2 = storage=80%% quota-warning 80 %u
|
||||
sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
|
||||
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
|
||||
sieve_extensions = +notify +imapflags +vacation-seconds +editheader
|
||||
sieve_max_script_size = 1M
|
||||
sieve_max_redirects = 100
|
||||
sieve_max_actions = 101
|
||||
sieve_quota_max_scripts = 0
|
||||
sieve_quota_max_storage = 0
|
||||
listescape_char = "\\"
|
||||
sieve_vacation_min_period = 5s
|
||||
sieve_vacation_max_period = 0
|
||||
sieve_vacation_default_period = 60s
|
||||
sieve_before = /var/vmail/sieve/global_sieve_before.sieve
|
||||
sieve_before2 = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
|
||||
sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
|
||||
sieve_after2 = /var/vmail/sieve/global_sieve_after.sieve
|
||||
sieve_duplicate_default_period = 1m
|
||||
sieve_duplicate_max_period = 7d
|
||||
|
||||
!include_try /etc/dovecot/conf.d/05-core.conf
|
||||
!include_try /etc/dovecot/conf.d/10-logging.conf
|
||||
!include_try /etc/dovecot/conf.d/10-mail.conf
|
||||
!include_try /etc/dovecot/conf.d/10-ssl.conf
|
||||
!include_try /etc/dovecot/conf.d/11-sql.conf
|
||||
!include_try /etc/dovecot/conf.d/12-mysql.conf
|
||||
!include_try /etc/dovecot/conf.d/12-storage-attachments.conf
|
||||
!include_try /etc/dovecot/conf.d/15-performance.conf
|
||||
!include_try /etc/dovecot/conf.d/20-auth.conf
|
||||
!include_try /etc/dovecot/conf.d/20-userdb.conf
|
||||
!include_try /etc/dovecot/conf.d/25-services.conf
|
||||
!include_try /etc/dovecot/conf.d/30-protocols.conf
|
||||
!include_try /etc/dovecot/conf.d/35-fts.conf
|
||||
!include_try /etc/dovecot/conf.d/40-acl.conf
|
||||
!include_try /etc/dovecot/conf.d/40-attributes.conf
|
||||
!include_try /etc/dovecot/conf.d/50-quota.conf
|
||||
!include_try /etc/dovecot/conf.d/60-sieve-pipeline.conf
|
||||
!include_try /etc/dovecot/conf.d/70-crypto.conf
|
||||
!include_try /etc/dovecot/conf.d/80-compress.conf
|
||||
!include_try /etc/dovecot/conf.d/80-mail-logging.conf
|
||||
!include_try /etc/dovecot/conf.d/90-limits.conf
|
||||
!include_try /etc/dovecot/conf.d/90-dict.conf
|
||||
!include_try /etc/dovecot/conf.d/99-includes.conf
|
||||
# -- Global keys
|
||||
mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
|
||||
mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
|
||||
mail_crypt_save_version = 2
|
||||
|
||||
# Last: local overrides
|
||||
!include_try /etc/dovecot/extra.conf
|
||||
# Enable compression while saving, lz4 Dovecot v2.3.17+
|
||||
zlib_save = lz4
|
||||
|
||||
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
||||
mail_log_fields = uid box msgid size
|
||||
mail_log_cached_only = yes
|
||||
|
||||
# Try set mail_replica
|
||||
!include_try /etc/dovecot/mail_replica.conf
|
||||
}
|
||||
service quota-warning {
|
||||
executable = script /usr/local/bin/quota_notify.py
|
||||
# use some unprivileged user for executing the quota warnings
|
||||
user = vmail
|
||||
unix_listener quota-warning {
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
dict {
|
||||
sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql-quota.conf
|
||||
sieve_after = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
||||
sieve_before = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||
}
|
||||
remote 127.0.0.1 {
|
||||
disable_plaintext_auth = no
|
||||
}
|
||||
submission_host = postfix:588
|
||||
mail_max_userip_connections = 500
|
||||
service stats {
|
||||
unix_listener stats-writer {
|
||||
mode = 0660
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
imap_max_line_length = 2 M
|
||||
auth_cache_verify_password_with_worker = yes
|
||||
auth_cache_negative_ttl = 60s
|
||||
auth_cache_ttl = 300s
|
||||
auth_cache_size = 10M
|
||||
auth_verbose_passwords = sha1:6
|
||||
service replicator {
|
||||
process_min_avail = 1
|
||||
}
|
||||
service aggregator {
|
||||
fifo_listener replication-notify-fifo {
|
||||
user = vmail
|
||||
}
|
||||
unix_listener replication-notify {
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
service replicator {
|
||||
unix_listener replicator-doveadm {
|
||||
mode = 0666
|
||||
}
|
||||
}
|
||||
replication_max_conns = 10
|
||||
doveadm_port = 12345
|
||||
replication_dsync_parameters = -d -l 30 -U -n INBOX
|
||||
# <Includes>
|
||||
!include_try /etc/dovecot/sni.conf
|
||||
!include_try /etc/dovecot/sogo_trusted_ip.conf
|
||||
!include_try /etc/dovecot/extra.conf
|
||||
!include_try /etc/dovecot/shared_namespace.conf
|
||||
!include_try /etc/dovecot/conf.d/fts.conf
|
||||
# </Includes>
|
||||
default_client_limit = 10400
|
||||
default_vsz_limit = 1024 M
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
namespace inbox {
|
||||
inbox = yes
|
||||
location =
|
||||
separator = /
|
||||
mailbox storage/* {
|
||||
quota_storage_extra = 100M
|
||||
}
|
||||
mailbox "Trash" {
|
||||
auto = subscribe
|
||||
special_use = \Trash
|
||||
quota_storage_percentage = 100
|
||||
fts_autoindex = no
|
||||
}
|
||||
mailbox "Deleted Messages" {
|
||||
special_use = \Trash
|
||||
@@ -199,7 +195,6 @@ namespace inbox {
|
||||
mailbox "Junk" {
|
||||
auto = subscribe
|
||||
special_use = \Junk
|
||||
fts_autoindex = no
|
||||
}
|
||||
mailbox "Junk-E-Mail" {
|
||||
special_use = \Junk
|
||||
|
||||
@@ -13,7 +13,6 @@ events {
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
server_tokens off;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
|
||||
@@ -14,6 +14,7 @@ ssl_session_tickets off;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=15768000;";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Robots-Tag none;
|
||||
add_header X-Download-Options noopen;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Whitelist generated by Postwhite v3.4 on Sun Mar 1 00:29:01 UTC 2026
|
||||
# Whitelist generated by Postwhite v3.4 on Sat Nov 1 00:21:43 UTC 2025
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 2174 total rules
|
||||
# 2161 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a01:111:f400::/48 permit
|
||||
2a01:111:f403:2800::/53 permit
|
||||
@@ -29,9 +29,7 @@
|
||||
2a01:b747:3005:200::/56 permit
|
||||
2a01:b747:3006:200::/56 permit
|
||||
2a02:a60:0:5::/64 permit
|
||||
2a0f:f640::/56 permit
|
||||
2c0f:fb50:4000::/36 permit
|
||||
2.207.151.53 permit
|
||||
2.207.217.30 permit
|
||||
3.64.237.68 permit
|
||||
3.65.3.180 permit
|
||||
@@ -52,14 +50,10 @@
|
||||
8.25.194.0/23 permit
|
||||
8.25.196.0/23 permit
|
||||
8.36.116.0/24 permit
|
||||
8.39.54.0/23 permit
|
||||
8.39.54.250/31 permit
|
||||
8.39.144.0/24 permit
|
||||
8.40.222.0/23 permit
|
||||
8.40.222.250/31 permit
|
||||
12.130.86.238 permit
|
||||
13.107.213.51 permit
|
||||
13.107.246.51 permit
|
||||
13.107.213.69 permit
|
||||
13.107.246.69 permit
|
||||
13.108.16.0/20 permit
|
||||
13.110.208.0/21 permit
|
||||
13.110.209.0/24 permit
|
||||
@@ -69,7 +63,6 @@
|
||||
13.111.191.0/24 permit
|
||||
13.216.7.111 permit
|
||||
13.216.54.180 permit
|
||||
13.247.164.219 permit
|
||||
15.200.21.50 permit
|
||||
15.200.44.248 permit
|
||||
15.200.201.185 permit
|
||||
@@ -173,7 +166,6 @@
|
||||
34.215.104.144 permit
|
||||
34.218.115.239 permit
|
||||
34.225.212.172 permit
|
||||
34.241.242.183 permit
|
||||
35.83.148.184 permit
|
||||
35.155.198.111 permit
|
||||
35.158.23.94 permit
|
||||
@@ -197,7 +189,6 @@
|
||||
40.233.64.216 permit
|
||||
40.233.83.78 permit
|
||||
40.233.88.28 permit
|
||||
43.239.212.33 permit
|
||||
44.206.138.57 permit
|
||||
44.210.169.44 permit
|
||||
44.217.45.156 permit
|
||||
@@ -279,7 +270,6 @@
|
||||
50.112.246.219 permit
|
||||
52.1.14.157 permit
|
||||
52.5.230.59 permit
|
||||
52.6.74.205 permit
|
||||
52.12.53.23 permit
|
||||
52.13.214.179 permit
|
||||
52.26.1.71 permit
|
||||
@@ -303,6 +293,14 @@
|
||||
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
|
||||
@@ -336,7 +334,6 @@
|
||||
54.244.54.130 permit
|
||||
54.244.242.0/24 permit
|
||||
54.255.61.23 permit
|
||||
56.124.6.228 permit
|
||||
57.103.64.0/18 permit
|
||||
57.129.93.249 permit
|
||||
62.13.128.0/24 permit
|
||||
@@ -397,12 +394,30 @@
|
||||
64.207.219.143 permit
|
||||
64.233.160.0/19 permit
|
||||
65.52.80.137 permit
|
||||
65.54.51.64/26 permit
|
||||
65.54.61.64/26 permit
|
||||
65.54.121.120/29 permit
|
||||
65.54.190.0/24 permit
|
||||
65.54.241.0/24 permit
|
||||
65.55.29.77 permit
|
||||
65.55.33.64/28 permit
|
||||
65.55.34.0/24 permit
|
||||
65.55.42.224/28 permit
|
||||
65.55.52.224/27 permit
|
||||
65.55.78.128/25 permit
|
||||
65.55.81.48/28 permit
|
||||
65.55.90.0/24 permit
|
||||
65.55.94.0/25 permit
|
||||
65.55.111.0/24 permit
|
||||
65.55.113.64/26 permit
|
||||
65.55.116.0/25 permit
|
||||
65.55.126.0/25 permit
|
||||
65.55.174.0/25 permit
|
||||
65.55.178.128/27 permit
|
||||
65.55.234.192/26 permit
|
||||
65.110.161.77 permit
|
||||
65.123.29.213 permit
|
||||
65.123.29.220 permit
|
||||
65.154.166.0/24 permit
|
||||
65.212.180.36 permit
|
||||
66.102.0.0/20 permit
|
||||
66.119.150.192/26 permit
|
||||
@@ -519,6 +534,7 @@
|
||||
69.169.224.0/20 permit
|
||||
69.171.232.0/24 permit
|
||||
69.171.244.0/23 permit
|
||||
70.37.151.128/25 permit
|
||||
70.42.149.35 permit
|
||||
72.3.185.0/24 permit
|
||||
72.14.192.0/18 permit
|
||||
@@ -615,6 +631,7 @@
|
||||
74.208.4.220 permit
|
||||
74.208.4.221 permit
|
||||
74.209.250.0/24 permit
|
||||
75.2.70.75 permit
|
||||
76.223.128.0/19 permit
|
||||
76.223.176.0/20 permit
|
||||
77.238.176.0/24 permit
|
||||
@@ -707,11 +724,11 @@
|
||||
87.248.117.205 permit
|
||||
87.253.232.0/21 permit
|
||||
89.22.108.0/24 permit
|
||||
91.198.2.177 permit
|
||||
91.198.2.217 permit
|
||||
91.198.2.222 permit
|
||||
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
|
||||
@@ -1204,11 +1221,9 @@
|
||||
98.139.245.208/30 permit
|
||||
98.139.245.212/31 permit
|
||||
99.78.197.208/28 permit
|
||||
99.83.190.102 permit
|
||||
103.9.96.0/22 permit
|
||||
103.28.42.0/24 permit
|
||||
103.84.217.15 permit
|
||||
103.84.217.238 permit
|
||||
103.89.75.238 permit
|
||||
103.151.192.0/23 permit
|
||||
103.168.172.128/27 permit
|
||||
103.237.104.0/22 permit
|
||||
@@ -1352,6 +1367,11 @@
|
||||
108.179.144.0/20 permit
|
||||
109.224.244.0/24 permit
|
||||
109.237.142.0/24 permit
|
||||
111.221.23.128/25 permit
|
||||
111.221.26.0/27 permit
|
||||
111.221.66.0/25 permit
|
||||
111.221.69.128/25 permit
|
||||
111.221.112.0/21 permit
|
||||
112.19.199.64/29 permit
|
||||
112.19.242.64/29 permit
|
||||
116.214.12.47 permit
|
||||
@@ -1369,9 +1389,6 @@
|
||||
117.120.16.0/21 permit
|
||||
119.42.242.52/31 permit
|
||||
119.42.242.156 permit
|
||||
121.244.91.48 permit
|
||||
121.244.91.52 permit
|
||||
122.15.156.182 permit
|
||||
123.126.78.64/29 permit
|
||||
124.108.96.24/31 permit
|
||||
124.108.96.28/31 permit
|
||||
@@ -1416,7 +1433,6 @@
|
||||
129.153.194.228 permit
|
||||
129.154.255.129 permit
|
||||
129.158.56.255 permit
|
||||
129.158.62.153 permit
|
||||
129.159.22.159 permit
|
||||
129.159.87.137 permit
|
||||
129.213.195.191 permit
|
||||
@@ -1437,21 +1453,7 @@
|
||||
134.170.141.64/26 permit
|
||||
134.170.143.0/24 permit
|
||||
134.170.174.0/24 permit
|
||||
135.84.80.0/24 permit
|
||||
135.84.81.0/24 permit
|
||||
135.84.82.0/24 permit
|
||||
135.84.83.0/24 permit
|
||||
135.84.216.0/22 permit
|
||||
136.143.160.0/24 permit
|
||||
136.143.161.0/24 permit
|
||||
136.143.162.0/24 permit
|
||||
136.143.176.0/24 permit
|
||||
136.143.177.0/24 permit
|
||||
136.143.178.49 permit
|
||||
136.143.182.0/23 permit
|
||||
136.143.184.0/24 permit
|
||||
136.143.188.0/24 permit
|
||||
136.143.190.0/23 permit
|
||||
136.146.128.0/20 permit
|
||||
136.147.128.0/20 permit
|
||||
136.147.135.0/24 permit
|
||||
@@ -1467,11 +1469,8 @@
|
||||
139.138.46.219 permit
|
||||
139.138.57.55 permit
|
||||
139.138.58.119 permit
|
||||
139.167.79.86 permit
|
||||
139.180.17.0/24 permit
|
||||
140.238.148.191 permit
|
||||
141.148.55.217 permit
|
||||
141.148.91.244 permit
|
||||
141.148.159.229 permit
|
||||
141.193.32.0/23 permit
|
||||
141.193.184.32/27 permit
|
||||
@@ -1517,7 +1516,6 @@
|
||||
149.72.234.184 permit
|
||||
149.72.248.236 permit
|
||||
149.97.173.180 permit
|
||||
150.136.21.199 permit
|
||||
150.230.98.160 permit
|
||||
151.145.38.14 permit
|
||||
152.67.105.195 permit
|
||||
@@ -1527,7 +1525,20 @@
|
||||
155.248.220.138 permit
|
||||
155.248.234.149 permit
|
||||
155.248.237.141 permit
|
||||
157.55.0.192/26 permit
|
||||
157.55.1.128/26 permit
|
||||
157.55.2.0/25 permit
|
||||
157.55.9.128/25 permit
|
||||
157.55.11.0/25 permit
|
||||
157.55.49.0/25 permit
|
||||
157.55.61.0/24 permit
|
||||
157.55.157.128/25 permit
|
||||
157.55.225.0/25 permit
|
||||
157.56.24.0/25 permit
|
||||
157.56.120.128/26 permit
|
||||
157.56.232.0/21 permit
|
||||
157.56.240.0/20 permit
|
||||
157.56.248.0/21 permit
|
||||
157.58.30.128/25 permit
|
||||
157.58.196.96/29 permit
|
||||
157.58.249.3 permit
|
||||
@@ -1556,10 +1567,8 @@
|
||||
159.135.224.0/20 permit
|
||||
159.135.228.10 permit
|
||||
159.183.0.0/16 permit
|
||||
159.183.14.233 permit
|
||||
159.183.68.71 permit
|
||||
159.183.79.38 permit
|
||||
159.183.121.182 permit
|
||||
159.183.129.172 permit
|
||||
160.1.62.192 permit
|
||||
161.38.192.0/20 permit
|
||||
@@ -1579,16 +1588,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
|
||||
165.173.128.0/24 permit
|
||||
165.173.180.1 permit
|
||||
165.173.180.250/31 permit
|
||||
165.173.182.250/31 permit
|
||||
166.78.68.0/22 permit
|
||||
166.78.68.221 permit
|
||||
166.78.69.169 permit
|
||||
@@ -1618,24 +1620,14 @@
|
||||
168.245.12.252 permit
|
||||
168.245.46.9 permit
|
||||
168.245.127.231 permit
|
||||
169.148.129.0/24 permit
|
||||
169.148.131.0/24 permit
|
||||
169.148.138.0/24 permit
|
||||
169.148.142.10 permit
|
||||
169.148.142.33 permit
|
||||
169.148.144.0/25 permit
|
||||
169.148.144.10 permit
|
||||
169.148.146.0/23 permit
|
||||
169.148.175.3 permit
|
||||
169.148.179.3 permit
|
||||
169.148.188.0/24 permit
|
||||
169.148.188.182 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
|
||||
@@ -1663,7 +1655,8 @@
|
||||
182.50.78.64/28 permit
|
||||
183.240.219.64/29 permit
|
||||
185.4.120.0/22 permit
|
||||
185.11.255.144 permit
|
||||
185.11.253.128/27 permit
|
||||
185.11.255.0/24 permit
|
||||
185.12.80.0/22 permit
|
||||
185.28.196.0/22 permit
|
||||
185.58.84.93 permit
|
||||
@@ -1677,16 +1670,8 @@
|
||||
185.138.56.128/25 permit
|
||||
185.189.236.0/22 permit
|
||||
185.211.120.0/22 permit
|
||||
185.233.188.68 permit
|
||||
185.233.188.75 permit
|
||||
185.233.188.84 permit
|
||||
185.233.188.160 permit
|
||||
185.233.188.176 permit
|
||||
185.233.188.247 permit
|
||||
185.233.189.44 permit
|
||||
185.233.189.98 permit
|
||||
185.233.189.122 permit
|
||||
185.233.189.228 permit
|
||||
185.233.188.0/23 permit
|
||||
185.233.190.0/23 permit
|
||||
185.250.236.0/22 permit
|
||||
185.250.239.148 permit
|
||||
185.250.239.168 permit
|
||||
@@ -1762,9 +1747,7 @@
|
||||
193.109.254.0/23 permit
|
||||
193.122.128.100 permit
|
||||
193.123.56.63 permit
|
||||
193.142.157.15 permit
|
||||
193.142.157.125 permit
|
||||
193.142.157.158 permit
|
||||
193.142.157.0/24 permit
|
||||
193.142.157.191 permit
|
||||
193.142.157.198 permit
|
||||
194.19.134.0/25 permit
|
||||
@@ -1788,7 +1771,6 @@
|
||||
194.97.212.12 permit
|
||||
194.106.220.0/23 permit
|
||||
194.113.24.0/22 permit
|
||||
194.113.42.0/26 permit
|
||||
194.154.193.192/27 permit
|
||||
195.4.92.0/23 permit
|
||||
195.54.172.0/23 permit
|
||||
@@ -1802,7 +1784,6 @@
|
||||
198.61.254.21 permit
|
||||
198.61.254.231 permit
|
||||
198.178.234.57 permit
|
||||
198.202.211.1 permit
|
||||
198.244.48.0/20 permit
|
||||
198.244.56.107 permit
|
||||
198.244.56.108 permit
|
||||
@@ -1824,16 +1805,7 @@
|
||||
199.16.156.0/22 permit
|
||||
199.33.145.1 permit
|
||||
199.33.145.32 permit
|
||||
199.34.22.36 permit
|
||||
199.59.148.0/22 permit
|
||||
199.67.80.2 permit
|
||||
199.67.80.20 permit
|
||||
199.67.82.2 permit
|
||||
199.67.82.20 permit
|
||||
199.67.84.0/24 permit
|
||||
199.67.86.0/24 permit
|
||||
199.67.88.0/24 permit
|
||||
199.67.90.0/24 permit
|
||||
199.101.161.130 permit
|
||||
199.101.162.0/25 permit
|
||||
199.122.120.0/21 permit
|
||||
@@ -1886,11 +1858,10 @@
|
||||
204.14.232.64/28 permit
|
||||
204.14.234.64/28 permit
|
||||
204.75.142.0/24 permit
|
||||
204.79.197.212 permit
|
||||
204.92.114.187 permit
|
||||
204.92.114.203 permit
|
||||
204.92.114.204/31 permit
|
||||
204.141.32.0/23 permit
|
||||
204.141.42.0/23 permit
|
||||
204.216.164.202 permit
|
||||
204.220.160.0/21 permit
|
||||
204.220.168.0/21 permit
|
||||
@@ -1913,13 +1884,24 @@
|
||||
206.165.246.80/29 permit
|
||||
206.191.224.0/19 permit
|
||||
206.246.157.1 permit
|
||||
207.46.4.128/25 permit
|
||||
207.46.22.35 permit
|
||||
207.46.50.72 permit
|
||||
207.46.50.82 permit
|
||||
207.46.50.192/26 permit
|
||||
207.46.50.224 permit
|
||||
207.46.52.71 permit
|
||||
207.46.52.79 permit
|
||||
207.46.58.128/25 permit
|
||||
207.46.116.128/29 permit
|
||||
207.46.117.0/24 permit
|
||||
207.46.132.128/27 permit
|
||||
207.46.198.0/25 permit
|
||||
207.46.200.0/27 permit
|
||||
207.67.38.0/24 permit
|
||||
207.67.98.192/27 permit
|
||||
207.68.176.0/26 permit
|
||||
207.68.176.96/27 permit
|
||||
207.97.204.96/29 permit
|
||||
207.126.144.0/20 permit
|
||||
207.171.160.0/19 permit
|
||||
@@ -2068,10 +2050,14 @@
|
||||
212.227.126.225 permit
|
||||
212.227.126.226 permit
|
||||
212.227.126.227 permit
|
||||
213.95.19.64/27 permit
|
||||
213.95.135.4 permit
|
||||
213.199.128.139 permit
|
||||
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
|
||||
@@ -2099,6 +2085,7 @@
|
||||
216.39.62.60/31 permit
|
||||
216.39.62.136/29 permit
|
||||
216.39.62.144/31 permit
|
||||
216.58.192.0/19 permit
|
||||
216.66.217.240/29 permit
|
||||
216.71.138.33 permit
|
||||
216.71.152.207 permit
|
||||
@@ -2157,9 +2144,11 @@
|
||||
2001:748:400:3301::3 permit
|
||||
2001:748:400:3301::4 permit
|
||||
2404:6800:4000::/36 permit
|
||||
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
|
||||
2603:1010:3:3::5b permit
|
||||
2603:1020:201:10::10f permit
|
||||
2603:1030:20e:3::23c permit
|
||||
2603:1030:b:3::152 permit
|
||||
2603:1030:c02:8::14 permit
|
||||
2607:f8b0:4000::/36 permit
|
||||
2620:109:c003:104::/64 permit
|
||||
2620:109:c003:104::215 permit
|
||||
@@ -2172,8 +2161,6 @@
|
||||
2620:10d:c09c:400::8:1 permit
|
||||
2620:119:50c0:207::/64 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2620:1ec:46::51 permit
|
||||
2620:1ec:bdf::51 permit
|
||||
2800:3f0:4000::/36 permit
|
||||
49.12.4.251 permit # checks.mailcow.email
|
||||
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
||||
|
||||
@@ -146,171 +146,8 @@ rspamd_config:register_symbol({
|
||||
return false
|
||||
end
|
||||
|
||||
-- Helper function to parse IPv6 into 8 segments
|
||||
local function ipv6_to_segments(ip_str)
|
||||
-- Remove zone identifier if present (e.g., %eth0)
|
||||
ip_str = ip_str:gsub("%%.*$", "")
|
||||
|
||||
local segments = {}
|
||||
|
||||
-- Handle :: compression
|
||||
if ip_str:find('::') then
|
||||
local before, after = ip_str:match('^(.*)::(.*)$')
|
||||
before = before or ''
|
||||
after = after or ''
|
||||
|
||||
local before_parts = {}
|
||||
local after_parts = {}
|
||||
|
||||
if before ~= '' then
|
||||
for seg in before:gmatch('[^:]+') do
|
||||
table.insert(before_parts, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
if after ~= '' then
|
||||
for seg in after:gmatch('[^:]+') do
|
||||
table.insert(after_parts, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add before segments
|
||||
for _, seg in ipairs(before_parts) do
|
||||
table.insert(segments, seg)
|
||||
end
|
||||
|
||||
-- Add compressed zeros
|
||||
local zeros_needed = 8 - #before_parts - #after_parts
|
||||
for i = 1, zeros_needed do
|
||||
table.insert(segments, 0)
|
||||
end
|
||||
|
||||
-- Add after segments
|
||||
for _, seg in ipairs(after_parts) do
|
||||
table.insert(segments, seg)
|
||||
end
|
||||
else
|
||||
-- No compression
|
||||
for seg in ip_str:gmatch('[^:]+') do
|
||||
table.insert(segments, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Ensure we have exactly 8 segments
|
||||
while #segments < 8 do
|
||||
table.insert(segments, 0)
|
||||
end
|
||||
|
||||
return segments
|
||||
end
|
||||
|
||||
-- Generate all common IPv6 notations
|
||||
local function get_ipv6_variants(ip_str)
|
||||
local variants = {}
|
||||
local seen = {}
|
||||
|
||||
local function add_variant(v)
|
||||
if v and not seen[v] then
|
||||
table.insert(variants, v)
|
||||
seen[v] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- For IPv4, just return the original
|
||||
if not ip_str:find(':') then
|
||||
add_variant(ip_str)
|
||||
return variants
|
||||
end
|
||||
|
||||
local segments = ipv6_to_segments(ip_str)
|
||||
|
||||
-- 1. Fully expanded form (all zeros shown as 0000)
|
||||
local expanded_parts = {}
|
||||
for _, seg in ipairs(segments) do
|
||||
table.insert(expanded_parts, string.format('%04x', seg))
|
||||
end
|
||||
add_variant(table.concat(expanded_parts, ':'))
|
||||
|
||||
-- 2. Standard form (no leading zeros, but all segments present)
|
||||
local standard_parts = {}
|
||||
for _, seg in ipairs(segments) do
|
||||
table.insert(standard_parts, string.format('%x', seg))
|
||||
end
|
||||
add_variant(table.concat(standard_parts, ':'))
|
||||
|
||||
-- 3. Find all possible :: compressions
|
||||
-- RFC 5952: compress the longest run of consecutive zeros
|
||||
-- But we need to check all possibilities since Redis might have any form
|
||||
|
||||
-- Find all zero runs
|
||||
local zero_runs = {}
|
||||
local in_run = false
|
||||
local run_start = 0
|
||||
local run_length = 0
|
||||
|
||||
for i = 1, 8 do
|
||||
if segments[i] == 0 then
|
||||
if not in_run then
|
||||
in_run = true
|
||||
run_start = i
|
||||
run_length = 1
|
||||
else
|
||||
run_length = run_length + 1
|
||||
end
|
||||
else
|
||||
if in_run then
|
||||
if run_length >= 1 then -- Allow single zero compression too
|
||||
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||
end
|
||||
in_run = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Don't forget the last run
|
||||
if in_run and run_length >= 1 then
|
||||
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||
end
|
||||
|
||||
-- Generate variant for each zero run compression
|
||||
for _, run in ipairs(zero_runs) do
|
||||
local parts = {}
|
||||
|
||||
-- Before compression
|
||||
for i = 1, run.start - 1 do
|
||||
table.insert(parts, string.format('%x', segments[i]))
|
||||
end
|
||||
|
||||
-- The compression
|
||||
if run.start == 1 then
|
||||
table.insert(parts, '')
|
||||
table.insert(parts, '')
|
||||
elseif run.start + run.length - 1 == 8 then
|
||||
table.insert(parts, '')
|
||||
table.insert(parts, '')
|
||||
else
|
||||
table.insert(parts, '')
|
||||
end
|
||||
|
||||
-- After compression
|
||||
for i = run.start + run.length, 8 do
|
||||
table.insert(parts, string.format('%x', segments[i]))
|
||||
end
|
||||
|
||||
local compressed = table.concat(parts, ':'):gsub('::+', '::')
|
||||
add_variant(compressed)
|
||||
end
|
||||
|
||||
return variants
|
||||
end
|
||||
|
||||
local from_ip_string = tostring(ip)
|
||||
local ip_check_table = {}
|
||||
|
||||
-- Add all variants of the exact IP
|
||||
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
|
||||
table.insert(ip_check_table, variant)
|
||||
end
|
||||
ip_check_table = {from_ip_string}
|
||||
|
||||
local maxbits = 128
|
||||
local minbits = 32
|
||||
@@ -318,18 +155,10 @@ rspamd_config:register_symbol({
|
||||
maxbits = 32
|
||||
minbits = 8
|
||||
end
|
||||
|
||||
-- Add all CIDR notations with variants
|
||||
for i=maxbits,minbits,-1 do
|
||||
local masked_ip = ip:apply_mask(i)
|
||||
local cidr_base = masked_ip:to_string()
|
||||
|
||||
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
|
||||
local cidr = variant .. "/" .. i
|
||||
table.insert(ip_check_table, cidr)
|
||||
end
|
||||
local nip = ip:apply_mask(i):to_string() .. "/" .. i
|
||||
table.insert(ip_check_table, nip)
|
||||
end
|
||||
|
||||
local function keep_spam_cb(err, data)
|
||||
if err then
|
||||
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
||||
@@ -337,15 +166,12 @@ rspamd_config:register_symbol({
|
||||
else
|
||||
for k,v in pairs(data) do
|
||||
if (v and v ~= userdata and v == '1') then
|
||||
rspamd_logger.infox(rspamd_config, "found ip %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k])
|
||||
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
|
||||
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
|
||||
task:set_flag('no_stat')
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(ip_check_table, 1, 'KEEP_SPAM')
|
||||
local redis_ret_user = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
@@ -384,7 +210,6 @@ rspamd_config:register_symbol({
|
||||
rspamd_config:register_symbol({
|
||||
name = 'TAG_MOO',
|
||||
type = 'postfilter',
|
||||
flags = 'ignore_passthrough',
|
||||
callback = function(task)
|
||||
local util = require("rspamd_util")
|
||||
local rspamd_logger = require "rspamd_logger"
|
||||
@@ -392,7 +217,9 @@ rspamd_config:register_symbol({
|
||||
local rspamd_http = require "rspamd_http"
|
||||
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)
|
||||
@@ -404,147 +231,101 @@ rspamd_config:register_symbol({
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if we have exactly one recipient
|
||||
if not (rcpts and #rcpts == 1) then
|
||||
rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0)
|
||||
remove_moo_tag()
|
||||
return
|
||||
end
|
||||
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
|
||||
local tag = tagged_rcpt[1].options[1]
|
||||
rspamd_logger.infox("found tag: %s", tag)
|
||||
local action = task:get_metric_action('default')
|
||||
rspamd_logger.infox("metric action now: %s", action)
|
||||
|
||||
local rcpt_addr = rcpts[1]['addr']
|
||||
local rcpt_user = rcpts[1]['user']
|
||||
local rcpt_domain = rcpts[1]['domain']
|
||||
if action ~= 'no action' and action ~= 'greylist' then
|
||||
rspamd_logger.infox("skipping tag handler for action: %s", action)
|
||||
remove_moo_tag()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if recipient has a tag (contains '+')
|
||||
local tag = nil
|
||||
if tagged_rcpt ~= nil then
|
||||
tag = tagged_rcpt
|
||||
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
|
||||
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")
|
||||
remove_moo_tag()
|
||||
end
|
||||
|
||||
else
|
||||
rspamd_logger.infox("user wants subject modified for tagged mail")
|
||||
local sbj = task:get_header('Subject')
|
||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
||||
task:set_milter_reply({
|
||||
remove_headers = {
|
||||
['Subject'] = 1,
|
||||
['X-Moo-Tag'] = 0
|
||||
},
|
||||
add_headers = {['Subject'] = new_sbj}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local redis_ret_subject = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
body, -- hash key
|
||||
false, -- is write
|
||||
tag_callback_subject, --callback
|
||||
'HGET', -- command
|
||||
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
|
||||
)
|
||||
if not redis_ret_subject then
|
||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
||||
remove_moo_tag()
|
||||
end
|
||||
|
||||
-- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
|
||||
local allow_processing = false
|
||||
|
||||
if task.has_pre_result then
|
||||
local has_pre, pre_action = task:has_pre_result()
|
||||
if has_pre then
|
||||
rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
|
||||
if pre_action == 'accept' then
|
||||
allow_processing = true
|
||||
rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Allow processing for mild actions or when we have pre-result accept
|
||||
if not allow_processing and action ~= 'no action' and action ~= 'greylist' then
|
||||
rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action)
|
||||
remove_moo_tag()
|
||||
return true
|
||||
end
|
||||
|
||||
rspamd_logger.infox("TAG_MOO: processing allowed")
|
||||
|
||||
local function http_callback(err_message, code, body, headers)
|
||||
if body ~= nil and body ~= "" then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
|
||||
|
||||
local function tag_callback_subject(err, data)
|
||||
if err or type(data) ~= 'string' or data == '' then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
|
||||
|
||||
local function tag_callback_subfolder(err, data)
|
||||
if err or type(data) ~= 'string' or data == '' then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
||||
remove_moo_tag()
|
||||
else
|
||||
rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
|
||||
task:set_milter_reply({
|
||||
add_headers = {['X-Moo-Tag'] = 'YES'}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local redis_ret_subfolder = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
body, -- hash key
|
||||
false, -- is write
|
||||
tag_callback_subfolder, --callback
|
||||
'HGET', -- command
|
||||
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
|
||||
)
|
||||
if not redis_ret_subfolder then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
|
||||
if rcpts and #rcpts == 1 then
|
||||
for _,rcpt in ipairs(rcpts) do
|
||||
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
||||
if #rcpt_split == 2 then
|
||||
if rcpt_split[1] == 'postmaster' then
|
||||
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
|
||||
remove_moo_tag()
|
||||
else
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/aliasexp.php',
|
||||
body='',
|
||||
callback=http_callback,
|
||||
headers={Rcpt=rcpt['addr']},
|
||||
})
|
||||
end
|
||||
|
||||
else
|
||||
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
||||
local sbj = task:get_header('Subject') or ''
|
||||
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
|
||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. 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,
|
||||
@@ -554,7 +335,6 @@ rspamd_config:register_symbol({
|
||||
rspamd_config:register_symbol({
|
||||
name = 'BCC',
|
||||
type = 'postfilter',
|
||||
flags = 'ignore_passthrough',
|
||||
callback = function(task)
|
||||
local util = require("rspamd_util")
|
||||
local rspamd_http = require "rspamd_http"
|
||||
@@ -583,13 +363,11 @@ rspamd_config:register_symbol({
|
||||
local email_content = tostring(task:get_content())
|
||||
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||
-- send mail
|
||||
local from_smtp = task:get_from('smtp')
|
||||
local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
|
||||
lua_smtp.sendmail({
|
||||
task = task,
|
||||
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||
port = 591,
|
||||
from = from_addr,
|
||||
from = task:get_from(stp)[1].addr,
|
||||
recipients = bcc_dest,
|
||||
helo = 'bcc',
|
||||
timeout = 20,
|
||||
@@ -619,41 +397,27 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
|
||||
local action = task:get_metric_action('default')
|
||||
rspamd_logger.infox("BCC: metric action: %s", action)
|
||||
|
||||
-- Check for pre-result accept (e.g., from KEEP_SPAM)
|
||||
local allow_bcc = false
|
||||
if task.has_pre_result then
|
||||
local has_pre, pre_action = task:has_pre_result()
|
||||
if has_pre and pre_action == 'accept' then
|
||||
allow_bcc = true
|
||||
rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
|
||||
end
|
||||
end
|
||||
|
||||
-- Allow BCC for mild actions or when we have pre-result accept
|
||||
if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
|
||||
rspamd_logger.infox("BCC: skipping for action: %s", action)
|
||||
return
|
||||
end
|
||||
rspamd_logger.infox("metric action now: %s", action)
|
||||
|
||||
local function rcpt_callback(err_message, code, body, headers)
|
||||
if err_message == nil and code == 201 and body ~= nil then
|
||||
rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
|
||||
send_mail(task, body)
|
||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
||||
send_mail(task, body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function from_callback(err_message, code, body, headers)
|
||||
if err_message == nil and code == 201 and body ~= nil then
|
||||
rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
|
||||
send_mail(task, body)
|
||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
||||
send_mail(task, body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if rcpt_table then
|
||||
for _,e in ipairs(rcpt_table) do
|
||||
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
|
||||
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/bcc.php',
|
||||
@@ -666,7 +430,7 @@ rspamd_config:register_symbol({
|
||||
|
||||
if from_table then
|
||||
for _,e in ipairs(from_table) do
|
||||
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
|
||||
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/bcc.php',
|
||||
@@ -677,7 +441,7 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
end
|
||||
|
||||
-- Don't return true to avoid symbol being logged
|
||||
return true
|
||||
end,
|
||||
priority = 20
|
||||
})
|
||||
|
||||
@@ -86,12 +86,6 @@
|
||||
SOGoMaximumFailedLoginInterval = 900;
|
||||
SOGoFailedLoginBlockInterval = 900;
|
||||
|
||||
// Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it.
|
||||
//SOGoURLEncryptionEnabled = NO;
|
||||
|
||||
// Set a 16 character encryption key for SOGo URL Description, change this to your own value
|
||||
//SOGoURLPathEncryptionKey = "SOGoSuperSecret0";
|
||||
|
||||
GCSChannelCollectionTimer = 60;
|
||||
GCSChannelExpireAge = 60;
|
||||
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
protect_route(['admin']);
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -3,11 +3,8 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
// Only redirect to dashboard if NO pending actions
|
||||
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
protect_route(['admin']);
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -2,7 +2,19 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
protect_route(['admin']);
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$js_minifier->add('/web/js/site/queue.js');
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
protect_route(['admin']);
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -2454,90 +2454,6 @@ paths:
|
||||
type: object
|
||||
type: object
|
||||
summary: Delete mails in Quarantine
|
||||
/api/v1/edit/qitem:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
release:
|
||||
value:
|
||||
- log:
|
||||
- quarantine
|
||||
- edit
|
||||
- id:
|
||||
- "33"
|
||||
action: release
|
||||
msg:
|
||||
- item_released
|
||||
- "33"
|
||||
type: success
|
||||
learnham:
|
||||
value:
|
||||
- log:
|
||||
- quarantine
|
||||
- edit
|
||||
- id:
|
||||
- "34"
|
||||
action: learnham
|
||||
msg:
|
||||
- item_learned
|
||||
- "34"
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Quarantine
|
||||
description: >-
|
||||
Using this endpoint you can perform actions on quarantine items. It is possible to release
|
||||
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
|
||||
You must provide the quarantine item IDs. You can get the IDs using the GET method.
|
||||
operationId: Edit mails in Quarantine
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
items:
|
||||
- "33"
|
||||
- "34"
|
||||
attr:
|
||||
action: release
|
||||
properties:
|
||||
items:
|
||||
description: contains list of quarantine item IDs to release or learn as ham
|
||||
type: object
|
||||
attr:
|
||||
description: attributes for the action
|
||||
type: object
|
||||
properties:
|
||||
action:
|
||||
type: string
|
||||
enum:
|
||||
- release
|
||||
- learnham
|
||||
description: "release - return email to inbox; learnham - learn as ham to improve filtering"
|
||||
type: object
|
||||
summary: Edit mails in Quarantine
|
||||
/api/v1/delete/recipient_map:
|
||||
post:
|
||||
responses:
|
||||
|
||||
@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="<?=$mailcow_hostname; ?>">
|
||||
<domain>%EMAILDOMAIN%</domain>
|
||||
<displayName><?=$autodiscover_config['displayName']; ?></displayName>
|
||||
<displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
|
||||
<displayName>A mailcow mail server</displayName>
|
||||
<displayShortName>mail server</displayShortName>
|
||||
|
||||
<incomingServer type="imap">
|
||||
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
||||
|
||||
@@ -60,31 +60,101 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
$iam_provider = identity_provider('init');
|
||||
$iam_settings = identity_provider('get');
|
||||
|
||||
// Passwordless autodiscover - no authentication required
|
||||
// Email will be extracted from the request body
|
||||
$login_user = null;
|
||||
$login_role = null;
|
||||
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
|
||||
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
|
||||
|
||||
header("Content-Type: application/xml");
|
||||
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
||||
if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => "none",
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: must be authenticated"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$login_role = check_login($login_user, $login_pass, array('eas' => TRUE));
|
||||
|
||||
if ($login_role === "user") {
|
||||
header("Content-Type: application/xml");
|
||||
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
||||
?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||
<?php
|
||||
if(!$data) {
|
||||
if(!$data) {
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: invalid or missing request data"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'msg' => 'Redis: '.$e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="2477272013">
|
||||
<ErrorCode>600</ErrorCode>
|
||||
<Message>Invalid Request</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
try {
|
||||
$discover = new SimpleXMLElement($data);
|
||||
$email = $discover->Request->EMailAddress;
|
||||
} catch (Exception $e) {
|
||||
$email = $_SERVER['PHP_AUTH_USER'];
|
||||
}
|
||||
|
||||
$username = trim($email);
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
catch(PDOException $e) {
|
||||
die("Failed to determine name from SQL");
|
||||
}
|
||||
if (!empty($MailboxData['name'])) {
|
||||
$displayname = $MailboxData['name'];
|
||||
}
|
||||
else {
|
||||
$displayname = $email;
|
||||
}
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => "none",
|
||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: invalid or missing request data"
|
||||
"service" => $autodiscover_config['autodiscoverType']
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -93,143 +163,7 @@ if(!$data) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>600</ErrorCode>
|
||||
<Message>Invalid Request</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
try {
|
||||
$discover = new SimpleXMLElement($data);
|
||||
$email = $discover->Request->EMailAddress;
|
||||
} catch (Exception $e) {
|
||||
// If parsing fails, return error
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => "none",
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: could not parse email from request"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
$redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
// Silently fail
|
||||
}
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>600</ErrorCode>
|
||||
<Message>Invalid Request</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$username = trim((string)$email);
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
|
||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||
WHERE `mailbox`.`username` = :username
|
||||
AND `mailbox`.`active` = '1'
|
||||
AND `domain`.`active` = '1'");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
catch(PDOException $e) {
|
||||
// Database error - return error response with complete XML
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>500</ErrorCode>
|
||||
<Message>Database Error</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Mailbox not found or not active - return generic error to prevent user enumeration
|
||||
if (empty($MailboxData)) {
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $email,
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: mailbox not found or inactive"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
// Silently fail
|
||||
}
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>600</ErrorCode>
|
||||
<Message>Invalid Request</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (!empty($MailboxData['name'])) {
|
||||
$displayname = $MailboxData['name'];
|
||||
}
|
||||
else {
|
||||
$displayname = $email;
|
||||
}
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $email,
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => $autodiscover_config['autodiscoverType']
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'msg' => 'Redis: '.$e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
?>
|
||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||
<User>
|
||||
@@ -304,3 +238,6 @@ if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
}
|
||||
?>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -3,11 +3,8 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
// Only redirect to mailbox if NO pending actions
|
||||
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
protect_route(['domainadmin']);
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
header('Location: /domainadmin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -2,28 +2,41 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
/*
|
||||
/ DOMAIN ADMIN
|
||||
*/
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
|
||||
protect_route(['domainadmin']);
|
||||
/*
|
||||
/ DOMAIN ADMIN
|
||||
*/
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$tfa_data = get_tfa();
|
||||
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$tfa_data = get_tfa();
|
||||
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
|
||||
$template = 'domainadmin.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
'acl_json' => json_encode($_SESSION['acl']),
|
||||
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
||||
'tfa_data' => $tfa_data,
|
||||
'fido2_data' => $fido2_data,
|
||||
'lang_user' => json_encode($lang['user']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
];
|
||||
$template = 'domainadmin.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
'acl_json' => json_encode($_SESSION['acl']),
|
||||
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
||||
'tfa_data' => $tfa_data,
|
||||
'fido2_data' => $fido2_data,
|
||||
'lang_user' => json_encode($lang['user']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
];
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
header('Location: /domainadmin');
|
||||
exit();
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/user.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
|
||||
protect_route();
|
||||
|
||||
$AuthUsers = array("admin", "domainadmin", "user");
|
||||
if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
|
||||
header('Location: /');
|
||||
exit();
|
||||
}
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
|
||||
$template = 'edit.twig';
|
||||
|
||||
@@ -129,16 +129,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
);
|
||||
}
|
||||
|
||||
// Check if domain is an alias domain and get target domain's MTA-STS
|
||||
$alias_domain_details = mailbox('get', 'alias_domain_details', $domain);
|
||||
$mta_sts_domain = $domain;
|
||||
|
||||
if ($alias_domain_details !== false && !empty($alias_domain_details['target_domain'])) {
|
||||
// This is an alias domain, check target domain for MTA-STS
|
||||
$mta_sts_domain = $alias_domain_details['target_domain'];
|
||||
}
|
||||
|
||||
$mta_sts = mailbox('get', 'mta_sts', $mta_sts_domain);
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
|
||||
if (!in_array($domain, $alias_domains)) {
|
||||
$records[] = array(
|
||||
|
||||
@@ -64,8 +64,6 @@ $globalVariables = [
|
||||
'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'],
|
||||
'pending_tfa_authmechs' => $pending_tfa_authmechs,
|
||||
'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'],
|
||||
'pending_tfa_setup' => !empty($_SESSION['pending_tfa_setup']),
|
||||
'pending_pw_update_modal' => !empty($_SESSION['pending_pw_update']),
|
||||
'lang_footer' => json_encode($lang['footer']),
|
||||
'lang_acl' => json_encode($lang['acl']),
|
||||
'lang_tfa' => json_encode($lang['tfa']),
|
||||
|
||||
@@ -121,56 +121,34 @@ function admin($_action, $_data = null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Check if this is a self password change via forced update
|
||||
if ($username == $_SESSION['mailcow_cc_username'] && !empty($_SESSION['pending_pw_update'])) {
|
||||
// Forced password update: only change password and clear force_pw_update flag
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $_data['password2']) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
unset($_SESSION['pending_pw_update']);
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Normal admin edit: update all attributes
|
||||
$force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0;
|
||||
$force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0;
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active,
|
||||
':force_tfa' => strval($force_tfa),
|
||||
':force_pw_update' => strval($force_pw_update)
|
||||
));
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active,
|
||||
':force_tfa' => strval($force_tfa),
|
||||
':force_pw_update' => strval($force_pw_update)
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
@@ -245,8 +223,7 @@ function admin($_action, $_data = null) {
|
||||
`tfa`.`active` AS `tfa_active`,
|
||||
`admin`.`username`,
|
||||
`admin`.`created`,
|
||||
`admin`.`active` AS `active`,
|
||||
`admin`.`attributes` AS `attributes`
|
||||
`admin`.`active` AS `active`
|
||||
FROM `admin`
|
||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username`
|
||||
WHERE `admin`.`username`= :admin AND `superadmin` = '1'");
|
||||
@@ -263,7 +240,6 @@ function admin($_action, $_data = null) {
|
||||
$admindata['active'] = $row['active'];
|
||||
$admindata['active_int'] = $row['active'];
|
||||
$admindata['created'] = $row['created'];
|
||||
$admindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
|
||||
return $admindata;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<?php
|
||||
function check_login($user, $pass, $extra = null) {
|
||||
function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
|
||||
global $pdo;
|
||||
global $redis;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$role = $extra['role'];
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
|
||||
// Try validate admin
|
||||
if (!isset($role) || $role == "admin") {
|
||||
@@ -26,20 +25,34 @@ function check_login($user, $pass, $extra = null) {
|
||||
|
||||
// Try validate app password
|
||||
if (!isset($role) || $role == "app") {
|
||||
$result = apppass_login($user, $pass, $extra);
|
||||
$result = apppass_login($user, $pass, $app_passwd_data);
|
||||
if ($result !== false) {
|
||||
if ($app_passwd_data['eas'] === true) {
|
||||
$service = 'EAS';
|
||||
} elseif ($app_passwd_data['dav'] === true) {
|
||||
$service = 'DAV';
|
||||
} else {
|
||||
$service = 'NONE';
|
||||
}
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $extra['service'], $pass);
|
||||
set_sasl_log($user, $real_rip, $service, $pass);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try validate user
|
||||
if (!isset($role) || $role == "user") {
|
||||
$result = user_login($user, $pass, $extra);
|
||||
$result = user_login($user, $pass);
|
||||
if ($result !== false) {
|
||||
if ($app_passwd_data['eas'] === true) {
|
||||
$service = 'EAS';
|
||||
} elseif ($app_passwd_data['dav'] === true) {
|
||||
$service = 'DAV';
|
||||
} else {
|
||||
$service = 'MAILCOWUI';
|
||||
}
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $extra['service']);
|
||||
set_sasl_log($user, $real_rip, $service);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -82,7 +95,7 @@ function admin_login($user, $pass){
|
||||
}
|
||||
|
||||
$user = strtolower(trim($user));
|
||||
$stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin`
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `superadmin` = '1'
|
||||
AND `active` = '1'
|
||||
AND `username` = :user");
|
||||
@@ -91,13 +104,6 @@ function admin_login($user, $pass){
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass)) {
|
||||
$admin_attrs = json_decode($row['attributes'], true) ?? [];
|
||||
|
||||
// Check force_pw_update
|
||||
if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) {
|
||||
$_SESSION['pending_pw_update'] = true;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
||||
@@ -117,10 +123,6 @@ function admin_login($user, $pass){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
@@ -146,7 +148,7 @@ function domainadmin_login($user, $pass){
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin`
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `superadmin` = '0'
|
||||
AND `active`='1'
|
||||
AND `username` = :user");
|
||||
@@ -155,13 +157,6 @@ function domainadmin_login($user, $pass){
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
$admin_attrs = json_decode($row['attributes'], true) ?? [];
|
||||
|
||||
// Check force_pw_update
|
||||
if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) {
|
||||
$_SESSION['pending_pw_update'] = true;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
||||
@@ -181,10 +176,6 @@ function domainadmin_login($user, $pass){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
@@ -202,7 +193,7 @@ function user_login($user, $pass, $extra = null){
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
$service = $extra['service'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
@@ -245,10 +236,10 @@ function user_login($user, $pass, $extra = null){
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($row)) {
|
||||
// check if user has access to service (imap, smtp, pop3, sieve, dav, eas) if service is set
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if ($extra['service'] != 'NONE') {
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
@@ -262,8 +253,8 @@ function user_login($user, $pass, $extra = null){
|
||||
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if ($extra['service'] != 'NONE') {
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
@@ -308,10 +299,6 @@ function user_login($user, $pass, $extra = null){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
|
||||
@@ -364,10 +351,6 @@ function user_login($user, $pass, $extra = null){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
|
||||
@@ -411,10 +394,6 @@ function user_login($user, $pass, $extra = null){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
|
||||
@@ -429,7 +408,7 @@ function user_login($user, $pass, $extra = null){
|
||||
|
||||
return false;
|
||||
}
|
||||
function apppass_login($user, $pass, $extra = null){
|
||||
function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
global $pdo;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
@@ -445,8 +424,20 @@ function apppass_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
if (!$is_internal && $extra['service'] == 'NONE') {
|
||||
$protocol = false;
|
||||
if ($app_passwd_data['eas']){
|
||||
$protocol = 'eas';
|
||||
} else if ($app_passwd_data['dav']){
|
||||
$protocol = 'dav';
|
||||
} else if ($app_passwd_data['smtp']){
|
||||
$protocol = 'smtp';
|
||||
} else if ($app_passwd_data['imap']){
|
||||
$protocol = 'imap';
|
||||
} else if ($app_passwd_data['sieve']){
|
||||
$protocol = 'sieve';
|
||||
} else if ($app_passwd_data['pop3']){
|
||||
$protocol = 'pop3';
|
||||
} else if (!$is_internal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -467,7 +458,7 @@ function apppass_login($user, $pass, $extra = null){
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
|
||||
if ($protocol && $row[$protocol . '_access'] != '1'){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -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?all=true');
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
|
||||
@@ -195,23 +195,17 @@ function domain_admin($_action, $_data = null) {
|
||||
));
|
||||
}
|
||||
}
|
||||
$force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0;
|
||||
$force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0;
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||
WHERE `username` = :username");
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active,
|
||||
':force_tfa' => strval($force_tfa),
|
||||
':force_pw_update' => strval($force_pw_update)
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
@@ -223,15 +217,11 @@ function domain_admin($_action, $_data = null) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||
WHERE `username` = :username");
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active,
|
||||
':force_tfa' => strval($force_tfa),
|
||||
':force_pw_update' => strval($force_pw_update)
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
@@ -254,37 +244,31 @@ function domain_admin($_action, $_data = null) {
|
||||
// Can only edit itself
|
||||
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$password_old = $_data['user_old_pass'] ?? '';
|
||||
$password_old = $_data['user_old_pass'];
|
||||
$password_new = $_data['user_new_pass'];
|
||||
$password_new2 = $_data['user_new_pass2'];
|
||||
|
||||
// Only verify old password if this is NOT a forced password update
|
||||
if (empty($_SESSION['pending_pw_update'])) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (password_check($password_new, $password_new2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password_new);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
|
||||
WHERE `username` = :username");
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
unset($_SESSION['pending_pw_update']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
@@ -376,11 +360,9 @@ function domain_admin($_action, $_data = null) {
|
||||
`tfa`.`active` AS `tfa_active`,
|
||||
`domain_admins`.`username`,
|
||||
`domain_admins`.`created`,
|
||||
`domain_admins`.`active` AS `active`,
|
||||
`admin`.`attributes` AS `attributes`
|
||||
`domain_admins`.`active` AS `active`
|
||||
FROM `domain_admins`
|
||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
||||
LEFT OUTER JOIN `admin` ON `admin`.`username`=`domain_admins`.`username`
|
||||
WHERE `domain_admins`.`username`= :domain_admin");
|
||||
$stmt->execute(array(
|
||||
':domain_admin' => $_data
|
||||
@@ -395,7 +377,6 @@ function domain_admin($_action, $_data = null) {
|
||||
$domainadmindata['active'] = $row['active'];
|
||||
$domainadmindata['active_int'] = $row['active'];
|
||||
$domainadmindata['created'] = $row['created'];
|
||||
$domainadmindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
|
||||
// GET SELECTED
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||
WHERE `domain` IN (
|
||||
|
||||
@@ -205,42 +205,6 @@ function password_complexity($_action, $_data = null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function password_generate(){
|
||||
$password_complexity = password_complexity('get');
|
||||
$min_length = max(16, intval($password_complexity['length']));
|
||||
|
||||
$lowercase = range('a', 'z');
|
||||
$uppercase = range('A', 'Z');
|
||||
$digits = range(0, 9);
|
||||
$special_chars = str_split('!@#$%^&*()?=');
|
||||
|
||||
$password = [
|
||||
$lowercase[random_int(0, count($lowercase) - 1)],
|
||||
$uppercase[random_int(0, count($uppercase) - 1)],
|
||||
$digits[random_int(0, count($digits) - 1)],
|
||||
$special_chars[random_int(0, count($special_chars) - 1)],
|
||||
];
|
||||
|
||||
$all = array_merge($lowercase, $uppercase, $digits, $special_chars);
|
||||
|
||||
while (count($password) < $min_length) {
|
||||
$password[] = $all[random_int(0, count($all) - 1)];
|
||||
}
|
||||
|
||||
// Cryptographically secure shuffle using Fisher-Yates algorithm
|
||||
$count = count($password);
|
||||
for ($i = $count - 1; $i > 0; $i--) {
|
||||
$j = random_int(0, $i);
|
||||
$temp = $password[$i];
|
||||
$password[$i] = $password[$j];
|
||||
$password[$j] = $temp;
|
||||
}
|
||||
|
||||
return implode('', $password);
|
||||
|
||||
}
|
||||
|
||||
function password_check($password1, $password2) {
|
||||
$password_complexity = password_complexity('get');
|
||||
|
||||
@@ -850,32 +814,6 @@ function verify_hash($hash, $password) {
|
||||
$hash = $components[4];
|
||||
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
|
||||
|
||||
case "PBKDF2-SHA512":
|
||||
// Handle FreeIPA-style hash: {PBKDF2-SHA512}10000$<base64_salt>$<base64_hash>
|
||||
$components = explode('$', $hash);
|
||||
if (count($components) !== 3) return false;
|
||||
|
||||
// 1st part: iteration count (integer)
|
||||
$iterations = intval($components[0]);
|
||||
if ($iterations <= 0) return false;
|
||||
|
||||
// 2nd part: salt (base64-encoded)
|
||||
$salt = $components[1];
|
||||
// 3rd part: hash (base64-encoded)
|
||||
$stored_hash_b64 = $components[2];
|
||||
|
||||
// Decode salt and hash from base64
|
||||
$salt_bin = base64_decode($salt, true);
|
||||
$hash_bin = base64_decode($stored_hash_b64, true);
|
||||
if ($salt_bin === false || $hash_bin === false) return false;
|
||||
// Get length of hash in bytes
|
||||
$hash_len = strlen($hash_bin);
|
||||
if ($hash_len === 0) return false;
|
||||
|
||||
// Calculate PBKDF2-SHA512 hash for provided password
|
||||
$test_hash = hash_pbkdf2('sha512', $password, $salt_bin, $iterations, $hash_len, true);
|
||||
return hash_equals($hash_bin, $test_hash);
|
||||
|
||||
case "PLAIN-MD4":
|
||||
return hash_equals(hash('md4', $password), $hash);
|
||||
|
||||
@@ -1033,24 +971,20 @@ function edit_user_account($_data) {
|
||||
}
|
||||
|
||||
// edit password
|
||||
$is_forced_pw_update = !empty($_SESSION['pending_pw_update']);
|
||||
if (((!empty($password_old) || $is_forced_pw_update) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2']))) {
|
||||
// Only verify old password if this is NOT a forced password update
|
||||
if (!$is_forced_pw_update) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user AND authsource = 'mailcow'");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user AND authsource = 'mailcow'");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$password_new = $_data['user_new_pass'];
|
||||
@@ -1214,52 +1148,50 @@ function set_tfa($_data) {
|
||||
global $iam_settings;
|
||||
|
||||
$_data_log = $_data;
|
||||
$access_denied = null;
|
||||
!isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
|
||||
// skip password check if this is a forced TFA enrollment after login
|
||||
if (!empty($_SESSION['pending_tfa_setup'])) {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
if (empty($username) || !isset($_SESSION['mailcow_cc_role'])) {
|
||||
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied');
|
||||
return false;
|
||||
// check for empty user and role
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
|
||||
|
||||
// check admin confirm password
|
||||
if ($access_denied === null) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
}
|
||||
} else {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$access_denied = null;
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
|
||||
|
||||
// check admin password
|
||||
if ($access_denied === null) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
// check mailbox confirm password
|
||||
if ($access_denied === null) {
|
||||
$stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if ($row['authsource'] == 'ldap'){
|
||||
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
} else {
|
||||
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check mailbox password
|
||||
if ($access_denied === null) {
|
||||
$stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if ($row['authsource'] == 'ldap'){
|
||||
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
} else {
|
||||
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($access_denied) {
|
||||
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied');
|
||||
return false;
|
||||
}
|
||||
// set access_denied error
|
||||
if ($access_denied){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($_data["tfa_method"]) {
|
||||
@@ -1312,7 +1244,6 @@ function set_tfa($_data) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
unset($_SESSION['pending_tfa_setup']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
@@ -1326,7 +1257,6 @@ function set_tfa($_data) {
|
||||
//$stmt->execute(array(':username' => $username));
|
||||
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
|
||||
$stmt->execute(array($username, $key_id, $_POST['totp_secret']));
|
||||
unset($_SESSION['pending_tfa_setup']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
@@ -1355,7 +1285,6 @@ function set_tfa($_data) {
|
||||
0
|
||||
));
|
||||
|
||||
unset($_SESSION['pending_tfa_setup']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
@@ -1363,25 +1292,6 @@ function set_tfa($_data) {
|
||||
);
|
||||
break;
|
||||
case "none":
|
||||
// Block TFA removal if force_tfa policy is active
|
||||
$is_forced_tfa = false;
|
||||
if ($_SESSION['mailcow_cc_role'] === 'user') {
|
||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
|
||||
$stmt_check->execute(array($username));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
} else {
|
||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
|
||||
$stmt_check->execute(array($username));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
}
|
||||
if ($is_forced_tfa) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'tfa_removal_blocked'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -1634,26 +1544,6 @@ function unset_tfa_key($_data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block key removal if force_tfa policy is active
|
||||
$is_forced_tfa = false;
|
||||
if ($_SESSION['mailcow_cc_role'] === 'user') {
|
||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
|
||||
$stmt_check->execute(array($username));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
} else {
|
||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
|
||||
$stmt_check->execute(array($username));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
}
|
||||
if ($is_forced_tfa) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'tfa_removal_blocked'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if it's last key
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
|
||||
WHERE `username` = :username AND `active` = '1'");
|
||||
@@ -1686,15 +1576,6 @@ function unset_tfa_key($_data) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function tfa_exists($username) {
|
||||
global $pdo;
|
||||
if (empty($username)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) as count FROM `tfa` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0;
|
||||
}
|
||||
function get_tfa($username = null, $id = null) {
|
||||
global $pdo;
|
||||
if (empty($username) && isset($_SESSION['mailcow_cc_username'])) {
|
||||
@@ -3497,49 +3378,6 @@ function set_user_loggedin_session($user) {
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
}
|
||||
function protect_route($allowed_roles = ['admin', 'domainadmin', 'user'], $redirects = []) {
|
||||
// Check if user is authenticated
|
||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
if (isset($redirects['unauthenticated'])) {
|
||||
header('Location: ' . $redirects['unauthenticated']);
|
||||
} else {
|
||||
header('Location: /');
|
||||
}
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check for pending actions (2FA setup, password update)
|
||||
if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) {
|
||||
$pending_redirect = '/';
|
||||
if ($_SESSION['mailcow_cc_role'] === 'admin') {
|
||||
$pending_redirect = '/admin';
|
||||
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
|
||||
$pending_redirect = '/domainadmin';
|
||||
}
|
||||
header('Location: ' . $pending_redirect);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check if user's role is in the allowed roles for the route
|
||||
if (!in_array($_SESSION['mailcow_cc_role'], $allowed_roles)) {
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
header('Location: /');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
function get_logs($application, $lines = false) {
|
||||
if ($lines === false) {
|
||||
$lines = $GLOBALS['LOG_LINES'] - 1;
|
||||
|
||||
@@ -49,12 +49,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
// Default to 1 yr
|
||||
$_data["validity"] = 8760;
|
||||
}
|
||||
if (isset($_data["permanent"]) && filter_var($_data["permanent"], FILTER_VALIDATE_BOOL)) {
|
||||
$permanent = 1;
|
||||
}
|
||||
else {
|
||||
$permanent = 0;
|
||||
}
|
||||
$domain = $_data['domain'];
|
||||
$description = $_data['description'];
|
||||
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
|
||||
@@ -71,14 +65,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
$validity = strtotime("+" . $_data["validity"] . " hour");
|
||||
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`, `permanent`) VALUES
|
||||
(:address, :description, :goto, :validity, :permanent)");
|
||||
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES
|
||||
(:address, :description, :goto, :validity)");
|
||||
$stmt->execute(array(
|
||||
':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain,
|
||||
':description' => $description,
|
||||
':goto' => $username,
|
||||
':validity' => $validity,
|
||||
':permanent' => $permanent
|
||||
':validity' => $validity
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
@@ -695,7 +688,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
|
||||
$internal = intval($_data['internal']);
|
||||
$active = intval($_data['active']);
|
||||
$sender_allowed = intval($_data['sender_allowed']);
|
||||
$sogo_visible = intval($_data['sogo_visible']);
|
||||
$goto_null = intval($_data['goto_null']);
|
||||
$goto_spam = intval($_data['goto_spam']);
|
||||
@@ -851,8 +843,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `sender_allowed`, `active`)
|
||||
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :sender_allowed, :active)");
|
||||
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`)
|
||||
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
|
||||
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
|
||||
$stmt->execute(array(
|
||||
':address' => '@'.$domain,
|
||||
@@ -863,7 +855,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => $domain,
|
||||
':sogo_visible' => $sogo_visible,
|
||||
':internal' => $internal,
|
||||
':sender_allowed' => $sender_allowed,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
@@ -876,7 +867,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => $domain,
|
||||
':sogo_visible' => $sogo_visible,
|
||||
':internal' => $internal,
|
||||
':sender_allowed' => $sender_allowed,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
@@ -1078,12 +1068,9 @@ 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']);
|
||||
$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
|
||||
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
||||
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
|
||||
$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
|
||||
@@ -1091,8 +1078,6 @@ 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']);
|
||||
@@ -1100,12 +1085,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
|
||||
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
||||
$force_pw_update = 0;
|
||||
$force_tfa = 0;
|
||||
}
|
||||
$mailbox_attrs = json_encode(
|
||||
array(
|
||||
'force_pw_update' => strval($force_pw_update),
|
||||
'force_tfa' => strval($force_tfa),
|
||||
'tls_enforce_in' => strval($tls_enforce_in),
|
||||
'tls_enforce_out' => strval($tls_enforce_out),
|
||||
'sogo_access' => strval($sogo_access),
|
||||
@@ -1113,8 +1096,6 @@ 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']),
|
||||
@@ -1723,7 +1704,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
|
||||
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
|
||||
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
||||
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
|
||||
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
|
||||
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1;
|
||||
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
||||
@@ -1734,16 +1714,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
|
||||
}
|
||||
else {
|
||||
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
|
||||
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
||||
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
||||
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
||||
$attr['eas_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
|
||||
$attr['dav_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
|
||||
}
|
||||
if (isset($_data['acl'])) {
|
||||
$_data['acl'] = (array)$_data['acl'];
|
||||
@@ -2127,23 +2103,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (empty($_data['validity']) && empty($_data['permanent'])) {
|
||||
if (empty($_data['validity'])) {
|
||||
continue;
|
||||
}
|
||||
if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) {
|
||||
$permanent = 1;
|
||||
$validity = 0;
|
||||
}
|
||||
else if (isset($_data['validity'])) {
|
||||
$permanent = 0;
|
||||
$validity = round((int)time() + ($_data['validity'] * 3600));
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity, `permanent` = :permanent WHERE
|
||||
$validity = round((int)time() + ($_data['validity'] * 3600));
|
||||
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE
|
||||
`address` = :address");
|
||||
$stmt->execute(array(
|
||||
':address' => $address,
|
||||
':validity' => $validity,
|
||||
':permanent' => $permanent
|
||||
':validity' => $validity
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
@@ -2518,7 +2486,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
if (!empty($is_now)) {
|
||||
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$sender_allowed = (isset($_data['sender_allowed'])) ? intval($_data['sender_allowed']) : $is_now['sender_allowed'];
|
||||
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
|
||||
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
|
||||
$goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
|
||||
@@ -2704,7 +2671,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`goto` = :goto,
|
||||
`sogo_visible`= :sogo_visible,
|
||||
`internal`= :internal,
|
||||
`sender_allowed`= :sender_allowed,
|
||||
`active`= :active
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
@@ -2715,7 +2681,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':goto' => $goto,
|
||||
':sogo_visible' => $sogo_visible,
|
||||
':internal' => $internal,
|
||||
':sender_allowed' => $sender_allowed,
|
||||
':active' => $active,
|
||||
':id' => $is_now['id']
|
||||
));
|
||||
@@ -3063,20 +3028,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
|
||||
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
|
||||
}
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
|
||||
(int)$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($is_now['attributes']['force_tfa']);
|
||||
(int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
|
||||
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
|
||||
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
|
||||
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
|
||||
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
|
||||
(int)$eas_access = (isset($_data['eas_access']) && hasACLAccess("protocol_access")) ? intval($_data['eas_access']) : intval($is_now['attributes']['eas_access']);
|
||||
(int)$dav_access = (isset($_data['dav_access']) && hasACLAccess("protocol_access")) ? intval($_data['dav_access']) : intval($is_now['attributes']['dav_access']);
|
||||
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
|
||||
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
|
||||
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
|
||||
@@ -3093,7 +3053,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
||||
$force_pw_update = 0;
|
||||
$force_tfa = 0;
|
||||
}
|
||||
$pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
|
||||
}
|
||||
@@ -3211,10 +3170,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
if (isset($_data['sender_acl'])) {
|
||||
// Get sender_acl items set by admin
|
||||
$current_sender_acls = mailbox('get', 'sender_acl_handles', $username);
|
||||
$sender_acl_admin = array_merge(
|
||||
$current_sender_acls['sender_acl_domains']['ro'],
|
||||
$current_sender_acls['sender_acl_addresses']['ro']
|
||||
mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'],
|
||||
mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro']
|
||||
);
|
||||
// Get sender_acl items from POST array
|
||||
// Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
|
||||
@@ -3302,25 +3260,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$sender_acl_handles = mailbox('get', 'sender_acl_handles', $username);
|
||||
$fixed_sender_aliases_allowed = $sender_acl_handles['fixed_sender_aliases_allowed'];
|
||||
$fixed_sender_aliases_blocked = $sender_acl_handles['fixed_sender_aliases_blocked'];
|
||||
|
||||
$fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases'];
|
||||
foreach ($sender_acl_merged as $sender_acl) {
|
||||
$domain = ltrim($sender_acl, '@');
|
||||
if (is_valid_domain_name($domain)) {
|
||||
$sender_acl = '@' . $domain;
|
||||
}
|
||||
|
||||
// Always add to sender_acl table to create explicit permission
|
||||
// Skip only if it's in allowed list (would be redundant)
|
||||
// But DO add if it's in blocked list (creates override)
|
||||
if (in_array($sender_acl, $fixed_sender_aliases_allowed)) {
|
||||
// Skip: already allowed by sender_allowed=1, no need for sender_acl entry
|
||||
// Don't add if allowed by alias
|
||||
if (in_array($sender_acl, $fixed_sender_aliases)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to sender_acl (either override for blocked aliases, or grant for selectable ones)
|
||||
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
|
||||
VALUES (:sender_acl, :username)");
|
||||
$stmt->execute(array(
|
||||
@@ -3365,15 +3314,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`quota` = :quota_b,
|
||||
`authsource` = :authsource,
|
||||
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
|
||||
`attributes` = JSON_SET(`attributes`, '$.force_tfa', :force_tfa),
|
||||
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
|
||||
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.eas_access', :eas_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.dav_access', :dav_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
|
||||
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
|
||||
WHERE `username` = :username");
|
||||
@@ -3383,14 +3329,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':quota_b' => $quota_b,
|
||||
':attribute_hash' => $attribute_hash,
|
||||
':force_pw_update' => $force_pw_update,
|
||||
':force_tfa' => $force_tfa,
|
||||
':sogo_access' => $sogo_access,
|
||||
':imap_access' => $imap_access,
|
||||
':pop3_access' => $pop3_access,
|
||||
':sieve_access' => $sieve_access,
|
||||
':smtp_access' => $smtp_access,
|
||||
':eas_access' => $eas_access,
|
||||
':dav_access' => $dav_access,
|
||||
':recovery_email' => $pw_recovery_email,
|
||||
':relayhost' => $relayhost,
|
||||
':username' => $username,
|
||||
@@ -3773,8 +3716,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
|
||||
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
|
||||
}
|
||||
else {
|
||||
foreach ($is_now as $key => $value){
|
||||
@@ -4204,22 +4145,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$data['sender_acl_addresses']['rw'] = array();
|
||||
$data['sender_acl_addresses']['selectable'] = array();
|
||||
$data['fixed_sender_aliases'] = array();
|
||||
$data['fixed_sender_aliases_allowed'] = array();
|
||||
$data['fixed_sender_aliases_blocked'] = array();
|
||||
$data['external_sender_aliases'] = array();
|
||||
// Fixed addresses - split by sender_allowed status
|
||||
$stmt = $pdo->prepare("SELECT `address`, `sender_allowed` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
|
||||
// Fixed addresses
|
||||
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
|
||||
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($rows)) {
|
||||
// Keep old array for backward compatibility
|
||||
$data['fixed_sender_aliases'][] = $row['address'];
|
||||
// Split into allowed/blocked for proper display
|
||||
if ($row['sender_allowed'] == '1') {
|
||||
$data['fixed_sender_aliases_allowed'][] = $row['address'];
|
||||
} else {
|
||||
$data['fixed_sender_aliases_blocked'][] = $row['address'];
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
|
||||
WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
|
||||
@@ -4652,12 +4584,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`description`,
|
||||
`validity`,
|
||||
`created`,
|
||||
`modified`,
|
||||
`permanent`
|
||||
`modified`
|
||||
FROM `spamalias`
|
||||
WHERE `goto` = :username
|
||||
AND (`validity` >= :unixnow
|
||||
OR `permanent` != 0)");
|
||||
AND `validity` >= :unixnow");
|
||||
$stmt->execute(array(':username' => $_data, ':unixnow' => time()));
|
||||
$tladata = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $tladata;
|
||||
@@ -4779,7 +4709,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`internal`,
|
||||
`active`,
|
||||
`sogo_visible`,
|
||||
`sender_allowed`,
|
||||
`created`,
|
||||
`modified`
|
||||
FROM `alias`
|
||||
@@ -4813,7 +4742,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$aliasdata['active_int'] = $row['active'];
|
||||
$aliasdata['sogo_visible'] = $row['sogo_visible'];
|
||||
$aliasdata['sogo_visible_int'] = $row['sogo_visible'];
|
||||
$aliasdata['sender_allowed'] = $row['sender_allowed'];
|
||||
$aliasdata['created'] = $row['created'];
|
||||
$aliasdata['modified'] = $row['modified'];
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
|
||||
@@ -5234,7 +5162,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username");
|
||||
$stmt->execute(array(':domain' => $row['domain'], ':username' => $_data));
|
||||
$MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND (`validity` >= :unixnow OR `permanent` != 0)");
|
||||
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow");
|
||||
$stmt->execute(array(':address' => $_data, ':unixnow' => time()));
|
||||
$SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
|
||||
|
||||
@@ -4,7 +4,7 @@ function init_db_schema()
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "19022026_1220";
|
||||
$db_version = "07102025_1015";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -76,8 +76,7 @@ function init_db_schema()
|
||||
"superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE NOW(0)",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"attributes" => "JSON"
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@@ -186,7 +185,6 @@ function init_db_schema()
|
||||
"public_comment" => "TEXT",
|
||||
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
@@ -556,8 +554,7 @@ function init_db_schema()
|
||||
"description" => "TEXT NOT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"validity" => "INT(11)",
|
||||
"permanent" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
||||
"validity" => "INT(11)"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@@ -1391,18 +1388,11 @@ function init_db_schema()
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.relayhost', \"0\") WHERE JSON_VALUE(`attributes`, '$.relayhost') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_tfa', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_tfa') IS NULL;");
|
||||
// admin attributes
|
||||
$pdo->query("UPDATE `admin` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;");
|
||||
$pdo->query("UPDATE `admin` SET `attributes` = JSON_SET(`attributes`, '$.force_tfa', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_tfa') IS NULL;");
|
||||
$pdo->query("UPDATE `admin` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.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;");
|
||||
@@ -1455,7 +1445,6 @@ function init_db_schema()
|
||||
"rl_frame" => "s",
|
||||
"rl_value" => "",
|
||||
"force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']),
|
||||
"force_tfa" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_tfa']),
|
||||
"sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']),
|
||||
"active" => 1,
|
||||
"tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']),
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"directorytree/ldaprecord": "^3.3",
|
||||
"twig/twig": "^3.0",
|
||||
"stevenmaguire/oauth2-keycloak": "^4.0",
|
||||
"league/oauth2-client": "^2.7",
|
||||
"bacon/bacon-qr-code": "^2.0"
|
||||
"league/oauth2-client": "^2.7"
|
||||
}
|
||||
}
|
||||
|
||||
424
data/web/inc/lib/composer.lock
generated
424
data/web/inc/lib/composer.lock
generated
@@ -4,62 +4,8 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "50fb4a320500820e36f30eabc45222a0",
|
||||
"content-hash": "8f5a147cdb147b935a158b86f47a4747",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"version": "2.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Bacon/BaconQrCode.git",
|
||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
|
||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dasprid/enum": "^1.0.3",
|
||||
"ext-iconv": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phly/keep-a-changelog": "^2.1",
|
||||
"phpunit/phpunit": "^7 | ^8 | ^9",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2.9",
|
||||
"squizlabs/php_codesniffer": "^3.4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BaconQrCode\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "BaconQrCode is a QR code generator for PHP.",
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"support": {
|
||||
"issues": "https://github.com/Bacon/BaconQrCode/issues",
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
|
||||
},
|
||||
"time": "2022-12-07T17:46:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bshaffer/oauth2-server-php",
|
||||
"version": "v1.11.1",
|
||||
@@ -191,56 +137,6 @@
|
||||
],
|
||||
"time": "2024-02-09T16:56:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dasprid/enum",
|
||||
"version": "1.0.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DASPRiD/Enum.git",
|
||||
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
|
||||
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1 <9.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
|
||||
"squizlabs/php_codesniffer": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DASPRiD\\Enum\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP 7.1 enum implementation",
|
||||
"keywords": [
|
||||
"enum",
|
||||
"map"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/DASPRiD/Enum/issues",
|
||||
"source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
|
||||
},
|
||||
"time": "2025-09-16T12:23:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ddeboer/imap",
|
||||
"version": "1.13.1",
|
||||
@@ -318,32 +214,30 @@
|
||||
},
|
||||
{
|
||||
"name": "directorytree/ldaprecord",
|
||||
"version": "v3.8.5",
|
||||
"version": "v2.20.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DirectoryTree/LdapRecord.git",
|
||||
"reference": "00e5f088f8c4028d5f398783cccc2e8119a27a65"
|
||||
"reference": "5bd0a5a9d257cf1049ae83055dbba4c3479ddf16"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/DirectoryTree/LdapRecord/zipball/00e5f088f8c4028d5f398783cccc2e8119a27a65",
|
||||
"reference": "00e5f088f8c4028d5f398783cccc2e8119a27a65",
|
||||
"url": "https://api.github.com/repos/DirectoryTree/LdapRecord/zipball/5bd0a5a9d257cf1049ae83055dbba4c3479ddf16",
|
||||
"reference": "5bd0a5a9d257cf1049ae83055dbba4c3479ddf16",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"ext-json": "*",
|
||||
"ext-ldap": "*",
|
||||
"illuminate/collections": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"nesbot/carbon": "*",
|
||||
"php": ">=8.1",
|
||||
"psr/log": "*",
|
||||
"psr/simple-cache": "^1.0|^2.0|^3.0"
|
||||
"illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
|
||||
"nesbot/carbon": "^1.0|^2.0",
|
||||
"php": ">=7.3",
|
||||
"psr/log": "^1.0|^2.0|^3.0",
|
||||
"psr/simple-cache": "^1.0|^2.0",
|
||||
"symfony/polyfill-php80": "^1.25",
|
||||
"tightenco/collect": "^5.6|^6.0|^7.0|^8.0|^9.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.21",
|
||||
"laravel/pint": "^1.6",
|
||||
"mockery/mockery": "^1.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"spatie/ray": "^1.24"
|
||||
@@ -390,7 +284,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-06T02:22:34+00:00"
|
||||
"time": "2023-10-11T16:34:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
@@ -783,107 +677,6 @@
|
||||
],
|
||||
"time": "2023-04-17T16:00:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/collections",
|
||||
"version": "v10.49.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/collections.git",
|
||||
"reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/collections/zipball/6ae9c74fa92d4e1824d1b346cd435e8eacdc3232",
|
||||
"reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/conditionable": "^10.0",
|
||||
"illuminate/contracts": "^10.0",
|
||||
"illuminate/macroable": "^10.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/var-dumper": "Required to use the dump method (^6.2)."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "10.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Illuminate\\Support\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The Illuminate Collections package.",
|
||||
"homepage": "https://laravel.com",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-09-08T19:05:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/conditionable",
|
||||
"version": "v10.49.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/conditionable.git",
|
||||
"reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/conditionable/zipball/47c700320b7a419f0d188d111f3bbed978fcbd3f",
|
||||
"reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "10.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Illuminate\\Support\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The Illuminate Conditionable package.",
|
||||
"homepage": "https://laravel.com",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-03-24T11:47:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/contracts",
|
||||
"version": "v10.44.0",
|
||||
@@ -932,52 +725,6 @@
|
||||
},
|
||||
"time": "2024-01-15T18:52:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/macroable",
|
||||
"version": "v10.49.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/macroable.git",
|
||||
"reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/macroable/zipball/dff667a46ac37b634dcf68909d9d41e94dc97c27",
|
||||
"reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "10.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Illuminate\\Support\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The Illuminate Macroable package.",
|
||||
"homepage": "https://laravel.com",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2023-06-05T12:46:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/oauth2-client",
|
||||
"version": "2.7.0",
|
||||
@@ -2705,6 +2452,145 @@
|
||||
],
|
||||
"time": "2023-12-26T14:02:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v6.4.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "0435a08f69125535336177c29d56af3abc1f69da"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/0435a08f69125535336177c29d56af3abc1f69da",
|
||||
"reference": "0435a08f69125535336177c29d56af3abc1f69da",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-iconv": "*",
|
||||
"symfony/console": "^5.4|^6.0|^7.0",
|
||||
"symfony/error-handler": "^6.3|^7.0",
|
||||
"symfony/http-kernel": "^5.4|^6.0|^7.0",
|
||||
"symfony/process": "^5.4|^6.0|^7.0",
|
||||
"symfony/uid": "^5.4|^6.0|^7.0",
|
||||
"twig/twig": "^2.13|^3.0.4"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/var-dump-server"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"Resources/functions/dump.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\VarDumper\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides mechanisms for walking through any arbitrary PHP variable",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"debug",
|
||||
"dump"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v6.4.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-01-23T14:53:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tightenco/collect",
|
||||
"version": "v9.52.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tighten/collect.git",
|
||||
"reference": "b15143cd11fe01a700fcc449df61adc64452fa6d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tighten/collect/zipball/b15143cd11fe01a700fcc449df61adc64452fa6d",
|
||||
"reference": "b15143cd11fe01a700fcc449df61adc64452fa6d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"symfony/var-dumper": "^3.4 || ^4.0 || ^5.0 || ^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.0",
|
||||
"nesbot/carbon": "^2.23.0",
|
||||
"phpunit/phpunit": "^8.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Collect/Support/helpers.php",
|
||||
"src/Collect/Support/alias.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Tightenco\\Collect\\": "src/Collect"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylorotwell@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Collect - Illuminate Collections as a separate package.",
|
||||
"keywords": [
|
||||
"collection",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/tighten/collect/issues",
|
||||
"source": "https://github.com/tighten/collect/tree/v9.52.7"
|
||||
},
|
||||
"time": "2023-04-14T21:51:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.14.0",
|
||||
@@ -2788,10 +2674,10 @@
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
Copyright (c) 2017, Ben Scholzen 'DASPRiD'
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,39 +0,0 @@
|
||||
# QR Code generator
|
||||
|
||||
[](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/Bacon/BaconQrCode)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
|
||||
|
||||
## Introduction
|
||||
BaconQrCode is a port of QR code portion of the ZXing library. It currently
|
||||
only features the encoder part, but could later receive the decoder part as
|
||||
well.
|
||||
|
||||
As the Reed Solomon codec implementation of the ZXing library performs quite
|
||||
slow in PHP, it was exchanged with the implementation by Phil Karn.
|
||||
|
||||
|
||||
## Example usage
|
||||
```php
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
|
||||
$renderer = new ImageRenderer(
|
||||
new RendererStyle(400),
|
||||
new ImagickImageBackEnd()
|
||||
);
|
||||
$writer = new Writer($renderer);
|
||||
$writer->writeFile('Hello World!', 'qrcode.png');
|
||||
```
|
||||
|
||||
## Available image renderer back ends
|
||||
BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
|
||||
|
||||
- `ImagickImageBackEnd`: renders raster images using the Imagick library
|
||||
- `SvgImageBackEnd`: renders SVG files using XMLWriter
|
||||
- `EpsImageBackEnd`: renders EPS files
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"description": "BaconQrCode is a QR code generator for PHP.",
|
||||
"license" : "BSD-2-Clause",
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"ext-iconv": "*",
|
||||
"dasprid/enum": "^1.0.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BaconQrCode\\": "src/"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7 | ^8 | ^9",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2.9",
|
||||
"squizlabs/php_codesniffer": "^3.4",
|
||||
"phly/keep-a-changelog": "^2.1"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"ocramius/package-versions": true
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"exclude": [
|
||||
"/test",
|
||||
"/phpunit.xml.dist"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="BaconQrCode Tests">
|
||||
<directory>./test</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -1,372 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* A simple, fast array of bits.
|
||||
*/
|
||||
final class BitArray
|
||||
{
|
||||
/**
|
||||
* Bits represented as an array of integers.
|
||||
*
|
||||
* @var SplFixedArray<int>
|
||||
*/
|
||||
private $bits;
|
||||
|
||||
/**
|
||||
* Size of the bit array in bits.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $size;
|
||||
|
||||
/**
|
||||
* Creates a new bit array with a given size.
|
||||
*/
|
||||
public function __construct(int $size = 0)
|
||||
{
|
||||
$this->size = $size;
|
||||
$this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size in bits.
|
||||
*/
|
||||
public function getSize() : int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size in bytes.
|
||||
*/
|
||||
public function getSizeInBytes() : int
|
||||
{
|
||||
return ($this->size + 7) >> 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the array has a minimum capacity.
|
||||
*/
|
||||
public function ensureCapacity(int $size) : void
|
||||
{
|
||||
if ($size > count($this->bits) << 5) {
|
||||
$this->bits->setSize(($size + 31) >> 5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific bit.
|
||||
*/
|
||||
public function get(int $i) : bool
|
||||
{
|
||||
return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific bit.
|
||||
*/
|
||||
public function set(int $i) : void
|
||||
{
|
||||
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips a specific bit.
|
||||
*/
|
||||
public function flip(int $i) : void
|
||||
{
|
||||
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next set bit position from a given position.
|
||||
*/
|
||||
public function getNextSet(int $from) : int
|
||||
{
|
||||
if ($from >= $this->size) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$bitsOffset = $from >> 5;
|
||||
$currentBits = $this->bits[$bitsOffset];
|
||||
$bitsLength = count($this->bits);
|
||||
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
|
||||
|
||||
while (0 === $currentBits) {
|
||||
if (++$bitsOffset === $bitsLength) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$currentBits = $this->bits[$bitsOffset];
|
||||
}
|
||||
|
||||
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
|
||||
return $result > $this->size ? $this->size : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next unset bit position from a given position.
|
||||
*/
|
||||
public function getNextUnset(int $from) : int
|
||||
{
|
||||
if ($from >= $this->size) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$bitsOffset = $from >> 5;
|
||||
$currentBits = ~$this->bits[$bitsOffset];
|
||||
$bitsLength = count($this->bits);
|
||||
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
|
||||
|
||||
while (0 === $currentBits) {
|
||||
if (++$bitsOffset === $bitsLength) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$currentBits = ~$this->bits[$bitsOffset];
|
||||
}
|
||||
|
||||
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
|
||||
return $result > $this->size ? $this->size : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a bulk of bits.
|
||||
*/
|
||||
public function setBulk(int $i, int $newBits) : void
|
||||
{
|
||||
$this->bits[$i >> 5] = $newBits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a range of bits.
|
||||
*
|
||||
* @throws InvalidArgumentException if end is smaller than start
|
||||
*/
|
||||
public function setRange(int $start, int $end) : void
|
||||
{
|
||||
if ($end < $start) {
|
||||
throw new InvalidArgumentException('End must be greater or equal to start');
|
||||
}
|
||||
|
||||
if ($end === $start) {
|
||||
return;
|
||||
}
|
||||
|
||||
--$end;
|
||||
|
||||
$firstInt = $start >> 5;
|
||||
$lastInt = $end >> 5;
|
||||
|
||||
for ($i = $firstInt; $i <= $lastInt; ++$i) {
|
||||
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
|
||||
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
|
||||
|
||||
if (0 === $firstBit && 31 === $lastBit) {
|
||||
$mask = 0x7fffffff;
|
||||
} else {
|
||||
$mask = 0;
|
||||
|
||||
for ($j = $firstBit; $j < $lastBit; ++$j) {
|
||||
$mask |= 1 << $j;
|
||||
}
|
||||
}
|
||||
|
||||
$this->bits[$i] = $this->bits[$i] | $mask;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the bit array, unsetting every bit.
|
||||
*/
|
||||
public function clear() : void
|
||||
{
|
||||
$bitsLength = count($this->bits);
|
||||
|
||||
for ($i = 0; $i < $bitsLength; ++$i) {
|
||||
$this->bits[$i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a range of bits is set or not set.
|
||||
|
||||
* @throws InvalidArgumentException if end is smaller than start
|
||||
*/
|
||||
public function isRange(int $start, int $end, bool $value) : bool
|
||||
{
|
||||
if ($end < $start) {
|
||||
throw new InvalidArgumentException('End must be greater or equal to start');
|
||||
}
|
||||
|
||||
if ($end === $start) {
|
||||
return true;
|
||||
}
|
||||
|
||||
--$end;
|
||||
|
||||
$firstInt = $start >> 5;
|
||||
$lastInt = $end >> 5;
|
||||
|
||||
for ($i = $firstInt; $i <= $lastInt; ++$i) {
|
||||
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
|
||||
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
|
||||
|
||||
if (0 === $firstBit && 31 === $lastBit) {
|
||||
$mask = 0x7fffffff;
|
||||
} else {
|
||||
$mask = 0;
|
||||
|
||||
for ($j = $firstBit; $j <= $lastBit; ++$j) {
|
||||
$mask |= 1 << $j;
|
||||
}
|
||||
}
|
||||
|
||||
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a bit to the array.
|
||||
*/
|
||||
public function appendBit(bool $bit) : void
|
||||
{
|
||||
$this->ensureCapacity($this->size + 1);
|
||||
|
||||
if ($bit) {
|
||||
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
|
||||
}
|
||||
|
||||
++$this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a number of bits (up to 32) to the array.
|
||||
|
||||
* @throws InvalidArgumentException if num bits is not between 0 and 32
|
||||
*/
|
||||
public function appendBits(int $value, int $numBits) : void
|
||||
{
|
||||
if ($numBits < 0 || $numBits > 32) {
|
||||
throw new InvalidArgumentException('Num bits must be between 0 and 32');
|
||||
}
|
||||
|
||||
$this->ensureCapacity($this->size + $numBits);
|
||||
|
||||
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
|
||||
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends another bit array to this array.
|
||||
*/
|
||||
public function appendBitArray(self $other) : void
|
||||
{
|
||||
$otherSize = $other->getSize();
|
||||
$this->ensureCapacity($this->size + $other->getSize());
|
||||
|
||||
for ($i = 0; $i < $otherSize; ++$i) {
|
||||
$this->appendBit($other->get($i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an exclusive-or comparision on the current bit array.
|
||||
*
|
||||
* @throws InvalidArgumentException if sizes don't match
|
||||
*/
|
||||
public function xorBits(self $other) : void
|
||||
{
|
||||
$bitsLength = count($this->bits);
|
||||
$otherBits = $other->getBitArray();
|
||||
|
||||
if ($bitsLength !== count($otherBits)) {
|
||||
throw new InvalidArgumentException('Sizes don\'t match');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $bitsLength; ++$i) {
|
||||
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the bit array to a byte array.
|
||||
*
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
|
||||
{
|
||||
$bytes = new SplFixedArray($numBytes);
|
||||
|
||||
for ($i = 0; $i < $numBytes; ++$i) {
|
||||
$byte = 0;
|
||||
|
||||
for ($j = 0; $j < 8; ++$j) {
|
||||
if ($this->get($bitOffset)) {
|
||||
$byte |= 1 << (7 - $j);
|
||||
}
|
||||
|
||||
++$bitOffset;
|
||||
}
|
||||
|
||||
$bytes[$i] = $byte;
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal bit array.
|
||||
*
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
public function getBitArray() : SplFixedArray
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the array.
|
||||
*/
|
||||
public function reverse() : void
|
||||
{
|
||||
$newBits = new SplFixedArray(count($this->bits));
|
||||
|
||||
for ($i = 0; $i < $this->size; ++$i) {
|
||||
if ($this->get($this->size - $i - 1)) {
|
||||
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
|
||||
}
|
||||
}
|
||||
|
||||
$this->bits = $newBits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the bit array.
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
for ($i = 0; $i < $this->size; ++$i) {
|
||||
if (0 === ($i & 0x07)) {
|
||||
$result .= ' ';
|
||||
}
|
||||
|
||||
$result .= $this->get($i) ? 'X' : '.';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* Bit matrix.
|
||||
*
|
||||
* Represents a 2D matrix of bits. In function arguments below, and throughout
|
||||
* the common module, x is the column position, and y is the row position. The
|
||||
* ordering is always x, y. The origin is at the top-left.
|
||||
*/
|
||||
class BitMatrix
|
||||
{
|
||||
/**
|
||||
* Width of the bit matrix.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $width;
|
||||
|
||||
/**
|
||||
* Height of the bit matrix.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $height;
|
||||
|
||||
/**
|
||||
* Size in bits of each individual row.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $rowSize;
|
||||
|
||||
/**
|
||||
* Bits representation.
|
||||
*
|
||||
* @var SplFixedArray<int>
|
||||
*/
|
||||
private $bits;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if a dimension is smaller than zero
|
||||
*/
|
||||
public function __construct(int $width, int $height = null)
|
||||
{
|
||||
if (null === $height) {
|
||||
$height = $width;
|
||||
}
|
||||
|
||||
if ($width < 1 || $height < 1) {
|
||||
throw new InvalidArgumentException('Both dimensions must be greater than zero');
|
||||
}
|
||||
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->rowSize = ($width + 31) >> 5;
|
||||
$this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the requested bit, where true means black.
|
||||
*/
|
||||
public function get(int $x, int $y) : bool
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given bit to true.
|
||||
*/
|
||||
public function set(int $x, int $y) : void
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips the given bit.
|
||||
*/
|
||||
public function flip(int $x, int $y) : void
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all bits (set to false).
|
||||
*/
|
||||
public function clear() : void
|
||||
{
|
||||
$max = count($this->bits);
|
||||
|
||||
for ($i = 0; $i < $max; ++$i) {
|
||||
$this->bits[$i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a square region of the bit matrix to true.
|
||||
*
|
||||
* @throws InvalidArgumentException if left or top are negative
|
||||
* @throws InvalidArgumentException if width or height are smaller than 1
|
||||
* @throws InvalidArgumentException if region does not fit into the matix
|
||||
*/
|
||||
public function setRegion(int $left, int $top, int $width, int $height) : void
|
||||
{
|
||||
if ($top < 0 || $left < 0) {
|
||||
throw new InvalidArgumentException('Left and top must be non-negative');
|
||||
}
|
||||
|
||||
if ($height < 1 || $width < 1) {
|
||||
throw new InvalidArgumentException('Width and height must be at least 1');
|
||||
}
|
||||
|
||||
$right = $left + $width;
|
||||
$bottom = $top + $height;
|
||||
|
||||
if ($bottom > $this->height || $right > $this->width) {
|
||||
throw new InvalidArgumentException('The region must fit inside the matrix');
|
||||
}
|
||||
|
||||
for ($y = $top; $y < $bottom; ++$y) {
|
||||
$offset = $y * $this->rowSize;
|
||||
|
||||
for ($x = $left; $x < $right; ++$x) {
|
||||
$index = $offset + ($x >> 5);
|
||||
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A fast method to retrieve one row of data from the matrix as a BitArray.
|
||||
*/
|
||||
public function getRow(int $y, BitArray $row = null) : BitArray
|
||||
{
|
||||
if (null === $row || $row->getSize() < $this->width) {
|
||||
$row = new BitArray($this->width);
|
||||
}
|
||||
|
||||
$offset = $y * $this->rowSize;
|
||||
|
||||
for ($x = 0; $x < $this->rowSize; ++$x) {
|
||||
$row->setBulk($x << 5, $this->bits[$offset + $x]);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a row of data from a BitArray.
|
||||
*/
|
||||
public function setRow(int $y, BitArray $row) : void
|
||||
{
|
||||
$bits = $row->getBitArray();
|
||||
|
||||
for ($i = 0; $i < $this->rowSize; ++$i) {
|
||||
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getEnclosingRectangle() : ?array
|
||||
{
|
||||
$left = $this->width;
|
||||
$top = $this->height;
|
||||
$right = -1;
|
||||
$bottom = -1;
|
||||
|
||||
for ($y = 0; $y < $this->height; ++$y) {
|
||||
for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
|
||||
$bits = $this->bits[$y * $this->rowSize + $x32];
|
||||
|
||||
if (0 !== $bits) {
|
||||
if ($y < $top) {
|
||||
$top = $y;
|
||||
}
|
||||
|
||||
if ($y > $bottom) {
|
||||
$bottom = $y;
|
||||
}
|
||||
|
||||
if ($x32 * 32 < $left) {
|
||||
$bit = 0;
|
||||
|
||||
while (($bits << (31 - $bit)) === 0) {
|
||||
$bit++;
|
||||
}
|
||||
|
||||
if (($x32 * 32 + $bit) < $left) {
|
||||
$left = $x32 * 32 + $bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($x32 * 32 + 31 > $right) {
|
||||
$bit = 31;
|
||||
|
||||
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
|
||||
--$bit;
|
||||
}
|
||||
|
||||
if (($x32 * 32 + $bit) > $right) {
|
||||
$right = $x32 * 32 + $bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$width = $right - $left;
|
||||
$height = $bottom - $top;
|
||||
|
||||
if ($width < 0 || $height < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$left, $top, $width, $height];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most top left set bit.
|
||||
*
|
||||
* This is useful in detecting a corner of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getTopLeftOnBit() : ?array
|
||||
{
|
||||
$bitsOffset = 0;
|
||||
|
||||
while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
|
||||
++$bitsOffset;
|
||||
}
|
||||
|
||||
if (count($this->bits) === $bitsOffset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = intdiv($bitsOffset, $this->rowSize);
|
||||
$y = ($bitsOffset % $this->rowSize) << 5;
|
||||
|
||||
$bits = $this->bits[$bitsOffset];
|
||||
$bit = 0;
|
||||
|
||||
while (0 === ($bits << (31 - $bit))) {
|
||||
++$bit;
|
||||
}
|
||||
|
||||
$x += $bit;
|
||||
|
||||
return [$x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most bottom right set bit.
|
||||
*
|
||||
* This is useful in detecting a corner of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getBottomRightOnBit() : ?array
|
||||
{
|
||||
$bitsOffset = count($this->bits) - 1;
|
||||
|
||||
while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
|
||||
--$bitsOffset;
|
||||
}
|
||||
|
||||
if ($bitsOffset < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = intdiv($bitsOffset, $this->rowSize);
|
||||
$y = ($bitsOffset % $this->rowSize) << 5;
|
||||
|
||||
$bits = $this->bits[$bitsOffset];
|
||||
$bit = 0;
|
||||
|
||||
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
|
||||
--$bit;
|
||||
}
|
||||
|
||||
$x += $bit;
|
||||
|
||||
return [$x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of the matrix,
|
||||
*/
|
||||
public function getWidth() : int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of the matrix.
|
||||
*/
|
||||
public function getHeight() : int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* General bit utilities.
|
||||
*
|
||||
* All utility methods are based on 32-bit integers and also work on 64-bit
|
||||
* systems.
|
||||
*/
|
||||
final class BitUtils
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an unsigned right shift.
|
||||
*
|
||||
* This is the same as the unsigned right shift operator ">>>" in other
|
||||
* languages.
|
||||
*/
|
||||
public static function unsignedRightShift(int $a, int $b) : int
|
||||
{
|
||||
return (
|
||||
$a >= 0
|
||||
? $a >> $b
|
||||
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of trailing zeros.
|
||||
*/
|
||||
public static function numberOfTrailingZeros(int $i) : int
|
||||
{
|
||||
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
|
||||
return $lastPos === false ? 32 : 31 - $lastPos;
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use DASPRiD\Enum\AbstractEnum;
|
||||
|
||||
/**
|
||||
* Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
|
||||
*
|
||||
* @method static self CP437()
|
||||
* @method static self ISO8859_1()
|
||||
* @method static self ISO8859_2()
|
||||
* @method static self ISO8859_3()
|
||||
* @method static self ISO8859_4()
|
||||
* @method static self ISO8859_5()
|
||||
* @method static self ISO8859_6()
|
||||
* @method static self ISO8859_7()
|
||||
* @method static self ISO8859_8()
|
||||
* @method static self ISO8859_9()
|
||||
* @method static self ISO8859_10()
|
||||
* @method static self ISO8859_11()
|
||||
* @method static self ISO8859_12()
|
||||
* @method static self ISO8859_13()
|
||||
* @method static self ISO8859_14()
|
||||
* @method static self ISO8859_15()
|
||||
* @method static self ISO8859_16()
|
||||
* @method static self SJIS()
|
||||
* @method static self CP1250()
|
||||
* @method static self CP1251()
|
||||
* @method static self CP1252()
|
||||
* @method static self CP1256()
|
||||
* @method static self UNICODE_BIG_UNMARKED()
|
||||
* @method static self UTF8()
|
||||
* @method static self ASCII()
|
||||
* @method static self BIG5()
|
||||
* @method static self GB18030()
|
||||
* @method static self EUC_KR()
|
||||
*/
|
||||
final class CharacterSetEci extends AbstractEnum
|
||||
{
|
||||
protected const CP437 = [[0, 2]];
|
||||
protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
|
||||
protected const ISO8859_2 = [[4], 'ISO-8859-2'];
|
||||
protected const ISO8859_3 = [[5], 'ISO-8859-3'];
|
||||
protected const ISO8859_4 = [[6], 'ISO-8859-4'];
|
||||
protected const ISO8859_5 = [[7], 'ISO-8859-5'];
|
||||
protected const ISO8859_6 = [[8], 'ISO-8859-6'];
|
||||
protected const ISO8859_7 = [[9], 'ISO-8859-7'];
|
||||
protected const ISO8859_8 = [[10], 'ISO-8859-8'];
|
||||
protected const ISO8859_9 = [[11], 'ISO-8859-9'];
|
||||
protected const ISO8859_10 = [[12], 'ISO-8859-10'];
|
||||
protected const ISO8859_11 = [[13], 'ISO-8859-11'];
|
||||
protected const ISO8859_12 = [[14], 'ISO-8859-12'];
|
||||
protected const ISO8859_13 = [[15], 'ISO-8859-13'];
|
||||
protected const ISO8859_14 = [[16], 'ISO-8859-14'];
|
||||
protected const ISO8859_15 = [[17], 'ISO-8859-15'];
|
||||
protected const ISO8859_16 = [[18], 'ISO-8859-16'];
|
||||
protected const SJIS = [[20], 'Shift_JIS'];
|
||||
protected const CP1250 = [[21], 'windows-1250'];
|
||||
protected const CP1251 = [[22], 'windows-1251'];
|
||||
protected const CP1252 = [[23], 'windows-1252'];
|
||||
protected const CP1256 = [[24], 'windows-1256'];
|
||||
protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
|
||||
protected const UTF8 = [[26], 'UTF-8'];
|
||||
protected const ASCII = [[27, 170], 'US-ASCII'];
|
||||
protected const BIG5 = [[28]];
|
||||
protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
|
||||
protected const EUC_KR = [[30], 'EUC-KR'];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $values;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $otherEncodingNames;
|
||||
|
||||
/**
|
||||
* @var array<int, self>|null
|
||||
*/
|
||||
private static $valueToEci;
|
||||
|
||||
/**
|
||||
* @var array<string, self>|null
|
||||
*/
|
||||
private static $nameToEci;
|
||||
|
||||
/**
|
||||
* @param int[] $values
|
||||
*/
|
||||
public function __construct(array $values, string ...$otherEncodingNames)
|
||||
{
|
||||
$this->values = $values;
|
||||
$this->otherEncodingNames = $otherEncodingNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary value.
|
||||
*/
|
||||
public function getValue() : int
|
||||
{
|
||||
return $this->values[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets character set ECI by value.
|
||||
*
|
||||
* Returns the representing ECI of a given value, or null if it is legal but unsupported.
|
||||
*
|
||||
* @throws InvalidArgumentException if value is not between 0 and 900
|
||||
*/
|
||||
public static function getCharacterSetEciByValue(int $value) : ?self
|
||||
{
|
||||
if ($value < 0 || $value >= 900) {
|
||||
throw new InvalidArgumentException('Value must be between 0 and 900');
|
||||
}
|
||||
|
||||
$valueToEci = self::valueToEci();
|
||||
|
||||
if (! array_key_exists($value, $valueToEci)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $valueToEci[$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns character set ECI by name.
|
||||
*
|
||||
* Returns the representing ECI of a given name, or null if it is legal but unsupported
|
||||
*/
|
||||
public static function getCharacterSetEciByName(string $name) : ?self
|
||||
{
|
||||
$nameToEci = self::nameToEci();
|
||||
$name = strtolower($name);
|
||||
|
||||
if (! array_key_exists($name, $nameToEci)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $nameToEci[$name];
|
||||
}
|
||||
|
||||
private static function valueToEci() : array
|
||||
{
|
||||
if (null !== self::$valueToEci) {
|
||||
return self::$valueToEci;
|
||||
}
|
||||
|
||||
self::$valueToEci = [];
|
||||
|
||||
foreach (self::values() as $eci) {
|
||||
foreach ($eci->values as $value) {
|
||||
self::$valueToEci[$value] = $eci;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$valueToEci;
|
||||
}
|
||||
|
||||
private static function nameToEci() : array
|
||||
{
|
||||
if (null !== self::$nameToEci) {
|
||||
return self::$nameToEci;
|
||||
}
|
||||
|
||||
self::$nameToEci = [];
|
||||
|
||||
foreach (self::values() as $eci) {
|
||||
self::$nameToEci[strtolower($eci->name())] = $eci;
|
||||
|
||||
foreach ($eci->otherEncodingNames as $name) {
|
||||
self::$nameToEci[strtolower($name)] = $eci;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$nameToEci;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* Encapsulates the parameters for one error-correction block in one symbol version.
|
||||
*
|
||||
* This includes the number of data codewords, and the number of times a block with these parameters is used
|
||||
* consecutively in the QR code version's format.
|
||||
*/
|
||||
final class EcBlock
|
||||
{
|
||||
/**
|
||||
* How many times the block is used.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $count;
|
||||
|
||||
/**
|
||||
* Number of data codewords.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $dataCodewords;
|
||||
|
||||
public function __construct(int $count, int $dataCodewords)
|
||||
{
|
||||
$this->count = $count;
|
||||
$this->dataCodewords = $dataCodewords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many times the block is used.
|
||||
*/
|
||||
public function getCount() : int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of data codewords.
|
||||
*/
|
||||
public function getDataCodewords() : int
|
||||
{
|
||||
return $this->dataCodewords;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* Encapsulates a set of error-correction blocks in one symbol version.
|
||||
*
|
||||
* Most versions will use blocks of differing sizes within one version, so, this encapsulates the parameters for each
|
||||
* set of blocks. It also holds the number of error-correction codewords per block since it will be the same across all
|
||||
* blocks within one version.
|
||||
*/
|
||||
final class EcBlocks
|
||||
{
|
||||
/**
|
||||
* Number of EC codewords per block.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $ecCodewordsPerBlock;
|
||||
|
||||
/**
|
||||
* List of EC blocks.
|
||||
*
|
||||
* @var EcBlock[]
|
||||
*/
|
||||
private $ecBlocks;
|
||||
|
||||
public function __construct(int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
|
||||
{
|
||||
$this->ecCodewordsPerBlock = $ecCodewordsPerBlock;
|
||||
$this->ecBlocks = $ecBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of EC codewords per block.
|
||||
*/
|
||||
public function getEcCodewordsPerBlock() : int
|
||||
{
|
||||
return $this->ecCodewordsPerBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of EC block appearances.
|
||||
*/
|
||||
public function getNumBlocks() : int
|
||||
{
|
||||
$total = 0;
|
||||
|
||||
foreach ($this->ecBlocks as $ecBlock) {
|
||||
$total += $ecBlock->getCount();
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total count of EC codewords.
|
||||
*/
|
||||
public function getTotalEcCodewords() : int
|
||||
{
|
||||
return $this->ecCodewordsPerBlock * $this->getNumBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the EC blocks included in this collection.
|
||||
*
|
||||
* @return EcBlock[]
|
||||
*/
|
||||
public function getEcBlocks() : array
|
||||
{
|
||||
return $this->ecBlocks;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\OutOfBoundsException;
|
||||
use DASPRiD\Enum\AbstractEnum;
|
||||
|
||||
/**
|
||||
* Enum representing the four error correction levels.
|
||||
*
|
||||
* @method static self L() ~7% correction
|
||||
* @method static self M() ~15% correction
|
||||
* @method static self Q() ~25% correction
|
||||
* @method static self H() ~30% correction
|
||||
*/
|
||||
final class ErrorCorrectionLevel extends AbstractEnum
|
||||
{
|
||||
protected const L = [0x01];
|
||||
protected const M = [0x00];
|
||||
protected const Q = [0x03];
|
||||
protected const H = [0x02];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $bits;
|
||||
|
||||
protected function __construct(int $bits)
|
||||
{
|
||||
$this->bits = $bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OutOfBoundsException if number of bits is invalid
|
||||
*/
|
||||
public static function forBits(int $bits) : self
|
||||
{
|
||||
switch ($bits) {
|
||||
case 0:
|
||||
return self::M();
|
||||
|
||||
case 1:
|
||||
return self::L();
|
||||
|
||||
case 2:
|
||||
return self::H();
|
||||
|
||||
case 3:
|
||||
return self::Q();
|
||||
}
|
||||
|
||||
throw new OutOfBoundsException('Invalid number of bits');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the two bits used to encode this error correction level.
|
||||
*/
|
||||
public function getBits() : int
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* BaconQrCode
|
||||
*
|
||||
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
|
||||
* @copyright 2013 Ben 'DASPRiD' Scholzen
|
||||
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
|
||||
*/
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* Encapsulates a QR Code's format information, including the data mask used and error correction level.
|
||||
*/
|
||||
class FormatInformation
|
||||
{
|
||||
/**
|
||||
* Mask for format information.
|
||||
*/
|
||||
private const FORMAT_INFO_MASK_QR = 0x5412;
|
||||
|
||||
/**
|
||||
* Lookup table for decoding format information.
|
||||
*
|
||||
* See ISO 18004:2006, Annex C, Table C.1
|
||||
*/
|
||||
private const FORMAT_INFO_DECODE_LOOKUP = [
|
||||
[0x5412, 0x00],
|
||||
[0x5125, 0x01],
|
||||
[0x5e7c, 0x02],
|
||||
[0x5b4b, 0x03],
|
||||
[0x45f9, 0x04],
|
||||
[0x40ce, 0x05],
|
||||
[0x4f97, 0x06],
|
||||
[0x4aa0, 0x07],
|
||||
[0x77c4, 0x08],
|
||||
[0x72f3, 0x09],
|
||||
[0x7daa, 0x0a],
|
||||
[0x789d, 0x0b],
|
||||
[0x662f, 0x0c],
|
||||
[0x6318, 0x0d],
|
||||
[0x6c41, 0x0e],
|
||||
[0x6976, 0x0f],
|
||||
[0x1689, 0x10],
|
||||
[0x13be, 0x11],
|
||||
[0x1ce7, 0x12],
|
||||
[0x19d0, 0x13],
|
||||
[0x0762, 0x14],
|
||||
[0x0255, 0x15],
|
||||
[0x0d0c, 0x16],
|
||||
[0x083b, 0x17],
|
||||
[0x355f, 0x18],
|
||||
[0x3068, 0x19],
|
||||
[0x3f31, 0x1a],
|
||||
[0x3a06, 0x1b],
|
||||
[0x24b4, 0x1c],
|
||||
[0x2183, 0x1d],
|
||||
[0x2eda, 0x1e],
|
||||
[0x2bed, 0x1f],
|
||||
];
|
||||
|
||||
/**
|
||||
* Offset i holds the number of 1 bits in the binary representation of i.
|
||||
*
|
||||
* @var int[]
|
||||
*/
|
||||
private const BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
|
||||
|
||||
/**
|
||||
* Error correction level.
|
||||
*
|
||||
* @var ErrorCorrectionLevel
|
||||
*/
|
||||
private $ecLevel;
|
||||
|
||||
/**
|
||||
* Data mask.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $dataMask;
|
||||
|
||||
protected function __construct(int $formatInfo)
|
||||
{
|
||||
$this->ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3);
|
||||
$this->dataMask = $formatInfo & 0x7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks how many bits are different between two integers.
|
||||
*/
|
||||
public static function numBitsDiffering(int $a, int $b) : int
|
||||
{
|
||||
$a ^= $b;
|
||||
|
||||
return (
|
||||
self::BITS_SET_IN_HALF_BYTE[$a & 0xf]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes format information.
|
||||
*/
|
||||
public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
|
||||
{
|
||||
$formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2);
|
||||
|
||||
if (null !== $formatInfo) {
|
||||
return $formatInfo;
|
||||
}
|
||||
|
||||
// Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the
|
||||
// pattern first.
|
||||
return self::doDecodeFormatInformation(
|
||||
$maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR,
|
||||
$maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method for decoding format information.
|
||||
*/
|
||||
private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
|
||||
{
|
||||
$bestDifference = PHP_INT_MAX;
|
||||
$bestFormatInfo = 0;
|
||||
|
||||
foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) {
|
||||
$targetInfo = $decodeInfo[0];
|
||||
|
||||
if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) {
|
||||
// Found an exact match
|
||||
return new self($decodeInfo[1]);
|
||||
}
|
||||
|
||||
$bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo);
|
||||
|
||||
if ($bitsDifference < $bestDifference) {
|
||||
$bestFormatInfo = $decodeInfo[1];
|
||||
$bestDifference = $bitsDifference;
|
||||
}
|
||||
|
||||
if ($maskedFormatInfo1 !== $maskedFormatInfo2) {
|
||||
// Also try the other option
|
||||
$bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo);
|
||||
|
||||
if ($bitsDifference < $bestDifference) {
|
||||
$bestFormatInfo = $decodeInfo[1];
|
||||
$bestDifference = $bitsDifference;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match.
|
||||
if ($bestDifference <= 3) {
|
||||
return new self($bestFormatInfo);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error correction level.
|
||||
*/
|
||||
public function getErrorCorrectionLevel() : ErrorCorrectionLevel
|
||||
{
|
||||
return $this->ecLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data mask.
|
||||
*/
|
||||
public function getDataMask() : int
|
||||
{
|
||||
return $this->dataMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the code of the EC level.
|
||||
*/
|
||||
public function hashCode() : int
|
||||
{
|
||||
return ($this->ecLevel->getBits() << 3) | $this->dataMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if this instance equals another one.
|
||||
*/
|
||||
public function equals(self $other) : bool
|
||||
{
|
||||
return (
|
||||
$this->ecLevel === $other->ecLevel
|
||||
&& $this->dataMask === $other->dataMask
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use DASPRiD\Enum\AbstractEnum;
|
||||
|
||||
/**
|
||||
* Enum representing various modes in which data can be encoded to bits.
|
||||
*
|
||||
* @method static self TERMINATOR()
|
||||
* @method static self NUMERIC()
|
||||
* @method static self ALPHANUMERIC()
|
||||
* @method static self STRUCTURED_APPEND()
|
||||
* @method static self BYTE()
|
||||
* @method static self ECI()
|
||||
* @method static self KANJI()
|
||||
* @method static self FNC1_FIRST_POSITION()
|
||||
* @method static self FNC1_SECOND_POSITION()
|
||||
* @method static self HANZI()
|
||||
*/
|
||||
final class Mode extends AbstractEnum
|
||||
{
|
||||
protected const TERMINATOR = [[0, 0, 0], 0x00];
|
||||
protected const NUMERIC = [[10, 12, 14], 0x01];
|
||||
protected const ALPHANUMERIC = [[9, 11, 13], 0x02];
|
||||
protected const STRUCTURED_APPEND = [[0, 0, 0], 0x03];
|
||||
protected const BYTE = [[8, 16, 16], 0x04];
|
||||
protected const ECI = [[0, 0, 0], 0x07];
|
||||
protected const KANJI = [[8, 10, 12], 0x08];
|
||||
protected const FNC1_FIRST_POSITION = [[0, 0, 0], 0x05];
|
||||
protected const FNC1_SECOND_POSITION = [[0, 0, 0], 0x09];
|
||||
protected const HANZI = [[8, 10, 12], 0x0d];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $characterCountBitsForVersions;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $bits;
|
||||
|
||||
/**
|
||||
* @param int[] $characterCountBitsForVersions
|
||||
*/
|
||||
protected function __construct(array $characterCountBitsForVersions, int $bits)
|
||||
{
|
||||
$this->characterCountBitsForVersions = $characterCountBitsForVersions;
|
||||
$this->bits = $bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bits used in a specific QR code version.
|
||||
*/
|
||||
public function getCharacterCountBits(Version $version) : int
|
||||
{
|
||||
$number = $version->getVersionNumber();
|
||||
|
||||
if ($number <= 9) {
|
||||
$offset = 0;
|
||||
} elseif ($number <= 26) {
|
||||
$offset = 1;
|
||||
} else {
|
||||
$offset = 2;
|
||||
}
|
||||
|
||||
return $this->characterCountBitsForVersions[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the four bits used to encode this mode.
|
||||
*/
|
||||
public function getBits() : int
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
}
|
||||
@@ -1,468 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* Reed-Solomon codec for 8-bit characters.
|
||||
*
|
||||
* Based on libfec by Phil Karn, KA9Q.
|
||||
*/
|
||||
final class ReedSolomonCodec
|
||||
{
|
||||
/**
|
||||
* Symbol size in bits.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $symbolSize;
|
||||
|
||||
/**
|
||||
* Block size in symbols.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $blockSize;
|
||||
|
||||
/**
|
||||
* First root of RS code generator polynomial, index form.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $firstRoot;
|
||||
|
||||
/**
|
||||
* Primitive element to generate polynomial roots, index form.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $primitive;
|
||||
|
||||
/**
|
||||
* Prim-th root of 1, index form.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $iPrimitive;
|
||||
|
||||
/**
|
||||
* RS code generator polynomial degree (number of roots).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $numRoots;
|
||||
|
||||
/**
|
||||
* Padding bytes at front of shortened block.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $padding;
|
||||
|
||||
/**
|
||||
* Log lookup table.
|
||||
*
|
||||
* @var SplFixedArray
|
||||
*/
|
||||
private $alphaTo;
|
||||
|
||||
/**
|
||||
* Anti-Log lookup table.
|
||||
*
|
||||
* @var SplFixedArray
|
||||
*/
|
||||
private $indexOf;
|
||||
|
||||
/**
|
||||
* Generator polynomial.
|
||||
*
|
||||
* @var SplFixedArray
|
||||
*/
|
||||
private $generatorPoly;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if symbol size ist not between 0 and 8
|
||||
* @throws InvalidArgumentException if first root is invalid
|
||||
* @throws InvalidArgumentException if num roots is invalid
|
||||
* @throws InvalidArgumentException if padding is invalid
|
||||
* @throws RuntimeException if field generator polynomial is not primitive
|
||||
*/
|
||||
public function __construct(
|
||||
int $symbolSize,
|
||||
int $gfPoly,
|
||||
int $firstRoot,
|
||||
int $primitive,
|
||||
int $numRoots,
|
||||
int $padding
|
||||
) {
|
||||
if ($symbolSize < 0 || $symbolSize > 8) {
|
||||
throw new InvalidArgumentException('Symbol size must be between 0 and 8');
|
||||
}
|
||||
|
||||
if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) {
|
||||
throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize));
|
||||
}
|
||||
|
||||
if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) {
|
||||
throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize));
|
||||
}
|
||||
|
||||
if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots)
|
||||
);
|
||||
}
|
||||
|
||||
$this->symbolSize = $symbolSize;
|
||||
$this->blockSize = (1 << $symbolSize) - 1;
|
||||
$this->padding = $padding;
|
||||
$this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
|
||||
$this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
|
||||
|
||||
// Generate galous field lookup table
|
||||
$this->indexOf[0] = $this->blockSize;
|
||||
$this->alphaTo[$this->blockSize] = 0;
|
||||
|
||||
$sr = 1;
|
||||
|
||||
for ($i = 0; $i < $this->blockSize; ++$i) {
|
||||
$this->indexOf[$sr] = $i;
|
||||
$this->alphaTo[$i] = $sr;
|
||||
|
||||
$sr <<= 1;
|
||||
|
||||
if ($sr & (1 << $symbolSize)) {
|
||||
$sr ^= $gfPoly;
|
||||
}
|
||||
|
||||
$sr &= $this->blockSize;
|
||||
}
|
||||
|
||||
if (1 !== $sr) {
|
||||
throw new RuntimeException('Field generator polynomial is not primitive');
|
||||
}
|
||||
|
||||
// Form RS code generator polynomial from its roots
|
||||
$this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false);
|
||||
$this->firstRoot = $firstRoot;
|
||||
$this->primitive = $primitive;
|
||||
$this->numRoots = $numRoots;
|
||||
|
||||
// Find prim-th root of 1, used in decoding
|
||||
for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) {
|
||||
}
|
||||
|
||||
$this->iPrimitive = intdiv($iPrimitive, $primitive);
|
||||
|
||||
$this->generatorPoly[0] = 1;
|
||||
|
||||
for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) {
|
||||
$this->generatorPoly[$i + 1] = 1;
|
||||
|
||||
for ($j = $i; $j > 0; $j--) {
|
||||
if ($this->generatorPoly[$j] !== 0) {
|
||||
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[
|
||||
$this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)
|
||||
];
|
||||
} else {
|
||||
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1];
|
||||
}
|
||||
}
|
||||
|
||||
$this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)];
|
||||
}
|
||||
|
||||
// Convert generator poly to index form for quicker encoding
|
||||
for ($i = 0; $i <= $numRoots; ++$i) {
|
||||
$this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes data and writes result back into parity array.
|
||||
*/
|
||||
public function encode(SplFixedArray $data, SplFixedArray $parity) : void
|
||||
{
|
||||
for ($i = 0; $i < $this->numRoots; ++$i) {
|
||||
$parity[$i] = 0;
|
||||
}
|
||||
|
||||
$iterations = $this->blockSize - $this->numRoots - $this->padding;
|
||||
|
||||
for ($i = 0; $i < $iterations; ++$i) {
|
||||
$feedback = $this->indexOf[$data[$i] ^ $parity[0]];
|
||||
|
||||
if ($feedback !== $this->blockSize) {
|
||||
// Feedback term is non-zero
|
||||
$feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback);
|
||||
|
||||
for ($j = 1; $j < $this->numRoots; ++$j) {
|
||||
$parity[$j] = $parity[$j] ^ $this->alphaTo[
|
||||
$this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
for ($j = 0; $j < $this->numRoots - 1; ++$j) {
|
||||
$parity[$j] = $parity[$j + 1];
|
||||
}
|
||||
|
||||
if ($feedback !== $this->blockSize) {
|
||||
$parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])];
|
||||
} else {
|
||||
$parity[$this->numRoots - 1] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes received data.
|
||||
*/
|
||||
public function decode(SplFixedArray $data, SplFixedArray $erasures = null) : ?int
|
||||
{
|
||||
// This speeds up the initialization a bit.
|
||||
$numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false);
|
||||
$numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false);
|
||||
|
||||
$lambda = clone $numRootsPlusOne;
|
||||
$b = clone $numRootsPlusOne;
|
||||
$t = clone $numRootsPlusOne;
|
||||
$omega = clone $numRootsPlusOne;
|
||||
$root = clone $numRoots;
|
||||
$loc = clone $numRoots;
|
||||
|
||||
$numErasures = (null !== $erasures ? count($erasures) : 0);
|
||||
|
||||
// Form the Syndromes; i.e., evaluate data(x) at roots of g(x)
|
||||
$syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false);
|
||||
|
||||
for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) {
|
||||
for ($j = 0; $j < $this->numRoots; ++$j) {
|
||||
if ($syndromes[$j] === 0) {
|
||||
$syndromes[$j] = $data[$i];
|
||||
} else {
|
||||
$syndromes[$j] = $data[$i] ^ $this->alphaTo[
|
||||
$this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert syndromes to index form, checking for nonzero conditions
|
||||
$syndromeError = 0;
|
||||
|
||||
for ($i = 0; $i < $this->numRoots; ++$i) {
|
||||
$syndromeError |= $syndromes[$i];
|
||||
$syndromes[$i] = $this->indexOf[$syndromes[$i]];
|
||||
}
|
||||
|
||||
if (! $syndromeError) {
|
||||
// If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[]
|
||||
// unmodified.
|
||||
return 0;
|
||||
}
|
||||
|
||||
$lambda[0] = 1;
|
||||
|
||||
if ($numErasures > 0) {
|
||||
// Init lambda to be the erasure locator polynomial
|
||||
$lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))];
|
||||
|
||||
for ($i = 1; $i < $numErasures; ++$i) {
|
||||
$u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i]));
|
||||
|
||||
for ($j = $i + 1; $j > 0; --$j) {
|
||||
$tmp = $this->indexOf[$lambda[$j - 1]];
|
||||
|
||||
if ($tmp !== $this->blockSize) {
|
||||
$lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i <= $this->numRoots; ++$i) {
|
||||
$b[$i] = $this->indexOf[$lambda[$i]];
|
||||
}
|
||||
|
||||
// Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial
|
||||
$r = $numErasures;
|
||||
$el = $numErasures;
|
||||
|
||||
while (++$r <= $this->numRoots) {
|
||||
// Compute discrepancy at the r-th step in poly form
|
||||
$discrepancyR = 0;
|
||||
|
||||
for ($i = 0; $i < $r; ++$i) {
|
||||
if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) {
|
||||
$discrepancyR ^= $this->alphaTo[
|
||||
$this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$discrepancyR = $this->indexOf[$discrepancyR];
|
||||
|
||||
if ($discrepancyR === $this->blockSize) {
|
||||
$tmp = $b->toArray();
|
||||
array_unshift($tmp, $this->blockSize);
|
||||
array_pop($tmp);
|
||||
$b = SplFixedArray::fromArray($tmp, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
$t[0] = $lambda[0];
|
||||
|
||||
for ($i = 0; $i < $this->numRoots; ++$i) {
|
||||
if ($b[$i] !== $this->blockSize) {
|
||||
$t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])];
|
||||
} else {
|
||||
$t[$i + 1] = $lambda[$i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (2 * $el <= $r + $numErasures - 1) {
|
||||
$el = $r + $numErasures - $el;
|
||||
|
||||
for ($i = 0; $i <= $this->numRoots; ++$i) {
|
||||
$b[$i] = (
|
||||
$lambda[$i] === 0
|
||||
? $this->blockSize
|
||||
: $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$tmp = $b->toArray();
|
||||
array_unshift($tmp, $this->blockSize);
|
||||
array_pop($tmp);
|
||||
$b = SplFixedArray::fromArray($tmp, false);
|
||||
}
|
||||
|
||||
$lambda = clone $t;
|
||||
}
|
||||
|
||||
// Convert lambda to index form and compute deg(lambda(x))
|
||||
$degLambda = 0;
|
||||
|
||||
for ($i = 0; $i <= $this->numRoots; ++$i) {
|
||||
$lambda[$i] = $this->indexOf[$lambda[$i]];
|
||||
|
||||
if ($lambda[$i] !== $this->blockSize) {
|
||||
$degLambda = $i;
|
||||
}
|
||||
}
|
||||
|
||||
// Find roots of the error+erasure locator polynomial by Chien search.
|
||||
$reg = clone $lambda;
|
||||
$reg[0] = 0;
|
||||
$count = 0;
|
||||
$i = 1;
|
||||
|
||||
for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) {
|
||||
$q = 1;
|
||||
|
||||
for ($j = $degLambda; $j > 0; $j--) {
|
||||
if ($reg[$j] !== $this->blockSize) {
|
||||
$reg[$j] = $this->modNn($reg[$j] + $j);
|
||||
$q ^= $this->alphaTo[$reg[$j]];
|
||||
}
|
||||
}
|
||||
|
||||
if ($q !== 0) {
|
||||
// Not a root
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store root (index-form) and error location number
|
||||
$root[$count] = $i;
|
||||
$loc[$count] = $k;
|
||||
|
||||
if (++$count === $degLambda) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($degLambda !== $count) {
|
||||
// deg(lambda) unequal to number of roots: uncorrectable error detected
|
||||
return null;
|
||||
}
|
||||
|
||||
// Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find
|
||||
// deg(omega).
|
||||
$degOmega = $degLambda - 1;
|
||||
|
||||
for ($i = 0; $i <= $degOmega; ++$i) {
|
||||
$tmp = 0;
|
||||
|
||||
for ($j = $i; $j >= 0; --$j) {
|
||||
if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) {
|
||||
$tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])];
|
||||
}
|
||||
}
|
||||
|
||||
$omega[$i] = $this->indexOf[$tmp];
|
||||
}
|
||||
|
||||
// Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and
|
||||
// den = lambda_pr(inv(X(l))) all in poly form.
|
||||
for ($j = $count - 1; $j >= 0; --$j) {
|
||||
$num1 = 0;
|
||||
|
||||
for ($i = $degOmega; $i >= 0; $i--) {
|
||||
if ($omega[$i] !== $this->blockSize) {
|
||||
$num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])];
|
||||
}
|
||||
}
|
||||
|
||||
$num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)];
|
||||
$den = 0;
|
||||
|
||||
// lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i]
|
||||
for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) {
|
||||
if ($lambda[$i + 1] !== $this->blockSize) {
|
||||
$den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply error to data
|
||||
if ($num1 !== 0 && $loc[$j] >= $this->padding) {
|
||||
$data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ (
|
||||
$this->alphaTo[
|
||||
$this->modNn(
|
||||
$this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den]
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $erasures) {
|
||||
if (count($erasures) < $count) {
|
||||
$erasures->setSize($count);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$erasures[$i] = $loc[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide.
|
||||
*/
|
||||
private function modNn(int $x) : int
|
||||
{
|
||||
while ($x >= $this->blockSize) {
|
||||
$x -= $this->blockSize;
|
||||
$x = ($x >> $this->symbolSize) + ($x & $this->blockSize);
|
||||
}
|
||||
|
||||
return $x;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user