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)
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...
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.
AddressMode
Used to determine if the address field should be expandable/collapsible.
Definition stringutil.h:108
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 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 Sat Dec 21 2024 16:59:12 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.