//
// SMTP.java
//

import java.util.Vector;
import java.util.Enumeration;
import java.io.Reader;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.BufferedReader;
import java.io.Writer;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

/**
  * Class that handles an SMTP connection to send an e-mail to
  * a list of recipients.
  *
  * It's highly recommended for you to read RFC 821 where this
  * protocol is described so it will help you to understand many
  * of the arguments given to this class.
  *
  * The typical usage of this object would be something like this:
  * <pre>
  * try {
  *    SMTP smtp = new SMTP("mail.somewhere.com");
  *    smtp.setFrom("<myname@somewhere.com>");
  *    smtp.addRecipient("Somebody <someone@somewhere.com>");
  *    smtp.addRecipient("Techs <technicians@somewhere.com>");
  *    smtp.setData(new FileReader("message.txt"));
  *    smtp.send()
  * }
  * catch (UnknownHostException uoe) {
  *    System.out.println("Not a valid SMTP server");
  * }
  * catch () {
  * }
  * </pre>
  * <br>
  * Where <code>message.txt</code> should contain something like:<br>
  * <pre>
  * From: Myself <myname@somewhere.com>
  * To: Somebody Called My Friend <Someone@somewhere.com>
  * Subject:
  * ...blah, blah, blah...
  * and more blah, even more blah, blah blah
  * </pre>
  */
public class SMTP {

   /**
     * SMTP connection states.<br><br>
     *
     * The states values map the situations where an answer is
     * expected from the server.
     *
     * <pre>
     * R: 220 BBN-UNIX.ARPA Simple Mail Transfer Service Ready 
     * S: HELO USC-ISIF.ARPA
     * R: 250 BBN-UNIX.ARPA
     * S: MAIL FROM:<Smith@USC-ISIF.ARPA>
     * R: 250 OK
     * S: RCPT TO:<Jones@BBN-UNIX.ARPA>
     * R: 250 OK
     * S: RCPT TO:<Brown@BBN-UNIX.ARPA>
     * R: 250 OK
     * S: DATA
     * R: 354 Start mail input; end with <CRLF>.<CRLF>
     * S: Blah blah blah...
     * S: ...etc. etc. etc.
     * S: .
     * R: 250 OK
     * S: QUIT
     * R: 221 BBN-UNIX.ARPA Service closing transmission channel
     * <pre>
     */
   private final static short INTRODUCTION_STATE        = 0;
   private final static short AFTER_HELO_STATE          = 1;
   private final static short AFTER_MAIL_FROM_STATE     = 2;
   private final static short SENDING_RECIPIENTS_STATE  = 3;
   private final static short AFTER_DATA_STATE          = 4;
   private final static short SENDING_DATA_STATE        = 5;
   private final static short QUITTING_STATE            = 6;
   private final static short ERROR_QUITTING_STATE      = 7;
   private final static short FINISHED_STATE            = 8;
   private final static short ERROR_FINISHED_STATE      = 9;

   /**
     * The state of the communication.
     *
     * @see INTRODUCTION_STATE
     * @see AFTER_HELO_STATE
     * @see AFTER_MAIL_FROM_STATE
     * @see SENDING_RECIPIENTS_STATE
     * @see AFTER_DATA_STATE
     * @see SENDING_DATA_STATE
     * @see QUITTING_STATE
     * @see ERROR_QUITTING_STATE
     * @see FINISHED_STATE
     * @see ERROR_FINISHED_STATE
     */
   private short _state;

   /**
     * The STMP server address. This attribute is set in the
     * constructor.
     */
   private String _server;

   /**
     * The socket connection with the SMTP server.
     */
   private Socket _socket;

   /**
     * Output to the SMTP server.
     */
   private Writer _output;

   /**
     * Input from the SMTP server.
     */
   private BufferedReader _input;

   /**
     * Sender's email address.<br>
     * This string is used in the MAIL FROM: _sender and
     * RCPT TO: _singleReceiver commands. There is defined
     * that the addesses are between "<>" characters.
     *
     * @see _recipients
     */
   private String _sender;

   /**
     * Recipients list, updateable.
     *
     * @see _sender
     */
   private Vector _recipients;

   /**
     * This enumeration will be got from the _recipients
     * Vector. This object will be initializated when the
     * message is about to be sent (in the send() method).
     *
     * @see _recipients
     */
   private Enumeration _recipientsEnumeration;

   /**
     * Reader from which the message is read.
     */
   private Reader _messageData;

   /**
     * The error messages that could appear. This will be helpful
     * to solve the situations as long as the send() command will
     * fail is case of any disfunction.
     *
     * @see getErrorMessage
     */
   private StringBuffer _errorMessage;

