【问题标题】:Sending E-mail Through O365通过 O365 发送电子邮件
【发布时间】:2017-09-01 19:41:54
【问题描述】:

我们公司最近通过我们的 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

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

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

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

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

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

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

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

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

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

另一件需要注意的事情:我用许多不同的配置做了一些额外的测试。 IF 我将SmtpClient 主机更改为smtp.office365.com AND 将端口设置为587(或25),AND 设置EnableSsl 属性到 True AND 明确提供了用户凭据(这是我试图摆脱的主要事情),我能够在没有击中的情况下获得一条消息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

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

【问题讨论】:

    标签: vb.net email office365


    【解决方案1】:

    正如我所怀疑的,这个问题显然是由 Exchange 配置引起的。我最终配置了一个“连接器”以使用 Office 365 SMTP 中继发送邮件,如 Microsoft 的 Office Support site 所述。设置中继连接器并放入 SPF 记录后,我通过发送到我托管的 Gmail 帐户再次进行了测试,没有提供任何凭据或为 SmtpClient 对象设置任何其他设置。我第一次尝试时,配置显然没有完全生效,但我等了几分钟又试了一次。这一次,一切都顺利完成了,没有碰到我的Catch 块,我的邮件正确显示在我的 Gmail 收件箱中。

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

    这里是我用来让它为我工作的步骤的概要(直接取自他们的页面,对澄清、语法和格式进行了一些小的编辑):

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

    2. Sign in to Office 365.

    3. 选择。确保选择了您的域(例如,contoso.com)。点击管理 DNS,找到 MX 记录。 MX 记录的 POINTS TO ADDRESS 值类似于cohowineinc-com.mail.protection.outlook.com,如下面的屏幕截图所示。记下 MX 记录 POINTS TO ADDRESS 值。您将在下面的第 9 步中需要它。

    4. 检查应用程序或设备将发送到的域是否已经过验证。 如果未验证域,电子邮件可能会丢失,并且您将无法使用 Exchange Online 邮件跟踪工具对其进行跟踪。

    5. 在 Office 365 中,单击 Admin,然后单击 Exchange 以转到 Exchange 管理中心 (EAC)。

    6. 在 Exchange 管理中心,点击邮件流,然后点击connectors

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

      一个。要启动向导,请单击加号 (+)。在第一个屏幕上,选择以下屏幕截图中描述的选项:

      单击下一步并为连接器命名。

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

      c。保留所有其他字段的默认值,然后单击保存

    8. 现在您已完成配置 Office 365 设置,请转到您的域注册商的网站以更新您的 DNS 记录。编辑您的SPF 记录如果您还没有 SPF 记录,则需要创建一个。包括您在上述步骤 1 中记下的 IP 地址。完成后的字符串应如下所示:

      v=spf1 ip4:###.###.###.### include:spf.protection.outlook.com ~all

      其中###.###.###.### 是您在步骤 1 中的公共 IP 地址。 跳过此步骤可能会导致电子邮件被发送到收件人的垃圾邮件文件夹。

    9. 最后,在您尝试发送邮件的设备或应用程序中,找到邮件服务器的设置并输入您在 第 3 步。因为每个设备和应用程序都不同,这个设置可能被标记为Mail ServerSMTP ServerSmart Host、简单的Server,或者完全是别的东西。您可能需要参考设备或应用程序的文档以找到合适的设置。

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

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

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

    Private Function SendFTPSuccessMail() As Boolean
        Dim Email As New System.Net.Mail.MailMessage
        Dim MailServer As New System.Net.Mail.SmtpClient("mail.domain")
        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
    

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

    【讨论】:

      【解决方案2】:

      您需要将用户名和密码发送至domain-com.mail.protection.outlook.com。它不接受 Exchange Online 组织外部收件人的匿名邮件。

      MailServer.Credentials = New System.Net.NetworkCredential("username", "password");
      

      【讨论】:

      • 感谢您的反馈,但您可能错过了我在原始帖子中声明我也尝试过但没有成功的部分。当我明天早上进入办公室时,我将更新我的问题,详细说明我使用 Credentials 属性时遇到的异常。
      猜你喜欢
      • 2018-11-30
      • 2012-01-07
      • 2012-04-27
      • 2016-07-04
      • 2018-09-19
      • 2021-03-23
      • 2017-05-16
      • 2011-07-14
      相关资源
      最近更新 更多