[GH-ISSUE #877] [INTEGRATION] Stalwart Mailserver #319

Closed
opened 2026-02-27 08:16:37 +03:00 by kerem · 14 comments
Owner

Originally created by @dereulenspiegel on GitHub (Mar 21, 2024).
Original GitHub issue: https://github.com/lldap/lldap/issues/877

Checklist

  • Check if there is already an example config for it.
  • Try to figure out the configuration values for the new service yourself.

Description of the service
I am currently trying to setup Stalwart Mailserver which is an all in one mailserver solution

What you've tried
I tried setting up lldap as the directory for Stalwart Mailserver so users can authenticate with their existing accounts

What's not working
Users logging in should be working, bit identifying an admin user does not. Unfortunately Stalwart does not support creating a filter for looking up group membership to identitfy admin users. Instead it is looking checking if a user has the objectClass admin or administrator.
I know that I can add such an objectClass to the schema via lldap-cli, but then unfortunately all users have this objectClass which I do not want.

Currently I am not sure if I miss something in the GraphQL-API and lldap-cli (I am running the version from the 18.04.2024) or if this is a current limitation of lldap.
Thanks in advance :)

Originally created by @dereulenspiegel on GitHub (Mar 21, 2024). Original GitHub issue: https://github.com/lldap/lldap/issues/877 **Checklist** - [x] Check if there is already an [example config](https://github.com/lldap/lldap/tree/main/example_configs) for it. - [x] Try to figure out the configuration values for the new service yourself. **Description of the service** I am currently trying to setup [Stalwart Mailserver](https://stalw.art/) which is an all in one mailserver solution **What you've tried** I tried setting up lldap as the directory for Stalwart Mailserver so users can authenticate with their existing accounts **What's not working** Users logging in should be working, bit identifying an admin user does not. Unfortunately Stalwart does not support creating a filter for looking up group membership to identitfy admin users. Instead it is looking checking if a user has the `objectClass` `admin` or `administrator`. I know that I can add such an objectClass to the schema via lldap-cli, but then unfortunately all users have this objectClass which I do not want. Currently I am not sure if I miss something in the GraphQL-API and lldap-cli (I am running the version from the 18.04.2024) or if this is a current limitation of lldap. Thanks in advance :)
kerem 2026-02-27 08:16:37 +03:00
Author
Owner

@nitnelave commented on GitHub (Mar 21, 2024):

Unfortunately, that's a limitation of LLDAP. It's also fairly unusual to check for object classes to identify admins: object classes are usually tied with attributes (e.g. a "person" has a first name and last name, a "groupOfNames" has a group id and members). Unless they use a specific attribute that admins have but not other users, it's weird to look for an object class.

Anyway, to answer your question: that's not something LLDAP will support. I recommend asking them to implement an admin filter (which can default to checking the object class if they want).

<!-- gh-comment-id:2011388594 --> @nitnelave commented on GitHub (Mar 21, 2024): Unfortunately, that's a limitation of LLDAP. It's also fairly unusual to check for object classes to identify admins: object classes are usually tied with attributes (e.g. a "person" has a first name and last name, a "groupOfNames" has a group id and members). Unless they use a specific attribute that admins have but not other users, it's weird to look for an object class. Anyway, to answer your question: that's not something LLDAP will support. I recommend asking them to implement an admin filter (which can default to checking the object class if they want).
Author
Owner

@dereulenspiegel commented on GitHub (Mar 21, 2024):

Thanks for the quick response. I digged deeper through the documentation of stalwart. And it is possible to map the type attribute to a custom attribute and fill this with the a string like admin. That seems to get me authenticated as the admin user, but I ran into other issues. So it will probably be some time until this integration works, but I will report back.

<!-- gh-comment-id:2012682490 --> @dereulenspiegel commented on GitHub (Mar 21, 2024): Thanks for the quick response. I digged deeper through the documentation of stalwart. And it is possible to map the `type` attribute to a custom attribute and fill this with the a string like `admin`. That seems to get me authenticated as the admin user, but I ran into other issues. So it will probably be some time until this integration works, but I will report back.
Author
Owner

@kri164 commented on GitHub (Apr 12, 2024):

@dereulenspiegel can you show your stalwart-mail's config as a example, please?

<!-- gh-comment-id:2051162066 --> @kri164 commented on GitHub (Apr 12, 2024): @dereulenspiegel can you show your stalwart-mail's config as a example, please?
Author
Owner

@ThatFave commented on GitHub (Aug 19, 2024):

I'd also like to know what you have configured

<!-- gh-comment-id:2297392883 --> @ThatFave commented on GitHub (Aug 19, 2024): I'd also like to know what you have configured
Author
Owner

@nitnelave commented on GitHub (Aug 19, 2024):

Re-opening since we don't have an example config.

<!-- gh-comment-id:2297416951 --> @nitnelave commented on GitHub (Aug 19, 2024): Re-opening since we don't have an example config.
Author
Owner

