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) .
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.
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.
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.
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.
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<n; 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. |
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.
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)) { ... }
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);