summaryrefslogtreecommitdiffstats
path: root/framework/src/domain/mime/mailtemplates.cpp
diff options
context:
space:
mode:
authorChristian Mollekopf <chrigi_1@fastmail.fm>2017-05-23 21:00:50 +0200
committerChristian Mollekopf <chrigi_1@fastmail.fm>2017-05-23 21:00:50 +0200
commit31bf3102fe8f8cdd3f1448f0f22f182d0c2820d2 (patch)
treeb5b508c3f065e0f51c8ce40aaf97d7070b5f9ef5 /framework/src/domain/mime/mailtemplates.cpp
parent1948369d4da2d0bc23b6af93683982b0e65d4992 (diff)
downloadkube-31bf3102fe8f8cdd3f1448f0f22f182d0c2820d2.tar.gz
kube-31bf3102fe8f8cdd3f1448f0f22f182d0c2820d2.zip
Moved MIME related stuff to a mime subdir
Diffstat (limited to 'framework/src/domain/mime/mailtemplates.cpp')
-rw-r--r--framework/src/domain/mime/mailtemplates.cpp840
1 files changed, 840 insertions, 0 deletions
diff --git a/framework/src/domain/mime/mailtemplates.cpp b/framework/src/domain/mime/mailtemplates.cpp
new file mode 100644
index 00000000..254dbba3
--- /dev/null
+++ b/framework/src/domain/mime/mailtemplates.cpp
@@ -0,0 +1,840 @@
1/*
2 Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
3 Copyright (c) 2010 Leo Franchi <lfranchi@kde.org>
4 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
5
6 This library is free software; you can redistribute it and/or modify it
7 under the terms of the GNU Library General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or (at your
9 option) any later version.
10
11 This library is distributed in the hope that it will be useful, but WITHOUT
12 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
14 License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301, USA.
20*/
21#include "mailtemplates.h"
22
23#include <functional>
24#include <QByteArray>
25#include <QList>
26#include <QDebug>
27#include <QWebEnginePage>
28#include <QWebEngineProfile>
29#include <QWebEngineSettings>
30#include <QWebEngineScript>
31#include <QSysInfo>
32#include <QTextCodec>
33
34#include <KCodecs/KCharsets>
35#include <KMime/Types>
36
37#include "stringhtmlwriter.h"
38#include "objecttreesource.h"
39
40#include <otp/objecttreeparser.h>
41
42namespace KMime {
43 namespace Types {
44static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right)
45{
46 return (left.asString() == right.asString());
47}
48
49static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right)
50{
51 return (left.addrSpec().asString() == right.addrSpec().asString());
52}
53 }
54}
55
56static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KMime::Types::AddrSpecList me)
57{
58 KMime::Types::Mailbox::List addresses(list);
59 for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) {
60 if (me.contains(it->addrSpec())) {
61 it = addresses.erase(it);
62 } else {
63 ++it;
64 }
65 }
66
67 return addresses;
68}
69
70void initHeader(const KMime::Message::Ptr &message)
71{
72 message->removeHeader<KMime::Headers::To>();
73 message->removeHeader<KMime::Headers::Subject>();
74 message->date()->setDateTime(QDateTime::currentDateTime());
75
76 const QStringList extraInfo = QStringList() << QSysInfo::prettyProductName();
77 message->userAgent()->fromUnicodeString(QString("%1/%2(%3)").arg(QString::fromLocal8Bit("Kube")).arg("0.1").arg(extraInfo.join(",")), "utf-8");
78 // This will allow to change Content-Type:
79 message->contentType()->setMimeType("text/plain");
80}
81
82QString replacePrefixes(const QString &str, const QStringList &prefixRegExps,
83 bool replace, const QString &newPrefix)
84{
85 bool recognized = false;
86 // construct a big regexp that
87 // 1. is anchored to the beginning of str (sans whitespace)
88 // 2. matches at least one of the part regexps in prefixRegExps
89 QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));
90 QRegExp rx(bigRegExp, Qt::CaseInsensitive);
91 if (rx.isValid()) {
92 QString tmp = str;
93 if (rx.indexIn(tmp) == 0) {
94 recognized = true;
95 if (replace) {
96 return tmp.replace(0, rx.matchedLength(), newPrefix + QLatin1String(" "));
97 }
98 }
99 } else {
100 qWarning() << "bigRegExp = \""
101 << bigRegExp << "\"\n"
102 << "prefix regexp is invalid!";
103 // try good ole Re/Fwd:
104 recognized = str.startsWith(newPrefix);
105 }
106
107 if (!recognized) {
108 return newPrefix + QLatin1String(" ") + str;
109 } else {
110 return str;
111 }
112}
113
114QString cleanSubject(const KMime::Message::Ptr &msg, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
115{
116 return replacePrefixes(msg->subject()->asUnicodeString(), prefixRegExps, replace, newPrefix);
117}
118
119QString forwardSubject(const KMime::Message::Ptr &msg)
120{
121 bool replaceForwardPrefix = true;
122 QStringList forwardPrefixes;
123 forwardPrefixes << "Fwd:";
124 forwardPrefixes << "FW:";
125 return cleanSubject(msg, forwardPrefixes, replaceForwardPrefix, QStringLiteral("Fwd:"));
126}
127
128QString replySubject(const KMime::Message::Ptr &msg)
129{
130 bool replaceReplyPrefix = true;
131 QStringList replyPrefixes;
132 //We're escaping the regex escape sequences. awesome
133 replyPrefixes << "Re\\\\s*:";
134 replyPrefixes << "Re[\\\\d+\\\\]:";
135 replyPrefixes << "Re\\\\d+:";
136 return cleanSubject(msg, replyPrefixes, replaceReplyPrefix, QStringLiteral("Re:"));
137}
138
139QByteArray getRefStr(const KMime::Message::Ptr &msg)
140{
141 QByteArray firstRef, lastRef, refStr, retRefStr;
142 int i, j;
143
144 if (auto hdr = msg->references(false)) {
145 refStr = hdr->as7BitString(false).trimmed();
146 }
147
148 if (refStr.isEmpty()) {
149 return msg->messageID()->as7BitString(false);
150 }
151
152 i = refStr.indexOf('<');
153 j = refStr.indexOf('>');
154 firstRef = refStr.mid(i, j - i + 1);
155 if (!firstRef.isEmpty()) {
156 retRefStr = firstRef + ' ';
157 }
158
159 i = refStr.lastIndexOf('<');
160 j = refStr.lastIndexOf('>');
161
162 lastRef = refStr.mid(i, j - i + 1);
163 if (!lastRef.isEmpty() && lastRef != firstRef) {
164 retRefStr += lastRef + ' ';
165 }
166
167 retRefStr += msg->messageID()->as7BitString(false);
168 return retRefStr;
169}
170
171KMime::Content *createPlainPartContent(const KMime::Message::Ptr &msg, const QString &plainBody)
172{
173 KMime::Content *textPart = new KMime::Content(msg.data());
174 textPart->contentType()->setMimeType("text/plain");
175 //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text
176 // QTextCodec *charset = selectCharset(m_charsets, plainBody);
177 // textPart->contentType()->setCharset(charset->name());
178 textPart->contentType()->setCharset("utf-8");
179 textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
180 textPart->fromUnicodeString(plainBody);
181 return textPart;
182}
183
184KMime::Content *createMultipartAlternativeContent(const KMime::Message::Ptr &msg, const QString &plainBody, const QString &htmlBody)
185{
186 KMime::Content *multipartAlternative = new KMime::Content(msg.data());
187 multipartAlternative->contentType()->setMimeType("multipart/alternative");
188 const QByteArray boundary = KMime::multiPartBoundary();
189 multipartAlternative->contentType()->setBoundary(boundary);
190
191 KMime::Content *textPart = createPlainPartContent(msg, plainBody);
192 multipartAlternative->addContent(textPart);
193
194 KMime::Content *htmlPart = new KMime::Content(msg.data());
195 htmlPart->contentType()->setMimeType("text/html");
196 //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text
197 // QTextCodec *charset = selectCharset(m_charsets, htmlBody);
198 // htmlPart->contentType()->setCharset(charset->name());
199 textPart->contentType()->setCharset("utf-8");
200 htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
201 htmlPart->fromUnicodeString(htmlBody);
202 multipartAlternative->addContent(htmlPart);
203
204 return multipartAlternative;
205}
206
207void addProcessedBodyToMessage(const KMime::Message::Ptr &msg, const QString &plainBody, const QString &htmlBody, bool forward)
208{
209 //FIXME
210 // MessageCore::ImageCollector ic;
211 // ic.collectImagesFrom(mOrigMsg.data());
212
213 // Now, delete the old content and set the new content, which
214 // is either only the new text or the new text with some attachments.
215 auto parts = msg->contents();
216 foreach (KMime::Content *content, parts) {
217 msg->removeContent(content, true/*delete*/);
218 }
219
220 msg->contentType()->clear(); // to get rid of old boundary
221
222 const QByteArray boundary = KMime::multiPartBoundary();
223 KMime::Content *const mainTextPart =
224 htmlBody.isEmpty() ?
225 createPlainPartContent(msg, plainBody) :
226 createMultipartAlternativeContent(msg, plainBody, htmlBody);
227 mainTextPart->assemble();
228
229 KMime::Content *textPart = mainTextPart;
230 // if (!ic.images().empty()) {
231 // textPart = createMultipartRelated(ic, mainTextPart);
232 // textPart->assemble();
233 // }
234
235 // If we have some attachments, create a multipart/mixed mail and
236 // add the normal body as well as the attachments
237 KMime::Content *mainPart = textPart;
238 //FIXME
239 // if (forward) {
240 // auto attachments = mOrigMsg->attachments();
241 // attachments += mOtp->nodeHelper()->attachmentsOfExtraContents();
242 // if (!attachments.isEmpty()) {
243 // mainPart = createMultipartMixed(attachments, textPart);
244 // mainPart->assemble();
245 // }
246 // }
247
248 msg->setBody(mainPart->encodedBody());
249 msg->setHeader(mainPart->contentType());
250 msg->setHeader(mainPart->contentTransferEncoding());
251 msg->assemble();
252 msg->parse();
253}
254
255QString plainToHtml(const QString &body)
256{
257 QString str = body;
258 str = str.toHtmlEscaped();
259 str.replace(QStringLiteral("\n"), QStringLiteral("<br />\n"));
260 return str;
261}
262
263//TODO implement this function using a DOM tree parser
264void makeValidHtml(QString &body, const QString &headElement)
265{
266 QRegExp regEx;
267 regEx.setMinimal(true);
268 regEx.setPattern(QStringLiteral("<html.*>"));
269
270 if (!body.isEmpty() && !body.contains(regEx)) {
271 regEx.setPattern(QStringLiteral("<body.*>"));
272 if (!body.contains(regEx)) {
273 body = QLatin1String("<body>") + body + QLatin1String("<br/></body>");
274 }
275 regEx.setPattern(QStringLiteral("<head.*>"));
276 if (!body.contains(regEx)) {
277 body = QLatin1String("<head>") + headElement + QLatin1String("</head>") + body;
278 }
279 body = QLatin1String("<html>") + body + QLatin1String("</html>");
280 }
281}
282
283//FIXME strip signature works partially for HTML mails
284QString stripSignature(const QString &msg)
285{
286 // Following RFC 3676, only > before --
287 // I prefer to not delete a SB instead of delete good mail content.
288 const QRegExp sbDelimiterSearch = QRegExp(QLatin1String("(^|\n)[> ]*-- \n"));
289 // The regular expression to look for prefix change
290 const QRegExp commonReplySearch = QRegExp(QLatin1String("^[ ]*>"));
291
292 QString res = msg;
293 int posDeletingStart = 1; // to start looking at 0
294
295 // While there are SB delimiters (start looking just before the deleted SB)
296 while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) {
297 QString prefix; // the current prefix
298 QString line; // the line to check if is part of the SB
299 int posNewLine = -1;
300
301 // Look for the SB beginning
302 int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart);
303 // The prefix before "-- "$
304 if (res.at(posDeletingStart) == QLatin1Char('\n')) {
305 ++posDeletingStart;
306 }
307
308 prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart);
309 posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1;
310
311 // now go to the end of the SB
312 while (posNewLine < res.size() && posNewLine > 0) {
313 // handle the undefined case for mid ( x , -n ) where n>1
314 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine);
315
316 if (nextPosNewLine < 0) {
317 nextPosNewLine = posNewLine - 1;
318 }
319
320 line = res.mid(posNewLine, nextPosNewLine - posNewLine);
321
322 // check when the SB ends:
323 // * does not starts with prefix or
324 // * starts with prefix+(any substring of prefix)
325 if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) ||
326 (!prefix.isEmpty() && line.startsWith(prefix) &&
327 line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) {
328 posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1;
329 } else {
330 break; // end of the SB
331 }
332 }
333
334 // remove the SB or truncate when is the last SB
335 if (posNewLine > 0) {
336 res.remove(posDeletingStart, posNewLine - posDeletingStart);
337 } else {
338 res.truncate(posDeletingStart);
339 }
340 }
341
342 return res;
343}
344
345void setupPage(QWebEnginePage *page)
346{
347 page->profile()->setHttpCacheType(QWebEngineProfile::MemoryHttpCache);
348 page->profile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
349 page->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
350 page->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
351 page->settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
352 page->settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, false);
353 page->settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
354 page->settings()->setAttribute(QWebEngineSettings::XSSAuditingEnabled, false);
355 page->settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
356 page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, false);
357 page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, false);
358 page->settings()->setAttribute(QWebEngineSettings::HyperlinkAuditingEnabled, false);
359 page->settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, false);
360 page->settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
361 page->settings()->setAttribute(QWebEngineSettings::WebGLEnabled, false);
362 page->settings()->setAttribute(QWebEngineSettings::AutoLoadIconsForPage, false);
363 page->settings()->setAttribute(QWebEngineSettings::Accelerated2dCanvasEnabled, false);
364 page->settings()->setAttribute(QWebEngineSettings::WebGLEnabled, false);
365
366#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
367 page->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
368 page->settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, false);
369#endif
370}
371
372void plainMessageText(const QString &plainTextContent, const QString &htmlContent, bool aStripSignature, const std::function<void(const QString &)> &callback)
373{
374 QString result = plainTextContent;
375 if (plainTextContent.isEmpty()) { //HTML-only mails
376 auto page = new QWebEnginePage;
377 setupPage(page);
378 page->setHtml(htmlContent);
379 page->toPlainText([=] (const QString &plaintext) {
380 page->deleteLater();
381 callback(plaintext);
382 });
383 return;
384 }
385
386 if (aStripSignature) {
387 result = stripSignature(result);
388 }
389 callback(result);
390}
391
392QString extractHeaderBodyScript()
393{
394 const QString source = QStringLiteral("(function() {"
395 "var res = {"
396 " body: document.getElementsByTagName('body')[0].innerHTML,"
397 " header: document.getElementsByTagName('head')[0].innerHTML"
398 "};"
399 "return res;"
400 "})()");
401 return source;
402}
403
404void htmlMessageText(const QString &plainTextContent, const QString &htmlContent, bool aStripSignature, const std::function<void(const QString &body, QString &head)> &callback)
405{
406 QString htmlElement = htmlContent;
407
408 if (htmlElement.isEmpty()) { //plain mails only
409 QString htmlReplace = plainTextContent.toHtmlEscaped();
410 htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("<br />"));
411 htmlElement = QStringLiteral("<html><head></head><body>%1</body></html>\n").arg(htmlReplace);
412 }
413
414 auto page = new QWebEnginePage;
415 setupPage(page);
416
417 page->setHtml(htmlElement);
418 page->runJavaScript(extractHeaderBodyScript(), QWebEngineScript::ApplicationWorld, [=](const QVariant &result){
419 page->deleteLater();
420 const QVariantMap map = result.toMap();
421 auto bodyElement = map.value(QStringLiteral("body")).toString();
422 auto headerElement = map.value(QStringLiteral("header")).toString();
423 if (!bodyElement.isEmpty()) {
424 if (aStripSignature) {
425 callback(stripSignature(bodyElement), headerElement);
426 }
427 return callback(bodyElement, headerElement);
428 }
429
430 if (aStripSignature) {
431 return callback(stripSignature(htmlElement), headerElement);
432 }
433 return callback(htmlElement, headerElement);
434 });
435}
436
437QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
438{
439 QString result;
440
441 if (wildString.isEmpty()) {
442 return wildString;
443 }
444
445 unsigned int strLength(wildString.length());
446 for (uint i = 0; i < strLength;) {
447 QChar ch = wildString[i++];
448 if (ch == QLatin1Char('%') && i < strLength) {
449 ch = wildString[i++];
450 switch (ch.toLatin1()) {
451 case 'f': { // sender's initals
452 if (fromDisplayString.isEmpty()) {
453 break;
454 }
455
456 uint j = 0;
457 const unsigned int strLength(fromDisplayString.length());
458 for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j)
459 ;
460 for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j)
461 ;
462 result += fromDisplayString[0];
463 if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) {
464 result += fromDisplayString[j];
465 } else if (strLength > 1) {
466 if (fromDisplayString[1] > QLatin1Char(' ')) {
467 result += fromDisplayString[1];
468 }
469 }
470 }
471 break;
472 case '_':
473 result += QLatin1Char(' ');
474 break;
475 case '%':
476 result += QLatin1Char('%');
477 break;
478 default:
479 result += QLatin1Char('%');
480 result += ch;
481 break;
482 }
483 } else {
484 result += ch;
485 }
486 }
487 return result;
488}
489
490QString quotedPlainText(const QString &selection, const QString &fromDisplayString)
491{
492 QString content = selection;
493 // Remove blank lines at the beginning:
494 const int firstNonWS = content.indexOf(QRegExp(QLatin1String("\\S")));
495 const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS);
496 if (lineStart >= 0) {
497 content.remove(0, static_cast<unsigned int>(lineStart));
498 }
499
500 const auto quoteString = QStringLiteral("> ");
501 const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString);
502 //FIXME
503 // if (TemplateParserSettings::self()->smartQuote() && mWrap) {
504 // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length());
505 // }
506 content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr);
507 content.prepend(indentStr);
508 content += QLatin1Char('\n');
509
510 return content;
511}
512
513QString quotedHtmlText(const QString &selection)
514{
515 QString content = selection;
516 //TODO 1) look for all the variations of <br> and remove the blank lines
517 //2) implement vertical bar for quoted HTML mail.
518 //3) After vertical bar is implemented, If a user wants to edit quoted message,
519 // then the <blockquote> tags below should open and close as when required.
520
521 //Add blockquote tag, so that quoted message can be differentiated from normal message
522 content = QLatin1String("<blockquote>") + content + QLatin1String("</blockquote>");
523 return content;
524}
525
526void applyCharset(const KMime::Message::Ptr msg, const KMime::Message::Ptr &origMsg)
527{
528 // first convert the body from its current encoding to unicode representation
529 QTextCodec *bodyCodec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
530 if (!bodyCodec) {
531 bodyCodec = KCharsets::charsets()->codecForName(QStringLiteral("UTF-8"));
532 }
533
534 const QString body = bodyCodec->toUnicode(msg->body());
535
536 // then apply the encoding of the original message
537 msg->contentType()->setCharset(origMsg->contentType()->charset());
538
539 QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
540 if (!codec) {
541 qCritical() << "Could not get text codec for charset" << msg->contentType()->charset();
542 } else if (!codec->canEncode(body)) { // charset can't encode body, fall back to preferred
543 const QStringList charsets /*= preferredCharsets() */;
544
545 QList<QByteArray> chars;
546 chars.reserve(charsets.count());
547 foreach (const QString &charset, charsets) {
548 chars << charset.toLatin1();
549 }
550
551 //FIXME
552 QByteArray fallbackCharset/* = selectCharset(chars, body)*/;
553 if (fallbackCharset.isEmpty()) { // UTF-8 as fall-through
554 fallbackCharset = "UTF-8";
555 }
556
557 codec = KCharsets::charsets()->codecForName(QString::fromLatin1(fallbackCharset));
558 msg->setBody(codec->fromUnicode(body));
559 } else {
560 msg->setBody(codec->fromUnicode(body));
561 }
562}
563
564enum ReplyStrategy {
565 ReplyList,
566 ReplySmart,
567 ReplyAll,
568 ReplyAuthor,
569 ReplyNone
570};
571
572void MailTemplates::reply(const KMime::Message::Ptr &origMsg, const std::function<void(const KMime::Message::Ptr &result)> &callback)
573{
574 //FIXME
575 const bool alwaysPlain = true;
576 //FIXME
577 const ReplyStrategy replyStrategy = ReplySmart;
578 KMime::Message::Ptr msg(new KMime::Message);
579 //FIXME
580 //Personal email addresses
581 KMime::Types::AddrSpecList me;
582 KMime::Types::Mailbox::List toList;
583 KMime::Types::Mailbox::List replyToList;
584 KMime::Types::Mailbox::List mailingListAddresses;
585
586 // const uint originalIdentity = identityUoid(origMsg);
587 initHeader(msg);
588 replyToList = origMsg->replyTo()->mailboxes();
589
590 msg->contentType()->setCharset("utf-8");
591
592 if (origMsg->headerByType("List-Post") &&
593 origMsg->headerByType("List-Post")->asUnicodeString().contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) {
594
595 const QString listPost = origMsg->headerByType("List-Post")->asUnicodeString();
596 QRegExp rx(QStringLiteral("<mailto:([^@>]+)@([^>]+)>"), Qt::CaseInsensitive);
597 if (rx.indexIn(listPost, 0) != -1) { // matched
598 KMime::Types::Mailbox mailbox;
599 mailbox.fromUnicodeString(rx.cap(1) + QLatin1Char('@') + rx.cap(2));
600 mailingListAddresses << mailbox;
601 }
602 }
603
604 switch (replyStrategy) {
605 case ReplySmart: {
606 if (auto hdr = origMsg->headerByType("Mail-Followup-To")) {
607 toList << KMime::Types::Mailbox::listFrom7BitString(hdr->as7BitString(false));
608 } else if (!replyToList.isEmpty()) {
609 toList = replyToList;
610 } else if (!mailingListAddresses.isEmpty()) {
611 toList = (KMime::Types::Mailbox::List() << mailingListAddresses.at(0));
612 } else {
613 // doesn't seem to be a mailing list, reply to From: address
614 toList = origMsg->from()->mailboxes();
615
616 bool listContainsMe = false;
617 for (const auto &m : me) {
618 KMime::Types::Mailbox mailbox;
619 mailbox.setAddress(m);
620 if (toList.contains(mailbox)) {
621 listContainsMe = true;
622 }
623 }
624 if (listContainsMe) {
625 // sender seems to be one of our own identities, so we assume that this
626 // is a reply to a "sent" mail where the users wants to add additional
627 // information for the recipient.
628 toList = origMsg->to()->mailboxes();
629 }
630 }
631 // strip all my addresses from the list of recipients
632 const KMime::Types::Mailbox::List recipients = toList;
633
634 toList = stripMyAddressesFromAddressList(recipients, me);
635
636 // ... unless the list contains only my addresses (reply to self)
637 if (toList.isEmpty() && !recipients.isEmpty()) {
638 toList << recipients.first();
639 }
640 }
641 break;
642 case ReplyList: {
643 if (auto hdr = origMsg->headerByType("Mail-Followup-To")) {
644 KMime::Types::Mailbox mailbox;
645 mailbox.from7BitString(hdr->as7BitString(false));
646 toList << mailbox;
647 } else if (!mailingListAddresses.isEmpty()) {
648 toList << mailingListAddresses[ 0 ];
649 } else if (!replyToList.isEmpty()) {
650 // assume a Reply-To header mangling mailing list
651 toList = replyToList;
652 }
653
654 //FIXME
655 // strip all my addresses from the list of recipients
656 const KMime::Types::Mailbox::List recipients = toList;
657 toList = stripMyAddressesFromAddressList(recipients, me);
658 }
659 break;
660 case ReplyAll: {
661 KMime::Types::Mailbox::List recipients;
662 KMime::Types::Mailbox::List ccRecipients;
663
664 // add addresses from the Reply-To header to the list of recipients
665 if (!replyToList.isEmpty()) {
666 recipients = replyToList;
667
668 // strip all possible mailing list addresses from the list of Reply-To addresses
669 foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) {
670 foreach (const KMime::Types::Mailbox &recipient, recipients) {
671 if (mailbox == recipient) {
672 recipients.removeAll(recipient);
673 }
674 }
675 }
676 }
677
678 if (!mailingListAddresses.isEmpty()) {
679 // this is a mailing list message
680 if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) {
681 // The sender didn't set a Reply-to address, so we add the From
682 // address to the list of CC recipients.
683 ccRecipients += origMsg->from()->mailboxes();
684 qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of CC recipients";
685 }
686
687 // if it is a mailing list, add the posting address
688 recipients.prepend(mailingListAddresses[ 0 ]);
689 } else {
690 // this is a normal message
691 if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) {
692 // in case of replying to a normal message only then add the From
693 // address to the list of recipients if there was no Reply-to address
694 recipients += origMsg->from()->mailboxes();
695 qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of recipients";
696 }
697 }
698
699 // strip all my addresses from the list of recipients
700 toList = stripMyAddressesFromAddressList(recipients, me);
701
702 // merge To header and CC header into a list of CC recipients
703 if (!origMsg->cc()->asUnicodeString().isEmpty() || !origMsg->to()->asUnicodeString().isEmpty()) {
704 KMime::Types::Mailbox::List list;
705 if (!origMsg->to()->asUnicodeString().isEmpty()) {
706 list += origMsg->to()->mailboxes();
707 }
708 if (!origMsg->cc()->asUnicodeString().isEmpty()) {
709 list += origMsg->cc()->mailboxes();
710 }
711
712 foreach (const KMime::Types::Mailbox &mailbox, list) {
713 if (!recipients.contains(mailbox) &&
714 !ccRecipients.contains(mailbox)) {
715 ccRecipients += mailbox;
716 qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients";
717 }
718 }
719 }
720
721 if (!ccRecipients.isEmpty()) {
722 // strip all my addresses from the list of CC recipients
723 ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me);
724
725 // in case of a reply to self, toList might be empty. if that's the case
726 // then propagate a cc recipient to To: (if there is any).
727 if (toList.isEmpty() && !ccRecipients.isEmpty()) {
728 toList << ccRecipients.at(0);
729 ccRecipients.pop_front();
730 }
731
732 foreach (const KMime::Types::Mailbox &mailbox, ccRecipients) {
733 msg->cc()->addAddress(mailbox);
734 }
735 }
736
737 if (toList.isEmpty() && !recipients.isEmpty()) {
738 // reply to self without other recipients
739 toList << recipients.at(0);
740 }
741 }
742 break;
743 case ReplyAuthor: {
744 if (!replyToList.isEmpty()) {
745 KMime::Types::Mailbox::List recipients = replyToList;
746
747 // strip the mailing list post address from the list of Reply-To
748 // addresses since we want to reply in private
749 foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) {
750 foreach (const KMime::Types::Mailbox &recipient, recipients) {
751 if (mailbox == recipient) {
752 recipients.removeAll(recipient);
753 }
754 }
755 }
756
757 if (!recipients.isEmpty()) {
758 toList = recipients;
759 } else {
760 // there was only the mailing list post address in the Reply-To header,
761 // so use the From address instead
762 toList = origMsg->from()->mailboxes();
763 }
764 } else if (!origMsg->from()->asUnicodeString().isEmpty()) {
765 toList = origMsg->from()->mailboxes();
766 }
767 }
768 break;
769 case ReplyNone:
770 // the addressees will be set by the caller
771 break;
772 }
773
774 foreach (const KMime::Types::Mailbox &mailbox, toList) {
775 msg->to()->addAddress(mailbox);
776 }
777
778 const QByteArray refStr = getRefStr(origMsg);
779 if (!refStr.isEmpty()) {
780 msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8");
781 }
782
783 //In-Reply-To = original msg-id
784 msg->inReplyTo()->from7BitString(origMsg->messageID()->as7BitString(false));
785
786 msg->subject()->fromUnicodeString(replySubject(origMsg), "utf-8");
787
788 auto definedLocale = QLocale::system();
789
790 //TODO set empty source instead
791 StringHtmlWriter htmlWriter;
792 MimeTreeParser::NodeHelper nodeHelper;
793 ObjectTreeSource source(&htmlWriter);
794 MimeTreeParser::ObjectTreeParser otp(&source, &nodeHelper);
795 otp.setAllowAsync(false);
796 otp.parseObjectTree(origMsg.data());
797
798 //Add quoted body
799 QString plainBody;
800 QString htmlBody;
801
802 //On $datetime you wrote:
803 const QDateTime date = origMsg->date()->dateTime();
804 const auto dateTimeString = QString("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat)).arg(definedLocale.toString(date.time(), QLocale::LongFormat));
805 const auto onDateYouWroteLine = QString("On %1 you wrote:").arg(dateTimeString);
806 plainBody.append(onDateYouWroteLine);
807 htmlBody.append(plainToHtml(onDateYouWroteLine));
808
809 //Strip signature for replies
810 const bool stripSignature = true;
811
812 const auto plainTextContent = otp.plainTextContent();
813 const auto htmlContent = otp.htmlContent();
814
815 plainMessageText(plainTextContent, htmlContent, stripSignature, [=] (const QString &body) {
816 //Quoted body
817 QString plainQuote = quotedPlainText(body, origMsg->from()->displayString());
818 if (plainQuote.endsWith(QLatin1Char('\n'))) {
819 plainQuote.chop(1);
820 }
821 //The plain body is complete
822 auto plainBodyResult = plainBody + plainQuote;
823 htmlMessageText(plainTextContent, htmlContent, stripSignature, [=] (const QString &body, const QString &headElement) {
824 //The html body is complete
825 auto htmlBodyResult = htmlBody + quotedHtmlText(body);
826 if (alwaysPlain) {
827 htmlBodyResult.clear();
828 } else {
829 makeValidHtml(htmlBodyResult, headElement);
830 }
831
832 //Assemble the message
833 addProcessedBodyToMessage(msg, plainBodyResult, htmlBodyResult, false);
834 applyCharset(msg, origMsg);
835 msg->assemble();
836 //We're done
837 callback(msg);
838 });
839 });
840}