/*
** 
**               Copyright (c) 2002-2004 Dave McMurtrie
**
** This file is part of imapproxy.
**
** imapproxy 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.
**
** imapproxy 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.
**
** You should have received a copy of the GNU General Public License
** along with imapproxy; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
**
**
**  Facility:
**
**	select.c
**
**  Abstract:
**
**      routines to support SELECT state caching in imapproxy.
**
**  Authors:
**
**	Dave McMurtrie <davemcmurtrie@hotmail.com>
**
**  RCS:
**
**      $Source: /afs/pitt.edu/usr12/dgm/work/IMAP_Proxy/src/RCS/select.c,v $
**      $Id: select.c,v 1.4 2005/06/15 12:11:12 dgm Exp $
**      
**  Modification History:
**  
**      $Log: select.c,v $
**      Revision 1.4  2005/06/15 12:11:12  dgm
**      Remove unused variables.
**
**      Revision 1.3  2004/11/10 15:34:14  dgm
**      Explictly NULL terminate all strings that are the result of strncpy.
**
**      Revision 1.2  2004/03/11 15:14:06  dgm
**      Behavior of calling code when Populate_Select_Cache() fails
**      modified to not send error back to client.
**      "safe" command list updated.
**
**      Revision 1.1  2004/02/24 15:13:21  dgm
**      Initial revision
**
**
**
*/

#define _REENTRANT

#include <syslog.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#include "imapproxy.h"

/*
 * External globals
 */
extern int errno;
extern IMAPCounter_Struct *IMAPCount;

/*
 * Internal prototypes
 */
static int Send_Cached_Select_Response( ITD_Struct *, ISC_Struct *, char * );
static int Populate_Select_Cache( ITD_Struct *, ISC_Struct *, char *, char *, unsigned int );


/*
 * Function definitions.
 */

/*++
 * Function:     Handle_Select_Command
 *
 * Purpose:      The client sent a SELECT command.  Either hit the cache,
 *               or get data from the imap server.
 *
 * Parameters:   ptr to ITD -- client transaction descriptor
 *               ptr to ITD -- server transaction descriptor
 *               ptr to ISC -- imap select cache structure
 *               ptr to char -- The select command string from the client.
 *               unsigned int -- the length of the select command
 *
 * Returns:      0 - The caller should consider the entire SELECT
 *                   transaction to be complete.  This return code does not
 *                   imply successful completion.  Rather, it implies that
 *                   neither the client nor the server should be sending more
 *                   data wrt this transaction.
 *
 *               1 - This return code implies that this function was not able
 *                   to really accomplish anything useful.  The caller should
 *                   attempt to send the SELECT command by an alternate means.
 *                   The SELECT command is still in the client read buffer and
 *                   may be proxied directly to the server without the use of
 *                   this function.
 * 
 *              -1 - A hard failure condition.  The client and server sockets
 *                   should be shut down.
 *
 * Authors:      Dave McMurtrie <davemcmurtrie@hotmail.com>
 *
 * Notes:        The SELECT command string passed into here will be the
 *               entire command, including the tag.
 *--
 */
