Package com.ericdaugherty.mail.server.services.pop3

Source Code of com.ericdaugherty.mail.server.services.pop3.Pop3Processor

/******************************************************************************
* $Workfile: Pop3Processor.java $
* $Revision: 149 $
* $Author: edaugherty $
* $Date: 2003-11-15 15:40:30 -0600 (Sat, 15 Nov 2003) $
*
******************************************************************************
* This program is a 100% Java Email Server.
******************************************************************************
* Copyright (C) 2001, Eric Daugherty
* All rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
******************************************************************************
* For current versions and more information, please visit:
* http://www.ericdaugherty.com/java/mail
*
* or contact the author at:
* java@ericdaugherty.com
*
******************************************************************************
* This program is based on the CSRMail project written by Calvin Smith.
* http://crsemail.sourceforge.net/
*****************************************************************************/

package com.ericdaugherty.mail.server.services.pop3;

//Java imports
import java.net.*;
import java.io.*;

//Log imports
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

//Local imports
import com.ericdaugherty.mail.server.info.*;
import com.ericdaugherty.mail.server.services.general.DeliveryService;
import com.ericdaugherty.mail.server.services.general.ConnectionProcessor;
import com.ericdaugherty.mail.server.configuration.ConfigurationManager;

/**
* Handles an incoming Pop3 connection.  See rfc 1939 for details.
*
* @author Eric Daugherty
*/
public class Pop3Processor extends Thread implements ConnectionProcessor {

    //***************************************************************
    // Variables
    //***************************************************************

    /** Logger Category for this class. */
    private static Log log = LogFactory.getLog( Pop3Processor.class.getName() );

    /** The ConfigurationManager */
    private static ConfigurationManager configurationManager = ConfigurationManager.getInstance();

    /** Indicates if this thread should continue to run or shut down */
    private boolean running = true;

    /** The server socket used to listen for incoming connections */
    private ServerSocket serverSocket;

    /** Socket connection to the client */
    private Socket socket;

    /** The IP address of the client */
    private String clientIp;

    /** The user currently logged in */
    private User user = null;

    /** Writer to sent data to the client */
    private PrintWriter out;
    /** Reader to read data from the client */
    private BufferedReader in;

    //***************************************************************
    // Public Interface
    //***************************************************************

    /**
     * Sets the socket used to communicate with the client.
     */
    public void setSocket( ServerSocket serverSocket ) {

        this.serverSocket = serverSocket;
    }

    /**
     * Entrypoint for the Thread, this method handles the interaction with
     * the client socket.
     */
    public void run() {

        try {
            //Set the socket to timeout every 10 seconds so it does not
            //just block forever.
            serverSocket.setSoTimeout( 1000 );
        }
        catch( SocketException se ) {
            log.fatal( "Error initializing Socket Timeout in Pop3Processor" );
        }

        while( running ) {
            try {
                socket = serverSocket.accept();

                //Prepare the input and output streams.
                out = new PrintWriter(socket.getOutputStream(), true);
                in = new BufferedReader(new InputStreamReader( socket.getInputStream() ));

                InetAddress remoteAddress = socket.getInetAddress();
                clientIp = remoteAddress.getHostAddress();
                if( log.isInfoEnabled() ) { log.info( remoteAddress.getHostName() + "(" + clientIp + ") socket connected via POP3." ); }

                //Output the welcome message.
                write( WELCOME_MESSAGE );

                //Forces the client to property authenticate.
                user = authenticate();
                user.reset();

                //Parses the input for commands and delegates to the appropriate methods.
                handleCommands();
            }
            catch( InterruptedIOException iioe ) {
                //This is fine, it should time out every 10 seconds if
                //a connection is not made.
            }
            //If any exception gets to here uncaught, it means we should just disconnect.
            catch( Throwable e ) {
                log.debug( "Disconnecting Exception:", e );
                log.info( "Disconnecting" );

                //Unlock the user's mailbox
                if( user != null ) {
                    EmailAddress userAddress = new EmailAddress( user.getUsername(), user.getDomain() );
                    DeliveryService.getDeliveryService().unlockMailbox( userAddress );
                }

                try {
                    write( MESSAGE_DISCONNECT );
                }
                catch( Exception e1 ) {
                    log.debug( "Error sending disconnect message.", e1 );
                    //Nothing to do.
                }
                try {
                    if( socket != null ) {
                        socket.close();
                    }
                }
                catch( IOException ioe ) {
                    log.debug( "Error disconnecting.", ioe );
                    //Nothing to do.
                }
            }
        }
        log.warn( "Pop3Processor shut down gracefully" );
    }

