/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsIAddrDatabase.h" #include "nsStringGlue.h" #include "nsAbLDIFService.h" #include "nsIFile.h" #include "nsILineInputStream.h" #include "nsIInputStream.h" #include "nsNetUtil.h" #include "nsISeekableStream.h" #include "mdb.h" #include "plstr.h" #include "prmem.h" #include "prprf.h" #include "nsCRTGlue.h" #include "nsTArray.h" #include NS_IMPL_ISUPPORTS(nsAbLDIFService, nsIAbLDIFService) // If we get a line longer than 32K it's just toooooo bad! #define kTextAddressBufferSz (64 * 1024) nsAbLDIFService::nsAbLDIFService() { mStoreLocAsHome = false; mLFCount = 0; mCRCount = 0; } nsAbLDIFService::~nsAbLDIFService() { } #define RIGHT2 0x03 #define RIGHT4 0x0f #define CONTINUED_LINE_MARKER '\001' // XXX TODO fix me // use the NSPR base64 library. see plbase64.h // see bug #145367 static unsigned char b642nib[0x80] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, 0xff, 0x3f, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff }; NS_IMETHODIMP nsAbLDIFService::ImportLDIFFile(nsIAddrDatabase *aDb, nsIFile *aSrc, bool aStoreLocAsHome, uint32_t *aProgress) { NS_ENSURE_ARG_POINTER(aSrc); NS_ENSURE_ARG_POINTER(aDb); mStoreLocAsHome = aStoreLocAsHome; char buf[1024]; char* pBuf = &buf[0]; int32_t startPos = 0; uint32_t len = 0; nsTArray listPosArray; // where each list/group starts in ldif file nsTArray listSizeArray; // size of the list/group info int32_t savedStartPos = 0; int32_t filePos = 0; uint64_t bytesLeft = 0; nsCOMPtr inputStream; nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aSrc); NS_ENSURE_SUCCESS(rv, rv); // Initialize the parser for a run... mLdifLine.Truncate(); while (NS_SUCCEEDED(inputStream->Available(&bytesLeft)) && bytesLeft > 0) { if (NS_SUCCEEDED(inputStream->Read(pBuf, sizeof(buf), &len)) && len > 0) { startPos = 0; while (NS_SUCCEEDED(GetLdifStringRecord(buf, len, startPos))) { if (mLdifLine.Find("groupOfNames") == -1) AddLdifRowToDatabase(aDb, false); else { //keep file position for mailing list listPosArray.AppendElement(savedStartPos); listSizeArray.AppendElement(filePos + startPos-savedStartPos); ClearLdifRecordBuffer(); } savedStartPos = filePos + startPos; } filePos += len; if (aProgress) *aProgress = (uint32_t)filePos; } } //last row if (!mLdifLine.IsEmpty() && mLdifLine.Find("groupOfNames") == -1) AddLdifRowToDatabase(aDb, false); // mail Lists int32_t i, pos; uint32_t size; int32_t listTotal = listPosArray.Length(); char *listBuf; ClearLdifRecordBuffer(); // make sure the buffer is clean nsCOMPtr seekableStream = do_QueryInterface(inputStream, &rv); NS_ENSURE_SUCCESS(rv, rv); for (i = 0; i < listTotal; i++) { pos = listPosArray[i]; size = listSizeArray[i]; if (NS_SUCCEEDED(seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, pos))) { // Allocate enough space for the lists/groups as the size varies. listBuf = (char *) PR_Malloc(size); if (!listBuf) continue; if (NS_SUCCEEDED(inputStream->Read(listBuf, size, &len)) && len > 0) { startPos = 0; while (NS_SUCCEEDED(GetLdifStringRecord(listBuf, len, startPos))) { if (mLdifLine.Find("groupOfNames") != -1) { AddLdifRowToDatabase(aDb, true); if (NS_SUCCEEDED(seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, 0))) break; } } } PR_FREEIF(listBuf); } } rv = inputStream->Close(); NS_ENSURE_SUCCESS(rv, rv); // Finally commit everything to the database and return. return aDb->Commit(nsAddrDBCommitType::kLargeCommit); } /* * str_parse_line - takes a line of the form "type:[:] value" and splits it * into components "type" and "value". if a double colon separates type from * value, then value is encoded in base 64, and parse_line un-decodes it * (in place) before returning. * in LDIF, non-ASCII data is treated as base64 encoded UTF-8 */ nsresult nsAbLDIFService::str_parse_line(char *line, char **type, char **value, int *vlen) const { char *p, *s, *d, *byte, *stop; char nib; int i, b64; /* skip any leading space */ while ( isspace( *line ) ) { line++; } *type = line; for ( s = line; *s && *s != ':'; s++ ) ; /* NULL */ if ( *s == '\0' ) { return NS_ERROR_FAILURE; } /* trim any space between type and : */ for ( p = s - 1; p > line && isspace( *p ); p-- ) { *p = '\0'; } *s++ = '\0'; /* check for double : - indicates base 64 encoded value */ if ( *s == ':' ) { s++; b64 = 1; /* single : - normally encoded value */ } else { b64 = 0; } /* skip space between : and value */ while ( isspace( *s ) ) { s++; } /* if no value is present, error out */ if ( *s == '\0' ) { return NS_ERROR_FAILURE; } /* check for continued line markers that should be deleted */ for ( p = s, d = s; *p; p++ ) { if ( *p != CONTINUED_LINE_MARKER ) *d++ = *p; } *d = '\0'; *value = s; if ( b64 ) { stop = PL_strchr( s, '\0' ); byte = s; for ( p = s, *vlen = 0; p < stop; p += 4, *vlen += 3 ) { for ( i = 0; i < 3; i++ ) { if ( p[i] != '=' && (p[i] & 0x80 || b642nib[ p[i] & 0x7f ] > 0x3f) ) { return NS_ERROR_FAILURE; } } /* first digit */ nib = b642nib[ p[0] & 0x7f ]; byte[0] = nib << 2; /* second digit */ nib = b642nib[ p[1] & 0x7f ]; byte[0] |= nib >> 4; byte[1] = (nib & RIGHT4) << 4; /* third digit */ if ( p[2] == '=' ) { *vlen += 1; break; } nib = b642nib[ p[2] & 0x7f ]; byte[1] |= nib >> 2; byte[2] = (nib & RIGHT2) << 6; /* fourth digit */ if ( p[3] == '=' ) { *vlen += 2; break; } nib = b642nib[ p[3] & 0x7f ]; byte[2] |= nib; byte += 3; } s[ *vlen ] = '\0'; } else { *vlen = (int) (d - s); } return NS_OK; } /* * str_getline - return the next "line" (minus newline) of input from a * string buffer of lines separated by newlines, terminated by \n\n * or \0. this routine handles continued lines, bundling them into * a single big line before returning. if a line begins with a white * space character, it is a continuation of the previous line. the white * space character (nb: only one char), and preceeding newline are changed * into CONTINUED_LINE_MARKER chars, to be deleted later by the * str_parse_line() routine above. * * it takes a pointer to a pointer to the buffer on the first call, * which it updates and must be supplied on subsequent calls. */ char* nsAbLDIFService::str_getline(char **next) const { char *lineStr; char c; if ( *next == nullptr || **next == '\n' || **next == '\0' ) { return( nullptr); } lineStr = *next; while ( (*next = PL_strchr( *next, '\n' )) != NULL ) { c = *(*next + 1); if ( isspace( c ) && c != '\n' ) { **next = CONTINUED_LINE_MARKER; *(*next+1) = CONTINUED_LINE_MARKER; } else { *(*next)++ = '\0'; break; } } return( lineStr ); } nsresult nsAbLDIFService::GetLdifStringRecord(char* buf, int32_t len, int32_t& stopPos) { for (; stopPos < len; stopPos++) { char c = buf[stopPos]; if (c == 0xA) { mLFCount++; } else if (c == 0xD) { mCRCount++; } else { if (mLFCount == 0 && mCRCount == 0) mLdifLine.Append(c); else if (( mLFCount > 1) || ( mCRCount > 2 && mLFCount ) || ( !mLFCount && mCRCount > 1 )) { return NS_OK; } else if ((mLFCount == 1 || mCRCount == 1)) { mLdifLine.Append('\n'); mLdifLine.Append(c); mLFCount = 0; mCRCount = 0; } } } if (((stopPos == len) && (mLFCount > 1)) || (mCRCount > 2 && mLFCount) || (!mLFCount && mCRCount > 1)) return NS_OK; return NS_ERROR_FAILURE; } void nsAbLDIFService::AddLdifRowToDatabase(nsIAddrDatabase *aDatabase, bool bIsList) { // If no data to process then reset CR/LF counters and return. if (mLdifLine.IsEmpty()) { mLFCount = 0; mCRCount = 0; return; } nsCOMPtr newRow; if (aDatabase) { if (bIsList) aDatabase->GetNewListRow(getter_AddRefs(newRow)); else aDatabase->GetNewRow(getter_AddRefs(newRow)); if (!newRow) return; } else return; char* cursor = ToNewCString(mLdifLine); char* saveCursor = cursor; /* keep for deleting */ char* line = 0; char* typeSlot = 0; char* valueSlot = 0; int length = 0; // the length of an ldif attribute while ( (line = str_getline(&cursor)) != nullptr) { if (NS_SUCCEEDED(str_parse_line(line, &typeSlot, &valueSlot, &length))) { AddLdifColToDatabase(aDatabase, newRow, typeSlot, valueSlot, bIsList); } else continue; // parse error: continue with next loop iteration } free(saveCursor); aDatabase->AddCardRowToDB(newRow); if (bIsList) aDatabase->AddListDirNode(newRow); // Clear buffer for next record ClearLdifRecordBuffer(); } void nsAbLDIFService::AddLdifColToDatabase(nsIAddrDatabase *aDatabase, nsIMdbRow* newRow, char* typeSlot, char* valueSlot, bool bIsList) { nsAutoCString colType(typeSlot); nsAutoCString column(valueSlot); // 4.x exports attributes like "givenname", // mozilla does "givenName" to be compliant with RFC 2798 ToLowerCase(colType); mdb_u1 firstByte = (mdb_u1)(colType.get())[0]; switch ( firstByte ) { case 'b': if (colType.EqualsLiteral("birthyear")) aDatabase->AddBirthYear(newRow, column.get()); else if (colType.EqualsLiteral("birthmonth")) aDatabase->AddBirthMonth(newRow, column.get()); else if (colType.EqualsLiteral("birthday")) aDatabase->AddBirthDay(newRow, column.get()); break; // 'b' case 'c': if (colType.EqualsLiteral("cn") || colType.EqualsLiteral("commonname")) { if (bIsList) aDatabase->AddListName(newRow, column.get()); else aDatabase->AddDisplayName(newRow, column.get()); } else if (colType.EqualsLiteral("c") || colType.EqualsLiteral("countryname")) { if (mStoreLocAsHome ) aDatabase->AddHomeCountry(newRow, column.get()); else aDatabase->AddWorkCountry(newRow, column.get()); } else if (colType.EqualsLiteral("cellphone") ) aDatabase->AddCellularNumber(newRow, column.get()); else if (colType.EqualsLiteral("carphone")) aDatabase->AddCellularNumber(newRow, column.get()); else if (colType.EqualsLiteral("custom1")) aDatabase->AddCustom1(newRow, column.get()); else if (colType.EqualsLiteral("custom2")) aDatabase->AddCustom2(newRow, column.get()); else if (colType.EqualsLiteral("custom3")) aDatabase->AddCustom3(newRow, column.get()); else if (colType.EqualsLiteral("custom4")) aDatabase->AddCustom4(newRow, column.get()); else if (colType.EqualsLiteral("company")) aDatabase->AddCompany(newRow, column.get()); break; // 'c' case 'd': if (colType.EqualsLiteral("description")) { if (bIsList) aDatabase->AddListDescription(newRow, column.get()); else aDatabase->AddNotes(newRow, column.get()); } else if (colType.EqualsLiteral("department")) aDatabase->AddDepartment(newRow, column.get()); else if (colType.EqualsLiteral("displayname")) { if (bIsList) aDatabase->AddListName(newRow, column.get()); else aDatabase->AddDisplayName(newRow, column.get()); } break; // 'd' case 'f': if (colType.EqualsLiteral("fax") || colType.EqualsLiteral("facsimiletelephonenumber")) aDatabase->AddFaxNumber(newRow, column.get()); break; // 'f' case 'g': if (colType.EqualsLiteral("givenname")) aDatabase->AddFirstName(newRow, column.get()); break; // 'g' case 'h': if (colType.EqualsLiteral("homephone")) aDatabase->AddHomePhone(newRow, column.get()); else if (colType.EqualsLiteral("homestreet")) aDatabase->AddHomeAddress(newRow, column.get()); else if (colType.EqualsLiteral("homeurl")) aDatabase->AddWebPage2(newRow, column.get()); break; // 'h' case 'l': if (colType.EqualsLiteral("l") || colType.EqualsLiteral("locality")) { if (mStoreLocAsHome) aDatabase->AddHomeCity(newRow, column.get()); else aDatabase->AddWorkCity(newRow, column.get()); } // labeledURI contains a URI and, optionally, a label // This will remove the label and place the URI as the work URL else if (colType.EqualsLiteral("labeleduri")) { int32_t index = column.FindChar(' '); if (index != -1) column.SetLength(index); aDatabase->AddWebPage1(newRow, column.get()); } break; // 'l' case 'm': if (colType.EqualsLiteral("mail")) aDatabase->AddPrimaryEmail(newRow, column.get()); else if (colType.EqualsLiteral("member") && bIsList) aDatabase->AddLdifListMember(newRow, column.get()); else if (colType.EqualsLiteral("mobile")) aDatabase->AddCellularNumber(newRow, column.get()); else if (colType.EqualsLiteral("mozilla_aimscreenname")) aDatabase->AddAimScreenName(newRow, column.get()); else if (colType.EqualsLiteral("mozillacustom1")) aDatabase->AddCustom1(newRow, column.get()); else if (colType.EqualsLiteral("mozillacustom2")) aDatabase->AddCustom2(newRow, column.get()); else if (colType.EqualsLiteral("mozillacustom3")) aDatabase->AddCustom3(newRow, column.get()); else if (colType.EqualsLiteral("mozillacustom4")) aDatabase->AddCustom4(newRow, column.get()); else if (colType.EqualsLiteral("mozillahomecountryname")) aDatabase->AddHomeCountry(newRow, column.get()); else if (colType.EqualsLiteral("mozillahomelocalityname")) aDatabase->AddHomeCity(newRow, column.get()); else if (colType.EqualsLiteral("mozillahomestate")) aDatabase->AddHomeState(newRow, column.get()); else if (colType.EqualsLiteral("mozillahomestreet")) aDatabase->AddHomeAddress(newRow, column.get()); else if (colType.EqualsLiteral("mozillahomestreet2")) aDatabase->AddHomeAddress2(newRow, column.get()); else if (colType.EqualsLiteral("mozillahomepostalcode")) aDatabase->AddHomeZipCode(newRow, column.get()); else if (colType.EqualsLiteral("mozillahomeurl")) aDatabase->AddWebPage2(newRow, column.get()); else if (colType.EqualsLiteral("mozillanickname")) { if (bIsList) aDatabase->AddListNickName(newRow, column.get()); else aDatabase->AddNickName(newRow, column.get()); } else if (colType.EqualsLiteral("mozillasecondemail")) aDatabase->Add2ndEmail(newRow, column.get()); else if (colType.EqualsLiteral("mozillausehtmlmail")) { ToLowerCase(column); if (-1 != column.Find("true")) aDatabase->AddPreferMailFormat(newRow, nsIAbPreferMailFormat::html); else if (-1 != column.Find("false")) aDatabase->AddPreferMailFormat(newRow, nsIAbPreferMailFormat::plaintext); else aDatabase->AddPreferMailFormat(newRow, nsIAbPreferMailFormat::unknown); } else if (colType.EqualsLiteral("mozillaworkstreet2")) aDatabase->AddWorkAddress2(newRow, column.get()); else if (colType.EqualsLiteral("mozillaworkurl")) aDatabase->AddWebPage1(newRow, column.get()); break; // 'm' case 'n': if (colType.EqualsLiteral("notes")) aDatabase->AddNotes(newRow, column.get()); else if (colType.EqualsLiteral("nscpaimscreenname") || colType.EqualsLiteral("nsaimid")) aDatabase->AddAimScreenName(newRow, column.get()); break; // 'n' case 'o': if (colType.EqualsLiteral("objectclass")) break; else if (colType.EqualsLiteral("ou") || colType.EqualsLiteral("orgunit")) aDatabase->AddDepartment(newRow, column.get()); else if (colType.EqualsLiteral("o")) // organization aDatabase->AddCompany(newRow, column.get()); break; // 'o' case 'p': if (colType.EqualsLiteral("postalcode")) { if (mStoreLocAsHome) aDatabase->AddHomeZipCode(newRow, column.get()); else aDatabase->AddWorkZipCode(newRow, column.get()); } else if (colType.EqualsLiteral("postofficebox")) { nsAutoCString workAddr1, workAddr2; SplitCRLFAddressField(column, workAddr1, workAddr2); aDatabase->AddWorkAddress(newRow, workAddr1.get()); aDatabase->AddWorkAddress2(newRow, workAddr2.get()); } else if (colType.EqualsLiteral("pager") || colType.EqualsLiteral("pagerphone")) aDatabase->AddPagerNumber(newRow, column.get()); break; // 'p' case 'r': if (colType.EqualsLiteral("region")) { aDatabase->AddWorkState(newRow, column.get()); } break; // 'r' case 's': if (colType.EqualsLiteral("sn") || colType.EqualsLiteral("surname")) aDatabase->AddLastName(newRow, column.get()); else if (colType.EqualsLiteral("street")) aDatabase->AddWorkAddress(newRow, column.get()); else if (colType.EqualsLiteral("streetaddress")) { nsAutoCString addr1, addr2; SplitCRLFAddressField(column, addr1, addr2); if (mStoreLocAsHome) { aDatabase->AddHomeAddress(newRow, addr1.get()); aDatabase->AddHomeAddress2(newRow, addr2.get()); } else { aDatabase->AddWorkAddress(newRow, addr1.get()); aDatabase->AddWorkAddress2(newRow, addr2.get()); } } else if (colType.EqualsLiteral("st")) { if (mStoreLocAsHome) aDatabase->AddHomeState(newRow, column.get()); else aDatabase->AddWorkState(newRow, column.get()); } break; // 's' case 't': if (colType.EqualsLiteral("title")) aDatabase->AddJobTitle(newRow, column.get()); else if (colType.EqualsLiteral("telephonenumber") ) { aDatabase->AddWorkPhone(newRow, column.get()); } break; // 't' case 'u': if (colType.EqualsLiteral("uniquemember") && bIsList) aDatabase->AddLdifListMember(newRow, column.get()); break; // 'u' case 'w': if (colType.EqualsLiteral("workurl")) aDatabase->AddWebPage1(newRow, column.get()); break; // 'w' case 'x': if (colType.EqualsLiteral("xmozillanickname")) { if (bIsList) aDatabase->AddListNickName(newRow, column.get()); else aDatabase->AddNickName(newRow, column.get()); } else if (colType.EqualsLiteral("xmozillausehtmlmail")) { ToLowerCase(column); if (-1 != column.Find("true")) aDatabase->AddPreferMailFormat(newRow, nsIAbPreferMailFormat::html); else if (-1 != column.Find("false")) aDatabase->AddPreferMailFormat(newRow, nsIAbPreferMailFormat::plaintext); else aDatabase->AddPreferMailFormat(newRow, nsIAbPreferMailFormat::unknown); } break; // 'x' case 'z': if (colType.EqualsLiteral("zip")) // alias for postalcode { if (mStoreLocAsHome) aDatabase->AddHomeZipCode(newRow, column.get()); else aDatabase->AddWorkZipCode(newRow, column.get()); } break; // 'z' default: break; // default } } void nsAbLDIFService::ClearLdifRecordBuffer() { if (!mLdifLine.IsEmpty()) { mLdifLine.Truncate(); mLFCount = 0; mCRCount = 0; } } // Some common ldif fields, it an ldif file has NONE of these entries // then it is most likely NOT an ldif file! static const char *const sLDIFFields[] = { "objectclass", "sn", "dn", "cn", "givenName", "mail", nullptr }; #define kMaxLDIFLen 14 // Count total number of legal ldif fields and records in the first 100 lines of the // file and if the average legal ldif field is 3 or higher than it's a valid ldif file. NS_IMETHODIMP nsAbLDIFService::IsLDIFFile(nsIFile *pSrc, bool *_retval) { NS_ENSURE_ARG_POINTER(pSrc); NS_ENSURE_ARG_POINTER(_retval); *_retval = false; nsresult rv = NS_OK; nsCOMPtr fileStream; rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), pSrc); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr lineInputStream(do_QueryInterface(fileStream, &rv)); NS_ENSURE_SUCCESS(rv, rv); int32_t lineLen = 0; int32_t lineCount = 0; int32_t ldifFields = 0; // total number of legal ldif fields. char field[kMaxLDIFLen]; int32_t fLen = 0; const char *pChar; int32_t recCount = 0; // total number of records. int32_t i; bool gotLDIF = false; bool more = true; nsCString line; while (more && NS_SUCCEEDED(rv) && (lineCount < 100)) { rv = lineInputStream->ReadLine(line, &more); if (NS_SUCCEEDED(rv) && more) { pChar = line.get(); lineLen = line.Length(); if (!lineLen && gotLDIF) { recCount++; gotLDIF = false; } if (lineLen && (*pChar != ' ') && (*pChar != '\t')) { fLen = 0; while (lineLen && (fLen < (kMaxLDIFLen - 1)) && (*pChar != ':')) { field[fLen] = *pChar; pChar++; fLen++; lineLen--; } field[fLen] = 0; if (lineLen && (*pChar == ':') && (fLen < (kMaxLDIFLen - 1))) { // see if this is an ldif field (case insensitive)? i = 0; while (sLDIFFields[i]) { if (!PL_strcasecmp( sLDIFFields[i], field)) { ldifFields++; gotLDIF = true; break; } i++; } } } } lineCount++; } // If we just saw ldif address, increment recCount. if (gotLDIF) recCount++; rv = fileStream->Close(); if (recCount > 1) ldifFields /= recCount; // If the average field number >= 3 then it's a good ldif file. if (ldifFields >= 3) { *_retval = true; } return rv; } void nsAbLDIFService::SplitCRLFAddressField(nsCString &inputAddress, nsCString &outputLine1, nsCString &outputLine2) const { int32_t crlfPos = inputAddress.Find("\r\n"); if (crlfPos != -1) { outputLine1 = Substring(inputAddress, 0, crlfPos); outputLine2 = Substring(inputAddress, crlfPos + 2); } else outputLine1.Assign(inputAddress); }