extern int Handle_Select_Command( ITD_Struct *Client,
				  ITD_Struct *Server,
				  ISC_Struct *ISC,
				  char *SelectCmd,
				  int SelectCmdLength )
{
    char *fn = "Handle_Select_Command";
    char *Mailbox;
    char *Tag;
    char *CP;

    char Buf[ BUFSIZE ];

    IMAPCount->TotalSelectCommands++;
    
    /*
     * Make a local copy of the select buffer so we can chop it to hell without
     * offending the caller.
     */
    if ( SelectCmdLength >= BUFSIZE )
    {
	IMAPCount->SelectCacheMisses++;
	syslog( LOG_ERR, "%s: Length of SELECT command (%d bytes) would overflow %d byte buffer.", fn, SelectCmdLength, BUFSIZE );
	return( 1 );
    }
    
    memcpy( Buf, SelectCmd, SelectCmdLength );
    Buf[ SelectCmdLength ] = '\0';

    /*
     * NULL terminate the buffer at the end of the mailbox name
     */
    CP = memchr( (const void *)Buf, '\r', SelectCmdLength );
    if ( ! CP )
    {
	IMAPCount->SelectCacheMisses++;
	
	syslog( LOG_ERR, "%s: Sanity check failed!  SELECT command from client sd [%d] has no CRLF after it.", fn, Client->conn->sd );
	return( -1 );
    }
    *CP = '\0';

    Tag = Buf;
    CP = memchr( ( const void * )Buf, ' ', SelectCmdLength );
    
    if ( ! CP )
    {
	IMAPCount->SelectCacheMisses++;
	
	syslog( LOG_ERR, "%s: Sanity check failed!  No tokens found in SELECT command '%s' sent from client sd [%d].", fn, Buf, Client->conn->sd );
	return( 1 );
    }
    
    *CP = '\0';
    CP++;

    Mailbox = memchr( ( const void * )CP, ' ', SelectCmdLength - 
		      ( strlen( Tag ) + 1 ) );
    
    if ( ! Mailbox )
    {
	IMAPCount->SelectCacheMisses++;
	
	syslog( LOG_WARNING, "%s: Protocol error.  Client sd [%d] sent SELECT command with no mailbox name: '%s'", fn, Client->conn->sd, SelectCmd );
	snprintf( Buf, sizeof Buf - 1, "%s BAD missing required argument to SELECT command\r\n", Tag );
	IMAP_Write( Client->conn, Buf, strlen( Buf ) );
	return( 0 );
    }

    Mailbox++;
    
    /*
     * We have a valid SELECT command.  See if we have a cache entry that
     * isn't expired.
     */
    if ( time( 0 ) > ( ISC->ISCTime + SELECT_CACHE_EXP ) )
    {
	/*
	 * The SELECT data that's cached has expired.
	 */
	IMAPCount->SelectCacheMisses++;
	
	if ( Populate_Select_Cache( Server, ISC, Mailbox, SelectCmd, SelectCmdLength ) == -1 )
	{
	    return( 1 );
	}
	
	if ( Send_Cached_Select_Response( Client, ISC, Tag ) == -1 )
	{
	    snprintf( Buf, sizeof Buf - 1, "%s BAD internal proxy server error\r\n", Tag );
	    IMAP_Write( Client->conn, Buf, strlen( Buf ) );
	    return( 0 );
	} 
	
	return( 0 );
    }


    /*
     * Our data isn't expired, but is it the correct mailbox?
     */
    if ( ! strcmp( Mailbox, ISC->MailboxName ) )
    {
	/*
	 * We have the correct mailbox selected and cached already
	 */
	IMAPCount->SelectCacheHits++;
	
	if ( Send_Cached_Select_Response( Client, ISC, Tag ) == -1 )
	{
	    snprintf( Buf, sizeof Buf - 1, "%s BAD internal proxy server error\r\n", Tag );
	    IMAP_Write( Client->conn, Buf, strlen( Buf ) );
	    return( 0 );
	}
	
	return( 0 );
	
    }

    IMAPCount->SelectCacheMisses++;
    
    if ( Populate_Select_Cache( Server, ISC, Mailbox, SelectCmd, SelectCmdLength ) == -1 )
    {
	return( 1 );
    }	
    
    if ( Send_Cached_Select_Response( Client, ISC, Tag ) == -1 )
    {
	snprintf( Buf, sizeof Buf - 1, "%s BAD internal proxy server error\r\n", Tag );
	IMAP_Write( Client->conn, Buf, strlen( Buf ) );
	return( 0 );
    }	
    
    return( 0 );
    
}



