通过 O365 发送电子邮件

G_H*_*hat 3 vb.net email office365

我们公司最近通过 Office 365 订阅从本地邮件服务器 (CommunigatePro) 迁移到托管邮件。从那时起,我在通过 VB.NET 应用程序向组织外部的人员发送电子邮件时遇到了问题。我找到了解决方法,但我想知道我是否只是在配置中遗漏了一些可能导致问题的内容。

我发送消息的代码非常标准(显然在发布时有点混乱),但我必须包含一个单独的函数,用于在失败时通过 CDO 发送消息:

Private Function SendFTPSuccessMail() As Boolean
    Dim Email As New System.Net.Mail.MailMessage
    Dim MailServer As New System.Net.Mail.SmtpClient("domain-com.mail.protection.outlook.com")
    Dim SenderAddress As New System.Net.Mail.MailAddress("helpdesk@domain.com", "IT HelpDesk")
    Dim BodyText As String = String.Empty

    With Email
        .From = SenderAddress
        .Bcc.Add(SenderAddress)

        BodyText = "SOME TEXT FOR THE E-MAIL MESSAGE"
        .To.Add(New System.Net.Mail.MailAddress("recipient@recipient.com", "Recipient"))
        .Subject = "MESSAGE SUBJECT"
        .Body = BodyText
    End With

    Try
        MailServer.Send(Email)
        Return True
    Catch ex As Exception
        Dim CDOError As String = SendCDOEmail(Email, ex)

        If Not String.IsNullOrEmpty(CDOError) Then
            Dim ExceptionMessage As String = ".NET SEND ERROR: " & ex.Message & vbCrLf

            If Not ex.InnerException Is Nothing Then
                ExceptionMessage += ex.InnerException.Message & vbCrLf
            End If

            ExceptionMessage += vbCrLf & CDOError

            MessageBox.Show(ExceptionMessage, "NOTIFICATION E-MAIL FAILED", MessageBoxButtons.OK, MessageBoxIcon.Error)
            Return False
        Else
            Return True
        End If
    Finally
        If Not Email Is Nothing Then
            Email.Dispose()
        End If

        MailServer = Nothing
    End Try
End Function
Run Code Online (Sandbox Code Playgroud)

MailServer.Send如果所有收件人都在同一(我的)域内,则该方法可以正常工作,但对于具有不同域的任何收件人地址将抛出SmtpFailedRecipientException或。SmtpFailedRecipientsException函数SendCDOEmail如下:

Option Strict Off    

