/* ** ** Copyright (c) 2002,2003 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: ** ** imapcommon.c ** ** Abstract: ** ** Routines common to making IMAP server connections and receiving ** data from an IMAP server or client. ** ** Authors: ** ** Dave McMurtrie ** ** RCS: ** ** $Source: /afs/pitt.edu/usr12/dgm/work/IMAP_Proxy/src/RCS/imapcommon.c,v $ ** $Id: imapcommon.c,v 1.21 2005/06/15 12:06:31 dgm Exp $ ** ** Modification History: ** ** $Log: imapcommon.c,v $ ** Revision 1.21 2005/06/15 12:06:31 dgm ** Added missing include directive for openssl/err.h. ** atoui function added to replace calls to atoi. ** Include limits.h and config.h. ** ** Revision 1.20 2005/01/12 17:50:16 dgm ** Applied patch by David Lancaster to provide force_tls ** config option. ** ** Revision 1.19 2004/11/10 15:29:23 dgm ** Explicitly NULL terminate all strings that are the result of ** strncpy. Also enforce checking of LiteralBytesRemaining ** after any calls to IMAP_Line_Read. ** ** Revision 1.18 2003/10/22 13:39:24 dgm ** Fixed really bad bug in for loop for string literal detection. ** Explicitly clear errno prior to calling atol(). ** ** Revision 1.17 2003/10/09 12:53:49 dgm ** Added source port to syslog messages. Added ability to send ** tcp keepalives. Added a poll() call in IMAP_Literal_Read() so ** read calls can't block forever. ** ** Revision 1.16 2003/07/14 16:37:44 dgm ** Applied patch by William Yodlowsky ** to allow TLS to work without /dev/random. ** ** Revision 1.15 2003/05/20 18:49:53 dgm ** Comment changes only. ** ** Revision 1.14 2003/05/15 11:47:59 dgm ** Added code to deal with possible unsolicited, untagged capability ** response from server in Get_Server_conn(). Added credit comment in ** function header block, also. ** ** Revision 1.13 2003/05/13 14:19:27 dgm ** Changed AF_INET constant reference to PF_INET. ** ** Revision 1.12 2003/05/13 11:39:58 dgm ** Patches by Ken Murchison to clean up build process. ** ** Revision 1.11 2003/05/06 12:09:57 dgm ** Applied patches by Ken Murchison to add SSL support. ** ** Revision 1.10 2003/02/20 13:52:16 dgm ** Logic changed in Get_Server_sd() such that authentication is attempted to the ** real server when the md5 checksums don't match instead of just dropping ** the connection. ** ** Revision 1.9 2003/02/19 12:47:31 dgm ** Replaced check for server response of "+ go ahead" with a check for ** "+". the "go ahead" appears to be cyrus specific and not RFC compliant ** on my part. ** ** Revision 1.8 2003/01/27 13:59:53 dgm ** Patch by Frode Nordahl to allow ** compilation on Linux platforms. ** ** Revision 1.7 2003/01/23 16:24:31 dgm ** NonSyncLiteral flag was not being cleared properly. ** ** Revision 1.6 2003/01/22 12:56:30 dgm ** Changes to Get_Server_sd() so it can support login attempts where ** the client sends the password as a string literal. ** ** Revision 1.5 2002/12/18 14:39:55 dgm ** Fixed bug in for loop for string literal processing. ** ** Revision 1.4 2002/12/17 14:22:41 dgm ** Added support for global configuration structure. ** ** Revision 1.3 2002/08/29 20:22:12 dgm ** Fixed nasty socket descriptor leak. ** ** Revision 1.2 2002/08/28 15:57:49 dgm ** replaced all internal log function calls with standard syslog calls. ** ** Revision 1.1 2002/07/03 12:07:26 dgm ** Initial revision ** ** */ #define _REENTRANT #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_UNISTD_H #include #endif #include #include #include #include "common.h" #include "imapproxy.h" /* * External globals */ extern ICC_Struct *ICC_free; extern ICC_Struct *ICC_HashTable[ HASH_TABLE_SIZE ]; extern ISD_Struct ISD; extern pthread_mutex_t mp; extern pthread_mutex_t trace; extern IMAPCounter_Struct *IMAPCount; extern ProxyConfig_Struct PC_Struct; #if HAVE_LIBSSL extern SSL_CTX *tls_ctx; /*++ * Function: SSLerrmessage * * Purpose: Obtain reason string for last SSL error * * Parameters: none * * Returns: SSL error text * * * Authors: http://developer.postgresql.org/docs/pgsql/src/backend/libpq/be-secure.c * * Notes: Some caution is needed here since ERR_reason_error_string will * return NULL if it doesn't recognize the error code. We don't * want to return NULL ever. * * This function submitted as a patch by William Yodlowsky * *-- */ static const char *SSLerrmessage( void ) { unsigned long errcode; const char *errreason; static char errbuf[32]; errcode = ERR_get_error(); if ( errcode == 0 ) return "No SSL error reported"; errreason = (const char *)ERR_reason_error_string( errcode ); if (errreason != NULL) return errreason; snprintf(errbuf, sizeof( errbuf ) - 1, "SSL error code %lu", errcode); return errbuf; } #endif /* HAVE_LIBSSL */ /*++ * Function: atoui * * Purpose: Convert a char array to an unsigned int value. * * Parameters: const char ptr -- the NULL terminated string to convert * unsigned int ptr -- where the converted value will be stored. * * Returns: 0 on success * -1 on failure * * Authors: Dave McMurtrie * * Notes: Will tolerate trailing plus sign since IMAP rfc allows that as * part of a literal specifier. */ extern int atoui( const char *Value, unsigned int *ConvertedValue ) { unsigned int Digit; #define MAX_TENTH ( UINT_MAX / 10 ) *ConvertedValue = 0; while ( *Value >= '0' && *Value <='9') { Digit = *Value - '0'; /* * Check for overflow before multiplying. */ if ( *ConvertedValue > MAX_TENTH ) { *ConvertedValue = 0; return( -1 ); } *ConvertedValue *= 10; /* * Check for overflow before adding. */ if ( Digit > ( UINT_MAX - *ConvertedValue ) ) { *ConvertedValue = 0; return( -1 ); } *ConvertedValue += Digit; Value++; } if ( *Value == '+' ) { if ( *(Value + 1) == '\0' ) { return( 0 ); } else { *ConvertedValue = 0; return( -1 ); } } if ( *Value != '\0' ) { *ConvertedValue = 0; return( -1 ); } return( 0 ); } /*++ * Function: LockMutex * * Purpose: lock a mutex * * Parameters: ptr to the mutex * * Returns: nada -- exits on failure * * Authors: Dave McMurtrie * * Notes: *-- */ extern void LockMutex( pthread_mutex_t *mutex ) { char *fn = "LockMutex()"; int rc; rc = pthread_mutex_lock( mutex ); if ( rc ) { syslog(LOG_ERR, "%s: pthread_mutex_lock() failed [%d]: Exiting.", fn, rc ); exit( 1 ); } return; } /*++ * Function: UnLockMutex * * Purpose: unlock a mutex * * Parameters: ptr to the mutex * * Returns: nada -- exits on failure * * Authors: Dave McMurtrie * * Notes: *-- */ extern void UnLockMutex( pthread_mutex_t *mutex ) { char *fn = "UnLockMutex()"; int rc; rc = pthread_mutex_unlock( mutex ); if ( rc ) { syslog( LOG_ERR, "%s: pthread_mutex_unlock() failed [%d]: Exiting.", fn, rc ); exit( 1 ); } return; } /*++ * Function: Get_Server_conn * * Purpose: When a client login attempt is made, fetch a usable server * connection descriptor. This means that either we reuse an * existing ICD, or we open a new one. Hide that abstraction from * the caller... * * Parameters: ptr to username string * ptr to password string * const ptr to client hostname or IP string (for logging only) * in_port_t, client port number (for logging only) * unsigned char - flag to indicate that the client sent the * password as a string literal. * * Returns: ICD * on success * NULL on failure * * Authors: Dave McMurtrie * * Credit: Major SSL additions by Ken Murchison * *-- */ extern ICD_Struct *Get_Server_conn( char *Username, char *Password, const char *ClientAddr, in_port_t sin_port, unsigned char LiteralPasswd ) { char *fn = "Get_Server_conn()"; unsigned int HashIndex; ICC_Struct *HashEntry = NULL; char SendBuf[BUFSIZE]; unsigned int BufLen = BUFSIZE - 1; char md5pw[MD5_DIGEST_LENGTH]; char *tokenptr; char *endptr; char *last; ICC_Struct *ICC_Active; ICC_Struct *ICC_tptr; ITD_Struct Server; int rc; unsigned int Expiration; EVP_MD_CTX mdctx; int md_len; Expiration = PC_Struct.cache_expiration_time; memset( &Server, 0, sizeof Server ); /* need to md5 the passwd regardless, so do that now */ EVP_DigestInit(&mdctx, EVP_md5()); EVP_DigestUpdate(&mdctx, Password, strlen(Password)); EVP_DigestFinal(&mdctx, md5pw, &md_len); /* see if we have a reusable connection available */ ICC_Active = NULL; HashIndex = Hash( Username, HASH_TABLE_SIZE ); LockMutex( &mp ); /* * Now we just iterate through the linked list at this hash index until * we either find the string we're looking for or we find a NULL. */ for ( HashEntry = ICC_HashTable[ HashIndex ]; HashEntry; HashEntry = HashEntry->next ) { if ( ( strcmp( Username, HashEntry->username ) == 0 ) && ( HashEntry->logouttime > 1 ) ) { ICC_Active = HashEntry; /* * we found this username in our hash table. Need to know if * the password matches. */ if ( memcmp( md5pw, ICC_Active->hashedpw, sizeof md5pw ) ) { syslog( LOG_NOTICE, "%s: Unable to reuse server sd [%d] for user '%s' (%s:%d) because password doesn't match.", fn, ICC_Active->server_conn->sd, Username, ClientAddr, sin_port ); ICC_Active->logouttime = 1; } else { /* * We found a matching password on an inactive server socket. * We can use this guy. Before we release the mutex, set the * logouttime such that we mark this connection as "active" * again. */ ICC_Active->logouttime = 0; /* * The fact that we have this stored in a table as an open * server socket doesn't really mean that it's open. The * server could've closed it on us. * We need a speedy way to make sure this is still open. * We'll set the fd to non-blocking and try to read from it. * If we get a zero back, the connection is closed. If we get * EWOULDBLOCK (or some data) we know it's still open. If we * do read data, make sure we read all the data so we "drain" * any puss that may be left on this socket. */ fcntl( ICC_Active->server_conn->sd, F_SETFL, fcntl( ICC_Active->server_conn->sd, F_GETFL, 0) | O_NONBLOCK ); while ( ( rc = IMAP_Read( ICC_Active->server_conn, Server.ReadBuf, sizeof Server.ReadBuf ) ) > 0 ); if ( !rc ) { syslog(LOG_NOTICE, "%s: Unable to reuse server sd [%d] for user '%s' (%s:%d). Connection closed by server.", fn, ICC_Active->server_conn->sd, Username, ClientAddr, sin_port ); ICC_Active->logouttime = 1; continue; } if ( errno != EWOULDBLOCK ) { syslog(LOG_NOTICE, "%s: Unable to reuse server sd [%d] for user '%s' (%s:%d). IMAP_read() error: %s", fn, ICC_Active->server_conn->sd, Username, ClientAddr, sin_port, strerror( errno ) ); ICC_Active->logouttime = 1; continue; } fcntl( ICC_Active->server_conn->sd, F_SETFL, fcntl( ICC_Active->server_conn->sd, F_GETFL, 0) & ~O_NONBLOCK ); /* now release the mutex and return the sd to the caller */ UnLockMutex( &mp ); /* * We're reusing an existing server socket. There are a few * counters we have to deal with. */ IMAPCount->RetainedServerConnections--; IMAPCount->InUseServerConnections++; IMAPCount->TotalServerConnectionsReused++; if ( IMAPCount->InUseServerConnections > IMAPCount->PeakInUseServerConnections ) IMAPCount->PeakInUseServerConnections = IMAPCount->InUseServerConnections; syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) on existing sd [%d]", Username, ClientAddr, sin_port, ICC_Active->server_conn->sd ); return( ICC_Active->server_conn ); } } } UnLockMutex( &mp ); /* * We don't have an active connection for this user, or the password * didn't match. * Open a connection to the IMAP server so we can attempt to login */ Server.conn = ( ICD_Struct * ) malloc( sizeof ( ICD_Struct ) ); memset( Server.conn, 0, sizeof ( ICD_Struct ) ); Server.conn->sd = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP ); if ( Server.conn->sd == -1 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: Unable to open server socket: %s", Username, ClientAddr, sin_port, strerror( errno ) ); goto fail; } if ( PC_Struct.send_tcp_keepalives ) { int onoff = 1; setsockopt( Server.conn->sd, SOL_SOCKET, SO_KEEPALIVE, &onoff, sizeof onoff ); } if ( connect( Server.conn->sd, (struct sockaddr *)&ISD.srv, sizeof(ISD.srv) ) == -1 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: Unable to connect to IMAP server: %s", Username, ClientAddr, sin_port, strerror( errno ) ); goto fail; } /* Read & throw away the banner line from the server */ if ( IMAP_Line_Read( &Server ) == -1 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: No banner line received from IMAP server", Username, ClientAddr, sin_port ); goto fail; } /* * Sanity check. We don't deal with literal responses in the * banner string. */ if ( Server.LiteralBytesRemaining ) { syslog(LOG_ERR, "%s: Unexpected string literal in server banner response.", fn ); goto fail; } /* * Do STARTTLS if necessary. */ #if HAVE_LIBSSL if ( PC_Struct.login_disabled || PC_Struct.force_tls ) { snprintf( SendBuf, BufLen, "S0001 STARTTLS\r\n" ); if ( IMAP_Write( Server.conn, SendBuf, strlen(SendBuf) ) == -1 ) { syslog(LOG_INFO, "STARTTLS failed: IMAP_Write() failed attempting to send STARTTLS command to IMAP server: %s", strerror( errno ) ); goto fail; } /* * Read the server response */ if ( ( rc = IMAP_Line_Read( &Server ) ) == -1 ) { syslog(LOG_INFO, "STARTTLS failed: No response from IMAP server after sending STARTTLS command" ); goto fail; } if ( Server.LiteralBytesRemaining ) { syslog(LOG_ERR, "%s: Unexpected string literal in server response.", fn ); goto fail; } /* * Try to match up the tag in the server response to the client tag. */ endptr = Server.ReadBuf + rc; tokenptr = memtok( Server.ReadBuf, endptr, &last ); if ( !tokenptr ) { /* * no tokens found in server response? Not likely, but we still * have to check. */ syslog(LOG_INFO, "STARTTLS failed: server response to STARTTLS command contained no tokens." ); goto fail; } if ( memcmp( (const void *)tokenptr, (const void *)"S0001", strlen( tokenptr ) ) ) { /* * non-matching tag read back from the server... Lord knows what this * is, so we'll fail. */ syslog(LOG_INFO, "STARTTLS failed: server response to STARTTLS command contained non-matching tag." ); goto fail; } /* * Now that we've matched the tags up, see if the response was 'OK' */ tokenptr = memtok( NULL, endptr, &last ); if ( !tokenptr ) { /* again, not likely but we still have to check... */ syslog(LOG_INFO, "STARTTLS failed: Malformed server response to STARTTLS command" ); goto fail; } if ( memcmp( (const void *)tokenptr, "OK", 2 ) ) { /* * If the server sent back a "NO" or "BAD", we can look at the actual * server logs to figure out why. We don't have to break our ass here * putting the string back together just for the sake of logging. */ syslog(LOG_INFO, "STARTTLS failed: non-OK server response to STARTTLS command" ); goto fail; } Server.conn->tls = SSL_new( tls_ctx ); if ( Server.conn->tls == NULL ) { syslog(LOG_INFO, "STARTTLS failed: SSL_new() failed" ); goto fail; } SSL_clear( Server.conn->tls ); rc = SSL_set_fd( Server.conn->tls, Server.conn->sd ); if ( rc == 0 ) { syslog(LOG_INFO, "STARTTLS failed: SSL_set_fd() failed: %d", SSL_get_error( Server.conn->tls, rc ) ); goto fail; } SSL_set_connect_state( Server.conn->tls ); rc = SSL_connect( Server.conn->tls ); if ( rc <= 0 ) { syslog(LOG_INFO, "STARTTLS failed: SSL_connect() failed, %d: %s", SSL_get_error( Server.conn->tls, rc ), SSLerrmessage() ); goto fail; } /* XXX Should we grab the session id for later reuse? */ } #endif /* HAVE_LIBSSL */ /* * Send the login command off to the IMAP server. Have to treat a literal * password different. */ if ( LiteralPasswd ) { snprintf( SendBuf, BufLen, "A0001 LOGIN %s {%d}\r\n", Username, strlen( Password ) ); if ( IMAP_Write( Server.conn, SendBuf, strlen(SendBuf) ) == -1 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: IMAP_Write() failed attempting to send LOGIN command to IMAP server: %s", Username, ClientAddr, sin_port, strerror( errno ) ); goto fail; } /* * the server response should be a go ahead */ if ( ( rc = IMAP_Line_Read( &Server ) ) == -1 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: Failed to receive go-ahead from IMAP server after sending LOGIN command", Username, ClientAddr, sin_port ); goto fail; } if ( Server.LiteralBytesRemaining ) { syslog(LOG_ERR, "%s: Unexpected string literal in server banner response. Should be a continuation response.", fn ); goto fail; } if ( Server.ReadBuf[0] != '+' ) { syslog( LOG_INFO, "LOGIN: '%s' (%s:%d) failed: bad response from server after sending string literal specifier", Username, ClientAddr, sin_port ); goto fail; } /* * now send the password */ snprintf( SendBuf, BufLen, "%s\r\n", Password ); if ( IMAP_Write( Server.conn, SendBuf, strlen( SendBuf ) ) == -1 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: IMAP_Write() failed attempting to send literal password to IMAP server: %s", Username, ClientAddr, sin_port, strerror( errno ) ); goto fail; } } else { /* * just send the login command via normal means. */ snprintf( SendBuf, BufLen, "A0001 LOGIN %s %s\r\n", Username, Password ); if ( IMAP_Write( Server.conn, SendBuf, strlen(SendBuf) ) == -1 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: IMAP_Write() failed attempting to send LOGIN command to IMAP server: %s", Username, ClientAddr, sin_port, strerror( errno ) ); goto fail; } } /* * Read the server response. From RFC 3501: * * A server MAY include a CAPABILITY response code in the tagged OK * response to a successful LOGIN command in order to send * capabilities automatically. It is unnecessary for a client to * send a separate CAPABILITY command if it recognizes these * automatic capabilities. * * We have to be ready for the possibility that this might be an * untagged response... In an ideal world, we'd want to pass the * untagged stuff back to the client. For now, since the RFC doesn't * mandate that behaviour, we're not going to since we don't have a client * socket descriptor to send it to. */ for ( ;; ) { if ( ( rc = IMAP_Line_Read( &Server ) ) == -1 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: No response from IMAP server after sending LOGIN command", Username, ClientAddr, sin_port ); goto fail; } if ( Server.LiteralBytesRemaining ) { syslog(LOG_ERR, "%s: Unexpected string literal in server LOGIN response.", fn ); goto fail; } if ( Server.ReadBuf[0] != '*' ) break; } /* * Try to match up the tag in the server response to the client tag. */ endptr = Server.ReadBuf + rc; tokenptr = memtok( Server.ReadBuf, endptr, &last ); if ( !tokenptr ) { /* * no tokens found in server response? Not likely, but we still * have to check. */ syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: server response to LOGIN command contained no tokens.", Username, ClientAddr, sin_port ); goto fail; } if ( memcmp( (const void *)tokenptr, (const void *)"A0001", strlen( tokenptr ) ) ) { /* * non-matching tag read back from the server... Lord knows what this * is, so we'll fail. */ syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: server response to LOGIN command contained non-matching tag.", Username, ClientAddr, sin_port ); goto fail; } /* * Now that we've matched the tags up, see if the response was 'OK' */ tokenptr = memtok( NULL, endptr, &last ); if ( !tokenptr ) { /* again, not likely but we still have to check... */ syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: Malformed server response to LOGIN command", Username, ClientAddr, sin_port ); goto fail; } if ( memcmp( (const void *)tokenptr, "OK", 2 ) ) { /* * If the server sent back a "NO" or "BAD", we can look at the actual * server logs to figure out why. We don't have to break our ass here * putting the string back together just for the sake of logging. */ syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: non-OK server response to LOGIN command", Username, ClientAddr, sin_port ); goto fail; } /* * put this in our used list and remove it from the free list */ for( ; ; ) { LockMutex( &mp ); if ( ICC_free->next ) { /* generate the hash index */ HashIndex = Hash( Username, HASH_TABLE_SIZE ); /* temporarily store the address of the next free structure */ ICC_tptr = ICC_free->next; /* * We want to add the newest "used" structure at the front of * the list at the hash index. */ ICC_free->next = ICC_HashTable[ HashIndex ]; ICC_HashTable[ HashIndex ] = ICC_free; /* * less typing and more readability, set an "active" pointer. */ ICC_Active = ICC_free; /* now point the free listhead to the next available free struct */ ICC_free = ICC_tptr; /* fill in the newest used (oxymoron?) structure */ strncpy( ICC_Active->username, Username, sizeof ICC_Active->username ); ICC_Active->username[ sizeof ICC_Active->username - 1 ] = '\0'; memcpy( ICC_Active->hashedpw, md5pw, sizeof ICC_Active->hashedpw ); ICC_Active->logouttime = 0; /* zero means, "it's active". */ ICC_Active->server_conn = Server.conn; UnLockMutex( &mp ); IMAPCount->InUseServerConnections++; IMAPCount->TotalServerConnectionsCreated++; if ( IMAPCount->InUseServerConnections > IMAPCount->PeakInUseServerConnections ) IMAPCount->PeakInUseServerConnections = IMAPCount->InUseServerConnections; syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) on new sd [%d]", Username, ClientAddr, sin_port, Server.conn->sd ); return( Server.conn ); } /* * There weren't any free ICC structs. Try to free one. Make sure * we unlock the mutex, since ICC_Recycle needs to obtain it. */ UnLockMutex( &mp ); Expiration = abs( Expiration / 2 ); /* * Eventually, we have to fail */ if ( Expiration <= 2 ) { syslog(LOG_INFO, "LOGIN: '%s' (%s:%d) failed: Out of free ICC structs.", Username, ClientAddr, sin_port ); goto fail; } ICC_Recycle( Expiration ); } fail: #if HAVE_LIBSSL if ( Server.conn->tls ) { SSL_shutdown( Server.conn->tls ); SSL_free( Server.conn->tls ); } #endif close( Server.conn->sd ); free( Server.conn ); return( NULL ); } /*++ * Function: imparse_isatom * * Purpose: determine if a string is an "atom" according to RFC2060 * which basically means (if you convolute your way through * the BNF notation in the back of the RFC): 1 or more of * any character except"(" / ")" / "{" / SPACE / CTL / "%" / * "*". * * Parameters: const char ptr to the string to examine. * * Returns: non-zero if the string is an atom. * * Authors: This was taken directly and exactly from cyrus-imap-1.5.14 *-- */ extern int imparse_isatom( const char *s ) { int len = 0; if (!*s) return 0; for (; *s; s++) { len++; if (*s & 0x80 || *s < 0x1f || *s == 0x7f || *s == ' ' || *s == '{' || *s == '(' || *s == ')' || *s == '\"' || *s == '%' || *s == '*' || *s == '\\') return 0; } if (len >= 1024) return 0; return 1; } /*++ * Function: memtok * * Purpose: similar to strtok_r, except doesn't require a NULL * terminated string. Since this is only for IMAP use, * it also doesn't require a "separator" to be passed in, * it assumes a single space will always be the token separator. * * Parameters: char ptr. Beginning of buffer (or NULL) * char ptr. End of buffer. * ptr to char ptr. Last position in buffer (for * subsequent calls). * * Returns: char pointer to the first character of the token. * NULL pointer if no tokens are found. * * Authors: Dave McMurtrie *-- */ extern char *memtok( char *Begin, char *End, char **Last ) { char *CP; char *First; /* * If the Begin address is NULL, pick up one character beyond * where we left off. Check to make sure we didn't leave off at the * end of our buffer, though. */ if ( ! Begin ) { if ( *Last == End ) { return( NULL ); } else { First = *Last + 1; } } else { First = Begin; } if ( ! First ) return( NULL ); /* Look for a space */ CP = memchr( First, ' ', End - First ); /* * If we don't find a space, maybe we're at the last token in the line. * If that's the case, we should be able to find a CR. */ if ( !CP ) CP = memchr( First, '\r', End - First ); if ( ! CP ) return( NULL ); /* * If we found the token where we started out, we have a double space, * a /r/r, or a /r at the beginning of the line. In any case, it's not * correct as far as RFC2060 goes. */ if ( CP == First ) return( NULL ); *Last = CP; *CP = '\0'; return( First ); } /*++ * Function: IMAP_Write * * Purpose: Write a buffer to a socket. * * Parameters: ptr to a IMAPTransactionDescriptor structure * ptr to buffer * number of bytes in buffer * * Returns: number of bytes written on success * -1 on failure * * Authors: Ken Murchison (ken@oceana.com) * * Notes: *-- */ extern int IMAP_Write( ICD_Struct *ICD, const void *buf, int count ) { #if HAVE_LIBSSL if ( ICD->tls ) return SSL_write( ICD->tls, buf, count ); else #endif return write( ICD->sd, buf, count ); } /*++ * Function: IMAP_Read * * Purpose: Read IMAP data from a socket. * * Parameters: ptr to a IMAPTransactionDescriptor structure * ptr to buffer * number of bytes to read * * Returns: number of bytes read on success * -1 on failure * * Authors: Ken Murchison (ken@oceana.com) * * Notes: *-- */ extern int IMAP_Read( ICD_Struct *ICD, void *buf, int count ) { #if HAVE_LIBSSL if ( ICD->tls ) return SSL_read( ICD->tls, buf, count ); else #endif return read( ICD->sd, buf, count ); } /*++ * Function: IMAP_Literal_Read * * Purpose: Read IMAP string literals from a socket. * * Parameters: ptr to a IMAPTransactionDescriptor structure * * Returns: number of bytes read on success * -1 on failure * * Authors: Dave McMurtrie * * Notes: *-- */ extern int IMAP_Literal_Read( ITD_Struct *ITD ) { char *fn = "IMAP_Literal_Read()"; int Status; unsigned int i, j; struct pollfd fds[2]; nfds_t nfds; int pollstatus; /* * If there aren't any LiteralBytesRemaining, just return 0. */ if ( ! ITD->LiteralBytesRemaining ) return( 0 ); /* scoot the buffer */ for ( i = ITD->ReadBytesProcessed, j = 0; i <= ITD->BytesInReadBuffer; i++, j++ ) { ITD->ReadBuf[j] = ITD->ReadBuf[i]; } ITD->BytesInReadBuffer -= ITD->ReadBytesProcessed; ITD->ReadBytesProcessed = 0; /* * If we have any data in our buffer, return what we have. */ if ( ITD->BytesInReadBuffer > 0 ) { if ( ITD->BytesInReadBuffer >= ITD->LiteralBytesRemaining ) { ITD->ReadBytesProcessed = ITD->LiteralBytesRemaining; ITD->LiteralBytesRemaining = 0; return( ITD->ReadBytesProcessed ); } else { ITD->ReadBytesProcessed += ITD->BytesInReadBuffer; ITD->LiteralBytesRemaining -= ITD->BytesInReadBuffer; return( ITD->ReadBytesProcessed ); } } /* * No data left in the buffer. Have to call read. Read either * the number of literal bytes left, or the rest of our buffer -- * whichever is smaller. * */ nfds = 1; fds[0].fd = ITD->conn->sd; fds[0].events = POLLIN; fds[0].revents = 0; pollstatus = poll( fds, nfds, POLL_TIMEOUT ); if ( !pollstatus ) { syslog( LOG_ERR, "%s: poll() for data on sd [%d] timed out.", fn, ITD->conn->sd ); return( -1 ); } if ( pollstatus < 0 ) { syslog( LOG_ERR, "%s: poll() for data on sd [%d] failed: %s.", fn, ITD->conn->sd, strerror( errno ) ); return( -1 ); } if ( !( fds[0].revents & POLLIN ) ) { syslog( LOG_ERR, "%s: poll() for data on sd [%d] returned nothing.", fn, ITD->conn->sd ); return( -1 ); } Status = IMAP_Read(ITD->conn, &ITD->ReadBuf[ITD->BytesInReadBuffer], (sizeof ITD->ReadBuf - ITD->BytesInReadBuffer ) ); if ( Status == 0 ) { syslog(LOG_WARNING, "%s: connection closed prematurely.", fn); return(-1); } else if ( Status == -1 ) { syslog(LOG_ERR, "%s: IMAP_Read() failed: %s", fn, strerror(errno) ); return(-1); } /* * update the bytes remaining and return the byte count read. */ ITD->BytesInReadBuffer += Status; if ( ITD->BytesInReadBuffer >= ITD->LiteralBytesRemaining ) { ITD->ReadBytesProcessed = ITD->LiteralBytesRemaining; ITD->LiteralBytesRemaining = 0; return( ITD->ReadBytesProcessed ); } else { ITD->ReadBytesProcessed += ITD->BytesInReadBuffer; ITD->LiteralBytesRemaining -= ITD->BytesInReadBuffer; return( ITD->ReadBytesProcessed ); } } /*++ * Function: IMAP_Line_Read * * Purpose: Line-oriented buffered reads from the imap server * * Parameters: ptr to a IMAPTransactionDescriptor structure * * Returns: Number of bytes on success * -1 on error * * Authors: Dave McMurtrie * * Notes: caller must be certain that the IMAPTransactionDescriptor * is initialized to zero on the first call. * * * Callers must check RemainingLiteralBytes after calling this * function. If this is set and a caller ignores it, it will * play havoc... Actually, it will exit() and kill the entire * process. *-- */ extern int IMAP_Line_Read( ITD_Struct *ITD ) { char *CP; int Status; unsigned int i, j; int rc; char *fn = "IMAP_Line_Read()"; char *EndOfBuffer; /* * Sanity check. This function should never be called if there are * literal bytes remaining to be processed. */ if ( ITD->LiteralBytesRemaining ) { syslog(LOG_ERR, "%s: Sanity check failed! Literal bytestream has not been fully processed (%d bytes remain) and line-oriented read function was called again. Exiting!", fn, ITD->LiteralBytesRemaining); exit(1); } /* Point End to the end of our buffer */ EndOfBuffer = &ITD->ReadBuf[sizeof ITD->ReadBuf - 1]; /* * Shift the contents of our buffer. This will erase any previous * line that we already gave to a caller. */ for ( i = ITD->ReadBytesProcessed, j = 0; i <= ITD->BytesInReadBuffer; i++, j++ ) { ITD->ReadBuf[j] = ITD->ReadBuf[i]; } ITD->BytesInReadBuffer -= ITD->ReadBytesProcessed; ITD->ReadBytesProcessed = 0; for (;;) { /* * If we've been called before, we may already have another line * in the buffer that we can return to the caller. */ CP = (char *)memchr( ITD->ReadBuf, '\n', ITD->BytesInReadBuffer ); if ( CP ) { /* * found a '\n'. Check to see if it's preceded by a '\r' * but make sure we catch the case where '\n' is the first * character sent. If we find this, it could be the result * of a "more data" scenerio. */ if ( CP != ITD->ReadBuf ) { /* reset the moredata flag */ ITD->MoreData = 0; if ( *(CP - 1) == '\r' ) { /* * Set ReadBytesProcessed to the length of the line * we just found. Just subtract the address of the * beginning of the buffer from the address of our * "/n" in the buffer. Always need to add one. */ ITD->ReadBytesProcessed = ( CP - ITD->ReadBuf + 1); /* * As if this isn't already ugly enough, now we have * to check whether this is a line that indicates a * string literal is coming next. How do we know? * If it is, the line will end with {bytecount}. */ if ( ((CP - ITD->ReadBuf + 1) > 2 ) && ( *(CP - 2) == '}' )) { char *LiteralEnd; char *LiteralStart; LiteralStart = NULL; /* * Found a '}' as the last character on the line. * Save it's place and then look for the * beginning '{'. */ LiteralEnd = CP - 2; CP -= 2; for ( ; CP >= ITD->ReadBuf; CP-- ) { if ( *CP == '{' ) { LiteralStart = CP; break; } } if ( LiteralStart ) { /* * We found an opening and closing { } pair. The * thing in between should be a number specifying * a byte count. That would be as much as we needed * to know if it wasn't for the fact that RFC 2088 * allows for a + sign in the literal specification * that has a different meaning when a client is * sending a literal to a server. */ if ( *(LiteralEnd - 1) == '+' ) { ITD->NonSyncLiteral = 1; } else if ( *(LiteralEnd - 1) == '{' ) { /* * This is a {}. No clue why any client * would ever send this, but just pretend * we never saw it. */ return( ITD->ReadBytesProcessed ); } else { ITD->NonSyncLiteral = 0; } /* To grab the number, bump our * starting char pointer forward a byte and temporarily * turn the closing '}' into a NULL. Don't worry about * the potential '+' sign, atoui won't care. */ LiteralStart++; *LiteralEnd = '\0'; rc = atoui( LiteralStart, &ITD->LiteralBytesRemaining ); if ( rc == -1 ) { syslog(LOG_WARNING, "%s: atoui() failed on string '%s'", fn, LiteralStart ); *LiteralEnd = '}'; return(0); } *LiteralEnd = '}'; } } /* * This looks redundant, huh? */ return( ITD->ReadBytesProcessed ); } else { /* * found a '\n' that's not preceded by a '\r'. */ syslog(LOG_WARNING, "%s: Protocol error. Line terminated by LF, not CRLF", fn); return(-1); } } else { /* * We just processed a line that has '\n' as the first * character. This may or may not be a problem. */ if ( ITD->MoreData ) { /* * When we set the MoreData flag, we return 20 bytes fewer * than what's in our buffer. It's possible for that * to send the CR but not the LF back to the caller, * leaving it as the first thing in the buffer now. * we can't be sure that this is the case, but the client * and the server can fight it out if it's not. */ ITD->ReadBytesProcessed = 1; return( ITD->ReadBytesProcessed ); } else { syslog(LOG_WARNING, "%s: Protocol error. Line begins with LF.", fn); return(-1); } } } /* * There weren't any "lines" in our buffer. We need to get more data * from the server. Ultimately, we'll want to call IMAP_Read and * add on to the end of the existing buffer. * * Before we go off and wildly start calling IMAP_Read() we should * really make sure that we have space left in our buffer. If not, * set the "more to come" flag and return what we have to the caller. */ if ( ( sizeof ITD->ReadBuf - ITD->BytesInReadBuffer ) < 1 ) { /* * less than one byte of storage left in our buffer. Return what * we have, but get a little bit tricky -- don't return everything * that we have. The reason for this is because the last thing * in our buffer right now could just happen to be {80 (the * beginning of a string literal specifier). If we just return * this and come back to start reading more, we'll completely miss * the fact that a literal is coming next. If we return all but 20 * bytes of the current buffer, we completely negate that potential * problem. */ ITD->MoreData = 1; CP = EndOfBuffer - 20; /* * Set ReadBytesProcessed to the length of the line * we're gonna return. */ ITD->ReadBytesProcessed = ( CP - ITD->ReadBuf + 1); return( ITD->ReadBytesProcessed ); } Status = IMAP_Read(ITD->conn, &ITD->ReadBuf[ITD->BytesInReadBuffer], (sizeof ITD->ReadBuf - ITD->BytesInReadBuffer ) ); if ( Status == 0 ) { syslog(LOG_WARNING, "%s: connection closed prematurely.", fn); return(-1); } else if ( Status == -1 ) { syslog(LOG_ERR, "%s: IMAP_Read() failed: %s", fn, strerror(errno) ); return(-1); } /* * update the buffer count and head back to the top of the * for loop. */ ITD->BytesInReadBuffer += Status; } } /* * _________ * / | * / | * / ______| * / / ________ * | | | / * | | |_____/ * | | ______ * | | | \ * | | |______\ * \ \_______ * \ | * \ | * \_________| */