File indexing completed on 2024-04-28 05:18:42

0001 /*
0002   KMime, the KDE Internet mail/usenet news message library.
0003   SPDX-FileCopyrightText: 2001 the KMime authors.
0004   See file AUTHORS for details
0005 
0006   SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include <config-kmime.h>
0010 
0011 #include "kmime_util_p.h"
0012 #include "kmime_debug.h"
0013 
0014 #include <QByteArray>
0015 #include <QChar>
0016 #include <QString>
0017 
0018 #include <cctype>
0019 
0020 using namespace KMime;
0021 
0022 int KMime::findHeaderLineEnd(QByteArrayView src, int &dataBegin, bool *folded)
0023 {
0024     int end = dataBegin;
0025     int len = src.length() - 1;
0026 
0027     if (folded) {
0028         *folded = false;
0029     }
0030 
0031     if (dataBegin < 0) {
0032         // Not found
0033         return -1;
0034     }
0035 
0036     if (dataBegin > len) {
0037         // No data available
0038         return len + 1;
0039     }
0040 
0041     // If the first line contains nothing, but the next line starts with a space
0042     // or a tab, that means a stupid mail client has made the first header field line
0043     // entirely empty, and has folded the rest to the next line(s).
0044     if (src.at(end) == '\n' && end + 1 < len &&
0045             (src[end + 1] == ' ' || src[end + 1] == '\t')) {
0046 
0047         // Skip \n and first whitespace
0048         dataBegin += 2;
0049         end += 2;
0050     }
0051 
0052     if (src.at(end) != '\n') {      // check if the header is not empty
0053         while (true) {
0054             end = src.indexOf('\n', end + 1);
0055             if (end == -1 || end == len) {
0056                 // end of string
0057                 break;
0058             } else if (src[end + 1] == ' ' || src[end + 1] == '\t' ||
0059                        (src[end + 1] == '=' && end + 3 <= len &&
0060                         ((src[end + 2] == '0' && src[end + 3] == '9') ||
0061                          (src[end + 2] == '2' && src[end + 3] == '0')))) {
0062                 // next line is header continuation or starts with =09/=20 (bug #86302)
0063                 if (folded) {
0064                     *folded = true;
0065                 }
0066             } else {
0067                 // end of header (no header continuation)
0068                 break;
0069             }
0070         }
0071     }
0072 
0073     if (end < 0) {
0074         end = len + 1; //take the rest of the string
0075     }
0076     return end;
0077 }
0078 
0079 #if !HAVE_STRCASESTR
0080 #ifdef WIN32
0081 #define strncasecmp _strnicmp
0082 #endif
0083 static const char *strcasestr(const char *haystack, const char *needle)
0084 {
0085     /* Copied from libreplace as part of qtwebengine 5.5.1 */
0086     const char *s;
0087     size_t nlen = strlen(needle);
0088     for (s = haystack; *s; s++) {
0089         if (toupper(*needle) == toupper(*s) && strncasecmp(s, needle, nlen) == 0) {
0090             return (char *)((uintptr_t)s);
0091         }
0092     }
0093     return NULL;
0094 }
0095 #endif
0096 
0097 int KMime::indexOfHeader(const QByteArray &src, const QByteArray &name, int &end, int &dataBegin, bool *folded)
0098 {
0099     QByteArray n = name;
0100     n.append(':');
0101     int begin = -1;
0102 
0103     if (qstrnicmp(n.constData(), src.constData(), n.length()) == 0) {
0104         begin = 0;
0105     } else {
0106         n.prepend('\n');
0107         const char *p = strcasestr(src.constData(), n.constData());
0108         if (!p) {
0109             begin = -1;
0110         } else {
0111             begin = p - src.constData();
0112             ++begin;
0113         }
0114     }
0115 
0116     if (begin > -1) {       //there is a header with the given name
0117         dataBegin = begin + name.length() + 1; //skip the name
0118         // skip the usual space after the colon
0119         if (dataBegin < src.length() && src.at(dataBegin) == ' ') {
0120             ++dataBegin;
0121         }
0122         end = findHeaderLineEnd(src, dataBegin, folded);
0123         return begin;
0124 
0125     } else {
0126         end = -1;
0127         dataBegin = -1;
0128         return -1; //header not found
0129     }
0130 }
0131 
0132 QByteArray KMime::extractHeader(const QByteArray &src, const QByteArray &name)
0133 {
0134     int begin;
0135     int end;
0136     bool folded;
0137     QByteArray result;
0138 
0139     if (src.isEmpty() || indexOfHeader(src, name, end, begin, &folded) < 0) {
0140         return result;
0141     }
0142 
0143     if (begin >= 0) {
0144         if (!folded) {
0145             result = src.mid(begin, end - begin);
0146         } else {
0147             if (end > begin) {
0148                 result = unfoldHeader(src.constData() + begin, end - begin);
0149             }
0150         }
0151     }
0152     return result;
0153 }
0154 
0155 QByteArray KMime::unfoldHeader(const char *header, size_t headerSize)
0156 {
0157     QByteArray result;
0158     if (headerSize == 0) {
0159         return result;
0160     }
0161 
0162     // unfolding skips characters so result will be at worst headerSize long
0163     result.reserve(headerSize);
0164 
0165     const char *end = header + headerSize;
0166     const char *pos = header;
0167     const char *foldBegin = nullptr;
0168     const char *foldMid = nullptr;
0169     const char *foldEnd = nullptr;
0170     while ((foldMid = strchr(pos, '\n')) && foldMid < end) {
0171         foldBegin = foldEnd = foldMid;
0172         // find the first space before the line-break
0173         while (foldBegin > header) {
0174             if (!QChar::isSpace(*(foldBegin - 1))) {
0175                 break;
0176             }
0177             --foldBegin;
0178         }
0179         // find the first non-space after the line-break
0180         while (foldEnd <= end - 1) {
0181             if (QChar::isSpace(*foldEnd)) {
0182                 ++foldEnd;
0183             } else if (foldEnd && *(foldEnd - 1) == '\n' &&
0184                        *foldEnd == '=' && foldEnd + 2 < (header + headerSize - 1) &&
0185                        ((*(foldEnd + 1) == '0' &&
0186                          *(foldEnd + 2) == '9') ||
0187                         (*(foldEnd + 1) == '2' &&
0188                          *(foldEnd + 2) == '0'))) {
0189                 // bug #86302: malformed header continuation starting with =09/=20
0190                 foldEnd += 3;
0191             } else {
0192                 break;
0193             }
0194         }
0195 
0196         result.append(pos, foldBegin - pos);
0197         if (foldBegin != pos && foldEnd < end - 1) {
0198             result += ' ';
0199         }
0200         pos = foldEnd;
0201     }
0202     if (end > pos) {
0203         result.append(pos, end - pos);
0204     }
0205     return result;
0206 }
0207 
0208 QByteArray KMime::unfoldHeader(const QByteArray &header)
0209 {
0210     return unfoldHeader(header.constData(), header.size());
0211 }
0212 
0213 namespace {
0214 // state machine used by foldHeader()
0215 struct HeaderContext {
0216     unsigned int isEscapePair : 1;
0217     unsigned int isQuotedStr : 1;
0218 
0219     HeaderContext() {
0220         isEscapePair = isQuotedStr = 0;
0221     }
0222 
0223     void push(char c) {
0224         if (c == '\"' && !isEscapePair) {
0225             ++isQuotedStr;
0226         } else if (c == '\\' || isEscapePair) {
0227             ++isEscapePair;
0228         }
0229     }
0230 };
0231 }
0232 
0233 QByteArray KMime::foldHeader(const QByteArray &header)
0234 {
0235     // RFC 5322 section 2.1.1. "Line Length Limits" says:
0236     //
0237     // "Each line of characters MUST be no more than 998 characters, and
0238     //  SHOULD be no more than 78 characters, excluding the CRLF."
0239     const int maxLen = 78;
0240 
0241     if (header.length() <= maxLen) {
0242         return header;
0243     }
0244 
0245     // fast forward to header body
0246     int pos = header.indexOf(':') + 1;
0247     if (pos <= 0 || pos >= header.length()) {
0248         return header;
0249     }
0250 
0251     // prepare for mutating header
0252     QByteArray hdr = header;
0253 
0254     // There are positions that are eligible for inserting FWS but discouraged
0255     // (e.g. existing white space within a quoted string), and there are
0256     // positions which are recommended for inserting FWS (e.g. after comma
0257     // separator of an address list).
0258     int eligible = pos;
0259     int recommended = pos;
0260 
0261     // reflects start position of "current line" in byte array
0262     int start = 0;
0263 
0264     HeaderContext ctx;
0265 
0266     for (; true; ++pos) {
0267         if (pos - start > maxLen && eligible) {
0268             // Fold line preferably at recommended position, at eligible position
0269             // otherwise.
0270             const int fws = recommended ? recommended : eligible;
0271             hdr.insert(fws, '\n');
0272             // We started a new line, so reset.
0273             if (eligible <= fws) {
0274                 eligible = 0;
0275             } else {
0276                 ++eligible; // LF
0277             }
0278             recommended = 0;
0279             start = fws + 1/* LF */;
0280             continue;
0281         }
0282 
0283         if (pos >= hdr.length()) {
0284             break;
0285         }
0286 
0287         // account for already inserted FWS
0288         // (NOTE: we are not caring about broken ones here)
0289         if (hdr[pos] == '\n') {
0290             recommended = eligible = 0;
0291             start = pos + 1/* LF */;
0292         }
0293 
0294         // Any white space character position is eligible for folding, except of
0295         // escape pair (i.e. BSP WSP must not be folded).
0296         if (hdr[pos] == ' ' && !ctx.isEscapePair && hdr[pos - 1] != '\n') {
0297             eligible = pos;
0298             if ((hdr[pos - 1] == ',' || hdr[pos - 1] == ';') && !ctx.isQuotedStr) {
0299                 recommended = pos;
0300             }
0301         }
0302 
0303         ctx.push(hdr[pos]);
0304     }
0305 
0306     return hdr;
0307 }
0308 
0309 namespace
0310 {
0311 template < typename StringType, typename CharType > void removeQuotesGeneric(StringType &str)
0312 {
0313     bool inQuote = false;
0314     for (int i = 0; i < str.length(); ++i) {
0315         if (str[i] == CharType('"')) {
0316             str.remove(i, 1);
0317             i--;
0318             inQuote = !inQuote;
0319         } else {
0320             if (inQuote && (str[i] == CharType('\\'))) {
0321                 str.remove(i, 1);
0322             }
0323         }
0324     }
0325 }
0326 }
0327 
0328 void KMime::removeQuotes(QByteArray &str)
0329 {
0330     removeQuotesGeneric<QByteArray, char>(str);
0331 }
0332 
0333 void KMime::removeQuotes(QString &str)
0334 {
0335     removeQuotesGeneric<QString, QLatin1Char>(str);
0336 }
0337 
0338 namespace {
0339 template<class StringType, class CharConverterType>
0340 void addQuotes_impl(StringType &str, bool forceQuotes)
0341 {
0342     constexpr const char reservedCharacters[] = R"(""(),.:;<=>@[\])"; // sorted!
0343 
0344     bool needsQuotes = false;
0345     for (qsizetype i = 0; i < str.length(); i++) {
0346         const auto cur = str.at(i);
0347         const auto it = std::lower_bound(std::begin(reservedCharacters), std::end(reservedCharacters), cur, [](char lhs, auto rhs) {
0348             return CharConverterType(lhs) < rhs;
0349         });
0350         if (it != std::end(reservedCharacters) && CharConverterType(*it) == cur) {
0351             needsQuotes = true;
0352         }
0353         if (cur == CharConverterType('\\') || cur == CharConverterType('\"')) {
0354             str.insert(i, CharConverterType('\\'));
0355             i++;
0356         }
0357     }
0358 
0359     if (needsQuotes || forceQuotes) {
0360         str.insert(0, CharConverterType('\"'));
0361         str.append(CharConverterType('\"'));
0362     }
0363 }
0364 }
0365 
0366 void KMime::addQuotes(QByteArray &str, bool forceQuotes)
0367 {
0368     addQuotes_impl<QByteArray, char>(str, forceQuotes);
0369 }
0370 
0371 void KMime::addQuotes(QString &str, bool forceQuotes)
0372 {
0373     addQuotes_impl<QString, QLatin1Char>(str, forceQuotes);
0374 }
0375 
0376 QString KMime::balanceBidiState(const QString &input)
0377 {
0378     const int LRO = 0x202D;
0379     const int RLO = 0x202E;
0380     const int LRE = 0x202A;
0381     const int RLE = 0x202B;
0382     const int PDF = 0x202C;
0383 
0384     QString result = input;
0385 
0386     int openDirChangers = 0;
0387     int numPDFsRemoved = 0;
0388     for (int i = 0; i < input.length(); i++) {
0389         const ushort &code = input.at(i).unicode();
0390         if (code == LRO || code == RLO || code == LRE || code == RLE) {
0391             openDirChangers++;
0392         } else if (code == PDF) {
0393             if (openDirChangers > 0) {
0394                 openDirChangers--;
0395             } else {
0396                 // One PDF too much, remove it
0397                 qCWarning(KMIME_LOG) << "Possible Unicode spoofing (unexpected PDF) detected in" << input;
0398                 result.remove(i - numPDFsRemoved, 1);
0399                 numPDFsRemoved++;
0400             }
0401         }
0402     }
0403 
0404     if (openDirChangers > 0) {
0405         qCWarning(KMIME_LOG) << "Possible Unicode spoofing detected in" << input;
0406 
0407         // At PDF chars to the end until the correct state is restored.
0408         // As a special exception, when encountering quoted strings, place the PDF before
0409         // the last quote.
0410         for (int i = openDirChangers; i > 0; i--) {
0411             if (result.endsWith(QLatin1Char('"'))) {
0412                 result.insert(result.length() - 1, QChar(PDF));
0413             } else {
0414                 result += QChar(PDF);
0415             }
0416         }
0417     }
0418 
0419     return result;
0420 }
0421 
0422 QString KMime::removeBidiControlChars(const QString &input)
0423 {
0424     const int LRO = 0x202D;
0425     const int RLO = 0x202E;
0426     const int LRE = 0x202A;
0427     const int RLE = 0x202B;
0428     QString result = input;
0429     result.remove(QChar(LRO));
0430     result.remove(QChar(RLO));
0431     result.remove(QChar(LRE));
0432     result.remove(QChar(RLE));
0433     return result;
0434 }