之前系统里发送邮件使用的是SMTP+账号密码的方式,但是近期公司通知这种方式不安全,需要使用OAuth2的方式进行替换,研究了一下,记录下操作步骤。当前使用了两种方式,但是第一种方式暂时无法添加邮件中的附件,PS:两种方式均需登录AAD注册APP,然后使用生成的APPID和tenantID以及生成的密码
开始之前,先看一下下文中Delegated权限和Application权限的区别:
使用 OAuth 对 EWS 应用程序进行身份验证 | Microsoft Learn
一、注册AppID等信息
1、申请AppID,tenantID操作步骤
管理员在AAD(Azure Active Directory)进行App 注册。注册完毕之后会生产Application (client) ID(APPID)和一个Directory (tenant) ID (租户ID)
2、申请secrets
按照如下图步骤,
注意:
1、在选择过期日期时,默认为6个月,可手动修改为24个月
2、生产的Value值只会在首次添加完成之后出现,所以添加完成之后,将步骤2中Value值保存下来
3、添加授权权限
点击左侧的API Permission 链接,添加如下的权限
注意:如果你是管理员,那么你可以添加Application类型的权限,否则你只能添加Delegation类型的权限
4、Config文件配置第一步骤中生成的三个ID
<!-- The application ID from your app registration -->
<add key="appId" value="{{YOUR APP ID}}" />
<!-- The tenant ID copied from your app registration -->
<add key="tenantId" value="{{YOUR TENANTID}}"/>
<!-- The application's client secret from your app registration. Needed for application permission access -->
<add key="clientSecret" value="{{YOUR SECRET ID}}"/>
二、代码执行逻辑
1、使用OAuth2方式代码:
public async Task Test()
{
string clientId = "{{YOUR APP ID}}";
string clientSecret = "{{YOUR SECRET ID}}";
string tenantId = "{{YOUR TENANTID}}";
string accessToken = await GetAccessToken(clientId, clientSecret, tenantId);
string emailEndpoint = "https://graph.microsoft.com/v1.0/users/{{ YOUR USER EMAIL }}/sendMail";
string emailContent = "{{ Email Content }}";
string recipientEmail = "{{ RecipientEmail }}";
string senderEmail = "{{ Sender Email }}";
string subject = "{{ Email Subject }}";
await SendEmail(accessToken, emailEndpoint, emailContent, recipientEmail, senderEmail, subject);
}
private async Task<string> GetAccessToken(string clientId, string clientSecret, string tenantId)
{
string tokenEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
string scope = "https://graph.microsoft.com/.default";
using (HttpClient client = new HttpClient())
{
var requestContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("scope", scope)
});
var response = await client.PostAsync(tokenEndpoint, requestContent);
var responseContent = await response.Content.ReadAsStringAsync();
TokenHelper tokenHelper = new TokenHelper();
if (responseContent == null)
{
return "";
}
else
{
tokenHelper = JsonConvert.DeserializeObject<TokenHelper>(responseContent ?? responseContent);
}
return tokenHelper.access_token;
}
}
private async Task SendEmail(string accessToken, string emailEndpoint, string emailContent, string recipientEmail, string senderEmail, string subject)
{
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
List<Microsoft.Graph.Models.Attachment> attachments = new List<Microsoft.Graph.Models.Attachment>();
FileAttachment fileAttachment = new FileAttachment();
fileAttachment.Name = "Test Attachment1";
fileAttachment.OdataType = "#Microsoft.graph.fileAttachment";
fileAttachment.ContentType = "text/plain";
attachments.Add(fileAttachment);
var email = new
{
message = new
{
subject = subject,
body = new { contentType = "Text", content = emailContent },
toRecipients = new[] { new { emailAddress = new { address = recipientEmail } } },
CcRecipients = new[] { new { emailAddress = new { address = recipientEmail } } },
BccRecipients = new[] { new { emailAddress = new { address = recipientEmail } } },
from = new { emailAddress = new { address = senderEmail } },
Attachments = attachments
}
};
var requestContent = new StringContent(JsonConvert.SerializeObject(email), Encoding.UTF8, "application/json");
var response = await client.PostAsync(emailEndpoint, requestContent);
var responseContent = await response.Content.ReadAsStringAsync();
}
}
使用这一种方式添加邮件中附件时,需要附件格式为FileAttachment,但是进行发送请求验证时,一直提示“{"error":{"code":"RequestBodyRead","message":"The property 'ContentBytes' does not exist on type 'microsoft.graph.attachment'. Make sure to only use property names that are defined by the type or mark the type as open type."}}”,但是“ContentBytes”属性又是FileAttachment required属性,因此就陷入了死循环中。有大佬知道咋回事儿的,可以告诉小弟一声。
2、使用Oauth2方式测试
2、使用Graph方式:
public async Task Test2()
{
string clientId = "{{YOUR APP ID}}"; // GET FROM AAD
string clientSecret = "{{YOUR SECRET ID}}"; // GET FROM AAD
string tenantId = "{{YOUR TENANTID}}"; // GET FROM AAD
var credential = new ClientSecretCredential(tenantId, AppId, clientSecret);
var graphClient = new GraphServiceClient(credential);
//模拟发送邮件中附件地址
var attachments = new List<string>
{
@"C:\Files\新建 Microsoft Word 文档.docx",
@"C:\Files\新建 文本文档.txt",
@"C:\Files\123123.xlsx"
};
List<Attachment> Graphattachments= new List<Attachment>();
foreach (var filePath in attachments)
{
string fileName = Path.GetFileName(filePath);
byte[] fileBytes = File.ReadAllBytes(filePath);
Graphattachments.Add(new FileAttachment
{
Name = fileName,
ContentBytes = fileBytes,
ContentType = GetMimeType(fileName),
OdataType = "#microsoft.graph.fileAttachment"
});
}
var requestBody = new SendMailPostRequestBody
{
Message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "{{ Email Content }}",
},
ToRecipients = new List<Recipient>
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "{{ Recipients Email }}",
},
},
},
CcRecipients = new List<Recipient>
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "{{ CC Recipients }}",
},
},
},
BccRecipients = new List<Recipient>
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "{{ BCC Recipients }}",
},
},
},
Attachments = Graphattachments
},
SaveToSentItems = false,
};
await graphClient.Users["{{ YOUR Sender Email URL }}"].SendMail.PostAsync(requestBody);
}
/// <summary>
/// 验证文件类型
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
private string GetMimeType(string filePath)
{
string ext = Path.GetExtension(filePath).ToLower();
return ext switch
{
".txt" => "text/plain",
".pdf" => "application/pdf",
".jpg" => "image/jpeg",
_ => "application/octet-stream"
};
}
2、使用Graph方式测试: