如何使用 Java 编写自己的 SMTP 服务器?

发布时间:2021-03-08 17:48

所以我有一个用 Java 编写的客户端,我想用它来测试发送电子邮件,但不是使用像谷歌这样的现有 SMTP,我想拥有自己的本地服务器来测试在两个模拟电子邮件之间发送模拟电子邮件.

我一直试图在互联网上寻找有关如何编写简单 SMTP 服务器的良好资源,但我的运气为零。

我确实有一个基本的服务器代码,当我运行它时,我可以将我的客户端连接到它,但目前它不会处理任何电子邮件功能。

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();
    }

}

我可以在我现有的服务器代码中添加什么来让它为我的客户处理电子邮件。我也会发布我的电子邮件客户端。

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)
            {
            }
            }
    }
}

好消息是当我将 ClientTester 连接到 smtp.google.com 时它可以工作,但我不想使用 Google,我想在 Java 中拥有自己的基本电子邮件服务器。

回答1

基本上喜欢我的代码。

  • 这只是概念验证,相当不安全和低效
  • 我正在使用 lombok。 read() 方法基本上是对套接字的 InputStream 的 BufferedReader.readLine() 调用。
  • send() 是一个 writeLine
  • 我的入口点 handleSocket() 是建立 Socket 连接的时间。
  • String.toNLine() 方法是 Lombok 扩展,您可以将其替换为 string.replace("\r\n" , "\n");

请注意,这只是一个很容易被愚弄的愚蠢实现,但它可以实现基本的电子邮件接收。您可以在 StringBuilder 中获得所有通信。您可以使用 MIME 类(HTTP、SMTP 等使用的标题/换行符/换行符正文方法)将最终的整个文本分开。

这种方法首先收集整个通信,然后(在给定代码之外)处理实际的 MIME 部分。您也可以以不同的方式实现它,因为在代码中知道当前的传输状态和它当前接收的 MIME 对象的详细信息,并使用每一行更新其状态/工作流。这样效率会更高,但代码会更复杂一些。

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));
    }



}

查看此文件以了解某些端口和其他信息。

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;
    }



}
回答2

好的,找到了这个早期开发的独立版本。 使用您的代码的这个 INSTEAD;完成您的代码所做的一切。 单线程 ServerSocket 处理,因此一次只能连接一个。

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;
    }



}

您需要的唯一附加文件(也包含在我之前的答案中;稍作编辑):

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();
    }



}