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

KDE's Doxygen guidelines are available online.