stringutil.cpp 28 KB
Newer Older
Laurent Montel's avatar
Laurent Montel committed
1
/*
2
3
   SPDX-FileCopyrightText: 2016-2020 Laurent Montel <montel@kde.org>
   SPDX-FileCopyrightText: 2009 Thomas McGuire <mcguire@kde.org>
4

5
   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
6
7
8
9
10
11
12
13
14
15
*/
#include "stringutil.h"

#include "config-enterprise.h"
#include "MessageCore/MessageCoreSettings"

#include <kmime/kmime_header_parsing.h>
#include <kmime/kmime_headers.h>
#include <kmime/kmime_message.h>
#include <KEmailAddress>
16
#include <KLocalizedString>
17
18
19
20

#include "messagecore_debug.h"
#include <KUser>

21
22
#include <KIdentityManagement/IdentityManager>
#include <KIdentityManagement/Identity>
23
#include <QHostInfo>
Laurent Montel's avatar
Laurent Montel committed
24
#include <QRegularExpression>
25
26
#include <QStringList>
#include <QUrlQuery>
27
#include <KPIMTextEdit/TextUtils>
Laurent Montel's avatar
Laurent Montel committed
28
#include <KCodecs>
29
30
31
32
using namespace KMime;
using namespace KMime::Types;
using namespace KMime::HeaderParsing;

Laurent Montel's avatar
Laurent Montel committed
33
34
namespace MessageCore {
namespace StringUtil {
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Removes trailing spaces and tabs at the end of the line
static void removeTrailingSpace(QString &line)
{
    int i = line.length() - 1;
    while ((i >= 0) && ((line[i] == QLatin1Char(' ')) || (line[i] == QLatin1Char('\t')))) {
        i--;
    }
    line.truncate(i + 1);
}

// Splits the line off in two parts: The quote prefixes and the actual text of the line.
// For example, for the string "> > > Hello", it would be split up in "> > > " as the quote
// prefix, and "Hello" as the actual text.
// The actual text is written back to the "line" parameter, and the quote prefix is returned.
static QString splitLine(QString &line)
{
    removeTrailingSpace(line);
    int i = 0;
    int startOfActualText = -1;

    // TODO: Replace tabs with spaces first.

    // Loop through the chars in the line to find the place where the quote prefix stops
    const int lineLength(line.length());
    while (i < lineLength) {
        const QChar c = line[i];
Laurent Montel's avatar
Laurent Montel committed
61
62
        const bool isAllowedQuoteChar = (c == QLatin1Char('>')) || (c == QLatin1Char(':')) || (c == QLatin1Char('|'))
                                        || (c == QLatin1Char(' ')) || (c == QLatin1Char('\t'));
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
        if (isAllowedQuoteChar) {
            startOfActualText = i + 1;
        } else {
            break;
        }
        ++i;
    }

    // If the quote prefix only consists of whitespace, don't consider it as a quote prefix at all
    if (line.left(startOfActualText).trimmed().isEmpty()) {
        startOfActualText = 0;
    }

    // No quote prefix there -> nothing to do
    if (startOfActualText <= 0) {
        return QString();
    }

    // Entire line consists of only the quote prefix
    if (i == line.length()) {
        const QString quotePrefix = line.left(startOfActualText);
        line.clear();
        return quotePrefix;
    }

    // Line contains both the quote prefix and the actual text, really split it up now
    const QString quotePrefix = line.left(startOfActualText);
    line = line.mid(startOfActualText);

    return quotePrefix;
}

// Writes all lines/text parts contained in the "textParts" list to the output text, "msg".
// Quote characters are added in front of each line, and no line is longer than
// maxLength.
//
// Although the lines in textParts are considered separate lines, they can actually be run
// together into a single line in some cases. This is basically the main difference to flowText().
//
// Example:
//   textParts = "Hello World, this is a test.", "Really"
//   indent = ">"
//   maxLength = 20
//   Result: "> Hello World, this\n
//            > is a test. Really"
// Notice how in this example, the text line "Really" is no longer a separate line, it was run
// together with a previously broken line.
//
// "textParts" is cleared upon return.
Laurent Montel's avatar
Laurent Montel committed
112
static bool flushPart(QString &msg, QStringList &textParts, const QString &indent, int maxLength)
113
114
115
116
117
118
119
120
121
122
123
{
    if (maxLength < 20) {
        maxLength = 20;
    }

    // Remove empty lines at end of quote
    while (!textParts.isEmpty() && textParts.last().isEmpty()) {
        textParts.removeLast();
    }

    QString text;
Laurent Montel's avatar
Laurent Montel committed
124
125

    for (const QString &line : textParts) {
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
        // An empty line in the input means that an empty line should be in the output as well.
        // Therefore, we write all of our text so far to the msg.
        if (line.isEmpty()) {
            if (!text.isEmpty()) {
                msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength) + QLatin1Char('\n');
            }
            msg += indent + QLatin1Char('\n');
        } else {
            if (text.isEmpty()) {
                text = line;
            } else {
                text += QLatin1Char(' ') + line.trimmed();
            }
            // If the line doesn't need to be wrapped at all, just write it out as-is.
            // When a line exceeds the maximum length and therefore needs to be broken, this statement
            // if false, and therefore we keep adding lines to our text, so they get ran together in the
            // next flowText call, as "text" contains several text parts/lines then.
            if ((text.length() < maxLength) || (line.length() < (maxLength - 10))) {
                msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength) + QLatin1Char('\n');
            }
        }
    }

