1 /*
2 SPDX-FileCopyrightText: 2009 Constantin Berzan <exit3219@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "job/maintextjob.h"
8
9 #include "contentjobbase_p.h"
10 #include "job/multipartjob.h"
11 #include "job/singlepartjob.h"
12 #include "part/globalpart.h"
13 #include "part/textpart.h"
14 #include "utils/util.h"
15
16 #include <QTextCodec>
17
18 #include "messagecomposer_debug.h"
19 #include <KCharsets>
20 #include <KLocalizedString>
21 #include <KMessageBox>
22
23 #include <KMime/Content>
24
25 using namespace MessageComposer;
26
27 class MessageComposer::MainTextJobPrivate : public ContentJobBasePrivate
28 {
29 public:
MainTextJobPrivate(MainTextJob * qq)30 MainTextJobPrivate(MainTextJob *qq)
31 : ContentJobBasePrivate(qq)
32 {
33 }
34
35 bool chooseSourcePlainText();
36 bool chooseCharsetAndEncode();
37 bool chooseCharset();
38 bool encodeTexts();
39 SinglepartJob *createPlainTextJob();
40 SinglepartJob *createHtmlJob();
41 SinglepartJob *createImageJob(const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image);
42
43 TextPart *textPart = nullptr;
44 QByteArray chosenCharset;
45 QString sourcePlainText;
46 QByteArray encodedPlainText;
47 QByteArray encodedHtml;
48
49 Q_DECLARE_PUBLIC(MainTextJob)
50 };
51
chooseSourcePlainText()52 bool MainTextJobPrivate::chooseSourcePlainText()
53 {
54 Q_Q(MainTextJob);
55 Q_ASSERT(textPart);
56 if (textPart->isWordWrappingEnabled()) {
57 sourcePlainText = textPart->wrappedPlainText();
58 if (sourcePlainText.isEmpty() && !textPart->cleanPlainText().isEmpty()) {
59 q->setError(JobBase::BugError);
60 q->setErrorText(i18n("Asked to use word wrapping, but not given wrapped plain text."));
61 return false;
62 }
63 } else {
64 sourcePlainText = textPart->cleanPlainText();
65 if (sourcePlainText.isEmpty() && !textPart->wrappedPlainText().isEmpty()) {
66 q->setError(JobBase::BugError);
67 q->setErrorText(i18n("Asked not to use word wrapping, but not given clean plain text."));
68 return false;
69 }
70 }
71 return true;
72 }
73
chooseCharsetAndEncode()74 bool MainTextJobPrivate::chooseCharsetAndEncode()
75 {
76 Q_Q(MainTextJob);
77
78 const QVector<QByteArray> charsets = q->globalPart()->charsets(true);
79 if (charsets.isEmpty()) {
80 q->setError(JobBase::BugError);
81 q->setErrorText(
82 i18n("No charsets were available for encoding. Please check your configuration and make sure it contains at least one charset for sending."));
83 return false;
84 }
85
86 Q_ASSERT(textPart);
87 QString toTry = sourcePlainText;
88 if (textPart->isHtmlUsed()) {
89 toTry = textPart->cleanHtml();
90 }
91 chosenCharset = MessageComposer::Util::selectCharset(charsets, toTry);
92 if (!chosenCharset.isEmpty()) {
93 // Good, found a charset that encodes the data without loss.
94 return encodeTexts();
95 } else {
96 // No good charset was found.
97 if (q->globalPart()->isGuiEnabled() && textPart->warnBadCharset()) {
98 // Warn the user and give them a chance to go back.
99 int result = KMessageBox::warningYesNo(q->globalPart()->parentWidgetForGui(),
100 i18n("Encoding the message with %1 will lose some characters.\n"
101 "Do you want to continue?",
102 QString::fromLatin1(charsets.first())),
103 i18n("Some Characters Will Be Lost"),
104 KGuiItem(i18n("Lose Characters")),
105 KGuiItem(i18n("Change Encoding")));
106 if (result == KMessageBox::No) {
107 q->setError(JobBase::UserCancelledError);
108 q->setErrorText(i18n("User decided to change the encoding."));
109 return false;
110 } else {
111 chosenCharset = charsets.first();
112 return encodeTexts();
113 }
114 } else if (textPart->warnBadCharset()) {
115 // Should warn user but no Gui available.
116 qCDebug(MESSAGECOMPOSER_LOG) << "warnBadCharset but Gui is disabled.";
117 q->setError(JobBase::UserError);
118 q->setErrorText(i18n("The selected encoding (%1) cannot fully encode the message.", QString::fromLatin1(charsets.first())));
119 return false;
120 } else {
121 // OK to go ahead with a bad charset.
122 chosenCharset = charsets.first();
123 return encodeTexts();
124
125 // FIXME: This is based on the assumption that QTextCodec will replace
126 // unknown characters with '?' or some other meaningful thing. The code in
127 // QTextCodec indeed uses '?', but this behaviour is not documented.
128 }
129 }
130
131 // Should not reach here.
132 Q_ASSERT(false);
133 return false;
134 }
135
encodeTexts()136 bool MainTextJobPrivate::encodeTexts()
137 {
138 Q_Q(MainTextJob);
139 QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(chosenCharset));
140 if (!codec) {
141 qCCritical(MESSAGECOMPOSER_LOG) << "Could not get text codec for charset" << chosenCharset;
142 q->setError(JobBase::BugError);
143 q->setErrorText(i18n("Could not get text codec for charset \"%1\".", QString::fromLatin1(chosenCharset)));
144 return false;
145 }
146 encodedPlainText = codec->fromUnicode(sourcePlainText);
147 if (!textPart->cleanHtml().isEmpty()) {
148 encodedHtml = codec->fromUnicode(textPart->cleanHtml());
149 }
150 qCDebug(MESSAGECOMPOSER_LOG) << "Done.";
151 return true;
152 }
153
createPlainTextJob()154 SinglepartJob *MainTextJobPrivate::createPlainTextJob()
155 {
156 auto cjob = new SinglepartJob; // No parent.
157 cjob->contentType()->setMimeType("text/plain");
158 cjob->contentType()->setCharset(chosenCharset);
159 cjob->setData(encodedPlainText);
160 // TODO standard recommends Content-ID.
161 return cjob;
162 }
163
createHtmlJob()164 SinglepartJob *MainTextJobPrivate::createHtmlJob()
165 {
166 auto cjob = new SinglepartJob; // No parent.
167 cjob->contentType()->setMimeType("text/html");
168 cjob->contentType()->setCharset(chosenCharset);
169 const QByteArray data = KPIMTextEdit::RichTextComposerImages::imageNamesToContentIds(encodedHtml, textPart->embeddedImages());
170 cjob->setData(data);
171 // TODO standard recommends Content-ID.
172 return cjob;
173 }
174
createImageJob(const QSharedPointer<KPIMTextEdit::EmbeddedImage> & image)175 SinglepartJob *MainTextJobPrivate::createImageJob(const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image)
176 {
177 Q_Q(MainTextJob);
178
179 // The image is a PNG encoded with base64.
180 auto cjob = new SinglepartJob; // No parent.
181 cjob->contentType()->setMimeType("image/png");
182 const QByteArray charset = MessageComposer::Util::selectCharset(q->globalPart()->charsets(true), image->imageName);
183 Q_ASSERT(!charset.isEmpty());
184 cjob->contentType()->setName(image->imageName, charset);
185 cjob->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
186 cjob->contentTransferEncoding()->setDecoded(false); // It is already encoded.
187 cjob->contentID()->setIdentifier(image->contentID.toLatin1());
188 qCDebug(MESSAGECOMPOSER_LOG) << "cid" << cjob->contentID()->identifier();
189 cjob->setData(image->image);
190 return cjob;
191 }
192
MainTextJob(TextPart * textPart,QObject * parent)193 MainTextJob::MainTextJob(TextPart *textPart, QObject *parent)
194 : ContentJobBase(*new MainTextJobPrivate(this), parent)
195 {
196 Q_D(MainTextJob);
197 d->textPart = textPart;
198 }
199
~MainTextJob()200 MainTextJob::~MainTextJob()
201 {
202 }
203
textPart() const204 TextPart *MainTextJob::textPart() const
205 {
206 Q_D(const MainTextJob);
207 return d->textPart;
208 }
209
setTextPart(TextPart * part)210 void MainTextJob::setTextPart(TextPart *part)
211 {
212 Q_D(MainTextJob);
213 d->textPart = part;
214 }
215
doStart()216 void MainTextJob::doStart()
217 {
218 Q_D(MainTextJob);
219 Q_ASSERT(d->textPart);
220
221 // Word wrapping.
222 if (!d->chooseSourcePlainText()) {
223 // chooseSourcePlainText has set an error.
224 Q_ASSERT(error());
225 emitResult();
226 return;
227 }
228
229 // Charset.
230 if (!d->chooseCharsetAndEncode()) {
231 // chooseCharsetAndEncode has set an error.
232 Q_ASSERT(error());
233 emitResult();
234 return;
235 }
236
237 // Assemble the Content.
238 SinglepartJob *plainJob = d->createPlainTextJob();
239 if (d->encodedHtml.isEmpty()) {
240 qCDebug(MESSAGECOMPOSER_LOG) << "Making text/plain";
241 // Content is text/plain.
242 appendSubjob(plainJob);
243 } else {
244 auto alternativeJob = new MultipartJob;
245 alternativeJob->setMultipartSubtype("alternative");
246 alternativeJob->appendSubjob(plainJob); // text/plain first.
247 alternativeJob->appendSubjob(d->createHtmlJob()); // text/html second.
248 if (!d->textPart->hasEmbeddedImages()) {
249 qCDebug(MESSAGECOMPOSER_LOG) << "Have no images. Making multipart/alternative.";
250 // Content is multipart/alternative.
251 appendSubjob(alternativeJob);
252 } else {
253 qCDebug(MESSAGECOMPOSER_LOG) << "Have related images. Making multipart/related.";
254 // Content is multipart/related with a multipart/alternative sub-Content.
255 auto multipartJob = new MultipartJob;
256 multipartJob->setMultipartSubtype("related");
257 multipartJob->appendSubjob(alternativeJob);
258 const auto embeddedImages = d->textPart->embeddedImages();
259 for (const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image : embeddedImages) {
260 multipartJob->appendSubjob(d->createImageJob(image));
261 }
262 appendSubjob(multipartJob);
263 }
264 }
265 ContentJobBase::doStart();
266 }
267
process()268 void MainTextJob::process()
269 {
270 Q_D(MainTextJob);
271 // The content has been created by our subjob.
272 Q_ASSERT(d->subjobContents.count() == 1);
273 d->resultContent = d->subjobContents.constFirst();
274 emitResult();
275 }
276