/*++
 * Function:     Send_Cached_Select_Response
 *
 * Purpose:      Send cached SELECT server response data back to a client.
 *
 * Parameters:   ptr to ITD -- client transaction descriptor
 *               ptr to ISC -- imap select cache structure
 *               ptr to char -- client tag for response
 *
 * Returns:      0 on success
 *               -1 on failure
 *
 * Authors:      Dave McMurtrie <davemcmurtrie@hotmail.com>
 *
 * Notes:
 *--
 */
static int Send_Cached_Select_Response( ITD_Struct *Client,
					ISC_Struct *ISC,
					char *Tag )
{
    char *fn = "Send_Cached_Select_Response()";
    char SendBuf[ BUFSIZE ];

    if ( IMAP_Write( Client->conn, ISC->SelectString, 
		     strlen( ISC->SelectString ) ) == -1 )
    {
	syslog( LOG_WARNING, "%s: Failed to send cached SELECT string to client on sd [%d]: %s", fn, Client->conn->sd, strerror( errno ) );
	return( -1 );
    }
    
    snprintf( SendBuf, sizeof SendBuf - 1, "%s %s", Tag, 
	      ISC->SelectStatus );
    
    if ( IMAP_Write( Client->conn, SendBuf, strlen( SendBuf ) ) == -1 )
    {
	syslog( LOG_WARNING, "%s: Failed to send cached SELECT status to client on sd [%d]: %s", fn, Client->conn->sd, strerror( errno ) );
	return( -1 );
    }

    return( 0 );
    
}




/*++
 * Function:     Populate_Select_Cache
 *
 * Purpose:      Send a SELECT command to the server and cache the response.
 *
 * Parameters:   ptr to ITD -- server transaction descriptor
 *               ptr to ISC -- the cache structure to populate
 *               ptr to char -- the mailbox name that's being selected
 *               ptr to char -- The select command string from the client.
 *               unsigned int -- the length of the select command
 *
 * Returns:      0 on success
 *               -1 on failure
 *
 * Authors:      Dave McMurtrie <davemcmurtrie@hotmail.com>
 *
 * Notes:
 *--
 */
static int Populate_Select_Cache( ITD_Struct *Server,
				  ISC_Struct *ISC,
				  char *MailboxName,
				  char *ClientCommand,
				  unsigned int Length )
{
    char *fn = "Populate_Select_Cache()";
    int rc;
    int BytesLeftInBuffer = SELECT_BUF_SIZE;
    char *BufPtr;
    char *CP;
    char *EOS;

    rc = IMAP_Write( Server->conn, ClientCommand, Length );
    
    if ( rc == -1 )
    {
	syslog( LOG_ERR, "%s: Unable to send SELECT command to imap server so can't populate cache.", fn );
	return( -1 );
    }

    BufPtr = ISC->SelectString;
    
    for( ;; )
    {
	if ( Server->LiteralBytesRemaining )
	{
	    syslog( LOG_ERR, "%s: Server response to SELECT command contains unexpected literal data on sd [%d].", fn );
	    /*
	     * Must eat the literal.
	     */
	    while ( Server->LiteralBytesRemaining )
	    {
		IMAP_Literal_Read( Server );
	    }
	    
	    return( -1 );
	}
	
	rc = IMAP_Line_Read( Server );
	
	if ( ( rc == -1 ) || ( rc == 0 ) )
	{
	    syslog( LOG_WARNING, "%s: Unable to read SELECT response from imap server so can't populate cache.", fn );
	    return( -1 );
	}
	
	/*
	 * If it's not untagged data, we're done
	 */
	if ( Server->ReadBuf[0] != '*' )
	    break;
	
	if ( rc >= BytesLeftInBuffer ) 
	{
	    syslog( LOG_WARNING, "%s: Size of SELECT response from server exceeds max cache size of %d bytes.  Unable to cache this response.", fn, SELECT_BUF_SIZE );
	    return( -1 );
	}
	
	memcpy( (void *)BufPtr, (const void *)Server->ReadBuf, rc );
	BytesLeftInBuffer -= rc;
	BufPtr += rc;
    }
    
    /*
     * NULL terminate the buffer that contains the select response.  Note
     * that we used the >= conditional above so we'd leave one byte of
     * space for this NULL
     */
    *BufPtr = '\0';
    
    /*
     * The SELECT output string is filled in.  Now fill in the status.
     */
    CP = memchr( (const void *)Server->ReadBuf, ' ', rc );
    if ( ! CP )
    {
	syslog( LOG_ERR, "%s: Invalid response to SELECT command.  Contains no tokens.", fn );
	return( -1 );
    }
    CP++;
    
    EOS = memchr( (const void *)Server->ReadBuf, '\r', rc );
    if ( ! EOS )
    {
	syslog( LOG_ERR, "%s: Invalid response to SELECT command.  Not CRLF terminated.", fn );
	return( -1 );
    }
    
    *EOS = '\0';
    snprintf( (char *)ISC->SelectStatus, SELECT_STATUS_BUF_SIZE - 1, "%s\r\n",
	      CP );
    *EOS = '\r';

    /*
     * Update the cache time
     */
    ISC->ISCTime = time( 0 );

    strncpy( (char *)ISC->MailboxName, (const char *)MailboxName, MAXMAILBOXNAME - 1 );
    ISC->MailboxName[ MAXMAILBOXNAME - 1 ] = '\0';

    return( 0 );
    
}




