File indexing completed on 2024-06-23 05:18:32

0001 /*
0002   SPDX-FileCopyrightText: 2009 Constantin Berzan <exit3219@gmail.com>
0003 
0004   SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "job/maintextjob.h"
0008 
0009 #include "contentjobbase_p.h"
0010 #include "job/multipartjob.h"
0011 #include "job/singlepartjob.h"
0012 #include "part/globalpart.h"
0013 #include "part/textpart.h"
0014 #include "utils/util.h"
0015 
0016 #include <QStringEncoder>
0017 
0018 #include "messagecomposer_debug.h"
0019 #include <KLocalizedString>
0020 #include <KMessageBox>
0021 
0022 #include <KMime/Content>
0023 
0024 using namespace MessageComposer;
0025 
0026 class MessageComposer::MainTextJobPrivate : public ContentJobBasePrivate
0027 {
0028 public:
0029     MainTextJobPrivate(MainTextJob *qq)
0030         : ContentJobBasePrivate(qq)
0031     {
0032     }
0033 
0034     bool chooseSourcePlainText();
0035     bool chooseCharsetAndEncode();
0036     bool chooseCharset();
0037     bool encodeTexts();
0038     SinglepartJob *createPlainTextJob();
0039     SinglepartJob *createHtmlJob();
0040     SinglepartJob *createImageJob(const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image);
0041 
0042     TextPart *textPart = nullptr;
0043     QByteArray chosenCharset;
0044     QString sourcePlainText;
0045     QByteArray encodedPlainText;
0046     QByteArray encodedHtml;
0047 
0048     Q_DECLARE_PUBLIC(MainTextJob)
0049 };
0050 
0051 bool MainTextJobPrivate::chooseSourcePlainText()
0052 {
0053     Q_Q(MainTextJob);
0054     Q_ASSERT(textPart);
0055     if (textPart->isWordWrappingEnabled()) {
0056         sourcePlainText = textPart->wrappedPlainText();
0057         if (sourcePlainText.isEmpty() && !textPart->cleanPlainText().isEmpty()) {
0058             q->setError(JobBase::BugError);
0059             q->setErrorText(i18n("Asked to use word wrapping, but not given wrapped plain text."));
0060             return false;
0061         }
0062     } else {
0063         sourcePlainText = textPart->cleanPlainText();
0064         if (sourcePlainText.isEmpty() && !textPart->wrappedPlainText().isEmpty()) {
0065             q->setError(JobBase::BugError);
0066             q->setErrorText(i18n("Asked not to use word wrapping, but not given clean plain text."));
0067             return false;
0068         }
0069     }
0070     return true;
0071 }
0072 
0073 bool MainTextJobPrivate::chooseCharsetAndEncode()
0074 {
0075     Q_Q(MainTextJob);
0076 
0077     const QList<QByteArray> charsets = q->globalPart()->charsets(true);
0078     if (charsets.isEmpty()) {
0079         q->setError(JobBase::BugError);
0080         q->setErrorText(
0081             i18n("No charsets were available for encoding. Please check your configuration and make sure it contains at least one charset for sending."));
0082         return false;
0083     }
0084 
0085     Q_ASSERT(textPart);
0086     QString toTry = sourcePlainText;
0087     if (textPart->isHtmlUsed()) {
0088         toTry = textPart->cleanHtml();
0089     }
0090     chosenCharset = MessageComposer::Util::selectCharset(charsets, toTry);
0091     if (!chosenCharset.isEmpty()) {
0092         // Good, found a charset that encodes the data without loss.
0093         return encodeTexts();
0094     } else {
0095         // No good charset was found.
0096         if (q->globalPart()->isGuiEnabled() && textPart->warnBadCharset()) {
0097             // Warn the user and give them a chance to go back.
0098             int result = KMessageBox::warningTwoActions(q->globalPart()->parentWidgetForGui(),
0099                                                         i18n("Encoding the message with %1 will lose some characters.\n"
0100                                                              "Do you want to continue?",
0101                                                              QString::fromLatin1(charsets.first())),
0102                                                         i18nc("@title:window", "Some Characters Will Be Lost"),
0103                                                         KGuiItem(i18n("Lose Characters")),
0104                                                         KGuiItem(i18n("Change Encoding")));
0105             if (result == KMessageBox::ButtonCode::SecondaryAction) {
0106                 q->setError(JobBase::UserCancelledError);
0107                 q->setErrorText(i18n("User decided to change the encoding."));
0108                 return false;
0109             } else {
0110                 chosenCharset = charsets.first();
0111                 return encodeTexts();
0112             }
0113         } else if (textPart->warnBadCharset()) {
0114             // Should warn user but no Gui available.
0115             qCDebug(MESSAGECOMPOSER_LOG) << "warnBadCharset but Gui is disabled.";
0116             q->setError(JobBase::UserError);
0117             q->setErrorText(i18n("The selected encoding (%1) cannot fully encode the message.", QString::fromLatin1(charsets.first())));
0118             return false;
0119         } else {
0120             // OK to go ahead with a bad charset.
0121             chosenCharset = charsets.first();
0122             return encodeTexts();
0123 
0124             // FIXME: This is based on the assumption that QTextCodec will replace
0125             // unknown characters with '?' or some other meaningful thing.  The code in
0126             // QTextCodec indeed uses '?', but this behaviour is not documented.
0127         }
0128     }
0129 
0130     // Should not reach here.
0131     Q_ASSERT(false);
0132     return false;
0133 }
0134 
0135 bool MainTextJobPrivate::encodeTexts()
0136 {
0137     Q_Q(MainTextJob);
0138     QStringEncoder codec(chosenCharset.constData());
0139     if (!codec.isValid()) {
0140         qCCritical(MESSAGECOMPOSER_LOG) << "Could not get text codec for charset" << chosenCharset;
0141         q->setError(JobBase::BugError);
0142         q->setErrorText(i18n("Could not get text codec for charset \"%1\".", QString::fromLatin1(chosenCharset)));
0143         return false;
0144     }
0145     encodedPlainText = codec.encode(sourcePlainText);
0146     if (!textPart->cleanHtml().isEmpty()) {
0147         encodedHtml = codec.encode(textPart->cleanHtml());
0148     }
0149     qCDebug(MESSAGECOMPOSER_LOG) << "Done.";
0150     return true;
0151 }
0152 
0153 SinglepartJob *MainTextJobPrivate::createPlainTextJob()
0154 {
0155     auto cjob = new SinglepartJob; // No parent.
0156     cjob->contentType()->setMimeType("text/plain");
0157     cjob->contentType()->setCharset(chosenCharset);
0158     cjob->setData(encodedPlainText);
0159     // TODO standard recommends Content-ID.
0160     return cjob;
0161 }
0162 
0163 SinglepartJob *MainTextJobPrivate::createHtmlJob()
0164 {
0165     auto cjob = new SinglepartJob; // No parent.
0166     cjob->contentType()->setMimeType("text/html");
0167     cjob->contentType()->setCharset(chosenCharset);
0168     const QByteArray data = KPIMTextEdit::RichTextComposerImages::imageNamesToContentIds(encodedHtml, textPart->embeddedImages());
0169     cjob->setData(data);
0170     // TODO standard recommends Content-ID.
0171     return cjob;
0172 }
0173 
0174 SinglepartJob *MainTextJobPrivate::createImageJob(const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image)
0175 {
0176     Q_Q(MainTextJob);
0177 
0178     // The image is a PNG encoded with base64.
0179     auto cjob = new SinglepartJob; // No parent.
0180     cjob->contentType()->setMimeType("image/png");
0181     const QByteArray charset = MessageComposer::Util::selectCharset(q->globalPart()->charsets(true), image->imageName);
0182     Q_ASSERT(!charset.isEmpty());
0183     cjob->contentType()->setName(image->imageName, charset);
0184     cjob->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
0185     cjob->contentTransferEncoding()->setDecoded(false); // It is already encoded.
0186     cjob->contentID()->setIdentifier(image->contentID.toLatin1());
0187     qCDebug(MESSAGECOMPOSER_LOG) << "cid" << cjob->contentID()->identifier();
0188     cjob->setData(image->image);
0189     return cjob;
0190 }
0191 
0192 MainTextJob::MainTextJob(TextPart *textPart, QObject *parent)
0193     : ContentJobBase(*new MainTextJobPrivate(this), parent)
0194 {
0195     Q_D(MainTextJob);
0196     d->textPart = textPart;
0197 }
0198 
0199 MainTextJob::~MainTextJob() = default;
0200 
0201 TextPart *MainTextJob::textPart() const
0202 {
0203     Q_D(const MainTextJob);
0204     return d->textPart;
0205 }
0206 
0207 void MainTextJob::setTextPart(TextPart *part)
0208 {
0209     Q_D(MainTextJob);
0210     d->textPart = part;
0211 }
0212 
0213 void MainTextJob::doStart()
0214 {
0215     Q_D(MainTextJob);
0216     Q_ASSERT(d->textPart);
0217 
0218     // Word wrapping.
0219     if (!d->chooseSourcePlainText()) {
0220         // chooseSourcePlainText has set an error.
0221         Q_ASSERT(error());
0222         emitResult();
0223         return;
0224     }
0225 
0226     // Charset.
0227     if (!d->chooseCharsetAndEncode()) {
0228         // chooseCharsetAndEncode has set an error.
0229         Q_ASSERT(error());
0230         emitResult();
0231         return;
0232     }
0233 
0234     // Assemble the Content.
0235     SinglepartJob *plainJob = d->createPlainTextJob();
0236     if (d->encodedHtml.isEmpty()) {
0237         qCDebug(MESSAGECOMPOSER_LOG) << "Making text/plain";
0238         // Content is text/plain.
0239         appendSubjob(plainJob);
0240     } else {
0241         auto alternativeJob = new MultipartJob;
0242         alternativeJob->setMultipartSubtype("alternative");
0243         alternativeJob->appendSubjob(plainJob); // text/plain first.
0244         alternativeJob->appendSubjob(d->createHtmlJob()); // text/html second.
0245         if (!d->textPart->hasEmbeddedImages()) {
0246             qCDebug(MESSAGECOMPOSER_LOG) << "Have no images.  Making multipart/alternative.";
0247             // Content is multipart/alternative.
0248             appendSubjob(alternativeJob);
0249         } else {
0250             qCDebug(MESSAGECOMPOSER_LOG) << "Have related images.  Making multipart/related.";
0251             // Content is multipart/related with a multipart/alternative sub-Content.
0252             auto multipartJob = new MultipartJob;
0253             multipartJob->setMultipartSubtype("related");
0254             multipartJob->appendSubjob(alternativeJob);
0255             const auto embeddedImages = d->textPart->embeddedImages();
0256             for (const QSharedPointer<KPIMTextEdit::EmbeddedImage> &image : embeddedImages) {
0257                 multipartJob->appendSubjob(d->createImageJob(image));
0258             }
0259             appendSubjob(multipartJob);
0260         }
0261     }
0262     ContentJobBase::doStart();
0263 }
0264 
0265 void MainTextJob::process()
0266 {
0267     Q_D(MainTextJob);
0268     // The content has been created by our subjob.
0269     Q_ASSERT(d->subjobContents.count() == 1);
0270     d->resultContent = d->subjobContents.constFirst();
0271     emitResult();
0272 }
0273 
0274 #include "moc_maintextjob.cpp"