com.dumbster.smtp.SimpleSmtpServer Maven / Gradle / Ivy
/*
* Dumbster - a dummy SMTP server
* Copyright 2016 Joachim Nicolay
* Copyright 2004 Jason Paul Kitchen
*
* Licensed 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 com.dumbster.smtp;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;
/** Dummy SMTP server for testing purposes. */
@Slf4j
public final class SimpleSmtpServer implements AutoCloseable {
/** Default SMTP port is 25. */
public static final int DEFAULT_SMTP_PORT = 25;
/** pick any free port. */
public static final int AUTO_SMTP_PORT = 0;
/** When stopping wait this long for any still ongoing transmission */
private static final int STOP_TIMEOUT = 20000;
private static final Pattern CRLF = Pattern.compile("\r\n");
/** Stores all of the email received since this instance started up. */
private final List receivedMail;
/** The server socket this server listens to. */
private final ServerSocket serverSocket;
/** Thread that does the work. */
private final Thread workerThread;
/** Indicates the server thread that it should stop */
private volatile boolean stopped = false;
/**
* Creates an instance of a started SimpleSmtpServer.
*
* @param port port number the server should listen to
* @return a reference to the running SMTP server
* @throws IOException when listening on the socket causes one
*/
public static SimpleSmtpServer start(int port) throws IOException {
return new SimpleSmtpServer(new ServerSocket(Math.max(port, 0)));
}
/**
* private constructor because factory method {@link #start(int)} better indicates that
* the created server is already running
* @param serverSocket socket to listen on
*/
private SimpleSmtpServer(ServerSocket serverSocket) {
this.receivedMail = new ArrayList<>();
this.serverSocket = serverSocket;
this.workerThread = new Thread(
new Runnable() {
@Override
public void run() {
performWork();
}
});
this.workerThread.start();
}
/**
* @return the port the server is listening on
*/
public int getPort() {
return serverSocket.getLocalPort();
}
/**
* @return list of {@link SmtpMessage}s received by since start up or last reset.
*/
public List getReceivedEmails() {
synchronized (receivedMail) {
return Collections.unmodifiableList(new ArrayList<>(receivedMail));
}
}
/**
* forgets all received emails
*/
public void reset() {
synchronized (receivedMail) {
receivedMail.clear();
}
}
/**
* Stops the server. Server is shutdown after processing of the current request is complete.
*/
public void stop() {
if (stopped) {
return;
}
// Mark us closed
stopped = true;
try {
// Kick the server accept loop
serverSocket.close();
} catch (IOException e) {
log.warn("trouble closing the server socket", e);
}
// and block until worker is finished
try {
workerThread.join(STOP_TIMEOUT);
} catch (InterruptedException e) {
log.warn("interrupted when waiting for worker thread to finish", e);
}
}
/**
* synonym for {@link #stop()}
*/
@Override
public void close() {
stop();
}
/**
* Main loop of the SMTP server.
*/
private void performWork() {
try {
// Server: loop until stopped
while (!stopped) {
// Start server socket and listen for client connections
//noinspection resource
try (Socket socket = serverSocket.accept();
Scanner input = new Scanner(new InputStreamReader(socket.getInputStream(), StandardCharsets.ISO_8859_1)).useDelimiter(CRLF);
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.ISO_8859_1));) {
synchronized (receivedMail) {
/*
* We synchronize over the handle method and the list update because the client call completes inside
* the handle method and we have to prevent the client from reading the list until we've updated it.
*/
receivedMail.addAll(handleTransaction(out, input));
}
}
}
} catch (Exception e) {
// SocketException expected when stopping the server
if (!stopped) {
log.error("hit exception when running server", e);
try {
serverSocket.close();
} catch (IOException ex) {
log.error("and one when closing the port", ex);
}
}
}
}
/**
* Handle an SMTP transaction, i.e. all activity between initial connect and QUIT command.
*
* @param out output stream
* @param input input stream
* @return List of SmtpMessage
* @throws IOException
*/
private static List handleTransaction(PrintWriter out, Iterator input) throws IOException {
// Initialize the state machine
SmtpState smtpState = SmtpState.CONNECT;
SmtpRequest smtpRequest = new SmtpRequest(SmtpActionType.CONNECT, "", smtpState);
// Execute the connection request
SmtpResponse smtpResponse = smtpRequest.execute();
// Send initial response
sendResponse(out, smtpResponse);
smtpState = smtpResponse.getNextState();
List msgList = new ArrayList<>();
SmtpMessage msg = new SmtpMessage();
while (smtpState != SmtpState.CONNECT) {
String line = input.next();
if (line == null) {
break;
}
// Create request from client input and current state
SmtpRequest request = SmtpRequest.createRequest(line, smtpState);
// Execute request and create response object
SmtpResponse response = request.execute();
// Move to next internal state
smtpState = response.getNextState();
// Send response to client
sendResponse(out, response);
// Store input in message
String params = request.params;
msg.store(response, params);
// If message reception is complete save it
if (smtpState == SmtpState.QUIT) {
msgList.add(msg);
msg = new SmtpMessage();
}
}
return msgList;
}
/**
* Send response to client.
*
* @param out socket output stream
* @param smtpResponse response object
*/
private static void sendResponse(PrintWriter out, SmtpResponse smtpResponse) {
if (smtpResponse.getCode() > 0) {
int code = smtpResponse.getCode();
String message = smtpResponse.getMessage();
out.print(code + " " + message + "\r\n");
out.flush();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy