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