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

KDE's Doxygen guidelines are available online.