MokaByte 47 - Dicembre 2000
Foto dell'autore non disponibile
di
Stefano Bussolon
SendMail.java
Una classe per l’invio di messaggi di posta elettronica
Nelle scorse due puntate abbiamo descritto Cartolina.java, una servlet capace di gestire il servizio di e-cards, le ormai famose cartoline virtuali. In quei numeri ci eravamo concentrati sulla classe servlet, dando per scontato che una classe esterna, SendMail.java, si assumesse l’onere di generare automaticamente un messaggio di posta elettronica al destinatario. Oggetto di questo articolo è proprio quella classe.

Nei numeri scorsi per spiegare la modalità di funzionamento delle cartoline elettroniche usammo la metafora della cartolina postale: l’utente sceglie l’immagine, il destinatario, scrive un messaggio e la firma. Ma mentre le cartoline postali vengono fisicamente portate al destinatario le e-cards vengono salvate su di un server. Affinché il destinatario le possa leggere è necessario che gli sia inviato un messaggio di notifica che lo avvisi che qualcuno gli ha mandato la cartolina, specificando l’indirizzo a cui si deve collegare per vederla. Naturalmente il mezzo più indicato per la notifica è dato dalla posta elettronica, ed un servizio di e-card dev’essere capace di inviare automaticamente un messaggio personalizzato al destinatario.
La posta elettronica è lo strumento più usato da chi si collega alla rete. Ognuno sa come spedire un messaggio, ma pochi sanno cosa succede dietro le quinte, in base a quali complessi meccanismi i nostri client di posta preferiti riescono a scrivere e leggere i messaggi. Le procedure necessarie per dialogare con i server di posta elettronica non sono banali ma nemmeno eccessivamente complessi, grazie all’efficacia dei protocolli utilizzati. I client di posta elettronica sono capaci sia di inviare che di leggere messaggi, e l’utente “ingenuo” può pensare che le due funzioni siano affidate ad uno stesso meccanismo. In realtà l’invio e la ricezione di messaggi avviene in base a protocolli diversi - SMTP e POP, rispettivamente - spesso utilizzando server differenti. Poiché il nostro fine è quello di inviare messaggi, ci focalizzeremo sul protocollo SMTP. Comprendere come questo protocollo funziona sarà lo scopo principale dell’articolo: una volta intuito il suo funzionamento analizzeremo le funzioni della classe SendMail.java, che sostanzialmente rispecchiano il protocollo di comunicazione descritto.
 
 
 

Il protocollo SMTP
SMTP è un protocollo di comunicazione fra due processi: un “sender-SMTP” e un “receiver-SMTP”. Il processo sender viene generalmente gestito da un client di posta elettronica che, su richiesta dell’utente, stabilisce una connessione con un receiver. Attraverso il protocollo SMTP il sender comunica l’indirizzo del mittente e la lista di indirizzi dei destinatari. Se il receiver risponde positivamente, il sender inizia ad inviare i dati veri e propri relativi alla mail. Infine fra i due processi intercorre un codice di saluto e la connessione viene interrotta.
Il receiver può essere il nodo finale del processo, oppure semplicemente un intermediario. È il nodo finale se la casella di mail del destinatario del messaggio è ospitata sulla propria macchina. In caso contrario il receiver stabilirà una connessione con un’altro server, il quale a sua volta farà altrettanto fino ad arrivare al server che ospita la casella del destinatario. In questo processo a catena, denominato relay, si stabiliscono nuovamente delle connessioni attraverso il protocollo SMTP. Il primo server, che nella prima comunicazione assumeva il ruolo di receiver-SMTP, fungerà ora da sender-SMTP, mentre il ruolo di receiver sarà svolto dal secondo server.
 
 
 

La sintassi del protocollo SMTP
In questo articolo ci focalizziamo soltanto sugli aspetti del protocollo che risultano essenziali al nostro scopo, che è quello di generare automaticamente una mail di notifica. Useremo pertanto soltanto i comandi più importanti del protocollo, rimandando al testo originale dell’rfc il lettore interessato ad approfondire l’argomento.
L’invio del messaggio può essere scomposto in quattro parti:

  • il sender stabilisce una connessione con il receiver;
  • il sender dichiara il mittente e la lista di destinatari del messaggio;
  • il sender invia, attraverso la connessione, il messaggio vero e proprio;
  • il sender si disconnette.