    // Write out pending text to the msg
    if (!text.isEmpty()) {
        msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength);
    }

    const bool appendEmptyLine = !textParts.isEmpty();
    textParts.clear();

    return appendEmptyLine;
}

Laurent Montel's avatar
Laurent Montel committed
160
QVector<QPair<QString, QString> > parseMailtoUrl(const QUrl &url)
161
{
Laurent Montel's avatar
Laurent Montel committed
162
    QVector<QPair<QString, QString> > values;
163
164
165
    if (url.scheme() != QLatin1String("mailto")) {
        return values;
    }
166
    QString str = url.toString();
167
    QStringList toStr;
168
    int i = 0;
169

Laurent Montel's avatar
Laurent Montel committed
170
171
172
173
    //String can be encoded.
    str = KCodecs::decodeRFC2047String(str);
    const QUrl newUrl = QUrl::fromUserInput(str);

174
    int indexTo = -1;
175
176
177
    //Workaround line with # see bug 406208
    const int indexOf = str.indexOf(QLatin1Char('?'));
    if (indexOf != -1) {
178
        str.remove(0, indexOf + 1);
179
180
181
182
        QUrlQuery query(str);
        const auto listQuery = query.queryItems(QUrl::FullyDecoded);
        for (const auto &queryItem : listQuery) {
            if (queryItem.first == QLatin1String("to")) {
183
                toStr << queryItem.second;
184
185
186
187
188
189
                indexTo = i;
            } else {
                QPair<QString, QString> pairElement;
                pairElement.first = queryItem.first;
                pairElement.second = queryItem.second;
                values.append(pairElement);
190
                i++;
191
            }
192
        }
193
    }
194
    QStringList to = {KEmailAddress::decodeMailtoUrl(newUrl)};
195
196
197
    if (!toStr.isEmpty()) {
        to << toStr;
    }
198
    const QString fullTo = to.join(QLatin1String(", "));
199
200
201
202
203
204
205
206
207
208
    if (!fullTo.isEmpty()) {
        QPair<QString, QString> pairElement;
        pairElement.first = QStringLiteral("to");
        pairElement.second = fullTo;
        if (indexTo != -1) {
            values.insert(indexTo, pairElement);
        } else {
            values.prepend(pairElement);
        }
    }
209
210
211
212
213
214
215
    return values;
}

QString stripSignature(const QString &msg)
{
    // Following RFC 3676, only > before --
    // I prefer to not delete a SB instead of delete good mail content.
Laurent Montel's avatar
Laurent Montel committed
216
    static const QRegularExpression sbDelimiterSearch(QStringLiteral("(^|\n)[> ]*-- \n"));
217
    // The regular expression to look for prefix change
Laurent Montel's avatar
Laurent Montel committed
218
    static const QRegularExpression commonReplySearch(QStringLiteral("^[ ]*>"));
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252

    QString res = msg;
    int posDeletingStart = 1; // to start looking at 0

    // While there are SB delimiters (start looking just before the deleted SB)
    while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) {
        QString prefix; // the current prefix
        QString line; // the line to check if is part of the SB
        int posNewLine = -1;

        // Look for the SB beginning
        int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart);
        // The prefix before "-- "$
        if (res.at(posDeletingStart) == QLatin1Char('\n')) {
            ++posDeletingStart;
        }

        prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart);
        posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1;

        // now go to the end of the SB
        while (posNewLine < res.size() && posNewLine > 0) {
            // handle the undefined case for mid ( x , -n ) where n>1
            int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine);

            if (nextPosNewLine < 0) {
                nextPosNewLine = posNewLine - 1;
            }

            line = res.mid(posNewLine, nextPosNewLine - posNewLine);

            // check when the SB ends:
            // * does not starts with prefix or
            // * starts with prefix+(any substring of prefix)