    /**
     * Notifies this thread to stop processing and exit.
     */
    public void shutdown() {
        log.warn( "Shutting down Pop3Processor." );
        running = false;
    }

    //***************************************************************
    // Private Interface
    //***************************************************************

    /**
     * Checks to verify that the command is not a quit command.  If it is,
     * the current state is finalized (all messaged marked as deleted are
     * actually deleted) and closes the connection.
     */
    private void checkQuit( String command ) {

        if( command.equals( COMMAND_QUIT ) ) {
            log.debug( "User has QUIT the session." );

            //Delete the messages marked as deleted from disk
            if( user != null ) {
                Message[] messages = user.getMessages();
                int numMessage = messages.length;
                Message currentMessage = null;

                for( int index = 0; index < numMessage; index++ ) {
                    currentMessage = messages[index];
                    if( currentMessage.isDeleted() ) {
                        messages[index].getMessageLocation().delete();
                    }
                }
            }

            // TODO Find a better way to handle user logoffs.
            throw new RuntimeException();
        }

    }

    /**
     * The user must authenticate before moving on to enter
     * more commands.  This method will listen to incoming
     * commands until the user either successfully autheticates
     * or quits.
     */
    private User authenticate() {

        //Reusable Variables.
        String inputString;
        String command;
        String argument;

        String username = "";
        String domain = "";
        String password = "";
        EmailAddress address = null;
        DeliveryService deliveryService = DeliveryService.getDeliveryService();

        //Get the username from the client.
        boolean userAccepted = false;

        while( !userAccepted ) {
            inputString = read();

            command = parseCommand( inputString );
            argument = parseArgument( inputString );

            //Check to see if they sent the user command.
            if( command.equals( COMMAND_USER ) ) {

                //Make sure they sent a username
                if( argument.equals( "" ) ) {
                    write( MESSAGE_TOO_FEW_ARGUMENTS );
                }
                else {
                    int atIndex = argument.indexOf( "@" );

                    //Verify that the username contains the domain.
                    if( atIndex == -1 ) {
                        write( MESSAGE_NEED_USER_DOMAIN );
                    }
                    else {
                        //Accept the user, and proceed to get the password.
                        username = argument.substring( 0, atIndex );
                        domain = argument.substring( atIndex + 1 );

                        address = new EmailAddress( username, domain );

                        //Check to see if the user's mailbox is locked
                        if( deliveryService.isMailboxLocked( address ) ) {
                            write( MESSAGE_USER_MAILBOX_LOCKED );
                        }
                        else {
                            write( MESSAGE_USER_ACCEPTED + argument );
                            userAccepted = true;
                        }
                    }
                }
            }
            else {
                write( MESSAGE_INVALID_COMMAND + command );
            }
        }

        //The user has been accepted, now get the password.
        boolean passwordAccepted = false;

        while( !passwordAccepted ) {
            inputString = read();

            command = parseCommand( inputString );
            argument = parseArgument( inputString );

            //Check to see if they sent the user command.
            if( command.equals( COMMAND_PASS ) ) {

                //Make sure they sent a password
                if( argument.equals( "" ) ) {
                    write( MESSAGE_TOO_FEW_ARGUMENTS );
                }
                else {
                    password = argument;
                    passwordAccepted = true;
                }
            }
            else {
                write( MESSAGE_INVALID_COMMAND + command );
            }
        }

        User user = configurationManager.getUser( address );
        if( user != null && user.isPasswordValid( password ) )
        {
            deliveryService.ipAuthenticated( clientIp );
            deliveryService.lockMailbox( address );
            write( MESSAGE_LOGIN_SUCCESSFUL );
            if( log.isInfoEnabled() ) log.info( "User: " + address.getAddress() + " logged in successfully.");
            return user;
        }
        else
        {
            //The login failed, display a message to the user and disconnect.
            write( MESSAGE_INVALID_LOGIN + username );
            log.info( "Login failed for user: " + username + "@" + domain );
            throw new RuntimeException();
        }
    }

