为了提升安全性,Office365
升级了邮件客户端认证模式,需要使用OAuth 2.0
认证,废弃以前老的用户名和密码的方式。因此以前用JavaMail
开发的邮件发送和接收功能将不能使用,比如从其他邮件服务迁移到Office365
,必须用新的方式修改相关代码。
升级说明
建议租户禁用基本身份验证,并迁移到新式客户端的新式身份验证租户。也就是【现代身份验证(基于 OAuth 2.0
令牌的身份验证)】
OAuth 2.0认证
OAuth 2.0认证模式有四种模式
-
客户端认证模式(POST)
- https://xxxxxxxx/oauth/token?grant_type=client_credentials&client_id=xxxx&client_secret=xxxx
- 第三方客户端授权,一般是可信任的客户端,不需要用户授权
-
资源密码模式(POST)
-
隐式授权模式(GET)
- http://xxxxxxxx/oauth/authorize?response_type=token&client_id=xxxx
- 授权后
redirect
带回access_token,安全性比授权码模式低
-
授权码模式(需要两步,常见且安全性较高)
- 第一步获取code(GET)
https://xxxxxxxx/oauth/authorize?response_type=code&client_id=xxxx - 第二步获取token,需要传递上一步返回的code(POST)
https://xxxxxxxx/oauth/token?grant_type=authorization_code&code=code&client_id=xxxx
- 第一步获取code(GET)
了解更多OAuth2
:https://learn.microsoft.com/zh-cn/azure/active-directory/develop/active-directory-v2-protocols
提前准备
目前似乎是保留了SMTP模式的基本身份验证:
在 2022 年 10 月 1 日永久禁用基本身份验证时,SMTP 身份验证仍可用。 SMTP 仍然可用的原因是,许多多功能设备(如打印机和扫描仪)无法更新为使用新式身份验证。 但是,我们强烈建议客户尽可能不使用基本身份验证和 SMTP AUTH。 发送经过身份验证的邮件的其他选项包括使用替代协议,例如 Microsoft 图形 API。
首先应该要注册APP:https://learn.microsoft.com/zh-cn/azure/active-directory/develop/quickstart-register-app
由于是服务器后台发送和接收邮件,是预设的邮件地址和账号,因此选用client_credentials
模式,在Office365
上配置好相应的APP
,并得到tenant
、clientId
和clientSecret
。
主要有下面三个配置项需要用到:
应用程序(客户端) ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
目录(租户) ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
clientSecret (12个月):xxxx~xxxxxxxxxxxxxxxxxxxxxx
服务地址
Email提供程序 | IMAP 设置 | POP 设置 | SMTP 设置 |
---|---|---|---|
Microsoft 365 Outlook Hotmail Live.com |
服务器:outlook.office365.com 端口:993 加密:SSL/TLS |
服务器:outlook.office365.com 端口:995 加密:SSL/TLS |
服务器:smtp.office365.com 端口:587 加密:STARTTLS |
Java客户端升级
JavaMail客户端需要升级:
文档地址:https://javaee.github.io/javamail/OAuth2
升级到1.5.5
之后,最新应该是1.6.2
。
不支持pop3
从JavaMail
不支持POP3
传输OAuth2
令牌,只能使用IMAP
协议
使用pop3
协议执行会报错误:
javax.mail.AuthenticationFailedException: Protocol error. Connection is closed. 10
at com.sun.mail.pop3.POP3Store.protocolConnect(POP3Store.java:213)
at javax.mail.Service.connect(Service.java:366)
at javax.mail.Service.connect(Service.java:246)
实际代码
简单依赖,httpclient
、slf4j
日志、jackson
的json
解析器、以及javax.mail
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.2</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
</dependencies>
测试代码:
public class Office365MailTest {
private static final Logger LOGGER = LoggerFactory.getLogger(Office365MailTest.class);
private static final String SMTP_SERVER_ADDR = "smtp.office365.com";
private static final int SMTP_SERVER_PORT = 587;
private static final String IMAP_SERVER_ADDR = "outlook.office365.com";
private static final int IMAP_SERVER_PORT = 993;
private static final String USER_NAME = "xxxx@xxxx.com";
private static final String MAIL_FROM = "xxxx@xxxx.com";
private static final String MAIL_TO = "xxxx@xxxx.com";
private static final String MAIL_SUBJECT = "Test测试邮件标题";
private static final String MAIL_CONTENT = "Test测试邮件正文";
private static final String TENANT_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx";
private static final String CLIENT_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx";
private static final String CLIENT_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxx";
public static void main(String[] args) throws Exception {
// sendEmail(false);
recvMail(false);
}
private static void sendEmail(boolean debug) throws Exception {
String protocol = "smtp";
Properties props = getBaseProperties(protocol, SMTP_SERVER_ADDR, SMTP_SERVER_PORT, debug);
final Session session = Session.getInstance(props);
try {
final Message message = new MimeMessage(session);
message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO));
message.setFrom(new InternetAddress(MAIL_FROM));
message.setSubject(MAIL_SUBJECT);
message.setText(MAIL_CONTENT);
message.setSentDate(new Date());
Transport transport = session.getTransport(protocol);
transport.connect(SMTP_SERVER_ADDR, USER_NAME, getAuthToken(TENANT_ID, CLIENT_ID, CLIENT_SECRET));
transport.send(message);
} catch (final MessagingException ex) {
LOGGER.error("邮件发送错误", ex);
}
}
private static void recvMail(boolean debug) throws Exception {
String protocol = "imap";
Properties props = getBaseProperties(protocol, IMAP_SERVER_ADDR, IMAP_SERVER_PORT, debug);
props.put("mail.store.protocol", protocol);
String token = getAuthToken(TENANT_ID, CLIENT_ID, CLIENT_SECRET);
Session session = Session.getInstance(props);
Store store = session.getStore(protocol);
store.connect(IMAP_SERVER_ADDR, USER_NAME, token);
Folder folder = store.getFolder("INBOX"); // 读inbox
folder.open(Folder.READ_WRITE);
Message[] messages = folder.getMessages();
for (Message message : messages) {
LOGGER.info("{}", message.getSubject());
}
}
private static Properties getBaseProperties(String protocol, String address, Integer port, boolean debug) {
Properties props = new Properties();
props.put("mail." + protocol + ".host", address);
props.put("mail." + protocol + ".port", port);
if ("smtp".equals(protocol)) {
props.put("mail." + protocol + ".starttls.enable", "true");
} else {
props.put("mail." + protocol + ".ssl.enable", "true");
}
props.put("mail." + protocol + ".ssl.protocols", "TLSv1.2");
props.put("mail." + protocol + ".auth", "true");
props.put("mail." + protocol + ".auth.mechanisms", "XOAUTH2");
if (debug) { // 调试模式
props.put("mail.debug", "true");
props.put("mail.debug.auth", "true");
}
return props;
}
private static String getAuthToken(String tenant, String clientId, String clientSecret) throws Exception {
CloseableHttpClient client = null;
CloseableHttpResponse loginResponse = null;
try {
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, (cert, authType) -> true).build();
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
client = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();
HttpPost loginPost = new HttpPost("https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token");
String scopes = "https://outlook.office365.com/.default";
String encodedBody = "client_id=" + clientId + "&scope=" + scopes + "&client_secret=" + clientSecret
+ "&grant_type=client_credentials";
loginPost.setEntity(new StringEntity(encodedBody, ContentType.APPLICATION_FORM_URLENCODED));
loginPost.addHeader(new BasicHeader("cache-control", "no-cache"));
loginResponse = client.execute(loginPost);
InputStream inputStream = loginResponse.getEntity().getContent();
Map<String, String> parsed = new ObjectMapper().readValue(inputStream, Map.class);
return parsed.get("access_token");
} finally {
HttpClientUtils.closeQuietly(client);
HttpClientUtils.closeQuietly(loginResponse);
}
}
}
收邮件成功:
16:26:41.567 [main] INFO com.fugary.mail.Office365MailTest - test标题
16:26:42.189 [main] INFO com.fugary.mail.Office365MailTest - test
16:26:42.811 [main] INFO com.fugary.mail.Office365MailTest - 最新测试版邮件发送
常见错误
- IMAP错误
javax.mail.MessagingException: A3 BAD User is authenticated but not connected.
;
nested exception is:
com.sun.mail.iap.BadCommandException: A3 BAD User is authenticated but not connected.
问题一般是用户没有权限,可以配置下权限或者换一个有权限的用户。
- SMTP错误
javax.mail.AuthenticationFailedException: 535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Tenant. Visit https://aka.ms/smtp_auth_disabled for more information. [SJ0PR03CA0292.namprd03.prod.outlook.com 2023-03-02T08:56:02.982Z 08DB1AFAC2F526FF]
at com.sun.mail.smtp.SMTPTransport$Authenticator.authenticate(SMTPTransport.java:965) at com.sun.mail.smtp.SMTPTransport.authenticate(SMTPTransport.java:876) at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:780) at javax.mail.Service.connect(Service.java:366) at javax.mail.Service.connect(Service.java:246) at com.fugary.mail.Office365MailTest.sendEmail(Office365MailTest.java:66) at com.fugary.mail.Office365MailTest.main(Office365MailTest.java:50)
没有开启SMTP客户端。
您好,我想问一下如果通过页面跳转方式认证,应该如何实现,能出一篇文章讲解吗?微软官方文档对这块没有写的特别清楚,看起来也需要事先注册分配好租户,但实际上我们用foxmail或者QQ邮箱登录时,并没有让你去填写租户id,他们是如何实现的呢
我的程序跟你没有太大差别,
Store store = session.getStore(protocol);
store.connect(IMAP_SERVER_ADDR, USER_NAME, token);、
链接时一直报 A1 NO AUTHENTICATE failed.
javax.mail.AuthenticationFailedException: AUTHENTICATE failed.
新版api权限已经不能手动添加:https://outlook.office365.com/IMAP.AccessAsUser.All 权限,请问有遇到这个问题吗?
权限问题一般找管理员开
2.Install ExchangeOnlineManagement Install-Module -Name ExchangeOnlineManagement -allowprerelease Import-module ExchangeOnlineManagement Connect-ExchangeOnline -Organization
3.Register Service Principal in Exchange: 1.New-ServicePrincipal -AppId -ServiceId [-Organization ] Make sure to use ObjectId from enterprise applications rather than object id of application registration.
For the same application you registered in Application Registration. A corresponding application has been created in Enterprise Application as well. You need to pass object id from there while registering service principal in Exchange: User’s image
2.Get-ServicePrincipal | fl
3.Add-MailboxPermission -Identity “[john.smith@contoso.com]” -User -AccessRights FullAccess
我在操作这些命令时powershell一直报错,你有遇到过类似问题吗?或者管理员应该怎么操作呢我的客户如果开通这些权限应该怎么操作这几个命令呢?