File indexing completed on 2025-01-19 04:51:57
0001 /* 0002 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net> 0003 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com> 0004 0005 This library is free software; you can redistribute it and/or modify it 0006 under the terms of the GNU Library General Public License as published by 0007 the Free Software Foundation; either version 2 of the License, or (at your 0008 option) any later version. 0009 0010 This library is distributed in the hope that it will be useful, but WITHOUT 0011 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 0012 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 0013 License for more details. 0014 0015 You should have received a copy of the GNU Library General Public License 0016 along with this library; see the file COPYING.LIB. If not, write to the 0017 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0018 02110-1301, USA. 0019 */ 0020 #include "composercontroller.h" 0021 #include <settings/settings.h> 0022 #include <KMime/Message> 0023 #include <QVariant> 0024 #include <QList> 0025 #include <QDebug> 0026 #include <QMimeDatabase> 0027 #include <QUrlQuery> 0028 #include <QFileInfo> 0029 #include <QFile> 0030 #include <QTemporaryFile> 0031 #include <KEmailAddress> 0032 #include <sink/store.h> 0033 #include <sink/log.h> 0034 0035 #include "identitiesmodel.h" 0036 #include "recepientautocompletionmodel.h" 0037 #include "mime/mailtemplates.h" 0038 #include "mime/mailcrypto.h" 0039 #include "async.h" 0040 #include "sinkutils.h" 0041 0042 std::vector<Crypto::Key> &operator+=(std::vector<Crypto::Key> &list, const std::vector<Crypto::Key> &add) 0043 { 0044 list.insert(std::end(list), std::begin(add), std::end(add)); 0045 return list; 0046 } 0047 0048 class IdentitySelector : public Selector { 0049 Q_OBJECT 0050 Q_PROPERTY (QString currentAccountId WRITE setCurrentAccountId) 0051 0052 public: 0053 IdentitySelector(ComposerController &controller) : Selector(new IdentitiesModel), mController(controller) 0054 { 0055 } 0056 0057 void setCurrent(const QModelIndex &index) Q_DECL_OVERRIDE 0058 { 0059 if (index.isValid()) { 0060 auto currentAccountId = index.data(IdentitiesModel::AccountId).toByteArray(); 0061 0062 KMime::Types::Mailbox mb; 0063 mb.setName(index.data(IdentitiesModel::Username).toString()); 0064 mb.setAddress(index.data(IdentitiesModel::Address).toString().toUtf8()); 0065 SinkLog() << "Setting current identity: " << mb.prettyAddress() << "Account: " << currentAccountId; 0066 0067 mController.setIdentity(mb); 0068 mController.setAccountId(currentAccountId); 0069 } else { 0070 SinkWarning() << "No valid identity for index: " << index; 0071 mController.clearIdentity(); 0072 mController.clearAccountId(); 0073 } 0074 0075 } 0076 0077 void setCurrentAccountId(const QString &accountId) 0078 { 0079 for (int i = 0; i < model()->rowCount(); i++) { 0080 if (model()->index(i, 0).data(IdentitiesModel::AccountId).toString() == accountId) { 0081 setCurrentIndex(i); 0082 return; 0083 } 0084 } 0085 } 0086 0087 void setCurrentIdentity(const QString &emailAddress) 0088 { 0089 for (int i = 0; i < model()->rowCount(); i++) { 0090 if (model()->index(i, 0).data(IdentitiesModel::Address).toString() == emailAddress) { 0091 setCurrentIndex(i); 0092 return; 0093 } 0094 } 0095 } 0096 0097 QVector<QByteArray> getAllAddresses() 0098 { 0099 QVector<QByteArray> list; 0100 for (int i = 0; i < model()->rowCount(); i++) { 0101 list << model()->data(model()->index(i, 0), IdentitiesModel::Address).toString().toUtf8(); 0102 } 0103 return list; 0104 } 0105 private: 0106 ComposerController &mController; 0107 }; 0108 0109 class RecipientCompleter : public Completer { 0110 public: 0111 RecipientCompleter() : Completer(new RecipientAutocompletionModel) 0112 { 0113 } 0114 0115 void setSearchString(const QString &s) { 0116 static_cast<RecipientAutocompletionModel*>(model())->setFilter(s); 0117 Completer::setSearchString(s); 0118 } 0119 }; 0120 0121 class AddresseeController : public Kube::ListPropertyController 0122 { 0123 Q_OBJECT 0124 Q_PROPERTY(bool foundAllKeys READ foundAllKeys NOTIFY foundAllKeysChanged) 0125 public: 0126 0127 bool mFoundAllKeys = true; 0128 0129 QSet<QByteArray> mMissingKeys; 0130 AddresseeController() : Kube::ListPropertyController{{"name", "keyFound", "key", "fetching"}} 0131 { 0132 QObject::connect( 0133 this, &Kube::ListPropertyController::added, this, [this](const QByteArray &id, const QVariantMap &map) { 0134 findKey(id, map.value("name").toString(), false); 0135 }); 0136 0137 QObject::connect(this, &Kube::ListPropertyController::removed, this, [this] (const QByteArray &id) { 0138 mMissingKeys.remove(id); 0139 setFoundAllKeys(mMissingKeys.isEmpty()); 0140 }); 0141 } 0142 0143 bool foundAllKeys() 0144 { 0145 return mFoundAllKeys; 0146 } 0147 0148 void setFoundAllKeys(bool found) 0149 { 0150 mFoundAllKeys = found; 0151 emit foundAllKeysChanged(); 0152 } 0153 0154 void findKey(const QByteArray &id, const QString &addressee, bool fetchRemote) 0155 { 0156 KMime::Types::Mailbox mb; 0157 mb.fromUnicodeString(addressee); 0158 const auto mbAddress = mb.address(); 0159 0160 if (mbAddress.isEmpty()) { 0161 return; 0162 } 0163 0164 SinkLog() << "Searching key for: " << mbAddress; 0165 0166 mMissingKeys << id; 0167 setFoundAllKeys(false); 0168 0169 setValue(id, "fetching", fetchRemote); 0170 0171 asyncRun<std::vector<Crypto::Key>>(this, 0172 [=] { 0173 return Crypto::findKeys({mbAddress}, false, fetchRemote); 0174 }, 0175 [this, addressee, id](const std::vector<Crypto::Key> &keys) { 0176 setValue(id, "fetching", false); 0177 if (!keys.empty()) { 0178 if (keys.size() > 1) { 0179 SinkWarning() << "Found more than one key, encrypting to all of them."; 0180 } 0181 for (const auto &key : keys) { 0182 SinkLog() << "Found key: " << key; 0183 } 0184 setValue(id, "keyFound", true); 0185 setValue(id, "key", QVariant::fromValue(keys)); 0186 mMissingKeys.remove(id); 0187 setFoundAllKeys(mMissingKeys.isEmpty()); 0188 } else { 0189 SinkWarning() << "Failed to find key for recipient."; 0190 } 0191 }); 0192 } 0193 0194 Q_INVOKABLE void fetchKeys(const QByteArray &id, const QString &addressee) 0195 { 0196 findKey(id, addressee, true); 0197 } 0198 0199 void set(const QStringList &list) 0200 { 0201 for (const auto &email : list) { 0202 Kube::ListPropertyController::add({{"name", email}}); 0203 } 0204 } 0205 0206 Q_INVOKABLE virtual void add(const QVariantMap &map) 0207 { 0208 // Support adding multiple addresses separated by comma 0209 for (const auto &part : KEmailAddress::splitAddressList(map.value("name").toString())) { 0210 const auto address = part.trimmed(); 0211 0212 //Validation 0213 KMime::Types::Mailbox mb; 0214 mb.fromUnicodeString(address); 0215 if (mb.address().isEmpty()) { 0216 SinkTrace() << "Ignoring invalid address " << address; 0217 continue; 0218 } 0219 0220 Kube::ListPropertyController::add({{"name", address}}); 0221 } 0222 } 0223 signals: 0224 void foundAllKeysChanged(); 0225 }; 0226 0227 class AttachmentController : public Kube::ListPropertyController 0228 { 0229 public: 0230 0231 AttachmentController() 0232 : Kube::ListPropertyController{{"name", "filename", "content", "mimetype", "description", "iconname", "url", "inline"}} 0233 { 0234 QObject::connect(this, &Kube::ListPropertyController::added, this, [this] (const QByteArray &id, const QVariantMap &map) { 0235 auto url = map.value("url").toUrl(); 0236 setAttachmentProperties(id, url); 0237 }); 0238 } 0239 0240 void setAttachmentProperties(const QByteArray &id, const QUrl &url) 0241 { 0242 QMimeDatabase db; 0243 auto mimeType = db.mimeTypeForUrl(url); 0244 if (mimeType.name() == QLatin1String("inode/directory")) { 0245 qWarning() << "Can't deal with directories yet."; 0246 } else { 0247 if (!url.isLocalFile()) { 0248 qWarning() << "Cannot attach remote file: " << url; 0249 return; 0250 } 0251 0252 QFileInfo fileInfo(url.toLocalFile()); 0253 if (!fileInfo.exists()) { 0254 qWarning() << "The file doesn't exist: " << url; 0255 } 0256 0257 QFile file{fileInfo.filePath()}; 0258 file.open(QIODevice::ReadOnly); 0259 const auto data = file.readAll(); 0260 QVariantMap map; 0261 map.insert("filename", fileInfo.fileName()); 0262 map.insert("mimetype", mimeType.name().toLatin1()); 0263 map.insert("filename", fileInfo.fileName().toLatin1()); 0264 map.insert("inline", false); 0265 map.insert("iconname", mimeType.iconName()); 0266 map.insert("url", url); 0267 map.insert("content", data); 0268 setValues(id, map); 0269 } 0270 } 0271 }; 0272 0273 ComposerController::ComposerController() 0274 : Kube::Controller(), 0275 controller_to{new AddresseeController}, 0276 controller_cc{new AddresseeController}, 0277 controller_bcc{new AddresseeController}, 0278 controller_attachments{new AttachmentController}, 0279 action_send{new Kube::ControllerAction{this, &ComposerController::send}}, 0280 action_saveAsDraft{new Kube::ControllerAction{this, &ComposerController::saveAsDraft}}, 0281 mRecipientCompleter{new RecipientCompleter}, 0282 mIdentitySelector{new IdentitySelector{*this}} 0283 { 0284 QObject::connect(this, &ComposerController::identityChanged, &ComposerController::findPersonalKey); 0285 } 0286 0287 void ComposerController::findPersonalKey() 0288 { 0289 const auto identityAddress = getIdentity().address(); 0290 if (identityAddress.isEmpty()) { 0291 SinkTrace() << "Not looking for personal key because of empty identity."; 0292 return; 0293 } 0294 SinkLog() << "Looking for personal key for: " << identityAddress; 0295 asyncRun<std::vector<Crypto::Key>>(this, [=] { 0296 return Crypto::findKeys({identityAddress}, true); 0297 }, 0298 [this](const std::vector<Crypto::Key> &keys) { 0299 if (keys.empty()) { 0300 SinkWarning() << "Failed to find a personal key."; 0301 } else if (keys.size() > 1) { 0302 SinkWarning() << "Found multiple keys, using all of them."; 0303 } 0304 setPersonalKeys(QVariant::fromValue(keys)); 0305 setFoundPersonalKeys(!keys.empty()); 0306 }); 0307 } 0308 0309 void ComposerController::clear() 0310 { 0311 Controller::clear(); 0312 //Reapply account and identity from selection 0313 mIdentitySelector->reapplyCurrentIndex(); 0314 //FIXME implement in Controller::clear instead 0315 toController()->clear(); 0316 ccController()->clear(); 0317 bccController()->clear(); 0318 } 0319 0320 Completer *ComposerController::recipientCompleter() const 0321 { 0322 return mRecipientCompleter.data(); 0323 } 0324 0325 Selector *ComposerController::identitySelector() const 0326 { 0327 return mIdentitySelector.data(); 0328 } 0329 0330 static void applyAddresses(const KMime::Types::Mailbox::List &list, std::function<void(const QByteArray &, const QByteArray &)> callback) 0331 { 0332 for (const auto &to : list) { 0333 callback(to.address(), to.name().toUtf8()); 0334 } 0335 } 0336 0337 static void applyAddresses(const QStringList &list, std::function<void(const QByteArray &, const QByteArray &)> callback) 0338 { 0339 KMime::Types::Mailbox::List mailboxes; 0340 for (const auto &s : list) { 0341 KMime::Types::Mailbox mb; 0342 mb.fromUnicodeString(s); 0343 mailboxes << mb; 0344 } 0345 applyAddresses(mailboxes, callback); 0346 } 0347 0348 static QStringList getStringListFromAddresses(const KMime::Types::Mailbox::List &mailboxes) 0349 { 0350 QStringList list; 0351 for (const auto &mb : mailboxes) { 0352 list << mb.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary); 0353 } 0354 return list; 0355 } 0356 0357 void ComposerController::addAttachmentPart(KMime::Content *partToAttach) 0358 { 0359 QVariantMap map; 0360 // May need special care for the multipart/digest MIME type 0361 map.insert("content", partToAttach->decodedContent()); 0362 map.insert("mimetype", partToAttach->contentType()->mimeType()); 0363 0364 QMimeDatabase db; 0365 auto mimeType = db.mimeTypeForName(partToAttach->contentType()->mimeType()); 0366 map.insert("iconname", mimeType.iconName()); 0367 0368 if (partToAttach->contentDescription(false)) { 0369 map.insert("description", partToAttach->contentDescription()->asUnicodeString()); 0370 } 0371 QString name; 0372 QString filename; 0373 if (partToAttach->contentType(false)) { 0374 if (partToAttach->contentType()->hasParameter(QStringLiteral("name"))) { 0375 name = partToAttach->contentType()->parameter(QStringLiteral("name")); 0376 } 0377 } 0378 if (partToAttach->contentDisposition(false)) { 0379 filename = partToAttach->contentDisposition()->filename(); 0380 map.insert("inline", partToAttach->contentDisposition()->disposition() == KMime::Headers::CDinline); 0381 } 0382 0383 if (name.isEmpty() && !filename.isEmpty()) { 0384 name = filename; 0385 } 0386 if (filename.isEmpty() && !name.isEmpty()) { 0387 filename = name; 0388 } 0389 0390 if (!filename.isEmpty()) { 0391 map.insert("filename", filename); 0392 } 0393 if (!name.isEmpty()) { 0394 map.insert("name", name); 0395 } 0396 attachmentsController()->add(map); 0397 } 0398 0399 void ComposerController::setMessage(const KMime::Message::Ptr &msg) 0400 { 0401 static_cast<AddresseeController*>(toController())->set(getStringListFromAddresses(msg->to(true)->mailboxes())); 0402 static_cast<AddresseeController*>(ccController())->set(getStringListFromAddresses(msg->cc(true)->mailboxes())); 0403 static_cast<AddresseeController*>(bccController())->set(getStringListFromAddresses(msg->bcc(true)->mailboxes())); 0404 0405 setSubject(msg->subject(true)->asUnicodeString()); 0406 bool isHtml = false; 0407 const auto body = MailTemplates::body(msg, isHtml); 0408 setHtmlBody(isHtml); 0409 setBody(body); 0410 0411 //TODO use ObjecTreeParser to get encrypted attachments as well 0412 foreach (const auto &att, msg->attachments()) { 0413 addAttachmentPart(att); 0414 } 0415 0416 setExistingMessage(msg); 0417 emit messageLoaded(body); 0418 } 0419 0420 void ComposerController::loadDraft(const QVariant &message) { 0421 clear(); 0422 loadMessage(message, [this] (const KMime::Message::Ptr &mail) { 0423 setEncrypt(KMime::isEncrypted(mail.data())); 0424 setSign(KMime::isSigned(mail.data())); 0425 mRemoveDraft = true; 0426 setMessage(mail); 0427 }); 0428 } 0429 0430 void ComposerController::selectIdentityFromMailboxes(const KMime::Types::Mailbox::List &mailboxes, const QVector<QString> &meStrings) 0431 { 0432 for (const auto &mb : mailboxes) { 0433 const auto address = mb.addrSpec().asString(); 0434 if (meStrings.contains(address)) { 0435 static_cast<IdentitySelector*>(mIdentitySelector.data())->setCurrentIdentity(address); 0436 return; 0437 } 0438 } 0439 } 0440 0441 void ComposerController::loadReply(const QVariant &message) { 0442 clear(); 0443 auto guard = QPointer<QObject>{this}; 0444 loadMessage(message, [this, guard] (const KMime::Message::Ptr &mail) { 0445 Q_ASSERT(guard); 0446 //Find all personal email addresses to exclude from reply 0447 KMime::Types::AddrSpecList me; 0448 QVector<QString> meStrings; 0449 auto list = static_cast<IdentitySelector*>(mIdentitySelector.data())->getAllAddresses(); 0450 for (const auto &a : list) { 0451 KMime::Types::Mailbox mb; 0452 mb.setAddress(a); 0453 me << mb.addrSpec(); 0454 meStrings << a; 0455 } 0456 0457 selectIdentityFromMailboxes(mail->to()->mailboxes() + mail->cc()->mailboxes() + mail->bcc()->mailboxes(), meStrings); 0458 0459 setEncrypt(KMime::isEncrypted(mail.data())); 0460 setSign(KMime::isSigned(mail.data())); 0461 MailTemplates::reply(mail, [this, guard] (const auto &msg) { 0462 Q_ASSERT(guard); 0463 setMessage(msg); 0464 }, me); 0465 }); 0466 } 0467 0468 void ComposerController::loadForward(const QVariant &message) { 0469 clear(); 0470 loadMessage(message, [this] (const KMime::Message::Ptr &mail) { 0471 setEncrypt(KMime::isEncrypted(mail.data())); 0472 setSign(KMime::isSigned(mail.data())); 0473 MailTemplates::forward(mail, [this] (const auto &msg) { 0474 setMessage(msg); 0475 }); 0476 }); 0477 } 0478 0479 void ComposerController::loadMessage(const QVariant &message, std::function<void(const KMime::Message::Ptr&)> callback) 0480 { 0481 using namespace Sink; 0482 using namespace Sink::ApplicationDomain; 0483 0484 auto msg = message.value<Mail::Ptr>(); 0485 Q_ASSERT(msg); 0486 Query query(*msg); 0487 query.request<Mail::MimeMessage>(); 0488 query.request<Mail::Draft>(); 0489 setLoading(true); 0490 Store::fetchOne<Mail>(query).then([this, callback](const Mail &mail) { 0491 setExistingMail(mail); 0492 setLoading(false); 0493 0494 const auto mailData = KMime::CRLFtoLF(mail.getMimeMessage()); 0495 if (!mailData.isEmpty()) { 0496 KMime::Message::Ptr mail(new KMime::Message); 0497 mail->setContent(mailData); 0498 mail->parse(); 0499 callback(mail); 0500 } else { 0501 qWarning() << "Retrieved empty message"; 0502 } 0503 }).exec(); 0504 } 0505 0506 void ComposerController::recordForAutocompletion(const QByteArray &addrSpec, const QByteArray &displayName) 0507 { 0508 if (auto model = static_cast<RecipientAutocompletionModel*>(recipientCompleter()->model())) { 0509 model->addEntry(addrSpec, displayName); 0510 } 0511 } 0512 0513 std::vector<Crypto::Key> ComposerController::getRecipientKeys() 0514 { 0515 std::vector<Crypto::Key> keys; 0516 { 0517 const auto list = toController()->getList<std::vector<Crypto::Key>>("key"); 0518 for (const auto &l: list) { 0519 keys.insert(std::end(keys), std::begin(l), std::end(l)); 0520 } 0521 } 0522 { 0523 const auto list = ccController()->getList<std::vector<Crypto::Key>>("key"); 0524 for (const auto &l: list) { 0525 keys.insert(std::end(keys), std::begin(l), std::end(l)); 0526 } 0527 } 0528 { 0529 const auto list = bccController()->getList<std::vector<Crypto::Key>>("key"); 0530 for (const auto &l: list) { 0531 keys.insert(std::end(keys), std::begin(l), std::end(l)); 0532 } 0533 } 0534 return keys; 0535 } 0536 0537 KMime::Message::Ptr ComposerController::assembleMessage() 0538 { 0539 auto toAddresses = toController()->getList<QString>("name"); 0540 auto ccAddresses = ccController()->getList<QString>("name"); 0541 auto bccAddresses = bccController()->getList<QString>("name"); 0542 applyAddresses(toAddresses + ccAddresses + bccAddresses, [&](const QByteArray &addrSpec, const QByteArray &displayName) { 0543 recordForAutocompletion(addrSpec, displayName); 0544 }); 0545 0546 QList<Attachment> attachments; 0547 attachmentsController()->traverse([&](const QVariantMap &value) { 0548 attachments << Attachment{ 0549 value["name"].toString(), 0550 value["filename"].toString(), 0551 value["mimetype"].toByteArray(), 0552 value["inline"].toBool(), 0553 value["content"].toByteArray() 0554 }; 0555 }); 0556 0557 Crypto::Key attachedKey; 0558 std::vector<Crypto::Key> signingKeys; 0559 if (getSign()) { 0560 signingKeys = getPersonalKeys().value<std::vector<Crypto::Key>>(); 0561 Q_ASSERT(!signingKeys.empty()); 0562 attachedKey = signingKeys[0]; 0563 } 0564 std::vector<Crypto::Key> encryptionKeys; 0565 if (getEncrypt()) { 0566 //Encrypt to self so we can read the sent message 0567 auto personalKeys = getPersonalKeys().value<std::vector<Crypto::Key>>(); 0568 0569 attachedKey = personalKeys[0]; 0570 0571 encryptionKeys += personalKeys; 0572 encryptionKeys += getRecipientKeys(); 0573 } 0574 0575 return MailTemplates::createMessage(mExistingMessage, toAddresses, ccAddresses, bccAddresses, getIdentity(), getSubject(), getBody(), getHtmlBody(), attachments, signingKeys, encryptionKeys, attachedKey); 0576 } 0577 0578 void ComposerController::send() 0579 { 0580 auto message = assembleMessage(); 0581 if (!message) { 0582 SinkWarning() << "Failed to assemble the message."; 0583 return; 0584 } 0585 0586 auto accountId = getAccountId(); 0587 Q_ASSERT(!accountId.isEmpty()); 0588 if (accountId.isEmpty()) { 0589 SinkWarning() << "No account id."; 0590 return; 0591 } 0592 auto job = SinkUtils::sendMail(message->encodedContent(true), accountId.toUtf8()) 0593 .then([&] (const KAsync::Error &error) { 0594 if (!error) { 0595 if (mRemoveDraft) { 0596 SinkLog() << "Removing draft message."; 0597 Sink::Store::remove(getExistingMail()).exec(); 0598 } 0599 } 0600 emit done(); 0601 }); 0602 0603 run(job); 0604 } 0605 0606 void ComposerController::saveAsDraft() 0607 { 0608 SinkLog() << "Save as draft"; 0609 const auto accountId = getAccountId(); 0610 Q_ASSERT(!accountId.isEmpty()); 0611 if (accountId.isEmpty()) { 0612 SinkWarning() << "No account id."; 0613 return; 0614 } 0615 auto existingMail = getExistingMail(); 0616 0617 auto message = assembleMessage(); 0618 if (!message) { 0619 SinkWarning() << "Failed to assemble the message."; 0620 return; 0621 } 0622 0623 using namespace Sink; 0624 using namespace Sink::ApplicationDomain; 0625 0626 auto job = [&] { 0627 if (existingMail.identifier().isEmpty() || !existingMail.getDraft()) { 0628 SinkLog() << "Creating a new draft" << existingMail.identifier() << "in account" << accountId; 0629 Query query; 0630 query.containsFilter<SinkResource::Capabilities>(ResourceCapabilities::Mail::drafts); 0631 query.filter<SinkResource::Account>(accountId.toLatin1()); 0632 return Store::fetchOne<SinkResource>(query) 0633 .then([=](const SinkResource &resource) { 0634 Mail mail(resource.identifier()); 0635 mail.setDraft(true); 0636 mail.setMimeMessage(message->encodedContent(true)); 0637 return Store::create(mail); 0638 }) 0639 .onError([] (const KAsync::Error &error) { 0640 SinkWarning() << "Error while creating draft: " << error.errorMessage; 0641 }); 0642 } else { 0643 SinkLog() << "Modifying an existing mail" << existingMail.identifier(); 0644 existingMail.setDraft(true); 0645 existingMail.setMimeMessage(message->encodedContent(true)); 0646 return Store::modify(existingMail); 0647 } 0648 }(); 0649 job = job.then([&] (const KAsync::Error &) { 0650 emit done(); 0651 }); 0652 run(job); 0653 } 0654 0655 #include "composercontroller.moc"