Messagelib

stringutil.cpp
1/*
2 SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org>
3 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#include "stringutil.h"
8
9#include "MessageCore/MessageCoreSettings"
10
11#include <KEmailAddress>
12#include <KLocalizedString>
13#include <KMime/HeaderParsing>
14#include <KMime/Headers>
15#include <KMime/Message>
16
17#include "messagecore_debug.h"
18#include <KUser>
19
20#include <KCodecs>
21#include <KIdentityManagementCore/Identity>
22#include <KIdentityManagementCore/IdentityManager>
23#include <KPIMTextEdit/TextUtils>
24#include <QHostInfo>
25#include <QRegularExpression>
26#include <QUrlQuery>
27
28using namespace KMime;
29using namespace KMime::Types;
30using namespace KMime::HeaderParsing;
31
32namespace MessageCore
33{
34namespace StringUtil
35{
36// Removes trailing spaces and tabs at the end of the line
37static void removeTrailingSpace(QString &line)
38{
39 int i = line.length() - 1;
40 while ((i >= 0) && ((line[i] == QLatin1Char(' ')) || (line[i] == QLatin1Char('\t')))) {
41 i--;
42 }
43 line.truncate(i + 1);
44}
45
46// Splits the line off in two parts: The quote prefixes and the actual text of the line.
47// For example, for the string "> > > Hello", it would be split up in "> > > " as the quote
48// prefix, and "Hello" as the actual text.
49// The actual text is written back to the "line" parameter, and the quote prefix is returned.
50static QString splitLine(QString &line)
51{
52 removeTrailingSpace(line);
53 int i = 0;
54 int startOfActualText = -1;
55
56 // TODO: Replace tabs with spaces first.
57
58 // Loop through the chars in the line to find the place where the quote prefix stops
59 const int lineLength(line.length());
60 while (i < lineLength) {
61 const QChar c = line[i];
62 const bool isAllowedQuoteChar =
63 (c == QLatin1Char('>')) || (c == QLatin1Char(':')) || (c == QLatin1Char('|')) || (c == QLatin1Char(' ')) || (c == QLatin1Char('\t'));
64 if (isAllowedQuoteChar) {
65 startOfActualText = i + 1;
66 } else {
67 break;
68 }
69 ++i;
70 }
71
72 // If the quote prefix only consists of whitespace, don't consider it as a quote prefix at all
73 if (line.left(startOfActualText).trimmed().isEmpty()) {
74 startOfActualText = 0;
75 }
76
77 // No quote prefix there -> nothing to do
78 if (startOfActualText <= 0) {
79 return {};
80 }
81
82 // Entire line consists of only the quote prefix
83 if (i == line.length()) {
84 const QString quotePrefix = line.left(startOfActualText);
85 line.clear();
86 return quotePrefix;
87 }
88
89 // Line contains both the quote prefix and the actual text, really split it up now
90 const QString quotePrefix = line.left(startOfActualText);
91 line = line.mid(startOfActualText);
92
93 return quotePrefix;
94}
95
96// Writes all lines/text parts contained in the "textParts" list to the output text, "msg".
97// Quote characters are added in front of each line, and no line is longer than
98// maxLength.
99//
100// Although the lines in textParts are considered separate lines, they can actually be run
101// together into a single line in some cases. This is basically the main difference to flowText().
102//
103// Example:
104// textParts = "Hello World, this is a test.", "Really"
105// indent = ">"
106// maxLength = 20
107// Result: "> Hello World, this\n
108// > is a test. Really"
109// Notice how in this example, the text line "Really" is no longer a separate line, it was run
110// together with a previously broken line.
111//
112// "textParts" is cleared upon return.
113static bool flushPart(QString &msg, QStringList &textParts, const QString &indent, int maxLength)
114{
115 if (maxLength < 20) {
116 maxLength = 20;
117 }
118
119 // Remove empty lines at end of quote
120 while (!textParts.isEmpty() && textParts.last().isEmpty()) {
121 textParts.removeLast();
122 }
123
124 QString text;
125
126 for (const QString &line : textParts) {
127 // An empty line in the input means that an empty line should be in the output as well.
128 // Therefore, we write all of our text so far to the msg.
129 if (line.isEmpty()) {
130 if (!text.isEmpty()) {
131 msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength) + QLatin1Char('\n');
132 }
133 msg += indent + QLatin1Char('\n');
134 } else {
135 if (text.isEmpty()) {
136 text = line;
137 } else {
138 text += QLatin1Char(' ') + line.trimmed();
139 }
140 // If the line doesn't need to be wrapped at all, just write it out as-is.
141 // When a line exceeds the maximum length and therefore needs to be broken, this statement
142 // if false, and therefore we keep adding lines to our text, so they get ran together in the
143 // next flowText call, as "text" contains several text parts/lines then.
144 if ((text.length() < maxLength) || (line.length() < (maxLength - 10))) {
145 msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength) + QLatin1Char('\n');
146 }
147 }
148 }
149
150 // Write out pending text to the msg
151 if (!text.isEmpty()) {
152 msg += KPIMTextEdit::TextUtils::flowText(text, indent, maxLength);
153 }
154
155 const bool appendEmptyLine = !textParts.isEmpty();
156 textParts.clear();
157
158 return appendEmptyLine;
159}
160
162{
164 if (url.scheme() != QLatin1StringView("mailto")) {
165 return values;
166 }
167 QString str = url.toString();
168 QStringList toStr;
169 int i = 0;
170
171 // String can be encoded.
173 // Bug 427697
174 str.replace(QStringLiteral("&#38;"), QStringLiteral("&"));
175 const QUrl newUrl = QUrl::fromUserInput(str);
176
177 int indexTo = -1;
178 // Workaround line with # see bug 406208
179 const int indexOf = str.indexOf(QLatin1Char('?'));
180 if (indexOf != -1) {
181 str.remove(0, indexOf + 1);
182 QUrlQuery query(str);
183 const auto listQuery = query.queryItems(QUrl::FullyDecoded);
184 for (const auto &queryItem : listQuery) {
185 if (queryItem.first == QLatin1StringView("to")) {
186 toStr << queryItem.second;
187 indexTo = i;
188 } else {
189 if (queryItem.second.isEmpty()) {
190 // Bug 206269 => A%26B => encoded '&'
191 if (i >= 1) {
192 values[i - 1].second = values[i - 1].second + QStringLiteral("&") + queryItem.first;
193 }
194 } else {
195 QPair<QString, QString> pairElement;
196 pairElement.first = queryItem.first.toLower();
197 pairElement.second = queryItem.second;
198 values.append(pairElement);
199 i++;
200 }
201 }
202 }
203 }
205 if (!toStr.isEmpty()) {
206 to << toStr;
207 }
208 const QString fullTo = to.join(QLatin1StringView(", "));
209 if (!fullTo.isEmpty()) {
210 QPair<QString, QString> pairElement;
211 pairElement.first = QStringLiteral("to");
212 pairElement.second = fullTo;
213 if (indexTo != -1) {
214 values.insert(indexTo, pairElement);
215 } else {
216 values.prepend(pairElement);
217 }
218 }
219 return values;
220}
221
223{
224 // Following RFC 3676, only > before --
225 // I prefer to not delete a SB instead of delete good mail content.
226 static const QRegularExpression sbDelimiterSearch(QStringLiteral("(^|\n)[> ]*-- \n"));
227 // The regular expression to look for prefix change
228 static const QRegularExpression commonReplySearch(QStringLiteral("^[ ]*>"));
229
230 QString res = msg;
231 int posDeletingStart = 1; // to start looking at 0
232
233 // While there are SB delimiters (start looking just before the deleted SB)
234 while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) {
235 QString prefix; // the current prefix
236 QString line; // the line to check if is part of the SB
237 int posNewLine = -1;
238
239 // Look for the SB beginning
240 const int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart);
241 // The prefix before "-- "$
242 if (res.at(posDeletingStart) == QLatin1Char('\n')) {
243 ++posDeletingStart;
244 }
245
246 prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart);
247 posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1;
248
249 // now go to the end of the SB
250 while (posNewLine < res.size() && posNewLine > 0) {
251 // handle the undefined case for mid ( x , -n ) where n>1
252 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine);
253
254 if (nextPosNewLine < 0) {
255 nextPosNewLine = posNewLine - 1;
256 }
257
258 line = res.mid(posNewLine, nextPosNewLine - posNewLine);
259
260 // check when the SB ends:
261 // * does not starts with prefix or
262 // * starts with prefix+(any substring of prefix)
263 if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0)
264 || (!prefix.isEmpty() && line.startsWith(prefix) && line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) {
265 posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1;
266 } else {
267 break; // end of the SB
268 }
269 }
270
271 // remove the SB or truncate when is the last SB
272 if (posNewLine > 0) {
273 res.remove(posDeletingStart, posNewLine - posDeletingStart);
274 } else {
275 res.truncate(posDeletingStart);
276 }
277 }
278
279 return res;
280}
281
283{
284 AddressList result;
285 const char *begin = text.begin();
286 if (!begin) {
287 return result;
288 }
289
290 const char *const end = text.begin() + text.length();
291
292 if (!parseAddressList(begin, end, result)) {
293 qCWarning(MESSAGECORE_LOG) << "Error in address splitting: parseAddressList returned false!" << text;
294 }
295
296 return result;
297}
298
299QString generateMessageId(const QString &address, const QString &suffix)
300{
301 const QDateTime dateTime = QDateTime::currentDateTime();
302
303 QString msgIdStr = QLatin1Char('<') + dateTime.toString(QStringLiteral("yyyyMMddhhmm.sszzz"));
304
305 if (!suffix.isEmpty()) {
306 msgIdStr += QLatin1Char('@') + suffix;
307 } else {
308 msgIdStr += QLatin1Char('.') + KEmailAddress::toIdn(address);
309 }
310
311 msgIdStr += QLatin1Char('>');
312
313 return msgIdStr;
314}
315
316QString quoteHtmlChars(const QString &str, bool removeLineBreaks)
317{
318 QString result;
319
320 int strLength(str.length());
321 result.reserve(6 * strLength); // maximal possible length
322 for (int i = 0; i < strLength; ++i) {
323 switch (str[i].toLatin1()) {
324 case '<':
325 result += QLatin1StringView("&lt;");
326 break;
327 case '>':
328 result += QLatin1StringView("&gt;");
329 break;
330 case '&':
331 result += QLatin1StringView("&amp;");
332 break;
333 case '"':
334 result += QLatin1StringView("&quot;");
335 break;
336 case '\n':
337 if (!removeLineBreaks) {
338 result += QLatin1StringView("<br>");
339 }
340 break;
341 case '\r':
342 // ignore CR
343 break;
344 default:
345 result += str[i];
346 }
347 }
348
349 result.squeeze();
350 return result;
351}
352
353void removePrivateHeaderFields(const KMime::Message::Ptr &message, bool cleanUpHeader)
354{
355 message->removeHeader("Status");
356 message->removeHeader("X-Status");
357 message->removeHeader("X-KMail-EncryptionState");
358 message->removeHeader("X-KMail-SignatureState");
359 message->removeHeader("X-KMail-Redirect-From");
360 message->removeHeader("X-KMail-Link-Message");
361 message->removeHeader("X-KMail-Link-Type");
362 message->removeHeader("X-KMail-QuotePrefix");
363 message->removeHeader("X-KMail-CursorPos");
364 message->removeHeader("X-KMail-Templates");
365 message->removeHeader("X-KMail-Drafts");
366 message->removeHeader("X-KMail-UnExpanded-To");
367 message->removeHeader("X-KMail-UnExpanded-CC");
368 message->removeHeader("X-KMail-UnExpanded-BCC");
369 message->removeHeader("X-KMail-UnExpanded-Reply-To");
370 message->removeHeader("X-KMail-FccDisabled");
371
372 if (cleanUpHeader) {
373 message->removeHeader("X-KMail-Fcc");
374 message->removeHeader("X-KMail-Transport");
375 message->removeHeader("X-KMail-Identity");
376 message->removeHeader("X-KMail-Transport-Name");
377 message->removeHeader("X-KMail-Identity-Name");
378 message->removeHeader("X-KMail-Dictionary");
379 }
380}
381
383{
385 message->setContent(originalMessage->encodedContent());
386
388 message->removeHeader<KMime::Headers::Bcc>();
389
390 return message->encodedContent();
391}
392
394{
396 message->setContent(originalMessage->encodedContent());
397
399 message->removeHeader<KMime::Headers::Bcc>();
400
401 return message->head();
402}
403
404QString emailAddrAsAnchor(const KMime::Types::Mailbox::List &mailboxList,
405 Display display,
406 const QString &cssStyle,
407 Link link,
408 AddressMode expandable,
409 const QString &fieldName,
410 int collapseNumber)
411{
412 QString result;
413 int numberAddresses = 0;
414 bool expandableInserted = false;
416
417 const QString i18nMe = i18nc("signal that this email is defined in my identity", "Me");
418 const bool onlyOneIdentity = (im->identities().count() == 1);
419 for (const KMime::Types::Mailbox &mailbox : mailboxList) {
420 const QString prettyAddressStr = mailbox.prettyAddress();
421 if (!prettyAddressStr.isEmpty()) {
422 numberAddresses++;
423 if (expandable == ExpandableAddresses && !expandableInserted && numberAddresses > collapseNumber) {
424 const QString actualListAddress = result;
425 QString shortListAddress = actualListAddress;
426 if (link == ShowLink) {
427 shortListAddress.truncate(result.length() - 2);
428 }
429 result = QStringLiteral("<span><input type=\"checkbox\" class=\"addresslist_checkbox\" id=\"%1\" checked=\"checked\"/><span class=\"short%1\">")
430 .arg(fieldName)
431 + shortListAddress;
432 result += QStringLiteral("<label class=\"addresslist_label_short\" for=\"%1\"></label></span>").arg(fieldName);
433 expandableInserted = true;
434 result += QStringLiteral("<span class=\"full%1\">").arg(fieldName) + actualListAddress;
435 }
436
437 if (link == ShowLink) {
438 result += QLatin1StringView("<a href=\"mailto:")
441 + QLatin1StringView("\" ") + cssStyle + QLatin1Char('>');
442 }
443 const bool foundMe = onlyOneIdentity && (im->identityForAddress(prettyAddressStr) != KIdentityManagementCore::Identity::null());
444
445 if (display == DisplayNameOnly) {
446 if (!mailbox.name().isEmpty()) { // Fallback to the email address when the name is not set.
447 result += foundMe ? i18nMe : quoteHtmlChars(mailbox.name(), true);
448 } else {
449 result += foundMe ? i18nMe : quoteHtmlChars(prettyAddressStr, true);
450 }
451 } else {
452 result += foundMe ? i18nMe : quoteHtmlChars(mailbox.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary), true);
453 }
454 if (link == ShowLink) {
455 result += QLatin1StringView("</a>, ");
456 }
457 }
458 }
459
460 if (link == ShowLink) {
461 result.chop(2);
462 }
463
464 if (expandableInserted) {
465 result += QStringLiteral("<label class=\"addresslist_label_full\" for=\"%1\"></label></span></span>").arg(fieldName);
466 }
467 return result;
468}
469
470QString emailAddrAsAnchor(const KMime::Headers::Generics::MailboxList *mailboxList,
471 Display display,
472 const QString &cssStyle,
473 Link link,
474 AddressMode expandable,
475 const QString &fieldName,
476 int collapseNumber)
477{
478 Q_ASSERT(mailboxList);
479 return emailAddrAsAnchor(mailboxList->mailboxes(), display, cssStyle, link, expandable, fieldName, collapseNumber);
480}
481
482QString emailAddrAsAnchor(const KMime::Headers::Generics::AddressList *addressList,
483 Display display,
484 const QString &cssStyle,
485 Link link,
486 AddressMode expandable,
487 const QString &fieldName,
488 int collapseNumber)
489{
490 Q_ASSERT(addressList);
491 return emailAddrAsAnchor(addressList->mailboxes(), display, cssStyle, link, expandable, fieldName, collapseNumber);
492}
493
494bool addressIsInAddressList(const QString &address, const QStringList &addresses)
495{
496 const QString addrSpec = KEmailAddress::extractEmailAddress(address);
497
498 QStringList::ConstIterator end(addresses.constEnd());
499 for (QStringList::ConstIterator it = addresses.constBegin(); it != end; ++it) {
500 if (qstricmp(addrSpec.toUtf8().data(), KEmailAddress::extractEmailAddress(*it).toUtf8().data()) == 0) {
501 return true;
502 }
503 }
504
505 return false;
506}
507
509{
510 if (loginName.isEmpty()) {
511 return {};
512 }
513
514 QString address = loginName;
515 address += QLatin1Char('@');
516 address += QHostInfo::localHostName();
517
518 // try to determine the real name
519 const KUser user(loginName);
520 if (user.isValid()) {
521 const QString fullName = user.property(KUser::FullName).toString();
522 address = KEmailAddress::quoteNameIfNecessary(fullName) + QLatin1StringView(" <") + address + QLatin1Char('>');
523 }
524
525 return address;
526}
527
528QString smartQuote(const QString &msg, int maxLineLength)
529{
530 // The algorithm here is as follows:
531 // We split up the incoming msg into lines, and then iterate over each line.
532 // We keep adding lines with the same indent ( = quote prefix, e.g. "> " ) to a
533 // "textParts" list. So the textParts list contains only lines with the same quote
534 // prefix.
535 //
536 // When all lines with the same indent are collected in "textParts", we write those out
537 // to the result by calling flushPart(), which does all the nice formatting for us.
538
539 QStringList textParts;
540 QString oldIndent;
541 bool firstPart = true;
542 QString result;
543
544 int lineStart = 0;
545 int lineEnd = msg.indexOf(QLatin1Char('\n'));
546 bool needToContinue = true;
547 for (; needToContinue; lineStart = lineEnd + 1, lineEnd = msg.indexOf(QLatin1Char('\n'), lineStart)) {
548 QString line;
549 if (lineEnd == -1) {
550 if (lineStart == 0) {
551 line = msg;
552 needToContinue = false;
553 } else if (lineStart != msg.length()) {
554 line = msg.mid(lineStart, msg.length() - lineStart);
555 needToContinue = false;
556 } else {
557 needToContinue = false;
558 }
559 } else {
560 line = msg.mid(lineStart, lineEnd - lineStart);
561 }
562 // Split off the indent from the line
563 const QString indent = splitLine(line);
564
565 if (line.isEmpty()) {
566 if (!firstPart) {
567 textParts.append(QString());
568 }
569 continue;
570 }
571
572 if (firstPart) {
573 oldIndent = indent;
574 firstPart = false;
575 }
576
577 // The indent changed, that means we have to write everything contained in textParts to the
578 // result, which we do by calling flushPart().
579 if (oldIndent != indent) {
580 // Check if the last non-blank line is a "From" line. A from line is the line containing the
581 // attribution to a quote, e.g. "Yesterday, you wrote:". We'll just check for the last colon
582 // here, to simply things.
583 // If there is a From line, remove it from the textParts to that flushPart won't break it.
584 // We'll manually add it to the result afterwards.
585 QString fromLine;
586 if (!textParts.isEmpty()) {
587 for (int i = textParts.count() - 1; i >= 0; i--) {
588 // Check if we have found the From line
589 const QString textPartElement(textParts[i]);
590 if (textPartElement.endsWith(QLatin1Char(':'))) {
591 fromLine = oldIndent + textPartElement + QLatin1Char('\n');
592 textParts.removeAt(i);
593 break;
594 }
595
596 // Abort on first non-empty line
597 if (!textPartElement.trimmed().isEmpty()) {
598 break;
599 }
600 }
601 }
602
603 // Write out all lines with the same indent using flushPart(). The textParts list
604 // is cleared for us.
605 if (flushPart(result, textParts, oldIndent, maxLineLength)) {
606 if (oldIndent.length() > indent.length()) {
607 result += indent + QLatin1Char('\n');
608 } else {
609 result += oldIndent + QLatin1Char('\n');
610 }
611 }
612
613 if (!fromLine.isEmpty()) {
614 result += fromLine;
615 }
616
617 oldIndent = indent;
618 }
619
620 textParts.append(line);
621 }
622
623 // Write out anything still pending
624 flushPart(result, textParts, oldIndent, maxLineLength);
625
626 // Remove superfluous newline which was appended in flowText
627 if (!result.isEmpty() && result.endsWith(QLatin1Char('\n'))) {
628 result.chop(1);
629 }
630
631 return result;
632}
633
634QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
635{
636 QString result;
637
638 if (wildString.isEmpty()) {
639 return wildString;
640 }
641
642 int strLength(wildString.length());
643 for (int i = 0; i < strLength;) {
644 QChar ch = wildString[i++];
645 if (ch == QLatin1Char('%') && i < strLength) {
646 ch = wildString[i++];
647 switch (ch.toLatin1()) {
648 case 'f': { // sender's initials
649 if (fromDisplayString.isEmpty()) {
650 break;
651 }
652
653 int j = 0;
654 const int strLengthFromDisplayString(fromDisplayString.length());
655 for (; j < strLengthFromDisplayString && fromDisplayString[j] > QLatin1Char(' '); ++j) { }
656 for (; j < strLengthFromDisplayString && fromDisplayString[j] <= QLatin1Char(' '); ++j) { }
657 result += fromDisplayString[0];
658 if (j < strLengthFromDisplayString && fromDisplayString[j] > QLatin1Char(' ')) {
659 result += fromDisplayString[j];
660 } else if (strLengthFromDisplayString > 1) {
661 if (fromDisplayString[1] > QLatin1Char(' ')) {
662 result += fromDisplayString[1];
663 }
664 }
665 break;
666 }
667 case '_':
668 result += QLatin1Char(' ');
669 break;
670 case '%':
671 result += QLatin1Char('%');
672 break;
673 default:
674 result += QLatin1Char('%');
675 result += ch;
676 break;
677 }
678 } else {
679 result += ch;
680 }
681 }
682 return result;
683}
684
686{
687 QString fileName = name.trimmed();
688
689 // We need to replace colons with underscores since those cause problems with
690 // KFileDialog (bug in KFileDialog though) and also on Windows filesystems.
691 // We also look at the special case of ": ", since converting that to "_ "
692 // would look strange, simply "_" looks better.
693 // https://issues.kolab.org/issue3805
694 fileName.replace(QLatin1StringView(": "), QStringLiteral("_"));
695 // replace all ':' with '_' because ':' isn't allowed on FAT volumes
696 fileName.replace(QLatin1Char(':'), QLatin1Char('_'));
697 // better not use a dir-delimiter in a filename
698 fileName.replace(QLatin1Char('/'), QLatin1Char('_'));
699 fileName.replace(QLatin1Char('\\'), QLatin1Char('_'));
700
701#ifdef Q_OS_WINDOWS
702 // replace all '.' with '_', not just at the start of the filename
703 // but don't replace the last '.' before the file extension.
704 int i = fileName.lastIndexOf(QLatin1Char('.'));
705 if (i != -1) {
706 i = fileName.lastIndexOf(QLatin1Char('.'), i - 1);
707 }
708
709 while (i != -1) {
710 fileName.replace(i, 1, QLatin1Char('_'));
711 i = fileName.lastIndexOf(QLatin1Char('.'), i - 1);
712 }
713#endif
714
715 // replace all '~' with '_', not just leading '~' either.
716 fileName.replace(QLatin1Char('~'), QLatin1Char('_'));
717
718 return fileName;
719}
720
722{
723 return cleanSubject(msg,
724 MessageCore::MessageCoreSettings::self()->replyPrefixes() + MessageCore::MessageCoreSettings::self()->forwardPrefixes(),
725 true,
726 QString())
727 .trimmed();
728}
729
730QString cleanSubject(KMime::Message *msg, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
731{
732 if (auto subject = msg->subject(false)) {
733 return replacePrefixes(subject->asUnicodeString(), prefixRegExps, replace, newPrefix);
734 } else {
735 return {};
736 }
737}
738
740{
741 return cleanSubject(msg,
742 MessageCore::MessageCoreSettings::self()->forwardPrefixes(),
743 MessageCore::MessageCoreSettings::self()->replaceForwardPrefix(),
744 QStringLiteral("Fwd:"));
745}
746
748{
749 return cleanSubject(msg,
750 MessageCore::MessageCoreSettings::self()->replyPrefixes(),
751 MessageCore::MessageCoreSettings::self()->replaceReplyPrefix(),
752 QStringLiteral("Re:"));
753}
754
755QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
756{
757 bool recognized = false;
758 // construct a big regexp that
759 // 1. is anchored to the beginning of str (sans whitespace)
760 // 2. matches at least one of the part regexps in prefixRegExps
761 const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));
763 if (rx.isValid()) {
764 QString tmp = str;
765 const QRegularExpressionMatch match = rx.match(tmp);
766 if (match.hasMatch()) {
767 recognized = true;
768 if (replace) {
769 return tmp.replace(0, match.capturedLength(0), newPrefix + QLatin1Char(' '));
770 }
771 }
772 } else {
773 qCWarning(MESSAGECORE_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n"
774 << "prefix regexp is invalid!";
775 // try good ole Re/Fwd:
776 recognized = str.startsWith(newPrefix);
777 }
778
779 if (!recognized) {
780 return newPrefix + QLatin1Char(' ') + str;
781 } else {
782 return str;
783 }
784}
785
787{
788 const QStringList replyPrefixes = MessageCoreSettings::self()->replyPrefixes();
789
790 const QStringList forwardPrefixes = MessageCoreSettings::self()->forwardPrefixes();
791
792 const QStringList prefixRegExps = replyPrefixes + forwardPrefixes;
793
794 // construct a big regexp that
795 // 1. is anchored to the beginning of str (sans whitespace)
796 // 2. matches at least one of the part regexps in prefixRegExps
797 const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));
798
799 static QRegularExpression regex;
800
801 if (regex.pattern() != bigRegExp) {
802 // the prefixes have changed, so update the regexp
803 regex.setPattern(bigRegExp);
805 }
806
807 if (regex.isValid()) {
808 QRegularExpressionMatch match = regex.match(subject);
809 if (match.hasMatch()) {
810 return subject.mid(match.capturedEnd(0));
811 }
812 } else {
813 qCWarning(MESSAGECORE_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n"
814 << "prefix regexp is invalid!";
815 }
816
817 return subject;
818}
819
820void setEncodingFile(QUrl &url, const QString &encoding)
821{
822 QUrlQuery query;
823 query.addQueryItem(QStringLiteral("charset"), encoding);
824 url.setQuery(query);
825}
826}
827}
static IdentityManager * self()
const Identity & identityForAddress(const QString &addresses) const
constexpr bool isEmpty() const
Types::Mailbox::List mailboxes() const
Types::Mailbox::List mailboxes() const
const KMime::Headers::Subject * subject() const
QVariant property(UserProperty which) const
bool isValid() const
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
KCODECS_EXPORT QString quoteNameIfNecessary(const QString &str)
KCODECS_EXPORT QString toIdn(const QString &addrSpec)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
KCODECS_EXPORT QString decodeRFC2047String(QByteArrayView src, QByteArray *usedCS, const QByteArray &defaultCS=QByteArray(), CharsetOption option=NoOption)
KCODECS_EXPORT QUrl encodeMailtoUrl(const QString &mailbox)
KCODECS_EXPORT QString decodeMailtoUrl(const QUrl &mailtoUrl)
KPIMTEXTEDIT_EXPORT QString flowText(QString &text, const QString &indent, int maxLength)
AddressMode
Used to determine if the address field should be expandable/collapsible.
Definition stringutil.h:108
QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
Check for prefixes prefixRegExps in str.
bool addressIsInAddressList(const QString &address, const QStringList &addresses)
Returns true if the given address is contained in the given address list.
QList< QPair< QString, QString > > parseMailtoUrl(const QUrl &url)
Parses a mailto: url and extracts the information in the QMap (field name as key).
QString stripOffPrefixes(const QString &subject)
Removes the forward and reply marks (e.g.
QString guessEmailAddressFromLoginName(const QString &loginName)
Uses the hostname as domain part and tries to determine the real name from the entries in the passwor...
Display
Used to determine if the visible part of the anchor contains only the name part and not the given ema...
Definition stringutil.h:92
QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
Convert quote wildcards into the final quote prefix.
QByteArray headerAsSendableString(const KMime::Message::Ptr &originalMessage)
Return the message header with the headers that should not be sent stripped off.
QString cleanSubject(KMime::Message *msg)
Return this mails subject, with all "forward" and "reply" prefixes removed.
QString stripSignature(const QString &msg)
Strips the signature blocks from a message text.
QString smartQuote(const QString &msg, int maxLineLength)
Relayouts the given string so that the individual lines don't exceed the given maximal length.
void removePrivateHeaderFields(const KMime::Message::Ptr &message, bool cleanUpHeader)
Removes all private header fields (e.g.
QString cleanFileName(const QString &name)
Cleans a filename by replacing characters not allowed or wanted on the filesystem e....
AddressList splitAddressField(const QByteArray &text)
Splits the given address list text into separate addresses.
QByteArray asSendableString(const KMime::Message::Ptr &originalMessage)
Returns the message contents with the headers that should not be sent stripped off.
QString replySubject(KMime::Message *msg)
Return this mails subject, formatted for "reply" mails.
QString generateMessageId(const QString &address, const QString &suffix)
Generates the Message-Id.
Link
Used to determine if the address should be a link or not.
Definition stringutil.h:100
QString quoteHtmlChars(const QString &str, bool removeLineBreaks)
Quotes the following characters which have a special meaning in HTML: '<' '>' '&' '"'....
QString forwardSubject(KMime::Message *msg)
Return this mails subject, formatted for "forward" mails.
char * data()
char toLatin1() const const
QDateTime currentDateTime()
QString toString(QStringView format, QCalendar cal) const const
QString localHostName()
typedef ConstIterator
void append(QList< T > &&value)
void clear()
const_iterator constBegin() const const
const_iterator constEnd() const const
qsizetype count() const const
T & first()
iterator insert(const_iterator before, parameter_type value)
bool isEmpty() const const
T & last()
void prepend(parameter_type value)
void removeAt(qsizetype i)
void removeLast()
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
bool isValid() const const
QString pattern() const const
void setPattern(const QString &pattern)
void setPatternOptions(PatternOptions options)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
void chop(qsizetype n)
void clear()
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString first(qsizetype n) const const
QString fromLatin1(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
void reserve(qsizetype size)
qsizetype size() const const
void squeeze()
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
QString trimmed() const const
void truncate(qsizetype position)
QString join(QChar separator) const const
FullyDecoded
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
QString path(ComponentFormattingOptions options) const const
QString scheme() const const
void setQuery(const QString &query, ParsingMode mode)
QByteArray toPercentEncoding(const QString &input, const QByteArray &exclude, const QByteArray &include)
QString toString(FormattingOptions options) const const
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:54:19 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.