使用 Java 原生方法发送邮件感觉略显麻烦,于是学习了下 Java 发送邮件的方法,然后自己封装了一层流式风格的 `MailSender`:
MailSender
见:https://github.com/YouthLin/java-utils/tree/master/mail
package com.youthlin.utils.mail;
import net.markenwerk.utils.mail.dkim.DkimMessage;
import net.markenwerk.utils.mail.dkim.DkimSigner;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.mail.Address;
import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.io.InputStream;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* SMTP 邮件发送工具.
* <p>
* Created by lin on 2017-01-23-023.
* <p>
* 示例:
* <pre>
* new MailSender()
* .start(new MailSender.SessionBuilder()
* .host("host")
* .auth("username", "password")
* .ssl(465) // JDK 7 OK. JDK8 由于安全原因需要替换俩 jar 包
* .debug() // 注释这行关闭调试信息的输出
* .toSession()
* )//Session 只能调用一次 start
* .from("email", "DisplayName")//发件人 email,name
* .to("to", "DisplayName")//收件人
* .cc("cc")//抄送
* .cc("another", "name")
* .bcc("bcc")//密送
* .subject("subject")//主题
* .text("content")//只能设置一次内容. 或[html("html content")][content("plain",false)][content("html",true);]
* .attachment("path/to/file", "cid")//内嵌附件
* .attachment("path/to/file")//普通附件
* .attachment(file)//普通附件
* .dkim(new File("D:/key.der"), "youthlin.com", "xxx.youthlin")//验证发信人身份
* .send();//发送
* </pre>
* <p>
* 使用 DKIM 验证发信人身份:
* <ol>
* <li>使用 OpenSSL 生成密钥对, 或者在 http://dkimcore.org/tools/ 生成</li>
* <li>再把 Base64 文本格式的私钥转换为 der 二进制证书:<br>
* <code>openssl pkcs8 -topk8 -nocrypt -in key.pem -outform der -out key.der</code><br>
* 其中 key.pem 是第一步生成的私钥, 以「-----BEGIN RSA PRIVATE KEY-----」开头的文件, key.der 是要保存的文件</li>
* <li>把公钥部署到域名的 TXT 记录中, 格式可参加步骤 1 网址.(记录名:xxx._domainkey, 记录值:p=xxx的一串 不含「p=」和末尾冒号)</li>
* <li>使用 dkim 方法验证自己的身份:<br>
* <code>privateDERKey</code> 是 der 私钥, <code>domain</code> 是域名, <code>selector</code> 是记录名(xxx._domainkey中的xxx)</li>
* </ol>
* <p>
* 注意:<br>
* Gmail 对附件要求严格, 对于下列类型的附件(或包含这些文件类型的压缩文件), 即使使用 DKIM 认证仍然会被退信:<br>
* <pre>
* .ADE、.ADP、.BAT、.CHM、.CMD、.COM、.CPL、.EXE、.HTA、.INS、.ISP、
* .JAR、.JS、.JSE、.LIB、.LNK、.MDE、.MSC、.MSI、.MSP、.MST、.PIF、
* .SCR、.SCT、.SHB、.SYS、.VB、.VBE、.VBS、.VXD、.WSC、.WSF、.WSH</pre>
* 详见<a href="https://support.google.com/mail/answer/6590">某些文件类型被阻止 - Gmail 帮助</a>
*/
@SuppressWarnings({"WeakerAccess", "SameParameterValue", "unused"})
public class MailSender {
/**
* Session 构造器.
* Session 是与服务器通信的前提环境, 如 host username password 等在此设置.
* <p>
* 示例:
* <pre>
* new MailSender.SessionBuilder()
* .host("host")
* .auth("username", "password")
* .ssl(465)
* .toSession()
* </pre>
*/
public static class SessionBuilder {
private final Properties props = new Properties();
private Authenticator authenticator = null;
public SessionBuilder host(String host) {
props.put("mail.host", host);
return this;
}
/**
* 如果需要授权才能登录的服务器, 那么请提供账号和密码.
*/
public SessionBuilder auth(final String username, final String password) {
props.put("mail.smtp.auth", true);
authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
};
return this;
}
/**
* 端口号,默认25
*/
public SessionBuilder port(int port) {
props.put("mail.smtp.port", port);
return this;
}
/**
* 需要 SSL 安全连接, 则请提供端口.
* <p>
* JDK8 不能使用 SSL, 需要替换 <code>JDK_HOME/jre/lib/security/</code> 下的两个 jar 包, 下载地址见下
*
* @see <a href="http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html">http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</a>
*/
public SessionBuilder ssl(int port) {
props.put("mail.smtp.ssl.enable", true);//使用 SSL
props.put("mail.smtp.socketFactory.port", port);
return port(port);
}
/**
* 是否开启 Debug 输出, 默认否.
*/
public SessionBuilder debug(boolean debug) {
props.put("mail.debug", Boolean.toString(debug));//必须要是 String 类型
return this;
}
public SessionBuilder debug() {
return debug(true);
}
public Session toSession() {
return Session.getInstance(props, authenticator);
}
}
//region //field
private static final String default_charset = "UTF-8";//默认字符编码
private final MimeMultipart content = new MimeMultipart();//邮件的所有内容(body+Attachment)
private final BodyPart body = new MimeBodyPart();//body
private final List<BodyPart> attachments = new ArrayList<BodyPart>();//attachments
private boolean started = false;//是否已经设置 Session
private boolean contentHasSet = false;//是否已经设置过内容
private MimeMessage msg;//每次设置的 Message 主体
private DkimSigner signer;//DKIM 邮件认证
//endregion //field
/**
* 从 Session 构造 Message.
*
* @throws IllegalStateException 当已经调用过本方法再次调用时抛出
*/
public MailSender start(Session session) {
if (started) {
throw new IllegalStateException("start() method already called.");
}
started = true;
msg = new MimeMessage(session);
return this;
}
public MailSender from(String email) throws MessagingException {
msg.setFrom(new InternetAddress(email));
return this;
}
public MailSender from(String email, String name) throws MessagingException {
try {
msg.setFrom(new InternetAddress(email, name, default_charset));
} catch (UnsupportedEncodingException e) {
from(email);
}
return this;
}
//region //recipients
public MailSender to(String email) throws MessagingException {
return to(new String[]{email}, null);
}
public MailSender to(String email, String name) throws MessagingException {
return to(new String[]{email}, new String[]{name});
}
public MailSender to(String[] emails) throws MessagingException {
return to(emails, null);
}
public MailSender to(String[] emails, String[] names) throws MessagingException {
return addRecipients(Message.RecipientType.TO, toAddresses(emails, names));
}
public MailSender cc(String email) throws MessagingException {
return cc(new String[]{email}, null);
}
public MailSender cc(String email, String name) throws MessagingException {
return cc(new String[]{email}, new String[]{name});
}
public MailSender cc(String[] emails) throws MessagingException {
return cc(emails, null);
}
public MailSender cc(String[] emails, String[] names) throws MessagingException {
return addRecipients(Message.RecipientType.CC, toAddresses(emails, names));
}
public MailSender bcc(String email) throws MessagingException {
return bcc(new String[]{email}, null);
}
public MailSender bcc(String email, String name) throws MessagingException {
return bcc(new String[]{email}, new String[]{name});
}
public MailSender bcc(String[] emails) throws MessagingException {
return bcc(emails, null);
}
public MailSender bcc(String[] emails, String[] names) throws MessagingException {
return addRecipients(Message.RecipientType.BCC, toAddresses(emails, names));
}
private Address[] toAddresses(String[] emails, String[] names) throws AddressException {
int eLen = emails.length;
Address[] addresses = new Address[eLen];
if (names != null && names.length == eLen) {//email 和 name 能对上时
try {
for (int i = 0; i < eLen; i++) {
addresses[i] = new InternetAddress(emails[i], names[i], default_charset);
}
return addresses;
} catch (UnsupportedEncodingException ignore) {
}
}
for (int i = 0; i < eLen; i++) {//对不上 或 异常
addresses[i] = new InternetAddress(emails[i]);
}
return addresses;
}
private MailSender addRecipients(Message.RecipientType type, Address[] addresses) throws MessagingException {
msg.addRecipients(type, addresses);
return this;
}
//endregion //recipients
public MailSender subject(String subject) throws MessagingException {
msg.setSubject(subject);
return this;
}
//region //content
public MailSender html(String html) throws MessagingException {
return content(html, true);
}
public MailSender text(String plain) throws MessagingException {
return content(plain, false);
}
/**
* 设置邮件内容.
* <p>
* 也可以使用 <code>html()</code> 或 <code>text()</code> 设置, 但只能设置一次内容, 否则将抛出异常.
*
* @throws IllegalStateException 当多次设置邮件内容时抛出.
*/
public MailSender content(String content, boolean isHtml) throws MessagingException {
if (contentHasSet) {
throw new IllegalStateException("Content already set.");
}
contentHasSet = true;
if (isHtml) {
body.setContent(content, "text/html;charset=" + default_charset);
} else {
body.setContent(content, "text/plain;charset=" + default_charset);
}
return this;
}
//endregion //content
//region //attachment
/**
* 带附件.
* <p>
* 在「附件」中显示.
*/
public MailSender attachment(String pathToFile) throws MessagingException {
return attachment(pathToFile, null);
}
/**
* 带附件.
* <p>
* 没有设置 cid 则只在附件中显示,设置了 cid 则 对应 html 邮件内容. 如:<img src="cid:img1"/>
* Outlook - cid 只支持图片, 文件的 cid 在 a 标签 href 属性中不起作用,只在附件中显示
* QQ - cid 支持二进制文件,有 cid 的附件将不会在「附件」中显示
*/
public MailSender attachment(String pathToFile, String cid) throws MessagingException {
return attachment(new File(pathToFile), cid);
}
public MailSender attachment(File file) throws MessagingException {
return attachment(file, null);
}
public MailSender attachment(File file, String cid) throws MessagingException {
BodyPart attach = new MimeBodyPart();
attach.setDataHandler(new DataHandler(new FileDataSource(file)));
try {
attach.setFileName(MimeUtility.encodeWord(file.getName(), default_charset, null));
} catch (UnsupportedEncodingException ignore) {
}
if (cid != null) {
attach.setHeader("Content-ID", cid);
}
attachments.add(attach);
return this;
}
//endregion //attachment
//region //dkim
public MailSender dkim(File privateDERKey, String domain, String selector) {
try {
signer = new DkimSigner(domain, selector, privateDERKey);
} catch (Exception e) {
signer = null;
e.printStackTrace();
}
return this;
}
public MailSender dkim(byte[] privateDERKey, String domain, String selector) {
return dkim(new ByteArrayInputStream(privateDERKey), domain, selector);
}
public MailSender dkim(InputStream privateDERKey, String domain, String selector) {
try {
signer = new DkimSigner(domain, selector, privateDERKey);
} catch (Exception e) {
signer = null;
e.printStackTrace();
}
return this;
}
public MailSender dkim(RSAPrivateKey privateKey, String domain, String selector) {
signer = new DkimSigner(domain, selector, privateKey);
return this;
}
//endregion
public Message toMessage() throws MessagingException {
content.removeBodyPart(body);//防止多次调用添加多次
content.addBodyPart(body);//顺序:body 在 attachment 之前
for (BodyPart attach : attachments) {
content.removeBodyPart(attach);
content.addBodyPart(attach);
}
msg.setContent(content);
if (signer != null) {
return new DkimMessage(msg, signer);
}
return msg;
}
public void send() throws MessagingException {
Transport.send(toMessage(), msg.getAllRecipients());
}
}
测试代码
public static void main(String[] args) throws javax.mail.MessagingException {
new MailSender()
.start(new MailSender.SessionBuilder()
.host("mail.qq.com")
.auth("[email protected]", "password")
//.ssl(465)// JDK 7 OK. JDK8 由于安全原因需要替换俩 jar 包
.debug()
.toSession()
)//Session 只能调用一次 start
.from("[email protected]")//发件人 email,name
.to("[email protected]")//收件人
.cc("[email protected]")//抄送
.subject("测试邮件")//主题
.html("<span style='color:blue;'>Hello</span>,<br>World!")//只能设置一次内容. 或[html("html content")][content("plain",false)][content("html",true);]
.attachment("/path/to/attachment/file")
//.dkim(new java.io.File("/path/to/rsa.der"), "domain", "selector")
.send();//发送
}
说明
- SSL JDK8 不能使用 SSL, 会报异常,需要替换 `JDK_HOME/jre/lib/security/` 下的两个 jar 包, 下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
- 附件 `attachment(String pathToFile)` 直接作为附件,`attachment(String pathToFile, String cid)` 作为 *内嵌附件*, 在 HTML 可以使用 cid:xxx 引用,一般用于内嵌图片(`attachment(“/path/to/pic.png”, “pic”)`, `<img src=’cid:pic’/>`)
- DKIM 用于垃圾邮件反查,确定邮件的确是发送方发送的。
你可以在 http://dkimcore.org/tools/ 生成一对密钥,公钥在 DNS 里设置 TXT 记录,私钥在发送邮件时使用 dkim() 方法带上,这样对方收到邮件时就会去你的域名下根据 selector 查 DNS TXT 记录从而确定邮件确实是域名所有者发送的。
这里 dkim 依赖的是第三方包:<dependency> <groupId>net.markenwerk</groupId> <artifactId>utils-mail-dkim</artifactId> <version>1.1.7</version> </dependency>
目标每月至少一篇文章。本月目标完成~ 竟然在最后一天交工……
目前处于实习技术培训期,特忙,心累;还有毕设还没怎么做,毕不了业了……
声明
- 本作品采用署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。除非特别注明, 霖博客文章均为原创。
- 转载请保留本文(《Java 邮件发送》)链接地址: https://youthlin.com/?p=1417
- 订阅本站:https://youthlin.com/feed/
“Java 邮件发送”上的1条回复
同样毕业在即,祝一切顺利~![[/调皮]](https://youthlin.com/wp-content/themes/twentytwenty-child/images/smilies/调皮.gif)