简体   繁体   中英

How do I code my own SMTP Server using Java?

So I have a client written in Java that i want to use to test out sending email but instead of using an already existing SMTP like google, i want to have my own local server to test out sending mock emails between two mock emails.

I've been trying to look all over the inte.net for good sources on how to code a simple SMTP Server but i've had zero luck.

I do have a basic server code that when i run it, i can connect my Client to it but at the moment it won't handle any email functionality.

TCPServer.java


import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.*;
import java.net.*;

public class TCPServer{
    private ServerSocket server;

    /**
     * The TCPServer constructor initiate the socket
     * @param ipAddress
     * @param port
     * @throws Exception
     */
    public TCPServer(String ipAddress, int port) throws Exception {
        if (ipAddress != null && !ipAddress.isEmpty())
            this.server = new ServerSocket(port, 1, InetAddress.getByName(ipAddress));
        else
            this.server = new ServerSocket(0, 1, InetAddress.getLocalHost());
    }

    /**
     * The listen method listen to incoming client's datagrams and requests
     * @throws Exception
     */
    private void listen() throws Exception {
        // listen to incoming client's requests via the ServerSocket
        //add your code here
        String data = null;
        Socket client = this.server.accept();
        String clientAddress = client.getInetAddress().getHostAddress();
        System.out.println("\r\nNew client connection from " + clientAddress);

        // print received datagrams from client
        //add your code here
        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
        while ( (data = in.readLine()) != null ) {
            System.out.println("\r\nMessage from " + clientAddress + ": " + data);
            client.sendUrgentData(1);
        }
    }

    public InetAddress getSocketAddress() {
        return this.server.getInetAddress();
    }

    public int getPort() {
        return this.server.getLocalPort();
    }


    public static void main(String[] args) throws Exception {
        // set the server address (IP) and port number
        //add your code here
        String serverIP = "192.168.1.235"; // local IP address
        int port = 8088;

        if (args.length > 0) {
            serverIP = args[0];
            port = Integer.parseInt(args[1]);
        }
        // call the constructor and pass the IP and port
        //add your code here
        TCPServer server = new TCPServer(serverIP, port);
        System.out.println("\r\nRunning Server: " +
                "Host=" + server.getSocketAddress().getHostAddress() +
                " Port=" + server.getPort());
        server.listen();
    }

}

What can i add to my existing server code to make it handle email for my Client. I'll also post my email client as well.

ClientTester.java


import java.io.*;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

/**
 * This program demonstrates a TCP client
 * @author jl922223
 * @version 1.0
 * @since 2020-12-12
 */

public class ClientTester{
    private Socket tcpSocket;
    private InetAddress serverAddress;
    private int serverPort;
    private Scanner scanner;

    /**
     * @param serverAddress
     * @param serverPort
     * @throws Exception
     */
    private ClientTester(InetAddress serverAddress, int serverPort) throws Exception {
        this.serverAddress = serverAddress;
        this.serverPort = serverPort;

        //Initiate the connection with the server using Socket.
        //For this, creates a stream socket and connects it to the specified port number at the specified IP address.
        //add your code here
        this.tcpSocket = new Socket(this.serverAddress, this.serverPort);
        this.scanner = new Scanner(System.in);
    }

    /**
     * The start method connect to the server and datagrams
     * @throws IOException
     */
/*    private void start() throws IOException {
        String input;
        //create a new PrintWriter from an existing OutputStream (i.e., tcpSocket).
        //This convenience constructor creates the necessary intermediateOutputStreamWriter, which will convert characters into bytes using the default character encoding
        //You may add your code in a loop so that client can keep send datagrams to server
        //add your code here
        while (true) {
            System.out.print ("C:");
            input = scanner.nextLine();
            PrintWriter output = new PrintWriter(this.tcpSocket.getOutputStream(), true);
            output.println(input);
            output.flush();
        }
    }*/

