diff --git a/autotests/data/vcard10.vcf.ref b/autotests/data/vcard10.vcf.ref index 7a21a4b7..149a7edd 100644 --- a/autotests/data/vcard10.vcf.ref +++ b/autotests/data/vcard10.vcf.ref @@ -1,52 +1,53 @@ BEGIN:VCARD VERSION:3.0 ADR;TYPE=work:;;DDDDDDDDDDDDDDD-Staße 2\n00000 DDDDDDDDDD;;;; N:;;;; NOTE:This one has a funny character at the end in the contact list ORG:Bike Shop UID:bfa5fdc5-fbfc-4ce2-8196-29ca0deb3bd3 END:VCARD BEGIN:VCARD VERSION:3.0 ADR;TYPE=home:;;AAAAAAA-AäAAAAA 11;Urt;;00000; FN:Field after Address broken N:;Broken;;; NOTE:This one's URL field is truncated (see contact's edit form). Remove th e linebreak in the VCF's address field and the URL will will be correct. UID:f8361311-b25c-4237-a94e-3015d8fe19de URL:http://some-webseite.com END:VCARD BEGIN:VCARD VERSION:3.0 ADR;TYPE=home:;;AAAAAAA-AäAAAAA 11;Urt;;00000; FN:Field after Address not broken N:;Broken;;; NOTE:this is identical to "Website broken"\, but with Note and URL swapped. The Note field does not get truncated as the URL field does. UID:462269fd-607c-4284-998b-6771143b63a7 URL:http://some-webseite.com END:VCARD BEGIN:VCARD VERSION:3.0 FN:Dr. med. Surname Name N:Name;Surname;;Dr. med.; NOTE:Sprechzeiten:\nMo 8–12\, 15–18\nDi 8–12\nMi 8–12\, 14 –15\nDo 14–19\nFr 8–12 -TITLE:Facharzt für Allgemeinmedizin +TITLE;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=46acharzt f=C3=BCr Allgemein + medizin UID:b4fc4930-043f-446a-9a76-cf0b548368d8 END:VCARD BEGIN:VCARD VERSION:3.0 ADR;TYPE=work:;;DDDDDDDDDDDstraße 26\n00000 DDDDDDDDDDDDDDDDD;;;; FN:Second Doctor N:Doctor;Second;;; NOTE:Sprechzeiten:\nMo 8–13\nDi 10–12\, 13–18\nMi 10–12\, 13 –18\nDo 13–18\nFr 8–12 TITLE:Dentist UID:941b057d-e959-4644-b41f-e122b26c3807 END:VCARD diff --git a/autotests/testroundtrip.cpp b/autotests/testroundtrip.cpp index 2d70cde9..77a9aca6 100644 --- a/autotests/testroundtrip.cpp +++ b/autotests/testroundtrip.cpp @@ -1,214 +1,214 @@ /* This file is part of the KContacts framework. Copyright (c) 2012 Kevin Krammer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "addressee.h" #include "helper_p.h" #include "converter/vcardconverter.h" #include #include #include #include using namespace KContacts; class RoundtripTest : public QObject { Q_OBJECT private: QDir mInputDir; QDir mOutput2_1Dir; QDir mOutput3_0Dir; QDir mOutput4_0Dir; QStringList mInputFiles; private Q_SLOTS: void initTestCase(); void testVCardRoundtrip_data(); void testVCardRoundtrip(); }; // check the validity of our test data set void RoundtripTest::initTestCase() { // check that all resource prefixes exist mInputDir = QDir(QStringLiteral(":/input")); QVERIFY(mInputDir.exists()); QVERIFY(mInputDir.cd(QStringLiteral("data"))); mOutput2_1Dir = QDir(QStringLiteral(":/output2.1")); QVERIFY(mOutput2_1Dir.exists()); QVERIFY(mOutput2_1Dir.cd(QStringLiteral("data"))); mOutput3_0Dir = QDir(QStringLiteral(":/output3.0")); QVERIFY(mOutput3_0Dir.exists()); QVERIFY(mOutput3_0Dir.cd(QStringLiteral("data"))); mOutput4_0Dir = QDir(QStringLiteral(":/output4.0")); QVERIFY(mOutput4_0Dir.exists()); QVERIFY(mOutput4_0Dir.cd(QStringLiteral("data"))); // check that there are input files mInputFiles = mInputDir.entryList(); QVERIFY(!mInputFiles.isEmpty()); } void RoundtripTest::testVCardRoundtrip_data() { QTest::addColumn("inputFile"); QTest::addColumn("output2_1File"); QTest::addColumn("output3_0File"); QTest::addColumn("output4_0File"); QString outFile21Pattern = QLatin1String("%1.2_1ref"); QString outFile4Pattern = QLatin1String("v4_0.%1.ref"); QString outFilePattern = QLatin1String("%1.ref"); for (const QString &inputFile : qAsConst(mInputFiles)) { const QString outFile = outFilePattern.arg(inputFile); const QString outFileV2_1 = outFile21Pattern.arg(inputFile); const QString outFileV4 = outFile4Pattern.arg(inputFile); QTest::newRow(QFile::encodeName(inputFile).constData()) << inputFile << (mOutput2_1Dir.exists(outFileV2_1) ? outFileV2_1 : QString()) << (mOutput3_0Dir.exists(outFile) ? outFile : QString()) << (mOutput4_0Dir.exists(outFileV4) ? outFileV4 : QString()); } } void RoundtripTest::testVCardRoundtrip() { QFETCH(QString, inputFile); QFETCH(QString, output2_1File); QFETCH(QString, output3_0File); QFETCH(QString, output4_0File); QVERIFY2(!output2_1File.isEmpty() || !output3_0File.isEmpty() || !output4_0File.isEmpty(), "No reference output file for either format version"); QFile input(QFileInfo(mInputDir, inputFile).absoluteFilePath()); QVERIFY(input.open(QIODevice::ReadOnly)); const QByteArray inputData = input.readAll(); QVERIFY(!inputData.isEmpty()); VCardConverter converter; const Addressee::List list = converter.parseVCards(inputData); QVERIFY(!list.isEmpty()); if (!output2_1File.isEmpty()) { const QByteArray outputData = converter.createVCards(list, VCardConverter::v2_1); QFile outputFile(QFileInfo(mOutput2_1Dir, output2_1File).absoluteFilePath()); QVERIFY(outputFile.open(QIODevice::ReadOnly)); const QByteArray outputRefData = outputFile.readAll(); QCOMPARE(outputData.size(), outputRefData.size()); const QList outputLines = outputData.split('\n'); const QList outputRefLines = outputRefData.split('\n'); QCOMPARE(outputLines.count(), outputRefLines.count()); for (int i = 0; i < outputLines.count(); ++i) { const QByteArray actual = outputLines[i]; const QByteArray expect = outputRefLines[i]; if (actual != expect) { qCritical() << "Mismatch in v2.1 output line" << (i + 1); QCOMPARE(actual.count(), expect.count()); qCritical() << "\nActual:" << actual << "\nExpect:" << expect; QCOMPARE(actual, expect); } } } if (!output3_0File.isEmpty()) { const QByteArray outputData = converter.createVCards(list, VCardConverter::v3_0); QFile outputFile(QFileInfo(mOutput3_0Dir, output3_0File).absoluteFilePath()); QVERIFY(outputFile.open(QIODevice::ReadOnly)); const QByteArray outputRefData = outputFile.readAll(); if (outputData.size() != outputRefData.size()) { qDebug() << " outputRefData " << outputRefData << endl; qDebug() << " outputData " << outputData; } - QCOMPARE(outputData.size(), outputRefData.size()); const QList outputLines = outputData.split('\n'); const QList outputRefLines = outputRefData.split('\n'); - QCOMPARE(outputLines.count(), outputRefLines.count()); - for (int i = 0; i < outputLines.count(); ++i) { + for (int i = 0; i < qMin(outputLines.count(), outputRefLines.count()); ++i) { const QByteArray actual = outputLines[i]; const QByteArray expect = outputRefLines[i]; if (actual != expect) { qCritical() << "Mismatch in v3.0 output line" << (i + 1); qCritical() << "\nActual:" << actual << "\nExpect:" << expect; QCOMPARE(actual.count(), expect.count()); QCOMPARE(actual, expect); } } + QCOMPARE(outputLines.count(), outputRefLines.count()); + QCOMPARE(outputData.size(), outputRefData.size()); } #if 0 if (!output4_0File.isEmpty()) { const QByteArray outputData = converter.createVCards(list, VCardConverter::v4_0); QFile outputFile(QFileInfo(mOutput4_0Dir, output4_0File).absoluteFilePath()); QVERIFY(outputFile.open(QIODevice::ReadOnly)); const QByteArray outputRefData = outputFile.readAll(); if (outputData.size() != outputRefData.size()) { qDebug() << " outputRefData " << outputRefData << endl; qDebug() << " outputData " << outputData; } //QCOMPARE( outputData.size(), outputRefData.size() ); const QList outputLines = outputData.split('\n'); const QList outputRefLines = outputRefData.split('\n'); //QCOMPARE(outputLines.count(), outputRefLines.count()); for (int i = 0; i < outputLines.count(); ++i) { const QByteArray actual = outputLines[i]; const QByteArray expect = outputRefLines[i]; if (actual != expect) { qCritical() << "Mismatch in v4.0 output line" << (i + 1); qCritical() << "\nActual:" << actual << "\nExpect:" << expect; QCOMPARE(actual.count(), expect.count()); QCOMPARE(actual, expect); } } } #endif } QTEST_MAIN(RoundtripTest) #include "testroundtrip.moc" diff --git a/autotests/testroundtrip.qrc b/autotests/testroundtrip.qrc index 4169cd4c..9fad99c8 100644 --- a/autotests/testroundtrip.qrc +++ b/autotests/testroundtrip.qrc @@ -1,65 +1,65 @@ data/vcard1.vcf data/vcard2.vcf - + data/vcard3.vcf + data/vcard4.vcf data/vcard5.vcf data/vcard6.vcf data/vcard7.vcf data/vcard8.vcf data/vcard9.vcf - + data/vcard10.vcf data/vcard11.vcf data/vcard12.vcf data/vcard13.vcf data/vcard14.vcf data/vcard15.vcf data/vcard1.vcf.2_1ref data/vcard2.vcf.2_1ref data/vcard3.vcf.2_1ref data/vcard6.vcf.2_1ref data/vcard7.vcf.2_1ref data/vcard12.vcf.2_1ref data/vcard14.vcf.2_1ref data/vcard1.vcf.ref data/vcard2.vcf.ref data/vcard3.vcf.ref data/vcard4.vcf.ref data/vcard5.vcf.ref data/vcard6.vcf.ref data/vcard7.vcf.ref data/vcard8.vcf.ref data/vcard9.vcf.ref data/vcard10.vcf.ref data/vcard11.vcf.ref data/vcard12.vcf.ref data/vcard13.vcf.ref data/vcard14.vcf.ref data/vcard15.vcf.ref data/v4_0.vcard1.vcf.ref data/v4_0.vcard2.vcf.ref data/v4_0.vcard3.vcf.ref data/v4_0.vcard4.vcf.ref data/v4_0.vcard5.vcf.ref data/v4_0.vcard6.vcf.ref data/v4_0.vcard7.vcf.ref data/v4_0.vcard8.vcf.ref diff --git a/src/vcardparser/vcardparser.cpp b/src/vcardparser/vcardparser.cpp index 51c52a08..03462b4a 100644 --- a/src/vcardparser/vcardparser.cpp +++ b/src/vcardparser/vcardparser.cpp @@ -1,456 +1,408 @@ /* This file is part of the KContacts framework. Copyright (c) 2003 Tobias Koenig This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "vcardparser.h" #include #include "kcontacts_debug.h" #include // This cache for QString::fromLatin1() isn't about improving speed, but about reducing memory usage by sharing common strings class StringCache { public: QString fromLatin1(const QByteArray &value) { if (value.isEmpty()) { return QString(); } QHash::const_iterator it = m_values.constFind(value); if (it != m_values.constEnd()) { return it.value(); } QString string = QString::fromLatin1(value); m_values.insert(value, string); return string; } private: QHash m_values; }; using namespace KContacts; static void addEscapes(QByteArray &str, bool excludeEscapedComma) { str.replace('\\', "\\\\"); if (!excludeEscapedComma) { str.replace(',', "\\,"); } str.replace('\r', "\\r"); str.replace('\n', "\\n"); } static void removeEscapes(QByteArray &str) { // It's more likely that no escape is present, so add fast path if (!str.contains('\\')) { return; } str.replace("\\n", "\n"); str.replace("\\N", "\n"); str.replace("\\r", "\r"); str.replace("\\,", ","); str.replace("\\\\", "\\"); } VCardParser::VCardParser() : d(nullptr) { } VCardParser::~VCardParser() { } VCard::List VCardParser::parseVCards(const QByteArray &text) { VCard currentVCard; VCard::List vCardList; QByteArray currentLine; const QList lines = text.split('\n'); bool inVCard = false; const QList::const_iterator linesEnd(lines.end()); StringCache cache; for (auto it = lines.begin(); it != linesEnd; ++it) { QByteArray cur = *it; // remove the trailing \r, left from \r\n if (cur.endsWith('\r')) { cur.chop(1); } if (cur.startsWith(' ') || cur.startsWith('\t')) { //folded line => append to previous currentLine.append(cur.mid(1)); continue; } else { if (cur.trimmed().isEmpty()) { // empty line continue; } if (inVCard && !currentLine.isEmpty()) { // now parse the line - int colon = currentLine.indexOf(':'); + // ### The syntax is key:value, but the key can contain semicolon-separated parameters, which can contain a ':', so indexOf(':') is wrong. + // EXAMPLE: "ADR;GEO=\"geo:22.500000,45.099998\";LABEL=\"My Label\";TYPE=home:P.O. Box 101;;;Any Town;CA;91921-1234; + const int colon = currentLine.indexOf(':'); if (colon == -1) { // invalid line currentLine = cur; continue; } - bool keyFound = false; - QByteArray key; - QList params; - const int currentLineLength(currentLine.length()); - for (int i = 0; i < currentLineLength; ++i) { - char character = currentLine.at(i); - if (keyFound) { - const QList tmpParams = currentLine.mid(i).split(';'); - QByteArray tmpParameter; - bool valueAdded = false; - for (const QByteArray ¶meter : tmpParams) { - if (parameter.contains('=')) { - if (tmpParameter.isEmpty()) { - tmpParameter = parameter; - } else { - params << tmpParameter; - tmpParameter = parameter; - } - } else { - if (tmpParameter.isEmpty() && !valueAdded) { - tmpParameter = parameter; - valueAdded = true; - } else { - tmpParameter += ';' + parameter; - } - } - } - if (!tmpParameter.isEmpty()) { - params << tmpParameter; - } - break; - } else { - if ((character == ';' || character == ':') && !keyFound) { - keyFound = true; - } else { - key += character; - } - } - } VCardLine vCardLine; - QByteArray value; - if (!params.isEmpty()) { - value = params.takeLast(); - if (value.contains('=')) { - colon = value.indexOf(':'); - const QByteArray lastParam = value.left(colon).trimmed(); - if ((lastParam != "geo")) { - params.append(lastParam); - value = value.mid(colon + 1); - } else { - params.append(value); - } - } - } - params.prepend(key); + const QByteArray key = currentLine.left(colon).trimmed(); + QByteArray value = currentLine.mid(colon + 1); + const QList params = key.split(';'); + //qDebug() << "key=" << QString::fromLatin1(key) << "params=" << params; // check for group const QByteArray firstParam = params.at(0); const int groupPos = firstParam.indexOf('.'); if (groupPos != -1) { vCardLine.setGroup(cache.fromLatin1(firstParam.left(groupPos))); vCardLine.setIdentifier(cache.fromLatin1(firstParam.mid(groupPos + 1))); + //qDebug() << "group" << vCardLine.group() << "identifier" << vCardLine.identifier(); } else { vCardLine.setIdentifier(cache.fromLatin1(firstParam)); + //qDebug() << "identifier" << vCardLine.identifier(); } if (params.count() > 1) { // find all parameters QList::ConstIterator paramIt(params.constBegin()); for (++paramIt; paramIt != params.constEnd(); ++paramIt) { + //qDebug() << "param" << QString::fromLatin1(*paramIt); QList pair = (*paramIt).split('='); QByteArray first = pair.at(0).toLower(); if (pair.count() == 1) { // correct the fucking 2.1 'standard' if (first == "quoted-printable") { pair[ 0 ] = "encoding"; pair.append("quoted-printable"); } else if (first == "base64") { pair[ 0 ] = "encoding"; pair.append("base64"); } else { pair.prepend("type"); } first = pair.at(0); } const QByteArray second = pair.at(1); - if (second.contains(':')) { - vCardLine.addParameter(cache.fromLatin1(first), - cache.fromLatin1(second)); - } else if (second.contains(',')) { // parameter in type=x,y,z format + if (second.contains(',')) { // parameter in type=x,y,z format const QList args = second.split(','); for (QByteArray tmpArg : args) { if (tmpArg.startsWith('"')) { tmpArg = tmpArg.mid(1); } if (tmpArg.endsWith('"')) { tmpArg.chop(1); } vCardLine.addParameter(cache.fromLatin1(first), cache.fromLatin1(tmpArg)); } } else { vCardLine.addParameter(cache.fromLatin1(first), cache.fromLatin1(second)); } } } removeEscapes(value); QByteArray output; bool wasBase64Encoded = false; const QString encoding = vCardLine.parameter(QStringLiteral("encoding")).toLower(); if (!encoding.isEmpty()) { // have to decode the data if (encoding == QLatin1String("b") || encoding == QLatin1String("base64")) { output = QByteArray::fromBase64(value); wasBase64Encoded = true; } else if (encoding == QLatin1String("quoted-printable")) { // join any qp-folded lines while (value.endsWith('=') && it != linesEnd) { value.chop(1); // remove the '=' value.append(cur); cur = *(++it); // remove the trailing \r, left from \r\n if (cur.endsWith('\r')) { cur.chop(1); } } KCodecs::quotedPrintableDecode(value, output); } else if (encoding == QLatin1String("8bit")) { output = value; } else { qDebug("Unknown vcard encoding type!"); } } else { output = value; } const QString charset = vCardLine.parameter(QStringLiteral("charset")); if (!charset.isEmpty()) { // have to convert the data QTextCodec *codec = QTextCodec::codecForName(charset.toLatin1()); if (codec) { vCardLine.setValue(codec->toUnicode(output)); } else { vCardLine.setValue(QString::fromUtf8(output)); } } else if (wasBase64Encoded) { vCardLine.setValue(output); } else { vCardLine.setValue(QString::fromUtf8(output)); } currentVCard.addLine(vCardLine); } const QByteArray curLower = cur.toLower(); // we do not save the start and end tag as vcardline if (curLower.startsWith("begin:vcard")) { //krazy:exclude=strings inVCard = true; currentLine.clear(); currentVCard.clear(); // flush vcard continue; } if (curLower.startsWith("end:vcard")) { //krazy:exclude=strings inVCard = false; vCardList.append(currentVCard); currentLine.clear(); currentVCard.clear(); // flush vcard continue; } currentLine = cur; } } return vCardList; } static const int FOLD_WIDTH = 75; QByteArray VCardParser::createVCards(const VCard::List &list) { QByteArray text; QByteArray textLine; QString encodingType; QStringList idents; QStringList params; QStringList values; QStringList::ConstIterator identIt; QStringList::Iterator paramIt; QStringList::ConstIterator valueIt; VCardLine::List lines; VCardLine::List::ConstIterator lineIt; VCard::List::ConstIterator cardIt; bool hasEncoding; text.reserve(list.size() * 300); // reserve memory to be more efficient // iterate over the cards const VCard::List::ConstIterator listEnd(list.end()); for (cardIt = list.begin(); cardIt != listEnd; ++cardIt) { text.append("BEGIN:VCARD\r\n"); idents = (*cardIt).identifiers(); //VERSION must be first if (idents.contains(QStringLiteral("VERSION"))) { const QString str = idents.takeAt(idents.indexOf(QStringLiteral("VERSION"))); idents.prepend(str); } for (identIt = idents.constBegin(); identIt != idents.constEnd(); ++identIt) { lines = (*cardIt).lines((*identIt)); // iterate over the lines for (lineIt = lines.constBegin(); lineIt != lines.constEnd(); ++lineIt) { QVariant val = (*lineIt).value(); if (val.isValid()) { if ((*lineIt).hasGroup()) { textLine = (*lineIt).group().toLatin1() + '.' + (*lineIt).identifier().toLatin1(); } else { textLine = (*lineIt).identifier().toLatin1(); } params = (*lineIt).parameterList(); hasEncoding = false; if (!params.isEmpty()) { // we have parameters for (paramIt = params.begin(); paramIt != params.end(); ++paramIt) { if ((*paramIt) == QLatin1String("encoding")) { hasEncoding = true; encodingType = (*lineIt).parameter(QStringLiteral("encoding")).toLower(); } values = (*lineIt).parameters(*paramIt); for (valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt) { textLine.append(';' + (*paramIt).toLatin1().toUpper()); if (!(*valueIt).isEmpty()) { textLine.append('=' + (*valueIt).toLatin1()); } } } } QByteArray input, output; bool checkMultibyte = false; // avoid splitting a multibyte character // handle charset const QString charset = (*lineIt).parameter(QStringLiteral("charset")); if (!charset.isEmpty()) { // have to convert the data const QString value = (*lineIt).value().toString(); QTextCodec *codec = QTextCodec::codecForName(charset.toLatin1()); if (codec) { input = codec->fromUnicode(value); } else { checkMultibyte = true; input = value.toUtf8(); } } else if ((*lineIt).value().type() == QVariant::ByteArray) { input = (*lineIt).value().toByteArray(); } else { checkMultibyte = true; input = (*lineIt).value().toString().toUtf8(); } // handle encoding if (hasEncoding) { // have to encode the data if (encodingType == QLatin1String("b")) { checkMultibyte = false; output = input.toBase64(); } else if (encodingType == QLatin1String("quoted-printable")) { checkMultibyte = false; KCodecs::quotedPrintableEncode(input, output, false); } } else { output = input; } addEscapes(output, ((*lineIt).identifier() == QLatin1String("CATEGORIES") || (*lineIt).identifier() == QLatin1String("GEO"))); if (!output.isEmpty()) { textLine.append(':' + output); if (textLine.length() > FOLD_WIDTH) { // we have to fold the line if (checkMultibyte) { // RFC 6350: Multi-octet characters MUST remain contiguous. // we know that textLine contains UTF-8 encoded characters int lineLength = 0; for (int i = 0; i < textLine.length(); ++i) { if ((textLine[i] & 0xC0) == 0xC0) { // a multibyte sequence follows int sequenceLength = 2; if ((textLine[i] & 0xE0) == 0xE0) { sequenceLength = 3; } else if ((textLine[i] & 0xF0) == 0xF0) { sequenceLength = 4; } if ((lineLength + sequenceLength) > FOLD_WIDTH) { // the current line would be too long. fold it text += "\r\n " + textLine.mid(i, sequenceLength); lineLength = 1 + sequenceLength; // incl. leading space } else { text += textLine.mid(i, sequenceLength); lineLength += sequenceLength; } i += sequenceLength - 1; } else { text += textLine[i]; ++lineLength; } if ((lineLength == FOLD_WIDTH) && (i < (textLine.length() - 1))) { text += "\r\n "; lineLength = 1; // leading space } } text += "\r\n"; } else { for (int i = 0; i <= (textLine.length() / FOLD_WIDTH); ++i) { text.append( (i == 0 ? "" : " ") + textLine.mid(i * FOLD_WIDTH, FOLD_WIDTH) + "\r\n"); } } } else { text.append(textLine + "\r\n"); } } } } } text.append("END:VCARD\r\n"); text.append("\r\n"); } return text; }