Skip to content

Commit

Permalink
Implements greenmail-mail-test#449 Make authentication details availa…
Browse files Browse the repository at this point in the history
…ble in MovingMessage

* Implements greenmail-mail-test#449: Stores the authentication details in the moving
message so that it is available in e.g. MessageDeliveryHandler
* Since different authentication methods have different details, there's
a AuthenticationState base interface with PlainAuthenticationState (with
an additional authorizationId) and LoginAuthenticationState POJOs for
the currently supported AUTH mechanisms LOGIN and AUTH. Other AUTH
mechanisms may or may not have a username and/or password.
* What I don't fully understand is why the SMTPState needs to be cleared
so often. It's also cleared by the MailCommand, which removes the
authentication details from the MovingMessage. I modfied the MailCommand
so that it clears the SMTPState, but transfers the authentication
details from the old moving message to the new one. We could also store
the authentication details in the SMTPState as well, but that feels a
bit redundant.
* When I run the tests locally, the
DummySSLServerSocketFactoryTest#testLoadKeyStoreViaSystemProperty fails,
but that's because I don't have a certificate with an `amazonrootca1` in
my system root key store (there is, e.g. a `debian:amazon_root_ca_1.pem`
certificate and when I change the test to use that, it passes)
  • Loading branch information
blutorange committed Mar 29, 2022
1 parent 22e8461 commit 6abf159
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Expand Up @@ -224,7 +224,7 @@ pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties

dependency-reduced-pom.xml

### Gradle ###
.gradle
Expand Down
Expand Up @@ -10,39 +10,110 @@
import java.util.LinkedList;
import java.util.List;

import com.icegreen.greenmail.smtp.auth.AuthenticationState;
import com.icegreen.greenmail.smtp.auth.LoginAuthenticationState;
import com.icegreen.greenmail.smtp.auth.PlainAuthenticationState;