    public static void main(String[] args) throws Exception {
        // set the server address (IP) and port number
        //add your code here
        //IP: 192.168.1.235
        //Port: 8088
        InetAddress serverIP = InetAddress.getByName("smtp.google.com"); // local IP address
        int port = 25;
        if (args.length > 0) {
            serverIP = InetAddress.getByName(args[0]);
            port = Integer.parseInt(args[1]);
        }

        // call the constructor and pass the IP and port
        //add your code here
        ClientTester client = new ClientTester(serverIP, port);

//        client.start();

        try{

            client = new ClientTester(serverIP, port);

            System.out.println("\r\n Connected to Server: " + client.tcpSocket.getInetAddress());

            BufferedReader stdin;
            stdin = new BufferedReader (new InputStreamReader (System.in));

            InputStream is = client.tcpSocket.getInputStream ();
            BufferedReader sockin;
            sockin = new BufferedReader (new InputStreamReader (is));

            OutputStream os = client.tcpSocket.getOutputStream();
            PrintWriter sockout;
            sockout = new PrintWriter (os, true);

            System.out.println ("S:" + sockin.readLine ());

            while (true){
                System.out.print ("C:");

                String cmd = stdin.readLine ();

                sockout.println (cmd);

                String reply = sockin.readLine ();

                System.out.println ("S:" + reply);
                if (cmd.toLowerCase ().startsWith ("data") &&
                        reply.substring (0, 3).equals ("354"))
                {
                    do
                    {
                        cmd = stdin.readLine ();

                        if (cmd != null && cmd.length () > 1 &&
                                cmd.charAt (0) == '.')
                            cmd = "."; // Must be no chars after . char.

                        sockout.println (cmd);

                        if (cmd.equals ("."))
                            break;
                    }
                    while (true);

                    // Read a reply string from the SMTP server program.

                    reply = sockin.readLine ();

                    // Display the first line of this reply string.

                    System.out.println ("S:" + reply);

                    continue;
                }

                // If the QUIT command was entered, quit.

                if (cmd.toLowerCase ().startsWith ("quit"))
                    break;
            }
        }
        catch (IOException e)
        {
            System.out.println (e.toString ());
        }
        finally
        {
            try
            {
                // Attempt to close the client socket.

                if (client != null)
                    client.tcpSocket.close();
            }
            catch (IOException e)
            {
            }
            }
    }
}

The good news is that the ClientTester works when i connect it to smtp.google.com but i don't want to use Googles, i want to have my own basic Email server in java.

Basically like my code.

  • It is just a proof of concept, and quite unsafe and inefficient
  • I'm using lombok. The read() method is basically a BufferedReader.readLine() call on the socket's InputStream.
  • send() is a writeLine
  • My entry point handleSocket() is when the Socket connection is established.
  • The String.toNLine() method is a Lombok extension, you can replace it with string.replace("\r\n", "\n");

Be aware that this is simply a stupid implementation that can be fooled easily, but it enables basic email receiving. You get ALL the communication in the StringBuilder. You could take that final whole text apart with MIME classes (Header / newline / newline body method that is used by HTTP, SMTP etc).

This approach collects the whole comunication first, then later (outside given code) handles the actual MIME part. You could also implement it differently, as in the code knows the current state of transmission and details of the MIME object it's currently receiving, and updates its status/workflow with each line. That would be much more efficient, but the code would be a bit more complex.

package jc.lib.io.net.email.smtp.server.receiver;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

import jc.lib.aop.lombok.java.lang.JcAString;
import jc.lib.collection.tuples.JcTriple;
import jc.lib.io.net.email.JcEMailBasics;
import jc.lib.io.net.email.util.JcAServerSocketHandlerBase;
import jc.lib.lang.thread.event.JcEvent;
import lombok.experimental.ExtensionMethod;

@ExtensionMethod({ JcAString.class })
public class JcSmtpReceiverSocketHandler extends JcAServerSocketHandlerBase {



    public final JcEvent<JcTriple<JcSmtpReceiver, JcSmtpReceiverSocketHandler, File>> EVENT_EMAIL_RECEIVED = new JcEvent<>();



    private final JcSmtpReceiver mJcAServerBase;

    private boolean mReceivingData;

