File indexing completed on 2025-03-09 04:54:13
0001 /* 0002 SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org> 0003 SPDX-FileCopyrightText: 2009 Thomas McGuire <mcguire@kde.org> 0004 0005 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0006 */ 0007 #include "stringutil.h" 0008 0009 #include "MessageCore/MessageCoreSettings" 0010 #include "config-enterprise.h" 0011 0012 #include <KEmailAddress> 0013 #include <KLocalizedString> 0014 #include <KMime/HeaderParsing> 0015 #include <KMime/Headers> 0016 #include <KMime/Message> 0017 0018 #include "messagecore_debug.h" 0019 #include <KUser> 0020 0021 #include <KCodecs> 0022 #include <KIdentityManagementCore/Identity> 0023 #include <KIdentityManagementCore/IdentityManager> 0024 #include <KPIMTextEdit/TextUtils> 0025 #include <QHostInfo> 0026 #include <QRegularExpression> 0027 #include <QUrlQuery> 0028 0029 using namespace KMime; 0030 using namespace KMime::Types; 0031 using namespace KMime::HeaderParsing; 0032 0033 namespace MessageCore 0034 { 0035 namespace StringUtil 0036 { 0037 // Removes trailing spaces and tabs at the end of the line 0038 static void removeTrailingSpace(QString &line) 0039 { 0040 int i = line.length() - 1; 0041 while ((i >= 0) && ((line[i] == QLatin1Char(' ')) || (line[i] == QLatin1Char('\t')))) { 0042 i--; 0043 } 0044 line.truncate(i + 1); 0045 } 0046 0047 // Splits the line off in two parts: The quote prefixes and the actual text of the line. 0048 // For example, for the string "> > > Hello", it would be split up in "> > > " as the quote 0049 // prefix, and "Hello" as the actual text. 0050 // The actual text is written back to the "line" parameter, and the quote prefix is returned. 0051 static QString splitLine(QString &line) 0052 { 0053 removeTrailingSpace(line); 0054 int i = 0; 0055 int startOfActualText = -1; 0056 0057 // TODO: Replace tabs with spaces first. 0058 0059 // Loop through the chars in the line to find the place where the quote prefix stops 0060 const int lineLength(line.length()); 0061 while (i < lineLength) { 0062 const QChar c = line[i]; 0063 const bool isAllowedQuoteChar = 0064 (c == QLatin1Char('>')) || (c == QLatin1Char(':')) || (c == QLatin1Char('|')) || (c == QLatin1Char(' ')) || (c == QLatin1Char('\t')); 0065 if (isAllowedQuoteChar) { 0066 startOfActualText = i + 1; 0067 } else { 0068 break; 0069 } 0070 ++i; 0071 } 0072 0073 // If the quote prefix only consists of whitespace, don't consider it as a quote prefix at all 0074 if (line.left(startOfActualText).trimmed().isEmpty()) { 0075 startOfActualText = 0; 0076 } 0077 0078 // No quote prefix there -> nothing to do 0079 if (startOfActualText <= 0) { 0080 return {}; 0081 } 0082 0083 // Entire line consists of only the quote prefix 0084 if (i == line.length()) { 0085 const QString quotePrefix = line.left(startOfActualText); 0086 line.clear(); 0087 return quotePrefix; 0088 } 0089 0090 // Line contains both the quote prefix and the actual text, really split it up now 0091 const QString quotePrefix = line.left(startOfActualText); 0092 line = line.mid(startOfActualText); 0093 0094 return quotePrefix; 0095 } 0096 0097 // Writes all lines/text parts contained in the "textParts" list to the output text, "msg". 0098 // Quote characters are added in front of each line, and no line is longer than 0099 // maxLength. 0100 // 0101 // Although the lines in textParts are considered separate lines, they can actually be run 0102 // together into a single line in some cases. This is basically the main difference to flowText(). 0103 // 0104 // Example: 0105 // textParts = "Hello World, this is a test.", "Really" 0106 // indent = ">" 0107 // maxLength = 20 0108 // Result: "> Hello World, this\n 0109 // > is a test. Really" 0110 // Notice how in this example, the text line "Really" is no longer a separate line, it was run 0111 // together with a previously broken line. 0112 // 0113 // "textParts" is cleared upon return. 0114 static bool flushPart(QString &msg, QStringList &textParts, const QString &indent, int maxLength) 0115 { 0116 if (maxLength < 20) { 0117 maxLength = 20; 0118 } 0119 0120 // Remove empty lines at end of quote 0121 while (!textParts.isEmpty() && textParts.last().isEmpty()) { 0122 textParts.removeLast(); 0123 } 0124 0125 QString text; 0126 0127 for (const QString &line : textParts) { 0128 // An empty line in the input means that an empty line should be in the output as well. 0129 // Therefore, we write all of our text so far to the msg. 0130 if (line.isEmpty()) { 0131 if (!text.isEmpty()) { 0132 msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength) + QLatin1Char('\n'); 0133 } 0134 msg += indent + QLatin1Char('\n'); 0135 } else { 0136 if (text.isEmpty()) { 0137 text = line; 0138 } else { 0139 text += QLatin1Char(' ') + line.trimmed(); 0140 } 0141 // If the line doesn't need to be wrapped at all, just write it out as-is. 0142 // When a line exceeds the maximum length and therefore needs to be broken, this statement 0143 // if false, and therefore we keep adding lines to our text, so they get ran together in the 0144 // next flowText call, as "text" contains several text parts/lines then. 0145 if ((text.length() < maxLength) || (line.length() < (maxLength - 10))) { 0146 msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength) + QLatin1Char('\n'); 0147 } 0148 } 0149 } 0150 0151 // Write out pending text to the msg 0152 if (!text.isEmpty()) { 0153 msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength); 0154 } 0155 0156 const bool appendEmptyLine = !textParts.isEmpty(); 0157 textParts.clear(); 0158 0159 return appendEmptyLine; 0160 } 0161 0162 QList<QPair<QString, QString>> parseMailtoUrl(const QUrl &url) 0163 { 0164 QList<QPair<QString, QString>> values; 0165 if (url.scheme() != QLatin1StringView("mailto")) { 0166 return values; 0167 } 0168 QString str = url.toString(); 0169 QStringList toStr; 0170 int i = 0; 0171 0172 // String can be encoded. 0173 str = KCodecs::decodeRFC2047String(str); 0174 // Bug 427697 0175 str.replace(QStringLiteral("&"), QStringLiteral("&")); 0176 const QUrl newUrl = QUrl::fromUserInput(str); 0177 0178 int indexTo = -1; 0179 // Workaround line with # see bug 406208 0180 const int indexOf = str.indexOf(QLatin1Char('?')); 0181 if (indexOf != -1) { 0182 str.remove(0, indexOf + 1); 0183 QUrlQuery query(str); 0184 const auto listQuery = query.queryItems(QUrl::FullyDecoded); 0185 for (const auto &queryItem : listQuery) { 0186 if (queryItem.first == QLatin1StringView("to")) { 0187 toStr << queryItem.second; 0188 indexTo = i; 0189 } else { 0190 if (queryItem.second.isEmpty()) { 0191 // Bug 206269 => A%26B => encoded '&' 0192 if (i >= 1) { 0193 values[i - 1].second = values[i - 1].second + QStringLiteral("&") + queryItem.first; 0194 } 0195 } else { 0196 QPair<QString, QString> pairElement; 0197 pairElement.first = queryItem.first.toLower(); 0198 pairElement.second = queryItem.second; 0199 values.append(pairElement); 0200 i++; 0201 } 0202 } 0203 } 0204 } 0205 QStringList to = {KEmailAddress::decodeMailtoUrl(newUrl)}; 0206 if (!toStr.isEmpty()) { 0207 to << toStr; 0208 } 0209 const QString fullTo = to.join(QLatin1StringView(", ")); 0210 if (!fullTo.isEmpty()) { 0211 QPair<QString, QString> pairElement; 0212 pairElement.first = QStringLiteral("to"); 0213 pairElement.second = fullTo; 0214 if (indexTo != -1) { 0215 values.insert(indexTo, pairElement); 0216 } else { 0217 values.prepend(pairElement); 0218 } 0219 } 0220 return values; 0221 } 0222 0223 QString stripSignature(const QString &msg) 0224 { 0225 // Following RFC 3676, only > before -- 0226 // I prefer to not delete a SB instead of delete good mail content. 0227 static const QRegularExpression sbDelimiterSearch(QStringLiteral("(^|\n)[> ]*-- \n")); 0228 // The regular expression to look for prefix change 0229 static const QRegularExpression commonReplySearch(QStringLiteral("^[ ]*>")); 0230 0231 QString res = msg; 0232 int posDeletingStart = 1; // to start looking at 0 0233 0234 // While there are SB delimiters (start looking just before the deleted SB) 0235 while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) { 0236 QString prefix; // the current prefix 0237 QString line; // the line to check if is part of the SB 0238 int posNewLine = -1; 0239 0240 // Look for the SB beginning 0241 const int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart); 0242 // The prefix before "-- "$ 0243 if (res.at(posDeletingStart) == QLatin1Char('\n')) { 0244 ++posDeletingStart; 0245 } 0246 0247 prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart); 0248 posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1; 0249 0250 // now go to the end of the SB 0251 while (posNewLine < res.size() && posNewLine > 0) { 0252 // handle the undefined case for mid ( x , -n ) where n>1 0253 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine); 0254 0255 if (nextPosNewLine < 0) { 0256 nextPosNewLine = posNewLine - 1; 0257 } 0258 0259 line = res.mid(posNewLine, nextPosNewLine - posNewLine); 0260 0261 // check when the SB ends: 0262 // * does not starts with prefix or 0263 // * starts with prefix+(any substring of prefix) 0264 if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) 0265 || (!prefix.isEmpty() && line.startsWith(prefix) && line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) { 0266 posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1; 0267 } else { 0268 break; // end of the SB 0269 } 0270 } 0271 0272 // remove the SB or truncate when is the last SB 0273 if (posNewLine > 0) { 0274 res.remove(posDeletingStart, posNewLine - posDeletingStart); 0275 } else { 0276 res.truncate(posDeletingStart); 0277 } 0278 } 0279 0280 return res; 0281 } 0282 0283 AddressList splitAddressField(const QByteArray &text) 0284 { 0285 AddressList result; 0286 const char *begin = text.begin(); 0287 if (!begin) { 0288 return result; 0289 } 0290 0291 const char *const end = text.begin() + text.length(); 0292 0293 if (!parseAddressList(begin, end, result)) { 0294 qCWarning(MESSAGECORE_LOG) << "Error in address splitting: parseAddressList returned false!" << text; 0295 } 0296 0297 return result; 0298 } 0299 0300 QString generateMessageId(const QString &address, const QString &suffix) 0301 { 0302 const QDateTime dateTime = QDateTime::currentDateTime(); 0303 0304 QString msgIdStr = QLatin1Char('<') + dateTime.toString(QStringLiteral("yyyyMMddhhmm.sszzz")); 0305 0306 if (!suffix.isEmpty()) { 0307 msgIdStr += QLatin1Char('@') + suffix; 0308 } else { 0309 msgIdStr += QLatin1Char('.') + KEmailAddress::toIdn(address); 0310 } 0311 0312 msgIdStr += QLatin1Char('>'); 0313 0314 return msgIdStr; 0315 } 0316 0317 QString quoteHtmlChars(const QString &str, bool removeLineBreaks) 0318 { 0319 QString result; 0320 0321 int strLength(str.length()); 0322 result.reserve(6 * strLength); // maximal possible length 0323 for (int i = 0; i < strLength; ++i) { 0324 switch (str[i].toLatin1()) { 0325 case '<': 0326 result += QLatin1StringView("<"); 0327 break; 0328 case '>': 0329 result += QLatin1StringView(">"); 0330 break; 0331 case '&': 0332 result += QLatin1StringView("&"); 0333 break; 0334 case '"': 0335 result += QLatin1StringView("""); 0336 break; 0337 case '\n': 0338 if (!removeLineBreaks) { 0339 result += QLatin1StringView("<br>"); 0340 } 0341 break; 0342 case '\r': 0343 // ignore CR 0344 break; 0345 default: 0346 result += str[i]; 0347 } 0348 } 0349 0350 result.squeeze(); 0351 return result; 0352 } 0353 0354 void removePrivateHeaderFields(const KMime::Message::Ptr &message, bool cleanUpHeader) 0355 { 0356 message->removeHeader("Status"); 0357 message->removeHeader("X-Status"); 0358 message->removeHeader("X-KMail-EncryptionState"); 0359 message->removeHeader("X-KMail-SignatureState"); 0360 message->removeHeader("X-KMail-Redirect-From"); 0361 message->removeHeader("X-KMail-Link-Message"); 0362 message->removeHeader("X-KMail-Link-Type"); 0363 message->removeHeader("X-KMail-QuotePrefix"); 0364 message->removeHeader("X-KMail-CursorPos"); 0365 message->removeHeader("X-KMail-Templates"); 0366 message->removeHeader("X-KMail-Drafts"); 0367 message->removeHeader("X-KMail-UnExpanded-To"); 0368 message->removeHeader("X-KMail-UnExpanded-CC"); 0369 message->removeHeader("X-KMail-UnExpanded-BCC"); 0370 message->removeHeader("X-KMail-UnExpanded-Reply-To"); 0371 message->removeHeader("X-KMail-FccDisabled"); 0372 0373 if (cleanUpHeader) { 0374 message->removeHeader("X-KMail-Fcc"); 0375 message->removeHeader("X-KMail-Transport"); 0376 message->removeHeader("X-KMail-Identity"); 0377 message->removeHeader("X-KMail-Transport-Name"); 0378 message->removeHeader("X-KMail-Identity-Name"); 0379 message->removeHeader("X-KMail-Dictionary"); 0380 } 0381 } 0382 0383 QByteArray asSendableString(const KMime::Message::Ptr &originalMessage) 0384 { 0385 KMime::Message::Ptr message(new KMime::Message); 0386 message->setContent(originalMessage->encodedContent()); 0387 0388 removePrivateHeaderFields(message); 0389 message->removeHeader<KMime::Headers::Bcc>(); 0390 0391 return message->encodedContent(); 0392 } 0393 0394 QByteArray headerAsSendableString(const KMime::Message::Ptr &originalMessage) 0395 { 0396 KMime::Message::Ptr message(new KMime::Message); 0397 message->setContent(originalMessage->encodedContent()); 0398 0399 removePrivateHeaderFields(message); 0400 message->removeHeader<KMime::Headers::Bcc>(); 0401 0402 return message->head(); 0403 } 0404 0405 QString emailAddrAsAnchor(const KMime::Types::Mailbox::List &mailboxList, 0406 Display display, 0407 const QString &cssStyle, 0408 Link link, 0409 AddressMode expandable, 0410 const QString &fieldName, 0411 int collapseNumber) 0412 { 0413 QString result; 0414 int numberAddresses = 0; 0415 bool expandableInserted = false; 0416 KIdentityManagementCore::IdentityManager *im = KIdentityManagementCore::IdentityManager::self(); 0417 0418 const QString i18nMe = i18nc("signal that this email is defined in my identity", "Me"); 0419 const bool onlyOneIdentity = (im->identities().count() == 1); 0420 for (const KMime::Types::Mailbox &mailbox : mailboxList) { 0421 const QString prettyAddressStr = mailbox.prettyAddress(); 0422 if (!prettyAddressStr.isEmpty()) { 0423 numberAddresses++; 0424 if (expandable == ExpandableAddresses && !expandableInserted && numberAddresses > collapseNumber) { 0425 const QString actualListAddress = result; 0426 QString shortListAddress = actualListAddress; 0427 if (link == ShowLink) { 0428 shortListAddress.truncate(result.length() - 2); 0429 } 0430 result = QStringLiteral("<span><input type=\"checkbox\" class=\"addresslist_checkbox\" id=\"%1\" checked=\"checked\"/><span class=\"short%1\">") 0431 .arg(fieldName) 0432 + shortListAddress; 0433 result += QStringLiteral("<label class=\"addresslist_label_short\" for=\"%1\"></label></span>").arg(fieldName); 0434 expandableInserted = true; 0435 result += QStringLiteral("<span class=\"full%1\">").arg(fieldName) + actualListAddress; 0436 } 0437 0438 if (link == ShowLink) { 0439 result += QLatin1StringView("<a href=\"mailto:") 0440 + QString::fromLatin1( 0441 QUrl::toPercentEncoding(KEmailAddress::encodeMailtoUrl(mailbox.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary)).path())) 0442 + QLatin1StringView("\" ") + cssStyle + QLatin1Char('>'); 0443 } 0444 const bool foundMe = onlyOneIdentity && (im->identityForAddress(prettyAddressStr) != KIdentityManagementCore::Identity::null()); 0445 0446 if (display == DisplayNameOnly) { 0447 if (!mailbox.name().isEmpty()) { // Fallback to the email address when the name is not set. 0448 result += foundMe ? i18nMe : quoteHtmlChars(mailbox.name(), true); 0449 } else { 0450 result += foundMe ? i18nMe : quoteHtmlChars(prettyAddressStr, true); 0451 } 0452 } else { 0453 result += foundMe ? i18nMe : quoteHtmlChars(mailbox.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary), true); 0454 } 0455 if (link == ShowLink) { 0456 result += QLatin1StringView("</a>, "); 0457 } 0458 } 0459 } 0460 0461 if (link == ShowLink) { 0462 result.chop(2); 0463 } 0464 0465 if (expandableInserted) { 0466 result += QStringLiteral("<label class=\"addresslist_label_full\" for=\"%1\"></label></span></span>").arg(fieldName); 0467 } 0468 return result; 0469 } 0470 0471 QString emailAddrAsAnchor(const KMime::Headers::Generics::MailboxList *mailboxList, 0472 Display display, 0473 const QString &cssStyle, 0474 Link link, 0475 AddressMode expandable, 0476 const QString &fieldName, 0477 int collapseNumber) 0478 { 0479 Q_ASSERT(mailboxList); 0480 return emailAddrAsAnchor(mailboxList->mailboxes(), display, cssStyle, link, expandable, fieldName, collapseNumber); 0481 } 0482 0483 QString emailAddrAsAnchor(const KMime::Headers::Generics::AddressList *addressList, 0484 Display display, 0485 const QString &cssStyle, 0486 Link link, 0487 AddressMode expandable, 0488 const QString &fieldName, 0489 int collapseNumber) 0490 { 0491 Q_ASSERT(addressList); 0492 return emailAddrAsAnchor(addressList->mailboxes(), display, cssStyle, link, expandable, fieldName, collapseNumber); 0493 } 0494 0495 bool addressIsInAddressList(const QString &address, const QStringList &addresses) 0496 { 0497 const QString addrSpec = KEmailAddress::extractEmailAddress(address); 0498 0499 QStringList::ConstIterator end(addresses.constEnd()); 0500 for (QStringList::ConstIterator it = addresses.constBegin(); it != end; ++it) { 0501 if (qstricmp(addrSpec.toUtf8().data(), KEmailAddress::extractEmailAddress(*it).toUtf8().data()) == 0) { 0502 return true; 0503 } 0504 } 0505 0506 return false; 0507 } 0508 0509 QString guessEmailAddressFromLoginName(const QString &loginName) 0510 { 0511 if (loginName.isEmpty()) { 0512 return {}; 0513 } 0514 0515 QString address = loginName; 0516 address += QLatin1Char('@'); 0517 address += QHostInfo::localHostName(); 0518 0519 // try to determine the real name 0520 const KUser user(loginName); 0521 if (user.isValid()) { 0522 const QString fullName = user.property(KUser::FullName).toString(); 0523 address = KEmailAddress::quoteNameIfNecessary(fullName) + QLatin1StringView(" <") + address + QLatin1Char('>'); 0524 } 0525 0526 return address; 0527 } 0528 0529 QString smartQuote(const QString &msg, int maxLineLength) 0530 { 0531 // The algorithm here is as follows: 0532 // We split up the incoming msg into lines, and then iterate over each line. 0533 // We keep adding lines with the same indent ( = quote prefix, e.g. "> " ) to a 0534 // "textParts" list. So the textParts list contains only lines with the same quote 0535 // prefix. 0536 // 0537 // When all lines with the same indent are collected in "textParts", we write those out 0538 // to the result by calling flushPart(), which does all the nice formatting for us. 0539 0540 QStringList textParts; 0541 QString oldIndent; 0542 bool firstPart = true; 0543 QString result; 0544 0545 int lineStart = 0; 0546 int lineEnd = msg.indexOf(QLatin1Char('\n')); 0547 bool needToContinue = true; 0548 for (; needToContinue; lineStart = lineEnd + 1, lineEnd = msg.indexOf(QLatin1Char('\n'), lineStart)) { 0549 QString line; 0550 if (lineEnd == -1) { 0551 if (lineStart == 0) { 0552 line = msg; 0553 needToContinue = false; 0554 } else if (lineStart != msg.length()) { 0555 line = msg.mid(lineStart, msg.length() - lineStart); 0556 needToContinue = false; 0557 } else { 0558 needToContinue = false; 0559 } 0560 } else { 0561 line = msg.mid(lineStart, lineEnd - lineStart); 0562 } 0563 // Split off the indent from the line 0564 const QString indent = splitLine(line); 0565 0566 if (line.isEmpty()) { 0567 if (!firstPart) { 0568 textParts.append(QString()); 0569 } 0570 continue; 0571 } 0572 0573 if (firstPart) { 0574 oldIndent = indent; 0575 firstPart = false; 0576 } 0577 0578 // The indent changed, that means we have to write everything contained in textParts to the 0579 // result, which we do by calling flushPart(). 0580 if (oldIndent != indent) { 0581 // Check if the last non-blank line is a "From" line. A from line is the line containing the 0582 // attribution to a quote, e.g. "Yesterday, you wrote:". We'll just check for the last colon 0583 // here, to simply things. 0584 // If there is a From line, remove it from the textParts to that flushPart won't break it. 0585 // We'll manually add it to the result afterwards. 0586 QString fromLine; 0587 if (!textParts.isEmpty()) { 0588 for (int i = textParts.count() - 1; i >= 0; i--) { 0589 // Check if we have found the From line 0590 const QString textPartElement(textParts[i]); 0591 if (textPartElement.endsWith(QLatin1Char(':'))) { 0592 fromLine = oldIndent + textPartElement + QLatin1Char('\n'); 0593 textParts.removeAt(i); 0594 break; 0595 } 0596 0597 // Abort on first non-empty line 0598 if (!textPartElement.trimmed().isEmpty()) { 0599 break; 0600 } 0601 } 0602 } 0603 0604 // Write out all lines with the same indent using flushPart(). The textParts list 0605 // is cleared for us. 0606 if (flushPart(result, textParts, oldIndent, maxLineLength)) { 0607 if (oldIndent.length() > indent.length()) { 0608 result += indent + QLatin1Char('\n'); 0609 } else { 0610 result += oldIndent + QLatin1Char('\n'); 0611 } 0612 } 0613 0614 if (!fromLine.isEmpty()) { 0615 result += fromLine; 0616 } 0617 0618 oldIndent = indent; 0619 } 0620 0621 textParts.append(line); 0622 } 0623 0624 // Write out anything still pending 0625 flushPart(result, textParts, oldIndent, maxLineLength); 0626 0627 // Remove superfluous newline which was appended in flowText 0628 if (!result.isEmpty() && result.endsWith(QLatin1Char('\n'))) { 0629 result.chop(1); 0630 } 0631 0632 return result; 0633 } 0634 0635 QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString) 0636 { 0637 QString result; 0638 0639 if (wildString.isEmpty()) { 0640 return wildString; 0641 } 0642 0643 int strLength(wildString.length()); 0644 for (int i = 0; i < strLength;) { 0645 QChar ch = wildString[i++]; 0646 if (ch == QLatin1Char('%') && i < strLength) { 0647 ch = wildString[i++]; 0648 switch (ch.toLatin1()) { 0649 case 'f': { // sender's initials 0650 if (fromDisplayString.isEmpty()) { 0651 break; 0652 } 0653 0654 int j = 0; 0655 const int strLengthFromDisplayString(fromDisplayString.length()); 0656 for (; j < strLengthFromDisplayString && fromDisplayString[j] > QLatin1Char(' '); ++j) { } 0657 for (; j < strLengthFromDisplayString && fromDisplayString[j] <= QLatin1Char(' '); ++j) { } 0658 result += fromDisplayString[0]; 0659 if (j < strLengthFromDisplayString && fromDisplayString[j] > QLatin1Char(' ')) { 0660 result += fromDisplayString[j]; 0661 } else if (strLengthFromDisplayString > 1) { 0662 if (fromDisplayString[1] > QLatin1Char(' ')) { 0663 result += fromDisplayString[1]; 0664 } 0665 } 0666 break; 0667 } 0668 case '_': 0669 result += QLatin1Char(' '); 0670 break; 0671 case '%': 0672 result += QLatin1Char('%'); 0673 break; 0674 default: 0675 result += QLatin1Char('%'); 0676 result += ch; 0677 break; 0678 } 0679 } else { 0680 result += ch; 0681 } 0682 } 0683 return result; 0684 } 0685 0686 QString cleanFileName(const QString &name) 0687 { 0688 QString fileName = name.trimmed(); 0689 0690 // We need to replace colons with underscores since those cause problems with 0691 // KFileDialog (bug in KFileDialog though) and also on Windows filesystems. 0692 // We also look at the special case of ": ", since converting that to "_ " 0693 // would look strange, simply "_" looks better. 0694 // https://issues.kolab.org/issue3805 0695 fileName.replace(QLatin1StringView(": "), QStringLiteral("_")); 0696 // replace all ':' with '_' because ':' isn't allowed on FAT volumes 0697 fileName.replace(QLatin1Char(':'), QLatin1Char('_')); 0698 // better not use a dir-delimiter in a filename 0699 fileName.replace(QLatin1Char('/'), QLatin1Char('_')); 0700 fileName.replace(QLatin1Char('\\'), QLatin1Char('_')); 0701 0702 #ifdef KDEPIM_ENTERPRISE_BUILD 0703 // replace all '.' with '_', not just at the start of the filename 0704 // but don't replace the last '.' before the file extension. 0705 int i = fileName.lastIndexOf(QLatin1Char('.')); 0706 if (i != -1) { 0707 i = fileName.lastIndexOf(QLatin1Char('.'), i - 1); 0708 } 0709 0710 while (i != -1) { 0711 fileName.replace(i, 1, QLatin1Char('_')); 0712 i = fileName.lastIndexOf(QLatin1Char('.'), i - 1); 0713 } 0714 #endif 0715 0716 // replace all '~' with '_', not just leading '~' either. 0717 fileName.replace(QLatin1Char('~'), QLatin1Char('_')); 0718 0719 return fileName; 0720 } 0721 0722 QString cleanSubject(KMime::Message *msg) 0723 { 0724 return cleanSubject(msg, 0725 MessageCore::MessageCoreSettings::self()->replyPrefixes() + MessageCore::MessageCoreSettings::self()->forwardPrefixes(), 0726 true, 0727 QString()) 0728 .trimmed(); 0729 } 0730 0731 QString cleanSubject(KMime::Message *msg, const QStringList &prefixRegExps, bool replace, const QString &newPrefix) 0732 { 0733 return replacePrefixes(msg->subject()->asUnicodeString(), prefixRegExps, replace, newPrefix); 0734 } 0735 0736 QString forwardSubject(KMime::Message *msg) 0737 { 0738 return cleanSubject(msg, 0739 MessageCore::MessageCoreSettings::self()->forwardPrefixes(), 0740 MessageCore::MessageCoreSettings::self()->replaceForwardPrefix(), 0741 QStringLiteral("Fwd:")); 0742 } 0743 0744 QString replySubject(KMime::Message *msg) 0745 { 0746 return cleanSubject(msg, 0747 MessageCore::MessageCoreSettings::self()->replyPrefixes(), 0748 MessageCore::MessageCoreSettings::self()->replaceReplyPrefix(), 0749 QStringLiteral("Re:")); 0750 } 0751 0752 QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, bool replace, const QString &newPrefix) 0753 { 0754 bool recognized = false; 0755 // construct a big regexp that 0756 // 1. is anchored to the beginning of str (sans whitespace) 0757 // 2. matches at least one of the part regexps in prefixRegExps 0758 const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); 0759 const QRegularExpression rx(bigRegExp, QRegularExpression::CaseInsensitiveOption); 0760 if (rx.isValid()) { 0761 QString tmp = str; 0762 const QRegularExpressionMatch match = rx.match(tmp); 0763 if (match.hasMatch()) { 0764 recognized = true; 0765 if (replace) { 0766 return tmp.replace(0, match.capturedLength(0), newPrefix + QLatin1Char(' ')); 0767 } 0768 } 0769 } else { 0770 qCWarning(MESSAGECORE_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n" 0771 << "prefix regexp is invalid!"; 0772 // try good ole Re/Fwd: 0773 recognized = str.startsWith(newPrefix); 0774 } 0775 0776 if (!recognized) { 0777 return newPrefix + QLatin1Char(' ') + str; 0778 } else { 0779 return str; 0780 } 0781 } 0782 0783 QString stripOffPrefixes(const QString &subject) 0784 { 0785 const QStringList replyPrefixes = MessageCoreSettings::self()->replyPrefixes(); 0786 0787 const QStringList forwardPrefixes = MessageCoreSettings::self()->forwardPrefixes(); 0788 0789 const QStringList prefixRegExps = replyPrefixes + forwardPrefixes; 0790 0791 // construct a big regexp that 0792 // 1. is anchored to the beginning of str (sans whitespace) 0793 // 2. matches at least one of the part regexps in prefixRegExps 0794 const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); 0795 0796 static QRegularExpression regex; 0797 0798 if (regex.pattern() != bigRegExp) { 0799 // the prefixes have changed, so update the regexp 0800 regex.setPattern(bigRegExp); 0801 regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); 0802 } 0803 0804 if (regex.isValid()) { 0805 QRegularExpressionMatch match = regex.match(subject); 0806 if (match.hasMatch()) { 0807 return subject.mid(match.capturedEnd(0)); 0808 } 0809 } else { 0810 qCWarning(MESSAGECORE_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n" 0811 << "prefix regexp is invalid!"; 0812 } 0813 0814 return subject; 0815 } 0816 0817 void setEncodingFile(QUrl &url, const QString &encoding) 0818 { 0819 QUrlQuery query; 0820 query.addQueryItem(QStringLiteral("charset"), encoding); 0821 url.setQuery(query); 0822 } 0823 } 0824 }