/* * Copyright (c) 1985 Regents of the University of California. * All rights reserved. * * Redistribution and use in source and binary forms are permitted provided * that: (1) source distributions retain this entire copyright notice and * comment, and (2) distributions including binaries display the following * acknowledgement: ``This product includes software developed by the * University of California, Berkeley and its contributors'' in the * documentation or other materials provided with the distribution and in * all advertising materials mentioning features or use of this software. * Neither the name of the University nor the names of its contributors may * be used to endorse or promote products derived from this software without * specific prior written permission. * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. */ /*++ Copyright (c) 1994 Microsoft Corporation Module Name : address.cxx Abstract: This module defines the module for the address (CAddr) class. Author: Rohan Phillips ( Rohanp ) 11-Dec-1995 Project: SMTP Server DLL Functions Exported: Revision History: --*/ /************************************************************ * Include Headers ************************************************************/ #include #include #include #include #include #include #include #include //initialize the pool CPool CAddr::Pool(ADDRESS_SIGNATURE_VALID); #if defined(TDC) LPFNAB_FREE_MEMORY CAddr::pfnABFreeMemory = NULL; #endif // // Statics // static BOOL pStripAddrQuotes( char *lpszAddress, char **lpDomain ); static BOOL pStripAddrSpaces(char *lpszAddress); // Quick and dirty string validation static BOOL pValidateStringPtr(LPSTR lpwszString, DWORD dwMaxLength) { if (IsBadStringPtr((LPCTSTR)lpwszString, dwMaxLength)) return(FALSE); while (dwMaxLength--) if (*lpwszString++ == 0) return(TRUE); return(FALSE); } //This routine scans an address for illegal characters //in a name BOOL IsInvalidAddr(char *Address) { char * addr = Address; for (; *addr != '\0'; addr++) { if ((*addr & 0340) == 0200) break; } if (*addr == '\0') { return FALSE; } SetLastError (ERROR_INVALID_DATA); return TRUE; } // Forward declaration of pValidateLocalPartOrDomain BOOL pValidateLocalPartOrDomain(char *lpszStart, DWORD dwLength, BOOL fLocalPart); /*++ Name : CAddr:CAddr Description: This is the default constructor for this class. It just initializes the member variables. Arguments: None Returns: nothing Limitations: --*/ CAddr::CAddr(void) { m_Signature = ADDRESS_SIGNATURE_VALID; m_Flags = ADDRESS_NO_DOMAIN; m_PlainAddrSize = 0; m_DomainOffset = NULL; m_HashInfo = NULL; m_Error = 0; m_listEntry.Flink = NULL; m_listEntry.Blink = NULL; } CAddr::~CAddr(VOID) { m_Signature = ADDRESS_SIGNATURE_FREE; m_Flags = 0; m_listEntry.Flink = NULL; m_listEntry.Blink = NULL; m_HashInfo = NULL; } /*++ Name : CAddr:CAddr(char * address) Description: This is the default constructor for this class. It just initializes the member variables. Arguments: None Returns: nothing Limitations: --*/ CAddr::CAddr(char * Address) { m_Signature = ADDRESS_SIGNATURE_VALID; m_Flags = ADDRESS_NO_DOMAIN; m_PlainAddrSize = 0; m_DomainOffset = NULL; m_HashInfo= NULL; m_Error = 0; m_listEntry.Flink = NULL; m_listEntry.Blink = NULL; if(Address == NULL) { SetLastError(ERROR_INVALID_DATA); return; } // This is an email name, extract its clean representation ExtractCleanEmailName( m_PlainAddress, &m_DomainOffset, &m_PlainAddrSize, Address); if (m_PlainAddrSize > MAX_INTERNET_NAME) { m_PlainAddrSize = 0; SetLastError (ERROR_INVALID_DATA); } else { if(m_DomainOffset) m_Flags &= (DWORD) ~ADDRESS_NO_DOMAIN; //turn off the no domain flag. } } /*++ Name : CAddr::InitializeAddress Description: This function parses an RFC address, stripping out all comments, and gets back an internet address Arguments: Address - RCF address from client ADDRTYPE - specify if address came from FROM or RCPT command Returns: TRUE if the RFC addressed was correctly parsed. FALSE otherwise. --*/ BOOL CAddr::InitializeAddress(char * Address, ADDRTYPE NameType) { if(::IsInvalidAddr(Address)) return(FALSE); m_PlainAddress[0] = '\0'; m_DomainOffset = NULL; m_PlainAddrSize = 0; // We have a special case for domains treated as addresses if (NameType == CLEANDOMAIN) { DWORD AddressSize = 0; char szCleanAddress[MAX_DOMAIN_NAME+1]; AddressSize = lstrlen(Address); if (AddressSize > MAX_DOMAIN_NAME) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } // We want an ABSOLUTELY clean domain name, but we will strip // out leading and trailing spaces for them lstrcpy(szCleanAddress, Address); if (!pStripAddrSpaces(szCleanAddress)) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } AddressSize = lstrlen(szCleanAddress); if (!pValidateLocalPartOrDomain(szCleanAddress, AddressSize, FALSE)) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } // This is a clean domain name, fill in the fields, and // return a positively lstrcpy(m_PlainAddress, szCleanAddress); m_PlainAddrSize = lstrlen(szCleanAddress); m_Flags &= (DWORD) ~ADDRESS_NO_DOMAIN; return(TRUE); } // This is an email name, extract its clean representation if (!ExtractCleanEmailName( m_PlainAddress, &m_DomainOffset, &m_PlainAddrSize, Address)) return(FALSE); // Take care of the flag if (!m_DomainOffset) m_Flags &= (DWORD) ~ADDRESS_NO_DOMAIN; // Further, we validate the whole local-part[@domain] email name // Special case: if the address is <>, then we skip the validation if (strcmp(m_PlainAddress, "<>")) { if (!ValidateCleanEmailName(m_PlainAddress, m_DomainOffset)) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } } else if (NameType != FROMADDR) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } // Take care of other anomalies ... if (m_DomainOffset) { // Make sure there is a local-part if (m_DomainOffset <= m_PlainAddress) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } // EMPIRE WTOP Bug 47233: Need to accept longer email names if they add up // to less than MAX_INTERNET_NAME. This is for replication of users across // a site connector. Due to this reason, we removed the user name check to // make sure it is less than MAX_EMAIL_NAME. } else { // We have no domain, make sure it is not an empty string if (m_PlainAddress[0] == '\0') { SetLastError(ERROR_INVALID_DATA); return(FALSE); } // EMPIRE WTOP Bug 47233: Need to accept longer email names if they add up // to less than MAX_INTERNET_NAME. This is for replication of users across // a site connector. Due to this reason, we removed the user name check to // make sure it is less than MAX_EMAIL_NAME. } return(TRUE); } /*++ Name : CAddr::CreateAddress Description: This function allocates memory for a CAddr class and calls StripAddrComments to Arguments: pszDest - Destination where new address should go pszSrc - Source buffer of address to strip comments from Returns: a pointer to a CAddr if comments were stripped and the format of the address is legal. NULL otherwise --*/ CAddr * CAddr::CreateAddress(char * Address, ADDRTYPE NameType) { CAddr * NewAddress; //create the memory for the address NewAddress = new CAddr (); if (!NewAddress) { SetLastError (ERROR_NOT_ENOUGH_MEMORY); return NULL; } //now initialize it if (!NewAddress->InitializeAddress(Address, NameType)) { delete NewAddress; return NULL; } //we have a good address...or so we think return NewAddress; } /*++ Name : CAddr::CreateKnownAddress Description: This function allocates memory for a CAddr class and sets "Address" as the internet address for this class. No error checking is done. It is assumed that the caller performed all necessary error checking Arguments: Address - Internet Address to initialize the class with Returns: a pointer to a CAddr if memory could be allocated NULL otherwise --*/ CAddr * CAddr::CreateKnownAddress(char * Address) { CAddr * NewAddress = NULL; //create the memory for the address NewAddress = new CAddr (Address); if (!NewAddress) { SetLastError (ERROR_NOT_ENOUGH_MEMORY); return NULL; } if(!NewAddress->GetAddrSize()) { delete NewAddress; NewAddress = NULL; } return NewAddress; } /*++ Name : CAddr::ReplaceAddress Description: Replaces the address stored in this CAddr with the one passed in Arguments: Address - new address Returns: TRUE if the address could be replaced. FALSE if we run out of memory, the new address is NULL, or the size of the address is zero --*/ BOOL CAddr::ReplaceAddress(const char * Address) { _ASSERT(IsValid()); if(Address == NULL) { SetLastError(ERROR_INVALID_DATA); return FALSE; } ExtractCleanEmailName( m_PlainAddress, &m_DomainOffset, &m_PlainAddrSize, (char *)Address); if (m_PlainAddrSize > MAX_INTERNET_NAME) { SetLastError(ERROR_INVALID_DATA); return FALSE; } if(m_DomainOffset) m_Flags &= (DWORD) ~ADDRESS_NO_DOMAIN; //turn off the no domain flag. return TRUE; } /*++ Name : CAddr::CAddr Description: returns a pointer to the 1st CAddr in the local list Arguments: a pointer to a PLIST_ENTRY Returns: --*/ CAddr * CAddr::GetFirstAddress(PLIST_ENTRY HeadOfList, PLIST_ENTRY * AddressLink) { PLIST_ENTRY ListEntry = NULL; CAddr * FirstAddress = NULL; //if the list is not empty, get the first address if(!IsListEmpty (HeadOfList)) { ListEntry = HeadOfList->Flink; FirstAddress = CONTAINING_RECORD( ListEntry, CAddr, m_listEntry); _ASSERT(FirstAddress->IsValid()); *AddressLink = ListEntry->Flink; //get the next link } return FirstAddress; } /*++ Name : CAddr::GetNextAddress Description: returns a pointer to the next CAddr in the local list Arguments: a pointer to a PLIST_ENTRY Returns: --*/ CAddr * CAddr::GetNextAddress(PLIST_ENTRY HeadOfList, PLIST_ENTRY * AddressLink) { CAddr * NextAddress = NULL; //make sure we are not at the end of the list if((*AddressLink) != HeadOfList) { NextAddress = CONTAINING_RECORD(*AddressLink, CAddr, m_listEntry); _ASSERT(NextAddress->IsValid()); *AddressLink = (*AddressLink)->Flink; } return NextAddress; } /*++ Name : CAddr::RemoveAllAddrs Description: Deletes all address from a CAddr list Arguments: a pointer to the head of the list Returns: --*/ void CAddr::RemoveAllAddrs(PLIST_ENTRY HeadOfList) { PLIST_ENTRY pEntry; CAddr * pAddr; // Remove all addresses while (!IsListEmpty (HeadOfList)) { pEntry = RemoveHeadList(HeadOfList); pAddr = CONTAINING_RECORD( pEntry, CAddr, m_listEntry); delete pAddr; } } /*++ Name : CAddr::RemoveAddress Description: removes an address from a list Arguments: a pointer to a PLIST_ENTRY Returns: --*/ void CAddr::RemoveAddress(IN OUT CAddr * pEntry) { if(pEntry != NULL) { _ASSERT(pEntry->IsValid()); //Remove from list of addresses RemoveEntryList( &pEntry->QueryListEntry()); } } /*++ Name : CAddr::InsertAddrHeadList Description: insert an address into the head of a list Arguments: a pointer to a PLIST_ENTRY Returns: --*/ void CAddr::InsertAddrHeadList(PLIST_ENTRY HeadOfList, CAddr *pEntry) { _ASSERT(pEntry); _ASSERT(pEntry->IsValid()); InsertHeadList(HeadOfList, &pEntry->QueryListEntry()); } /*++ Name : CAddr::InsertAddrTailList Description: insert an address into the head of a list Arguments: a pointer to a PLIST_ENTRY Returns: --*/ void CAddr::InsertAddrTailList(PLIST_ENTRY HeadOfList, CAddr *pEntry) { _ASSERT(pEntry); _ASSERT(pEntry->IsValid()); InsertTailList(HeadOfList, &pEntry->QueryListEntry()); } // ======================================================================== // // Validation Parser stuff added by KeithLau on 7/25/96 // #define QUOTE_SQUARE 0x1 #define QUOTE_ANGLE 0x2 #define QUOTE_QUOTES 0x4 #define QUOTE_PARENTHESES 0x8 #define MAX_QUOTE_TYPES 3 static char acOpen[MAX_QUOTE_TYPES] = { '[', '<', '\"' }; static char acClose[MAX_QUOTE_TYPES] = { ']', '>', '\"' }; static char *pFindNextUnquotedOccurrence(char *lpszString, char cSearch, LPBOOL lpfNotFound, DWORD dwDefaultState); static inline BOOL IsControl(char ch) { return( ((ch >= 0) && (ch <= 31)) || (ch == 127) ); } static BOOL IsSpecial(char ch) { switch (ch) { case '(': case ')': case '<': case '>': case '@': case ',': case ':': case ';': case '\\': case '\"': case '.': case '[': case ']': return(TRUE); default: return(FALSE); } } static BOOL pIsSpecialOrSpace(char ch) { return((ch == ' ') || (ch == '\t') || (ch == '\0') || IsSpecial(ch)); } static BOOL pValidateAsciiString(char *lpszString) { // Verifies that a string only contains ASCII chars (0-127) // Relies totally on upstream pointer checking _ASSERT(lpszString); if (!lpszString) return(FALSE); while (*lpszString) { if ((*lpszString >= 0) && (*lpszString <= 127)) lpszString++; else return(FALSE); } return(TRUE); } static BOOL pValidateAtom(char *lpszStart, DWORD dwLength) { // Atoms can be any ASCII char, except specials, controls, and spaces // Note zero-length atom is invalid _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); if (!dwLength) return(FALSE); while (dwLength) { dwLength--; if ((*lpszStart == ' ') || (IsSpecial(*lpszStart)) || (IsControl(*lpszStart)) ) { // Process quoted (escape) char if (*lpszStart == '\\') { if (!dwLength) return(FALSE); else { lpszStart++; dwLength--; } } else return(FALSE); } lpszStart++; } return(TRUE); } static BOOL pValidateAtomNoWildcard(char *lpszStart, DWORD dwLength) { // Atoms can be any ASCII char, except specials, controls, and spaces // Note zero-length atom is invalid _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); if (!dwLength) return(FALSE); while (dwLength) { // Apart from the usual, we also dislike the asterisk wildcard character. // This is just for domains. dwLength--; if ((*lpszStart == ' ') || (*lpszStart == '*') || (IsSpecial(*lpszStart)) || (IsControl(*lpszStart)) ) return(FALSE); lpszStart++; } return(TRUE); } static BOOL pValidateQtext(char *lpszStart, DWORD dwLength) { // Qtext can be any ASCII char, except '\"', '\\', and CR // Note zero-length is valid _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); while (dwLength) { dwLength--; if ((*lpszStart == '\"') || (*lpszStart == '\r') ) return(FALSE); // Process quoted (escape) char if (*lpszStart == '\\') { if (!dwLength) return(FALSE); else { lpszStart++; dwLength--; } } lpszStart++; } return(TRUE); } static BOOL pValidateDtext(char *lpszStart, DWORD dwLength) { // Dtext can be any ASCII char, except '\"', '\\', and CR // Note zero-length is valid _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); while (dwLength) { dwLength--; if ((*lpszStart == '[') || (*lpszStart == ']') || (*lpszStart == '\r') ) return(FALSE); // Process quoted (escape) char if (*lpszStart == '\\') { if (!dwLength) return(FALSE); else { lpszStart++; dwLength--; } } lpszStart++; } return(TRUE); } static BOOL pValidateQuotedString(char *lpszStart, DWORD dwLength) { // Quoted-stirngs are quotes between Qtext // an empty Qtext is valid _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); if (dwLength < 2) return(FALSE); if ((*lpszStart != '\"') || (lpszStart[dwLength-1] != '\"')) return(FALSE); return(pValidateQtext(lpszStart + 1, dwLength - 2)); } static BOOL pValidateDomainLiteral(char *lpszStart, DWORD dwLength) { // Domain-literals are square braces between Dtext // an empty Dtext is valid _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); if (dwLength < 2) return(FALSE); if ((*lpszStart != '[') || (lpszStart[dwLength-1] != ']')) return(FALSE); return(pValidateDtext(lpszStart + 1, dwLength - 2)); } static BOOL pValidateWord(char *lpszStart, DWORD dwLength) { // A word is any sequence of atoms and quoted-strings // words may not be zero-length by inheritance _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); if ((pValidateAtom(lpszStart, dwLength)) || (pValidateQuotedString(lpszStart, dwLength))) return(TRUE); return(FALSE); } static BOOL pValidateSubdomain(char *lpszStart, DWORD dwLength) { // A subdomain may be an atom or domain-literal // words may not be zero-length by inheritance _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); if ((pValidateAtomNoWildcard(lpszStart, dwLength)) || (pValidateDomainLiteral(lpszStart, dwLength))) return(TRUE); return(FALSE); } // // Since validating the local part is so similar to validating the // domain part, we write one function to do both. If fLocalPart // is TRUE, we treat it as the local part of an email address, if // it is FALSE, we treat it as the domain part. // static BOOL pValidateLocalPartOrDomain(char *lpszStart, DWORD dwLength, BOOL fLocalPart) { // A domain is one or more dot-delimited sub-domains, where a local-part // is one or more dot-delimited words // We rely on upstream calls to make sure the string is properly // NULL-terminated char *lpszBegin, *lpszEnd; BOOL fNotFound; _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); lpszBegin = lpszStart; lpszEnd = (char *)NULL; while (lpszEnd = pFindNextUnquotedOccurrence(lpszBegin, '.', &fNotFound, 0)) { // If it starts with a dot or has 2 dots in a row, we fail! if (lpszBegin == lpszEnd) return(FALSE); // Now, check if the chunk is indeed a valid token if (fLocalPart) { if (!pValidateWord(lpszBegin, (DWORD)(lpszEnd - lpszBegin))) return(FALSE); } else { if (!pValidateSubdomain(lpszBegin, (DWORD)(lpszEnd - lpszBegin))) return(FALSE); } // Allright, go to the next guy lpszBegin = lpszEnd + 1; } if (!fNotFound) return(FALSE); // If it ends with a dot, it's also bad lpszEnd = lpszStart + dwLength; if (lpszEnd == lpszBegin) return(FALSE); // Don't forget the last chunk if (fLocalPart) return(pValidateWord(lpszBegin, (DWORD)(lpszEnd - lpszBegin))); else return(pValidateSubdomain(lpszBegin, (DWORD)(lpszEnd - lpszBegin))); } static BOOL pValidatePhrase(char *lpszStart, DWORD dwLength) { // A phrase is a collection of words, possibly separated // by spaces // We don't validate for now ... _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); return(TRUE); } // // The following functions attempt to extract a clean addr-spec // in the form of local-part[@domain] from a generic address // Note that groups are not supported at this point // static BOOL pExtractAddressFromRouteAddress(char *lpszStart, DWORD dwLength, char *lpCleanAddress) { // A route address is in the form: // phrase < [1#(@ domain) :] addr-spec > char *lpMarker; char *lpRoute; BOOL fNotFound; _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); // First, find the opening angle brace lpMarker = pFindNextUnquotedOccurrence(lpszStart, '<', &fNotFound, 0); if (!lpMarker) return(FALSE); // Between lpStart and lpMarker is the phrase _ASSERT(lpMarker >= lpszStart); if (!pValidatePhrase(lpszStart, (DWORD)(lpMarker - lpszStart))) return(FALSE); // Now, find the closing angle bracket _ASSERT(*lpMarker == '<'); lpszStart = lpMarker + 1; lpMarker = pFindNextUnquotedOccurrence(lpszStart, '>', &fNotFound, QUOTE_ANGLE); if (!lpMarker) return(FALSE); _ASSERT(*lpMarker == '>'); // There should be nothing but white space after the closing brace //Trim the whitespace or flag an error char * lpTemp = lpMarker; while(*++lpTemp != '\0') { if(*lpTemp != ' ' && *lpTemp != '\t') return(FALSE); } *(lpMarker + 1) = '\0'; // The special address <> is reserved for NDR, and should be // allowed if (lpszStart == lpMarker) { lpCleanAddress[0] = '<'; lpCleanAddress[1] = '>'; lpCleanAddress[2] = '\0'; return(TRUE); } // The stuff enclosed in the angle braces is an addr-spec, and // optionally preceded by route specifiers. We don't care about // route specifiers, but we have to skip them. lpRoute = pFindNextUnquotedOccurrence(lpszStart, ':', &fNotFound, 0); if (!lpRoute) { // If we have no colon, then the whole lump should be the // addr-spec _ASSERT(lpMarker >= lpszStart); *lpMarker++ = '\0'; lstrcpyn(lpCleanAddress, lpszStart, (DWORD)(lpMarker - lpszStart)); return(TRUE); } else { // We found a colon, the stuff to its right should be the // addr-spec. As usual, we don't validate the route specifiers lpRoute++; _ASSERT(lpMarker >= lpRoute); *lpMarker++ = '\0'; lstrcpyn(lpCleanAddress, lpRoute, (DWORD)(lpMarker - lpRoute)); return(TRUE); } } static BOOL pExtractAddressFromMailbox( char *lpszStart, DWORD dwLength, char *lpCleanAddress) { // A mailbox can either be an addr-spec, or a route address _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); if (pExtractAddressFromRouteAddress(lpszStart, dwLength, lpCleanAddress)) return(TRUE); // If it's not a route address, trhen it should ba an addr-spec lstrcpyn(lpCleanAddress, lpszStart, dwLength + 1); return(TRUE); } static BOOL pExtractAddressFromGroup( char *lpszStart, DWORD dwLength, char *lpCleanAddress) { // We always return false return(FALSE); /* // A group is in the form: // phrase : [#mailbox] ; char *lpMarker; char *lpSemiColon; BOOL fNotFound; _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); // First, find the opening angle brace lpMarker = pFindNextUnquotedOccurrence(lpszStart, ':', &fNotFound, 0); if (!lpMarker || fNotFound) return(FALSE); // Between lpStart and lpMarker is the phrase _ASSERT(lpMarker >= lpszStart); if (!pValidatePhrase(lpszStart, (DWORD)(lpMarker - lpszStart))) return(FALSE); // Between the colon lpMarker++; lpSemiColon = pFindNextUnquotedOccurrence(lpMarker, ';', &fNotFound, 0); if (!lpSemiColon || fNotFound) return(FALSE); _ASSERT(lpSemiColon >= lpMarker); if (lpSemiColon == lpMarker) ; */ } static BOOL pExtractAddress( char *lpszStart, DWORD dwLength, char *lpCleanAddress) { // A address is either an mailbox, or a group _ASSERT(!IsBadStringPtr(lpszStart, dwLength)); *lpCleanAddress = '\0'; if (pExtractAddressFromMailbox(lpszStart, dwLength, lpCleanAddress) || pExtractAddressFromGroup(lpszStart, dwLength, lpCleanAddress)) return(TRUE); return(FALSE); } // This function finds braces pairs in a given string, and returns // pointers to the start and end of the first occurence of a // [nested] pair of braces. The starting and ending character // may be specified by the caller (starting and ending chars // must be unique). static char *pFindNextUnquotedOccurrence(char *lpszString, char cSearch, LPBOOL lpfNotFound, DWORD dwDefaultState) { DWORD dwState = dwDefaultState; DWORD i; char ch; char *lpStart = lpszString; BOOL fFallThru; *lpfNotFound = FALSE; // If dwState is 0, then we are not inside any kind of quotes while (ch = *lpStart) { // If we are not in any quotes, and the char is found, // then we are done! if (!dwState && (ch == cSearch)) return(lpStart); // Another disgusting kludge, BUT WORKS!! // For closing parentheses, we don't it in the // acClose[] set, so we have to explicitly check for them // here, since pMatchParentheses is very dependent on this. // For open parentheses, similar case: since parentheses // nest, we allow multiple open braces if (dwState == QUOTE_PARENTHESES) { if (((cSearch == ')') || (cSearch == '(')) && (ch == cSearch)) return(lpStart); } // If it is a quoted char, we can skip it and the following // char right away ... If the char following a quote '\' is // the terminating NULL, we have an error. if (ch == '\\') { lpStart++; if (!*lpStart) return(NULL); // Quoted pairs not allowed outside quotes! // if (!dwState) // return(NULL); } else { // See if we have a state change ... for (i = 0; i < MAX_QUOTE_TYPES; i++) { // Check the close case, too fFallThru = TRUE; // See if we have an opening quote of any sort if (ch == acOpen[i]) { // If it is an open brace, it shouldn't be a close brace // EXCEPT: quotes. fFallThru = FALSE; // Special case for quotes: open = close if (dwState & (1 << i)) { // This is not a quoted pair, error! // If it is a quote, and if the current state is // inside quotes, then we let it if ((ch == '\"') && (dwState == QUOTE_QUOTES)) fFallThru = TRUE; else return(NULL); } else if (!dwState) { // We are not in any quotes, so we can safely // claim we are now inside quotes dwState |= (1 << i); } } // See if we have an closing quote of any sort if (fFallThru && (ch == acClose[i])) { if (dwState & (1 << i)) { // We are closing the correct kind of quote, // so we cancel it ... dwState = 0; // Do a second check, in case we are looking // for a close quote if (ch == cSearch) return(lpStart); } else if (!dwState) { // We are not in any quotes, so we have // unmatched quotes! return(NULL); } } } } lpStart++; } *lpfNotFound = TRUE; return(NULL); } static BOOL pMatchParentheses( char **ppszStart, char **ppszEnd, LPDWORD lpdwPairs) { DWORD dwIteration = 0; DWORD dwState = 0; char *lpszString = *ppszStart; char *lpStart; char *lpEnd; BOOL fNotFound = FALSE; *lpdwPairs = 0; lpStart = *ppszStart; lpEnd = *ppszEnd; for (;;) { // Find open brace if (!(lpStart = pFindNextUnquotedOccurrence(lpStart, '(', &fNotFound, dwState))) { // If it's not found, we're done, else it's a format error! if (fNotFound) break; else return(FALSE); } // Save the start position, and since we are inside the first open // parenthesis, we force suppress all quotes mode in subsequent // open parenthesis searches if (!dwIteration) { *ppszStart = lpStart; dwState = QUOTE_PARENTHESES; } // If iteration > 0 and the start pointer surpasses // the end pointer, we have the end of the first set // of [nested] braces if (dwIteration && (lpStart > lpEnd)) break; // Find close brace if (!(lpEnd = pFindNextUnquotedOccurrence(lpEnd, ')', &fNotFound, QUOTE_PARENTHESES))) return(FALSE); // Open brace pointer must always be in front of close // brace. if (lpStart > lpEnd) return(FALSE); // Next iteration lpStart++; lpEnd++; dwIteration++; } // Fill in the end pointer and leave (start ptr already // filled in) if (dwIteration) lpEnd--; *lpdwPairs = dwIteration; *ppszEnd = lpEnd; return(TRUE); } /*++ Name : pStripAddrComments Description: This function strips comments from an RFC address Arguments: lpszAddress - Original address comes in, and clean addres comes out Returns: TRUE if comments were stripped and the format of the address is legal. FALSE otherwise --*/ static BOOL pStripAddrComments(char *lpszAddress) { char *lpCopyStart; char *lpStart, *lpEnd; DWORD dwPairs; DWORD dwCopyLen; // First, we strip the comments // We call the function above to find matching parenthesis pairs lpStart = lpszAddress; lpEnd = lpszAddress; do { // Mark the actual start lpCopyStart = lpEnd; if (!pMatchParentheses(&lpStart, &lpEnd, &dwPairs)) { // Failed! SetLastError(ERROR_INVALID_DATA); return(FALSE); } if (dwPairs) { // If fFound, then we found some comments _ASSERT(*lpStart == '('); _ASSERT(*lpEnd == ')'); // Copy the stuff over, excluding comments _ASSERT(lpStart >= lpCopyStart); dwCopyLen = (DWORD)(lpStart - lpCopyStart); // Reset the pointer, and match again ... lpEnd++; lpStart = lpEnd; } else { dwCopyLen = lstrlen(lpCopyStart); } while (dwCopyLen--) { *lpszAddress++ = *lpCopyStart++; } } while (dwPairs); // Terminate the string *lpszAddress = '\0'; return(TRUE); } /*++ Name : pStripAddrSpaces Description: This function strips extraneous spaces from an RFC address An extraneous space is one that is not in a quoted pair, and not inside any quoting pairs Arguments: lpszAddress - Original address comes in, and clean addres comes out Returns: TRUE if spaces were stripped FALSE if any quotes/braces mismatch, or parameter error --*/ static BOOL pStripAddrSpaces(char *lpszAddress) { char *lpszWrite; char *lpszCopyStart; char *lpszSearch; DWORD dwCopyLen, i; BOOL fNotFound; BOOL fValidSpace; char cSet[2] = { ' ', '\t' }; // First, get rid of spaces, then TABs for (i = 0; i < 2; i++) { lpszWrite = lpszAddress; lpszCopyStart = lpszAddress; lpszSearch = lpszAddress; do { lpszCopyStart = lpszSearch; // Find unquoted space lpszSearch = pFindNextUnquotedOccurrence(lpszSearch, cSet[i], &fNotFound, 0); // We cannot just allow casual spaces; An unquoted space // must satisfy one or more of the following: // 1) Leading space // 2) Trailiing space // 3) A space or TAB is in either side or both sides of the space // 4) A special character is on either or both sides of the space if (lpszSearch) { // Make sure it satisfies the above fValidSpace = FALSE; if (lpszSearch > lpszAddress) { if (pIsSpecialOrSpace(*(lpszSearch - 1))) fValidSpace = TRUE; } else fValidSpace = TRUE; if (pIsSpecialOrSpace(*(lpszSearch + 1))) fValidSpace = TRUE; if (!fValidSpace) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } _ASSERT(lpszSearch >= lpszCopyStart); dwCopyLen = (DWORD)(lpszSearch - lpszCopyStart); lpszSearch++; } else dwCopyLen = lstrlen(lpszCopyStart); // 1) Leading spaces are automatically stripped! while (dwCopyLen--) { *lpszWrite++ = *lpszCopyStart++; } } while (lpszSearch); // If the reason it failed is not because it cannot find one, // we have a formatting error here! if (!fNotFound) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } *lpszWrite = '\0'; } return(TRUE); } /*++ Name : pStripAddrQuotes Description: This function strips all quotes in the local part of an address. All quotes pairs within quotes are also collapsed. Arguments: lpszLocalPart - Local part comes in, and comes out without quotes Returns: TRUE if quotes were stripped FALSE if any quotes/braces mismatch, or parameter error --*/ static BOOL pStripAddrQuotes(char *lpszLocalPart, char **lpDomain) { char *lpszWrite; char *lpszCopyStart; char *lpszSearch; DWORD dwCopyLen; DWORD dwState = 0; BOOL fNotFound; _ASSERT(lpDomain); // First, get rid of quotes lpszWrite = lpszLocalPart; lpszCopyStart = lpszLocalPart; lpszSearch = lpszLocalPart; do { lpszCopyStart = lpszSearch; // Find next quote lpszSearch = pFindNextUnquotedOccurrence(lpszSearch, '\"', &fNotFound, dwState); if (lpszSearch) { // Toggle the state dwState = (dwState)?0:QUOTE_QUOTES; // Found a quote, copy all the stuff before the space _ASSERT(lpszSearch >= lpszCopyStart); dwCopyLen = (DWORD)(lpszSearch - lpszCopyStart); lpszSearch++; // Move the domain offset back each time we find // an unquoted quote if (*lpDomain) (*lpDomain)--; } else dwCopyLen = lstrlen(lpszCopyStart); while (dwCopyLen--) { // Another caveat: since we are stripping out the // quotes, we must also take care of the quoted // pairs. That is, we must remove the backslash // delimiting each quoted pair. if (*lpszCopyStart == '\\') { // If the last char of the string is a backslash, or // if the backslash is not within quotes, error! // CAUTION: if dwState == QUOTE_QUOTES, this means // that the stuff we are copying is OUTSIDE of the quote, // since we are copying stuff before the found quote if ((dwState == QUOTE_QUOTES) || !dwCopyLen) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } lpszCopyStart++; dwCopyLen--; } *lpszWrite++ = *lpszCopyStart++; } } while (lpszSearch); *lpszWrite = '\0'; // If the reason it failed is not because it cannot find one, // we have a formatting error here! if (!fNotFound) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } // If we are left with no closing quote, then we also have an error if (dwState == QUOTE_QUOTES) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } return(TRUE); } /*++ Name : pStripUucpRoutes Description: This function strips all UUCP routing paths Arguments: lpszAddress - lpszAddress comes in, and comes out without UUCP paths Returns: TRUE if UUCP routes were stripped FALSE if any quotes/braces mismatch, or parameter error --*/ static BOOL pStripUucpRoutes(char *lpszAddress) { char *lpszDomainOffset; char *lpszCopyStart; char *lpszSearch; BOOL fNotFound; if (!(lpszDomainOffset = pFindNextUnquotedOccurrence(lpszAddress, '@', &fNotFound, 0))) if (!fNotFound) return(FALSE); lpszSearch = lpszAddress; lpszCopyStart = NULL; // Find the last BANG while (lpszSearch = pFindNextUnquotedOccurrence(lpszSearch, '!', &fNotFound, 0)) { // If an unquoted bang occurs after the @ sign, we will return error if (lpszDomainOffset && (lpszSearch > lpszDomainOffset)) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } lpszCopyStart = lpszSearch++; } // If the reason is other than not found, we have an error if (!fNotFound) { SetLastError(ERROR_INVALID_DATA); return(FALSE); } if (lpszCopyStart) { lpszCopyStart++; while (*lpszCopyStart) *lpszAddress++ = *lpszCopyStart++; *lpszAddress = '\0'; } return(TRUE); } /*++ Name : CAddr::ValidateDomainName Description: Determines whether a specified domain name is valid Arguments: lpszDomainName - ANSI domain name string to validate Returns: TRUE if valid, FALSE if not --*/ BOOL CAddr::ValidateDomainName(char *lpszDomainName) { char szClean[MAX_INTERNET_NAME+1]; _ASSERT(lpszDomainName); // Routine checking if ((!lpszDomainName) || (!pValidateStringPtr(lpszDomainName, MAX_INTERNET_NAME+1))) return(FALSE); if (!pValidateAsciiString(lpszDomainName)) return(FALSE); // Strip all comments lstrcpy(szClean, lpszDomainName); if (!pStripAddrComments(szClean)) return(FALSE); if (!pStripAddrSpaces(szClean)) return(FALSE); // Call our appropriate private return(pValidateLocalPartOrDomain(szClean, lstrlen(szClean), FALSE)); } /*++ Name : CAddr::ExtractCleanEmailName Description: Extracts an absolutely clean email name from an address, quotes are NOT stripped for the local part. Arguments: lpszCleanEmail - Pre allocated buffer to return the clean address ppszDomainOffset - Pointer to '@' symbol separating the local-part and the domain; NULL if not domain is specified in the address. lpdwCleanEmailLength - Length of the whole clean email returned lpszSource - Source address to clean up Returns: TRUE if valid, FALSE if not --*/ BOOL CAddr::ExtractCleanEmailName( char *lpszCleanEmail, char **ppszDomainOffset, DWORD *lpdwCleanEmailLength, char *lpszSource) { char szClean[MAX_INTERNET_NAME+1]; char *lpDomainOffset; BOOL fNotFound; TraceFunctEnter("CAddr::ExtractCleanEmailName"); _ASSERT(lpszSource); _ASSERT(lpszCleanEmail); _ASSERT(ppszDomainOffset); _ASSERT(lpdwCleanEmailLength); // Routine checking if (!lpszSource || !pValidateStringPtr(lpszSource, MAX_INTERNET_NAME+1) || !lpszCleanEmail || IsBadWritePtr(lpszCleanEmail, MAX_INTERNET_NAME+1) || !ppszDomainOffset || IsBadWritePtr(ppszDomainOffset, sizeof(char *)) || !lpdwCleanEmailLength || IsBadWritePtr(lpdwCleanEmailLength, sizeof(DWORD))) { SetLastError(ERROR_INVALID_DATA); goto LeaveWithError; } szClean[0] = '\0'; if (!pValidateAsciiString(lpszSource)) { SetLastError(ERROR_INVALID_DATA); goto LeaveWithError; } // Strip all comments and spaces lstrcpy(szClean, lpszSource); StateTrace(0, " Source: %s", szClean); if (!pStripAddrComments(szClean)) goto LeaveWithError; StateTrace(0, " Comments stripped: %s", szClean); // Extract the clean email name in a simple local-part@domain // form. However, the local part may still have UUCP headers if (!pExtractAddress(szClean, lstrlen(szClean), lpszCleanEmail)) { SetLastError(ERROR_INVALID_DATA); goto LeaveWithError; } StateTrace(0, " Address: %s", lpszCleanEmail); // Strip comments again ... if (!pStripAddrComments(lpszCleanEmail)) { SetLastError(ERROR_INVALID_DATA); goto LeaveWithError; } StateTrace(0, " Comments stripped (2): %s", lpszCleanEmail); if (!pStripAddrSpaces(lpszCleanEmail)) { SetLastError(ERROR_INVALID_DATA); goto LeaveWithError; } StateTrace(0, " Spaces stripped: %s", lpszCleanEmail); // Now we examine the clean address, and try to locate the // local-part and the domain lpDomainOffset = pFindNextUnquotedOccurrence(lpszCleanEmail, '@', &fNotFound, 0); if (lpDomainOffset) { _ASSERT(lpDomainOffset >= lpszCleanEmail); if (lpDomainOffset == lpszCleanEmail) { // First char cannot be '@' SetLastError(ERROR_INVALID_DATA); goto LeaveWithError; } } else { // If it's not found, we assume there's no domain if (!fNotFound) { SetLastError(ERROR_INVALID_DATA); goto LeaveWithError; } } *lpdwCleanEmailLength = lstrlen(lpszCleanEmail); *ppszDomainOffset = lpDomainOffset; TraceFunctLeave(); return(TRUE); LeaveWithError: ErrorTrace(0, "CAddr::ExtractCleanEmailName failed"); TraceFunctLeave(); return(FALSE); } /*++ Name : CAddr::ValidateCleanEmailName Description: Determines whether a specified email name is valid. The input email name must be clean, i.e. no comments, spaces, routing specifiers, etc. Arguments: lpszCleanEmailName - ANSI email name string to validate lpszDomainOffset - Pointer to '@' sign in email string Returns: TRUE if valid, FALSE if not --*/ BOOL CAddr::ValidateCleanEmailName( char *lpszCleanEmailName, char *lpszDomainOffset) { DWORD dwLength, dwDomainLength; TraceFunctEnterEx(0, "CAddr::ValidateCleanEmailName"); _ASSERT(lpszCleanEmailName); if (!lpszCleanEmailName || !pValidateStringPtr(lpszCleanEmailName, MAX_INTERNET_NAME+1)) { SetLastError(ERROR_INVALID_DATA); goto LeaveWithError; } if (lpszDomainOffset) { _ASSERT(*lpszDomainOffset == '@'); _ASSERT(lpszDomainOffset > lpszCleanEmailName); dwLength = (DWORD)(lpszDomainOffset - lpszCleanEmailName); dwDomainLength = lstrlen(lpszCleanEmailName) - dwLength - 1; *lpszDomainOffset++ = '\0'; } else dwLength = lstrlen(lpszCleanEmailName); StateTrace(0, " Local-part: %s", lpszCleanEmailName); if (!pValidateLocalPartOrDomain(lpszCleanEmailName, dwLength, TRUE)) { ErrorTrace(0, "Invalid local part"); goto LeaveWithError; } if (lpszDomainOffset) { StateTrace(0, " Domain: %s", lpszDomainOffset); if (!pValidateLocalPartOrDomain(lpszDomainOffset, dwDomainLength, FALSE)) { ErrorTrace(0, "Invalid domain"); goto LeaveWithError; } } // Restore the string ... if (lpszDomainOffset) *--lpszDomainOffset = '@'; TraceFunctLeave(); return(TRUE); LeaveWithError: if (lpszDomainOffset) if (*--lpszDomainOffset == '\0') *lpszDomainOffset = '@'; ErrorTrace(0, "CAddr::ValidateCleanEmailName failed"); TraceFunctLeave(); return(FALSE); } /*++ Name : CAddr::ValidateEmailName Description: Determines whether a specified email name is valid Arguments: lpszEmailName - ANSI email name string to validate fDomainOptional - TRUE if domain is optional, FLASE forces a domain to be included. Returns: TRUE if valid, FALSE if not --*/ BOOL CAddr::ValidateEmailName( char *lpszEmailName, BOOL fDomainOptional) { char szSource[MAX_INTERNET_NAME+1]; char *lpDomainOffset; DWORD dwLength; szSource[0] = '\0'; if (!ExtractCleanEmailName( szSource, &lpDomainOffset, &dwLength, lpszEmailName)) return(FALSE); if (!fDomainOptional && !lpDomainOffset) return(FALSE); return(ValidateCleanEmailName(szSource, lpDomainOffset)); } /*++ Name : CAddr::FindStartOfDomain Description: Finds the start of the domain part from a CLEAN email address. The clean address may not contain any comments, routing specifications, UUCP addresses, and the such. No validation is done for the "clean address". Arguments: lpszCleanEmail - ANSI CLEAN email name string whose local part to extract Returns: A pointer to the '@' sign separating the local part and the domain. NULL if the '@' sign is not found. Note that '@' signs enclosed in quotes or in a proper quoted pair are skipped. --*/ CHAR * CAddr::FindStartOfDomain(CHAR *lpszCleanEmail) { BOOL fNotFound; return(pFindNextUnquotedOccurrence(lpszCleanEmail, '@', &fNotFound, 0)); } //---[ CAddr::GetRFC822AddressCount ]------------------------------------------ // // // Description: // Counts the number of addresses in a RFC822 list of addresses. // Parameters: // IN szAddressList List of addresses to count // Returns: // # of recipients in list // History: // 2/17/99 - MikeSwa Created // //----------------------------------------------------------------------------- DWORD CAddr::GetRFC822AddressCount(char *szAddressList) { DWORD cRecips = 0; LPSTR szCommentStart = szAddressList; LPSTR szCommentEnd = szAddressList; LPSTR szSearchStart = szAddressList; LPSTR szSearchDelimiter = NULL; //ptr to delimiter LPSTR szLastDelimiter = NULL; //ptr to last delimiter found BOOL fSeenAlphaNum = FALSE; BOOL fInQuote = TRUE; DWORD dwPairs = 0; CHAR chSaved = '\0'; CHAR *pchChanged = NULL; BOOL fDelimiterNotFound = TRUE; CHAR rgchDelimiters[] = {',', ';', '\0'}; //We look for RFC822 delimiters (, or ;) that are not in comments (which //are delimited by ()'s or which are in quotes. //No string... no recips if (!szAddressList) goto Exit; //If we have any string... we start out with 1 recip cRecips++; // First, we strip the comments // We call the function above to find matching parenthesis pairs do { //Set start of search to start of string being scanned for ()'s szSearchStart = szCommentStart; if (!pMatchParentheses(&szCommentStart, &szCommentEnd, &dwPairs)) { // Failed! cRecips = 0; goto Exit; } if (dwPairs) //we have comments { //We found some comments _ASSERT(*szCommentStart == '('); _ASSERT(*szCommentEnd == ')'); //If the first character of our search was a '('... then we //should not bother searching for delimiters if (szSearchStart == szCommentStart) { szCommentStart = szCommentEnd + 1; continue; } //Set the end of our search for delimiters & set to NULL character pchChanged = szCommentStart; chSaved = *szCommentStart; *szCommentStart = '\0'; // Reset the pointers, and match again ... szCommentEnd++; szCommentStart = szCommentEnd; } else { //We found no further comments... there is no need to save a character chSaved = '\0'; pchChanged = NULL; } //Now we will search for unqoted delimiters in this uncommented section //Iterate over all the delimiters we have for (CHAR *pchDelimiter = rgchDelimiters; *pchDelimiter; pchDelimiter++) { szSearchDelimiter = szSearchStart; do { szSearchDelimiter = pFindNextUnquotedOccurrence( szSearchDelimiter, *pchDelimiter, &fDelimiterNotFound, 0); if (!fDelimiterNotFound) { _ASSERT(*pchDelimiter == *szSearchDelimiter); cRecips++; if (szSearchDelimiter && (szSearchDelimiter > szLastDelimiter)) szLastDelimiter = szSearchDelimiter; } else { //We know we won't find anymore in this section break; } //Keep on looping while we are still finding delimiters and we are not //at the end of the string } while (!fDelimiterNotFound && szSearchDelimiter && *szSearchDelimiter && *(++szSearchDelimiter)); } //Restore changed character if (pchChanged && ('\0' != chSaved)) *pchChanged = chSaved; } while (dwPairs); //Make sure the last delimiter was not at the end of the buffer if (szLastDelimiter && cRecips && *szLastDelimiter) { while (*(++szLastDelimiter)) { //if it is not a space... count it as a recipient if (!isspace((UCHAR)*szLastDelimiter)) goto Exit; } //Only whitespace after last delimiter... we have counted 1 too many recips cRecips--; } Exit: return cRecips; } //---[ IsRecipientInRFC822AddressList ]---------------------------------------- // // // Description: // Determines if a given recipient is in the given RFC822 formatted // recipient list. // Parameters: // IN szAddressList Address list to check in // IN szRecip Recipient Address to check for // Returns: // TRUE if there was a match // FALSE if there was no match // History: // 2/17/99 - MikeSwa Created // //----------------------------------------------------------------------------- BOOL CAddr::IsRecipientInRFC822AddressList(char *szAddressList, char *szRecip) { LPSTR szRecipEnd = NULL; CHAR chSaved = '\0'; LPSTR szCurrentAddress = szAddressList; BOOL fFound = FALSE; DWORD cbAddress; if (!szAddressList || !szRecip) goto Exit; //Convert everything to lower case so we match correctly szCurrentAddress = szAddressList; do { *szCurrentAddress = (CHAR) tolower(*szCurrentAddress); } while(*(++szCurrentAddress)); szCurrentAddress = szRecip; do { *szCurrentAddress = (CHAR) tolower(*szCurrentAddress); } while(*(++szCurrentAddress)); //skip past white space in recipient while (*szRecip && isspace((UCHAR)*szRecip)) szRecip++; //Find and skip past extranious trailing whitespace cbAddress = strlen(szRecip); szRecipEnd = szRecip + cbAddress/sizeof(CHAR); szRecipEnd--; while (isspace((UCHAR)*szRecipEnd)) { cbAddress--; szRecipEnd--; } //Make szRecipEnd point to last space szRecipEnd++; //Null terminate before trailing whitespace chSaved = *szRecipEnd; *szRecipEnd = '\0'; //Search for addresss as substring, and see if it looks like a lone address for (szCurrentAddress = strstr(szAddressList, szRecip); szCurrentAddress && !fFound; szCurrentAddress = strstr(++szCurrentAddress, szRecip)) { //look for surrounding characters to not match partial addresses //We don't want "user" to match "user1" or "user@foo" or "foo@user" if ((szCurrentAddress != szAddressList)) { if (!pIsSpecialOrSpace(*(szCurrentAddress-1)) || ('@' == *(szCurrentAddress-1))) continue; } if (szCurrentAddress[cbAddress/sizeof(CHAR)]) { if (!pIsSpecialOrSpace(szCurrentAddress[cbAddress/sizeof(CHAR)]) || ('@' == szCurrentAddress[cbAddress/sizeof(CHAR)])) continue; } //The address looks like a match fFound = TRUE; break; } Exit: //Restore saved space if (szRecipEnd && chSaved) *szRecipEnd = chSaved; return fFound; }