Compare commits

..

No commits in common. "master" and "2022-05a" have entirely different histories.

320 changed files with 13307 additions and 72497 deletions

120
.drone.yml Normal file
View File

@ -0,0 +1,120 @@
---
kind: pipeline
name: integration-testing
platform:
os: linux
arch: amd64
clone:
disable: true
steps:
- name: prepare-tests
pull: default
image: timovibritannia/ansible
commands:
- git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch .
- chmod +x ci.sh
- chmod +x ci-ssh.sh
- chmod +x ci-piprequierments.sh
- ./ci.sh
- wget -O group_vars/all/secrets.yml $SECRETS_DOWNLOAD_URL --quiet
environment:
SECRETS_DOWNLOAD_URL:
from_secret: SECRETS_DOWNLOAD_URL
VAULT_PW:
from_secret: VAULT_PW
when:
branch:
- master
- staging
event:
- push
- name: lint
pull: default
image: timovibritannia/ansible
commands:
- ansible-lint ./
when:
branch:
- master
- staging
event:
- push
- name: create-server
pull: default
image: timovibritannia/ansible
commands:
- ./ci-piprequierments.sh
- ansible-playbook mailcow-start-server.yml --diff
- ./ci-ssh.sh
environment:
ANSIBLE_HOST_KEY_CHECKING: false
ANSIBLE_FORCE_COLOR: true
when:
branch:
- master
- staging
event:
- push
- name: setup-server
pull: default
image: timovibritannia/ansible
commands:
- sleep 120
- ./ci-piprequierments.sh
- ansible-playbook mailcow-setup-server.yml --private-key /drone/src/id_ssh_rsa --diff
environment:
ANSIBLE_HOST_KEY_CHECKING: false
ANSIBLE_FORCE_COLOR: true
when:
branch:
- master
- staging
event:
- push
- name: run-tests
pull: default
image: timovibritannia/ansible
commands:
- ./ci-piprequierments.sh
- ansible-playbook mailcow-integration-tests.yml --private-key /drone/src/id_ssh_rsa --diff
environment:
ANSIBLE_HOST_KEY_CHECKING: false
ANSIBLE_FORCE_COLOR: true
when:
branch:
- master
- staging
event:
- push
- name: delete-server
pull: default
image: timovibritannia/ansible
commands:
- ./ci-piprequierments.sh
- ansible-playbook mailcow-delete-server.yml --diff
environment:
ANSIBLE_HOST_KEY_CHECKING: false
ANSIBLE_FORCE_COLOR: true
when:
branch:
- master
- staging
event:
- push
status:
- failure
- success
---
kind: signature
hmac: f6619243fe2a27563291c9f2a46d93ffbc3b6dced9a05f23e64b555ce03a31e5
...

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
custom: ["https://www.servercow.de/mailcow?lang=en#sal"] custom: https://mailcow.github.io/mailcow-dockerized-docs/#help-mailcow

View File

@ -7,8 +7,8 @@ body:
label: Contribution guidelines label: Contribution guidelines
description: Please read the contribution guidelines before proceeding. description: Please read the contribution guidelines before proceeding.
options: options:
- label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree - label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree
required: true required: true
- type: checkboxes - type: checkboxes
attributes: attributes:
label: I've found a bug and checked that ... label: I've found a bug and checked that ...
@ -26,132 +26,69 @@ body:
attributes: attributes:
label: Description label: Description
description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI. description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI.
render: plain text
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Logs:" label: Logs
description: "Please take a look at the [official documentation](https://docs.mailcow.email/troubleshooting/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks." description: Please take a look at the [official documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks.
render: plain text render: bash
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Steps to reproduce:" label: Steps to reproduce
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful." description: Please describe the steps to reproduce the bug. Screenshots can be added, if helpful.
render: plain text
placeholder: |- placeholder: |-
1. ... 1. ...
2. ... 2. ...
3. ... 3. ...
validations: validations:
required: true required: true
- type: markdown
attributes:
value: |
## System information
### In this stage we would kindly ask you to attach general system information about your setup.
- type: dropdown
attributes:
label: "Which branch are you using?"
description: "#### `git rev-parse --abbrev-ref HEAD`"
multiple: false
options:
- master
- nightly
validations:
required: true
- type: input
attributes:
label: "Operating System:"
placeholder: "e.g. Ubuntu 22.04 LTS"
validations:
required: true
- type: input
attributes:
label: "Server/VM specifications:"
placeholder: "Memory, CPU Cores"
validations:
required: true
- type: input
attributes:
label: "Is Apparmor, SELinux or similar active?"
placeholder: "yes/no"
validations:
required: true
- type: input
attributes:
label: "Virtualization technology:"
placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**"
validations:
required: true
- type: input
attributes:
label: "Docker version:"
description: "#### `docker version`"
placeholder: "20.10.21"
validations:
required: true
- type: input
attributes:
label: "docker-compose version or docker compose version:"
description: "#### `docker-compose version` or `docker compose version`"
placeholder: "v2.12.2"
validations:
required: true
- type: input
attributes:
label: "mailcow version:"
description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```"
placeholder: "2022-08"
validations:
required: true
- type: input
attributes:
label: "Reverse proxy:"
placeholder: "e.g. Nginx/Traefik"
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Logs of git diff:" label: System information
description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:" description: In this stage we would kindly ask you to attach general system information about your setup.
render: plain text value: |-
validations: | Question | Answer |
required: true | --- | --- |
- type: textarea | My operating system | I_DO_REPLY_HERE |
attributes: | Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE |
label: "Logs of iptables -L -vn:" | Virtualization technlogy (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | I_DO_REPLY_HERE |
description: "#### Output of `iptables -L -vn`" | Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE |
render: plain text | Docker Version (`docker version`) | I_DO_REPLY_HERE |
validations: | Docker-Compose Version (`docker-compose version`) | I_DO_REPLY_HERE |
required: true | Reverse proxy (custom solution) | I_DO_REPLY_HERE |
- type: textarea
attributes: Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:
label: "Logs of ip6tables -L -vn:" ```
description: "#### Output of `ip6tables -L -vn`" YOUR OUTPUT GOES HERE
render: plain text ```
validations:
required: true All third-party firewalls and custom iptables rules are unsupported. **Please check the Docker docs about how to use Docker with your own ruleset**. Nevertheless, iptabels output can help us to help you:
- type: textarea iptables -L -vn:
attributes: ```
label: "Logs of iptables -L -vn -t nat:" YOUR OUTPUT GOES HERE
description: "#### Output of `iptables -L -vn -t nat`" ```
render: plain text
validations: ip6tables -L -vn:
required: true ```
- type: textarea YOUR OUTPUT GOES HERE
attributes: ```
label: "Logs of ip6tables -L -vn -t nat:"
description: "#### Output of `ip6tables -L -vn -t nat`" iptables -L -vn -t nat:
render: plain text ```
validations: YOUR OUTPUT GOES HERE
required: true ```
- type: textarea
attributes: ip6tables -L -vn -t nat:
label: "DNS check:" ```
description: "#### Output of `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network)" YOUR OUTPUT GOES HERE
render: plain text ```
DNS problems? Please run `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network) and post the output:
```
YOUR OUTPUT GOES HERE
```
validations: validations:
required: true required: true

View File

@ -1,13 +0,0 @@
## :memo: Brief description
<!-- Diff summary - START -->
<!-- Diff summary - END -->
## :computer: Commits
<!-- Diff commits - START -->
<!-- Diff commits - END -->
## :file_folder: Modified files
<!-- Diff files - START -->
<!-- Diff files - END -->

31
.github/renovate.json vendored
View File

