MokaByte 82 - Febbraio 2004 
Java Mail 2003
Gestire la posta elettronica in Java
IV parte: approfondimento sulla gestione dei messaggi
di
Massimiliano
Bigatti
Il protocollo della posta Internet è per sua natura dinamico: non solo, ed ovviamente, per il contenuto e la sua lunghezza, ma anche relativamente alle intestazioni che sono associate ad un messaggio che contengono preziose informazioni relative al percorso intrapreso dal messaggio, oltre che a dati vitali come il mittente e l’oggetto.

Intestazioni dinamiche
Quello che segue è un estratto delle intestazioni di un messaggio di prova inviato e ricevuto tramite un provider Internet non particolare. All'interno di questo si possono riconoscere molte chiavi: From, Return-Path, Delivered-To, Received, Message-ID, ecc. Ciascuna di queste ha un preciso significato all'interno del mondo della posta elettronica che, per una corretta interoperabilità tra diversi server e client, deve essere uniforme.

From spammer_99@null.it Sat Dec 20 16:57:57 2003
Return-Path: <spammer_99@null.it>
Delivered-To: max@0
Received: (qmail 23653 invoked by uid 511); 20 Dec 2003 12:05:44 -0000
Received: from spammer_99@null.it by mxavas1.aruba.it by uid 503 with qmail-scanner-1.20rc4
(fsecure: 4.50/2111/fprot(2003-10-15)/avp(2003-10-16). spamassassin: 2.60. Clear:RC:0:SA:0(1.2/5.0):.
Processed in 0.133694 secs); 20 Dec 2003 12:05:44 -0000
Received: from unknown (HELO vsmtp2.tin.it) (212.216.176.222)
by mxavas1.aruba.it with SMTP; 20 Dec 2003 12:05:43 -0000
Received: from Computer-di-Massimiliano-Bigatti.local (82.48.89.160) by vsmtp2.tin.it (7.0.019)
id 3FE030400010B68D for max@bigatti.it; Sat, 20 Dec 2003 13:05:44 +0100
Message-ID: <15820815.1071922119368.JavaMail.max@Computer-di-Massimiliano-Bigatti.local>
Date: Sat, 20 Dec 2003 13:08:38 +0100 (CET)
From: spammer_99@null.it
To: max@bigatti.it
Subject: Messaggio di prova
Mime-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit


La particolarità delle intestazioni di posta è che queste sono espandibili con chiavi aggiuntive. Ad esempio, il sistema contro la posta spazzatura Spam Assassin aggiunge una serie di intestazioni al messaggio al fine di indicare il server che ha eseguito la verifica, la sua versione, le anomalie riscontrate sul messaggio e così via. Spam Assassin è un sistema antispam che analizza un messaggio al fine di trovare elementi indicativi del fatto che è indesiderato, come ad esempio la presenza di testo sospetto o la mancanza di dati essenziali, come il nome del mittente. Un esempio di queste intestazioni è il seguente:

X-Spam-Rating: mxavas1.aruba.it 1.6.2 0/1000/N
X-Spam-Checker-Version: SpamAssassin 2.60 (1.212-2003-09-23-exp) on
mxavas1.aruba.it
X-Spam-Level: *
X-Spam-Status: No, hits=1.2 required=5.0 tests=FROM_ENDS_IN_NUMS,NO_REAL_NAME
autolearn=no version=2.60


I metodi principali per operare con le intestazioni sono definiti all'interno dell'interfaccia javax.mail.Part e sono:

  • addHeader(String,String). Si aspetta il nome dell'header ed il valore ed aggiunge quest'ultimo ai valori esistenti per il nome indicato;
  • Enumeration getAllHeaders(). Ritorna una enumerazione che elenca tutti i valori delle intestazioni presenti nella parte;
  • String[] getHeader(String). Ritorna un array contentente i valori dell'intestazione passata come parametro;
  • Enumeration getMatchingHeaders(String[]). Ritorna una enumerazione che elenca tutte i valori delle intestazioni relative ai nomi passati come parametro, sotto forma di array di stringhe;
  • Enumeration getNonMatchingHeaders(String[]). Come la precedente, ma ritorna tutte le intestazioni le cui chiavi non sono presenti nell'array passato come parametro;
  • removeHeader(String). Rimuove tutte le intestazioni associate al nome indicato.
  • setHeader(String,String). Imposta il valore di una intestazione, cancellando il valore di tutte le intestazioni con il nome indicato se presenti.

