ASP.Net Enviar correo electrónico con ficheros adjuntos

Otra de las preguntas bastante frecuente por los foros de Microsoft es la posibilidad de enviar un correo electrónico desde una página pero que contenga uno o varios ficheros adjuntos. Aunque en una búsqueda genérica nos aparecen miles de entradas sobre el tema, intentaremos, en esta ocasión, crear un ejemplo lo más sencillo posible.

El envío de un correo electrónico es relativamente sencillo, sobre todo, si nos abstraemos de la configuración del correo y dejamos los datos en el archivo web.config.

   1: <system.net>
   2:   <mailSettings>
   3:     <smtp from="cuentaValidaDelServidor">
   4:       <network host="servidorSMTP"
   5:                userName ="usuarioServidorSMTP"
   6:                password="contraseñaServidorSMTP"
   7:                />
   8:     </smtp>
   9:   </mailSettings>
  10: </system.net>

De esta manera, el envío de correo sería algo así como:

   1: void enviaCorreo()
   2: {
   3:     using (System.Net.Mail.MailMessage message = new System.Net.Mail.MailMessage())
   4:     {
   5:         // Dirección de destino
   6:         message.To.Add("cuentaDestino");
   7:         // Asunto 
   8:         message.Subject = "Asunto del mensaje";
   9:         // Mensaje 
  10:         message.Body = "Cuerpo del mensaje";
  11:  
  12:         // Se envía el mensaje
  13:         System.Net.Mail.SmtpClient smpt = new System.Net.Mail.SmtpClient();
  14:         smpt.Send(message);
  15:     }
  16: }

Ahora que ya tenemos definido el envío del correo, nos podemos centrar en cómo hacerlo desde una página ASP.Net.

Para el envío de un mensaje estándar (sin datos adjuntos) bastaría con definir unos cuadros de texto para el destinatario, el asunto y el mensaje, pero a la hora de enviar ficheros adjuntos podemos encontrarnos con que otro problema, pues deberemos tener en cuenta lo siguiente:

  • Si vamos a enviar ficheros adjuntos, éstos deberán encontrarse en el servidor, nunca el cliente
  • Salvo que usemos controles externos, el control UploadFile sólo nos permite subir un fichero a la vez
  • queremos que el número de ficheros a enviar sea variable.

Con estas premisas, sólo nos queda hacer una cosa:

  1. Deberemos subir los ficheros, de uno en uno y previo al envío del correo.
  2. El usuario se puede equivocar a la hora de subir un fichero y debería poder borrarlo

Esto nos lleva a pensar que, además de tener los ficheros en una carpeta del servidor, deberemos añadir los nombres de dichos ficheros en algún control, de tal manera que el usuario pueda modificarlo. Para el ejemplo actual escojo uno sencillo, el ListBox, que nos permitirá mostrar al cliente los ficheros subidos y, en caso necesario, podrá eliminar alguna entrada. A nivel de diseño, creo una tabla con los controles UploadFile y el ListBox que encapsulo dentro de un UpdatePanel.

   1: <asp:UpdatePanel ID="pnUpdateFile" runat="server" UpdateMode="Conditional">
   2:     <Triggers>
   3:         <asp:PostBackTrigger ControlID="cmdAddFile" />
   4:     </Triggers>
   5:     <ContentTemplate>
   6:         <table>
   7:             <tr>
   8:                 <td style="width: 450px;">
   9:                     <asp:FileUpload ID="fUpload" runat="server" /></td>
  10:                 <td style="width: 50px;">
  11:                     <asp:Button ID="cmdAddFile" 
  12:                         runat="server" 
  13:                         Text="+" 
  14:                         ToolTip="Añade el fichero a la lista"
  15:                         OnClick="cmdAddFile_Click" />
  16:                 </td>
  17:             </tr>
  18:             <tr>
  19:                 <td>
  20:                     <asp:ListBox ID="lstFiles" runat="server"></asp:ListBox>
  21:                 </td>
  22:                 <td>
  23:                     <asp:Button ID="cmdDelFile" 
  24:                         runat="server" 
  25:                         Text="-" 
  26:                         ToolTip="Elimina el fichero seleccionado de la lista"
  27:                         OnClick="cmdDelFile_Click" />
  28:                 </td>
  29:             </tr>
  30:         </table>
  31:     </ContentTemplate>
  32: </asp:UpdatePanel>