@ -1,31 +0,0 @@
{
"enabled": true,
"timezone": "Europe/Berlin",
"dependencyDashboard": true,
"dependencyDashboardTitle": "Renovate Dashboard",
"commitBody": "Signed-off-by: milkmaker <milkmaker@mailcow.de>",
"rebaseWhen": "auto",
"labels": ["renovate"],
"assignees": [
"@magiccc"
],
"baseBranches": ["staging"],
"enabledManagers": ["github-actions", "regex", "docker-compose"],
"ignorePaths": [
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**"
],
"regexManagers": [
{
"fileMatch": ["^helper-scripts\/nextcloud.sh$"],
"matchStrings": [
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?( extractVersion=(?<extractVersion>.*?))?\\s.*?_VERSION=(?<currentValue>.*)"
]
},
{
"fileMatch": ["(^|/)Dockerfile[^/]*$"],
"matchStrings": [
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s(ENV|ARG) .*?_VERSION=(?<currentValue>.*)\\s"
]
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@ -1,33 +0,0 @@
name: Check PRs if on staging
on:
pull_request_target:
types: [opened, edited]
permissions: {}
jobs:
is_not_staging:
runs-on: ubuntu-latest
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
steps:
- name: Send message
uses: thollander/actions-comment-pull-request@v2.4.0
with:
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
message: |
Thanks for contributing!
I noticed that you didn't select `staging` as your base branch. Please change the base branch to `staging`.
See the attached picture on how to change the base branch to `staging`:
![check_prs_if_on_staging.png](https://raw.githubusercontent.com/mailcow/mailcow-dockerized/master/.github/workflows/assets/check_prs_if_on_staging.png)
- name: Fail #we want to see failed checks in the PR
if: ${{ success() }} #set exit code to 1 even if commenting somehow failed
run: exit 1
is_staging:
runs-on: ubuntu-latest
if: github.event.pull_request.base.ref == 'staging' #check if the target branch is staging
steps:
- name: Success
run: exit 0

View File

@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Mark/Close Stale Issues and Pull Requests 🗑️ - name: Mark/Close Stale Issues and Pull Requests 🗑️
uses: actions/stale@v8.0.0 uses: actions/stale@v5.0.0
with: with:
repo-token: ${{ secrets.STALE_ACTION_PAT }} repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60 days-before-stale: 60
@ -30,7 +30,6 @@ jobs:
stale-issue-label: "stale" stale-issue-label: "stale"
stale-pr-label: "stale" stale-pr-label: "stale"
exempt-draft-pr: "true" exempt-draft-pr: "true"
close-issue-reason: "not_planned"
operations-per-run: "250" operations-per-run: "250"
ascending: "true" ascending: "true"
#DRY-RUN #DRY-RUN

View File

@ -1,43 +0,0 @@
name: Build mailcow Docker Images
on:
push:
branches: [ "master", "staging" ]
workflow_dispatch:
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
docker_image_builds:
strategy:
matrix:
images:
- "acme-mailcow"
- "clamd-mailcow"
- "dockerapi-mailcow"
- "dovecot-mailcow"
- "netfilter-mailcow"
- "olefy-mailcow"
- "php-fpm-mailcow"
- "postfix-mailcow"
- "rspamd-mailcow"
- "sogo-mailcow"
- "solr-mailcow"
- "unbound-mailcow"
- "watchdog-mailcow"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Docker
run: |
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
sudo service docker start
- name: Prepair Image Builds
run: |
cp helper-scripts/docker-compose.override.yml.d/BUILD_FLAGS/docker-compose.override.yml docker-compose.override.yml
- name: Build Docker Images
run: |
docker compose build ${image}
env:
image: ${{ matrix.images }}

View File

@ -1,25 +0,0 @@
name: Create PR to merge to nightly from staging
on:
push:
branches:
- staging
jobs:
action-pull-request:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Run the Action
uses: devops-infra/action-pull-request@v0.5.5
with:
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
assignee: DerLinkman
source_branch: staging
target_branch: nightly
reviewer: DerLinkman
label: upstream
template: .github/ISSUE_TEMPLATE/pr_to_nighty_template.yml
get_diff: true

View File

@ -1,34 +0,0 @@
name: Build mailcow backup image
on:
schedule:
# At 00:00 on Sunday
- cron: "0 0 * * 0"
workflow_dispatch: # Allow to run workflow manually
jobs:
docker_image_build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }}
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: data/Dockerfiles/backup/Dockerfile
push: true
tags: mailcow/backup:latest

View File

@ -1,39 +0,0 @@
name: Update postscreen_access.cidr
on:
schedule:
# Monthly
- cron: "0 0 1 * *"
workflow_dispatch: # Allow to run workflow manually
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
Update-postscreen_access_cidr:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate postscreen_access.cidr
run: |
bash helper-scripts/update_postscreen_whitelist.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
commit-message: update postscreen_access.cidr
committer: milkmaker <milkmaker@mailcow.de>
author: milkmaker <milkmaker@mailcow.de>
signoff: false
branch: update/postscreen_access.cidr
base: staging
delete-branch: true
add-paths: |
data/conf/postfix/postscreen_access.cidr
title: '[Postfix] update postscreen_access.cidr'
body: |
This PR updates the postscreen_access.cidr using GitHub Actions and [helper-scripts/update_postscreen_whitelist.sh](https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/update_postscreen_whitelist.sh)

2
.gitignore vendored
View File

@ -36,8 +36,6 @@ data/conf/postfix/extra.cf
data/conf/postfix/sni.map data/conf/postfix/sni.map
data/conf/postfix/sni.map.db data/conf/postfix/sni.map.db
data/conf/postfix/sql data/conf/postfix/sql
data/conf/postfix/dns_blocklists.cf
data/conf/postfix/dnsbl_reply.map
data/conf/rspamd/custom/* data/conf/rspamd/custom/*
data/conf/rspamd/local.d/* data/conf/rspamd/local.d/*
data/conf/rspamd/override.d/* data/conf/rspamd/override.d/*

16
.travis.yml Normal file
View File

@ -0,0 +1,16 @@
sudo: required
services:
- docker
script:
- echo 'Europe/Berlin' | MAILCOW_HOSTNAME=build.mailcow ./generate_config.sh
- docker-compose pull --ignore-pull-failures --parallel
- docker-compose build
- docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD
- docker-compose push
branches:
only:
- master_disabled
env:
global:
- secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w=
- secure: fWzZisT6nGDNL4lf6tXB07eFG2drgBakHxzdF/NFVvzuP861RFR6omuL+ED0PgXrEHDJBxaBLv52je8irmUXrAH1CNr7T8DWiZo/h5h609Uzr+38T1NnIu4krL0Wo6/CDwlLKnzqTq9yBIZLQSHVJmo8AOpo1JPIi2ajodqj9ZfmAxDQTQl+G6zvQjtqIkYHsHY7A44Rto0f14ykn7w2S82Jn6Ry89VNI5V1WEO3sMpM/XekNP/HokNcRIuntL/0+kuLvTJ5akGoTjBQxSnSW95opzPeGky74HRU2obExJYqKvF0VfVJRNAqejwjIiFIbbjqV0Sk5391kFuhuBErQQDM1bOHGdxZ41HsJH29qNWIl7C33Yl10qERoqecgsJ1N/bS2ZEmWqm/zQh5GClCXPvYmzEqMYsMGM3vjbKdjDlc1Wh2w/eFclsXN9LSXh1mc35rtj46frcT6e5Kof87AIfC9hTgDvk9kAsyjaHMkSHSZthbZXCIcsD8qriNm5UqfFBYD79mPIP1S2YMQ2jscCsjHOZgYVrcm0kzDF21J1w6H0Lo7d1jw37LYlegBdtLQ9gYgqY2D5m+nxWuVoD5FZmpR+5JGtK+ootyLFF8aiFoHXd4op1JCxRLjgkmnZKXzw3kTQSpE7oa7CgzchtQmK2nqcqla1b5Qk7ilVcjooo=

View File

@ -1,6 +1,8 @@
# mailcow: dockerized - 🐮 + 🐋 = 💕 # mailcow: dockerized - 🐮 + 🐋 = 💕
[![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/) ## We stand with 🇺🇦
[![master build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/master?label=master%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![staging build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/staging?label=staging%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/)
[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email)
## Want to support mailcow? ## Want to support mailcow?
@ -33,9 +35,3 @@ Telegram desktop clients are available for [multiple platforms](https://desktop.
**Important**: mailcow makes use of various open-source software. Please assure you agree with their license before using mailcow. **Important**: mailcow makes use of various open-source software. Please assure you agree with their license before using mailcow.
Any part of mailcow itself is released under **GNU General Public License, Version 3**. Any part of mailcow itself is released under **GNU General Public License, Version 3**.
mailcow is a registered word mark of The Infrastructure Company GmbH, Parkstr. 42, 47877 Willich, Germany.
The project is managed and maintained by The Infrastructure Company GmbH.
Originated from @andryyy (André)

View File

@ -1,42 +0,0 @@
# Security Policies and Procedures
This document outlines security procedures and general policies for the _mailcow: dockerized_ project as found on [mailcow-dockerized](https://github.com/mailcow/mailcow-dockerized).
* [Reporting a Vulnerability](#reporting-a-vulnerability)
* [Disclosure Policy](#disclosure-policy)
* [Comments on this Policy](#comments-on-this-policy)
## Reporting a Vulnerability
The mailcow team and community take all security vulnerabilities
seriously. Thank you for improving the security of our open source
software. We appreciate your efforts and responsible disclosure and will
make every effort to acknowledge your contributions.
Report security vulnerabilities by emailing the mailcow team at:
info at servercow.de
mailcow team will acknowledge your email as soon as possible, and will
send a more detailed response afterwards indicating the next steps in
handling your report. After the initial reply to your report, the mailcow
team will endeavor to keep you informed of the progress towards a fix and
full announcement, and may ask for additional information or guidance.
Report security vulnerabilities in third-party modules to the person or
team maintaining the module.
## Disclosure Policy
When the mailcow team receives a security bug report, they will assign it
to a primary handler. This person will coordinate the fix and release
process, involving the following steps:
* Confirm the problem and determine the affected versions.
* Audit code to find any potential similar problems.
* Prepare fixes for all releases still under maintenance.
## Comments on this Policy
If you have suggestions on how this process could be improved please submit a
pull request.

0
create_cold_standby.sh Executable file → Normal file
View File

View File

@ -1,6 +1,6 @@
FROM alpine:3.17 FROM alpine:3.15
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
RUN apk upgrade --no-cache \ RUN apk upgrade --no-cache \
&& apk add --update --no-cache \ && apk add --update --no-cache \

View File

@ -213,13 +213,11 @@ while true; do
done done
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig') ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
# Start IP detection # Start IP detection
log_f "Detecting IP addresses..." log_f "Detecting IP addresses..."
IPV4=$(get_ipv4) IPV4=$(get_ipv4)
IPV6=$(get_ipv6) IPV6=$(get_ipv6)
log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}" log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}"
fi
######################################### #########################################
# IP and webroot challenge verification # # IP and webroot challenge verification #

View File

@ -1,3 +0,0 @@
FROM debian:bullseye-slim
RUN apt update && apt install pigz

View File

@ -1,4 +1,4 @@
FROM clamav/clamav:1.0.1-1_base FROM clamav/clamav:0.104.2-2_base
LABEL maintainer "André Peters <andre.peters@servercow.de>" LABEL maintainer "André Peters <andre.peters@servercow.de>"
@ -8,14 +8,8 @@ RUN apk upgrade --no-cache \
bind-tools \ bind-tools \
bash bash
# init COPY clamd.sh ./
COPY clamd.sh /clamd.sh
RUN chmod +x /sbin/tini RUN chmod +x /sbin/tini
# healthcheck
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
ENTRYPOINT [] ENTRYPOINT []
CMD ["/sbin/tini", "-g", "--", "/clamd.sh"] CMD ["/sbin/tini", "-g", "--", "/clamd.sh"]

View File

@ -1,9 +0,0 @@
#!/bin/bash
if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "SKIP_CLAMD=y, skipping ClamAV..."
exit 0
fi
# run clamd healthcheck
/usr/local/bin/clamdcheck.sh

View File

@ -1,6 +1,6 @@
FROM alpine:3.17 FROM alpine:3.15
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
WORKDIR /app WORKDIR /app
@ -8,18 +8,11 @@ RUN apk add --update --no-cache python3 \
py3-pip \ py3-pip \
openssl \ openssl \
tzdata \ tzdata \
py3-psutil \
&& pip3 install --upgrade pip \ && pip3 install --upgrade pip \
fastapi \
uvicorn \
aiodocker \
docker \ docker \
aioredis flask \
RUN mkdir /app/modules flask-restful
COPY docker-entrypoint.sh /app/ COPY dockerapi.py /app/
COPY main.py /app/main.py
COPY modules/ /app/modules/
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"] CMD ["python3", "-u", "/app/dockerapi.py"]
CMD exec python main.py

View File

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

View File

@ -0,0 +1,419 @@
#!/usr/bin/env python3
from flask import Flask
from flask_restful import Resource, Api
from flask import jsonify
from flask import Response
from flask import request
from threading import Thread
import docker
import uuid
import signal
import time
import os
import re
import sys
import ssl
import socket
import subprocess
import traceback
docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
app = Flask(__name__)
api = Api(app)
class containers_get(Resource):
def get(self):
containers = {}
try:
for container in docker_client.containers.list(all=True):
containers.update({container.attrs['Id']: container.attrs})
return containers
except Exception as e:
return jsonify(type='danger', msg=str(e))
class container_get(Resource):
def get(self, container_id):
if container_id and container_id.isalnum():
try:
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
return container.attrs
except Exception as e:
return jsonify(type='danger', msg=str(e))
else:
return jsonify(type='danger', msg='no or invalid id defined')
class container_post(Resource):
def post(self, container_id, post_action):
if container_id and container_id.isalnum() and post_action:
try:
"""Dispatch container_post api call"""
if post_action == 'exec':
if not request.json or not 'cmd' in request.json:
return jsonify(type='danger', msg='cmd is missing')
if not request.json or not 'task' in request.json:
return jsonify(type='danger', msg='task is missing')
api_call_method_name = '__'.join(['container_post', str(post_action), str(request.json['cmd']), str(request.json['task']) ])
else:
api_call_method_name = '__'.join(['container_post', str(post_action) ])
api_call_method = getattr(self, api_call_method_name, lambda container_id: jsonify(type='danger', msg='container_post - unknown api call'))
print("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return api_call_method(container_id)
except Exception as e:
print("error - container_post: %s" % str(e))
return jsonify(type='danger', msg=str(e))
else:
return jsonify(type='danger', msg='invalid container id or missing action')
# api call: container_post - post_action: stop
def container_post__stop(self, container_id):
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
container.stop()
return jsonify(type='success', msg='command completed successfully')
# api call: container_post - post_action: start
def container_post__start(self, container_id):
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
container.start()
return jsonify(type='success', msg='command completed successfully')
# api call: container_post - post_action: restart
def container_post__restart(self, container_id):
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
container.restart()
return jsonify(type='success', msg='command completed successfully')
# api call: container_post - post_action: top
def container_post__top(self, container_id):
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
return jsonify(type='success', msg=container.top())
# api call: container_post - post_action: stats
def container_post__stats(self, container_id):
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
for stat in container.stats(decode=True, stream=True):
return jsonify(type='success', msg=stat )
# api call: container_post - post_action: exec - cmd: mailq - task: delete
def container_post__exec__mailq__delete(self, container_id):
if 'items' in request.json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request.json['items'])
if filtered_qids:
flagged_qids = ['-d %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids));
for container in docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: hold
def container_post__exec__mailq__hold(self, container_id):
if 'items' in request.json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request.json['items'])
if filtered_qids:
flagged_qids = ['-h %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids));
for container in docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: cat
def container_post__exec__mailq__cat(self, container_id):
if 'items' in request.json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request.json['items'])
if filtered_qids:
sanitized_string = str(' '.join(filtered_qids));
for container in docker_client.containers.list(filters={"id": container_id}):
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
if not postcat_return:
postcat_return = 'err: invalid'
return exec_run_handler('utf8_text_only', postcat_return)
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
def container_post__exec__mailq__unhold(self, container_id):
if 'items' in request.json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request.json['items'])
if filtered_qids:
flagged_qids = ['-H %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids));
for container in docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
def container_post__exec__mailq__deliver(self, container_id):
if 'items' in request.json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request.json['items'])
if filtered_qids:
flagged_qids = ['-i %s' % i for i in filtered_qids]
for container in docker_client.containers.list(filters={"id": container_id}):
for i in flagged_qids:
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
# todo: check each exit code
return jsonify(type='success', msg=str("Scheduled immediate delivery"))
# api call: container_post - post_action: exec - cmd: mailq - task: list
def container_post__exec__mailq__list(self, container_id):
for container in docker_client.containers.list(filters={"id": container_id}):
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
return exec_run_handler('utf8_text_only', mailq_return)
# api call: container_post - post_action: exec - cmd: mailq - task: flush
def container_post__exec__mailq__flush(self, container_id):
for container in docker_client.containers.list(filters={"id": container_id}):
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
return exec_run_handler('generic', postqueue_r)
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
def container_post__exec__mailq__super_delete(self, container_id):
for container in docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
def container_post__exec__system__fts_rescan(self, container_id):
if 'username' in request.json:
for container in docker_client.containers.list(filters={"id": container_id}):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail')
if rescan_return.exit_code == 0:
return jsonify(type='success', msg='fts_rescan: rescan triggered')
else:
return jsonify(type='warning', msg='fts_rescan error')
if 'all' in request.json:
for container in docker_client.containers.list(filters={"id": container_id}):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
if rescan_return.exit_code == 0:
return jsonify(type='success', msg='fts_rescan: rescan triggered')
else:
return jsonify(type='warning', msg='fts_rescan error')
# api call: container_post - post_action: exec - cmd: system - task: df
def container_post__exec__system__df(self, container_id):
if 'dir' in request.json:
for container in docker_client.containers.list(filters={"id": container_id}):
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
if df_return.exit_code == 0:
return df_return.output.decode('utf-8').rstrip()
else:
return "0,0,0,0,0,0"
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
def container_post__exec__system__mysql_upgrade(self, container_id):
for container in docker_client.containers.list(filters={"id": container_id}):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
if sql_return.exit_code == 0:
matched = False
for line in sql_return.output.decode('utf-8').split("\n"):
if 'is already upgraded to' in line:
matched = True
if matched:
return jsonify(type='success', msg='mysql_upgrade: already upgraded', text=sql_return.output.decode('utf-8'))
else:
container.restart()
return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied', text=sql_return.output.decode('utf-8'))
else:
return jsonify(type='error', msg='mysql_upgrade: error running command', text=sql_return.output.decode('utf-8'))
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id):
for container in docker_client.containers.list(filters={"id": container_id}):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
if sql_return.exit_code == 0:
return jsonify(type='info', msg='mysql_tzinfo_to_sql: command completed successfully', text=sql_return.output.decode('utf-8'))
else:
return jsonify(type='error', msg='mysql_tzinfo_to_sql: error running command', text=sql_return.output.decode('utf-8'))
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
def container_post__exec__reload__dovecot(self, container_id):
for container in docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: postfix
def container_post__exec__reload__postfix(self, container_id):
for container in docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: nginx
def container_post__exec__reload__nginx(self, container_id):
for container in docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: sieve - task: list
def container_post__exec__sieve__list(self, container_id):
if 'username' in request.json:
for container in docker_client.containers.list(filters={"id": container_id}):
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"])
return exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: sieve - task: print
def container_post__exec__sieve__print(self, container_id):
if 'username' in request.json and 'script_name' in request.json:
for container in docker_client.containers.list(filters={"id": container_id}):
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"]
sieve_return = container.exec_run(cmd)
return exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
def container_post__exec__maildir__cleanup(self, container_id):
if 'maildir' in request.json:
for container in docker_client.containers.list(filters={"id": container_id}):
sane_name = re.sub(r'\W+', '', request.json['maildir'])
cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"]
maildir_cleanup = container.exec_run(cmd, user='vmail')
return exec_run_handler('generic', maildir_cleanup)
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
def container_post__exec__rspamd__worker_password(self, container_id):
if 'raw' in request.json:
for container in docker_client.containers.list(filters={"id": container_id}):
cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
matched = False
for line in cmd_response.split("\n"):
if '$2$' in line:
hash = line.strip()
hash_out = re.search('\$2\$.+$', hash).group(0)
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
container.restart()
matched = True
if matched:
return jsonify(type='success', msg='command completed successfully')
else:
return jsonify(type='danger', msg='command did not complete')
def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
def recv_socket_data(c_socket, timeout):
c_socket.setblocking(0)
total_data=[];
data='';
begin=time.time()
while True:
if total_data and time.time()-begin > timeout:
break
elif time.time()-begin > timeout*2:
break
try:
data = c_socket.recv(8192)
if data:
total_data.append(data.decode('utf-8'))
#change the beginning time for measurement
begin=time.time()
else:
#sleep for sometime to indicate a gap
time.sleep(0.1)
break
except:
pass
return ''.join(total_data)
try :
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
if not cmd.endswith("\n"):
cmd = cmd + "\n"
socket.send(cmd.encode('utf-8'))
data = recv_socket_data(socket, timeout)
socket.close()
return data
except Exception as e:
print("error - exec_cmd_container: %s" % str(e))
traceback.print_exc(file=sys.stdout)
def exec_run_handler(type, output):
if type == 'generic':
if output.exit_code == 0:
return jsonify(type='success', msg='command completed successfully')
else:
return jsonify(type='danger', msg='command failed: ' + output.output.decode('utf-8'))
if type == 'utf8_text_only':
r = Response(response=output.output.decode('utf-8'), status=200, mimetype="text/plain")
r.headers["Content-Type"] = "text/plain; charset=utf-8"
return r
class GracefulKiller:
kill_now = False
def __init__(self):
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame):
self.kill_now = True
def create_self_signed_cert():
process = subprocess.Popen(
"openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout /app/dockerapi_key.pem -out /app/dockerapi_cert.pem -subj /CN=dockerapi/O=mailcow -addext subjectAltName=DNS:dockerapi".split(),
stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=False
)
process.wait()
def startFlaskAPI():
create_self_signed_cert()
try:
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.check_hostname = False
ctx.load_cert_chain(certfile='/app/dockerapi_cert.pem', keyfile='/app/dockerapi_key.pem')
except:
print ("Cannot initialize TLS, retrying in 5s...")
time.sleep(5)
app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx)
api.add_resource(containers_get, '/containers/json')
api.add_resource(container_get, '/containers/<string:container_id>/json')
api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>')
if __name__ == '__main__':
api_thread = Thread(target=startFlaskAPI)
api_thread.daemon = True
api_thread.start()
killer = GracefulKiller()
while True:
time.sleep(1)
if killer.kill_now:
break
print ("Stopping dockerapi-mailcow")

View File

@ -1,260 +0,0 @@
import os
import sys
import uvicorn
import json
import uuid
import async_timeout
import asyncio
import aioredis
import aiodocker
import docker
import logging
from logging.config import dictConfig
from fastapi import FastAPI, Response, Request
from modules.DockerApi import DockerApi
dockerapi = None
app = FastAPI()
# Define Routes
@app.get("/host/stats")
async def get_host_update_stats():
global dockerapi
if dockerapi.host_stats_isUpdating == False:
asyncio.create_task(dockerapi.get_host_stats())
dockerapi.host_stats_isUpdating = True
while True:
if await dockerapi.redis_client.exists('host_stats'):
break
await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.redis_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@app.get("/containers/{container_id}/json")
async def get_container(container_id : str):
global dockerapi
if container_id and container_id.isalnum():
try:
for container in (await dockerapi.async_docker_client.containers.list()):
if container._id == container_id:
container_info = await container.show()
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
res = {
"type": "danger",
"msg": "no container found"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
"type": "danger",
"msg": "no or invalid id defined"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.get("/containers/json")
async def get_containers():
global dockerapi
containers = {}
try:
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")
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.post("/containers/{container_id}/{post_action}")
async def post_containers(container_id : str, post_action : str, request: Request):
global dockerapi
try :
request_json = await request.json()
except Exception as err:
request_json = {}
if container_id and container_id.isalnum() and post_action:
try:
"""Dispatch container_post api call"""
if post_action == 'exec':
if not request_json or not 'cmd' in request_json:
res = {
"type": "danger",
"msg": "cmd is missing"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if not request_json or not 'task' in request_json:
res = {
"type": "danger",
"msg": "task is missing"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
else:
api_call_method_name = '__'.join(['container_post', str(post_action) ])
api_call_method = getattr(dockerapi, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return api_call_method(request_json, container_id=container_id)
except Exception as e:
dockerapi.logger.error("error - container_post: %s" % str(e))
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
"type": "danger",
"msg": "invalid container id or missing action"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.post("/container/{container_id}/stats/update")
async def post_container_update_stats(container_id : str):
global dockerapi
# start update task for container if no task is running
if container_id not in dockerapi.containerIds_to_update:
asyncio.create_task(dockerapi.get_container_stats(container_id))
dockerapi.containerIds_to_update.append(container_id)
while True:
if await dockerapi.redis_client.exists(container_id + '_stats'):
break
await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
# Events
@app.on_event("startup")
async def startup_event():
global dockerapi
# Initialize a custom logger
logger = logging.getLogger("dockerapi")
logger.setLevel(logging.INFO)
# Configure the logger to output logs to the terminal
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(levelname)s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("Init APP")
# Init redis client
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
else:
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
# Init docker clients
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to redis channel")
# Subscribe to redis channel
dockerapi.pubsub = redis.pubsub()
await dockerapi.pubsub.subscribe("MC_CHANNEL")
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
@app.on_event("shutdown")
async def shutdown_event():
global dockerapi
# Close docker connections
dockerapi.sync_docker_client.close()
await dockerapi.async_docker_client.close()
# Close redis
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.redis_client.close()
# PubSub Handler
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
global dockerapi
while True:
try:
async with async_timeout.timeout(1):
message = await channel.get_message(ignore_subscribe_messages=True)
if message is not None:
# Parse message
data_json = json.loads(message['data'].decode('utf-8'))
dockerapi.logger.info(f"PubSub Received - {json.dumps(data_json)}")
# Handle api_call
if 'api_call' in data_json:
# api_call: container_post
if data_json['api_call'] == "container_post":
if 'post_action' in data_json and 'container_name' in data_json:
try:
"""Dispatch container_post api call"""
request_json = {}
if data_json['post_action'] == 'exec':
if 'request' in data_json:
request_json = data_json['request']
if 'cmd' in request_json:
if 'task' in request_json:
api_call_method_name = '__'.join(['container_post', str(data_json['post_action']), str(request_json['cmd']), str(request_json['task']) ])
else:
dockerapi.logger.error("api call: task missing")
else:
dockerapi.logger.error("api call: cmd missing")
else:
dockerapi.logger.error("api call: request missing")
else:
api_call_method_name = '__'.join(['container_post', str(data_json['post_action'])])
if api_call_method_name:
api_call_method = getattr(dockerapi, api_call_method_name)
if api_call_method:
dockerapi.logger.info("api call: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
api_call_method(request_json, container_name=data_json['container_name'])
else:
dockerapi.logger.error("api call not found: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
except Exception as e:
dockerapi.logger.error("container_post: %s" % str(e))
else:
dockerapi.logger.error("api call: missing container_name, post_action or request")
else:
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
else:
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
await asyncio.sleep(0.01)
except asyncio.TimeoutError:
pass
if __name__ == '__main__':
uvicorn.run(
app,
host="0.0.0.0",
port=443,
ssl_certfile="/app/dockerapi_cert.pem",
ssl_keyfile="/app/dockerapi_key.pem",
log_level="info",
loop="none"
)

View File

@ -1,487 +0,0 @@
import psutil
import sys
import os
import re
import time
import json
import asyncio
import platform
from datetime import datetime
from fastapi import FastAPI, Response, Request
class DockerApi:
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
self.redis_client = redis_client
self.sync_docker_client = sync_docker_client
self.async_docker_client = async_docker_client
self.logger = logger
self.host_stats_isUpdating = False
self.containerIds_to_update = []
# api call: container_post - post_action: stop
def container_post__stop(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
container.stop()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: start
def container_post__start(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
container.start()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: restart
def container_post__restart(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
container.restart()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: top
def container_post__top(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
res = { 'type': 'success', 'msg': container.top()}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: stats
def container_post__stats(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
for stat in container.stats(decode=True, stream=True):
res = { 'type': 'success', 'msg': stat}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: delete
def container_post__exec__mailq__delete(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-d %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in self.sync_docker_client.containers.list(filters=filters):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return self.exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: hold
def container_post__exec__mailq__hold(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-h %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in self.sync_docker_client.containers.list(filters=filters):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return self.exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: cat
def container_post__exec__mailq__cat(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
sanitized_string = str(' '.join(filtered_qids))
for container in self.sync_docker_client.containers.list(filters=filters):
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
if not postcat_return:
postcat_return = 'err: invalid'
return self.exec_run_handler('utf8_text_only', postcat_return)
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
def container_post__exec__mailq__unhold(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-H %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in self.sync_docker_client.containers.list(filters=filters):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return self.exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
def container_post__exec__mailq__deliver(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-i %s' % i for i in filtered_qids]
for container in self.sync_docker_client.containers.list(filters=filters):
for i in flagged_qids:
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
# todo: check each exit code
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: list
def container_post__exec__mailq__list(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
return self.exec_run_handler('utf8_text_only', mailq_return)
# api call: container_post - post_action: exec - cmd: mailq - task: flush
def container_post__exec__mailq__flush(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
return self.exec_run_handler('generic', postqueue_r)
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
def container_post__exec__mailq__super_delete(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
return self.exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
def container_post__exec__system__fts_rescan(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'username' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if 'all' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: df
def container_post__exec__system__df(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'dir' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
if df_return.exit_code == 0:
return df_return.output.decode('utf-8').rstrip()
else:
return "0,0,0,0,0,0"
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
def container_post__exec__system__mysql_upgrade(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
if sql_return.exit_code == 0:
matched = False
for line in sql_return.output.decode('utf-8').split("\n"):
if 'is already upgraded to' in line:
matched = True
if matched:
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
container.restart()
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
def container_post__exec__system__mysql_tzinfo_to_sql(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
if sql_return.exit_code == 0:
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
def container_post__exec__reload__dovecot(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
return self.exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: postfix
def container_post__exec__reload__postfix(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
return self.exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: nginx
def container_post__exec__reload__nginx(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
return self.exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: sieve - task: list
def container_post__exec__sieve__list(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'username' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
return self.exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: sieve - task: print
def container_post__exec__sieve__print(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'username' in request_json and 'script_name' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
sieve_return = container.exec_run(cmd)
return self.exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
def container_post__exec__maildir__cleanup(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'maildir' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
sane_name = re.sub(r'\W+', '', request_json['maildir'])
vmail_name = request_json['maildir'].replace("'", "'\\''")
cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"
index_name = request_json['maildir'].split("/")
if len(index_name) > 1:
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi"
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
else:
cmd = ["/bin/bash", "-c", cmd_vmail]
maildir_cleanup = container.exec_run(cmd, user='vmail')
return self.exec_run_handler('generic', maildir_cleanup)
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'raw' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
matched = False
for line in cmd_response.split("\n"):
if '$2$' in line:
hash = line.strip()
hash_out = re.search('\$2\$.+$', hash).group(0)
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
container.restart()
matched = True
if matched:
res = { 'type': 'success', 'msg': 'command completed successfully' }
self.logger.info('success changing Rspamd password')
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
self.logger.error('failed changing Rspamd password')
res = { 'type': 'danger', 'msg': 'command did not complete' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# Collect host stats
async def get_host_stats(self, wait=5):
try:
system_time = datetime.now()
host_stats = {
"cpu": {
"cores": psutil.cpu_count(),
"usage": psutil.cpu_percent()
},
"memory": {
"total": psutil.virtual_memory().total,
"usage": psutil.virtual_memory().percent,
"swap": psutil.swap_memory()
},
"uptime": time.time() - psutil.boot_time(),
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S"),
"architecture": platform.machine()
}
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10)
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
await asyncio.sleep(wait)
self.host_stats_isUpdating = False
# Collect container stats
async def get_container_stats(self, container_id, wait=5, stop=False):
if container_id and container_id.isalnum():
try:
for container in (await self.async_docker_client.containers.list()):
if container._id == container_id:
res = await container.stats(stream=False)
if await self.redis_client.exists(container_id + '_stats'):
stats = json.loads(await self.redis_client.get(container_id + '_stats'))
else:
stats = []
stats.append(res[0])
if len(stats) > 3:
del stats[0]
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
else:
res = {
"type": "danger",
"msg": "no or invalid id defined"
}
await asyncio.sleep(wait)
if stop == True:
# update task was called second time, stop
self.containerIds_to_update.remove(container_id)
else:
# call update task a second time
await self.get_container_stats(container_id, wait=0, stop=True)
def exec_cmd_container(self, container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
def recv_socket_data(c_socket, timeout):
c_socket.setblocking(0)
total_data=[]
data=''
begin=time.time()
while True:
if total_data and time.time()-begin > timeout:
break
elif time.time()-begin > timeout*2:
break
try:
data = c_socket.recv(8192)
if data:
total_data.append(data.decode('utf-8'))
#change the beginning time for measurement
begin=time.time()
else:
#sleep for sometime to indicate a gap
time.sleep(0.1)
break
except:
pass
return ''.join(total_data)
try :
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
if not cmd.endswith("\n"):
cmd = cmd + "\n"
socket.send(cmd.encode('utf-8'))
data = recv_socket_data(socket, timeout)
socket.close()
return data
except Exception as e:
self.logger.error("error - exec_cmd_container: %s" % str(e))
traceback.print_exc(file=sys.stdout)
def exec_run_handler(self, type, output):
if type == 'generic':
if output.exit_code == 0:
res = { 'type': 'success', 'msg': 'command completed successfully' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if type == 'utf8_text_only':
return Response(content=output.output.decode('utf-8'), media_type="text/plain")

View File

@ -1,13 +1,10 @@
FROM debian:bullseye-slim FROM debian:bullseye-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced ARG DOVECOT=2.3.18
ARG DOVECOT=2.3.20
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
ARG GOSU_VERSION=1.16
ENV LC_ALL C ENV LC_ALL C
ENV GOSU_VERSION 1.14
# Add groups and users before installing Dovecot to not break compatibility # Add groups and users before installing Dovecot to not break compatibility
RUN groupadd -g 5000 vmail \ RUN groupadd -g 5000 vmail \
@ -21,7 +18,6 @@ RUN groupadd -g 5000 vmail \
&& touch /etc/default/locale \ && touch /etc/default/locale \
&& apt-get update \ && apt-get update \
&& apt-get -y --no-install-recommends install \ && apt-get -y --no-install-recommends install \
build-essential \
apt-transport-https \ apt-transport-https \
ca-certificates \ ca-certificates \
cpanminus \ cpanminus \
@ -62,7 +58,6 @@ RUN groupadd -g 5000 vmail \
libproc-processtable-perl \ libproc-processtable-perl \
libreadonly-perl \ libreadonly-perl \
libregexp-common-perl \ libregexp-common-perl \
libssl-dev \
libsys-meminfo-perl \ libsys-meminfo-perl \
libterm-readkey-perl \ libterm-readkey-perl \
libtest-deep-perl \ libtest-deep-perl \
@ -112,8 +107,6 @@ RUN groupadd -g 5000 vmail \
&& apt-get autoclean \ && apt-get autoclean \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* /var/tmp/* /root/.cache/ && rm -rf /tmp/* /var/tmp/* /root/.cache/
# imapsync dependencies
RUN cpan Crypt::OpenSSL::PKCS12
COPY trim_logs.sh /usr/local/bin/trim_logs.sh COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh

View File

@ -159,7 +159,7 @@ function auth_password_verify(req, pass)
VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip))) VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
cur:close() cur:close()
con:close() con:close()
return dovecot.auth.PASSDB_RESULT_OK, "" return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
end end
row = cur:fetch (row, "a") row = cur:fetch (row, "a")
end end
@ -180,13 +180,13 @@ function auth_password_verify(req, pass)
if tostring(req.real_rip) == "__IPV4_SOGO__" then if tostring(req.real_rip) == "__IPV4_SOGO__" then
cur:close() cur:close()
con:close() con:close()
return dovecot.auth.PASSDB_RESULT_OK, "" return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
elseif row.has_prot_access == "1" then elseif row.has_prot_access == "1" then
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip))) VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
cur:close() cur:close()
con:close() con:close()
return dovecot.auth.PASSDB_RESULT_OK, "" return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
end end
end end
row = cur:fetch (row, "a") row = cur:fetch (row, "a")
@ -307,7 +307,6 @@ namespace {
} }
EOF EOF
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
# Autogenerated by mailcow # Autogenerated by mailcow
remote ${IPV4_NETWORK}.248 { remote ${IPV4_NETWORK}.248 {
@ -350,14 +349,6 @@ sievec /var/vmail/sieve/global_sieve_after.sieve
sievec /usr/lib/dovecot/sieve/report-spam.sieve sievec /usr/lib/dovecot/sieve/report-spam.sieve
sievec /usr/lib/dovecot/sieve/report-ham.sieve sievec /usr/lib/dovecot/sieve/report-ham.sieve
for file in /var/vmail/*/*/sieve/*.sieve ; do
if [[ "$file" == "/var/vmail/*/*/sieve/*.sieve" ]]; then
continue
fi
sievec "$file" "$(dirname "$file")/../.dovecot.svbin"
chown vmail:vmail "$(dirname "$file")/../.dovecot.svbin"
done
# Fix permissions # Fix permissions
chown root:root /etc/dovecot/sql/*.conf chown root:root /etc/dovecot/sql/*.conf
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua

View File

@ -8492,7 +8492,6 @@ sub xoauth2
require HTML::Entities ; require HTML::Entities ;
require JSON ; require JSON ;
require JSON::WebToken::Crypt::RSA ; require JSON::WebToken::Crypt::RSA ;
require Crypt::OpenSSL::PKCS12;
require Crypt::OpenSSL::RSA ; require Crypt::OpenSSL::RSA ;
require Encode::Byte ; require Encode::Byte ;
require IO::Socket::SSL ; require IO::Socket::SSL ;
@ -8533,9 +8532,8 @@ sub xoauth2
$sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n"); $sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n");
# Get private key from p12 file # Get private key from p12 file (would be better in perl...)
my $pkcs12 = Crypt::OpenSSL::PKCS12->new_from_file($keyfile); $key = `openssl pkcs12 -in "$keyfile" -nodes -nocerts -passin pass:$keypass -nomacver`;
$key = $pkcs12->private_key($keypass);
$sync->{ debug } and myprint( "Private key:\n$key\n"); $sync->{ debug } and myprint( "Private key:\n$key\n");
} }

View File

@ -51,8 +51,8 @@ sub sig_handler {
die "sig_handler received signal, preparing to exit...\n"; die "sig_handler received signal, preparing to exit...\n";
}; };
open my $file, '<', "/etc/sogo/sieve.creds"; open my $file, '<', "/etc/sogo/sieve.creds";
my $creds = <$file>; my $creds = <$file>;
close $file; close $file;
my ($master_user, $master_pass) = split /:/, $creds; my ($master_user, $master_pass) = split /:/, $creds;
my $sth = $dbh->prepare("SELECT id, my $sth = $dbh->prepare("SELECT id,
@ -166,11 +166,17 @@ while ($row = $sth->fetchrow_arrayref()) {
$success = 1; $success = 1;
} }
$update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?"); $keep_job_active = 1;
if (defined $exit_status && $exit_status eq "EXIT_AUTHENTICATION_FAILURE_USER1") {
$keep_job_active = 0;
}
$update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ?, active = ? WHERE id = ?");
$update->bind_param( 1, ${stdout} ); $update->bind_param( 1, ${stdout} );
$update->bind_param( 2, ${success} ); $update->bind_param( 2, ${success} );
$update->bind_param( 3, ${exit_status} ); $update->bind_param( 3, ${exit_status} );
$update->bind_param( 4, ${id} ); $update->bind_param( 4, ${keep_job_active} );
$update->bind_param( 5, ${id} );
$update->execute(); $update->execute();
} catch { } catch {
$update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?"); $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?");

View File

@ -50,7 +50,7 @@ try:
def query_mysql(query, headers = True, update = False): def query_mysql(query, headers = True, update = False):
while True: while True:
try: try:
cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci") cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8")
except Exception as ex: except Exception as ex:
print('%s - trying again...' % (ex)) print('%s - trying again...' % (ex))
time.sleep(3) time.sleep(3)
@ -166,4 +166,4 @@ try:
notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category']) notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category'])
finally: finally:
os.unlink(pidfile) os.unlink(pidfile)

View File

@ -1,5 +1,5 @@
FROM alpine:3.17 FROM alpine:3.15
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ENV XTABLES_LIBDIR /usr/lib/xtables ENV XTABLES_LIBDIR /usr/lib/xtables
ENV PYTHON_IPTABLES_XTABLES_VERSION 12 ENV PYTHON_IPTABLES_XTABLES_VERSION 12

View File

@ -64,40 +64,28 @@ def refreshF2boptions():
global f2boptions global f2boptions
global quit_now global quit_now
global exit_code global exit_code
f2boptions = {}
if not r.get('F2B_OPTIONS'): if not r.get('F2B_OPTIONS'):
f2boptions['ban_time'] = r.get('F2B_BAN_TIME') f2boptions = {}
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME') f2boptions['ban_time'] = int
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT') f2boptions['max_attempts'] = int
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') f2boptions['retry_window'] = int
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') f2boptions['netban_ipv4'] = int
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') f2boptions['netban_ipv6'] = int
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 32
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 128
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
else: else:
try: try:
f2boptions = {}
f2boptions = json.loads(r.get('F2B_OPTIONS')) f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError: except ValueError:
print('Error loading F2B options: F2B_OPTIONS is not json') print('Error loading F2B options: F2B_OPTIONS is not json')
quit_now = True quit_now = True
exit_code = 2 exit_code = 2
verifyF2boptions(f2boptions)
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
def verifyF2boptions(f2boptions):
verifyF2boption(f2boptions,'ban_time', 1800)
verifyF2boption(f2boptions,'max_ban_time', 10000)
verifyF2boption(f2boptions,'ban_time_increment', True)
verifyF2boption(f2boptions,'max_attempts', 10)
verifyF2boption(f2boptions,'retry_window', 600)
verifyF2boption(f2boptions,'netban_ipv4', 32)
verifyF2boption(f2boptions,'netban_ipv6', 128)
def verifyF2boption(f2boptions, f2boption, f2bdefault):
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
def refreshF2bregex(): def refreshF2bregex():
global f2bregex global f2bregex
global quit_now global quit_now
@ -109,9 +97,9 @@ def refreshF2bregex():
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+' f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+' f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+' f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' f2bregex[6] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' f2bregex[7] = '-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' f2bregex[8] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False)) r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
@ -159,7 +147,6 @@ def ban(address):
global lock global lock
refreshF2boptions() refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time']) BAN_TIME = int(f2boptions['ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
RETRY_WINDOW = int(f2boptions['retry_window']) RETRY_WINDOW = int(f2boptions['retry_window'])
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4']) NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
@ -187,16 +174,20 @@ def ban(address):
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False) net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
net = str(net) net = str(net)
if not net in bans: if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0} bans[net] = { 'attempts': 0 }
active_window = RETRY_WINDOW
else:
active_window = time.time() - bans[net]['last_attempt']
bans[net]['attempts'] += 1 bans[net]['attempts'] += 1
bans[net]['last_attempt'] = time.time() bans[net]['last_attempt'] = time.time()
active_window = time.time() - bans[net]['last_attempt']
if bans[net]['attempts'] >= MAX_ATTEMPTS: if bans[net]['attempts'] >= MAX_ATTEMPTS:
cur_time = int(round(time.time())) cur_time = int(round(time.time()))
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter'] logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address: if type(ip) is ipaddress.IPv4Address:
with lock: with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
@ -215,7 +206,7 @@ def ban(address):
rule.target = target rule.target = target
if rule not in chain.rules: if rule not in chain.rules:
chain.insert_rule(rule) chain.insert_rule(rule)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME) r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
else: else:
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
@ -247,8 +238,7 @@ def unban(net):
r.hdel('F2B_ACTIVE_BANS', '%s' % net) r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net) r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans: if net in bans:
bans[net]['attempts'] = 0 del bans[net]
bans[net]['ban_counter'] += 1
def permBan(net, unban=False): def permBan(net, unban=False):
global lock global lock
@ -262,7 +252,7 @@ def permBan(net, unban=False):
if rule not in chain.rules and not unban: if rule not in chain.rules and not unban:
logCrit('Add host/network %s to blacklist' % net) logCrit('Add host/network %s to blacklist' % net)
chain.insert_rule(rule) chain.insert_rule(rule)
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
elif rule in chain.rules and unban: elif rule in chain.rules and unban:
logCrit('Remove host/network %s from blacklist' % net) logCrit('Remove host/network %s from blacklist' % net)
chain.delete_rule(rule) chain.delete_rule(rule)
@ -277,7 +267,7 @@ def permBan(net, unban=False):
if rule not in chain.rules and not unban: if rule not in chain.rules and not unban:
logCrit('Add host/network %s to blacklist' % net) logCrit('Add host/network %s to blacklist' % net)
chain.insert_rule(rule) chain.insert_rule(rule)
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
elif rule in chain.rules and unban: elif rule in chain.rules and unban:
logCrit('Remove host/network %s from blacklist' % net) logCrit('Remove host/network %s from blacklist' % net)
chain.delete_rule(rule) chain.delete_rule(rule)
@ -342,7 +332,7 @@ def watch():
logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data'])) logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
ban(addr) ban(addr)
except Exception as ex: except Exception as ex:
logWarn('Error reading log line from pubsub: %s' % ex) logWarn('Error reading log line from pubsub')
quit_now = True quit_now = True
exit_code = 2 exit_code = 2
@ -356,8 +346,6 @@ def snat4(snat_target):
rule.dst = '!' + rule.src rule.dst = '!' + rule.src
target = rule.create_target("SNAT") target = rule.create_target("SNAT")
target.to_source = snat_target target.to_source = snat_target
match = rule.create_match("comment")
match.comment = f'{int(round(time.time()))}'
return rule return rule
while not quit_now: while not quit_now:
@ -368,35 +356,19 @@ def snat4(snat_target):
table.refresh() table.refresh()
chain = iptc.Chain(table, 'POSTROUTING') chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False table.autocommit = False
new_rule = get_snat4_rule() if get_snat4_rule() not in chain.rules:
logCrit('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat4_rule().src, snat_target))
if not chain.rules: chain.insert_rule(get_snat4_rule())
# if there are no rules in the chain, insert the new rule directly table.commit()
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else: else:
for position, rule in enumerate(chain.rules): for position, item in enumerate(chain.rules):
if not hasattr(rule.target, 'parameter'): if item == get_snat4_rule():
continue if position != 0:
match = all(( chain.delete_rule(get_snat4_rule())
new_rule.get_src() == rule.get_src(), table.commit()
new_rule.get_dst() == rule.get_dst(),
new_rule.target.parameters == rule.target.parameters,
new_rule.target.name == rule.target.name
))
if position == 0:
if not match:
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
if match:
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
chain.delete_rule(rule)
table.commit()
table.autocommit = True table.autocommit = True
except: except:
print('Error running SNAT4, retrying...') print('Error running SNAT4, retrying...')
def snat6(snat_target): def snat6(snat_target):
global lock global lock
@ -430,15 +402,13 @@ def snat6(snat_target):
table.commit() table.commit()
table.autocommit = True table.autocommit = True
except: except:
print('Error running SNAT6, retrying...') print('Error running SNAT6, retrying...')
def autopurge(): def autopurge():
while not quit_now: while not quit_now:
time.sleep(10) time.sleep(10)
refreshF2boptions() refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time']) BAN_TIME = int(f2boptions['ban_time'])
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN') QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
if QUEUE_UNBAN: if QUEUE_UNBAN:
@ -446,9 +416,7 @@ def autopurge():
unban(str(net)) unban(str(net))
for net in bans.copy(): for net in bans.copy():
if bans[net]['attempts'] >= MAX_ATTEMPTS: if bans[net]['attempts'] >= MAX_ATTEMPTS:
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter'] if time.time() - bans[net]['last_attempt'] > BAN_TIME:
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
unban(net) unban(net)
def isIpNetwork(address): def isIpNetwork(address):
@ -500,7 +468,7 @@ def whitelistUpdate():
if Counter(new_whitelist) != Counter(WHITELIST): if Counter(new_whitelist) != Counter(WHITELIST):
WHITELIST = new_whitelist WHITELIST = new_whitelist
logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST)) logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
time.sleep(60.0 - ((time.time() - start_time) % 60.0)) time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def blacklistUpdate(): def blacklistUpdate():
global quit_now global quit_now
@ -511,7 +479,7 @@ def blacklistUpdate():
new_blacklist = [] new_blacklist = []
if list: if list:
new_blacklist = genNetworkList(list) new_blacklist = genNetworkList(list)
if Counter(new_blacklist) != Counter(BLACKLIST): if Counter(new_blacklist) != Counter(BLACKLIST):
addban = set(new_blacklist).difference(BLACKLIST) addban = set(new_blacklist).difference(BLACKLIST)
delban = set(BLACKLIST).difference(new_blacklist) delban = set(BLACKLIST).difference(new_blacklist)
BLACKLIST = new_blacklist BLACKLIST = new_blacklist
@ -522,7 +490,7 @@ def blacklistUpdate():
if delban: if delban:
for net in delban: for net in delban:
permBan(net=net, unban=True) permBan(net=net, unban=True)
time.sleep(60.0 - ((time.time() - start_time) % 60.0)) time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def initChain(): def initChain():
# Is called before threads start, no locking # Is called before threads start, no locking

View File

@ -1,5 +1,5 @@
FROM alpine:3.17 FROM alpine:3.15
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
WORKDIR /app WORKDIR /app

View File

@ -1,18 +1,12 @@
FROM php:8.2-fpm-alpine3.17 FROM php:8.0-fpm-alpine3.14
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced ENV APCU_PECL 5.1.20
ARG APCU_PECL_VERSION=5.1.22 ENV IMAGICK_PECL 3.5.1
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced # Mailparse is pulled from master branch
ARG IMAGICK_PECL_VERSION=3.7.0 #ENV MAILPARSE_PECL 3.0.2
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced ENV MEMCACHED_PECL 3.1.5
ARG MAILPARSE_PECL_VERSION=3.1.4 ENV REDIS_PECL 5.3.4
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced
ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced
ARG REDIS_PECL_VERSION=5.3.7
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced
ARG COMPOSER_VERSION=2.5.5
RUN apk add -U --no-cache autoconf \ RUN apk add -U --no-cache autoconf \
aspell-dev \ aspell-dev \
@ -24,7 +18,6 @@ RUN apk add -U --no-cache autoconf \
freetype-dev \ freetype-dev \
g++ \ g++ \
git \ git \
gettext \
gettext-dev \ gettext-dev \
gmp-dev \ gmp-dev \
gnupg \ gnupg \
@ -34,11 +27,8 @@ RUN apk add -U --no-cache autoconf \
imagemagick-dev \ imagemagick-dev \
imap-dev \ imap-dev \
jq \ jq \
libavif \
libavif-dev \
libjpeg-turbo \ libjpeg-turbo \
libjpeg-turbo-dev \ libjpeg-turbo-dev \
libmemcached \
libmemcached-dev \ libmemcached-dev \
libpng \ libpng \
libpng-dev \ libpng-dev \
@ -48,11 +38,8 @@ RUN apk add -U --no-cache autoconf \
libtool \ libtool \
libwebp-dev \ libwebp-dev \
libxml2-dev \ libxml2-dev \
libxpm \
libxpm-dev \ libxpm-dev \
libzip \
libzip-dev \ libzip-dev \
linux-headers \
make \ make \
mysql-client \ mysql-client \
openldap-dev \ openldap-dev \
@ -62,24 +49,22 @@ RUN apk add -U --no-cache autoconf \
samba-client \ samba-client \
zlib-dev \ zlib-dev \
tzdata \ tzdata \
&& pecl install APCu-${APCU_PECL_VERSION} \ && git clone https://github.com/php/pecl-mail-mailparse \
&& pecl install imagick-${IMAGICK_PECL_VERSION} \ && cd pecl-mail-mailparse \
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \ && pecl install package.xml \
&& pecl install memcached-${MEMCACHED_PECL_VERSION} \ && cd .. \
&& pecl install redis-${REDIS_PECL_VERSION} \ && rm -r pecl-mail-mailparse \
&& pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} \
&& docker-php-ext-enable apcu imagick memcached mailparse redis \ && docker-php-ext-enable apcu imagick memcached mailparse redis \
&& pecl clear-cache \ && pecl clear-cache \
&& docker-php-ext-configure intl \ && docker-php-ext-configure intl \
&& docker-php-ext-configure exif \ && docker-php-ext-configure exif \
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \ && docker-php-ext-configure gd --with-freetype=/usr/include/ \
--with-jpeg=/usr/include/ \ --with-jpeg=/usr/include/ \
--with-webp \ && docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \
--with-xpm \
--with-avif \
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets sysvsem zip bcmath gmp \
&& docker-php-ext-configure imap --with-imap --with-imap-ssl \ && docker-php-ext-configure imap --with-imap --with-imap-ssl \
&& docker-php-ext-install -j 4 imap \ && docker-php-ext-install -j 4 imap \
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \ && curl --silent --show-error https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer \ && mv composer.phar /usr/local/bin/composer \
&& chmod +x /usr/local/bin/composer \ && chmod +x /usr/local/bin/composer \
&& apk del --purge autoconf \ && apk del --purge autoconf \
@ -87,22 +72,15 @@ RUN apk add -U --no-cache autoconf \
cyrus-sasl-dev \ cyrus-sasl-dev \
freetype-dev \ freetype-dev \
g++ \ g++ \
gettext-dev \
icu-dev \ icu-dev \
imagemagick-dev \ imagemagick-dev \
imap-dev \ imap-dev \
libavif-dev \
libjpeg-turbo-dev \ libjpeg-turbo-dev \
libmemcached-dev \
libpng-dev \ libpng-dev \
libressl-dev \ libressl-dev \
libwebp-dev \ libwebp-dev \
libxml2-dev \ libxml2-dev \
libxpm-dev \
libzip-dev \
linux-headers \
make \ make \
openldap-dev \
pcre-dev \ pcre-dev \
zlib-dev zlib-dev
@ -110,4 +88,4 @@ COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["php-fpm"] CMD ["php-fpm"]

View File

@ -172,24 +172,6 @@ BEGIN
END; END;
// //
DELIMITER ; DELIMITER ;
DROP EVENT IF EXISTS clean_sasl_log;
DELIMITER //
CREATE EVENT clean_sasl_log
ON SCHEDULE EVERY 1 DAY DO
BEGIN
DELETE sasl_log.* FROM sasl_log
LEFT JOIN (
SELECT username, service, MAX(datetime) AS lastdate
FROM sasl_log
GROUP BY username, service
) AS last ON sasl_log.username = last.username AND sasl_log.service = last.service
WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY) AND datetime < lastdate;
DELETE FROM sasl_log
WHERE username NOT IN (SELECT username FROM mailbox) AND
datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
END;
//
DELIMITER ;
EOF EOF
fi fi

View File

@ -1,5 +1,5 @@
FROM debian:bullseye-slim FROM debian:buster-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV LC_ALL C ENV LC_ALL C
@ -17,10 +17,10 @@ RUN groupadd -g 102 postfix \
ca-certificates \ ca-certificates \
curl \ curl \
dirmngr \ dirmngr \
dnsutils \ dnsutils \
gnupg \ gnupg \
libsasl2-modules \ libsasl2-modules \
mariadb-client \ mariadb-client \
perl \ perl \
postfix \ postfix \
postfix-mysql \ postfix-mysql \
@ -32,7 +32,7 @@ RUN groupadd -g 102 postfix \
syslog-ng \ syslog-ng \
syslog-ng-core \ syslog-ng-core \
syslog-ng-mod-redis \ syslog-ng-mod-redis \
tzdata \ tzdata \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& touch /etc/default/locale \ && touch /etc/default/locale \
&& printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \ && printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \

View File

@ -323,19 +323,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
dbname = ${DBNAME} dbname = ${DBNAME}
# First select queries domain and alias_domain to determine if domains are active. # First select queries domain and alias_domain to determine if domains are active.
query = SELECT goto FROM alias query = SELECT goto FROM alias
WHERE id IN ( WHERE address='%s'
SELECT COALESCE (
(
SELECT id FROM alias
WHERE address='%s'
AND (active='1' OR active='2')
), (
SELECT id FROM alias
WHERE address='@%d'
AND (active='1' OR active='2')
)
)
)
AND active='1' AND active='1'
AND (domain IN AND (domain IN
(SELECT domain FROM domain (SELECT domain FROM domain
@ -366,7 +354,7 @@ query = SELECT goto FROM alias
WHERE alias_domain.alias_domain = '%d' WHERE alias_domain.alias_domain = '%d'
AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain) AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain)
AND (mailbox.active = '1' OR mailbox.active ='2') AND (mailbox.active = '1' OR mailbox.active ='2')
AND alias_domain.active='1'; AND alias_domain.active='1'
EOF EOF
# MX based routing # MX based routing
@ -393,101 +381,12 @@ query = SELECT goto FROM spamalias
AND validity >= UNIX_TIMESTAMP() AND validity >= UNIX_TIMESTAMP()
EOF EOF
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then sed -i '/User overrides/q' /opt/postfix/conf/main.cf
cat <<EOF > /opt/postfix/conf/dns_blocklists.cf
# This file can be edited.
# Delete this file and restart postfix container to revert any changes.
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
hostkarma.junkemailfilter.com=127.0.0.1*-2
list.dnswl.org=127.0.[0..255].0*-2
list.dnswl.org=127.0.[0..255].1*-4
list.dnswl.org=127.0.[0..255].2*-6
list.dnswl.org=127.0.[0..255].3*-8
ix.dnsbl.manitu.net*2
bl.spamcop.net*2
bl.suomispam.net*2
hostkarma.junkemailfilter.com=127.0.0.2*3
hostkarma.junkemailfilter.com=127.0.0.4*2
hostkarma.junkemailfilter.com=127.0.1.2*1
backscatter.spameatingmonkey.net*2
bl.ipv6.spameatingmonkey.net*2
bl.spameatingmonkey.net*2
b.barracudacentral.org=127.0.0.2*7
bl.mailspike.net=127.0.0.2*5
bl.mailspike.net=127.0.0.[10;11;12]*4
dnsbl.sorbs.net=127.0.0.10*8
dnsbl.sorbs.net=127.0.0.5*6
dnsbl.sorbs.net=127.0.0.7*3
dnsbl.sorbs.net=127.0.0.8*2
dnsbl.sorbs.net=127.0.0.6*2
dnsbl.sorbs.net=127.0.0.9*2
EOF
fi
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
if [ ! -z "$DNSBL_CONFIG" ]; then
echo -e "\e[33mChecking if ASN for your IP is listed for Spamhaus Bad ASN List...\e[0m"
if [ -n "$SPAMHAUS_DQS_KEY" ]; then
echo -e "\e[32mDetected SPAMHAUS_DQS_KEY variable from mailcow.conf...\e[0m"
echo -e "\e[33mUsing DQS Blocklists from Spamhaus!\e[0m"
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[4..7]*6
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[10;11]*8
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.3*4
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.2*3
postscreen_dnsbl_reply_map = texthash:/opt/postfix/conf/dnsbl_reply.map
EOF
cat <<EOF > /opt/postfix/conf/dnsbl_reply.map
# Autogenerated by mailcow, using Spamhaus DQS reply domains
${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net sbl.spamhaus.org
${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net xbl.spamhaus.org
${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net pbl.spamhaus.org
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net zen.spamhaus.org
${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net dbl.spamhaus.org
${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net zrd.spamhaus.org
EOF
)
else
if [ -f "/opt/postfix/conf/dnsbl_reply.map" ]; then
rm /opt/postfix/conf/dnsbl_reply.map
fi
response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email")
if [ "$response" -eq 503 ]; then
echo -e "\e[31mThe AS of your IP is listed as a banned AS from Spamhaus!\e[0m"
echo -e "\e[33mNo SPAMHAUS_DQS_KEY found... Skipping Spamhaus blocklists entirely!\e[0m"
SPAMHAUS_DNSBL_CONFIG=""
elif [ "$response" -eq 200 ]; then
echo -e "\e[32mThe AS of your IP is NOT listed as a banned AS from Spamhaus!\e[0m"
echo -e "\e[33mUsing the open Spamhaus blocklists.\e[0m"
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
zen.spamhaus.org=127.0.0.[10;11]*8
zen.spamhaus.org=127.0.0.[4..7]*6
zen.spamhaus.org=127.0.0.3*4
zen.spamhaus.org=127.0.0.2*3
EOF
)
else
echo -e "\e[31mWe couldn't determine your AS... (maybe DNS/Network issue?) Response Code: $response\e[0m"
echo -e "\e[33mDeactivating Spamhaus DNS Blocklists to be on the safe site!\e[0m"
SPAMHAUS_DNSBL_CONFIG=""
fi
fi
fi
# Reset main.cf
sed -i '/Overrides/q' /opt/postfix/conf/main.cf
echo >> /opt/postfix/conf/main.cf echo >> /opt/postfix/conf/main.cf
# Append postscreen dnsbl sites to main.cf
if [ ! -z "$DNSBL_CONFIG" ]; then
echo -e "${DNSBL_CONFIG}\n${SPAMHAUS_DNSBL_CONFIG}" >> /opt/postfix/conf/main.cf
fi
# Append user overrides
echo -e "\n# User Overrides" >> /opt/postfix/conf/main.cf
touch /opt/postfix/conf/extra.cf touch /opt/postfix/conf/extra.cf
sed -i '/myhostname/d' /opt/postfix/conf/extra.cf sed -i '/myhostname/d' /opt/postfix/conf/extra.cf
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then

View File

@ -1,4 +1,4 @@
@version: 3.28 @version: 3.19
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);

View File

@ -1,4 +1,4 @@
@version: 3.28 @version: 3.19
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);

View File

@ -1,5 +1,5 @@
FROM debian:bullseye-slim FROM debian:bullseye-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@tinc.gmbh>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG CODENAME=bullseye ARG CODENAME=bullseye
@ -26,7 +26,6 @@ RUN apt-get update && apt-get install -y \
COPY settings.conf /etc/rspamd/settings.conf COPY settings.conf /etc/rspamd/settings.conf
COPY metadata_exporter.lua /usr/share/rspamd/plugins/metadata_exporter.lua COPY metadata_exporter.lua /usr/share/rspamd/plugins/metadata_exporter.lua
COPY set_worker_password.sh /set_worker_password.sh
COPY docker-entrypoint.sh /docker-entrypoint.sh COPY docker-entrypoint.sh /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@ -1,12 +0,0 @@
#!/bin/bash
password_file='/etc/rspamd/override.d/worker-controller-password.inc'
password_hash=`/usr/bin/rspamadm pw -e -p $1`
echo 'enable_password = "'$password_hash'";' > $password_file
if grep -q "$password_hash" "$password_file"; then
echo "OK"
else
echo "ERROR"
fi

View File

@ -1,11 +1,10 @@
FROM debian:bullseye-slim FROM debian:bullseye-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/ ARG SOGO_DEBIAN_REPOSITORY=http://packages.inverse.ca/SOGo/nightly/5/debian/
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
ARG GOSU_VERSION=1.16
ENV LC_ALL C ENV LC_ALL C
ENV GOSU_VERSION 1.14
# Prerequisites # Prerequisites
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
@ -31,7 +30,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
&& gosu nobody true \ && gosu nobody true \
&& mkdir /usr/share/doc/sogo \ && mkdir /usr/share/doc/sogo \
&& touch /usr/share/doc/sogo/empty.sh \ && touch /usr/share/doc/sogo/empty.sh \
&& apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \ && apt-key adv --keyserver keyserver.ubuntu.com --recv-key 0x810273C4 \
&& echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \ && echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \
&& apt-get update && apt-get install -y --no-install-recommends \ && apt-get update && apt-get install -y --no-install-recommends \
sogo \ sogo \
@ -53,4 +52,4 @@ RUN chmod +x /bootstrap-sogo.sh \
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf

View File

@ -142,10 +142,6 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string> <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string>
<key>SOGoIMAPServer</key> <key>SOGoIMAPServer</key>
<string>imap://${IPV4_NETWORK}.250:143/?TLS=YES&amp;tlsVerifyMode=none</string> <string>imap://${IPV4_NETWORK}.250:143/?TLS=YES&amp;tlsVerifyMode=none</string>
<key>SOGoSieveServer</key>
<string>sieve://${IPV4_NETWORK}.250:4190/?TLS=YES&amp;tlsVerifyMode=none</string>
<key>SOGoSMTPServer</key>
<string>smtp://${IPV4_NETWORK}.253:588/?TLS=YES&amp;tlsVerifyMode=none</string>
<key>SOGoTrustProxyAuthentication</key> <key>SOGoTrustProxyAuthentication</key>
<string>YES</string> <string>YES</string>
<key>SOGoEncryptionKey</key> <key>SOGoEncryptionKey</key>

View File

@ -2,8 +2,7 @@ FROM solr:7.7-slim
USER root USER root
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced ENV GOSU_VERSION 1.11
ARG GOSU_VERSION=1.16
COPY solr.sh / COPY solr.sh /
COPY solr-config-7.7.0.xml / COPY solr-config-7.7.0.xml /

View File

@ -1,6 +1,6 @@
FROM alpine:3.17 FROM alpine:3.15
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
curl \ curl \

View File

@ -1,4 +1,4 @@
FROM alpine:3.17 FROM alpine:3.15
LABEL maintainer "André Peters <andre.peters@servercow.de>" LABEL maintainer "André Peters <andre.peters@servercow.de>"
# Installation # Installation

View File

@ -24,7 +24,7 @@ server {
add_header X-Download-Options "noopen" always; add_header X-Download-Options "noopen" always;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always; add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "noindex, nofollow" always; add_header X-Robots-Tag "none" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
fastcgi_hide_header X-Powered-By; fastcgi_hide_header X-Powered-By;

View File

@ -24,11 +24,6 @@ mail_plugins = </etc/dovecot/mail_plugins
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix: mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
mail_attachment_dir = /var/attachments mail_attachment_dir = /var/attachments
mail_attachment_min_size = 128k 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 # Dovecot 2.2
#ssl_protocols = !SSLv3 #ssl_protocols = !SSLv3

View File

@ -114,7 +114,7 @@
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off; proxy_redirect off;
error_page 401 /_rspamderror.php; error_page 403 /_rspamderror.php;
} }
proxy_pass http://rspamd:11334/; proxy_pass http://rspamd:11334/;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;

View File

@ -40,6 +40,34 @@ postscreen_blacklist_action = drop
postscreen_cache_cleanup_interval = 24h postscreen_cache_cleanup_interval = 24h
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
postscreen_dnsbl_action = enforce postscreen_dnsbl_action = enforce
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
hostkarma.junkemailfilter.com=127.0.0.1*-2
list.dnswl.org=127.0.[0..255].0*-2
list.dnswl.org=127.0.[0..255].1*-4
list.dnswl.org=127.0.[0..255].2*-6
list.dnswl.org=127.0.[0..255].3*-8
ix.dnsbl.manitu.net*2
bl.spamcop.net*2
bl.suomispam.net*2
hostkarma.junkemailfilter.com=127.0.0.2*3
hostkarma.junkemailfilter.com=127.0.0.4*2
hostkarma.junkemailfilter.com=127.0.1.2*1
backscatter.spameatingmonkey.net*2
bl.ipv6.spameatingmonkey.net*2
bl.spameatingmonkey.net*2
b.barracudacentral.org=127.0.0.2*7
bl.mailspike.net=127.0.0.2*5
bl.mailspike.net=127.0.0.[10;11;12]*4
dnsbl.sorbs.net=127.0.0.10*8
dnsbl.sorbs.net=127.0.0.5*6
dnsbl.sorbs.net=127.0.0.7*3
dnsbl.sorbs.net=127.0.0.8*2
dnsbl.sorbs.net=127.0.0.6*2
dnsbl.sorbs.net=127.0.0.9*2
zen.spamhaus.org=127.0.0.[10;11]*8
zen.spamhaus.org=127.0.0.[4..7]*6
zen.spamhaus.org=127.0.0.3*4
zen.spamhaus.org=127.0.0.2*3
postscreen_dnsbl_threshold = 6 postscreen_dnsbl_threshold = 6
postscreen_dnsbl_ttl = 5m postscreen_dnsbl_ttl = 5m
postscreen_greet_action = enforce postscreen_greet_action = enforce
@ -169,4 +197,4 @@ smtps_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
# DO NOT EDIT ANYTHING BELOW # # DO NOT EDIT ANYTHING BELOW #
# Overrides # # User overrides #

View File

@ -1,20 +1,15 @@
# Whitelist generated by Postwhite v3.4 on Mon Jul 31 10:06:06 UTC 2023 # Whitelist generated by Postwhite v3.4 on Mon 21 Mar 2022 06:50:26 PM CET
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 2043 total rules # 1898 total rules
2a00:1450:4000::/36 permit 2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit 2a01:111:f400::/48 permit
2a01:111:f403:8000::/50 permit 2a01:111:f403::/48 permit
2a01:111:f403::/49 permit 2a01:4180:4050:0400::/64 permit
2a01:111:f403:c000::/51 permit 2a01:4180:4050:0800::/64 permit
2a01:111:f403:f000::/52 permit 2a01:4180:4051:0400::/64 permit
2a01:4180:4051:0800::/64 permit
2a02:a60:0:5::/64 permit 2a02:a60:0:5::/64 permit
2c0f:fb50:4000::/36 permit 2c0f:fb50:4000::/36 permit
2.207.151.53 permit
3.14.230.16 permit
3.70.123.177 permit
3.93.157.0/24 permit
3.129.120.190 permit
3.210.190.0/24 permit
8.20.114.31 permit 8.20.114.31 permit
8.25.194.0/23 permit 8.25.194.0/23 permit
8.25.196.0/23 permit 8.25.196.0/23 permit
@ -24,53 +19,41 @@
13.70.32.43 permit 13.70.32.43 permit
13.72.50.45 permit 13.72.50.45 permit
13.74.143.28 permit 13.74.143.28 permit
13.77.161.179 permit
13.78.233.182 permit 13.78.233.182 permit
13.92.31.129 permit 13.92.31.129 permit
13.110.208.0/21 permit 13.110.208.0/21 permit
13.110.209.0/24 permit
13.110.216.0/22 permit 13.110.216.0/22 permit
13.110.224.0/20 permit 13.110.224.0/20 permit
13.111.0.0/16 permit 13.111.0.0/16 permit
15.200.21.50 permit 17.41.0.0/16 permit
15.200.44.248 permit
15.200.201.185 permit
17.57.155.0/24 permit 17.57.155.0/24 permit
17.57.156.0/24 permit 17.57.156.0/24 permit
17.58.0.0/16 permit 17.58.0.0/16 permit
18.156.89.250 permit 17.110.0.0/15 permit
18.157.243.190 permit 17.142.0.0/15 permit
17.162.0.0/15 permit
17.164.0.0/16 permit
17.171.37.0/24 permit
17.172.0.0/16 permit
17.179.168.0/23 permit
18.194.95.56 permit 18.194.95.56 permit
18.198.96.88 permit 18.198.96.88 permit
18.208.124.128/25 permit 20.47.149.138 permit
18.216.232.154 permit 20.48.0.0/12 permit
18.234.1.244 permit
18.236.40.242 permit
20.51.6.32/30 permit
20.52.52.2 permit 20.52.52.2 permit
20.52.128.133 permit 20.52.128.133 permit
20.59.80.4/30 permit
20.63.210.192/28 permit 20.63.210.192/28 permit
20.69.8.108/30 permit 20.64.0.0/10 permit
20.70.246.20 permit
20.76.201.171 permit
20.83.222.104/30 permit
20.88.157.184/30 permit
20.94.180.64/28 permit 20.94.180.64/28 permit
20.97.34.220/30 permit
20.98.148.156/30 permit
20.98.194.68/30 permit
20.105.209.76/30 permit
20.107.239.64/30 permit
20.112.250.133 permit
20.118.139.208/30 permit
20.185.213.160/27 permit 20.185.213.160/27 permit
20.185.213.224/27 permit 20.185.213.224/27 permit
20.185.214.0/27 permit 20.185.214.0/27 permit
20.185.214.2 permit 20.185.214.2 permit
20.185.214.32/27 permit 20.185.214.32/27 permit
20.185.214.64/27 permit 20.185.214.64/27 permit
20.231.239.246 permit 20.192.0.0/10 permit
20.236.44.162 permit 23.100.85.1 permit
23.103.224.0/19 permit 23.103.224.0/19 permit
23.249.208.0/20 permit 23.249.208.0/20 permit
23.251.224.0/19 permit 23.251.224.0/19 permit
@ -95,38 +78,46 @@
27.123.206.56/29 permit 27.123.206.56/29 permit
27.123.206.76/30 permit 27.123.206.76/30 permit
27.123.206.80/28 permit 27.123.206.80/28 permit
31.25.48.222 permit 34.194.25.167 permit
34.195.217.107 permit 34.194.144.120 permit
34.202.239.6 permit
34.212.163.75 permit 34.212.163.75 permit
34.215.104.144 permit
34.225.212.172 permit 34.225.212.172 permit
34.247.168.44 permit 34.247.168.44 permit
35.161.32.253 permit
35.167.93.243 permit
35.176.132.251 permit 35.176.132.251 permit
35.190.247.0/24 permit 35.190.247.0/24 permit
35.191.0.0/16 permit 35.191.0.0/16 permit
37.188.97.188 permit
37.218.248.47 permit 37.218.248.47 permit
37.218.249.47 permit 37.218.249.47 permit
37.218.251.62 permit 37.218.251.62 permit
39.156.163.64/29 permit 39.156.163.64/29 permit
40.71.187.0/24 permit 40.71.187.0/24 permit
40.76.4.15 permit
40.77.102.222 permit
40.92.0.0/15 permit 40.92.0.0/15 permit
40.97.116.82 permit
40.97.128.194 permit
40.97.148.226 permit
40.97.153.146 permit
40.97.156.114 permit
40.97.160.2 permit
40.97.161.50 permit
40.97.164.146 permit
40.107.0.0/16 permit 40.107.0.0/16 permit
40.112.65.63 permit 40.112.65.63 permit
40.112.72.205 permit
40.113.200.201 permit
40.117.80.0/24 permit 40.117.80.0/24 permit
40.121.71.46 permit
41.74.192.0/22 permit 41.74.192.0/22 permit
41.74.196.0/22 permit 41.74.196.0/22 permit
41.74.200.0/23 permit 41.74.200.0/23 permit
41.74.204.0/23 permit 41.74.204.0/23 permit
41.74.206.0/24 permit 41.74.206.0/24 permit
42.159.163.81 permit
42.159.163.82 permit
42.159.163.83 permit
43.228.184.0/22 permit 43.228.184.0/22 permit
44.206.138.57 permit
44.209.42.157 permit
44.236.56.93 permit
44.238.220.251 permit
46.19.168.0/23 permit
46.226.48.0/21 permit 46.226.48.0/21 permit
46.228.36.37 permit 46.228.36.37 permit
46.228.36.38/31 permit 46.228.36.38/31 permit
@ -176,8 +167,6 @@
46.243.88.175 permit 46.243.88.175 permit
46.243.88.176 permit 46.243.88.176 permit
46.243.88.177 permit 46.243.88.177 permit
46.243.95.179 permit
46.243.95.180 permit
50.18.45.249 permit 50.18.45.249 permit
50.18.121.236 permit 50.18.121.236 permit
50.18.121.248 permit 50.18.121.248 permit
@ -189,6 +178,11 @@
50.31.32.0/19 permit 50.31.32.0/19 permit
50.31.156.96/27 permit 50.31.156.96/27 permit
50.31.205.0/24 permit 50.31.205.0/24 permit
51.4.71.62 permit
51.4.72.0/24 permit
51.4.80.0/27 permit
51.5.72.0/24 permit
51.5.80.0/27 permit
51.137.58.21 permit 51.137.58.21 permit
51.140.75.55 permit 51.140.75.55 permit
51.144.100.179 permit 51.144.100.179 permit
@ -197,28 +191,17 @@
52.5.230.59 permit 52.5.230.59 permit
52.27.5.72 permit 52.27.5.72 permit
52.27.28.47 permit 52.27.28.47 permit
52.28.63.81 permit 52.33.191.91 permit
52.36.138.31 permit 52.36.138.31 permit
52.37.142.146 permit 52.37.142.146 permit
52.58.216.183 permit 52.38.191.253 permit
52.59.143.3 permit 52.41.64.145 permit
52.60.41.5 permit 52.60.41.5 permit
52.60.115.116 permit 52.60.115.116 permit
52.61.91.9 permit
52.71.0.205 permit
52.82.172.0/22 permit 52.82.172.0/22 permit
52.94.124.0/28 permit 52.94.124.0/28 permit
52.95.48.152/29 permit 52.95.48.152/29 permit
52.95.49.88/29 permit 52.95.49.88/29 permit
52.96.91.34 permit
52.96.111.82 permit
52.96.172.98 permit
52.96.214.50 permit
52.96.222.194 permit
52.96.222.226 permit
52.96.223.2 permit
52.96.228.130 permit
52.96.229.242 permit
52.100.0.0/14 permit 52.100.0.0/14 permit
52.119.213.144/28 permit 52.119.213.144/28 permit
52.160.39.140 permit 52.160.39.140 permit
@ -231,29 +214,23 @@
52.222.73.83 permit 52.222.73.83 permit
52.222.73.120 permit 52.222.73.120 permit
52.222.75.85 permit 52.222.75.85 permit
52.222.89.228 permit
52.234.172.96/28 permit 52.234.172.96/28 permit
52.236.28.240/28 permit 52.236.28.240/28 permit
52.237.141.173 permit
52.244.206.214 permit 52.244.206.214 permit
52.247.53.144 permit 52.247.53.144 permit
52.250.107.196 permit 52.250.107.196 permit
52.250.126.174 permit 52.250.126.174 permit
52.251.55.143 permit
54.90.148.255 permit 54.90.148.255 permit
54.156.255.69 permit
54.172.97.247 permit 54.172.97.247 permit
54.174.52.0/24 permit
54.174.53.128/30 permit
54.174.57.0/24 permit
54.174.59.0/24 permit
54.174.60.0/23 permit
54.174.63.0/24 permit
54.186.193.102 permit 54.186.193.102 permit
54.191.223.56 permit 54.191.223.5 permit
54.194.61.95 permit 54.194.61.95 permit
54.195.113.45 permit 54.195.113.45 permit
54.213.20.246 permit
54.214.39.184 permit 54.214.39.184 permit
54.216.77.168 permit 54.216.77.168 permit
54.221.227.204 permit
54.240.0.0/18 permit 54.240.0.0/18 permit
54.240.64.0/19 permit 54.240.64.0/19 permit
54.240.96.0/19 permit 54.240.96.0/19 permit
@ -261,9 +238,7 @@
54.244.54.130 permit 54.244.54.130 permit
54.244.242.0/24 permit 54.244.242.0/24 permit
54.246.232.180 permit 54.246.232.180 permit
54.255.61.23 permit
62.13.128.0/24 permit 62.13.128.0/24 permit
62.13.128.150 permit
62.13.129.128/25 permit 62.13.129.128/25 permit
62.13.136.0/22 permit 62.13.136.0/22 permit
62.13.140.0/22 permit 62.13.140.0/22 permit
@ -274,29 +249,22 @@
62.17.146.128/26 permit 62.17.146.128/26 permit
62.140.7.0/24 permit 62.140.7.0/24 permit
62.140.10.21 permit 62.140.10.21 permit
62.179.121.0/24 permit
62.201.172.0/27 permit
62.201.172.32/27 permit
62.253.227.114 permit
63.32.13.159 permit 63.32.13.159 permit
63.80.14.0/23 permit 63.80.14.0/23 permit
63.111.28.137 permit
63.128.21.0/24 permit 63.128.21.0/24 permit
63.143.57.128/25 permit 63.143.57.128/25 permit
63.143.59.128/25 permit 63.143.59.128/25 permit
64.18.0.0/20 permit 64.18.0.0/20 permit
64.20.241.45 permit 64.20.241.45 permit
64.69.212.0/24 permit 64.34.47.128/27 permit
64.34.57.192/26 permit
64.71.149.160/28 permit 64.71.149.160/28 permit
64.79.155.0/24 permit 64.79.155.0/24 permit
64.79.155.192 permit
64.79.155.193 permit
64.79.155.205 permit
64.79.155.206 permit
64.89.44.85 permit 64.89.44.85 permit
64.89.45.80 permit 64.89.45.80 permit
64.89.45.194 permit 64.89.45.194 permit
64.89.45.196 permit 64.89.45.196 permit
64.95.144.196 permit
64.127.115.252 permit 64.127.115.252 permit
64.132.88.0/23 permit 64.132.88.0/23 permit
64.132.92.0/24 permit 64.132.92.0/24 permit
@ -322,7 +290,6 @@
64.207.219.71 permit 64.207.219.71 permit
64.207.219.72 permit 64.207.219.72 permit
64.207.219.73 permit 64.207.219.73 permit
64.207.219.75 permit
64.207.219.77 permit 64.207.219.77 permit
64.207.219.78 permit 64.207.219.78 permit
64.207.219.79 permit 64.207.219.79 permit
@ -333,6 +300,9 @@
64.207.219.142 permit 64.207.219.142 permit
64.207.219.143 permit 64.207.219.143 permit
64.233.160.0/19 permit 64.233.160.0/19 permit
65.38.115.76 permit
65.38.115.84 permit
65.39.215.0/24 permit
65.52.80.137 permit 65.52.80.137 permit
65.54.51.64/26 permit 65.54.51.64/26 permit
65.54.61.64/26 permit 65.54.61.64/26 permit
@ -372,10 +342,6 @@
66.111.4.225 permit 66.111.4.225 permit
66.111.4.229 permit 66.111.4.229 permit
66.111.4.230 permit 66.111.4.230 permit
66.119.150.192/26 permit
66.135.202.0/27 permit
66.135.215.0/24 permit
66.135.222.1 permit
66.162.193.226/31 permit 66.162.193.226/31 permit
66.163.184.0/21 permit 66.163.184.0/21 permit
66.163.184.0/24 permit 66.163.184.0/24 permit
@ -407,8 +373,7 @@
66.196.81.234 permit 66.196.81.234 permit
66.211.168.230/31 permit 66.211.168.230/31 permit
66.211.170.86/31 permit 66.211.170.86/31 permit
66.211.170.88/29 permit 66.211.170.88/30 permit
66.211.184.0/23 permit
66.218.74.64/30 permit 66.218.74.64/30 permit
66.218.74.68/31 permit 66.218.74.68/31 permit
66.218.75.112/30 permit 66.218.75.112/30 permit
@ -480,8 +445,6 @@
68.142.230.72/30 permit 68.142.230.72/30 permit
68.142.230.76/31 permit 68.142.230.76/31 permit
68.142.230.78 permit 68.142.230.78 permit
68.232.140.138 permit
68.232.157.143 permit
68.232.192.0/20 permit 68.232.192.0/20 permit
69.63.178.128/25 permit 69.63.178.128/25 permit
69.63.181.0/24 permit 69.63.181.0/24 permit
@ -489,10 +452,6 @@
69.65.42.195 permit 69.65.42.195 permit
69.65.49.192/29 permit 69.65.49.192/29 permit
69.72.32.0/20 permit 69.72.32.0/20 permit
69.72.40.93 permit
69.72.40.94/31 permit
69.72.40.96/30 permit
69.72.47.205 permit
69.147.84.227 permit 69.147.84.227 permit
69.162.98.0/24 permit 69.162.98.0/24 permit
69.169.224.0/20 permit 69.169.224.0/20 permit
@ -501,7 +460,7 @@
70.37.151.128/25 permit 70.37.151.128/25 permit
70.42.149.0/24 permit 70.42.149.0/24 permit
70.42.149.35 permit 70.42.149.35 permit
72.3.237.64/28 permit 72.3.185.0/24 permit
72.14.192.0/18 permit 72.14.192.0/18 permit
72.21.192.0/19 permit 72.21.192.0/19 permit
72.21.217.142 permit 72.21.217.142 permit
@ -563,11 +522,15 @@
72.30.239.228/31 permit 72.30.239.228/31 permit
72.30.239.244/30 permit 72.30.239.244/30 permit
72.30.239.248/31 permit 72.30.239.248/31 permit
72.32.154.0/24 permit
72.32.217.0/24 permit
72.32.243.0/24 permit
72.34.168.76 permit 72.34.168.76 permit
72.34.168.80 permit 72.34.168.80 permit
72.34.168.85 permit 72.34.168.85 permit
72.34.168.86 permit 72.34.168.86 permit
72.52.72.32/28 permit 72.52.72.32/28 permit
72.52.72.36 permit
74.6.128.0/21 permit 74.6.128.0/21 permit
74.6.128.0/24 permit 74.6.128.0/24 permit
74.6.129.0/24 permit 74.6.129.0/24 permit
@ -595,11 +558,8 @@
74.112.67.243 permit 74.112.67.243 permit
74.125.0.0/16 permit 74.125.0.0/16 permit
74.202.227.40 permit 74.202.227.40 permit
74.208.4.192/26 permit
74.208.5.64/26 permit
74.208.122.0/26 permit
74.209.250.0/24 permit 74.209.250.0/24 permit
76.223.128.0/19 permit 74.209.250.12 permit
76.223.176.0/20 permit 76.223.176.0/20 permit
77.238.176.0/22 permit 77.238.176.0/22 permit
77.238.176.0/24 permit 77.238.176.0/24 permit
@ -623,13 +583,7 @@
77.238.189.146/31 permit 77.238.189.146/31 permit
77.238.189.148/30 permit 77.238.189.148/30 permit
81.223.46.0/27 permit 81.223.46.0/27 permit
82.165.159.0/24 permit 84.16.77.1 permit
82.165.159.0/26 permit
82.165.229.31 permit
82.165.229.130 permit
82.165.230.21 permit
82.165.230.22 permit
84.116.36.0/24 permit
85.158.136.0/21 permit 85.158.136.0/21 permit
86.61.88.25 permit 86.61.88.25 permit
87.198.219.130 permit 87.198.219.130 permit
@ -670,11 +624,11 @@
87.248.117.201 permit 87.248.117.201 permit
87.248.117.202 permit 87.248.117.202 permit
87.248.117.205 permit 87.248.117.205 permit
87.252.219.254 permit
87.253.232.0/21 permit 87.253.232.0/21 permit
89.22.108.0/24 permit 89.22.108.0/24 permit
91.194.248.0/23 permit
91.211.240.0/22 permit
91.220.42.0/24 permit 91.220.42.0/24 permit
94.236.119.0/26 permit
94.245.112.0/27 permit 94.245.112.0/27 permit
94.245.112.10/31 permit 94.245.112.10/31 permit
95.131.104.0/21 permit 95.131.104.0/21 permit
@ -684,7 +638,6 @@
96.43.148.64/28 permit 96.43.148.64/28 permit
96.43.148.64/31 permit 96.43.148.64/31 permit
96.43.151.64/28 permit 96.43.151.64/28 permit
98.97.248.0/21 permit
98.136.44.181 permit 98.136.44.181 permit
98.136.44.182/31 permit 98.136.44.182/31 permit
98.136.44.184 permit 98.136.44.184 permit
@ -1189,21 +1142,23 @@
98.139.245.212/31 permit 98.139.245.212/31 permit
99.78.197.208/28 permit 99.78.197.208/28 permit
103.2.140.0/22 permit 103.2.140.0/22 permit
103.9.8.121 permit
103.9.8.122 permit
103.9.8.123 permit
103.9.96.0/22 permit 103.9.96.0/22 permit
103.13.69.0/24 permit 103.13.69.0/24 permit
103.28.42.0/24 permit
103.47.204.0/22 permit 103.47.204.0/22 permit
103.96.21.0/24 permit 103.96.21.0/24 permit
103.96.22.0/24 permit
103.96.23.0/24 permit 103.96.23.0/24 permit
103.151.192.0/23 permit 103.151.192.0/23 permit
103.168.172.128/27 permit 103.237.104.0/22 permit
104.43.243.237 permit 104.43.243.237 permit
104.44.112.128/25 permit
104.47.0.0/17 permit 104.47.0.0/17 permit
104.130.96.0/28 permit 104.130.96.0/28 permit
104.130.122.0/23 permit 104.130.122.0/23 permit
104.214.25.77 permit 104.214.25.77 permit
104.215.148.63 permit
104.215.186.3 permit
104.245.209.192/26 permit 104.245.209.192/26 permit
106.10.144.64/27 permit 106.10.144.64/27 permit
106.10.144.100/31 permit 106.10.144.100/31 permit
@ -1365,8 +1320,6 @@
117.120.16.0/21 permit 117.120.16.0/21 permit
119.42.242.52/31 permit 119.42.242.52/31 permit
119.42.242.156 permit 119.42.242.156 permit
121.244.91.48 permit
122.15.156.182 permit
123.126.78.64/29 permit 123.126.78.64/29 permit
124.47.150.0/24 permit 124.47.150.0/24 permit
124.47.189.0/24 permit 124.47.189.0/24 permit
@ -1382,35 +1335,20 @@
128.127.70.0/26 permit 128.127.70.0/26 permit
128.245.0.0/20 permit 128.245.0.0/20 permit
128.245.64.0/20 permit 128.245.64.0/20 permit
128.245.176.0/20 permit
128.245.242.0/24 permit
128.245.242.16 permit
128.245.242.17 permit
128.245.242.18 permit
128.245.243.0/24 permit
128.245.244.0/24 permit
128.245.245.0/24 permit
128.245.246.0/24 permit
128.245.247.0/24 permit
129.41.77.70 permit 129.41.77.70 permit
129.41.169.249 permit 129.41.169.249 permit
129.80.5.164 permit
129.80.67.121 permit
129.146.88.28 permit
129.146.147.105 permit
129.146.236.58 permit 129.146.236.58 permit
129.151.67.221 permit
129.153.62.216 permit
129.153.104.71 permit
129.153.168.146 permit
129.153.190.200 permit
129.153.194.228 permit 129.153.194.228 permit
129.159.87.137 permit 129.159.87.137 permit
129.213.195.191 permit
130.61.9.72 permit 130.61.9.72 permit
130.211.0.0/22 permit 130.211.0.0/22 permit
130.248.172.0/24 permit
130.248.173.0/24 permit
131.107.0.0/16 permit
131.253.30.0/24 permit 131.253.30.0/24 permit
131.253.121.0/26 permit 131.253.121.0/26 permit
131.253.121.20 permit
131.253.121.52 permit
132.145.13.209 permit 132.145.13.209 permit
132.226.26.225 permit 132.226.26.225 permit
132.226.49.32 permit 132.226.49.32 permit
@ -1420,13 +1358,9 @@
134.170.141.64/26 permit 134.170.141.64/26 permit
134.170.143.0/24 permit 134.170.143.0/24 permit
134.170.174.0/24 permit 134.170.174.0/24 permit
135.84.80.0/24 permit 135.84.80.192/26 permit
135.84.81.0/24 permit
135.84.82.0/24 permit 135.84.82.0/24 permit
135.84.83.0/24 permit
135.84.216.0/22 permit 135.84.216.0/22 permit
136.143.160.0/24 permit
136.143.161.0/24 permit
136.143.182.0/23 permit 136.143.182.0/23 permit
136.143.184.0/24 permit 136.143.184.0/24 permit
136.143.188.0/24 permit 136.143.188.0/24 permit
@ -1435,53 +1369,34 @@
136.147.176.0/20 permit 136.147.176.0/20 permit
136.147.176.0/24 permit 136.147.176.0/24 permit
136.147.182.0/24 permit 136.147.182.0/24 permit
136.179.50.206 permit
138.91.172.26 permit 138.91.172.26 permit
139.60.152.0/22 permit 139.60.152.0/22 permit
139.138.35.44 permit 139.178.64.159 permit
139.138.46.121 permit 139.178.64.195 permit
139.138.46.176 permit
139.138.46.219 permit
139.138.57.55 permit
139.138.58.119 permit
139.180.17.0/24 permit
141.148.159.229 permit
141.193.32.0/23 permit 141.193.32.0/23 permit
143.55.224.0/21 permit 143.55.224.0/21 permit
143.55.232.0/22 permit 143.55.232.0/22 permit
143.55.236.0/22 permit 143.55.236.0/22 permit
143.244.80.0/20 permit
144.24.6.140 permit
144.34.8.247 permit
144.34.9.247 permit
144.34.32.247 permit
144.34.33.247 permit
144.178.36.0/24 permit 144.178.36.0/24 permit
144.178.38.0/24 permit 144.178.38.0/24 permit
145.253.228.160/29 permit
145.253.239.128/29 permit
146.20.112.0/26 permit 146.20.112.0/26 permit
146.20.113.0/24 permit 146.20.113.0/24 permit
146.20.191.0/24 permit 146.20.191.0/24 permit
146.20.215.0/24 permit 146.20.215.0/24 permit
146.20.215.182 permit
146.88.28.0/24 permit
146.101.78.0/24 permit 146.101.78.0/24 permit
147.28.36.0/24 permit 147.75.65.173 permit
147.75.65.174 permit
147.75.98.190 permit
147.160.158.0/24 permit 147.160.158.0/24 permit
147.243.1.47 permit 147.243.1.47 permit
147.243.1.48 permit 147.243.1.48 permit
147.243.1.153 permit 147.243.1.153 permit
147.243.128.24 permit 147.243.128.24 permit
147.243.128.26 permit 147.243.128.26 permit
148.105.0.0/16 permit 148.105.0.14 permit
148.105.8.0/21 permit 148.105.8.0/21 permit
149.72.0.0/16 permit 149.72.0.0/16 permit
149.97.173.180 permit
150.230.98.160 permit
152.67.105.195 permit 152.67.105.195 permit
152.69.200.236 permit
155.248.208.51 permit
157.55.0.192/26 permit 157.55.0.192/26 permit
157.55.1.128/26 permit 157.55.1.128/26 permit
157.55.2.0/25 permit 157.55.2.0/25 permit
@ -1497,43 +1412,32 @@
157.56.232.0/21 permit 157.56.232.0/21 permit
157.56.240.0/20 permit 157.56.240.0/20 permit
157.56.248.0/21 permit 157.56.248.0/21 permit
157.58.30.128/25 permit
157.58.196.96/29 permit 157.58.196.96/29 permit
157.58.249.3 permit 157.58.249.3 permit
157.151.208.65 permit 157.151.208.65 permit
157.255.1.64/29 permit 157.255.1.64/29 permit
158.101.211.207 permit
158.120.80.0/21 permit
158.247.16.0/20 permit
159.92.157.0/24 permit 159.92.157.0/24 permit
159.92.157.16 permit
159.92.157.17 permit
159.92.157.18 permit
159.92.158.0/24 permit 159.92.158.0/24 permit
159.92.159.0/24 permit 159.92.159.0/24 permit
159.92.160.0/24 permit 159.92.160.0/24 permit
159.92.161.0/24 permit 159.92.161.0/24 permit
159.92.162.0/24 permit 159.92.162.0/24 permit
159.112.240.0/20 permit
159.112.242.162 permit
159.135.132.128/25 permit 159.135.132.128/25 permit
159.135.140.80/29 permit 159.135.140.80/29 permit
159.135.224.0/20 permit 159.135.224.0/20 permit
159.135.228.10 permit
159.183.0.0/16 permit 159.183.0.0/16 permit
160.1.62.192 permit
161.38.192.0/20 permit 161.38.192.0/20 permit
161.38.204.0/22 permit 161.38.204.0/22 permit
161.71.32.0/19 permit 161.71.32.0/19 permit
161.71.64.0/20 permit 161.71.64.0/20 permit
162.208.119.181 permit
162.247.216.0/22 permit 162.247.216.0/22 permit
163.47.180.0/22 permit
163.47.180.0/23 permit 163.47.180.0/23 permit
163.114.130.16 permit 163.114.130.16 permit
163.114.132.120 permit 163.114.132.120 permit
165.173.128.0/24 permit
166.78.68.0/22 permit 166.78.68.0/22 permit
166.78.68.221 permit 166.78.68.221 permit
166.78.69.146 permit
166.78.69.169 permit 166.78.69.169 permit
166.78.69.170 permit 166.78.69.170 permit
166.78.71.131 permit 166.78.71.131 permit
@ -1553,13 +1457,10 @@
167.216.129.210 permit 167.216.129.210 permit
167.216.131.180 permit 167.216.131.180 permit
167.220.67.232/29 permit 167.220.67.232/29 permit
167.220.67.238 permit
168.138.5.36 permit 168.138.5.36 permit
168.138.73.51 permit
168.245.0.0/17 permit 168.245.0.0/17 permit
169.148.129.0/24 permit
169.148.131.0/24 permit
170.10.68.0/22 permit 170.10.68.0/22 permit
170.10.128.0/24 permit
170.10.129.0/24 permit 170.10.129.0/24 permit
170.10.133.0/24 permit 170.10.133.0/24 permit
172.217.0.0/19 permit 172.217.0.0/19 permit
@ -1574,8 +1475,10 @@
173.194.0.0/16 permit 173.194.0.0/16 permit
173.203.79.182 permit 173.203.79.182 permit
173.203.81.39 permit 173.203.81.39 permit
173.224.160.128/25 permit
173.224.160.188 permit
173.224.161.128/25 permit 173.224.161.128/25 permit
173.224.165.0/26 permit 173.228.155.0/24 permit
174.36.84.8/29 permit 174.36.84.8/29 permit
174.36.84.16/29 permit 174.36.84.16/29 permit
174.36.84.32/29 permit 174.36.84.32/29 permit
@ -1588,7 +1491,6 @@
174.36.114.152/29 permit 174.36.114.152/29 permit
174.37.67.28/30 permit 174.37.67.28/30 permit
174.129.203.189 permit 174.129.203.189 permit
175.41.215.51 permit
176.32.105.0/24 permit 176.32.105.0/24 permit
176.32.127.0/24 permit 176.32.127.0/24 permit
178.236.10.128/26 permit 178.236.10.128/26 permit
@ -1596,9 +1498,8 @@
182.50.76.0/22 permit 182.50.76.0/22 permit
182.50.78.64/28 permit 182.50.78.64/28 permit
183.240.219.64/29 permit 183.240.219.64/29 permit
185.4.120.0/23 permit
185.4.122.0/24 permit
185.12.80.0/22 permit 185.12.80.0/22 permit
185.28.196.0/22 permit
185.58.84.93 permit 185.58.84.93 permit
185.58.85.0/24 permit 185.58.85.0/24 permit
185.58.86.0/24 permit 185.58.86.0/24 permit
@ -1608,13 +1509,9 @@
185.80.93.204 permit 185.80.93.204 permit
185.80.93.227 permit 185.80.93.227 permit
185.80.95.31 permit 185.80.95.31 permit
185.90.20.0/22 permit
185.189.236.0/22 permit 185.189.236.0/22 permit
185.211.120.0/22 permit 185.211.120.0/22 permit
185.250.236.0/22 permit 185.250.236.0/22 permit
185.250.239.148 permit
185.250.239.168 permit
185.250.239.190 permit
188.125.68.132 permit 188.125.68.132 permit
188.125.68.152/31 permit 188.125.68.152/31 permit
188.125.68.156 permit 188.125.68.156 permit
@ -1666,7 +1563,7 @@
188.125.85.238 permit 188.125.85.238 permit
188.172.128.0/20 permit 188.172.128.0/20 permit
192.0.64.0/18 permit 192.0.64.0/18 permit
192.18.139.154 permit 192.28.128.0/18 permit
192.30.252.0/22 permit 192.30.252.0/22 permit
192.64.236.0/24 permit 192.64.236.0/24 permit
192.64.237.0/24 permit 192.64.237.0/24 permit
@ -1682,21 +1579,16 @@
192.254.113.10 permit 192.254.113.10 permit
192.254.113.101 permit 192.254.113.101 permit
192.254.114.176 permit 192.254.114.176 permit
192.254.118.63 permit
193.7.206.0/25 permit 193.7.206.0/25 permit
193.7.207.0/25 permit 193.7.207.0/25 permit
193.109.254.0/23 permit 193.109.254.0/23 permit
193.122.128.100 permit 193.122.128.100 permit
194.64.234.128/27 permit
194.64.234.129 permit 194.64.234.129 permit
194.104.109.0/24 permit 194.104.109.0/24 permit
194.104.110.21 permit
194.104.110.240/28 permit
194.104.111.0/24 permit 194.104.111.0/24 permit
194.106.220.0/23 permit 194.106.220.0/23 permit
194.113.24.0/22 permit
194.154.193.192/27 permit 194.154.193.192/27 permit
195.4.92.0/23 permit
195.54.172.0/23 permit
195.130.217.0/24 permit 195.130.217.0/24 permit
195.234.109.226 permit 195.234.109.226 permit
195.245.230.0/23 permit 195.245.230.0/23 permit
@ -1713,23 +1605,19 @@
198.37.144.0/20 permit 198.37.144.0/20 permit
198.37.152.186 permit 198.37.152.186 permit
198.61.254.0/23 permit 198.61.254.0/23 permit
198.61.254.21 permit
198.61.254.231 permit 198.61.254.231 permit
198.74.56.28 permit
198.178.234.57 permit 198.178.234.57 permit
198.244.48.0/20 permit
198.244.60.0/22 permit
198.245.80.0/20 permit 198.245.80.0/20 permit
198.245.81.0/24 permit 198.245.81.0/24 permit
199.15.176.173 permit 199.15.176.173 permit
199.15.212.0/22 permit
199.15.213.187 permit 199.15.213.187 permit
199.15.226.37 permit 199.15.226.37 permit
199.16.156.0/22 permit 199.16.156.0/22 permit
199.33.145.1 permit 199.33.145.1 permit
199.33.145.32 permit 199.33.145.32 permit
199.59.148.0/22 permit 199.59.148.0/22 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
199.101.161.130 permit 199.101.161.130 permit
199.101.162.0/25 permit 199.101.162.0/25 permit
199.122.120.0/21 permit 199.122.120.0/21 permit
@ -1742,10 +1630,8 @@
202.177.148.110 permit 202.177.148.110 permit
203.31.36.0/22 permit 203.31.36.0/22 permit
203.32.4.25 permit 203.32.4.25 permit
203.55.21.0/24 permit
203.81.17.0/24 permit 203.81.17.0/24 permit
203.122.32.250 permit 203.122.32.250 permit
203.145.57.160/27 permit
203.188.194.32 permit 203.188.194.32 permit
203.188.194.151 permit 203.188.194.151 permit
203.188.194.203 permit 203.188.194.203 permit
@ -1780,31 +1666,28 @@
203.209.230.76/31 permit 203.209.230.76/31 permit
204.11.168.0/21 permit 204.11.168.0/21 permit
204.13.11.48/29 permit 204.13.11.48/29 permit
204.13.11.48/30 permit
204.14.232.0/21 permit 204.14.232.0/21 permit
204.14.232.64/28 permit 204.14.232.64/28 permit
204.14.234.64/28 permit 204.14.234.64/28 permit
204.29.186.0/23 permit 204.29.186.0/23 permit
204.75.142.0/24 permit
204.79.197.212 permit 204.79.197.212 permit
204.92.114.187 permit 204.92.114.187 permit
204.92.114.203 permit 204.92.114.203 permit
204.92.114.204/31 permit 204.92.114.204/31 permit
204.141.32.0/23 permit 204.141.32.0/23 permit
204.141.42.0/23 permit 204.141.42.0/23 permit
204.153.121.0/24 permit
204.232.168.0/24 permit 204.232.168.0/24 permit
205.139.110.0/24 permit 205.139.110.0/24 permit
205.201.128.0/20 permit 205.201.128.0/20 permit
205.201.131.128/25 permit 205.201.131.128/25 permit
205.201.134.128/25 permit 205.201.134.128/25 permit
205.201.136.0/23 permit 205.201.136.0/23 permit
205.201.137.229 permit
205.201.139.0/24 permit 205.201.139.0/24 permit
205.207.104.0/22 permit 205.207.104.0/22 permit
205.207.104.108 permit
205.220.167.17 permit 205.220.167.17 permit
205.220.167.98 permit
205.220.179.17 permit 205.220.179.17 permit
205.220.179.98 permit
205.251.233.32 permit 205.251.233.32 permit
205.251.233.36 permit 205.251.233.36 permit
206.25.247.143 permit 206.25.247.143 permit
@ -1840,7 +1723,6 @@
207.211.31.0/25 permit 207.211.31.0/25 permit
207.211.41.113 permit 207.211.41.113 permit
207.218.90.0/24 permit 207.218.90.0/24 permit
207.218.90.122 permit
207.250.68.0/24 permit 207.250.68.0/24 permit
208.40.232.70 permit 208.40.232.70 permit
208.43.21.28/30 permit 208.43.21.28/30 permit
@ -1876,10 +1758,8 @@
208.71.42.212/31 permit 208.71.42.212/31 permit
208.71.42.214 permit 208.71.42.214 permit
208.72.249.240/29 permit 208.72.249.240/29 permit
208.74.204.0/22 permit
208.74.204.9 permit 208.74.204.9 permit
208.75.120.0/22 permit 208.75.120.0/22 permit
208.75.121.246 permit
208.75.122.246 permit 208.75.122.246 permit
208.82.237.96/29 permit 208.82.237.96/29 permit
208.82.237.104/31 permit 208.82.237.104/31 permit
@ -1893,13 +1773,14 @@
209.46.117.168 permit 209.46.117.168 permit
209.46.117.179 permit 209.46.117.179 permit
209.61.151.0/24 permit 209.61.151.0/24 permit
209.61.151.236 permit
209.61.151.249 permit
209.61.151.251 permit
209.67.98.46 permit 209.67.98.46 permit
209.67.98.59 permit 209.67.98.59 permit
209.85.128.0/17 permit 209.85.128.0/17 permit
212.4.136.0/26 permit 212.4.136.0/26 permit
212.25.240.80 permit
212.25.240.83 permit
212.25.240.84/31 permit
212.25.240.88 permit
212.82.96.0/24 permit 212.82.96.0/24 permit
212.82.96.32/27 permit 212.82.96.32/27 permit
212.82.96.64/29 permit 212.82.96.64/29 permit
@ -1940,12 +1821,6 @@
212.82.111.228/31 permit 212.82.111.228/31 permit
212.82.111.230 permit 212.82.111.230 permit
212.123.28.40 permit 212.123.28.40 permit
212.227.15.0/24 permit
212.227.15.0/25 permit
212.227.17.0/27 permit
212.227.126.128/25 permit
213.46.255.0/24 permit
213.165.64.0/23 permit
213.167.75.0/25 permit 213.167.75.0/25 permit
213.167.81.0/25 permit 213.167.81.0/25 permit
213.199.128.139 permit 213.199.128.139 permit
@ -1986,10 +1861,6 @@
216.46.168.0/24 permit 216.46.168.0/24 permit
216.58.192.0/19 permit 216.58.192.0/19 permit
216.66.217.240/29 permit 216.66.217.240/29 permit
216.71.138.33 permit
216.71.152.207 permit
216.71.154.29 permit
216.71.155.89 permit
216.74.162.13 permit 216.74.162.13 permit
216.74.162.14 permit 216.74.162.14 permit
216.82.240.0/20 permit 216.82.240.0/20 permit
@ -1999,49 +1870,33 @@
216.109.114.0/24 permit 216.109.114.0/24 permit
216.109.114.32/27 permit 216.109.114.32/27 permit
216.109.114.64/29 permit 216.109.114.64/29 permit
216.113.160.0/24 permit
216.113.172.0/25 permit
216.113.175.0/24 permit
216.128.126.97 permit 216.128.126.97 permit
216.136.162.65 permit 216.136.162.65 permit
216.136.162.120/29 permit 216.136.162.120/29 permit
216.136.168.80/28 permit 216.136.168.80/28 permit
216.145.217.0/24 permit
216.145.221.0/24 permit
216.198.0.0/18 permit 216.198.0.0/18 permit
216.203.30.55 permit 216.203.30.55 permit
216.203.33.178/31 permit 216.203.33.178/31 permit
216.205.24.0/24 permit 216.205.24.0/24 permit
216.239.32.0/19 permit 216.239.32.0/19 permit
217.72.192.64/26 permit
217.72.192.248/29 permit
217.72.207.0/27 permit
217.77.141.52 permit 217.77.141.52 permit
217.77.141.59 permit 217.77.141.59 permit
217.175.194.0/24 permit
222.73.195.64/29 permit 222.73.195.64/29 permit
223.165.113.0/24 permit 223.165.113.0/24 permit
223.165.115.0/24 permit 223.165.115.0/24 permit
223.165.118.0/23 permit 223.165.118.0/23 permit
223.165.120.0/23 permit 223.165.120.0/23 permit
2001:0868:0100:0600::/64 permit
2001:4860:4000::/36 permit 2001:4860:4000::/36 permit
2001:748:100:40::2:0/112 permit
2404:6800:4000::/36 permit 2404:6800:4000::/36 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 2607:f8b0:4000::/36 permit
2620:109:c003:104::/64 permit
2620:109:c003:104::215 permit 2620:109:c003:104::215 permit
2620:109:c006:104::/64 permit 2620:109:c003:104::/64 permit
2620:109:c006:104::215 permit 2620:109:c006:104::215 permit
2620:109:c006:104::/64 permit
2620:109:c00d:104::/64 permit 2620:109:c00d:104::/64 permit
2620:10d:c090:450::120 permit 2620:10d:c090:450::120 permit
2620:10d:c091:400::8:1 permit 2620:10d:c091:450::16 permit
2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit 2620:119:50c0:207::215 permit
2620:119:50c0:207::/64 permit
2800:3f0:4000::/36 permit 2800:3f0:4000::/36 permit
194.25.134.0/24 permit # t-online.de 194.25.134.0/24 permit # t-online.de

View File

@ -27,5 +27,4 @@
#197518 2 #Rackmarkt SL, Spain #197518 2 #Rackmarkt SL, Spain
#197695 2 #Domain names registrar REG.RU Ltd, Russia #197695 2 #Domain names registrar REG.RU Ltd, Russia
#198068 2 #P.A.G.M. OU, Estonia #198068 2 #P.A.G.M. OU, Estonia
#201942 5 #Soltia Consulting SL, Spain #201942 5 #Soltia Consulting SL, Spain
#213373 4 #IP Connect Inc

View File

@ -3,6 +3,7 @@
/.*episerver.*/i /.*episerver.*/i
/.*supergewinne.*/i /.*supergewinne.*/i
/List-Unsubscribe.*nbps\.eu/i /List-Unsubscribe.*nbps\.eu/i
/X-Mailer: AWeber.*/i
/.*regiofinder.*/i /.*regiofinder.*/i
/.*EmailSocket.*/i /.*EmailSocket.*/i
/List-Unsubscribe:.*respread.*/i /List-Unsubscribe:.*respread.*/i

View File

@ -8,7 +8,7 @@ VIRUS_FOUND {
} }
# Bad policy from free mail providers # Bad policy from free mail providers
FREEMAIL_POLICY_FAILURE { FREEMAIL_POLICY_FAILURE {
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies"; expression = "-g+:policies & !DMARC_POLICY_ALLOW & !MAILLIST & ( FREEMAIL_ENVFROM | FREEMAIL_FROM ) & !WHITELISTED_FWD_HOST";
score = 16.0; score = 16.0;
} }
# Applies to freemail with undisclosed recipients # Applies to freemail with undisclosed recipients
@ -68,39 +68,3 @@ WL_FWD_HOST {
ENCRYPTED_CHAT { ENCRYPTED_CHAT {
expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP"; expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
} }
CLAMD_SPAM_FOUND {
expression = "CLAM_SECI_SPAM & !MAILCOW_WHITE";
description = "Probably Spam, Securite Spam Flag set through ClamAV";
score = 5;
}
CLAMD_BAD_PDF {
expression = "CLAM_SECI_PDF & !MAILCOW_WHITE";
description = "Bad PDF Found, Securite bad PDF Flag set through ClamAV";
score = 8;
}
CLAMD_BAD_JPG {
expression = "CLAM_SECI_JPG & !MAILCOW_WHITE";
description = "Bad JPG Found, Securite bad JPG Flag set through ClamAV";
score = 8;
}
CLAMD_ASCII_MALWARE {
expression = "CLAM_SECI_ASCII & !MAILCOW_WHITE";
description = "ASCII malware found, Securite ASCII malware Flag set through ClamAV";
score = 8;
}
CLAMD_HTML_MALWARE {
expression = "CLAM_SECI_HTML & !MAILCOW_WHITE";
description = "HTML malware found, Securite HTML malware Flag set through ClamAV";
score = 8;
}
CLAMD_JS_MALWARE {
expression = "CLAM_SECI_JS & !MAILCOW_WHITE";
description = "JS malware found, Securite JS malware Flag set through ClamAV";
score = 8;
}

View File

@ -18,9 +18,6 @@ symbols {
"ENCRYPTED_CHAT" { "ENCRYPTED_CHAT" {
score = -20.0; score = -20.0;
} }
"SOGO_CONTACT" {
score = -99.0;
}
} }
group "MX" { group "MX" {

View File

@ -16,7 +16,8 @@ rules {
backend = "http"; backend = "http";
url = "http://nginx:9081/pushover.php"; url = "http://nginx:9081/pushover.php";
selector = "mailcow_rcpt"; selector = "mailcow_rcpt";
formatter = "json"; # Only return msgid, do not parse the full message
formatter = "msgid";
meta_headers = true; meta_headers = true;
} }
} }

