Messagelib

maintextjob.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Constantin Berzan <[email protected]>
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:
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 
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 
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 
136 bool MainTextJobPrivate::encodeTexts()
137 {
138  Q_Q(MainTextJob);
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 
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 
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 
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 
193 MainTextJob::MainTextJob(TextPart *textPart, QObject *parent)
194  : ContentJobBase(*new MainTextJobPrivate(this), parent)
195 {
196  Q_D(MainTextJob);
197  d->textPart = textPart;
198 }
199 
200 MainTextJob::~MainTextJob() = default;
201 
202 TextPart *MainTextJob::textPart() const
203 {
204  Q_D(const MainTextJob);
205  return d->textPart;
206 }
207 
208 void MainTextJob::setTextPart(TextPart *part)
209 {
210  Q_D(MainTextJob);
211  d->textPart = part;
212 }
213 
214 void MainTextJob::doStart()
215 {
216  Q_D(MainTextJob);
217  Q_ASSERT(d->textPart);
218 
219  // Word wrapping.
220  if (!d->chooseSourcePlainText()) {
221  // chooseSourcePlainText has set an error.
222  Q_ASSERT(error());
223  emitResult();
224  return;
225  }
226 
227  // Charset.
228  if (!d->chooseCharsetAndEncode()) {
229  // chooseCharsetAndEncode has set an error.
230  Q_ASSERT(error());
231  emitResult();
232  return;
233  }
234 
235  // Assemble the Content.
236  SinglepartJob *plainJob = d->createPlainTextJob();
237  if (d->encodedHtml.isEmpty()) {
238  qCDebug(MESSAGECOMPOSER_LOG) << "Making text/plain";
239  // Content is text/plain.
240  appendSubjob(plainJob);
241  } else {
242  auto alternativeJob = new MultipartJob;
243  alternativeJob->setMultipartSubtype("alternative");
244  alternativeJob->appendSubjob(plainJob); // text/plain first.
245  alternativeJob->appendSubjob(d->createHtmlJob()); // text/html second.
246  if (!d->textPart->hasEmbeddedImages()) {
247  qCDebug(MESSAGECOMPOSER_LOG) << "Have no images. Making multipart/alternative.";
248  // Content is multipart/alternative.
249  appendSubjob(alternativeJob);
250  } else {
251  qCDebug(MESSAGECOMPOSER_LOG) << "Have related images. Making multipart/related.";
252  // Content is multipart/related with a multipart/alternative sub-Content.
253  auto multipartJob = new MultipartJob;
254  multipartJob->setMultipartSubtype("related");
255  multipartJob->appendSubjob(alternativeJob);
256  const auto embeddedImages = d->textPart->embeddedImages();
257  for (const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image : embeddedImages) {
258  multipartJob->appendSubjob(d->createImageJob(image));
259  }
260  appendSubjob(multipartJob);
261  }
262  }
263  ContentJobBase::doStart();
264 }
265 
266 void MainTextJob::process()
267 {
268  Q_D(MainTextJob);
269  // The content has been created by our subjob.
270  Q_ASSERT(d->subjobContents.count() == 1);
271  d->resultContent = d->subjobContents.constFirst();
272  emitResult();
273 }
QTextCodec * codecForName(const QString &name) const
QByteArray fromUnicode(const QString &str) const const
void setMimeType(const QByteArray &mimeType)
bool isEmpty() const const
T & first()
void error(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
KCharsets * charsets()
static KCharsets * charsets()
QString i18n(const char *text, const TYPE &arg...)
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
bool isEmpty() const const
QString fromLatin1(const char *str, int size)
The TextPart class.
Definition: textpart.h:20
The MainTextJob class.
Definition: maintextjob.h:19
The MultipartJob class.
Definition: multipartjob.h:19
The SinglepartJob class.
Definition: singlepartjob.h:31
ButtonCode warningYesNo(QWidget *parent, const QString &text, const QString &caption=QString(), const KGuiItem &buttonYes=KStandardGuiItem::yes(), const KGuiItem &buttonNo=KStandardGuiItem::no(), const QString &dontAskAgainName=QString(), Options options=Options(Notify|Dangerous))
The ContentJobBase class.
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Tue Nov 30 2021 23:05:47 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.