Come possiamo vedere l’iniziativa del dialogo è sempre gestita dal sender, mentre il receiver si limita a porsi in ascolto ed a fornire una risposta (di conferma o di errore) ad ogni comando del sender. La risposta viene codificata attraverso un numero ed una stringa di spiegazione; attraverso il numero il sender può capire se la transazione sta proseguendo regolarmente o se è stato rilevato un errore.
Il sender stabilisce una connessione con il proprio server SMTP attraverso una porta TCP/IP (la porta standard per SMTP è la 25). Su quella porta vi è un demone SMTP in ascolto, che accetta la connessione inviando (se tutto va bene) il messaggio 220, denominato “service ready” (servizio pronto e in ascolto). Il sender “saluta” il receiver attraverso il comando “HELO nomedelserver” ed il receiver risponde con un messaggio 250 (messaggio di conferma: “OK reply”).

Il sender identifica il mittente della mail attraverso il comando “MAIL FROM: maildelmittente”; il receiver risponde con il messaggio 250 (salvo problemi); il sender invia il comando “RCPT TO: maildeldestinatario” per ognuno dei destinatari del messaggio, ed ogni volta il receiver invia un messaggio di risposta. Se il destinatario è locale (ovvero la sua casella è ospitata sul server del receiver) la risposta sarà un messaggio “250 ok”. Se il destinatario non è locale il receiver si assume la responsabilità di fare un “forward” del messaggio attraverso il meccanismo descritto all’inizio; in questo caso il messaggio di ritorno al sender potrebbe essere “251 User not local; will forward to maildeldestinatario”. Se, infine, l’indirizzo del destinatario risulta inesistente, verrà inviato il messaggio “550 No such user here”.

Dichiarato l’indirizzo di tutti i destinatari, il sender comunica che inizia ad inviare i dati, attraverso il comando “DATA”. La risposta del receiver sarà “354 Start mail input; end with <CRLF>.<CRLF>”. La risposta ricorda al sender una regola molto importante: da questo momento in poi ogni dato inviato dal sender verrà interpretato dal receiver non come un comando ma come corpo del messaggio. Pertanto risulta importante codificare un codice di fine messaggio. Il codice è rappresentato da una linea contenente esclusivamente un punto: la sintassi <CRLF>.<CRLF> sta ad indicare che la sequenza “a capo - punto - a capo” verrà interpretata come fine del messaggio.
Il sender invia il corpo della mail, che terminerà con il codice stabilito. Il corpo conterrà inoltre tutte le etichette (rfc 821 le definisce “memo header items”) che appariranno a contorno del messaggio: Date, Subject, To, Cc, From.

Ricevuta la linea contenente un solo punto, il receiver risponderà con il codice “250 OK”. A questo punto il sender può inviare il comando di uscita “QUIT”; il receiver risponde con il messaggio di chiusura 221 e la connessione si interrompe.

Riassumiamo la procedura di invio del messaggio attraverso un esempio.

R: 220 BBN-UNIX.ARPA Simple Mail Transfer Service Ready
S: HELO mail.tin.it
R: 250 fep02-svc.tin.it
S: MAIL FROM:<bussolon@psy.unipd.it>
R: 250 Sender <bussolon@psy.unipd.it> OK
S: RCPT TO:<info@mokabyte.it>
R: 250 Recipient <info@mokabyte.it> OK
S: DATA
R: 354 OK Send data ending with <CRLF>.<CRLF>
S: From: bussolon@psy.unipd.it
S: To: info@mokabyte.it
S: Subject: prova
S: prova di trasmissione.
S: Sto provando il protocollo SMTP
S: .
R: 250 Message received
S: QUIT
R: 221 fep02-svc.tin.it ESMTP server closing connection 

