原创

发送邮件

温馨提示:
本文最后更新于 2024年07月12日,已超过 280 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

要在网络中实现邮件功能,必须有专门的邮件服务器,相当于现实生活中的邮局。邮件服务器负责接收用户投递过来的邮件,然后将邮件投递给邮件接收人的电子邮箱中。在Java中发送电子邮件主要涉及到两种方式:直接使用SMTP(Simple Mail Transfer Protocol)协议和JavaMail API。SMTP是一个应用层协议,用于在互联网上传输电子邮件。而JavaMail API则是Java的一个扩展,提供了一套高级接口来发送和接收电子邮件,它封装了底层的协议细节。

1.SMTP协议

直接使用SMTP协议意味着你需要手动处理所有与SMTP服务器的交互,包括连接、认证、发送邮件数据以及断开连接。这种方式通常涉及Socket编程,你需要了解SMTP命令集,例如HELO/EHLO、AUTH、MAIL FROM、RCPT TO、DATA等。

优点:

对底层协议有更深入的理解和控制。

可以实现定制化的邮件发送逻辑。

缺点:

编程复杂度高,需要处理各种错误情况和协议细节。

安全性问题,如TLS/SSL的使用,需要额外处理。

需要处理邮件的编码、MIME类型等问题。

下面说一下SMTP的工作流程

  1. 建立连接:邮件客户端通过TCP协议连接到SMTP服务器的默认端口(通常是25)。如果使用SSL/TLS,则会连接到465或587端口。
  2. 服务器欢迎消息:连接建立后,SMTP服务器会发送一条欢迎消息,通常是“220 server_name ESMTP”。
  3. 客户端发出HELO或EHLO命令:客户端通过发送“HELO”或“EHLO”命令来标识自己。
  4. 身份验证:在邮件系统中,邮件客户端通常需要通过某种身份验证机制进行身份验证。
  5. MAIL FROM命令:客户端发送“MAIL FROM:”命令,指定邮件的发件人。
  6. RCPT TO命令:客户端发送“RCPT TO:”命令,指定邮件的一个或多个收件人。
  7. DATA命令:一旦收件人被接受,客户端发送“DATA”命令,随后是邮件的实际内容。邮件内容以点号(.)结尾。
  8. 发送邮件内容:客户端发送邮件的头部和正文,直到以单独的一行点号结束。
  9. 服务器响应:服务器会发送响应代码,如“250 OK”,表示邮件已被接受。
  10. QUIT命令:发送完邮件后,客户端发送“QUIT”命令来结束会话。