Bueno, ahora que ya tenemos definida la estructura HTML procederemos a implementar el code-behind delos botones que nos permitirán subir ficheros al servidor (+) y eliminarlos (-)

   1: private string tempPath = @"~/uploads/temp";
   2:  
   3:    protected void cmdAddFile_Click(object sender, EventArgs e)
   4:    {
   5:  
   6:        FileUpload f = fUpload;
   7:  
   8:        // No se hace nada si no hay fichero
   9:        if (!f.HasFile)
  10:            return;
  11:  
  12:        // Se crea un Item para el ListBox
  13:        //  - Value: Nombre del fichero
  14:        //  - Text : Texto para mostrar
  15:        ListItem item = new ListItem();
  16:        item.Value = f.FileName;
  17:        item.Text = f.FileName + 
  18:                    " (" + f.FileContent.Length.ToString("N0") + 
  19:                    " bytes).";
  20:  
  21:        // Se sube el fichero a la carpeta temporal
  22:        f.SaveAs(Server.MapPath(Path.Combine(tempPath, item.Value)));
  23:  
  24:        // Se deja el nombre del fichero en el ListBox
  25:        lstFiles.Items.Add(item);
  26:    }
  27:  
  28:    protected void cmdDelFile_Click(object sender, EventArgs e)
  29:    {
  30:        ListBox lb = lstFiles;
  31:        // Se comprueba que exista algún item seleccionado
  32:        if (lb.SelectedItem == null)
  33:            return;
  34:  
  35:        // Se elimina el fichero seleccionado
  36:        borraEntrada(lb.SelectedItem.Value);
  37:    }
  38:  
  39:    /// <summary>
  40:    /// Elimina el fichero de la carpeta temporal y del ListBox.
  41:    /// </summary>
  42:    /// <param name="fileName"></param>
  43:    private void borraEntrada(string fileName)
  44:    {
  45:        string fichero = Server.MapPath(Path.Combine(tempPath, fileName));
  46:        File.Delete(fichero);
  47:  
  48:        ListItem l = lstFiles.Items.FindByValue(fileName);
  49:        if (l != null)
  50:            lstFiles.Items.Remove(l);
  51:    }

 

Una vez hemos cumplido los requisitos iniciales, ya tan sólo nos queda crear el botón para que envíe el mensaje, leyendo los archivos de los Items del ListBox.

   1: /// <summary>
   2:   /// Envía el correo electrónico.
   3:   /// Los datos de configuración del servidor de correo SMTP se configuran 
   4:   /// en el fichero web.config.
   5:   /// </summary>
   6:   private void enviaCorreo()
   7:   {
   8:       using (System.Net.Mail.MailMessage message = new System.Net.Mail.MailMessage())
   9:       {
  10:           // Dirección de destino
  11:           message.To.Add(txtPara.Text);
  12:           // Asunto 
  13:           message.Subject = txtAsunto.Text;
  14:           // Mensaje 
  15:           message.Body = txtMensaje.Text;
  16:  
  17:           // Se recuperan los ficheros
  18:           foreach (ListItem l in lstFiles.Items)
  19:           {
  20:               // Lectura del nombre del fichero
  21:               string fichero = Server.MapPath(Path.Combine(tempPath, l.Value));
  22:  
  23:               // Adjuntado del fichero a la colección Attachments
  24:               message.Attachments.Add(new System.Net.Mail.Attachment(fichero));
  25:           }
  26:  
  27:           // Se envía el mensaje y se informa al usuario
  28:           System.Net.Mail.SmtpClient smpt = new System.Net.Mail.SmtpClient();
  29:           string mensaje = string.Empty;
  30:           try
  31:           {
  32:               smpt.Send(message);
  33:               mensaje = "Correo enviado con éxito";
  34:           }
  35:           catch (Exception ex)
  36:           {
  37:               mensaje = "Ocurrió un error: " + ex.Message;
  38:           }
  39:           resultado.Text = mensaje;
  40:       }
  41:  
  42:       // Se borran los ficheros de la carpeta temporal
  43:       while (lstFiles.Items.Count > 0)
  44:       {
  45:           borraEntrada(lstFiles.Items[0].Value);
  46:       }
  47:   }

Como va siendo habitual, por si me he dejado algo en el Copy & Paste o por si alguien quiere descargar el ejemplo, dejo un enlace con la aplicación. Como se puede apreciar, he intentado hacer uso de Themes y hoja de estilos externas para dejar en la página de envío tan sólo aquellos elementos que se refieran al envío del correo.

Enlace

C# Encuadrar una imagen mediante GDI+

