使用AzureAPI实现outlook邮件扫描

Microsoft_Graph_API

Posted by John Doe on 2024-12-11
Words 1.6k and Reading Time 8 Minutes
Viewed Times

背景

之前产品有审批流程达到一定阶段后给领导发邮件发短信,其中发邮件部分功能是通过Microsoft Graph API实现,特此记录。

Azure官方文档

Graph API:

https://learn.microsoft.com/zh-cn/graph/use-the-api?context=graph%2Fapi%2F1.0&view=graph-rest-1.0

Azure 注册APP

https://portal.azure.cn/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps

Java程序依赖

不多bb直接上代码

1.POM依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>com.microsoft.graph</groupId>
<artifactId>microsoft-graph</artifactId>
<version>5.40.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.graph</groupId>
<artifactId>microsoft-graph-core</artifactId>
<version>2.0.14</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.23</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.7.0</version>
</dependency>

2.程序实现

2.1 获取邮件组邮件&http请求

这里使用OkHttp实现http请求

类属性介绍:

  1. authority:

    这是一个 URL 字符串,表示用于 Azure Active Directory (AAD) 认证的授权端点。

    它用于将认证请求定向到正确的 AAD 端点。

  1. scope:

    这个字符串定义了应用程序从 Microsoft Graph 请求的权限或范围。

  1. tokenCredAuthProvider:

    这是 TokenCredentialAuthProvider 的一个实例,用于对 Microsoft Graph 的请求进行认证。

    它通常使用凭据(如客户端 ID、密钥和租户 ID)来获取访问令牌,然后使用该令牌授权 API 请求

  1. tenantId:

    这是一个配置属性,保存 Azure Active Directory 的租户 ID。

    租户 ID 是应用程序注册所在的 Azure AD 实例的唯一标识符。

  2. clientId:

    这是一个配置属性,保存应用程序的客户端 ID。

    客户端 ID 是应用程序在 Azure AD 中注册时分配的唯一标识符。

  3. secret:

    这是一个配置属性,保存应用程序的客户端密钥。

    客户端密钥与客户端 ID 一起用于对应用程序进行 Azure AD 认证。

  4. mailbox:

    这是一个配置属性,指定应用程序将访问或扫描的邮箱的电子邮件地址。

    它用于标识应用程序在调用 Microsoft Graph API 时应与哪个邮箱进行交互。

  5. count:

    这是一个配置属性,可能指定要检索或处理的项目数量。

    它可以用于限制应用程序在单次操作中处理的电子邮件或其他项目的数量。

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
@Service
public class MicGraphServiceImpl implements MicGraphService {

private static final String authority = "https://login.chinacloudapi.cn/";
private final static String scope = "https://microsoftgraph.chinacloudapi.cn/.default";
private static TokenCredentialAuthProvider tokenCredAuthProvider = null;
@Value("${scanner.tenantId}")
private String tenantId;
@Value("${scanner.clientId}")
private String clientId;
@Value("${scanner.secret}")
private String secret;
@Value("${scanner.email}")
private String mailbox;

@Value("${scanner.count}")
private Integer count;

@Override
public MessageCollectionPage getGraph() {
try {
ArrayList<String> scopeList = new ArrayList<>();
scopeList.add(scope);
ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder()
.clientId(clientId)
.tenantId(tenantId)
.clientSecret(secret).authorityHost(AzureAuthorityHosts.AZURE_CHINA)
.build();
tokenCredAuthProvider = new TokenCredentialAuthProvider(scopeList, clientSecretCredential);
final GraphServiceClient<Request> graphClient = GraphServiceClient.builder().httpClient(okHttpClient()).authenticationProvider(tokenCredAuthProvider).buildClient();

graphClient.setServiceRoot("https://microsoftgraph.chinacloudapi.cn/v1.0");

List<QueryOption> queryOptions = Lists.newArrayList();
QueryOption queryOption = new QueryOption("top", count);
QueryOption queryOptionCount = new QueryOption("count", true);
queryOptions.add(queryOption);
queryOptions.add(queryOptionCount);
MessageCollectionPage pa = graphClient.users(mailbox).messages().buildRequest(queryOptions).get();
return pa;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException(JSON.toJSONString(e));
}
}

public OkHttpClient okHttpClient() {
try {
OkHttpClient build = new OkHttpClient.Builder()
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.connectTimeout(25, TimeUnit.SECONDS)
.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager())
.hostnameVerifier(SSLSocketClient.getHostnameVerifier())
.addInterceptor(new TelemetryHandler())
.addInterceptor(new AuthenticationHandler(tokenCredAuthProvider))
.addInterceptor(new RetryHandler())
.addInterceptor(new RedirectHandler())
.build();
return build;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

}

附:

跳过SSL验证配置

1、新建一个实现com.azure.core.http.HttpClientProvider接口的类,其中的核心代码为:

1
2
3
4
5
6
7
8
9
public class NettyAsyncHttpClientProvider implements HttpClientProvider {
...
trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init((KeyStore) null);
final Field factorySpi = trustManagerFactory.getClass().getDeclaredField("factorySpi");
factorySpi.setAccessible(true);
factorySpi.set(trustManagerFactory, new TrustManagerFactorySpi1());
...
}

这段代码用于将SslContext中的证书管理器替换为我们自己实现的证书管理器,从而实现跳过ssl认证。

Ps:这个类是从微软的实现中copy而来,类名没有改动,包名为:com.azure.core.http.netty , 官方代码如下:

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
package com.azure.core.http.netty;

import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpClientProvider;
import com.azure.core.util.Configuration;
import com.azure.core.util.HttpClientOptions;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.resources.ConnectionProvider.Builder;

public final class NettyAsyncHttpClientProvider implements HttpClientProvider {
private static final boolean AZURE_ENABLE_HTTP_CLIENT_SHARING;
private final boolean enableHttpClientSharing;
private static final int DEFAULT_MAX_CONNECTIONS = 500;

public NettyAsyncHttpClientProvider() {
this.enableHttpClientSharing = AZURE_ENABLE_HTTP_CLIENT_SHARING;
}

NettyAsyncHttpClientProvider(Configuration configuration) {
this.enableHttpClientSharing = (Boolean)configuration.get("AZURE_ENABLE_HTTP_CLIENT_SHARING", Boolean.FALSE);
}

public HttpClient createInstance() {
return this.enableHttpClientSharing ? NettyAsyncHttpClientProvider.GlobalNettyHttpClient.HTTP_CLIENT.getHttpClient() : (new NettyAsyncHttpClientBuilder()).build();
}

public HttpClient createInstance(HttpClientOptions clientOptions) {
if (clientOptions == null) {
return this.createInstance();
} else {
NettyAsyncHttpClientBuilder builder = new NettyAsyncHttpClientBuilder();
builder = builder.proxy(clientOptions.getProxyOptions()).configuration(clientOptions.getConfiguration()).connectTimeout(clientOptions.getConnectTimeout()).writeTimeout(clientOptions.getWriteTimeout()).responseTimeout(clientOptions.getResponseTimeout()).readTimeout(clientOptions.getReadTimeout());
Builder connectionProviderBuilder = ConnectionProvider.builder("azure-sdk");
connectionProviderBuilder.maxIdleTime(clientOptions.getConnectionIdleTimeout());
Integer maximumConnectionPoolSize = clientOptions.getMaximumConnectionPoolSize();
if (maximumConnectionPoolSize != null && maximumConnectionPoolSize > 0) {
connectionProviderBuilder.maxConnections(maximumConnectionPoolSize);
} else {
connectionProviderBuilder.maxConnections(500);
}

builder = builder.connectionProvider(connectionProviderBuilder.build());
return builder.build();
}
}

static {
AZURE_ENABLE_HTTP_CLIENT_SHARING = (Boolean)Configuration.getGlobalConfiguration().get("AZURE_ENABLE_HTTP_CLIENT_SHARING", Boolean.FALSE);
}

private static enum GlobalNettyHttpClient {
HTTP_CLIENT((new NettyAsyncHttpClientBuilder()).build());

private final HttpClient httpClient;

private GlobalNettyHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}

private HttpClient getHttpClient() {
return this.httpClient;
}
}
}