Public Function SendCDOEmail(ByRef OriginalMessage As System.Net.Mail.MailMessage, ByVal OriginalException As Exception) As String
    Dim ErrorMessage As String = String.Empty

    If TypeOf (OriginalException) Is System.Net.Mail.SmtpFailedRecipientsException Then
        Dim SMTPEX As System.Net.Mail.SmtpFailedRecipientsException = CType(OriginalException, System.Net.Mail.SmtpFailedRecipientsException)
        Dim FailedAddresses As New List(Of String)
        Dim NewTo As New List(Of System.Net.Mail.MailAddress)
        Dim NewCC As New List(Of System.Net.Mail.MailAddress)
        Dim NewBCC As New List(Of System.Net.Mail.MailAddress)

        For Each InnerEx As System.Net.Mail.SmtpFailedRecipientException In SMTPEX.InnerExceptions
            FailedAddresses.Add(InnerEx.FailedRecipient.ToString.Replace("<", "").Replace(">", ""))
        Next

        For Each Recipient As System.Net.Mail.MailAddress In OriginalMessage.To
            For Each KeepAddress As String In FailedAddresses
                If Recipient.Address = KeepAddress Then
                    NewTo.Add(Recipient)
                    Exit For
                End If
            Next
        Next

        OriginalMessage.To.Clear()

        For Each Recipient As System.Net.Mail.MailAddress In NewTo
            OriginalMessage.To.Add(Recipient)
        Next

        For Each Recipient As System.Net.Mail.MailAddress In OriginalMessage.CC
            For Each KeepAddress As String In FailedAddresses
                If Recipient.Address = KeepAddress Then
                    NewCC.Add(Recipient)
                    Exit For
                End If
            Next
        Next

        OriginalMessage.CC.Clear()

        For Each Recipient As System.Net.Mail.MailAddress In NewCC
            OriginalMessage.CC.Add(Recipient)
        Next

        For Each Recipient As System.Net.Mail.MailAddress In OriginalMessage.Bcc
            For Each KeepAddress As String In FailedAddresses
                If Recipient.Address = KeepAddress Then
                    NewBCC.Add(Recipient)
                    Exit For
                End If
            Next
        Next

        OriginalMessage.Bcc.Clear()

        For Each Recipient As System.Net.Mail.MailAddress In NewBCC
            OriginalMessage.Bcc.Add(Recipient)
        Next
    ElseIf TypeOf (OriginalException) Is System.Net.Mail.SmtpFailedRecipientException Then
        Dim SMTPEX As SmtpFailedRecipientException = CType(OriginalException, SmtpFailedRecipientException)
        Dim NewTo As New List(Of System.Net.Mail.MailAddress)
        Dim NewCC As New List(Of System.Net.Mail.MailAddress)
        Dim NewBCC As New List(Of System.Net.Mail.MailAddress)
        Dim FailedAddress As String = SMTPEX.FailedRecipient.ToString.Replace("<", "").Replace(">", "")

        For Each Recipient As System.Net.Mail.MailAddress In OriginalMessage.To
            If Recipient.Address = FailedAddress Then
                NewTo.Add(Recipient)
            End If
        Next

        OriginalMessage.To.Clear()

        For Each Recipient As System.Net.Mail.MailAddress In NewTo
            OriginalMessage.To.Add(Recipient)
        Next

        For Each Recipient As System.Net.Mail.MailAddress In OriginalMessage.CC
            If Recipient.Address = FailedAddress Then
                NewCC.Add(Recipient)
            End If
        Next

        OriginalMessage.CC.Clear()

        For Each Recipient As System.Net.Mail.MailAddress In NewCC
            OriginalMessage.CC.Add(Recipient)
        Next

        For Each Recipient As System.Net.Mail.MailAddress In OriginalMessage.Bcc
            If Recipient.Address = FailedAddress Then
                NewBCC.Add(Recipient)
            End If
        Next

        OriginalMessage.Bcc.Clear()

        For Each Recipient As System.Net.Mail.MailAddress In NewBCC
            OriginalMessage.Bcc.Add(Recipient)
        Next
    End If

    Dim CDOMessage As Object = CreateObject("CDO.Message")

    With CDOMessage.Configuration.Fields
        .Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
        .Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = "smtp.office365.com"
        .Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25

        .Item("http://schemas.microsoft.com/cdo/configuration/smtpusessl") = 1
        .Item("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate") = 1
        .Item("http://schemas.microsoft.com/cdo/configuration/sendusername") = "myemailaddress@domain.com"
        .Item("http://schemas.microsoft.com/cdo/configuration/sendpassword") = "mypassword"

        .Update()
    End With

    With OriginalMessage
        CDOMessage.Subject = .Subject
        CDOMessage.From = .From.DisplayName & " <" & .From.Address & ">"

        For Each ToAddress As System.Net.Mail.MailAddress In .To
            CDOMessage.To = CDOMessage.To & ToAddress.DisplayName & " <" & ToAddress.Address & ">; "
        Next

        For Each ToAddress As System.Net.Mail.MailAddress In .CC
            CDOMessage.CC = CDOMessage.CC & ToAddress.DisplayName & " <" & ToAddress.Address & ">; "
        Next

        For Each ToAddress As System.Net.Mail.MailAddress In .Bcc
            CDOMessage.BCC = CDOMessage.BCC & ToAddress.DisplayName & " <" & ToAddress.Address & ">; "
        Next

        Dim FileNames = .Attachments.[Select](Function(a) a.ContentStream).OfType(Of System.IO.FileStream)().[Select](Function(fs) fs.Name)

        For Each File As String In FileNames
            CDOMessage.AddAttachment(File)
        Next

        CDOMessage.TextBody = .Body

        Try
            CDOMessage.Send()
        Catch ex As Exception
            ErrorMessage = "CDO SEND ERROR: " & ex.Message
        Finally
            CDOMessage = Nothing
        End Try
    End With

    Return ErrorMessage
End Function
Run Code Online (Sandbox Code Playgroud)

这似乎每次都有效,但我不禁想知道是否有一种方法可以完全避免使用 CDO 发送方法。我已经尝试使用该对象的地址domain-com.mail.protection.outlook.com和服务器地址,以及创建一个新对象并输入与 CDO 发送方法相同的用户名/密码。关于如何让对象工作而不必求助于老式 CDO 有什么想法吗?smtp.office365.comSmtpClientCredentialsSystem.Net.Mail

编辑:为了澄清和完整性,我对该功能进行了另一次测试,尝试向该域之外的我的 Google 托管电子邮件帐户之一发送消息。这次,我再次在主邮件功能的 Try/Catch 块中手动设置对象Credentials的属性SmtpClient,如下所示:

Try
    MailServer.Credentials = New System.Net.NetworkCredential("myemailaddress@domain.com", "mypassword")
    MailServer.Send(Email)
    Return True
Catch ex As Exception
    Dim CDOError As String = SendCDOEmail(Email, ex)
    [...]