Tutti questi metodi sollevano una MessagingException nel caso di generici problemi di accesso al messaggio; i metodi dispositivi, come setHeader(), sollevano anche una IllegalWriteException nel caso la parte sia stata ottenuta da un Folder aperto in sola lettura.
La sottointerfaccia MimePart, che definisce una parte di un messaggio multiparte secondo lo standard RFC822 e quello MIME RFC2045 (la posta Internet), definisce anche i seguenti metodi:
  • addHeaderLine(String). Aggiunge una linea di intestazione secondo lo standard RFC822;
  • Enumeration getAllHeaderLines(). Ritorna una enumerazione di tutte le linee di intestazione in formato grezzo RFC822, composte cioè dalla forma "chiave: valore";
  • String getHeader(String,String). Ritorna tutti i valori dell'intestazione indicata, separati dal delimitatore passato come secondo parametro;
  • Enumeration getMatchingHeaders(String[]). Ritorna una enumerazione che elenca tutte i valori delle intestazioni relative ai nomi passati come parametro, sotto forma di array di stringhe grezze RFC822;
  • Enumeration getNonMatchingHeaders(String[]). Come la precedente, ma ritorna tutte le intestazioni le cui chiavi non sono presenti nell'array passato come parametro grezze RFC822;

Sostanzialmente, accedendo alle intestazioni tramite i metodi dell'interfaccia Part, si ottiene un accesso astratto dal particolare protocollo, dove il concetto di intestazione è limitato ad una chiave a cui sono associati zero o più valori; nell'interfaccia MimePart, questo concetto viene contestualizzato al protocollo RFC822 che definisce gli standard di comunicazione dei messaggi di posta testuali su Internet.

E' una questione di priorità
Un esempio di header spesso utilizzata è quella relativa alla priorità: X-Priority. Quando impostata ad 1 rappresenta un'alta priorità, 2 è media e 3 è bassa. Per default, quando l'intestazione non è presente, il messaggio viene considerato solitamente di media prorità. I client Microsoft come Outlook aggiungono anche le intestazioni Priority ed Importance, che riportano informazioni descrittive.

Messaggi e Flag
Ciascun messaggio può disporre di un insieme di flag che indicano lo stato del messaggio. I flag di sistema sono rappresentati dalla classe interna Flags.Flag, mentre quelli definiti dall'utente sono implementati come semplici stringhe.
La classe Flag definisce le seguenti costanti:
  • ANSWERED. Il messaggio ha avuto una risposta;
  • DELETED. Il messaggio è stato cancellato;
  • DRAFT. Il messaggio è una bozza;
  • FLAGGED. Il messaggio è stato marcato;
  • RECENT. Il messaggio è recente;
  • SEEN. Il messaggio è stato letto;
  • USER. Flag speciale che indica che il Folder supporta flag personalizzati.

Per operare con i flag di un messaggio (si noti che un singolo oggetto Flags contiene tutti i flag impostati di uno specifico messaggio) sono disponibili i seguenti metodi della classe Message:
  • Flags getFlags(): ritorna i flag di questo messaggio;
  • boolean isSet(Flags.Flag): indica se un particolare flag è impostato;
  • setFlag(Flags.Flag,boolean): imposta il valore di uno specifico flag di sistema;
  • setFlags(Flags,boolean): imposta i flag indicati al valore passato come parametro.

Questi metodi sollevano una generica MessagingException in caso di problemi ad ottenere le informazioni richieste, oppure un IllegalWriteException se il messaggio proviene da un Folder aperto in sola lettura.
Per cancellare un messaggio si può dunque procedere come segue:
message.setFlag(Flags.Flag.DELETED, true);

si noti che questa operazione di cancellazione non produce alcun effetto immediato. Per rimuovere definitivamente il messaggio dal Folder che lo contiene, è necessario chiamare il metodo expunge():
Message[] deleted = folder.expunge();

