diff options
Diffstat (limited to 'framework/domain/mailtemplates.cpp')
-rw-r--r-- | framework/domain/mailtemplates.cpp | 804 |
1 files changed, 804 insertions, 0 deletions
diff --git a/framework/domain/mailtemplates.cpp b/framework/domain/mailtemplates.cpp new file mode 100644 index 00000000..e5ee8533 --- /dev/null +++ b/framework/domain/mailtemplates.cpp | |||
@@ -0,0 +1,804 @@ | |||
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 <QByteArray> | ||
24 | #include <QList> | ||
25 | #include <QDebug> | ||
26 | #include <QImage> | ||
27 | #include <QWebPage> | ||
28 | #include <QWebFrame> | ||
29 | #include <QSysInfo> | ||
30 | #include <QTextCodec> | ||
31 | #include <QApplication> | ||
32 | |||
33 | #include <KCodecs/KCharsets> | ||
34 | #include <KMime/Types> | ||
35 | |||
36 | #include "stringhtmlwriter.h" | ||
37 | #include "objecttreesource.h" | ||
38 | #include "csshelper.h" | ||
39 | |||
40 | #include <MessageViewer/ObjectTreeParser> | ||
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 | QString stripSignature(const QString &msg) | ||
284 | { | ||
285 | // Following RFC 3676, only > before -- | ||
286 | // I prefer to not delete a SB instead of delete good mail content. | ||
287 | const QRegExp sbDelimiterSearch = QRegExp(QLatin1String("(^|\n)[> ]*-- \n")); | ||
288 | // The regular expression to look for prefix change | ||
289 | const QRegExp commonReplySearch = QRegExp(QLatin1String("^[ ]*>")); | ||
290 | |||
291 | QString res = msg; | ||
292 | int posDeletingStart = 1; // to start looking at 0 | ||
293 | |||
294 | // While there are SB delimiters (start looking just before the deleted SB) | ||
295 | while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) { | ||
296 | QString prefix; // the current prefix | ||
297 | QString line; // the line to check if is part of the SB | ||
298 | int posNewLine = -1; | ||
299 | |||
300 | // Look for the SB beginning | ||
301 | int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart); | ||
302 | // The prefix before "-- "$ | ||
303 | if (res.at(posDeletingStart) == QLatin1Char('\n')) { | ||
304 | ++posDeletingStart; | ||
305 | } | ||
306 | |||
307 | prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart); | ||
308 | posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1; | ||
309 | |||
310 | // now go to the end of the SB | ||
311 | while (posNewLine < res.size() && posNewLine > 0) { | ||
312 | // handle the undefined case for mid ( x , -n ) where n>1 | ||
313 | int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine); | ||
314 | |||
315 | if (nextPosNewLine < 0) { | ||
316 | nextPosNewLine = posNewLine - 1; | ||
317 | } | ||
318 | |||
319 | line = res.mid(posNewLine, nextPosNewLine - posNewLine); | ||
320 | |||
321 | // check when the SB ends: | ||
322 | // * does not starts with prefix or | ||
323 | // * starts with prefix+(any substring of prefix) | ||
324 | if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) || | ||
325 | (!prefix.isEmpty() && line.startsWith(prefix) && | ||
326 | line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) { | ||
327 | posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1; | ||
328 | } else { | ||
329 | break; // end of the SB | ||
330 | } | ||
331 | } | ||
332 | |||
333 | // remove the SB or truncate when is the last SB | ||
334 | if (posNewLine > 0) { | ||
335 | res.remove(posDeletingStart, posNewLine - posDeletingStart); | ||
336 | } else { | ||
337 | res.truncate(posDeletingStart); | ||
338 | } | ||
339 | } | ||
340 | |||
341 | return res; | ||
342 | } | ||
343 | |||
344 | QString plainMessageText(MessageViewer::ObjectTreeParser &otp, bool aStripSignature) | ||
345 | { | ||
346 | QString result = otp.plainTextContent(); | ||
347 | if (result.isEmpty()) { //HTML-only mails | ||
348 | QWebPage doc; | ||
349 | doc.mainFrame()->setHtml(otp.htmlContent()); | ||
350 | result = doc.mainFrame()->toPlainText(); | ||
351 | } | ||
352 | |||
353 | if (aStripSignature) { | ||
354 | result = stripSignature(result); | ||
355 | } | ||
356 | |||
357 | return result; | ||
358 | } | ||
359 | |||
360 | QString htmlMessageText(MessageViewer::ObjectTreeParser &otp, bool aStripSignature, QString &headElement) | ||
361 | { | ||
362 | QString htmlElement = otp.htmlContent(); | ||
363 | |||
364 | if (htmlElement.isEmpty()) { //plain mails only | ||
365 | QString htmlReplace = otp.plainTextContent().toHtmlEscaped(); | ||
366 | htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("<br />")); | ||
367 | htmlElement = QStringLiteral("<html><head></head><body>%1</body></html>\n").arg(htmlReplace); | ||
368 | } | ||
369 | |||
370 | //QWebPage relies on this | ||
371 | Q_ASSERT(QApplication::style()); | ||
372 | QWebPage page; | ||
373 | page.settings()->setAttribute(QWebSettings::JavascriptEnabled, false); | ||
374 | page.settings()->setAttribute(QWebSettings::JavaEnabled, false); | ||
375 | page.settings()->setAttribute(QWebSettings::PluginsEnabled, false); | ||
376 | page.settings()->setAttribute(QWebSettings::AutoLoadImages, false); | ||
377 | |||
378 | page.currentFrame()->setHtml(htmlElement); | ||
379 | |||
380 | //TODO to be tested/verified if this is not an issue | ||
381 | page.settings()->setAttribute(QWebSettings::JavascriptEnabled, true); | ||
382 | const QString bodyElement = page.currentFrame()->evaluateJavaScript( | ||
383 | QStringLiteral("document.getElementsByTagName('body')[0].innerHTML")).toString(); | ||
384 | |||
385 | headElement = page.currentFrame()->evaluateJavaScript( | ||
386 | QStringLiteral("document.getElementsByTagName('head')[0].innerHTML")).toString(); | ||
387 | |||
388 | page.settings()->setAttribute(QWebSettings::JavascriptEnabled, false); | ||
389 | |||
390 | if (!bodyElement.isEmpty()) { | ||
391 | if (aStripSignature) { | ||
392 | //FIXME strip signature works partially for HTML mails | ||
393 | return stripSignature(bodyElement); | ||
394 | } | ||
395 | return bodyElement; | ||
396 | } | ||
397 | |||
398 | if (aStripSignature) { | ||
399 | //FIXME strip signature works partially for HTML mails | ||
400 | return stripSignature(htmlElement); | ||
401 | } | ||
402 | return htmlElement; | ||
403 | } | ||
404 | |||
405 | QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString) | ||
406 | { | ||
407 | QString result; | ||
408 | |||
409 | if (wildString.isEmpty()) { | ||
410 | return wildString; | ||
411 | } | ||
412 | |||
413 | unsigned int strLength(wildString.length()); | ||
414 | for (uint i = 0; i < strLength;) { | ||
415 | QChar ch = wildString[i++]; | ||
416 | if (ch == QLatin1Char('%') && i < strLength) { | ||
417 | ch = wildString[i++]; | ||
418 | switch (ch.toLatin1()) { | ||
419 | case 'f': { // sender's initals | ||
420 | if (fromDisplayString.isEmpty()) { | ||
421 | break; | ||
422 | } | ||
423 | |||
424 | uint j = 0; | ||
425 | const unsigned int strLength(fromDisplayString.length()); | ||
426 | for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j) | ||
427 | ; | ||
428 | for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j) | ||
429 | ; | ||
430 | result += fromDisplayString[0]; | ||
431 | if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) { | ||
432 | result += fromDisplayString[j]; | ||
433 | } else if (strLength > 1) { | ||
434 | if (fromDisplayString[1] > QLatin1Char(' ')) { | ||
435 | result += fromDisplayString[1]; | ||
436 | } | ||
437 | } | ||
438 | } | ||
439 | break; | ||
440 | case '_': | ||
441 | result += QLatin1Char(' '); | ||
442 | break; | ||
443 | case '%': | ||
444 | result += QLatin1Char('%'); | ||
445 | break; | ||
446 | default: | ||
447 | result += QLatin1Char('%'); | ||
448 | result += ch; | ||
449 | break; | ||
450 | } | ||
451 | } else { | ||
452 | result += ch; | ||
453 | } | ||
454 | } | ||
455 | return result; | ||
456 | } | ||
457 | |||
458 | QString quotedPlainText(const QString &selection, const QString &fromDisplayString) | ||
459 | { | ||
460 | QString content = selection; | ||
461 | // Remove blank lines at the beginning: | ||
462 | const int firstNonWS = content.indexOf(QRegExp(QLatin1String("\\S"))); | ||
463 | const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS); | ||
464 | if (lineStart >= 0) { | ||
465 | content.remove(0, static_cast<unsigned int>(lineStart)); | ||
466 | } | ||
467 | |||
468 | const auto quoteString = QStringLiteral("> "); | ||
469 | const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString); | ||
470 | //FIXME | ||
471 | // if (TemplateParserSettings::self()->smartQuote() && mWrap) { | ||
472 | // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length()); | ||
473 | // } | ||
474 | content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr); | ||
475 | content.prepend(indentStr); | ||
476 | content += QLatin1Char('\n'); | ||
477 | |||
478 | return content; | ||
479 | } | ||
480 | |||
481 | QString quotedHtmlText(const QString &selection) | ||
482 | { | ||
483 | QString content = selection; | ||
484 | //TODO 1) look for all the variations of <br> and remove the blank lines | ||
485 | //2) implement vertical bar for quoted HTML mail. | ||
486 | //3) After vertical bar is implemented, If a user wants to edit quoted message, | ||
487 | // then the <blockquote> tags below should open and close as when required. | ||
488 | |||
489 | //Add blockquote tag, so that quoted message can be differentiated from normal message | ||
490 | content = QLatin1String("<blockquote>") + content + QLatin1String("</blockquote>"); | ||
491 | return content; | ||
492 | } | ||
493 | |||
494 | void applyCharset(const KMime::Message::Ptr msg, const KMime::Message::Ptr &origMsg) | ||
495 | { | ||
496 | // first convert the body from its current encoding to unicode representation | ||
497 | QTextCodec *bodyCodec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset())); | ||
498 | if (!bodyCodec) { | ||
499 | bodyCodec = KCharsets::charsets()->codecForName(QStringLiteral("UTF-8")); | ||
500 | } | ||
501 | |||
502 | const QString body = bodyCodec->toUnicode(msg->body()); | ||
503 | |||
504 | // then apply the encoding of the original message | ||
505 | msg->contentType()->setCharset(origMsg->contentType()->charset()); | ||
506 | |||
507 | QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset())); | ||
508 | if (!codec) { | ||
509 | qCritical() << "Could not get text codec for charset" << msg->contentType()->charset(); | ||
510 | } else if (!codec->canEncode(body)) { // charset can't encode body, fall back to preferred | ||
511 | const QStringList charsets /*= preferredCharsets() */; | ||
512 | |||
513 | QList<QByteArray> chars; | ||
514 | chars.reserve(charsets.count()); | ||
515 | foreach (const QString &charset, charsets) { | ||
516 | chars << charset.toLatin1(); | ||
517 | } | ||
518 | |||
519 | //FIXME | ||
520 | QByteArray fallbackCharset/* = selectCharset(chars, body)*/; | ||
521 | if (fallbackCharset.isEmpty()) { // UTF-8 as fall-through | ||
522 | fallbackCharset = "UTF-8"; | ||
523 | } | ||
524 | |||
525 | codec = KCharsets::charsets()->codecForName(QString::fromLatin1(fallbackCharset)); | ||
526 | msg->setBody(codec->fromUnicode(body)); | ||
527 | } else { | ||
528 | msg->setBody(codec->fromUnicode(body)); | ||
529 | } | ||
530 | } | ||
531 | |||
532 | enum ReplyStrategy { | ||
533 | ReplyList, | ||
534 | ReplySmart, | ||
535 | ReplyAll, | ||
536 | ReplyAuthor, | ||
537 | ReplyNone | ||
538 | }; | ||
539 | |||
540 | KMime::Message::Ptr MailTemplates::reply(const KMime::Message::Ptr &origMsg) | ||
541 | { | ||
542 | //FIXME | ||
543 | const bool alwaysPlain = true; | ||
544 | //FIXME | ||
545 | const ReplyStrategy replyStrategy = ReplySmart; | ||
546 | KMime::Message::Ptr msg(new KMime::Message); | ||
547 | //FIXME | ||
548 | //Personal email addresses | ||
549 | KMime::Types::AddrSpecList me; | ||
550 | KMime::Types::Mailbox::List toList; | ||
551 | KMime::Types::Mailbox::List replyToList; | ||
552 | KMime::Types::Mailbox::List mailingListAddresses; | ||
553 | |||
554 | // const uint originalIdentity = identityUoid(origMsg); | ||
555 | initHeader(msg); | ||
556 | replyToList = origMsg->replyTo()->mailboxes(); | ||
557 | |||
558 | msg->contentType()->setCharset("utf-8"); | ||
559 | |||
560 | if (origMsg->headerByType("List-Post") && | ||
561 | origMsg->headerByType("List-Post")->asUnicodeString().contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) { | ||
562 | |||
563 | const QString listPost = origMsg->headerByType("List-Post")->asUnicodeString(); | ||
564 | QRegExp rx(QStringLiteral("<mailto:([^@>]+)@([^>]+)>"), Qt::CaseInsensitive); | ||
565 | if (rx.indexIn(listPost, 0) != -1) { // matched | ||
566 | KMime::Types::Mailbox mailbox; | ||
567 | mailbox.fromUnicodeString(rx.cap(1) + QLatin1Char('@') + rx.cap(2)); | ||
568 | mailingListAddresses << mailbox; | ||
569 | } | ||
570 | } | ||
571 | |||
572 | switch (replyStrategy) { | ||
573 | case ReplySmart: { | ||
574 | if (auto hdr = origMsg->headerByType("Mail-Followup-To")) { | ||
575 | toList << KMime::Types::Mailbox::listFrom7BitString(hdr->as7BitString(false)); | ||
576 | } else if (!replyToList.isEmpty()) { | ||
577 | toList = replyToList; | ||
578 | } else if (!mailingListAddresses.isEmpty()) { | ||
579 | toList = (KMime::Types::Mailbox::List() << mailingListAddresses.at(0)); | ||
580 | } else { | ||
581 | // doesn't seem to be a mailing list, reply to From: address | ||
582 | toList = origMsg->from()->mailboxes(); | ||
583 | |||
584 | bool listContainsMe = false; | ||
585 | for (const auto &m : me) { | ||
586 | KMime::Types::Mailbox mailbox; | ||
587 | mailbox.setAddress(m); | ||
588 | if (toList.contains(mailbox)) { | ||
589 | listContainsMe = true; | ||
590 | } | ||
591 | } | ||
592 | if (listContainsMe) { | ||
593 | // sender seems to be one of our own identities, so we assume that this | ||
594 | // is a reply to a "sent" mail where the users wants to add additional | ||
595 | // information for the recipient. | ||
596 | toList = origMsg->to()->mailboxes(); | ||
597 | } | ||
598 | } | ||
599 | // strip all my addresses from the list of recipients | ||
600 | const KMime::Types::Mailbox::List recipients = toList; | ||
601 | |||
602 | toList = stripMyAddressesFromAddressList(recipients, me); | ||
603 | |||
604 | // ... unless the list contains only my addresses (reply to self) | ||
605 | if (toList.isEmpty() && !recipients.isEmpty()) { | ||
606 | toList << recipients.first(); | ||
607 | } | ||
608 | } | ||
609 | break; | ||
610 | case ReplyList: { | ||
611 | if (auto hdr = origMsg->headerByType("Mail-Followup-To")) { | ||
612 | KMime::Types::Mailbox mailbox; | ||
613 | mailbox.from7BitString(hdr->as7BitString(false)); | ||
614 | toList << mailbox; | ||
615 | } else if (!mailingListAddresses.isEmpty()) { | ||
616 | toList << mailingListAddresses[ 0 ]; | ||
617 | } else if (!replyToList.isEmpty()) { | ||
618 | // assume a Reply-To header mangling mailing list | ||
619 | toList = replyToList; | ||
620 | } | ||
621 | |||
622 | //FIXME | ||
623 | // strip all my addresses from the list of recipients | ||
624 | const KMime::Types::Mailbox::List recipients = toList; | ||
625 | toList = stripMyAddressesFromAddressList(recipients, me); | ||
626 | } | ||
627 | break; | ||
628 | case ReplyAll: { | ||
629 | KMime::Types::Mailbox::List recipients; | ||
630 | KMime::Types::Mailbox::List ccRecipients; | ||
631 | |||
632 | // add addresses from the Reply-To header to the list of recipients | ||
633 | if (!replyToList.isEmpty()) { | ||
634 | recipients = replyToList; | ||
635 | |||
636 | // strip all possible mailing list addresses from the list of Reply-To addresses | ||
637 | foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) { | ||
638 | foreach (const KMime::Types::Mailbox &recipient, recipients) { | ||
639 | if (mailbox == recipient) { | ||
640 | recipients.removeAll(recipient); | ||
641 | } | ||
642 | } | ||
643 | } | ||
644 | } | ||
645 | |||
646 | if (!mailingListAddresses.isEmpty()) { | ||
647 | // this is a mailing list message | ||
648 | if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) { | ||
649 | // The sender didn't set a Reply-to address, so we add the From | ||
650 | // address to the list of CC recipients. | ||
651 | ccRecipients += origMsg->from()->mailboxes(); | ||
652 | qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of CC recipients"; | ||
653 | } | ||
654 | |||
655 | // if it is a mailing list, add the posting address | ||
656 | recipients.prepend(mailingListAddresses[ 0 ]); | ||
657 | } else { | ||
658 | // this is a normal message | ||
659 | if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) { | ||
660 | // in case of replying to a normal message only then add the From | ||
661 | // address to the list of recipients if there was no Reply-to address | ||
662 | recipients += origMsg->from()->mailboxes(); | ||
663 | qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of recipients"; | ||
664 | } | ||
665 | } | ||
666 | |||
667 | // strip all my addresses from the list of recipients | ||
668 | toList = stripMyAddressesFromAddressList(recipients, me); | ||
669 | |||
670 | // merge To header and CC header into a list of CC recipients | ||
671 | if (!origMsg->cc()->asUnicodeString().isEmpty() || !origMsg->to()->asUnicodeString().isEmpty()) { | ||
672 | KMime::Types::Mailbox::List list; | ||
673 | if (!origMsg->to()->asUnicodeString().isEmpty()) { | ||
674 | list += origMsg->to()->mailboxes(); | ||
675 | } | ||
676 | if (!origMsg->cc()->asUnicodeString().isEmpty()) { | ||
677 | list += origMsg->cc()->mailboxes(); | ||
678 | } | ||
679 | |||
680 | foreach (const KMime::Types::Mailbox &mailbox, list) { | ||
681 | if (!recipients.contains(mailbox) && | ||
682 | !ccRecipients.contains(mailbox)) { | ||
683 | ccRecipients += mailbox; | ||
684 | qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients"; | ||
685 | } | ||
686 | } | ||
687 | } | ||
688 | |||
689 | if (!ccRecipients.isEmpty()) { | ||
690 | // strip all my addresses from the list of CC recipients | ||
691 | ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me); | ||
692 | |||
693 | // in case of a reply to self, toList might be empty. if that's the case | ||
694 | // then propagate a cc recipient to To: (if there is any). | ||
695 | if (toList.isEmpty() && !ccRecipients.isEmpty()) { | ||
696 | toList << ccRecipients.at(0); | ||
697 | ccRecipients.pop_front(); | ||
698 | } | ||
699 | |||
700 | foreach (const KMime::Types::Mailbox &mailbox, ccRecipients) { | ||
701 | msg->cc()->addAddress(mailbox); | ||
702 | } | ||
703 | } | ||
704 | |||
705 | if (toList.isEmpty() && !recipients.isEmpty()) { | ||
706 | // reply to self without other recipients | ||
707 | toList << recipients.at(0); | ||
708 | } | ||
709 | } | ||
710 | break; | ||
711 | case ReplyAuthor: { | ||
712 | if (!replyToList.isEmpty()) { | ||
713 | KMime::Types::Mailbox::List recipients = replyToList; | ||
714 | |||
715 | // strip the mailing list post address from the list of Reply-To | ||
716 | // addresses since we want to reply in private | ||
717 | foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) { | ||
718 | foreach (const KMime::Types::Mailbox &recipient, recipients) { | ||
719 | if (mailbox == recipient) { | ||
720 | recipients.removeAll(recipient); | ||
721 | } | ||
722 | } | ||
723 | } | ||
724 | |||
725 | if (!recipients.isEmpty()) { | ||
726 | toList = recipients; | ||
727 | } else { | ||
728 | // there was only the mailing list post address in the Reply-To header, | ||
729 | // so use the From address instead | ||
730 | toList = origMsg->from()->mailboxes(); | ||
731 | } | ||
732 | } else if (!origMsg->from()->asUnicodeString().isEmpty()) { | ||
733 | toList = origMsg->from()->mailboxes(); | ||
734 | } | ||
735 | } | ||
736 | break; | ||
737 | case ReplyNone: | ||
738 | // the addressees will be set by the caller | ||
739 | break; | ||
740 | } | ||
741 | |||
742 | foreach (const KMime::Types::Mailbox &mailbox, toList) { | ||
743 | msg->to()->addAddress(mailbox); | ||
744 | } | ||
745 | |||
746 | const QByteArray refStr = getRefStr(origMsg); | ||
747 | if (!refStr.isEmpty()) { | ||
748 | msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); | ||
749 | } | ||
750 | |||
751 | //In-Reply-To = original msg-id | ||
752 | msg->inReplyTo()->from7BitString(origMsg->messageID()->as7BitString(false)); | ||
753 | |||
754 | msg->subject()->fromUnicodeString(replySubject(origMsg), "utf-8"); | ||
755 | |||
756 | auto definedLocale = QLocale::system(); | ||
757 | |||
758 | //TODO set empty source instead | ||
759 | StringHtmlWriter htmlWriter; | ||
760 | QImage paintDevice; | ||
761 | CSSHelper cssHelper(&paintDevice); | ||
762 | MessageViewer::NodeHelper nodeHelper; | ||
763 | ObjectTreeSource source(&htmlWriter, &cssHelper); | ||
764 | MessageViewer::ObjectTreeParser otp(&source, &nodeHelper); | ||
765 | otp.setAllowAsync(false); | ||
766 | otp.parseObjectTree(origMsg.data()); | ||
767 | |||
768 | //Add quoted body | ||
769 | QString plainBody; | ||
770 | QString htmlBody; | ||
771 | |||
772 | //On $datetime you wrote: | ||
773 | const QDateTime date = origMsg->date()->dateTime(); | ||
774 | const auto dateTimeString = QString("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat)).arg(definedLocale.toString(date.time(), QLocale::LongFormat)); | ||
775 | const auto onDateYouWroteLine = QString("On %1 you wrote:").arg(dateTimeString); | ||
776 | plainBody.append(onDateYouWroteLine); | ||
777 | htmlBody.append(plainToHtml(onDateYouWroteLine)); | ||
778 | |||
779 | //Strip signature for replies | ||
780 | const bool stripSignature = true; | ||
781 | |||
782 | //Quoted body | ||
783 | QString plainQuote = quotedPlainText(plainMessageText(otp, stripSignature), origMsg->from()->displayString()); | ||
784 | if (plainQuote.endsWith(QLatin1Char('\n'))) { | ||
785 | plainQuote.chop(1); | ||
786 | } | ||
787 | plainBody.append(plainQuote); | ||
788 | QString headElement; | ||
789 | htmlBody.append(quotedHtmlText(htmlMessageText(otp, stripSignature, headElement))); | ||
790 | |||
791 | if (alwaysPlain) { | ||
792 | htmlBody.clear(); | ||
793 | } else { | ||
794 | makeValidHtml(htmlBody, headElement); | ||
795 | } | ||
796 | |||
797 | addProcessedBodyToMessage(msg, plainBody, htmlBody, false); | ||
798 | |||
799 | applyCharset(msg, origMsg); | ||
800 | |||
801 | msg->assemble(); | ||
802 | |||
803 | return msg; | ||
804 | } | ||