Laurent Montel's avatar
Laurent Montel committed
253
254
255
            if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0)
                || (!prefix.isEmpty() && line.startsWith(prefix)
                    && line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) {
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
                posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1;
            } else {
                break;    // end of the SB
            }
        }

        // remove the SB or truncate when is the last SB
        if (posNewLine > 0) {
            res.remove(posDeletingStart, posNewLine - posDeletingStart);
        } else {
            res.truncate(posDeletingStart);
        }
    }

    return res;
}

AddressList splitAddressField(const QByteArray &text)
{
    AddressList result;
    const char *begin = text.begin();
    if (!begin) {
        return result;
    }

    const char *const end = text.begin() + text.length();

    if (!parseAddressList(begin, end, result)) {
        qCDebug(MESSAGECORE_LOG) << "Error in address splitting: parseAddressList returned false!";
    }

    return result;
}

QString generateMessageId(const QString &address, const QString &suffix)
{
    const QDateTime dateTime = QDateTime::currentDateTime();

    QString msgIdStr = QLatin1Char('<') + dateTime.toString(QStringLiteral("yyyyMMddhhmm.sszzz"));

    if (!suffix.isEmpty()) {
        msgIdStr += QLatin1Char('@') + suffix;
    } else {
        msgIdStr += QLatin1Char('.') + KEmailAddress::toIdn(address);
    }

    msgIdStr += QLatin1Char('>');

    return msgIdStr;
}

QString quoteHtmlChars(const QString &str, bool removeLineBreaks)
{
    QString result;

Laurent Montel's avatar
Laurent Montel committed
311
    int strLength(str.length());
312
    result.reserve(6 * strLength); // maximal possible length
Laurent Montel's avatar
Laurent Montel committed
313
    for (int i = 0; i < strLength; ++i) {
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
        switch (str[i].toLatin1()) {
        case '<':
            result += QLatin1String("&lt;");
            break;
        case '>':
            result += QLatin1String("&gt;");
            break;
        case '&':
            result += QLatin1String("&amp;");
            break;
        case '"':
            result += QLatin1String("&quot;");
            break;
        case '\n':
            if (!removeLineBreaks) {
                result += QLatin1String("<br>");
            }
            break;
        case '\r':
            // ignore CR
            break;
        default:
            result += str[i];
        }
    }

    result.squeeze();
    return result;
}

void removePrivateHeaderFields(const KMime::Message::Ptr &message, bool cleanUpHeader)
{
    message->removeHeader("Status");
    message->removeHeader("X-Status");
    message->removeHeader("X-KMail-EncryptionState");
    message->removeHeader("X-KMail-SignatureState");
    message->removeHeader("X-KMail-Redirect-From");
    message->removeHeader("X-KMail-Link-Message");
    message->removeHeader("X-KMail-Link-Type");
    message->removeHeader("X-KMail-QuotePrefix");
    message->removeHeader("X-KMail-CursorPos");
    message->removeHeader("X-KMail-Templates");
    message->removeHeader("X-KMail-Drafts");
    message->removeHeader("X-KMail-UnExpanded-To");
    message->removeHeader("X-KMail-UnExpanded-CC");
    message->removeHeader("X-KMail-UnExpanded-BCC");
360
    message->removeHeader("X-KMail-UnExpanded-Reply-To");
361
362
363
    message->removeHeader("X-KMail-FccDisabled");

    if (cleanUpHeader) {
364
365
        message->removeHeader("X-KMail-Fcc");
        message->removeHeader("X-KMail-Transport");
366
        message->removeHeader("X-KMail-Identity");
367
368
        message->removeHeader("X-KMail-Transport-Name");
        message->removeHeader("X-KMail-Identity-Name");
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
        message->removeHeader("X-KMail-Dictionary");
    }
}