    /**
     * Handles all the commands related the the retrieval of mail.
     */
    private void handleCommands() {

        //Reusable Variables.
        String inputString;
        String command;
        String argument;

        //This just runs until a SystemException is thrown, which
        //signals us to disconnect.
        while( true ) {

            inputString = read();

            command = parseCommand( inputString );
            argument = parseArgument( inputString );

            //Identify the command and call the appropriate helper method.
            if( command.equals( COMMAND_STAT ) ) {
                handleStat();
            }
            else if( command.equals( COMMAND_LIST ) ) {
                handleList( argument );
            }
            else if( command.equals( COMMAND_RETR ) ) {
                handleRetr( argument );
            }
            else if( command.equals( COMMAND_DELE ) ) {
                handleDele( argument );
            }
            else if( command.equals( COMMAND_NOOP ) ) {
                write( "+OK" );
            }
            else if( command.equals( COMMAND_RSET ) ) {
                handleRset();
            }
      else if( command.equals( COMMAND_TOP ) ) {
        handleTop( argument );
      }
      else if( command.equals( COMMAND_UIDL ) ) {
        handleUidl( argument );
      }
            else {
                write( MESSAGE_INVALID_COMMAND + command );
            }
        }
    }

    /**
     * Handles the 'stat' command, which returns the total number of message
     * and the total size of those message.
     */
    private void handleStat() {

        write( "+OK " + user.getNumberOfMessage() + " " + user.getSizeOfAllMessage() );
    }

    /**
     * Handles the 'list' command, which returns the total number of messages and
     * size along with a list of the individual message sizes.
     */
    private void handleList( String argument ) {

        if( argument.equals( "" ) ) {
            long numMessages = user.getNumberOfMessage();
            long sizeMessage = user.getSizeOfAllMessage();

            write( "+OK " + numMessages + " messages (" + sizeMessage + " octets)" );

            for( int index = 0; index < numMessages; index++ ) {
                write( (index + 1) + " " + user.getMessage( index + 1 ).getMessageLocation().length() );
            }
            write( "." );
        }
        else {
            int messageNumber = 0;

            try {
                messageNumber = Integer.parseInt( argument );
            }
            catch( NumberFormatException nfe ) {
                write( MESSAGE_NOT_A_NUMBER );
                return;
            }

            long numMessages = user.getNumberOfMessage();

            if( messageNumber > numMessages || user.getMessage( messageNumber ).isDeleted() ) {
                write( MESSAGE_NO_SUCH_MESSAGE );
                return;
            }

            write( "+OK " + messageNumber + " " + user.getMessage( messageNumber ).getMessageLocation().length() );
        }
    }

    /**
     * Sends the specified email message to the client.
     */
    private void handleRetr( String argument ) {

        int messageNumber = 0;

        try {
            messageNumber = Integer.parseInt( argument );
        }
        catch( NumberFormatException nfe ) {
            write( MESSAGE_NOT_A_NUMBER );
            return;
        }

        long numMessages = user.getNumberOfMessage();

        if( log.isDebugEnabled() ) {
            log.debug( "Is Msg Deleted: " + user.getMessage( messageNumber ).isDeleted() );
            log.debug( "Message: " + messageNumber + " of " + numMessages );
        }
        if( messageNumber > numMessages || user.getMessage( messageNumber ).isDeleted() ) {
            write( MESSAGE_NO_SUCH_MESSAGE );
            return;
        }

        write( MESSAGE_OK );

        BufferedReader fileIn = null;
        try {
            //Open an reader to read the file.
            fileIn = new BufferedReader( new FileReader( user.getMessage( messageNumber ).getMessageLocation() ) );

            //Write the file to the client.
            String currentLine = fileIn.readLine();
            while (currentLine != null) {
                write( currentLine );
                currentLine = fileIn.readLine();
            }
            write( "." );
        }
        catch( FileNotFoundException fnfe ) {
            log.error( "Requested message for user " + user.getFullUsername() + " could not be found on disk.", fnfe );
        }
        catch( IOException ioe ) {
            log.error( "Error retrieving message.", ioe );
            write( "-ERR Error retrieving message" );
        }
        finally {
            //Make sure the input stream gets closed.
            try {
                if( fileIn != null ) {
                    fileIn.close();
                }
            }
            catch( IOException ioe ) {
                //Nothing to do...
            }
        }
    }

