chibenwa commented on code in PR #3072:
URL: https://github.com/apache/james-project/pull/3072#discussion_r3465982693
##########
protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java:
##########
@@ -165,356 +167,273 @@ private Response doAUTH(SMTPSession session, String
argument) {
argument = argument.substring(0,argument.indexOf(" "));
}
String authType = argument.toUpperCase(Locale.US);
- if (authType.equals(AUTH_TYPE_PLAIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
- return handlePlainContinuation(session, initialResponse);
- } else if (authType.equals(AUTH_TYPE_LOGIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
- return handleLoginAuthContinuation(session, initialResponse);
- } else if ((authType.equals(AUTH_TYPE_OAUTHBEARER) ||
authType.equals(AUTH_TYPE_XOAUTH2)) && session.supportsOAuth()) {
- return handleOauth2Continuation(session, initialResponse);
- } else {
- return doUnknownAuth(authType);
- }
+ return handleSaslAuthentication(session, authType,
Optional.ofNullable(initialResponse).map(String::trim));
}
}
- private Response handlePlainContinuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(String::trim)
- .map(userpass -> doPlainAuth(session, userpass))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String l) {
- return doPlainAuth(session, l);
- }
- });
- return AUTH_READY_USERNAME_LOGIN;
- });
- }
+ private Response handleSaslAuthentication(SMTPSession session, String
authType, Optional<String> initialResponse) {
+ if (authType.equals(AUTH_TYPE_LOGIN)) {
+ return handleLoginFraming(session, initialResponse);
+ }
- private Response handleLoginAuthContinuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(String::trim)
- .map(user -> doLoginAuthPass(session, user))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String
l) {
- return doLoginAuthPass(session, l);
- }
- });
- return AUTH_READY_USERNAME_LOGIN;
- });
+ Optional<SaslMechanism> maybeMechanism =
findAvailableMechanism(session, authType);
+ if (maybeMechanism.isEmpty()) {
+ return doUnknownAuth(authType);
+ }
+
+ SaslMechanism mechanism = maybeMechanism.get();
+ SaslExchange exchange;
+ try {
+ SaslInitialRequest request = SASL_BRIDGE.initialRequest(authType,
initialResponse);
+ exchange = startExchange(session, mechanism, request);
+ return handleFirstSaslStep(session, authType, exchange);
+ } catch (IllegalArgumentException e) {
+ LOGGER.info("Could not decode parameters for AUTH {}", authType,
e);
+ return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could
not decode parameters for AUTH " + authType);
+ }
}
- private Response handleOauth2Continuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(token -> doOauth2Authentication(session, token))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String
l) {
- Response response = doOauth2Authentication(session, l);
- session.popLineHandler();
- return response;
- }
- });
- return new SMTPResponse(SMTPRetCode.AUTH_READY, "");
- });
+ private Response handleFirstSaslStep(SMTPSession session, String authType,
SaslExchange exchange) {
+ try {
+ SaslStep step = exchange.firstStep();
+ return handleSaslStep(session, authType, exchange, step);
+ } catch (RuntimeException e) {
+ SASL_BRIDGE.close(exchange);
+ throw e;
+ }
}
- private Response doOauth2Authentication(SMTPSession session, String
initialResponseString) {
- return session.getConfiguration().saslConfiguration()
- .map(oidcSASLConfiguration -> hooks.stream()
- .flatMap(hook -> Optional.ofNullable(executeHook(session, hook,
- hook2 -> hook2.doSasl(session, oidcSASLConfiguration,
initialResponseString))).stream())
- .filter(response ->
!SMTPRetCode.AUTH_FAILED.equals(response.getRetCode()))
- .findFirst()
- .orElseGet(() -> failSasl(oidcSASLConfiguration, session)))
- .orElseGet(() -> doUnknownAuth(AUTH_TYPE_OAUTHBEARER));
+ private Response handleSaslStep(SMTPSession session, String authType,
SaslExchange exchange, SaslStep step) {
+ return switch (step) {
+ case SaslStep.Challenge challenge -> {
+ session.pushLineHandler(saslLineHandler(authType, exchange));
+ yield SASL_BRIDGE.challenge(challenge);
+ }
+ case SaslStep.Success success -> handleSaslSuccess(session,
authType, exchange, success);
+ case SaslStep.Failure failure -> handleSaslFailure(session,
authType, exchange, failure.failure());
+ };
}
- private Response failSasl(OidcSASLConfiguration saslConfiguration,
SMTPSession session) {
- String rawResponse =
String.format("{\"status\":\"invalid_token\",\"scope\":\"%s\",\"schemes\":\"%s\"}",
- saslConfiguration.getScope(),
- saslConfiguration.getOidcConfigurationURL().toString());
+ private Response handleSaslContinuation(SMTPSession session, String
authType, SaslExchange exchange, String line) {
+ try {
+ SaslStep step = SASL_BRIDGE.onClientResponse(exchange,
line.getBytes(session.getCharset()));
+ return switch (step) {
+ case SaslStep.Challenge challenge ->
SASL_BRIDGE.challenge(challenge);
+ case SaslStep.Success success -> {
+ session.popLineHandler();
+ yield handleSaslSuccess(session, authType, exchange,
success);
+ }
+ case SaslStep.Failure failure -> {
+ session.popLineHandler();
+ yield handleSaslFailure(session, authType, exchange,
failure.failure());
+ }
+ };
Review Comment:
Extract this block, mutualize it use a Runnable to either `() -> {}` or
`session::popLineHandler`
##########
server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SmtpGuiceProbe.java:
##########
@@ -56,6 +57,12 @@ private SmtpGuiceProbe(SMTPServerFactory smtpServerFactory) {
this.smtpServerFactory = smtpServerFactory;
}
+ @PreDestroy
+ void destroy() {
+ // SMTPServerFactory is provided through a factory method; dispose it
explicitly on Guice shutdown.
+ smtpServerFactory.destroy();
+ }
Review Comment:
Why is this needed ?
##########
server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServerFactory.java:
##########
@@ -35,26 +37,73 @@
import org.apache.james.dnsservice.api.DNSService;
import org.apache.james.filesystem.api.FileSystem;
import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismFactory;
import org.apache.james.protocols.lib.handler.ProtocolHandlerLoader;
import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
import org.apache.james.protocols.lib.netty.AbstractServerFactory;
import org.apache.james.protocols.netty.Encryption;
+import org.apache.james.protocols.sasl.BuiltInSaslMechanismFactories;
+import org.apache.james.protocols.sasl.OauthBearerSaslMechanismFactory;
+import org.apache.james.protocols.sasl.PlainSaslMechanismFactory;
+import org.apache.james.protocols.sasl.XOauth2SaslMechanismFactory;
+import
org.apache.james.smtpserver.netty.SMTPServer.AuthAnnouncementConfiguration;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.collect.ImmutableList;
public class SMTPServerFactory extends AbstractServerFactory implements
Disconnector, ConnectionDescriptionSupplier {
+ @FunctionalInterface
+ public interface SmtpSaslMechanismLoader {
+ static SmtpSaslMechanismLoader defaultLoader() {
+ ImmutableList<SaslMechanismFactory> defaultFactories =
ImmutableList.of(
+ new
PlainSaslMechanismFactory(AuthAnnouncementConfiguration.REQUIRE_SSL_DEFAULT),
+ new OauthBearerSaslMechanismFactory(),
+ new XOauth2SaslMechanismFactory());
+ return configuration -> loadBuiltInMechanisms(defaultFactories,
configuration);
+ }
+
+ ImmutableList<SaslMechanism>
load(HierarchicalConfiguration<ImmutableNode> configuration) throws
ConfigurationException;
+ }
Review Comment:
The interface is the same as for IMAP stack, no need to duplicate it IMO,
likely idem for default mechanisms
##########
protocols/sasl/src/main/java/org/apache/james/protocols/sasl/oidc/OAuthSaslMechanism.java:
##########
@@ -41,17 +41,30 @@
public class OAuthSaslMechanism implements SaslMechanism {
private final String name;
private final OidcJwtTokenVerifier verifier;
+ private final boolean requiresSsl;
+ private final byte[] invalidTokenResponse;
Review Comment:
Can't we supply a default / static value ?
I'd try if possible to get a similar invalidToken response in SMTP and IMAP
in order to allign IMAP with SMTP
##########
protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java:
##########
@@ -165,356 +167,273 @@ private Response doAUTH(SMTPSession session, String
argument) {
argument = argument.substring(0,argument.indexOf(" "));
}
String authType = argument.toUpperCase(Locale.US);
- if (authType.equals(AUTH_TYPE_PLAIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
- return handlePlainContinuation(session, initialResponse);
- } else if (authType.equals(AUTH_TYPE_LOGIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
- return handleLoginAuthContinuation(session, initialResponse);
- } else if ((authType.equals(AUTH_TYPE_OAUTHBEARER) ||
authType.equals(AUTH_TYPE_XOAUTH2)) && session.supportsOAuth()) {
- return handleOauth2Continuation(session, initialResponse);
- } else {
- return doUnknownAuth(authType);
- }
+ return handleSaslAuthentication(session, authType,
Optional.ofNullable(initialResponse).map(String::trim));
}
}
- private Response handlePlainContinuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(String::trim)
- .map(userpass -> doPlainAuth(session, userpass))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String l) {
- return doPlainAuth(session, l);
- }
- });
- return AUTH_READY_USERNAME_LOGIN;
- });
- }
+ private Response handleSaslAuthentication(SMTPSession session, String
authType, Optional<String> initialResponse) {
+ if (authType.equals(AUTH_TYPE_LOGIN)) {
+ return handleLoginFraming(session, initialResponse);
+ }
- private Response handleLoginAuthContinuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(String::trim)
- .map(user -> doLoginAuthPass(session, user))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String
l) {
- return doLoginAuthPass(session, l);
- }
- });
- return AUTH_READY_USERNAME_LOGIN;
- });
+ Optional<SaslMechanism> maybeMechanism =
findAvailableMechanism(session, authType);
+ if (maybeMechanism.isEmpty()) {
+ return doUnknownAuth(authType);
+ }
+
+ SaslMechanism mechanism = maybeMechanism.get();
+ SaslExchange exchange;
+ try {
+ SaslInitialRequest request = SASL_BRIDGE.initialRequest(authType,
initialResponse);
+ exchange = startExchange(session, mechanism, request);
+ return handleFirstSaslStep(session, authType, exchange);
Review Comment:
```
try {
SaslInitialRequest request = ;
188 +
SaslExchange exchange = startExchange(session,
maybeMechanism.get(), SASL_BRIDGE.initialRequest(authType, initialResponse));
189 +
return handleFirstSaslStep(session, authType, exchange)
```
##########
protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java:
##########
@@ -165,356 +167,273 @@ private Response doAUTH(SMTPSession session, String
argument) {
argument = argument.substring(0,argument.indexOf(" "));
}
String authType = argument.toUpperCase(Locale.US);
- if (authType.equals(AUTH_TYPE_PLAIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
- return handlePlainContinuation(session, initialResponse);
- } else if (authType.equals(AUTH_TYPE_LOGIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
- return handleLoginAuthContinuation(session, initialResponse);
- } else if ((authType.equals(AUTH_TYPE_OAUTHBEARER) ||
authType.equals(AUTH_TYPE_XOAUTH2)) && session.supportsOAuth()) {
- return handleOauth2Continuation(session, initialResponse);
- } else {
- return doUnknownAuth(authType);
- }
+ return handleSaslAuthentication(session, authType,
Optional.ofNullable(initialResponse).map(String::trim));
}
}
- private Response handlePlainContinuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(String::trim)
- .map(userpass -> doPlainAuth(session, userpass))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String l) {
- return doPlainAuth(session, l);
- }
- });
- return AUTH_READY_USERNAME_LOGIN;
- });
- }
+ private Response handleSaslAuthentication(SMTPSession session, String
authType, Optional<String> initialResponse) {
+ if (authType.equals(AUTH_TYPE_LOGIN)) {
+ return handleLoginFraming(session, initialResponse);
+ }
- private Response handleLoginAuthContinuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(String::trim)
- .map(user -> doLoginAuthPass(session, user))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String
l) {
- return doLoginAuthPass(session, l);
- }
- });
- return AUTH_READY_USERNAME_LOGIN;
- });
+ Optional<SaslMechanism> maybeMechanism =
findAvailableMechanism(session, authType);
+ if (maybeMechanism.isEmpty()) {
+ return doUnknownAuth(authType);
+ }
+
+ SaslMechanism mechanism = maybeMechanism.get();
+ SaslExchange exchange;
+ try {
+ SaslInitialRequest request = SASL_BRIDGE.initialRequest(authType,
initialResponse);
+ exchange = startExchange(session, mechanism, request);
+ return handleFirstSaslStep(session, authType, exchange);
+ } catch (IllegalArgumentException e) {
+ LOGGER.info("Could not decode parameters for AUTH {}", authType,
e);
+ return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could
not decode parameters for AUTH " + authType);
+ }
}
- private Response handleOauth2Continuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(token -> doOauth2Authentication(session, token))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String
l) {
- Response response = doOauth2Authentication(session, l);
- session.popLineHandler();
- return response;
- }
- });
- return new SMTPResponse(SMTPRetCode.AUTH_READY, "");
- });
+ private Response handleFirstSaslStep(SMTPSession session, String authType,
SaslExchange exchange) {
+ try {
+ SaslStep step = exchange.firstStep();
+ return handleSaslStep(session, authType, exchange, step);
+ } catch (RuntimeException e) {
+ SASL_BRIDGE.close(exchange);
+ throw e;
+ }
}
- private Response doOauth2Authentication(SMTPSession session, String
initialResponseString) {
- return session.getConfiguration().saslConfiguration()
- .map(oidcSASLConfiguration -> hooks.stream()
- .flatMap(hook -> Optional.ofNullable(executeHook(session, hook,
- hook2 -> hook2.doSasl(session, oidcSASLConfiguration,
initialResponseString))).stream())
- .filter(response ->
!SMTPRetCode.AUTH_FAILED.equals(response.getRetCode()))
- .findFirst()
- .orElseGet(() -> failSasl(oidcSASLConfiguration, session)))
- .orElseGet(() -> doUnknownAuth(AUTH_TYPE_OAUTHBEARER));
+ private Response handleSaslStep(SMTPSession session, String authType,
SaslExchange exchange, SaslStep step) {
+ return switch (step) {
+ case SaslStep.Challenge challenge -> {
+ session.pushLineHandler(saslLineHandler(authType, exchange));
+ yield SASL_BRIDGE.challenge(challenge);
+ }
+ case SaslStep.Success success -> handleSaslSuccess(session,
authType, exchange, success);
+ case SaslStep.Failure failure -> handleSaslFailure(session,
authType, exchange, failure.failure());
+ };
}
- private Response failSasl(OidcSASLConfiguration saslConfiguration,
SMTPSession session) {
- String rawResponse =
String.format("{\"status\":\"invalid_token\",\"scope\":\"%s\",\"schemes\":\"%s\"}",
- saslConfiguration.getScope(),
- saslConfiguration.getOidcConfigurationURL().toString());
+ private Response handleSaslContinuation(SMTPSession session, String
authType, SaslExchange exchange, String line) {
+ try {
+ SaslStep step = SASL_BRIDGE.onClientResponse(exchange,
line.getBytes(session.getCharset()));
+ return switch (step) {
+ case SaslStep.Challenge challenge ->
SASL_BRIDGE.challenge(challenge);
+ case SaslStep.Success success -> {
+ session.popLineHandler();
+ yield handleSaslSuccess(session, authType, exchange,
success);
+ }
+ case SaslStep.Failure failure -> {
+ session.popLineHandler();
+ yield handleSaslFailure(session, authType, exchange,
failure.failure());
+ }
+ };
+ } catch (IllegalArgumentException e) {
+ LOGGER.info("Could not decode parameters for AUTH {}", authType,
e);
+ session.popLineHandler();
+ SASL_BRIDGE.close(exchange);
+ return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could
not decode parameters for AUTH " + authType);
+ } catch (RuntimeException e) {
+ session.popLineHandler();
+ SASL_BRIDGE.close(exchange);
+ throw e;
+ }
+ }
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String l) {
+ private LineHandler<SMTPSession> saslLineHandler(String authType,
SaslExchange exchange) {
+ return (session, line) -> {
+ if (SASL_BRIDGE.isAbort(line)) {
session.popLineHandler();
-
- return AUTH_FAILED;
+ SASL_BRIDGE.abort(exchange);
+ return AUTH_ABORTED;
}
- });
- return new SMTPResponse("334",
Base64.getEncoder().encodeToString(rawResponse.getBytes()));
+ return handleSaslContinuation(session, authType, exchange, new
String(line, session.getCharset()));
+ };
}
- /**
- * Carries out the Plain AUTH SASL exchange.
- *
- * According to RFC 2595 the client must send: [authorize-id] \0
authenticate-id \0 password.
- *
- * >>> AUTH PLAIN dGVzdAB0ZXN0QHdpei5leGFtcGxlLmNvbQB0RXN0NDI=
- * Decoded: test\[email protected]\000tEst42
- *
- * >>> AUTH PLAIN dGVzdAB0ZXN0AHRFc3Q0Mg==
- * Decoded: test\000test\000tEst42
- *
- * @param session SMTP session object
- * @param line the initial response line passed in with the AUTH command
- */
- private Response doPlainAuth(SMTPSession session, String line) {
+ private Response handleSaslSuccess(SMTPSession session, String authType,
SaslExchange exchange, SaslStep.Success success) {
+ if (success.serverData().isPresent()) {
+
session.pushLineHandler(successDataAcknowledgementLineHandler(authType,
exchange, success));
+ return SASL_BRIDGE.successData(success);
+ }
try {
+ return applySaslSuccess(session, authType, exchange, success);
+ } finally {
+ SASL_BRIDGE.close(exchange);
+ }
+ }
- AuthValues authValues =
- Optional.ofNullable(decodeBase64(line))
- .flatMap(AuthCmdHandler::parseAuthValues)
- .orElseThrow(() -> new
IllegalArgumentException("Can't parse line as authentication values"));
-
- Response response;
+ private LineHandler<SMTPSession>
successDataAcknowledgementLineHandler(String authType, SaslExchange exchange,
SaslStep.Success success) {
+ return (session, line) ->
handleSaslSuccessDataAcknowledgement(session, authType, exchange, success, new
String(line, session.getCharset()));
+ }
- if (authValues.password.isEmpty()) {
- response = doDelegation(session, authValues.username);
- } else {
- response = doAuthTest(session,
Optional.of(authValues.username), authValues.password, AUTH_TYPE_PLAIN);
+ private Response handleSaslSuccessDataAcknowledgement(SMTPSession session,
String authType, SaslExchange exchange,
+ SaslStep.Success
success, String line) {
+ session.popLineHandler();
+ boolean aborted = false;
+ try {
+ byte[] bytes = line.getBytes(session.getCharset());
+ if (SASL_BRIDGE.isAbort(bytes)) {
+ aborted = true;
+ SASL_BRIDGE.abort(exchange);
+ return AUTH_ABORTED;
+ }
+ if (!SASL_BRIDGE.isEmptyClientResponse(bytes)) {
+ return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,
"Could not decode parameters for AUTH " + authType);
+ }
+ return applySaslSuccess(session, authType, exchange, success);
+ } finally {
+ if (!aborted) {
+ SASL_BRIDGE.close(exchange);
}
-
- session.popLineHandler();
- return response;
- } catch (Exception e) {
- LOGGER.info("Could not decode parameters for AUTH PLAIN", e);
- return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could
not decode parameters for AUTH PLAIN");
}
}
- @VisibleForTesting
- static Optional<AuthValues> parseAuthValues(String input) {
-
- List<String> parts =
Splitter.on('\0').splitToStream(input).filter(token ->
!token.isBlank()).toList();
-
- return switch (parts.size()) {
- case 1 -> Optional.of(new AuthValues(Username.of(parts.get(0)),
Optional.empty()));
- // If we got here, this is what happened. RFC 2595
- // says that "the client may leave the authorization
- // identity empty to indicate that it is the same as
- // the authentication identity." As noted above,
- // that would be represented as a decoded string of
- // the form: "\0authenticate-id\0password". The
- // first call to nextToken will skip the empty
- // authorize-id, and give us the authenticate-id,
- // which we would store as the authorize-id. The
- // second call will give us the password, which we
- // think is the authenticate-id (user). Then when
- // we ask for the password, there are no more
- // elements, leading to the exception we just
- // caught. So we need to move the user to the
- // password, and the authorize_id to the user.
- case 2 -> Optional.of(new AuthValues(Username.of(parts.get(0)),
Optional.of(parts.get(1))));
- case 3 -> Optional.of(new AuthValues(Username.of(parts.get(1)),
Optional.of(parts.get(2))));
- default -> Optional.empty();
- };
+ private Response applySaslSuccess(SMTPSession session, String authType,
SaslExchange exchange, SaslStep.Success success) {
+ Username username = success.identity().authorizationId();
+ session.setUsername(username);
+ session.setRelayingAllowed(true);
+ saslAuthResultHooks.forEach(hook -> hook.onSuccess(session, authType,
success.identity()));
+
+ AUTHENTICATION_DEDICATED_LOGGER.debug("AUTH method {} succeeded",
authType);
+
+ AuditTrail.entry()
+ .username(username::asString)
+ .remoteIP(() -> Optional.ofNullable(session.getRemoteAddress()))
+ .sessionId(session::getSessionID)
+ .protocol("SMTP")
+ .action("AUTH")
+ .parameters(() -> ImmutableMap.of("authType", authType))
+ .log("SMTP Authentication succeeded.");
+
+ return AuthHookSaslMechanism.terminalResponse(exchange)
+ .orElse(AUTH_SUCCEEDED);
}
- record AuthValues(Username username, Optional<String> password) {
+ private Response handleSaslFailure(SMTPSession session, String authType,
SaslExchange exchange, SaslFailure failure) {
+ try {
+ saslAuthResultHooks.forEach(hook -> hook.onFailure(session,
authType, failure));
+
+ failure.authenticationId().ifPresent(username -> AuditTrail.entry()
+ .username(username::asString)
+ .remoteIP(() ->
Optional.ofNullable(session.getRemoteAddress()))
+ .protocol("SMTP")
+ .action("AUTH")
+ .parameters(() -> ImmutableMap.of("authType", authType))
+ .log("SMTP Authentication failed."));
+
+ return AuthHookSaslMechanism.terminalResponse(exchange)
+ .orElseGet(() -> switch (failure.type()) {
+ case MALFORMED -> new
SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could not decode parameters
for AUTH " + authType);
+ case SERVER_ERROR -> SERVER_ERROR;
+ case INVALID_CREDENTIALS, AUTHENTICATION_FAILED,
USER_DOES_NOT_EXIST, DELEGATION_FORBIDDEN -> AUTH_FAILED;
+ });
+ } finally {
+ SASL_BRIDGE.close(exchange);
+ }
}
- private String decodeBase64(String line) {
- if (line != null) {
- String lineWithoutTrailingCrLf = StringUtils.replace(line, "\r\n",
"");
- return new
String(Base64.getDecoder().decode(lineWithoutTrailingCrLf),
StandardCharsets.UTF_8);
+ private Response handleLoginFraming(SMTPSession session, Optional<String>
initialResponse) {
+ Optional<SaslMechanism> plain = findAvailableMechanism(session,
SaslMechanismNames.PLAIN);
+ if (plain.isEmpty()) {
+ return doUnknownAuth(AUTH_TYPE_LOGIN);
}
- return null;
+
+ return initialResponse
+ .map(user -> promptLoginPasswordThenDelegateToPlain(session,
plain.get(), user))
+ .orElseGet(() -> {
+ session.pushLineHandler(new AbstractSMTPLineHandler() {
+ @Override
+ protected Response onCommand(SMTPSession session, String
line) {
+ session.popLineHandler();
+ return promptLoginPasswordThenDelegateToPlain(session,
plain.get(), line);
+ }
+ });
+ return AUTH_READY_USERNAME_LOGIN;
+ });
}
- /**
- * Carries out the Login AUTH SASL exchange.
- *
- * @param session SMTP session object
- * @param user the user passed in with the AUTH command
- */
- private Response doLoginAuthPass(SMTPSession session, String user) {
- session.popLineHandler();
+ private Response promptLoginPasswordThenDelegateToPlain(SMTPSession
session, SaslMechanism plain, String encodedUsername) {
session.pushLineHandler(new AbstractSMTPLineHandler() {
@Override
- protected Response onCommand(SMTPSession session, String l) {
- return doLoginAuthPassCheck(session, asUsername(user), l);
+ protected Response onCommand(SMTPSession session, String
encodedPassword) {
+ session.popLineHandler();
+ return delegateLoginCredentialsToPlain(session, plain,
decodeLoginUsername(encodedUsername), encodedPassword);
}
});
return AUTH_READY_PASSWORD_LOGIN;
}
- private Optional<Username> asUsername(String user) {
- try {
- return Optional.of(Username.of(decodeBase64(user)));
- } catch (Exception e) {
- LOGGER.info("Failed parsing base64 username {}", user, e);
- return Optional.empty();
+ private Response delegateLoginCredentialsToPlain(SMTPSession session,
SaslMechanism plain, Optional<Username> username, String encodedPassword) {
+ Optional<String> password = decodeLoginPassword(username,
encodedPassword);
+ if (username.isEmpty() || password.isEmpty()) {
+ return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could
not decode parameters for AUTH " + AUTH_TYPE_LOGIN);
}
+ SaslInitialRequest request = new
SaslInitialRequest(SaslMechanismNames.PLAIN,
+ Optional.of(toPlainInitialResponse(username.get(),
password.get())));
+ SaslExchange exchange = startExchange(session, plain, request);
+ return handleFirstSaslStep(session, AUTH_TYPE_LOGIN, exchange);
}
- private Response doLoginAuthPassCheck(SMTPSession session,
Optional<Username> username, String pass) {
- session.popLineHandler();
- // Authenticate user
- return doAuthTest(session, username, sanitizePassword(username, pass),
"LOGIN");
+ private byte[] toPlainInitialResponse(Username username, String password) {
+ return ("\0" + username.asString() + "\0" +
password).getBytes(StandardCharsets.UTF_8);
}
- private Optional<String> sanitizePassword(Optional<Username> username,
String pass) {
+ private Optional<Username> decodeLoginUsername(String response) {
try {
- return Optional.of(decodeBase64(pass));
- } catch (Exception e) {
- LOGGER.info("Failed parsing base64 password for user {}",
username, e);
- // Ignored - this parse error will be
- // addressed in the if clause below
+ return Optional.of(Username.of(decodeSaslLoginResponse(response)));
+ } catch (IllegalArgumentException e) {
+ LOGGER.info("Could not decode LOGIN username", e);
return Optional.empty();
}
}
- protected Response doDelegation(SMTPSession session, Username username) {
- List<AuthHook> hooks = Optional.ofNullable(getHooks())
- .orElse(List.of());
-
- for (AuthHook rawHook : hooks) {
- rawHook.doDelegation(session, username);
- Response res = executeHook(session, rawHook, hook ->
rawHook.doDelegation(session, username));
-
- if (res != null) {
- if (SMTPRetCode.AUTH_FAILED.equals(res.getRetCode())) {
- LOGGER.warn("{} was not authorized to connect as {}",
session.getUsername(), username);
- } else if (SMTPRetCode.AUTH_OK.equals(res.getRetCode())) {
- LOGGER.info("{} was authorized to connect as {}",
session.getUsername(), username);
- }
- return res;
- }
+ private Optional<String> decodeLoginPassword(Optional<Username> username,
String response) {
+ try {
+ return Optional.of(decodeSaslLoginResponse(response));
+ } catch (IllegalArgumentException e) {
+ LOGGER.info("Could not decode LOGIN password for user {}",
username, e);
+ return Optional.empty();
}
+ }
- LOGGER.info("DELEGATE failed from {}@{}", username,
session.getRemoteAddress().getAddress().getHostAddress());
- return AUTH_FAILED;
+ private String decodeSaslLoginResponse(String response) {
+ return new String(Base64.getDecoder().decode(response.replace("\r\n",
"")), StandardCharsets.UTF_8);
}
- protected Response doAuthTest(SMTPSession session, Optional<Username>
username, Optional<String> pass, String authType) {
- if (username.isEmpty() || pass.isEmpty()) {
- return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,"Could
not decode parameters for AUTH " + authType);
- }
+ private Optional<SaslMechanism> findAvailableMechanism(SMTPSession
session, String authType) {
+ return effectiveSaslMechanisms
+ .stream()
+ .filter(mechanism -> mechanism.name().equalsIgnoreCase(authType))
+ .filter(mechanism ->
mechanism.isAvailableOnTransport(session.isTLSStarted()))
+ .findFirst();
+ }
- List<AuthHook> hooks = getHooks();
-
- if (hooks != null) {
- for (AuthHook rawHook : hooks) {
- Response res = executeHook(session, rawHook, hook ->
hook.doAuth(session, username.get(), pass.get()));
-
- if (res != null) {
- if (SMTPRetCode.AUTH_FAILED.equals(res.getRetCode())) {
- AUTHENTICATION_DEDICATED_LOGGER.info("AUTH method {}
failed", authType);
- } else if (SMTPRetCode.AUTH_OK.equals(res.getRetCode())) {
- // TODO: Make this string a more useful debug message
- AUTHENTICATION_DEDICATED_LOGGER.debug("AUTH method {}
succeeded", authType);
-
- AuditTrail.entry()
- .username(username.get()::asString)
- .remoteIP(() ->
Optional.ofNullable(session.getRemoteAddress()))
- .sessionId(session::getSessionID)
- .protocol("SMTP")
- .action("AUTH")
- .parameters(() -> ImmutableMap.of("authType",
authType))
- .log("SMTP Authentication succeeded.");
- }
- return res;
- }
- }
+ private SaslExchange startExchange(SMTPSession session, SaslMechanism
mechanism, SaslInitialRequest request) {
+ if (mechanism instanceof AuthHookSaslMechanism authHookSaslMechanism) {
Review Comment:
Why sasl mechanism specific internals leaks in here ?
##########
server/apps/scaling-pulsar-smtp/src/main/java/org/apache/james/UsersRepositoryBackedAuthenticator.java:
##########
@@ -0,0 +1,48 @@
+/****************************************************************
+ * 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 java.util.Optional;
+
+import jakarta.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.mailbox.Authenticator;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+
+class UsersRepositoryBackedAuthenticator implements Authenticator {
Review Comment:
Why is this needed ?
##########
protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java:
##########
@@ -165,356 +167,273 @@ private Response doAUTH(SMTPSession session, String
argument) {
argument = argument.substring(0,argument.indexOf(" "));
}
String authType = argument.toUpperCase(Locale.US);
- if (authType.equals(AUTH_TYPE_PLAIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
- return handlePlainContinuation(session, initialResponse);
- } else if (authType.equals(AUTH_TYPE_LOGIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
- return handleLoginAuthContinuation(session, initialResponse);
- } else if ((authType.equals(AUTH_TYPE_OAUTHBEARER) ||
authType.equals(AUTH_TYPE_XOAUTH2)) && session.supportsOAuth()) {
- return handleOauth2Continuation(session, initialResponse);
- } else {
- return doUnknownAuth(authType);
- }
+ return handleSaslAuthentication(session, authType,
Optional.ofNullable(initialResponse).map(String::trim));
}
}
- private Response handlePlainContinuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(String::trim)
- .map(userpass -> doPlainAuth(session, userpass))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String l) {
- return doPlainAuth(session, l);
- }
- });
- return AUTH_READY_USERNAME_LOGIN;
- });
- }
+ private Response handleSaslAuthentication(SMTPSession session, String
authType, Optional<String> initialResponse) {
+ if (authType.equals(AUTH_TYPE_LOGIN)) {
+ return handleLoginFraming(session, initialResponse);
+ }
- private Response handleLoginAuthContinuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(String::trim)
- .map(user -> doLoginAuthPass(session, user))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String
l) {
- return doLoginAuthPass(session, l);
- }
- });
- return AUTH_READY_USERNAME_LOGIN;
- });
+ Optional<SaslMechanism> maybeMechanism =
findAvailableMechanism(session, authType);
+ if (maybeMechanism.isEmpty()) {
+ return doUnknownAuth(authType);
+ }
+
+ SaslMechanism mechanism = maybeMechanism.get();
+ SaslExchange exchange;
+ try {
+ SaslInitialRequest request = SASL_BRIDGE.initialRequest(authType,
initialResponse);
+ exchange = startExchange(session, mechanism, request);
+ return handleFirstSaslStep(session, authType, exchange);
+ } catch (IllegalArgumentException e) {
+ LOGGER.info("Could not decode parameters for AUTH {}", authType,
e);
+ return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could
not decode parameters for AUTH " + authType);
+ }
}
- private Response handleOauth2Continuation(SMTPSession session, String
initialResponse) {
- return Optional.ofNullable(initialResponse)
- .map(token -> doOauth2Authentication(session, token))
- .orElseGet(() -> {
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String
l) {
- Response response = doOauth2Authentication(session, l);
- session.popLineHandler();
- return response;
- }
- });
- return new SMTPResponse(SMTPRetCode.AUTH_READY, "");
- });
+ private Response handleFirstSaslStep(SMTPSession session, String authType,
SaslExchange exchange) {
+ try {
+ SaslStep step = exchange.firstStep();
+ return handleSaslStep(session, authType, exchange, step);
+ } catch (RuntimeException e) {
+ SASL_BRIDGE.close(exchange);
+ throw e;
+ }
}
- private Response doOauth2Authentication(SMTPSession session, String
initialResponseString) {
- return session.getConfiguration().saslConfiguration()
- .map(oidcSASLConfiguration -> hooks.stream()
- .flatMap(hook -> Optional.ofNullable(executeHook(session, hook,
- hook2 -> hook2.doSasl(session, oidcSASLConfiguration,
initialResponseString))).stream())
- .filter(response ->
!SMTPRetCode.AUTH_FAILED.equals(response.getRetCode()))
- .findFirst()
- .orElseGet(() -> failSasl(oidcSASLConfiguration, session)))
- .orElseGet(() -> doUnknownAuth(AUTH_TYPE_OAUTHBEARER));
+ private Response handleSaslStep(SMTPSession session, String authType,
SaslExchange exchange, SaslStep step) {
+ return switch (step) {
+ case SaslStep.Challenge challenge -> {
+ session.pushLineHandler(saslLineHandler(authType, exchange));
+ yield SASL_BRIDGE.challenge(challenge);
+ }
+ case SaslStep.Success success -> handleSaslSuccess(session,
authType, exchange, success);
+ case SaslStep.Failure failure -> handleSaslFailure(session,
authType, exchange, failure.failure());
+ };
}
- private Response failSasl(OidcSASLConfiguration saslConfiguration,
SMTPSession session) {
- String rawResponse =
String.format("{\"status\":\"invalid_token\",\"scope\":\"%s\",\"schemes\":\"%s\"}",
- saslConfiguration.getScope(),
- saslConfiguration.getOidcConfigurationURL().toString());
+ private Response handleSaslContinuation(SMTPSession session, String
authType, SaslExchange exchange, String line) {
+ try {
+ SaslStep step = SASL_BRIDGE.onClientResponse(exchange,
line.getBytes(session.getCharset()));
+ return switch (step) {
+ case SaslStep.Challenge challenge ->
SASL_BRIDGE.challenge(challenge);
+ case SaslStep.Success success -> {
+ session.popLineHandler();
+ yield handleSaslSuccess(session, authType, exchange,
success);
+ }
+ case SaslStep.Failure failure -> {
+ session.popLineHandler();
+ yield handleSaslFailure(session, authType, exchange,
failure.failure());
+ }
+ };
+ } catch (IllegalArgumentException e) {
+ LOGGER.info("Could not decode parameters for AUTH {}", authType,
e);
+ session.popLineHandler();
+ SASL_BRIDGE.close(exchange);
+ return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could
not decode parameters for AUTH " + authType);
+ } catch (RuntimeException e) {
+ session.popLineHandler();
+ SASL_BRIDGE.close(exchange);
+ throw e;
+ }
+ }
- session.pushLineHandler(new AbstractSMTPLineHandler() {
- @Override
- protected Response onCommand(SMTPSession session, String l) {
+ private LineHandler<SMTPSession> saslLineHandler(String authType,
SaslExchange exchange) {
+ return (session, line) -> {
+ if (SASL_BRIDGE.isAbort(line)) {
session.popLineHandler();
-
- return AUTH_FAILED;
+ SASL_BRIDGE.abort(exchange);
+ return AUTH_ABORTED;
}
- });
- return new SMTPResponse("334",
Base64.getEncoder().encodeToString(rawResponse.getBytes()));
+ return handleSaslContinuation(session, authType, exchange, new
String(line, session.getCharset()));
+ };
}
- /**
- * Carries out the Plain AUTH SASL exchange.
- *
- * According to RFC 2595 the client must send: [authorize-id] \0
authenticate-id \0 password.
- *
- * >>> AUTH PLAIN dGVzdAB0ZXN0QHdpei5leGFtcGxlLmNvbQB0RXN0NDI=
- * Decoded: test\[email protected]\000tEst42
- *
- * >>> AUTH PLAIN dGVzdAB0ZXN0AHRFc3Q0Mg==
- * Decoded: test\000test\000tEst42
- *
- * @param session SMTP session object
- * @param line the initial response line passed in with the AUTH command
- */
- private Response doPlainAuth(SMTPSession session, String line) {
+ private Response handleSaslSuccess(SMTPSession session, String authType,
SaslExchange exchange, SaslStep.Success success) {
+ if (success.serverData().isPresent()) {
+
session.pushLineHandler(successDataAcknowledgementLineHandler(authType,
exchange, success));
+ return SASL_BRIDGE.successData(success);
+ }
try {
+ return applySaslSuccess(session, authType, exchange, success);
+ } finally {
+ SASL_BRIDGE.close(exchange);
+ }
+ }
- AuthValues authValues =
- Optional.ofNullable(decodeBase64(line))
- .flatMap(AuthCmdHandler::parseAuthValues)
- .orElseThrow(() -> new
IllegalArgumentException("Can't parse line as authentication values"));
-
- Response response;
+ private LineHandler<SMTPSession>
successDataAcknowledgementLineHandler(String authType, SaslExchange exchange,
SaslStep.Success success) {
+ return (session, line) ->
handleSaslSuccessDataAcknowledgement(session, authType, exchange, success, new
String(line, session.getCharset()));
+ }
- if (authValues.password.isEmpty()) {
- response = doDelegation(session, authValues.username);
- } else {
- response = doAuthTest(session,
Optional.of(authValues.username), authValues.password, AUTH_TYPE_PLAIN);
+ private Response handleSaslSuccessDataAcknowledgement(SMTPSession session,
String authType, SaslExchange exchange,
+ SaslStep.Success
success, String line) {
+ session.popLineHandler();
+ boolean aborted = false;
+ try {
+ byte[] bytes = line.getBytes(session.getCharset());
+ if (SASL_BRIDGE.isAbort(bytes)) {
+ aborted = true;
+ SASL_BRIDGE.abort(exchange);
+ return AUTH_ABORTED;
+ }
+ if (!SASL_BRIDGE.isEmptyClientResponse(bytes)) {
+ return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,
"Could not decode parameters for AUTH " + authType);
+ }
+ return applySaslSuccess(session, authType, exchange, success);
+ } finally {
+ if (!aborted) {
+ SASL_BRIDGE.close(exchange);
}
-
- session.popLineHandler();
- return response;
- } catch (Exception e) {
- LOGGER.info("Could not decode parameters for AUTH PLAIN", e);
- return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could
not decode parameters for AUTH PLAIN");
}
}
- @VisibleForTesting
- static Optional<AuthValues> parseAuthValues(String input) {
-
- List<String> parts =
Splitter.on('\0').splitToStream(input).filter(token ->
!token.isBlank()).toList();
-
- return switch (parts.size()) {
- case 1 -> Optional.of(new AuthValues(Username.of(parts.get(0)),
Optional.empty()));
- // If we got here, this is what happened. RFC 2595
- // says that "the client may leave the authorization
- // identity empty to indicate that it is the same as
- // the authentication identity." As noted above,
- // that would be represented as a decoded string of
- // the form: "\0authenticate-id\0password". The
- // first call to nextToken will skip the empty
- // authorize-id, and give us the authenticate-id,
- // which we would store as the authorize-id. The
- // second call will give us the password, which we
- // think is the authenticate-id (user). Then when
- // we ask for the password, there are no more
- // elements, leading to the exception we just
- // caught. So we need to move the user to the
- // password, and the authorize_id to the user.
- case 2 -> Optional.of(new AuthValues(Username.of(parts.get(0)),
Optional.of(parts.get(1))));
- case 3 -> Optional.of(new AuthValues(Username.of(parts.get(1)),
Optional.of(parts.get(2))));
- default -> Optional.empty();
- };
+ private Response applySaslSuccess(SMTPSession session, String authType,
SaslExchange exchange, SaslStep.Success success) {
+ Username username = success.identity().authorizationId();
+ session.setUsername(username);
+ session.setRelayingAllowed(true);
+ saslAuthResultHooks.forEach(hook -> hook.onSuccess(session, authType,
success.identity()));
+
+ AUTHENTICATION_DEDICATED_LOGGER.debug("AUTH method {} succeeded",
authType);
+
+ AuditTrail.entry()
+ .username(username::asString)
+ .remoteIP(() -> Optional.ofNullable(session.getRemoteAddress()))
+ .sessionId(session::getSessionID)
+ .protocol("SMTP")
+ .action("AUTH")
+ .parameters(() -> ImmutableMap.of("authType", authType))
+ .log("SMTP Authentication succeeded.");
+
+ return AuthHookSaslMechanism.terminalResponse(exchange)
+ .orElse(AUTH_SUCCEEDED);
}
- record AuthValues(Username username, Optional<String> password) {
+ private Response handleSaslFailure(SMTPSession session, String authType,
SaslExchange exchange, SaslFailure failure) {
+ try {
+ saslAuthResultHooks.forEach(hook -> hook.onFailure(session,
authType, failure));
+
+ failure.authenticationId().ifPresent(username -> AuditTrail.entry()
+ .username(username::asString)
+ .remoteIP(() ->
Optional.ofNullable(session.getRemoteAddress()))
+ .protocol("SMTP")
+ .action("AUTH")
+ .parameters(() -> ImmutableMap.of("authType", authType))
+ .log("SMTP Authentication failed."));
+
+ return AuthHookSaslMechanism.terminalResponse(exchange)
+ .orElseGet(() -> switch (failure.type()) {
+ case MALFORMED -> new
SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could not decode parameters
for AUTH " + authType);
+ case SERVER_ERROR -> SERVER_ERROR;
+ case INVALID_CREDENTIALS, AUTHENTICATION_FAILED,
USER_DOES_NOT_EXIST, DELEGATION_FORBIDDEN -> AUTH_FAILED;
+ });
+ } finally {
+ SASL_BRIDGE.close(exchange);
+ }
}
- private String decodeBase64(String line) {
- if (line != null) {
- String lineWithoutTrailingCrLf = StringUtils.replace(line, "\r\n",
"");
- return new
String(Base64.getDecoder().decode(lineWithoutTrailingCrLf),
StandardCharsets.UTF_8);
+ private Response handleLoginFraming(SMTPSession session, Optional<String>
initialResponse) {
Review Comment:
Maybe it would be easier to handle login as a dedicated Sasl mechanism
registered only for SMTP
Then we won't have this specific handling here.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]