Per distinguere i messaggi inviati dal sender al receiver dalle risposte di quest’ultimo abbiamo anteposto una S ai messaggi del sender ed una R a quelli del receiver.
L’esempio riassume tutto quello che era stato descritto in precedenza e lascia intuire il formato utilizzato per definire i “memo header items”. Come possiamo notare, le prime righe che il sender invia subito dopo il comando “DATA” sono delle parole chiave, codificate nella forma “chiave: valore”. Il campo “From:” non è, in realtà, necessario: il receiver lo potrebbe ricostruire attraverso l’argomento che il sender ha passato con il comando “MAIL FROM”. Lo stesso non si può dire per il campo “To:”: tecnicamente il receiver potrebbe ricostruire la lista di destinatari attraverso i comandi “RCPT TO”. In realtà questo non avviene per permettere, ad esempio, i destinatari in “Ccn:” (copia carbone nascosta) o per salvaguardare, in questo modo, la privacy di ognuno dei destinatari. Pertanto sarà compito del sender specificare, attraverso i campi “To:” e “Cc:”, la lista dei destinatari “in chiaro”; in caso contrario chi riceverà la mail non potrà identificare i destinatari. Lo stesso vale per il campo Subject, che dev’essere specificato all’interno del messaggio.
 
 
 

La classe SendMail.java
Una volta compreso il meccanismo sottostante l’invio di un messaggio di posta elettronica potremmo iniziare a costruire una classe capace di assumere il ruolo di SMTP-sender. Come sovente succede la filosofia open source ci viene incontro: quando decisi di realizzare la servlet cercai in rete una classe per l’invio di e-mail bella e pronta, ed ebbi la fortuna di trovare un esempio, perfettamente funzionante, scritto da Thornton Rose per Gamelan. In realtà l’autore aveva pubblicato una servlet per l’invio di messaggi di posta, ed il mio personale adattamento ha cambiato in modo molto marginale la classe: ho corretto alcune piccole imprecisioni ma soprattutto ho trasformato la servlet in una classe normale.
La classe si avvale di una funzione principale e due funzioni di servizio. Inoltre dichiara alcune costanti numeriche: la porta SMTP, che per default è la 25, e due codici di errore che verranno utilizzati per analizzare le risposte del receiver.

private static final int  SMTP_PORT = 25;
private static final char SMTP_ERROR_CODE1 = '4';
private static final char SMTP_ERROR_CODE2 = '5';
 

La funzione principale
 protected void sendMail(String host,String domain,String sender,String recipients,String subject,String maildata,Vector sessionTrace) throws IOException {
// la funzione chiede alcuni parametri:
// l’indirizzo ip dell’host ed il suo nome di dominio,
// che generalmente coincidono.
// il mittente (sender), i destinatari, l’oggetto della mail,
// il corpo della mail.
// sessionTrace è un vettore sul quale le funzioni ausiliarie
// memorizzeranno tutte le transizioni.

// il Socket che collega sender a receiver
Socket mailSocket;
// gli stream di input e output verso il receiver
  BufferedReader socketIn;
  DataOutputStream socketOut;
  String address;
  StringTokenizer tokenizer;
// apre la connessione verso il server SMTP e collega gli stream
  mailSocket = new Socket(host, SMTP_PORT); 
  socketIn = new BufferedReader(new InputStreamReader(mailSocket.getInputStream()) ); 
  socketOut = new DataOutputStream(mailSocket.getOutputStream()); 
// riceve la prima risposta dal server 
  readReply(socketIn, sessionTrace); 
// il saluto al server, e relativa risposta
  sendCommand(socketOut, "HELO " + domain, sessionTrace); 
  readReply(socketIn, sessionTrace); 
// invia l’indirizzo del mittente
  sendCommand(socketOut, "MAIL FROM: " + sender, sessionTrace); 
  readReply(socketIn, sessionTrace); 
  // invia l’elenco dei destinatari, che 
  //nell’argomento erano passati
  // sotto forma di un’unica stringa, separati dalla virgola
  // per ogniuno dei destinatari invia il comando RCPT TO
  // e legge la risposta del server
  tokenizer = new StringTokenizer(recipients, ","); 
  while (tokenizer.hasMoreElements()) { 
   sendCommand(socketOut, "RCPT TO: " + tokenizer.nextToken(), sessionTrace); 
   readReply(socketIn, sessionTrace);
  } 
  // invia il comando “DATA”. Da ora il receiver 
  // interpreta gli input
  // come corpo del testo e si attende la sequenza <CRLF>.<CRLF>
  // come conclusione del corpo
  sendCommand(socketOut, "DATA", sessionTrace); 
  readReply(socketIn, sessionTrace); 

  // crea la stringa body e vi scrive i “memo header items”
  String body = "";
  body += "From: " + sender + "\n";
  body += "To: " + recipients + "\n";
  body += "Subject: " + subject + "\n";

  // aggiunge il corpo vero e proprio della mail
  body += maildata;

  // invia il corpo della mail e la sequenza di chiusura
  sendCommand(socketOut, body, sessionTrace); 
  sendCommand(socketOut, ".", sessionTrace); 
  readReply(socketIn, sessionTrace); 

  // conclude la sessione
  sendCommand(socketOut, "QUIT", sessionTrace); 
  readReply(socketIn, sessionTrace); 
 }
 
 

