Tema 1: Usando el API de JavaMail

1.1. Introducción

JavaMail proporciona un conjunto de clases abstractas que definen los objetos e interfaces necesarias para implementar un sistema de e-mail.Los proveedores JavaMail que implementan el API proporcionan la funcionalidad necesaria para establecer la comunicación a través de protocolos concretos. Concretamente, la implementación de Sun permite manejar los protocolos SMTP (Simple Mail Transfer Protocol), IMAP (Internet Message Access Protocol), MIME (Multipurspose Internet Mail Extensions) y Post Office Protocol 3 (POP3).

En terminología JavaMail, usando los protocolos podemos implementar o bien un Transport o un Store. El primero se refiere a un servicio con capacidad para enviar mensajes a su destino (usualmente con SMTP), mientras que el segundo es un servicio con el que hay conectar para descargar mensajes que han sido enviados a nuestro buzón (p.e. POP3 o IMAP) .

1.2. Instalación y prueba

En cuanto a la instalación, lo primero es descargar la extensión JavaMail de java.sun.com/products/javamail. La última versión, por el momento es la 1.3. A continuación copiaremos el fichero mail.jar en el directorio de extensiones $JAVA_HOME/jre/lib/ext. Seguidamente instalaremos JAF (JavaBeans Activation Framework) tras descargarlo de java.sun.com/beans/glasgow/jaf.html. Copiaremos el fichero activation.jar en el fichero de extensiones.

Para comprobar que todo está correctamente instalado, compilaremos y ejecutaremos el programa de ejemplo msgsend.java. Desde línea de comandos escribiremos algo como:

java msgsend -o sco@dccia.ua.es -M luceros.dccia.ua.es otto@dccia.ua.es

donde especificamos la dirección origen (-o) el servidor SMTP (-M) y el destino. A continuación nos pedirá el subject y después el texto (terminar con CTRL-D). Generará un mensaje de éxito y luego podremos comprobar que nos ha llegado el e-mail.

1.3. Clase javax.mail.Session

Esta clase permite definir una sesión a partir de objetos java.util.Properties, en donde se supone que tenemos almacenados. En el ejemplo anterior, suponiendo que la variable mailhost o servidor SMTP contiene p.e. luceros.dccia.ua.es, tendríamos que

Properties props = System.getProperties();

if (mailhost != null)
  props.put("mail.smtp.host", mailhost);

// Get a Session object
Session session = Session.getDefaultInstance(props, null);
if (debug)
   session.setDebug(true);

crea una sesión compartida, mientras que usando getInstance() creamos una sesión no-compartida. El argumento null es un autentificador que veremos después.

1.4 Clases javax.mail.Message y javax.mail.internet.MimeMessage

Esta clase permite construir mensajes con todos sus atributos. Lo más habitual es utilizar la subclase MimeMessage que permite entender tipos MIME en donde los encabezados no se restringen a caracteres ASCII

Message msg = new MimeMessage(session);

El método setFrom() fija la dirección origen del mensaje (from) haciendo uso de objetos de la clase de direcciones javax.mail.internet.InternetAddress sublclase de la clase abstracta javax.mail.Address.

if (from != null)
    msg.setFrom(new InternetAddress(from));
else
    msg.setFrom();

El método setRecipients() determina los receptores del mensaje (to, cc, bcc):

msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to, false));

EL método setSubject() establece el tema del mensaje.

msg.setSubject(subject);

Para rellenar el contenido del mensaje hay que tener en cuenta si se trata de un mensaje de solo-de-texto o no. En caso de que sea solo texto se dispone del método setText() que recibe una cadena de entrada. Si como en el ejemplo el contenido se lee de un stream entonces esta cadena se va construyendo a medida que se leen las líneas y una vez terminado el stream se hace la llamada al método. Si por el contrario estamos incluyendo tipos MIME, como HTML en el contenido, utilizaremos el método setContent():

String contenido = "<HTML><HEAD><TITLE> Hola</TITLE></HEAD><BODY> Texto<BODY></HTML>"
msg.setContent(contenido, "text/html");

Finalmente, para fijar el encabezado hacemos uso de setHeader(), inclueyendo el nombre de la clase java desde la que se envía el mail, y para incluir la fecha de envío usaremos setSentDate():

