Skip to content

Commit

Permalink
Support for POP3 SASL Plain (fixes #301)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelmay committed Apr 3, 2020
1 parent 7d177fd commit 14ad55b
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@
*/
package com.icegreen.greenmail.pop3;

import java.io.IOException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.StringTokenizer;

import com.icegreen.greenmail.pop3.commands.Pop3Command;
import com.icegreen.greenmail.pop3.commands.Pop3CommandRegistry;
import com.icegreen.greenmail.server.BuildInfo;
import com.icegreen.greenmail.server.ProtocolHandler;
import com.icegreen.greenmail.user.UserManager;

import java.io.IOException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.StringTokenizer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Pop3Handler implements ProtocolHandler {
protected final Logger log = LoggerFactory.getLogger(getClass());
Pop3CommandRegistry registry;
Pop3Connection conn;
UserManager manager;
Expand Down Expand Up @@ -52,12 +53,16 @@ public void run() {
conn.close();
} catch (SocketTimeoutException ste) {
conn.println("421 Service shutting down and closing transmission channel");

} catch (Exception e) {
if (!quitting) {
log.error("Can not handle POP3 connection", e);
throw new IllegalStateException("Can not handle POP3 connection", e);
}
} finally {
try {
socket.close();
} catch (IOException ioe) {
// Nothing
}
}

Expand All @@ -77,20 +82,17 @@ void handleCommand()
return;
}

String commandName = new StringTokenizer(currentLine, " ").nextToken()
.toUpperCase();
String commandName = new StringTokenizer(currentLine, " ").nextToken().toUpperCase();

Pop3Command command = registry.getCommand(commandName);

if (command == null) {
conn.println("-ERR Command not recognized");

return;
}

if (!command.isValidForState(state)) {
conn.println("-ERR Command not valid for this state");

return;
}

Expand All @@ -99,12 +101,12 @@ void handleCommand()

@Override
public void close() {
quitting = true;
quitting = true;
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch(IOException ignored) {
} catch (IOException ignored) {
//empty
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public GreenMailUser getUser() {
public GreenMailUser getUser(String username) throws UserException {
GreenMailUser user = manager.getUser(username);
if (null == user) {
throw new NoSuchUserException(username + " doesn't exist");
throw new NoSuchUserException("User <" + username + "> doesn't exist");
}
return user;
}
Expand All @@ -57,7 +57,6 @@ public void authenticate(String pass)
}

public MailFolder getFolder() {

return inbox;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2014 Wael Chatila / Icegreen Technologies. All Rights Reserved.
* This software is released under the Apache license 2.0
* This file has been used and modified.
* Original file can be found on http://foedus.sourceforge.net
*/
package com.icegreen.greenmail.pop3.commands;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import com.icegreen.greenmail.pop3.Pop3Connection;
import com.icegreen.greenmail.pop3.Pop3State;
import com.icegreen.greenmail.store.FolderException;
import com.icegreen.greenmail.user.GreenMailUser;
import com.icegreen.greenmail.user.UserException;
import com.icegreen.greenmail.util.EncodingUtil;
import com.sun.mail.util.BASE64DecoderStream;

/**
* SASL : PLAIN
* <p>
* https://tools.ietf.org/html/rfc5034
* <p>
* AUTH mechanism [initial-response]
* mechanism PLAIN : See https://tools.ietf.org/html/rfc4616
*/
public class AuthCommand
extends Pop3Command {

public static final String CONTINUATION = "+ ";

@Override
public boolean isValidForState(Pop3State state) {

return !state.isAuthenticated();
}

public enum Pop3SaslAuthMechanism {
PLAIN;

/**
* WS separated list of supported auth mechanism.
*
* @return a list of supported auth mechanism.
*/
static String list() {
StringBuilder buf = new StringBuilder();
for (Pop3SaslAuthMechanism mechanism : values()) {
if (buf.length() > 0) {
buf.append(' ');
}
buf.append(mechanism.name());
}
return buf.toString();
}
}

@Override
public void execute(Pop3Connection conn, Pop3State state, String cmd) {
if (state.isAuthenticated()) {
conn.println("-ERR Already authenticated");
return;
}

String[] args = cmd.split(" ");
if (args.length < 2) {
conn.println("-ERR Required syntax: AUTH mechanism [initial-response]");
return;
}

String mechanism = args[1];
if (Pop3SaslAuthMechanism.PLAIN.name().equalsIgnoreCase(mechanism)) {
authPlain(conn, state, args);
} else {
conn.println("-ERR Required syntax: AUTH mechanism <" + mechanism +
"> not supported, expected one of " + Arrays.toString(Pop3SaslAuthMechanism.values()));
}
}

private void authPlain(Pop3Connection conn, Pop3State state, String[] args) {
// https://tools.ietf.org/html/rfc4616
String initialResponse;
if (args.length == 2 || args.length == 3 && "=".equals(args[2])) { // Continuation?
conn.println(CONTINUATION);
try {
initialResponse = conn.readLine();
} catch (IOException e) {
conn.println("-ERR Invalid syntax, expected continuation with iniital-response");
return;
}
} else if (args.length == 3) {
initialResponse = args[2];
} else {
conn.println("-ERR Invalid syntax, expected initial-response : AUTH PLAIN [initial-response]");
return;
}

// authorization-id\0authentication-id\0passwd
final BASE64DecoderStream stream = new BASE64DecoderStream(
new ByteArrayInputStream(initialResponse.getBytes(StandardCharsets.UTF_8)));
readTillNullChar(stream); // authorizationId Not used
String authenticationId = readTillNullChar(stream);

GreenMailUser user;
try {
user = state.getUser(authenticationId);
state.setUser(user);
} catch (UserException e) {
log.error("Can not get user <" + authenticationId + ">", e);
conn.println("-ERR Authentication failed: " + e.getMessage() /* GreenMail is just a test server */);
return;
}

try {
state.authenticate(readTillNullChar(stream));
conn.println("+OK");
} catch (UserException e) {
log.error("Can not authenticate using user <" + user.getLogin() + ">", e);
conn.println("-ERR Authentication failed: " + e.getMessage());
} catch (FolderException e) {
log.error("Can not authenticate using user " + user + ", internal error", e);
conn.println("-ERR Authentication failed, internal error: " + e.getMessage());
}
}


@Deprecated // Remove once JDK baseline is 1.8
private String readTillNullChar(BASE64DecoderStream stream) {
try {
return EncodingUtil.readTillNullChar(stream);
} catch (IOException e) {
log.error("Can not decode", e);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public void execute(Pop3Connection conn, Pop3State state, String cmd) {
// We don't support any additional capabilities
conn.println("+OK");
conn.println("UIDL");
conn.println("SASL "+ AuthCommand.Pop3SaslAuthMechanism.list());
conn.println(".");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@

import com.icegreen.greenmail.pop3.Pop3Connection;
import com.icegreen.greenmail.pop3.Pop3State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public abstract class Pop3Command {
protected final Logger log = LoggerFactory.getLogger(getClass());

public abstract boolean isValidForState(Pop3State state);

public abstract void execute(Pop3Connection conn, Pop3State state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class Pop3CommandRegistry {
commands.put("NOOP", new NoopCommand());
commands.put("RSET", new RsetCommand());
commands.put("CAPA", new CapaCommand());
commands.put("AUTH", new AuthCommand());
}

public Pop3Command getCommand(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ public void execute(SmtpConnection conn, SmtpState state,
}

private void authPlain(SmtpConnection conn, SmtpManager manager, String[] commandParts) throws IOException {
AuthMechanism authMechanism = AuthMechanism.PLAIN;

// Continuation?
String initialResponse;
if (commandParts.length == 2) {
Expand All @@ -86,7 +84,7 @@ private void authPlain(SmtpConnection conn, SmtpManager manager, String[] comman
initialResponse = commandParts[2];
}

if (authenticate(manager.getUserManager(), authMechanism, initialResponse)) {
if (authenticate(manager.getUserManager(), initialResponse)) {
conn.setAuthenticated(true);
conn.send(AUTH_SUCCEDED);
} else {
Expand Down Expand Up @@ -114,27 +112,20 @@ private void authLogin(SmtpConnection conn, SmtpManager manager, String commandL
}
}

private boolean authenticate(UserManager userManager, AuthMechanism authMechanism, String value) {
if (AuthMechanism.PLAIN == authMechanism) {
// authorization-id\0authentication-id\0passwd
final BASE64DecoderStream stream = new BASE64DecoderStream(
new ByteArrayInputStream(value.getBytes(StandardCharsets.US_ASCII)));
readTillNullChar(stream); // authorizationId Not used
String authenticationId = readTillNullChar(stream);
String passwd = readTillNullChar(stream);
return userManager.test(authenticationId, passwd);
}
return false;
private boolean authenticate(UserManager userManager, String value) {
// authorization-id\0authentication-id\0passwd
final BASE64DecoderStream stream = new BASE64DecoderStream(
new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)));
readTillNullChar(stream); // authorizationId Not used
String authenticationId = readTillNullChar(stream);
String passwd = readTillNullChar(stream);
return userManager.test(authenticationId, passwd);
}

@Deprecated // Remove once JDK baseline is 1.8
private String readTillNullChar(BASE64DecoderStream stream) {
try {
StringBuilder buf = new StringBuilder();
for (int chr = stream.read(); chr != '\0' && chr > 0; chr = stream.read()) {
buf.append((char) chr);
}
return buf.toString();
return EncodingUtil.readTillNullChar(stream);
} catch (IOException e) {
log.error("Can not decode", e);
return null;
Expand All @@ -144,7 +135,7 @@ private String readTillNullChar(BASE64DecoderStream stream) {
private static String getValuesWsSeparated() {
StringBuilder buf = new StringBuilder();
for (AuthMechanism mechanism : AuthMechanism.values()) {
if(buf.length()>0) {
if (buf.length() > 0) {
buf.append(' ');
}
buf.append(mechanism);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import com.sun.mail.util.BASE64DecoderStream;

/**
* Helper for handling encodings.
*/
Expand Down Expand Up @@ -60,4 +62,13 @@ public static String toString(InputStream is, Charset charset) {
public static String decodeBase64(String encoded) {
return new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
}

@Deprecated // Remove once JDK baseline is 1.8
public static String readTillNullChar(BASE64DecoderStream stream) throws IOException {
StringBuilder buf = new StringBuilder();
for (int chr = stream.read(); chr != '\u0000' && chr > 0; chr = stream.read()) {
buf.append((char) chr);
}
return buf.toString();
}
}
Loading

0 comments on commit 14ad55b

Please sign in to comment.