简体   繁体   English

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

[英]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.所以我有一个用 Java 编写的客户端,我想用它来测试发送 email 但不是像谷歌那样使用已经存在的 SMTP,我想拥有自己的本地服务器来测试在两个模拟电子邮件之间发送模拟电子邮件。

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.我一直在尝试在整个 inte.net 上寻找有关如何编写简单的 SMTP 服务器代码的良好资源,但我的运气为零。

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.我确实有一个基本的服务器代码,当我运行它时,我可以将我的客户端连接到它,但目前它不会处理任何 email 功能。

TCPServer.java 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.我可以向我现有的服务器代码添加什么以使其为我的客户处理 email。 I'll also post my email client as well.我也会发布我的 email 客户。

ClientTester.java 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.好消息是,当我将 ClientTester 连接到smtp.google.com时它可以工作,但我不想使用 Google,我想在 java 中拥有自己的基本 Email 服务器。

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. read()方法基本上是对套接字 InputStream 的BufferedReader.readLine()调用。
  • send() is a writeLine send() 是一个 writeLine
  • My entry point handleSocket() is when the Socket connection is established.我的入口点handleSocket()是建立 Socket 连接时。
  • The String.toNLine() method is a Lombok extension, you can replace it with string.replace("\r\n", "\n"); String.toNLine() 方法是 Lombok 扩展,你可以用 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.请注意,这只是一个很容易被愚弄的愚蠢实现,但它支持基本的 email 接收。 You get ALL the communication in the StringBuilder.您可以在 StringBuilder 中获得所有通信。 You could take that final whole text apart with MIME classes (Header / newline / newline body method that is used by HTTP, SMTP etc).您可以将最终的整个文本与 MIME 类(HTTP、SMTP 等使用的标题/换行/换行正文方法)分开。

This approach collects the whole comunication first, then later (outside given code) handles the actual MIME part.这种方法首先收集整个通信,然后(在给定代码之外)处理实际的 MIME 部分。 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.您也可以以不同的方式实现它,因为代码知道当前的传输 state 和它当前接收的 MIME object 的详细信息,并使用每一行更新其状态/工作流程。 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.单线程 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;
    }



}

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.首先让服务器向客户端发送类似“220 Smtp 服务器”(只有 220 个事项)的内容。
I used PrintWriter:我使用了 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.然后,您将在从 inputStream 获取行时收到来自客户端的 EHLO。

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在这里您可以找到服务器与客户端之间的通信示例(没有来自服务器 (220) 的起始消息): https://postmarkapp.com/guides/everything-you-need-to-know-about-smtp#基本 smtp 命令

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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