/**
* Contains information for delivering a mime email.
*/
public class MovingMessage {
private AuthenticationState authenticationState;
private MailAddress returnPath;
private final List<MailAddress> toAddresses = new LinkedList<>();
private MimeMessage message;

/**
* Retrieves the state object with the data used for authentication. Currently
* {@link PlainAuthenticationState PLAIN} and {@link LoginAuthenticationState LOGIN}
* authentication is supported. You can use this, for example, to retrieve the username
* or password that was sent by the client.
*
* Note that this will return {@code null} when no authentication was performed or needed.
*
* @return The state used by the AUTH command, if any.
*/
public AuthenticationState getAuthenticationState() {
return authenticationState;
}

/**
* Retrieves the state object with the data used for authentication. Currently
* {@link PlainAuthenticationState PLAIN} and {@link LoginAuthenticationState LOGIN}
* authentication is supported. You can use this, for example, to retrieve the username
* or password that was sent by the client.
*
* Note that this will return {@code null} when no authentication was performed or needed.
*
* @return The state used by the AUTH command, if any.
*/
public void setAuthenticationState(AuthenticationState authenticationState) {
this.authenticationState = authenticationState;
}

/**
* Retrieves the addresses from which the email was sent. Note that these are
* the {@code RCPT TO} addresses from the SMTP envelope, not the {@code TO}
* addresses from the mail header.
* @return The address to which the mail is directed.
*/
public List<MailAddress> getToAddresses() {
return toAddresses;
}

/**
* Retrieves the contents of the mail message, including all mail headers and the body.
* @return The message that was sent.
*/
public MimeMessage getMessage() {
return message;
}

/**
* Retrieves the address from which the email was sent. Note that this is the
* {@code MAIL FROM} address from the SMTP envelope, not the {@code FROM}
* address(es) from the mail header.
* @return The address from which the email was sent.
*/
public MailAddress getReturnPath() {
return returnPath;
}

/**
* Sets or overwrites the address from which the email was sent. Note that this is
* the {@code MAIL FROM} address from the SMTP envelope, not the {@code FROM}
* address(es) from the mail header.
* @param fromAddress The address from which the email was sent.
*/
public void setReturnPath(MailAddress fromAddress) {
this.returnPath = fromAddress;
}

/**
* Adds an address from which the email was sent. Note that these are the {@code RCPT TO}
* addresses from the SMTP envelope, not the {@code TO} addresses from the mail header.
* @return The address to which the mail is directed.
*/
public void addRecipient(MailAddress s) {
toAddresses.add(s);
}

/**
* Removes an address from the list of addresses from which the email was sent. Note
* that these are the {@code RCPT TO} addresses from the SMTP envelope, not the {@code TO}
* addresses from the mail header.
* @return An address to remove form the addresses to which the mail is directed.
*/
public void removeRecipient(MailAddress s) {
toAddresses.remove(s);
}

/**
* Sets or overwrites the contents of the mail message, including all mail headers
* and the body.
* @param message The message that was sent.
*/
public void setMimeMessage(MimeMessage message) {
this.message = message;
}
Expand Down
Expand Up @@ -7,6 +7,7 @@
package com.icegreen.greenmail.smtp;

import com.icegreen.greenmail.mail.MovingMessage;
import com.icegreen.greenmail.smtp.auth.AuthenticationState;

public class SmtpState {
MovingMessage currentMessage;
Expand All @@ -25,4 +26,14 @@ public MovingMessage getMessage() {
public void clearMessage() {
currentMessage = new MovingMessage();
}

/**
* To destroy a half-constructed message, but preserves the
* {@link MovingMessage#getAuthenticationState()}.
*/
public void clearMessagePreservingAuthenticationState() {
AuthenticationState authState = currentMessage != null ? currentMessage.getAuthenticationState() : null;
currentMessage = new MovingMessage();
currentMessage.setAuthenticationState(authState);
}
}
@@ -0,0 +1,14 @@
package com.icegreen.greenmail.smtp.auth;

/**
* Base interface for the state used by various authentication methods. The data contained
* in the state depends on the authentication method that was used. The state can be
* mutable, such as when the authentication method requires multiple exchanges between the
* client and the server.
*/
public interface AuthenticationState {
/**
* @return The type of the used authentication mechanism, e.g. {@code PLAIN} or {@code LOGIN}.
*/
String getType();
}
@@ -0,0 +1,45 @@
package com.icegreen.greenmail.smtp.auth;

import com.icegreen.greenmail.smtp.commands.AuthCommand.AuthMechanism;

/**
* Details from the {@link AuthMechanism#LOGIN} authorization mechanism, when
* that mechanism was used for authentication.
*/
public class LoginAuthenticationState implements AuthenticationState, UsernameAuthentication {
private final String username;

private final String password;

/**
* @param username The username from the AUTH command.
* @param password The password from the AUTH command.
*/
public LoginAuthenticationState(String username, String password) {
this.username = username;
this.password = password;
}

@Override
public String getType() {
return AuthMechanism.LOGIN.name();
}

/**
* Retrieves the username that was used for {@code PLAIN} or {@code LOGIN} authentication.
* Note that this will return {@code null} when no authentication was performed or needed.
* @return The username from the AUTH command.
*/
public String getUsername() {
return username;
}

/**
* Retrieves the password that was used for {@code PLAIN} or {@code LOGIN} authentication.
* Note that this will return {@code null} when no authentication was performed or needed.
* @return The password from the AUTH command.
*/
public String getPassword() {
return password;
}
}
@@ -0,0 +1,63 @@
package com.icegreen.greenmail.smtp.auth;

import com.icegreen.greenmail.smtp.commands.AuthCommand.AuthMechanism;
import com.icegreen.greenmail.util.SaslMessage;

/**
* Details from the {@link AuthMechanism#PLAIN} authorization mechanism, when
* that mechanism was used for authentication.
*/
public class PlainAuthenticationState implements AuthenticationState, UsernameAuthentication {
private final String authorizationId;
private final String authenticationId;
private final String password;

/**
* @param saslMessage The parsed message sent by the client with the {@code AUTH} command.
*/
public PlainAuthenticationState(SaslMessage saslMessage) {
this(saslMessage.getAuthzid(), saslMessage.getAuthcid(), saslMessage.getPasswd());
}

@Override
public String getType() {
return AuthMechanism.PLAIN.name();
}

/**
* @param authorizationId The authorization ID sent by the client with the {@code AUTH} command.
* @param authenticationId The authentication ID sent by the client with the {@code AUTH} command.
* @param password The password sent by the client with the {@code AUTH} command.
*/
public PlainAuthenticationState(String authorizationId, String authenticationId, String password) {
this.authorizationId = authorizationId;
this.authenticationId = authenticationId;
this.password = password;
}

/**
* @return The authorization ID sent by the client with the {@code AUTH} command.
*/
public String getAuthorizationId() {
return authorizationId;
}

/**
* @return The authentication ID sent by the client with the {@code AUTH} command.
*/
public String getAuthenticationId() {
return authenticationId;
}

/**
* @return password The password sent by the client with the {@code AUTH} command.
*/
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return authenticationId;
}
}
@@ -0,0 +1,10 @@
package com.icegreen.greenmail.smtp.auth;

/**
* Base interface for authentication methods that provide or make use of
* a plain text username to identify a user, such as the {@code PLAIN} or
* {@code LOGIN} authentication mechanisms.
*/
public interface UsernameAuthentication {
String getUsername();
}
Expand Up @@ -9,6 +9,9 @@
import com.icegreen.greenmail.smtp.SmtpConnection;
import com.icegreen.greenmail.smtp.SmtpManager;
import com.icegreen.greenmail.smtp.SmtpState;
import com.icegreen.greenmail.smtp.auth.AuthenticationState;
import com.icegreen.greenmail.smtp.auth.LoginAuthenticationState;
import com.icegreen.greenmail.smtp.auth.PlainAuthenticationState;
import com.icegreen.greenmail.user.UserManager;
import com.icegreen.greenmail.util.EncodingUtil;
import com.icegreen.greenmail.util.SaslMessage;
Expand Down Expand Up @@ -62,16 +65,16 @@ public void execute(SmtpConnection conn, SmtpState state,
// Check auth mechanism
final String authMechanismValue = commandParts[1];
if (AuthMechanism.LOGIN.name().equalsIgnoreCase(authMechanismValue)) {
authLogin(conn, manager, commandLine, commandParts, authMechanismValue);
authLogin(conn, state, manager, commandLine, commandParts, authMechanismValue);
} else if (AuthMechanism.PLAIN.name().equalsIgnoreCase(authMechanismValue)) {
authPlain(conn, manager, commandParts);
authPlain(conn, state, manager, commandParts);
} else {
conn.send(SMTP_SYNTAX_ERROR + " : Unsupported auth mechanism " + authMechanismValue +
". Only auth mechanism <" + Arrays.toString(AuthMechanism.values()) + "> supported.");
}
}

private void authPlain(SmtpConnection conn, SmtpManager manager, String[] commandParts) throws IOException {
private void authPlain(SmtpConnection conn, SmtpState state, SmtpManager manager, String[] commandParts) throws IOException {
// Continuation?
String initialResponse;
if (commandParts.length == 2) {
Expand All @@ -80,17 +83,21 @@ private void authPlain(SmtpConnection conn, SmtpManager manager, String[] comman
} else {
initialResponse = commandParts[2];
}

SaslMessage saslMessage = parseInitialResponse(initialResponse);
AuthenticationState authenticationContext = new PlainAuthenticationState(saslMessage);
state.getMessage().setAuthenticationState(authenticationContext);

if (authenticate(manager.getUserManager(), EncodingUtil.decodeBase64(initialResponse))) {
if (authenticate(manager.getUserManager(), saslMessage)) {
conn.setAuthenticated(true);
conn.send(AUTH_SUCCEDED);
} else {
conn.send(AUTH_CREDENTIALS_INVALID);
}
}

private void authLogin(SmtpConnection conn, SmtpManager manager, String commandLine, String[] commandParts,
String authMechanismValue) throws IOException {
private void authLogin(SmtpConnection conn, SmtpState state, SmtpManager manager, String commandLine,
String[] commandParts, String authMechanismValue) throws IOException {
// https://www.samlogic.net/articles/smtp-commands-reference-auth.htm
if (commandParts.length != 2) {
conn.send(SMTP_SYNTAX_ERROR + " : Unsupported auth mechanism " + authMechanismValue +
Expand All @@ -100,8 +107,12 @@ private void authLogin(SmtpConnection conn, SmtpManager manager, String commandL
String username = conn.readLine();
conn.send(SMTP_SERVER_CONTINUATION + "UGFzc3dvcmQ6"); // "Password:"
String pwd = conn.readLine();
String plainUsername = EncodingUtil.decodeBase64(username);
String plainPwd = EncodingUtil.decodeBase64(pwd);
AuthenticationState authenticationContext = new LoginAuthenticationState(plainUsername, plainPwd);
state.getMessage().setAuthenticationState(authenticationContext);

if (manager.getUserManager().test(EncodingUtil.decodeBase64(username), EncodingUtil.decodeBase64(pwd))) {
if (manager.getUserManager().test(plainUsername, plainPwd)) {
conn.setAuthenticated(true);
conn.send(AUTH_SUCCEDED);
} else {
Expand All @@ -110,11 +121,15 @@ private void authLogin(SmtpConnection conn, SmtpManager manager, String commandL
}
}

private boolean authenticate(UserManager userManager, String value) {
// authorization-id\0authentication-id\0passwd
final SaslMessage saslMessage = SaslMessage.parse(value);
private boolean authenticate(UserManager userManager, SaslMessage saslMessage) {
return userManager.test(saslMessage.getAuthcid(), saslMessage.getPasswd());
}

private SaslMessage parseInitialResponse(String initialReponse) {
String value = EncodingUtil.decodeBase64(initialReponse);
// authorization-id\0authentication-id\0passwd
return SaslMessage.parse(value);
}

private static String getValuesWsSeparated() {
StringBuilder buf = new StringBuilder();
Expand Down
Expand Up @@ -45,11 +45,11 @@ public void execute(SmtpConnection conn, SmtpState state,
conn.send(err);
return;
}
state.clearMessage();
state.clearMessagePreservingAuthenticationState();
state.getMessage().setReturnPath(fromAddr);
conn.send("250 OK");
} else {
state.clearMessage();
state.clearMessagePreservingAuthenticationState();
state.getMessage();
conn.send("250 OK");
}
Expand Down

0 comments on commit 6abf159

Please sign in to comment.