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"