Messagelib

composerviewbase.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
3 SPDX-FileCopyrightText: 2010 Leo Franchi <lfranchi@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "composerviewbase.h"
9
10#include "attachment/attachmentcontrollerbase.h"
11#include "attachment/attachmentmodel.h"
12#include "composer-ng/richtextcomposerng.h"
13#include "composer-ng/richtextcomposersignatures.h"
14#include "composer.h"
15#include "composer/keyresolver.h"
16#include "composer/signaturecontroller.h"
17#include "draftstatus/draftstatus.h"
18#include "imagescaling/imagescalingutils.h"
19#include "job/emailaddressresolvejob.h"
20#include "part/globalpart.h"
21#include "part/infopart.h"
22#include "utils/kleo_util.h"
23#include "utils/util.h"
24#include "utils/util_p.h"
25#include <KPIMTextEdit/RichTextComposerControler>
26#include <KPIMTextEdit/RichTextComposerImages>
27
28#include "sendlater/sendlatercreatejob.h"
29#include "sendlater/sendlaterinfo.h"
30
31#include <PimCommonAkonadi/RecentAddresses>
32
33#include "settings/messagecomposersettings.h"
34#include <MessageComposer/RecipientsEditor>
35
36#include <KCursorSaver>
37#include <KIdentityManagementCore/Identity>
38#include <MimeTreeParser/ObjectTreeParser>
39#include <MimeTreeParser/SimpleObjectTreeSource>
40#include <Sonnet/DictionaryComboBox>
41
42#include <MessageCore/AutocryptStorage>
43#include <MessageCore/NodeHelper>
44#include <MessageCore/StringUtil>
45
46#include <Akonadi/MessageQueueJob>
47#include <MailTransport/TransportComboBox>
48#include <MailTransport/TransportManager>
49
50#include <Akonadi/CollectionComboBox>
51#include <Akonadi/CollectionFetchJob>
52#include <Akonadi/ItemCreateJob>
53#include <Akonadi/MessageFlags>
54#include <Akonadi/SpecialMailCollections>
55
56#include <KEmailAddress>
57#include <KIdentityManagementCore/IdentityManager>
58#include <KIdentityManagementWidgets/IdentityCombo>
59
60#include "messagecomposer_debug.h"
61
62#include <Libkleo/ExpiryChecker>
63#include <Libkleo/ExpiryCheckerSettings>
64
65#include <QGpgME/ExportJob>
66#include <QGpgME/ImportJob>
67#include <QGpgME/Protocol>
68#include <gpgme++/context.h>
69#include <gpgme++/importresult.h>
70
71#include <KLocalizedString>
72#include <KMessageBox>
73#include <QSaveFile>
74
75#include "followupreminder/followupremindercreatejob.h"
76#include <QDir>
77#include <QStandardPaths>
78#include <QTemporaryDir>
79#include <QTimer>
80#include <QUuid>
81
82using namespace MessageComposer;
83
84ComposerViewBase::ComposerViewBase(QObject *parent, QWidget *parentGui)
85 : QObject(parent)
86 , m_msg(KMime::Message::Ptr(new KMime::Message))
87 , m_parentWidget(parentGui)
88 , m_cryptoMessageFormat(Kleo::AutoFormat)
89 , m_autoSaveInterval(60000) // default of 1 min
90{
91 m_charsets << "utf-8"; // default, so we have a backup in case client code forgot to set.
92
93 initAutoSave();
94}
95
96ComposerViewBase::~ComposerViewBase() = default;
97
99{
100 return !m_composers.isEmpty();
101}
102
103void ComposerViewBase::setMessage(const KMime::Message::Ptr &msg, bool allowDecryption)
104{
105 if (m_attachmentModel) {
106 const auto attachments{m_attachmentModel->attachments()};
107 for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) {
108 if (!m_attachmentModel->removeAttachment(attachment)) {
109 qCWarning(MESSAGECOMPOSER_LOG) << "Attachment not found.";
110 }
111 }
112 }
113 m_msg = msg;
114 if (m_recipientsEditor) {
115 m_recipientsEditor->clear();
116 bool resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->to()->mailboxes(), MessageComposer::Recipient::To);
118 resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->cc()->mailboxes(), MessageComposer::Recipient::Cc);
119 }
121 resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->bcc()->mailboxes(), MessageComposer::Recipient::Bcc);
122 }
124 resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->replyTo()->mailboxes(), MessageComposer::Recipient::ReplyTo);
125 }
126 m_recipientsEditor->setFocusBottom();
127
129 // If we are loading from a draft, load unexpanded aliases as well
130 if (auto hrd = m_msg->headerByType("X-KMail-UnExpanded-To")) {
131 const QStringList spl = hrd->asUnicodeString().split(QLatin1Char(','));
132 for (const QString &addr : spl) {
133 if (m_recipientsEditor->addRecipient(addr, MessageComposer::Recipient::To)) {
135 qCWarning(MESSAGECOMPOSER_LOG) << "Impossible to add recipient.";
136 break;
137 }
138 }
139 }
140 }
142 if (auto hrd = m_msg->headerByType("X-KMail-UnExpanded-CC")) {
143 const QStringList spl = hrd->asUnicodeString().split(QLatin1Char(','));
144 for (const QString &addr : spl) {
145 if (m_recipientsEditor->addRecipient(addr, MessageComposer::Recipient::Cc)) {
146 qCWarning(MESSAGECOMPOSER_LOG) << "Impossible to add recipient.";
148 break;
149 }
150 }
151 }
152 }
154 if (auto hrd = m_msg->headerByType("X-KMail-UnExpanded-BCC")) {
155 const QStringList spl = hrd->asUnicodeString().split(QLatin1Char(','));
156 for (const QString &addr : spl) {
157 if (m_recipientsEditor->addRecipient(addr, MessageComposer::Recipient::Bcc)) {
158 qCWarning(MESSAGECOMPOSER_LOG) << "Impossible to add recipient.";
160 break;
161 }
162 }
163 }
164 }
166 if (auto hrd = m_msg->headerByType("X-KMail-UnExpanded-Reply-To")) {
167 const QStringList spl = hrd->asUnicodeString().split(QLatin1Char(','));
168 for (const QString &addr : spl) {
169 if (m_recipientsEditor->addRecipient(addr, MessageComposer::Recipient::ReplyTo)) {
170 qCWarning(MESSAGECOMPOSER_LOG) << "Impossible to add recipient.";
172 break;
173 }
174 }
175 }
176 }
177 Q_EMIT tooManyRecipient(resultTooManyRecipients);
178 }
179 // First, we copy the message and then parse it to the object tree parser.
180 // The otp gets the message text out of it, in textualContent(), and also decrypts
181 // the message if necessary.
182 auto msgContent = new KMime::Content;
183 msgContent->setContent(m_msg->encodedContent());
184 msgContent->parse();
186 MimeTreeParser::ObjectTreeParser otp(&emptySource); // All default are ok
187 emptySource.setDecryptMessage(allowDecryption);
188 otp.parseObjectTree(msgContent);
189
190 // Load the attachments
191 const auto attachmentsOfExtraContents{otp.nodeHelper()->attachmentsOfExtraContents()};
192 for (const auto &att : attachmentsOfExtraContents) {
193 addAttachmentPart(att);
194 }
195 const auto attachments{msgContent->attachments()};
196 for (const auto &att : attachments) {
197 addAttachmentPart(att);
198 }
199
200 int transportId = -1;
201 if (auto hdr = m_msg->headerByType("X-KMail-Transport")) {
202 transportId = hdr->asUnicodeString().toInt();
203 }
204
205 if (m_transport) {
207 if (transport) {
208 if (!m_transport->setCurrentTransport(transport->id())) {
209 qCWarning(MESSAGECOMPOSER_LOG) << "Impossible to find transport id" << transport->id();
210 }
211 }
212 }
213
214 // Set the HTML text and collect HTML images
215 QString htmlContent = otp.htmlContent();
216 if (htmlContent.isEmpty()) {
217 m_editor->setPlainText(otp.plainTextContent());
218 } else {
219 // Bug 372085 <div id="name"> is replaced in qtextedit by <a id="name">... => break url
220 htmlContent.replace(QRegularExpression(QStringLiteral("<div\\s*id=\".*\">")), QStringLiteral("<div>"));
221 m_editor->setHtml(htmlContent);
222 Q_EMIT enableHtml();
223 collectImages(m_msg.data());
224 }
225
226 if (auto hdr = m_msg->headerByType("X-KMail-CursorPos")) {
227 m_editor->setCursorPositionFromStart(hdr->asUnicodeString().toUInt());
228 }
229 delete msgContent;
230}
231
232void ComposerViewBase::updateTemplate(const KMime::Message::Ptr &msg)
233{
234 // First, we copy the message and then parse it to the object tree parser.
235 // The otp gets the message text out of it, in textualContent(), and also decrypts
236 // the message if necessary.
237 auto msgContent = new KMime::Content;
238 msgContent->setContent(msg->encodedContent());
239 msgContent->parse();
241 MimeTreeParser::ObjectTreeParser otp(&emptySource); // All default are ok
242 otp.parseObjectTree(msgContent);
243 // Set the HTML text and collect HTML images
244 if (!otp.htmlContent().isEmpty()) {
245 m_editor->setHtml(otp.htmlContent());
246 Q_EMIT enableHtml();
247 collectImages(msg.data());
248 } else {
249 m_editor->setPlainText(otp.plainTextContent());
250 }
251
252 if (auto hdr = msg->headerByType("X-KMail-CursorPos")) {
253 m_editor->setCursorPositionFromStart(hdr->asUnicodeString().toInt());
254 }
255 delete msgContent;
256}
257
258void ComposerViewBase::saveMailSettings()
259{
260 const auto identity = currentIdentity();
261 auto header = new KMime::Headers::Generic("X-KMail-Transport");
262 header->fromUnicodeString(QString::number(m_transport->currentTransportId()), "utf-8");
263 m_msg->setHeader(header);
264
265 header = new KMime::Headers::Generic("X-KMail-Transport-Name");
266 header->fromUnicodeString(m_transport->currentText(), "utf-8");
267 m_msg->setHeader(header);
268
269 header = new KMime::Headers::Generic("X-KMail-Fcc");
270 header->fromUnicodeString(QString::number(m_fccCollection.id()), "utf-8");
271 m_msg->setHeader(header);
272
273 header = new KMime::Headers::Generic("X-KMail-Identity");
274 header->fromUnicodeString(QString::number(identity.uoid()), "utf-8");
275 m_msg->setHeader(header);
276
277 header = new KMime::Headers::Generic("X-KMail-Identity-Name");
278 header->fromUnicodeString(identity.identityName(), "utf-8");
279 m_msg->setHeader(header);
280
281 header = new KMime::Headers::Generic("X-KMail-Dictionary");
282 header->fromUnicodeString(m_dictionary->currentDictionary(), "utf-8");
283 m_msg->setHeader(header);
284
285 // Save the quote prefix which is used for this message. Each message can have
286 // a different quote prefix, for example depending on the original sender.
287 if (m_editor->quotePrefixName().isEmpty()) {
288 m_msg->removeHeader("X-KMail-QuotePrefix");
289 } else {
290 header = new KMime::Headers::Generic("X-KMail-QuotePrefix");
291 header->fromUnicodeString(m_editor->quotePrefixName(), "utf-8");
292 m_msg->setHeader(header);
293 }
294
295 if (m_editor->composerControler()->isFormattingUsed()) {
296 qCDebug(MESSAGECOMPOSER_LOG) << "HTML mode";
297 header = new KMime::Headers::Generic("X-KMail-Markup");
298 header->fromUnicodeString(QStringLiteral("true"), "utf-8");
299 m_msg->setHeader(header);
300 } else {
301 m_msg->removeHeader("X-KMail-Markup");
302 qCDebug(MESSAGECOMPOSER_LOG) << "Plain text";
303 }
304}
305
306void ComposerViewBase::clearFollowUp()
307{
308 mFollowUpDate = QDate();
309 mFollowUpCollection = Akonadi::Collection();
310}
311
312void ComposerViewBase::send(MessageComposer::MessageSender::SendMethod method, MessageComposer::MessageSender::SaveIn saveIn, bool checkMailDispatcher)
313{
314 mSendMethod = method;
315 mSaveIn = saveIn;
316
318 const auto identity = currentIdentity();
319
320 if (identity.attachVcard() && m_attachmentController->attachOwnVcard()) {
321 const QString vcardFileName = identity.vCardFile();
322 if (!vcardFileName.isEmpty()) {
323 m_attachmentController->addAttachmentUrlSync(QUrl::fromLocalFile(vcardFileName));
324 }
325 }
326 saveMailSettings();
327
328 if (m_editor->composerControler()->isFormattingUsed() && inlineSigningEncryptionSelected()) {
329 const QString keepBtnText =
330 m_encrypt ? m_sign ? i18n("&Keep markup, do not sign/encrypt") : i18n("&Keep markup, do not encrypt") : i18n("&Keep markup, do not sign");
331 const QString yesBtnText = m_encrypt ? m_sign ? i18n("Sign/Encrypt (delete markup)") : i18n("Encrypt (delete markup)") : i18n("Sign (delete markup)");
332 int ret = KMessageBox::warningTwoActionsCancel(m_parentWidget,
333 i18n("<qt><p>Inline signing/encrypting of HTML messages is not possible;</p>"
334 "<p>do you want to delete your markup?</p></qt>"),
335 i18nc("@title:window", "Sign/Encrypt Message?"),
338 if (KMessageBox::Cancel == ret) {
339 return;
340 }
341 if (KMessageBox::ButtonCode::SecondaryAction == ret) {
342 m_encrypt = false;
343 m_sign = false;
344 } else {
345 Q_EMIT disableHtml(NoConfirmationNeeded);
346 }
347 }
348
349 if (m_neverEncrypt && saveIn != MessageComposer::MessageSender::SaveInNone) {
350 // we can't use the state of the mail itself, to remember the
351 // signing and encryption state, so let's add a header instead
352 DraftSignatureState(m_msg).setState(m_sign);
353 DraftEncryptionState(m_msg).setState(m_encrypt);
354 DraftCryptoMessageFormatState(m_msg).setState(m_cryptoMessageFormat);
355 } else {
356 removeDraftCryptoHeaders(m_msg);
357 }
358
359 if (mSendMethod == MessageComposer::MessageSender::SendImmediate && checkMailDispatcher) {
360 if (!MessageComposer::Util::sendMailDispatcherIsOnline(m_parentWidget)) {
361 qCWarning(MESSAGECOMPOSER_LOG) << "Impossible to set sendmaildispatcher online. Please verify it";
362 return;
363 }
364 }
365
366 readyForSending();
367}
368
369void ComposerViewBase::setCustomHeader(const QMap<QByteArray, QString> &customHeader)
370{
371 m_customHeader = customHeader;
372}
373
374void ComposerViewBase::readyForSending()
375{
376 qCDebug(MESSAGECOMPOSER_LOG) << "Entering readyForSending";
377 if (!m_msg) {
378 qCDebug(MESSAGECOMPOSER_LOG) << "m_msg == 0!";
379 return;
380 }
381
382 if (!m_composers.isEmpty()) {
383 // This may happen if e.g. the autosave timer calls applyChanges.
384 qCDebug(MESSAGECOMPOSER_LOG) << "ready for sending: Called while composer active; ignoring. Number of composer " << m_composers.count();
385 return;
386 }
387
388 // first, expand all addresses
389 auto job = new MessageComposer::EmailAddressResolveJob(this);
390 const auto identity = currentIdentity();
391 if (!identity.isNull()) {
392 job->setDefaultDomainName(identity.defaultDomainName());
393 }
394 job->setFrom(from());
395 job->setTo(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::To));
396 job->setCc(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::Cc));
397 job->setBcc(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::Bcc));
398 job->setReplyTo(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::ReplyTo));
399
400 connect(job, &MessageComposer::EmailAddressResolveJob::result, this, &ComposerViewBase::slotEmailAddressResolved);
401 job->start();
402}
403
404void ComposerViewBase::slotEmailAddressResolved(KJob *job)
405{
406 if (job->error()) {
407 qCWarning(MESSAGECOMPOSER_LOG) << "An error occurred while resolving the email addresses:" << job->errorString();
408 // This error could be caused by a broken search infrastructure, so we ignore it for now
409 // to not block sending emails completely.
410 }
411
412 bool autoresizeImage = MessageComposer::MessageComposerSettings::self()->autoResizeImageEnabled();
413
415 if (mSaveIn == MessageComposer::MessageSender::SaveInNone) {
416 mExpandedFrom = resolveJob->expandedFrom();
417 mExpandedTo = resolveJob->expandedTo();
418 mExpandedCc = resolveJob->expandedCc();
419 mExpandedBcc = resolveJob->expandedBcc();
420 mExpandedReplyTo = resolveJob->expandedReplyTo();
421 if (autoresizeImage) {
422 const QStringList listEmails = QStringList() << mExpandedFrom << mExpandedTo << mExpandedCc << mExpandedBcc << mExpandedReplyTo;
423 MessageComposer::Utils resizeUtils;
424 autoresizeImage = resizeUtils.filterRecipients(listEmails);
425 }
426 } else { // saved to draft, so keep the old values, not very nice.
427 mExpandedFrom = from();
428 const auto recipients{m_recipientsEditor->recipients()};
429 for (const MessageComposer::Recipient::Ptr &r : recipients) {
430 switch (r->type()) {
431 case MessageComposer::Recipient::To:
432 mExpandedTo << r->email();
433 break;
434 case MessageComposer::Recipient::Cc:
435 mExpandedCc << r->email();
436 break;
437 case MessageComposer::Recipient::Bcc:
438 mExpandedBcc << r->email();
439 break;
440 case MessageComposer::Recipient::ReplyTo:
441 mExpandedReplyTo << r->email();
442 break;
443 case MessageComposer::Recipient::Undefined:
444 Q_ASSERT(!"Unknown recipient type!");
445 break;
446 }
447 }
452 const auto expandedToLst{resolveJob->expandedTo()};
453 for (const QString &exp : expandedToLst) {
454 if (!mExpandedTo.contains(exp)) { // this address was expanded, so save it explicitly
455 unExpandedTo << exp;
456 }
457 }
458 const auto expandedCcLst{resolveJob->expandedCc()};
459 for (const QString &exp : expandedCcLst) {
460 if (!mExpandedCc.contains(exp)) {
461 unExpandedCc << exp;
462 }
463 }
464 const auto expandedBCcLst{resolveJob->expandedBcc()};
465 for (const QString &exp : expandedBCcLst) {
466 if (!mExpandedBcc.contains(exp)) { // this address was expanded, so save it explicitly
468 }
469 }
470 const auto expandedReplyLst{resolveJob->expandedReplyTo()};
471 for (const QString &exp : expandedReplyLst) {
472 if (!mExpandedReplyTo.contains(exp)) { // this address was expanded, so save it explicitly
474 }
475 }
476 auto header = new KMime::Headers::Generic("X-KMail-UnExpanded-To");
477 header->from7BitString(unExpandedTo.join(QLatin1StringView(", ")).toLatin1());
478 m_msg->setHeader(header);
479 header = new KMime::Headers::Generic("X-KMail-UnExpanded-CC");
480 header->from7BitString(unExpandedCc.join(QLatin1StringView(", ")).toLatin1());
481 m_msg->setHeader(header);
482 header = new KMime::Headers::Generic("X-KMail-UnExpanded-BCC");
483 header->from7BitString(unExpandedBcc.join(QLatin1StringView(", ")).toLatin1());
484 m_msg->setHeader(header);
485 header = new KMime::Headers::Generic("X-KMail-UnExpanded-Reply-To");
486 header->from7BitString(unExpandedReplyTo.join(QLatin1StringView(", ")).toLatin1());
487 m_msg->setHeader(header);
488 autoresizeImage = false;
489 }
490
491 Q_ASSERT(m_composers.isEmpty()); // composers should be empty. The caller of this function
492 // checks for emptiness before calling it
493 // so just ensure it actually is empty
494 // and document it
495 // we first figure out if we need to create multiple messages with different crypto formats
496 // if so, we create a composer per format
497 // if we aren't signing or encrypting, this just returns a single empty message
498 if (m_neverEncrypt && mSaveIn != MessageComposer::MessageSender::SaveInNone && !mSendLaterInfo) {
500 composer->setNoCrypto(true);
501 m_composers.append(composer);
502 } else {
503 bool wasCanceled = false;
504 m_composers = generateCryptoMessages(wasCanceled);
505 if (wasCanceled) {
506 return;
507 }
508 }
509
510 if (m_composers.isEmpty()) {
511 Q_EMIT failed(i18n("It was not possible to create a message composer."));
512 return;
513 }
514
515 if (autoresizeImage) {
516 if (MessageComposer::MessageComposerSettings::self()->askBeforeResizing()) {
517 if (m_attachmentModel) {
518 MessageComposer::Utils resizeUtils;
519 if (resizeUtils.containsImage(m_attachmentModel->attachments())) {
520 const int rc = KMessageBox::warningTwoActions(m_parentWidget,
521 i18n("Do you want to resize images?"),
522 i18nc("@title:window", "Auto Resize Images"),
523 KGuiItem(i18nc("@action:button", "Auto Resize")),
524 KGuiItem(i18nc("@action:button", "Do Not Resize")));
525 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
526 autoresizeImage = true;
527 } else {
528 autoresizeImage = false;
529 }
530 } else {
531 autoresizeImage = false;
532 }
533 }
534 }
535 }
536 // Compose each message and prepare it for queueing, sending, or storing
537
538 // working copy in case composers instantly emit result
539 const auto composers = m_composers;
541 fillComposer(composer, UseExpandedRecipients, autoresizeImage);
542 connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotSendComposeResult);
543 composer->start();
544 qCDebug(MESSAGECOMPOSER_LOG) << "Started a composer for sending!";
545 }
546}
547
548namespace
549{
550// helper methods for reading encryption settings
551
552inline Kleo::chrono::days encryptOwnKeyNearExpiryWarningThresholdInDays()
553{
554 if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
555 return Kleo::chrono::days{-1};
556 }
557 const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnOwnEncrKeyNearExpiryThresholdDays();
558 return Kleo::chrono::days{qMax(1, num)};
559}
560
561inline Kleo::chrono::days encryptKeyNearExpiryWarningThresholdInDays()
562{
563 if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
564 return Kleo::chrono::days{-1};
565 }
566 const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrKeyNearExpiryThresholdDays();
567 return Kleo::chrono::days{qMax(1, num)};
568}
569
570inline Kleo::chrono::days encryptRootCertNearExpiryWarningThresholdInDays()
571{
572 if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
573 return Kleo::chrono::days{-1};
574 }
575 const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrRootNearExpiryThresholdDays();
576 return Kleo::chrono::days{qMax(1, num)};
577}
578
579inline Kleo::chrono::days encryptChainCertNearExpiryWarningThresholdInDays()
580{
581 if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
582 return Kleo::chrono::days{-1};
583 }
584 const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrChaincertNearExpiryThresholdDays();
585 return Kleo::chrono::days{qMax(1, num)};
586}
587
588inline bool showKeyApprovalDialog()
589{
590 return MessageComposer::MessageComposerSettings::self()->cryptoShowKeysForApproval();
591}
592
593inline bool cryptoWarningUnsigned(const KIdentityManagementCore::Identity &identity)
594{
595 if (identity.encryptionOverride()) {
596 return identity.warnNotSign();
597 }
598 return MessageComposer::MessageComposerSettings::self()->cryptoWarningUnsigned();
599}
600
601inline bool cryptoWarningUnencrypted(const KIdentityManagementCore::Identity &identity)
602{
603 if (identity.encryptionOverride()) {
604 return identity.warnNotEncrypt();
605 }
606 return MessageComposer::MessageComposerSettings::self()->cryptoWarningUnencrypted();
607}
608} // nameless namespace
609
610bool ComposerViewBase::addKeysToContext(const QString &gnupgHome,
611 const QList<QPair<QStringList, std::vector<GpgME::Key>>> &data,
612 const std::map<QByteArray, QString> &autocryptMap)
613{
614 bool needSpecialContext = false;
615
616 for (const auto &p : data) {
617 for (const auto &k : p.second) {
618 const auto it = autocryptMap.find(k.primaryFingerprint());
619 if (it != autocryptMap.end()) {
620 needSpecialContext = true;
621 break;
622 }
623 }
624 if (needSpecialContext) {
625 break;
626 }
627 }
628
629 if (!needSpecialContext) {
630 return false;
631 }
632 const QGpgME::Protocol *proto(QGpgME::openpgp());
633
634 const auto storage = MessageCore::AutocryptStorage::self();
635 QEventLoop loop;
636 int runningJobs = 0;
637 for (const auto &p : data) {
638 for (const auto &k : p.second) {
639 const auto it = autocryptMap.find(k.primaryFingerprint());
640 if (it == autocryptMap.end()) {
641 qCDebug(MESSAGECOMPOSER_LOG) << "Adding " << k.primaryFingerprint() << "via Export/Import";
642 auto exportJob = proto->publicKeyExportJob(false);
644 &QGpgME::ExportJob::result,
645 [&gnupgHome, &proto, &runningJobs, &loop, &k](const GpgME::Error &result,
646 const QByteArray &keyData,
647 const QString &auditLogAsHtml,
648 const GpgME::Error &auditLogError) {
649 Q_UNUSED(auditLogAsHtml);
650 Q_UNUSED(auditLogError);
651 if (result) {
652 qCWarning(MESSAGECOMPOSER_LOG) << "Failed to export " << k.primaryFingerprint() << result.asString();
653 --runningJobs;
654 if (runningJobs < 1) {
655 loop.quit();
656 }
657 }
658
659 auto importJob = proto->importJob();
660 QGpgME::Job::context(importJob)->setEngineHomeDirectory(gnupgHome.toUtf8().constData());
661 importJob->exec(keyData);
662 importJob->deleteLater();
663 --runningJobs;
664 if (runningJobs < 1) {
665 loop.quit();
666 }
667 });
668 QStringList patterns;
669 patterns << QString::fromUtf8(k.primaryFingerprint());
670 runningJobs++;
671 exportJob->start(patterns);
672 exportJob->setExportFlags(GpgME::Context::ExportMinimal);
673 } else {
674 qCDebug(MESSAGECOMPOSER_LOG) << "Adding " << k.primaryFingerprint() << "from Autocrypt storage";
675 const auto recipient = storage->getRecipient(it->second.toUtf8());
676 auto key = recipient->gpgKey();
677 auto keydata = recipient->gpgKeydata();
678 if (QByteArray(key.primaryFingerprint()) != QByteArray(k.primaryFingerprint())) {
679 qCDebug(MESSAGECOMPOSER_LOG) << "Using gossipkey";
680 keydata = recipient->gossipKeydata();
681 }
682 auto importJob = proto->importJob();
683 QGpgME::Job::context(importJob)->setEngineHomeDirectory(gnupgHome.toUtf8().constData());
684 const auto result = importJob->exec(keydata);
685 importJob->deleteLater();
686 }
687 }
688 }
689 loop.exec();
690 return true;
691}
692
693void ComposerViewBase::setAkonadiLookupEnabled(bool akonadiLookupEnabled)
694{
695 m_akonadiLookupEnabled = akonadiLookupEnabled;
696}
697
698QList<MessageComposer::Composer *> ComposerViewBase::generateCryptoMessages(bool &wasCanceled)
699{
700 const auto id = currentIdentity();
701
702 bool canceled = false;
703
704 qCDebug(MESSAGECOMPOSER_LOG) << "filling crypto info";
705 connect(expiryChecker().get(),
706 &Kleo::ExpiryChecker::expiryMessage,
707 this,
708 [&canceled](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) {
709 if (!isNewMessage) {
710 return;
711 }
712
713 if (canceled) {
714 return;
715 }
716 QString title;
718 if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) {
719 dontAskAgainName = QStringLiteral("own key expires soon warning");
720 } else {
721 dontAskAgainName = QStringLiteral("other encryption key near expiry warning");
722 }
723 if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OtherKeyExpired) {
724 title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expired") : i18n("S/MIME Certificate Expired");
725 } else {
726 title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expires Soon") : i18n("S/MIME Certificate Expires Soon");
727 }
730 canceled = true;
731 }
732 });
733
735 new Kleo::KeyResolver(true, showKeyApprovalDialog(), id.pgpAutoEncrypt(), m_cryptoMessageFormat, expiryChecker()));
736
737 keyResolver->setAutocryptEnabled(autocryptEnabled());
738 keyResolver->setAkonadiLookupEnabled(m_akonadiLookupEnabled);
739
741 QStringList signKeys;
742
743 bool signSomething = m_sign;
744 bool doSignCompletely = m_sign;
745 bool encryptSomething = m_encrypt;
746 bool doEncryptCompletely = m_encrypt;
747
748 // Add encryptionkeys from id to keyResolver
749 if (!id.pgpEncryptionKey().isEmpty()) {
750 encryptToSelfKeys.push_back(QLatin1StringView(id.pgpEncryptionKey()));
751 }
752 if (!id.smimeEncryptionKey().isEmpty()) {
753 encryptToSelfKeys.push_back(QLatin1StringView(id.smimeEncryptionKey()));
754 }
755 if (canceled || keyResolver->setEncryptToSelfKeys(encryptToSelfKeys) != Kleo::Ok) {
756 qCDebug(MESSAGECOMPOSER_LOG) << "Failed to set encryptoToSelf keys!";
757 return {};
758 }
759
760 // Add signingkeys from id to keyResolver
761 if (!id.pgpSigningKey().isEmpty()) {
762 signKeys.push_back(QLatin1StringView(id.pgpSigningKey()));
763 }
764 if (!id.smimeSigningKey().isEmpty()) {
765 signKeys.push_back(QLatin1StringView(id.smimeSigningKey()));
766 }
767 if (canceled || keyResolver->setSigningKeys(signKeys) != Kleo::Ok) {
768 qCDebug(MESSAGECOMPOSER_LOG) << "Failed to set signing keys!";
769 return {};
770 }
771
772 if (m_attachmentModel) {
773 const auto attachments = m_attachmentModel->attachments();
774 for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) {
775 if (attachment->isSigned()) {
776 signSomething = true;
777 } else {
778 doEncryptCompletely = false;
779 }
780 if (attachment->isEncrypted()) {
781 encryptSomething = true;
782 } else {
783 doSignCompletely = false;
784 }
785 }
786 }
787
788 const QStringList recipients = mExpandedTo + mExpandedCc;
789 const QStringList bcc(mExpandedBcc);
790
791 keyResolver->setPrimaryRecipients(recipients);
792 keyResolver->setSecondaryRecipients(bcc);
793
794 bool result = true;
795 canceled = false;
796 signSomething = determineWhetherToSign(doSignCompletely, keyResolver.data(), signSomething, result, canceled);
797 if (!result) {
798 // TODO handle failure
799 qCDebug(MESSAGECOMPOSER_LOG) << "determineWhetherToSign: failed to resolve keys! oh noes";
800 if (!canceled) {
801 Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug."));
802 } else {
804 }
805 wasCanceled = canceled;
806 return {};
807 }
808
809 canceled = false;
810 encryptSomething = determineWhetherToEncrypt(doEncryptCompletely, keyResolver.data(), encryptSomething, signSomething, result, canceled);
811 if (!result) {
812 // TODO handle failure
813 qCDebug(MESSAGECOMPOSER_LOG) << "determineWhetherToEncrypt: failed to resolve keys! oh noes";
814 if (!canceled) {
815 Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug."));
816 } else {
818 }
819
820 wasCanceled = canceled;
821 return {};
822 }
823
825
826 // No encryption or signing is needed
829 if (m_cryptoMessageFormat & Kleo::OpenPGPMIMEFormat) {
830 composer->setAutocryptEnabled(autocryptEnabled());
831 if (keyResolver->encryptToSelfKeysFor(Kleo::OpenPGPMIMEFormat).size() > 0) {
832 composer->setSenderEncryptionKey(keyResolver->encryptToSelfKeysFor(Kleo::OpenPGPMIMEFormat)[0]);
833 }
834 }
835 composers.append(composer);
836 return composers;
837 }
838
839 canceled = false;
840 const Kleo::Result kpgpResult = keyResolver->resolveAllKeys(signSomething, encryptSomething);
841 if (kpgpResult == Kleo::Canceled || canceled) {
842 qCDebug(MESSAGECOMPOSER_LOG) << "resolveAllKeys: one key resolution canceled by user";
843 return {};
844 } else if (kpgpResult != Kleo::Ok) {
845 // TODO handle failure
846 qCDebug(MESSAGECOMPOSER_LOG) << "resolveAllKeys: failed to resolve keys! oh noes";
847 Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug."));
848 return {};
849 }
850
851 qCDebug(MESSAGECOMPOSER_LOG) << "done resolving keys.";
852
854 Kleo::CryptoMessageFormat concreteFormat = Kleo::AutoFormat;
855 for (unsigned int i = 0; i < numConcreteCryptoMessageFormats; ++i) {
856 concreteFormat = concreteCryptoMessageFormats[i];
857 const auto encData = keyResolver->encryptionItems(concreteFormat);
858 if (encData.empty()) {
859 continue;
860 }
861
862 if (!(concreteFormat & m_cryptoMessageFormat)) {
863 continue;
864 }
865
867
868 if (encryptSomething || autocryptEnabled()) {
869 auto end(encData.end());
871 data.reserve(encData.size());
872 for (auto it = encData.begin(); it != end; ++it) {
873 QPair<QStringList, std::vector<GpgME::Key>> p(it->recipients, it->keys);
874 data.append(p);
875 qCDebug(MESSAGECOMPOSER_LOG) << "got resolved keys for:" << it->recipients;
876 }
877 composer->setEncryptionKeys(data);
878 if (concreteFormat & Kleo::OpenPGPMIMEFormat && autocryptEnabled()) {
879 composer->setAutocryptEnabled(autocryptEnabled());
880 composer->setSenderEncryptionKey(keyResolver->encryptToSelfKeysFor(concreteFormat)[0]);
882 bool specialGnupgHome = addKeysToContext(dir.path(), data, keyResolver->useAutocrypt());
883 if (specialGnupgHome) {
884 dir.setAutoRemove(false);
885 composer->setGnupgHome(dir.path());
886 }
887 }
888 }
889
890 if (signSomething) {
891 // find signing keys for this format
892 std::vector<GpgME::Key> signingKeys = keyResolver->signingKeys(concreteFormat);
893 composer->setSigningKeys(signingKeys);
894 }
895
896 composer->setMessageCryptoFormat(concreteFormat);
897 composer->setSignAndEncrypt(signSomething, encryptSomething);
898
899 composers.append(composer);
900 }
901 } else {
903 composers.append(composer);
904 // If we canceled sign or encrypt be sure to change status in attachment.
905 markAllAttachmentsForSigning(false);
906 markAllAttachmentsForEncryption(false);
907 }
908
909 if (composers.isEmpty() && (signSomething || encryptSomething)) {
910 Q_ASSERT_X(false, "ComposerViewBase::generateCryptoMessages", "No concrete sign or encrypt method selected");
911 }
912
913 return composers;
914}
915
916void ComposerViewBase::fillGlobalPart(MessageComposer::GlobalPart *globalPart)
917{
918 globalPart->setParentWidgetForGui(m_parentWidget);
919 globalPart->setCharsets(m_charsets);
920 globalPart->setMDNRequested(m_mdnRequested);
921 globalPart->setRequestDeleveryConfirmation(m_requestDeleveryConfirmation);
922}
923
924void ComposerViewBase::fillInfoPart(MessageComposer::InfoPart *infoPart, ComposerViewBase::RecipientExpansion expansion)
925{
926 // TODO splitAddressList and expandAliases ugliness should be handled by a
927 // special AddressListEdit widget... (later: see RecipientsEditor)
928
929 if (m_fccCombo) {
930 infoPart->setFcc(QString::number(m_fccCombo->currentCollection().id()));
931 } else {
932 if (m_fccCollection.isValid()) {
933 infoPart->setFcc(QString::number(m_fccCollection.id()));
934 }
935 }
936
937 infoPart->setTransportId(m_transport->currentTransportId());
938 if (expansion == UseExpandedRecipients) {
939 infoPart->setFrom(mExpandedFrom);
940 infoPart->setTo(mExpandedTo);
941 infoPart->setCc(mExpandedCc);
942 infoPart->setBcc(mExpandedBcc);
943 infoPart->setReplyTo(mExpandedReplyTo);
944 } else {
945 infoPart->setFrom(from());
946 infoPart->setTo(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::To));
947 infoPart->setCc(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::Cc));
948 infoPart->setBcc(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::Bcc));
949 infoPart->setReplyTo(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::ReplyTo));
950 }
951 infoPart->setSubject(subject());
952 infoPart->setUserAgent(QStringLiteral("KMail"));
953 infoPart->setUrgent(m_urgent);
954
955 if (auto inReplyTo = m_msg->inReplyTo(false)) {
956 infoPart->setInReplyTo(inReplyTo->asUnicodeString());
957 }
958
959 if (auto references = m_msg->references(false)) {
960 infoPart->setReferences(references->asUnicodeString());
961 }
962
964 if (auto hdr = m_msg->headerByType("X-KMail-SignatureActionEnabled")) {
965 extras << hdr;
966 }
967 if (auto hdr = m_msg->headerByType("X-KMail-EncryptActionEnabled")) {
968 extras << hdr;
969 }
970 if (auto hdr = m_msg->headerByType("X-KMail-CryptoMessageFormat")) {
971 extras << hdr;
972 }
973 if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-To")) {
974 extras << hdr;
975 }
976 if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-CC")) {
977 extras << hdr;
978 }
979 if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-BCC")) {
980 extras << hdr;
981 }
982 if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-Reply-To")) {
983 extras << hdr;
984 }
985 if (auto hdr = m_msg->organization(false)) {
986 extras << hdr;
987 }
988 if (auto hdr = m_msg->headerByType("X-KMail-Identity")) {
989 extras << hdr;
990 }
991 if (auto hdr = m_msg->headerByType("X-KMail-Transport")) {
992 extras << hdr;
993 }
994 if (auto hdr = m_msg->headerByType("X-KMail-Fcc")) {
995 extras << hdr;
996 }
997 if (auto hdr = m_msg->headerByType("X-KMail-Drafts")) {
998 extras << hdr;
999 }
1000 if (auto hdr = m_msg->headerByType("X-KMail-Templates")) {
1001 extras << hdr;
1002 }
1003 if (auto hdr = m_msg->headerByType("X-KMail-Link-Message")) {
1004 extras << hdr;
1005 }
1006 if (auto hdr = m_msg->headerByType("X-KMail-Link-Type")) {
1007 extras << hdr;
1008 }
1009 if (auto hdr = m_msg->headerByType("X-Face")) {
1010 extras << hdr;
1011 }
1012 if (auto hdr = m_msg->headerByType("Face")) {
1013 extras << hdr;
1014 }
1015 if (auto hdr = m_msg->headerByType("X-KMail-FccDisabled")) {
1016 extras << hdr;
1017 }
1018 if (auto hdr = m_msg->headerByType("X-KMail-Identity-Name")) {
1019 extras << hdr;
1020 }
1021 if (auto hdr = m_msg->headerByType("X-KMail-Transport-Name")) {
1022 extras << hdr;
1023 }
1024
1025 infoPart->setExtraHeaders(extras);
1026}
1027
1028void ComposerViewBase::slotSendComposeResult(KJob *job)
1029{
1030 Q_ASSERT(dynamic_cast<MessageComposer::Composer *>(job));
1031 auto composer = static_cast<MessageComposer::Composer *>(job);
1032 if (composer->error() != MessageComposer::Composer::NoError) {
1033 qCDebug(MESSAGECOMPOSER_LOG) << "compose job might have error: " << job->error() << " errorString: " << job->errorString();
1034 }
1035
1036 if (composer->error() == MessageComposer::Composer::NoError) {
1037 Q_ASSERT(m_composers.contains(composer));
1038 // The messages were composed successfully.
1039 qCDebug(MESSAGECOMPOSER_LOG) << "NoError.";
1040 const int numberOfMessage(composer->resultMessages().size());
1041 for (int i = 0; i < numberOfMessage; ++i) {
1042 if (mSaveIn == MessageComposer::MessageSender::SaveInNone) {
1043 queueMessage(composer->resultMessages().at(i), composer);
1044 } else {
1045 saveMessage(composer->resultMessages().at(i), mSaveIn);
1046 }
1047 }
1048 saveRecentAddresses(composer->resultMessages().at(0));
1049 } else if (composer->error() == MessageComposer::Composer::UserCancelledError) {
1050 // The job warned the user about something, and the user chose to return
1051 // to the message. Nothing to do.
1052 qCDebug(MESSAGECOMPOSER_LOG) << "UserCancelledError.";
1053 Q_EMIT failed(i18n("Job cancelled by the user"));
1054 } else {
1055 qCDebug(MESSAGECOMPOSER_LOG) << "other Error." << composer->error();
1056 QString msg;
1057 if (composer->error() == MessageComposer::Composer::BugError) {
1058 msg = i18n("Could not compose message: %1 \n Please report this bug.", job->errorString());
1059 } else {
1060 msg = i18n("Could not compose message: %1", job->errorString());
1061 }
1062 Q_EMIT failed(msg);
1063 }
1064
1065 if (!composer->gnupgHome().isEmpty()) {
1066 QDir dir(composer->gnupgHome());
1067 dir.removeRecursively();
1068 }
1069
1070 m_composers.removeAll(composer);
1071}
1072
1073void ComposerViewBase::saveRecentAddresses(const KMime::Message::Ptr &msg)
1074{
1075 KConfig *config = MessageComposer::MessageComposerSettings::self()->config();
1076 if (auto to = msg->to(false)) {
1077 const auto toAddresses = to->mailboxes();
1078 for (const auto &address : toAddresses) {
1079 PimCommon::RecentAddresses::self(config)->add(address.prettyAddress());
1080 }
1081 }
1082 if (auto cc = msg->cc(false)) {
1083 const auto ccAddresses = cc->mailboxes();
1084 for (const auto &address : ccAddresses) {
1085 PimCommon::RecentAddresses::self(config)->add(address.prettyAddress());
1086 }
1087 }
1088 if (auto bcc = msg->bcc(false)) {
1089 const auto bccAddresses = bcc->mailboxes();
1090 for (const auto &address : bccAddresses) {
1091 PimCommon::RecentAddresses::self(config)->add(address.prettyAddress());
1092 }
1093 }
1094}
1095
1096void ComposerViewBase::queueMessage(const KMime::Message::Ptr &message, MessageComposer::Composer *composer)
1097{
1098 const MessageComposer::InfoPart *infoPart = composer->infoPart();
1099 auto qjob = new Akonadi::MessageQueueJob(this);
1100 qjob->setMessage(message);
1101 qjob->transportAttribute().setTransportId(infoPart->transportId());
1102 if (mSendMethod == MessageComposer::MessageSender::SendLater) {
1103 qjob->dispatchModeAttribute().setDispatchMode(Akonadi::DispatchModeAttribute::Manual);
1104 }
1105
1106 if (message->hasHeader("X-KMail-FccDisabled")) {
1107 qjob->sentBehaviourAttribute().setSentBehaviour(Akonadi::SentBehaviourAttribute::Delete);
1108 } else if (!infoPart->fcc().isEmpty()) {
1109 qjob->sentBehaviourAttribute().setSentBehaviour(Akonadi::SentBehaviourAttribute::MoveToCollection);
1110
1111 const Akonadi::Collection sentCollection(infoPart->fcc().toLongLong());
1112 qjob->sentBehaviourAttribute().setMoveToCollection(sentCollection);
1113 } else {
1114 qjob->sentBehaviourAttribute().setSentBehaviour(Akonadi::SentBehaviourAttribute::MoveToDefaultSentCollection);
1115 }
1116
1117 MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportById(infoPart->transportId());
1118 if (transport && transport->specifySenderOverwriteAddress()) {
1119 qjob->addressAttribute().setFrom(
1121 } else {
1123 }
1124 // if this header is not empty, it contains the real recipient of the message, either the primary or one of the
1125 // secondary recipients. so we set that to the transport job, while leaving the message itself alone.
1126 if (KMime::Headers::Base *realTo = message->headerByType("X-KMail-EncBccRecipients")) {
1127 qjob->addressAttribute().setTo(MessageComposer::Util::cleanUpEmailListAndEncoding(realTo->asUnicodeString().split(QLatin1Char('%'))));
1128 message->removeHeader("X-KMail-EncBccRecipients");
1129 message->assemble();
1130 qCDebug(MESSAGECOMPOSER_LOG) << "sending with-bcc encr mail to a/n recipient:" << qjob->addressAttribute().to();
1131 } else {
1132 qjob->addressAttribute().setTo(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->to()));
1133 qjob->addressAttribute().setCc(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->cc()));
1134 qjob->addressAttribute().setBcc(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->bcc()));
1135 }
1136 if (m_requestDeleveryConfirmation) {
1137 qjob->addressAttribute().setDeliveryStatusNotification(true);
1138 }
1139 MessageComposer::Util::addSendReplyForwardAction(message, qjob);
1141
1142 MessageComposer::Util::addCustomHeaders(message, m_customHeader);
1143 message->assemble();
1144 connect(qjob, &Akonadi::MessageQueueJob::result, this, &ComposerViewBase::slotQueueResult);
1145 m_pendingQueueJobs++;
1146 qjob->start();
1147
1148 qCDebug(MESSAGECOMPOSER_LOG) << "Queued a message.";
1149}
1150
1151void ComposerViewBase::slotQueueResult(KJob *job)
1152{
1153 m_pendingQueueJobs--;
1154 auto qjob = static_cast<Akonadi::MessageQueueJob *>(job);
1155 qCDebug(MESSAGECOMPOSER_LOG) << "mPendingQueueJobs" << m_pendingQueueJobs;
1156 Q_ASSERT(m_pendingQueueJobs >= 0);
1157
1158 if (job->error()) {
1159 qCDebug(MESSAGECOMPOSER_LOG) << "Failed to queue a message:" << job->errorString();
1160 // There is not much we can do now, since all the MessageQueueJobs have been
1161 // started. So just wait for them to finish.
1162 // TODO show a message box or something
1163 const QString msg = i18n("There were problems trying to queue the message for sending: %1", job->errorString());
1164
1165 if (m_pendingQueueJobs == 0) {
1166 Q_EMIT failed(msg);
1167 return;
1168 }
1169 }
1170
1171 if (m_pendingQueueJobs == 0) {
1172 addFollowupReminder(qjob->message()->messageID(false)->asUnicodeString());
1174 }
1175}
1176
1177void ComposerViewBase::initAutoSave()
1178{
1179 qCDebug(MESSAGECOMPOSER_LOG) << "initialising autosave";
1180
1181 // Ensure that the autosave directory exists.
1183 if (!dataDirectory.exists(QStringLiteral("autosave"))) {
1184 qCDebug(MESSAGECOMPOSER_LOG) << "Creating autosave directory.";
1185 dataDirectory.mkdir(QStringLiteral("autosave"));
1186 }
1187
1188 // Construct a file name
1189 if (m_autoSaveUUID.isEmpty()) {
1190 m_autoSaveUUID = QUuid::createUuid().toString();
1191 }
1192
1194}
1195
1196Akonadi::Collection ComposerViewBase::followUpCollection() const
1197{
1198 return mFollowUpCollection;
1199}
1200
1201void ComposerViewBase::setFollowUpCollection(const Akonadi::Collection &followUpCollection)
1202{
1203 mFollowUpCollection = followUpCollection;
1204}
1205
1206QDate ComposerViewBase::followUpDate() const
1207{
1208 return mFollowUpDate;
1209}
1210
1211void ComposerViewBase::setFollowUpDate(const QDate &followUpDate)
1212{
1213 mFollowUpDate = followUpDate;
1214}
1215
1216Sonnet::DictionaryComboBox *ComposerViewBase::dictionary() const
1217{
1218 return m_dictionary;
1219}
1220
1221void ComposerViewBase::setDictionary(Sonnet::DictionaryComboBox *dictionary)
1222{
1223 m_dictionary = dictionary;
1224}
1225
1227{
1228 if (m_autoSaveInterval == 0) {
1229 delete m_autoSaveTimer;
1230 m_autoSaveTimer = nullptr;
1231 } else {
1232 if (!m_autoSaveTimer) {
1233 m_autoSaveTimer = new QTimer(this);
1234 if (m_parentWidget) {
1235 connect(m_autoSaveTimer, SIGNAL(timeout()), m_parentWidget, SLOT(autoSaveMessage()));
1236 } else {
1238 }
1239 }
1240 m_autoSaveTimer->start(m_autoSaveInterval);
1241 }
1242}
1243
1245{
1246 delete m_autoSaveTimer;
1247 m_autoSaveTimer = nullptr;
1248 if (!m_autoSaveUUID.isEmpty()) {
1249 qCDebug(MESSAGECOMPOSER_LOG) << "deleting autosave files" << m_autoSaveUUID;
1250
1251 // Delete the autosave files
1253
1254 // Filter out only this composer window's autosave files
1255 const QStringList autoSaveFilter{m_autoSaveUUID + QLatin1StringView("*")};
1256 autoSaveDir.setNameFilters(autoSaveFilter);
1257
1258 // Return the files to be removed
1259 const QStringList autoSaveFiles = autoSaveDir.entryList();
1260 qCDebug(MESSAGECOMPOSER_LOG) << "There are" << autoSaveFiles.count() << "to be deleted.";
1261
1262 // Delete each file
1263 for (const QString &file : autoSaveFiles) {
1264 autoSaveDir.remove(file);
1265 }
1266 m_autoSaveUUID.clear();
1267 }
1268}
1269
1270//-----------------------------------------------------------------------------
1272{
1273 qCDebug(MESSAGECOMPOSER_LOG) << "Autosaving message";
1274
1275 if (m_autoSaveTimer) {
1276 m_autoSaveTimer->stop();
1277 }
1278
1279 if (!m_composers.isEmpty()) {
1280 // This may happen if e.g. the autosave timer calls applyChanges.
1281 qCDebug(MESSAGECOMPOSER_LOG) << "Autosave: Called while composer active; ignoring. Number of composer " << m_composers.count();
1282 return;
1283 }
1284
1285 auto composer = new Composer();
1286 fillComposer(composer);
1287 composer->setAutoSave(true);
1288 composer->setAutocryptEnabled(autocryptEnabled());
1289 m_composers.append(composer);
1290 connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotAutoSaveComposeResult);
1291 composer->start();
1292}
1293
1295{
1296 m_autoSaveUUID = fileName;
1297
1298 Q_EMIT modified(true);
1299}
1300
1301void ComposerViewBase::slotAutoSaveComposeResult(KJob *job)
1302{
1304
1305 Q_ASSERT(dynamic_cast<Composer *>(job));
1306 auto composer = static_cast<Composer *>(job);
1307
1308 if (composer->error() == Composer::NoError) {
1309 Q_ASSERT(m_composers.contains(composer));
1310
1311 // The messages were composed successfully. Only save the first message, there should
1312 // only be one anyway, since crypto is disabled.
1313 qCDebug(MESSAGECOMPOSER_LOG) << "NoError.";
1314 writeAutoSaveToDisk(composer->resultMessages().constFirst());
1315 Q_ASSERT(composer->resultMessages().size() == 1);
1316
1317 if (m_autoSaveInterval > 0) {
1319 }
1320 } else if (composer->error() == MessageComposer::Composer::UserCancelledError) {
1321 // The job warned the user about something, and the user chose to return
1322 // to the message. Nothing to do.
1323 qCDebug(MESSAGECOMPOSER_LOG) << "UserCancelledError.";
1324 Q_EMIT failed(i18n("Job cancelled by the user"), AutoSave);
1325 } else {
1326 qCDebug(MESSAGECOMPOSER_LOG) << "other Error.";
1327 Q_EMIT failed(i18n("Could not autosave message: %1", job->errorString()), AutoSave);
1328 }
1329
1330 m_composers.removeAll(composer);
1331}
1332
1333void ComposerViewBase::writeAutoSaveToDisk(const KMime::Message::Ptr &message)
1334{
1337 const QString filename = autosavePath + m_autoSaveUUID;
1338 QSaveFile file(filename);
1340 qCDebug(MESSAGECOMPOSER_LOG) << "Writing message to disk as" << filename;
1341
1342 if (file.open(QIODevice::WriteOnly)) {
1343 file.setPermissions(QFile::ReadUser | QFile::WriteUser);
1344
1345 if (file.write(message->encodedContent()) != static_cast<qint64>(message->encodedContent().size())) {
1346 errorMessage = i18n("Could not write all data to file.");
1347 } else {
1348 if (!file.commit()) {
1349 errorMessage = i18n("Could not finalize the file.");
1350 }
1351 }
1352 } else {
1353 errorMessage = i18n("Could not open file.");
1354 }
1355
1356 if (!errorMessage.isEmpty()) {
1357 qCWarning(MESSAGECOMPOSER_LOG) << "Auto saving failed:" << errorMessage << file.errorString() << " m_autoSaveUUID" << m_autoSaveUUID;
1358 if (!m_autoSaveErrorShown) {
1359 KMessageBox::error(m_parentWidget,
1360 i18n("Autosaving the message as %1 failed.\n"
1361 "%2\n"
1362 "Reason: %3",
1363 filename,
1364 errorMessage,
1365 file.errorString()),
1366 i18nc("@title:window", "Autosaving Message Failed"));
1367
1368 // Error dialog shown, hide the errors the next time
1369 m_autoSaveErrorShown = true;
1370 }
1371 } else {
1372 // No error occurred, the next error should be shown again
1373 m_autoSaveErrorShown = false;
1374 }
1375 file.commit();
1376 message->clear();
1377}
1378
1379void ComposerViewBase::saveMessage(const KMime::Message::Ptr &message, MessageComposer::MessageSender::SaveIn saveIn)
1380{
1381 Akonadi::Collection target;
1382 const auto identity = currentIdentity();
1383 message->date()->setDateTime(QDateTime::currentDateTime());
1384 if (!identity.isNull()) {
1385 if (auto header = message->headerByType("X-KMail-Fcc")) {
1386 const int sentCollectionId = header->asUnicodeString().toInt();
1387 if (identity.fcc() == QString::number(sentCollectionId)) {
1388 message->removeHeader("X-KMail-Fcc");
1389 }
1390 }
1391 }
1392 MessageComposer::Util::addCustomHeaders(message, m_customHeader);
1393
1394 message->assemble();
1395
1396 Akonadi::Item item;
1397 item.setMimeType(QStringLiteral("message/rfc822"));
1398 item.setPayload(message);
1400
1401 if (!identity.isNull()) { // we have a valid identity
1402 switch (saveIn) {
1403 case MessageComposer::MessageSender::SaveInTemplates:
1404 if (!identity.templates().isEmpty()) { // the user has specified a custom templates collection
1405 target = Akonadi::Collection(identity.templates().toLongLong());
1406 }
1407 break;
1408 case MessageComposer::MessageSender::SaveInDrafts:
1409 if (!identity.drafts().isEmpty()) { // the user has specified a custom drafts collection
1410 target = Akonadi::Collection(identity.drafts().toLongLong());
1411 }
1412 break;
1413 case MessageComposer::MessageSender::SaveInOutbox: // We don't define save outbox in identity
1415 break;
1416 case MessageComposer::MessageSender::SaveInNone:
1417 break;
1418 }
1419
1421 saveMessageJob->setProperty("Akonadi::Item", QVariant::fromValue(item));
1422 QObject::connect(saveMessageJob, &Akonadi::CollectionFetchJob::result, this, &ComposerViewBase::slotSaveMessage);
1423 } else {
1424 // preinitialize with the default collections
1425 target = defaultSpecialTarget();
1426 auto create = new Akonadi::ItemCreateJob(item, target, this);
1427 connect(create, &Akonadi::ItemCreateJob::result, this, &ComposerViewBase::slotCreateItemResult);
1428 ++m_pendingQueueJobs;
1429 }
1430}
1431
1432void ComposerViewBase::slotSaveMessage(KJob *job)
1433{
1434 Akonadi::Collection target;
1435 auto item = job->property("Akonadi::Item").value<Akonadi::Item>();
1436 if (job->error()) {
1437 target = defaultSpecialTarget();
1438 } else {
1440 if (fetchJob->collections().isEmpty()) {
1441 target = defaultSpecialTarget();
1442 } else {
1443 target = fetchJob->collections().at(0);
1444 }
1445 }
1446 auto create = new Akonadi::ItemCreateJob(item, target, this);
1447 connect(create, &Akonadi::ItemCreateJob::result, this, &ComposerViewBase::slotCreateItemResult);
1448 ++m_pendingQueueJobs;
1449}
1450
1451Akonadi::Collection ComposerViewBase::defaultSpecialTarget() const
1452{
1453 Akonadi::Collection target;
1454 switch (mSaveIn) {
1455 case MessageComposer::MessageSender::SaveInNone:
1456 break;
1457 case MessageComposer::MessageSender::SaveInDrafts:
1459 break;
1460 case MessageComposer::MessageSender::SaveInTemplates:
1462 break;
1463 case MessageComposer::MessageSender::SaveInOutbox:
1465 break;
1466 }
1467
1468 return target;
1469}
1470
1471void ComposerViewBase::slotCreateItemResult(KJob *job)
1472{
1473 --m_pendingQueueJobs;
1474 qCDebug(MESSAGECOMPOSER_LOG) << "mPendingCreateItemJobs" << m_pendingQueueJobs;
1475 Q_ASSERT(m_pendingQueueJobs >= 0);
1476
1477 if (job->error()) {
1478 qCWarning(MESSAGECOMPOSER_LOG) << "Failed to save a message:" << job->errorString();
1479 Q_EMIT failed(i18n("Failed to save the message: %1", job->errorString()));
1480 return;
1481 }
1482
1483 Akonadi::Item::Id id = -1;
1484 if (mSendLaterInfo) {
1485 auto createJob = static_cast<Akonadi::ItemCreateJob *>(job);
1486 const Akonadi::Item item = createJob->item();
1487 if (item.isValid()) {
1488 id = item.id();
1489 addSendLaterItem(item);
1490 }
1491 }
1492
1493 if (m_pendingQueueJobs == 0) {
1495 }
1496}
1497
1498void ComposerViewBase::addAttachment(const QUrl &url, const QString &comment, bool sync)
1499{
1500 Q_UNUSED(comment)
1501 qCDebug(MESSAGECOMPOSER_LOG) << "adding attachment with url:" << url;
1502 if (sync) {
1503 m_attachmentController->addAttachmentUrlSync(url);
1504 } else {
1505 m_attachmentController->addAttachment(url);
1506 }
1507}
1508
1509void ComposerViewBase::addAttachment(const QString &name, const QString &filename, const QString &charset, const QByteArray &data, const QByteArray &mimeType)
1510{
1512 if (!data.isEmpty()) {
1513 attachment->setName(name);
1514 attachment->setFileName(filename);
1515 attachment->setData(data);
1516 attachment->setCharset(charset.toLatin1());
1517 attachment->setMimeType(mimeType);
1518 // TODO what about the other fields?
1519
1520 m_attachmentController->addAttachment(attachment);
1521 }
1522}
1523
1524void ComposerViewBase::addAttachmentPart(KMime::Content *partToAttach)
1525{
1527 if (partToAttach->contentType()->mimeType() == "multipart/digest" || partToAttach->contentType(false)->mimeType() == "message/rfc822") {
1528 // if it is a digest or a full message, use the encodedContent() of the attachment,
1529 // which already has the proper headers
1530 part->setData(partToAttach->encodedContent());
1531 } else {
1532 part->setData(partToAttach->decodedContent());
1533 }
1534 part->setMimeType(partToAttach->contentType(false)->mimeType());
1535 if (auto cd = partToAttach->contentDescription(false)) {
1536 part->setDescription(cd->asUnicodeString());
1537 }
1538 if (auto ct = partToAttach->contentType(false)) {
1539 if (ct->hasParameter(QStringLiteral("name"))) {
1540 part->setName(ct->parameter(QStringLiteral("name")));
1541 }
1542 }
1543 if (auto cd = partToAttach->contentDisposition(false)) {
1544 part->setFileName(cd->filename());
1545 part->setInline(cd->disposition() == KMime::Headers::CDinline);
1546 }
1547 if (part->name().isEmpty() && !part->fileName().isEmpty()) {
1548 part->setName(part->fileName());
1549 }
1550 if (part->fileName().isEmpty() && !part->name().isEmpty()) {
1551 part->setFileName(part->name());
1552 }
1553 m_attachmentController->addAttachment(part);
1554}
1555
1556void ComposerViewBase::fillComposer(MessageComposer::Composer *composer)
1557{
1558 fillComposer(composer, UseUnExpandedRecipients, false);
1559}
1560
1561void ComposerViewBase::fillComposer(MessageComposer::Composer *composer, ComposerViewBase::RecipientExpansion expansion, bool autoresize)
1562{
1563 fillGlobalPart(composer->globalPart());
1564 m_editor->fillComposerTextPart(composer->textPart());
1565 fillInfoPart(composer->infoPart(), expansion);
1566 if (m_attachmentModel) {
1567 composer->addAttachmentParts(m_attachmentModel->attachments(), autoresize);
1568 }
1569}
1570
1571//-----------------------------------------------------------------------------
1573{
1574 if (m_recipientsEditor) {
1575 return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(MessageComposer::Recipient::To));
1576 }
1577 return {};
1578}
1579
1580//-----------------------------------------------------------------------------
1581QString ComposerViewBase::cc() const
1582{
1583 if (m_recipientsEditor) {
1584 return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(MessageComposer::Recipient::Cc));
1585 }
1586 return {};
1587}
1588
1589//-----------------------------------------------------------------------------
1590QString ComposerViewBase::bcc() const
1591{
1592 if (m_recipientsEditor) {
1593 return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(MessageComposer::Recipient::Bcc));
1594 }
1595 return {};
1596}
1597
1598QString ComposerViewBase::from() const
1599{
1600 return MessageComposer::Util::cleanedUpHeaderString(m_from);
1601}
1602
1603QString ComposerViewBase::replyTo() const
1604{
1605 if (m_recipientsEditor) {
1606 return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(MessageComposer::Recipient::ReplyTo));
1607 }
1608 return {};
1609}
1610
1611QString ComposerViewBase::subject() const
1612{
1613 return MessageComposer::Util::cleanedUpHeaderString(m_subject);
1614}
1615
1616const KIdentityManagementCore::Identity &ComposerViewBase::currentIdentity() const
1617{
1618 return m_identMan->identityForUoidOrDefault(m_identityCombo->currentIdentity());
1619}
1620
1621bool ComposerViewBase::autocryptEnabled() const
1622{
1623 return currentIdentity().autocryptEnabled();
1624}
1625
1626void ComposerViewBase::setParentWidgetForGui(QWidget *w)
1627{
1628 m_parentWidget = w;
1629}
1630
1631void ComposerViewBase::setAttachmentController(MessageComposer::AttachmentControllerBase *controller)
1632{
1633 m_attachmentController = controller;
1634}
1635
1636MessageComposer::AttachmentControllerBase *ComposerViewBase::attachmentController()
1637{
1638 return m_attachmentController;
1639}
1640
1642{
1643 m_attachmentModel = model;
1644}
1645
1646MessageComposer::AttachmentModel *ComposerViewBase::attachmentModel()
1647{
1648 return m_attachmentModel;
1649}
1650
1651void ComposerViewBase::setRecipientsEditor(MessageComposer::RecipientsEditor *recEditor)
1652{
1653 m_recipientsEditor = recEditor;
1654}
1655
1656MessageComposer::RecipientsEditor *ComposerViewBase::recipientsEditor()
1657{
1658 return m_recipientsEditor;
1659}
1660
1661void ComposerViewBase::setSignatureController(MessageComposer::SignatureController *sigController)
1662{
1663 m_signatureController = sigController;
1664}
1665
1666MessageComposer::SignatureController *ComposerViewBase::signatureController()
1667{
1668 return m_signatureController;
1669}
1670
1671void ComposerViewBase::setIdentityCombo(KIdentityManagementWidgets::IdentityCombo *identCombo)
1672{
1673 m_identityCombo = identCombo;
1674}
1675
1676KIdentityManagementWidgets::IdentityCombo *ComposerViewBase::identityCombo()
1677{
1678 return m_identityCombo;
1679}
1680
1681void ComposerViewBase::updateRecipients(const KIdentityManagementCore::Identity &ident,
1682 const KIdentityManagementCore::Identity &oldIdent,
1683 MessageComposer::Recipient::Type type)
1684{
1687 switch (type) {
1688 case MessageComposer::Recipient::Bcc: {
1689 oldIdentList = oldIdent.bcc();
1690 newIdentList = ident.bcc();
1691 break;
1692 }
1693 case MessageComposer::Recipient::Cc: {
1694 oldIdentList = oldIdent.cc();
1695 newIdentList = ident.cc();
1696 break;
1697 }
1698 case MessageComposer::Recipient::ReplyTo: {
1699 oldIdentList = oldIdent.replyToAddr();
1700 newIdentList = ident.replyToAddr();
1701 break;
1702 }
1703 case MessageComposer::Recipient::To:
1704 case MessageComposer::Recipient::Undefined:
1705 return;
1706 }
1707
1708 if (oldIdentList != newIdentList) {
1710 for (const KMime::Types::Mailbox &recipient : oldRecipients) {
1711 m_recipientsEditor->removeRecipient(recipient.prettyAddress(), type);
1712 }
1713
1715 for (const KMime::Types::Mailbox &recipient : newRecipients) {
1716 m_recipientsEditor->addRecipient(recipient.prettyAddress(), type);
1717 }
1718 m_recipientsEditor->setFocusBottom();
1719 }
1720}
1721
1722void ComposerViewBase::identityChanged(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, bool msgCleared)
1723{
1724 updateRecipients(ident, oldIdent, MessageComposer::Recipient::Bcc);
1725 updateRecipients(ident, oldIdent, MessageComposer::Recipient::Cc);
1726 updateRecipients(ident, oldIdent, MessageComposer::Recipient::ReplyTo);
1727
1730 // replace existing signatures
1731 const bool replaced = editor()->composerSignature()->replaceSignature(oldSig, newSig);
1732 // Just append the signature if there was no old signature
1733 if (!replaced && (msgCleared || oldSig.rawText().isEmpty())) {
1734 signatureController()->applySignature(newSig);
1735 }
1736 const QString vcardFileName = ident.vCardFile();
1737 attachmentController()->setIdentityHasOwnVcard(!vcardFileName.isEmpty());
1738 attachmentController()->setAttachOwnVcard(ident.attachVcard());
1739
1740 m_editor->setAutocorrectionLanguage(ident.autocorrectionLanguage());
1741}
1742
1743void ComposerViewBase::setEditor(MessageComposer::RichTextComposerNg *editor)
1744{
1745 m_editor = editor;
1746 m_editor->document()->setModified(false);
1747}
1748
1749MessageComposer::RichTextComposerNg *ComposerViewBase::editor() const
1750{
1751 return m_editor;
1752}
1753
1754void ComposerViewBase::setTransportCombo(MailTransport::TransportComboBox *transpCombo)
1755{
1756 m_transport = transpCombo;
1757}
1758
1759MailTransport::TransportComboBox *ComposerViewBase::transportComboBox() const
1760{
1761 return m_transport;
1762}
1763
1764void ComposerViewBase::setIdentityManager(KIdentityManagementCore::IdentityManager *identMan)
1765{
1766 m_identMan = identMan;
1767}
1768
1769KIdentityManagementCore::IdentityManager *ComposerViewBase::identityManager()
1770{
1771 return m_identMan;
1772}
1773
1774void ComposerViewBase::setFcc(const Akonadi::Collection &fccCollection)
1775{
1776 if (m_fccCombo) {
1778 } else {
1779 m_fccCollection = fccCollection;
1780 }
1782 connect(checkFccCollectionJob, &KJob::result, this, &ComposerViewBase::slotFccCollectionCheckResult);
1783}
1784
1785void ComposerViewBase::slotFccCollectionCheckResult(KJob *job)
1786{
1787 if (job->error()) {
1788 qCWarning(MESSAGECOMPOSER_LOG) << " void ComposerViewBase::slotFccCollectionCheckResult(KJob *job) error " << job->errorString();
1790 if (m_fccCombo) {
1791 m_fccCombo->setDefaultCollection(sentMailCol);
1792 } else {
1793 m_fccCollection = sentMailCol;
1794 }
1795 }
1796}
1797
1798void ComposerViewBase::setFccCombo(Akonadi::CollectionComboBox *fcc)
1799{
1800 m_fccCombo = fcc;
1801}
1802
1803Akonadi::CollectionComboBox *ComposerViewBase::fccCombo() const
1804{
1805 return m_fccCombo;
1806}
1807
1809{
1810 m_from = from;
1811}
1812
1813void ComposerViewBase::setSubject(const QString &subject)
1814{
1815 m_subject = subject;
1816 if (mSendLaterInfo) {
1817 mSendLaterInfo->setSubject(m_subject);
1818 mSendLaterInfo->setTo(to());
1819 }
1820}
1821
1822void ComposerViewBase::setAutoSaveInterval(int interval)
1823{
1824 m_autoSaveInterval = interval;
1825}
1826
1827void ComposerViewBase::setCryptoOptions(bool sign, bool encrypt, Kleo::CryptoMessageFormat format, bool neverEncryptDrafts)
1828{
1829 m_sign = sign;
1830 m_encrypt = encrypt;
1831 m_cryptoMessageFormat = format;
1832 m_neverEncrypt = neverEncryptDrafts;
1833}
1834
1835void ComposerViewBase::setCharsets(const QList<QByteArray> &charsets)
1836{
1837 m_charsets = charsets;
1838}
1839
1840void ComposerViewBase::setMDNRequested(bool mdnRequested)
1841{
1842 m_mdnRequested = mdnRequested;
1843}
1844
1845void ComposerViewBase::setUrgent(bool urgent)
1846{
1847 m_urgent = urgent;
1848}
1849
1850int ComposerViewBase::autoSaveInterval() const
1851{
1852 return m_autoSaveInterval;
1853}
1854
1855//-----------------------------------------------------------------------------
1856void ComposerViewBase::collectImages(KMime::Content *root)
1857{
1858 if (KMime::Content *n = Util::findTypeInMessage(root, "multipart", "alternative")) {
1859 KMime::Content *parentnode = n->parent();
1860 if (parentnode && parentnode->contentType()->isMultipart() && parentnode->contentType()->subType() == "related") {
1862 while (node) {
1863 if (node->contentType()->isImage()) {
1864 qCDebug(MESSAGECOMPOSER_LOG) << "found image in multipart/related : " << node->contentType()->name();
1865 QImage img;
1866 img.loadFromData(node->decodedContent());
1867 m_editor->composerControler()->composerImages()->loadImage(
1868 img,
1869 QString::fromLatin1(QByteArray(QByteArrayLiteral("cid:") + node->contentID()->identifier())),
1870 node->contentType()->name());
1871 }
1873 }
1874 }
1875 }
1876}
1877
1878//-----------------------------------------------------------------------------
1879bool ComposerViewBase::inlineSigningEncryptionSelected() const
1880{
1881 if (!m_sign && !m_encrypt) {
1882 return false;
1883 }
1884 return m_cryptoMessageFormat == Kleo::InlineOpenPGPFormat;
1885}
1886
1887bool ComposerViewBase::hasMissingAttachments(const QStringList &attachmentKeywords)
1888{
1889 if (attachmentKeywords.isEmpty()) {
1890 return false;
1891 }
1892 if (m_attachmentModel && m_attachmentModel->rowCount() > 0) {
1893 return false;
1894 }
1895
1896 return MessageComposer::Util::hasMissingAttachments(attachmentKeywords, m_editor->document(), subject());
1897}
1898
1899ComposerViewBase::MissingAttachment ComposerViewBase::checkForMissingAttachments(const QStringList &attachmentKeywords)
1900{
1901 if (!hasMissingAttachments(attachmentKeywords)) {
1902 return NoMissingAttachmentFound;
1903 }
1904 const int rc = KMessageBox::warningTwoActionsCancel(m_editor,
1905
1906 i18n("The message you have composed seems to refer to an "
1907 "attached file but you have not attached anything.\n"
1908 "Do you want to attach a file to your message?"),
1909 i18nc("@title:window", "File Attachment Reminder"),
1910 KGuiItem(i18n("&Attach File..."), QLatin1StringView("mail-attachment")),
1911 KGuiItem(i18n("&Send as Is"), QLatin1StringView("mail-send")));
1912 if (rc == KMessageBox::Cancel) {
1913 return FoundMissingAttachmentAndCancel;
1914 }
1915 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
1916 m_attachmentController->showAddAttachmentFileDialog();
1917 return FoundMissingAttachmentAndAddedAttachment;
1918 }
1919
1920 return FoundMissingAttachmentAndSending;
1921}
1922
1923void ComposerViewBase::markAllAttachmentsForSigning(bool sign)
1924{
1925 if (m_attachmentModel) {
1926 const auto attachments = m_attachmentModel->attachments();
1927 for (MessageCore::AttachmentPart::Ptr attachment : attachments) {
1928 attachment->setSigned(sign);
1929 }
1930 }
1931}
1932
1933void ComposerViewBase::markAllAttachmentsForEncryption(bool encrypt)
1934{
1935 if (m_attachmentModel) {
1936 const auto attachments = m_attachmentModel->attachments();
1937 for (MessageCore::AttachmentPart::Ptr attachment : attachments) {
1938 attachment->setEncrypted(encrypt);
1939 }
1940 }
1941}
1942
1943bool ComposerViewBase::determineWhetherToSign(bool doSignCompletely, Kleo::KeyResolver *keyResolver, bool signSomething, bool &result, bool &canceled)
1944{
1945 bool sign = false;
1946 switch (keyResolver->checkSigningPreferences(signSomething)) {
1947 case Kleo::DoIt:
1948 if (!signSomething) {
1949 markAllAttachmentsForSigning(true);
1950 return true;
1951 }
1952 sign = true;
1953 break;
1954 case Kleo::DontDoIt:
1955 sign = false;
1956 break;
1957 case Kleo::AskOpportunistic:
1958 assert(0);
1959 case Kleo::Ask: {
1960 // the user wants to be asked or has to be asked
1962 const QString msg = i18n(
1963 "Examination of the recipient's signing preferences "
1964 "yielded that you be asked whether or not to sign "
1965 "this message.\n"
1966 "Sign this message?");
1967 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
1968 msg,
1969 i18nc("@title:window", "Sign Message?"),
1970 KGuiItem(i18nc("to sign", "&Sign")),
1971 KGuiItem(i18n("Do &Not Sign")))) {
1973 result = false;
1974 canceled = true;
1975 return false;
1976 case KMessageBox::ButtonCode::PrimaryAction:
1977 markAllAttachmentsForSigning(true);
1978 return true;
1979 case KMessageBox::ButtonCode::SecondaryAction:
1980 markAllAttachmentsForSigning(false);
1981 return false;
1982 default:
1983 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
1984 return false;
1985 }
1986 break;
1987 }
1988 case Kleo::Conflict: {
1989 // warn the user that there are conflicting signing preferences
1991 const QString msg = i18n(
1992 "There are conflicting signing preferences "
1993 "for these recipients.\n"
1994 "Sign this message?");
1995 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
1996 msg,
1997 i18nc("@title:window", "Sign Message?"),
1998 KGuiItem(i18nc("to sign", "&Sign")),
1999 KGuiItem(i18n("Do &Not Sign")))) {
2001 result = false;
2002 canceled = true;
2003 return false;
2004 case KMessageBox::ButtonCode::PrimaryAction:
2005 markAllAttachmentsForSigning(true);
2006 return true;
2007 case KMessageBox::ButtonCode::SecondaryAction:
2008 markAllAttachmentsForSigning(false);
2009 return false;
2010 default:
2011 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2012 return false;
2013 }
2014 break;
2015 }
2016 case Kleo::Impossible: {
2018 const QString msg = i18n(
2019 "You have requested to sign this message, "
2020 "but no valid signing keys have been configured "
2021 "for this identity.");
2022 if (KMessageBox::warningContinueCancel(m_parentWidget, msg, i18nc("@title:window", "Send Unsigned?"), KGuiItem(i18n("Send &Unsigned")))
2024 result = false;
2025 return false;
2026 } else {
2027 markAllAttachmentsForSigning(false);
2028 return false;
2029 }
2030 }
2031 }
2032
2033 if (!sign || !doSignCompletely) {
2034 if (cryptoWarningUnsigned(currentIdentity())) {
2036 const QString msg = sign && !doSignCompletely ? i18n(
2037 "Some parts of this message will not be signed.\n"
2038 "Sending only partially signed messages might violate site policy.\n"
2039 "Sign all parts instead?") // oh, I hate this...
2040 : i18n(
2041 "This message will not be signed.\n"
2042 "Sending unsigned message might violate site policy.\n"
2043 "Sign message instead?"); // oh, I hate this...
2044 const QString buttonText = sign && !doSignCompletely ? i18n("&Sign All Parts") : i18n("&Sign");
2045 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
2046 msg,
2047 i18nc("@title:window", "Unsigned-Message Warning"),
2048 KGuiItem(buttonText),
2049 KGuiItem(i18n("Send &As Is")))) {
2051 result = false;
2052 canceled = true;
2053 return false;
2054 case KMessageBox::ButtonCode::PrimaryAction:
2055 markAllAttachmentsForSigning(true);
2056 return true;
2057 case KMessageBox::ButtonCode::SecondaryAction:
2058 return sign || doSignCompletely;
2059 default:
2060 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2061 return false;
2062 }
2063 }
2064 }
2065 return sign || doSignCompletely;
2066}
2067
2068bool ComposerViewBase::determineWhetherToEncrypt(bool doEncryptCompletely,
2069 Kleo::KeyResolver *keyResolver,
2070 bool encryptSomething,
2071 bool signSomething,
2072 bool &result,
2073 bool &canceled)
2074{
2075 bool encrypt = false;
2076 bool opportunistic = false;
2077 switch (keyResolver->checkEncryptionPreferences(encryptSomething)) {
2078 case Kleo::DoIt:
2079 if (!encryptSomething) {
2080 markAllAttachmentsForEncryption(true);
2081 return true;
2082 }
2083 encrypt = true;
2084 break;
2085 case Kleo::DontDoIt:
2086 encrypt = false;
2087 break;
2088 case Kleo::AskOpportunistic:
2089 opportunistic = true;
2090 // fall through...
2091 [[fallthrough]];
2092 case Kleo::Ask: {
2093 // the user wants to be asked or has to be asked
2095 const QString msg = opportunistic ? i18n(
2096 "Valid trusted encryption keys were found for all recipients.\n"
2097 "Encrypt this message?")
2098 : i18n(
2099 "Examination of the recipient's encryption preferences "
2100 "yielded that you be asked whether or not to encrypt "
2101 "this message.\n"
2102 "Encrypt this message?");
2103 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
2104 msg,
2105 i18n("Encrypt Message?"),
2106 KGuiItem(signSomething ? i18n("Sign && &Encrypt") : i18n("&Encrypt")),
2107 KGuiItem(signSomething ? i18n("&Sign Only") : i18n("&Send As-Is")))) {
2109 result = false;
2110 canceled = true;
2111 return false;
2112 case KMessageBox::ButtonCode::PrimaryAction:
2113 markAllAttachmentsForEncryption(true);
2114 return true;
2115 case KMessageBox::ButtonCode::SecondaryAction:
2116 markAllAttachmentsForEncryption(false);
2117 return false;
2118 default:
2119 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2120 return false;
2121 }
2122 break;
2123 }
2124 case Kleo::Conflict: {
2125 // warn the user that there are conflicting encryption preferences
2127 const QString msg = i18n(
2128 "There are conflicting encryption preferences "
2129 "for these recipients.\n"
2130 "Encrypt this message?");
2132
2133 m_parentWidget,
2134 msg,
2135 i18n("Encrypt Message?"),
2136 KGuiItem(i18n("&Encrypt")),
2137 KGuiItem(i18n("Do &Not Encrypt")))) {
2139 result = false;
2140 canceled = true;
2141 return false;
2142 case KMessageBox::ButtonCode::PrimaryAction:
2143 markAllAttachmentsForEncryption(true);
2144 return true;
2145 case KMessageBox::ButtonCode::SecondaryAction:
2146 markAllAttachmentsForEncryption(false);
2147 return false;
2148 default:
2149 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2150 return false;
2151 }
2152 break;
2153 }
2154 case Kleo::Impossible: {
2156 const QString msg = i18n(
2157 "You have requested to encrypt this message, "
2158 "and to encrypt a copy to yourself, "
2159 "but no valid trusted encryption keys have been "
2160 "configured for this identity.");
2161 if (KMessageBox::warningContinueCancel(m_parentWidget, msg, i18nc("@title:window", "Send Unencrypted?"), KGuiItem(i18n("Send &Unencrypted")))
2163 result = false;
2164 return false;
2165 } else {
2166 markAllAttachmentsForEncryption(false);
2167 return false;
2168 }
2169 }
2170 }
2171
2172 if (!encrypt || !doEncryptCompletely) {
2173 if (cryptoWarningUnencrypted(currentIdentity())) {
2175 const QString msg = !doEncryptCompletely ? i18n(
2176 "Some parts of this message will not be encrypted.\n"
2177 "Sending only partially encrypted messages might violate "
2178 "site policy and/or leak sensitive information.\n"
2179 "Encrypt all parts instead?") // oh, I hate this...
2180 : i18n(
2181 "This message will not be encrypted.\n"
2182 "Sending unencrypted messages might violate site policy and/or "
2183 "leak sensitive information.\n"
2184 "Encrypt messages instead?"); // oh, I hate this...
2185 const QString buttonText = !doEncryptCompletely ? i18n("&Encrypt All Parts") : i18n("&Encrypt");
2186 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
2187 msg,
2188 i18nc("@title:window", "Unencrypted Message Warning"),
2189 KGuiItem(buttonText),
2190 KGuiItem(signSomething ? i18n("&Sign Only") : i18n("&Send As-Is")))) {
2192 result = false;
2193 canceled = true;
2194 return false;
2195 case KMessageBox::ButtonCode::PrimaryAction:
2196 markAllAttachmentsForEncryption(true);
2197 return true;
2198 case KMessageBox::ButtonCode::SecondaryAction:
2199 return encrypt || doEncryptCompletely;
2200 default:
2201 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2202 return false;
2203 }
2204 }
2205 }
2206
2207 return encrypt || doEncryptCompletely;
2208}
2209
2210void ComposerViewBase::setSendLaterInfo(SendLaterInfo *info)
2211{
2212 mSendLaterInfo.reset(info);
2213}
2214
2215SendLaterInfo *ComposerViewBase::sendLaterInfo() const
2216{
2217 return mSendLaterInfo.get();
2218}
2219
2220void ComposerViewBase::addFollowupReminder(const QString &messageId)
2221{
2222 if (!messageId.isEmpty()) {
2223 if (mFollowUpDate.isValid()) {
2225 job->setSubject(m_subject);
2226 job->setMessageId(messageId);
2227 job->setTo(mExpandedReplyTo.isEmpty() ? mExpandedTo.join(QLatin1Char(',')) : mExpandedReplyTo.join(QLatin1Char(',')));
2228 job->setFollowUpReminderDate(mFollowUpDate);
2229 job->setCollectionToDo(mFollowUpCollection);
2230 job->start();
2231 }
2232 }
2233}
2234
2235void ComposerViewBase::addSendLaterItem(const Akonadi::Item &item)
2236{
2237 mSendLaterInfo->setItemId(item.id());
2238
2239 auto job = new MessageComposer::SendLaterCreateJob(*mSendLaterInfo, this);
2240 job->start();
2241}
2242
2243bool ComposerViewBase::requestDeleveryConfirmation() const
2244{
2245 return m_requestDeleveryConfirmation;
2246}
2247
2248void ComposerViewBase::setRequestDeleveryConfirmation(bool requestDeleveryConfirmation)
2249{
2250 m_requestDeleveryConfirmation = requestDeleveryConfirmation;
2251}
2252
2253KMime::Message::Ptr ComposerViewBase::msg() const
2254{
2255 return m_msg;
2256}
2257
2258std::shared_ptr<Kleo::ExpiryChecker> ComposerViewBase::expiryChecker()
2259{
2260 if (!mExpiryChecker) {
2261 mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(),
2265 }
2266 return mExpiryChecker;
2267}
2268
2269#include "moc_composerviewbase.cpp"
Akonadi::Collection currentCollection() const
void setDefaultCollection(const Collection &collection)
bool isValid() const
void setMimeType(const QString &mimeType)
Id id() const
bool isValid() const
void setPayload(const T &p)
static SpecialMailCollections * self()
Akonadi::Collection defaultCollection(Type type) const
const Identity & identityForUoidOrDefault(uint uoid) const
KIdentityManagementCore::Identity::Id currentIdentity() const
virtual QString errorString() const
int error() const
void result(KJob *job)
virtual Q_SCRIPTABLE void start()=0
Headers::ContentType * contentType(bool create=true)
QByteArray decodedContent()
Headers::ContentID * contentID(bool create=true)
static QList< Mailbox > listFromUnicodeString(const QString &s)
bool setCurrentTransport(int transportId)
Transport * transportById(Transport::Id id, bool def=true) const
static TransportManager * self()
void addAttachment(const MessageCore::AttachmentPart::Ptr &part)
sets sign, encrypt, shows properties dialog if so configured
The AttachmentModel class.
void addAttachment(const QUrl &url, const QString &comment, bool sync)
Add the given attachment to the message.
void setCryptoOptions(bool sign, bool encrypt, Kleo::CryptoMessageFormat format, bool neverEncryptDrafts=false)
The following are various settings the user can modify when composing a message.
void setFrom(const QString &from)
Widgets for editing differ in client classes, so values are set before sending.
QString to() const
Header fields in recipients editor.
void send(MessageComposer::MessageSender::SendMethod method, MessageComposer::MessageSender::SaveIn saveIn, bool checkMailDispatcher=true)
Send the message with the specified method, saving it in the specified folder.
void setMessage(const KMime::Message::Ptr &newMsg, bool allowDecryption)
Set the message to be opened in the composer window, and set the internal data structures to keep tra...
void setAttachmentModel(MessageComposer::AttachmentModel *model)
The following are for setting the various options and widgets in the composer.
bool isComposing() const
Returns true if there is at least one composer job running.
void failed(const QString &errorMessage, MessageComposer::ComposerViewBase::FailedType type=Sending)
Message sending failed with given error message.
void sentSuccessfully(Akonadi::Item::Id id)
Message sending completed successfully.
void updateAutoSave()
Enables/disables autosaving depending on the value of the autosave interval.
void cleanupAutoSave()
Stop autosaving and delete the autosaved message.
ComposerViewBase::MissingAttachment checkForMissingAttachments(const QStringList &attachmentKeywords)
Check if the mail has references to attachments, but no attachments are added to it.
void disableHtml(MessageComposer::ComposerViewBase::Confirmation)
Enabling or disabling HTML in the editor is affected by various client options, so when that would ot...
void setAutoSaveFileName(const QString &fileName)
Sets the filename to use when autosaving something.
void modified(bool isModified)
The composer was modified.
void autoSaveMessage()
Save the message.
The Composer class.
Definition composer.h:35
A job to resolve nicknames, distribution lists and email addresses for queued emails.
The GlobalPart class.
Definition globalpart.h:20
The InfoPart class contains the message header.
Definition infopart.h:22
QStringList cc
Carbon copy: The email address and optionally the name of the secondary recipients.
Definition infopart.h:32
QString from
The email address and optionally the name of the author of the mail.
Definition infopart.h:26
QStringList to
The email address and optionally the name of the primary recipients.
Definition infopart.h:29
QString fcc
The name of a file, to which a copy of the sent message should be appended.
Definition infopart.h:45
QStringList bcc
Blind Carbon copy: The email address and optionally the name of the secondary recipients.
Definition infopart.h:36
The RecipientsEditor class.
void removeRecipient(const QString &recipient, Recipient::Type type)
Removes the recipient provided it can be found and has the given type.
bool addRecipient(const QString &recipient, Recipient::Type type)
Adds a recipient (or multiple recipients) to one line of the editor.
The RichTextComposerNg class.
Send later information.
The SignatureController class Controls signature (the footer thing, not the crypto thing) operations ...
void applySignature(const KIdentityManagementCore::Signature &signature)
Adds the given signature to the editor, taking user preferences into account.
A class that encapsulates an attachment.
QSharedPointer< AttachmentPart > Ptr
Defines a pointer to an attachment object.
Parses messages and generates HTML display code out of them.
void add(const QString &entry)
static RecentAddresses * self(KConfig *config=nullptr)
QString currentDictionary() const
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
KCODECS_EXPORT QString normalizeAddressesAndEncodeIdn(const QString &str)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_MIME_EXPORT void copyMessageFlags(KMime::Message &from, Akonadi::Item &to)
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
QAction * create(GameStandardAction id, const QObject *recvr, const char *slot, QObject *parent)
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
PostalAddress address(const QVariant &location)
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
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))
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
ButtonCode warningTwoActionsCancel(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const KGuiItem &cancelAction=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Options(Notify|Dangerous))
KIOCORE_EXPORT QString dir(const QString &fileClass)
KGuiItem cont()
KGuiItem cancel()
const QList< QKeySequence > & end()
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
MESSAGECORE_EXPORT KMime::Content * nextSibling(const KMime::Content *node)
Returns the next sibling node of the given node.
void removePrivateHeaderFields(const KMime::Message::Ptr &message, bool cleanUpHeader)
Removes all private header fields (e.g.
const char * constData() const const
bool isEmpty() const const
bool isValid(int year, int month, int day)
QDateTime currentDateTime()
bool mkpath(const QString &dirPath) const const
int exec(ProcessEventsFlags flags)
void quit()
void append(QList< T > &&value)
bool contains(const AT &value) const const
qsizetype count() const const
bool isEmpty() const const
void push_back(parameter_type value)
qsizetype removeAll(const AT &t)
void reserve(qsizetype size)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QVariant property(const char *name) const const
T qobject_cast(QObject *object)
T * data() const const
QString writableLocation(StandardLocation type)
void clear()
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QByteArray toLatin1() const const
qlonglong toLongLong(bool *ok, int base) const const
QByteArray toUtf8() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QString join(QChar separator) const const
WaitCursor
void start()
void stop()
void timeout()
QUrl fromLocalFile(const QString &localFile)
QUuid createUuid()
QString toString(StringFormat mode) const const
QVariant fromValue(T &&value)
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.