Le funzioni di servizio
Le funzioni di servizio hanno lo scopo di inviare i comandi al receiver e di leggere le risposte (e generare una eccezione in caso di codice di errore).

 private void sendCommand(DataOutputStream out, String command, Vector sessionTrace) 
 throws IOException { 
  out.writeBytes(command + "\r\n"); 
  sessionTrace.addElement(command); 
  System.out.println(command); 
 } 

sendCommand è la prima delle due funzioni; come il nome lascia intendere, invia un comando attraverso uno stream di output e tiene traccia della stringa su di un vettore.

 private void readReply(BufferedReader reader, Vector sessionTrace) 
 throws IOException { 
  String reply; 
  char   statusCode; 
  reply = reader.readLine(); 
  statusCode = reply.charAt(0); 
  sessionTrace.addElement(reply); 
  System.out.println(reply); 
  if ( (statusCode == SMTP_ERROR_CODE1) | (statusCode == SMTP_ERROR_CODE2) )
   { throw (new IOException("SMTP: " + reply));}
 }

readReply legge la risposta del receiver. Abbiamo visto come le risposte siano codificate nella forma di un numero ed una descrizione. Il codice numerico è formato da tre cifre, la prima delle quali è la più significativa: se il server risponde con un codice 4xx o 5xx significa che è occorso un errore. Questo giustifica la dichiarazione, vista all’inizio, delle due costanti numeriche. La funzione legge la risposta e controlla il primo carattere ricevuto; se il carattere è pari alle cifre 4 o 5 (rispettivamente dichiarate ERROR_CODE1 E ERROR_CODE2) genera un’eccezione. In entrambe le funzioni vi è una riga attraverso cui le stringhe vengono stampate sullo standard output. Questo comportamento, utile in fase di debug, può venire in seguito disabilitato commentando le rispettive linee di codice.
 
 
 

Conclusioni
Il successo della posta elettronica è ragionevolmente da attribuirsi alla sua semplicità ed efficacia. Il protocollo SMTP è uno degli artefici di questa efficace semplicità: attraverso pochi comandi chiari è possibile inviare un messaggio.
La classe descritta risulta estremamente versatile: ogni qualvolta in una applicazione o in una servlet si riscontri la necessità di inviare un messaggio di posta elettronica è sufficiente istanziarne un oggetto ed utilizzarne la funzione descritta.
 
 
 

Link utili
La fonte “originale” della classe SendMail.java:
http://dev-gamelan.earthweb.com/journal/techworkshop/061198_servletmail.html
Il protocollo SMTP: rfc 821:
http://www.rfc.org
La mia versione della classe:
http://www.hyperlab.net/java/source/SendMail.java
 

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


MokaByte®  è un marchio registrato da MokaByte s.r.l.
Java® è un marchio registrato da Sun Microsystems; tutti i diritti riservati
E' vietata la riproduzione anche parziale
Per comunicazioni inviare una mail a
mokainfo@mokabyte.it