轻量级邮件发送组件:基于 Socket/SMTP 协议的浅层封装

Java 发送邮件组件,大家第一时间想到的就是 JavaMail。JavaMail 本身也不大,才 500kb 的 jar 包,足够轻量级。而今天为大家介绍的,可以说“羽量级”,核心一个类就可以发送邮件了,除了依赖 JDK 无须依赖什么。如果我们了解 SMTP 协议以及 Socket 的简单使用,就知道其中过程并不复杂——实质都是基于文本的协议交互。

完整源码在:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-backend/aj-base/src/main/java/com/ajaxjs/base/service/message/email

使用方法

前期准备

你需要一个 SMTP 发送邮件的服务器。以网易邮箱为例子,登陆网易邮箱163,在设置中打开并勾选 POP3/SMTP/IMAP 服务,然后会得到一个授权码,这个邮箱和授权码将用作登陆认证。

在这里插入图片描述

简单的例子

Mail 这个 Java Bean 包含了发邮件的所有信息。然后调用静态方法Sender.send(mail)即可发送。

Mail mail = new Mail();
mail.setMailServer("smtp.163.com");
mail.setAccount("paco11"); // 账号
mail.setPassword("xxxxxxxxxx"); // 密码,就是网易邮箱的授权码
mail.setFrom("paco11@163.com");
mail.setTo("frank@qq.com");
mail.setSubject("你好容祯");
mail.setHtmlBody(false);
mail.setContent("我希望可以跟你做朋友。");

// 附件列表,key 是文件名,byte[] 是文件内容
Map<String, byte[]> attachment = new HashMap<>();

try {
    attachment.put("test14.txt", Files.readAllBytes(Paths.get("C:\\Users\\admin\\Desktop\\test.txt")));
    attachment.put("test2.txt", Files.readAllBytes(Paths.get("C:\\Users\\admin\\Desktop\\test.txt")));
} catch (IOException e) {
    e.printStackTrace();
}

mail.setAttachment(attachment);

assertTrue(Sender.send(mail));

实体 Mail 源码如下。

package com.ajaxjs.net.email;

import lombok.Data;

import java.util.Map;

/**
 * 邮件模型
 */
@Data
public class Mail {
    /**
     * 邮件服务器地址
     */
    private String mailServer;

    /**
     * SMTP 服务器的端口
     */
    private int port = 25;

    /**
     * 发件人账号
     */
    private String account;

    /**
     * 发件人密码
     */
    private String password;

    /**
     * 发件人邮箱
     */
    private String from;

    /**
     * 收件人邮箱
     */
    private String to;

    /**
     * 邮件主题
     */
    private String subject;

    /**
     * 邮件内容
     */
    private String content;

    /**
     * 邮件内容是否为 HTML 格式
     */
    private boolean isHtmlBody;

    /**
     * 附件列表,key 是文件名,byte[] 是文件内容
     */
    private Map<String, byte[]> attachment;

    public String getMailServer() {
        if (mailServer == null)
            throw new IllegalArgumentException("没有指定 MailServer!");

        return mailServer;
    }

    public String getFrom() {
        if (from == null)
            throw new IllegalArgumentException("没有指定发件人!");

        return from;
    }

    public String getTo() {
        if (to == null)
            throw new IllegalArgumentException("没有指定收件人!");

        return to;
    }

    public boolean isHtmlBody() {
        return isHtmlBody;
    }

    public void setHtmlBody(boolean isHTML_body) {
        this.isHtmlBody = isHTML_body;
    }

}

与 Spring 集成

YAML 配置如下:

Message: # 消息
email:
smtpServer: smtp.163.com # 服务器
port: 25 # 端口
account: xxxxx # 账号
password: xxxxxxx # 密码

Java 注入配置:

import com.ajaxjs.base.service.message.email.Mail;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**
 * 消息配置
 */
@Configuration
public class MessageConfiguration {
    @Value("${Message.email.smtpServer}")
    private String smtpServer;

    @Value("${Message.email.port}")
    private int port;

    @Value("${Message.email.account}")
    private String account;

    @Value("${Message.email.password}")
    private String password;

    @Bean
    @Scope("prototype")
    Mail getMailConfig() {
        Mail mailCfg = new Mail();
        mailCfg.setMailServer(smtpServer);
        mailCfg.setPort(port);
        mailCfg.setAccount(account);
        mailCfg.setPassword(password);

        return mailCfg;
    }

}

