1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
|
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());
}
}
|
Reader Echoes
1 comment