Programming Assignment 1: Email Client with Browser Capabilities

This assignment counts for 13% of the total module mark. Failure in this assessment may be compensated for by higher marks in other components of the module. The purpose of the assignment is to learn the SMTP and HTTP protocols and socket programming. It partly assesses the following learning outcome:

The assignment is due Friday, 6 November 2020, 17:00.

Late submissions are subject to the University standard system of penalties. (cf. Section 6 in Code of Practice on Assessment)

In this assignment you will implement a mail client that sends mail to other users. You can use any operating system when programming this assignment. However, your programm will be tested on one of the csc linux machines (lxfarm01-lxfarm08). Make sure that your programm works on those machines. In particular your program should compile from the command line. You can open a MobaXterm session to work on those machines.

There are two parts to this assignment, part one is worth up to 70%, part two is worth up to 30%.


Submission Instructions

If you implemented part one and part two of the assignment then you have to submit two files:

Important Instructions


If you finished your assignment click here to SUBMIT.



Overview

In this programming assignment you are going to implement a mail client that sends mail to other users. Here's what the user interface looks like:

[Interface]

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. You will also need to give the name and portnumber of your local SMTP mailserver. For security reason, we will not use a live SMTP server. Instead we use MailCatcher (https://mailcatcher.me), a simple SMTP server that catches any message sent to it and displays it a web interface. MailCatcher is installed on my Google Cloud server. The web interface can be accessed via http://35.246.110.174:1080. To send emails to MailCatcher, you can use the SMTP server 35.246.110.174 on port 1025, which is already filled in. Emails delivered to this SMTP server will be displayed in the web frontend.

Defaults values for the fields in the mailclient can be specified in Emailclient.java. You can also specify default email addresses here, which will save you from a lot of typing.

In case that MailCatcher crashes, you can also use some other fake SMTP server; e.g. fakeSMTP (easier to run). When running this pick a random port number between 1024 and 65535. In the EmailClient you will need to use the same port number and localhost as SMTP server name.

In the "HTTP://" field you can provide a URL (e.g.: "comp211.gairing.com/test.txt"). When you press the "Get" buttom, the source of the corresponding object should appear in the "Message" field.

Your code will be tested on one of the csc linux machines:
lxfarm01 ... lxfarm08.csc.liv.ac.uk


The Code

For the first part you will need to complete the code in the SMTPInteraction class so that in the end you will have a program that is capable of sending mail to any recipient.
For the second part you will need to complete the code in the HTTPInteraction class so that in the end your program can download the mail message from any web address.

The program consists of four classes:

EmailClient The user interface
EmailMessage Email message
SMTPInteraction Connection to the SMTP server
HTTPInteraction Interaction with HTTP server

The code for the SMTPInteraction class and the HTTPInteraction class is at the end of this page. The code for the other classes is provided on this page.
A zip-archive of all files is here.

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.

Comments within the java-files also give additional instructions/hints that you should follow.

The EmailClient class provides the user interface and calls the other classes as needed.



Part One (SMTPInteraction)

The first task is to program the SMTP interaction between the MUA and the local 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.

When you have finished composing your mail, press Send to send it.

When you press Send, the EmailClient class constructs a EmailMessage class object to hold the mail message. The EmailMessage object holds the actual message headers and body, SMTP sender and recipient information, snd the SMTP server of the recipient's domain. Then the EmailClient object creates the SMTPInteraction object which opens a connection to the SMTP server and the EmailClient object sends the message over the connection. The sending of the mail happens in three phases:

  1. The EmailClient object creates the SMTPInteraction object by calling the constructor of SMTPInteraction. This opens the connection to the SMTP server (given in the DestHost and DestHostPortvariables of the EmailMessage object.).
  2. The EmailClient object sends the message using the function SMTPInteraction.send().
  3. The EmailClient object closes the SMTP connection.

The EmailMessage 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 more involved error checking.


Reply Codes

For the basic interaction of sending one message, you will only need to implement a part of SMTP. In this lab you need only to implement the following SMTP commands:

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 telnet or nc 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 HTTP and SMTP 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.

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 IO exceptions like this:

           throw new IOException();

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.



Part Two (HTTPInteraction)

For part two you have to uncommend a block of code in the GetListener method of the EmailClient class.

The graphical interface contains a field marked "HTTP://" which can be used to enter a URL.

When you press Get, the following happens:

  1. The EmailClient constructs a HTTPInteraction class object to hold the request message.
  2. The EmailClient sends the request using the function HTTPInteraction.send().
    This function returns a String containing the requested object, or an error message, if one occured.
  3. The EmailClient updates the message field to the returned String.

Your job is to complete the code of the HTTPInteraction constructor and the function HTTPInteraction.send().

The constructor splits the URL into its host and path part, contruct the requestMessage and assign the corresponding class variables.

In the function HTTPInteraction.send() you have to:

  1. Open a TCP connection to the host and assign the appropriate input and output streams to the connection.
  2. Request the object by sending the requestMessage into the OuputStream.
  3. Read the status line from the InputStream and extract the status code.
  4. If the request was successful (status code = 200) then read the headers from the InputStream and extract the lenght of the body. This information is stored in the "Content-Length:" (or sometimes "Content-length:") field. For reading the status line and header you can use the readLine() of the BufferedReader
  5. Read the body from the InputStream and return a String containing the body. Here you should use one of the read() methodes of the BufferedReader.

If you now press Send and you specified sender and recepient addresses, you should be able to send the downloaded object as the body of an E-mail.

Remark: This setup will only work for unsecured webservers (i.e., no HTTPS). More and more webservers are now secured and use SSL sockets. For simplicity (and because we haven't covered SSL yet), we will restrict to unsecure HTTP. Here are some URLs that we will use to test your code:



SMTPInteraction.java

This is the code for the SMTPConncetion class that you will need to complete for part 1. The code for the other classes is provided on this page.

/*************************************
 * Filename:  SMTPInteraction.java
 *************************************/
import java.net.*;
import java.io.*;
import java.util.*;

/**
 * Open an SMTP connection to mailserver and send one mail.
 *
 */
public class SMTPInteraction {
    /* Socket to the server */
    private Socket connection;

    /* Streams for reading from and writing to socket */
    private BufferedReader fromServer;
    private DataOutputStream toServer;

    private static final String CRLF = "\r\n";

    /* Are we connected? Used in close() to determine what to do. */
    private boolean isConnected = false;

    /* Create an SMTPInteraction object. Create the socket and the 
       associated streams. Initialise SMTP connection. */
    public SMTPInteraction(EmailMessage mailmessage) throws IOException {
        // Open a TCP client socket with hostname and portnumber specified in
        // mailmessage.DestHost and  mailmessage.DestHostPort, respectively.
	connection = /* Fill in */;
	
        // attach the BufferedReader fromServer to read from the socket and
        // the DataOutputStream toServer to write to the socket
        fromServer = /* Fill in */;
	toServer =   /* Fill in */;
	
	/* Fill in */
	/* Read one 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 = InetAddress.getLocalHost().getHostName();
	sendCommand( /* Fill in */ );

	isConnected = true;
    }

    /* Send message. Write the correct SMTP-commands in the
       correct order. No checking for errors, just throw them to the
       caller. */
    public void send(EmailMessage mailmessage) 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 SMTP 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 */
    }
} 