    public JcSmtpReceiverSocketHandler(final JcSmtpReceiver pJcAServerBase, final ServerSocket pServerSocket, final Socket pSocket) throws IOException {
        super(pServerSocket, pSocket);
        mJcAServerBase = pJcAServerBase;
    }



    @Override protected void handleSocket() throws IOException {
        send("220 cbsoft.dev SMTP " + JcEMailBasics.NAME);

        final StringBuilder sb = new StringBuilder();

        mainLoop: while (!mSocket.isClosed()) {
            final String read = read();
            if (read == null) break;

            switch (read) {
                case JcEMailBasics.COMMAND_DATA: {
                    send("354 End data with <CR><LF>.<CR><LF>");
                    mReceivingData = true;
                    break;
                }
                case JcEMailBasics.COMMAND_END_OF_DATA: {
                    send("250 OK");
                    mReceivingData = false;
                    break;
                }
                case JcEMailBasics.COMMAND_QUIT: {
                    send("221 " + JcEMailBasics.NAME + " signing off");
                    break mainLoop;
                }
                default: {
                    final String correctedRead = read.startsWith(".") ? read.substring(1) : read;
                    sb.append(correctedRead + "\n");
                    if (!mReceivingData) send("250 Ok");
                }
            }
        }

        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
        final File file = new File("mails/inc_" + sdf.format(new Date()) + ".email.txt");
        file.getParentFile().mkdirs();

        String msg = sb.toString();
        msg = msg.toNLineBreak();
        final String header = msg.subStringBefore("\n\n");
        System.out.println("header:");



        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(msg.getBytes());
        }
        System.out.println("File saved as " + file.getCanonicalPath());

        EVENT_EMAIL_RECEIVED.trigger(new JcTriple<>(mJcAServerBase, this, file));
    }



}

Check out this file for some ports and other info.

package jc.lib.io.net.email;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

import jc.lib.io.net.email.util.JcAServerBase;
import jc.lib.lang.JcUArray;

public class JcEMailBasics {



    static public final int     SMTP_PORT_1 = 25;
    static public final int     SMTP_PORT_2 = 587;
    static public final int     SMTP_PORT_3 = 465;
    static public final int[]   SMTP_PORTS  = { SMTP_PORT_1, SMTP_PORT_2, SMTP_PORT_3 };

    static public final int     POP_PORT_1          = 110;
    static public final int     POP_PORT_SSL        = 995;
    static public final int     POP_PORT_KERBEROS   = 1109;
    static public final int[]   POP_PORTS           = { POP_PORT_1, POP_PORT_SSL, POP_PORT_KERBEROS };

    // netstat -aon | findstr '587'



    static public final String DEFAULT_CHARSET_SMTP_POP3 = "8859_1";

    static public final String  NAME                = "JC Oblivionat0r POP3 Server";
    static public final String  SERVICE_ADDRESS     = "oblivionat0r@cbsoft.dev";
    static public final String  CONNECTION_CLOSED   = "CONNECTION_CLOSED_dtnt495n3479r5zb3tr47c3b49c3";
    static public final String  COMMAND_QUIT        = "QUIT";
    static public final String  COMMAND_DATA        = "DATA";
    static public final String  COMMAND_END_OF_DATA = ".";



    static public void send(final BufferedWriter pBufferedWriter, final String pMessage) throws IOException {
        pBufferedWriter.write(pMessage + "\n");
        pBufferedWriter.flush();
        if (JcAServerBase.DEBUG) System.out.println("SENT:\t" + pMessage);
    }
    static public String sendExpect(final BufferedWriter pBufferedWriter, final String pMessage, final BufferedReader pBufferedReader, final String... pExpectedResponsePrefixes) throws IOException {
        send(pBufferedWriter, pMessage);
        final String read = read(pBufferedReader);
        for (final String erp : pExpectedResponsePrefixes) {
            if (read.startsWith(erp)) return read;
        }
        throw new IllegalStateException("Bad response: Expected [" + JcUArray.toString(", ", pExpectedResponsePrefixes) + "] got [" + read + "] instead!");
    }