调用发送:

@Service
public class MessageService implements MessageController {
    @Autowired
    Mail mailCfg;

    @Override
    public boolean email(MailVo mail) {
        System.out.println(mailCfg);
        BeanUtils.copyProperties(mail, mailCfg);

        return Sender.send(mailCfg);
    }
}

发送邮件的原理

先科普一下 SMTP:

SMTP 全称为 Simple Mail Transfer Protocol(简单邮件传输协议),它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP 认证要求必须提供账号和密码才能登陆服务器,其设计目的在于避免用户受到垃圾邮件的侵扰。

再上核心源码Sender

package com.ajaxjs.net.email;


import com.ajaxjs.util.StrUtil;
import com.ajaxjs.util.logger.LogHelper;
import com.ajaxjs.util.regexp.RegExpUtils;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Map;

/**
 * 简易邮件发送器
 *
 * @author sp42 frank@ajaxjs.com
 */
public class Sender extends Socket {
    private static final LogHelper LOGGER = LogHelper.getLog(Sender.class);

    /**
     * 发送一封邮件
     *
     * @param bean 邮件实体
     * @throws IOException          IO异常
     * @throws UnknownHostException 未知主机异常
     */
    public Sender(Mail bean) throws UnknownHostException, IOException {
        super(bean.getMailServer(), bean.getPort());
        this.bean = bean;
    }

    public static final String LINEFEET = "\r\n"; // 换行符常量

    private static final int OK_250_CODE = 250;// 成功标识

    private final Mail bean; // 邮件信息

    private BufferedReader in; // 接受指令用的缓冲区

    private DataOutputStream os; // 发送指令用的流

    /**
     * 发送邮件
     *
     * @return 是否成功
     * @throws MailException 邮件异常
     */
    public boolean sendMail() throws MailException {
        LOGGER.info("发送邮件:" + bean.getSubject());

        try (BufferedReader in = new BufferedReader(new InputStreamReader(getInputStream()));
             DataOutputStream os = new DataOutputStream(getOutputStream())) {
            this.in = in;
            this.os = os;

            String result = in.readLine();// 初始化连接
            if (!isOkCode(result, 220))
                throw new MailException("初始化连接:" + result, 220);

            // 进行握手
            result = sendCommand("HELO %s", bean.getMailServer());
            if (!isOkCode(result, OK_250_CODE))
                throw new MailException("握手失败:" + result, OK_250_CODE);

            // 验证发信人信息
            result = sendCommand("AUTH LOGIN");
            if (!isOkCode(result, 334))
                throw new MailException("验证发信人信息失败:" + result, 334);

            result = sendCommand(toBase64(bean.getAccount()));
            if (!isOkCode(result, 334))
                throw new MailException("发信人名称发送失败:" + result, 334);

            result = sendCommand(toBase64(bean.getPassword()));
            if (!isOkCode(result, 235))
                throw new MailException("認証不成功" + result, 235);

            // 发送指令
            result = sendCommand("Mail From:<%s>", bean.getFrom());
            if (!isOkCode(result, OK_250_CODE))
                throw new MailException("发送指令 From 不成功" + result, OK_250_CODE);// 235?

            result = sendCommand("RCPT TO:<%s>", bean.getTo());
            if (!isOkCode(result, OK_250_CODE))
                throw new MailException("发送指令 To 不成功" + result, OK_250_CODE);

            result = sendCommand("DATA");
            if (!isOkCode(result, 354))
                throw new MailException("認証不成功" + result, 354);

            String data = data();
            LOGGER.info(data);

            result = sendCommand(data);
            if (!isOkCode(result, OK_250_CODE))
                throw new MailException("发送邮件失败:" + result, OK_250_CODE);

            result = sendCommand("QUIT");// quit
            if (!isOkCode(result, 221))
                throw new MailException("QUIT 失败:" + result, 221);

        } catch (UnknownHostException e) {
            System.err.println("初始化 失败!建立连接失败!");
            LOGGER.warning(e);
            return false;
        } catch (IOException e) {
            System.err.println("初始化 失败!读取流失败!");
            LOGGER.warning(e);
            return false;
        } finally {
            try {
                close();
            } catch (IOException e) {
                LOGGER.warning(e);
            }
        }

        return true;
    }


