Messagelib

stringutil.cpp
1 /*
2  SPDX-FileCopyrightText: 2016-2022 Laurent Montel <[email protected]>
3  SPDX-FileCopyrightText: 2009 Thomas McGuire <mcguire[email protected]>
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 <KIdentityManagement/Identity>
23 #include <KIdentityManagement/IdentityManager>
24 #include <KPIMTextEdit/TextUtils>
25 #include <QHostInfo>
26 #include <QRegularExpression>
27 #include <QUrlQuery>
28 
29 using namespace KMime;
30 using namespace KMime::Types;
31 using namespace KMime::HeaderParsing;
32 
33 namespace MessageCore
34 {
35 namespace StringUtil
36 {
37 // Removes trailing spaces and tabs at the end of the line
38 static 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.
51 static 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.
114 static 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() != QLatin1String("mailto")) {
166  return values;
167  }
168  QString str = url.toString();
169  QStringList toStr;
170  int i = 0;
171 
172  // String can be encoded.
173  str = KCodecs::decodeRFC2047String(str);
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 == QLatin1String("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(QLatin1String(", "));
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 
300 QString 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 
317 QString 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 += QLatin1String("&lt;");
327  break;
328  case '>':
329  result += QLatin1String("&gt;");
330  break;
331  case '&':
332  result += QLatin1String("&amp;");
333  break;
334  case '"':
335  result += QLatin1String("&quot;");
336  break;
337  case '\n':
338  if (!removeLineBreaks) {
339  result += QLatin1String("<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 
354 void 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 
405 QString 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 += QLatin1String("<a href=\"mailto:")
442  + QLatin1String("\" ") + cssStyle + QLatin1Char('>');
443  }
444  const bool foundMe = onlyOneIdentity && (im->identityForAddress(prettyAddressStr) != KIdentityManagement::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 += QLatin1String("</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 
471 QString 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 
483 QString 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 
495 bool 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) + QLatin1String(" <") + address + QLatin1Char('>');
524  }
525 
526  return address;
527 }
528 
529 QString 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 
635 QString 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(QLatin1String(": "), 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 
731 QString cleanSubject(KMime::Message *msg, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
732 {
733  return replacePrefixes(msg->subject()->asUnicodeString(), prefixRegExps, replace, newPrefix);
734 }
735 
737 {
738  return cleanSubject(msg,
739  MessageCore::MessageCoreSettings::self()->forwardPrefixes(),
740  MessageCore::MessageCoreSettings::self()->replaceForwardPrefix(),
741  QStringLiteral("Fwd:"));
742 }
743 
745 {
746  return cleanSubject(msg,
747  MessageCore::MessageCoreSettings::self()->replyPrefixes(),
748  MessageCore::MessageCoreSettings::self()->replaceReplyPrefix(),
749  QStringLiteral("Re:"));
750 }
751 
752 QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
753 {
754  bool recognized = false;
755  // construct a big regexp that
756  // 1. is anchored to the beginning of str (sans whitespace)
757  // 2. matches at least one of the part regexps in prefixRegExps
758  const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));
760  if (rx.isValid()) {
761  QString tmp = str;
762  const QRegularExpressionMatch match = rx.match(tmp);
763  if (match.hasMatch()) {
764  recognized = true;
765  if (replace) {
766  return tmp.replace(0, match.capturedLength(0), newPrefix + QLatin1Char(' '));
767  }
768  }
769  } else {
770  qCWarning(MESSAGECORE_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n"
771  << "prefix regexp is invalid!";
772  // try good ole Re/Fwd:
773  recognized = str.startsWith(newPrefix);
774  }
775 
776  if (!recognized) {
777  return newPrefix + QLatin1Char(' ') + str;
778  } else {
779  return str;
780  }
781 }
782 
784 {
785  const QStringList replyPrefixes = MessageCoreSettings::self()->replyPrefixes();
786 
787  const QStringList forwardPrefixes = MessageCoreSettings::self()->forwardPrefixes();
788 
789  const QStringList prefixRegExps = replyPrefixes + forwardPrefixes;
790 
791  // construct a big regexp that
792  // 1. is anchored to the beginning of str (sans whitespace)
793  // 2. matches at least one of the part regexps in prefixRegExps
794  const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));
795 
796  static QRegularExpression regex;
797 
798  if (regex.pattern() != bigRegExp) {
799  // the prefixes have changed, so update the regexp
800  regex.setPattern(bigRegExp);
802  }
803 
804  if (regex.isValid()) {
805  QRegularExpressionMatch match = regex.match(subject);
806  if (match.hasMatch()) {
807  return subject.mid(match.capturedEnd(0));
808  }
809  } else {
810  qCWarning(MESSAGECORE_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n"
811  << "prefix regexp is invalid!";
812  }
813 
814  return subject;
815 }
816 
817 void setEncodingFile(QUrl &url, const QString &encoding)
818 {
819  QUrlQuery query;
820  query.addQueryItem(QStringLiteral("charset"), encoding);
821  url.setQuery(query);
822 }
823 // code from kitinerary/src/lib/stringutil.cpp
824 QString normalize(QStringView str)
825 {
826  QString out;
827  out.reserve(str.size());
828  for (const auto c : str) {
829  // case folding
830  const auto n = c.toCaseFolded();
831 
832  // if the character has a canonical decomposition use that and skip the
833  // combining diacritic markers following it
834  // see https://en.wikipedia.org/wiki/Unicode_equivalence
835  // see https://en.wikipedia.org/wiki/Combining_character
836  if (n.decompositionTag() == QChar::Canonical) {
837  out.push_back(n.decomposition().at(0));
838  }
839  // handle compatibility compositions such as ligatures
840  // see https://en.wikipedia.org/wiki/Unicode_compatibility_characters
841  else if (n.decompositionTag() == QChar::Compat && n.isLetter() && n.script() == QChar::Script_Latin) {
842  out.append(n.decomposition());
843  } else {
844  out.push_back(n);
845  }
846  }
847  return out;
848 }
849 }
850 }
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
void append(const T &value)
QString pattern() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
void squeeze()
void truncate(int position)
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
KCODECS_EXPORT QString quoteNameIfNecessary(const QString &str)
QString cleanSubject(KMime::Message *msg)
Return this mails subject, with all "forward" and "reply" prefixes removed.
Definition: stringutil.cpp:722
QString replySubject(KMime::Message *msg)
Return this mails subject, formatted for "reply" mails.
Definition: stringutil.cpp:744
int size() const const
KPIMTEXTEDIT_EXPORT QString flowText(QString &text, const QString &indent, int maxLength)
void removePrivateHeaderFields(const KMime::Message::Ptr &message, bool cleanUpHeader)
Removes all private header fields (e.g.
Definition: stringutil.cpp:354
QString forwardSubject(KMime::Message *msg)
Return this mails subject, formatted for "forward" mails.
Definition: stringutil.cpp:736
QString scheme() const const
QDateTime currentDateTime()
int count(const T &value) const const
QString trimmed() const const
void clear()
QString asUnicodeString() const override
Link
Used to determine if the address should be a link or not.
Definition: stringutil.h:97
void chop(int n)
QString stripSignature(const QString &msg)
Strips the signature blocks from a message text.
Definition: stringutil.cpp:223
QVector< QPair< QString, QString > > parseMailtoUrl(const QUrl &url)
Parses a mailto: url and extracts the information in the QMap (field name as key).
Definition: stringutil.cpp:162
Types::Mailbox::List mailboxes() const
QList::const_iterator constBegin() const const
void reserve(int size)
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
AddressList splitAddressField(const QByteArray &text)
Splits the given address list text into separate addresses.
Definition: stringutil.cpp:283
qsizetype size() const const
void removeAt(int i)
QString stripOffPrefixes(const QString &subject)
Removes the forward and reply marks (e.g.
Definition: stringutil.cpp:783
void removeLast()
KCODECS_EXPORT QString decodeMailtoUrl(const QUrl &mailtoUrl)
QString toString(QUrl::FormattingOptions options) const const
const Identity & identityForAddress(const QString &addresses) const
void setPattern(const QString &pattern)
KCODECS_EXPORT QString decodeRFC2047String(const QByteArray &src, QByteArray *usedCS, const QByteArray &defaultCS=QByteArray(), CharsetOption option=NoOption)
constexpr bool isEmpty() const
KCODECS_EXPORT QString toIdn(const QString &addrSpec)
QString smartQuote(const QString &msg, int maxLineLength)
Relayouts the given string so that the individual lines don't exceed the given maximal length.
Definition: stringutil.cpp:529
FullyDecoded
Types::Mailbox::List mailboxes() const
void setPatternOptions(QRegularExpression::PatternOptions options)
bool isEmpty() const const
bool isValid() const
QByteArray toUtf8() const const
int length() const const
void push_back(QChar ch)
QByteArray headerAsSendableString(const KMime::Message::Ptr &originalMessage)
Return the message header with the headers that should not be sent stripped off.
Definition: stringutil.cpp:394
QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
Check for prefixes prefixRegExps in str.
Definition: stringutil.cpp:752
QString guessEmailAddressFromLoginName(const QString &loginName)
Uses the hostname as domain part and tries to determine the real name from the entries in the passwor...
Definition: stringutil.cpp:509
AddressMode
Used to determine if the address field should be expandable/collapsible.
Definition: stringutil.h:102
bool isEmpty() const const
QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
Convert quote wildcards into the final quote prefix.
Definition: stringutil.cpp:635
QString join(const QString &separator) const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
static IdentityManager * self()
QByteArray toPercentEncoding(const QString &input, const QByteArray &exclude, const QByteArray &include)
QRegularExpressionMatch match(const QString &subject, int offset, QRegularExpression::MatchType matchType, QRegularExpression::MatchOptions matchOptions) const const
bool addressIsInAddressList(const QString &address, const QStringList &addresses)
Returns true if the given address is contained in the given address list.
Definition: stringutil.cpp:495
QByteArray asSendableString(const KMime::Message::Ptr &originalMessage)
Returns the message contents with the headers that should not be sent stripped off.
Definition: stringutil.cpp:383
QString & replace(int position, int n, QChar after)
T & last()
QString & remove(int position, int n)
void setQuery(const QString &query, QUrl::ParsingMode mode)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString quoteHtmlChars(const QString &str, bool removeLineBreaks)
Quotes the following characters which have a special meaning in HTML: '<' '>' '&' '"'....
Definition: stringutil.cpp:317
typedef ConstIterator
QVariant property(UserProperty which) const
QList::const_iterator constEnd() const const
QString path(QUrl::ComponentFormattingOptions options) const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString localHostName()
QString left(int n) const const
QString fromLatin1(const char *str, int size)
QString cleanFileName(const QString &name)
Cleans a filename by replacing characters not allowed or wanted on the filesystem e....
Definition: stringutil.cpp:686
KMime::Headers::Subject * subject(bool create=true)
const QChar at(int position) const const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString toString(Qt::DateFormat format) const const
char toLatin1() const const
QString mid(int position, int n) const const
QVector< V > values(const QMultiHash< K, V > &c)
QString message
QString & append(QChar ch)
KCODECS_EXPORT QUrl encodeMailtoUrl(const QString &mailbox)
QUrl fromUserInput(const QString &userInput)
char * data()
bool isValid() const const
QString toString() const const
QString generateMessageId(const QString &address, const QString &suffix)
Generates the Message-Id.
Definition: stringutil.cpp:300
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Tue May 17 2022 04:00:05 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.