    static public String read(final BufferedReader pBufferedReader) throws IOException {
        final String reply = pBufferedReader.readLine();
        if (JcAServerBase.DEBUG) System.out.println("RECV:\t" + reply);
        return reply;
    }



}

Okay, found this early-development standalone version. Use this INSTEAD of your code; does everything your code does an more. Single-threaded ServerSocket handling, so only one connection at a time.

package jc.lib.io.net.email.smtp.test1;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;

import jc.lib.io.net.email.JcEMailBasics;

public class Test_SMTP_Server {



    static public boolean DEBUG = true;



    public static void main(final String s[]) throws UnknownHostException, IOException {
        final Test_SMTP_Server server = new Test_SMTP_Server(JcEMailBasics.SMTP_PORTS);
        server.start();

        try {
            Thread.sleep(1 * 60 * 60 * 1000);
        } catch (final InterruptedException e) { /* */ }
    }



    /*
     * OBJECT
     */

    private final ServerSocket[]    mSockets;
    private volatile boolean        mStopRequested;
    private static boolean          mReceivingData;



    public Test_SMTP_Server(final int[] pPorts) throws IOException {
        mSockets = new ServerSocket[pPorts.length];
        for (int i = 0; i < pPorts.length; i++) {
            final int port = pPorts[i];
            try {
                mSockets[i] = new ServerSocket(port);
            } catch (final java.net.BindException e) {
                new java.net.BindException("When mountin port " + port + ": " + e.getMessage()).printStackTrace();
            }
            System.out.println("Created server socket on port " + port);
        }
    }



