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