Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements #449 Make authentication details available in MovingMessage #450

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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