File indexing completed on 2024-09-01 13:20:53
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 }