martes, 9 de enero de 2018

Envío de correos con .NET

Una de las funciones más importantes y comunes de los sistemas es el envío de correos electrónicos. La tecnología .NET tiene clases y objetos especialmente diseñados para eso. Pero se puede simplificar todavía más para que sea aún más fácil enviar correos. Aquí explicaré como construí mi propio conjunto de rutinas para enviar correos.

Lo primero que se necesita son algunas enumeraciones. Una es para obtener un estatus de éxito o error cuando se realiza el envío. Y la otra es para elegir entre un correo de texto plano y uno con contenido HTML.

Public Enum Estatus_Email As Byte
    EXITO = 0
    [ERROR] = 1
End Enum

Public Enum EnumTipoMensaje
    TextoPlano
    HTML
End Enum


También es conveniente tener una excepción especial para cuando ocurra un error de envío de correos.

Public Class Excepcion_Email
    Inherits Exception

    Sub New(ByVal mensaje As String)
        MyBase.New(mensaje)
    End Sub

    Sub New()
    End Sub
End Class

Aun es necesario otro objeto más. Este será para cuando se necesite adjuntar un archivo.

Public Class ArchivoAdjunto
    Public archivo As String = "" ' Ruta y nombre del archivo
    Public stream As IO.Stream = Nothing ' Un stream con los bytes. 

    ' Tipo MIME. Ejemplo: "image/bmp" o "application/doc"
    Public tipoMime As String 
    ' cid para incrustar imágenes. Ejemplo: <img src="cid:Logo"/>    
    Public cid As String 
  
End Class

Los datos que almacena este objeto son los siguientes:

archivo. Aquí va el nombre del archivo que se va a adjuntar. Por ejemplo: "c:\imagen.png"
stream. Este se usa cuando el archivo se encuentra en un stream. 
tipoMIME. Aquí se pone el tipo MIME del archivo. Por ejemplo: "image/png" 
cid. Cuando se trata de una imagen que estará incrustada como parte del cuerpo del mensaje, se necesita el dato cid. Se trata de una simple referencia a la imagen, por ejemplo: "logo"

Este objeto lleva dos constructores, uno para cuando se adjunta el archivo con una ruta del disco, y el otro para cuando se adjunta un stream. Lo único que hacen los constructores es poner los datos dentro de los campos del objeto.

Public Class ArchivoAdjunto
    Public archivo As String = "" 
    Public stream As IO.Stream = Nothing 

    Public tipoMime As String 
    Public cid As String 


    Sub New(ByVal archivo As String, ByVal tipoMime As String
                                     Optional ByVal cid As String = "")
        Me.archivo = archivo
        Me.tipoMime = tipoMime
        Me.cid = cid
    End Sub

    Sub New(ByRef stream As IO.Stream, ByVal tipoMime As String
                                       Optional ByVal cid As String = "")
        Me.stream = stream
        Me.tipoMime = tipoMime
        Me.cid = cid
    End Sub
End Class

Y ahora si, lo que sigue es tener un objeto que sea el encargado de enviar el correo.

Public Class Email

End Class

Esta clase también necesita algunos campos que usará para poder realizar el envío. Yo uso los siguientes:

Public Class Email
    Public Shared HabilitarEnvio As Boolean = True
    Public Shared Host, Port As String
    Public Shared UserName, Pwd, Domain As String

    Private smtp As SmtpClient
    Private adjuntos As New Collection

    Public errorDesc As String = ""
    Public hayError As Boolean = False

End Class

El campo HabilitarEnvio lo tengo porque hay veces que necesitas probar y depurar, pero sin que ocurra realmente un envío.
Los campos Host, Port, UserName, Pwd y Domain son los parámetros de configuración que las librerías de .NET necesita para poder hacer el envío del correo.

Después tengo un par de objetos. Uno es el objeto SmtpClient de .NET que es el que hace los envíos. El otro es una colección donde se almacenan los archivos adjuntos que tendrá el correo.

Por último, tengo dos campos que inicialmente están en blanco. En ellos se almacena la información de cualquier error que haya ocurrido durante el proceso.

También es necesario un constructor del objeto, precisamente para recibir los parámetros. Se aprovecha también el constructor para ajustar el objeto SmtpClient, por ejemplo, para pasarle las credenciales, o para habilitar el SSL:

