diff options
author | Christian Mollekopf <chrigi_1@fastmail.fm> | 2017-05-23 21:00:50 +0200 |
---|---|---|
committer | Christian Mollekopf <chrigi_1@fastmail.fm> | 2017-05-23 21:00:50 +0200 |
commit | 31bf3102fe8f8cdd3f1448f0f22f182d0c2820d2 (patch) | |
tree | b5b508c3f065e0f51c8ce40aaf97d7070b5f9ef5 /framework/src/domain/mime/mailtemplates.cpp | |
parent | 1948369d4da2d0bc23b6af93683982b0e65d4992 (diff) | |
download | kube-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.cpp | 840 |
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 | |||
42 | namespace KMime { | ||
43 | namespace Types { | ||
44 | static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right) | ||
45 | { | ||
46 | return (left.asString() == right.asString()); | ||
47 | } | ||
48 | |||
49 | static 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 | |||
56 | static 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 | |||
70 | void 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 | |||
82 | QString 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 | |||
114 | QString 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 | |||
119 | QString 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 | |||
128 | QString 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 | |||
139 | QByteArray 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 | |||
171 | KMime::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 | |||
184 | KMime::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 | |||
207 | void 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 | |||
255 | QString 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 | ||
264 | void 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 | ||
284 | QString 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 | |||
345 | void 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 | |||
372 | void 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 | |||
392 | QString 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 | |||
404 | void 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 | |||
437 | QString 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 | |||
490 | QString 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 | |||
513 | QString 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 | |||
526 | void 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 | |||
564 | enum ReplyStrategy { | ||
565 | ReplyList, | ||
566 | ReplySmart, | ||
567 | ReplyAll, | ||
568 | ReplyAuthor, | ||
569 | ReplyNone | ||
570 | }; | ||
571 | |||
572 | void 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 | } | ||