Skip to content
Open
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
FROM python:3-alpine

RUN apk --no-cache add build-base openldap-dev python2-dev python3-dev
RUN apk --no-cache add build-base openldap-dev python2-dev python3-dev ca-certificates
RUN pip3 install python-ldap sqlalchemy requests
RUN update-ca-certificates

COPY templates ./templates
COPY api.py filedb.py syncer.py ./
Expand All @@ -10,4 +11,5 @@ VOLUME [ "/db" ]
VOLUME [ "/conf/dovecot" ]
VOLUME [ "/conf/sogo" ]

ENTRYPOINT [ "python3", "syncer.py" ]
COPY ./entrypoint.sh /entrypoint.sh
CMD ["/bin/ash", "/entrypoint.sh"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ A python script periodically checks and creates new LDAP accounts and deactivate
- ./data/ldap:/db:rw
- ./data/conf/dovecot:/conf/dovecot:rw
- ./data/conf/sogo:/conf/sogo:rw
#- /usr/local/share/ca-certificates/:/usr/local/share/ca-certificates/
environment:
- LDAP-MAILCOW_LDAP_URI=ldap(s)://dc.example.local
- LDAP-MAILCOW_LDAP_BASE_DN=OU=Mail Users,DC=example,DC=local
Expand All @@ -40,6 +41,8 @@ A python script periodically checks and creates new LDAP accounts and deactivate
- LDAP-MAILCOW_SYNC_INTERVAL=300
- LDAP-MAILCOW_LDAP_FILTER=(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=CN=Group,CN=Users,DC=example DC=local))
- LDAP-MAILCOW_SOGO_LDAP_FILTER=objectClass='user' AND objectCategory='person' AND memberOf:1.2.840.113556.1.4.1941:='CN=Group,CN=Users,DC=example DC=local'
- LDAP-MAILCOW_USER_ATTR=userPrincipalName
#- LDAP-MAILCOW_REPLACE_DOMAIN=example.com
```

3. Configure environmental variables:
Expand All @@ -51,9 +54,11 @@ A python script periodically checks and creates new LDAP accounts and deactivate
* `LDAP-MAILCOW_API_HOST` - mailcow API url. Make sure it's enabled and accessible from within the container for both reads and writes
* `LDAP-MAILCOW_API_KEY` - mailcow API key (read/write)
* `LDAP-MAILCOW_SYNC_INTERVAL` - interval in seconds between LDAP synchronizations
* `LDAP-MAILCOW_USER_ATTR` - user attribute to use
* **Optional** LDAP filters (see example above). SOGo uses special syntax, so you either have to **specify both or none**:
* `LDAP-MAILCOW_LDAP_FILTER` - LDAP filter to apply, defaults to `(&(objectClass=user)(objectCategory=person))`
* `LDAP-MAILCOW_SOGO_LDAP_FILTER` - LDAP filter to apply for SOGo ([special syntax](https://sogo.nu/files/docs/SOGoInstallationGuide.html#_authentication_using_ldap)), defaults to `objectClass='user' AND objectCategory='person'`
* `LDAP-MAILCOW_REPLACE_DOMAIN` - **Optional** Replace domain (eg you have public domain and internal AD domain)

4. Start additional container: `docker-compose up -d ldap-mailcow`
5. Check logs `docker-compose logs ldap-mailcow`
Expand Down
16 changes: 12 additions & 4 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ def __post_request(url, json_data):
if rsp['type'] != 'success':
sys.exit(f"API {url}: {rsp['type']} - {rsp['msg']}")

def add_user(email, name, active):
def add_user(email, name, active, replaceDomain):
if replaceDomain is not None:
email = email.replace(email.split('@')[1], replaceDomain)
password = ''.join(random.choices(string.ascii_letters + string.digits, k=20))
json_data = {
'local_part':email.split('@')[0],
Expand All @@ -31,7 +33,9 @@ def add_user(email, name, active):

__post_request('api/v1/add/mailbox', json_data)

def edit_user(email, active=None, name=None):
def edit_user(email, replaceDomain, active=None, name=None):
if replaceDomain is not None:
email = email.replace(email.split('@')[1], replaceDomain)
attr = {}
if (active is not None):
attr['active'] = 1 if active else 0
Expand All @@ -45,12 +49,16 @@ def edit_user(email, active=None, name=None):

__post_request('api/v1/edit/mailbox', json_data)

def __delete_user(email):
def __delete_user(email, replaceDomain):
if replaceDomain is not None:
email = email.replace(email.split('@')[1], replaceDomain)
json_data = [email]

__post_request('api/v1/delete/mailbox', json_data)

def check_user(email):
def check_user(email, replaceDomain):
if replaceDomain is not None:
email = email.replace(email.split('@')[1], replaceDomain)
url = f"{api_host}/api/v1/get/mailbox/{email}"
headers = {'X-API-Key': api_key, 'Content-type': 'application/json'}
req = requests.get(url, headers=headers)
Expand Down
3 changes: 3 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
update-ca-certificates
python3 syncer.py
27 changes: 19 additions & 8 deletions syncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,23 @@ def sync():

ldap_results = ldap_connector.search_s(config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
config['LDAP_FILTER'],
['userPrincipalName', 'cn', 'userAccountControl'])
[config['USER_ATTR'], 'cn', 'userAccountControl'])

ldap_results = map(lambda x: (
x[1]['userPrincipalName'][0].decode(),
x[1][config['USER_ATTR']][0].decode(),
x[1]['cn'][0].decode(),
False if int(x[1]['userAccountControl'][0].decode()) & 0b10 else True), ldap_results)

filedb.session_time = datetime.datetime.now()

if 'REPLACE_DOMAIN' in config:
replaceDomain = config['REPLACE_DOMAIN']
else:
replaceDomain = None

for (email, ldap_name, ldap_active) in ldap_results:
(db_user_exists, db_user_active) = filedb.check_user(email)
(api_user_exists, api_user_active, api_name) = api.check_user(email)
(api_user_exists, api_user_active, api_name) = api.check_user(email, replaceDomain)

unchanged = True

Expand All @@ -62,7 +67,7 @@ def sync():
unchanged = False

if not api_user_exists:
api.add_user(email, ldap_name, ldap_active)
api.add_user(email, ldap_name, ldap_active, replaceDomain)
(api_user_exists, api_user_active, api_name) = (True, ldap_active, ldap_name)
logging.info (f"Added Mailcow user: {email} (Active: {ldap_active})")
unchanged = False
Expand All @@ -73,23 +78,23 @@ def sync():
unchanged = False

if api_user_active != ldap_active:
api.edit_user(email, active=ldap_active)
api.edit_user(email, replaceDomain, active=ldap_active)
logging.info (f"{'Activated' if ldap_active else 'Deactived'} {email} in Mailcow")
unchanged = False

if api_name != ldap_name:
api.edit_user(email, name=ldap_name)
api.edit_user(email, replaceDomain, name=ldap_name)
logging.info (f"Changed name of {email} in Mailcow to {ldap_name}")
unchanged = False

if unchanged:
logging.info (f"Checked user {email}, unchanged")

for email in filedb.get_unchecked_active_users():
(api_user_exists, api_user_active, _) = api.check_user(email)
(api_user_exists, api_user_active, _) = api.check_user(email, replaceDomain)

if (api_user_active and api_user_active):
api.edit_user(email, active=False)
api.edit_user(email, replaceDomain, active=False)
logging.info (f"Deactivated user {email} in Mailcow, not found in LDAP")

filedb.user_set_active_to(email, False)
Expand Down Expand Up @@ -148,6 +153,12 @@ def read_config():
config['LDAP_FILTER'] = os.environ['LDAP-MAILCOW_LDAP_FILTER'] if 'LDAP-MAILCOW_LDAP_FILTER' in os.environ else '(&(objectClass=user)(objectCategory=person))'
config['SOGO_LDAP_FILTER'] = os.environ['LDAP-MAILCOW_SOGO_LDAP_FILTER'] if 'LDAP-MAILCOW_SOGO_LDAP_FILTER' in os.environ else "objectClass='user' AND objectCategory='person'"

if 'LDAP-MAILCOW_REPLACE_DOMAIN' in os.environ:
config['REPLACE_DOMAIN'] = os.environ['LDAP-MAILCOW_REPLACE_DOMAIN']

if 'LDAP-MAILCOW_USER_ATTR' in os.environ:
config['USER_ATTR'] = os.environ['LDAP-MAILCOW_USER_ATTR']

return config

def read_dovecot_passdb_conf_template():
Expand Down
6 changes: 3 additions & 3 deletions templates/sogo/plist_ldap
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<key>IDFieldName</key>
<string>cn</string>
<key>UIDFieldName</key>
<string>userPrincipalName</string>
<string>$user_attr</string>

<key>baseDN</key>
<string>$ldap_base_dn</string>
Expand All @@ -21,7 +21,7 @@
<string>$ldap_bind_dn_password</string>
<key>bindFields</key>
<array>
<string>userPrincipalName</string>
<string>$user_attr</string>
</array>

<key>bindAsCurrentUser</key>
Expand All @@ -42,4 +42,4 @@

<key>scope</key>
<string>SUB</string>
</dict>
</dict>