import javax.net.ssl.SSLSocket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class SendMailNative {
    public static void main(String[] args) {
        // SMTP服务地址
        String smtpHost = "smtp.163.com";
        // SMTP服务端口
        int smtpPort = 587;
        // 邮箱地址
        String username = "13512344321@163.com";
        // 邮箱密码
        String password = "VFMJGJGJKHFYYYU";
        // 发件人邮箱
        String sender = "13512344321@163.com";
        // 收件人邮箱
        String recipient = "741233216@qq.com";
        // 邮件内容
        String subject = "Test Subject";
        String body = "This is the email body.";
        FlexibleTrustSSLSocketFactory factory = new FlexibleTrustSSLSocketFactory(FlexibleTrustSSLSocketFactory.TrustMode.TRUST_ALL);

        PrintWriter out = null;
        BufferedReader in = null;
        SSLSocket sslSocket = null;
        try {
            // 连接到SMTP服务器
            sslSocket = (SSLSocket) factory.createSocket(smtpHost, smtpPort);
            out = new PrintWriter(sslSocket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(sslSocket.getInputStream()))

            // 读取SMTP服务器的欢迎信息
            String response = in.readLine();
            System.out.println("创建连接响应:" + response);
            if (!response.startsWith("220")) {
                throw new RuntimeException("Unexpected response from server: " + response);
            }

            out.println("EHLO " + smtpHost);
            response = in.readLine();
            System.out.println("EHLO响应:" + response);
            if (!response.startsWith("250")) {
                throw new RuntimeException("Unexpected response from server: " + response);
            }

            // 启动TLS加密(如果服务器支持)
            out.println("STARTTLS");
            response = in.readLine();
            System.out.println("STARTTLS响应:" + response);

            // 使用PLAIN认证
            String authStr = "\u0000" + username + "\u0000" + password;
            out.println("AUTH PLAIN " + Base64.getEncoder().encodeToString(authStr.getBytes(StandardCharsets.UTF_8)));
            out.println("MAIL FROM:<" + sender + ">");
            out.println("RCPT TO:<" + recipient + ">");
            // 发送邮件内容
            out.println("DATA");
            out.println("Subject: " + subject);
            out.println("");
            out.println(body);
            out.println(".");
            // 结束SMTP会话
            out.println("QUIT");

            StringBuilder responseBuilder = new StringBuilder();
            String line = null;
            // 使用循环不断读取下一行,直到没有更多行可读
            while ((line = in.readLine()) != null) {
                // 将读取的行添加到StringBuilder中,每行之间添加换行符
                responseBuilder.append(line).append(System.lineSeparator());
            }
            String responseNew = responseBuilder.toString().trim();
            System.out.println("邮件响应结果:" + responseNew);
            if (!responseNew.startsWith("250")) {
                throw new RuntimeException("Unexpected response from server: " + responseNew);
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 关闭连接
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
                if (sslSocket != null) {
                    sslSocket.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

FlexibleTrustSSLSocketFactory类用于指定哪些主机的 SSL/TLS 证书会被无条件信任,即使这些证书是由不受信任的证书颁发机构(CA)签发的,或者是自签名证书。这在测试环境中非常有用,但不推荐在生产环境中使用,因为它会降低安全性。默认情况下,JavaMail 会验证 SSL/TLS 证书的完整信任链,确保它们是由受信任的 CA 签发的,并且在证书的生命周期内是有效的。如果证书不能被信任,JavaMail 会抛出一个 SSL 握手异常。

import javax.net.ssl.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class FlexibleTrustSSLSocketFactory extends SSLSocketFactory {

    private final SSLSocketFactory delegate;
    private final TrustManager trustManager;

    public FlexibleTrustSSLSocketFactory(TrustMode mode, String... trustedHosts) {
        try {
            SSLContext context = SSLContext.getInstance("TLS");
            switch (mode) {
                case TRUST_ALL:
                    trustManager = new TrustAllManager();
                    break;
                case TRUST_SPECIFIC_HOSTS:
                    trustManager = new TrustSpecificHostsManager(trustedHosts);
                    break;
                case NO_TRUST:
                default:
                    trustManager = new NoTrustManager();
                    break;
            }
            context.init(null, new TrustManager[]{trustManager}, null);
            delegate = context.getSocketFactory();
        } catch (Exception e) {
            throw new RuntimeException("Failed to initialize SSLContext", e);
        }
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return delegate.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return delegate.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        return wrapSocket(host, delegate.createSocket(host, port));
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        return wrapSocket(host, delegate.createSocket(host, port, localHost, localPort));
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        return wrapSocket(host.getHostAddress(), delegate.createSocket(host, port));
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return wrapSocket(address.getHostAddress(), delegate.createSocket(address, port, localAddress, localPort));
    }

    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        return wrapSocket(host, delegate.createSocket(s, host, port, autoClose));
    }

    private Socket wrapSocket(String host, Socket socket) {
        if (socket instanceof SSLSocket) {
            ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"}); // Set preferred protocols
        }
        return socket;
    }

    private static class TrustAllManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {}
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {}
        @Override
        public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
    }

    private static class TrustSpecificHostsManager implements X509TrustManager {
        private final Set<String> trustedHosts = new HashSet<>();

        public TrustSpecificHostsManager(String... trustedHosts) {
            this.trustedHosts.addAll(Arrays.asList(trustedHosts));
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {}
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
            if (!trustedHosts.contains(chain[0].getSubjectDN().getName())) {
                throw new IllegalArgumentException("Untrusted host certificate.");
            }
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
    }

    private static class NoTrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
            throw new IllegalArgumentException("Client certificate not trusted.");
        }
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
            throw new IllegalArgumentException("Server certificate not trusted.");
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
    }

    public enum TrustMode {
        TRUST_ALL(1, "信任所有证书"),
        TRUST_SPECIFIC_HOSTS(2, "信任列表中的主机证书"),
        NO_TRUST(3, "不信任任何证书");

        private Integer code;
        private String msg;

        TrustMode(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }

        public Integer getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }
    }
}

响应结果

创建连接响应:220 163.com Anti-spam GT for Coremail System (163com[20141201])
EHLO响应:250-mail
STARTTLS响应:250-PIPELINING
邮件响应结果:250-AUTH LOGIN PLAIN XOAUTH2
250-AUTH=LOGIN PLAIN XOAUTH2
250-coremail 1Uxr2xKj7kG0xkI17xGrU7I0s8FY2U3Uj8Cz28x1UUUUU7Ic2I0Y2UF2fSh7UCa0xDrUUUUj
250-STARTTLS
250-ID
250 8BITMIME
454 Command not permitted when TLS active
235 Authentication successful
250 Mail OK
250 Mail OK
354 End data with <CR><LF>.<CR><LF>
250 Mail OK queued as gzga-smtp-mta-g3-4,_____wD3v8+3dIZmP4qSBg--.24498S2 1720087736
221 Bye

2.JavaMail API

JavaMail API是一个Java SE和Java EE平台的标准扩展,它提供了发送和接收邮件的高级接口,包括对SMTP、POP3、IMAP等协议的支持。JavaMail API封装了底层协议的复杂性,使得开发者可以更专注于业务逻辑而不是协议细节。

优点:

简化了邮件发送和接收的代码,提高了开发效率。

提供了错误处理和重试机制。

支持多种邮件协议和高级特性,如附件、HTML邮件、多部分邮件等。

支持TLS/SSL安全连接,提高了邮件通信的安全性。

缺点:

相对于直接使用SMTP,它引入了一定的性能开销,因为有额外的抽象层。

需要额外的库文件(JavaMail API和JAF),增加了项目的依赖。

使用JavaMail发送邮件分三步:

  1. 配置Properties:Properties对象中设置SMTP主机、端口、认证方式,并开启SSL支持。
  2. 创建Session:使用上述Properties对象创建Session实例,并提供一个Authenticator对象用于处理SMTP认证。
  3. 创建并发送邮件:创建MimeMessage实例,设置收件人、主题、正文等,然后使用Transport发送邮件。
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
public class SendEmailSSL {
    public static void main(String[] args) {
        // SMTP服务地址
        final String smtpHost = "smtp.163.com";
        // SMTP服务端口
        final String smtpPort = "25";
        // 邮箱地址
        final String username = "13512344321@163.com";
        // 邮箱密码
        final String password = "VFMJJIUIGUGFYR";
        // 发件人邮箱
        final String sender = "13512344321@163.com";
        // 收件人邮箱
        final String recipient = "741233216@qq.com";

        // 配置邮件服务器
        Properties props = new Properties();
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");
//         props.put("mail.smtp.ssl.protocols", "TLSv1.3");
        // SMTP服务器地址
        props.put("mail.smtp.host", smtpHost);
        // 端口号
        props.put("mail.smtp.port", smtpPort);
        // ssl配置
//        props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
//        props.put("mail.smtp.socketFactory.fallback", "false");
        // 不允许非SSL连接
//        props.put("mail.smtp.ssl.enable", "true");
        // 添加服务器证书信任
//        props.put("mail.smtp.ssl.trust", "*");

        // 开启调试模式
        // props.put("mail.debug", "true");

        // 创建Session
        Session session = Session.getInstance(props,
                new javax.mail.Authenticator() {
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication(username, password);
                    }
                });

        try {
            // 创建MimeMessage
            Message message = new MimeMessage(session);
            // 发件人邮箱
            message.setFrom(new InternetAddress(sender));
            // 收件人邮箱
            message.setRecipients(Message.RecipientType.TO,
                    InternetAddress.parse(recipient));
            message.setSubject("测试邮件主题");
            message.setText("测试邮件正文");

            // 发送邮件
            Transport.send(message);

            System.out.println("邮件发送成功");

        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }
}

3.其他

发邮件还需要注意下加密协议,TLS(Transport Layer Security)和SSL(Secure Sockets Layer)都是用于在网络上安全传输数据的协议,尤其在发送电子邮件时,它们用于保护邮件内容免受窃听或篡改。尽管它们有相似的目标,但TLS和SSL之间存在一些关键的区别

1. 历史背景:

● SSL 是由Netscape公司于1994年首次提出,目的是为了保护Web通讯的安全。

● TLS 是SSL的后续版本,由IETF(Internet Engineering Task Force)在1999年提出,基于SSL 3.0进行改进和标准化。

2. 安全性:

● TLS通常被认为比SSL更安全,因为它修复了许多SSL中存在的漏洞,并引入了更强的加密算法。

● TLS还支持更安全的密钥交换算法,如Diffie-Hellman。

3. 版本:

● SSL有三个主要版本:SSL 2.0、SSL 3.0。

● TLS有多个版本:TLS 1.0、TLS 1.1、TLS 1.2、TLS 1.3,其中TLS 1.3是最新的版本,提供了最先进的安全特性。

4. 端口使用:

● SSL通常使用端口443(HTTPS)或端口465(SMTPS)。

● TLS可以使用与SSL相同的端口,但更多时候与STARTTLS一起使用,先建立一个非加密连接,然后升级到加密连接,通常使用端口587。

综合来说,当选择使用TLS还是SSL时,由于TLS提供了更强的安全性并且修复了SSL中发现的许多安全漏洞,TLS通常是更好的选择。在实际应用中,大多数现代邮件服务都推荐使用TLS,并且可能不支持较旧的SSL版本。因此,除非有特定的要求,否则应优先使用TLS。

正文到此结束
本文目录