如需转载,请根据 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 许可,附上本文作者及链接。
本文作者: 执笔成念
作者昵称: zbcn
本文链接: https://1363653611.github.io/zbcn.github.io/2021/02/19/socket_03Mail/
发送邮件
Email就是电子邮件。
MUA(邮件软件):Mail User Agent,意思是给用户服务的邮件代理
MTA(邮件服务器):Mail Transfer Agent,意思是邮件中转的代理
MDA(最终到达的邮件服务器):Mail Delivery Agent,意思是邮件到达的代理,电子邮件一旦到达MDA,就不再动了。实际上 电子邮件通常就存储在MDA服务器的硬盘上,然后等收件人通过软件或者登陆浏览器查看邮件。
MTA和MDA这样的服务器软件通常是现成的,我们不关心这些服务器内部是如何运行的。要发送邮件,我们关心的是如何编写一个MUA的软件,把邮件发送到MTA上。
MUA到MTA发送邮件的协议就是SMTP协议,它是Simple Mail Transport Protocol的缩写,使用标准端口25,也可以使用加密端口465或587。
SMTP协议是一个建立在TCP之上的协议,任何程序发送邮件都必须遵守SMTP协议。使用Java程序发送邮件时,我们无需关心SMTP协议的底层原理,只需要使用JavaMail这个标准API就可以直接发送邮件。
准备SMTP登录信息
假设我们准备使用自己的邮件地址me@example.com
给小明发送邮件,已知小明的邮件地址是xiaoming@somewhere.com
,发送邮件前,我们首先要确定作为MTA的邮件服务器地址和端口号。邮件服务器地址通常是smtp.example.com
,端口号由邮件服务商确定使用25、465还是587。以下是一些常用邮件服务商的SMTP信息:
- QQ邮箱:SMTP服务器是smtp.qq.com,端口是465/587;
- 163邮箱:SMTP服务器是smtp.163.com,端口是465;
- Gmail邮箱:SMTP服务器是smtp.gmail.com,端口是465/587。
有了SMTP服务器的域名和端口号,我们还需要SMTP服务器的登录信息,通常是使用自己的邮件地址作为用户名,登录口令是用户口令或者一个独立设置的SMTP口令。
使用 javaMail 来发送邮件
引入 JavaMail 的两个相关依赖
1 | <dependency> |
2 | <groupId>javax.mail</groupId> |
3 | <artifactId>javax.mail-api</artifactId> |
4 | <version>1.6.2</version> |
5 | </dependency> |
6 | <dependency> |
7 | <groupId>com.sun.mail</groupId> |
8 | <artifactId>javax.mail</artifactId> |
9 | <version>1.6.2</version> |
10 | </dependency> |
### 我们通过JavaMail API连接到SMTP服务器上
1 | private static Session buildMailSession() { |
2 | Properties props = new Properties(); |
3 | props.put("mail.smtp.host", smtp); // SMTP主机名 |
4 | props.put("mail.smtp.port", port); // 主机端口号 |
5 | props.put("mail.smtp.auth", "true"); // 是否需要用户认证 |
6 | props.put("mail.smtp.starttls.enable", "true"); // 启用TLS加密 |
7 | |
8 | Session session = Session.getDefaultInstance(props, new Authenticator() { |
9 | |
10 | protected PasswordAuthentication getPasswordAuthentication() { |
11 | return new PasswordAuthentication(userName, pwd); |
12 | } |
13 | }); |
14 | |
15 | // 设置debug模式便于调试: |
16 | session.setDebug(true); |
17 | return session; |
18 | } |
发送邮件
发送邮件时,我们需要构造一个Message
对象,然后调用Transport.send(Message)
即可完成发送:
1 | private static void sendMessage(Session session){ |
2 | try { |
3 | MimeMessage message = new MimeMessage(session); |
4 | // 设置发送方地址: |
5 | message.setFrom(new InternetAddress(userName)); |
6 | // 设置接收方地址: |
7 | message.setRecipient(Message.RecipientType.TO, new InternetAddress("zbcn810@163.com")); |
8 | // 设置邮件主题: |
9 | message.setSubject("test Mail", "UTF-8"); |
10 | // 设置邮件正文: |
11 | message.setText("Hi zbcn...", "UTF-8"); |
12 | // 发送: |
13 | Transport.send(message); |
14 | } catch (MessagingException e) { |
15 | e.printStackTrace(); |
16 | } |
17 | } |
绝大多数邮件服务器要求发送方地址和登录用户名必须一致,否则发送将失败。
填入真实的地址,运行上述代码,我们可以在控制台看到JavaMail打印的调试信息:
1 | 这是JavaMail打印的调试信息: |
2 | DEBUG: setDebug: JavaMail version 1.6.2 |
3 | DEBUG: getProvider() returning javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle] |
4 | DEBUG SMTP: need username and password for authentication |
5 | DEBUG SMTP: protocolConnect returning false, host=smtp.qq.com, user=zbcn8, password=<null> |
6 | DEBUG SMTP: useEhlo true, useAuth true |
7 | 尝试连接 smtp.qq.com 服务 |
8 | DEBUG SMTP: trying to connect to host "smtp.qq.com", port 25, isSSL false |
9 | 220 newxmesmtplogicsvrszb6.qq.com XMail Esmtp QQ Mail Server. |
10 | DEBUG SMTP: connected to host "smtp.qq.com", port: 25 |
11 | 发送命令EHLO: |
12 | EHLO windows10.microdone.cn |
13 | SMTP服务器响应250: |
14 | 250-newxmesmtplogicsvrszb6.qq.com |
15 | 250-PIPELINING |
16 | ... |
17 | 发送命令STARTTLS: |
18 | STARTTLS |
19 | SMTP服务器响应220: |
20 | 220 Ready to start TLS from 125.35.5.253 to newxmesmtplogicsvrszb6.qq.com. |
21 | EHLO windows10.microdone.cn |
22 | ... |
23 | DEBUG SMTP: protocolConnect login, host=smtp.qq.com, user=1363653611@qq.com, password=<non-null> |
24 | DEBUG SMTP: Attempt to authenticate using mechanisms: LOGIN PLAIN DIGEST-MD5 NTLM XOAUTH2 |
25 | DEBUG SMTP: Using mechanism LOGIN |
26 | DEBUG SMTP: AUTH LOGIN command trace suppressed |
27 | 登录成功: |
28 | DEBUG SMTP: AUTH LOGIN succeeded |
29 | DEBUG SMTP: use8bit false |
30 | 开始发送邮件,设置FROM: |
31 | MAIL FROM:<1363653611@qq.com> |
32 | 250 OK. |
33 | 设置TO: |
34 | RCPT TO:<zbcn810@163.com> |
35 | 250 OK |
36 | DEBUG SMTP: Verified Addresses |
37 | DEBUG SMTP: zbcn810@163.com |
38 | 发送邮件数据: |
39 | DATA |
40 | 354 End data with <CR><LF>.<CR><LF>. |
41 | 真正的邮件数据 |
42 | Date: Wed, 30 Sep 2020 10:03:50 +0800 (CST) |
43 | From: 1363653611@qq.com |
44 | To: zbcn810@163.com |
45 | 邮件主题是编码后的文本: |
46 | Message-ID: <1144748369.0.1601431430697@windows10.microdone.cn> |
47 | Subject: test Mail |
48 | MIME-Version: 1.0 |
49 | Content-Type: text/plain; charset=UTF-8 |
50 | Content-Transfer-Encoding: 7bit |
51 | |
52 | Hi zbcn... |
53 | . |
54 | 250 OK: queued as. |
55 | DEBUG SMTP: message successfully delivered to mail server |
56 | 发送QUIT命令: |
57 | QUIT |
58 | 服务器响应221结束TCP连接: |
59 | 221 Bye. |
上面的调试信息可以看出,SMTP协议是一个请求-响应协议,客户端总是发送命令,然后等待服务器响应。服务器响应总是以数字开头,后面的信息才是用于调试的文本。这些响应码已经被定义在SMTP协议中了,查看具体的响应码就可以知道出错原因。
发送html 邮件
发送HTML邮件和文本邮件是类似的,只需要把:
1 | message.setText(body, "UTF-8"); |
改为:
1 | message.setText(body, "UTF-8", "html"); |
发送附件
要在电子邮件中携带附件,我们就不能直接调用message.setText()
方法,而是要构造一个Multipart
对象:
1 | /** |
2 | * 发送带附件邮件 |
3 | * @param session |
4 | */ |
5 | private static void sendAnneMessage(Session session){ |
6 | try { |
7 | MimeMessage message = new MimeMessage(session); |
8 | |
9 | Multipart multipart = new MimeMultipart(); |
10 | // 添加text: |
11 | BodyPart textpart = new MimeBodyPart(); |
12 | textpart.setContent("带附件邮件", "text/html;charset=utf-8"); |
13 | multipart.addBodyPart(textpart); |
14 | // 添加image: |
15 | BodyPart imagepart = new MimeBodyPart(); |
16 | imagepart.setFileName("lunix目录说明.jpg"); |
17 | InputStream input = getFileAsStream("C:\\Users\\zbcn8\\Pictures\\lunix目录说明.jpg"); |
18 | imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(input, "application/octet-stream"))); |
19 | multipart.addBodyPart(imagepart); |
20 | |
21 | // 设置邮件内容为multipart: |
22 | message.setContent(multipart); |
23 | |
24 | // 设置发送方地址: |
25 | message.setFrom(new InternetAddress(userName)); |
26 | // 设置接收方地址: |
27 | message.setRecipient(Message.RecipientType.TO, new InternetAddress("zbcn810@163.com")); |
28 | // 设置邮件主题: |
29 | message.setSubject("Test Anne Mail", "UTF-8"); |
30 | // 发送: |
31 | Transport.send(message); |
32 | } catch (MessagingException | IOException e) { |
33 | e.printStackTrace(); |
34 | } |
35 | } |
- 一个
Multipart
对象可以添加若干个BodyPart
,其中第一个BodyPart
是文本,即邮件正文,后面的BodyPart是附件。 BodyPart
依靠setContent()
决定添加的内容,- 如果添加文本,用
setContent("...", "text/plain;charset=utf-8")
- 添加纯文本,或者用
setContent("...", "text/html;charset=utf-8")
添加HTML文本。 - 添加附件,需要设置文件名(不一定和真实文件名一致),并且添加一个
DataHandler()
,传入文件的MIME类型。二进制文件可以用application/octet-stream
,Word文档则是application/msword
。
- 如果添加文本,用
最后,通过setContent()
把Multipart
添加到Message
中,即可发送。
发送内嵌图片的HTML邮件
HTML邮件中可以内嵌图片,这是怎么做到的?
如果给一个
<img src="http://example.com/test.jpg">
,这样的外部图片链接通常会被邮件客户端过滤,并提示用户显示图片并不安全。只有内嵌的图片才能正常在邮件中显示。内嵌图片实际上也是一个附件,即邮件本身也是
Multipart
,但需要做一点额外的处理:
1 | Multipart multipart = new MimeMultipart(); |
2 | // 添加text: |
3 | BodyPart textpart = new MimeBodyPart(); |
4 | textpart.setContent("<h1>Hello</h1><p><img src=\"cid:img01\"></p>", "text/html;charset=utf-8"); |
5 | multipart.addBodyPart(textpart); |
6 | // 添加image: |
7 | BodyPart imagepart = new MimeBodyPart(); |
8 | imagepart.setFileName(fileName); |
9 | imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(input, "image/jpeg"))); |
10 | // 与HTML的<img src="cid:img01">关联: |
11 | imagepart.setHeader("Content-ID", "<img01>"); |
12 | multipart.addBodyPart(imagepart); |
在HTML邮件中引用图片时,需要设定一个ID,用类似<img src=\"cid:img01\">
引用,然后,在添加图片作为BodyPart时,除了要正确设置MIME类型(根据图片类型使用image/jpeg
或image/png
),还需要设置一个Header:
1 | imagepart.setHeader("Content-ID", "<img01>"); |
总结
- 使用JavaMail API发送邮件本质上是一个MUA软件通过SMTP协议发送邮件至MTA服务器;
- 打开调试模式可以看到详细的SMTP交互信息;
- 某些邮件服务商需要开启SMTP,并需要独立的SMTP登录密码。
接受邮件
- 经过发送给邮件后,邮件最终到达收件人的MDA服务器,所以,接收邮件是收件人用自己的客户端把邮件从MDA服务器上抓取到本地的过程。
- 接收邮件使用最广泛的协议是POP3:Post Office Protocol version 3,它也是一个建立在TCP连接之上的协议.POP3服务器的标准端口是110,如果整个会话需要加密,那么使用加密端口995。
- 另一种接收邮件的协议是IMAP:Internet Mail Access Protocol,它使用标准端口143和加密端口993。
- IMAP和POP3的主要区别是,IMAP协议在本地的所有操作都会自动同步到服务器上,并且,IMAP可以允许用户在邮件服务器的收件箱中创建文件夹。
JavaMail也提供了IMAP协议的支持。因为POP3和IMAP的使用方式非常类似,因此我们只介绍POP3的用法。
POP3 接受邮件
使用POP3收取Email时,我们无需关心POP3协议底层,因为JavaMail提供了高层接口。首先需要连接到Store对象:
1 | /** |
2 | * 创建Store ,用来接受和存储邮件 |
3 | * @return |
4 | * @throws MessagingException |
5 | */ |
6 | private static Store buildStore() throws MessagingException { |
7 | Session session = buildSession(); |
8 | URLName url = new URLName("pop3", host, port, "", username, password); |
9 | Store store = new POP3SSLStore(session, url); |
10 | store.connect(); |
11 | return store; |
12 | } |
13 | |
14 | /** |
15 | * 获取连接session |
16 | * @return |
17 | */ |
18 | public static Session buildSession(){ |
19 | Properties props = new Properties(); |
20 | props.setProperty("mail.store.protocol", "pop3"); // 协议名称 |
21 | props.setProperty("mail.pop3.host", host);// POP3主机名 |
22 | props.setProperty("mail.pop3.port", String.valueOf(port)); // 端口号 |
23 | |
24 | // 启动SSL: |
25 | props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); |
26 | props.put("mail.smtp.socketFactory.port", String.valueOf(port)); |
27 | Session session = Session.getInstance(props, null); |
28 | session.setDebug(true); // 显示调试信息 |
29 | return session; |
30 | } |
一个Store
对象表示整个邮箱的存储,要收取邮件,我们需要通过Store
访问指定的Folder
(文件夹),通常是INBOX
表示收件箱:
获取收件箱
1 | //创建指定文件夹("INBOX" 表示收件箱) |
2 | public static Folder buildFolder(Store store,String type){ |
3 | try { |
4 | Folder folder = store.getFolder(type); |
5 | |
6 | // 以读写方式打开: |
7 | folder.open(Folder.READ_WRITE); |
8 | // 打印邮件总数/新邮件数量/未读数量/已删除数量: |
9 | System.out.println("Total messages: " + folder.getMessageCount()); |
10 | System.out.println("New messages: " + folder.getNewMessageCount()); |
11 | System.out.println("Unread messages: " + folder.getUnreadMessageCount()); |
12 | System.out.println("Deleted messages: " + folder.getDeletedMessageCount()); |
13 | return folder; |
14 | } catch (MessagingException e) { |
15 | e.printStackTrace(); |
16 | } |
17 | return null; |
18 | } |
19 | private static void handleMessages(Folder folder) throws MessagingException, UnsupportedEncodingException { |
20 | Message[] messages = folder.getMessages(); |
21 | for (Message message : messages) { |
22 | // 打印每一封邮件: |
23 | printMessage((MimeMessage) message); |
24 | } |
25 | } |
26 | private static void printMessage(MimeMessage msg) throws MessagingException, UnsupportedEncodingException { |
27 | // 邮件主题: |
28 | System.out.println("Subject: " + MimeUtility.decodeText(msg.getSubject())); |
29 | // 发件人: |
30 | Address[] froms = msg.getFrom(); |
31 | InternetAddress address = (InternetAddress) froms[0]; |
32 | String personal = address.getPersonal(); |
33 | String from = personal == null ? address.getAddress() : (MimeUtility.decodeText(personal) + " <" + address.getAddress() + ">"); |
34 | System.out.println("From: " + from); |
35 | // 继续打印收件人: |
36 | Address[] allRecipients = msg.getAllRecipients(); |
37 | for (int i = 0; i < allRecipients.length; i++) { |
38 | Address recipient =allRecipients[i]; |
39 | System.out.println("Receive"+i + ":"+ recipient); |
40 | } |
41 | } |
比较麻烦的是获取邮件的正文。一个MimeMessage
对象也是一个Part
对象,它可能只包含一个文本,也可能是一个Multipart
对象,即由几个Part
构成,因此,需要递归地解析出完整的正文:
1 | private String getBody(Part part) throws MessagingException, IOException { |
2 | if (part.isMimeType("text/*")) { |
3 | // Part是文本: |
4 | return part.getContent().toString(); |
5 | } |
6 | if (part.isMimeType("multipart/*")) { |
7 | // Part是一个Multipart对象: |
8 | Multipart multipart = (Multipart) part.getContent(); |
9 | // 循环解析每个子Part: |
10 | for (int i = 0; i < multipart.getCount(); i++) { |
11 | BodyPart bodyPart = multipart.getBodyPart(i); |
12 | String body = getBody(bodyPart); |
13 | if (!body.isEmpty()) { |
14 | return body; |
15 | } |
16 | } |
17 | } |
18 | return ""; |
19 | } |
最后记得关闭Folder
和Store
:
1 | folder.close(true); // 传入true表示删除操作会同步到服务器上(即删除服务器收件箱的邮件) |
2 | store.close(); |
总结
- 使用Java接收Email时,可以用POP3协议或IMAP协议。
- 使用POP3协议时,需要用Maven引入JavaMail依赖,并确定POP3服务器的域名/端口/是否使用SSL等,然后,调用相关API接收Email。
- 设置debug模式可以查看通信详细内容,便于排查错误。