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) {
423 listEmails << mExpandedFrom;
424 listEmails << mExpandedTo;
425 listEmails << mExpandedCc;
426 listEmails << mExpandedBcc;
427 listEmails << mExpandedReplyTo;
428 MessageComposer::Utils resizeUtils;
429 autoresizeImage = resizeUtils.filterRecipients(listEmails);
430 }
431 } else { // saved to draft, so keep the old values, not very nice.
432 mExpandedFrom = from();
433 const auto recipients{m_recipientsEditor->recipients()};
434 for (const MessageComposer::Recipient::Ptr &r : recipients) {
435 switch (r->type()) {
436 case MessageComposer::Recipient::To:
437 mExpandedTo << r->email();
438 break;
439 case MessageComposer::Recipient::Cc:
440 mExpandedCc << r->email();
441 break;
442 case MessageComposer::Recipient::Bcc:
443 mExpandedBcc << r->email();
444 break;
445 case MessageComposer::Recipient::ReplyTo:
446 mExpandedReplyTo << r->email();
447 break;
448 case MessageComposer::Recipient::Undefined:
449 Q_ASSERT(!"Unknown recipient type!");
450 break;
451 }
452 }
457 const auto expandedToLst{resolveJob->expandedTo()};
458 for (const QString &exp : expandedToLst) {
459 if (!mExpandedTo.contains(exp)) { // this address was expanded, so save it explicitly
460 unExpandedTo << exp;
461 }
462 }
463 const auto expandedCcLst{resolveJob->expandedCc()};
464 for (const QString &exp : expandedCcLst) {
465 if (!mExpandedCc.contains(exp)) {
466 unExpandedCc << exp;
467 }
468 }
469 const auto expandedBCcLst{resolveJob->expandedBcc()};
470 for (const QString &exp : expandedBCcLst) {
471 if (!mExpandedBcc.contains(exp)) { // this address was expanded, so save it explicitly
473 }
474 }
475 const auto expandedReplyLst{resolveJob->expandedReplyTo()};
476 for (const QString &exp : expandedReplyLst) {
477 if (!mExpandedReplyTo.contains(exp)) { // this address was expanded, so save it explicitly
479 }
480 }
481 auto header = new KMime::Headers::Generic("X-KMail-UnExpanded-To");
482 header->from7BitString(unExpandedTo.join(QLatin1StringView(", ")).toLatin1());
483 m_msg->setHeader(header);
484 header = new KMime::Headers::Generic("X-KMail-UnExpanded-CC");
485 header->from7BitString(unExpandedCc.join(QLatin1StringView(", ")).toLatin1());
486 m_msg->setHeader(header);
487 header = new KMime::Headers::Generic("X-KMail-UnExpanded-BCC");
488 header->from7BitString(unExpandedBcc.join(QLatin1StringView(", ")).toLatin1());
489 m_msg->setHeader(header);
490 header = new KMime::Headers::Generic("X-KMail-UnExpanded-Reply-To");
491 header->from7BitString(unExpandedReplyTo.join(QLatin1StringView(", ")).toLatin1());
492 m_msg->setHeader(header);
493 autoresizeImage = false;
494 }
495
496 Q_ASSERT(m_composers.isEmpty()); // composers should be empty. The caller of this function
497 // checks for emptiness before calling it
498 // so just ensure it actually is empty
499 // and document it
500 // we first figure out if we need to create multiple messages with different crypto formats
501 // if so, we create a composer per format
502 // if we aren't signing or encrypting, this just returns a single empty message
503 if (m_neverEncrypt && mSaveIn != MessageComposer::MessageSender::SaveInNone && !mSendLaterInfo) {
505 composer->setNoCrypto(true);
506 m_composers.append(composer);
507 } else {
508 bool wasCanceled = false;
509 m_composers = generateCryptoMessages(wasCanceled);
510 if (wasCanceled) {
511 return;
512 }
513 }
514
515 if (m_composers.isEmpty()) {
516 Q_EMIT failed(i18n("It was not possible to create a message composer."));
517 return;
518 }
519
520 if (autoresizeImage) {
521 if (MessageComposer::MessageComposerSettings::self()->askBeforeResizing()) {
522 if (m_attachmentModel) {
523 MessageComposer::Utils resizeUtils;
524 if (resizeUtils.containsImage(m_attachmentModel->attachments())) {
525 const int rc = KMessageBox::warningTwoActions(m_parentWidget,
526 i18n("Do you want to resize images?"),
527 i18nc("@title:window", "Auto Resize Images"),
528 KGuiItem(i18nc("@action:button", "Auto Resize")),
529 KGuiItem(i18nc("@action:button", "Do Not Resize")));
530 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
531 autoresizeImage = true;
532 } else {
533 autoresizeImage = false;
534 }
535 } else {
536 autoresizeImage = false;
537 }
538 }
539 }
540 }
541 // Compose each message and prepare it for queueing, sending, or storing
542
543 // working copy in case composers instantly emit result
544 const auto composers = m_composers;
546 fillComposer(composer, UseExpandedRecipients, autoresizeImage);
547 connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotSendComposeResult);
548 composer->start();
549 qCDebug(MESSAGECOMPOSER_LOG) << "Started a composer for sending!";
550 }
551}
552
553namespace
554{
555// helper methods for reading encryption settings
556
557inline Kleo::chrono::days encryptOwnKeyNearExpiryWarningThresholdInDays()
558{
559 if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
560 return Kleo::chrono::days{-1};
561 }
562 const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnOwnEncrKeyNearExpiryThresholdDays();
563 return Kleo::chrono::days{qMax(1, num)};
564}
565
566inline Kleo::chrono::days encryptKeyNearExpiryWarningThresholdInDays()
567{
568 if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
569 return Kleo::chrono::days{-1};
570 }
571 const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrKeyNearExpiryThresholdDays();
572 return Kleo::chrono::days{qMax(1, num)};
573}
574
575inline Kleo::chrono::days encryptRootCertNearExpiryWarningThresholdInDays()
576{
577 if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
578 return Kleo::chrono::days{-1};
579 }
580 const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrRootNearExpiryThresholdDays();
581 return Kleo::chrono::days{qMax(1, num)};
582}
583
584inline Kleo::chrono::days encryptChainCertNearExpiryWarningThresholdInDays()
585{
586 if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
587 return Kleo::chrono::days{-1};
588 }
589 const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrChaincertNearExpiryThresholdDays();
590 return Kleo::chrono::days{qMax(1, num)};
591}
592
593inline bool showKeyApprovalDialog()
594{
595 return MessageComposer::MessageComposerSettings::self()->cryptoShowKeysForApproval();
596}
597
598inline bool cryptoWarningUnsigned(const KIdentityManagementCore::Identity &identity)
599{
600 if (identity.encryptionOverride()) {
601 return identity.warnNotSign();
602 }
603 return MessageComposer::MessageComposerSettings::self()->cryptoWarningUnsigned();
604}
605
606inline bool cryptoWarningUnencrypted(const KIdentityManagementCore::Identity &identity)
607{
608 if (identity.encryptionOverride()) {
609 return identity.warnNotEncrypt();
610 }
611 return MessageComposer::MessageComposerSettings::self()->cryptoWarningUnencrypted();
612}
613} // nameless namespace
614
615bool ComposerViewBase::addKeysToContext(const QString &gnupgHome,
616 const QList<QPair<QStringList, std::vector<GpgME::Key>>> &data,
617 const std::map<QByteArray, QString> &autocryptMap)
618{
619 bool needSpecialContext = false;
620
621 for (const auto &p : data) {
622 for (const auto &k : p.second) {
623 const auto it = autocryptMap.find(k.primaryFingerprint());
624 if (it != autocryptMap.end()) {
625 needSpecialContext = true;
626 break;
627 }
628 }
629 if (needSpecialContext) {
630 break;
631 }
632 }
633
634 if (!needSpecialContext) {
635 return false;
636 }
637 const QGpgME::Protocol *proto(QGpgME::openpgp());
638
639 const auto storage = MessageCore::AutocryptStorage::self();
640 QEventLoop loop;
641 int runningJobs = 0;
642 for (const auto &p : data) {
643 for (const auto &k : p.second) {
644 const auto it = autocryptMap.find(k.primaryFingerprint());
645 if (it == autocryptMap.end()) {
646 qCDebug(MESSAGECOMPOSER_LOG) << "Adding " << k.primaryFingerprint() << "via Export/Import";
647 auto exportJob = proto->publicKeyExportJob(false);
649 &QGpgME::ExportJob::result,
650 [&gnupgHome, &proto, &runningJobs, &loop, &k](const GpgME::Error &result,
651 const QByteArray &keyData,
652 const QString &auditLogAsHtml,
653 const GpgME::Error &auditLogError) {
654 Q_UNUSED(auditLogAsHtml);
655 Q_UNUSED(auditLogError);
656 if (result) {
657 qCWarning(MESSAGECOMPOSER_LOG) << "Failed to export " << k.primaryFingerprint() << result.asString();
658 --runningJobs;
659 if (runningJobs < 1) {
660 loop.quit();
661 }
662 }
663
664 auto importJob = proto->importJob();
665 QGpgME::Job::context(importJob)->setEngineHomeDirectory(gnupgHome.toUtf8().constData());
666 importJob->exec(keyData);
667 importJob->deleteLater();
668 --runningJobs;
669 if (runningJobs < 1) {
670 loop.quit();
671 }
672 });
673 QStringList patterns;
674 patterns << QString::fromUtf8(k.primaryFingerprint());
675 runningJobs++;
676 exportJob->start(patterns);
677 exportJob->setExportFlags(GpgME::Context::ExportMinimal);
678 } else {
679 qCDebug(MESSAGECOMPOSER_LOG) << "Adding " << k.primaryFingerprint() << "from Autocrypt storage";
680 const auto recipient = storage->getRecipient(it->second.toUtf8());
681 auto key = recipient->gpgKey();
682 auto keydata = recipient->gpgKeydata();
683 if (QByteArray(key.primaryFingerprint()) != QByteArray(k.primaryFingerprint())) {
684 qCDebug(MESSAGECOMPOSER_LOG) << "Using gossipkey";
685 keydata = recipient->gossipKeydata();
686 }
687 auto importJob = proto->importJob();
688 QGpgME::Job::context(importJob)->setEngineHomeDirectory(gnupgHome.toUtf8().constData());
689 const auto result = importJob->exec(keydata);
690 importJob->deleteLater();
691 }
692 }
693 }
694 loop.exec();
695 return true;
696}
697
698void ComposerViewBase::setAkonadiLookupEnabled(bool akonadiLookupEnabled)
699{
700 m_akonadiLookupEnabled = akonadiLookupEnabled;
701}
702
703QList<MessageComposer::Composer *> ComposerViewBase::generateCryptoMessages(bool &wasCanceled)
704{
705 const auto id = currentIdentity();
706
707 bool canceled = false;
708
709 qCDebug(MESSAGECOMPOSER_LOG) << "filling crypto info";
710 connect(expiryChecker().get(),
711 &Kleo::ExpiryChecker::expiryMessage,
712 this,
713 [&canceled](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) {
714 if (!isNewMessage) {
715 return;
716 }
717
718 if (canceled) {
719 return;
720 }
721 QString title;
723 if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) {
724 dontAskAgainName = QStringLiteral("own key expires soon warning");
725 } else {
726 dontAskAgainName = QStringLiteral("other encryption key near expiry warning");
727 }
728 if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OtherKeyExpired) {
729 title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expired") : i18n("S/MIME Certificate Expired");
730 } else {
731 title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expires Soon") : i18n("S/MIME Certificate Expires Soon");
732 }
735 canceled = true;
736 }
737 });
738
740 new Kleo::KeyResolver(true, showKeyApprovalDialog(), id.pgpAutoEncrypt(), m_cryptoMessageFormat, expiryChecker()));
741
742 keyResolver->setAutocryptEnabled(autocryptEnabled());
743 keyResolver->setAkonadiLookupEnabled(m_akonadiLookupEnabled);
744
746 QStringList signKeys;
747
748 bool signSomething = m_sign;
749 bool doSignCompletely = m_sign;
750 bool encryptSomething = m_encrypt;
751 bool doEncryptCompletely = m_encrypt;
752
753 // Add encryptionkeys from id to keyResolver
754 if (!id.pgpEncryptionKey().isEmpty()) {
755 encryptToSelfKeys.push_back(QLatin1StringView(id.pgpEncryptionKey()));
756 }
757 if (!id.smimeEncryptionKey().isEmpty()) {
758 encryptToSelfKeys.push_back(QLatin1StringView(id.smimeEncryptionKey()));
759 }
760 if (canceled || keyResolver->setEncryptToSelfKeys(encryptToSelfKeys) != Kleo::Ok) {
761 qCDebug(MESSAGECOMPOSER_LOG) << "Failed to set encryptoToSelf keys!";
762 return {};
763 }
764
765 // Add signingkeys from id to keyResolver
766 if (!id.pgpSigningKey().isEmpty()) {
767 signKeys.push_back(QLatin1StringView(id.pgpSigningKey()));
768 }
769 if (!id.smimeSigningKey().isEmpty()) {
770 signKeys.push_back(QLatin1StringView(id.smimeSigningKey()));
771 }
772 if (canceled || keyResolver->setSigningKeys(signKeys) != Kleo::Ok) {
773 qCDebug(MESSAGECOMPOSER_LOG) << "Failed to set signing keys!";
774 return {};
775 }
776
777 if (m_attachmentModel) {
778 const auto attachments = m_attachmentModel->attachments();
779 for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) {
780 if (attachment->isSigned()) {
781 signSomething = true;
782 } else {
783 doEncryptCompletely = false;
784 }
785 if (attachment->isEncrypted()) {
786 encryptSomething = true;
787 } else {
788 doSignCompletely = false;
789 }
790 }
791 }
792
793 const QStringList recipients = mExpandedTo + mExpandedCc;
794 const QStringList bcc(mExpandedBcc);
795
796 keyResolver->setPrimaryRecipients(recipients);
797 keyResolver->setSecondaryRecipients(bcc);
798
799 bool result = true;
800 canceled = false;
801 signSomething = determineWhetherToSign(doSignCompletely, keyResolver.data(), signSomething, result, canceled);
802 if (!result) {
803 // TODO handle failure
804 qCDebug(MESSAGECOMPOSER_LOG) << "determineWhetherToSign: failed to resolve keys! oh noes";
805 if (!canceled) {
806 Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug."));
807 } else {
809 }
810 wasCanceled = canceled;
811 return {};
812 }
813
814 canceled = false;
815 encryptSomething = determineWhetherToEncrypt(doEncryptCompletely, keyResolver.data(), encryptSomething, signSomething, result, canceled);
816 if (!result) {
817 // TODO handle failure
818 qCDebug(MESSAGECOMPOSER_LOG) << "determineWhetherToEncrypt: failed to resolve keys! oh noes";
819 if (!canceled) {
820 Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug."));
821 } else {
823 }
824
825 wasCanceled = canceled;
826 return {};
827 }
828
830
831 // No encryption or signing is needed
834 if (m_cryptoMessageFormat & Kleo::OpenPGPMIMEFormat) {
835 composer->setAutocryptEnabled(autocryptEnabled());
836 if (keyResolver->encryptToSelfKeysFor(Kleo::OpenPGPMIMEFormat).size() > 0) {
837 composer->setSenderEncryptionKey(keyResolver->encryptToSelfKeysFor(Kleo::OpenPGPMIMEFormat)[0]);
838 }
839 }
840 composers.append(composer);
841 return composers;
842 }
843
844 canceled = false;
845 const Kleo::Result kpgpResult = keyResolver->resolveAllKeys(signSomething, encryptSomething);
846 if (kpgpResult == Kleo::Canceled || canceled) {
847 qCDebug(MESSAGECOMPOSER_LOG) << "resolveAllKeys: one key resolution canceled by user";
848 return {};
849 } else if (kpgpResult != Kleo::Ok) {
850 // TODO handle failure
851 qCDebug(MESSAGECOMPOSER_LOG) << "resolveAllKeys: failed to resolve keys! oh noes";
852 Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug."));
853 return {};
854 }
855
856 qCDebug(MESSAGECOMPOSER_LOG) << "done resolving keys.";
857
859 Kleo::CryptoMessageFormat concreteFormat = Kleo::AutoFormat;
860 for (unsigned int i = 0; i < numConcreteCryptoMessageFormats; ++i) {
861 concreteFormat = concreteCryptoMessageFormats[i];
862 const auto encData = keyResolver->encryptionItems(concreteFormat);
863 if (encData.empty()) {
864 continue;
865 }
866
867 if (!(concreteFormat & m_cryptoMessageFormat)) {
868 continue;
869 }
870
872
873 if (encryptSomething || autocryptEnabled()) {
874 auto end(encData.end());
876 data.reserve(encData.size());
877 for (auto it = encData.begin(); it != end; ++it) {
878 QPair<QStringList, std::vector<GpgME::Key>> p(it->recipients, it->keys);
879 data.append(p);
880 qCDebug(MESSAGECOMPOSER_LOG) << "got resolved keys for:" << it->recipients;
881 }
882 composer->setEncryptionKeys(data);
883 if (concreteFormat & Kleo::OpenPGPMIMEFormat && autocryptEnabled()) {
884 composer->setAutocryptEnabled(autocryptEnabled());
885 composer->setSenderEncryptionKey(keyResolver->encryptToSelfKeysFor(concreteFormat)[0]);
887 bool specialGnupgHome = addKeysToContext(dir.path(), data, keyResolver->useAutocrypt());
888 if (specialGnupgHome) {
889 dir.setAutoRemove(false);
890 composer->setGnupgHome(dir.path());
891 }
892 }
893 }
894
895 if (signSomething) {
896 // find signing keys for this format
897 std::vector<GpgME::Key> signingKeys = keyResolver->signingKeys(concreteFormat);
898 composer->setSigningKeys(signingKeys);
899 }
900
901 composer->setMessageCryptoFormat(concreteFormat);
902 composer->setSignAndEncrypt(signSomething, encryptSomething);
903
904 composers.append(composer);
905 }
906 } else {
908 composers.append(composer);
909 // If we canceled sign or encrypt be sure to change status in attachment.
910 markAllAttachmentsForSigning(false);
911 markAllAttachmentsForEncryption(false);
912 }
913
914 if (composers.isEmpty() && (signSomething || encryptSomething)) {
915 Q_ASSERT_X(false, "ComposerViewBase::generateCryptoMessages", "No concrete sign or encrypt method selected");
916 }
917
918 return composers;
919}
920
921void ComposerViewBase::fillGlobalPart(MessageComposer::GlobalPart *globalPart)
922{
923 globalPart->setParentWidgetForGui(m_parentWidget);
924 globalPart->setCharsets(m_charsets);
925 globalPart->setMDNRequested(m_mdnRequested);
926 globalPart->setRequestDeleveryConfirmation(m_requestDeleveryConfirmation);
927}
928
929void ComposerViewBase::fillInfoPart(MessageComposer::InfoPart *infoPart, ComposerViewBase::RecipientExpansion expansion)
930{
931 // TODO splitAddressList and expandAliases ugliness should be handled by a
932 // special AddressListEdit widget... (later: see RecipientsEditor)
933
934 if (m_fccCombo) {
935 infoPart->setFcc(QString::number(m_fccCombo->currentCollection().id()));
936 } else {
937 if (m_fccCollection.isValid()) {
938 infoPart->setFcc(QString::number(m_fccCollection.id()));
939 }
940 }
941
942 infoPart->setTransportId(m_transport->currentTransportId());
943 if (expansion == UseExpandedRecipients) {
944 infoPart->setFrom(mExpandedFrom);
945 infoPart->setTo(mExpandedTo);
946 infoPart->setCc(mExpandedCc);
947 infoPart->setBcc(mExpandedBcc);
948 infoPart->setReplyTo(mExpandedReplyTo);
949 } else {
950 infoPart->setFrom(from());
951 infoPart->setTo(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::To));
952 infoPart->setCc(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::Cc));
953 infoPart->setBcc(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::Bcc));
954 infoPart->setReplyTo(m_recipientsEditor->recipientStringList(MessageComposer::Recipient::ReplyTo));
955 }
956 infoPart->setSubject(subject());
957 infoPart->setUserAgent(QStringLiteral("KMail"));
958 infoPart->setUrgent(m_urgent);
959
960 if (auto inReplyTo = m_msg->inReplyTo(false)) {
961 infoPart->setInReplyTo(inReplyTo->asUnicodeString());
962 }
963
964 if (auto references = m_msg->references(false)) {
965 infoPart->setReferences(references->asUnicodeString());
966 }
967
969 if (auto hdr = m_msg->headerByType("X-KMail-SignatureActionEnabled")) {
970 extras << hdr;
971 }
972 if (auto hdr = m_msg->headerByType("X-KMail-EncryptActionEnabled")) {
973 extras << hdr;
974 }
975 if (auto hdr = m_msg->headerByType("X-KMail-CryptoMessageFormat")) {
976 extras << hdr;
977 }
978 if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-To")) {
979 extras << hdr;
980 }
981 if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-CC")) {
982 extras << hdr;
983 }
984 if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-BCC")) {
985 extras << hdr;
986 }
987 if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-Reply-To")) {
988 extras << hdr;
989 }
990 if (auto hdr = m_msg->organization(false)) {
991 extras << hdr;
992 }
993 if (auto hdr = m_msg->headerByType("X-KMail-Identity")) {
994 extras << hdr;
995 }
996 if (auto hdr = m_msg->headerByType("X-KMail-Transport")) {
997 extras << hdr;
998 }
999 if (auto hdr = m_msg->headerByType("X-KMail-Fcc")) {
1000 extras << hdr;
1001 }
1002 if (auto hdr = m_msg->headerByType("X-KMail-Drafts")) {
1003 extras << hdr;
1004 }
1005 if (auto hdr = m_msg->headerByType("X-KMail-Templates")) {
1006 extras << hdr;
1007 }
1008 if (auto hdr = m_msg->headerByType("X-KMail-Link-Message")) {
1009 extras << hdr;
1010 }
1011 if (auto hdr = m_msg->headerByType("X-KMail-Link-Type")) {
1012 extras << hdr;
1013 }
1014 if (auto hdr = m_msg->headerByType("X-Face")) {
1015 extras << hdr;
1016 }
1017 if (auto hdr = m_msg->headerByType("Face")) {
1018 extras << hdr;
1019 }
1020 if (auto hdr = m_msg->headerByType("X-KMail-FccDisabled")) {
1021 extras << hdr;
1022 }
1023 if (auto hdr = m_msg->headerByType("X-KMail-Identity-Name")) {
1024 extras << hdr;
1025 }
1026 if (auto hdr = m_msg->headerByType("X-KMail-Transport-Name")) {
1027 extras << hdr;
1028 }
1029
1030 infoPart->setExtraHeaders(extras);
1031}
1032
1033void ComposerViewBase::slotSendComposeResult(KJob *job)
1034{
1035 Q_ASSERT(dynamic_cast<MessageComposer::Composer *>(job));
1036 auto composer = static_cast<MessageComposer::Composer *>(job);
1037 if (composer->error() != MessageComposer::Composer::NoError) {
1038 qCDebug(MESSAGECOMPOSER_LOG) << "compose job might have error: " << job->error() << " errorString: " << job->errorString();
1039 }
1040
1041 if (composer->error() == MessageComposer::Composer::NoError) {
1042 Q_ASSERT(m_composers.contains(composer));
1043 // The messages were composed successfully.
1044 qCDebug(MESSAGECOMPOSER_LOG) << "NoError.";
1045 const int numberOfMessage(composer->resultMessages().size());
1046 for (int i = 0; i < numberOfMessage; ++i) {
1047 if (mSaveIn == MessageComposer::MessageSender::SaveInNone) {
1048 queueMessage(composer->resultMessages().at(i), composer);
1049 } else {
1050 saveMessage(composer->resultMessages().at(i), mSaveIn);
1051 }
1052 }
1053 saveRecentAddresses(composer->resultMessages().at(0));
1054 } else if (composer->error() == MessageComposer::Composer::UserCancelledError) {
1055 // The job warned the user about something, and the user chose to return
1056 // to the message. Nothing to do.
1057 qCDebug(MESSAGECOMPOSER_LOG) << "UserCancelledError.";
1058 Q_EMIT failed(i18n("Job cancelled by the user"));
1059 } else {
1060 qCDebug(MESSAGECOMPOSER_LOG) << "other Error." << composer->error();
1061 QString msg;
1062 if (composer->error() == MessageComposer::Composer::BugError) {
1063 msg = i18n("Could not compose message: %1 \n Please report this bug.", job->errorString());
1064 } else {
1065 msg = i18n("Could not compose message: %1", job->errorString());
1066 }
1067 Q_EMIT failed(msg);
1068 }
1069
1070 if (!composer->gnupgHome().isEmpty()) {
1071 QDir dir(composer->gnupgHome());
1072 dir.removeRecursively();
1073 }
1074
1075 m_composers.removeAll(composer);
1076}
1077
1078void ComposerViewBase::saveRecentAddresses(const KMime::Message::Ptr &msg)
1079{
1080 KConfig *config = MessageComposer::MessageComposerSettings::self()->config();
1081 const QList<QByteArray> toAddresses = msg->to()->addresses();
1082 for (const QByteArray &address : toAddresses) {
1084 }
1085 const QList<QByteArray> ccAddresses = msg->cc()->addresses();
1086 for (const QByteArray &address : ccAddresses) {
1088 }
1089 const QList<QByteArray> bccAddresses = msg->bcc()->addresses();
1090 for (const QByteArray &address : bccAddresses) {
1092 }
1093}
1094
1095void ComposerViewBase::queueMessage(const KMime::Message::Ptr &message, MessageComposer::Composer *composer)
1096{
1097 const MessageComposer::InfoPart *infoPart = composer->infoPart();
1098 auto qjob = new Akonadi::MessageQueueJob(this);
1099 qjob->setMessage(message);
1100 qjob->transportAttribute().setTransportId(infoPart->transportId());
1101 if (mSendMethod == MessageComposer::MessageSender::SendLater) {
1102 qjob->dispatchModeAttribute().setDispatchMode(Akonadi::DispatchModeAttribute::Manual);
1103 }
1104
1105 if (message->hasHeader("X-KMail-FccDisabled")) {
1106 qjob->sentBehaviourAttribute().setSentBehaviour(Akonadi::SentBehaviourAttribute::Delete);
1107 } else if (!infoPart->fcc().isEmpty()) {
1108 qjob->sentBehaviourAttribute().setSentBehaviour(Akonadi::SentBehaviourAttribute::MoveToCollection);
1109
1110 const Akonadi::Collection sentCollection(infoPart->fcc().toLongLong());
1111 qjob->sentBehaviourAttribute().setMoveToCollection(sentCollection);
1112 } else {
1113 qjob->sentBehaviourAttribute().setSentBehaviour(Akonadi::SentBehaviourAttribute::MoveToDefaultSentCollection);
1114 }
1115
1116 MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportById(infoPart->transportId());
1117 if (transport && transport->specifySenderOverwriteAddress()) {
1118 qjob->addressAttribute().setFrom(
1120 } else {
1122 }
1123 // if this header is not empty, it contains the real recipient of the message, either the primary or one of the
1124 // secondary recipients. so we set that to the transport job, while leaving the message itself alone.
1125 if (KMime::Headers::Base *realTo = message->headerByType("X-KMail-EncBccRecipients")) {
1126 qjob->addressAttribute().setTo(MessageComposer::Util::cleanUpEmailListAndEncoding(realTo->asUnicodeString().split(QLatin1Char('%'))));
1127 message->removeHeader("X-KMail-EncBccRecipients");
1128 message->assemble();
1129 qCDebug(MESSAGECOMPOSER_LOG) << "sending with-bcc encr mail to a/n recipient:" << qjob->addressAttribute().to();
1130 } else {
1131 qjob->addressAttribute().setTo(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->to()));
1132 qjob->addressAttribute().setCc(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->cc()));
1133 qjob->addressAttribute().setBcc(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->bcc()));
1134 }
1135 if (m_requestDeleveryConfirmation) {
1136 qjob->addressAttribute().setDeliveryStatusNotification(true);
1137 }
1138 MessageComposer::Util::addSendReplyForwardAction(message, qjob);
1140
1141 MessageComposer::Util::addCustomHeaders(message, m_customHeader);
1142 message->assemble();
1143 connect(qjob, &Akonadi::MessageQueueJob::result, this, &ComposerViewBase::slotQueueResult);
1144 m_pendingQueueJobs++;
1145 qjob->start();
1146
1147 qCDebug(MESSAGECOMPOSER_LOG) << "Queued a message.";
1148}
1149
1150void ComposerViewBase::slotQueueResult(KJob *job)
1151{
1152 m_pendingQueueJobs--;
1153 auto qjob = static_cast<Akonadi::MessageQueueJob *>(job);
1154 qCDebug(MESSAGECOMPOSER_LOG) << "mPendingQueueJobs" << m_pendingQueueJobs;
1155 Q_ASSERT(m_pendingQueueJobs >= 0);
1156
1157 if (job->error()) {
1158 qCDebug(MESSAGECOMPOSER_LOG) << "Failed to queue a message:" << job->errorString();
1159 // There is not much we can do now, since all the MessageQueueJobs have been
1160 // started. So just wait for them to finish.
1161 // TODO show a message box or something
1162 QString msg = i18n("There were problems trying to queue the message for sending: %1", job->errorString());
1163
1164 if (m_pendingQueueJobs == 0) {
1165 Q_EMIT failed(msg);
1166 return;
1167 }
1168 }
1169
1170 if (m_pendingQueueJobs == 0) {
1171 addFollowupReminder(qjob->message()->messageID(false)->asUnicodeString());
1173 }
1174}
1175
1176void ComposerViewBase::initAutoSave()
1177{
1178 qCDebug(MESSAGECOMPOSER_LOG) << "initialising autosave";
1179
1180 // Ensure that the autosave directory exists.
1182 if (!dataDirectory.exists(QStringLiteral("autosave"))) {
1183 qCDebug(MESSAGECOMPOSER_LOG) << "Creating autosave directory.";
1184 dataDirectory.mkdir(QStringLiteral("autosave"));
1185 }
1186
1187 // Construct a file name
1188 if (m_autoSaveUUID.isEmpty()) {
1189 m_autoSaveUUID = QUuid::createUuid().toString();
1190 }
1191
1193}
1194
1195Akonadi::Collection ComposerViewBase::followUpCollection() const
1196{
1197 return mFollowUpCollection;
1198}
1199
1200void ComposerViewBase::setFollowUpCollection(const Akonadi::Collection &followUpCollection)
1201{
1202 mFollowUpCollection = followUpCollection;
1203}
1204
1205QDate ComposerViewBase::followUpDate() const
1206{
1207 return mFollowUpDate;
1208}
1209
1210void ComposerViewBase::setFollowUpDate(const QDate &followUpDate)
1211{
1212 mFollowUpDate = followUpDate;
1213}
1214
1215Sonnet::DictionaryComboBox *ComposerViewBase::dictionary() const
1216{
1217 return m_dictionary;
1218}
1219
1220void ComposerViewBase::setDictionary(Sonnet::DictionaryComboBox *dictionary)
1221{
1222 m_dictionary = dictionary;
1223}
1224
1226{
1227 if (m_autoSaveInterval == 0) {
1228 delete m_autoSaveTimer;
1229 m_autoSaveTimer = nullptr;
1230 } else {
1231 if (!m_autoSaveTimer) {
1232 m_autoSaveTimer = new QTimer(this);
1233 if (m_parentWidget) {
1234 connect(m_autoSaveTimer, SIGNAL(timeout()), m_parentWidget, SLOT(autoSaveMessage()));
1235 } else {
1237 }
1238 }
1239 m_autoSaveTimer->start(m_autoSaveInterval);
1240 }
1241}
1242
1244{
1245 delete m_autoSaveTimer;
1246 m_autoSaveTimer = nullptr;
1247 if (!m_autoSaveUUID.isEmpty()) {
1248 qCDebug(MESSAGECOMPOSER_LOG) << "deleting autosave files" << m_autoSaveUUID;
1249
1250 // Delete the autosave files
1252
1253 // Filter out only this composer window's autosave files
1254 const QStringList autoSaveFilter{m_autoSaveUUID + QLatin1StringView("*")};
1255 autoSaveDir.setNameFilters(autoSaveFilter);
1256
1257 // Return the files to be removed
1258 const QStringList autoSaveFiles = autoSaveDir.entryList();
1259 qCDebug(MESSAGECOMPOSER_LOG) << "There are" << autoSaveFiles.count() << "to be deleted.";
1260
1261 // Delete each file
1262 for (const QString &file : autoSaveFiles) {
1263 autoSaveDir.remove(file);
1264 }
1265 m_autoSaveUUID.clear();
1266 }
1267}
1268
1269//-----------------------------------------------------------------------------
1271{
1272 qCDebug(MESSAGECOMPOSER_LOG) << "Autosaving message";
1273
1274 if (m_autoSaveTimer) {
1275 m_autoSaveTimer->stop();
1276 }
1277
1278 if (!m_composers.isEmpty()) {
1279 // This may happen if e.g. the autosave timer calls applyChanges.
1280 qCDebug(MESSAGECOMPOSER_LOG) << "Autosave: Called while composer active; ignoring. Number of composer " << m_composers.count();
1281 return;
1282 }
1283
1284 auto composer = new Composer();
1285 fillComposer(composer);
1286 composer->setAutoSave(true);
1287 composer->setAutocryptEnabled(autocryptEnabled());
1288 m_composers.append(composer);
1289 connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotAutoSaveComposeResult);
1290 composer->start();
1291}
1292
1294{
1295 m_autoSaveUUID = fileName;
1296
1297 Q_EMIT modified(true);
1298}
1299
1300void ComposerViewBase::slotAutoSaveComposeResult(KJob *job)
1301{
1303
1304 Q_ASSERT(dynamic_cast<Composer *>(job));
1305 auto composer = static_cast<Composer *>(job);
1306
1307 if (composer->error() == Composer::NoError) {
1308 Q_ASSERT(m_composers.contains(composer));
1309
1310 // The messages were composed successfully. Only save the first message, there should
1311 // only be one anyway, since crypto is disabled.
1312 qCDebug(MESSAGECOMPOSER_LOG) << "NoError.";
1313 writeAutoSaveToDisk(composer->resultMessages().constFirst());
1314 Q_ASSERT(composer->resultMessages().size() == 1);
1315
1316 if (m_autoSaveInterval > 0) {
1318 }
1319 } else if (composer->error() == MessageComposer::Composer::UserCancelledError) {
1320 // The job warned the user about something, and the user chose to return
1321 // to the message. Nothing to do.
1322 qCDebug(MESSAGECOMPOSER_LOG) << "UserCancelledError.";
1323 Q_EMIT failed(i18n("Job cancelled by the user"), AutoSave);
1324 } else {
1325 qCDebug(MESSAGECOMPOSER_LOG) << "other Error.";
1326 Q_EMIT failed(i18n("Could not autosave message: %1", job->errorString()), AutoSave);
1327 }
1328
1329 m_composers.removeAll(composer);
1330}
1331
1332void ComposerViewBase::writeAutoSaveToDisk(const KMime::Message::Ptr &message)
1333{
1336 const QString filename = autosavePath + m_autoSaveUUID;
1337 QSaveFile file(filename);
1339 qCDebug(MESSAGECOMPOSER_LOG) << "Writing message to disk as" << filename;
1340
1341 if (file.open(QIODevice::WriteOnly)) {
1342 file.setPermissions(QFile::ReadUser | QFile::WriteUser);
1343
1344 if (file.write(message->encodedContent()) != static_cast<qint64>(message->encodedContent().size())) {
1345 errorMessage = i18n("Could not write all data to file.");
1346 } else {
1347 if (!file.commit()) {
1348 errorMessage = i18n("Could not finalize the file.");
1349 }
1350 }
1351 } else {
1352 errorMessage = i18n("Could not open file.");
1353 }
1354
1355 if (!errorMessage.isEmpty()) {
1356 qCWarning(MESSAGECOMPOSER_LOG) << "Auto saving failed:" << errorMessage << file.errorString() << " m_autoSaveUUID" << m_autoSaveUUID;
1357 if (!m_autoSaveErrorShown) {
1358 KMessageBox::error(m_parentWidget,
1359 i18n("Autosaving the message as %1 failed.\n"
1360 "%2\n"
1361 "Reason: %3",
1362 filename,
1363 errorMessage,
1364 file.errorString()),
1365 i18nc("@title:window", "Autosaving Message Failed"));
1366
1367 // Error dialog shown, hide the errors the next time
1368 m_autoSaveErrorShown = true;
1369 }
1370 } else {
1371 // No error occurred, the next error should be shown again
1372 m_autoSaveErrorShown = false;
1373 }
1374 file.commit();
1375 message->clear();
1376}
1377
1378void ComposerViewBase::saveMessage(const KMime::Message::Ptr &message, MessageComposer::MessageSender::SaveIn saveIn)
1379{
1380 Akonadi::Collection target;
1381 const auto identity = currentIdentity();
1382 message->date()->setDateTime(QDateTime::currentDateTime());
1383 if (!identity.isNull()) {
1384 if (auto header = message->headerByType("X-KMail-Fcc")) {
1385 const int sentCollectionId = header->asUnicodeString().toInt();
1386 if (identity.fcc() == QString::number(sentCollectionId)) {
1387 message->removeHeader("X-KMail-Fcc");
1388 }
1389 }
1390 }
1391 MessageComposer::Util::addCustomHeaders(message, m_customHeader);
1392
1393 message->assemble();
1394
1395 Akonadi::Item item;
1396 item.setMimeType(QStringLiteral("message/rfc822"));
1397 item.setPayload(message);
1399
1400 if (!identity.isNull()) { // we have a valid identity
1401 switch (saveIn) {
1402 case MessageComposer::MessageSender::SaveInTemplates:
1403 if (!identity.templates().isEmpty()) { // the user has specified a custom templates collection
1404 target = Akonadi::Collection(identity.templates().toLongLong());
1405 }
1406 break;
1407 case MessageComposer::MessageSender::SaveInDrafts:
1408 if (!identity.drafts().isEmpty()) { // the user has specified a custom drafts collection
1409 target = Akonadi::Collection(identity.drafts().toLongLong());
1410 }
1411 break;
1412 case MessageComposer::MessageSender::SaveInOutbox: // We don't define save outbox in identity
1414 break;
1415 case MessageComposer::MessageSender::SaveInNone:
1416 break;
1417 }
1418
1420 saveMessageJob->setProperty("Akonadi::Item", QVariant::fromValue(item));
1421 QObject::connect(saveMessageJob, &Akonadi::CollectionFetchJob::result, this, &ComposerViewBase::slotSaveMessage);
1422 } else {
1423 // preinitialize with the default collections
1424 target = defaultSpecialTarget();
1425 auto create = new Akonadi::ItemCreateJob(item, target, this);
1426 connect(create, &Akonadi::ItemCreateJob::result, this, &ComposerViewBase::slotCreateItemResult);
1427 ++m_pendingQueueJobs;
1428 }
1429}
1430
1431void ComposerViewBase::slotSaveMessage(KJob *job)
1432{
1433 Akonadi::Collection target;
1434 auto item = job->property("Akonadi::Item").value<Akonadi::Item>();
1435 if (job->error()) {
1436 target = defaultSpecialTarget();
1437 } else {
1439 if (fetchJob->collections().isEmpty()) {
1440 target = defaultSpecialTarget();
1441 } else {
1442 target = fetchJob->collections().at(0);
1443 }
1444 }
1445 auto create = new Akonadi::ItemCreateJob(item, target, this);
1446 connect(create, &Akonadi::ItemCreateJob::result, this, &ComposerViewBase::slotCreateItemResult);
1447 ++m_pendingQueueJobs;
1448}
1449
1450Akonadi::Collection ComposerViewBase::defaultSpecialTarget() const
1451{
1452 Akonadi::Collection target;
1453 switch (mSaveIn) {
1454 case MessageComposer::MessageSender::SaveInNone:
1455 break;
1456 case MessageComposer::MessageSender::SaveInDrafts:
1458 break;
1459 case MessageComposer::MessageSender::SaveInTemplates:
1461 break;
1462 case MessageComposer::MessageSender::SaveInOutbox:
1464 break;
1465 }
1466
1467 return target;
1468}
1469
1470void ComposerViewBase::slotCreateItemResult(KJob *job)
1471{
1472 --m_pendingQueueJobs;
1473 qCDebug(MESSAGECOMPOSER_LOG) << "mPendingCreateItemJobs" << m_pendingQueueJobs;
1474 Q_ASSERT(m_pendingQueueJobs >= 0);
1475
1476 if (job->error()) {
1477 qCWarning(MESSAGECOMPOSER_LOG) << "Failed to save a message:" << job->errorString();
1478 Q_EMIT failed(i18n("Failed to save the message: %1", job->errorString()));
1479 return;
1480 }
1481
1482 Akonadi::Item::Id id = -1;
1483 if (mSendLaterInfo) {
1484 auto createJob = static_cast<Akonadi::ItemCreateJob *>(job);
1485 const Akonadi::Item item = createJob->item();
1486 if (item.isValid()) {
1487 id = item.id();
1488 addSendLaterItem(item);
1489 }
1490 }
1491
1492 if (m_pendingQueueJobs == 0) {
1494 }
1495}
1496
1497void ComposerViewBase::addAttachment(const QUrl &url, const QString &comment, bool sync)
1498{
1499 Q_UNUSED(comment)
1500 qCDebug(MESSAGECOMPOSER_LOG) << "adding attachment with url:" << url;
1501 if (sync) {
1502 m_attachmentController->addAttachmentUrlSync(url);
1503 } else {
1504 m_attachmentController->addAttachment(url);
1505 }
1506}
1507
1508void ComposerViewBase::addAttachment(const QString &name, const QString &filename, const QString &charset, const QByteArray &data, const QByteArray &mimeType)
1509{
1511 if (!data.isEmpty()) {
1512 attachment->setName(name);
1513 attachment->setFileName(filename);
1514 attachment->setData(data);
1515 attachment->setCharset(charset.toLatin1());
1516 attachment->setMimeType(mimeType);
1517 // TODO what about the other fields?
1518
1519 m_attachmentController->addAttachment(attachment);
1520 }
1521}
1522
1523void ComposerViewBase::addAttachmentPart(KMime::Content *partToAttach)
1524{
1526 if (partToAttach->contentType()->mimeType() == "multipart/digest" || partToAttach->contentType(false)->mimeType() == "message/rfc822") {
1527 // if it is a digest or a full message, use the encodedContent() of the attachment,
1528 // which already has the proper headers
1529 part->setData(partToAttach->encodedContent());
1530 } else {
1531 part->setData(partToAttach->decodedContent());
1532 }
1533 part->setMimeType(partToAttach->contentType(false)->mimeType());
1534 if (auto cd = partToAttach->contentDescription(false)) {
1535 part->setDescription(cd->asUnicodeString());
1536 }
1537 if (auto ct = partToAttach->contentType(false)) {
1538 if (ct->hasParameter(QStringLiteral("name"))) {
1539 part->setName(ct->parameter(QStringLiteral("name")));
1540 }
1541 }
1542 if (auto cd = partToAttach->contentDisposition(false)) {
1543 part->setFileName(cd->filename());
1544 part->setInline(cd->disposition() == KMime::Headers::CDinline);
1545 }
1546 if (part->name().isEmpty() && !part->fileName().isEmpty()) {
1547 part->setName(part->fileName());
1548 }
1549 if (part->fileName().isEmpty() && !part->name().isEmpty()) {
1550 part->setFileName(part->name());
1551 }
1552 m_attachmentController->addAttachment(part);
1553}
1554
1555void ComposerViewBase::fillComposer(MessageComposer::Composer *composer)
1556{
1557 fillComposer(composer, UseUnExpandedRecipients, false);
1558}
1559
1560void ComposerViewBase::fillComposer(MessageComposer::Composer *composer, ComposerViewBase::RecipientExpansion expansion, bool autoresize)
1561{
1562 fillGlobalPart(composer->globalPart());
1563 m_editor->fillComposerTextPart(composer->textPart());
1564 fillInfoPart(composer->infoPart(), expansion);
1565 if (m_attachmentModel) {
1566 composer->addAttachmentParts(m_attachmentModel->attachments(), autoresize);
1567 }
1568}
1569
1570//-----------------------------------------------------------------------------
1572{
1573 if (m_recipientsEditor) {
1574 return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(MessageComposer::Recipient::To));
1575 }
1576 return {};
1577}
1578
1579//-----------------------------------------------------------------------------
1580QString ComposerViewBase::cc() const
1581{
1582 if (m_recipientsEditor) {
1583 return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(MessageComposer::Recipient::Cc));
1584 }
1585 return {};
1586}
1587
1588//-----------------------------------------------------------------------------
1589QString ComposerViewBase::bcc() const
1590{
1591 if (m_recipientsEditor) {
1592 return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(MessageComposer::Recipient::Bcc));
1593 }
1594 return {};
1595}
1596
1597QString ComposerViewBase::from() const
1598{
1599 return MessageComposer::Util::cleanedUpHeaderString(m_from);
1600}
1601
1602QString ComposerViewBase::replyTo() const
1603{
1604 if (m_recipientsEditor) {
1605 return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(MessageComposer::Recipient::ReplyTo));
1606 }
1607 return {};
1608}
1609
1610QString ComposerViewBase::subject() const
1611{
1612 return MessageComposer::Util::cleanedUpHeaderString(m_subject);
1613}
1614
1615const KIdentityManagementCore::Identity &ComposerViewBase::currentIdentity() const
1616{
1617 return m_identMan->identityForUoidOrDefault(m_identityCombo->currentIdentity());
1618}
1619
1620bool ComposerViewBase::autocryptEnabled() const
1621{
1622 return currentIdentity().autocryptEnabled();
1623}
1624
1625void ComposerViewBase::setParentWidgetForGui(QWidget *w)
1626{
1627 m_parentWidget = w;
1628}
1629
1630void ComposerViewBase::setAttachmentController(MessageComposer::AttachmentControllerBase *controller)
1631{
1632 m_attachmentController = controller;
1633}
1634
1635MessageComposer::AttachmentControllerBase *ComposerViewBase::attachmentController()
1636{
1637 return m_attachmentController;
1638}
1639
1641{
1642 m_attachmentModel = model;
1643}
1644
1645MessageComposer::AttachmentModel *ComposerViewBase::attachmentModel()
1646{
1647 return m_attachmentModel;
1648}
1649
1650void ComposerViewBase::setRecipientsEditor(MessageComposer::RecipientsEditor *recEditor)
1651{
1652 m_recipientsEditor = recEditor;
1653}
1654
1655MessageComposer::RecipientsEditor *ComposerViewBase::recipientsEditor()
1656{
1657 return m_recipientsEditor;
1658}
1659
1660void ComposerViewBase::setSignatureController(MessageComposer::SignatureController *sigController)
1661{
1662 m_signatureController = sigController;
1663}
1664
1665MessageComposer::SignatureController *ComposerViewBase::signatureController()
1666{
1667 return m_signatureController;
1668}
1669
1670void ComposerViewBase::setIdentityCombo(KIdentityManagementWidgets::IdentityCombo *identCombo)
1671{
1672 m_identityCombo = identCombo;
1673}
1674
1675KIdentityManagementWidgets::IdentityCombo *ComposerViewBase::identityCombo()
1676{
1677 return m_identityCombo;
1678}
1679
1680void ComposerViewBase::updateRecipients(const KIdentityManagementCore::Identity &ident,
1681 const KIdentityManagementCore::Identity &oldIdent,
1682 MessageComposer::Recipient::Type type)
1683{
1686 if (type == MessageComposer::Recipient::Bcc) {
1687 oldIdentList = oldIdent.bcc();
1688 newIdentList = ident.bcc();
1689 } else if (type == MessageComposer::Recipient::Cc) {
1690 oldIdentList = oldIdent.cc();
1691 newIdentList = ident.cc();
1692 } else if (type == MessageComposer::Recipient::ReplyTo) {
1693 oldIdentList = oldIdent.replyToAddr();
1694 newIdentList = ident.replyToAddr();
1695 } else {
1696 return;
1697 }
1698
1699 if (oldIdentList != newIdentList) {
1701 for (const KMime::Types::Mailbox &recipient : oldRecipients) {
1702 m_recipientsEditor->removeRecipient(recipient.prettyAddress(), type);
1703 }
1704
1706 for (const KMime::Types::Mailbox &recipient : newRecipients) {
1707 m_recipientsEditor->addRecipient(recipient.prettyAddress(), type);
1708 }
1709 m_recipientsEditor->setFocusBottom();
1710 }
1711}
1712
1713void ComposerViewBase::identityChanged(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, bool msgCleared)
1714{
1715 updateRecipients(ident, oldIdent, MessageComposer::Recipient::Bcc);
1716 updateRecipients(ident, oldIdent, MessageComposer::Recipient::Cc);
1717 updateRecipients(ident, oldIdent, MessageComposer::Recipient::ReplyTo);
1718
1721 // replace existing signatures
1722 const bool replaced = editor()->composerSignature()->replaceSignature(oldSig, newSig);
1723 // Just append the signature if there was no old signature
1724 if (!replaced && (msgCleared || oldSig.rawText().isEmpty())) {
1725 signatureController()->applySignature(newSig);
1726 }
1727 const QString vcardFileName = ident.vCardFile();
1728 attachmentController()->setIdentityHasOwnVcard(!vcardFileName.isEmpty());
1729 attachmentController()->setAttachOwnVcard(ident.attachVcard());
1730
1731 m_editor->setAutocorrectionLanguage(ident.autocorrectionLanguage());
1732}
1733
1734void ComposerViewBase::setEditor(MessageComposer::RichTextComposerNg *editor)
1735{
1736 m_editor = editor;
1737 m_editor->document()->setModified(false);
1738}
1739
1740MessageComposer::RichTextComposerNg *ComposerViewBase::editor() const
1741{
1742 return m_editor;
1743}
1744
1745void ComposerViewBase::setTransportCombo(MailTransport::TransportComboBox *transpCombo)
1746{
1747 m_transport = transpCombo;
1748}
1749
1750MailTransport::TransportComboBox *ComposerViewBase::transportComboBox() const
1751{
1752 return m_transport;
1753}
1754
1755void ComposerViewBase::setIdentityManager(KIdentityManagementCore::IdentityManager *identMan)
1756{
1757 m_identMan = identMan;
1758}
1759
1760KIdentityManagementCore::IdentityManager *ComposerViewBase::identityManager()
1761{
1762 return m_identMan;
1763}
1764
1765void ComposerViewBase::setFcc(const Akonadi::Collection &fccCollection)
1766{
1767 if (m_fccCombo) {
1769 } else {
1770 m_fccCollection = fccCollection;
1771 }
1773 connect(checkFccCollectionJob, &KJob::result, this, &ComposerViewBase::slotFccCollectionCheckResult);
1774}
1775
1776void ComposerViewBase::slotFccCollectionCheckResult(KJob *job)
1777{
1778 if (job->error()) {
1779 qCWarning(MESSAGECOMPOSER_LOG) << " void ComposerViewBase::slotFccCollectionCheckResult(KJob *job) error " << job->errorString();
1781 if (m_fccCombo) {
1782 m_fccCombo->setDefaultCollection(sentMailCol);
1783 } else {
1784 m_fccCollection = sentMailCol;
1785 }
1786 }
1787}
1788
1789void ComposerViewBase::setFccCombo(Akonadi::CollectionComboBox *fcc)
1790{
1791 m_fccCombo = fcc;
1792}
1793
1794Akonadi::CollectionComboBox *ComposerViewBase::fccCombo() const
1795{
1796 return m_fccCombo;
1797}
1798
1800{
1801 m_from = from;
1802}
1803
1804void ComposerViewBase::setSubject(const QString &subject)
1805{
1806 m_subject = subject;
1807 if (mSendLaterInfo) {
1808 mSendLaterInfo->setSubject(m_subject);
1809 mSendLaterInfo->setTo(to());
1810 }
1811}
1812
1813void ComposerViewBase::setAutoSaveInterval(int interval)
1814{
1815 m_autoSaveInterval = interval;
1816}
1817
1818void ComposerViewBase::setCryptoOptions(bool sign, bool encrypt, Kleo::CryptoMessageFormat format, bool neverEncryptDrafts)
1819{
1820 m_sign = sign;
1821 m_encrypt = encrypt;
1822 m_cryptoMessageFormat = format;
1823 m_neverEncrypt = neverEncryptDrafts;
1824}
1825
1826void ComposerViewBase::setCharsets(const QList<QByteArray> &charsets)
1827{
1828 m_charsets = charsets;
1829}
1830
1831void ComposerViewBase::setMDNRequested(bool mdnRequested)
1832{
1833 m_mdnRequested = mdnRequested;
1834}
1835
1836void ComposerViewBase::setUrgent(bool urgent)
1837{
1838 m_urgent = urgent;
1839}
1840
1841int ComposerViewBase::autoSaveInterval() const
1842{
1843 return m_autoSaveInterval;
1844}
1845
1846//-----------------------------------------------------------------------------
1847void ComposerViewBase::collectImages(KMime::Content *root)
1848{
1849 if (KMime::Content *n = Util::findTypeInMessage(root, "multipart", "alternative")) {
1850 KMime::Content *parentnode = n->parent();
1851 if (parentnode && parentnode->contentType()->isMultipart() && parentnode->contentType()->subType() == "related") {
1853 while (node) {
1854 if (node->contentType()->isImage()) {
1855 qCDebug(MESSAGECOMPOSER_LOG) << "found image in multipart/related : " << node->contentType()->name();
1856 QImage img;
1857 img.loadFromData(node->decodedContent());
1858 m_editor->composerControler()->composerImages()->loadImage(
1859 img,
1860 QString::fromLatin1(QByteArray(QByteArrayLiteral("cid:") + node->contentID()->identifier())),
1861 node->contentType()->name());
1862 }
1864 }
1865 }
1866 }
1867}
1868
1869//-----------------------------------------------------------------------------
1870bool ComposerViewBase::inlineSigningEncryptionSelected() const
1871{
1872 if (!m_sign && !m_encrypt) {
1873 return false;
1874 }
1875 return m_cryptoMessageFormat == Kleo::InlineOpenPGPFormat;
1876}
1877
1878bool ComposerViewBase::hasMissingAttachments(const QStringList &attachmentKeywords)
1879{
1880 if (attachmentKeywords.isEmpty()) {
1881 return false;
1882 }
1883 if (m_attachmentModel && m_attachmentModel->rowCount() > 0) {
1884 return false;
1885 }
1886
1887 return MessageComposer::Util::hasMissingAttachments(attachmentKeywords, m_editor->document(), subject());
1888}
1889
1890ComposerViewBase::MissingAttachment ComposerViewBase::checkForMissingAttachments(const QStringList &attachmentKeywords)
1891{
1892 if (!hasMissingAttachments(attachmentKeywords)) {
1893 return NoMissingAttachmentFound;
1894 }
1895 const int rc = KMessageBox::warningTwoActionsCancel(m_editor,
1896
1897 i18n("The message you have composed seems to refer to an "
1898 "attached file but you have not attached anything.\n"
1899 "Do you want to attach a file to your message?"),
1900 i18nc("@title:window", "File Attachment Reminder"),
1901 KGuiItem(i18n("&Attach File..."), QLatin1StringView("mail-attachment")),
1902 KGuiItem(i18n("&Send as Is"), QLatin1StringView("mail-send")));
1903 if (rc == KMessageBox::Cancel) {
1904 return FoundMissingAttachmentAndCancel;
1905 }
1906 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
1907 m_attachmentController->showAddAttachmentFileDialog();
1908 return FoundMissingAttachmentAndAddedAttachment;
1909 }
1910
1911 return FoundMissingAttachmentAndSending;
1912}
1913
1914void ComposerViewBase::markAllAttachmentsForSigning(bool sign)
1915{
1916 if (m_attachmentModel) {
1917 const auto attachments = m_attachmentModel->attachments();
1918 for (MessageCore::AttachmentPart::Ptr attachment : attachments) {
1919 attachment->setSigned(sign);
1920 }
1921 }
1922}
1923
1924void ComposerViewBase::markAllAttachmentsForEncryption(bool encrypt)
1925{
1926 if (m_attachmentModel) {
1927 const auto attachments = m_attachmentModel->attachments();
1928 for (MessageCore::AttachmentPart::Ptr attachment : attachments) {
1929 attachment->setEncrypted(encrypt);
1930 }
1931 }
1932}
1933
1934bool ComposerViewBase::determineWhetherToSign(bool doSignCompletely, Kleo::KeyResolver *keyResolver, bool signSomething, bool &result, bool &canceled)
1935{
1936 bool sign = false;
1937 switch (keyResolver->checkSigningPreferences(signSomething)) {
1938 case Kleo::DoIt:
1939 if (!signSomething) {
1940 markAllAttachmentsForSigning(true);
1941 return true;
1942 }
1943 sign = true;
1944 break;
1945 case Kleo::DontDoIt:
1946 sign = false;
1947 break;
1948 case Kleo::AskOpportunistic:
1949 assert(0);
1950 case Kleo::Ask: {
1951 // the user wants to be asked or has to be asked
1953 const QString msg = i18n(
1954 "Examination of the recipient's signing preferences "
1955 "yielded that you be asked whether or not to sign "
1956 "this message.\n"
1957 "Sign this message?");
1958 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
1959 msg,
1960 i18nc("@title:window", "Sign Message?"),
1961 KGuiItem(i18nc("to sign", "&Sign")),
1962 KGuiItem(i18n("Do &Not Sign")))) {
1964 result = false;
1965 canceled = true;
1966 return false;
1967 case KMessageBox::ButtonCode::PrimaryAction:
1968 markAllAttachmentsForSigning(true);
1969 return true;
1970 case KMessageBox::ButtonCode::SecondaryAction:
1971 markAllAttachmentsForSigning(false);
1972 return false;
1973 default:
1974 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
1975 return false;
1976 }
1977 break;
1978 }
1979 case Kleo::Conflict: {
1980 // warn the user that there are conflicting signing preferences
1982 const QString msg = i18n(
1983 "There are conflicting signing preferences "
1984 "for these recipients.\n"
1985 "Sign this message?");
1986 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
1987 msg,
1988 i18nc("@title:window", "Sign Message?"),
1989 KGuiItem(i18nc("to sign", "&Sign")),
1990 KGuiItem(i18n("Do &Not Sign")))) {
1992 result = false;
1993 canceled = true;
1994 return false;
1995 case KMessageBox::ButtonCode::PrimaryAction:
1996 markAllAttachmentsForSigning(true);
1997 return true;
1998 case KMessageBox::ButtonCode::SecondaryAction:
1999 markAllAttachmentsForSigning(false);
2000 return false;
2001 default:
2002 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2003 return false;
2004 }
2005 break;
2006 }
2007 case Kleo::Impossible: {
2009 const QString msg = i18n(
2010 "You have requested to sign this message, "
2011 "but no valid signing keys have been configured "
2012 "for this identity.");
2013 if (KMessageBox::warningContinueCancel(m_parentWidget, msg, i18nc("@title:window", "Send Unsigned?"), KGuiItem(i18n("Send &Unsigned")))
2015 result = false;
2016 return false;
2017 } else {
2018 markAllAttachmentsForSigning(false);
2019 return false;
2020 }
2021 }
2022 }
2023
2024 if (!sign || !doSignCompletely) {
2025 if (cryptoWarningUnsigned(currentIdentity())) {
2027 const QString msg = sign && !doSignCompletely ? i18n(
2028 "Some parts of this message will not be signed.\n"
2029 "Sending only partially signed messages might violate site policy.\n"
2030 "Sign all parts instead?") // oh, I hate this...
2031 : i18n(
2032 "This message will not be signed.\n"
2033 "Sending unsigned message might violate site policy.\n"
2034 "Sign message instead?"); // oh, I hate this...
2035 const QString buttonText = sign && !doSignCompletely ? i18n("&Sign All Parts") : i18n("&Sign");
2036 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
2037 msg,
2038 i18nc("@title:window", "Unsigned-Message Warning"),
2039 KGuiItem(buttonText),
2040 KGuiItem(i18n("Send &As Is")))) {
2042 result = false;
2043 canceled = true;
2044 return false;
2045 case KMessageBox::ButtonCode::PrimaryAction:
2046 markAllAttachmentsForSigning(true);
2047 return true;
2048 case KMessageBox::ButtonCode::SecondaryAction:
2049 return sign || doSignCompletely;
2050 default:
2051 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2052 return false;
2053 }
2054 }
2055 }
2056 return sign || doSignCompletely;
2057}
2058
2059bool ComposerViewBase::determineWhetherToEncrypt(bool doEncryptCompletely,
2060 Kleo::KeyResolver *keyResolver,
2061 bool encryptSomething,
2062 bool signSomething,
2063 bool &result,
2064 bool &canceled)
2065{
2066 bool encrypt = false;
2067 bool opportunistic = false;
2068 switch (keyResolver->checkEncryptionPreferences(encryptSomething)) {
2069 case Kleo::DoIt:
2070 if (!encryptSomething) {
2071 markAllAttachmentsForEncryption(true);
2072 return true;
2073 }
2074 encrypt = true;
2075 break;
2076 case Kleo::DontDoIt:
2077 encrypt = false;
2078 break;
2079 case Kleo::AskOpportunistic:
2080 opportunistic = true;
2081 // fall through...
2082 [[fallthrough]];
2083 case Kleo::Ask: {
2084 // the user wants to be asked or has to be asked
2086 const QString msg = opportunistic ? i18n(
2087 "Valid trusted encryption keys were found for all recipients.\n"
2088 "Encrypt this message?")
2089 : i18n(
2090 "Examination of the recipient's encryption preferences "
2091 "yielded that you be asked whether or not to encrypt "
2092 "this message.\n"
2093 "Encrypt this message?");
2094 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
2095 msg,
2096 i18n("Encrypt Message?"),
2097 KGuiItem(signSomething ? i18n("Sign && &Encrypt") : i18n("&Encrypt")),
2098 KGuiItem(signSomething ? i18n("&Sign Only") : i18n("&Send As-Is")))) {
2100 result = false;
2101 canceled = true;
2102 return false;
2103 case KMessageBox::ButtonCode::PrimaryAction:
2104 markAllAttachmentsForEncryption(true);
2105 return true;
2106 case KMessageBox::ButtonCode::SecondaryAction:
2107 markAllAttachmentsForEncryption(false);
2108 return false;
2109 default:
2110 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2111 return false;
2112 }
2113 break;
2114 }
2115 case Kleo::Conflict: {
2116 // warn the user that there are conflicting encryption preferences
2118 const QString msg = i18n(
2119 "There are conflicting encryption preferences "
2120 "for these recipients.\n"
2121 "Encrypt this message?");
2123
2124 m_parentWidget,
2125 msg,
2126 i18n("Encrypt Message?"),
2127 KGuiItem(i18n("&Encrypt")),
2128 KGuiItem(i18n("Do &Not Encrypt")))) {
2130 result = false;
2131 canceled = true;
2132 return false;
2133 case KMessageBox::ButtonCode::PrimaryAction:
2134 markAllAttachmentsForEncryption(true);
2135 return true;
2136 case KMessageBox::ButtonCode::SecondaryAction:
2137 markAllAttachmentsForEncryption(false);
2138 return false;
2139 default:
2140 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2141 return false;
2142 }
2143 break;
2144 }
2145 case Kleo::Impossible: {
2147 const QString msg = i18n(
2148 "You have requested to encrypt this message, "
2149 "and to encrypt a copy to yourself, "
2150 "but no valid trusted encryption keys have been "
2151 "configured for this identity.");
2152 if (KMessageBox::warningContinueCancel(m_parentWidget, msg, i18nc("@title:window", "Send Unencrypted?"), KGuiItem(i18n("Send &Unencrypted")))
2154 result = false;
2155 return false;
2156 } else {
2157 markAllAttachmentsForEncryption(false);
2158 return false;
2159 }
2160 }
2161 }
2162
2163 if (!encrypt || !doEncryptCompletely) {
2164 if (cryptoWarningUnencrypted(currentIdentity())) {
2166 const QString msg = !doEncryptCompletely ? i18n(
2167 "Some parts of this message will not be encrypted.\n"
2168 "Sending only partially encrypted messages might violate "
2169 "site policy and/or leak sensitive information.\n"
2170 "Encrypt all parts instead?") // oh, I hate this...
2171 : i18n(
2172 "This message will not be encrypted.\n"
2173 "Sending unencrypted messages might violate site policy and/or "
2174 "leak sensitive information.\n"
2175 "Encrypt messages instead?"); // oh, I hate this...
2176 const QString buttonText = !doEncryptCompletely ? i18n("&Encrypt All Parts") : i18n("&Encrypt");
2177 switch (KMessageBox::warningTwoActionsCancel(m_parentWidget,
2178 msg,
2179 i18nc("@title:window", "Unencrypted Message Warning"),
2180 KGuiItem(buttonText),
2181 KGuiItem(signSomething ? i18n("&Sign Only") : i18n("&Send As-Is")))) {
2183 result = false;
2184 canceled = true;
2185 return false;
2186 case KMessageBox::ButtonCode::PrimaryAction:
2187 markAllAttachmentsForEncryption(true);
2188 return true;
2189 case KMessageBox::ButtonCode::SecondaryAction:
2190 return encrypt || doEncryptCompletely;
2191 default:
2192 qCWarning(MESSAGECOMPOSER_LOG) << "Unhandled MessageBox response";
2193 return false;
2194 }
2195 }
2196 }
2197
2198 return encrypt || doEncryptCompletely;
2199}
2200
2201void ComposerViewBase::setSendLaterInfo(SendLaterInfo *info)
2202{
2203 mSendLaterInfo.reset(info);
2204}
2205
2206SendLaterInfo *ComposerViewBase::sendLaterInfo() const
2207{
2208 return mSendLaterInfo.get();
2209}
2210
2211void ComposerViewBase::addFollowupReminder(const QString &messageId)
2212{
2213 if (!messageId.isEmpty()) {
2214 if (mFollowUpDate.isValid()) {
2216 job->setSubject(m_subject);
2217 job->setMessageId(messageId);
2218 job->setTo(mExpandedReplyTo.isEmpty() ? mExpandedTo.join(QLatin1Char(',')) : mExpandedReplyTo.join(QLatin1Char(',')));
2219 job->setFollowUpReminderDate(mFollowUpDate);
2220 job->setCollectionToDo(mFollowUpCollection);
2221 job->start();
2222 }
2223 }
2224}
2225
2226void ComposerViewBase::addSendLaterItem(const Akonadi::Item &item)
2227{
2228 mSendLaterInfo->setItemId(item.id());
2229
2230 auto job = new MessageComposer::SendLaterCreateJob(*mSendLaterInfo, this);
2231 job->start();
2232}
2233
2234bool ComposerViewBase::requestDeleveryConfirmation() const
2235{
2236 return m_requestDeleveryConfirmation;
2237}
2238
2239void ComposerViewBase::setRequestDeleveryConfirmation(bool requestDeleveryConfirmation)
2240{
2241 m_requestDeleveryConfirmation = requestDeleveryConfirmation;
2242}
2243
2244KMime::Message::Ptr ComposerViewBase::msg() const
2245{
2246 return m_msg;
2247}
2248
2249std::shared_ptr<Kleo::ExpiryChecker> ComposerViewBase::expiryChecker()
2250{
2251 if (!mExpiryChecker) {
2252 mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(),
2256 }
2257 return mExpiryChecker;
2258}
2259
2260#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)
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() const const
QDateTime currentDateTime()
bool mkpath(const QString &dirPath) const const
int exec(QEventLoop::ProcessEventsFlags flags)
void quit()
void append(const T &value)
bool contains(const T &value) const const
int count(const T &value) const const
bool isEmpty() const const
void push_back(const T &value)
int removeAll(const T &value)
void reserve(int alloc)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QVariant property(const char *name) const const
T qobject_cast(QObject *object)
T * data() const const
QString writableLocation(QStandardPaths::StandardLocation type)
void clear()
QString fromLatin1(const char *str, int size)
QString fromUtf8(const char *str, int size)
bool isEmpty() const const
QString number(int n, int base)
QString & replace(int position, int n, QChar after)
QByteArray toLatin1() const const
qlonglong toLongLong(bool *ok, int base) const const
QByteArray toUtf8() const const
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
QString join(const QString &separator) const const
WaitCursor
void start(int msec)
void stop()
void timeout()
QUrl fromLocalFile(const QString &localFile)
QUuid createUuid()
QString toString() const const
QVariant fromValue(const T &value)
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sun Feb 25 2024 18:37:30 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.