背景 之前产品有审批流程达到一定阶段后给领导发邮件发短信,其中发邮件部分功能是通过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请求
类属性介绍:
authority:
这是一个 URL 字符串,表示用于 Azure Active Directory (AAD) 认证的授权端点。
它用于将认证请求定向到正确的 AAD 端点。
 
 
scope:
这个字符串定义了应用程序从 Microsoft Graph 请求的权限或范围。
 
 
tokenCredAuthProvider:
这是 TokenCredentialAuthProvider 的一个实例,用于对 Microsoft Graph 的请求进行认证。
它通常使用凭据(如客户端 ID、密钥和租户 ID)来获取访问令牌,然后使用该令牌授权 API 请求
 
 
tenantId:
这是一个配置属性,保存 Azure Active Directory 的租户 ID。
租户 ID 是应用程序注册所在的 Azure AD 实例的唯一标识符。
 
clientId:
这是一个配置属性,保存应用程序的客户端 ID。
客户端 ID 是应用程序在 Azure AD 中注册时分配的唯一标识符。
 
secret:
这是一个配置属性,保存应用程序的客户端密钥。
客户端密钥与客户端 ID 一起用于对应用程序进行 Azure AD 认证。
 
mailbox:
这是一个配置属性,指定应用程序将访问或扫描的邮箱的电子邮件地址。
它用于标识应用程序在调用 Microsoft Graph API 时应与哪个邮箱进行交互。
 
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目录下新建如下结构的文件,文件的类容为上面实现类的全类名,即包名+类名
  
文件夹名称
META-INF /services/com.azure.core.http.HttpClientProvider
3.实际使用 通过上面代码我们就可以拿到这个租的邮件组的邮箱内部邮件,由此我们便可以实现回复审批功能,比如用户回复了一个approve后程序流程往下继续走下去。
实例代码:
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 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 MessageCollectionPage messages = micGraphService.getGraph();     for  (Message message : messages.getCurrentPage()) {     String content = message.body.content;          List<String> messageText = messageHtml(content, email, approveList);               ... } 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.