File indexing completed on 2024-04-14 14:18:29

0001 /*
0002     SPDX-FileCopyrightText: 2004 Matt Douhan <matt@fruitsalad.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "kemailaddress.h"
0008 #include "kcodecs.h"
0009 #include "kcodecs_debug.h"
0010 
0011 #include <QRegularExpression>
0012 
0013 using namespace KEmailAddress;
0014 
0015 //-----------------------------------------------------------------------------
0016 QStringList KEmailAddress::splitAddressList(const QString &aStr)
0017 {
0018     // Features:
0019     // - always ignores quoted characters
0020     // - ignores everything (including parentheses and commas)
0021     //   inside quoted strings
0022     // - supports nested comments
0023     // - ignores everything (including double quotes and commas)
0024     //   inside comments
0025 
0026     QStringList list;
0027 
0028     if (aStr.isEmpty()) {
0029         return list;
0030     }
0031 
0032     QString addr;
0033     uint addrstart = 0;
0034     int commentlevel = 0;
0035     bool insidequote = false;
0036 
0037     for (int index = 0; index < aStr.length(); index++) {
0038         // the following conversion to latin1 is o.k. because
0039         // we can safely ignore all non-latin1 characters
0040         switch (aStr[index].toLatin1()) {
0041         case '"': // start or end of quoted string
0042             if (commentlevel == 0) {
0043                 insidequote = !insidequote;
0044             }
0045             break;
0046         case '(': // start of comment
0047             if (!insidequote) {
0048                 ++commentlevel;
0049             }
0050             break;
0051         case ')': // end of comment
0052             if (!insidequote) {
0053                 if (commentlevel > 0) {
0054                     --commentlevel;
0055                 } else {
0056                     return list;
0057                 }
0058             }
0059             break;
0060         case '\\': // quoted character
0061             index++; // ignore the quoted character
0062             break;
0063         case ',':
0064         case ';':
0065             if (!insidequote && (commentlevel == 0)) {
0066                 addr = aStr.mid(addrstart, index - addrstart);
0067                 if (!addr.isEmpty()) {
0068                     list += addr.trimmed();
0069                 }
0070                 addrstart = index + 1;
0071             }
0072             break;
0073         }
0074     }
0075     // append the last address to the list
0076     if (!insidequote && (commentlevel == 0)) {
0077         addr = aStr.mid(addrstart, aStr.length() - addrstart);
0078         if (!addr.isEmpty()) {
0079             list += addr.trimmed();
0080         }
0081     }
0082 
0083     return list;
0084 }
0085 
0086 //-----------------------------------------------------------------------------
0087 // Used by KEmailAddress::splitAddress(...) and KEmailAddress::firstEmailAddress(...).
0088 KEmailAddress::EmailParseResult
0089 splitAddressInternal(const QByteArray &address, QByteArray &displayName, QByteArray &addrSpec, QByteArray &comment, bool allowMultipleAddresses)
0090 {
0091     //  qCDebug(KCODECS_LOG) << "address";
0092     displayName = "";
0093     addrSpec = "";
0094     comment = "";
0095 
0096     if (address.isEmpty()) {
0097         return AddressEmpty;
0098     }
0099 
0100     // The following is a primitive parser for a mailbox-list (cf. RFC 2822).
0101     // The purpose is to extract a displayable string from the mailboxes.
0102     // Comments in the addr-spec are not handled. No error checking is done.
0103 
0104     enum {
0105         TopLevel,
0106         InComment,
0107         InAngleAddress,
0108     } context = TopLevel;
0109     bool inQuotedString = false;
0110     int commentLevel = 0;
0111     bool stop = false;
0112 
0113     for (const char *p = address.data(); *p && !stop; ++p) {
0114         switch (context) {
0115         case TopLevel: {
0116             switch (*p) {
0117             case '"':
0118                 inQuotedString = !inQuotedString;
0119                 displayName += *p;
0120                 break;
0121             case '(':
0122                 if (!inQuotedString) {
0123                     context = InComment;
0124                     commentLevel = 1;
0125                 } else {
0126                     displayName += *p;
0127                 }
0128                 break;
0129             case '<':
0130                 if (!inQuotedString) {
0131                     context = InAngleAddress;
0132                 } else {
0133                     displayName += *p;
0134                 }
0135                 break;
0136             case '\\': // quoted character
0137                 displayName += *p;
0138                 ++p; // skip the '\'
0139                 if (*p) {
0140                     displayName += *p;
0141                 } else {
0142                     return UnexpectedEnd;
0143                 }
0144                 break;
0145             case ',':
0146                 if (!inQuotedString) {
0147                     if (allowMultipleAddresses) {
0148                         stop = true;
0149                     } else {
0150                         return UnexpectedComma;
0151                     }
0152                 } else {
0153                     displayName += *p;
0154                 }
0155                 break;
0156             default:
0157                 displayName += *p;
0158             }
0159             break;
0160         }
0161         case InComment: {
0162             switch (*p) {
0163             case '(':
0164                 ++commentLevel;
0165                 comment += *p;
0166                 break;
0167             case ')':
0168                 --commentLevel;
0169                 if (commentLevel == 0) {
0170                     context = TopLevel;
0171                     comment += ' '; // separate the text of several comments
0172                 } else {
0173                     comment += *p;
0174                 }
0175                 break;
0176             case '\\': // quoted character
0177                 comment += *p;
0178                 ++p; // skip the '\'
0179                 if (*p) {
0180                     comment += *p;
0181                 } else {
0182                     return UnexpectedEnd;
0183                 }
0184                 break;
0185             default:
0186                 comment += *p;
0187             }
0188             break;
0189         }
0190         case InAngleAddress: {
0191             switch (*p) {
0192             case '"':
0193                 inQuotedString = !inQuotedString;
0194                 addrSpec += *p;
0195                 break;
0196             case '>':
0197                 if (!inQuotedString) {
0198                     context = TopLevel;
0199                 } else {
0200                     addrSpec += *p;
0201                 }
0202                 break;
0203             case '\\': // quoted character
0204                 addrSpec += *p;
0205                 ++p; // skip the '\'
0206                 if (*p) {
0207                     addrSpec += *p;
0208                 } else {
0209                     return UnexpectedEnd;
0210                 }
0211                 break;
0212             default:
0213                 addrSpec += *p;
0214             }
0215             break;
0216         }
0217         } // switch ( context )
0218     }
0219     // check for errors
0220     if (inQuotedString) {
0221         return UnbalancedQuote;
0222     }
0223     if (context == InComment) {
0224         return UnbalancedParens;
0225     }
0226     if (context == InAngleAddress) {
0227         return UnclosedAngleAddr;
0228     }
0229 
0230     displayName = displayName.trimmed();
0231     comment = comment.trimmed();
0232     addrSpec = addrSpec.trimmed();
0233 
0234     if (addrSpec.isEmpty()) {
0235         if (displayName.isEmpty()) {
0236             return NoAddressSpec;
0237         } else {
0238             addrSpec = displayName;
0239             displayName.truncate(0);
0240         }
0241     }
0242     /*
0243       qCDebug(KCODECS_LOG) << "display-name : \"" << displayName << "\"";
0244       qCDebug(KCODECS_LOG) << "comment      : \"" << comment << "\"";
0245       qCDebug(KCODECS_LOG) << "addr-spec    : \"" << addrSpec << "\"";
0246     */
0247     return AddressOk;
0248 }
0249 
0250 //-----------------------------------------------------------------------------
0251 EmailParseResult KEmailAddress::splitAddress(const QByteArray &address, QByteArray &displayName, QByteArray &addrSpec, QByteArray &comment)
0252 {
0253     return splitAddressInternal(address, displayName, addrSpec, comment, false /* don't allow multiple addresses */);
0254 }
0255 
0256 //-----------------------------------------------------------------------------
0257 EmailParseResult KEmailAddress::splitAddress(const QString &address, QString &displayName, QString &addrSpec, QString &comment)
0258 {
0259     QByteArray d;
0260     QByteArray a;
0261     QByteArray c;
0262     // FIXME: toUtf8() is probably not safe here, what if the second byte of a multi-byte character
0263     //        has the same code as one of the ASCII characters that splitAddress uses as delimiters?
0264     EmailParseResult result = splitAddress(address.toUtf8(), d, a, c);
0265 
0266     if (result == AddressOk) {
0267         displayName = QString::fromUtf8(d);
0268         addrSpec = QString::fromUtf8(a);
0269         comment = QString::fromUtf8(c);
0270     }
0271     return result;
0272 }
0273 
0274 //-----------------------------------------------------------------------------
0275 EmailParseResult KEmailAddress::isValidAddress(const QString &aStr)
0276 {
0277     // If we are passed an empty string bail right away no need to process
0278     // further and waste resources
0279     if (aStr.isEmpty()) {
0280         return AddressEmpty;
0281     }
0282 
0283     // count how many @'s are in the string that is passed to us
0284     // if 0 or > 1 take action
0285     // at this point to many @'s cannot bail out right away since
0286     // @ is allowed in quotes, so we use a bool to keep track
0287     // and then make a judgment further down in the parser
0288 
0289     bool tooManyAtsFlag = false;
0290 
0291     int atCount = aStr.count(QLatin1Char('@'));
0292     if (atCount > 1) {
0293         tooManyAtsFlag = true;
0294     } else if (atCount == 0) {
0295         return TooFewAts;
0296     }
0297 
0298     int dotCount = aStr.count(QLatin1Char('.'));
0299 
0300     // The main parser, try and catch all weird and wonderful
0301     // mistakes users and/or machines can create
0302 
0303     enum {
0304         TopLevel,
0305         InComment,
0306         InAngleAddress,
0307     } context = TopLevel;
0308     bool inQuotedString = false;
0309     int commentLevel = 0;
0310 
0311     unsigned int strlen = aStr.length();
0312 
0313     for (unsigned int index = 0; index < strlen; index++) {
0314         switch (context) {
0315         case TopLevel: {
0316             switch (aStr[index].toLatin1()) {
0317             case '"':
0318                 inQuotedString = !inQuotedString;
0319                 break;
0320             case '(':
0321                 if (!inQuotedString) {
0322                     context = InComment;
0323                     commentLevel = 1;
0324                 }
0325                 break;
0326             case '[':
0327                 if (!inQuotedString) {
0328                     return InvalidDisplayName;
0329                 }
0330                 break;
0331             case ']':
0332                 if (!inQuotedString) {
0333                     return InvalidDisplayName;
0334                 }
0335                 break;
0336             case ':':
0337                 if (!inQuotedString) {
0338                     return DisallowedChar;
0339                 }
0340                 break;
0341             case '<':
0342                 if (!inQuotedString) {
0343                     context = InAngleAddress;
0344                 }
0345                 break;
0346             case '\\': // quoted character
0347                 ++index; // skip the '\'
0348                 if ((index + 1) > strlen) {
0349                     return UnexpectedEnd;
0350                 }
0351                 break;
0352             case ',':
0353                 if (!inQuotedString) {
0354                     return UnexpectedComma;
0355                 }
0356                 break;
0357             case ')':
0358                 if (!inQuotedString) {
0359                     return UnbalancedParens;
0360                 }
0361                 break;
0362             case '>':
0363                 if (!inQuotedString) {
0364                     return UnopenedAngleAddr;
0365                 }
0366                 break;
0367             case '@':
0368                 if (!inQuotedString) {
0369                     if (index == 0) { // Missing local part
0370                         return MissingLocalPart;
0371                     } else if (index == strlen - 1) {
0372                         return MissingDomainPart;
0373                     }
0374                 } else {
0375                     --atCount;
0376                     if (atCount == 1) {
0377                         tooManyAtsFlag = false;
0378                     }
0379                 }
0380                 break;
0381             case '.':
0382                 if (inQuotedString) {
0383                     --dotCount;
0384                 }
0385                 break;
0386             }
0387             break;
0388         }
0389         case InComment: {
0390             switch (aStr[index].toLatin1()) {
0391             case '(':
0392                 ++commentLevel;
0393                 break;
0394             case ')':
0395                 --commentLevel;
0396                 if (commentLevel == 0) {
0397                     context = TopLevel;
0398                 }
0399                 break;
0400             case '\\': // quoted character
0401                 ++index; // skip the '\'
0402                 if ((index + 1) > strlen) {
0403                     return UnexpectedEnd;
0404                 }
0405                 break;
0406             }
0407             break;
0408         }
0409 
0410         case InAngleAddress: {
0411             switch (aStr[index].toLatin1()) {
0412             case ',':
0413                 if (!inQuotedString) {
0414                     return UnexpectedComma;
0415                 }
0416                 break;
0417             case '"':
0418                 inQuotedString = !inQuotedString;
0419                 break;
0420             case '@':
0421                 if (inQuotedString) {
0422                     --atCount;
0423                 }
0424                 if (atCount == 1) {
0425                     tooManyAtsFlag = false;
0426                 }
0427                 break;
0428             case '.':
0429                 if (inQuotedString) {
0430                     --dotCount;
0431                 }
0432                 break;
0433             case '>':
0434                 if (!inQuotedString) {
0435                     context = TopLevel;
0436                     break;
0437                 }
0438                 break;
0439             case '\\': // quoted character
0440                 ++index; // skip the '\'
0441                 if ((index + 1) > strlen) {
0442                     return UnexpectedEnd;
0443                 }
0444                 break;
0445             }
0446             break;
0447         }
0448         }
0449     }
0450 
0451     if (dotCount == 0 && !inQuotedString) {
0452         return TooFewDots;
0453     }
0454 
0455     if (atCount == 0 && !inQuotedString) {
0456         return TooFewAts;
0457     }
0458 
0459     if (inQuotedString) {
0460         return UnbalancedQuote;
0461     }
0462 
0463     if (context == InComment) {
0464         return UnbalancedParens;
0465     }
0466 
0467     if (context == InAngleAddress) {
0468         return UnclosedAngleAddr;
0469     }
0470 
0471     if (tooManyAtsFlag) {
0472         return TooManyAts;
0473     }
0474 
0475     return AddressOk;
0476 }
0477 
0478 //-----------------------------------------------------------------------------
0479 KEmailAddress::EmailParseResult KEmailAddress::isValidAddressList(const QString &aStr, QString &badAddr)
0480 {
0481     if (aStr.isEmpty()) {
0482         return AddressEmpty;
0483     }
0484 
0485     const QStringList list = splitAddressList(aStr);
0486     EmailParseResult errorCode = AddressOk;
0487     auto it = std::find_if(list.cbegin(), list.cend(), [&errorCode](const QString &addr) {
0488         qCDebug(KCODECS_LOG) << " address" << addr;
0489         errorCode = isValidAddress(addr);
0490         return errorCode != AddressOk;
0491     });
0492     if (it != list.cend()) {
0493         badAddr = *it;
0494     }
0495     return errorCode;
0496 }
0497 
0498 //-----------------------------------------------------------------------------
0499 QString KEmailAddress::emailParseResultToString(EmailParseResult errorCode)
0500 {
0501     switch (errorCode) {
0502     case TooManyAts:
0503         return QObject::tr(
0504             "The email address you entered is not valid because it "
0505             "contains more than one @.\n"
0506             "You will not create valid messages if you do not "
0507             "change your address.");
0508     case TooFewAts:
0509         return QObject::tr(
0510             "The email address you entered is not valid because it "
0511             "does not contain a @.\n"
0512             "You will not create valid messages if you do not "
0513             "change your address.");
0514     case AddressEmpty:
0515         return QObject::tr("You have to enter something in the email address field.");
0516     case MissingLocalPart:
0517         return QObject::tr(
0518             "The email address you entered is not valid because it "
0519             "does not contain a local part.");
0520     case MissingDomainPart:
0521         return QObject::tr(
0522             "The email address you entered is not valid because it "
0523             "does not contain a domain part.");
0524     case UnbalancedParens:
0525         return QObject::tr(
0526             "The email address you entered is not valid because it "
0527             "contains unclosed comments/brackets.");
0528     case AddressOk:
0529         return QObject::tr("The email address you entered is valid.");
0530     case UnclosedAngleAddr:
0531         return QObject::tr(
0532             "The email address you entered is not valid because it "
0533             "contains an unclosed angle bracket.");
0534     case UnopenedAngleAddr:
0535         return QObject::tr(
0536             "The email address you entered is not valid because it "
0537             "contains too many closing angle brackets.");
0538     case UnexpectedComma:
0539         return QObject::tr(
0540             "The email address you have entered is not valid because it "
0541             "contains an unexpected comma.");
0542     case UnexpectedEnd:
0543         return QObject::tr(
0544             "The email address you entered is not valid because it ended "
0545             "unexpectedly.\nThis probably means you have used an escaping "
0546             "type character like a '\\' as the last character in your "
0547             "email address.");
0548     case UnbalancedQuote:
0549         return QObject::tr(
0550             "The email address you entered is not valid because it "
0551             "contains quoted text which does not end.");
0552     case NoAddressSpec:
0553         return QObject::tr(
0554             "The email address you entered is not valid because it "
0555             "does not seem to contain an actual email address, i.e. "
0556             "something of the form joe@example.org.");
0557     case DisallowedChar:
0558         return QObject::tr(
0559             "The email address you entered is not valid because it "
0560             "contains an illegal character.");
0561     case InvalidDisplayName:
0562         return QObject::tr(
0563             "The email address you have entered is not valid because it "
0564             "contains an invalid display name.");
0565     case TooFewDots:
0566         return QObject::tr(
0567             "The email address you entered is not valid because it "
0568             "does not contain a \'.\'.\n"
0569             "You will not create valid messages if you do not "
0570             "change your address.");
0571     }
0572     return QObject::tr("Unknown problem with email address");
0573 }
0574 
0575 //-----------------------------------------------------------------------------
0576 bool KEmailAddress::isValidSimpleAddress(const QString &aStr)
0577 {
0578     // If we are passed an empty string bail right away no need to process further
0579     // and waste resources
0580     if (aStr.isEmpty()) {
0581         return false;
0582     }
0583 
0584     int atChar = aStr.lastIndexOf(QLatin1Char('@'));
0585     QString domainPart = aStr.mid(atChar + 1);
0586     QString localPart = aStr.left(atChar);
0587 
0588     // Both of these parts must be non empty
0589     // after all we cannot have emails like:
0590     // @kde.org, or  foo@
0591     if (localPart.isEmpty() || domainPart.isEmpty()) {
0592         return false;
0593     }
0594 
0595     bool inQuotedString = false;
0596     int atCount = localPart.count(QLatin1Char('@'));
0597 
0598     unsigned int strlen = localPart.length();
0599     for (unsigned int index = 0; index < strlen; index++) {
0600         switch (localPart[index].toLatin1()) {
0601         case '"':
0602             inQuotedString = !inQuotedString;
0603             break;
0604         case '@':
0605             if (inQuotedString) {
0606                 --atCount;
0607             }
0608             break;
0609         }
0610     }
0611 
0612     QString addrRx;
0613 
0614     if (localPart[0] == QLatin1Char('\"') || localPart[localPart.length() - 1] == QLatin1Char('\"')) {
0615         addrRx = QStringLiteral("\"[a-zA-Z@]*[\\w.@-]*[a-zA-Z0-9@]\"@");
0616     } else {
0617         addrRx = QStringLiteral("[a-zA-Z]*[~|{}`\\^?=/+*'&%$#!_\\w.-]*[~|{}`\\^?=/+*'&%$#!_a-zA-Z0-9-]@");
0618     }
0619     if (domainPart[0] == QLatin1Char('[') || domainPart[domainPart.length() - 1] == QLatin1Char(']')) {
0620         addrRx += QStringLiteral("\\[[0-9]{1,3}(\\.[0-9]{1,3}){3}\\]");
0621     } else {
0622         addrRx += QStringLiteral("[\\w#-]+(\\.[\\w#-]+)*");
0623     }
0624 
0625     const QRegularExpression rx(QRegularExpression::anchoredPattern(addrRx), QRegularExpression::UseUnicodePropertiesOption);
0626     return rx.match(aStr).hasMatch();
0627 }
0628 
0629 //-----------------------------------------------------------------------------
0630 QString KEmailAddress::simpleEmailAddressErrorMsg()
0631 {
0632     return QObject::tr(
0633         "The email address you entered is not valid.\nIt "
0634         "does not seem to contain an actual email address, i.e. "
0635         "something of the form joe@example.org.");
0636 }
0637 
0638 //-----------------------------------------------------------------------------
0639 QByteArray KEmailAddress::extractEmailAddress(const QByteArray &address)
0640 {
0641     QString errorMessage;
0642     return extractEmailAddress(address, errorMessage);
0643 }
0644 
0645 QByteArray KEmailAddress::extractEmailAddress(const QByteArray &address, QString &errorMessage)
0646 {
0647     QByteArray dummy1;
0648     QByteArray dummy2;
0649     QByteArray addrSpec;
0650     const EmailParseResult result = splitAddressInternal(address, dummy1, addrSpec, dummy2, false /* don't allow multiple addresses */);
0651     if (result != AddressOk) {
0652         addrSpec = QByteArray();
0653         if (result != AddressEmpty) {
0654             errorMessage = emailParseResultToString(result);
0655             qCDebug(KCODECS_LOG) << "Input:" << address << "\nError:" << errorMessage;
0656         }
0657     } else {
0658         errorMessage.clear();
0659     }
0660 
0661     return addrSpec;
0662 }
0663 
0664 //-----------------------------------------------------------------------------
0665 QString KEmailAddress::extractEmailAddress(const QString &address)
0666 {
0667     QString errorMessage;
0668     return extractEmailAddress(address, errorMessage);
0669 }
0670 
0671 QString KEmailAddress::extractEmailAddress(const QString &address, QString &errorMessage)
0672 {
0673     return QString::fromUtf8(extractEmailAddress(address.toUtf8(), errorMessage));
0674 }
0675 
0676 //-----------------------------------------------------------------------------
0677 QByteArray KEmailAddress::firstEmailAddress(const QByteArray &addresses)
0678 {
0679     QString errorMessage;
0680     return firstEmailAddress(addresses, errorMessage);
0681 }
0682 
0683 QByteArray KEmailAddress::firstEmailAddress(const QByteArray &addresses, QString &errorMessage)
0684 {
0685     QByteArray dummy1;
0686     QByteArray dummy2;
0687     QByteArray addrSpec;
0688     const EmailParseResult result = splitAddressInternal(addresses, dummy1, addrSpec, dummy2, true /* allow multiple addresses */);
0689     if (result != AddressOk) {
0690         addrSpec = QByteArray();
0691         if (result != AddressEmpty) {
0692             errorMessage = emailParseResultToString(result);
0693             qCDebug(KCODECS_LOG) << "Input: aStr\nError:" << errorMessage;
0694         }
0695     } else {
0696         errorMessage.clear();
0697     }
0698 
0699     return addrSpec;
0700 }
0701 
0702 //-----------------------------------------------------------------------------
0703 QString KEmailAddress::firstEmailAddress(const QString &addresses)
0704 {
0705     QString errorMessage;
0706     return firstEmailAddress(addresses, errorMessage);
0707 }
0708 
0709 QString KEmailAddress::firstEmailAddress(const QString &addresses, QString &errorMessage)
0710 {
0711     return QString::fromUtf8(firstEmailAddress(addresses.toUtf8(), errorMessage));
0712 }
0713 
0714 //-----------------------------------------------------------------------------
0715 bool KEmailAddress::extractEmailAddressAndName(const QString &aStr, QString &mail, QString &name)
0716 {
0717     name.clear();
0718     mail.clear();
0719 
0720     const int len = aStr.length();
0721     const char cQuotes = '"';
0722 
0723     bool bInComment = false;
0724     bool bInQuotesOutsideOfEmail = false;
0725     int i = 0;
0726     int iAd = 0;
0727     int iMailStart = 0;
0728     int iMailEnd = 0;
0729     QChar c;
0730     unsigned int commentstack = 0;
0731 
0732     // Find the '@' of the email address
0733     // skipping all '@' inside "(...)" comments:
0734     while (i < len) {
0735         c = aStr[i];
0736         if (QLatin1Char('(') == c) {
0737             ++commentstack;
0738         }
0739         if (QLatin1Char(')') == c) {
0740             --commentstack;
0741         }
0742         bInComment = commentstack != 0;
0743         if (QLatin1Char('"') == c && !bInComment) {
0744             bInQuotesOutsideOfEmail = !bInQuotesOutsideOfEmail;
0745         }
0746 
0747         if (!bInComment && !bInQuotesOutsideOfEmail) {
0748             if (QLatin1Char('@') == c) {
0749                 iAd = i;
0750                 break; // found it
0751             }
0752         }
0753         ++i;
0754     }
0755 
0756     if (!iAd) {
0757         // We suppose the user is typing the string manually and just
0758         // has not finished typing the mail address part.
0759         // So we take everything that's left of the '<' as name and the rest as mail
0760         for (i = 0; len > i; ++i) {
0761             c = aStr[i];
0762             if (QLatin1Char('<') != c) {
0763                 name.append(c);
0764             } else {
0765                 break;
0766             }
0767         }
0768         mail = aStr.mid(i + 1);
0769         if (mail.endsWith(QLatin1Char('>'))) {
0770             mail.truncate(mail.length() - 1);
0771         }
0772 
0773     } else {
0774         // Loop backwards until we find the start of the string
0775         // or a ',' that is outside of a comment
0776         //          and outside of quoted text before the leading '<'.
0777         bInComment = false;
0778         bInQuotesOutsideOfEmail = false;
0779         for (i = iAd - 1; 0 <= i; --i) {
0780             c = aStr[i];
0781             if (bInComment) {
0782                 if (QLatin1Char('(') == c) {
0783                     if (!name.isEmpty()) {
0784                         name.prepend(QLatin1Char(' '));
0785                     }
0786                     bInComment = false;
0787                 } else {
0788                     name.prepend(c); // all comment stuff is part of the name
0789                 }
0790             } else if (bInQuotesOutsideOfEmail) {
0791                 if (QLatin1Char(cQuotes) == c) {
0792                     bInQuotesOutsideOfEmail = false;
0793                 } else if (c != QLatin1Char('\\')) {
0794                     name.prepend(c);
0795                 }
0796             } else {
0797                 // found the start of this addressee ?
0798                 if (QLatin1Char(',') == c) {
0799                     break;
0800                 }
0801                 // stuff is before the leading '<' ?
0802                 if (iMailStart) {
0803                     if (QLatin1Char(cQuotes) == c) {
0804                         bInQuotesOutsideOfEmail = true; // end of quoted text found
0805                     } else {
0806                         name.prepend(c);
0807                     }
0808                 } else {
0809                     switch (c.toLatin1()) {
0810                     case '<':
0811                         iMailStart = i;
0812                         break;
0813                     case ')':
0814                         if (!name.isEmpty()) {
0815                             name.prepend(QLatin1Char(' '));
0816                         }
0817                         bInComment = true;
0818                         break;
0819                     default:
0820                         if (QLatin1Char(' ') != c) {
0821                             mail.prepend(c);
0822                         }
0823                     }
0824                 }
0825             }
0826         }
0827 
0828         name = name.simplified();
0829         mail = mail.simplified();
0830 
0831         if (mail.isEmpty()) {
0832             return false;
0833         }
0834 
0835         mail.append(QLatin1Char('@'));
0836 
0837         // Loop forward until we find the end of the string
0838         // or a ',' that is outside of a comment
0839         //          and outside of quoted text behind the trailing '>'.
0840         bInComment = false;
0841         bInQuotesOutsideOfEmail = false;
0842         int parenthesesNesting = 0;
0843         for (i = iAd + 1; len > i; ++i) {
0844             c = aStr[i];
0845             if (bInComment) {
0846                 if (QLatin1Char(')') == c) {
0847                     if (--parenthesesNesting == 0) {
0848                         bInComment = false;
0849                         if (!name.isEmpty()) {
0850                             name.append(QLatin1Char(' '));
0851                         }
0852                     } else {
0853                         // nested ")", add it
0854                         name.append(QLatin1Char(')')); // name can't be empty here
0855                     }
0856                 } else {
0857                     if (QLatin1Char('(') == c) {
0858                         // nested "("
0859                         ++parenthesesNesting;
0860                     }
0861                     name.append(c); // all comment stuff is part of the name
0862                 }
0863             } else if (bInQuotesOutsideOfEmail) {
0864                 if (QLatin1Char(cQuotes) == c) {
0865                     bInQuotesOutsideOfEmail = false;
0866                 } else if (c != QLatin1Char('\\')) {
0867                     name.append(c);
0868                 }
0869             } else {
0870                 // found the end of this addressee ?
0871                 if (QLatin1Char(',') == c) {
0872                     break;
0873                 }
0874                 // stuff is behind the trailing '>' ?
0875                 if (iMailEnd) {
0876                     if (QLatin1Char(cQuotes) == c) {
0877                         bInQuotesOutsideOfEmail = true; // start of quoted text found
0878                     } else {
0879                         name.append(c);
0880                     }
0881                 } else {
0882                     switch (c.toLatin1()) {
0883                     case '>':
0884                         iMailEnd = i;
0885                         break;
0886                     case '(':
0887                         if (!name.isEmpty()) {
0888                             name.append(QLatin1Char(' '));
0889                         }
0890                         if (++parenthesesNesting > 0) {
0891                             bInComment = true;
0892                         }
0893                         break;
0894                     default:
0895                         if (QLatin1Char(' ') != c) {
0896                             mail.append(c);
0897                         }
0898                     }
0899                 }
0900             }
0901         }
0902     }
0903 
0904     name = name.simplified();
0905     mail = mail.simplified();
0906 
0907     return !(name.isEmpty() || mail.isEmpty());
0908 }
0909 
0910 //-----------------------------------------------------------------------------
0911 bool KEmailAddress::compareEmail(const QString &email1, const QString &email2, bool matchName)
0912 {
0913     QString e1Name;
0914     QString e1Email;
0915     QString e2Name;
0916     QString e2Email;
0917 
0918     extractEmailAddressAndName(email1, e1Email, e1Name);
0919     extractEmailAddressAndName(email2, e2Email, e2Name);
0920 
0921     return e1Email == e2Email && (!matchName || (e1Name == e2Name));
0922 }
0923 
0924 //-----------------------------------------------------------------------------
0925 // Used internally by normalizedAddress()
0926 QString removeBidiControlChars(const QString &input)
0927 {
0928     constexpr QChar LRO(0x202D);
0929     constexpr QChar RLO(0x202E);
0930     constexpr QChar LRE(0x202A);
0931     constexpr QChar RLE(0x202B);
0932     QString result = input;
0933     result.remove(LRO);
0934     result.remove(RLO);
0935     result.remove(LRE);
0936     result.remove(RLE);
0937     return result;
0938 }
0939 
0940 QString KEmailAddress::normalizedAddress(const QString &displayName, const QString &addrSpec, const QString &comment)
0941 {
0942     const QString realDisplayName = removeBidiControlChars(displayName);
0943     if (realDisplayName.isEmpty() && comment.isEmpty()) {
0944         return addrSpec;
0945     } else if (comment.isEmpty()) {
0946         if (!realDisplayName.startsWith(QLatin1Char('\"'))) {
0947             return quoteNameIfNecessary(realDisplayName) + QLatin1String(" <") + addrSpec + QLatin1Char('>');
0948         } else {
0949             return realDisplayName + QLatin1String(" <") + addrSpec + QLatin1Char('>');
0950         }
0951     } else if (realDisplayName.isEmpty()) {
0952         return quoteNameIfNecessary(comment) + QLatin1String(" <") + addrSpec + QLatin1Char('>');
0953     } else {
0954         return realDisplayName + QLatin1String(" (") + comment + QLatin1String(") <") + addrSpec + QLatin1Char('>');
0955     }
0956 }
0957 
0958 //-----------------------------------------------------------------------------
0959 QString KEmailAddress::fromIdn(const QString &addrSpec)
0960 {
0961     const int atPos = addrSpec.lastIndexOf(QLatin1Char('@'));
0962     if (atPos == -1) {
0963         return addrSpec;
0964     }
0965 
0966     QString idn = QUrl::fromAce(addrSpec.mid(atPos + 1).toLatin1());
0967     if (idn.isEmpty()) {
0968         return QString();
0969     }
0970 
0971     return addrSpec.left(atPos + 1) + idn;
0972 }
0973 
0974 //-----------------------------------------------------------------------------
0975 QString KEmailAddress::toIdn(const QString &addrSpec)
0976 {
0977     const int atPos = addrSpec.lastIndexOf(QLatin1Char('@'));
0978     if (atPos == -1) {
0979         return addrSpec;
0980     }
0981 
0982     QString idn = QLatin1String(QUrl::toAce(addrSpec.mid(atPos + 1)));
0983     if (idn.isEmpty()) {
0984         return addrSpec;
0985     }
0986 
0987     return addrSpec.left(atPos + 1) + idn;
0988 }
0989 
0990 //-----------------------------------------------------------------------------
0991 QString KEmailAddress::normalizeAddressesAndDecodeIdn(const QString &str)
0992 {
0993     //  qCDebug(KCODECS_LOG) << str;
0994     if (str.isEmpty()) {
0995         return str;
0996     }
0997 
0998     const QStringList addressList = splitAddressList(str);
0999     QStringList normalizedAddressList;
1000 
1001     QByteArray displayName;
1002     QByteArray addrSpec;
1003     QByteArray comment;
1004 
1005     for (const auto &addr : addressList) {
1006         if (!addr.isEmpty()) {
1007             if (splitAddress(addr.toUtf8(), displayName, addrSpec, comment) == AddressOk) {
1008                 QByteArray cs;
1009                 displayName = KCodecs::decodeRFC2047String(displayName, &cs).toUtf8();
1010                 comment = KCodecs::decodeRFC2047String(comment, &cs).toUtf8();
1011 
1012                 normalizedAddressList << normalizedAddress(QString::fromUtf8(displayName), fromIdn(QString::fromUtf8(addrSpec)), QString::fromUtf8(comment));
1013             }
1014         }
1015     }
1016     /*
1017       qCDebug(KCODECS_LOG) << "normalizedAddressList: \""
1018                << normalizedAddressList.join( ", " )
1019                << "\"";
1020     */
1021     return normalizedAddressList.join(QStringLiteral(", "));
1022 }
1023 
1024 //-----------------------------------------------------------------------------
1025 QString KEmailAddress::normalizeAddressesAndEncodeIdn(const QString &str)
1026 {
1027     // qCDebug(KCODECS_LOG) << str;
1028     if (str.isEmpty()) {
1029         return str;
1030     }
1031 
1032     const QStringList addressList = splitAddressList(str);
1033     QStringList normalizedAddressList;
1034 
1035     QByteArray displayName;
1036     QByteArray addrSpec;
1037     QByteArray comment;
1038 
1039     for (const auto &addr : addressList) {
1040         if (!addr.isEmpty()) {
1041             if (splitAddress(addr.toUtf8(), displayName, addrSpec, comment) == AddressOk) {
1042                 normalizedAddressList << normalizedAddress(QString::fromUtf8(displayName), toIdn(QString::fromUtf8(addrSpec)), QString::fromUtf8(comment));
1043             }
1044         }
1045     }
1046 
1047     /*
1048       qCDebug(KCODECS_LOG) << "normalizedAddressList: \""
1049                << normalizedAddressList.join( ", " )
1050                << "\"";
1051     */
1052     return normalizedAddressList.join(QStringLiteral(", "));
1053 }
1054 
1055 //-----------------------------------------------------------------------------
1056 // Escapes unescaped doublequotes in str.
1057 static QString escapeQuotes(const QString &str)
1058 {
1059     if (str.isEmpty()) {
1060         return QString();
1061     }
1062 
1063     QString escaped;
1064     // reserve enough memory for the worst case ( """..."" -> \"\"\"...\"\" )
1065     escaped.reserve(2 * str.length());
1066     unsigned int len = 0;
1067     for (int i = 0, total = str.length(); i < total; ++i, ++len) {
1068         const QChar &c = str[i];
1069         if (c == QLatin1Char('"')) { // unescaped doublequote
1070             escaped.append(QLatin1Char('\\'));
1071             ++len;
1072         } else if (c == QLatin1Char('\\')) { // escaped character
1073             escaped.append(QLatin1Char('\\'));
1074             ++len;
1075             ++i;
1076             if (i >= str.length()) { // handle trailing '\' gracefully
1077                 break;
1078             }
1079         }
1080         // Keep str[i] as we increase i previously
1081         escaped.append(str[i]);
1082     }
1083     escaped.truncate(len);
1084     return escaped;
1085 }
1086 
1087 //-----------------------------------------------------------------------------
1088 QString KEmailAddress::quoteNameIfNecessary(const QString &str)
1089 {
1090     if (str.isEmpty()) {
1091         return str;
1092     }
1093     QString quoted = str;
1094 
1095     static const QRegularExpression needQuotes(QStringLiteral("[^ 0-9A-Za-z\\x{0080}-\\x{FFFF}]"));
1096     // avoid double quoting
1097     if ((quoted[0] == QLatin1Char('"')) && (quoted[quoted.length() - 1] == QLatin1Char('"'))) {
1098         quoted = QLatin1String("\"") + escapeQuotes(quoted.mid(1, quoted.length() - 2)) + QLatin1String("\"");
1099     } else if (quoted.indexOf(needQuotes) != -1) {
1100         quoted = QLatin1String("\"") + escapeQuotes(quoted) + QLatin1String("\"");
1101     }
1102 
1103     return quoted;
1104 }
1105 
1106 QUrl KEmailAddress::encodeMailtoUrl(const QString &mailbox)
1107 {
1108     const QByteArray encodedPath = KCodecs::encodeRFC2047String(mailbox, "utf-8");
1109     QUrl mailtoUrl;
1110     mailtoUrl.setScheme(QStringLiteral("mailto"));
1111     mailtoUrl.setPath(QLatin1String(encodedPath));
1112     return mailtoUrl;
1113 }
1114 
1115 QString KEmailAddress::decodeMailtoUrl(const QUrl &mailtoUrl)
1116 {
1117     Q_ASSERT(mailtoUrl.scheme() == QLatin1String("mailto"));
1118     return KCodecs::decodeRFC2047String(mailtoUrl.path());
1119 }