通过O365发送电子邮件

3
我们公司最近从一个本地的邮件服务器(CommunigatePro)迁移到了通过Office 365订阅托管的邮件服务。自那以后,我在我的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方法可以在所有收件人都在同一个(我的)域中时无错误地工作,但对于任何具有不同域的收件人地址,会抛出 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

这种方法似乎几乎每次都有效,但我不禁想知道是否有一种完全避免使用CDO发送方法的方法。 我尝试过使用SmtpClient对象的domain-com.mail.protection.outlook.comsmtp.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。服务器响应如下:"Mailbox unavailable. The server response was: 5.7.64 TenantAttribution; Relay Access Denied [SN1NAM01FT060.eop-nam01.prod.protection.outlook.com]"。个人而言,我想通过设置某种“通用”登录或实现一些方法来允许仅在我的网络中进行“匿名”发送,以摆脱使用邮件服务器的凭据,但这超出了本问题的范围。无论如何,我只是不确定自己缺少什么。也许是我Exchange配置中的某个设置或其他我忽略的“怪异”之处。谢谢您的帮助。
另外需要注意的一点是:我进行了一些其他配置的额外测试。如果我将SmtpClient主机更改为smtp.office365.com,并将端口设置为587(或25),并将EnableSsl属性设置为True,并明确提供用户凭据(这是我试图避免的主要事情),那么我就能够在不触发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作为主机名)提供凭据,因此并没有真正解决任何问题。
2个回答

5
正如我所怀疑的那样,问题显然是由Exchange配置引起的。最终,我按照Microsoft Office支持网站上概述的方法配置了一个“连接器”来使用Office 365 SMTP中继发送邮件。一旦我设置好中继连接器并放置SPF记录,我通过向我的托管Gmail帐户发送邮件进行了测试,无需提供任何凭据或为SmtpClient对象设置任何其他设置。第一次尝试时,配置显然没有完全生效,但我等了几分钟后再试了一次。这次,一切都顺利进行,没有触发我的Catch块,并且我的消息在我的Gmail收件箱中正确显示。
我似乎遇到了一些问题,我的多功能设备的某些消息现在被垃圾邮件过滤器捕获,尽管我已经更新了SPF记录。我希望这只是因为DNS还没有完全传播,但是由于我在O365管理控制面板中进行了更改,所以我还不能100%确定。我将等待几个小时看看会发生什么。
以下是我用来使其正常工作的步骤(直接从其页面取出并进行了一些小的编辑以进行澄清,语法和格式化):
  1. 获取将要发送邮件的设备或应用程序使用的公共(静态)IP地址。不支持或允许动态IP地址。可以与其他设备和用户分享您的静态IP地址,但不要与公司之外的任何人分享IP地址。记下此IP地址以在步骤7b和8中使用。

  2. 登录Office 365

  3. 选择。确保所选域名(例如,contoso.com)已经被选中。点击管理DNS并找到MX记录。MX记录将有一个指向地址值,看起来类似于以下截图所示的cohowineinc-com.mail.protection.outlook.com。注意MX记录的指向地址值。您将在步骤9中需要它。
    Make a note of the MX record Points to address value.

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

  5. 在Office 365中,点击管理,然后点击Exchange进入Exchange管理中心(EAC)。

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

  7. 检查为您的组织设置的连接器列表。如果没有列出从您的组织电子邮件服务器到Office 365的连接器,请创建一个。
    a. 点击加号符号(+)开始向导。在第一个屏幕上,选择以下选项:
    Choose "From your organization's email server" to "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中记录的MX记录的指向地址值。由于每个设备和应用程序都不同,因此此设置可能被标记为Mail ServerSMTP ServerSmart Host 编辑:看起来邮件被拦截的问题确实是由于DNS没有完全传播造成的。等待了略过24小时后,我没有看到任何来自内部系统的信息被隔离。

    顺便说一下,我们在内部DNS服务器上为mail.domain设置了一个CNAME(别名)记录,指向domain-com.mail.protection.outlook.com,以使我的编程更加方便。这个CNAME记录不会传播到公共DNS服务器,只会被我们的内部网络使用(因此省略了.com部分)。我已经有很多代码使用mail.domain地址作为SMTP服务器,所以将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发送方法,但是我现在把它留作“备用措施”。一旦我成功运行了一段时间,我可能会完全删除它,以便最终实现摆脱明确定义的用户凭据的目标。


0
你需要向 domain-com.mail.protection.outlook.com 发送用户名和密码。它不接受匿名邮件发送到你的Exchange Online组织外部的收件人。
MailServer.Credentials = New System.Net.NetworkCredential("username", "password");

感谢您的反馈,但您可能错过了我原始帖子中提到的部分,即我也尝试过那种方法,但没有成功。明天早上我到办公室后,我会在我的问题中更新有关使用“凭据”属性时遇到的异常的更多细节。 - G_Hosa_Phat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接