View File

@ -159,8 +159,8 @@ BAZAAR_ABUSE_CH {
} }
URLHAUS_ABUSE_CH { URLHAUS_ABUSE_CH {
type = "selector"; type = "url";
selector = "urls"; filter = "full";
map = "https://urlhaus.abuse.ch/downloads/text_online/"; map = "https://urlhaus.abuse.ch/downloads/text_online/";
score = 10.0; score = 10.0;
} }
@ -175,7 +175,7 @@ BAD_SUBJECT_00 {
type = "header"; type = "header";
header = "subject"; header = "subject";
regexp = true; regexp = true;
map = "http://fuzzy.mailcow.email/bad-subject-regex.txt"; map = "http://nullnull.org/bad-subject-regex.txt";
score = 6.0; score = 6.0;
symbols_set = ["BAD_SUBJECT_00"]; symbols_set = ["BAD_SUBJECT_00"];
} }

View File

@ -340,10 +340,6 @@ rspamd_config:register_symbol({
if not bcc_dest then if not bcc_dest then
return -- stop return -- stop
end end
-- dot stuff content before sending
local email_content = tostring(task:get_content())
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
-- send mail
lua_smtp.sendmail({ lua_smtp.sendmail({
task = task, task = task,
host = os.getenv("IPV4_NETWORK") .. '.253', host = os.getenv("IPV4_NETWORK") .. '.253',
@ -351,8 +347,8 @@ rspamd_config:register_symbol({
from = task:get_from(stp)[1].addr, from = task:get_from(stp)[1].addr,
recipients = bcc_dest, recipients = bcc_dest,
helo = 'bcc', helo = 'bcc',
timeout = 20, timeout = 10,
}, email_content, sendmail_cb) }, task:get_content(), sendmail_cb)
end end
-- determine from -- determine from

View File

@ -47,14 +47,12 @@ if (!function_exists('getallheaders')) {
} }
$headers = getallheaders(); $headers = getallheaders();
$json_body = json_decode(file_get_contents('php://input'));
$qid = $headers['X-Rspamd-Qid']; $qid = $headers['X-Rspamd-Qid'];
$rcpts = $headers['X-Rspamd-Rcpt']; $rcpts = $headers['X-Rspamd-Rcpt'];
$sender = $headers['X-Rspamd-From']; $sender = $headers['X-Rspamd-From'];
$ip = $headers['X-Rspamd-Ip']; $ip = $headers['X-Rspamd-Ip'];
$subject = $headers['X-Rspamd-Subject']; $subject = $headers['X-Rspamd-Subject'];
$messageid= $json_body->message_id;
$priority = 0; $priority = 0;
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); $symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
@ -67,20 +65,6 @@ if (is_array($symbols_array)) {
} }
} }
$sender_address = $json_body->header_from[0];
$sender_name = '-';
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
$sender_address = $matches['address'];
$sender_name = trim($matches['name'], '"\' ');
}
$to_address = $json_body->header_to[0];
$to_name = '-';
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
$to_address = $matches['address'];
$to_name = trim($matches['name'], '"\' ');
}
$rcpt_final_mailboxes = array(); $rcpt_final_mailboxes = array();
// Loop through all rcpts // Loop through all rcpts
@ -245,16 +229,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) {
$post_fields = array( $post_fields = array(
"token" => $api_data['token'], "token" => $api_data['token'],
"user" => $api_data['key'], "user" => $api_data['key'],
"title" => sprintf("%s", str_replace( "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $title)),
array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}'),
array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid), $title)
),
"priority" => $priority, "priority" => $priority,
"message" => sprintf("%s", str_replace( "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $text))
array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}', '\n'),
array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid, PHP_EOL), $text)
),
"sound" => $attributes['sound'] ?? "pushover"
); );
if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) { if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) {
$post_fields['expire'] = 600; $post_fields['expire'] = 600;

View File

@ -32,6 +32,8 @@
// ); // );
// self-signed is not trusted anymore // self-signed is not trusted anymore
SOGoSieveServer = "sieve://dovecot:4190/?TLS=YES&tlsVerifyMode=none";
SOGoSMTPServer = "smtp://postfix:588/?TLS=YES&tlsVerifyMode=none";
WOPort = "0.0.0.0:20000"; WOPort = "0.0.0.0:20000";
SOGoMemcachedHost = "memcached"; SOGoMemcachedHost = "memcached";
@ -62,7 +64,7 @@
SOGoFirstDayOfWeek = "1"; SOGoFirstDayOfWeek = "1";
SOGoSieveFolderEncoding = "UTF-8"; SOGoSieveFolderEncoding = "UTF-8";
SOGoPasswordChangeEnabled = NO; SOGoPasswordChangeEnabled = YES;
SOGoSentFolderName = "Sent"; SOGoSentFolderName = "Sent";
SOGoMailShowSubscribedFoldersOnly = NO; SOGoMailShowSubscribedFoldersOnly = NO;
NGImap4ConnectionStringSeparator = "/"; NGImap4ConnectionStringSeparator = "/";

View File

@ -13,12 +13,12 @@
Please check the logs or contact support if the error persists.</p> Please check the logs or contact support if the error persists.</p>
<h2>Quick debugging</h2> <h2>Quick debugging</h2>
<p>Check Nginx and PHP logs:</p> <p>Check Nginx and PHP logs:</p>
<pre>docker compose logs --tail=200 php-fpm-mailcow nginx-mailcow</pre> <pre>docker-compose logs --tail=200 php-fpm-mailcow nginx-mailcow</pre>
<p>Make sure your SQL credentials in mailcow.conf (a link to .env) do fit your initialized SQL volume. If you see an access denied, you might have the wrong mailcow.conf:</p> <p>Make sure your SQL credentials in mailcow.conf (a link to .env) do fit your initialized SQL volume. If you see an access denied, you might have the wrong mailcow.conf:</p>
<pre>source mailcow.conf ; docker compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME}</pre> <pre>source mailcow.conf ; docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME}</pre>
<p>In case of a previous failed installation, create a backup of your existing data, followed by removing all volumes and starting over (<b>NEVER</b> do this with a production system, it will remove <b>ALL</b> data):</p> <p>In case of a previous failed installation, create a backup of your existing data, followed by removing all volumes and starting over (<b>NEVER</b> do this with a production system, it will remove <b>ALL</b> data):</p>
<pre>BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all</pre> <pre>BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all</pre>
<pre>docker compose down --volumes ; docker compose up -d</pre> <pre>docker-compose down --volumes ; docker-compose up -d</pre>
<p>Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">here</a> for a list.</p> <p>Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">here</a> for a list.</p>
<br>Click to learn more about <a style="color:red;text-decoration:none;" href="https://mailcow.github.io/mailcow-dockerized-docs/#get-support" target="_blank">getting support.</a> <br>Click to learn more about <a style="color:red;text-decoration:none;" href="https://mailcow.github.io/mailcow-dockerized-docs/#get-support" target="_blank">getting support.</a>
</body> </body>

