In this lab you will implement a mail user agent that sends mail to
remote hosts. Your task is to program the SMTP interaction between the
MUA and the remote SMTP server. The client provides a graphical user
interface containing fields for entering the sender and recipient
addresses, the subject of the message and the message itself. Here's
what the user interface looks like:
With this interface, when you want to send a mail, you must fill in
complete addresses for both the sender and the recipient, i.e.,
user@someschool.edu , not just simply
user . You can send mail to only one
recipient. Furthermore, the domain part of the recipient's address
must be the name of the SMTP server handling incoming mail at
the recipient's site. For example, if you are sending mail to address
user@someschool.edu and the SMTP server of
someschool.edu is smtp.somechool.edu, you will have
to use the address user@smtp.someschool.edu in the
To-field. This is because Java doesn't support DNS lookups except for
simple name-to-address queries. See Querying the
DNS below for more information on how to obtain the address of the
SMTP-server.
When you have finished composing your mail, press Send to
send it.
The Code
The program consists of four classes:
MailClient | The user interface |
Message | Mail message |
Envelope | SMTP envelope around the Message |
SMTPConnection | Connection to the SMTP server |
You will need to complete the code in the
SMTPConnection class so that in the end you will have a
program that is capable of sending mail to any recipient. The code for
the SMTPConnection class is at the end of this page. The code for the other
three classes is provided in ...
The places where you need to complete the code have been marked
with the comments /* Fill in */ . Each of the places
requires one or more lines of code.
The MailClient class provides the user interface and
calls the other classes as needed. When you press Send, the
MailClient class constructs a Message class
object to hold the mail message. The Message object holds
the actual message headers and body. Then the MailClient
object builds the SMTP envelope using the Envelope
class. This class holds the SMTP sender and recipient information, the
SMTP server of the recipient's domain, and the Message
object. Then the MailClient object creates the
SMTPConnection object which opens a connection to the
SMTP server and the MailClient object sends the message
over the connection. The sending of the mail happens in three phases:
- The
MailClient object creates the
SMTPConnection object and opens the connection to
the SMTP server.
- The
MailClient object sends the message using the
function SMTPConnection.send() .
- The
MailClient object closes the SMTP connection.
The Message class contains the function
isValid() which is used to check the addresses of the
sender and recipient to make sure that there is only one address and
that the address contains the @-sign. The provided code does not do
any other error checking.
Reply Codes
For the basic interaction of sending one message, you will only
need to implement a part of SMTP. Section 2.4 provides a more complete description of SMTP, but
in this lab you need only to implement the commands in the following
table.
Command | Reply Code |
DATA | 354 |
HELO | 250 |
MAIL FROM | 250 |
QUIT | 221 |
RCPT TO | 250 |
The above table also lists the accepted reply codes for each of the
SMTP commands you need to implement. For simplicity, you can assume
that any other reply from the server indicates a fatal error and abort
the sending of the message. In reality, SMTP distinguishes between
transient (reply codes 4xx) and permanent (reply codes 5xx) errors,
and the sender is allowed to repeat commands that yielded in a
transient error. See Appendix E of RFC 821 for more details.
In addition, when you open a connection to the server, it will
reply with the code 220.
Note: RFC 821 allows the code 251 as a response to a RCPT
TO-command to indicate that the recipient is not a local user. You may
want to verify manually with the telnet command what your
local SMTP server replies.
Hints
Most of the code you will need to fill in is similar to the code
you wrote in the WebServer lab. You may want to use the code you have
written there to help you.
To make it easier to debug your program, do not, at first, include
the code that opens the socket, but use the following definitions for
fromServer and toServer . This way, your
program sends the commands to the terminal. Acting as the SMTP server,
you will need to give the correct reply codes. When your program
works, add the code to open the socket to the server.
fromServer = new BufferedReader(new
InputStreamReader(System.in));
toServer = System.out;
The lines for opening and closing the socket, i.e., the lines
connection = ... in the constructor and the line
connection.close() in function close() , have
been commented out by default.
Start by completing the function parseReply() . You
will need this function in many places. In the function
parseReply() , you should use the
StringTokenizer -class for parsing the reply strings. You
can convert a string to an integer as follows:
int i = Integer.parseInt(argv[0]);
In the function sendCommand() , you should use the
function writeBytes() to write the commands to the
server. The advantage of using writeBytes() instead of
write() is that the former automatically converts the
strings to bytes which is what the server expects. Do not forget to
terminate each command with the string CRLF.
You can throw exceptions like this:
throw new Exception();
You do not need to worry about details, since the exceptions in
this lab are only used to signal an error, not to give detailed
information about what went wrong.
Optional Exercises
You may want to try the following optional exercises to make your
program more sophisticated.. For these exercises, you will need to
modify also the other classes (MailClient, Message, and Envelope).
- Verify sender address. Java's System-class contains
information about the username and the InetAddress-class
contains methods for finding the name of the local host. Use
these to construct the sender address for the Envelope instead
of using the user-supplied value in the From-header.
- Additional headers. The generated mails have only four
header fields, From, To, Subject, and Date. Add other header
fields from RFC 822, e.g., Message-ID, Keywords. Check RFC 822
for the definitions of the different fields.
- Multiple recipients. Currently the program only allows
sending mail to a single recipient. Modify the user interface to
include a Cc-field and modify the program to send mail to both
recipients. For a more challenging exercise, modify the program
to send mail to an arbitrary number of recipients.
- More error checking. The provided code assumes that all
errors that occur during the SMTP connection are fatal. Add code
to distinguish between fatal and non-fatal errors and add a
mechanism for signaling them to the user. Check the RFC to see
what the different reply codes mean. This exercise may require
large modifications to the
send() ,
sendCommand() , and parseReply()
functions.
The Domain Name System (DNS) stores information in resource
records. Normal name to IP-address mappings are stored in type A
(Address) resource records. Type NS (NameServer) records hold
information about nameservers and type MX (Mail eXchange) records tell
which server is handling the mail delivery of the domain.
The server you need to find is the server handling the mail for the
domain to which you are sending mail, i.e., the MX-host of that
domain. First, you must find the nameserver of the target domain and
then query this nameserver for the MX-host. Assuming you were sending
mail to the address user@someschool.edu you would do the
following:
- Find the address of a nameserver for the top-level
domain
.edu (NS query)
- Query the nameserver for .edu about the nameserver for the
domain
someschool.edu to get the address of
Someschool's nameserver. (NS query)
- Query Someschool's nameserver for MX-records for the domain
someschool.edu . (MX query)
Ask your local system administrator how to perform DNS queries
manually.
Under Unix you can query DNS manually with the
nslookup -command. The syntax of the
nslookup -command is as follows. Note that the argument
host can also be a domain.
Normal query | nslookup host |
Normal query using a given server | nslookup host
server |
NS-query | nslookup -type=NS host |
MX-query | nslookup -type=MX host |
For the first step, finding the nameserver of the top-level domain,
you will need to send your query to one of the 13 DNS root
nameservers. You can find more information about the DNS root servers
in Section 2.5 . The root servers are listed in the
file root-servers.txt,
available from Internic.
The reply to the MX-query may contain multiple mail
exchangers. Each of them is preceded by a number which is the
preference value for this server. Lower preference values indicate
preferred servers so you should use the server with the lowest
preference value.
This is the code for the SMTPConncetion class that you will need to
complete. The complete code for the other three classes is given below.
import java.net.*;
import java.io.*;
import java.util.*;
/**
* Open an SMTP connection to a remote machine and send one mail.
*
*/
public class SMTPConnection {
/* The socket to the server */
private Socket connection;
/* Streams for reading and writing the socket */
private BufferedReader fromServer;
private DataOutputStream toServer;
private static final int SMTP_PORT = 25;
private static final String CRLF = "\r\n";
/* Are we connected? Used in close() to determine what to do. */
private boolean isConnected = false;
/* Create an SMTPConnection object. Create the socket and the
associated streams. Initialize SMTP connection. */
public SMTPConnection(Envelope envelope) throws IOException {
// connection = /* Fill in */;
fromServer = /* Fill in */;
toServer = /* Fill in */;
/* Fill in */
/* Read a line from server and check that the reply code is
220.
If not, throw an IOException. */
/* Fill in */
/* SMTP handshake. We need the name of the local machine.
Send the appropriate SMTP handshake command. */
String localhost = /* Fill in */;
sendCommand( /* Fill in */ );
isConnected = true;
}
/* Send the message. Write the correct SMTP-commands in the
correct order. No checking for errors, just throw them to the
caller. */
public void send(Envelope envelope) throws IOException {
/* Fill in */
/* Send all the necessary commands to send a message. Call
sendCommand() to do the dirty work. Do _not_ catch the
exception thrown from sendCommand(). */
/* Fill in */
}
/* Close the connection. First, terminate on SMTP level, then
close the socket. */
public void close() {
isConnected = false;
try {
sendCommand( /* Fill in */ );
// connection.close();
} catch (IOException e) {
System.out.println("Unable to close connection: " + e);
isConnected = true;
}
}
/* Send an SMTP command to the server. Check that the reply code
is what is is supposed to be according to RFC 821. */
private void sendCommand(String command, int rc) throws
IOException
{
/* Fill in */
/* Write command to server and read reply from server. */
/* Fill in */
/* Fill in */
/* Check that the server's reply code is the same as the
parameter rc. If not, throw an IOException. */
/* Fill in */
}
/* Parse the reply line from the server. Returns the reply
code. */
private int parseReply(String reply) {
/* Fill in */
}
/* Destructor. Closes the connection if something bad happens. */
protected void finalize() throws Throwable {
if(isConnected) {
close();
}
super.finalize();
}
}
import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
/**
* A simple mail client with a GUI for sending mail.
*/
public class MailClient extends Frame {
/* The stuff for the GUI. */
private Button btSend = new Button("Send");
private Button btClear = new Button("Clear");
private Button btQuit = new Button("Quit");
private Label fromLabel = new Label("From:");
private TextField fromField = new TextField("", 40);
private Label toLabel = new Label("To:");
private TextField toField = new TextField("", 40);
private Label subjectLabel = new Label("Subject:");
private TextField subjectField = new TextField("", 40);
private Label messageLabel = new Label("Message:");
private TextArea messageText = new TextArea(10, 40);
/**
* Create a new MailClient window with fields for entering all
* the relevant information (From, To, Subject, and message).
*/
public MailClient() {
super("Java Mailclient");
/* Create panels for holding the fields. To make it look nice,
create an extra panel for holding all the child panels. */
Panel fromPanel = new Panel(new BorderLayout());
Panel toPanel = new Panel(new BorderLayout());
Panel subjectPanel = new Panel(new BorderLayout());
Panel messagePanel = new Panel(new BorderLayout());
fromPanel.add(fromLabel, BorderLayout.WEST);
fromPanel.add(fromField, BorderLayout.CENTER);
toPanel.add(toLabel, BorderLayout.WEST);
toPanel.add(toField, BorderLayout.CENTER);
subjectPanel.add(subjectLabel, BorderLayout.WEST);
subjectPanel.add(subjectField, BorderLayout.CENTER);
messagePanel.add(messageLabel, BorderLayout.NORTH);
messagePanel.add(messageText, BorderLayout.CENTER);
Panel fieldPanel = new Panel(new GridLayout(0, 1));
fieldPanel.add(fromPanel);
fieldPanel.add(toPanel);
fieldPanel.add(subjectPanel);
/* Create a panel for the buttons and add listeners to the
buttons. */
Panel buttonPanel = new Panel(new GridLayout(1, 0));
btSend.addActionListener(new SendListener());
btClear.addActionListener(new ClearListener());
btQuit.addActionListener(new QuitListener());
buttonPanel.add(btSend);
buttonPanel.add(btClear);
buttonPanel.add(btQuit);
/* Add, pack, and show. */
add(fieldPanel, BorderLayout.NORTH);
add(messagePanel, BorderLayout.CENTER);
add(buttonPanel, BorderLayout.SOUTH);
pack();
show();
}
static public void main(String argv[]) {
new MailClient();
}
/* Handler for the Send-button. */
class SendListener implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("Sending mail");
/* First, check that we have the sender and recipient. */
if((fromField.getText()).equals("")) {
System.out.println("Need sender!");
return;
}
if((toField.getText()).equals("")) {
System.out.println("Need recipient!");
return;
}
/* Create the message */
Message mailMessage = new Message(fromField.getText(),
toField.getText(),
subjectField.getText(),
messageText.getText());
/* Check that the message is valid, i.e., sender and
recipient addresses look ok. */
if(!mailMessage.isValid()) {
return;
}
/* Create the envelope, open the connection and try to
sendthe message. */
Envelope envelope = new Envelope(mailMessage);
try {
SMTPConnection connection = new
SMTPConnection(envelope);
connection.send(envelope);
connection.close();
} catch (IOException error) {
System.out.println("Sending failed: " + error);
return;
}
System.out.println("Mail sent succesfully!");
}
}
/* Clear the fields on the GUI. */
class ClearListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println("Clearing fields");
fromField.setText("");
toField.setText("");
subjectField.setText("");
messageText.setText("");
}
}
/* Quit. */
class QuitListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
}
}
import java.util.*;
import java.text.*;
/**
* Mail message.
*/
public class Message {
/* The headers and the body of the message. */
public String Headers;
public String Body;
/* Sender and recipient. With these, we don't need to extract
them from the headers. */
private String From;
private String To;
/* To make it look nicer */
private static final String CRLF = "\r\n";
/* Create the message object by inserting the required headers
from RFC 822 (From, To, Date). */
public Message(String from, String to, String subject, String text)
{
/* Remove whitespace */
From = from.trim();
To = to.trim();
Headers = "From: " + From + CRLF;
Headers += "To: " + To + CRLF;
Headers += "Subject: " + subject.trim() + CRLF;
/* A close approximation of the required format. Unfortunately
only GMT. */
SimpleDateFormat format =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'");
String dateString = format.format(new Date());
Headers += "Date: " + dateString + CRLF;
Body = text;
}
/* Two functions to access the sender and recipient. */
public String getFrom() {
return From;
}
public String getTo() {
return To;
}
/* Check whether the message is valid. In other words, check that
both sender and recipient contain only one @-sign. */
public boolean isValid() {
int fromat = From.indexOf('@');
int toat = To.indexOf('@');
if(fromat < 1 || (From.length() - fromat) <= 1) {
System.out.println("Sender address is invalid");
return false;
}
if(toat < 1 || (To.length() - toat) <= 1) {
System.out.println("Recipient address is invalid");
return false;
}
if(fromat != From.lastIndexOf('@')) {
System.out.println("Sender address is invalid");
return false;
}
if(toat != To.lastIndexOf('@')) {
System.out.println("Recipient address is invalid");
return false;
}
return true;
}
/* For printing the message. */
public String toString() {
String res;
res = Headers + CRLF;
res += Body;
return res;
}
}
import java.io.*;
import java.net.*;
import java.util.*;
/**
* SMTP envelope for one mail message.
*/
public class Envelope {
/* SMTP-sender of the message (in this case, contents of
From-header. */
public String Sender;
/* SMTP-recipient, or contents of To-header. */
public String Recipient;
/* Target MX-host */
public String DestHost;
public InetAddress DestAddr;
/* The actual message */
public Message Message;
/* Create the envelope. */
public Envelope(Message message) {
/* Get sender and recipient. */
Sender = message.getFrom();
Recipient = message.getTo();
/* Get message. We must escape the message to make sure that
there are no single periods on a line. This would mess up
sending the mail. */
Message = escapeMessage(message);
/* Get the hostname part of the recipient. It should be the
name of the MX-host for the recipient's domain. */
int atsign = Recipient.lastIndexOf('@');
DestHost = Recipient.substring(atsign + 1);
/* Map the name into an IP-address */
try {
DestAddr = InetAddress.getByName(DestHost);
} catch (UnknownHostException e) {
System.out.println("Unknown host: " + DestHost);
System.out.println(e);
return;
}
return;
}
/* Escape the message by doubling all periods at the beginning of
a line. */
private Message escapeMessage(Message message) {
String escapedBody = "";
String token;
StringTokenizer parser = new StringTokenizer(message.Body,
"\n", true);
while(parser.hasMoreTokens()) {
token = parser.nextToken();
if(token.startsWith(".")) {
token = "." + token;
}
escapedBody += token;
}
message.Body = escapedBody;
return message;
}
/* For printing the envelope. Only for debug. */
public String toString() {
String res = "Sender: " + Sender + '\n';
res += "Recipient: " + Recipient + '\n';
res += "MX-host: " + DestHost + ", address: " + DestAddr +
'\n';
res += "Message:" + '\n';
res += Message.toString();
return res;
}
}
|