    /**
     * Marks the specified message for deletion.  The message will only
     * be deleted if the user later enters the QUIT command, as per the
     * spec.
     */
    private void handleDele( String argument ) {

        int messageNumber = 0;

        try {
            messageNumber = Integer.parseInt( argument );
        }
        catch( NumberFormatException nfe ) {
            write( MESSAGE_NOT_A_NUMBER );
            return;
        }

        long numMessages = user.getNumberOfMessage();

        if( messageNumber > numMessages ) {
            write( MESSAGE_NO_SUCH_MESSAGE );
        }
        else ifuser.getMessage( messageNumber ).isDeleted() ) {
            write( MESSAGE_ALREADY_DELETED );
        } else {
            user.getMessage( messageNumber ).setDeleted( true );
            write( MESSAGE_OK );
        }
    }

    /**
     * Unmarks all deleted messages.
     */
    private void handleRset() {

        Message[] messages = user.getMessages();
        int numMessage = messages.length;

        for( int index = 0; index < numMessage; index++ ) {
            messages[index].setDeleted( false );
        }

        write( MESSAGE_OK );
    }

  /**
   * Returns the header and first x lines for the
   * specified message.
   */
  private void handleTop( String argument ) {

    log.debug( "In Top" );

    int messageNumber = 0;
    int numLines = 0;

    int spaceIndex = argument.indexOf( " " );
    if( spaceIndex == -1 ) {
      write( MESSAGE_TOO_FEW_ARGUMENTS );
      return;
    }
    String arg1 = argument.substring( 0, spaceIndex ).trim();
    String arg2 = argument.substring( spaceIndex + 1 ).trim();

        try {
            messageNumber = Integer.parseInt( arg1 );
      numLines = Integer.parseInt( arg2 );
        }
        catch( NumberFormatException nfe ) {
            write( MESSAGE_NOT_A_NUMBER );
            return;
        }

        long numMessages = user.getNumberOfMessage();

        if( log.isDebugEnabled() ) {
            log.debug( "Is Msg Deleted: " + user.getMessage( messageNumber ).isDeleted() );
            log.debug( "Message: " + messageNumber + " of " + numMessages );
        }
        if( messageNumber > numMessages || user.getMessage( messageNumber ).isDeleted() ) {
            write( MESSAGE_NO_SUCH_MESSAGE );
            return;
        }

        write( MESSAGE_OK );

        BufferedReader fileIn = null;
        try {
            //Open an reader to read the file.
            fileIn = new BufferedReader( new FileReader( user.getMessage( messageNumber ).getMessageLocation() ) );

            //Write the Message Header.
            String currentLine = fileIn.readLine();
            while (currentLine != null && !currentLine.equals( "" ) ) {
        write( currentLine );
                currentLine = fileIn.readLine();
            }

      //Write an empty line to seperate header from body.
      write( currentLine );
            currentLine = fileIn.readLine();

      //Write the requested number of lines from the body of the
      //message, or until the entire message has been written.
      int index = 0;
      while( index < numLines && currentLine != null ) {
        write( currentLine );
        currentLine = fileIn.readLine();
        index++;
      }

            write( "." );
        }
        catch( FileNotFoundException fnfe ) {
            log.error( "Requested message for user " + user.getFullUsername() + " could not be found on disk.", fnfe );
        }
        catch( IOException ioe ) {
            log.error( "Error retrieving message.", ioe );
            write( "-ERR Error retrieving message" );
        }
        finally {
            //Make sure the input stream gets closed.
            try {
                if( fileIn != null ) {
                    fileIn.close();
                }
            }
            catch( IOException ioe ) {
                //Nothing to do...
            }
        }
  }

  /**
   * Returns the unique id of the specified message, or all the unique
   * ids of the non-deleted messages.
   */
  private void handleUidl( String argument ) {

    //Return all messages unique ids
    if( argument == null || argument.length() == 0 ) {

      long numMessages = user.getNumberOfMessage();
      Message message;

      write( MESSAGE_OK );

      //Write out each non-deleted message id.
      for( int index = 0; index < numMessages; index++ ) {
        message = user.getMessage( index + 1 );
        if( !message.isDeleted() ) {
          write( (index + 1) + " " + message.getUniqueId() );
        }
      }

      write( "." );
    }
    //Ouput a single messages unique id.
    else {

      int messageNumber = 0;

      try {
        messageNumber = Integer.parseInt( argument );
      }
      catch( NumberFormatException nfe ) {
        write( MESSAGE_NOT_A_NUMBER );
        return;
      }

      long numMessages = user.getNumberOfMessage();

      if( messageNumber > numMessages || user.getMessage( messageNumber ).isDeleted() ) {
        write( MESSAGE_NO_SUCH_MESSAGE );
        return;
      }

      write( MESSAGE_OK + " " + messageNumber + " " + user.getMessage( messageNumber ).getUniqueId() );
    }
  }

    /**
     * Reads a line from the input stream and returns it.
     */
    private String read() {
        try {
            String inputLine = in.readLine().trim();
            //Log the input, unless it is a password.
            if( log.isDebugEnabled() && !inputLine.startsWith( "PASS" ) ) {
                log.debug( "Read Input: " + inputLine );
            }
            return inputLine;
        }
        catch( IOException ioe ) {
            log.error( "Error reading from socket.", ioe );
            throw new RuntimeException();
        }
    }

    /**
     * Writes the specified output message to the client.
     */
    private void write( String message ) {
        if( log.isDebugEnabled() ) { log.debug( "Writing Output: " + message ); }
        out.print( message + "\r\n" );
        out.flush();
    }

    /**
     * Parses the input stream for the command.  The command is the
     * begining of the input stream to the first space.  If there is
     * space found, the entire input string is returned.
     * <p>
     * This method converts the returned command to uppercase to allow
     * for easier comparison.
     * <p>
     * Additinally, this method checks to verify that the quit command
     * was not issued.  If it was, a SystemException is thrown to terminate
     * the connection.
     */
    private String parseCommand( String inputString ) {

        int index = inputString.indexOf( " " );

        if( index == -1 ) {
            String command = inputString.toUpperCase();
            checkQuit( command );
            return command;
        }
        else {
            String command = inputString.substring( 0, index ).toUpperCase();
            checkQuit( command );
            return command;
        }
    }

    /**
     * Parses the input stream for the argument.  The argument is the
     * text starting afer the first space until the end of the inputstring.
     * If there is no space found, an empty string is returned.
     * <p>
     * This method does not convert the case of the argument.
     */
    private String parseArgument( String inputString ) {

        int index = inputString.indexOf( " " );

        if( index == -1 ) {
            return "";
        }
        else {
            return inputString.substring( index + 1 ).trim();
        }
    }

    //***************************************************************
    // Constants
    //***************************************************************

    //Message Constants
    //General Message
    private static final String WELCOME_MESSAGE = "+OK EricDaugherty's Java Pop Server Ready";
    private static final String MESSAGE_DISCONNECT = "+OK Pop server signing off.";
    private static final String MESSAGE_OK = "+OK";
    private static final String MESSAGE_INVALID_COMMAND = "-ERR Unknown command: ";
    private static final String MESSAGE_TOO_FEW_ARGUMENTS = "-ERR Too few arguments for this command.";

    //Authentication Messages
    private static final String MESSAGE_NEED_USER_DOMAIN = "-ERR User names must contain the username and domain.  ex: \"root@mydomain.com\"";
    private static final String MESSAGE_USER_ACCEPTED = "+OK Password required for ";
    private static final String MESSAGE_LOGIN_SUCCESSFUL = "+OK Login successful";
    private static final String MESSAGE_USER_MAILBOX_LOCKED = "-ERR User's Mailbox is locked";
    private static final String MESSAGE_INVALID_LOGIN = "-ERR Password supplied is incorrect for user: ";

    //Other Messages
    private static final String MESSAGE_NOT_A_NUMBER = "-ERR Command requires a valid number as an argument.";
    private static final String MESSAGE_NO_SUCH_MESSAGE = "-ERR No such message.";
    private static final String MESSAGE_ALREADY_DELETED = "-ERR Message already deleted.";

    //Command Constants
    private static final String COMMAND_QUIT = "QUIT";
    private static final String COMMAND_USER = "USER";
    private static final String COMMAND_PASS = "PASS";
    private static final String COMMAND_STAT = "STAT";
    private static final String COMMAND_LIST = "LIST";
    private static final String COMMAND_RETR = "RETR";
    private static final String COMMAND_DELE = "DELE";
    private static final String COMMAND_NOOP = "NOOP";
    private static final String COMMAND_RSET = "REST";
  private static final String COMMAND_TOP = "TOP";
  private static final String COMMAND_UIDL = "UIDL";

}

TOP

Related Classes of com.ericdaugherty.mail.server.services.pop3.Pop3Processor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.