Il metodo ritorna l'array dei messaggi eliminati, con il loro numero univoco progressivo (ottenuto con getMessageNumber()), mentre i messaggi rimasti nella cartella sono soggetti a rinumerazione. Si noti che sul messaggio cancellato definitivamente sono attivi i soli metodi isExpunged() - che ritorna true - e getMessageNumber(): gli altri dovrebbero sollevare una MessageRemovedException.
In alternativa ad expunge() è possibile scrivere:
folder.close(true);

in questo caso, alla chiusura della cartella segue un'operazione di cancellazione definitiva. Se non si desidera rimuovere permanentemente i messaggi marcati per la cancellazione, si può richiamare lo stesso metodo con parametro false:
folder.close(false);

Ricerca di messaggi
Quando una cassetta postale o una cartella cominciano a contenere un numero considerevole di messaggi, diventa necessaria una funzione di ricerca che consenta di individuare con facilità gli elementi desiderati.
Le API Javamail supportano la ricerca dei messaggi tramite due metodi della classe Folder:
  • Message[] search(SearchTerm). Ricerca i messaggi all'interno di tutta la cartella;
  • Message[] search(SearchTerm, Message[]). Ricerca i messaggi all'interno dell'elenco dei messaggi indicato; i messaggi devono appartenere alla cartella su cui viene chiamato il metodo.


Entrambi i metodi sollevano una MessagingException in caso di problemi o una IllegalStateException se la cartella non è aperta. I due metodi si aspettano un oggetto SearchTerm, che definisce le chiavi di ricerca da applicare; se questi sono troppo complessi, viene sollevata una SearchException.
La classe SearchTerm funge da superclasse astratta comune alle diverse tipologie di ricerca possibili (circa una ventina), che sono divise per tipo di condizione logica e per parte del messaggio da confrontare. I diversi termini di ricerca possono essere infatti uniti con operatori AND, OR e NOT (classi AndTerm, OrTerm e NotTerm), mentre le parti del messaggio da ricercare possono essere la data di invio, il contenuto e le intestazioni (classi SentDateTerm, BodyTerm, FromTerm, RecipientTerm, SubjectTerm ed altre). L'elenco completo dei termini di ricerca è presente in tabella 1 e riassunto in figura 1.

Tabella 1 - Classi Javamail per la ricerca dei messaggi
AddressStringTerm: classe astratta che implementa un confronto degli indirizzi di un messaggio a livello di stringa
AddressTerm: implementa il confronto tra indirizzi
AndTerm: realizza un AND logico su termini di ricerca singoli
BodyTerm: implementa una ricerca sul corpo del messaggio
ComparisonTerm: rappresenta un operatore di confronto
DateTerm: implementa confronti per date
FlagTerm: implementa un confronto di flag
FromStringTerm: implementa un confronto a livello di stringa del mittente del messaggio
FromTerm: realizza un confronto del mittente del messaggio
HeaderTerm: implementa confronti per le intestazioni dei messaggi
IntegerComparisonTerm: implementa confronti tra numeri interi
MessageIDTerm: definisce un identificativo univoco secondo specifiche RFC822
MessageNumberTerm: implementa un confronto per numero di messaggio
NotTerm: rappresenta un NOT booleano
OrTerm: implementa un OR logico su due termini singoli
ReceivedDateTerm: confronto tra date di ricezione del messaggio
RecipientStringTerm: implementa un confronto a livello di stringa del destinatario
RecipientTerm: implementa un confronto del destinatario
SearchTerm: superclasse generica di ricerca. Il criterio di ricerca è espresso come albero di termini di ricerca
SentDateTerm: implementa un confronto per le date di invio.
SizeTerm: realizza un confronto sulla dimensione del messaggio.
StringTerm: classe generica per il confronto di stringhe.
SubjectTerm: implementa un confronto con l'oggetto del messaggio.


Figura 1
- Gerarchia delle classi di ricerca. In giallo sono evidenziate le classi che implementano operatori booleani mentre in azzurro sono evidenziati le classi finali che possono essere utilizzate per le ricerche. Quelle in bianco sono invece classi astratte infrastrutturali di supporto.