Finally
    If Not Email Is Nothing Then
        Email.Dispose()
    End If

    MailServer = Nothing
End Try
Run Code Online (Sandbox Code Playgroud)

为了尽可能减少变量的数量,我直接从 CDO 发送方法复制并粘贴用户名/密码信息。这些凭据适用于我的电子邮件帐户,该帐户是我们公司的 O365 管理员帐户。不幸的是,测试仍然失败,结果完全相同SmtpFailedRecipientException。给出的服务器响应是:

“邮箱不可用。服务器响应为:5.7.64 TenantAttribution;中继访问被拒绝 [SN1NAM01FT060.eop-nam01.prod.protection.outlook.com]”

就个人而言,我想通过设置某种“通用”登录或实现某种仅允许从我的网络“匿名”发送的方法来摆脱对邮件服务器使用我的凭据,但这超出了范围这个问题。

无论如何,我只是不确定我错过了什么。也许这是我的 Exchange 配置中的一个设置,或者是我忽略的其他一些“怪癖”。感谢您的帮助。

另一件需要注意的事情是:我使用许多不同的配置进行了一些额外的测试。 如果我将SmtpClient主机更改为smtp.office365.com 并将端口设置为587(或25),并将属性设置EnableSslTrue 显式提供用户凭据(这是我试图摆脱的主要事情),我就能够收到一条消息没有它击中Catch块并使用该SendCDOEmail方法(如上所述,该方法使用显式定义的凭据)。

Dim MailServer As New System.Net.Mail.SmtpClient("smtp.office365.com")
[...]
With MailServer
    .Credentials = New System.Net.NetworkCredential("myemailaddress@domain.com", "mypassword")
    .Port = 587 'or 25
    .EnableSsl = True
    .Send(Email)
End With
Run Code Online (Sandbox Code Playgroud)

虽然此方法在技术上可行,但我确实试图避免在应用程序中显式分配凭据,特别是因为我在 Exchange 服务器上没有可以使用的“通用”用户帐户。domain-com.mail.protection.outlook.com这就是使用作为主机名的目的SmtpClient。问题是我仍然必须提供 CDO 发送方法的凭据(以及smtp.office365.com主机名),因此没有任何问题真正得到解决。

G_H*_*hat 5

正如我所怀疑的,该问题显然是由 Exchange 配置引起的。我最终配置了一个“连接器”来使用 Office 365 SMTP 中继发送邮件,如 Microsoft Office 支持网站上所述。设置中继连接器并输入 SPF 记录后,我通过发送到我的托管 Gmail 帐户再次进行测试,而无需提供任何凭据或设置任何其他设置SmtpClient。我第一次尝试时,配置显然没有完全生效,但我等了几分钟再试一次。这一次,一切顺利,没有遇到任何Catch障碍,我的邮件正确显示在我的 Gmail 收件箱中。

\n

然而,我显然遇到了一些问题,即使我更新了 SPF 记录,我的多功能设备中的一些消息现在仍被垃圾邮件过滤器捕获。我希望这只是因为 DNS 尚未完全传播,但是,由于我在 O365 管理控制面板中进行了更改,所以我还不能 100% 确定这一点。我要等几个小时看看会发生什么。

\n

以下是我用来让它为我工作的步骤的概述(直接从他们的页面获取,并进行了一些小的澄清、语法和格式编辑):

