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("&#38;"), 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("&lt;");
0327             break;
0328         case '>':
0329             result += QLatin1StringView("&gt;");
0330             break;
0331         case '&':
0332             result += QLatin1StringView("&amp;");
0333             break;
0334         case '"':
0335             result += QLatin1StringView("&quot;");
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 }