QByteArray asSendableString(const KMime::Message::Ptr &originalMessage)
{
    KMime::Message::Ptr message(new KMime::Message);
    message->setContent(originalMessage->encodedContent());

    removePrivateHeaderFields(message);
    message->removeHeader<KMime::Headers::Bcc>();

    return message->encodedContent();
}

QByteArray headerAsSendableString(const KMime::Message::Ptr &originalMessage)
{
    KMime::Message::Ptr message(new KMime::Message);
    message->setContent(originalMessage->encodedContent());

    removePrivateHeaderFields(message);
    message->removeHeader<KMime::Headers::Bcc>();

    return message->head();
}

Laurent Montel's avatar
Laurent Montel committed
395
QString emailAddrAsAnchor(const KMime::Types::Mailbox::List &mailboxList, Display display, const QString &cssStyle, Link link, AddressMode expandable, const QString &fieldName, int collapseNumber)
396
397
398
399
{
    QString result;
    int numberAddresses = 0;
    bool expandableInserted = false;
400
    KIdentityManagement::IdentityManager *im = KIdentityManagement::IdentityManager::self();
401

Laurent Montel's avatar
Laurent Montel committed
402
    const QString i18nMe = i18nc("signal that this email is defined in my identity", "Me");
Laurent Montel's avatar
Laurent Montel committed
403
    const bool onlyOneIdentity = (im->identities().count() == 1);
Laurent Montel's avatar
Laurent Montel committed
404
    for (const KMime::Types::Mailbox &mailbox : mailboxList) {
405
        const QString prettyAddressStr = mailbox.prettyAddress();
Laurent Montel's avatar
Laurent Montel committed
406
        if (!prettyAddressStr.isEmpty()) {
407
408
            numberAddresses++;
            if (expandable == ExpandableAddresses && !expandableInserted && numberAddresses > collapseNumber) {
409
410
411
412
413
414
                const QString actualListAddress = result;
                QString shortListAddress = actualListAddress;
                if (link == ShowLink) {
                    shortListAddress.truncate(result.length() - 2);
                }
                result = QStringLiteral("<span><input type=\"checkbox\" class=\"addresslist_checkbox\" id=\"%1\" checked=\"checked\"/><span class=\"short%1\">").arg(fieldName) + shortListAddress;
415
                result += QStringLiteral("<label class=\"addresslist_label_short\" for=\"%1\"></label></span>").arg(fieldName);
416
                expandableInserted = true;
417
                result += QStringLiteral("<span class=\"full%1\">").arg(fieldName) + actualListAddress;
418
419
420
421
422
423
424
            }

            if (link == ShowLink) {
                result += QLatin1String("<a href=\"mailto:")
                          + QString::fromLatin1(QUrl::toPercentEncoding(KEmailAddress::encodeMailtoUrl(mailbox.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary)).path()))
                          + QLatin1String("\" ") + cssStyle + QLatin1Char('>');
            }
Laurent Montel's avatar
Laurent Montel committed
425
            const bool foundMe = onlyOneIdentity && (im->identityForAddress(prettyAddressStr) != KIdentityManagement::Identity::null());
Laurent Montel's avatar
Laurent Montel committed
426

427
428
            if (display == DisplayNameOnly) {
                if (!mailbox.name().isEmpty()) { // Fallback to the email address when the name is not set.
Laurent Montel's avatar
Laurent Montel committed
429
                    result += foundMe ? i18nMe : quoteHtmlChars(mailbox.name(), true);
430
                } else {
Laurent Montel's avatar
Laurent Montel committed
431
                    result += foundMe ? i18nMe : quoteHtmlChars(prettyAddressStr, true);
432
433
                }
            } else {
434
                result += foundMe ? i18nMe : quoteHtmlChars(mailbox.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary), true);
435
436
437
438
439
440
441
442
            }
            if (link == ShowLink) {
                result += QLatin1String("</a>, ");
            }
        }
    }

    if (link == ShowLink) {
Laurent Montel's avatar
Laurent Montel committed
443
        result.chop(2);
444
445
446
    }

    if (expandableInserted) {
447
        result += QStringLiteral("<label class=\"addresslist_label_full\" for=\"%1\"></label></span></span>").arg(fieldName);
448
449
450
451
    }
    return result;
}