String mailer = "msgsend";
...
msg.setHeader("X-Mailer", mailer);
msg.setSentDate(new Date());

Finalmente, hay que reseñar que los métodos setXXX tienen sus contrapartidas getXXX como veremos más adelante.

1.5 Clase javax.mail.Transport

Una vez construido el mensaje utilizamos esta clase para enviarlo, normalmente a través del protocolo SMTP. Para ello utilizamos los métodos send() al cual le pasamos o bien un único argumento (el objeto Message en cuestión) o bien dicho objeto seguido de un array de direcciones (objetos Address) con lo que se sustituyen los destinatarios previamente establecidos.

// send the thing off
Transport.send(msg);
System.out.println("\nMail was sent successfully.");

Alternativamente podemos utilizar un objeto Transport,obtenido a partir de la sesión actual, y llamar a sendMessage():

msg.saveChanges(); // send() incluye una llamada a este método.
Transport tr = session.getTransport("smtp");
tr.connect(host, usuario, password);
tr.sendMessage(msg, msg.getAllRecipients());
tr.close();

lo cual tiene la ventaja de que aprovechamos la misma conexión al servidor para enviar muchos mensajes, mientras que con send() se establece una conexión diferente para cada uno de ellos.

1.6 Clases javax.mail.Store y javax.mail.Folder

Hasta ahora hemos visto cómo enviar mensajes. Para leerlos del servidor empezaremos definiendo un objeto sesión, y a partir de él llamaremos al método getStore() indicando el protocolo de lectura (p.e. pop3 o imap). Seguidamente conectaremos, a través del método connect() con el servidor de mail una vez hayamos indicado el host (p.e. luceros.dccia.ua.es) el usuario y su password correspondiente (ver código en GetMessageExample.java):

// Create empty properties
Properties props = new Properties();

// Get session
Session session = Session.getDefaultInstance(props, null);

// Get the store
Store store = session.getStore("pop3");

// Connect to store
store.connect(host, username, password);

Seguidamente, se obtiene una o varias carpetas (objetos Folder) y se accede a los mensajes:

// Get folder
Folder folder = store.getFolder("INBOX");

// Open read-only
folder.open(Folder.READ_ONLY);

BufferedReader reader = new BufferedReader (
      new InputStreamReader(System.in));

// Get directory
Message message[] = folder.getMessages();

for (int i=0, n=message.length; i&ltn; i++) {

  // Display from field and subject
  System.out.println(i + ": " + message[i].getFrom()[0]
     + "\t" + message[i].getSubject());

  System.out.println("Do you want to read message? [YES to read/QUIT to end]");
  String line = reader.readLine();
  if ("YES".equals(line)) {

     // Display message content
     System.out.println(message[i].getContent());

  } else if ("QUIT".equals(line)) {
        break;
}
}

Para probar este ejemplo haremos lo siguiente:

java GetMessageExample luceros.dccia.ua.es sco password_sco

Adicionalmente, algunos de los métodos para acceder y consultar las carpetas son:

Folder getDefaultFolder() Obtiene la carpeta por defecto.
Folder getFolder(String nom) Obtiene la carpeta de nombre nom.
Folder getParent() Obtiene el padre de esta carpeta o null si estamos en el tope.
Folder[] list() Lista todas las sub-carpetas.
Folder[] list(String str) Lista todas las sub-carpetas que hacen matching con el patrón str, donde "*" indica cualquier carácter y "%" indica cualquier carácter menos el separador.
boolean exists() Devuelve true si la carpeta existe físicamente.
boolean create(int tipo) Crea una carpeta del tipo tipo, donde los tipos válidos son Folder.HOLDS_FOLDERS, Folder.HOLDS_MESSAGES o ambos.
void open(int modo) Abrir con el modo especificado: Folder.READ_ONLY o Folder.READ_WRITE.
Message[] expunge() Borra todos los mensajes marcados con el flag Flags.Flag.DELETED y devuelve los mensajes borrados en un array.
void close(boolean exp) Cierra la carpeta indicando si debe haber expunge o no.
boolean delete(boolean rec) Borra una carpeta aplicando recursividad o no en función de rec.