En los foros de MSDN es frecuente leer alguna pregunta de cómo se puede encuadrar una fotografía para poder adaptarla a un determinado tamaño. A menudo las respuestas se enfocan al entorno de desarrollo, así por ejemplo, en el caso de ser en el foro de ASP.Net, la respuesta comúnmente se enfoca a la creación de un DIV que permita simular el cuadro externo, en caso de WinForms, un panel que recubra la imagen… No obstante, cuando tratamos de hacer algo así, casi siempre nos encontramos con problemas adicionales puesto que no todas las imágenes que almacenamos tienen ni la misma resolución ni la misma orientación.

Pongamos un ejemplo que seguro que entenderemos mejor los problemas. Para el ejemplo opto por usar WebForms y busco por internet tres fotografías diferentes de la Sagrada Familia de Barcelona con resoluciones y orientación diferentes: 160×230, 640×480 y 4000×3000.

Crear el entorno ASP.Net

Para simular el marco de la fotografía, lo que hago es crear un div que envolverá la fotografía

   1: <div id="showFoto">
   2:     <asp:Image ID="foto" runat="server"   />
   3: </div>

Y, lógicamente, defino los estilos:

   1: #showFoto
   2: {
   3:     background-color: #FFFF00;
   4:     border: thin inset #FFFF00;
   5:     width: 500px;
   6:     height:375px;
   7:     margin: 0 auto;
   8: }
   9:  
  10: #showFoto img
  11: {
  12:     position:relative;
  13:     top: 50%;
  14:     left: 50%;
  15:     width: 460px;
  16:     height:345px;
  17:     margin-top: -172px;
  18:     margin-left: -230px;
  19: }

Con esto un resultado más o menos óptimo en IE9, Firefox 9 y Chrome 16 siempre y cuando la fotografía tenga una orientación horizontal.

image

Eso sí, al haber hecho uso de medidas fijas tanto para el ancho como para el alto de la capa y de la foto, en el caso de que ésta última tenga una orientación vertical, podemos decir que no queda tan bonito puesto que se deforma la imagen.

image

Démosle la vuelta a la tortilla

No sé si existe algún elemento desde el HTML o los CSS que solventen este problema pero aun así, siempre nos encontraríamos con el mismo problema si acudimos a WinForms, WPF… Le daré un vuelco al planteamiento y lo que intentaré será, desde C#, añadir un marco de un tamaño variable a la imagen y crear los métodos necesarios para que se adapte una imagen redimensionada al marco. Por tanto, creo una clase con una serie de métodos públicos que permitan realizar tal acción.

image

Dos propiedades RecuadroExterior y CapasDegradado permitirán definir el tamaño de la nueva imagen y el ancho del marco respectivamente; y tres métodos sobrecargados que intentarán facilitarnos el trabajo según el origen sea un array de bytes extraído de la base de datos, una imagen o, sencillamente, la ruta completa de un fichero.

Para la conversión del array de bytes en Imagen (o si necesitamos la inversa) uso una rutina muy conocida por los foros de ASP.Net que nos lo permite hacer:

   1: /// <summary>
   2:  /// Convierte un array de bytes en una imagen
   3:  /// </summary>
   4:  /// <remarks>
   5:  /// Extraido de http://www.daniweb.com/software-development/csharp/code/365920
   6:  /// </remarks>
   7:  public static Image byteArrayToImage(byte[] byteArrayIn)
   8:  {
   9:      MemoryStream ms = new MemoryStream(byteArrayIn);
  10:      Image returnImage = Image.FromStream(ms);
  11:      return returnImage;
  12:  }
  13:  
  14:  /// <summary>
  15:  /// Convierte una imagen en un array de bytes
  16:  /// </summary>
  17:  /// /// <remarks>
  18:  /// Extraido de http://www.daniweb.com/software-development/csharp/code/365920
  19:  /// </remarks>
  20:  public static byte[] imageToByteArray(System.Drawing.Image imageIn)
  21:  {
  22:      MemoryStream ms = new MemoryStream();
  23:      imageIn.Save(ms, System.Drawing.Imaging.ImageFormat.Gif);
  24:      return ms.ToArray();
  25:  }