\n
    \n
  1. 获取设备或应用程序将从中发送邮件的公共(静态)IP 地址。不支持或允许动态 IP 地址\xe2\x80\x99。您可以与其他设备和用户共享您的静态 IP 地址,但请勿与公司外部的任何人共享该 IP 地址。记下此 IP 地址,以便在下面的步骤 7b 和 8中使用。

    \n
  2. \n
  3. 登录 Office 365

    \n
  4. \n
  5. 选择。确保contoso.com选择您的域(例如 )。单击“管理 DNS”并找到 MX 记录。MX 记录将具有POINTS TO ADDRESS值,类似于cohowineinc-com.mail.protection.outlook.com以下屏幕截图中所示。记下 MX 记录POINTS TO ADDRESS值。您将在下面的步骤 9中需要它。
    \n记下 MX 记录指向地址值。

    \n
  6. \n
  7. 检查应用程序或设备将发送到的域是否已经过验证。如果未验证域,电子邮件可能会丢失,并且您将\xe2\x80\x99 无法使用 Exchange Online 邮件跟踪工具来跟踪它们。

    \n
  8. \n
  9. 在 Office 365 中,单击“管理”,然后单击“Exchange”以转到 Exchange 管理中心 (EAC)。

    \n
  10. \n
  11. 在 Exchange 管理中心中,单击邮件流,然后单击连接器

    \n
  12. \n
  13. 检查为您的组织设置的连接器列表。如果没有列出从组织的电子邮件服务器到 Office 365 的连接器,请创建一个。

    \n

    A。要启动向导,请单击加号 ( + )。在第一个屏幕上,选择以下屏幕截图中所示的选项:
    \n选择“从您组织的电子邮件服务器”到“Office 365”
    \n单击“下一步”并为连接器命名。

    \n

    b. 在下一个屏幕上,选择标有“通过验证发送服务器的 IP 地址是否与属于您组织的这些 IP 地址之一匹配”的选项,然后添加步骤 1中的 IP 地址

    \n

    C。将所有其他字段保留为默认值,然后单击“保存”

    \n
  14. \n
  15. 现在您已完成 Office 365 设置的配置,请转到您的域名注册商\xe2\x80\x99s 网站以更新您的 DNS 记录。编辑您的SPF 记录如果您还没有 SPF 记录,则需要创建一个。包括您在上面步骤 1中记下的 IP 地址。完成的字符串应类似于以下内容:

    \n \n步骤 1中的公共 IP 地址在哪里。跳过此步骤可能会导致电子邮件发送到收件人\xe2\x80\x99 垃圾邮件文件夹。 v=spf1 ip4:###.###.###.### include:spf.protection.outlook.com ~all

    ###.###.###.###

    \n
  16. \n
  17. 最后,在您尝试发送邮件的设备或应用程序中,找到邮件服务器的设置,并输入您在步骤 3中记录的 MX 记录中的POINTS TO ADDRESS值。由于每个设备和应用程序都不同,因此此设置可能会被标记为、、、或完全其他名称。您可能需要参阅设备或应用程序的文档以找到适当的设置。Mail ServerSMTP ServerSmart HostServer

    \n
  18. \n
  19. 要测试配置,请从您的设备或应用程序发送测试电子邮件,并确认收件人已收到该电子邮件。

    \n
  20. \n
\n

编辑:看起来邮件被垃圾邮件过滤器捕获的问题由于 DNS 未完全传播所致。等待了 24 小时多一点后,我还没有看到任何来自内部系统的邮件被隔离。

\n

顺便说一句,为了充分披露,我们在内部 DNS 服务器上有一个 CNAME(别名)记录mail.domain,该记录domain-com.mail.protection.outlook.com使我的编程变得更加容易。此 CNAME 记录不会传播到公共 DNS 服务器,仅由我们的内部网络使用(因此排除了该.com部分)。我已经编写了很多代码来使用mail.domainSMTP 服务器的地址,因此设置 DNS 来处理快速转换并将其重定向到 Microsoft 服务器更加容易。在上述问题的示例代码中,我将地址替换为 Microsoft 服务器地址,以免进一步混淆问题,但我的代码实际上如下所示:

\n
Private Function SendFTPSuccessMail() As Boolean\n    Dim Email As New System.Net.Mail.MailMessage\n    Dim MailServer As New System.Net.Mail.SmtpClient("mail.domain")\n    Dim SenderAddress As New System.Net.Mail.MailAddress("helpdesk@domain.com", "IT HelpDesk")\n    Dim BodyText As String = String.Empty\n\n    With Email\n        .From = SenderAddress\n        .Bcc.Add(SenderAddress)\n\n        BodyText = "SOME TEXT FOR THE E-MAIL MESSAGE"\n        .To.Add(New System.Net.Mail.MailAddress("recipient@recipient.com", "Recipient"))\n        .Subject = "MESSAGE SUBJECT"\n        .Body = BodyText\n    End With\n\n    Try\n        MailServer.Send(Email)\n        Return True\n    Catch ex As Exception\n        Dim CDOError As String = SendCDOEmail(Email, ex)\n\n        If Not String.IsNullOrEmpty(CDOError) Then\n            Dim ExceptionMessage As String = ".NET SEND ERROR: " & ex.Message & vbCrLf\n\n            If Not ex.InnerException Is Nothing Then\n                ExceptionMessage += ex.InnerException.Message & vbCrLf\n            End If\n\n            ExceptionMessage += vbCrLf & CDOError\n\n            MessageBox.Show(ExceptionMessage, "NOTIFICATION E-MAIL FAILED", MessageBoxButtons.OK, MessageBoxIcon.Error)\n            Return False\n        Else\n            Return True\n        End If\n    Finally\n        If Not Email Is Nothing Then\n            Email.Dispose()\n        End If\n\n        MailServer = Nothing\n    End Try\nEnd Function\n
Run Code Online (Sandbox Code Playgroud)\n

看起来我现在应该能够摆脱 CDO 发送方法,但我暂时将其保留为“故障保护”。一旦我成功运行了一段时间,我可能会完全删除它,这样我最终就可以实现摆脱显式定义的用户凭据的目标。

\n