Esempi di ricerca
Ad esempio, per individuare i messaggi di dimensione superiore ai 200K, eventualmente per richiedere all'utente una conferma al loro scaricamento, si può scrivere:

SearchTerm grandiMessaggi =
new SizeTerm( SizeTerm.GE, 200 * 1024 );
Message[] elenco = folder.search( grandiMessaggi );

Il costruttore di SizeTerm si aspetta come primo parametro il termine di confronto (definite nella classe ComparisionTerm), che può assumere i valori:

  • EQ - (equal) equivale a;
  • GE - (greater/equals than) maggiore o uguale a;
  • GT - (greater than) maggiore di;
  • LE - (lesser/equals than) minore o uguale a;
  • LT - (lesser than) minore di;
  • NE - (not equal) non equivalente a;


E' anche possibile individuare i messaggi in funzione dello stato dei loro flag. Ad esempio, per individuare i messaggi che devono essere ancora letti si può scrivere:
SearchTerm nonLetti =
new FlagTerm( Flags.Flag.SEEN, false );
Message[] elenco = folder.search( nonLetti );

Tramite gli operatori booleani è anche possibile concatenare più termini per eseguire ricerche complesse. Ad esempio, per cercare tutte le newsletter inviate da Mokabyte, con oggetto "Mokabyte Newsletter" e con mittente la redazione si potrebbe scrivere:

SearchTerm st = new AndTerm(new SubjectTerm("Mokabyte Newsletter"),
                            new FromStringTerm("redazione@mokabyte.com"));
Message[] msgs = folder.search(st);

Un esempio completo
Il programma presentato nel listato 1 implementa una semplice ricerca dei messaggi ricevuti nella giornata odierna. La ricerca viene eseguita tramite la classe ReceivedDateTerm; l'elenco dei messaggi ottenuti viene stampato a video, presentando l'oggetto e la data di ricezione.

Listato 1 - Ricerca.java
package com.mokabyte.mokabook2.javamail;

import java.util.*;

import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.search.*;

public class Ricerca {

public static void main(String args[]) {

  try {
    Properties props = System.getProperties();
    //props.put("mail.debug","true");
    Session session = Session.getDefaultInstance(props, null);

    Store store = session.getStore("pop3");
    store.connect(args[0], args[1], args[2]);

    Folder folder = store.getDefaultFolder();
    if (folder != null) {

      folder = folder.getFolder("INBOX");
      if (folder != null) {
        folder.open(Folder.READ_ONLY);

        System.out.println("Ricerca messaggi di oggi (totale="
  
                          + folder.getMessageCount() + ")" );

        Calendar oggi = Calendar.getInstance();
        oggi.add( Calendar.DATE, -1 );

        SearchTerm st = new SentDateTerm(SentDateTerm.GE,oggi.getTime());
        //SearchTerm st = new FromStringTerm( "giuliani" );

        Message[] elencoMessaggi = folder.search( st );
        if( elencoMessaggi != null && elencoMessaggi.length != 0 ) {
          for (int indice = 0; indice < elencoMessaggi.length; indice++) {
            Message messaggio = elencoMessaggi[ indice ];
            System.out.println( " OGGETTO: " + messaggio.getSubject() +
                                " DATA: " + messaggio.getSentDate());
          }
        } else {
          System.out.println( "Nessun messaggio trovato" );
        }
        folder.close(false);
        } else {
          System.out.println( "Folder non trovato" );
        }
        } else {
          System.out.println( "Folder di default non trovato" );
        }
        store.close();
      }
      catch (Exception ex) {
        ex.printStackTrace();
      }
    }
  }

Conclusioni
Con questo articolo si conclude questa breve serie di articoli su Javamail che, come abbiamo visto, è molto completa e potente; il framework generale supporta un generico sistema di posta, che può essere esteso ed adattato anche a protocolli diversi da quelli utilizzati su Internet. In aggiunta sono presenti tutti gli elementi necessari a supportare i protocolli della rete, tra cui POP3, IMAP ed SMTP. Sono interessanti anche i prodotti di terze parti che stanno nascendo su queste API, che SUN elenca alla pagina http://java.sun.com/products/javamail/Third_Party.html.

Bibliografia
RFC822 - http://www.w3.org/Protocols/rfc822/Overview.html

 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it