Laurent Montel's avatar
Laurent Montel committed
452
QString emailAddrAsAnchor(const KMime::Headers::Generics::MailboxList *mailboxList, Display display, const QString &cssStyle, Link link, AddressMode expandable, const QString &fieldName, int collapseNumber)
453
454
455
456
457
{
    Q_ASSERT(mailboxList);
    return emailAddrAsAnchor(mailboxList->mailboxes(), display, cssStyle, link, expandable, fieldName, collapseNumber);
}

Laurent Montel's avatar
Laurent Montel committed
458
QString emailAddrAsAnchor(const KMime::Headers::Generics::AddressList *addressList, Display display, const QString &cssStyle, Link link, AddressMode expandable, const QString &fieldName, int collapseNumber)
459
460
461
462
463
{
    Q_ASSERT(addressList);
    return emailAddrAsAnchor(addressList->mailboxes(), display, cssStyle, link, expandable, fieldName, collapseNumber);
}

Laurent Montel's avatar
Laurent Montel committed
464
bool addressIsInAddressList(const QString &address, const QStringList &addresses)
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
{
    const QString addrSpec = KEmailAddress::extractEmailAddress(address);

    QStringList::ConstIterator end(addresses.constEnd());
    for (QStringList::ConstIterator it = addresses.constBegin(); it != end; ++it) {
        if (qstricmp(addrSpec.toUtf8().data(),
                     KEmailAddress::extractEmailAddress(*it).toUtf8().data()) == 0) {
            return true;
        }
    }

    return false;
}

QString guessEmailAddressFromLoginName(const QString &loginName)
{
    if (loginName.isEmpty()) {
        return QString();
    }

    QString address = loginName;
    address += QLatin1Char('@');
    address += QHostInfo::localHostName();

    // try to determine the real name
    const KUser user(loginName);
    if (user.isValid()) {
        const QString fullName = user.property(KUser::FullName).toString();
        address = KEmailAddress::quoteNameIfNecessary(fullName) + QLatin1String(" <") + address + QLatin1Char('>');
    }

    return address;
}

QString smartQuote(const QString &msg, int maxLineLength)
{
    // The algorithm here is as follows:
    // We split up the incoming msg into lines, and then iterate over each line.
    // We keep adding lines with the same indent ( = quote prefix, e.g. "> " ) to a
    // "textParts" list. So the textParts list contains only lines with the same quote
    // prefix.
    //
    // When all lines with the same indent are collected in "textParts", we write those out
    // to the result by calling flushPart(), which does all the nice formatting for us.

    QStringList textParts;
    QString oldIndent;
    bool firstPart = true;
    QString result;

515
516
517
    int lineStart = 0;
    int lineEnd = msg.indexOf(QLatin1Char('\n'));
    bool needToContinue = true;
Laurent Montel's avatar
Laurent Montel committed
518
    for (; needToContinue; lineStart = lineEnd + 1, lineEnd = msg.indexOf(QLatin1Char('\n'), lineStart)) {
519
520
521
522
523
524
525
526
527
528
529
530
531
532
        QString line;
        if (lineEnd == -1) {
            if (lineStart == 0) {
                line = msg;
                needToContinue = false;
            } else if (lineStart != 0 && lineStart != msg.length()) {
                line = msg.mid(lineStart, msg.length() - lineStart);
                needToContinue = false;
            } else {
                needToContinue = false;
            }
        } else {
            line = msg.mid(lineStart, lineEnd - lineStart);
        }
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
        // Split off the indent from the line
        const QString indent = splitLine(line);

        if (line.isEmpty()) {
            if (!firstPart) {
                textParts.append(QString());
            }
            continue;
        }

        if (firstPart) {
            oldIndent = indent;
            firstPart = false;
        }

        // The indent changed, that means we have to write everything contained in textParts to the
        // result, which we do by calling flushPart().
        if (oldIndent != indent) {
            // Check if the last non-blank line is a "From" line. A from line is the line containing the
            // attribution to a quote, e.g. "Yesterday, you wrote:". We'll just check for the last colon
            // here, to simply things.
            // If there is a From line, remove it from the textParts to that flushPart won't break it.
            // We'll manually add it to the result afterwards.
            QString fromLine;
            if (!textParts.isEmpty()) {
                for (int i = textParts.count() - 1; i >= 0; i--) {
                    // Check if we have found the From line
Laurent Montel's avatar
Laurent Montel committed
560
561
562
                    const QString textPartElement(textParts[i]);
                    if (textPartElement.endsWith(QLatin1Char(':'))) {
                        fromLine = oldIndent + textPartElement + QLatin1Char('\n');
563
564
565
566
567
                        textParts.removeAt(i);
                        break;
                    }

                    // Abort on first non-empty line
Laurent Montel's avatar
Laurent Montel committed
568
                    if (!textPartElement.trimmed().isEmpty()) {
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
                        break;
                    }
                }
            }

            // Write out all lines with the same indent using flushPart(). The textParts list
            // is cleared for us.
            if (flushPart(result, textParts, oldIndent, maxLineLength)) {
                if (oldIndent.length() > indent.length()) {
                    result += indent + QLatin1Char('\n');
                } else {
                    result += oldIndent + QLatin1Char('\n');
                }
            }

            if (!fromLine.isEmpty()) {
                result += fromLine;
            }

            oldIndent = indent;
        }

        textParts.append(line);
    }

    // Write out anything still pending
    flushPart(result, textParts, oldIndent, maxLineLength);

    // Remove superfluous newline which was appended in flowText
    if (!result.isEmpty() && result.endsWith(QLatin1Char('\n'))) {
        result.chop(1);
    }

    return result;
}

QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
{
    QString result;

    if (wildString.isEmpty()) {
        return wildString;
    }

Laurent Montel's avatar
Laurent Montel committed
613
614
    int strLength(wildString.length());
    for (int i = 0; i < strLength;) {
615
616
617
618
        QChar ch = wildString[i++];
        if (ch == QLatin1Char('%') && i < strLength) {
            ch = wildString[i++];
            switch (ch.toLatin1()) {
Laurent Montel's avatar
Laurent Montel committed
619
            case 'f':
620
            {           // sender's initials
621
622
623
624
                if (fromDisplayString.isEmpty()) {
                    break;
                }

Laurent Montel's avatar
Laurent Montel committed
625
626
                int j = 0;
                const int strLength(fromDisplayString.length());
Laurent Montel's avatar
Laurent Montel committed
627
628
629
630
                for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j) {
                }
                for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j) {
                }
631
632
633
634
635
636
637
638
                result += fromDisplayString[0];
                if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) {
                    result += fromDisplayString[j];
                } else if (strLength > 1) {
                    if (fromDisplayString[1] > QLatin1Char(' ')) {
                        result += fromDisplayString[1];
                    }
                }
Laurent Montel's avatar
Laurent Montel committed
639
                break;
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
            }
            case '_':
                result += QLatin1Char(' ');
                break;
            case '%':
                result += QLatin1Char('%');
                break;
            default:
                result += QLatin1Char('%');
                result += ch;
                break;
            }
        } else {
            result += ch;
        }
    }
    return result;
}

QString cleanFileName(const QString &name)
{
    QString fileName = name.trimmed();

    // We need to replace colons with underscores since those cause problems with
    // KFileDialog (bug in KFileDialog though) and also on Windows filesystems.
    // We also look at the special case of ": ", since converting that to "_ "
    // would look strange, simply "_" looks better.
    // https://issues.kolab.org/issue3805
    fileName.replace(QLatin1String(": "), QStringLiteral("_"));
    // replace all ':' with '_' because ':' isn't allowed on FAT volumes
    fileName.replace(QLatin1Char(':'), QLatin1Char('_'));
    // better not use a dir-delimiter in a filename
    fileName.replace(QLatin1Char('/'), QLatin1Char('_'));
    fileName.replace(QLatin1Char('\\'), QLatin1Char('_'));

#ifdef KDEPIM_ENTERPRISE_BUILD
    // replace all '.' with '_', not just at the start of the filename
    // but don't replace the last '.' before the file extension.
    int i = fileName.lastIndexOf(QLatin1Char('.'));
    if (i != -1) {
        i = fileName.lastIndexOf(QLatin1Char('.'), i - 1);
    }

    while (i != -1) {
        fileName.replace(i, 1, QLatin1Char('_'));
        i = fileName.lastIndexOf(QLatin1Char('.'), i - 1);
    }
#endif

    // replace all '~' with '_', not just leading '~' either.
    fileName.replace(QLatin1Char('~'), QLatin1Char('_'));

    return fileName;
}