@ThatFave commented on GitHub (Nov 26, 2024):

I have a running config:

in lldap, add the following to the user schema:

Attribute name Type Editable Visible
description String [x] [x]
diskQuota Integer [x] [x]
mailAlias List [x] [x]
mailList List [x] [x]
userPassword String [x] [x]

to a user you want to be able to log in with, add anything to the userpassword field, I used 'test'

in Stalwart config.toml:

directory.lldap.attributes.class = "objectClass"
directory.lldap.attributes.description = "description"
directory.lldap.attributes.email = "mail"
directory.lldap.attributes.email-alias = "mailAlias"
directory.lldap.attributes.groups = "memberOf"
directory.lldap.attributes.name = "uid"
directory.lldap.attributes.quota = "diskQuota"
directory.lldap.attributes.secret = "userPassword"
directory.lldap.base-dn = "dc=example,dc=com"
directory.lldap.bind.auth.dn = "cn=?,ou=people,dc=example,dc=com"
directory.lldap.bind.auth.enable = true
directory.lldap.bind.auth.search = true
directory.lldap.bind.dn = "uid=ro_admin,ou=people,dc=example,dc=com"
directory.lldap.bind.secret = "SUPERSECRET"
directory.lldap.cache.entries = 500
directory.lldap.cache.ttl.negative = "10m"
directory.lldap.cache.ttl.positive = "1h"
directory.lldap.filter.domains = "(&(objectClass=inetOrgPerson)(|(mail=*?*)(uid=*?*)))"
directory.lldap.filter.email = "(&(objectClass=inetOrgPerson)(|(mail=?)(uid=?)))"
directory.lldap.filter.expand = "(&(objectClass=inetOrgPerson)(uid=?))"
directory.lldap.filter.name = "(&(objectClass=inetOrgPerson)(mail=?@*))"
directory.lldap.filter.verify = "(&(objectClass=inetOrgPerson)(mailList=?))"
directory.lldap.timeout = "15s"
directory.lldap.tls.allow-invalid-certs = false
directory.lldap.tls.enable = false
directory.lldap.type = "ldap"
directory.lldap.url = "ldap://docker:3890"

I don't know if quota, alias or lists work, will investigate.

<!-- gh-comment-id:2499640419 --> @ThatFave commented on GitHub (Nov 26, 2024): I have a running config: in lldap, add the following to the user schema: | Attribute name | Type | Editable | Visible | |--------|--------|--------|--------| | description | String | [x] | [x] | | diskQuota | Integer | [x] | [x] | | mailAlias | List<String> | [x] | [x] | | mailList | List<String> | [x] | [x] | | userPassword | String | [x] | [x] | to a user you want to be able to log in with, add anything to the userpassword field, I used 'test' in Stalwart config.toml: ``` directory.lldap.attributes.class = "objectClass" directory.lldap.attributes.description = "description" directory.lldap.attributes.email = "mail" directory.lldap.attributes.email-alias = "mailAlias" directory.lldap.attributes.groups = "memberOf" directory.lldap.attributes.name = "uid" directory.lldap.attributes.quota = "diskQuota" directory.lldap.attributes.secret = "userPassword" directory.lldap.base-dn = "dc=example,dc=com" directory.lldap.bind.auth.dn = "cn=?,ou=people,dc=example,dc=com" directory.lldap.bind.auth.enable = true directory.lldap.bind.auth.search = true directory.lldap.bind.dn = "uid=ro_admin,ou=people,dc=example,dc=com" directory.lldap.bind.secret = "SUPERSECRET" directory.lldap.cache.entries = 500 directory.lldap.cache.ttl.negative = "10m" directory.lldap.cache.ttl.positive = "1h" directory.lldap.filter.domains = "(&(objectClass=inetOrgPerson)(|(mail=*?*)(uid=*?*)))" directory.lldap.filter.email = "(&(objectClass=inetOrgPerson)(|(mail=?)(uid=?)))" directory.lldap.filter.expand = "(&(objectClass=inetOrgPerson)(uid=?))" directory.lldap.filter.name = "(&(objectClass=inetOrgPerson)(mail=?@*))" directory.lldap.filter.verify = "(&(objectClass=inetOrgPerson)(mailList=?))" directory.lldap.timeout = "15s" directory.lldap.tls.allow-invalid-certs = false directory.lldap.tls.enable = false directory.lldap.type = "ldap" directory.lldap.url = "ldap://docker:3890" ``` I don't know if quota, alias or lists work, will investigate.
Author
Owner

@nitnelave commented on GitHub (Nov 26, 2024):

@ThatFave I'm worried about the userPassword field: won't that take precedence over logging in via LDAP ? I don't want to have 2 sources of truth for passwords, especially not if it's a cleartext password.

If Stalwart requires userPassword, I cannot recommend using LLDAP with it.

