Envío de mail con framework 2.0, mi clase con autenticación

30 10 2007

A todos nos llega en algún momento la necesidad de que nuestro sistema envíe mails. Es ahí donde donde toda nuestra pasión por la programación se reduce a google y “ojala que alguién lo tenga resuelto”.

No es difícil encontrar como enviar mails con .Net, pero luego nos encontramos con diferentes requerimientos de formato, seguridad, etc. y como no queremos interiorizarnos en como MS implementó el aburrido protocolo SMTP en el framework, recurrimos nuevamente a google para aburrirnos un rato más. Bueno, he aquí el producto de mi tiempo desperdiciado y algunos comentarios sobre el tema. Si solamente necesitás una clase para enviar mails sin mucha vuelta te recomiendo ir directamente al código y aplicar un “copy-paste”, ya que el código es bastante auto-explicativo y hay un solo método para utilizar.

El namespace utilizado es System.Net.Mail (solo framework 2.0, antes era System.Web.Mail)

Basicamente, lo que hacemos es:

  • Crear un objeto MailMessage
  • Crear y asignar a la propiedad “From” del objeto MailMessage un objeto MailAddress con la dirección de salida
  • Crear y asignar a la propiedad “To” del objeto MailMessage una colección de objetos MailAddress conteniendo las direcciones de los destinatarios
  • Asignar un texto a las propiedades “Subject” y Body” del objeto MailMessage
  • Crear un objeto SmtpClient, definirle la dirección del host y el puerto, e invocar a su método Send pasándole como parámetro nuestro MailMessage

