This is an automated email from the ASF dual-hosted git repository.

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new 2f53109f0d JAMES-4215 Allow customization of kerberos principal -> 
username mapping
2f53109f0d is described below

commit 2f53109f0d925591c88bd0f61536a238cf767b40
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Sep 2 11:37:28 2026 +0200

    JAMES-4215 Allow customization of kerberos principal -> username mapping
---
 .../servers/partials/configure/kerberos.adoc       |  54 +++++-
 protocols/sasl-kerberos/pom.xml                    |   4 +
 .../sasl/kerberos/CanonicalKerberosPrincipal.java  |  62 ++++++
 .../kerberos/GssapiAuthorizeCallbackHandler.java   | 103 ++++++----
 .../sasl/kerberos/GssapiSaslConfiguration.java     |   5 +-
 .../sasl/kerberos/GssapiSaslExchange.java          |   2 +-
 .../protocols/sasl/kerberos/RealmMapping.java      | 118 ++++++++++++
 .../kerberos/CanonicalKerberosPrincipalTest.java   | 102 ++++++++++
 .../GssapiAuthorizeCallbackHandlerTest.java        |  61 +++++-
 .../sasl/kerberos/GssapiJdkInteropTest.java        |  56 +++++-
 .../sasl/kerberos/GssapiSaslConfigurationTest.java |   2 +-
 .../sasl/kerberos/GssapiSaslMechanismTest.java     |  27 ++-
 .../sasl/kerberos/KeyTabPrincipalVerifierTest.java |   6 +-
 .../protocols/sasl/kerberos/RealmMappingTest.java  | 213 +++++++++++++++++++++
 .../SaslGssapiRealmMappingIntegrationTest.java     | 146 ++++++++++++++
 .../resources/imapserver-gssapi-realm-mapping.xml  |  48 +++++
 16 files changed, 949 insertions(+), 60 deletions(-)

diff --git a/docs/modules/servers/partials/configure/kerberos.adoc 
b/docs/modules/servers/partials/configure/kerberos.adoc
index 5509e3c138..e884b20d53 100644
--- a/docs/modules/servers/partials/configure/kerberos.adoc
+++ b/docs/modules/servers/partials/configure/kerberos.adoc
@@ -73,6 +73,9 @@ The `auth.gssapi` properties are:
 | `keyTab`
 | Absolute path or `file:` URI of a readable keytab containing the acceptor 
principal.
 
+| `realmMapping`
+| Optional. Maps Kerberos realms onto mail domains, see <<_identity_mapping>>. 
Absent, the mail domain is the Kerberos realm.
+
 |===
 
 GSSAPI uses the top-level `auth.requireSSL` transport policy and defaults to 
requiring TLS when that property is absent.
@@ -80,14 +83,51 @@ SMTP also applies `auth.announce` and `auth.requireSSL` 
when advertising authent
 Kerberos tokens can exceed SMTP's default 8,192-byte line limit; set 
`maxLineLength` to a measured operational value when needed.
 The value applies globally to SMTP input and must remain bounded.
 
-== Identity And Security
+== Identity Mapping
+
+Because James usernames are case-insensitive while Kerberos principals are 
case-sensitive, James accepts only canonical
+client principals: the identity must contain only ASCII characters, principal 
components must be lower case, and the
+realm must be upper case, for example `[email protected]`.
+Noncanonical variants such as `[email protected]` and `[email protected]` are 
rejected so that distinct Kerberos
+principals cannot map to the same James identity.
+
+By default the James username is the Kerberos principal itself, so the mail 
domain is the Kerberos realm:
+`[email protected]` authenticates as `[email protected]`, and `example.com` 
has to be one of the James domains.
+
+Deployments whose Kerberos realm is not their mail domain declare the 
correspondence explicitly:
+
+[source,xml]
+....
+<gssapi>
+    <serviceName>imap</serviceName>
+    <serverName>mail.example.com</serverName>
+    <principal>imap/[email protected]</principal>
+    <keyTab>/run/secrets/james/imap.keytab</keyTab>
+    <realmMapping>
+        <realm name="CORP.EXAMPLE.COM" domain="example.com"/>
+        <realm name="SUB.EXAMPLE.COM" domain="sub.example.com"/>
+    </realmMapping>
+</gssapi>
+....
+
+`[email protected]` then authenticates as `[email protected]`.
+Realm names are written as Kerberos spells them, in upper case, and the 
mapping is exhaustive: a principal whose realm
+is not listed has no James identity and is rejected.
+Two realms may not share a mail domain, as that would let the principals of 
one realm authenticate as those of the
+other.
+
+Only the realm is rewritten: the principal name components become the local 
part of the James username as they are.
+A deployment whose Kerberos account names differ from its mail local parts 
provisions James accounts named after the
+Kerberos names and declares the mail addresses as aliases, as it would for any 
other login.
+
+The resulting James username is lower case; provision that username and domain 
when the protocol needs a local
+account, notably for IMAP mailbox access.
+
+When a client requests a SASL authorization identity, a canonically spelled 
Kerberos principal goes through the realm
+mapping as well, and any other value is read as a James username.
+James then applies its normal authorization rules to the result.
 
-The authenticated Kerberos principal is mapped to a James username, including 
its realm.
-Because James usernames are case-insensitive while Kerberos principals are 
case-sensitive, James accepts only canonical client principals:
-the identity must contain only ASCII characters, principal components must be 
lower case, and the realm must be upper case, for example `[email protected]`.
-Noncanonical variants such as `[email protected]` and `[email protected]` are 
rejected so that distinct Kerberos principals cannot map to the same James 
identity.
-The resulting James username is lower case; provision that username and domain 
when the protocol needs a local account, notably for IMAP mailbox access.
-When a client requests a different SASL authorization identity, James applies 
its normal authorization rules.
+== Security
 
 The implementation accepts only the SASL `auth` quality of protection.
 It does not negotiate a SASL integrity or confidentiality layer; use TLS to 
protect protocol traffic.
diff --git a/protocols/sasl-kerberos/pom.xml b/protocols/sasl-kerberos/pom.xml
index 6217464232..d9e987a972 100644
--- a/protocols/sasl-kerberos/pom.xml
+++ b/protocols/sasl-kerberos/pom.xml
@@ -42,6 +42,10 @@
             <groupId>${james.protocols.groupId}</groupId>
             <artifactId>protocols-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-configuration2</artifactId>
diff --git 
a/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/CanonicalKerberosPrincipal.java
 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/CanonicalKerberosPrincipal.java
