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
|