<!-- gh-comment-id:2499976490 --> @nitnelave commented on GitHub (Nov 26, 2024): @ThatFave I'm worried about the userPassword field: won't that take precedence over logging in via LDAP ? I don't want to have 2 sources of truth for passwords, especially not if it's a cleartext password. If Stalwart requires userPassword, I cannot recommend using LLDAP with it.
Author
Owner

@ThatFave commented on GitHub (Nov 26, 2024):

I see your concern, but that does not seem to be the case. In my testing, it looks like Stalwart needs a dummy value there when logging in, or else the login fails with ERROR Authentication error (auth.error) listenerId = "http", localPort = 8080, remoteIp = 10.20.0.101, remotePort = 58325, details = Authentication error (auth.error) { details = Account does not contain secrets, causedBy = crates/common/src/auth/oauth/token.rs:239, causedBy = crates/common/src/auth/oauth/token.rs:53 }, causedBy = crates/jmap/src/auth/oauth/token.rs:123. This does not mean the password which is used to log in is actually the dummy value, rather this should be the reason: https://stalw.art/docs/auth/backend/ldap/#limitations-regarding-password-hashes (?)

<!-- gh-comment-id:2500793661 --> @ThatFave commented on GitHub (Nov 26, 2024): I see your concern, but that does not seem to be the case. In my testing, it looks like Stalwart needs a dummy value there when logging in, or else the login fails with `ERROR Authentication error (auth.error) listenerId = "http", localPort = 8080, remoteIp = 10.20.0.101, remotePort = 58325, details = Authentication error (auth.error) { details = Account does not contain secrets, causedBy = crates/common/src/auth/oauth/token.rs:239, causedBy = crates/common/src/auth/oauth/token.rs:53 }, causedBy = crates/jmap/src/auth/oauth/token.rs:123`. This does not mean the password which is used to log in is actually the dummy value, rather this should be the reason: https://stalw.art/docs/auth/backend/ldap/#limitations-regarding-password-hashes (?)
Author
Owner

@nitnelave commented on GitHub (Nov 26, 2024):

I see. Then there's no point in calling it "userPassword", we can call it "stalwartDummySecret", no? Since it's configurable.

<!-- gh-comment-id:2500833771 --> @nitnelave commented on GitHub (Nov 26, 2024): I see. Then there's no point in calling it "userPassword", we can call it "stalwartDummySecret", no? Since it's configurable.
Author
Owner

@ThatFave commented on GitHub (Nov 26, 2024):

I guess so, I just went with the default.

<!-- gh-comment-id:2500866819 --> @ThatFave commented on GitHub (Nov 26, 2024): I guess so, I just went with the default.
Author
Owner

@choucavalier commented on GitHub (Jan 15, 2025):

@ThatFave thx for sharing your config. it got me further. however it seems that mailAlias does not work. did you manage to get it to work? i think the lookup query needs to be adapted so that an account can be looked up based on the mailAlias. right now it says "account not found"

<!-- gh-comment-id:2592799747 --> @choucavalier commented on GitHub (Jan 15, 2025): @ThatFave thx for sharing your config. it got me further. however it seems that mailAlias does not work. did you manage to get it to work? i think the lookup query needs to be adapted so that an account can be looked up based on the mailAlias. right now it says "account not found"
Author
Owner

@nitnelave commented on GitHub (Jan 15, 2025):

@choucavalier a known limitation of LLDAP is that you can't search in attribute lists (or have equality filters). The common workaround for that is to have a fixed number of single value aliases: mailAlias1, mailAlias2 and so on.

Wildcards are also not supported on custom attributes, so no "*@mydomain.com"

<!-- gh-comment-id:2592836288 --> @nitnelave commented on GitHub (Jan 15, 2025): @choucavalier a known limitation of LLDAP is that you can't search in attribute lists (or have equality filters). The common workaround for that is to have a fixed number of single value aliases: mailAlias1, mailAlias2 and so on. Wildcards are also not supported on custom attributes, so no "*@mydomain.com"
Author
Owner

@ThatFave commented on GitHub (Jan 15, 2025):

@choucavalier no problem! I did not try to use mailAlias yet, so I don't know. Maybe a workaround is possible, but as @nitnelave put it, I don't think there is an easy way. kind regards

<!-- gh-comment-id:2593098795 --> @ThatFave commented on GitHub (Jan 15, 2025): @choucavalier no problem! I did not try to use mailAlias yet, so I don't know. Maybe a workaround is possible, but as @nitnelave put it, I don't think there is an easy way. kind regards
Author
Owner

@nitnelave commented on GitHub (Feb 22, 2025):

There's now an example config for stalwart!

<!-- gh-comment-id:2676390058 --> @nitnelave commented on GitHub (Feb 22, 2025): There's now an example config for stalwart!
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/lldap-lldap#319
No description provided.