new file mode 100644
index 0000000000..973341746f
--- /dev/null
+++ 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/CanonicalKerberosPrincipal.java
@@ -0,0 +1,62 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.sasl.kerberos;
+
+import java.util.Locale;
+
+import javax.security.auth.kerberos.KerberosPrincipal;
+
+record CanonicalKerberosPrincipal(String components, String realm) {
+    static CanonicalKerberosPrincipal parse(String authenticationId) {
+        if (authenticationId == null || 
authenticationId.chars().anyMatch(character -> character > 0x7F)) {
+            throw new IllegalArgumentException("GSSAPI authentication identity 
must contain only ASCII characters");
+        }
+
+        KerberosPrincipal principal = new KerberosPrincipal(authenticationId);
+        String realm = principal.getRealm();
+        String name = principal.getName();
+        CanonicalKerberosPrincipal parsed = new 
CanonicalKerberosPrincipal(name.substring(0, name.length() - realm.length() - 
1), realm);
+
+        // The principal is respelled by the parser: reject anything the 
parser did not read back verbatim.
+        if (!authenticationId.equals(parsed.asString())) {
+            throw new IllegalArgumentException("GSSAPI authentication identity 
is not canonical");
+        }
+        return parsed;
+    }
+
+    CanonicalKerberosPrincipal {
+        if (components == null || components.isEmpty() || 
components.indexOf('@') >= 0) {
+            throw new IllegalArgumentException("Kerberos principal components 
must not be empty nor contain a realm separator");
+        }
+        if (realm == null || realm.isEmpty()) {
+            throw new IllegalArgumentException("Kerberos realm must not be 
empty");
+        }
+        if (!components.equals(components.toLowerCase(Locale.ROOT))) {
+            throw new IllegalArgumentException("Kerberos principal components 
must be lower case");
+        }
+        if (!realm.equals(realm.toUpperCase(Locale.ROOT))) {
+            throw new IllegalArgumentException("Kerberos realm must be upper 
case");
+        }
+    }
+
+    String asString() {
+        return components + "@" + realm;
+    }
+}
diff --git 
a/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiAuthorizeCallbackHandler.java
 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiAuthorizeCallbackHandler.java
index 92d0207760..48bb3cecdf 100644
--- 
a/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiAuthorizeCallbackHandler.java
+++ 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiAuthorizeCallbackHandler.java
@@ -20,13 +20,11 @@
 package org.apache.james.protocols.sasl.kerberos;
 
 import java.io.IOException;
-import java.util.Locale;
 import java.util.Optional;
 
 import javax.security.auth.callback.Callback;
 import javax.security.auth.callback.CallbackHandler;
 import javax.security.auth.callback.UnsupportedCallbackException;
-import javax.security.auth.kerberos.KerberosPrincipal;
 import javax.security.sasl.AuthorizeCallback;
 
 import org.apache.james.core.Username;
@@ -36,35 +34,28 @@ import org.apache.james.protocols.api.sasl.SaslFailure;
 import org.apache.james.protocols.api.sasl.SaslIdentity;
 
 class GssapiAuthorizeCallbackHandler implements CallbackHandler {
-    private static Username canonicalAuthenticationId(String authenticationId) 
{
-        if (authenticationId == null || 
authenticationId.chars().anyMatch(character -> character > 0x7F)) {
-            throw new IllegalArgumentException("GSSAPI authentication identity 
must contain only ASCII characters");
-        }
-
-        KerberosPrincipal principal = new KerberosPrincipal(authenticationId);
-        String realm = principal.getRealm();
-        String principalName = principal.getName();
-        String principalComponents = principalName.substring(0, 
principalName.length() - realm.length() - 1);
-        String canonicalPrincipal = 
principalComponents.toLowerCase(Locale.ROOT) + "@" + 
realm.toUpperCase(Locale.ROOT);
+    private static class UnresolvableIdentity extends RuntimeException {
+        private final SaslFailure failure;
 
-        // James usernames are case-insensitive, so accept one Kerberos 
spelling to prevent case-distinct principals from collapsing.
-        if (!authenticationId.equals(canonicalPrincipal)) {
-            throw new IllegalArgumentException("GSSAPI authentication identity 
is not canonical");
+        UnresolvableIdentity(SaslFailure failure) {
+            super(failure.reason(), null, false, false);
+            this.failure = failure;
         }
 
-        Username username = Username.of(canonicalPrincipal);
-        // Case folding is intentional; reject any additional normalization 
that could collapse distinct Kerberos principals.
-        if 
(!username.asString().equals(canonicalPrincipal.toLowerCase(Locale.US))) {
-            throw new IllegalArgumentException("GSSAPI authentication identity 
cannot be mapped without normalization");
+        SaslFailure failure() {
+            return failure;
         }
-        return username;
     }
 
+    private static final SaslFailure MALFORMED = 
SaslFailure.malformed("Malformed GSSAPI identity.");
+
     private final SaslAuthenticator authenticator;
+    private final RealmMapping realmMapping;
     private Optional<SaslAuthenticationResult> result;
 
-    GssapiAuthorizeCallbackHandler(SaslAuthenticator authenticator) {
+    GssapiAuthorizeCallbackHandler(SaslAuthenticator authenticator, 
RealmMapping realmMapping) {
         this.authenticator = authenticator;
+        this.realmMapping = realmMapping;
         this.result = Optional.empty();
     }
 
@@ -83,24 +74,66 @@ class GssapiAuthorizeCallbackHandler implements 
CallbackHandler {
     }
 
     private void authorize(AuthorizeCallback callback) {
+        SaslIdentity identity;
         try {
-            Username authenticationId = 
canonicalAuthenticationId(callback.getAuthenticationID());
-            Username authorizationId = 
Optional.ofNullable(callback.getAuthorizationID())
+            Username authenticationId = 
resolve(canonicalPrincipal(callback.getAuthenticationID()));
+            identity = new SaslIdentity(authenticationId, 
Optional.ofNullable(callback.getAuthorizationID())
                 .filter(value -> !value.isEmpty())
-                .map(Username::of)
-                .orElse(authenticationId);
-
-            result = Optional.of(authenticator.authorize(new 
SaslIdentity(authenticationId, authorizationId)));
-            switch (result.orElseThrow()) {
-                case SaslAuthenticationResult.Success success -> {
-                    callback.setAuthorized(true);
-                    
callback.setAuthorizedID(success.identity().authorizationId().asString());
-                }
-                case SaslAuthenticationResult.Failure ignored -> 
callback.setAuthorized(false);
+                .map(this::authorizationId)
+                .orElse(authenticationId));
+        } catch (UnresolvableIdentity e) {
+            result = Optional.of(new 
SaslAuthenticationResult.Failure(e.failure()));
+            callback.setAuthorized(false);
+            return;
+        }
+
+        result = Optional.of(authenticator.authorize(identity));
+        switch (result.orElseThrow()) {
+            case SaslAuthenticationResult.Success success -> {
+                callback.setAuthorized(true);
+                
callback.setAuthorizedID(success.identity().authorizationId().asString());
             }
+            case SaslAuthenticationResult.Failure ignored -> 
callback.setAuthorized(false);
+        }
+    }
+
+    /**
+     * GSSAPI implementations echo the client principal when the client 
requests no specific authorization identity, so
+     * a canonically spelled Kerberos principal goes through the realm mapping 
like the authentication identity. Anything
+     * else is the James username the client asks to act as, and the 
authenticator applies the usual delegation rules.
+     */
+    private Username authorizationId(String authorizationId) {
+        Optional<CanonicalKerberosPrincipal> principal = 
principal(authorizationId);
+        if (principal.isPresent()) {
+            return resolve(principal.get());
+        }
+        try {
+            return Username.of(authorizationId);
         } catch (IllegalArgumentException e) {
-            result = Optional.of(new 
SaslAuthenticationResult.Failure(SaslFailure.malformed("Malformed GSSAPI 
identity.")));
-            callback.setAuthorized(false);
+            throw new UnresolvableIdentity(MALFORMED);
+        }
+    }
+
+    private CanonicalKerberosPrincipal canonicalPrincipal(String 
authenticationId) {
+        return principal(authenticationId).orElseThrow(() -> new 
UnresolvableIdentity(MALFORMED));
+    }
+
+    private Optional<CanonicalKerberosPrincipal> principal(String value) {
+        try {
+            return Optional.of(CanonicalKerberosPrincipal.parse(value));
+        } catch (IllegalArgumentException e) {
+            return Optional.empty();
+        }
+    }
+
+    private Username resolve(CanonicalKerberosPrincipal principal) {
+        Optional<Username> username;
+        try {
+            username = realmMapping.resolve(principal);
+        } catch (IllegalArgumentException e) {
+            throw new UnresolvableIdentity(MALFORMED);
         }
+        return username.orElseThrow(() -> new 
UnresolvableIdentity(SaslFailure.authenticationFailed(
+            Optional.empty(), Optional.empty(), "GSSAPI principal is not 
mapped to a James identity.")));
     }
 }
diff --git 
a/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslConfiguration.java
 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslConfiguration.java
index aed55a8a73..f5ea97994e 100644
--- 
a/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslConfiguration.java
+++ 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslConfiguration.java
@@ -33,7 +33,8 @@ public record GssapiSaslConfiguration(String serviceName,
                                       String serverName,
                                       String principal,
                                       Path keyTab,
-                                      boolean requireSSL) {
+                                      boolean requireSSL,
+                                      RealmMapping realmMapping) {
     private static final String CONFIGURATION_PATH = "auth.gssapi";
     private static final boolean REQUIRE_SSL_DEFAULT = true;
 
@@ -50,7 +51,7 @@ public record GssapiSaslConfiguration(String serviceName,
         validatePrincipal(serviceName, serverName, principal);
 
         return new GssapiSaslConfiguration(serviceName, serverName, principal, 
keyTab,
-            serverConfiguration.getBoolean("auth.requireSSL", 
REQUIRE_SSL_DEFAULT));
+            serverConfiguration.getBoolean("auth.requireSSL", 
REQUIRE_SSL_DEFAULT), RealmMapping.from(configuration));
     }
 
     private static String required(HierarchicalConfiguration<ImmutableNode> 
configuration, String property) throws ConfigurationException {
diff --git 
a/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslExchange.java
 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslExchange.java
index 648b263c65..02cf8f2b8b 100644
--- 
a/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslExchange.java
+++ 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslExchange.java
@@ -63,7 +63,7 @@ class GssapiSaslExchange implements SaslExchange {
         this.configuration = configuration;
         this.loginContextFactory = loginContextFactory;
         this.saslServerFactory = saslServerFactory;
-        this.callbackHandler = new 
GssapiAuthorizeCallbackHandler(authenticator);
+        this.callbackHandler = new 
GssapiAuthorizeCallbackHandler(authenticator, configuration.realmMapping());
         this.state = State.NEW;
     }
 
diff --git 
a/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/RealmMapping.java
 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/RealmMapping.java
new file mode 100644
index 0000000000..a6d05b5c80
--- /dev/null
+++ 
b/protocols/sasl-kerberos/src/main/java/org/apache/james/protocols/sasl/kerberos/RealmMapping.java
@@ -0,0 +1,118 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.sasl.kerberos;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+
+import com.google.common.collect.ImmutableMap;
+
+public record RealmMapping(Map<String, Domain> domainByRealm) {
+    public static final RealmMapping REALM_AS_DOMAIN = new 
RealmMapping(Map.of());
+
+    private static final String CONFIGURATION_PATH = "realmMapping";
+
+    static RealmMapping from(HierarchicalConfiguration<ImmutableNode> 
gssapiConfiguration) throws ConfigurationException {
+        if 
(gssapiConfiguration.immutableConfigurationsAt(CONFIGURATION_PATH).isEmpty()) {
+            return REALM_AS_DOMAIN;
+        }
+
+        List<HierarchicalConfiguration<ImmutableNode>> realms = 
gssapiConfiguration.configurationsAt(CONFIGURATION_PATH + ".realm");
+        if (realms.isEmpty()) {
+            throw new ConfigurationException("auth.gssapi.realmMapping must 
declare at least one realm");
+        }
+
+        Map<String, Domain> domainByRealm = new LinkedHashMap<>();
+        for (HierarchicalConfiguration<ImmutableNode> realm : realms) {
+            String name = name(realm);
+            if (domainByRealm.put(name, domain(realm, name)) != null) {
+                throw new ConfigurationException("auth.gssapi.realmMapping 
declares realm " + name + " twice");
+            }
+        }
+
+        try {
+            return new RealmMapping(domainByRealm);
+        } catch (IllegalArgumentException e) {
+            throw new ConfigurationException("auth.gssapi.realmMapping is 
invalid", e);
+        }
+    }
+
+    private static String name(HierarchicalConfiguration<ImmutableNode> realm) 
throws ConfigurationException {
+        String name = realm.getString("[@name]");
+        if (name == null || name.isBlank()) {
+            throw new ConfigurationException("auth.gssapi.realmMapping.realm 
must carry a name attribute");
+        }
+        return name.trim();
+    }
+
+    private static Domain domain(HierarchicalConfiguration<ImmutableNode> 
realm, String name) throws ConfigurationException {
+        String domain = realm.getString("[@domain]");
+        if (domain == null || domain.isBlank()) {
+            throw new ConfigurationException("auth.gssapi.realmMapping.realm " 
+ name + " must carry a domain attribute");
+        }
+        try {
+            return Domain.of(domain.trim());
+        } catch (IllegalArgumentException e) {
+            throw new ConfigurationException("auth.gssapi.realmMapping.realm " 
+ name + " carries an invalid domain", e);
+        }
+    }
+
+    public RealmMapping {
+        Map<Domain, String> realmByDomain = new HashMap<>();
+        domainByRealm.forEach((realm, domain) -> {
+            if (!realm.equals(realm.toUpperCase(Locale.ROOT))) {
+                throw new IllegalArgumentException("Kerberos realm " + realm + 
" must be upper case");
+            }
+            String conflictingRealm = realmByDomain.put(domain, realm);
+            if (conflictingRealm != null) {
+                throw new IllegalArgumentException("Realms " + 
conflictingRealm + " and " + realm
+                    + " are both mapped onto domain " + domain.asString());
+            }
+        });
+        domainByRealm = ImmutableMap.copyOf(domainByRealm);
+    }
+
+    Optional<Username> resolve(CanonicalKerberosPrincipal principal) {
+        return domain(principal.realm())
+            .map(domain -> Username.of(principal.components() + "@" + 
domain.asString()));
+    }
+
+    private Optional<Domain> domain(String realm) {
+        if (domainByRealm.isEmpty()) {
+            Domain domain = Domain.of(realm);
+            // Case folding is intentional; reject any additional 
normalization that could collapse distinct realms.
+            if (!domain.asString().equals(realm.toLowerCase(Locale.US))) {
+                throw new IllegalArgumentException("Kerberos realm " + realm + 
" cannot be used as a mail domain without normalization");
+            }
+            return Optional.of(domain);
+        }
+        return Optional.ofNullable(domainByRealm.get(realm));
+    }
+}
diff --git 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/CanonicalKerberosPrincipalTest.java
 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/CanonicalKerberosPrincipalTest.java
new file mode 100644
index 0000000000..8476053d92
--- /dev/null
+++ 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/CanonicalKerberosPrincipalTest.java
@@ -0,0 +1,102 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.sasl.kerberos;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class CanonicalKerberosPrincipalTest {
+    @Test
+    void shouldParseCanonicalPrincipal() {
+        assertThat(CanonicalKerberosPrincipal.parse("[email protected]"))
+            .isEqualTo(new CanonicalKerberosPrincipal("alice", "EXAMPLE.COM"));
+    }
+
+    @Test
+    void shouldParseMultiComponentPrincipal() {
+        assertThat(CanonicalKerberosPrincipal.parse("alice/[email protected]"))
+            .isEqualTo(new CanonicalKerberosPrincipal("alice/admin", 
"EXAMPLE.COM"));
+    }
+
+    @Test
+    void asStringShouldRoundTripTheParsedIdentity() {
+        
assertThat(CanonicalKerberosPrincipal.parse("[email protected]").asString()).isEqualTo("[email protected]");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "",
+        "@EXAMPLE.COM",
+        "alicé@EXAMPLE.COM",
+        // U+212A KELVIN SIGN lower cases to an ASCII 'k', colliding with the 
[email protected] principal.
+        "alice@EXAMPLE.K"})
+    void shouldRejectNonCanonicalIdentities(String authenticationId) {
+        assertThatThrownBy(() -> 
CanonicalKerberosPrincipal.parse(authenticationId))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void shouldRejectNullIdentity() {
+        assertThatThrownBy(() -> CanonicalKerberosPrincipal.parse(null))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessage("GSSAPI authentication identity must contain only 
ASCII characters");
+    }
+
+    @Test
+    void constructorShouldRejectUpperCaseComponents() {
+        assertThatThrownBy(() -> new CanonicalKerberosPrincipal("Alice", 
"EXAMPLE.COM"))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessage("Kerberos principal components must be lower case");
+    }
+
+    @Test
+    void constructorShouldRejectLowerCaseRealm() {
+        assertThatThrownBy(() -> new CanonicalKerberosPrincipal("alice", 
"example.com"))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessage("Kerberos realm must be upper case");
+    }
+
+    @Test
+    void constructorShouldRejectComponentsCarryingARealm() {
+        assertThatThrownBy(() -> new 
CanonicalKerberosPrincipal("[email protected]", "EXAMPLE.COM"))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessage("Kerberos principal components must not be empty nor 
contain a realm separator");
+    }
+
+    @Test
+    void constructorShouldRejectEmptyRealm() {
+        assertThatThrownBy(() -> new CanonicalKerberosPrincipal("alice", ""))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessage("Kerberos realm must not be empty");
+    }
+
+    @Test
+    void constructorShouldAcceptCaselessComponents() {
+        assertThatCode(() -> new CanonicalKerberosPrincipal("alice.1", 
"EXAMPLE.COM")).doesNotThrowAnyException();
+    }
+}
diff --git 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiAuthorizeCallbackHandlerTest.java
 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiAuthorizeCallbackHandlerTest.java
index a6fe3721c5..4ca6b763f0 100644
--- 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiAuthorizeCallbackHandlerTest.java
+++ 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiAuthorizeCallbackHandlerTest.java
@@ -22,6 +22,7 @@ package org.apache.james.protocols.sasl.kerberos;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Stream;
@@ -30,6 +31,7 @@ import javax.security.auth.callback.NameCallback;
 import javax.security.auth.callback.UnsupportedCallbackException;
 import javax.security.sasl.AuthorizeCallback;
 
+import org.apache.james.core.Domain;
 import org.apache.james.core.Username;
 import org.apache.james.protocols.api.sasl.SaslAuthenticationResult;
 import org.apache.james.protocols.api.sasl.SaslAuthenticator;
@@ -50,7 +52,7 @@ class GssapiAuthorizeCallbackHandlerTest {
     @Test
     void shouldAuthorizeCanonicalSelfIdentity() throws Exception {
         AtomicReference<SaslIdentity> identity = new AtomicReference<>();
-        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity));
+        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity), 
RealmMapping.REALM_AS_DOMAIN);
         AuthorizeCallback callback = new 
AuthorizeCallback("[email protected]", "[email protected]");
 
         testee.handle(new AuthorizeCallback[] {callback});
@@ -64,7 +66,7 @@ class GssapiAuthorizeCallbackHandlerTest {
     @Test
     void shouldRejectNonCanonicalPrincipalComponentCase() throws Exception {
         AtomicReference<SaslIdentity> identity = new AtomicReference<>();
-        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity));
+        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity), 
RealmMapping.REALM_AS_DOMAIN);
         AuthorizeCallback callback = new 
AuthorizeCallback("[email protected]", "[email protected]");
 
         testee.handle(new AuthorizeCallback[] {callback});
@@ -79,7 +81,7 @@ class GssapiAuthorizeCallbackHandlerTest {
     @Test
     void shouldRejectNonCanonicalRealmCase() throws Exception {
         AtomicReference<SaslIdentity> identity = new AtomicReference<>();
-        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity));
+        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity), 
RealmMapping.REALM_AS_DOMAIN);
         AuthorizeCallback callback = new 
AuthorizeCallback("[email protected]", "[email protected]");
 
         testee.handle(new AuthorizeCallback[] {callback});
@@ -95,7 +97,7 @@ class GssapiAuthorizeCallbackHandlerTest {
     @MethodSource("normalizationCollisions")
     void shouldRejectAuthenticationIdentityNormalizationCollisions(String 
authenticationId, String collidingCanonicalIdentity) throws Exception {
         AtomicReference<SaslIdentity> identity = new AtomicReference<>();
-        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity));
+        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity), 
RealmMapping.REALM_AS_DOMAIN);
         AuthorizeCallback callback = new AuthorizeCallback(authenticationId, 
authenticationId);
 
         
assertThat(Username.of(authenticationId)).isEqualTo(Username.of(collidingCanonicalIdentity));
@@ -109,11 +111,56 @@ class GssapiAuthorizeCallbackHandlerTest {
                 actual -> 
assertThat(actual.failure().type()).isEqualTo(SaslFailure.Type.MALFORMED)));
     }
 
+    @Test
+    void shouldAuthorizeThroughTheConfiguredRealmMapping() throws Exception {
+        AtomicReference<SaslIdentity> identity = new AtomicReference<>();
+        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity),
+            new RealmMapping(Map.of("CORP.EXAMPLE.COM", 
Domain.of("example.com"))));
+        AuthorizeCallback callback = new 
AuthorizeCallback("[email protected]", "[email protected]");
+
+        testee.handle(new AuthorizeCallback[] {callback});
+
+        assertThat(identity.get()).isEqualTo(new 
SaslIdentity(Username.of("[email protected]"), 
Username.of("[email protected]")));
+        assertThat(callback.isAuthorized()).isTrue();
+        assertThat(callback.getAuthorizedID()).isEqualTo("[email protected]");
+    }
+
+    @Test
+    void shouldRejectPrincipalOfAnUnmappedRealm() throws Exception {
+        AtomicReference<SaslIdentity> identity = new AtomicReference<>();
+        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity),
+            new RealmMapping(Map.of("CORP.EXAMPLE.COM", 
Domain.of("example.com"))));
+        AuthorizeCallback callback = new 
AuthorizeCallback("[email protected]", "[email protected]");
+
+        testee.handle(new AuthorizeCallback[] {callback});
+
+        assertThat(identity.get()).isNull();
+        assertThat(callback.isAuthorized()).isFalse();
+        assertThat(testee.result()).hasValueSatisfying(result -> 
assertThat(result)
+            .isInstanceOfSatisfying(SaslAuthenticationResult.Failure.class,
+                actual -> 
assertThat(actual.failure().type()).isEqualTo(SaslFailure.Type.AUTHENTICATION_FAILED)));
+    }
+
+    @Test
+    void shouldRejectMalformedAuthorizationIdentity() throws Exception {
+        AtomicReference<SaslIdentity> identity = new AtomicReference<>();
+        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(authorizing(identity), 
RealmMapping.REALM_AS_DOMAIN);
+        AuthorizeCallback callback = new 
AuthorizeCallback("[email protected]", "bob@exa mple.com");
+
+        testee.handle(new AuthorizeCallback[] {callback});
+
+        assertThat(identity.get()).isNull();
+        assertThat(callback.isAuthorized()).isFalse();
+        assertThat(testee.result()).hasValueSatisfying(result -> 
assertThat(result)
+            .isInstanceOfSatisfying(SaslAuthenticationResult.Failure.class,
+                actual -> 
assertThat(actual.failure().type()).isEqualTo(SaslFailure.Type.MALFORMED)));
+    }
+
     @Test
     void shouldPreserveTypedAuthorizationFailure() throws Exception {
         SaslFailure failure = SaslFailure.delegationForbidden(
             Username.of("[email protected]"), Username.of("[email protected]"), 
"Delegation is forbidden.");
-        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(failing(failure));
+        GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(failing(failure), RealmMapping.REALM_AS_DOMAIN);
         AuthorizeCallback callback = new 
AuthorizeCallback("[email protected]", "[email protected]");
 
         testee.handle(new AuthorizeCallback[] {callback});
@@ -125,7 +172,7 @@ class GssapiAuthorizeCallbackHandlerTest {
     @Test
     void shouldRejectMalformedIdentity() throws Exception {
         GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(failing(
-            SaslFailure.authenticationFailed(Optional.empty(), 
Optional.empty(), "unused")));
+            SaslFailure.authenticationFailed(Optional.empty(), 
Optional.empty(), "unused")), RealmMapping.REALM_AS_DOMAIN);
         AuthorizeCallback callback = new AuthorizeCallback("", "");
 
         testee.handle(new AuthorizeCallback[] {callback});
@@ -139,7 +186,7 @@ class GssapiAuthorizeCallbackHandlerTest {
     @Test
     void shouldRejectUnexpectedCallback() {
         GssapiAuthorizeCallbackHandler testee = new 
GssapiAuthorizeCallbackHandler(failing(
-            SaslFailure.authenticationFailed(Optional.empty(), 
Optional.empty(), "unused")));
+            SaslFailure.authenticationFailed(Optional.empty(), 
Optional.empty(), "unused")), RealmMapping.REALM_AS_DOMAIN);
 
         assertThatThrownBy(() -> testee.handle(new NameCallback[] {new 
NameCallback("name")}))
             .isInstanceOf(UnsupportedCallbackException.class);
diff --git 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiJdkInteropTest.java
 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiJdkInteropTest.java
index 93c787a099..3033be37e1 100644
--- 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiJdkInteropTest.java
+++ 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiJdkInteropTest.java
@@ -23,12 +23,15 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.fail;
 
 import java.nio.file.Path;
+import java.util.Map;
 import java.util.Optional;
 
+import org.apache.james.core.Domain;
 import org.apache.james.core.Username;
 import org.apache.james.protocols.api.sasl.SaslAuthenticationResult;
 import org.apache.james.protocols.api.sasl.SaslAuthenticator;
 import org.apache.james.protocols.api.sasl.SaslExchange;
+import org.apache.james.protocols.api.sasl.SaslFailure;
 import org.apache.james.protocols.api.sasl.SaslIdentity;
 import org.apache.james.protocols.api.sasl.SaslInitialRequest;
 import org.apache.james.protocols.api.sasl.SaslMechanism;
@@ -48,10 +51,7 @@ class GssapiJdkInteropTest {
     void shouldInteroperateWithJdkGssapiClient() throws Exception {
         try (KerberosTestFixture kerberos = new 
KerberosTestFixture(temporaryDirectory)) {
             KerberosTestFixture.Service service = 
kerberos.provisionService("imap", SERVER_NAME);
-            GssapiSaslConfiguration configuration = new 
GssapiSaslConfiguration(
-                service.serviceName(), service.serverName(), 
service.principal(), service.keyTab(), true);
-            SaslMechanism mechanism = new GssapiSaslMechanism(
-                configuration, new KerberosLoginContextFactory(), new 
JdkSaslServerFactory());
+            SaslMechanism mechanism = mechanism(service, 
RealmMapping.REALM_AS_DOMAIN);
 
             try (GssapiTestClient client = kerberos.client(service);
                     SaslExchange exchange = mechanism.start(
@@ -69,6 +69,54 @@ class GssapiJdkInteropTest {
         }
     }
 
+    @Test
+    @ResourceLock(KerberosTestFixture.KRB5_CONFIGURATION_RESOURCE)
+    void shouldMapTheRealmOntoTheConfiguredDomain() throws Exception {
+        try (KerberosTestFixture kerberos = new 
KerberosTestFixture(temporaryDirectory)) {
+            KerberosTestFixture.Service service = 
kerberos.provisionService("imap", SERVER_NAME);
+            SaslMechanism mechanism = mechanism(service, new RealmMapping(
+                Map.of(KerberosTestFixture.REALM, Domain.of("example.com"))));
+
+            try (GssapiTestClient client = kerberos.client(service);
+                    SaslExchange exchange = mechanism.start(
+                        new SaslInitialRequest("GSSAPI", 
Optional.of(client.initialResponse())),
+                        allowingSelfAuthorization())) {
+                SaslStep result = completeExchange(exchange, client);
+
+                
assertThat(result).isInstanceOfSatisfying(SaslStep.Success.class, success -> {
+                    
assertThat(success.identity().authenticationId()).isEqualTo(Username.of("[email protected]"));
+                    
assertThat(success.identity().authorizationId()).isEqualTo(Username.of("[email protected]"));
+                });
+            }
+        }
+    }
+
+    @Test
+    @ResourceLock(KerberosTestFixture.KRB5_CONFIGURATION_RESOURCE)
+    void shouldRejectPrincipalsOfAnUnmappedRealm() throws Exception {
+        try (KerberosTestFixture kerberos = new 
KerberosTestFixture(temporaryDirectory)) {
+            KerberosTestFixture.Service service = 
kerberos.provisionService("imap", SERVER_NAME);
+            SaslMechanism mechanism = mechanism(service, new RealmMapping(
+                Map.of("OTHER.TEST", Domain.of("example.com"))));
+
+            try (GssapiTestClient client = kerberos.client(service);
+                    SaslExchange exchange = mechanism.start(
+                        new SaslInitialRequest("GSSAPI", 
Optional.of(client.initialResponse())),
+                        allowingSelfAuthorization())) {
+                SaslStep result = completeExchange(exchange, client);
+
+                
assertThat(result).isInstanceOfSatisfying(SaslStep.Failure.class,
+                    failure -> 
assertThat(failure.failure().type()).isEqualTo(SaslFailure.Type.AUTHENTICATION_FAILED));
+            }
+        }
+    }
+
+    private SaslMechanism mechanism(KerberosTestFixture.Service service, 
RealmMapping realmMapping) {
+        GssapiSaslConfiguration configuration = new GssapiSaslConfiguration(
+            service.serviceName(), service.serverName(), service.principal(), 
service.keyTab(), true, realmMapping);
+        return new GssapiSaslMechanism(configuration, new 
KerberosLoginContextFactory(), new JdkSaslServerFactory());
+    }
+
     private SaslStep completeExchange(SaslExchange exchange, GssapiTestClient 
client) throws Exception {
         SaslStep step = exchange.firstStep();
         for (int round = 0; round < 10 && step instanceof SaslStep.Challenge 
challenge; round++) {
diff --git 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslConfigurationTest.java
 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslConfigurationTest.java
index b359ce1ba6..77bebbc0c1 100644
--- 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslConfigurationTest.java
+++ 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslConfigurationTest.java
@@ -52,7 +52,7 @@ class GssapiSaslConfigurationTest {
         GssapiSaslConfiguration configuration = 
GssapiSaslConfiguration.from(configuration(keyTab.toString()));
 
         assertThat(configuration).isEqualTo(new GssapiSaslConfiguration(
-            SERVICE_NAME, SERVER_NAME, PRINCIPAL, keyTab.toAbsolutePath(), 
true));
+            SERVICE_NAME, SERVER_NAME, PRINCIPAL, keyTab.toAbsolutePath(), 
true, RealmMapping.REALM_AS_DOMAIN));
     }
 
     @Test
diff --git 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslMechanismTest.java
 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslMechanismTest.java
index b2438ac13a..d8fff685db 100644
--- 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslMechanismTest.java
+++ 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/GssapiSaslMechanismTest.java
@@ -249,6 +249,30 @@ class GssapiSaslMechanismTest {
         verify(loginContextFactory, never()).login(any());
     }
 
+    @Test
+    void factoryShouldResolveIdentitiesThroughTheConfiguredRealmMapping() 
throws Exception {
+        Path keyTab = 
Files.createFile(temporaryDirectory.resolve("imap.keytab"));
+        KerberosLoginContextFactory loginContextFactory = 
mock(KerberosLoginContextFactory.class);
+        loginContext(loginContextFactory);
+        SaslServer saslServer = mock(SaslServer.class);
+        AtomicReference<CallbackHandler> callbackHandler = new 
AtomicReference<>();
+        when(saslServer.evaluateResponse(any())).thenAnswer(invocation -> {
+            authorize(callbackHandler.get(), "[email protected]", 
"[email protected]");
+            return null;
+        });
+        when(saslServer.isComplete()).thenReturn(true);
+        when(saslServer.getNegotiatedProperty(Sasl.QOP)).thenReturn("auth");
+        GssapiSaslMechanismFactory testee = new 
GssapiSaslMechanismFactory(mock(KeyTabPrincipalVerifier.class), 
loginContextFactory,
+            capturingFactory(saslServer, callbackHandler));
+        BaseHierarchicalConfiguration configuration = configuration(keyTab);
+        configuration.addProperty("auth.gssapi.realmMapping.realm(-1)[@name]", 
"CORP.EXAMPLE.COM");
+        configuration.addProperty("auth.gssapi.realmMapping.realm[@domain]", 
"example.com");
+
+        try (SaslExchange exchange = 
testee.create(configuration).start(request(Optional.of(INITIAL_TOKEN)), 
authenticating())) {
+            assertThat(exchange.firstStep()).isEqualTo(new 
SaslStep.Success(IDENTITY, Optional.empty()));
+        }
+    }
+
     private TestExchange completedExchange(byte[] output, String qop, 
SaslAuthenticator authenticator) throws Exception {
         KerberosLoginContextFactory loginContextFactory = 
mock(KerberosLoginContextFactory.class);
         KerberosLoginContext loginContext = loginContext(loginContextFactory);
@@ -311,7 +335,8 @@ class GssapiSaslMechanismTest {
                                           KerberosLoginContextFactory 
loginContextFactory,
                                           GssapiSaslServerFactory 
saslServerFactory) {
         GssapiSaslConfiguration configuration = new GssapiSaslConfiguration(
-            "imap", "mail.example.com", "imap/[email protected]", 
Path.of("imap.keytab"), requireSSL);
+            "imap", "mail.example.com", "imap/[email protected]", 
Path.of("imap.keytab"), requireSSL,
+            RealmMapping.REALM_AS_DOMAIN);
         return new GssapiSaslMechanism(configuration, loginContextFactory, 
saslServerFactory);
     }
 
diff --git 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/KeyTabPrincipalVerifierTest.java
 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/KeyTabPrincipalVerifierTest.java
index ca9a419018..b742a7eddf 100644
--- 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/KeyTabPrincipalVerifierTest.java
+++ 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/KeyTabPrincipalVerifierTest.java
@@ -41,7 +41,8 @@ class KeyTabPrincipalVerifierTest {
         try (KerberosTestFixture kerberos = new 
KerberosTestFixture(temporaryDirectory)) {
             KerberosTestFixture.Service service = 
kerberos.provisionService("imap", SERVER_NAME);
             GssapiSaslConfiguration configuration = new 
GssapiSaslConfiguration(
-                service.serviceName(), service.serverName(), 
service.principal(), service.keyTab(), true);
+                service.serviceName(), service.serverName(), 
service.principal(), service.keyTab(), true,
+                RealmMapping.REALM_AS_DOMAIN);
 
             assertThatCode(() -> new 
KeyTabPrincipalVerifier().verify(configuration))
                 .doesNotThrowAnyException();
@@ -54,7 +55,8 @@ class KeyTabPrincipalVerifierTest {
         try (KerberosTestFixture kerberos = new 
KerberosTestFixture(temporaryDirectory)) {
             KerberosTestFixture.Service service = 
kerberos.provisionService("smtp", SERVER_NAME);
             GssapiSaslConfiguration configuration = new 
GssapiSaslConfiguration(
-                "imap", SERVER_NAME, "imap/" + SERVER_NAME + "@" + 
KerberosTestFixture.REALM, service.keyTab(), true);
+                "imap", SERVER_NAME, "imap/" + SERVER_NAME + "@" + 
KerberosTestFixture.REALM, service.keyTab(), true,
+                RealmMapping.REALM_AS_DOMAIN);
 
             assertThatThrownBy(() -> new 
KeyTabPrincipalVerifier().verify(configuration))
                 .isInstanceOf(ConfigurationException.class)
diff --git 
a/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/RealmMappingTest.java
 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/RealmMappingTest.java
new file mode 100644
index 0000000000..6695f0ccc7
--- /dev/null
+++ 
b/protocols/sasl-kerberos/src/test/java/org/apache/james/protocols/sasl/kerberos/RealmMappingTest.java
@@ -0,0 +1,213 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.sasl.kerberos;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.commons.configuration2.XMLConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.io.FileHandler;
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.junit.jupiter.api.Test;
+
+class RealmMappingTest {
+    private static final CanonicalKerberosPrincipal ALICE = new 
CanonicalKerberosPrincipal("alice", "CORP.EXAMPLE.COM");
+
+    @Test
+    void shouldDefaultToRealmAsDomain() throws Exception {
+        assertThat(RealmMapping.from(new 
BaseHierarchicalConfiguration())).isEqualTo(RealmMapping.REALM_AS_DOMAIN);
+    }
+
+    @Test
+    void realmAsDomainShouldMakeTheRealmTheMailDomain() {
+        assertThat(RealmMapping.REALM_AS_DOMAIN.resolve(new 
CanonicalKerberosPrincipal("alice", "EXAMPLE.COM")))
+            .contains(Username.of("[email protected]"));
+    }
+
+    @Test
+    void realmAsDomainShouldPreserveMultiComponentPrincipals() {
+        assertThat(RealmMapping.REALM_AS_DOMAIN.resolve(new 
CanonicalKerberosPrincipal("alice/admin", "EXAMPLE.COM")))
+            .contains(Username.of("alice/[email protected]"));
+    }
+
+    @Test
+    void realmAsDomainShouldRejectRealmsCollapsingOntoAnotherPrincipal() {
+        // Domain.of strips the brackets, so this realm would silently 
collapse onto the EXAMPLE.COM one.
+        
assertThat(Username.of("alice@[EXAMPLE.COM]")).isEqualTo(Username.of("[email protected]"));
+
+        assertThatThrownBy(() -> RealmMapping.REALM_AS_DOMAIN.resolve(new 
CanonicalKerberosPrincipal("alice", "[EXAMPLE.COM]")))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void shouldMapRealmOntoConfiguredDomain() throws Exception {
+        RealmMapping testee = 
RealmMapping.from(realmMapping("CORP.EXAMPLE.COM", "example.com"));
+
+        
assertThat(testee.resolve(ALICE)).contains(Username.of("[email protected]"));
+    }
+
+    @Test
+    void shouldMapEachConfiguredRealm() throws Exception {
+        BaseHierarchicalConfiguration configuration = 
realmMapping("CORP.EXAMPLE.COM", "example.com");
+        addRealm(configuration, "OTHER.EXAMPLE.COM", "other.example.com");
+        RealmMapping testee = RealmMapping.from(configuration);
+
+        assertThat(testee.resolve(new CanonicalKerberosPrincipal("bob", 
"OTHER.EXAMPLE.COM")))
+            .contains(Username.of("[email protected]"));
+    }
+
+    @Test
+    void shouldNotResolveUnlistedRealm() throws Exception {
+        RealmMapping testee = 
RealmMapping.from(realmMapping("CORP.EXAMPLE.COM", "example.com"));
+
+        assertThat(testee.resolve(new CanonicalKerberosPrincipal("alice", 
"EXAMPLE.COM"))).isEmpty();
+    }
+
+    @Test
+    void shouldPreserveMultiComponentPrincipals() throws Exception {
+        RealmMapping testee = 
RealmMapping.from(realmMapping("CORP.EXAMPLE.COM", "example.com"));
+
+        assertThat(testee.resolve(new 
CanonicalKerberosPrincipal("alice/admin", "CORP.EXAMPLE.COM")))
+            .contains(Username.of("alice/[email protected]"));
+    }
+
+    @Test
+    void shouldReadRealmMappingFromXmlConfiguration() throws Exception {
+        XMLConfiguration configuration = new XMLConfiguration();
+        FileHandler fileHandler = new FileHandler(configuration);
+        fileHandler.load(new ByteArrayInputStream("""
+            <gssapi>
+                <realmMapping>
+                    <realm name="CORP.EXAMPLE.COM" domain="example.com"/>
+                    <realm name="OTHER.EXAMPLE.COM" 
domain="other.example.com"/>
+                </realmMapping>
+            </gssapi>""".getBytes(StandardCharsets.UTF_8)));
+
+        RealmMapping testee = RealmMapping.from(configuration);
+
+        
assertThat(testee.resolve(ALICE)).contains(Username.of("[email protected]"));
+        assertThat(testee.resolve(new CanonicalKerberosPrincipal("bob", 
"OTHER.EXAMPLE.COM")))
+            .contains(Username.of("[email protected]"));
+    }
+
+    @Test
+    void shouldRejectEmptyRealmMapping() {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("realmMapping", "");
+
+        assertThatThrownBy(() -> RealmMapping.from(configuration))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("auth.gssapi.realmMapping must declare at least one 
realm");
+    }
+
+    @Test
+    void shouldRejectRealmWithoutName() {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("realmMapping.realm(-1)[@domain]", 
"example.com");
+
+        assertThatThrownBy(() -> RealmMapping.from(configuration))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("auth.gssapi.realmMapping.realm must carry a name 
attribute");
+    }
+
+    @Test
+    void shouldRejectRealmWithoutDomain() {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("realmMapping.realm(-1)[@name]", 
"CORP.EXAMPLE.COM");
+
+        assertThatThrownBy(() -> RealmMapping.from(configuration))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("auth.gssapi.realmMapping.realm CORP.EXAMPLE.COM must 
carry a domain attribute");
+    }
+
+    @Test
+    void shouldRejectInvalidDomain() {
+        assertThatThrownBy(() -> 
RealmMapping.from(realmMapping("CORP.EXAMPLE.COM", "exa mple.com")))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("auth.gssapi.realmMapping.realm CORP.EXAMPLE.COM 
carries an invalid domain");
+    }
+
+    @Test
+    void shouldRejectDuplicatedRealm() {
+        BaseHierarchicalConfiguration configuration = 
realmMapping("CORP.EXAMPLE.COM", "example.com");
+        addRealm(configuration, "CORP.EXAMPLE.COM", "other.example.com");
+
+        assertThatThrownBy(() -> RealmMapping.from(configuration))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("auth.gssapi.realmMapping declares realm 
CORP.EXAMPLE.COM twice");
+    }
+
+    @Test
+    void shouldRejectRealmsSharingADomain() {
+        BaseHierarchicalConfiguration configuration = 
realmMapping("CORP.EXAMPLE.COM", "example.com");
+        addRealm(configuration, "OTHER.EXAMPLE.COM", "example.com");
+
+        assertThatThrownBy(() -> RealmMapping.from(configuration))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("auth.gssapi.realmMapping is invalid")
+            .hasRootCauseMessage("Realms CORP.EXAMPLE.COM and 
OTHER.EXAMPLE.COM are both mapped onto domain example.com");
+    }
+
+    @Test
+    void shouldRejectRealmsSharingADomainSpelledDifferently() {
+        BaseHierarchicalConfiguration configuration = 
realmMapping("CORP.EXAMPLE.COM", "example.com");
+        addRealm(configuration, "OTHER.EXAMPLE.COM", "EXAMPLE.COM");
+
+        assertThatThrownBy(() -> RealmMapping.from(configuration))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("auth.gssapi.realmMapping is invalid");
+    }
+
+    @Test
+    void shouldRejectLowerCaseRealm() {
+        assertThatThrownBy(() -> 
RealmMapping.from(realmMapping("corp.example.com", "example.com")))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("auth.gssapi.realmMapping is invalid")
+            .hasRootCauseMessage("Kerberos realm corp.example.com must be 
upper case");
+    }
+
+    @Test
+    void constructorShouldRejectRealmsSharingADomain() {
+        Map<String, Domain> domainByRealm = Map.of(
+            "CORP.EXAMPLE.COM", Domain.of("example.com"),
+            "OTHER.EXAMPLE.COM", Domain.of("example.com"));
+
+        assertThatThrownBy(() -> new RealmMapping(domainByRealm))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    private BaseHierarchicalConfiguration realmMapping(String realm, String 
domain) {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        addRealm(configuration, realm, domain);
+        return configuration;
+    }
+
+    private void addRealm(BaseHierarchicalConfiguration configuration, String 
realm, String domain) {
+        configuration.addProperty("realmMapping.realm(-1)[@name]", realm);
+        configuration.addProperty("realmMapping.realm[@domain]", domain);
+    }
+}
diff --git 
a/server/apps/memory-app/src/test/java/org/apache/james/SaslGssapiRealmMappingIntegrationTest.java
 
b/server/apps/memory-app/src/test/java/org/apache/james/SaslGssapiRealmMappingIntegrationTest.java
new file mode 100644
index 0000000000..e06e6f8482
--- /dev/null
+++ 
b/server/apps/memory-app/src/test/java/org/apache/james/SaslGssapiRealmMappingIntegrationTest.java
@@ -0,0 +1,146 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james;
+
+import static 
org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Base64;
+import java.util.Optional;
+
+import org.apache.commons.net.imap.AuthenticatingIMAPClient;
+import org.apache.commons.net.imap.IMAPReply;
+import org.apache.james.modules.protocols.ImapGuiceProbe;
+import org.apache.james.protocols.api.utils.BogusSslContextFactory;
+import org.apache.james.protocols.api.utils.BogusTrustManagerFactory;
+import org.apache.james.protocols.sasl.kerberos.GssapiTestClient;
+import org.apache.james.protocols.sasl.kerberos.KerberosTestExtension;
+import org.apache.james.protocols.sasl.kerberos.KerberosTestFixture;
+import org.apache.james.utils.DataProbeImpl;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.parallel.ResourceLock;
+
+// The embedded KDC temporarily replaces the JVM-wide Kerberos configuration.
+@ResourceLock(KerberosTestFixture.KRB5_CONFIGURATION_RESOURCE)
+class SaslGssapiRealmMappingIntegrationTest {
+    private static final String DOMAIN = "example.com";
+    private static final String USERNAME = "alice@" + DOMAIN;
+    private static final String HOST = "127.0.0.1";
+    private static final int MAX_SASL_ROUNDS = 10;
+
+    // Provision Kerberos credentials before James resolves them from its 
server configuration.
+    @Order(1)
+    @RegisterExtension
+    static KerberosTestExtension kerberos = new KerberosTestExtension("imap");
+
+    @Order(2)
+    @RegisterExtension
+    static JamesServerExtension jamesServerExtension = new 
JamesServerBuilder<MemoryJamesConfiguration>(tmpDir -> {
+        TemporaryJamesServer temporaryJamesServer = new 
TemporaryJamesServer(tmpDir);
+        
temporaryJamesServer.copyResources("imapserver-gssapi-realm-mapping.xml", 
"imapserver.xml");
+        return MemoryJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .usersRepository(DEFAULT)
+            .build();
+    })
+        .server(MemoryJamesServerMain::createServer)
+        .lifeCycle(JamesServerExtension.Lifecycle.PER_ENCLOSING_CLASS)
+        .build();
+
+    // The Kerberos realm is JAMES.TEST while the mail domain is example.com.
+    @BeforeAll
+    static void provisionMappedUser(GuiceJamesServer server) throws Exception {
+        server.getProbe(DataProbeImpl.class).fluent()
+            .addDomain(DOMAIN)
+            .addUser(USERNAME, "unused-password");
+    }
+
+    @Test
+    void shouldAuthenticateThroughTheConfiguredRealmMapping(GuiceJamesServer 
server) throws Exception {
+        // Connect to IMAP over TLS, as required by the configured GSSAPI 
mechanism.
+        int port = server.getProbe(ImapGuiceProbe.class).getImapStartTLSPort();
+        AuthenticatingIMAPClient imapClient = imapClient(port);
+
+        try (GssapiTestClient gssapiClient = kerberos.client("imap")) {
+            // Authenticate as [email protected], whose realm the configuration 
maps onto example.com.
+            int replyCode = completeImapExchange(imapClient, gssapiClient,
+                imapClient.sendCommand("AUTHENTICATE GSSAPI " + 
encode(gssapiClient.initialResponse())));
+
+            assertThat(replyCode)
+                .withFailMessage("Unexpected IMAP authentication reply: %s", 
imapClient.getReplyString())
+                .isEqualTo(IMAPReply.OK);
+            assertThat(gssapiClient.isComplete()).isTrue();
+
+            // Verify the session is bound to the mapped account rather than 
to the realm as a domain.
+            assertThat(imapClient.select("INBOX")).isTrue();
+        } finally {
+            imapClient.disconnect();
+        }
+    }
+
+    @Test
+    void shouldMapAnExplicitPrincipalAuthorizationIdentity(GuiceJamesServer 
server) throws Exception {
+        // Connect over TLS and create a client requesting its own Kerberos 
principal as authorization identity.
+        int port = server.getProbe(ImapGuiceProbe.class).getImapStartTLSPort();
+        AuthenticatingIMAPClient imapClient = imapClient(port);
+
+        try (GssapiTestClient gssapiClient = kerberos.client("imap", 
Optional.of(KerberosTestFixture.USER_PRINCIPAL))) {
+            int replyCode = completeImapExchange(imapClient, gssapiClient,
+                imapClient.sendCommand("AUTHENTICATE GSSAPI " + 
encode(gssapiClient.initialResponse())));
+
+            // Verify the authorization identity went through the realm 
mapping too.
+            assertThat(replyCode)
+                .withFailMessage("Unexpected IMAP authentication reply: %s", 
imapClient.getReplyString())
+                .isEqualTo(IMAPReply.OK);
+            assertThat(gssapiClient.isComplete()).isTrue();
+            assertThat(imapClient.select("INBOX")).isTrue();
+        } finally {
+            imapClient.disconnect();
+        }
+    }
+
+    private static AuthenticatingIMAPClient imapClient(int port) throws 
Exception {
+        AuthenticatingIMAPClient client = new AuthenticatingIMAPClient(false, 
BogusSslContextFactory.getClientContext());
+        client.setTrustManager(BogusTrustManagerFactory.getTrustManagers()[0]);
+        client.connect(HOST, port);
+        assertThat(client.execTLS()).isTrue();
+        return client;
+    }
+
+    private static byte[] imapChallenge(String reply) {
+        String challenge = reply.trim().substring(1).trim();
+        return challenge.isEmpty() ? new byte[0] : 
Base64.getDecoder().decode(challenge);
+    }
+
+    private static String encode(byte[] token) {
+        return Base64.getEncoder().encodeToString(token);
+    }
+
+    private static int completeImapExchange(AuthenticatingIMAPClient 
imapClient, GssapiTestClient gssapiClient, int replyCode) throws Exception {
+        // Relay each server challenge through the JDK GSSAPI client until 
SASL completes.
+        for (int round = 0; round < MAX_SASL_ROUNDS && 
IMAPReply.isContinuation(replyCode); round++) {
+            replyCode = 
imapClient.sendData(encode(gssapiClient.evaluate(imapChallenge(imapClient.getReplyString()))));
+        }
+        return replyCode;
+    }
+}
diff --git 
a/server/apps/memory-app/src/test/resources/imapserver-gssapi-realm-mapping.xml 
b/server/apps/memory-app/src/test/resources/imapserver-gssapi-realm-mapping.xml
new file mode 100644
index 0000000000..8f67b67fed
--- /dev/null
+++ 
b/server/apps/memory-app/src/test/resources/imapserver-gssapi-realm-mapping.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<imapservers>
+    <imapserver enabled="true">
+        <jmxName>imapserver-gssapi-realm-mapping</jmxName>
+        <bind>127.0.0.1:0</bind>
+        <tls socketTLS="false" startTLS="true">
+            <keystore>classpath://keystore</keystore>
+            <secret>james72laBalle</secret>
+            
<provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+        </tls>
+        <auth>
+            <requireSSL>true</requireSSL>
+            
<saslMechanisms>org.apache.james.protocols.sasl.kerberos.GssapiSaslMechanismFactory</saslMechanisms>
+            <gssapi>
+                <serviceName>imap</serviceName>
+                <serverName>localhost</serverName>
+                
<principal>${sys:james.test.kerberos.imap.principal}</principal>
+                <keyTab>${sys:james.test.kerberos.imap.keytab}</keyTab>
+                <realmMapping>
+                    <realm name="JAMES.TEST" domain="example.com"/>
+                </realmMapping>
+            </gssapi>
+        </auth>
+        <connectionLimit>0</connectionLimit>
+        <connectionLimitPerIP>0</connectionLimitPerIP>
+        <gracefulShutdown>false</gracefulShutdown>
+    </imapserver>
+</imapservers>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to