summaryrefslogtreecommitdiffstats
path: root/framework
diff options
context:
space:
mode:
authorChristian Mollekopf <chrigi_1@fastmail.fm>2016-03-01 00:08:45 +0100
committerChristian Mollekopf <chrigi_1@fastmail.fm>2016-03-01 00:08:45 +0100
commit235b8cb38214fe1ec919fce8701737d7b944c6de (patch)
tree92468fe9c9f939086d8dac6f01d36f4f71a40c95 /framework
parent0467b39e1ca034ec7298017e3055e352c755a386 (diff)
downloadkube-235b8cb38214fe1ec919fce8701737d7b944c6de.tar.gz
kube-235b8cb38214fe1ec919fce8701737d7b944c6de.zip
Support for mail replies
A template message is generated based on the input message, including appropriate recepients and quoted text. Encoding and and options are mostly hardcoded still, and there might be one or the other crash with HTML mails. Also image/attachment support is incomplete.
Diffstat (limited to 'framework')
-rw-r--r--framework/mail/CMakeLists.txt3
-rw-r--r--framework/mail/composer.cpp33
-rw-r--r--framework/mail/composer.h7
-rw-r--r--framework/mail/mailtemplates.cpp801
-rw-r--r--framework/mail/mailtemplates.h28
5 files changed, 869 insertions, 3 deletions
diff --git a/framework/mail/CMakeLists.txt b/framework/mail/CMakeLists.txt
index 29fda0e4..822b2981 100644
--- a/framework/mail/CMakeLists.txt
+++ b/framework/mail/CMakeLists.txt
@@ -13,6 +13,7 @@ set(mailplugin_SRCS
13 composer.cpp 13 composer.cpp
14 messageparser.cpp 14 messageparser.cpp
15 mailtransport.cpp 15 mailtransport.cpp
16 mailtemplates.cpp
16 retriever.cpp 17 retriever.cpp
17) 18)
18add_definitions(-DMAIL_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") 19add_definitions(-DMAIL_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data")
@@ -23,7 +24,7 @@ include_directories(${CURL_INCLUDE_DIRS})
23 24
24add_library(mailplugin SHARED ${mailplugin_SRCS}) 25add_library(mailplugin SHARED ${mailplugin_SRCS})
25 26
26qt5_use_modules(mailplugin Core Quick Qml) 27qt5_use_modules(mailplugin Core Quick Qml WebKitWidgets)
27 28
28target_link_libraries(mailplugin actionplugin settingsplugin sink KF5::Otp KF5::Codecs ${CURL_LIBRARIES}) 29target_link_libraries(mailplugin actionplugin settingsplugin sink KF5::Otp KF5::Codecs ${CURL_LIBRARIES})
29 30
diff --git a/framework/mail/composer.cpp b/framework/mail/composer.cpp
index 4ef112fa..9edc8345 100644
--- a/framework/mail/composer.cpp
+++ b/framework/mail/composer.cpp
@@ -27,6 +27,8 @@
27#include <QVariant> 27#include <QVariant>
28#include <QDebug> 28#include <QDebug>
29 29
30#include "mailtemplates.h"
31
30Composer::Composer(QObject *parent) : QObject(parent) 32Composer::Composer(QObject *parent) : QObject(parent)
31{ 33{
32 m_identityModel << "Kuberich <kuberich@kolabnow.com>" << "Uni <kuberich@university.edu>" << "Spam <hello.spam@spam.to>"; 34 m_identityModel << "Kuberich <kuberich@kolabnow.com>" << "Uni <kuberich@university.edu>" << "Spam <hello.spam@spam.to>";
@@ -115,9 +117,36 @@ void Composer::setFromIndex(int fromIndex)
115 } 117 }
116} 118}
117 119
120QVariant Composer::originalMessage() const
121{
122 return m_originalMessage;
123}
124
125void Composer::setOriginalMessage(const QVariant &originalMessage)
126{
127 const auto mailData = KMime::CRLFtoLF(originalMessage.toByteArray());
128 if (!mailData.isEmpty()) {
129 KMime::Message::Ptr mail(new KMime::Message);
130 mail->setContent(mailData);
131 mail->parse();
132 auto reply = MailTemplates::reply(mail);
133 //We assume reply
134 setTo(reply->to(true)->asUnicodeString());
135 setCc(reply->cc(true)->asUnicodeString());
136 setSubject(reply->subject(true)->asUnicodeString());
137 setBody(reply->body());
138 m_msg = QVariant::fromValue(reply);
139 } else {
140 m_msg = QVariant();
141 }
142}
143
118void Composer::send() 144void Composer::send()
119{ 145{
120 auto mail = KMime::Message::Ptr::create(); 146 auto mail = m_msg.value<KMime::Message::Ptr>();
147 if (!mail) {
148 mail = KMime::Message::Ptr::create();
149 }
121 for (const auto &to : KEmailAddress::splitAddressList(m_to)) { 150 for (const auto &to : KEmailAddress::splitAddressList(m_to)) {
122 QByteArray displayName; 151 QByteArray displayName;
123 QByteArray addrSpec; 152 QByteArray addrSpec;
@@ -159,4 +188,4 @@ void Composer::clear()
159 setCc(""); 188 setCc("");
160 setBcc(""); 189 setBcc("");
161 setFromIndex(-1); 190 setFromIndex(-1);
162} \ No newline at end of file 191}
diff --git a/framework/mail/composer.h b/framework/mail/composer.h
index bdb59840..ee38187f 100644
--- a/framework/mail/composer.h
+++ b/framework/mail/composer.h
@@ -22,10 +22,12 @@
22#include <QObject> 22#include <QObject>
23#include <QString> 23#include <QString>
24#include <QStringList> 24#include <QStringList>
25#include <QVariant>
25 26
26class Composer : public QObject 27class Composer : public QObject
27{ 28{
28 Q_OBJECT 29 Q_OBJECT
30 Q_PROPERTY (QVariant originalMessage READ originalMessage WRITE setOriginalMessage)
29 Q_PROPERTY (QString to READ to WRITE setTo NOTIFY toChanged) 31 Q_PROPERTY (QString to READ to WRITE setTo NOTIFY toChanged)
30 Q_PROPERTY (QString cc READ cc WRITE setCc NOTIFY ccChanged) 32 Q_PROPERTY (QString cc READ cc WRITE setCc NOTIFY ccChanged)
31 Q_PROPERTY (QString bcc READ bcc WRITE setBcc NOTIFY bccChanged) 33 Q_PROPERTY (QString bcc READ bcc WRITE setBcc NOTIFY bccChanged)
@@ -57,6 +59,9 @@ public:
57 int fromIndex() const; 59 int fromIndex() const;
58 void setFromIndex(int fromIndex); 60 void setFromIndex(int fromIndex);
59 61
62 QVariant originalMessage() const;
63 void setOriginalMessage(const QVariant &originalMessage);
64
60signals: 65signals:
61 void subjectChanged(); 66 void subjectChanged();
62 void bodyChanged(); 67 void bodyChanged();
@@ -78,4 +83,6 @@ private:
78 QString m_body; 83 QString m_body;
79 QStringList m_identityModel; 84 QStringList m_identityModel;
80 int m_fromIndex; 85 int m_fromIndex;
86 QVariant m_originalMessage;
87 QVariant m_msg;
81}; 88};
diff --git a/framework/mail/mailtemplates.cpp b/framework/mail/mailtemplates.cpp
new file mode 100644
index 00000000..7cbd887f
--- /dev/null
+++ b/framework/mail/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
32#include <KCodecs/KCharsets>
33#include <KMime/Types>
34
35#include "stringhtmlwriter.h"
36#include "objecttreesource.h"
37#include "csshelper.h"
38
39#include <MessageViewer/ObjectTreeParser>
40
41namespace KMime {
42 namespace Types {
43static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right)
44{
45 return (left.asString() == right.asString());
46}
47
48static 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
55static 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
69void 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
81QString 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
113QString 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
118QString 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
127QString 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
138QByteArray 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
170KMime::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
183KMime::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
206void 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
254QString 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
263void 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
282QString 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
343QString plainMessageText(MessageViewer::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
359QString htmlMessageText(MessageViewer::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 page;
370 page.settings()->setAttribute(QWebSettings::JavascriptEnabled, false);
371 page.settings()->setAttribute(QWebSettings::JavaEnabled, false);
372 page.settings()->setAttribute(QWebSettings::PluginsEnabled, false);
373 page.settings()->setAttribute(QWebSettings::AutoLoadImages, false);
374
375 page.currentFrame()->setHtml(htmlElement);
376
377 //TODO to be tested/verified if this is not an issue
378 page.settings()->setAttribute(QWebSettings::JavascriptEnabled, true);
379 const QString bodyElement = page.currentFrame()->evaluateJavaScript(
380 QStringLiteral("document.getElementsByTagName('body')[0].innerHTML")).toString();
381
382 headElement = page.currentFrame()->evaluateJavaScript(
383 QStringLiteral("document.getElementsByTagName('head')[0].innerHTML")).toString();
384
385 page.settings()->setAttribute(QWebSettings::JavascriptEnabled, false);
386
387 if (!bodyElement.isEmpty()) {
388 if (aStripSignature) {
389 //FIXME strip signature works partially for HTML mails
390 return stripSignature(bodyElement);
391 }
392 return bodyElement;
393 }
394
395 if (aStripSignature) {
396 //FIXME strip signature works partially for HTML mails
397 return stripSignature(htmlElement);
398 }
399 return htmlElement;
400}
401
402QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
403{
404 QString result;
405
406 if (wildString.isEmpty()) {
407 return wildString;
408 }
409
410 unsigned int strLength(wildString.length());
411 for (uint i = 0; i < strLength;) {
412 QChar ch = wildString[i++];
413 if (ch == QLatin1Char('%') && i < strLength) {
414 ch = wildString[i++];
415 switch (ch.toLatin1()) {
416 case 'f': { // sender's initals
417 if (fromDisplayString.isEmpty()) {
418 break;
419 }
420
421 uint j = 0;
422 const unsigned int strLength(fromDisplayString.length());
423 for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j)
424 ;
425 for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j)
426 ;
427 result += fromDisplayString[0];
428 if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) {
429 result += fromDisplayString[j];
430 } else if (strLength > 1) {
431 if (fromDisplayString[1] > QLatin1Char(' ')) {
432 result += fromDisplayString[1];
433 }
434 }
435 }
436 break;
437 case '_':
438 result += QLatin1Char(' ');
439 break;
440 case '%':
441 result += QLatin1Char('%');
442 break;
443 default:
444 result += QLatin1Char('%');
445 result += ch;
446 break;
447 }
448 } else {
449 result += ch;
450 }
451 }
452 return result;
453}
454
455QString quotedPlainText(const QString &selection, const QString &fromDisplayString)
456{
457 QString content = selection;
458 // Remove blank lines at the beginning:
459 const int firstNonWS = content.indexOf(QRegExp(QLatin1String("\\S")));
460 const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS);
461 if (lineStart >= 0) {
462 content.remove(0, static_cast<unsigned int>(lineStart));
463 }
464
465 const auto quoteString = QStringLiteral("> ");
466 const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString);
467 //FIXME
468 // if (TemplateParserSettings::self()->smartQuote() && mWrap) {
469 // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length());
470 // }
471 content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr);
472 content.prepend(indentStr);
473 content += QLatin1Char('\n');
474
475 return content;
476}
477
478QString quotedHtmlText(const QString &selection)
479{
480 QString content = selection;
481 //TODO 1) look for all the variations of <br> and remove the blank lines
482 //2) implement vertical bar for quoted HTML mail.
483 //3) After vertical bar is implemented, If a user wants to edit quoted message,
484 // then the <blockquote> tags below should open and close as when required.
485
486 //Add blockquote tag, so that quoted message can be differentiated from normal message
487 content = QLatin1String("<blockquote>") + content + QLatin1String("</blockquote>");
488 return content;
489}
490
491void applyCharset(const KMime::Message::Ptr msg, const KMime::Message::Ptr &origMsg)
492{
493 // first convert the body from its current encoding to unicode representation
494 QTextCodec *bodyCodec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
495 if (!bodyCodec) {
496 bodyCodec = KCharsets::charsets()->codecForName(QStringLiteral("UTF-8"));
497 }
498
499 const QString body = bodyCodec->toUnicode(msg->body());
500
501 // then apply the encoding of the original message
502 msg->contentType()->setCharset(origMsg->contentType()->charset());
503
504 QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
505 if (!codec) {
506 qCritical() << "Could not get text codec for charset" << msg->contentType()->charset();
507 } else if (!codec->canEncode(body)) { // charset can't encode body, fall back to preferred
508 const QStringList charsets /*= preferredCharsets() */;
509
510 QList<QByteArray> chars;
511 chars.reserve(charsets.count());
512 foreach (const QString &charset, charsets) {
513 chars << charset.toLatin1();
514 }
515
516 //FIXME
517 QByteArray fallbackCharset/* = selectCharset(chars, body)*/;
518 if (fallbackCharset.isEmpty()) { // UTF-8 as fall-through
519 fallbackCharset = "UTF-8";
520 }
521
522 codec = KCharsets::charsets()->codecForName(QString::fromLatin1(fallbackCharset));
523 msg->setBody(codec->fromUnicode(body));
524 } else {
525 msg->setBody(codec->fromUnicode(body));
526 }
527}
528
529enum ReplyStrategy {
530 ReplyList,
531 ReplySmart,
532 ReplyAll,
533 ReplyAuthor,
534 ReplyNone
535};
536
537KMime::Message::Ptr MailTemplates::reply(const KMime::Message::Ptr &origMsg)
538{
539 //FIXME
540 const bool alwaysPlain = true;
541 //FIXME
542 const ReplyStrategy replyStrategy = ReplySmart;
543 KMime::Message::Ptr msg(new KMime::Message);
544 //FIXME
545 //Personal email addresses
546 KMime::Types::AddrSpecList me;
547 KMime::Types::Mailbox::List toList;
548 KMime::Types::Mailbox::List replyToList;
549 KMime::Types::Mailbox::List mailingListAddresses;
550
551 // const uint originalIdentity = identityUoid(origMsg);
552 initHeader(msg);
553 replyToList = origMsg->replyTo()->mailboxes();
554
555 msg->contentType()->setCharset("utf-8");
556
557 if (origMsg->headerByType("List-Post") &&
558 origMsg->headerByType("List-Post")->asUnicodeString().contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) {
559
560 const QString listPost = origMsg->headerByType("List-Post")->asUnicodeString();
561 QRegExp rx(QStringLiteral("<mailto:([^@>]+)@([^>]+)>"), Qt::CaseInsensitive);
562 if (rx.indexIn(listPost, 0) != -1) { // matched
563 KMime::Types::Mailbox mailbox;
564 mailbox.fromUnicodeString(rx.cap(1) + QLatin1Char('@') + rx.cap(2));
565 mailingListAddresses << mailbox;
566 }
567 }
568
569 switch (replyStrategy) {
570 case ReplySmart: {
571 if (auto hdr = origMsg->headerByType("Mail-Followup-To")) {
572 toList << KMime::Types::Mailbox::listFrom7BitString(hdr->as7BitString(false));
573 } else if (!replyToList.isEmpty()) {
574 toList = replyToList;
575 } else if (!mailingListAddresses.isEmpty()) {
576 toList = (KMime::Types::Mailbox::List() << mailingListAddresses.at(0));
577 } else {
578 // doesn't seem to be a mailing list, reply to From: address
579 toList = origMsg->from()->mailboxes();
580
581 bool listContainsMe = false;
582 for (const auto &m : me) {
583 KMime::Types::Mailbox mailbox;
584 mailbox.setAddress(m);
585 if (toList.contains(mailbox)) {
586 listContainsMe = true;
587 }
588 }
589 if (listContainsMe) {
590 // sender seems to be one of our own identities, so we assume that this
591 // is a reply to a "sent" mail where the users wants to add additional
592 // information for the recipient.
593 toList = origMsg->to()->mailboxes();
594 }
595 }
596 // strip all my addresses from the list of recipients
597 const KMime::Types::Mailbox::List recipients = toList;
598
599 toList = stripMyAddressesFromAddressList(recipients, me);
600
601 // ... unless the list contains only my addresses (reply to self)
602 if (toList.isEmpty() && !recipients.isEmpty()) {
603 toList << recipients.first();
604 }
605 }
606 break;
607 case ReplyList: {
608 if (auto hdr = origMsg->headerByType("Mail-Followup-To")) {
609 KMime::Types::Mailbox mailbox;
610 mailbox.from7BitString(hdr->as7BitString(false));
611 toList << mailbox;
612 } else if (!mailingListAddresses.isEmpty()) {
613 toList << mailingListAddresses[ 0 ];
614 } else if (!replyToList.isEmpty()) {
615 // assume a Reply-To header mangling mailing list
616 toList = replyToList;
617 }
618
619 //FIXME
620 // strip all my addresses from the list of recipients
621 const KMime::Types::Mailbox::List recipients = toList;
622 toList = stripMyAddressesFromAddressList(recipients, me);
623 }
624 break;
625 case ReplyAll: {
626 KMime::Types::Mailbox::List recipients;
627 KMime::Types::Mailbox::List ccRecipients;
628
629 // add addresses from the Reply-To header to the list of recipients
630 if (!replyToList.isEmpty()) {
631 recipients = replyToList;
632
633 // strip all possible mailing list addresses from the list of Reply-To addresses
634 foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) {
635 foreach (const KMime::Types::Mailbox &recipient, recipients) {
636 if (mailbox == recipient) {
637 recipients.removeAll(recipient);
638 }
639 }
640 }
641 }
642
643 if (!mailingListAddresses.isEmpty()) {
644 // this is a mailing list message
645 if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) {
646 // The sender didn't set a Reply-to address, so we add the From
647 // address to the list of CC recipients.
648 ccRecipients += origMsg->from()->mailboxes();
649 qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of CC recipients";
650 }
651
652 // if it is a mailing list, add the posting address
653 recipients.prepend(mailingListAddresses[ 0 ]);
654 } else {
655 // this is a normal message
656 if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) {
657 // in case of replying to a normal message only then add the From
658 // address to the list of recipients if there was no Reply-to address
659 recipients += origMsg->from()->mailboxes();
660 qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of recipients";
661 }
662 }
663
664 // strip all my addresses from the list of recipients
665 toList = stripMyAddressesFromAddressList(recipients, me);
666
667 // merge To header and CC header into a list of CC recipients
668 if (!origMsg->cc()->asUnicodeString().isEmpty() || !origMsg->to()->asUnicodeString().isEmpty()) {
669 KMime::Types::Mailbox::List list;
670 if (!origMsg->to()->asUnicodeString().isEmpty()) {
671 list += origMsg->to()->mailboxes();
672 }
673 if (!origMsg->cc()->asUnicodeString().isEmpty()) {
674 list += origMsg->cc()->mailboxes();
675 }
676
677 foreach (const KMime::Types::Mailbox &mailbox, list) {
678 if (!recipients.contains(mailbox) &&
679 !ccRecipients.contains(mailbox)) {
680 ccRecipients += mailbox;
681 qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients";
682 }
683 }
684 }
685
686 if (!ccRecipients.isEmpty()) {
687 // strip all my addresses from the list of CC recipients
688 ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me);
689
690 // in case of a reply to self, toList might be empty. if that's the case
691 // then propagate a cc recipient to To: (if there is any).
692 if (toList.isEmpty() && !ccRecipients.isEmpty()) {
693 toList << ccRecipients.at(0);
694 ccRecipients.pop_front();
695 }
696
697 foreach (const KMime::Types::Mailbox &mailbox, ccRecipients) {
698 msg->cc()->addAddress(mailbox);
699 }
700 }
701
702 if (toList.isEmpty() && !recipients.isEmpty()) {
703 // reply to self without other recipients
704 toList << recipients.at(0);
705 }
706 }
707 break;
708 case ReplyAuthor: {
709 if (!replyToList.isEmpty()) {
710 KMime::Types::Mailbox::List recipients = replyToList;
711
712 // strip the mailing list post address from the list of Reply-To
713 // addresses since we want to reply in private
714 foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) {
715 foreach (const KMime::Types::Mailbox &recipient, recipients) {
716 if (mailbox == recipient) {
717 recipients.removeAll(recipient);
718 }
719 }
720 }
721
722 if (!recipients.isEmpty()) {
723 toList = recipients;
724 } else {
725 // there was only the mailing list post address in the Reply-To header,
726 // so use the From address instead
727 toList = origMsg->from()->mailboxes();
728 }
729 } else if (!origMsg->from()->asUnicodeString().isEmpty()) {
730 toList = origMsg->from()->mailboxes();
731 }
732 }
733 break;
734 case ReplyNone:
735 // the addressees will be set by the caller
736 break;
737 }
738
739 foreach (const KMime::Types::Mailbox &mailbox, toList) {
740 msg->to()->addAddress(mailbox);
741 }
742
743 const QByteArray refStr = getRefStr(origMsg);
744 if (!refStr.isEmpty()) {
745 msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8");
746 }
747
748 //In-Reply-To = original msg-id
749 msg->inReplyTo()->from7BitString(origMsg->messageID()->as7BitString(false));
750
751 msg->subject()->fromUnicodeString(replySubject(origMsg), "utf-8");
752
753 auto definedLocale = QLocale::system();
754
755 //TODO set empty source instead
756 StringHtmlWriter htmlWriter;
757 QImage paintDevice;
758 CSSHelper cssHelper(&paintDevice);
759 MessageViewer::NodeHelper nodeHelper;
760 ObjectTreeSource source(&htmlWriter, &cssHelper);
761 MessageViewer::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}
diff --git a/framework/mail/mailtemplates.h b/framework/mail/mailtemplates.h
new file mode 100644
index 00000000..6519122a
--- /dev/null
+++ b/framework/mail/mailtemplates.h
@@ -0,0 +1,28 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <QByteArray>
23#include <KMime/Message>
24
25namespace MailTemplates
26{
27 KMime::Message::Ptr reply(const KMime::Message::Ptr &message);
28};