    public void start() {
        mStopRequested = false;
        for (final ServerSocket ss : mSockets) {
            if (ss == null) continue;

            final Thread t = new Thread(() -> handleServerSocket(ss), "handleServerSocket(" + ss.getLocalPort() + ")");
            t.setDaemon(true);
            t.start();
        }
    }
    private void handleServerSocket(final ServerSocket pSS) {
        final String name = "handleServerSocket(" + pSS.getLocalPort() + ")";
        while (!mStopRequested) {
            System.out.println(name + "\tListening for connection...");
            try (final Socket socket = pSS.accept();
                    final BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), JcEMailBasics.DEFAULT_CHARSET_SMTP_POP3));
                    final BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), JcEMailBasics.DEFAULT_CHARSET_SMTP_POP3));) {
                System.out.println(name + "\tGot new Socket.");
                handle(socket, in, out);
                System.out.println(name + "\tClosing Socket.");
            } catch (final IOException e) {
                System.err.println("In " + name + ":");
                e.printStackTrace();
            }
            System.out.println(name + "\tComm Done.");
        }
    }

    public void stop() {
        mStopRequested = true;
        for (final ServerSocket ss : mSockets) {
            try {
                ss.close();
            } catch (final Exception e) { /* */ }
        }
    }



    static private void handle(final Socket pSocket, final BufferedReader pBR, final BufferedWriter pBW) throws IOException {
        //      send("+OK POP3 server ready <" + Test_EMails.SERVICE_ADDRESS + ">", out);
        send("220 cbsoft.dev SMTP " + JcEMailBasics.NAME, pBW);

        final StringBuilder sb = new StringBuilder();

        mainLoop: while (!pSocket.isClosed()) {
            final String read = read(pBR);
            if (read == null) break;

            switch (read) {
                case JcEMailBasics.COMMAND_DATA: {
                    send("354 End data with <CR><LF>.<CR><LF>", pBW);
                    mReceivingData = true;
                    break;
                }
                case JcEMailBasics.COMMAND_END_OF_DATA: {
                    send("250 OK", pBW);
                    mReceivingData = false;
                    break;
                }
                case JcEMailBasics.COMMAND_QUIT: {
                    send("221 " + JcEMailBasics.NAME + " signing off", pBW);
                    break mainLoop;
                }
                default: {
                    final String correctedRead = read.startsWith(".") ? read.substring(1) : read;
                    sb.append(correctedRead + "\n");
                    if (!mReceivingData) send("250 Ok", pBW);
                }
            }
        }

        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
        final File file = new File("mails/inc_" + sdf.format(new Date()) + ".email.txt");
        file.getParentFile().mkdirs();
        final String msg = sb.toString();
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(msg.getBytes());
        }
        System.out.println("File saved as " + file.getCanonicalPath());
    }

    static private void send(final String pMessage, final BufferedWriter pBW) {
        try {
            pBW.write(pMessage + "\n");
            pBW.flush();
            if (DEBUG) System.out.println("SENT:\t" + pMessage);
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    static private String read(final BufferedReader pBR) throws IOException {
        try {
            final String reply = pBR.readLine();
            if (DEBUG) System.out.println("RECV:\t" + reply);
            return reply;

        } catch (final SocketTimeoutException e) {
            System.err.println("SERVER TIMEOUT");
        }
        return null;
    }



}

the only additional file you will need (also included in my previous answer; edited a bit):

package jc.lib.io.net.email;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;


public class JcEMailBasics {



    static public final int     SMTP_PORT_1 = 25;
    static public final int     SMTP_PORT_2 = 587;
    static public final int     SMTP_PORT_3 = 465;
    static public final int[]   SMTP_PORTS  = { SMTP_PORT_1, SMTP_PORT_2, SMTP_PORT_3 };

    static public final int     POP_PORT_1          = 110;
    static public final int     POP_PORT_SSL        = 995;
    static public final int     POP_PORT_KERBEROS   = 1109;
    static public final int[]   POP_PORTS           = { POP_PORT_1, POP_PORT_SSL, POP_PORT_KERBEROS };

    // netstat -aon | findstr '587'



    static public final String DEFAULT_CHARSET_SMTP_POP3 = "8859_1";

    static public final String  NAME                = "JC Oblivionat0r POP3 Server";
    static public final String  SERVICE_ADDRESS     = "oblivionat0r@cbsoft.dev";
    static public final String  CONNECTION_CLOSED   = "CONNECTION_CLOSED_dtnt495n3479r5zb3tr47c3b49c3";
    static public final String  COMMAND_QUIT        = "QUIT";
    static public final String  COMMAND_DATA        = "DATA";
    static public final String  COMMAND_END_OF_DATA = ".";



    static public void send(final BufferedWriter pBufferedWriter, final String pMessage) throws IOException {
        pBufferedWriter.write(pMessage + "\n");
        pBufferedWriter.flush();
        System.out.println("SENT:\t" + pMessage);
    }
    static public String sendExpect(final BufferedWriter pBufferedWriter, final String pMessage, final BufferedReader pBufferedReader, final String... pExpectedResponsePrefixes) throws IOException {
        send(pBufferedWriter, pMessage);
        final String read = read(pBufferedReader);
        for (final String erp : pExpectedResponsePrefixes) {
            if (read.startsWith(erp)) return read;
        }
        throw new IllegalStateException("Bad response: Expected [" + toString(", ", pExpectedResponsePrefixes) + "] got [" + read + "] instead!");
    }

    static public String read(final BufferedReader pBufferedReader) throws IOException {
        final String reply = pBufferedReader.readLine();
        System.out.println("RECV:\t" + reply);
        return reply;
    }

    @SafeVarargs public static <T> String toString(final String pSeparator, final T... pObjects) {
        if (pObjects == null) return null;
        final StringBuilder ret = new StringBuilder();
        for (final T o : pObjects) {
            ret.append(o + pSeparator);
        }
        if (ret.length() > 0) ret.setLength(ret.length() - pSeparator.length());
        return ret.toString();
    }



}

You need to communicate with the client.
First let the server send something like "220 Smtp server" (only 220 matters) to the client.
I used PrintWriter:

PrintWriter out = new PrintWriter(client.getOutputStream(), true);
out.println("220 Smtp server");

Then you will receive an EHLO from the client while getting lines from the inputStream.

Here you can find an example of the communication between server an client (without the starting message from the server (220)): https://postmarkapp.com/guides/everything-you-need-to-know-about-smtp#basic-smtp-commands

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM