initial_upload
This commit is contained in:
commit
021fad453f
5 changed files with 447 additions and 0 deletions
39
docker-compose.override.yml
Normal file
39
docker-compose.override.yml
Normal file
|
@ -0,0 +1,39 @@
|
|||
version: "2.1"
|
||||
services:
|
||||
linuxmuster-mailcow:
|
||||
image: ghcr.io/linuxmuster/linuxmuster-mailcow:latest
|
||||
container_name: mailcowcustomized_linuxmuster-mailcow
|
||||
volumes:
|
||||
- ./data/conf/dovecot:/conf/dovecot:rw
|
||||
- ./data/conf/sogo:/conf/sogo:rw
|
||||
- ../syncer.py:/syncer.py:rw
|
||||
- ../independent_mailboxes.txt:/independent_mailboxes.txt:r
|
||||
- ../independent_maillists.txt:/independent_maillists.txt:r
|
||||
- ../independent_aliases.txt:/independent_aliases.txt:r
|
||||
depends_on:
|
||||
- nginx-mailcow
|
||||
- dockerapi-mailcow
|
||||
- php-fpm-mailcow
|
||||
- sogo-mailcow
|
||||
- dovecot-mailcow
|
||||
environment:
|
||||
- LINUXMUSTER_MAILCOW_LDAP_URI=ldap://10.16.1.1
|
||||
- LINUXMUSTER_MAILCOW_LDAP_BASE_DN=DC=morz,DC=de
|
||||
- LINUXMUSTER_MAILCOW_LDAP_BIND_DN=CN=********,OU=******,OU=******,DC=******,DC=**
|
||||
- LINUXMUSTER_MAILCOW_LDAP_BIND_DN_PASSWORD=******
|
||||
- LINUXMUSTER_MAILCOW_API_KEY=******-******-******-******-******
|
||||
- LINUXMUSTER_MAILCOW_SYNC_INTERVAL=300
|
||||
- LINUXMUSTER_MAILCOW_DOMAIN_QUOTA=500000
|
||||
- LINUXMUSTER_MAILCOW_ENABLE_GAL=1
|
||||
# Port muss stimmen... bei mir eben 9443, weil ich den Port umgebogen hab
|
||||
- LINUXMUSTER_MAILCOW_API_URI=https://nginx-mailcow:9443
|
||||
- LINUXMUSTER_MAILCOW_DOCKERAPI_URI=https://dockerapi-mailcow:9443
|
||||
networks:
|
||||
mailcow-network:
|
||||
aliases:
|
||||
- linuxmuster
|
||||
|
||||
# ich hab den Look von SOGo noch an die Schulfarben angepasst, deshalb zusätzlich noch das hier...
|
||||
sogo-mailcow:
|
||||
volumes:
|
||||
- ./data/conf/sogo/custom-theme.css:/usr/lib/GNUstep/SOGo/WebServerResources/css/theme-default.css:z
|
2
independent_aliases.txt
Normal file
2
independent_aliases.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
beispiel1@schuldomain.de:adresse1@woauchimmer.tld,adresse2@irgendwoandersodernicht.net
|
||||
beispiel2@schuldomain.de:adresse3@egalwo.tld
|
5
independent_mailboxes.txt
Normal file
5
independent_mailboxes.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
Support:support@schuldomain.tld:10240
|
||||
Nextcloud:claudi@schuldomain.tld:1
|
||||
Sekretariat:sekretariat@schuldomain.tld:10240
|
||||
Hausmeister:hausmeister@schuldomain.tld:10240
|
||||
Sozialarbeiter:nachhilfe@schuldomain.tld:10240
|
1
independent_maillists.txt
Normal file
1
independent_maillists.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Name für das Verteilerpostfach:beispiel@adresse.de:empfaenger1@domain.de,empfaenger2@woanders.de,empfaenger3@woanders.de
|
400
syncer.py
Normal file
400
syncer.py
Normal file
|
@ -0,0 +1,400 @@
|
|||
import sys
|
||||
import os
|
||||
import string
|
||||
import time
|
||||
import datetime
|
||||
import logging
|
||||
import coloredlogs
|
||||
import random
|
||||
import templateHelper
|
||||
import json
|
||||
|
||||
from mailcowHelper import MailcowHelper, MailcowException
|
||||
from ldapHelper import LdapHelper
|
||||
from objectStorageHelper import DomainListStorage, MailboxListStorage, AliasListStorage, FilterListStorage
|
||||
from dockerapiHelper import DockerapiHelper
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
coloredlogs.install(
|
||||
level='INFO', fmt='%(asctime)s - [%(levelname)s] %(message)s')
|
||||
|
||||
|
||||
class LinuxmusterMailcowSyncer:
|
||||
|
||||
ldapSogoUserFilter = "(sophomorixRole='student' OR sophomorixRole='teacher')"
|
||||
ldapUserFilter = "(|(sophomorixRole=student)(sophomorixRole=teacher))"
|
||||
ldapMailingListFilter = "(|(sophomorixType=adminclass)(sophomorixType=project))"
|
||||
ldapMailingListMemberFilter = f"(&(memberof:1.2.840.113556.1.4.1941:=@@mailingListDn@@){ldapUserFilter})"
|
||||
|
||||
def __init__(self):
|
||||
self._config = self._readConfig()
|
||||
|
||||
self._mailcow = MailcowHelper(
|
||||
self._config['API_URI'],
|
||||
self._config['API_KEY']
|
||||
)
|
||||
self._ldap = LdapHelper(
|
||||
self._config['LDAP_URI'],
|
||||
self._config['LDAP_BIND_DN'],
|
||||
self._config['LDAP_BIND_DN_PASSWORD'],
|
||||
self._config['LDAP_BASE_DN']
|
||||
)
|
||||
|
||||
self._dockerapi = DockerapiHelper(self._config["DOCKERAPI_URI"])
|
||||
|
||||
templateHelper.applyAllTemplates(self._config, self._dockerapi)
|
||||
|
||||
def sync(self):
|
||||
while (True):
|
||||
logging.info("=== Starting sync ===")
|
||||
logging.info("##### angepasst von Jesko - Datei liegt in /srv/docker/syncer.py #####")
|
||||
if not self._sync():
|
||||
logging.critical("!!! The sync failed, see above errors !!!")
|
||||
interval = 30
|
||||
else:
|
||||
logging.info("=== Sync finished successfully ==")
|
||||
interval = int(self._config['SYNC_INTERVAL'])
|
||||
|
||||
logging.info(f"sleeping {interval} seconds before next cycle")
|
||||
time.sleep(interval)
|
||||
|
||||
def _sync(self):
|
||||
|
||||
logging.info("Step 1: Loading current Data from AD")
|
||||
|
||||
logging.info(" * Binding to ldap")
|
||||
if not self._ldap.bind():
|
||||
return False
|
||||
|
||||
logging.info(" * Loading users from AD")
|
||||
ret, adUsers = self._ldap.search(
|
||||
self.ldapUserFilter,
|
||||
["mail", "proxyAddresses", "sophomorixStatus",
|
||||
"sophomorixMailQuotaCalculated", "displayName"]
|
||||
)
|
||||
if not ret:
|
||||
logging.critical("!!! Error getting users from AD !!!")
|
||||
return False
|
||||
|
||||
logging.info(" * Loading groups from AD")
|
||||
ret, adLists = self._ldap.search(
|
||||
self.ldapMailingListFilter,
|
||||
["mail", "proxyAddresses", "distinguishedName",
|
||||
"sophomorixMailList", "sAMAccountName"]
|
||||
)
|
||||
if not ret:
|
||||
logging.critical("!!! Error getting lists from AD !!!")
|
||||
return False
|
||||
|
||||
mailcowDomains = DomainListStorage()
|
||||
mailcowMailboxes = MailboxListStorage(mailcowDomains)
|
||||
mailcowAliases = AliasListStorage(mailcowDomains)
|
||||
mailcowFilters = FilterListStorage(mailcowDomains)
|
||||
|
||||
logging.info("Step 2: Loading current Data from Mailcow")
|
||||
|
||||
try:
|
||||
rawData = self._mailcow.getAllElementsOfType("domain")
|
||||
mailcowDomains.loadRawData(rawData)
|
||||
|
||||
rawData = self._mailcow.getAllElementsOfType("mailbox")
|
||||
mailcowMailboxes.loadRawData(rawData)
|
||||
|
||||
rawData = self._mailcow.getAllElementsOfType("alias")
|
||||
mailcowAliases.loadRawData(rawData)
|
||||
|
||||
# It is actially "filters" (plural); nobody knows why
|
||||
rawData = self._mailcow.getAllElementsOfType("filters")
|
||||
mailcowFilters.loadRawData(rawData)
|
||||
#with open('independent_maillists.txt', 'w') as file:
|
||||
# json.dump(rawData, file)
|
||||
except MailcowException:
|
||||
return False
|
||||
except ConnectionError as e:
|
||||
logging.error(e)
|
||||
logging.critical(
|
||||
"!!! A connection error occured, is mailcow still starting up? !!!")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.exception("An exception occured: ", exc_info=e)
|
||||
return False
|
||||
|
||||
logging.info("Step 3: Loading current data from independent lists")
|
||||
logging.info(" * Processing /independent_maillists.txt")
|
||||
try:
|
||||
with open("/independent_maillists.txt", "r") as file:
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":")
|
||||
if len(parts) != 2:
|
||||
logging.error(f"invalid line in independent_maillists.txt: {line}")
|
||||
continue
|
||||
listAddress = parts[0]
|
||||
memberAddresses = parts[1].split(",")
|
||||
self._addMailbox({
|
||||
"mail": listAddress,
|
||||
"sophomorixStatus": "U",
|
||||
"sophomorixMailQuotaCalculated": 1,
|
||||
"displayName": listAddress + " (independent list)"
|
||||
}, mailcowMailboxes)
|
||||
self._addListFilter(listAddress, memberAddresses, mailcowFilters)
|
||||
logging.info(f" - adding independent mailinglist {listAddress} with members {memberAddresses}")
|
||||
except Exception as e:
|
||||
logging.exception("An exception occured during processing of independent_maillists.txt: ", exc_info=e)
|
||||
return False
|
||||
|
||||
logging.info(" * processing /independent_mailboxes.txt")
|
||||
try:
|
||||
with open("/independent_mailboxes.txt", "r") as file:
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":")
|
||||
if len(parts) != 3:
|
||||
logging.error(f"invalid line in independent_mailboxes.txt: {line}")
|
||||
continue
|
||||
mboxName = parts[0]
|
||||
mboxAddress = parts[1]
|
||||
mboxQuota = parts[2]
|
||||
self._addMailbox({
|
||||
"mail": mboxAddress,
|
||||
"sophomorixStatus": "U",
|
||||
"sophomorixMailQuotaCalculated": mboxQuota,
|
||||
"displayName": mboxName,
|
||||
}, mailcowMailboxes)
|
||||
logging.info(f" - adding independent mbox {mboxAddress} for {mboxName} with quota {mboxQuota}")
|
||||
except Exception as e:
|
||||
logging.exception("An exception occured during processing of independent_mailboxes.txt: ", exc_info=e)
|
||||
return False
|
||||
logging.info(" * Processing /independent_mailaliases.txt")
|
||||
try:
|
||||
with open("/independent_aliases.txt", "r") as file:
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":")
|
||||
if len(parts) != 2:
|
||||
logging.error(f"invalid line in independent_aliases.txt: {line}")
|
||||
continue
|
||||
alias = parts[0]
|
||||
forwardingTo = parts[1]
|
||||
self._addAlias(alias, forwardingTo, mailcowAliases)
|
||||
logging.info(f" - adding independent alias {alias} forwarding to {forwardingTo}")
|
||||
except Exception as e:
|
||||
logging.exception("An exception occured during processing of independent_aliases.txt: ", exc_info=e)
|
||||
return False
|
||||
|
||||
|
||||
logging.info("Step 4: Calculating deltas between AD and Mailcow")
|
||||
|
||||
for user in adUsers:
|
||||
mail = user["mail"]
|
||||
maildomain = mail.split("@")[-1]
|
||||
|
||||
if not self._addDomain(maildomain, mailcowDomains):
|
||||
continue
|
||||
|
||||
self._addMailbox(user, mailcowMailboxes)
|
||||
self._addAliasesFromProxyAddresses(user, mail, mailcowAliases)
|
||||
|
||||
for mailingList in adLists:
|
||||
if not mailingList["sophomorixMailList"] == "TRUE":
|
||||
continue
|
||||
|
||||
mail = mailingList["mail"]
|
||||
maildomain = mail.split("@")[-1]
|
||||
ret, members = self._ldap.search(
|
||||
self.ldapMailingListMemberFilter.replace(
|
||||
"@@mailingListDn@@", mailingList["distinguishedName"]),
|
||||
["mail"]
|
||||
)
|
||||
#logging.info(f" * Adding mailinglist {mail} with members {members}")
|
||||
if not ret:
|
||||
continue
|
||||
|
||||
if not self._addDomain(maildomain, mailcowDomains):
|
||||
continue
|
||||
|
||||
self._addMailbox({
|
||||
"mail": mail,
|
||||
"sophomorixStatus": "U",
|
||||
"sophomorixMailQuotaCalculated": 1,
|
||||
"displayName": mailingList["sAMAccountName"] + " (list)"
|
||||
}, mailcowMailboxes)
|
||||
self._addAliasesFromProxyAddresses(
|
||||
mailingList, mail, mailcowAliases)
|
||||
|
||||
self._addListFilter(mail, list(
|
||||
map(lambda x: x["mail"], members)), mailcowFilters)
|
||||
|
||||
if mailcowDomains.queuesAreEmpty() and mailcowMailboxes.queuesAreEmpty() and mailcowAliases.queuesAreEmpty() and mailcowFilters.queuesAreEmpty():
|
||||
logging.info(" * Everything up-to-date!")
|
||||
return True
|
||||
else:
|
||||
logging.info("* Found deltas:")
|
||||
logging.info(
|
||||
f" * {mailcowDomains.getQueueCountsString('domains')}")
|
||||
logging.info(
|
||||
f" * {mailcowMailboxes.getQueueCountsString('mailboxes')}")
|
||||
logging.info(
|
||||
f" * {mailcowAliases.getQueueCountsString('aliases')}")
|
||||
logging.info(
|
||||
f" * {mailcowFilters.getQueueCountsString('filters')}")
|
||||
|
||||
logging.info("Step 4: Syncing deltas to Mailcow")
|
||||
|
||||
try:
|
||||
self._mailcow.killElementsOfType(
|
||||
"filter", mailcowFilters.killQueue())
|
||||
self._mailcow.killElementsOfType(
|
||||
"alias", mailcowAliases.killQueue())
|
||||
self._mailcow.killElementsOfType(
|
||||
"mailbox", mailcowMailboxes.killQueue())
|
||||
self._mailcow.killElementsOfType(
|
||||
"domain", mailcowDomains.killQueue())
|
||||
|
||||
self._mailcow.addElementsOfType(
|
||||
"domain", mailcowDomains.addQueue())
|
||||
self._mailcow.updateElementsOfType(
|
||||
"domain", mailcowDomains.updateQueue())
|
||||
|
||||
self._mailcow.addElementsOfType(
|
||||
"mailbox", mailcowMailboxes.addQueue())
|
||||
self._mailcow.updateElementsOfType(
|
||||
"mailbox", mailcowMailboxes.updateQueue())
|
||||
|
||||
self._mailcow.addElementsOfType("alias", mailcowAliases.addQueue())
|
||||
self._mailcow.updateElementsOfType(
|
||||
"alias", mailcowAliases.updateQueue())
|
||||
|
||||
self._mailcow.addElementsOfType(
|
||||
"filter", mailcowFilters.addQueue())
|
||||
self._mailcow.updateElementsOfType(
|
||||
"filter", mailcowFilters.updateQueue())
|
||||
except MailcowException:
|
||||
return False
|
||||
|
||||
self._ldap.unbind()
|
||||
return True
|
||||
|
||||
def _addDomain(self, domainName, mailcowDomains):
|
||||
return mailcowDomains.addElement({
|
||||
"domain": domainName,
|
||||
"defquota": 1,
|
||||
"maxquota": self._config['DOMAIN_QUOTA'],
|
||||
"quota": self._config['DOMAIN_QUOTA'],
|
||||
"description": DomainListStorage.validityCheckDescription,
|
||||
"active": 1,
|
||||
"restart_sogo": 1,
|
||||
"mailboxes": 10000,
|
||||
"aliases": 10000,
|
||||
"gal": int(self._config['ENABLE_GAL'])
|
||||
}, domainName)
|
||||
|
||||
def _addMailbox(self, user, mailcowMailboxes):
|
||||
mail = user["mail"]
|
||||
domain = mail.split("@")[-1]
|
||||
localPart = mail.split("@")[0]
|
||||
password = ''.join(random.choices(
|
||||
string.ascii_letters + string.digits, k=20))
|
||||
active = 0 if user["sophomorixStatus"] in [
|
||||
"L", "D", "R", "K", "F"] else 1
|
||||
return mailcowMailboxes.addElement({
|
||||
"domain": domain,
|
||||
"local_part": localPart,
|
||||
"active": active,
|
||||
"quota": user["sophomorixMailQuotaCalculated"],
|
||||
"password": password,
|
||||
"password2": password,
|
||||
"name": user["displayName"]
|
||||
}, mail)
|
||||
|
||||
def _addAliasesFromProxyAddresses(self, user, mail, mailcowAliases):
|
||||
aliases = []
|
||||
|
||||
if "proxyAddresses" in user:
|
||||
if isinstance(user["proxyAddresses"], list):
|
||||
aliases = user["proxyAddresses"]
|
||||
else:
|
||||
aliases = [user["proxyAddresses"]]
|
||||
|
||||
if len(aliases) > 0:
|
||||
for alias in aliases:
|
||||
self._addAlias(alias, mail, mailcowAliases)
|
||||
|
||||
def _addAlias(self, alias, goto, mailcowAliases):
|
||||
mailcowAliases.addElement({
|
||||
"address": alias,
|
||||
"goto": goto,
|
||||
"active": 1,
|
||||
"sogo_visible": 1
|
||||
}, alias)
|
||||
pass
|
||||
|
||||
def _addListFilter(self, listAddress, memberAddresses, mailcowFilters):
|
||||
scriptData = "### Auto-generated mailinglist filter by linuxmuster ###\r\n\r\n"
|
||||
scriptData += "require \"copy\";\r\n\r\n"
|
||||
for memberAddress in memberAddresses:
|
||||
scriptData += f"redirect :copy \"{memberAddress}\";\r\n"
|
||||
scriptData += "\r\ndiscard;stop;"
|
||||
mailcowFilters.addElement({
|
||||
'active': 1,
|
||||
'username': listAddress,
|
||||
'filter_type': 'prefilter',
|
||||
'script_data': scriptData,
|
||||
'script_desc': f"Auto-generated mailinglist filter for {listAddress}"
|
||||
}, listAddress)
|
||||
|
||||
def _readConfig(self):
|
||||
requiredConfigKeys = [
|
||||
'LINUXMUSTER_MAILCOW_LDAP_URI',
|
||||
'LINUXMUSTER_MAILCOW_LDAP_BASE_DN',
|
||||
'LINUXMUSTER_MAILCOW_LDAP_BIND_DN',
|
||||
'LINUXMUSTER_MAILCOW_LDAP_BIND_DN_PASSWORD',
|
||||
'LINUXMUSTER_MAILCOW_API_KEY',
|
||||
'LINUXMUSTER_MAILCOW_SYNC_INTERVAL',
|
||||
'LINUXMUSTER_MAILCOW_DOMAIN_QUOTA',
|
||||
'LINUXMUSTER_MAILCOW_ENABLE_GAL'
|
||||
]
|
||||
|
||||
allowedConfigKeys = [
|
||||
"LINUXMUSTER_MAILCOW_DOCKERAPI_URI",
|
||||
"LINUXMUSTER_MAILCOW_API_URI"
|
||||
]
|
||||
|
||||
config = {
|
||||
"LDAP_SOGO_USER_FILTER": self.ldapSogoUserFilter,
|
||||
"LDAP_USER_FILTER": self.ldapUserFilter,
|
||||
"DOCKERAPI_URI": "https://dockerapi-mailcow",
|
||||
"API_URI": "https://nginx-mailcow"
|
||||
}
|
||||
|
||||
for configKey in requiredConfigKeys:
|
||||
if configKey not in os.environ:
|
||||
sys.exit(f"Required environment value {configKey} is not set")
|
||||
config[configKey.replace(
|
||||
'LINUXMUSTER_MAILCOW_', '')] = os.environ[configKey]
|
||||
|
||||
for configKey in allowedConfigKeys:
|
||||
if configKey in os.environ:
|
||||
config[configKey.replace(
|
||||
'LINUXMUSTER_MAILCOW_', '')] = os.environ[configKey]
|
||||
|
||||
logging.info("CONFIG:")
|
||||
for key, value in config.items():
|
||||
logging.info(" * {:25}: {}".format(key, value))
|
||||
|
||||
return config
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
syncer = LinuxmusterMailcowSyncer()
|
||||
syncer.sync()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
Loading…
Add table
Reference in a new issue