View File

@ -10,6 +10,9 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
$tfa_data = get_tfa(); $tfa_data = get_tfa();
$fido2_data = fido2(array("action" => "get_friendly_names")); $fido2_data = fido2(array("action" => "get_friendly_names"));
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
$_SESSION['gal'] = json_decode($license_cache, true);
}
$js_minifier->add('/web/js/site/admin.js'); $js_minifier->add('/web/js/site/admin.js');
$js_minifier->add('/web/js/presets/rspamd.js'); $js_minifier->add('/web/js/presets/rspamd.js');
@ -80,10 +83,6 @@ foreach ($RSPAMD_MAPS['regex'] as $rspamd_regex_desc => $rspamd_regex_map) {
]; ];
} }
// cors settings
$cors_settings = cors('get');
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
$template = 'admin.twig'; $template = 'admin.twig';
$template_data = [ $template_data = [
@ -91,6 +90,8 @@ $template_data = [
'tfa_id' => @$_SESSION['tfa_id'], 'tfa_id' => @$_SESSION['tfa_id'],
'fido2_cid' => @$_SESSION['fido2_cid'], 'fido2_cid' => @$_SESSION['fido2_cid'],
'fido2_data' => $fido2_data, 'fido2_data' => $fido2_data,
'gal' => @$_SESSION['gal'],
'license_guid' => license('guid'),
'api' => [ 'api' => [
'ro' => admin_api('ro', 'get'), 'ro' => admin_api('ro', 'get'),
'rw' => admin_api('rw', 'get'), 'rw' => admin_api('rw', 'get'),
@ -108,12 +109,9 @@ $template_data = [
'rsettings' => $rsettings, 'rsettings' => $rsettings,
'rspamd_regex_maps' => $rspamd_regex_maps, 'rspamd_regex_maps' => $rspamd_regex_maps,
'logo_specs' => customize('get', 'main_logo_specs'), 'logo_specs' => customize('get', 'main_logo_specs'),
'ip_check' => customize('get', 'ip_check'),
'password_complexity' => password_complexity('get'), 'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'], 'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
'cors_settings' => $cors_settings,
'lang_admin' => json_encode($lang['admin']), 'lang_admin' => json_encode($lang['admin']),
'lang_datatables' => json_encode($lang['datatables'])
]; ];
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';

View File

@ -1,16 +0,0 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}

View File

@ -5,15 +5,56 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Swagger UI</title> <title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" /> <link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="index.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" /> <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head> </head>
<body> <body>
<div id="swagger-ui"></div> <div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script> <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script> <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="./swagger-initializer.js" charset="UTF-8"> </script> <script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "/api/openapi.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body> </body>
</html> </html>

View File

@ -13,7 +13,7 @@
var isValid, qp, arr; var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) { if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&'); qp = window.location.hash.substring(1);
} else { } else {
qp = location.search.substring(1); qp = location.search.substring(1);
} }
@ -38,7 +38,7 @@
authId: oauth2.auth.name, authId: oauth2.auth.name,
source: "auth", source: "auth",
level: "warning", level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server." message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
}); });
} }
@ -58,7 +58,7 @@
authId: oauth2.auth.name, authId: oauth2.auth.name,
source: "auth", source: "auth",
level: "error", level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server." message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
}); });
} }
} else { } else {
@ -67,13 +67,9 @@
window.close(); window.close();
} }
if (document.readyState !== 'loading') { window.addEventListener('DOMContentLoaded', function () {
run(); run();
} else { });
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,4 +1,4 @@
openapi: 3.1.0 openapi: 3.0.0
info: info:
description: >- description: >-
mailcow is complete e-mailing solution with advanced antispam, antivirus, mailcow is complete e-mailing solution with advanced antispam, antivirus,
@ -209,17 +209,10 @@ paths:
- app_passwd - app_passwd
- add - add
- active: "1" - active: "1"
username: info@domain.tld app_name: emclient
app_name: wordpress
app_passwd: keyleudecticidechothistishownsan31 app_passwd: keyleudecticidechothistishownsan31
app_passwd2: keyleudecticidechothistishownsan31 app_passwd2: keyleudecticidechothistishownsan31
protocols: username: hello@mailcow.email
- imap_access
- dav_access
- smtp_access
- eas_access
- pop3_access
- sieve_access
msg: app_passwd_added msg: app_passwd_added
type: success type: success
schema: schema:
@ -256,13 +249,6 @@ paths:
app_name: wordpress app_name: wordpress
app_passwd: keyleudecticidechothistishownsan31 app_passwd: keyleudecticidechothistishownsan31
app_passwd2: keyleudecticidechothistishownsan31 app_passwd2: keyleudecticidechothistishownsan31
protocols:
- imap_access
- dav_access
- smtp_access
- eas_access
- pop3_access
- sieve_access
properties: properties:
active: active:
description: is alias active or not description: is alias active or not
@ -518,23 +504,21 @@ paths:
- domain.tld - domain.tld
type: success type: success
schema: schema:
type: array properties:
items: log:
type: object description: contains request object
properties: items: {}
log: type: array
description: contains request object msg:
items: {} items: {}
type: array type: array
msg: type:
items: {} enum:
type: array - success
type: - danger
enum: - error
- success type: string
- danger type: object
- error
type: string
description: OK description: OK
headers: {} headers: {}
tags: tags:
@ -581,11 +565,6 @@ paths:
domain: domain:
description: Fully qualified domain name description: Fully qualified domain name
type: string type: string
gal:
description: >-
is domain global address list active or not, it enables
shared contacts accross domain in SOGo webmail
type: boolean
mailboxes: mailboxes:
description: limit count of mailboxes associated with this domain description: limit count of mailboxes associated with this domain
type: number type: number
@ -603,9 +582,6 @@ paths:
if not, them you have to create "dummy" mailbox for each if not, them you have to create "dummy" mailbox for each
address to relay address to relay
type: boolean type: boolean
relay_unknown_only:
description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally.
type: boolean
rl_frame: rl_frame:
enum: enum:
- s - s
@ -616,11 +592,6 @@ paths:
rl_value: rl_value:
description: rate limit value description: rate limit value
type: number type: number
tags:
description: tags for this Domain
type: array
items:
type: string
type: object type: object
summary: Create domain summary: Create domain
/api/v1/add/domain-admin: /api/v1/add/domain-admin:
@ -699,38 +670,6 @@ paths:
type: string type: string
type: object type: object
summary: Create Domain Admin user summary: Create Domain Admin user
/api/v1/add/sso/domain-admin:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC"
description: OK
headers: { }
tags:
- Single Sign-On
description: >-
Using this endpoint you can issue a token for Domain Admin user. This token can be used for
autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s
operationId: Issue Domain Admin SSO token
requestBody:
content:
application/json:
schema:
example:
username: testadmin
properties:
username:
description: the username for the admin user
type: object
type: object
summary: Issue Domain Admin SSO token
/api/v1/edit/da-acl: /api/v1/edit/da-acl:
post: post:
responses: responses:
@ -1999,23 +1938,21 @@ paths:
- domain2.tld - domain2.tld
type: success type: success
schema: schema:
type: array properties:
items: log:
type: object description: contains request object
properties: items: {}
log: type: array
description: contains request object msg:
items: {} items: {}
type: array type: array
msg: type:
items: {} enum:
type: array - success
type: - danger
enum: - error
- success type: string
- danger type: object
- error
type: string
description: OK description: OK
headers: {} headers: {}
tags: tags:
@ -2026,15 +1963,14 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: object
example: example:
- domain.tld - domain.tld
- domain2.tld - domain2.tld
properties: properties:
items: items:
type: array description: contains list of domains you want to delete
items: type: object
type: string type: object
summary: Delete domain summary: Delete domain
/api/v1/delete/domain-admin: /api/v1/delete/domain-admin:
post: post:
@ -3022,25 +2958,23 @@ paths:
$ref: "#/components/responses/Unauthorized" $ref: "#/components/responses/Unauthorized"
"200": "200":
content: content:
application/json: "*/*":
schema: schema:
type: array properties:
items: log:
type: object description: contains request object
properties: items: {}
log: type: array
type: array msg:
description: contains request object items: {}
items: {} type: array
msg: type:
type: array enum:
items: {} - success
type: - danger
enum: - error
- success type: string
- danger type: object
- error
type: string
description: OK description: OK
headers: {} headers: {}
tags: tags:
@ -3108,33 +3042,13 @@ paths:
if not, them you have to create "dummy" mailbox for each if not, them you have to create "dummy" mailbox for each
address to relay address to relay
type: boolean type: boolean
relay_unknown_only:
description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally.
type: boolean
relayhost: relayhost:
description: id of relayhost description: id of relayhost
type: number type: number
rl_frame:
enum:
- s
- m
- h
- d
type: string
rl_value:
description: rate limit value
type: number
tags:
description: tags for this Domain
type: array
items:
type: string
type: object type: object
items: items:
description: contains list of domain names you want update description: contains list of domain names you want update
type: array type: object
items:
type: string
type: object type: object
summary: Update domain summary: Update domain
/api/v1/edit/fail2ban: /api/v1/edit/fail2ban:
@ -3176,10 +3090,8 @@ paths:
example: example:
attr: attr:
ban_time: "86400" ban_time: "86400"
ban_time_increment: "1"
blacklist: "10.100.6.5/32,10.100.8.4/32" blacklist: "10.100.6.5/32,10.100.8.4/32"
max_attempts: "5" max_attempts: "5"
max_ban_time: "86400"
netban_ipv4: "24" netban_ipv4: "24"
netban_ipv6: "64" netban_ipv6: "64"
retry_window: "600" retry_window: "600"
@ -3193,17 +3105,11 @@ paths:
description: the backlisted ips or hostnames separated by comma description: the backlisted ips or hostnames separated by comma
type: string type: string
ban_time: ban_time:
description: the time an ip should be banned description: the time a ip should be banned
type: number type: number
ban_time_increment:
description: if the time of the ban should increase each time
type: boolean
max_attempts: max_attempts:
description: the maximum numbe of wrong logins before a ip is banned description: the maximum numbe of wrong logins before a ip is banned
type: number type: number
max_ban_time:
description: the maximum time an ip should be banned
type: number
netban_ipv4: netban_ipv4:
description: the networks mask to ban for ipv4 description: the networks mask to ban for ipv4
type: number type: number
@ -3389,7 +3295,6 @@ paths:
evaluate_x_prio: "0" evaluate_x_prio: "0"
key: 21e8918e1jksdjcpis712 key: 21e8918e1jksdjcpis712
only_x_prio: "0" only_x_prio: "0"
sound: "pushover"
senders: "" senders: ""
senders_regex: "" senders_regex: ""
text: "" text: ""
@ -3433,7 +3338,6 @@ paths:
evaluate_x_prio: "0" evaluate_x_prio: "0"
key: 21e8918e1jksdjcpis712 key: 21e8918e1jksdjcpis712
only_x_prio: "0" only_x_prio: "0"
sound: "pushover"
senders: "" senders: ""
senders_regex: "" senders_regex: ""
text: "" text: ""
@ -3455,9 +3359,6 @@ paths:
only_x_prio: only_x_prio:
description: Only send push for prio mails description: Only send push for prio mails
type: number type: number
sound:
description: Set notification sound
type: string
senders: senders:
description: Only send push for emails from these senders description: Only send push for emails from these senders
type: string type: string
@ -4038,8 +3939,6 @@ paths:
in: query in: query
name: tags name: tags
required: false required: false
schema:
type: string
- description: e.g. api-key-string - description: e.g. api-key-string
example: api-key-string example: api-key-string
in: header in: header
@ -4121,12 +4020,10 @@ paths:
response: response:
value: value:
ban_time: 604800 ban_time: 604800
ban_time_increment: 1
blacklist: |- blacklist: |-
45.82.153.37/32 45.82.153.37/32
92.118.38.52/32 92.118.38.52/32
max_attempts: 1 max_attempts: 1
max_ban_time: 604800
netban_ipv4: 32 netban_ipv4: 32
netban_ipv6: 128 netban_ipv6: 128
perm_bans: perm_bans:
@ -4601,8 +4498,6 @@ paths:
in: query in: query
name: tags name: tags
required: false required: false
schema:
type: string
- description: e.g. api-key-string - description: e.g. api-key-string
example: api-key-string example: api-key-string
in: header in: header
@ -5548,104 +5443,6 @@ paths:
attr: attr:
spam_score: "8,15" spam_score: "8,15"
summary: Edit mailbox spam filter score summary: Edit mailbox spam filter score
"/api/v1/get/mailbox/all/{domain}":
get:
parameters:
- description: name of domain
in: path
name: domain
required: false
schema:
type: string
- description: e.g. api-key-string
example: api-key-string
in: header
name: X-API-Key
required: false
schema:
type: string
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- active: "1"
attributes:
force_pw_update: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_access: "1"
tls_enforce_in: "0"
tls_enforce_out: "0"
domain: domain3.tld
is_relayed: 0
local_part: info
max_new_quota: 10737418240
messages: 0
name: Full name
percent_class: success
percent_in_use: 0
quota: 3221225472
quota_used: 0
rl: false
spam_aliases: 0
username: info@domain3.tld
tags: ["tag1", "tag2"]
description: OK
headers: {}
tags:
- Mailboxes
description: You can list all mailboxes existing in system for a specific domain.
operationId: Get mailboxes of a domain
summary: Get mailboxes of a domain
/api/v1/edit/cors:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- type: "success"
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
msg: "cors_headers_edited"
description: OK
headers: { }
tags:
- Cross-Origin Resource Sharing (CORS)
description: >-
This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API.
CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests.
By editing the CORS settings, you can specify which domains and which methods are permitted to access the API resources from outside the mailcow domain.
operationId: Edit Cross-Origin Resource Sharing (CORS) settings
requestBody:
content:
application/json:
schema:
example:
attr:
allowed_origins: ["*", "mail.mailcow.tld"]
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
properties:
attr:
type: object
properties:
allowed_origins:
type: array
items:
type: string
allowed_methods:
type: array
items:
type: string
summary: Edit Cross-Origin Resource Sharing (CORS) settings
tags: tags:
- name: Domains - name: Domains
@ -5672,8 +5469,6 @@ tags:
description: Manage DKIM keys description: Manage DKIM keys
- name: Domain admin - name: Domain admin
description: Create or udpdate domain admin users description: Create or udpdate domain admin users
- name: Single Sign-On
description: Issue tokens for users
- name: Address Rewriting - name: Address Rewriting
description: Create BCC maps or recipient maps description: Create BCC maps or recipient maps
- name: Outgoing TLS Policy Map Overrides - name: Outgoing TLS Policy Map Overrides
@ -5690,5 +5485,3 @@ tags:
description: Get the status of your cow description: Get the status of your cow
- name: Ratelimits - name: Ratelimits
description: Edit domain ratelimits description: Edit domain ratelimits
- name: Cross-Origin Resource Sharing (CORS)
description: Manage Cross-Origin Resource Sharing (CORS) settings

View File

@ -1,18 +0,0 @@
window.onload = function() {
// Begin Swagger UI call region
window.ui = SwaggerUIBundle({
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
@media (max-width:1050px){.navbar-header{float:none}.navbar-left,.navbar-nav,.navbar-right{float:none!important}.navbar-toggle{display:block}.navbar-collapse{border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-nav{margin-top:7.5px}.navbar-nav>li{float:none}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px}.collapse.in{display:block!important}.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}} @media (max-width:1050px){.navbar-header{float:none}.navbar-left,.navbar-nav,.navbar-right{float:none!important}.navbar-toggle{display:block}.navbar-collapse{border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-collapse.collapse{display:none!important}.navbar-nav{margin-top:7.5px}.navbar-nav>li{float:none}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px}.collapse.in{display:block!important}.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}}

View File

@ -1,487 +0,0 @@
/*!
* Bootstrap-select v1.14.0-beta2 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2021 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
@-webkit-keyframes bs-notify-fadeOut {
0% {
opacity: 0.9;
}
100% {
opacity: 0;
}
}
@-o-keyframes bs-notify-fadeOut {
0% {
opacity: 0.9;
}
100% {
opacity: 0;
}
}
@keyframes bs-notify-fadeOut {
0% {
opacity: 0.9;
}
100% {
opacity: 0;
}
}
select.bs-select-hidden,
.bootstrap-select > select.bs-select-hidden,
select.selectpicker {
display: none !important;
}
.bootstrap-select {
width: 220px \0;
/*IE9 and below*/
vertical-align: middle;
}
.bootstrap-select > .dropdown-toggle {
position: relative;
width: 100%;
text-align: right;
white-space: nowrap;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.bootstrap-select > .dropdown-toggle:after {
margin-top: -1px;
}
.bootstrap-select > .dropdown-toggle.bs-placeholder,
.bootstrap-select > .dropdown-toggle.bs-placeholder:hover,
.bootstrap-select > .dropdown-toggle.bs-placeholder:focus,
.bootstrap-select > .dropdown-toggle.bs-placeholder:active {
color: #999;
}
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:hover,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:hover,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:hover,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:hover,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:hover,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:hover,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:focus,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:focus,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:focus,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:focus,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:focus,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:focus,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:active,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:active,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:active,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:active,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:active,
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:active {
color: rgba(255, 255, 255, 0.5);
}
.bootstrap-select > select {
position: absolute !important;
bottom: 0;
left: 50%;
display: block !important;
width: 0.5px !important;
height: 100% !important;
padding: 0 !important;
opacity: 0 !important;
border: none;
z-index: 0 !important;
}
.bootstrap-select > select.mobile-device {
top: 0;
left: 0;
display: block !important;
width: 100% !important;
z-index: 2 !important;
}
.has-error .bootstrap-select .dropdown-toggle,
.error .bootstrap-select .dropdown-toggle,
.bootstrap-select.is-invalid .dropdown-toggle,
.was-validated .bootstrap-select select:invalid + .dropdown-toggle {
border-color: #b94a48;
}
.bootstrap-select.is-valid .dropdown-toggle,
.was-validated .bootstrap-select select:valid + .dropdown-toggle {
border-color: #28a745;
}
.bootstrap-select.fit-width {
width: auto !important;
}
.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) {
width: 220px;
}
.bootstrap-select > select.mobile-device:focus + .dropdown-toggle,
.bootstrap-select .dropdown-toggle:focus {
outline: thin dotted #333333 !important;
outline: 5px auto -webkit-focus-ring-color !important;
outline-offset: -2px;
}
.bootstrap-select.form-control {
margin-bottom: 0;
padding: 0;
border: none;
height: auto;
}
:not(.input-group) > .bootstrap-select.form-control:not([class*="col-"]) {
width: 100%;
}
.bootstrap-select.form-control.input-group-btn {
float: none;
z-index: auto;
}
.form-inline .bootstrap-select,
.form-inline .bootstrap-select.form-control:not([class*="col-"]) {
width: auto;
}
.bootstrap-select:not(.input-group-btn),
.bootstrap-select[class*="col-"] {
float: none;
display: inline-block;
margin-left: 0;
}
.bootstrap-select.dropdown-menu-right,
.bootstrap-select[class*="col-"].dropdown-menu-right,
.row .bootstrap-select[class*="col-"].dropdown-menu-right {
float: right;
}
.form-inline .bootstrap-select,
.form-horizontal .bootstrap-select,
.form-group .bootstrap-select {
margin-bottom: 0;
}
.form-group-lg .bootstrap-select.form-control,
.form-group-sm .bootstrap-select.form-control {
padding: 0;
}
.form-group-lg .bootstrap-select.form-control .dropdown-toggle,
.form-group-sm .bootstrap-select.form-control .dropdown-toggle {
height: 100%;
font-size: inherit;
line-height: inherit;
border-radius: inherit;
}
.bootstrap-select.form-control-sm .dropdown-toggle,
.bootstrap-select.form-control-lg .dropdown-toggle {
font-size: inherit;
line-height: inherit;
border-radius: inherit;
}
.bootstrap-select.form-control-sm .dropdown-toggle {
padding: 0.25rem 0.5rem;
}
.bootstrap-select.form-control-lg .dropdown-toggle {
padding: 0.5rem 1rem;
}
.form-inline .bootstrap-select .form-control {
width: 100%;
}
.bootstrap-select.disabled,
.bootstrap-select > .disabled {
cursor: not-allowed;
}
.bootstrap-select.disabled:focus,
.bootstrap-select > .disabled:focus {
outline: none !important;
}
.bootstrap-select.bs-container {
position: absolute;
top: 0;
left: 0;
height: 0 !important;
padding: 0 !important;
}
.bootstrap-select.bs-container .dropdown-menu {
z-index: 1060;
}
.bootstrap-select .dropdown-toggle .filter-option {
position: static;
top: 0;
left: 0;
float: left;
height: 100%;
width: 100%;
text-align: left;
overflow: hidden;
-webkit-box-flex: 0;
-webkit-flex: 0 1 auto;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
}
.bs3.bootstrap-select .dropdown-toggle .filter-option {
padding-right: inherit;
}
.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option {
position: absolute;
padding-top: inherit;
padding-bottom: inherit;
padding-left: inherit;
float: none;
}
.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner {
padding-right: inherit;
}
.bootstrap-select .dropdown-toggle .filter-option-inner-inner {
overflow: hidden;
}
.bootstrap-select .dropdown-toggle .filter-expand {
width: 0 !important;
float: left;
opacity: 0 !important;
overflow: hidden;
}
.bootstrap-select .dropdown-toggle .caret {
position: absolute;
top: 50%;
right: 12px;
margin-top: -2px;
vertical-align: middle;
}
.bootstrap-select .dropdown-toggle .bs-select-clear-selected {
position: relative;
display: block;
margin-right: 5px;
text-align: center;
}
.bs3.bootstrap-select .dropdown-toggle .bs-select-clear-selected {
padding-right: inherit;
}
.bootstrap-select .dropdown-toggle .bs-select-clear-selected span {
position: relative;
top: -webkit-calc(((-1em / 1.5) + 1ex) / 2);
top: calc(((-1em / 1.5) + 1ex) / 2);
pointer-events: none;
}
.bs3.bootstrap-select .dropdown-toggle .bs-select-clear-selected span {
top: auto;
}
.bootstrap-select .dropdown-toggle.bs-placeholder .bs-select-clear-selected {
display: none;
}
.input-group .bootstrap-select.form-control .dropdown-toggle {
border-radius: inherit;
}
.bootstrap-select[class*="col-"] .dropdown-toggle {
width: 100%;
}
.bootstrap-select .dropdown-menu {
min-width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.bootstrap-select .dropdown-menu > .inner:focus {
outline: none !important;
}
.bootstrap-select .dropdown-menu.inner {
position: static;
float: none;
border: 0;
padding: 0;
margin: 0;
border-radius: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
.bootstrap-select .dropdown-menu li {
position: relative;
}
.bootstrap-select .dropdown-menu li.active small {
color: rgba(255, 255, 255, 0.5) !important;
}
.bootstrap-select .dropdown-menu li.disabled a {
cursor: not-allowed;
}
.bootstrap-select .dropdown-menu li a {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.bootstrap-select .dropdown-menu li a.opt {
position: relative;
padding-left: 2.25em;
}
.bootstrap-select .dropdown-menu li a span.check-mark {
display: none;
}
.bootstrap-select .dropdown-menu li a span.text {
display: inline-block;
}
.bootstrap-select .dropdown-menu li small {
padding-left: 0.5em;
}
.bootstrap-select .dropdown-menu .notify {
position: absolute;
bottom: 5px;
width: 96%;
margin: 0 2%;
min-height: 26px;
padding: 3px 5px;
background: #f5f5f5;
border: 1px solid #e3e3e3;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
pointer-events: none;
opacity: 0.9;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.bootstrap-select .dropdown-menu .notify.fadeOut {
-webkit-animation: 300ms linear 750ms forwards bs-notify-fadeOut;
-o-animation: 300ms linear 750ms forwards bs-notify-fadeOut;
animation: 300ms linear 750ms forwards bs-notify-fadeOut;
}
.bootstrap-select .no-results {
padding: 3px;
background: #f5f5f5;
margin: 0 5px;
white-space: nowrap;
}
.bootstrap-select.fit-width .dropdown-toggle .filter-option {
position: static;
display: inline;
padding: 0;
}
.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,
.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner {
display: inline;
}
.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before {
content: '\00a0';
}
.bootstrap-select.fit-width .dropdown-toggle .caret {
position: static;
top: auto;
margin-top: -1px;
}
.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark {
position: absolute;
display: inline-block;
right: 15px;
top: 5px;
}
.bootstrap-select.show-tick .dropdown-menu li a span.text {
margin-right: 34px;
}
.bootstrap-select .bs-ok-default:after {
content: '';
display: block;
width: 0.5em;
height: 1em;
border-style: solid;
border-width: 0 0.26em 0.26em 0;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.bootstrap-select.show-menu-arrow.open > .dropdown-toggle,
.bootstrap-select.show-menu-arrow.show > .dropdown-toggle {
z-index: 1061;
}
.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before {
content: '';
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid rgba(204, 204, 204, 0.2);
position: absolute;
bottom: -4px;
left: 9px;
display: none;
}
.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after {
content: '';
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid white;
position: absolute;
bottom: -4px;
left: 10px;
display: none;
}
.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before {
bottom: auto;
top: -4px;
border-top: 7px solid rgba(204, 204, 204, 0.2);
border-bottom: 0;
}
.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after {
bottom: auto;
top: -4px;
border-top: 6px solid white;
border-bottom: 0;
}
.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before {
right: 12px;
left: auto;
}
.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after {
right: 13px;
left: auto;
}
.bootstrap-select.show-menu-arrow.open > .dropdown-toggle .filter-option:before,
.bootstrap-select.show-menu-arrow.show > .dropdown-toggle .filter-option:before,
.bootstrap-select.show-menu-arrow.open > .dropdown-toggle .filter-option:after,
.bootstrap-select.show-menu-arrow.show > .dropdown-toggle .filter-option:after {
display: block;
}
.bs-searchbox,
.bs-actionsbox,
.bs-donebutton {
padding: 4px 8px;
}
.bs-actionsbox {
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.bs-actionsbox .btn-group {
display: block;
}
.bs-actionsbox .btn-group button {
width: 50%;
}
.bs-donebutton {
float: left;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.bs-donebutton .btn-group {
display: block;
}
.bs-donebutton .btn-group button {
width: 100%;
}
.bs-searchbox + .bs-actionsbox {
padding: 0 8px 4px;
}
.bs-searchbox .form-control {
margin-bottom: 0;
width: 100%;
float: none;
}
/*# sourceMappingURL=bootstrap-select.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,323 @@
table.footable-details,
table.footable > thead > tr.footable-filtering > th div.form-group {
margin-bottom: 0;
}
table.footable,
table.footable-details {
position: relative;
width: 100%;
border-spacing: 0;
border-collapse: collapse;
}
table.footable-hide-fouc {
display: none;
}
table > tbody > tr > td > span.footable-toggle {
margin-right: 8px;
opacity: 0.3;
}
table > tbody > tr > td > span.footable-toggle.last-column {
margin-left: 8px;
float: right;
}
table.table-condensed > tbody > tr > td > span.footable-toggle {
margin-right: 5px;
}
table.footable-details > tbody > tr > th:nth-child(1) {
min-width: 40px;
width: 120px;
}
table.footable-details > tbody > tr > td:nth-child(2) {
word-break: break-all;
}
table.footable-details > tbody > tr:first-child > td,
table.footable-details > tbody > tr:first-child > th,
table.footable-details > tfoot > tr:first-child > td,
table.footable-details > tfoot > tr:first-child > th,
table.footable-details > thead > tr:first-child > td,
table.footable-details > thead > tr:first-child > th {
border-top-width: 0;
}
table.footable-details.table-bordered > tbody > tr:first-child > td,
table.footable-details.table-bordered > tbody > tr:first-child > th,
table.footable-details.table-bordered > tfoot > tr:first-child > td,
table.footable-details.table-bordered > tfoot > tr:first-child > th,
table.footable-details.table-bordered > thead > tr:first-child > td,
table.footable-details.table-bordered > thead > tr:first-child > th {
border-top-width: 1px;
}
div.footable-loader {
vertical-align: middle;
text-align: center;
height: 300px;
position: relative;
}
div.footable-loader > span.fooicon {
display: inline-block;
opacity: 0.3;
font-size: 30px;
line-height: 32px;
width: 32px;
height: 32px;
margin-top: -16px;
margin-left: -16px;
position: absolute;
top: 50%;
left: 50%;
-webkit-animation: fooicon-spin-r 2s infinite linear;
animation: fooicon-spin-r 2s infinite linear;
}
table.footable > tbody > tr.footable-empty > td {
vertical-align: middle;
text-align: center;
font-size: 30px;
}
table.footable > tbody > tr > td,
table.footable > tbody > tr > th {
display: none;
}
table.footable > tbody > tr.footable-detail-row > td,
table.footable > tbody > tr.footable-detail-row > th,
table.footable > tbody > tr.footable-empty > td,
table.footable > tbody > tr.footable-empty > th {
display: table-cell;
}
@-webkit-keyframes fooicon-spin-r {
0% {
-webkit-transform: rotate(0);
transform: rotate(0);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes fooicon-spin-r {
0% {
-webkit-transform: rotate(0);
transform: rotate(0);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
.fooicon {
position: relative;
top: 0px;
display: inline-block;
font-family: "bootstrap-icons" !important;
font-style: normal;
font-weight: 400;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@-moz-document url-prefix() {
.fooicon {
top: 2px;
}
}
.fooicon:after,
.fooicon:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.fooicon-loader:before {
content: "\f130";
}
.fooicon-plus:before {
content: "\f4fc";
}
.fooicon-minus:before {
content: "\f2e8";
}
.fooicon-search:before {
content: "\f52a";
}
.fooicon-remove:before {
content: "\f62a";
}
.fooicon-sort:before {
content: "\f3c6";
}
.fooicon-sort-asc:before {
content: "\f575";
}
.fooicon-sort-desc:before {
content: "\f57b";
}
.fooicon-pencil:before {
content: "\f4c9";
}
.fooicon-trash:before {
content: "\f62a";
}
.fooicon-eye-close:before {
content: "\f33f";
}
.fooicon-flash:before {
content: "\f46e";
}
.fooicon-cog:before {
content: "\f3e2";
}
.fooicon-stats:before {
content: "\f359";
}
table.footable > thead > tr.footable-filtering > th {
border-bottom-width: 1px;
font-weight: 400;
}
.footable-filtering-external.footable-filtering-right,
table.footable.footable-filtering-right > thead > tr.footable-filtering > th,
table.footable > thead > tr.footable-filtering > th {
text-align: right;
}
.footable-filtering-external.footable-filtering-left,
table.footable.footable-filtering-left > thead > tr.footable-filtering > th {
text-align: left;
}
.footable-filtering-external.footable-filtering-center,
.footable-paging-external.footable-paging-center,
table.footable-paging-center > tfoot > tr.footable-paging > td,
table.footable.footable-filtering-center > thead > tr.footable-filtering > th,
table.footable > tfoot > tr.footable-paging > td {
text-align: center;
}
table.footable > thead > tr.footable-filtering > th div.form-group + div.form-group {
margin-top: 5px;
}
table.footable > thead > tr.footable-filtering > th div.input-group {
width: 100%;
}
.footable-filtering-external ul.dropdown-menu > li > a.checkbox,
table.footable > thead > tr.footable-filtering > th ul.dropdown-menu > li > a.checkbox {
margin: 0;
display: block;
position: relative;
}
.footable-filtering-external ul.dropdown-menu > li > a.checkbox > label,
table.footable > thead > tr.footable-filtering > th ul.dropdown-menu > li > a.checkbox > label {
display: block;
padding-left: 20px;
}
.footable-filtering-external ul.dropdown-menu > li > a.checkbox input[type="checkbox"],
table.footable > thead > tr.footable-filtering > th ul.dropdown-menu > li > a.checkbox input[type="checkbox"] {
position: absolute;
margin-left: -20px;
}
@media (min-width: 768px) {
table.footable > thead > tr.footable-filtering > th div.input-group {
width: auto;
}
table.footable > thead > tr.footable-filtering > th div.form-group {
margin-left: 2px;
margin-right: 2px;
}
table.footable > thead > tr.footable-filtering > th div.form-group + div.form-group {
margin-top: 0;
}
}
table.footable > tbody > tr > td.footable-sortable,
table.footable > tbody > tr > th.footable-sortable,
table.footable > tfoot > tr > td.footable-sortable,
table.footable > tfoot > tr > th.footable-sortable,
table.footable > thead > tr > td.footable-sortable,
table.footable > thead > tr > th.footable-sortable {
position: relative;
padding-right: 30px;
cursor: pointer;
}
td.footable-sortable > span.fooicon,
th.footable-sortable > span.fooicon {
position: absolute;
right: 6px;
top: 50%;
margin-top: -7px;
opacity: 0;
transition: opacity 0.3s ease-in;
}
td.footable-sortable.footable-asc > span.fooicon,
td.footable-sortable.footable-desc > span.fooicon,
td.footable-sortable:hover > span.fooicon,
th.footable-sortable.footable-asc > span.fooicon,
th.footable-sortable.footable-desc > span.fooicon,
th.footable-sortable:hover > span.fooicon {
opacity: 1;
}
table.footable-sorting-disabled td.footable-sortable.footable-asc > span.fooicon,
table.footable-sorting-disabled td.footable-sortable.footable-desc > span.fooicon,
table.footable-sorting-disabled td.footable-sortable:hover > span.fooicon,
table.footable-sorting-disabled th.footable-sortable.footable-asc > span.fooicon,
table.footable-sorting-disabled th.footable-sortable.footable-desc > span.fooicon,
table.footable-sorting-disabled th.footable-sortable:hover > span.fooicon {
opacity: 0;
visibility: hidden;
}
.footable-paging-external ul.pagination,
table.footable > tfoot > tr.footable-paging > td > ul.pagination {
margin: 10px 0 0;
}
.footable-paging-external span.label,
table.footable > tfoot > tr.footable-paging > td > span.label {
display: inline-block;
margin: 0 0 10px;
padding: 4px 10px;
}
.footable-paging-external.footable-paging-left,
table.footable-paging-left > tfoot > tr.footable-paging > td {
text-align: left;
}
.footable-paging-external.footable-paging-right,
table.footable-editing-right td.footable-editing,
table.footable-editing-right tr.footable-editing,
table.footable-paging-right > tfoot > tr.footable-paging > td {
text-align: right;
}
ul.pagination > li.footable-page {
display: none;
}
ul.pagination > li.footable-page.visible {
display: inline;
}
td.footable-editing {
width: 90px;
max-width: 90px;
}
table.footable-editing-no-delete td.footable-editing,
table.footable-editing-no-edit td.footable-editing,
table.footable-editing-no-view td.footable-editing {
width: 70px;
max-width: 70px;
}
table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,
table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,
table.footable-editing-no-edit.footable-editing-no-view td.footable-editing {
width: 50px;
max-width: 50px;
}
table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,
table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing {
width: 0;
max-width: 0;
display: none !important;
}
table.footable-editing-left td.footable-editing,
table.footable-editing-left tr.footable-editing {
text-align: left;
}
table.footable-editing button.footable-add,
table.footable-editing button.footable-hide,
table.footable-editing-show button.footable-show,
table.footable-editing.footable-editing-always-show button.footable-hide,
table.footable-editing.footable-editing-always-show button.footable-show,
table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing {
display: none;
}
table.footable-editing.footable-editing-always-show button.footable-add,
table.footable-editing.footable-editing-show button.footable-add,
table.footable-editing.footable-editing-show button.footable-hide {
display: inline-block;
}

File diff suppressed because one or more lines are too long

View File

@ -63,17 +63,6 @@
.navbar-nav { .navbar-nav {
margin: 0; margin: 0;
} }
.navbar-nav .nav-item {
flex-direction: column;
display: flex;
padding: 0 10px !important;
}
.navbar-nav .nav-link {
height: 44px;
display: flex;
align-items: center;
padding: 0 10px !important;
}
.navbar-fixed-bottom .navbar-collapse, .navbar-fixed-bottom .navbar-collapse,
.navbar-fixed-top .navbar-collapse { .navbar-fixed-top .navbar-collapse {
max-height: 1000px max-height: 1000px
@ -86,12 +75,6 @@
display: inline-block; display: inline-block;
font-size: inherit; font-size: inherit;
} }
.btn-group-xs > .btn, .btn-xs {
padding: .25rem .4rem;
font-size: .875rem;
line-height: 1rem;
border-radius: .2rem;
}
.icon-spin { .icon-spin {
animation-name: spin; animation-name: spin;
animation-duration: 2000ms; animation-duration: 2000ms;
@ -122,22 +105,13 @@
transform: rotate(359deg); transform: rotate(359deg);
} }
} }
@keyframes blink {
50% {
color: transparent
}
}
.loader-dot {
animation: 1s blink infinite
}
.loader-dot:nth-child(2) {
animation-delay: 250ms
}
.loader-dot:nth-child(3) {
animation-delay: 500ms
}
pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;} pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;}
.footable-sortable {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Fix modal moving content left */ /* Fix modal moving content left */
body.modal-open { body.modal-open {
overflow: inherit; overflow: inherit;
@ -192,11 +166,19 @@ legend {
top: 0; right: 0; bottom: 0; left: 0; top: 0; right: 0; bottom: 0; left: 0;
opacity: 0.7; opacity: 0.7;
} }
#top {
padding-top: 70px;
}
.bootstrap-select.btn-group .no-results { .bootstrap-select.btn-group .no-results {
display: none; display: none;
} }
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary { .dropdown-desc {
color: rgb(197, 197, 197) !important; display: block;
padding: 3px 10px;
clear: both;
font-weight: bold;
color: #5a5a5a;
white-space: nowrap;
} }
.haveibeenpwned { .haveibeenpwned {
cursor: pointer; cursor: pointer;
@ -224,7 +206,7 @@ legend {
flex-direction: column; flex-direction: column;
} }
.footer .version { .footer .version {
margin-left: auto; margin-left: auto;
margin-top: 20px; margin-top: 20px;
} }
.slave-info { .slave-info {
@ -243,8 +225,12 @@ legend {
.btn-input-missing:active:hover, .btn-input-missing:active:hover,
.btn-input-missing:active:focus { .btn-input-missing:active:focus {
color: #000 !important; color: #000 !important;
background-color: #ff2f24 !important; background-color: #ff4136;
border-color: #e21207 !important; border-color: #ff291c;
}
table.footable>tbody>tr.footable-empty>td {
font-style:italic;
font-size: 1rem;
} }
.navbar-nav > li { .navbar-nav > li {
font-size: 1rem !important; font-size: 1rem !important;
@ -271,11 +257,6 @@ code {
margin-right: 5px; margin-right: 5px;
} }
.dropdown-header {
font-weight: 600;
}
.tag-box { .tag-box {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -299,7 +280,7 @@ code {
} }
.tag-input { .tag-input {
margin-left: 10px; margin-left: 10px;
border: 0 !important; border: 0;
flex: 1; flex: 1;
height: 24px; height: 24px;
min-width: 150px; min-width: 150px;
@ -311,62 +292,4 @@ code {
padding: 0 5px 0 5px; padding: 0 5px 0 5px;
align-items: center; align-items: center;
display: inline-flex; display: inline-flex;
} }
#dnstable {
overflow-x: auto!important;
}
.well {
border: 1px solid #dfdfdf;
background-color: #f9f9f9;
padding: 10px;
}
.btn-check-label {
color: #555;
}
.caret {
transform: rotate(0deg);
}
a[aria-expanded='true'] > .caret,
button[aria-expanded='true'] > .caret {
transform: rotate(-180deg);
}
.list-group-details {
background: #fff;
}
.list-group-header {
background: #f7f7f7;
}
.bg-primary, .alert-primary, .btn-primary {
background-color: #0F688D !important;
border-color: #0d526d !important;
}
.bg-info, .alert-info, .btn-info {
background-color: #148DBC !important;
border-color: #127ea8 !important;
}
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
color: rgb(137 137 137)!important;
}
.progress {
background-color: #d5d5d5;
}
.btn-outline-secondary:hover {
background-color: #f0f0f0;
}
.btn.btn-outline-secondary {
border-color: #cfcfcf !important;
}
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
background-color: #f0f0f0 !important;
}

File diff suppressed because one or more lines are too long

View File

@ -1,691 +0,0 @@
/*
* This combined file was created by the DataTables downloader builder:
* https://datatables.net/download
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#bs5/dt-1.13.1/r-2.4.0/sl-1.5.0
*
* Included libraries:
* DataTables 1.13.1, Responsive 2.4.0, Select 1.5.0
*/
@charset "UTF-8";
table.dataTable td.dt-control {
text-align: center;
cursor: pointer;
}
table.dataTable td.dt-control:before {
height: 1em;
width: 1em;
margin-top: -9px;
display: inline-block;
color: white;
border: 0.15em solid white;
border-radius: 1em;
box-shadow: 0 0 0.2em #444;
box-sizing: content-box;
text-align: center;
text-indent: 0 !important;
font-family: "Courier New", Courier, monospace;
line-height: 1em;
content: "+";
background-color: #31b131;
}
table.dataTable tr.dt-hasChild td.dt-control:before {
content: "-";
background-color: #d33333;
}
table.dataTable thead > tr > th.sorting, table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting_asc_disabled, table.dataTable thead > tr > th.sorting_desc_disabled,
table.dataTable thead > tr > td.sorting,
table.dataTable thead > tr > td.sorting_asc,
table.dataTable thead > tr > td.sorting_desc,
table.dataTable thead > tr > td.sorting_asc_disabled,
table.dataTable thead > tr > td.sorting_desc_disabled {
cursor: pointer;
position: relative;
padding-right: 26px;
}
table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:after,
table.dataTable thead > tr > td.sorting:before,
table.dataTable thead > tr > td.sorting:after,
table.dataTable thead > tr > td.sorting_asc:before,
table.dataTable thead > tr > td.sorting_asc:after,
table.dataTable thead > tr > td.sorting_desc:before,
table.dataTable thead > tr > td.sorting_desc:after,
table.dataTable thead > tr > td.sorting_asc_disabled:before,
table.dataTable thead > tr > td.sorting_asc_disabled:after,
table.dataTable thead > tr > td.sorting_desc_disabled:before,
table.dataTable thead > tr > td.sorting_desc_disabled:after {
position: absolute;
display: block;
opacity: 0.125;
right: 10px;
line-height: 9px;
font-size: 0.8em;
}
table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before,
table.dataTable thead > tr > td.sorting:before,
table.dataTable thead > tr > td.sorting_asc:before,
table.dataTable thead > tr > td.sorting_desc:before,
table.dataTable thead > tr > td.sorting_asc_disabled:before,
table.dataTable thead > tr > td.sorting_desc_disabled:before {
bottom: 50%;
content: "▲";
}
table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after,
table.dataTable thead > tr > td.sorting:after,
table.dataTable thead > tr > td.sorting_asc:after,
table.dataTable thead > tr > td.sorting_desc:after,
table.dataTable thead > tr > td.sorting_asc_disabled:after,
table.dataTable thead > tr > td.sorting_desc_disabled:after {
top: 50%;
content: "▼";
}
table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after,
table.dataTable thead > tr > td.sorting_asc:before,
table.dataTable thead > tr > td.sorting_desc:after {
opacity: 0.6;
}
table.dataTable thead > tr > th.sorting_desc_disabled:after, table.dataTable thead > tr > th.sorting_asc_disabled:before,
table.dataTable thead > tr > td.sorting_desc_disabled:after,
table.dataTable thead > tr > td.sorting_asc_disabled:before {
display: none;
}
table.dataTable thead > tr > th:active,
table.dataTable thead > tr > td:active {
outline: none;
}
div.dataTables_scrollBody table.dataTable thead > tr > th:before, div.dataTables_scrollBody table.dataTable thead > tr > th:after,
div.dataTables_scrollBody table.dataTable thead > tr > td:before,
div.dataTables_scrollBody table.dataTable thead > tr > td:after {
display: none;
}
div.dataTables_processing {
position: absolute;
top: 50%;
left: 50%;
width: 200px;
margin-left: -100px;
margin-top: -26px;
text-align: center;
padding: 2px;
}
div.dataTables_processing > div:last-child {
position: relative;
width: 80px;
height: 15px;
margin: 1em auto;
}
div.dataTables_processing > div:last-child > div {
position: absolute;
top: 0;
width: 13px;
height: 13px;
border-radius: 50%;
background: rgba(13, 110, 253, 0.9);
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
div.dataTables_processing > div:last-child > div:nth-child(1) {
left: 8px;
animation: datatables-loader-1 0.6s infinite;
}
div.dataTables_processing > div:last-child > div:nth-child(2) {
left: 8px;
animation: datatables-loader-2 0.6s infinite;
}
div.dataTables_processing > div:last-child > div:nth-child(3) {
left: 32px;
animation: datatables-loader-2 0.6s infinite;
}
div.dataTables_processing > div:last-child > div:nth-child(4) {
left: 56px;
animation: datatables-loader-3 0.6s infinite;
}
@keyframes datatables-loader-1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes datatables-loader-3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes datatables-loader-2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
table.dataTable.nowrap th, table.dataTable.nowrap td {
white-space: nowrap;
}
table.dataTable th.dt-left,
table.dataTable td.dt-left {
text-align: left;
}
table.dataTable th.dt-center,
table.dataTable td.dt-center,
table.dataTable td.dataTables_empty {
text-align: center;
}
table.dataTable th.dt-right,
table.dataTable td.dt-right {
text-align: right;
}
table.dataTable th.dt-justify,
table.dataTable td.dt-justify {
text-align: justify;
}
table.dataTable th.dt-nowrap,
table.dataTable td.dt-nowrap {
white-space: nowrap;
}
table.dataTable thead th,
table.dataTable thead td,
table.dataTable tfoot th,
table.dataTable tfoot td {
text-align: left;
}
table.dataTable thead th.dt-head-left,
table.dataTable thead td.dt-head-left,
table.dataTable tfoot th.dt-head-left,
table.dataTable tfoot td.dt-head-left {
text-align: left;
}
table.dataTable thead th.dt-head-center,
table.dataTable thead td.dt-head-center,
table.dataTable tfoot th.dt-head-center,
table.dataTable tfoot td.dt-head-center {
text-align: center;
}
table.dataTable thead th.dt-head-right,
table.dataTable thead td.dt-head-right,
table.dataTable tfoot th.dt-head-right,
table.dataTable tfoot td.dt-head-right {
text-align: right;
}
table.dataTable thead th.dt-head-justify,
table.dataTable thead td.dt-head-justify,
table.dataTable tfoot th.dt-head-justify,
table.dataTable tfoot td.dt-head-justify {
text-align: justify;
}
table.dataTable thead th.dt-head-nowrap,
table.dataTable thead td.dt-head-nowrap,
table.dataTable tfoot th.dt-head-nowrap,
table.dataTable tfoot td.dt-head-nowrap {
white-space: nowrap;
}
table.dataTable tbody th.dt-body-left,
table.dataTable tbody td.dt-body-left {
text-align: left;
}
table.dataTable tbody th.dt-body-center,
table.dataTable tbody td.dt-body-center {
text-align: center;
}
table.dataTable tbody th.dt-body-right,
table.dataTable tbody td.dt-body-right {
text-align: right;
}
table.dataTable tbody th.dt-body-justify,
table.dataTable tbody td.dt-body-justify {
text-align: justify;
}
table.dataTable tbody th.dt-body-nowrap,
table.dataTable tbody td.dt-body-nowrap {
white-space: nowrap;
}
/*! Bootstrap 5 integration for DataTables
*
* ©2020 SpryMedia Ltd, all rights reserved.
* License: MIT datatables.net/license/mit
*/
table.dataTable {
clear: both;
margin-top: 6px !important;
margin-bottom: 6px !important;
max-width: none !important;
border-collapse: separate !important;
border-spacing: 0;
}
table.dataTable td,
table.dataTable th {
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
table.dataTable td.dataTables_empty,
table.dataTable th.dataTables_empty {
text-align: center;
}
table.dataTable.nowrap th,
table.dataTable.nowrap td {
white-space: nowrap;
}
table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * {
box-shadow: none;
}
table.dataTable > tbody > tr {
background-color: transparent;
}
table.dataTable > tbody > tr.selected > * {
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9);
color: white;
}
table.dataTable > tbody > tr.selected a {
color: #090a0b;
}
table.dataTable.table-striped > tbody > tr.odd > * {
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05);
}
table.dataTable.table-striped > tbody > tr.odd.selected > * {
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95);
}
table.dataTable.table-hover > tbody > tr:hover > * {
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075);
}
table.dataTable.table-hover > tbody > tr.selected:hover > * {
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975);
}
div.dataTables_wrapper div.dataTables_length label {
font-weight: normal;
text-align: left;
white-space: nowrap;
}
div.dataTables_wrapper div.dataTables_length select {
width: auto;
display: inline-block;
}
div.dataTables_wrapper div.dataTables_filter {
text-align: right;
}
div.dataTables_wrapper div.dataTables_filter label {
font-weight: normal;
white-space: nowrap;
text-align: left;
}
div.dataTables_wrapper div.dataTables_filter input {
margin-left: 0.5em;
display: inline-block;
width: auto;
}
div.dataTables_wrapper div.dataTables_info {
padding-top: 0.85em;
}
div.dataTables_wrapper div.dataTables_paginate {
margin: 0;
white-space: nowrap;
text-align: right;
}
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
margin: 2px 0;
white-space: nowrap;
justify-content: flex-end;
}
div.dataTables_wrapper div.dt-row {
position: relative;
}
div.dataTables_wrapper span.sorting-value {
display: none;
}
div.dataTables_scrollHead table.dataTable {
margin-bottom: 0 !important;
}
div.dataTables_scrollBody > table {
border-top: none;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
div.dataTables_scrollBody > table > thead .sorting:before,
div.dataTables_scrollBody > table > thead .sorting_asc:before,
div.dataTables_scrollBody > table > thead .sorting_desc:before,
div.dataTables_scrollBody > table > thead .sorting:after,
div.dataTables_scrollBody > table > thead .sorting_asc:after,
div.dataTables_scrollBody > table > thead .sorting_desc:after {
display: none;
}
div.dataTables_scrollBody > table > tbody tr:first-child th,
div.dataTables_scrollBody > table > tbody tr:first-child td {
border-top: none;
}
div.dataTables_scrollFoot > .dataTables_scrollFootInner {
box-sizing: content-box;
}
div.dataTables_scrollFoot > .dataTables_scrollFootInner > table {
margin-top: 0 !important;
border-top: none;
}
@media screen and (max-width: 767px) {
div.dataTables_wrapper div.dataTables_length,
div.dataTables_wrapper div.dataTables_filter,
div.dataTables_wrapper div.dataTables_info,
div.dataTables_wrapper div.dataTables_paginate {
text-align: center;
}
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
justify-content: center !important;
}
}
table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) {
padding-right: 20px;
}
table.table-bordered.dataTable {
border-right-width: 0;
}
table.table-bordered.dataTable thead tr:first-child th,
table.table-bordered.dataTable thead tr:first-child td {
border-top-width: 1px;
}
table.table-bordered.dataTable th,
table.table-bordered.dataTable td {
border-left-width: 0;
}
table.table-bordered.dataTable th:first-child, table.table-bordered.dataTable th:first-child,
table.table-bordered.dataTable td:first-child,
table.table-bordered.dataTable td:first-child {
border-left-width: 1px;
}
table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child,
table.table-bordered.dataTable td:last-child,
table.table-bordered.dataTable td:last-child {
border-right-width: 1px;
}
table.table-bordered.dataTable th,
table.table-bordered.dataTable td {
border-bottom-width: 1px;
}
div.dataTables_scrollHead table.table-bordered {
border-bottom-width: 0;
}
div.table-responsive > div.dataTables_wrapper > div.row {
margin: 0;
}
div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:first-child {
padding-left: 0;
}
div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-child {
padding-right: 0;
}
table.dataTable.dtr-inline.collapsed > tbody > tr > td.child,
table.dataTable.dtr-inline.collapsed > tbody > tr > th.child,
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty {
cursor: default !important;
}
table.dataTable.dtr-inline.collapsed > tbody > tr > td.child:before,
table.dataTable.dtr-inline.collapsed > tbody > tr > th.child:before,
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty:before {
display: none !important;
}
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control,
table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control {
position: relative;
padding-left: 30px;
cursor: pointer;
}
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control:before,
table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control:before {
top: 50%;
left: 5px;
height: 1em;
width: 1em;
margin-top: -9px;
display: block;
position: absolute;
color: white;
border: 0.15em solid white;
border-radius: 1em;
box-shadow: 0 0 0.2em #444;
box-sizing: content-box;
text-align: center;
text-indent: 0 !important;
font-family: "Courier New", Courier, monospace;
line-height: 1em;
content: "+";
background-color: #0d6efd;
}
table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td.dtr-control:before,
table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th.dtr-control:before {
content: "-";
background-color: #d33333;
}
table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td.dtr-control,
table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th.dtr-control {
padding-left: 27px;
}
table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td.dtr-control:before,
table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th.dtr-control:before {
left: 4px;
height: 14px;
width: 14px;
border-radius: 14px;
line-height: 14px;
text-indent: 3px;
}
table.dataTable.dtr-column > tbody > tr > td.dtr-control,
table.dataTable.dtr-column > tbody > tr > th.dtr-control,
table.dataTable.dtr-column > tbody > tr > td.control,
table.dataTable.dtr-column > tbody > tr > th.control {
position: relative;
cursor: pointer;
}
table.dataTable.dtr-column > tbody > tr > td.dtr-control:before,
table.dataTable.dtr-column > tbody > tr > th.dtr-control:before,
table.dataTable.dtr-column > tbody > tr > td.control:before,
table.dataTable.dtr-column > tbody > tr > th.control:before {
top: 50%;
left: 50%;
height: 0.8em;
width: 0.8em;
margin-top: -0.5em;
margin-left: -0.5em;
display: block;
position: absolute;
color: white;
border: 0.15em solid white;
border-radius: 1em;
box-shadow: 0 0 0.2em #444;
box-sizing: content-box;
text-align: center;
text-indent: 0 !important;
font-family: "Courier New", Courier, monospace;
line-height: 1em;
content: "+";
background-color: #0d6efd;
}
table.dataTable.dtr-column > tbody > tr.parent td.dtr-control:before,
table.dataTable.dtr-column > tbody > tr.parent th.dtr-control:before,
table.dataTable.dtr-column > tbody > tr.parent td.control:before,
table.dataTable.dtr-column > tbody > tr.parent th.control:before {
content: "-";
background-color: #d33333;
}
table.dataTable > tbody > tr.child {
padding: 0.5em 1em;
}
table.dataTable > tbody > tr.child:hover {
background: transparent !important;
}
table.dataTable > tbody > tr.child ul.dtr-details {
display: inline-block;
list-style-type: none;
margin: 0;
padding: 0;
}
table.dataTable > tbody > tr.child ul.dtr-details > li {
border-bottom: 1px solid #efefef;
padding: 0.5em 0;
}
table.dataTable > tbody > tr.child ul.dtr-details > li:first-child {
padding-top: 0;
}
table.dataTable > tbody > tr.child ul.dtr-details > li:last-child {
border-bottom: none;
}
table.dataTable > tbody > tr.child span.dtr-title {
display: inline-block;
min-width: 75px;
font-weight: bold;
}
div.dtr-modal {
position: fixed;
box-sizing: border-box;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 100;
padding: 10em 1em;
}
div.dtr-modal div.dtr-modal-display {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 50%;
height: 50%;
overflow: auto;
margin: auto;
z-index: 102;
overflow: auto;
background-color: #f5f5f7;
border: 1px solid black;
border-radius: 0.5em;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
}
div.dtr-modal div.dtr-modal-content {
position: relative;
padding: 1em;
}
div.dtr-modal div.dtr-modal-close {
position: absolute;
top: 6px;
right: 6px;
width: 22px;
height: 22px;
border: 1px solid #eaeaea;
background-color: #f9f9f9;
text-align: center;
border-radius: 3px;
cursor: pointer;
z-index: 12;
}
div.dtr-modal div.dtr-modal-close:hover {
background-color: #eaeaea;
}
div.dtr-modal div.dtr-modal-background {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 101;
background: rgba(0, 0, 0, 0.6);
}
@media screen and (max-width: 767px) {
div.dtr-modal div.dtr-modal-display {
width: 95%;
}
}
div.dtr-bs-modal table.table tr:first-child td {
border-top: none;
}
table.dataTable.table-bordered th.dtr-control.dtr-hidden + *,
table.dataTable.table-bordered td.dtr-control.dtr-hidden + * {
border-left-width: 1px;
}
table.dataTable > tbody > tr > .selected {
background-color: rgba(13, 110, 253, 0.9);
color: white;
}
table.dataTable > tbody > tr > td.select-checkbox,
table.dataTable > tbody > tr > th.select-checkbox {
position: relative;
}
table.dataTable > tbody > tr > td.select-checkbox:before, table.dataTable > tbody > tr > td.select-checkbox:after,
table.dataTable > tbody > tr > th.select-checkbox:before,
table.dataTable > tbody > tr > th.select-checkbox:after {
display: block;
position: absolute;
top: 1.2em;
left: 50%;
width: 12px;
height: 12px;
box-sizing: border-box;
}
table.dataTable > tbody > tr > td.select-checkbox:before,
table.dataTable > tbody > tr > th.select-checkbox:before {
content: " ";
margin-top: -5px;
margin-left: -6px;
border: 1px solid black;
border-radius: 3px;
}
table.dataTable > tbody > tr.selected > td.select-checkbox:before,
table.dataTable > tbody > tr.selected > th.select-checkbox:before {
border: 1px solid white;
}
table.dataTable > tbody > tr.selected > td.select-checkbox:after,
table.dataTable > tbody > tr.selected > th.select-checkbox:after {
content: "✓";
font-size: 20px;
margin-top: -19px;
margin-left: -6px;
text-align: center;
text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9;
}
table.dataTable.compact > tbody > tr > td.select-checkbox:before,
table.dataTable.compact > tbody > tr > th.select-checkbox:before {
margin-top: -12px;
}
table.dataTable.compact > tbody > tr.selected > td.select-checkbox:after,
table.dataTable.compact > tbody > tr.selected > th.select-checkbox:after {
margin-top: -16px;
}
div.dataTables_wrapper span.select-info,
div.dataTables_wrapper span.select-item {
margin-left: 0.5em;
}
@media screen and (max-width: 640px) {
div.dataTables_wrapper span.select-info,
div.dataTables_wrapper span.select-item {
margin-left: 0;
display: block;
}
}
table.dataTable.table-sm tbody td.select-checkbox::before {
margin-top: -9px;
}

1
data/web/css/build/012-Chart.min.css vendored Normal file
View File

@ -0,0 +1 @@
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}

View File

@ -1,7 +1,7 @@
@font-face { @font-face {
font-family: "bootstrap-icons"; font-family: "bootstrap-icons";
src: url("/fonts/bootstrap-icons.woff2?524846017b983fc8ded9325d94ed40f3") format("woff2"), src: url("/fonts/bootstrap-icons.woff2?45695e8b569b2b0178db2713ca47065c") format("woff2"),
url("/fonts/bootstrap-icons.woff?524846017b983fc8ded9325d94ed40f3") format("woff"); url("/fonts/bootstrap-icons.woff?45695e8b569b2b0178db2713ca47065c") format("woff");
} }
.bi::before, .bi::before,
@ -19,7 +19,6 @@ url("/fonts/bootstrap-icons.woff?524846017b983fc8ded9325d94ed40f3") format("woff
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.bi-123::before { content: "\f67f"; }
.bi-alarm-fill::before { content: "\f101"; } .bi-alarm-fill::before { content: "\f101"; }
.bi-alarm::before { content: "\f102"; } .bi-alarm::before { content: "\f102"; }
.bi-align-bottom::before { content: "\f103"; } .bi-align-bottom::before { content: "\f103"; }
@ -1426,279 +1425,3 @@ url("/fonts/bootstrap-icons.woff?524846017b983fc8ded9325d94ed40f3") format("woff
.bi-webcam-fill::before { content: "\f67c"; } .bi-webcam-fill::before { content: "\f67c"; }
.bi-webcam::before { content: "\f67d"; } .bi-webcam::before { content: "\f67d"; }
.bi-yin-yang::before { content: "\f67e"; } .bi-yin-yang::before { content: "\f67e"; }
.bi-bandaid-fill::before { content: "\f680"; }
.bi-bandaid::before { content: "\f681"; }
.bi-bluetooth::before { content: "\f682"; }
.bi-body-text::before { content: "\f683"; }
.bi-boombox::before { content: "\f684"; }
.bi-boxes::before { content: "\f685"; }
.bi-dpad-fill::before { content: "\f686"; }
.bi-dpad::before { content: "\f687"; }
.bi-ear-fill::before { content: "\f688"; }
.bi-ear::before { content: "\f689"; }
.bi-envelope-check-1::before { content: "\f68a"; }
.bi-envelope-check-fill::before { content: "\f68b"; }
.bi-envelope-check::before { content: "\f68c"; }
.bi-envelope-dash-1::before { content: "\f68d"; }
.bi-envelope-dash-fill::before { content: "\f68e"; }
.bi-envelope-dash::before { content: "\f68f"; }
.bi-envelope-exclamation-1::before { content: "\f690"; }
.bi-envelope-exclamation-fill::before { content: "\f691"; }
.bi-envelope-exclamation::before { content: "\f692"; }
.bi-envelope-plus-fill::before { content: "\f693"; }
.bi-envelope-plus::before { content: "\f694"; }
.bi-envelope-slash-1::before { content: "\f695"; }
.bi-envelope-slash-fill::before { content: "\f696"; }
.bi-envelope-slash::before { content: "\f697"; }
.bi-envelope-x-1::before { content: "\f698"; }
.bi-envelope-x-fill::before { content: "\f699"; }
.bi-envelope-x::before { content: "\f69a"; }
.bi-explicit-fill::before { content: "\f69b"; }
.bi-explicit::before { content: "\f69c"; }
.bi-git::before { content: "\f69d"; }
.bi-infinity::before { content: "\f69e"; }
.bi-list-columns-reverse::before { content: "\f69f"; }
.bi-list-columns::before { content: "\f6a0"; }
.bi-meta::before { content: "\f6a1"; }
.bi-mortorboard-fill::before { content: "\f6a2"; }
.bi-mortorboard::before { content: "\f6a3"; }
.bi-nintendo-switch::before { content: "\f6a4"; }
.bi-pc-display-horizontal::before { content: "\f6a5"; }
.bi-pc-display::before { content: "\f6a6"; }
.bi-pc-horizontal::before { content: "\f6a7"; }
.bi-pc::before { content: "\f6a8"; }
.bi-playstation::before { content: "\f6a9"; }
.bi-plus-slash-minus::before { content: "\f6aa"; }
.bi-projector-fill::before { content: "\f6ab"; }
.bi-projector::before { content: "\f6ac"; }
.bi-qr-code-scan::before { content: "\f6ad"; }
.bi-qr-code::before { content: "\f6ae"; }
.bi-quora::before { content: "\f6af"; }
.bi-quote::before { content: "\f6b0"; }
.bi-robot::before { content: "\f6b1"; }
.bi-send-check-fill::before { content: "\f6b2"; }
.bi-send-check::before { content: "\f6b3"; }
.bi-send-dash-fill::before { content: "\f6b4"; }
.bi-send-dash::before { content: "\f6b5"; }
.bi-send-exclamation-1::before { content: "\f6b6"; }
.bi-send-exclamation-fill::before { content: "\f6b7"; }
.bi-send-exclamation::before { content: "\f6b8"; }
.bi-send-fill::before { content: "\f6b9"; }
.bi-send-plus-fill::before { content: "\f6ba"; }
.bi-send-plus::before { content: "\f6bb"; }
.bi-send-slash-fill::before { content: "\f6bc"; }
.bi-send-slash::before { content: "\f6bd"; }
.bi-send-x-fill::before { content: "\f6be"; }
.bi-send-x::before { content: "\f6bf"; }
.bi-send::before { content: "\f6c0"; }
.bi-steam::before { content: "\f6c1"; }
.bi-terminal-dash-1::before { content: "\f6c2"; }
.bi-terminal-dash::before { content: "\f6c3"; }
.bi-terminal-plus::before { content: "\f6c4"; }
.bi-terminal-split::before { content: "\f6c5"; }
.bi-ticket-detailed-fill::before { content: "\f6c6"; }
.bi-ticket-detailed::before { content: "\f6c7"; }
.bi-ticket-fill::before { content: "\f6c8"; }
.bi-ticket-perforated-fill::before { content: "\f6c9"; }
.bi-ticket-perforated::before { content: "\f6ca"; }
.bi-ticket::before { content: "\f6cb"; }
.bi-tiktok::before { content: "\f6cc"; }
.bi-window-dash::before { content: "\f6cd"; }
.bi-window-desktop::before { content: "\f6ce"; }
.bi-window-fullscreen::before { content: "\f6cf"; }
.bi-window-plus::before { content: "\f6d0"; }
.bi-window-split::before { content: "\f6d1"; }
.bi-window-stack::before { content: "\f6d2"; }
.bi-window-x::before { content: "\f6d3"; }
.bi-xbox::before { content: "\f6d4"; }
.bi-ethernet::before { content: "\f6d5"; }
.bi-hdmi-fill::before { content: "\f6d6"; }
.bi-hdmi::before { content: "\f6d7"; }
.bi-usb-c-fill::before { content: "\f6d8"; }
.bi-usb-c::before { content: "\f6d9"; }
.bi-usb-fill::before { content: "\f6da"; }
.bi-usb-plug-fill::before { content: "\f6db"; }
.bi-usb-plug::before { content: "\f6dc"; }
.bi-usb-symbol::before { content: "\f6dd"; }
.bi-usb::before { content: "\f6de"; }
.bi-boombox-fill::before { content: "\f6df"; }
.bi-displayport-1::before { content: "\f6e0"; }
.bi-displayport::before { content: "\f6e1"; }
.bi-gpu-card::before { content: "\f6e2"; }
.bi-memory::before { content: "\f6e3"; }
.bi-modem-fill::before { content: "\f6e4"; }
.bi-modem::before { content: "\f6e5"; }
.bi-motherboard-fill::before { content: "\f6e6"; }
.bi-motherboard::before { content: "\f6e7"; }
.bi-optical-audio-fill::before { content: "\f6e8"; }
.bi-optical-audio::before { content: "\f6e9"; }
.bi-pci-card::before { content: "\f6ea"; }
.bi-router-fill::before { content: "\f6eb"; }
.bi-router::before { content: "\f6ec"; }
.bi-ssd-fill::before { content: "\f6ed"; }
.bi-ssd::before { content: "\f6ee"; }
.bi-thunderbolt-fill::before { content: "\f6ef"; }
.bi-thunderbolt::before { content: "\f6f0"; }
.bi-usb-drive-fill::before { content: "\f6f1"; }
.bi-usb-drive::before { content: "\f6f2"; }
.bi-usb-micro-fill::before { content: "\f6f3"; }
.bi-usb-micro::before { content: "\f6f4"; }
.bi-usb-mini-fill::before { content: "\f6f5"; }
.bi-usb-mini::before { content: "\f6f6"; }
.bi-cloud-haze2::before { content: "\f6f7"; }
.bi-device-hdd-fill::before { content: "\f6f8"; }
.bi-device-hdd::before { content: "\f6f9"; }
.bi-device-ssd-fill::before { content: "\f6fa"; }
.bi-device-ssd::before { content: "\f6fb"; }
.bi-displayport-fill::before { content: "\f6fc"; }
.bi-mortarboard-fill::before { content: "\f6fd"; }
.bi-mortarboard::before { content: "\f6fe"; }
.bi-terminal-x::before { content: "\f6ff"; }
.bi-arrow-through-heart-fill::before { content: "\f700"; }
.bi-arrow-through-heart::before { content: "\f701"; }
.bi-badge-sd-fill::before { content: "\f702"; }
.bi-badge-sd::before { content: "\f703"; }
.bi-bag-heart-fill::before { content: "\f704"; }
.bi-bag-heart::before { content: "\f705"; }
.bi-balloon-fill::before { content: "\f706"; }
.bi-balloon-heart-fill::before { content: "\f707"; }
.bi-balloon-heart::before { content: "\f708"; }
.bi-balloon::before { content: "\f709"; }
.bi-box2-fill::before { content: "\f70a"; }
.bi-box2-heart-fill::before { content: "\f70b"; }
.bi-box2-heart::before { content: "\f70c"; }
.bi-box2::before { content: "\f70d"; }
.bi-braces-asterisk::before { content: "\f70e"; }
.bi-calendar-heart-fill::before { content: "\f70f"; }
.bi-calendar-heart::before { content: "\f710"; }
.bi-calendar2-heart-fill::before { content: "\f711"; }
.bi-calendar2-heart::before { content: "\f712"; }
.bi-chat-heart-fill::before { content: "\f713"; }
.bi-chat-heart::before { content: "\f714"; }
.bi-chat-left-heart-fill::before { content: "\f715"; }
.bi-chat-left-heart::before { content: "\f716"; }
.bi-chat-right-heart-fill::before { content: "\f717"; }
.bi-chat-right-heart::before { content: "\f718"; }
.bi-chat-square-heart-fill::before { content: "\f719"; }
.bi-chat-square-heart::before { content: "\f71a"; }
.bi-clipboard-check-fill::before { content: "\f71b"; }
.bi-clipboard-data-fill::before { content: "\f71c"; }
.bi-clipboard-fill::before { content: "\f71d"; }
.bi-clipboard-heart-fill::before { content: "\f71e"; }
.bi-clipboard-heart::before { content: "\f71f"; }
.bi-clipboard-minus-fill::before { content: "\f720"; }
.bi-clipboard-plus-fill::before { content: "\f721"; }
.bi-clipboard-pulse::before { content: "\f722"; }
.bi-clipboard-x-fill::before { content: "\f723"; }
.bi-clipboard2-check-fill::before { content: "\f724"; }
.bi-clipboard2-check::before { content: "\f725"; }
.bi-clipboard2-data-fill::before { content: "\f726"; }
.bi-clipboard2-data::before { content: "\f727"; }
.bi-clipboard2-fill::before { content: "\f728"; }
.bi-clipboard2-heart-fill::before { content: "\f729"; }
.bi-clipboard2-heart::before { content: "\f72a"; }
.bi-clipboard2-minus-fill::before { content: "\f72b"; }
.bi-clipboard2-minus::before { content: "\f72c"; }
.bi-clipboard2-plus-fill::before { content: "\f72d"; }
.bi-clipboard2-plus::before { content: "\f72e"; }
.bi-clipboard2-pulse-fill::before { content: "\f72f"; }
.bi-clipboard2-pulse::before { content: "\f730"; }
.bi-clipboard2-x-fill::before { content: "\f731"; }
.bi-clipboard2-x::before { content: "\f732"; }
.bi-clipboard2::before { content: "\f733"; }
.bi-emoji-kiss-fill::before { content: "\f734"; }
.bi-emoji-kiss::before { content: "\f735"; }
.bi-envelope-heart-fill::before { content: "\f736"; }
.bi-envelope-heart::before { content: "\f737"; }
.bi-envelope-open-heart-fill::before { content: "\f738"; }
.bi-envelope-open-heart::before { content: "\f739"; }
.bi-envelope-paper-fill::before { content: "\f73a"; }
.bi-envelope-paper-heart-fill::before { content: "\f73b"; }
.bi-envelope-paper-heart::before { content: "\f73c"; }
.bi-envelope-paper::before { content: "\f73d"; }
.bi-filetype-aac::before { content: "\f73e"; }
.bi-filetype-ai::before { content: "\f73f"; }
.bi-filetype-bmp::before { content: "\f740"; }
.bi-filetype-cs::before { content: "\f741"; }
.bi-filetype-css::before { content: "\f742"; }
.bi-filetype-csv::before { content: "\f743"; }
.bi-filetype-doc::before { content: "\f744"; }
.bi-filetype-docx::before { content: "\f745"; }
.bi-filetype-exe::before { content: "\f746"; }
.bi-filetype-gif::before { content: "\f747"; }
.bi-filetype-heic::before { content: "\f748"; }
.bi-filetype-html::before { content: "\f749"; }
.bi-filetype-java::before { content: "\f74a"; }
.bi-filetype-jpg::before { content: "\f74b"; }
.bi-filetype-js::before { content: "\f74c"; }
.bi-filetype-jsx::before { content: "\f74d"; }
.bi-filetype-key::before { content: "\f74e"; }
.bi-filetype-m4p::before { content: "\f74f"; }
.bi-filetype-md::before { content: "\f750"; }
.bi-filetype-mdx::before { content: "\f751"; }
.bi-filetype-mov::before { content: "\f752"; }
.bi-filetype-mp3::before { content: "\f753"; }
.bi-filetype-mp4::before { content: "\f754"; }
.bi-filetype-otf::before { content: "\f755"; }
.bi-filetype-pdf::before { content: "\f756"; }
.bi-filetype-php::before { content: "\f757"; }
.bi-filetype-png::before { content: "\f758"; }
.bi-filetype-ppt-1::before { content: "\f759"; }
.bi-filetype-ppt::before { content: "\f75a"; }
.bi-filetype-psd::before { content: "\f75b"; }
.bi-filetype-py::before { content: "\f75c"; }
.bi-filetype-raw::before { content: "\f75d"; }
.bi-filetype-rb::before { content: "\f75e"; }
.bi-filetype-sass::before { content: "\f75f"; }
.bi-filetype-scss::before { content: "\f760"; }
.bi-filetype-sh::before { content: "\f761"; }
.bi-filetype-svg::before { content: "\f762"; }
.bi-filetype-tiff::before { content: "\f763"; }
.bi-filetype-tsx::before { content: "\f764"; }
.bi-filetype-ttf::before { content: "\f765"; }
.bi-filetype-txt::before { content: "\f766"; }
.bi-filetype-wav::before { content: "\f767"; }
.bi-filetype-woff::before { content: "\f768"; }
.bi-filetype-xls-1::before { content: "\f769"; }
.bi-filetype-xls::before { content: "\f76a"; }
.bi-filetype-xml::before { content: "\f76b"; }
.bi-filetype-yml::before { content: "\f76c"; }
.bi-heart-arrow::before { content: "\f76d"; }
.bi-heart-pulse-fill::before { content: "\f76e"; }
.bi-heart-pulse::before { content: "\f76f"; }
.bi-heartbreak-fill::before { content: "\f770"; }
.bi-heartbreak::before { content: "\f771"; }
.bi-hearts::before { content: "\f772"; }
.bi-hospital-fill::before { content: "\f773"; }
.bi-hospital::before { content: "\f774"; }
.bi-house-heart-fill::before { content: "\f775"; }
.bi-house-heart::before { content: "\f776"; }
.bi-incognito::before { content: "\f777"; }
.bi-magnet-fill::before { content: "\f778"; }
.bi-magnet::before { content: "\f779"; }
.bi-person-heart::before { content: "\f77a"; }
.bi-person-hearts::before { content: "\f77b"; }
.bi-phone-flip::before { content: "\f77c"; }
.bi-plugin::before { content: "\f77d"; }
.bi-postage-fill::before { content: "\f77e"; }
.bi-postage-heart-fill::before { content: "\f77f"; }
.bi-postage-heart::before { content: "\f780"; }
.bi-postage::before { content: "\f781"; }
.bi-postcard-fill::before { content: "\f782"; }
.bi-postcard-heart-fill::before { content: "\f783"; }
.bi-postcard-heart::before { content: "\f784"; }
.bi-postcard::before { content: "\f785"; }
.bi-search-heart-fill::before { content: "\f786"; }
.bi-search-heart::before { content: "\f787"; }
.bi-sliders2-vertical::before { content: "\f788"; }
.bi-sliders2::before { content: "\f789"; }
.bi-trash3-fill::before { content: "\f78a"; }
.bi-trash3::before { content: "\f78b"; }
.bi-valentine::before { content: "\f78c"; }
.bi-valentine2::before { content: "\f78d"; }
.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; }
.bi-wrench-adjustable-circle::before { content: "\f78f"; }
.bi-wrench-adjustable::before { content: "\f790"; }
.bi-filetype-json::before { content: "\f791"; }
.bi-filetype-pptx::before { content: "\f792"; }
.bi-filetype-xlsx::before { content: "\f793"; }

View File

@ -1,98 +0,0 @@
.dataTables_info {
margin: 15px 0 !important;
padding: 0px !important;
}
.dataTables_paginate, .dataTables_length, .dataTables_filter {
margin: 15px 0 !important;
}
.dtr-details {
width: 100%;
}
.table-striped>tbody>tr:nth-of-type(odd) {
background-color: #F2F2F2;
}
td.child>ul>li {
display: flex;
}
table.dataTable>tbody>tr.child ul.dtr-details>li {
border-bottom: 1px solid rgba(0, 0, 0, 0.129);
padding: 0.5em 0;
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
background-color: #5e5e5e;
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
table.dataTable td.dt-control:before {
background-color: #979797 !important;
border: 1.5px solid #616161 !important;
border-radius: 2px !important;
color: #fff;
height: 1em;
width: 1em;
line-height: 1.25em;
border-radius: 0px;
box-shadow: none;
font-size: 14px;
transition: 0.5s all;
}
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
table.dataTable td.dt-control:before {
background-color: #979797 !important;
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
background-color: #fbfbfb;
}
table.dataTable.table-striped>tbody>tr>td {
vertical-align: middle;
}
table.dataTable.table-striped>tbody>tr>td>input[type="checkbox"] {
margin-top: 7px;
}
td.dtr-col-lg {
min-width: 350px;
word-break: break-word;
}
td.dtr-col-md {
min-width: 250px;
word-break: break-word;
}
td.dtr-col-sm {
min-width: 125px;
word-break: break-word;
}
.dt-data-w100 .dtr-data {
width: 100%;
}
li .dtr-data {
word-break: break-all;
flex: 1;
padding-left: 5px;
padding-right: 5px;
}
table.dataTable>tbody>tr.child span.dtr-title {
width: 30%;
max-width: 250px;
}
div.dataTables_wrapper div.dataTables_filter {
text-align: left;
}
div.dataTables_wrapper div.dataTables_length {
text-align: right;
}
.dataTables_paginate, .dataTables_length, .dataTables_filter {
margin: 10px 0!important;
}
td.dt-text-right {
text-align: end !important;
}
th.dt-text-right {
text-align: end !important;
}

View File

@ -0,0 +1,308 @@
.space20 {
margin-bottom: 20px;
}
.btn-xs-lg>.lang-sm:after {
margin-left: 4px;
}
.bootstrap-select {
max-width: 350px;
}
.panel-login .apps .btn {
width: auto;
float: left;
margin-right: 10px;
margin-top: auto;
}
.panel-login .apps .btn:hover {
margin-top: 1px !important;
border-bottom-width: 3px;
}
@media (max-width: 767px) {
.panel-login .apps .btn {
width: 100%;
float: none;
margin-bottom: 10px;
}
.panel-login .apps .btn {
border-bottom-width: 4px;
}
.media-clearfix::after {
clear: both;
box-sizing: border-box;
}
.media-clearfix::before {
display: table;
content: " ";
box-sizing: border-box;
}
.xs-show {
display: block !important;
}
.js-tabcollapse-panel-group .panel{
border: none;
box-shadow: none;
}
.js-tabcollapse-panel-group .panel-body {
padding: 10px 0;
}
.js-tabcollapse-panel-group .js-tabcollapse-panel-body .panel-body {
padding: 0;
}
.js-tabcollapse-panel-body .panel-heading {
display: none;
}
.js-tabcollapse-panel-body .well,
.panel-body .form-inline.well {
border: none;
padding: 0;
margin: 0;
box-shadow: none;
background-color: #fff;
}
.js-tabcollapse-panel-heading {
display: block;
height: 37px;
line-height: 37px;
text-indent: 15px;
}
.js-tabcollapse-panel-heading:hover {
text-decoration: none;
}
.js-tabcollapse-panel-heading {
position: relative;
}
.js-tabcollapse-panel-heading:after {
content: '';
display: block;
position: absolute;
top: 17px;
right: 17px;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-bottom: 4px dashed;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
.js-tabcollapse-panel-heading.collapsed:after {
border-bottom: none;
border-top: 4px dashed;
}
.recent-login-success {
font-size: 14px;
margin-top: 10px !important;
}
.pull-xs-right {
float: right !important;
}
.pull-xs-right .dropdown-menu {
right: 0;
left: auto;
}
.text-xs-left {
text-align: left;
}
.text-xs-bold {
font-weight: bold;
}
.text-xs-bold .small {
font-weight: normal;
text-align: justify;
}
.help-block {
text-align: justify;
}
.btn.visible-xs-block {
width: 100%;
float: none;
white-space: normal;
}
.btn-group.footable-actions .btn.btn-xs-half,
.btn.visible-xs-block.btn-xs-half {
width: 50%;
float: left;
}
.btn-group.footable-actions .btn.btn-xs-third,
.btn.visible-xs-block.btn-xs-third {
width: 33.33%;
float: left;
}
.btn-group.footable-actions .btn.btn-xs-quart,
.btn.visible-xs-block.btn-xs-quart {
width: 25%;
float: left;
}
.btn.visible-xs-block.btn-sm,
.btn-xs-lg {
padding: 15px 16px 13px;
line-height: 15px;
}
.input-xs-lg {
height: 47px;
padding: 13px 16px;
}
.btn-group:not(.input-group-btn) {
display: flex;
flex-wrap: wrap;
}
.btn-group.nowrap {
flex-wrap: nowrap;
}
.btn-group.nowrap .dropdown-menu {
width: 100%;
}
.panel-login .btn-group {
display: block;
}
.mass-actions-user .btn-group {
float: none;
}
div[class^='mass-actions'] .dropdown-menu,
.panel-xs-lg .dropdown-menu,
.dropdown-menu.login {
width: 100%;
}
div[class^='mass-actions'] .btn-group .dropdown-menu {
top: 50%;
}
div[class^='mass-actions'] .btn-group .btn-group .dropdown-menu,
div.mass-actions-quarantine .btn-group .dropdown-menu,
.panel-xs-lg .dropdown-menu {
top: 100%;
}
div[class^='mass-actions'] .dropdown-menu>li>a,
.panel-xs-lg .dropdown-menu>li>a,
.dropdown-menu.login>li>a {
padding: 8px 20px;
}
div[class^='mass-actions'] .dropdown-header {
font-size: 14px;
font-weight: bold;
}
.space20 {
margin-bottom: 10px;
}
.top100 {
top: 100% !important;
}
.top33 {
top: 33% !important;
}
.footable-filtering .form {
width: 65%;
}
.btn-xs-lg>.lang-sm:after {
top: 1px;
}
table.footable>tfoot>tr.footable-paging>td {
text-align: left;
}
.footable-first-visible {
min-width: 55px;
}
table>tbody>tr>td>span.footable-toggle {
font-size: 24px;
margin-right: 14px !important;
}
table>tbody>tr>td>span.footable-toggle + input {
position: absolute;
left: 38px;
}
.pagination {
margin-bottom: 5px;
}
tr.footable-filtering>th>form {
width: 270px;
}
.mass-actions-mailbox {
padding: 0;
}
.panel-xs-lg .panel-heading {
height: 66px;
line-height: 47px;
}
.panel-xs-lg .btn-group .btn {
padding-right: 5px;
padding-left: 5px;
}
.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) {
width: 100%;
}
.btn-group:not(.bootstrap-select) {
width: auto !important;
}
.bootstrap-select {
max-width: 100%;
}
.img-responsive {
margin: 0 auto;
}
.btn-group.footable-actions {
position: absolute;
width: 90vw !important;
left: 0;
height: 36px;
margin-top: -8px;
}
.btn-group.footable-actions .btn {
padding: 10px 16px 7px;
line-height: 15px;
display: block;
width: 100%;
}
.btn-group.footable-actions:after {
content: "";
display: block;
clear: both;
}
.bootstrap-select.btn-group.show-tick .dropdown-menu li a span.text {
margin-right: 14px;
white-space: normal;
}
.clearfix {
flex-basis: 100%;
height: 0;
}
.btn-group > .btn-group {
flex-basis: 100%;
}
.btn-group .btn {
display: flex !important;
align-items: center;
justify-content: center;
}
.btn-group .btn i {
margin-right: 5px;
}
.btn-group .btn .caret {
margin-left: 5px;
}
.panel-login .btn-group .btn {
display: block !important;
}
.panel-login .clearfix {
height: auto;
}
}
@media (max-width: 350px) {
.mailcow-logo img {
max-width: 250px;
}
}

View File

@ -1,221 +0,0 @@
.btn-xs-lg>.lang-sm:after {
margin-left: 4px;
}
.bootstrap-select {
max-width: 350px;
}
.card-login .apps .btn {
width: auto;
float: left;
margin-right: 10px;
margin-top: auto;
}
.card-login .apps .btn:hover {
margin-top: 1px !important;
border-bottom-width: 3px;
}
.responsive-tabs .nav-tabs {
display: none;
}
.dataTables_paginate.paging_simple_numbers .pagination {
display: flex;
flex-wrap: wrap;
}
@media (min-width: 768px) {
.responsive-tabs .nav-tabs {
display: flex;
}
.responsive-tabs .card .card-body.collapse {
display: block;
}
}
@media (max-width: 767px) {
.responsive-tabs .tab-pane {
display: block !important;
opacity: 1;
}
.card-login .apps .btn {
width: 100%;
float: none;
margin-bottom: 10px;
}
.card-login .apps .btn {
border-bottom-width: 4px;
}
.xs-show {
display: block !important;
}
.recent-login-success {
font-size: 14px;
margin-top: 10px !important;
}
.pull-xs-right {
float: right !important;
}
.pull-xs-right .dropdown-menu {
right: 0;
left: auto;
}
.text-xs-left {
text-align: left;
}
.text-xs-bold {
font-weight: bold;
}
.text-xs-bold .small {
font-weight: normal;
text-align: justify;
}
.btn.d-block {
width: 100%;
white-space: normal;
}
.btn.btn-xs-half,
.btn.d-block.btn-xs-half {
width: 50%;
}
.btn.btn-xs-third,
.btn.d-block.btn-xs-third {
width: 33.33%;
}
.btn.btn-xs-quart,
.btn.d-block.btn-xs-quart {
width: 25%;
}
.btn.d-block.btn-sm,
.btn-xs-lg {
padding: .5rem 1rem;
line-height: 20px;
}
.input-xs-lg {
height: 47px;
padding: 13px 16px;
}
.btn-group:not(.input-group-btn) {
display: flex;
flex-wrap: wrap;
}
.btn-group.nowrap {
flex-wrap: nowrap;
}
.btn-group.nowrap .dropdown-menu {
width: 100%;
}
.card-login .btn-group {
display: block;
}
.mass-actions-user .btn-group {
float: none;
}
div[class^='mass-actions'] .dropdown-menu,
.card-xs-lg .dropdown-menu,
.dropdown-menu.login {
width: 100%;
}
div[class^='mass-actions'] .btn-group .dropdown-menu {
top: 50%;
}
div[class^='mass-actions'] .btn-group .btn-group .dropdown-menu,
div.mass-actions-quarantine .btn-group .dropdown-menu,
.card-xs-lg .dropdown-menu {
top: 100%;
}
div[class^='mass-actions'] .dropdown-menu>li>a,
.card-xs-lg .dropdown-menu>li>a,
.dropdown-menu.login>li>a {
padding: 8px 20px;
}
div[class^='mass-actions'] .dropdown-header {
font-size: 14px;
font-weight: bold;
}
.top100 {
top: 100% !important;
}
.top33 {
top: 33% !important;
}
.footable-filtering .form {
width: 65%;
}
.btn-xs-lg>.lang-sm:after {
top: 1px;
}
.pagination {
margin-bottom: 5px;
}
.mass-actions-mailbox {
padding: 0;
}
.card-xs-lg .card-header {
height: 66px;
line-height: 47px;
}
.card-xs-lg .btn-group .btn {
padding-right: 5px;
padding-left: 5px;
}
.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) {
width: 100%;
}
.btn-group:not(.bootstrap-select) {
width: auto !important;
}
.bootstrap-select {
max-width: 100%;
}
.bootstrap-select.btn-group.show-tick .dropdown-menu li a span.text {
margin-right: 14px;
white-space: normal;
}
.btn-group > .btn-group {
flex-basis: 100%;
}
.btn-group .btn {
display: flex !important;
align-items: center;
justify-content: center;
}
.btn-group .btn i {
margin-right: 5px;
}
.card-login .btn-group .btn {
display: block !important;
}
.dt-sm-head-hidden .dtr-title {
display: none !important;
}
div.dataTables_wrapper div.dataTables_length {
text-align: left;
}
.senders-mw220 {
max-width: 100% !important;
}
}
@media (max-width: 350px) {
.mailcow-logo img {
max-width: 250px;
}
}
@media (min-width: 1400px) {
.container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {
max-width: 1600px;
}
}

View File

@ -25,6 +25,7 @@ body.modal-open {
} }
.mass-actions-admin { .mass-actions-admin {
user-select: none; user-select: none;
padding:10px 0 10px 0;
} }
.inputMissingAttr { .inputMissingAttr {
border-color: #FF4136; border-color: #FF4136;

View File

@ -26,6 +26,7 @@
} }
.mass-actions-debug { .mass-actions-debug {
user-select: none; user-select: none;
padding:10px 0 10px 10px;
} }
.inputMissingAttr { .inputMissingAttr {
border-color: #FF4136; border-color: #FF4136;

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