Si queremos, por ejemplo borrar mensajes utilizaremos cualquiera de las siguientes variantes de setFlags suministrando el flag Flags.Flag.DELETED y posteriormente cerrando el folder con expunge close(true). Previamente la carpeta tendrá que estar abierta para lectura-escritura:

void setFlags(int[] nums, Flags flag, boolean valor) Indicando los números de mensajes a borrar en un array
void setFlags(int i, int j, Flags flag, boolean valor) Indicando el rango de mensajes
void setFlags(Message[] mens, Flags flag, boolean valor) Indicando el array de mensajes

La información sobre los mensajes se obtiene de los métodos siguientes:

int getMessageCount() Devuelve el número de mensajes de la carpeta o -1 si no es posible.
boolean hasNewMessage() Indica si se ha recibido un nuevo mensaje desde la última vez que se abrió la carpeta.
int getNewMessageCount() Devuelve el número de mensajes nuevos o -1 si no es posible.
int getUnreadMessageCount() Devuelve el número de mensajes no leidos.
int getMessage(int num) Devuelve el mensaje correspondiente al número num.
Message[] getMessages() Devuelve todos los mensajes.
Message[] getMessages(int i, int j) Devuelve todos los mensajes entre los números i y j.

1.7 Estructura de Mensajes: Enviar y Recibir Attachments

Hasta ahora hemos hecho referencia al contenido de los mensajes sin entrar en detalle en el complejo modelo de contenido utilizado en JavaMail. Pero conocer dicho modelo es necesario si queremos manejar attachments.

En términos generales, un mensaje está compuesto de múltiples partes, cada una de las cuales es una javax.mail.BodyPart o una javax.mail.internet.MimeBodyPart y todas las partes pueden combinarse en un contenedor llamado javax.mail.MultiPart (o bien un javax.mail.internet.MimeMultiPart).

Veamos el código de una aplicación sencilla para enviar un attachment (ver AttachExample.java). Lo primero, tras crear la sesión es definir el mensaje:

Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject("Hello JavaMail Attachment");

Seguidamente, creamos la parte de texto y la rellenamos

// Create the message part
BodyPart messageBodyPart = new MimeBodyPart();

// Fill the message
messageBodyPart.setText("Here's the file");

Entonces creamos un contenedor y le añadimos el cuerpo de texto

// Create a Multipart
Multipart multipart = new MimeMultipart();

// Add part one
multipart.addBodyPart(messageBodyPart);

Entonces comenzamos a construir la parte que contendrá el attachment: (1) obtener el attachment, (2) instanciar un javax.activation.DataHandler que es una clase de JAF que permite manejar contenidos (por ello se requiere JAF), (3) indicar el fichero con el attachment, (4) >a>ñadir la parte con el attachment.

// Create second body part
 messageBodyPart = new MimeBodyPart();

// 1. Get the attachment
DataSource source = new FileDataSource(filename);

// 2. Set the data handler to the attachment
messageBodyPart.setDataHandler(new DataHandler(source));

// 3. Set the filename
messageBodyPart.setFileName(filename);

// 4. Add part two
multipart.addBodyPart(messageBodyPart);

Finalmente se pone el contenido del mensaje y se envía:

// Put parts in message
message.setContent(multipart);

// Send the message
Transport.send(message);

Una vez compilado, enviaremos un attachment desde la línea de comandos:

java AttachExample luceros.dccia.ua.es sco@dccia.ua.es sco@dccia.ua.es AttachExample.java

Supongamos ahora que queremos obtener los attachments. Para ello hemos de partir de que el contenido de nuestro mensaje es un Multipart. Hay que procesar las partes para extraer el cuerpo de texto y el attachment. Cuando llamamos al método getDisposition() de la clase Part para conocer la disposición de una parte, devuelve Part.ATTACHMENT o Part.INLINE si se trata de un attachment. En el código GetAttachExample.java mostramos como podemos descargar el attachment de un mensaje en nuestro directorio. Modificando el ejemplo anterior GetMessageExample.java en donde nos conectábamos a un servidor de mail para leer un mensaje. Dentro del bucle que lee los mensajes, si finalmente contestamos YES cuando nos pregunta si efectivamente queremos leer un mensaje dado, entonces se procede a descargar el attachment si se detecta que dicho mensaje contiene uno:

if ("YES".equals(line)) {

  // Crear una multipart
  Multipart mp = (Multipart)(message[i].getContent());

  // Bucle para extraer las parts
  for (int j=0, m=mp.getCount(); j< m; j++) {

     // Obtener las partes y su disposición
     Part part = mp.getBodyPart(j);
     String disposition = part.getDisposition();

     // Testar Attachment
     if ((disposition != null) &&
         (disposition.equals(Part.ATTACHMENT) || disposition.equals(Part.INLINE))) {
           saveFile(part.getFileName(), part.getInputStream());
     }

  }
  ...

donde el método saveFile() realiza la copia del attachment en un fichero con extensión "*.sco" para que no sobre-escriba uno de los ficheros de nuestro directorio.Así, con el siguiente comando:

java GetAttachExample luceros.dccia.ua.es sco password_sco

podemos descargar el fichero AttachExample.java.sco.

1.8 Búsqueda de Mensajes: javax.mail.search

JavaMail incorpora la posibilidad de realizar búsquedas de mensajes. Lo primero que hay que hacer es construir un objeto SearchTerm que implementa un filtro de búsqueda. Algunas de las clases que constituyen los argumentos de las expresiones del filtro son:

AndTerm(SearchTerm t1, SearchTerm t2) Implementa el AND de dos términos t1 y t2.
OrTerm(SearchTerm t1, SearchTerm t2) Implementa el OR de dos términos t1 y t2.
NotTerm(SearchTerm t) Implementa el NOT del término t.
FromTerm(Address a) Buscar mensajes cuyo "From" sea la dirección d.
FromStringTerm(String p) Buscar mensajes cuyo "From" haga matching con p.
SubjectTerm(String p) Buscar mensajes cuyo "Subject" haga matching con p.
BodyTerm(String p) En mensajes de texto busca mensaje "Subject" haga matching con p.
RecipientStringTerm(Message.RecipientType t, String p) Buscar receptores cuyo tipo sea t (p.e. CC, TO) y hagan matching con p.
SentDateTerm(int c, Date d) Mensajes enviados comparados con d según c (EQ, GE, GT, LE, LT, NE).
ReceivedDateTerm(int c, Date d) Mensajes recibidos comparados con d según c (EQ, GE, GT, LE, LT, NE).

Por ejemplo, para buscar aquellos mensajes recibidos de "otto@dccia.ua.es" o de "domingo@dccia.ua.es" cuyo subject sea "J2EE" y que contenga "Notas" haremos lo siguiente:

SearchTerm term =
   new AndTerm(
     new OrTerm(new FromTerm(new InternetAddress("otto@dccia.ua.es")) ,
                new FromStringTerm("sco@dccia.ua.es")
               ),
     new SubjectTerm("J2EE"),
     new BodyTerm("Notas")
   );

Para realizar la búsqueda, llamaremos al método search() de la clase Folder:

...
Message[] mensajes=folder.search(term)

Y para comprobar si un determinado mensaje hace match con el término anterior llamanremos al método match() de la clase Message.

if (mensage.match(term)) {
 ...
}

1.9. Clases javax.Authenticator y javax.PasswordAuthenticator

Estas clases tienen por objeto controlar el acceso al servidor de mail. La primera es una clase abstracta por lo que utilizamos la segunda para crear instancias. Concretamente, la autentificación tiene lugar al construir el objeto sesión. De hecho el segundo parámetro que se le pasa al constructor de la sesión es un objeto de una subclase de Authenticator que debe implementar el método getPasswordauthentication() que devuelve un objeto PasswordAuthentication (ver código en Autentificador.java):

import javax.mail.*;
import javax.swing.*;
import java.util.*;

public class Autentificador extends Authenticator {

  public PasswordAuthentication getPasswordAuthentication() {
    String username, password;

    String result = JOptionPane.showInputDialog(
      "Enter 'username,password'");

    StringTokenizer st = new StringTokenizer(result, ",");
    username = st.nextToken();
    password = st.nextToken();

    return new PasswordAuthentication(username, password);
  }

}

En este método incluimos el código necesario para solicitar el password (incluidos elementos gráficos).Posteriormente antes de crear una sesión haremos lo siguiente

...
Authenticator aut = new Autentificador();
Session session = Session.getDefaultInstance(props, aut);