    /**
     * 生成正文
     *
     * @return 正文
     */
    private String data() {
        String boundary = "------=_NextPart_" + System.currentTimeMillis();

        Map<String, byte[]> attachment = bean.getAttachment();

        StringBuilder sb = new StringBuilder();
        sb.append("From:<").append(bean.getFrom()).append(">").append(LINEFEET);
        sb.append("To:<").append(bean.getTo()).append(">").append(LINEFEET);
        sb.append("Subject:=?UTF-8?B?").append(toBase64(bean.getSubject())).append("?=").append(LINEFEET);
//		sb.append("Date:2016/10/27 17:30" + LINEFEET);
        // sb.append("MIME-Version: 1.0" + lineFeet);

        if (attachment != null) {
            sb.append("Content-Type: multipart/mixed;boundary=\"").append(boundary).append("\"").append(LINEFEET);
            sb.append(LINEFEET);
            sb.append("--").append(boundary).append(LINEFEET);
        }

        sb.append(bean.isHtmlBody() ? "Content-Type:text/html;charset=\"utf-8\"" : "Content-Type:text/plain;charset=\"utf-8\"").append(LINEFEET);
        sb.append("Content-Transfer-Encoding: base64" + LINEFEET);
        sb.append(LINEFEET);
        sb.append(toBase64(bean.getContent()));

        if (attachment != null) {
            for (String fileName : attachment.keySet()) {
                sb.append(LINEFEET);
                sb.append("--").append(boundary).append(LINEFEET);
                sb.append("Content-Type: application/octet-stream; name=\"").append(fileName).append("\"").append(LINEFEET);
                sb.append("Content-Disposition: attachment; filename=\"").append(fileName).append("\"").append(LINEFEET);
                sb.append("Content-Transfer-Encoding: base64").append(LINEFEET);
                sb.append(LINEFEET);
                sb.append(Base64.getEncoder().encodeToString(attachment.get(fileName)));
            }
        }

        sb.append(LINEFEET + ".");

        return sb.toString();
    }

    public static String readFile(String filePath) {
        try {
            byte[] fileContent = Files.readAllBytes(Paths.get(filePath));
            return Base64.getEncoder().encodeToString(fileContent);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 发送smtp指令 并返回服务器响应信息
     *
     * @param string 指令
     * @param from   指令参数
     * @return 服务器响应信息
     */
    private String sendCommand(String string, String from) {
        return sendCommand(String.format(string, from));
    }

    /**
     * 发送smtp指令 并返回服务器响应信息
     *
     * @param msg 指令,会在字符串后面自动加上 lineFeet
     * @return 服务器响应信息
     */
    private String sendCommand(String msg) {
        try {
            os.writeBytes(msg + LINEFEET);
            os.flush();

            return in.readLine(); // 读取服务器端响应信息
        } catch (IOException e) {
            LOGGER.warning(e);
            return null;
        }
    }

    /**
     * Base64 编码的一种实现
     *
     * @param str 待编码的字符串
     * @return 已编码的字符串
     */
    public static String toBase64(String str) {
        return StrUtil.base64Encode(str);
    }

    /**
     * 输入期望 code,然后查找字符串中的数字,看是否与之匹配。匹配则返回 true。
     *
     * @param str  输入的字符串,应该要包含数字
     * @param code 期望值
     * @return 是否与之匹配
     */
    private static boolean isOkCode(String str, int code) {
        int _code = Integer.parseInt(RegExpUtils.regMatch("^\\d+", str));

        return _code == code;
    }

    /**
     * 发送邮件
     *
     * @param mail 服务器信息和邮件信息
     * @return true 表示为发送成功,否则为失败
     */
    public static boolean send(Mail mail) {
        try (Sender sender = new Sender(mail)) {
            return sender.sendMail();
        } catch (IOException | MailException e) {
            LOGGER.warning(e);
            return false;
        }
    }
}

这个类是一个 Socket 套接字的子类,它通过 SMTP 连接与邮件服务器通信。

该类的主要方法是sendMail(),它使用传递给构造函数的邮件实体对象,通过SMTP连接与邮件服务器通信,并将邮件发送给目标收件人。

该类还包括一些辅助方法,如data(),用于生成包含邮件正文和附件的纯文本邮件消息; sendCommand(),用于向 SMTP 服务器发送命令并接收响应消息;toBase64(),用于将字符串编码为 Base64 格式等。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值