Esa es la manera de enviar un mail simple, con un body de texto plano, sin attachments y por el puerto 25 (default de SMTP), sin embargo, el único método público de mi clase (Enviar) tiene una serie de parámetros opcionales (serán sobrecargas para quien lo quiera pasar a C#) que permiten utilizar otras características:

MailPort permite especificar la utilización de otro puerto para conectarse al host SMTP (por ejemplo 443 para utilizar ssl), altarando la propiedad Port del SmtpClient .

MailIsBodyHtml permite especificar si el texto enviado es Html o texto plano (propiedad IsBodyHtml del objeto MailMessage).

EnableSSL determina si se utiliza (1), o no (0) este protocolo en la conexión con el host (propiedad EnableSsl de SmtpClient).

SSLuser y SSLpass permite enviar al host un user y password para establecer la conexión (propiedad Credentials de SmtpClient).

En el código no manejo attachments, pero para quien deba hacerlo, estos se incluyen por medio instancias de la clase System.Net.Attachment, que permite especificar un path o un stream como origen, el tipo de contenido (su mimetype) y algunas propiedades más. Estos objetos se adjuntan mediante el método Add de la colección Attachments del objeto MailMessage. Cuando pueda probar la clase con archivos adjuntos en un ambiente real, actualizo el código comentando si hubo algún issue al respecto.

Respecto al body en formato html, se puede comentar, aunque hablamos de SMTP y no de .Net, que si incluimos imágenes, nuestro html puede hacer referencia a imágenes hosteadas en algún server o a imágenes adjuntas, en cuyo caso se incluye solo el nombre de la imágen en el tag img.

Autenticación en un host con ssl

Donde hay muchas veces un gran problema, es en el uso de SSL para conectarse al host SMTP. Ocurre que para establecer el canal SSL el host utiliza un certificado, nuestra máquina cliente intentará validar ese certificado contra las autoridades certificantes registradas un su propio repositorio, y si no lo consigue arrojará una excepción con el fatídico texto “The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel” y dentro de ella otra con el más aclaratorio: “The remote certificate is invalid according to the validation procedure“. Esto generalmente ocurre porque el certificado no está emitido por una autoridad (CA) reconocida en nuestra máquina cliente, entonces podemos:

  • Obtener el certificado root de la CA e instalarlo la máquina donde correrá nuestro sistema para que esta reconozca el certificado del host SMTP (esto no pasa mucho más lejos de un click derecho en el archivo del certificado), normalmente le pediremos el mismo al administrador del host.
  • Solicitarle al responsable del host que pague un certificado de una CA reconocida, digamos VeriSign, etc. (esto siempre por teléfono o mail, nunca personalmente y si es el caso preparar antes un CV, por si nos quedamos sin cliente por insolentes)
  • Considerar que lo unico que nos interesa es que nuestro sistema envíe los condenados mails y forzar al framework a realizar una validación personalizada o a aceptar como válido cualquier certificado que el host posea (está última la opción del ejemplo)

Para ello (la tercera opción) el framework 2.0 nos expone una propiedad del objeto ServicePointManager, ServerCertificateValidationCallback, que nos permite especificar un delegado a una función que se encargue de validar el certificado a nuestro gusto, por ejemplo, retornando siempre true. Si quisieramos realizar una validación personalizada, como chequear el DN del certificado, deberíamos hacerlo aquí, ya que nuestro método de validación recibe el certificado en un parámetro, permitiendos acceder a todas sus propiedades.

Es importante destacar que esto último se aplica también a cualquier conexión http vía ssl realizada por el framework, llámese Web Service o HttpWebRequest.

Por último aquí un sitio dedicado al namespace System.Net.Mail y, si necesitamos tocar los headers del mensaje, este artículo de Scott Mitchell (cuando no) desde donde podemos acceder también a otros artículos de su autoría sobre envío de mails con .Net en sus diferente versiones.

Public Class Correo


    Public Shared Sub Enviar(ByVal MailDestinatarios As System.Net.Mail.MailAddressCollection, _
    ByVal MailDireccionOrigen As String, _
    ByVal MailNombreOrigen As String, _
    ByVal MailSubject As String, _
    ByVal MailBody As String, _
    ByVal MailHost As String, _
    Optional ByVal MailPort As Integer = 25, _
    Optional ByVal MailIsBodyHtml As Boolean = True, _
    Optional ByVal EnableSSL As Integer = 0, _
    Optional ByVal SSLuser As String = "", _
    Optional ByVal SSLpass As String = "")

        'Se crea el mensaje
        Dim oMensaje As New MailMessage

        'Se crea el cliente SMTP
        Dim oSMTP As New SmtpClient()

        'Se agregan las direcciones
        oMensaje.From = New MailAddress(MailDireccionOrigen, MailNombreOrigen)
        For Each addr As MailAddress In MailDestinatarios
            oMensaje.To.Add(addr)
        Next

        oMensaje.Subject = MailSubject
        oMensaje.Body = MailBody
        oMensaje.IsBodyHtml = MailIsBodyHtml
        'Seteo que el server notifique solamente en el error de entrega
        oMensaje.DeliveryNotificationOptions = DeliveryNotificationOptions.OnFailure

        'servidor smtp (dirección IP ó nombre de host)
        oSMTP.Host = MailHost
        'Puerto a utilizar (25 por defecto)
        oSMTP.Port = MailPort

        'Si se solicitó SSL, lo activo
        If EnableSSL = 1 Then
            oSMTP.EnableSsl = True
            'Bypass de validación de certificado (para problemas con servidores de SMTP con SSL con certificados que no validan en nuestra máquina)
            ServicePointManager.ServerCertificateValidationCallback = New RemoteCertificateValidationCallback(AddressOf ValidarCertificado)
        End If
        'Cargo las credenciales si hacen falta
        If Not String.IsNullOrEmpty(SSLuser) Then
            Dim credenciales As New System.Net.NetworkCredential(SSLuser, SSLpass)
            oSMTP.Credentials = credenciales
        End If

        Try
            oSMTP.Send(oMensaje)
        Catch ex As SmtpException
            Throw 'Aqui rutina de comprobación propia y logueo de excepción
        Catch ex As Exception
            Throw 'Aqui logueo de excepción
        Finally
            oMensaje = Nothing
            oSMTP = Nothing
        End Try
    End Sub


    Private Shared Function ValidarCertificado(ByVal sender As Object, ByVal certificate As X509Certificate, ByVal chain As X509Chain, ByVal sslPolicyErrors As System.Net.Security.SslPolicyErrors) As Boolean
        'bypass de la validación del certificado (aplicar aquí validación personalizada si fuera el caso)
        Return True
    End Function
End Class

Cuando actualice el código con utilización de archivos adjuntos y/o la versión en C# avisaré como comentario del artículo.

Saludos.