   /**
     * This constructor initializes the fields with null values. It's
     * a way for not to repeat initializations in constructors, but 
     * be able to use this as a method more than as a constructor
     * (this is more flexible).
     */
   public void SMTPConstructor () {
      _state = INTRODUCTION_STATE;
      _server = null;
      _socket = null;
      _input = null;
      _output = null;
      _sender = "nobody@nowhere.com";
      _recipients = new Vector();
      _recipientsEnumeration = null;
      _messageData = null;
      _errorMessage = new StringBuffer();
   }

   /**
     * Single constructor, it receives the parameters needed to 
     * connect (basically the SMTP address). From here, the connection
     * with the server is open, and the _input and _output streams
     * (reader and writer) are defined. The connection is closed in
     * case of error.
     *
     * @param smtp SMTP server address.
     * @throws UnknownHostException
     * @throws IOException
     */
   public SMTP (String smtp) throws UnknownHostException, IOException {
      SMTPConstructor();
      _server = smtp;
      try {
         if (true) {
            _socket = new Socket(_server, 25);
            _input = new BufferedReader(new InputStreamReader(_socket.getInputStream()));
            _output = new OutputStreamWriter(_socket.getOutputStream());
         }
         else {
            _input = new BufferedReader(new InputStreamReader(System.in));
            _output = new OutputStreamWriter(System.out);
         }

      } catch (UnknownHostException uhe) {
         appendErrorMessage(uhe);
         throw uhe;
      } catch (IOException ioe) {
         appendErrorMessage(ioe);
         throw ioe;
      }
   }

   /**
     * Sets message sender. This sender should be set as specified in
     * RFC 821 (i.e "<somebody@hello.com>").
     *
     * @param sender The sender address that will be the "From: "
     *               field.
     * @see _sender
     * @see _recipients
     * @see addRecipients
     */
   public void setSender (String sender) {
      _sender = sender;
   }

   /**
     * Adds a new recipient to the _recipients list.
     *
     * @param rcpt The address of a new recipient.
     */
   public void addRecipient (String rcpt) {
      _recipients.addElement(rcpt);
   }

   /**
     * Defines the Reader from which the
     *
     * @param messageData The Reader from which the message is read to
     *                    given use after the DATA command.
     */
   public void setData (Reader messageData) {
      _messageData = messageData;
   }

   /**
     * Returns the explanation of the errors.
     *
     * @see _errorMessage
     */
   public String getErrorMessage () {
      return _errorMessage.toString();
   }

   /**
     * This method is the core of the protocol, and launches different
     * actions depending on the _state of the connection. This method
     * makes every change in the SMTP connection state and will handle
     * any errors in the protocol, this means that although errors
     * appear, it will return true. It will return false only in case
     * of communication errors.
     *
     * @return This method will return <code>true</code> unless it can't
     *         perform any action. In that case if will return
     *         <code>false</code> and the main loop should return an
     *         error too.
     * @see _state
     */
   private boolean processAnswer (String answer) {
      try {
         // Check it's a valid message
         if (answer.length() < 3)
            return false;

         // First of all, check whether an error has appeared. In that
         // case move to the error ERROR_QUITTING_STATE.
         char returnCode = answer.charAt(0);
         // Check for unrecoverable errors
         if ((returnCode == '4') || (returnCode == '5')) {
            // If we already are in ERROR_QUITTING_STATE
            // we have nothing else to do but leave.
            if (_state == ERROR_QUITTING_STATE)
               return true;

            // Send the "quit" command
            _output.write("quit\r\n");
            // Report the error to the error sink.
            _errorMessage.append(getMessage(answer));
            // Switch to error state
            _state = ERROR_QUITTING_STATE;
            return true;
         }

         // Now let's check the different states
         switch (_state) {
            case INTRODUCTION_STATE:
               // Send "helo" command
               _output.write("helo\r\n");
               _state = AFTER_HELO_STATE;
               break;

            case AFTER_HELO_STATE:
               // Send the "mail from:" command.
               _output.write("MAIL FROM:");
               _output.write(_sender);
               _output.write("\r\n");
               _state = AFTER_MAIL_FROM_STATE;
               break;

            case AFTER_MAIL_FROM_STATE:
               // Send recipients while remaining.
               if (_recipientsEnumeration.hasMoreElements()) {
                  _output.write("RCPT TO:");
                  _output.write((String) (_recipientsEnumeration.nextElement()));
                  _output.write("\r\n");
                  break;
               }
               else
                  // Pass to the next state
                  _state = SENDING_RECIPIENTS_STATE;
                  // DON'T PUT A BREAK HERE, IT MUST CONTINUE

            case SENDING_RECIPIENTS_STATE:
               _output.write("DATA\r\n");
               _state = AFTER_DATA_STATE;
               break;

            case AFTER_DATA_STATE: {
               int character;
               // Here we shall send all the email message until EOF
               while((character = _messageData.read()) != -1) {
                  _output.write(character);
               }
               // And close the message
               _output.write("\r\n.\r\n");
               _state = SENDING_DATA_STATE;
               break;
            }

            case SENDING_DATA_STATE:
               _output.write("QUIT\r\n");
               _state = QUITTING_STATE;
               break;

            case QUITTING_STATE:
               _state = FINISHED_STATE;
               break;

            case ERROR_QUITTING_STATE:
               _state = ERROR_FINISHED_STATE;
               break;

         }  // End of switch over the possible states.
         return true;

      } catch (Exception e) {
         // In case of any exception move to ERROR_FINISHED_STATE
         // and return false. The errorMessage is also updated with
         // the exception description.
         _state = ERROR_FINISHED_STATE;
         appendErrorMessage(e);
         return false;
      }

   }  // End of processAnswer method