Para las propiedades de los tamaños abogo por establecer unos valores más o menos estándares sin olvidar que podemos modificarlos en cada momento:

   1: /// <summary>
   2: /// Obtiene o establece el tamaño del cuadro de salida
   3: /// Por defecto: 640 x 480
   4: /// </summary>
   5: public Size RecuadroExterior
   6: {
   7:     get { return _recuadroExterior; }
   8:     set { _recuadroExterior = value; }
   9: }
  10:  
  11: /// <summary>
  12: /// Obtiene o establece las capas del marco, es decir, su ancho en 
  13: /// píxeles
  14: /// </summary>
  15: public int CapasDegradado
  16: {
  17:     get { return _capasDegradado; }
  18:     set { _capasDegradado = value; }
  19: }
  20:  
  21: private Size _recuadroExterior = new Size(640, 480);
  22: private int _capasDegradado = 20;

Una vez creado más o menos el entorno, nos falta definir la metodología a seguir que, más o menos, será la siguiente:

  1. Lectura de la imagen inicial
  2. Creación de una nueva imagen con el tamaño definido en RecuadroExterior
  3. Creación del marco
  4. Comparar el tamaño de la imagen inicial con el tamaño del marco.
    1. Si la imagen es mayor que el marco, se redimensionará adaptándola al extremo más amplio
  5. Se centrará la imagen tanto horizontal como verticalmente.

Los métodos de trabajo.

Pasaremos ahora a la implementación de los métodos y comenzaremos por los métodos sobrecargados que devolverán la imagen:

   1: /// <summary>
   2:  /// Obtiene una imagen en base al parámetro <para>fichero</para>
   3:  /// </summary>
   4:  /// <param name="fichero">
   5:  /// Ruta completa del fichero gráfico que se quiere encuadrar
   6:  /// </param>
   7:  /// <returns>
   8:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
   9:  /// </returns>
  10:  public Image GetImagenConMarco(string fichero)
  11:  {
  12:      Image img = Image.FromFile(fichero);
  13:      return GetImagenConMarco(img);
  14:  }
  15:  
  16:  
  17:  /// <summary>
  18:  /// Obtiene una imagen encuadrada con el array de bytes pasado
  19:  /// como parámetro
  20:  /// </summary>
  21:  /// <param name="bytes">
  22:  /// Array de bytes que representa una imagen
  23:  /// </param>
  24:  /// <returns>
  25:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
  26:  /// </returns>
  27:  /// <remarks>Se crea este método para facilitar el trabajo
  28:  /// al extraer imágenes de una base de datos.</remarks>
  29:  public Image GetImagenConMarco(byte[] bytes)
  30:  {
  31:      Image img = JnSoftware.Utiles.GestionImagenes.byteArrayToImage(bytes);
  32:      return GetImagenConMarco(img);
  33:  }
  34:  
  35:  
  36:  /// <summary>
  37:  /// Encuadra la imagen dentro de un marco
  38:  /// </summary>
  39:  /// <param name="imagen">Imagen que quiere encuadrarse</param>
  40:  /// <returns>
  41:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
  42:  /// </returns>
  43:  public Image GetImagenConMarco(Image imagen)
  44:  {
  45:      imagenOriginal = imagen;
  46:      adaptaImagenACuadro();
  47:      return fondo;
  48:  }
  49:  
  50:  private Image imagenOriginal;

 

Se puede apreciar cómo el único método que realmente hace algo con respecto de la imagen es el último y, sencillamente, queda delegado al método privado adaptaImagenACuadro().

   1: /// <summary>
   2: /// Gestor de acciones.
   3: /// </summary>
   4: private void adaptaImagenACuadro()
   5: {
   6:     creaEntornoTrabajo();
   7:     estableceFondo();
   8:     creaMarco();
   9:     insertaImagen();
  10: }

Crear una nueva imagen como fondo

Creación de una nueva imagen con el tamaño especificado a la vez que la dejamos preparada para poder trabajar con GDI+.

   1: /// <summary>
   2:  /// Crea el fondo con el tamaño indicado.
   3:  /// </summary>
   4:  private void creaEntornoTrabajo()
   5:  {
   6:      // Creamos el fondo
   7:      fondo = new Bitmap(RecuadroExterior.Width, RecuadroExterior.Height);
   8:      entorno = Graphics.FromImage(fondo);
   9:  }
  10:  
  11:  private Image fondo;
  12:  Graphics entorno;

Rellenamos el fondo de color blanco que, aunque en el ejemplo es fijo, se puede cambiar fácilmente. La metodología: dibujar un rectángulo lleno que cubra toda la superficie.

   1: /// <summary>
   2: /// Crea el fondo de la imagen
   3: /// </summary>
   4: private void estableceFondo()
   5: {
   6:     Point posInicial = new Point(0, 0);
   7:     Brush colorFondo = Brushes.WhiteSmoke;
   8:  
   9:     Rectangle r = new Rectangle(posInicial, RecuadroExterior);
  10:     entorno.FillRectangle(colorFondo, r);
  11: }