HTTPInteraction.java

This is the code for the HTTPInteraction class that you will need to complete for part 2. The code for the other classes is provided on this page.

/*************************************
 * Filename:  HTTPInteraction.java
 *************************************/

import java.net.*;
import java.io.*;
import java.util.*;

/**
 * Class for downloading one object from http server.
 *
 */
public class HTTPInteraction {
	private String host;
	private String path;
	private String requestMessage;
	
		
	private static final int HTTP_PORT = 80;
	private static final String CRLF = "\r\n";
	private static final int BUF_SIZE = 4096; 
	private static final int MAX_OBJECT_SIZE = 102400;

 	/* Create HTTPInteraction object. */
	public HTTPInteraction(String url) {
		
		/* Split "URL" into "host name" and "path name", and
		 * set host and path class variables. 
		 * if the URL is only a host name, use "/" as path 
		 */		
		
		/* Fill in */			

	
		/* Construct requestMessage, add a header line so that
		 * server closes connection after one response. */		
	
		/* Fill in */
	
		return;
	}	
	
	
	/* Send Http request, parse response and return requested object 
	 * as a String (if no errors), 
	 * otherwise return meaningful error message. 
	 * Don't catch Exceptions. EmailClient will handle them. */		
	public String send() throws IOException {
		
		/* buffer to read object in 4kB chunks */
		char[] buf = new char[BUF_SIZE];

		/* Maximum size of object is 100kB, which should be enough for most objects. 
		 * Change constant if you need more. */		
		char[] body = new char[MAX_OBJECT_SIZE];
		
		String statusLine="";	// status line
		int status;		// status code
		String headers="";	// headers
		int bodyLength=-1;	// lenghth of body
				
		String[] tmp;
		
		/* The socket to the server */
		Socket connection;
		
		/* Streams for reading from and writing to socket */
		BufferedReader fromServer;
		DataOutputStream toServer;
		
		System.out.println("Connecting server: " + host+CRLF);
		
		/* Connect to http server on port 80.
		 * Assign input and output streams to connection. */		
		connection = /* Fill in */;
		fromServer = /* Fill in */;
		toServer = /* Fill in */;
		
		System.out.println("Send request:\n" + requestMessage);


		/* Send requestMessage to http server */
		/* Fill in */
		
		/* Read the status line from response message */
		statusLine= /* Fill in */;
		System.out.println("Status Line:\n"+statusLine+CRLF);
		
		/* Extract status code from status line. If status code is not 200,
		 * close connection and return an error message. 
		 * Do NOT throw an exception */		
		/* Fill in */

		/* Read header lines from response message, convert to a string, 
 		 * and assign to "headers" variable. 
		 * Recall that an empty line indicates end of headers.
		 * Extract length  from "Content-Length:" (or "Content-length:") 
		 * header line, if present, and assign to "bodyLength" variable. 
		*/
		/* Fill in */ 		// requires about 10 lines of code
		System.out.println("Headers:\n"+headers+CRLF);
	

		/* If object is larger than MAX_OBJECT_SIZE, close the connection and 
		 * return meaningful message. */
		if (/* Fill in */) {
			/* Fill in */
			return(/* Fill in */ +bodyLength);
		}
					    
		/* Read the body in chunks of BUF_SIZE using buf[] and copy the chunk
		 * into body[]. Stop when either we have
		 * read Content-Length bytes or when the connection is
		 * closed (when there is no Content-Length in the response). 
		 * Use one of the read() methods of BufferedReader here, NOT readLine().
		 * Make sure not to read more than MAX_OBJECT_SIZE characters.
		 */				
		int bytesRead = 0;

		/* Fill in */   // Requires 10-20 lines of code

		/* At this points body[] should hold to body of the downloaded object and 
		 * bytesRead should hold the number of bytes read from the BufferedReader
		 */
		
		/* Close connection and return object as String. */
		System.out.println("Done reading file. Closing connection.");
		connection.close();
		return(new String(body, 0, bytesRead));
	}
}