2、在resource目录下新建如下结构的文件,文件的类容为上面实现类的全类名,即包名+类名

azure

文件夹名称

META-INF /services/com.azure.core.http.HttpClientProvider

3.实际使用

通过上面代码我们就可以拿到这个租的邮件组的邮箱内部邮件,由此我们便可以实现回复审批功能,比如用户回复了一个approve后程序流程往下继续走下去。

实例代码:

  1. Microsoft Graph 官方定义的邮件接收对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.microsoft.graph.requests;

import com.microsoft.graph.http.BaseCollectionPage;
import com.microsoft.graph.models.Message;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class MessageCollectionPage extends BaseCollectionPage<Message, MessageCollectionRequestBuilder> {
public MessageCollectionPage(@Nonnull final MessageCollectionResponse response, @Nonnull final MessageCollectionRequestBuilder builder) {
super(response, builder);
}

public MessageCollectionPage(@Nonnull final List<Message> pageContents, @Nullable final MessageCollectionRequestBuilder nextRequestBuilder) {
super(pageContents, nextRequestBuilder);
}
}
  1. 获取邮件信息

核心逻辑代码

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
// 对应最开头的代码,获取邮件组内邮件信息,messages为官方封装类
MessageCollectionPage messages = micGraphService.getGraph();
for (Message message : messages.getCurrentPage()) {
String content = message.body.content;
//通过html标签获取邮件信息内容,实现见下方代码
List<String> messageText = messageHtml(content, email, approveList);

//todo 业务逻辑,拿到邮件内的语句进行业务操作,比如这个list中拿到了approve单词
...
}


private List<String> messageHtml(String html) {
List<String> content = Lists.newArrayList();
org.jsoup.nodes.Document doc = Jsoup.parse(html);
// 纯文本格式下会导致失效,获取不到
Elements inputRows = doc.select("div").select("#emailSubject");

//从文件内容文本中直接截取
String contentText = doc.select("body").text();
content.add(contentText.toLowerCase());

// 实例判断代码
if(content.contains('approve')) .... dosomething();
return content;
}

This is copyright.

...

...

00:00
00:00