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