diff options
Diffstat (limited to 'framework')
-rw-r--r-- | framework/mail/CMakeLists.txt | 3 | ||||
-rw-r--r-- | framework/mail/composer.cpp | 33 | ||||
-rw-r--r-- | framework/mail/composer.h | 7 | ||||
-rw-r--r-- | framework/mail/mailtemplates.cpp | 801 | ||||
-rw-r--r-- | framework/mail/mailtemplates.h | 28 |
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 | ) |
18 | add_definitions(-DMAIL_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") | 19 | add_definitions(-DMAIL_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") |
@@ -23,7 +24,7 @@ include_directories(${CURL_INCLUDE_DIRS}) | |||
23 | 24 | ||
24 | add_library(mailplugin SHARED ${mailplugin_SRCS}) | 25 | add_library(mailplugin SHARED ${mailplugin_SRCS}) |
25 | 26 | ||
26 | qt5_use_modules(mailplugin Core Quick Qml) | 27 | qt5_use_modules(mailplugin Core Quick Qml WebKitWidgets) |
27 | 28 | ||
28 | target_link_libraries(mailplugin actionplugin settingsplugin sink KF5::Otp KF5::Codecs ${CURL_LIBRARIES}) | 29 | target_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 | |||
30 | Composer::Composer(QObject *parent) : QObject(parent) | 32 | Composer::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 | ||
120 | QVariant Composer::originalMessage() const | ||
121 | { | ||
122 | return m_originalMessage; | ||
123 | } | ||
124 | |||
125 | void 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 | |||
118 | void Composer::send() | 144 | void 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 | ||
26 | class Composer : public QObject | 27 | class 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 | |||
60 | signals: | 65 | signals: |
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 | |||
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(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 | |||
359 | QString 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 | |||
402 | QString 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 | |||
455 | QString 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 | |||
478 | QString 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 | |||
491 | void 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 | |||
529 | enum ReplyStrategy { | ||
530 | ReplyList, | ||
531 | ReplySmart, | ||
532 | ReplyAll, | ||
533 | ReplyAuthor, | ||
534 | ReplyNone | ||
535 | }; | ||
536 | |||
537 | KMime::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 | |||
25 | namespace MailTemplates | ||
26 | { | ||
27 | KMime::Message::Ptr reply(const KMime::Message::Ptr &message); | ||
28 | }; | ||