Para el recuadro exterior optamos por usar la misma técnica, es decir, crear tantos recuadros no llenos en los que cada uno de ellos tiene un borde de color diferente que va desde oscuro a claro. De esta manera obtendremos un efecto de degradado.

   1: /// <summary>
   2: /// Crea un marco degradado en base a rectángulos.
   3: /// </summary>
   4: private void creaMarco()
   5: {
   6:     // Añadimos el marco
   7:     int intervalo = 255 / CapasDegradado;
   8:     for (int i = 0; i < CapasDegradado; i++)
   9:     {
  10:         Rectangle r = new Rectangle();
  11:         r.X = i;
  12:         r.Y = i;
  13:         r.Width = RecuadroExterior.Width - (i * 2);
  14:         r.Height = RecuadroExterior.Height - (i * 2);
  15:  
  16:         Pen p = new Pen(Color.FromArgb(i * intervalo, i * intervalo, i * intervalo));
  17:         entorno.DrawRectangle(p, r);
  18:     }
  19: }

Tratamiento de la imagen original

Tal como comentaba al principio, en el caso que la imagen pasada como parámetro sea inferior al recuadro, tan sólo será cuestión de centrarla en esta nueva imagen que nos hemos creado como fondo.

   1: /// <summary>
   2: /// Redimensiona la imagen y la centra
   3: /// </summary>
   4: private void insertaImagen()
   5: {
   6:     Size tamanoImagen = imagenOriginal.Size;
   7:  
   8:     // Se comprueba si la imagen es menor que el marco
   9:     if (!((tamanoImagen.Width < RecuadroExterior.Width - CapasDegradado * 2)
  10:      && (tamanoImagen.Height < RecuadroExterior.Height - CapasDegradado * 2)))
  11:     {
  12:         redimensionaImagen();
  13:     }
  14:     centraImagen();
  15: }
  16:  
  17:  
  18: // <summary>
  19: /// La imagen ya está redimensionada a un tamaño más pequeño 
  20: /// que el cuadro. 
  21: /// Tan sólo se trata de centrarla.
  22: /// </summary>
  23: private void centraImagen()
  24: {
  25:     Point posInicial = new Point();
  26:     posInicial.X = (RecuadroExterior.Width - imagenOriginal.Width) / 2;
  27:     posInicial.Y = (RecuadroExterior.Height - imagenOriginal.Height) / 2;
  28:  
  29:     Rectangle r = new Rectangle(posInicial, imagenOriginal.Size);
  30:     entorno.DrawImage(imagenOriginal, r);
  31: }

Hasta el momento, la clase será operativa en el caso que la imagen sea inferior en tamaño al recuadro deseado. Por lo tanto, tan sólo nos quedará redimensionar la imagen en el caso que tenga un tamaño superior. Para que el redimensionado sea lo más efectivo posible lo que hago es mirar la proporción ancho-alto de la imagen y comparar la longitud que sea superior. De esta manera, obtendremos el mayor tamaño posible sin que se deforme la misma.

   1: /// <summary>
   2: /// La imagen es mayor que el cuadro de presentación, así que se 
   3: /// se redimensiona para que se adapte, sin deformarse al ancho/alto 
   4: /// mayor
   5: /// </summary>
   6: private void redimensionaImagen()
   7: {
   8:     double proporcionAncho;
   9:     double proporcionAlto;
  10:     double factorReduccion;
  11:     Size s = imagenOriginal.Size;
  12:  
  13:     proporcionAncho = (double)imagenOriginal.Width / (double)(RecuadroExterior.Width - CapasDegradado * 2);
  14:     proporcionAlto = (double)imagenOriginal.Height / (double)(RecuadroExterior.Height - CapasDegradado * 2);
  15:  
  16:     if (proporcionAncho > proporcionAlto)
  17:         factorReduccion = proporcionAncho;
  18:     else
  19:         factorReduccion = proporcionAlto;
  20:  
  21:     imagenOriginal = new Bitmap(imagenOriginal, (int)(s.Width / factorReduccion), (int)(s.Height / factorReduccion));
  22: }

 

Por si acaso se ha quedado algo en el tintero a la hora de hacer los Copy&Paste del código dejo un ejemplo que muestra la funcionalidad en un entorno WinForms.

Enlace