   /**
     * This is the main method in this class. Handles all the process
     * that sends the email, and also handles the errors.
     *
     * @return <code>true</code> if the message was sent correctly,
     *         <code>false</code> otherwise.
     */
   public boolean send () {
      String answer;

      // Check if any recipient has been defined
      if (_recipients.size() != 0)
         _recipientsEnumeration = _recipients.elements();
      else {
         _errorMessage.append("No recipients defined");
         return false;
      }

      while (true) {
         // If in the finished state, close the socket
         if ((_state == ERROR_FINISHED_STATE) || (_state == FINISHED_STATE)) {
            try {
               _socket.close();
            } catch (Exception e) {
               appendErrorMessage(e);
            }

            // Check for the error code
            if (_state == ERROR_FINISHED_STATE)
               return false;
            else
               return true;
         }

         // Read and dispatch answers
         try {
            answer = _input.readLine();
            if (processAnswer(answer) == false)
               _state = ERROR_FINISHED_STATE;
            _output.flush();
         } catch (Exception e) {
            _state = ERROR_FINISHED_STATE;
         }
            
      }  // End of the answers loop.
   }  // End of send method.

   /**
     * Retrieves the description of the message removing the
     * code (3 digits code) at the beginning.<br><br>
     * for example:<br>
     * <code>220 BBN-UNIX.ARPA Simple Mail Transfer Service Ready</code>
     * getMessage would return <code>"BBN-UNIX.ARPA Simple Mail Transfer
     * Service Ready"</code>.
     *
     * @return The message without the code.
     */
   private String getMessage (String message) {
      if (message.length() >= 4)
         return message.substring(4);
      else
         return "";
   }

   /**
     * Adds the exception description to the _errorMessage string.
     *
     * @see _errorMessage
     */
   private void appendErrorMessage(Exception e) {
      _errorMessage.append(e.toString());
   }

   /**
     * A simple application to test.
     */
   public static void main (String[] args) {
      // Check the arguments.
      if (args.length != 5) {
         System.out.println("Bad parameters. Usage:");
         System.out.println("   java SMTP server from to subject message");
         System.out.println("example:");
         System.out.println("   java SMTP mail.here.com me@here.com someone@somewhere.com \"Hello\" \"Hi there\"");
         System.exit(-1);
      }

      try {
         // Init the connection.
         SMTP smtpConnection = new SMTP(args[0]);
         smtpConnection.setSender("<" + args[1] + ">");
         smtpConnection.addRecipient("<" + args[2] + ">");

         // Let's compose the message
         StringBuffer message = new StringBuffer("From: <");
         message.append(args[1]);
         message.append(">\r\nTo: <");
         message.append(args[2]);
         message.append(">\r\nSubject: ");
         message.append(args[3]);
         message.append("\r\n");
         message.append("\r\n");
         message.append(args[4]);
         StringReader reader = new StringReader(message.toString());
         smtpConnection.setData(reader);

         // just send it
         if (smtpConnection.send())
            System.out.println("Message sent.");
         else {
            System.out.println("Failed to send message.");
            System.out.println(smtpConnection.getErrorMessage());
            System.exit(-1);
         }

      } catch (Exception e) {
         System.out.println(e);
         System.exit(-1);
      }
   }


}  // End of class.
