网站搜索

从 Send-MailMessage 继续:使用 Graph API 从 PowerShell 发送电子邮件


弃用基本身份验证

正如之前所讨论的,微软大声疾呼需要停止使用基本身份验证并开始使用现代身份验证。这是一个大问题,因为黑客不断利用密码喷射攻击等技术危害 Office 365 帐户和租户。转向现代身份验证并通过多重身份验证保护尽可能多的帐户(尤其是 Azure AD 安全默认值规定的管理员帐户)是正确的步骤。

更新(2021 年 9 月 25 日):Microsoft 停用电子邮件连接协议基本身份验证的最新计划不包括 SMTP AUTH。目前,Microsoft 的重点是删除 IMAP4 和 POP3 等协议的基本身份验证,但他们将回来删除 SMTP AUTH 的基本身份验证。因此,明智的做法是考虑到那时如何在生产中替换此协议。

更新(2022 年 11 月 15 日):可以通过三种方式使用图表发送电子邮件。使用此处描述的 API 就是其中之一。阅读本文了解完整详细信息。

发送邮件消息难题

很大程度上由于历史原因,Exchange Online 支持多种连接协议。 Microsoft 在说服客户禁用他们从未使用的协议的基本身份验证方面取得了一些进展,并升级了 POP3 和 IMAP4 等旧协议,以使用 OAuth 2.0 进行现代身份验证。正如本博客中所讨论的,租户需要找到调用 Send-MailMessage cmdlet 的 PowerShell 脚本,并最终使用更现代的方法升级代码来发送电子邮件。

Send-MailMessage cmdlet 依赖于 SMTP AUTH 协议来使用基本身份验证发送电子邮件。 Microsoft 于 2020 年 4 月宣布 OAuth 2.0 支持 SMTP AUTH,但这并不意味着可以使用现成的替代 cmdlet。微软表示,该公告“是为了交互式应用程序为 IMAP 和 SMTP [AUTH] 启用 OAuth”。实际上,这意味着发送、阅读或以其他方式处理电子邮件的邮件客户端或其他应用程序。快速浏览一下所引用的页面,毫无疑问,这不仅仅意味着替换 PowerShell 脚本中的几行代码。

如果 Send-MailMessage 没有明显的替代品,如果您想要消除基本身份验证,但有一堆使用 cmdlet 发送电子邮件的脚本,您应该怎么做?这是一个难题。

输入图表

Microsoft 关于 SMTP AUTH 的说明向人们指出 Microsoft Graph API 作为发送电子邮件的替代方法。 SendMail 调用是 Graph Outlook API 的一部分。与所有 Graph API 调用一样,它可以通过 PowerShell 调用。

SendMail 可以通过两种方式使用,具体取决于调用它的应用程序所拥有的权限:

  • 如果应用具有Mail.Send应用程序权限,则应用可以以组织中的任何用户或共享邮箱(但不是组邮箱)的身份发送电子邮件。管理员必须同意应用程序使用应用程序权限。
  • 如果应用具有 Mail.Send 委派权限,则应用可以以登录用户的身份发送邮件。

因此,使用应用程序权限发送电子邮件非常强大(或危险)。该图表会忽略邮箱现有的 Exchange 代理发送或代表发送权限,并且能够模拟任何用户或共享邮箱来发送电子邮件,就像邮件源自该邮箱一样。

从 Send-MailMessage 转换 PowerShell 脚本

为了测试将脚本从“经典:Send-MailMessage”代码转换为使用 SendMail 调用有多容易,我编写了一个示例脚本来说明如何向新用户发送欢迎消息。转换的基本步骤是:

  • 在 Azure AD 中创建已注册的应用程序。
  • 将 Mail.Send Graph(应用程序或委派)权限分配给应用程序。
  • 记下应用程序标识符和租户标识符的 GUID,并生成应用程序密钥(如果使用应用程序权限)。
  • 在脚本中,添加代码以生成访问令牌,并将调用 Send-MailMessage 的代码替换为 SendMail 的等效代码。

