Messagelib

templateparserjob.cpp
1 /*
2  SPDX-FileCopyrightText: 2017-2022 Laurent Montel <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "templateparserjob.h"
8 #include "customtemplates_kfg.h"
9 #include "globalsettings_templateparser.h"
10 #include "templateparserextracthtmlinfo.h"
11 #include "templateparserjob_p.h"
12 #include "templatesconfiguration.h"
13 #include "templatesconfiguration_kfg.h"
14 #include "templatesutil.h"
15 #include "templatesutil_p.h"
16 
17 #include <MessageCore/ImageCollector>
18 #include <MessageCore/StringUtil>
19 
20 #include <MimeTreeParser/MessagePart>
21 #include <MimeTreeParser/ObjectTreeParser>
22 #include <MimeTreeParser/SimpleObjectTreeSource>
23 
24 #include <KIdentityManagement/Identity>
25 #include <KIdentityManagement/IdentityManager>
26 
27 #include "templateparser_debug.h"
28 #include <KCharsets>
29 #include <KLocalizedString>
30 #include <KMessageBox>
31 #include <KProcess>
32 #include <KShell>
33 
34 #include <QDir>
35 #include <QFile>
36 #include <QFileInfo>
37 #include <QLocale>
38 #include <QRegularExpression>
39 #include <QTextCodec>
40 
41 namespace
42 {
43 Q_DECL_CONSTEXPR inline int pipeTimeout()
44 {
45  return 15 * 1000;
46 }
47 
48 static QTextCodec *selectCharset(const QStringList &charsets, const QString &text)
49 {
50  for (const QString &name : charsets) {
51  // We use KCharsets::codecForName() instead of QTextCodec::codecForName() here, because
52  // the former knows us-ascii is latin1.
53  bool ok = true;
54  QTextCodec *codec = nullptr;
55  if (name == QLatin1String("locale")) {
57  } else {
58  codec = KCharsets::charsets()->codecForName(name, ok);
59  }
60  if (!ok || !codec) {
61  qCWarning(TEMPLATEPARSER_LOG) << "Could not get text codec for charset" << name;
62  continue;
63  }
64  if (codec->canEncode(text)) {
65  // Special check for us-ascii (needed because us-ascii is not exactly latin1).
66  if (name == QLatin1String("us-ascii") && !KMime::isUsAscii(text)) {
67  continue;
68  }
69  qCDebug(TEMPLATEPARSER_LOG) << "Chosen charset" << name << codec->name();
70  return codec;
71  }
72  }
73  if (!charsets.isEmpty()) {
74  qCDebug(TEMPLATEPARSER_LOG) << "No appropriate charset found.";
75  }
76  return KCharsets::charsets()->codecForName(QStringLiteral("utf-8"));
77 }
78 }
79 
80 using namespace TemplateParser;
81 
82 TemplateParserJobPrivate::TemplateParserJobPrivate(const KMime::Message::Ptr &amsg, const TemplateParserJob::Mode amode)
83  : mMsg(amsg)
84  , mMode(amode)
85 {
86  mEmptySource = new MimeTreeParser::SimpleObjectTreeSource;
87  mEmptySource->setDecryptMessage(mAllowDecryption);
88 
89  mOtp = new MimeTreeParser::ObjectTreeParser(mEmptySource);
90  mOtp->setAllowAsync(false);
91 }
92 
93 TemplateParserJobPrivate::~TemplateParserJobPrivate()
94 {
95  delete mEmptySource;
96  delete mOtp;
97 }
98 
99 void TemplateParserJobPrivate::setAllowDecryption(const bool allowDecryption)
100 {
101  mAllowDecryption = allowDecryption;
102  mEmptySource->setDecryptMessage(mAllowDecryption);
103 }
104 
105 TemplateParserJob::TemplateParserJob(const KMime::Message::Ptr &amsg, const Mode amode, QObject *parent)
106  : QObject(parent)
107  , d(new TemplateParserJobPrivate(amsg, amode))
108 {
109 }
110 
111 TemplateParserJob::~TemplateParserJob() = default;
112 
114 {
115  d->mSelection = selection;
116 }
117 
118 void TemplateParserJob::setAllowDecryption(const bool allowDecryption)
119 {
120  d->setAllowDecryption(allowDecryption);
121 }
122 
123 bool TemplateParserJob::shouldStripSignature() const
124 {
125  // Only strip the signature when replying, it should be preserved when forwarding
126  return (d->mMode == Reply || d->mMode == ReplyAll) && TemplateParserSettings::self()->stripSignature();
127 }
128 
130 {
131  d->m_identityManager = ident;
132 }
133 
135 {
136  d->mCharsets = charsets;
137 }
138 
139 int TemplateParserJob::parseQuotes(const QString &prefix, const QString &str, QString &quote)
140 {
141  int pos = prefix.length();
142  int len;
143  const int str_len = str.length();
144 
145  // Also allow the german lower double-quote sign as quote separator, not only
146  // the standard ASCII quote ("). This fixes bug 166728.
147  const QList<QChar> quoteChars = {QLatin1Char('"'), 0x201C};
148 
149  QChar prev(QChar::Null);
150 
151  pos++;
152  len = pos;
153 
154  while (pos < str_len) {
155  const QChar c = str[pos];
156 
157  pos++;
158  len++;
159 
160  if (!prev.isNull()) {
161  quote.append(c);
162  prev = QChar::Null;
163  } else {
164  if (c == QLatin1Char('\\')) {
165  prev = c;
166  } else if (quoteChars.contains(c)) {
167  break;
168  } else {
169  quote.append(c);
170  }
171  }
172  }
173 
174  return len;
175 }
176 
177 void TemplateParserJob::process(const KMime::Message::Ptr &aorig_msg, qint64 afolder)
178 {
179  if (aorig_msg == nullptr) {
180  qCDebug(TEMPLATEPARSER_LOG) << "aorig_msg == 0!";
181  Q_EMIT parsingDone(d->mForceCursorPosition);
182  deleteLater();
183  return;
184  }
185 
186  d->mOrigMsg = aorig_msg;
187  d->mFolder = afolder;
188  const QString tmpl = findTemplate();
189  if (tmpl.isEmpty()) {
190  Q_EMIT parsingDone(d->mForceCursorPosition);
191  deleteLater();
192  return;
193  }
194  processWithTemplate(tmpl);
195 }
196 
197 void TemplateParserJob::process(const QString &tmplName, const KMime::Message::Ptr &aorig_msg, qint64 afolder)
198 {
199  d->mForceCursorPosition = false;
200  d->mOrigMsg = aorig_msg;
201  d->mFolder = afolder;
202  const QString tmpl = findCustomTemplate(tmplName);
203  processWithTemplate(tmpl);
204 }
205 
206 void TemplateParserJob::processWithIdentity(uint uoid, const KMime::Message::Ptr &aorig_msg, qint64 afolder)
207 {
208  d->mIdentity = uoid;
209  process(aorig_msg, afolder);
210 }
211 
213 {
214  const auto subParts = messageTree->subParts();
215  for (const auto &mp : subParts) {
216  auto text = mp.dynamicCast<MimeTreeParser::TextMessagePart>();
217  const auto attach = mp.dynamicCast<MimeTreeParser::AttachmentMessagePart>();
218  if (text && !attach) {
219  // TextMessagePart can have several subparts cause of PGP inline, we search for the first part with content
220  const auto mpSubParts{mp->subParts()};
221  for (const auto &sub : mpSubParts) {
222  if (!sub->text().trimmed().isEmpty()) {
223  return sub;
224  }
225  }
226  return text;
227  } else if (const auto html = mp.dynamicCast<MimeTreeParser::HtmlMessagePart>()) {
228  return html;
229  } else if (const auto alternative = mp.dynamicCast<MimeTreeParser::AlternativeMessagePart>()) {
230  return alternative;
231  } else {
232  auto ret = toplevelTextNode(mp);
233  if (ret) {
234  return ret;
235  }
236  }
237  }
238  return {};
239 }
240 
241 void TemplateParserJob::processWithTemplate(const QString &tmpl)
242 {
243  d->mOtp->parseObjectTree(d->mOrigMsg.data());
244 
245  const auto mp = toplevelTextNode(d->mOtp->parsedPart());
246  if (!mp) {
247  qCWarning(TEMPLATEPARSER_LOG) << "Invalid message! mp is null ";
248  Q_EMIT parsingFailed();
249  return;
250  }
251 
252  QString plainText = mp->plaintextContent();
253  QString htmlElement;
254 
255  if (mp->isHtml()) {
256  htmlElement = d->mOtp->htmlContent();
257  if (plainText.isEmpty()) { // HTML-only mails
258  plainText = htmlElement;
259  }
260  } else { // plain mails only
261  QString htmlReplace = plainText.toHtmlEscaped();
262  htmlReplace.replace(QLatin1Char('\n'), QStringLiteral("<br />"));
263  htmlElement = QStringLiteral("<html><head></head><body>%1</body></html>\n").arg(htmlReplace);
264  }
265 
266  auto job = new TemplateParserExtractHtmlInfo(this);
267  connect(job, &TemplateParserExtractHtmlInfo::finished, this, &TemplateParserJob::slotExtractInfoDone);
268 
269  job->setHtmlForExtractingTextPlain(plainText);
270  job->setTemplate(tmpl);
271 
272  job->setHtmlForExtractionHeaderAndBody(htmlElement);
273  job->start();
274 }
275 
276 void TemplateParserJob::setReplyAsHtml(bool replyAsHtml)
277 {
278  d->mReplyAsHtml = replyAsHtml;
279 }
280 
281 void TemplateParserJob::slotExtractInfoDone(const TemplateParserExtractHtmlInfoResult &result)
282 {
283  d->mExtractHtmlInfoResult = result;
284  const QString tmpl = d->mExtractHtmlInfoResult.mTemplate;
285  const int tmpl_len = tmpl.length();
286  QString plainBody;
287  QString htmlBody;
288 
289  bool dnl = false;
290  auto definedLocale = QLocale();
291  for (int i = 0; i < tmpl_len; ++i) {
292  QChar c = tmpl[i];
293  // qCDebug(TEMPLATEPARSER_LOG) << "Next char: " << c;
294  if (c == QLatin1Char('%')) {
295  const QString cmd = tmpl.mid(i + 1);
296 
297  if (cmd.startsWith(QLatin1Char('-'))) {
298  // dnl
299  qCDebug(TEMPLATEPARSER_LOG) << "Command: -";
300  dnl = true;
301  i += 1;
302  } else if (cmd.startsWith(QLatin1String("REM="))) {
303  // comments
304  qCDebug(TEMPLATEPARSER_LOG) << "Command: REM=";
305  QString q;
306  const int len = parseQuotes(QStringLiteral("REM="), cmd, q);
307  i += len;
308  } else if (cmd.startsWith(QLatin1String("LANGUAGE="))) {
309  QString q;
310  const int len = parseQuotes(QStringLiteral("LANGUAGE="), cmd, q);
311  i += len;
312  if (!q.isEmpty()) {
313  definedLocale = QLocale(q);
314  }
315  } else if (cmd.startsWith(QLatin1String("DICTIONARYLANGUAGE="))) {
316  QString q;
317  const int len = parseQuotes(QStringLiteral("DICTIONARYLANGUAGE="), cmd, q);
318  i += len;
319  if (!q.isEmpty()) {
320  auto header = new KMime::Headers::Generic("X-KMail-Dictionary");
321  header->fromUnicodeString(q, "utf-8");
322  d->mMsg->setHeader(header);
323  }
324  } else if (cmd.startsWith(QLatin1String("INSERT=")) || cmd.startsWith(QLatin1String("PUT="))) {
325  QString q;
326  int len = 0;
327  if (cmd.startsWith(QLatin1String("INSERT="))) {
328  // insert content of specified file as is
329  qCDebug(TEMPLATEPARSER_LOG) << "Command: INSERT=";
330  len = parseQuotes(QStringLiteral("INSERT="), cmd, q);
331  } else {
332  // insert content of specified file as is
333  qCDebug(TEMPLATEPARSER_LOG) << "Command: PUT=";
334  len = parseQuotes(QStringLiteral("PUT="), cmd, q);
335  }
336  i += len;
337  QString path = KShell::tildeExpand(q);
338  QFileInfo finfo(path);
339  if (finfo.isRelative()) {
340  path = QDir::homePath() + QLatin1Char('/') + q;
341  }
342  QFile file(path);
343  if (file.open(QIODevice::ReadOnly)) {
344  const QByteArray content = file.readAll();
345  const QString str = QString::fromLocal8Bit(content.constData(), content.size());
346  plainBody.append(str);
347  const QString body = plainTextToHtml(str);
348  htmlBody.append(body);
349  } else if (d->mDebug) {
350  KMessageBox::error(nullptr, i18nc("@info", "Cannot insert content from file %1: %2", path, file.errorString()));
351  }
352  } else if (cmd.startsWith(QLatin1String("SYSTEM="))) {
353  // insert content of specified file as is
354  qCDebug(TEMPLATEPARSER_LOG) << "Command: SYSTEM=";
355  QString q;
356  const int len = parseQuotes(QStringLiteral("SYSTEM="), cmd, q);
357  i += len;
358  const QString pipe_cmd = q;
359  const QString str = pipe(pipe_cmd, QString());
360  plainBody.append(str);
361  const QString body = plainTextToHtml(str);
362  htmlBody.append(body);
363  } else if (cmd.startsWith(QLatin1String("QUOTEPIPE="))) {
364  // pipe message body through command and insert it as quotation
365  qCDebug(TEMPLATEPARSER_LOG) << "Command: QUOTEPIPE=";
366  QString q;
367  const int len = parseQuotes(QStringLiteral("QUOTEPIPE="), cmd, q);
368  i += len;
369  const QString pipe_cmd = q;
370  if (d->mOrigMsg) {
371  const QString plainStr = pipe(pipe_cmd, plainMessageText(shouldStripSignature(), NoSelectionAllowed));
372  QString plainQuote = quotedPlainText(plainStr);
373  if (plainQuote.endsWith(QLatin1Char('\n'))) {
374  plainQuote.chop(1);
375  }
376  plainBody.append(plainQuote);
377 
378  const QString htmlStr = pipe(pipe_cmd, htmlMessageText(shouldStripSignature(), NoSelectionAllowed));
379  const QString htmlQuote = quotedHtmlText(htmlStr);
380  htmlBody.append(htmlQuote);
381  }
382  } else if (cmd.startsWith(QLatin1String("QUOTE"))) {
383  qCDebug(TEMPLATEPARSER_LOG) << "Command: QUOTE";
384  i += strlen("QUOTE");
385  if (d->mOrigMsg) {
386  QString plainQuote = quotedPlainText(plainMessageText(shouldStripSignature(), SelectionAllowed));
387  if (plainQuote.endsWith(QLatin1Char('\n'))) {
388  plainQuote.chop(1);
389  }
390  plainBody.append(plainQuote);
391 
392  const QString htmlQuote = quotedHtmlText(htmlMessageText(shouldStripSignature(), SelectionAllowed));
393  htmlBody.append(htmlQuote);
394  }
395  } else if (cmd.startsWith(QLatin1String("FORCEDPLAIN"))) {
396  qCDebug(TEMPLATEPARSER_LOG) << "Command: FORCEDPLAIN";
397  d->mQuotes = ReplyAsPlain;
398  i += strlen("FORCEDPLAIN");
399  } else if (cmd.startsWith(QLatin1String("FORCEDHTML"))) {
400  qCDebug(TEMPLATEPARSER_LOG) << "Command: FORCEDHTML";
401  d->mQuotes = ReplyAsHtml;
402  i += strlen("FORCEDHTML");
403  } else if (cmd.startsWith(QLatin1String("QHEADERS"))) {
404  qCDebug(TEMPLATEPARSER_LOG) << "Command: QHEADERS";
405  i += strlen("QHEADERS");
406  if (d->mOrigMsg) {
408  QString plainQuote = quotedPlainText(headerStr);
409  if (plainQuote.endsWith(QLatin1Char('\n'))) {
410  plainQuote.chop(1);
411  }
412  plainBody.append(plainQuote);
413 
414  const QString htmlQuote = quotedHtmlText(headerStr);
415  const QString str = plainTextToHtml(htmlQuote);
416  htmlBody.append(str);
417  }
418  } else if (cmd.startsWith(QLatin1String("HEADERS"))) {
419  qCDebug(TEMPLATEPARSER_LOG) << "Command: HEADERS";
420  i += strlen("HEADERS");
421  if (d->mOrigMsg) {
423  plainBody.append(str);
424  const QString body = plainTextToHtml(str);
425  htmlBody.append(body);
426  }
427  } else if (cmd.startsWith(QLatin1String("TEXTPIPE="))) {
428  // pipe message body through command and insert it as is
429  qCDebug(TEMPLATEPARSER_LOG) << "Command: TEXTPIPE=";
430  QString q;
431  const int len = parseQuotes(QStringLiteral("TEXTPIPE="), cmd, q);
432  i += len;
433  const QString pipe_cmd = q;
434  if (d->mOrigMsg) {
435  const QString plainStr = pipe(pipe_cmd, plainMessageText(shouldStripSignature(), NoSelectionAllowed));
436  plainBody.append(plainStr);
437 
438  const QString htmlStr = pipe(pipe_cmd, htmlMessageText(shouldStripSignature(), NoSelectionAllowed));
439  htmlBody.append(htmlStr);
440  }
441  } else if (cmd.startsWith(QLatin1String("MSGPIPE="))) {
442  // pipe full message through command and insert result as is
443  qCDebug(TEMPLATEPARSER_LOG) << "Command: MSGPIPE=";
444  QString q;
445  const int len = parseQuotes(QStringLiteral("MSGPIPE="), cmd, q);
446  i += len;
447  if (d->mOrigMsg) {
448  const QString str = pipe(q, QString::fromLatin1(d->mOrigMsg->encodedContent()));
449  plainBody.append(str);
450 
451  const QString body = plainTextToHtml(str);
452  htmlBody.append(body);
453  }
454  } else if (cmd.startsWith(QLatin1String("BODYPIPE="))) {
455  // pipe message body generated so far through command and insert result as is
456  qCDebug(TEMPLATEPARSER_LOG) << "Command: BODYPIPE=";
457  QString q;
458  const int len = parseQuotes(QStringLiteral("BODYPIPE="), cmd, q);
459  i += len;
460  const QString pipe_cmd = q;
461  const QString plainStr = pipe(q, plainBody);
462  plainBody.append(plainStr);
463 
464  const QString htmlStr = pipe(pipe_cmd, htmlBody);
465  const QString body = plainTextToHtml(htmlStr);
466  htmlBody.append(body);
467  } else if (cmd.startsWith(QLatin1String("CLEARPIPE="))) {
468  // pipe message body generated so far through command and
469  // insert result as is replacing current body
470  qCDebug(TEMPLATEPARSER_LOG) << "Command: CLEARPIPE=";
471  QString q;
472  const int len = parseQuotes(QStringLiteral("CLEARPIPE="), cmd, q);
473  i += len;
474  const QString pipe_cmd = q;
475  const QString plainStr = pipe(pipe_cmd, plainBody);
476  plainBody = plainStr;
477 
478  const QString htmlStr = pipe(pipe_cmd, htmlBody);
479  htmlBody = htmlStr;
480 
481  auto header = new KMime::Headers::Generic("X-KMail-CursorPos");
482  header->fromUnicodeString(QString::number(0), "utf-8");
483  d->mMsg->setHeader(header);
484  } else if (cmd.startsWith(QLatin1String("TEXT"))) {
485  qCDebug(TEMPLATEPARSER_LOG) << "Command: TEXT";
486  i += strlen("TEXT");
487  if (d->mOrigMsg) {
488  const QString plainStr = plainMessageText(shouldStripSignature(), NoSelectionAllowed);
489  plainBody.append(plainStr);
490 
491  const QString htmlStr = htmlMessageText(shouldStripSignature(), NoSelectionAllowed);
492  htmlBody.append(htmlStr);
493  }
494  } else if (cmd.startsWith(QLatin1String("OTEXTSIZE"))) {
495  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTEXTSIZE";
496  i += strlen("OTEXTSIZE");
497  if (d->mOrigMsg) {
498  const QString str = QStringLiteral("%1").arg(d->mOrigMsg->body().length());
499  plainBody.append(str);
500  const QString body = plainTextToHtml(str);
501  htmlBody.append(body);
502  }
503  } else if (cmd.startsWith(QLatin1String("OTEXT"))) {
504  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTEXT";
505  i += strlen("OTEXT");
506  if (d->mOrigMsg) {
507  const QString plainStr = plainMessageText(shouldStripSignature(), NoSelectionAllowed);
508  plainBody.append(plainStr);
509 
510  const QString htmlStr = htmlMessageText(shouldStripSignature(), NoSelectionAllowed);
511  htmlBody.append(htmlStr);
512  }
513  } else if (cmd.startsWith(QLatin1String("OADDRESSEESADDR"))) {
514  qCDebug(TEMPLATEPARSER_LOG) << "Command: OADDRESSEESADDR";
515  i += strlen("OADDRESSEESADDR");
516  if (d->mOrigMsg) {
517  const QString to = d->mOrigMsg->to()->asUnicodeString();
518  const QString cc = d->mOrigMsg->cc()->asUnicodeString();
519  if (!to.isEmpty()) {
520  const QString toLine = i18nc("@item:intext email To", "To:") + QLatin1Char(' ') + to;
521  plainBody.append(toLine);
522  const QString body = plainTextToHtml(toLine);
523  htmlBody.append(body);
524  }
525  if (!to.isEmpty() && !cc.isEmpty()) {
526  plainBody.append(QLatin1Char('\n'));
527  const QString str = plainTextToHtml(QString(QLatin1Char('\n')));
528  htmlBody.append(str);
529  }
530  if (!cc.isEmpty()) {
531  const QString ccLine = i18nc("@item:intext email CC", "CC:") + QLatin1Char(' ') + cc;
532  plainBody.append(ccLine);
533  const QString str = plainTextToHtml(ccLine);
534  htmlBody.append(str);
535  }
536  }
537  } else if (cmd.startsWith(QLatin1String("CCADDR"))) {
538  qCDebug(TEMPLATEPARSER_LOG) << "Command: CCADDR";
539  i += strlen("CCADDR");
540  const QString str = d->mMsg->cc()->asUnicodeString();
541  plainBody.append(str);
542  const QString body = plainTextToHtml(str);
543  htmlBody.append(body);
544  } else if (cmd.startsWith(QLatin1String("CCNAME"))) {
545  qCDebug(TEMPLATEPARSER_LOG) << "Command: CCNAME";
546  i += strlen("CCNAME");
547  const QString str = d->mMsg->cc()->displayString();
548  plainBody.append(str);
549  const QString body = plainTextToHtml(str);
550  htmlBody.append(body);
551  } else if (cmd.startsWith(QLatin1String("CCFNAME"))) {
552  qCDebug(TEMPLATEPARSER_LOG) << "Command: CCFNAME";
553  i += strlen("CCFNAME");
554  const QString str = d->mMsg->cc()->displayString();
555  const QString firstNameFromEmail = TemplateParser::Util::getFirstNameFromEmail(str);
556  plainBody.append(firstNameFromEmail);
557  const QString body = plainTextToHtml(firstNameFromEmail);
558  htmlBody.append(body);
559  } else if (cmd.startsWith(QLatin1String("CCLNAME"))) {
560  qCDebug(TEMPLATEPARSER_LOG) << "Command: CCLNAME";
561  i += strlen("CCLNAME");
562  const QString str = d->mMsg->cc()->displayString();
563  plainBody.append(TemplateParser::Util::getLastNameFromEmail(str));
564  const QString body = plainTextToHtml(TemplateParser::Util::getLastNameFromEmail(str));
565  htmlBody.append(body);
566  } else if (cmd.startsWith(QLatin1String("TOADDR"))) {
567  qCDebug(TEMPLATEPARSER_LOG) << "Command: TOADDR";
568  i += strlen("TOADDR");
569  const QString str = d->mMsg->to()->asUnicodeString();
570  plainBody.append(str);
571  const QString body = plainTextToHtml(str);
572  htmlBody.append(body);
573  } else if (cmd.startsWith(QLatin1String("TONAME"))) {
574  qCDebug(TEMPLATEPARSER_LOG) << "Command: TONAME";
575  i += strlen("TONAME");
576  const QString str = (d->mMsg->to()->displayString());
577  plainBody.append(str);
578  const QString body = plainTextToHtml(str);
579  htmlBody.append(body);
580  } else if (cmd.startsWith(QLatin1String("TOFNAME"))) {
581  qCDebug(TEMPLATEPARSER_LOG) << "Command: TOFNAME";
582  i += strlen("TOFNAME");
583  const QString str = d->mMsg->to()->displayString();
584  const QString firstNameFromEmail = TemplateParser::Util::getFirstNameFromEmail(str);
585  plainBody.append(firstNameFromEmail);
586  const QString body = plainTextToHtml(firstNameFromEmail);
587  htmlBody.append(body);
588  } else if (cmd.startsWith(QLatin1String("TOLNAME"))) {
589  qCDebug(TEMPLATEPARSER_LOG) << "Command: TOLNAME";
590  i += strlen("TOLNAME");
591  const QString str = d->mMsg->to()->displayString();
592  plainBody.append(TemplateParser::Util::getLastNameFromEmail(str));
593  const QString body = plainTextToHtml(TemplateParser::Util::getLastNameFromEmail(str));
594  htmlBody.append(body);
595  } else if (cmd.startsWith(QLatin1String("TOLIST"))) {
596  qCDebug(TEMPLATEPARSER_LOG) << "Command: TOLIST";
597  i += strlen("TOLIST");
598  const QString str = d->mMsg->to()->asUnicodeString();
599  plainBody.append(str);
600  const QString body = plainTextToHtml(str);
601  htmlBody.append(body);
602  } else if (cmd.startsWith(QLatin1String("FROMADDR"))) {
603  qCDebug(TEMPLATEPARSER_LOG) << "Command: FROMADDR";
604  i += strlen("FROMADDR");
605  const QString str = d->mMsg->from()->asUnicodeString();
606  plainBody.append(str);
607  const QString body = plainTextToHtml(str);
608  htmlBody.append(body);
609  } else if (cmd.startsWith(QLatin1String("FROMNAME"))) {
610  qCDebug(TEMPLATEPARSER_LOG) << "Command: FROMNAME";
611  i += strlen("FROMNAME");
612  const QString str = d->mMsg->from()->displayString();
613  plainBody.append(str);
614  const QString body = plainTextToHtml(str);
615  htmlBody.append(body);
616  } else if (cmd.startsWith(QLatin1String("FROMFNAME"))) {
617  qCDebug(TEMPLATEPARSER_LOG) << "Command: FROMFNAME";
618  i += strlen("FROMFNAME");
619  const QString str = d->mMsg->from()->displayString();
620  const QString firstNameFromEmail = TemplateParser::Util::getFirstNameFromEmail(str);
621  plainBody.append(firstNameFromEmail);
622  const QString body = plainTextToHtml(firstNameFromEmail);
623  htmlBody.append(body);
624  } else if (cmd.startsWith(QLatin1String("FROMLNAME"))) {
625  qCDebug(TEMPLATEPARSER_LOG) << "Command: FROMLNAME";
626  i += strlen("FROMLNAME");
627  const QString str = d->mMsg->from()->displayString();
628  plainBody.append(TemplateParser::Util::getLastNameFromEmail(str));
629  const QString body = plainTextToHtml(TemplateParser::Util::getLastNameFromEmail(str));
630  htmlBody.append(body);
631  } else if (cmd.startsWith(QLatin1String("FULLSUBJECT")) || cmd.startsWith(QLatin1String("FULLSUBJ"))) {
632  if (cmd.startsWith(QLatin1String("FULLSUBJ"))) {
633  qCDebug(TEMPLATEPARSER_LOG) << "Command: FULLSUBJ";
634  i += strlen("FULLSUBJ");
635  } else {
636  qCDebug(TEMPLATEPARSER_LOG) << "Command: FULLSUBJECT";
637  i += strlen("FULLSUBJECT");
638  }
639  const QString str = d->mMsg->subject()->asUnicodeString();
640  plainBody.append(str);
641  const QString body = plainTextToHtml(str);
642  htmlBody.append(body);
643  } else if (cmd.startsWith(QLatin1String("MSGID"))) {
644  qCDebug(TEMPLATEPARSER_LOG) << "Command: MSGID";
645  i += strlen("MSGID");
646  const QString str = d->mMsg->messageID()->asUnicodeString();
647  plainBody.append(str);
648  const QString body = plainTextToHtml(str);
649  htmlBody.append(body);
650  } else if (cmd.startsWith(QLatin1String("OHEADER="))) {
651  // insert specified content of header from original message
652  qCDebug(TEMPLATEPARSER_LOG) << "Command: OHEADER=";
653  QString q;
654  const int len = parseQuotes(QStringLiteral("OHEADER="), cmd, q);
655  i += len;
656  if (d->mOrigMsg) {
657  const QString hdr = q;
658  QString str;
659  if (auto hrdMsgOrigin = d->mOrigMsg->headerByType(hdr.toLocal8Bit().constData())) {
660  str = hrdMsgOrigin->asUnicodeString();
661  }
662  plainBody.append(str);
663  const QString body = plainTextToHtml(str);
664  htmlBody.append(body);
665  }
666  } else if (cmd.startsWith(QLatin1String("HEADER="))) {
667  // insert specified content of header from current message
668  qCDebug(TEMPLATEPARSER_LOG) << "Command: HEADER=";
669  QString q;
670  const int len = parseQuotes(QStringLiteral("HEADER="), cmd, q);
671  i += len;
672  const QString hdr = q;
673  QString str;
674  if (auto hrdMsgOrigin = d->mOrigMsg->headerByType(hdr.toLocal8Bit().constData())) {
675  str = hrdMsgOrigin->asUnicodeString();
676  }
677  plainBody.append(str);
678  const QString body = plainTextToHtml(str);
679  htmlBody.append(body);
680  } else if (cmd.startsWith(QLatin1String("HEADER( "))) {
681  // insert specified content of header from current message
682  qCDebug(TEMPLATEPARSER_LOG) << "Command: HEADER(";
684  static QRegularExpression reg(QStringLiteral("^HEADER\\((.+)\\)"));
685  const int res = cmd.indexOf(reg, 0, &match);
686  if (res != 0) {
687  // something wrong
688  i += strlen("HEADER( ");
689  } else {
690  i += match.capturedLength(0); // length of HEADER(<space> + <space>)
691  const QString hdr = match.captured(1).trimmed();
692  QString str;
693  if (auto hrdMsgOrigin = d->mOrigMsg->headerByType(hdr.toLocal8Bit().constData())) {
694  str = hrdMsgOrigin->asUnicodeString();
695  }
696  plainBody.append(str);
697  const QString body = plainTextToHtml(str);
698  htmlBody.append(body);
699  }
700  } else if (cmd.startsWith(QLatin1String("OCCADDR"))) {
701  qCDebug(TEMPLATEPARSER_LOG) << "Command: OCCADDR";
702  i += strlen("OCCADDR");
703  if (d->mOrigMsg) {
704  const QString str = d->mOrigMsg->cc()->asUnicodeString();
705  plainBody.append(str);
706  const QString body = plainTextToHtml(str);
707  htmlBody.append(body);
708  }
709  } else if (cmd.startsWith(QLatin1String("OCCNAME"))) {
710  qCDebug(TEMPLATEPARSER_LOG) << "Command: OCCNAME";
711  i += strlen("OCCNAME");
712  if (d->mOrigMsg) {
713  const QString str = d->mOrigMsg->cc()->displayString();
714  plainBody.append(str);
715  const QString body = plainTextToHtml(str);
716  htmlBody.append(body);
717  }
718  } else if (cmd.startsWith(QLatin1String("OCCFNAME"))) {
719  qCDebug(TEMPLATEPARSER_LOG) << "Command: OCCFNAME";
720  i += strlen("OCCFNAME");
721  if (d->mOrigMsg) {
722  const QString str = d->mOrigMsg->cc()->displayString();
723  const QString firstNameFromEmail = TemplateParser::Util::getFirstNameFromEmail(str);
724  plainBody.append(firstNameFromEmail);
725  const QString body = plainTextToHtml(firstNameFromEmail);
726  htmlBody.append(body);
727  }
728  } else if (cmd.startsWith(QLatin1String("OCCLNAME"))) {
729  qCDebug(TEMPLATEPARSER_LOG) << "Command: OCCLNAME";
730  i += strlen("OCCLNAME");
731  if (d->mOrigMsg) {
732  const QString str = d->mOrigMsg->cc()->displayString();
733  plainBody.append(TemplateParser::Util::getLastNameFromEmail(str));
734  const QString body = plainTextToHtml(TemplateParser::Util::getLastNameFromEmail(str));
735  htmlBody.append(body);
736  }
737  } else if (cmd.startsWith(QLatin1String("OTOADDR"))) {
738  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTOADDR";
739  i += strlen("OTOADDR");
740  if (d->mOrigMsg) {
741  const QString str = d->mOrigMsg->to()->asUnicodeString();
742  plainBody.append(str);
743  const QString body = plainTextToHtml(str);
744  htmlBody.append(body);
745  }
746  } else if (cmd.startsWith(QLatin1String("OTONAME"))) {
747  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTONAME";
748  i += strlen("OTONAME");
749  if (d->mOrigMsg) {
750  const QString str = d->mOrigMsg->to()->displayString();
751  plainBody.append(str);
752  const QString body = plainTextToHtml(str);
753  htmlBody.append(body);
754  }
755  } else if (cmd.startsWith(QLatin1String("OTOFNAME"))) {
756  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTOFNAME";
757  i += strlen("OTOFNAME");
758  if (d->mOrigMsg) {
759  const QString str = d->mOrigMsg->to()->displayString();
760  const QString firstNameFromEmail = TemplateParser::Util::getFirstNameFromEmail(str);
761  plainBody.append(firstNameFromEmail);
762  const QString body = plainTextToHtml(firstNameFromEmail);
763  htmlBody.append(body);
764  }
765  } else if (cmd.startsWith(QLatin1String("OTOLNAME"))) {
766  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTOLNAME";
767  i += strlen("OTOLNAME");
768  if (d->mOrigMsg) {
769  const QString str = d->mOrigMsg->to()->displayString();
770  plainBody.append(TemplateParser::Util::getLastNameFromEmail(str));
771  const QString body = plainTextToHtml(TemplateParser::Util::getLastNameFromEmail(str));
772  htmlBody.append(body);
773  }
774  } else if (cmd.startsWith(QLatin1String("OTOLIST"))) {
775  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTOLIST";
776  i += strlen("OTOLIST");
777  if (d->mOrigMsg) {
778  const QString str = d->mOrigMsg->to()->asUnicodeString();
779  plainBody.append(str);
780  const QString body = plainTextToHtml(str);
781  htmlBody.append(body);
782  }
783  } else if (cmd.startsWith(QLatin1String("OTO"))) {
784  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTO";
785  i += strlen("OTO");
786  if (d->mOrigMsg) {
787  const QString str = d->mOrigMsg->to()->asUnicodeString();
788  plainBody.append(str);
789  const QString body = plainTextToHtml(str);
790  htmlBody.append(body);
791  }
792  } else if (cmd.startsWith(QLatin1String("OFROMADDR"))) {
793  qCDebug(TEMPLATEPARSER_LOG) << "Command: OFROMADDR";
794  i += strlen("OFROMADDR");
795  if (d->mOrigMsg) {
796  const QString str = d->mOrigMsg->from()->asUnicodeString();
797  plainBody.append(str);
798  const QString body = plainTextToHtml(str);
799  htmlBody.append(body);
800  }
801  } else if (cmd.startsWith(QLatin1String("OFROMNAME"))) {
802  qCDebug(TEMPLATEPARSER_LOG) << "Command: OFROMNAME";
803  i += strlen("OFROMNAME");
804  if (d->mOrigMsg) {
805  const QString str = d->mOrigMsg->from()->displayString();
806  plainBody.append(str);
807  const QString body = plainTextToHtml(str);
808  htmlBody.append(body);
809  }
810  } else if (cmd.startsWith(QLatin1String("OFROMFNAME"))) {
811  qCDebug(TEMPLATEPARSER_LOG) << "Command: OFROMFNAME";
812  i += strlen("OFROMFNAME");
813  if (d->mOrigMsg) {
814  const QString str = d->mOrigMsg->from()->displayString();
815  const QString firstNameFromEmail = TemplateParser::Util::getFirstNameFromEmail(str);
816  plainBody.append(firstNameFromEmail);
817  const QString body = plainTextToHtml(firstNameFromEmail);
818  htmlBody.append(body);
819  }
820  } else if (cmd.startsWith(QLatin1String("OFROMLNAME"))) {
821  qCDebug(TEMPLATEPARSER_LOG) << "Command: OFROMLNAME";
822  i += strlen("OFROMLNAME");
823  if (d->mOrigMsg) {
824  const QString str = d->mOrigMsg->from()->displayString();
825  plainBody.append(TemplateParser::Util::getLastNameFromEmail(str));
826  const QString body = plainTextToHtml(TemplateParser::Util::getLastNameFromEmail(str));
827  htmlBody.append(body);
828  }
829  } else if (cmd.startsWith(QLatin1String("OFULLSUBJECT")) || cmd.startsWith(QLatin1String("OFULLSUBJ"))) {
830  if (cmd.startsWith(QLatin1String("OFULLSUBJECT"))) {
831  qCDebug(TEMPLATEPARSER_LOG) << "Command: OFULLSUBJECT";
832  i += strlen("OFULLSUBJECT");
833  } else {
834  qCDebug(TEMPLATEPARSER_LOG) << "Command: OFULLSUBJ";
835  i += strlen("OFULLSUBJ");
836  }
837  if (d->mOrigMsg) {
838  const QString str = d->mOrigMsg->subject()->asUnicodeString();
839  plainBody.append(str);
840  const QString body = plainTextToHtml(str);
841  htmlBody.append(body);
842  }
843  } else if (cmd.startsWith(QLatin1String("OMSGID"))) {
844  qCDebug(TEMPLATEPARSER_LOG) << "Command: OMSGID";
845  i += strlen("OMSGID");
846  if (d->mOrigMsg) {
847  const QString str = d->mOrigMsg->messageID()->asUnicodeString();
848  plainBody.append(str);
849  const QString body = plainTextToHtml(str);
850  htmlBody.append(body);
851  }
852  } else if (cmd.startsWith(QLatin1String("DATEEN"))) {
853  qCDebug(TEMPLATEPARSER_LOG) << "Command: DATEEN";
854  i += strlen("DATEEN");
855  const QDateTime date = QDateTime::currentDateTime();
857  const QString str = locale.toString(date.date(), QLocale::LongFormat);
858  plainBody.append(str);
859  const QString body = plainTextToHtml(str);
860  htmlBody.append(body);
861  } else if (cmd.startsWith(QLatin1String("DATESHORT"))) {
862  qCDebug(TEMPLATEPARSER_LOG) << "Command: DATESHORT";
863  i += strlen("DATESHORT");
864  const QDateTime date = QDateTime::currentDateTime();
865  const QString str = definedLocale.toString(date.date(), QLocale::ShortFormat);
866  plainBody.append(str);
867  const QString body = plainTextToHtml(str);
868  htmlBody.append(body);
869  } else if (cmd.startsWith(QLatin1String("DATE"))) {
870  qCDebug(TEMPLATEPARSER_LOG) << "Command: DATE";
871  i += strlen("DATE");
872  const QDateTime date = QDateTime::currentDateTime();
873  const QString str = definedLocale.toString(date.date(), QLocale::LongFormat);
874  plainBody.append(str);
875  const QString body = plainTextToHtml(str);
876  htmlBody.append(body);
877  } else if (cmd.startsWith(QLatin1String("DOW"))) {
878  qCDebug(TEMPLATEPARSER_LOG) << "Command: DOW";
879  i += strlen("DOW");
880  const QDateTime date = QDateTime::currentDateTime();
881  const QString str = definedLocale.dayName(date.date().dayOfWeek(), QLocale::LongFormat);
882  plainBody.append(str);
883  const QString body = plainTextToHtml(str);
884  htmlBody.append(body);
885  } else if (cmd.startsWith(QLatin1String("TIMELONGEN"))) {
886  qCDebug(TEMPLATEPARSER_LOG) << "Command: TIMELONGEN";
887  i += strlen("TIMELONGEN");
888  const QDateTime date = QDateTime::currentDateTime();
890  const QString str = locale.toString(date.time(), QLocale::LongFormat);
891  plainBody.append(str);
892  const QString body = plainTextToHtml(str);
893  htmlBody.append(body);
894  } else if (cmd.startsWith(QLatin1String("TIMELONG"))) {
895  qCDebug(TEMPLATEPARSER_LOG) << "Command: TIMELONG";
896  i += strlen("TIMELONG");
897  const QDateTime date = QDateTime::currentDateTime();
898  const QString str = definedLocale.toString(date.time(), QLocale::LongFormat);
899  plainBody.append(str);
900  const QString body = plainTextToHtml(str);
901  htmlBody.append(body);
902  } else if (cmd.startsWith(QLatin1String("TIME"))) {
903  qCDebug(TEMPLATEPARSER_LOG) << "Command: TIME";
904  i += strlen("TIME");
905  const QDateTime date = QDateTime::currentDateTime();
906  const QString str = definedLocale.toString(date.time(), QLocale::ShortFormat);
907  plainBody.append(str);
908  const QString body = plainTextToHtml(str);
909  htmlBody.append(body);
910  } else if (cmd.startsWith(QLatin1String("ODATEEN"))) {
911  qCDebug(TEMPLATEPARSER_LOG) << "Command: ODATEEN";
912  i += strlen("ODATEEN");
913  if (d->mOrigMsg) {
914  const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime();
915  const QString str = QLocale::c().toString(date.date(), QLocale::LongFormat);
916  plainBody.append(str);
917  const QString body = plainTextToHtml(str);
918  htmlBody.append(body);
919  }
920  } else if (cmd.startsWith(QLatin1String("ODATESHORT"))) {
921  qCDebug(TEMPLATEPARSER_LOG) << "Command: ODATESHORT";
922  i += strlen("ODATESHORT");
923  if (d->mOrigMsg) {
924  const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime();
925  const QString str = definedLocale.toString(date.date(), QLocale::ShortFormat);
926  plainBody.append(str);
927  const QString body = plainTextToHtml(str);
928  htmlBody.append(body);
929  }
930  } else if (cmd.startsWith(QLatin1String("ODATE"))) {
931  qCDebug(TEMPLATEPARSER_LOG) << "Command: ODATE";
932  i += strlen("ODATE");
933  if (d->mOrigMsg) {
934  const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime();
935  const QString str = definedLocale.toString(date.date(), QLocale::LongFormat);
936  plainBody.append(str);
937  const QString body = plainTextToHtml(str);
938  htmlBody.append(body);
939  }
940  } else if (cmd.startsWith(QLatin1String("ODOW"))) {
941  qCDebug(TEMPLATEPARSER_LOG) << "Command: ODOW";
942  i += strlen("ODOW");
943  if (d->mOrigMsg) {
944  const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime();
945  const QString str = definedLocale.dayName(date.date().dayOfWeek(), QLocale::LongFormat);
946  plainBody.append(str);
947  const QString body = plainTextToHtml(str);
948  htmlBody.append(body);
949  }
950  } else if (cmd.startsWith(QLatin1String("OTIMELONGEN"))) {
951  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTIMELONGEN";
952  i += strlen("OTIMELONGEN");
953  if (d->mOrigMsg) {
954  const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime();
956  const QString str = locale.toString(date.time(), QLocale::LongFormat);
957  plainBody.append(str);
958  const QString body = plainTextToHtml(str);
959  htmlBody.append(body);
960  }
961  } else if (cmd.startsWith(QLatin1String("OTIMELONG"))) {
962  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTIMELONG";
963  i += strlen("OTIMELONG");
964  if (d->mOrigMsg) {
965  const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime();
966  const QString str = definedLocale.toString(date.time(), QLocale::LongFormat);
967  plainBody.append(str);
968  const QString body = plainTextToHtml(str);
969  htmlBody.append(body);
970  }
971  } else if (cmd.startsWith(QLatin1String("OTIME"))) {
972  qCDebug(TEMPLATEPARSER_LOG) << "Command: OTIME";
973  i += strlen("OTIME");
974  if (d->mOrigMsg) {
975  const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime();
976  const QString str = definedLocale.toString(date.time(), QLocale::ShortFormat);
977  plainBody.append(str);
978  const QString body = plainTextToHtml(str);
979  htmlBody.append(body);
980  }
981  } else if (cmd.startsWith(QLatin1String("BLANK"))) {
982  // do nothing
983  qCDebug(TEMPLATEPARSER_LOG) << "Command: BLANK";
984  i += strlen("BLANK");
985  } else if (cmd.startsWith(QLatin1String("NOP"))) {
986  // do nothing
987  qCDebug(TEMPLATEPARSER_LOG) << "Command: NOP";
988  i += strlen("NOP");
989  } else if (cmd.startsWith(QLatin1String("CLEAR"))) {
990  // clear body buffer; not too useful yet
991  qCDebug(TEMPLATEPARSER_LOG) << "Command: CLEAR";
992  i += strlen("CLEAR");
993  plainBody.clear();
994  htmlBody.clear();
995  auto header = new KMime::Headers::Generic("X-KMail-CursorPos");
996  header->fromUnicodeString(QString::number(0), "utf-8");
997  d->mMsg->setHeader(header);
998  } else if (cmd.startsWith(QLatin1String("DEBUGOFF"))) {
999  // turn off debug
1000  qCDebug(TEMPLATEPARSER_LOG) << "Command: DEBUGOFF";
1001  i += strlen("DEBUGOFF");
1002  d->mDebug = false;
1003  } else if (cmd.startsWith(QLatin1String("DEBUG"))) {
1004  // turn on debug
1005  qCDebug(TEMPLATEPARSER_LOG) << "Command: DEBUG";
1006  i += strlen("DEBUG");
1007  d->mDebug = true;
1008  } else if (cmd.startsWith(QLatin1String("CURSOR"))) {
1009  // turn on debug
1010  qCDebug(TEMPLATEPARSER_LOG) << "Command: CURSOR";
1011  int oldI = i;
1012  i += strlen("CURSOR");
1013  auto header = new KMime::Headers::Generic("X-KMail-CursorPos");
1014  header->fromUnicodeString(QString::number(plainBody.length()), "utf-8");
1015  /* if template is:
1016  * FOOBAR
1017  * %CURSOR
1018  *
1019  * Make sure there is an empty line for the cursor otherwise it will be placed at the end of FOOBAR
1020  */
1021  if (oldI > 0 && tmpl[oldI - 1] == QLatin1Char('\n') && i == tmpl_len - 1) {
1022  plainBody.append(QLatin1Char('\n'));
1023  }
1024  d->mMsg->setHeader(header);
1025  d->mForceCursorPosition = true;
1026  // FIXME HTML part for header remaining
1027  } else if (cmd.startsWith(QLatin1String("SIGNATURE"))) {
1028  qCDebug(TEMPLATEPARSER_LOG) << "Command: SIGNATURE";
1029  i += strlen("SIGNATURE");
1030  plainBody.append(getPlainSignature());
1031  htmlBody.append(getHtmlSignature());
1032  } else {
1033  // wrong command, do nothing
1034  plainBody.append(c);
1035  htmlBody.append(c);
1036  }
1037  } else if (dnl && (c == QLatin1Char('\n') || c == QLatin1Char('\r'))) {
1038  // skip
1039  if ((tmpl.size() > i + 1)
1040  && ((c == QLatin1Char('\n') && tmpl[i + 1] == QLatin1Char('\r')) || (c == QLatin1Char('\r') && tmpl[i + 1] == QLatin1Char('\n')))) {
1041  // skip one more
1042  i += 1;
1043  }
1044  dnl = false;
1045  } else {
1046  plainBody.append(c);
1047  if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
1048  htmlBody.append(QLatin1String("<br />"));
1049  htmlBody.append(c);
1050  if (tmpl.size() > i + 1
1051  && ((c == QLatin1Char('\n') && tmpl[i + 1] == QLatin1Char('\r')) || (c == QLatin1Char('\r') && tmpl[i + 1] == QLatin1Char('\n')))) {
1052  htmlBody.append(tmpl[i + 1]);
1053  plainBody.append(tmpl[i + 1]);
1054  i += 1;
1055  }
1056  } else {
1057  htmlBody.append(c);
1058  }
1059  }
1060  }
1061  // Clear the HTML body if FORCEDPLAIN has set ReplyAsPlain, OR if,
1062  // there is no use of FORCED command but a configure setting has ReplyUsingHtml disabled,
1063  // OR the original mail has no HTML part.
1064  const KMime::Content *content = d->mOrigMsg->mainBodyPart("text/html");
1065  if (d->mQuotes == ReplyAsPlain || (!d->mReplyAsHtml && TemplateParserSettings::self()->replyUsingVisualFormat())
1066  || !TemplateParserSettings::self()->replyUsingVisualFormat() || (!content || !content->hasContent())) {
1067  htmlBody.clear();
1068  } else {
1069  // qDebug() << "htmlBody********************* " << htmlBody;
1070  makeValidHtml(htmlBody);
1071  }
1072  if (d->mMode == NewMessage && plainBody.isEmpty() && !d->mExtractHtmlInfoResult.mPlainText.isEmpty()) {
1073  plainBody = d->mExtractHtmlInfoResult.mPlainText;
1074  }
1075 
1076  addProcessedBodyToMessage(plainBody, htmlBody);
1077  Q_EMIT parsingDone(d->mForceCursorPosition);
1078  deleteLater();
1079 }
1080 
1081 QString TemplateParserJob::getPlainSignature() const
1082 {
1083  const KIdentityManagement::Identity &identity = d->m_identityManager->identityForUoid(d->mIdentity);
1084 
1085  if (identity.isNull()) {
1086  return {};
1087  }
1088 
1089  KIdentityManagement::Signature signature = const_cast<KIdentityManagement::Identity &>(identity).signature();
1090 
1091  if (signature.type() == KIdentityManagement::Signature::Inlined && signature.isInlinedHtml()) {
1092  return signature.toPlainText();
1093  } else {
1094  return signature.rawText();
1095  }
1096 }
1097 
1098 // TODO If %SIGNATURE command is on, then override it with signature from
1099 // "KMail configure->General->identity->signature".
1100 // There should be no two signatures.
1101 QString TemplateParserJob::getHtmlSignature() const
1102 {
1103  const KIdentityManagement::Identity &identity = d->m_identityManager->identityForUoid(d->mIdentity);
1104  if (identity.isNull()) {
1105  return {};
1106  }
1107 
1108  KIdentityManagement::Signature signature = const_cast<KIdentityManagement::Identity &>(identity).signature();
1109 
1110  if (!signature.isInlinedHtml()) {
1111  signature = signature.rawText().toHtmlEscaped();
1112  return signature.rawText().replace(QLatin1Char('\n'), QStringLiteral("<br />"));
1113  }
1114  return signature.rawText();
1115 }
1116 
1117 void TemplateParserJob::addProcessedBodyToMessage(const QString &plainBody, const QString &htmlBody) const
1118 {
1120  ic.collectImagesFrom(d->mOrigMsg.data());
1121 
1122  // Now, delete the old content and set the new content, which
1123  // is either only the new text or the new text with some attachments.
1124  const auto parts = d->mMsg->contents();
1125  for (KMime::Content *content : parts) {
1126  d->mMsg->removeContent(content, true /*delete*/);
1127  }
1128 
1129  // Set To and CC from the template
1130  if (!d->mTo.isEmpty()) {
1131  d->mMsg->to()->fromUnicodeString(d->mMsg->to()->asUnicodeString() + QLatin1Char(',') + d->mTo, "utf-8");
1132  }
1133 
1134  if (!d->mCC.isEmpty()) {
1135  d->mMsg->cc()->fromUnicodeString(d->mMsg->cc()->asUnicodeString() + QLatin1Char(',') + d->mCC, "utf-8");
1136  }
1137 
1138  d->mMsg->contentType()->clear(); // to get rid of old boundary
1139 
1140  // const QByteArray boundary = KMime::multiPartBoundary();
1141  KMime::Content *const mainTextPart = htmlBody.isEmpty() ? createPlainPartContent(plainBody) : createMultipartAlternativeContent(plainBody, htmlBody);
1142  mainTextPart->assemble();
1143 
1144  KMime::Content *textPart = mainTextPart;
1145  if (!ic.images().empty()) {
1146  textPart = createMultipartRelated(ic, mainTextPart);
1147  textPart->assemble();
1148  }
1149 
1150  // If we have some attachments, create a multipart/mixed mail and
1151  // add the normal body as well as the attachments
1152  KMime::Content *mainPart = textPart;
1153  if (d->mMode == Forward) {
1154  auto attachments = d->mOrigMsg->attachments();
1155  attachments += d->mOtp->nodeHelper()->attachmentsOfExtraContents();
1156  if (!attachments.isEmpty()) {
1157  mainPart = createMultipartMixed(attachments, textPart);
1158  mainPart->assemble();
1159  }
1160  }
1161 
1162  d->mMsg->setBody(mainPart->encodedBody());
1163  d->mMsg->setHeader(mainPart->contentType());
1164  d->mMsg->setHeader(mainPart->contentTransferEncoding());
1165  d->mMsg->assemble();
1166  d->mMsg->parse();
1167 }
1168 
1169 KMime::Content *TemplateParserJob::createMultipartMixed(const QVector<KMime::Content *> &attachments, KMime::Content *textPart) const
1170 {
1171  auto mixedPart = new KMime::Content(d->mMsg.data());
1172  const QByteArray boundary = KMime::multiPartBoundary();
1173  auto contentType = mixedPart->contentType();
1174  contentType->setMimeType("multipart/mixed");
1175  contentType->setBoundary(boundary);
1176  mixedPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
1177  mixedPart->addContent(textPart);
1178 
1179  int attachmentNumber = 1;
1180  for (KMime::Content *attachment : attachments) {
1181  mixedPart->addContent(attachment);
1182  // If the content type has no name or filename parameter, add one, since otherwise the name
1183  // would be empty in the attachment view of the composer, which looks confusing
1184  if (auto ct = attachment->contentType(false)) {
1185  if (!ct->hasParameter(QStringLiteral("name")) && !ct->hasParameter(QStringLiteral("filename"))) {
1186  ct->setParameter(QStringLiteral("name"), i18nc("@item:intext", "Attachment %1", attachmentNumber));
1187  }
1188  }
1189  ++attachmentNumber;
1190  }
1191  return mixedPart;
1192 }
1193 
1194 KMime::Content *TemplateParserJob::createMultipartRelated(const MessageCore::ImageCollector &ic, KMime::Content *mainTextPart) const
1195 {
1196  auto relatedPart = new KMime::Content(d->mMsg.data());
1197  const QByteArray boundary = KMime::multiPartBoundary();
1198  auto contentType = relatedPart->contentType();
1199  contentType->setMimeType("multipart/related");
1200  contentType->setBoundary(boundary);
1201  relatedPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
1202  relatedPart->addContent(mainTextPart);
1203  for (KMime::Content *image : ic.images()) {
1204  qCWarning(TEMPLATEPARSER_LOG) << "Adding" << image->contentID() << "as an embedded image";
1205  relatedPart->addContent(image);
1206  }
1207  return relatedPart;
1208 }
1209 
1210 KMime::Content *TemplateParserJob::createPlainPartContent(const QString &plainBody) const
1211 {
1212  auto textPart = new KMime::Content(d->mMsg.data());
1213  auto ct = textPart->contentType(true);
1214  ct->setMimeType("text/plain");
1215  QTextCodec *charset = selectCharset(d->mCharsets, plainBody);
1216  ct->setCharset(charset->name());
1217  textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
1218  textPart->fromUnicodeString(plainBody);
1219  return textPart;
1220 }
1221 
1222 KMime::Content *TemplateParserJob::createMultipartAlternativeContent(const QString &plainBody, const QString &htmlBody) const
1223 {
1224  auto multipartAlternative = new KMime::Content(d->mMsg.data());
1225  multipartAlternative->contentType()->setMimeType("multipart/alternative");
1226  const QByteArray boundary = KMime::multiPartBoundary();
1227  multipartAlternative->contentType(false)->setBoundary(boundary); // Already created
1228 
1229  KMime::Content *textPart = createPlainPartContent(plainBody);
1230  multipartAlternative->addContent(textPart);
1231 
1232  auto htmlPart = new KMime::Content(d->mMsg.data());
1233  htmlPart->contentType(true)->setMimeType("text/html");
1234  QTextCodec *charset = selectCharset(d->mCharsets, htmlBody);
1235  htmlPart->contentType(false)->setCharset(charset->name()); // Already created
1236  htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
1237  htmlPart->fromUnicodeString(htmlBody);
1238  multipartAlternative->addContent(htmlPart);
1239 
1240  return multipartAlternative;
1241 }
1242 
1243 QString TemplateParserJob::findCustomTemplate(const QString &tmplName)
1244 {
1245  CTemplates t(tmplName);
1246  d->mTo = t.to();
1247  d->mCC = t.cC();
1248  const QString content = t.content();
1249  if (!content.isEmpty()) {
1250  return content;
1251  } else {
1252  return findTemplate();
1253  }
1254 }
1255 
1256 QString TemplateParserJob::findTemplate()
1257 {
1258  // qCDebug(TEMPLATEPARSER_LOG) << "Trying to find template for mode" << mode;
1259 
1260  QString tmpl;
1261 
1262  qCDebug(TEMPLATEPARSER_LOG) << "Folder identify found:" << d->mFolder;
1263  if (d->mFolder >= 0) { // only if a folder was found
1264  QString fid = QString::number(d->mFolder);
1265  Templates fconf(fid);
1266  if (fconf.useCustomTemplates()) { // does folder use custom templates?
1267  switch (d->mMode) {
1268  case NewMessage:
1269  tmpl = fconf.templateNewMessage();
1270  break;
1271  case Reply:
1272  tmpl = fconf.templateReply();
1273  break;
1274  case ReplyAll:
1275  tmpl = fconf.templateReplyAll();
1276  break;
1277  case Forward:
1278  tmpl = fconf.templateForward();
1279  break;
1280  default:
1281  qCDebug(TEMPLATEPARSER_LOG) << "Unknown message mode:" << d->mMode;
1282  return {};
1283  }
1284  d->mQuoteString = fconf.quoteString();
1285  if (!tmpl.isEmpty()) {
1286  return tmpl; // use folder-specific template
1287  }
1288  }
1289  }
1290 
1291  if (!d->mIdentity) { // find identity message belongs to
1292  d->mIdentity = identityUoid(d->mMsg);
1293  if (!d->mIdentity && d->mOrigMsg) {
1294  d->mIdentity = identityUoid(d->mOrigMsg);
1295  }
1296  d->mIdentity = d->m_identityManager->identityForUoidOrDefault(d->mIdentity).uoid();
1297  if (!d->mIdentity) {
1298  qCDebug(TEMPLATEPARSER_LOG) << "Oops! No identity for message";
1299  }
1300  }
1301  qCDebug(TEMPLATEPARSER_LOG) << "Identity found:" << d->mIdentity;
1302 
1303  QString iid;
1304  if (d->mIdentity) {
1305  iid = TemplatesConfiguration::configIdString(d->mIdentity); // templates ID for that identity
1306  } else {
1307  iid = QStringLiteral("IDENTITY_NO_IDENTITY"); // templates ID for no identity
1308  }
1309 
1310  Templates iconf(iid);
1311  if (iconf.useCustomTemplates()) { // does identity use custom templates?
1312  switch (d->mMode) {
1313  case NewMessage:
1314  tmpl = iconf.templateNewMessage();
1315  break;
1316  case Reply:
1317  tmpl = iconf.templateReply();
1318  break;
1319  case ReplyAll:
1320  tmpl = iconf.templateReplyAll();
1321  break;
1322  case Forward:
1323  tmpl = iconf.templateForward();
1324  break;
1325  default:
1326  qCDebug(TEMPLATEPARSER_LOG) << "Unknown message mode:" << d->mMode;
1327  return {};
1328  }
1329  d->mQuoteString = iconf.quoteString();
1330  if (!tmpl.isEmpty()) {
1331  return tmpl; // use identity-specific template
1332  }
1333  }
1334 
1335  switch (d->mMode) { // use the global template
1336  case NewMessage:
1337  tmpl = TemplateParserSettings::self()->templateNewMessage();
1338  break;
1339  case Reply:
1340  tmpl = TemplateParserSettings::self()->templateReply();
1341  break;
1342  case ReplyAll:
1343  tmpl = TemplateParserSettings::self()->templateReplyAll();
1344  break;
1345  case Forward:
1346  tmpl = TemplateParserSettings::self()->templateForward();
1347  break;
1348  default:
1349  qCDebug(TEMPLATEPARSER_LOG) << "Unknown message mode:" << d->mMode;
1350  return {};
1351  }
1352 
1353  d->mQuoteString = TemplateParserSettings::self()->quoteString();
1354  return tmpl;
1355 }
1356 
1357 QString TemplateParserJob::pipe(const QString &cmd, const QString &buf)
1358 {
1359  KProcess process;
1360  bool success;
1361 
1363  process.setShellCommand(cmd);
1364  process.start();
1365  if (process.waitForStarted(pipeTimeout())) {
1366  bool finished = false;
1367  if (!buf.isEmpty()) {
1368  process.write(buf.toLatin1());
1369  }
1370  if (buf.isEmpty() || process.waitForBytesWritten(pipeTimeout())) {
1371  if (!buf.isEmpty()) {
1372  process.closeWriteChannel();
1373  }
1374  if (process.waitForFinished(pipeTimeout())) {
1375  success = (process.exitStatus() == QProcess::NormalExit);
1376  finished = true;
1377  } else {
1378  finished = false;
1379  success = false;
1380  }
1381  } else {
1382  success = false;
1383  finished = false;
1384  }
1385 
1386  // The process has started, but did not finish in time. Kill it.
1387  if (!finished) {
1388  process.kill();
1389  }
1390  } else {
1391  success = false;
1392  }
1393 
1394  if (!success && d->mDebug) {
1395  KMessageBox::error(nullptr, xi18nc("@info", "Pipe command <command>%1</command> failed.", cmd));
1396  }
1397 
1398  if (success) {
1400  } else {
1401  return {};
1402  }
1403 }
1404 
1405 void TemplateParserJob::setWordWrap(bool wrap, int wrapColWidth)
1406 {
1407  d->mWrap = wrap;
1408  d->mColWrap = wrapColWidth;
1409 }
1410 
1411 QString TemplateParserJob::plainMessageText(bool aStripSignature, AllowSelection isSelectionAllowed) const
1412 {
1413  if (!d->mSelection.isEmpty() && (isSelectionAllowed == SelectionAllowed)) {
1414  return d->mSelection;
1415  }
1416 
1417  if (!d->mOrigMsg) {
1418  return {};
1419  }
1420  const auto mp = toplevelTextNode(d->mOtp->parsedPart());
1421  QString result = mp->plaintextContent();
1422  if (result.isEmpty()) {
1423  result = d->mExtractHtmlInfoResult.mPlainText;
1424  }
1425  if (aStripSignature) {
1426  result = MessageCore::StringUtil::stripSignature(result);
1427  }
1428 
1429  return result;
1430 }
1431 
1432 QString TemplateParserJob::htmlMessageText(bool aStripSignature, AllowSelection isSelectionAllowed)
1433 {
1434  if (!d->mSelection.isEmpty() && (isSelectionAllowed == SelectionAllowed)) {
1435  // TODO implement d->mSelection for HTML
1436  return d->mSelection;
1437  }
1438  d->mHeadElement = d->mExtractHtmlInfoResult.mHeaderElement;
1439  const QString bodyElement = d->mExtractHtmlInfoResult.mBodyElement;
1440  if (!bodyElement.isEmpty()) {
1441  if (aStripSignature) {
1442  // FIXME strip signature works partially for HTML mails
1443  return MessageCore::StringUtil::stripSignature(bodyElement);
1444  }
1445  return bodyElement;
1446  }
1447 
1448  if (aStripSignature) {
1449  // FIXME strip signature works partially for HTML mails
1450  return MessageCore::StringUtil::stripSignature(d->mExtractHtmlInfoResult.mHtmlElement);
1451  }
1452  return d->mExtractHtmlInfoResult.mHtmlElement;
1453 }
1454 
1455 QString TemplateParserJob::quotedPlainText(const QString &selection) const
1456 {
1457  QString content = TemplateParser::Util::removeSpaceAtBegin(selection);
1458 
1459  const QString indentStr = MessageCore::StringUtil::formatQuotePrefix(d->mQuoteString, d->mOrigMsg->from()->displayString());
1460  if (TemplateParserSettings::self()->smartQuote() && d->mWrap) {
1461  content = MessageCore::StringUtil::smartQuote(content, d->mColWrap - indentStr.length());
1462  }
1463  content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr);
1464  content.prepend(indentStr);
1465  content += QLatin1Char('\n');
1466 
1467  return content;
1468 }
1469 
1470 QString TemplateParserJob::quotedHtmlText(const QString &selection) const
1471 {
1472  QString content = selection;
1473  // TODO 1) look for all the variations of <br> and remove the blank lines
1474  // 2) implement vertical bar for quoted HTML mail.
1475  // 3) After vertical bar is implemented, If a user wants to edit quoted message,
1476  // then the <blockquote> tags below should open and close as when required.
1477 
1478  // Add blockquote tag, so that quoted message can be differentiated from normal message
1479  // Bug 419978 remove \n by <br>
1480  content = QLatin1String("<blockquote>") + content.replace(QStringLiteral("\n"), QStringLiteral("<br>")) + QLatin1String("</blockquote>");
1481  return content;
1482 }
1483 
1484 uint TemplateParserJob::identityUoid(const KMime::Message::Ptr &msg) const
1485 {
1486  QString idString;
1487  if (auto hrd = msg->headerByType("X-KMail-Identity")) {
1488  idString = hrd->asUnicodeString().trimmed();
1489  }
1490  bool ok = false;
1491  unsigned int id = idString.toUInt(&ok);
1492 
1493  if (!ok || id == 0) {
1494  id = d->m_identityManager->identityForAddress(msg->to()->asUnicodeString() + QLatin1String(", ") + msg->cc()->asUnicodeString()).uoid();
1495  }
1496 
1497  return id;
1498 }
1499 
1500 bool TemplateParserJob::isHtmlSignature() const
1501 {
1502  const KIdentityManagement::Identity &identity = d->m_identityManager->identityForUoid(d->mIdentity);
1503 
1504  if (identity.isNull()) {
1505  return false;
1506  }
1507 
1508  const KIdentityManagement::Signature signature = const_cast<KIdentityManagement::Identity &>(identity).signature();
1509 
1510  return signature.isInlinedHtml();
1511 }
1512 
1513 QString TemplateParserJob::plainTextToHtml(const QString &body)
1514 {
1515  QString str = body;
1516  str = str.toHtmlEscaped();
1517  str.replace(QLatin1Char('\n'), QStringLiteral("<br />\n"));
1518  return str;
1519 }
1520 
1521 void TemplateParserJob::makeValidHtml(QString &body)
1522 {
1523  if (body.isEmpty()) {
1524  return;
1525  }
1526 
1527  QRegularExpression regEx;
1528 
1529  regEx.setPattern(QStringLiteral("<html.*?>"));
1530  if (!body.contains(regEx)) {
1531  regEx.setPattern(QStringLiteral("<body.*?>"));
1532  if (!body.contains(regEx)) {
1533  body = QLatin1String("<body>") + body + QLatin1String("<br/></body>");
1534  }
1535  regEx.setPattern(QStringLiteral("<head.*?>"));
1536  if (!body.contains(regEx)) {
1537  body = QLatin1String("<head>") + d->mHeadElement + QLatin1String("</head>") + body;
1538  }
1539  body = QLatin1String("<html>") + body + QLatin1String("</html>");
1540  }
1541 }
Content * content(const ContentIndex &index) const
QByteArray encodedBody()
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString xi18nc(const char *context, const char *text, const TYPE &arg...)
QString number(int n, int base)
int size() const const
QString toHtmlEscaped() const const
bool waitForFinished(int msecs)
Q_EMITQ_EMIT
void setShellCommand(const QString &cmd)
QDateTime currentDateTime()
virtual bool waitForBytesWritten(int msecs) override
QTime time() const const
QString trimmed() const const
void clear()
A helper class to collect the embedded images of a email.
QTextCodec * codecForName(const QString &n, bool &ok) const
QString & prepend(QChar ch)
void chop(int n)
QString stripSignature(const QString &msg)
Strips the signature blocks from a message text.
Definition: stringutil.cpp:223
QByteArray readAllStandardOutput()
Parses messages and generates HTML display code out of them.
QByteArray toLatin1() const const
QString homePath()
static QString configIdString(uint id)
Returns the template configuration identifier string for a given identity.
void setMimeType(const QByteArray &mimeType)
void error(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
bool contains(const T &value) const const
void setOutputChannelMode(OutputChannelMode mode)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QTextCodec * codecForLocale()
KCharsets * charsets()
void setSelection(const QString &selection)
Sets the selection.
void start()
void deleteLater()
void setPattern(const QString &pattern)
void fromUnicodeString(const QString &s)
QString fromLocal8Bit(const char *str, int size)
void setWordWrap(bool wrap, int wrapColWidth=80)
Tell template parser whether or not to wrap words, and at what column to wrap at.
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
QLocale c()
QProcess::ExitStatus exitStatus() const const
bool isEmpty() const const
int length() const const
bool waitForStarted(int msecs)
void kill()
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 toString(qlonglong i) const const
KCOREADDONS_EXPORT QString tildeExpand(const QString &path)
Headers::ContentTransferEncoding * contentTransferEncoding(bool create=true)
void setEncoding(contentEncoding e)
QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
Convert quote wildcards into the final quote prefix.
Definition: stringutil.cpp:635
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
uint toUInt(bool *ok, int base) const const
QString & replace(int position, int n, QChar after)
void setCharsets(const QStringList &charsets)
Sets the list of charsets to try to use to encode the resulting text.
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
LocaleWrapper locale()
static KCharsets * charsets()
const char * constData() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString fromLatin1(const char *str, int size)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
const char * name(StandardAction id)
QDate date() const const
const std::vector< KMime::Content * > & images() const
Returns the collected images.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
int size() const const
bool hasContent() const
bool canEncode(QChar ch) const const
QByteArray toLocal8Bit() const const
Headers::ContentType * contentType(bool create=true)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
int dayOfWeek() const const
QString toUnicode(const QByteArray &a) const const
void collectImagesFrom(KMime::Content *content)
Starts collecting the images.
QString mid(int position, int n) const const
void setIdentityManager(KIdentityManagement::IdentityManager *ident)
Set the identity manager to be used when creating the template.
QString rawText(bool *ok=nullptr, QString *errorMessage=nullptr) const
QString & append(QChar ch)
void closeWriteChannel()
void setAllowDecryption(const bool allowDecryption)
Sets whether the template parser is allowed to decrypt the original message when needing its message ...
qint64 write(const char *data, qint64 maxSize)
virtual QByteArray name() const const=0
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Thu May 19 2022 03:53:26 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.