Stalwart doesn't assign users to groups with LDAP directory

Issue Description

I am having an issue concerning group membership: when a user is logged in Stalwart fetches all the attributes, including “memberOf” listing their memberships (the logs state that), the groups tab shows those groups but they has no members, is there a way i can retrieve and sync memberships also? it would drastically simplify mailing administration.

Method for authentication is “Bind authentication after lookup” (that is what works at least at my setup)
Filter Name is (&(|(objectClass=user)(objectClass=group))(sAMAccountName=?))
Filter Email is (&(|(objectClass=user)(objectClass=group))(|(mail=?)(mailAlias=?)))

Groups attribute is set to “memberOf”

Is there anything else that could help targeting the issue?

Stalwart Version

v0.15.x

Installation Method

Binary (Linux)

Database Backend

PostgreSQL

Blob Storage

PostgreSQL

Search Engine

PostgreSQL

Directory Backend

LDAP

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The core issue is that Stalwart shows the groups a user belongs to (via memberOf on the user entry), but the groups themselves show no members because Stalwart needs to be able to look up group entries and identify their groupClass. There are two related problems to check in your Active Directory setup:

1. groupClass mismatch

By default Stalwart identifies LDAP entries as groups when their objectClass is groupOfNames. Active Directory uses group instead. You need to set groupClass to "group" in your Directory object so that Stalwart recognises AD group entries correctly.

2. Your filterLogin covers both users and groups but may not resolve group entries correctly

Your current filter (&(|(objectClass=user)(objectClass=group))(sAMAccountName=?)) is fine for lookup, but Stalwart needs to be able to fetch a group entry and know it is a group via the groupClass setting above.

3. filterMemberOf for reverse lookup

When Stalwart needs to know which groups a user belongs to, it can use either:

  • attrMemberOf – reads the memberOf attribute directly off the user entry (your current approach, default ["memberOf"]), OR
  • filterMemberOf – performs a separate LDAP search for groups that contain the user as a member. Default: (&(objectClass=groupOfNames)(member=?)).

For Active Directory, if you want to go via a reverse search, you’d set filterMemberOf to something like:

(&(objectClass=group)(member=?))

where ? is replaced with the user’s DN at runtime.

Summary of recommended changes to your Directory object:

{
  "@type": "Ldap",
  "groupClass": "group",
  "filterMemberOf": "(&(objectClass=group)(member=?))"
}

Set groupClass to "group" (matching AD’s object class) and optionally configure filterMemberOf with the AD-appropriate filter so Stalwart can also look up group membership from the group side.

Relevant docs:

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Group membership in Stalwart is stored per user and resolved lazily: it’s populated when each user authenticates or is reconciled, and there’s no bulk “pull all members of this group” sync from LDAP. So a group can exist (synced via someone’s memberOf) while its member list looks empty until the individual users have logged in. The list fills in over time.

One thing to check with AD specifically: when Stalwart resolves a memberOf group DN it replaces it with the group’s email from the attrEmail attribute (default mail), and AD group objects often don’t have a mail attribute, in which case the link is dropped. Confirm your group objects carry whatever you’ve set attrEmail to. See stalw.art/docs/auth/backend/ldap.

Exactly that happens - when user logs in the list of groups they are the member of are populated and shown in Groups tab but neither the logged user itself or other users that are members of the same groups has that group membership it has 0 members any way.

It has no difference if a group has different type Universal or Security group or has it or doesn’t have a mail address set.
The interesting part is that many of these groups cannot be edited or deleted giving an error “GROUP_NAME was not found”.
The mail address is not populated in those added groups however.

When logged in as test user the trace log finds user memberships:
2026-06-18T12:44:05Z TRACE LDAP query executed (store.ldap-query) details = “(&(|(objectClass=user)(objectClass=group))(sAMAccountName=check))”, result = [[“objectClass”, “top, person, organizationalPerson, us
er”], [“sAMAccountName”, “check”], [“memberOf”, “CN=SOME_GROUP_ONE,OU=PATH,OU=PATH,OU=PATH,DC=domain,DC=local, CN=SOME_GROUP_TWO,OU=PATH,OU=PATH,OU=PATH
,DC=domain,DC=local”], [“pwdLastSet”, “134262466972912005”], [“mail”, “[email protected]”], “CN=check,OU=TEST,OU=Users,OU=Accounts,OU=PATH,DC=domain,DC=local”]

The LDAP directory has the following attributes set:
directory.ldap.attributes.class = “objectClass”
directory.ldap.attributes.description = “displayName”
directory.ldap.attributes.email = “mail”
directory.ldap.attributes.groups = “memberOf”
directory.ldap.attributes.name = “sAMAccountName”
directory.ldap.attributes.quota = “diskQuota”
directory.ldap.attributes.secret-changed = “pwdLastSet”
directory.ldap.base-dn = “ou=PATH,dc=domain,dc=local”
directory.ldap.bind.auth.method = “lookup”
directory.ldap.bind.dn = “cn=ldap_binder,ou=PATH,ou=PATH,ou=PATH,ou=PATH,dc=domain,dc=local”
directory.ldap.bind.secret = “PASSWORD”
directory.ldap.cache.size = 157286400
directory.ldap.cache.ttl.negative = “1h”
directory.ldap.cache.ttl.positive = “8h”
directory.ldap.filter.email = “(&(|(objectClass=user)(objectClass=group))(|(mail=?)))”
directory.ldap.filter.name = “(&(|(objectClass=user)(objectClass=group))(sAMAccountName=?))”
directory.ldap.timeout = “15s”
directory.ldap.tls.allow-invalid-certs = false
directory.ldap.tls.enable = false
directory.ldap.type = “ldap”
directory.ldap.url = “ldap://AD_DC_IP:389”

Directory path are correct and point to OUs that contains appropriate objects.
We’re unfortunately still running AD DC Windows Server 2008 R2, could the problem be in LDAP connection/attributes details in our outdated schema?
I hope the provided info would be helpful somehow at least, thank you.