首先,我们来看看使用应用程序权限。创建访问令牌的代码如下所示。在您的组织中,为应用程序 ID ($AppId) 和租户标识符 ($TenantId) 传递的值将有所不同。

$AppId = "980e01d1-ce75-46ba-a054-4b61c787f682"
$AppSecret = "~_Q2.IPI3kpbmhOG.VtCb1r0_J9G4-D8jG"
$TenantId = "b562313f-14fc-43a2-9a7a-d2e27f4f3478"

# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $AppId
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $AppSecret
    grant_type    = "client_credentials"
}

$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
# Unpack Access Token
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$Headers = @{
            'Content-Type'  = "application\json"
            'Authorization' = "Bearer $Token" }

获得有效的访问令牌后,脚本现在可以使用令牌中编码的权限进行图形 API 调用。该脚本查找最近创建的用户帐户集,并循环遍历每个用户以撰写自定义的欢迎消息。使用Send-MailMessage 需要完成大部分工作,因此更改的部分是用代码替换参数的创建来创建和发送消息,以使用SendMail 完成这项工作。

该流程中发生以下步骤:

  • 使用发件人的有效电子邮件地址填充 $MsgFrom 变量(发送消息的人)。由于 SendMail API(具有应用程序权限)可以作为任何有效(许可)邮箱发送,因此您可以对任何用户或共享邮箱使用 SMTP 代理地址,包括辅助代理地址。重要的是所使用的地址对于支持的邮箱类型是可解析的。 SendMail 在尝试发送邮件时验证发件人,如果在租户目录中找不到发件人地址,则返回 404 错误。
  • 循环访问最近创建的邮箱集以创建邮件的自定义 HMTL 正文。然后填充定义邮件正文(收件人和抄送收件人以及邮件主题)、附件以及用于将邮件提交到 Exchange Online 的命令的 JSON 格式参数结构。确保使用收件人的主 SMTP 地址,其中可以包括通讯组、邮件联系人、来宾帐户和 Microsoft 365 组。
  • 调用Invoke-RestMethod cmdlet 来处理参数并发送消息。
$MsgFrom = “HRWelcomeMailbox@office365itpros.com”
$ccRecipient1 = “Kim.Akers@office365itpros.com”
$ccRecipient2 = “James.Ryan@Office365itpros.com”
# Define attachment to send to new users
$AttachmentFile = "C:\temp\WelcomeToOffice365ITPros.docx"
$ContentBase64 = [convert]::ToBase64String( [system.io.file]::readallbytes($AttachmentFile))
ForEach ($User in $Users) {
      $EmailRecipient = $User.PrimarySmtpAddress
      Write-Host "Sending welcome email to" $User.DisplayName
      $MsgSubject = "A Hundred Thousand Welcomes to " + $User.DisplayName
      $htmlHeaderUser = "<h2>New User " + $User.DisplayName + "</h2>"
      $htmlline1 = "<p><b>Welcome to Office 365</b></p>"
      $htmlline2 = "<p>You can open Office Online by clicking <a href=https://www.office.com/?auth=2>here</a> </p>"
      $htmlline3 = "<p>Have a great time and be sure to call the help desk if you need assistance.</p>"
      $htmlbody = $htmlheaderUser + $htmlline1 + $htmlline2 + $htmlline3 + "<p>"
      $HtmlMsg = "</body></html>" + $HtmlHead + $HtmlBody
# Create message body and properties and send
        $MessageParams = @{
          "URI"         = "https://graph.microsoft.com/v1.0/users/$MsgFrom/sendMail"
          "Headers"     = $Headers
          "Method"      = "POST"
          "ContentType" = 'application/json'
          "Body" = (@{
                "message" = @{
                "subject" = $MsgSubject
                "body"    = @{
                    "contentType" = 'HTML' 
                     "content"     = $htmlMsg }
         "attachments" = @(
             @{
              "@odata.type" = "#microsoft.graph.fileAttachment"
              "name" = $AttachmentFile
              "contentType" = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
              "contentBytes" = $ContentBase64 })  
           "toRecipients" = @(
           @{
             "emailAddress" = @{"address" = $EmailRecipient }
           } ) 
          "ccRecipients" = @(
           @{
             "emailAddress" = @{"address" = $ccRecipient1 }
           } ,
            @{
             "emailAddress" = @{"address" = $ccRecipient2 }
           } )       
         }
      }) | ConvertTo-JSON -Depth 6
   }   # Send the message
   Invoke-RestMethod @Messageparams
}