Laurent Montel's avatar
Laurent Montel committed
695
QString cleanSubject(KMime::Message *msg)
696
697
698
699
700
{
    return cleanSubject(msg, MessageCore::MessageCoreSettings::self()->replyPrefixes() + MessageCore::MessageCoreSettings::self()->forwardPrefixes(),
                        true, QString()).trimmed();
}

Laurent Montel's avatar
Laurent Montel committed
701
QString cleanSubject(KMime::Message *msg, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
702
703
704
705
706
{
    return replacePrefixes(msg->subject()->asUnicodeString(), prefixRegExps, replace,
                           newPrefix);
}

Laurent Montel's avatar
Laurent Montel committed
707
QString forwardSubject(KMime::Message *msg)
708
{
709
710
711
    return cleanSubject(msg, MessageCore::MessageCoreSettings::self()->forwardPrefixes(),
                        MessageCore::MessageCoreSettings::self()->replaceForwardPrefix(), QStringLiteral("Fwd:"));
}
712

Laurent Montel's avatar
Laurent Montel committed
713
QString replySubject(KMime::Message *msg)
714
715
716
717
{
    return cleanSubject(msg, MessageCore::MessageCoreSettings::self()->replyPrefixes(),
                        MessageCore::MessageCoreSettings::self()->replaceReplyPrefix(), QStringLiteral("Re:"));
}
718

719
720
721
722
723
724
QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
{
    bool recognized = false;
    // construct a big regexp that
    // 1. is anchored to the beginning of str (sans whitespace)
    // 2. matches at least one of the part regexps in prefixRegExps
725
726
727
    const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*")
                              .arg(prefixRegExps.join(QStringLiteral(")|(?:")));
    const QRegularExpression rx(bigRegExp, QRegularExpression::CaseInsensitiveOption);
728
729
    if (rx.isValid()) {
        QString tmp = str;
730
731
        const QRegularExpressionMatch match = rx.match(tmp);
        if (match.hasMatch()) {
732
733
            recognized = true;
            if (replace) {
734
                return tmp.replace(0, match.capturedLength(0), newPrefix + QLatin1Char(' '));
735
736
737
738
            }
        }
    } else {
        qCWarning(MESSAGECORE_LOG) << "bigRegExp = \""
Laurent Montel's avatar
Laurent Montel committed
739
740
                                   << bigRegExp << "\"\n"
                                   << "prefix regexp is invalid!";
741
742
        // try good ole Re/Fwd:
        recognized = str.startsWith(newPrefix);
743
744
    }

745
746
747
748
    if (!recognized) {
        return newPrefix + QLatin1Char(' ') + str;
    } else {
        return str;
749
    }
750
751
752
753
754
755
756
}

QString stripOffPrefixes(const QString &subject)
{
    const QStringList replyPrefixes = MessageCoreSettings::self()->replyPrefixes();

    const QStringList forwardPrefixes = MessageCoreSettings::self()->forwardPrefixes();
757
758
759
760
761
762
763
764

    const QStringList prefixRegExps = replyPrefixes + forwardPrefixes;

    // construct a big regexp that
    // 1. is anchored to the beginning of str (sans whitespace)
    // 2. matches at least one of the part regexps in prefixRegExps
    const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));

765
    static QRegularExpression regex;
766

767
768
    regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
    if (regex.pattern() != bigRegExp) {
769
        // the prefixes have changed, so update the regexp
770
        regex.setPattern(bigRegExp);
771
772
    }

773
774
775
776
    if (regex.isValid()) {
        QRegularExpressionMatch match = regex.match(subject);
        if (match.hasMatch()) {
            return subject.mid(match.capturedEnd(0));
777
778
779
780
781
782
783
784
785
786
787
788
        }
    } else {
        qCWarning(MESSAGECORE_LOG) << "bigRegExp = \""
                                   << bigRegExp << "\"\n"
                                   << "prefix regexp is invalid!";
    }

    return subject;
}

void setEncodingFile(QUrl &url, const QString &encoding)
{
Laurent Montel's avatar
Laurent Montel committed
789
790
791
    QUrlQuery query;
    query.addQueryItem(QStringLiteral("charset"), encoding);
    url.setQuery(query);
792
793
794
}
}
}