Messagelib

maintextjob.cpp
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 <QStringEncoder>
17
18#include "messagecomposer_debug.h"
19#include <KLocalizedString>
20#include <KMessageBox>
21
22#include <KMime/Content>
23
24using namespace MessageComposer;
25
26class MessageComposer::MainTextJobPrivate : public ContentJobBasePrivate
27{
28public:
29 MainTextJobPrivate(MainTextJob *qq)
30 : ContentJobBasePrivate(qq)
31 {
32 }
33
34 [[nodiscard]] bool chooseSourcePlainText();
35 [[nodiscard]] bool chooseCharsetAndEncode();
36 [[nodiscard]] bool chooseCharset();
37 [[nodiscard]] bool encodeTexts();
38 [[nodiscard]] SinglepartJob *createPlainTextJob();
39 [[nodiscard]] SinglepartJob *createHtmlJob();
40 [[nodiscard]] SinglepartJob *createImageJob(const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image);
41
42 TextPart *textPart = nullptr;
43 QByteArray chosenCharset;
44 QString sourcePlainText;
45 QByteArray encodedPlainText;
46 QByteArray encodedHtml;
47
48 Q_DECLARE_PUBLIC(MainTextJob)
49};
50
51bool MainTextJobPrivate::chooseSourcePlainText()
52{
53 Q_Q(MainTextJob);
54 Q_ASSERT(textPart);
55 if (textPart->isWordWrappingEnabled()) {
56 sourcePlainText = textPart->wrappedPlainText();
57 if (sourcePlainText.isEmpty() && !textPart->cleanPlainText().isEmpty()) {
58 q->setError(JobBase::BugError);
59 q->setErrorText(i18n("Asked to use word wrapping, but not given wrapped plain text."));
60 return false;
61 }
62 } else {
63 sourcePlainText = textPart->cleanPlainText();
64 if (sourcePlainText.isEmpty() && !textPart->wrappedPlainText().isEmpty()) {
65 q->setError(JobBase::BugError);
66 q->setErrorText(i18n("Asked not to use word wrapping, but not given clean plain text."));
67 return false;
68 }
69 }
70 return true;
71}
72
73bool MainTextJobPrivate::chooseCharsetAndEncode()
74{
75 Q_Q(MainTextJob);
76
77 const QList<QByteArray> charsets = q->globalPart()->charsets(true);
78 if (charsets.isEmpty()) {
79 q->setError(JobBase::BugError);
80 q->setErrorText(
81 i18n("No charsets were available for encoding. Please check your configuration and make sure it contains at least one charset for sending."));
82 return false;
83 }
84
85 Q_ASSERT(textPart);
86 QString toTry = sourcePlainText;
87 if (textPart->isHtmlUsed()) {
88 toTry = textPart->cleanHtml();
89 }
90 chosenCharset = MessageComposer::Util::selectCharset(charsets, toTry);
91 if (!chosenCharset.isEmpty()) {
92 // Good, found a charset that encodes the data without loss.
93 return encodeTexts();
94 } else {
95 // No good charset was found.
96 if (q->globalPart()->isGuiEnabled() && textPart->warnBadCharset()) {
97 // Warn the user and give them a chance to go back.
98 int result = KMessageBox::warningTwoActions(q->globalPart()->parentWidgetForGui(),
99 i18n("Encoding the message with %1 will lose some characters.\n"
100 "Do you want to continue?",
101 QString::fromLatin1(charsets.first())),
102 i18nc("@title:window", "Some Characters Will Be Lost"),
103 KGuiItem(i18n("Lose Characters")),
104 KGuiItem(i18n("Change Encoding")));
105 if (result == KMessageBox::ButtonCode::SecondaryAction) {
106 q->setError(JobBase::UserCancelledError);
107 q->setErrorText(i18n("User decided to change the encoding."));
108 return false;
109 } else {
110 chosenCharset = charsets.first();
111 return encodeTexts();
112 }
113 } else if (textPart->warnBadCharset()) {
114 // Should warn user but no Gui available.
115 qCDebug(MESSAGECOMPOSER_LOG) << "warnBadCharset but Gui is disabled.";
116 q->setError(JobBase::UserError);
117 q->setErrorText(i18n("The selected encoding (%1) cannot fully encode the message.", QString::fromLatin1(charsets.first())));
118 return false;
119 } else {
120 // OK to go ahead with a bad charset.
121 chosenCharset = charsets.first();
122 return encodeTexts();
123
124 // FIXME: This is based on the assumption that QTextCodec will replace
125 // unknown characters with '?' or some other meaningful thing. The code in
126 // QTextCodec indeed uses '?', but this behaviour is not documented.
127 }
128 }
129
130 // Should not reach here.
131 Q_ASSERT(false);
132 return false;
133}
134
135bool MainTextJobPrivate::encodeTexts()
136{
137 Q_Q(MainTextJob);
138 QStringEncoder codec(chosenCharset.constData());
139 if (!codec.isValid()) {
140 qCCritical(MESSAGECOMPOSER_LOG) << "Could not get text codec for charset" << chosenCharset;
141 q->setError(JobBase::BugError);
142 q->setErrorText(i18n("Could not get text codec for charset \"%1\".", QString::fromLatin1(chosenCharset)));
143 return false;
144 }
145 encodedPlainText = codec.encode(sourcePlainText);
146 if (!textPart->cleanHtml().isEmpty()) {
147 encodedHtml = codec.encode(textPart->cleanHtml());
148 }
149 qCDebug(MESSAGECOMPOSER_LOG) << "Done.";
150 return true;
151}
152
153SinglepartJob *MainTextJobPrivate::createPlainTextJob()
154{
155 auto cjob = new SinglepartJob; // No parent.
156 cjob->contentType()->setMimeType("text/plain");
157 cjob->contentType()->setCharset(chosenCharset);
158 cjob->setData(encodedPlainText);
159 // TODO standard recommends Content-ID.
160 return cjob;
161}
162
163SinglepartJob *MainTextJobPrivate::createHtmlJob()
164{
165 auto cjob = new SinglepartJob; // No parent.
166 cjob->contentType()->setMimeType("text/html");
167 cjob->contentType()->setCharset(chosenCharset);
168 const QByteArray data = KPIMTextEdit::RichTextComposerImages::imageNamesToContentIds(encodedHtml, textPart->embeddedImages());
169 cjob->setData(data);
170 // TODO standard recommends Content-ID.
171 return cjob;
172}
173
174SinglepartJob *MainTextJobPrivate::createImageJob(const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image)
175{
176 Q_Q(MainTextJob);
177
178 // The image is a PNG encoded with base64.
179 auto cjob = new SinglepartJob; // No parent.
180 cjob->contentType()->setMimeType("image/png");
181 const QByteArray charset = MessageComposer::Util::selectCharset(q->globalPart()->charsets(true), image->imageName);
182 Q_ASSERT(!charset.isEmpty());
183 cjob->contentType()->setName(image->imageName, charset);
184 cjob->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
185 cjob->contentTransferEncoding()->setDecoded(false); // It is already encoded.
186 cjob->contentID()->setIdentifier(image->contentID.toLatin1());
187 qCDebug(MESSAGECOMPOSER_LOG) << "cid" << cjob->contentID()->identifier();
188 cjob->setData(image->image);
189 return cjob;
190}
191
192MainTextJob::MainTextJob(TextPart *textPart, QObject *parent)
193 : ContentJobBase(*new MainTextJobPrivate(this), parent)
194{
196 d->textPart = textPart;
197}
198
199MainTextJob::~MainTextJob() = default;
200
201TextPart *MainTextJob::textPart() const
202{
203 Q_D(const MainTextJob);
204 return d->textPart;
205}
206
207void MainTextJob::setTextPart(TextPart *part)
208{
210 d->textPart = part;
211}
212
213void MainTextJob::doStart()
214{
216 Q_ASSERT(d->textPart);
217
218 // Word wrapping.
219 if (!d->chooseSourcePlainText()) {
220 // chooseSourcePlainText has set an error.
221 Q_ASSERT(error());
222 emitResult();
223 return;
224 }
225
226 // Charset.
227 if (!d->chooseCharsetAndEncode()) {
228 // chooseCharsetAndEncode has set an error.
229 Q_ASSERT(error());
230 emitResult();
231 return;
232 }
233
234 // Assemble the Content.
235 SinglepartJob *plainJob = d->createPlainTextJob();
236 if (d->encodedHtml.isEmpty()) {
237 qCDebug(MESSAGECOMPOSER_LOG) << "Making text/plain";
238 // Content is text/plain.
240 } else {
241 auto alternativeJob = new MultipartJob;
242 alternativeJob->setMultipartSubtype("alternative");
243 alternativeJob->appendSubjob(plainJob); // text/plain first.
244 alternativeJob->appendSubjob(d->createHtmlJob()); // text/html second.
245 if (!d->textPart->hasEmbeddedImages()) {
246 qCDebug(MESSAGECOMPOSER_LOG) << "Have no images. Making multipart/alternative.";
247 // Content is multipart/alternative.
249 } else {
250 qCDebug(MESSAGECOMPOSER_LOG) << "Have related images. Making multipart/related.";
251 // Content is multipart/related with a multipart/alternative sub-Content.
252 auto multipartJob = new MultipartJob;
253 multipartJob->setMultipartSubtype("related");
254 multipartJob->appendSubjob(alternativeJob);
255 const auto embeddedImages = d->textPart->embeddedImages();
256 for (const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image : embeddedImages) {
257 multipartJob->appendSubjob(d->createImageJob(image));
258 }
260 }
261 }
263}
264
265void MainTextJob::process()
266{
268 // The content has been created by our subjob.
269 Q_ASSERT(d->subjobContents.count() == 1);
270 d->resultContent = d->subjobContents.constFirst();
271 emitResult();
272}
273
274#include "moc_maintextjob.cpp"
void emitResult()
int error() const
void setMimeType(const QByteArray &mimeType)
The ContentJobBase class.
virtual void doStart()
Reimplement to do additional stuff before processing children, such as adding more subjobs.
bool appendSubjob(ContentJobBase *job)
This is meant to be used instead of KCompositeJob::addSubjob(), making it possible to add subjobs fro...
The MainTextJob class.
Definition maintextjob.h:20
The MultipartJob class.
The SinglepartJob class.
The TextPart class.
Definition textpart.h:21
bool warnBadCharset
Default true.
Definition textpart.h:28
bool isWordWrappingEnabled
True iff the text is word wrapped. By default: true.
Definition textpart.h:25
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
ButtonCode warningTwoActions(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const QString &dontAskAgainName=QString(), Options options=Options(Notify|Dangerous))
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
const char * constData() const const
bool isEmpty() const const
T & first()
bool isEmpty() const const
T qobject_cast(QObject *object)
QString fromLatin1(const char *str, int size)
bool isEmpty() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sun Feb 25 2024 18:37:31 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.