您可以从 GitHub 下载完整的脚本。您将注意到,向邮件添加附件并不像使用 Send-MailMessage 那样简单。需要注意的两点是:

  • 发送邮件时上传附件最多可处理 3 MB。如果您需要发送最多 150 MB 的较大附件,则需要使用不同的方法上传它们(请参阅 Microsoft 的说明)。当然,邮箱和组织限制等其他因素也会影响邮件的最大大小。
  • 内容类型是附件的 MIME 类型(请记住,我们使用的是电子邮件,这就是附件内容必须采用 Base64 编码的原因)。我发现此处提供的 MIME 类型列表很有帮助。我没有测试所有可能的附件类型,但我知道当我遇到问题时,有时“multipart/mixed”内容类型可以工作,而官方 MIME 类型则不能。

Mail.Send API 可以在邮件中设置 x 标头,这是 Send-MailMessage cmdlet 无法做到的事情。这就是人们使用(现已弃用).Net SmtpClient 类在脚本中创建和发送电子邮件的原因之一。您应该将这些脚本转换为使用图表。

那个大的权限问题

如果您不担心拥有一个可以为组织中的任何邮箱发送电子邮件的应用程序,则可以轻松使用同一应用程序作为升级所有脚本的基础。用于保护访问令牌的代码保持不变,可以粘贴到每个脚本中。剩下的任务是更新 Send-MailMessage 代码以使用 SendMail。

拥有一个全能的邮件发送应用程序将使程序员的生活更轻松(或者至少使转换更容易),但企业安全团队可能不会认为可接受,除非保证该应用程序不会被滥用。既然这样的保证是不可能的,你就知道问题出在哪里了。

使用应用程序访问策略来限制应用程序使用的邮箱

一种解决方案是使用应用程序访问策略来允许应用程序仅访问某些邮箱。应用程序访问策略结合了三个要素:

  • 定义拒绝或允许访问。
  • 涵盖在 Azure AD 中注册的一个或多个应用程序。
  • 适用于对启用邮件的安全组成员或个人用户(具有安全主体的对象)的邮箱进行的 Exchange Web 服务或图形 API 调用。

因此,我们可以创建应用程序访问策略来限制对一组邮箱的访问。例如:

New-ApplicationAccessPolicy -AppId 970e01d1-ce75-46ba-a054-4b61c787f682 -PolicyScopeGroupId SendMailApp.Control@office365itpros.com -AccessRight RestrictAccess -Description "Restrict access to app allowed to send email using the Graph SendMail API"

此策略将对标识符为 970e01d1-ce75-46ba-a054-4b61c787f682 的应用程序的访问限制为安全组 SendMailApp.Control@office365itpros.com 中定义的邮箱。创建策略后,只有安全组成员的邮箱可用于通过 SendMail API 调用发送电子邮件。尝试使用任何其他帐户发送电子邮件会在调用 SendMail 时生成 403 错误,因为策略拒绝访问邮箱。

Exchange Online 缓存访问策略的组成员身份,以使检查性能更好,您可能需要等待一个小时左右才能使成员资格更改生效,但策略限制效果很好。

使用委派权限

另一种选择是使用具有委派 Mail.Send 权限的 Azure AD 注册应用程序。这限制了向登录帐户发送电子邮件的能力,因此它更符合 Send-MailMessage 的工作方式。创建和发送消息的代码保持不变;唯一发生变化的是,应用程序进行身份验证时 Azure AD 生成的访问令牌将其使用限制为登录用户。在大多数情况下,使用委派权限来调用 SendMail API 是更好的选择。当然,除非您确实希望能够以租户中的每个人的身份发送消息。

相关文章