/*++
 * Function:     Is_Safe_Command
 *
 * Purpose:      Determine whether a given command is "safe".  A command that
 *               is not "safe" should cause the select cache for a given
 *               mailbox to be invalidated.
 *
 * Parameters:   char ptr -- the command
 *
 * Returns:      1 if the command is safe
 *               0 if the command is not safe
 *
 * Authors:      Dave McMurtrie <davemcmurtrie@hotmail.com>
 *
 * Notes:
 *--
 */
extern unsigned int Is_Safe_Command( char *Command )
{
    unsigned int i;
    
    /*
     * The safety of some of these is very questionable, particularly
     * APPEND.  Since time also invalidates the cache, hopefully the
     * contents will be accurate enough to not break stuff...
     */
    char *SafeCommands[] = 
	{
	    "CAPABILITY",
	    "NOOP",
	    "LOGOUT",
	    "STARTTLS",
	    "AUTHENTICATE",
	    "LOGIN",
	    "SELECT",
	    "EXAMINE",
	    "CREATE",
	    "DELETE",
	    "RENAME",
	    "SUBSCRIBE",
	    "UNSUBSCRIBE",
	    "LIST",
	    "LSUB",
	    "STATUS",
	    "APPEND",
	    "CHECK",
	    "SEARCH",
	    "FETCH",
	    "COPY",
	    "UID",
	    NULL
	};

    /*
     * For a list this small a simple linear search should suffice.
     */
    for ( i = 0; SafeCommands[i] != 0; i++ )
    {
	if ( ! strncasecmp( SafeCommands[i], Command,
			    strlen( SafeCommands[i] ) ) )
	{
	    return( 1 );
	}
    }
    
    return( 0 );
}



/*++
 * Function:     Invalidate_Cache_Entry
 *
 * Purpose:      Reset the cache time so the entry will not be valid
 *
 * Parameters:   ptr to ISC -- imap select cache structure
 *
 * Returns:      nothing
 *
 * Authors:      Dave McMurtrie <davemcmurtrie@hotmail.com>
 *
 * Notes:
 *--
 */
extern void Invalidate_Cache_Entry( ISC_Struct *ISC )
{
    ISC->ISCTime = 0;
}




/*
 *                            _________
 *                           /        |
 *                          /         |
 *                         /    ______|
 *                        /    /       ________
 *                       |    |        |      /
 *                       |    |        |_____/
 *                       |    |        ______
 *                       |    |        |     \
 *                       |    |        |______\
 *                        \    \_______
 *                         \           |
 *                          \          |
 *                           \_________|
 */



syntax highlighted by Code2HTML, v. 0.9.1