Public Class Email
    Sub New()
        ' Prepara el obteto smtp
        smtp = New SmtpClient(Host, Port)
        smtp.DeliveryMethod = SmtpDeliveryMethod.Network
        smtp.Credentials = New System.Net.NetworkCredential(UserName, Pwd)
        smtp.EnableSsl = True
        smtp.Timeout = 30000 ' 30 segundos


    End Sub
End Class

Bien. Dentro de esta clase es donde tengo la opción de agregar archivos adjuntos. Para eso tengo dos funciones, una para agregar un archivo adjunto directo desde una ruta de disco, y el otro para agregarlo desde un stream. En ambas funciones, lo único que se hace es generar un nuevo objeto de tipo ArchivoAdjunto, y se agrega a la colección de adjuntos:

Public Class Email

    Public Sub AgregarAttachment(ByVal archivo As String
                                 ByVal tipoMime As String
                                 Optional ByVal cid As String = "")
        adjuntos.Add(New ArchivoAdjunto(archivo, tipoMime, cid))
    End Sub

    Public Sub AgregarAttachment(ByRef stream As IO.Stream, 
                                 ByVal tipoMime As String
                                 Optional ByVal cid As String = "")
        adjuntos.Add(New ArchivoAdjunto(stream, tipoMime, cid))
    End Sub
End Class

También será necesario, en algún momento, después de haber realizado el envío, dejar en blanco la colección, para que otros envíos subsecuentes de otros correos no lleven esos archivos adjuntados. Para eso utilizo la siguiente rutina. En realidad, solo libera la memoria utilizada por los streams, en caso de haber alguno.

Public Class Email
    Public Sub ClearAdjuntos()
        For Each aa As ArchivoAdjunto In adjuntos
            If aa.stream IsNot Nothing Then
                If aa.stream.GetType().Name = "IO.MemoryStream" Then
                    aa.stream.Dispose()
                End If
            End If
        Next
        adjuntos.Clear()
    End Sub
End Class

Y por último, solo falta la rutina que realiza los envíos. Esta recibe los destinatarios separados por comas, la cuenta de envío, el asunto, el cuerpo del mensaje, el tipo de mensaje (texto plano o HTML), las cuentas para hacer copia o copia oculta, cuenta de "respuesta para", y nombre del remitente. Todo esto lo acomoda dentro de un objeto MailMessage.

Además, considera todos los archivos adjuntos, ya sea por medio de un stream o por medio de una ruta de disco, y los agrega al mensaje. 

Después, en caso de que los envíos estén habilitados, realiza el envío. Tiene un bloque Try-Catch para atrapar cualquier error que pueda ocurrir, y en caso de haberlo se puede registrar en un archivo de Log. 

Al terminar, utiliza la rutina ClearAdjuntos para que, como se mencionó antes, los archivos adjuntos no vayan incluidos en otros envíos.

Aquí está la rutina:

Public Class Email

    ' Envia un eMail con imagenes incrustadas, puede tener attachments con streams
    Public Function Enviar(ByVal de As String, ByVal para As String
                           ByVal asunto As String
                           ByVal mensaje As String, tipo As EnumTipoMensaje,
                           Optional ByVal nombre_remitente As String = ""
                           Optional ByVal copia_para As String = ""
                           Optional ByVal copia_oculta_para As String = ""
                           Optional ByVal respuesta_para As String = ""
                           As Estatus_Email

        Dim _para(), _copia_para(), _copia_oculta_para() As String
        Dim _respuesta_para() As String
        Dim msg As New MailMessage

        Enviar = Estatus_Email.EXITO

        hayError = False
        errorDesc = ""

        Try
            ' Prepara los destinatarios
            If Not para = "" Then
                _para = para.Split(",")
                For i As Integer = 0 To _para.Length - 1
                    If _para(i).Trim <> "" Then
                        msg.To.Add(_para(i).Trim)
                    End If
                Next
            End If
            If Not copia_para = "" Then
                _copia_para = copia_para.Split(",")
                For i As Integer = 0 To _copia_para.Length - 1
                    If _copia_para(i).Trim <> "" Then
                        msg.CC.Add(_copia_para(i).Trim)
                    End If
                Next
            End If
            If Not copia_oculta_para = "" Then
                _copia_oculta_para = copia_oculta_para.Split(",")
                For i As Integer = 0 To _copia_oculta_para.Length - 1
                    If _copia_oculta_para(i).Trim <> "" Then
                        msg.CC.Add(_copia_oculta_para(i).Trim)
                    End If
                Next
            End If

            ' Prepara el remitente
            If Not respuesta_para = "" Then
                _respuesta_para = respuesta_para.Split(",")
                For i As Integer = 0 To _respuesta_para.Length - 1
                    If _respuesta_para(i).Trim <> "" Then
                        msg.ReplyToList.Add(_respuesta_para(i).Trim)
                    End If
                Next
            End If
            If Not de = "" Then
                If nombre_remitente = "" Then
                    msg.From = New MailAddress(de)
                Else
                    msg.From = New MailAddress(de, nombre_remitente)
                End If
                msg.Sender = New MailAddress(de)
            End If


            ' Titulo y cuerpo del mensaje
            msg.Subject = asunto
            msg.SubjectEncoding = Encoding.UTF8
            If tipo = EnumTipoMensaje.HTML Then
                msg.IsBodyHtml = True
                msg.AlternateViews.Add(
                              AlternateView.CreateAlternateViewFromString(
                                    mensaje, Encoding.UTF8, "text/html"))
            Else
                msg.IsBodyHtml = False
                msg.Body = mensaje
            End If

            ' Attachments
            If adjuntos.Count > 0 Then
                Dim lnk As LinkedResource
                For Each aa As ArchivoAdjunto In adjuntos
                    If aa.cid = "" Then
                        ' Es un archivo adjunto
                        If aa.archivo <> "" Then
                            msg.Attachments.Add(New Attachment(
                                                      aa.archivo, aa.tipoMime))
                        Else
                            msg.Attachments.Add(New Attachment(
                                                      aa.stream, aa.tipoMime))
                        End If
                    Else
                        ' Es una imagen incrustada
                        If aa.archivo <> "" Then
                            lnk = New LinkedResource(aa.archivo, aa.tipoMime)
                            lnk.ContentId = aa.cid
                            msg.AlternateViews(0).LinkedResources.Add(lnk)
                        Else
                            lnk = New LinkedResource(aa.stream, aa.tipoMime)
                            lnk.ContentId = aa.cid
                            msg.AlternateViews(0).LinkedResources.Add(lnk)
                        End If
                    End If
                Next
            End If

            ' Manda el correo
            If HabilitarEnvio Then
                smtp.Send(msg)
            End If

        Catch ex As Exception
            hayError = True
            errorDesc = ex.Message
            Enviar = Estatus_Email.ERROR

        Finally

            ClearAdjuntos()

        End Try

    End Function


End Class

No olvides que necesitarás también importar algunos espacios de nombres. Yo tengo los siguientes:

Imports System.Net.Mail
Imports Microsoft.VisualBasic
Imports System.Configuration.ConfigurationSettings
Imports System.Text
Imports System.Net.Security

Bien, ahora veamos como funciona. Verás que es fácil. Primero que nada, en cualquier lugar donde se puedan inicializar variables, se necesita lo siguiente:

Email.Host = "smtp.live.com"
Email.Port = 587
Email.UserName = "micorreo@hotmail.com"
Email.Pwd = "xxxxxxxx"

Después, donde se va a enviar el correo, primero se genera un nuevo objeto:

Dim objCorreo As New Email

Y por último, solo es cuestión de usar el objeto:

objCorreo.AgregarAttachment("E:\Media\PAISAJES\prado.jpg", "images/jpg")
objCorreo.AgregarAttachment("e:\ciencia\tangram\tangram.pdf", "application/pdf")
objCorreo.Enviar("micuenta@hotmail.com", "cuentadestino@yahoo.com", "Hola mundo", "Mensaje de prueba.", EnumTipoMensaje.TextoPlano)


objCorreo.Enviar("micuenta@hotmail.com""otracuentadestino@yahoo.com""Hola mundo""Mensaje de <strong>prueba</strong>."EnumTipoMensaje.HTML)

objCorreo.AgregarAttachment("E:\Media\Gif Animados\roses.gif", "images/gif", "roses")
objCorreo.Enviar("micuenta@hotmail.com""otracuentadestino@yahoo.com", "Hola mundo", "Mensaje de prueba. <img src='cid:roses'/>", EnumTipoMensaje.HTML)

Espero que este objeto te sea muy útil.

Puedes descargar el archivo completo aquí