diff options
author | Christian Mollekopf <chrigi_1@fastmail.fm> | 2016-03-01 00:08:45 +0100 |
---|---|---|
committer | Christian Mollekopf <chrigi_1@fastmail.fm> | 2016-03-01 00:08:45 +0100 |
commit | 235b8cb38214fe1ec919fce8701737d7b944c6de (patch) | |
tree | 92468fe9c9f939086d8dac6f01d36f4f71a40c95 /framework | |
parent | 0467b39e1ca034ec7298017e3055e352c755a386 (diff) | |
download | kube-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.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 | }; | ||