File indexing completed on 2024-11-24 04:44:21

0001 /*
0002     SPDX-FileCopyrightText: 2009 Bertjan Broeksem <broeksema@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "mboxresource.h"
0008 #include "mboxresource_debug.h"
0009 
0010 #include <Akonadi/AttributeFactory>
0011 #include <Akonadi/ChangeRecorder>
0012 #include <Akonadi/CollectionFetchJob>
0013 #include <Akonadi/CollectionModifyJob>
0014 #include <Akonadi/ItemFetchScope>
0015 #include <Akonadi/MessageFlags>
0016 #include <Akonadi/SpecialCollectionAttribute>
0017 
0018 #include <KMbox/MBox>
0019 
0020 #include <KMime/Message>
0021 
0022 #include "deleteditemsattribute.h"
0023 #include "settingsadaptor.h"
0024 
0025 #include <QDBusConnection>
0026 
0027 using namespace Akonadi;
0028 
0029 static Collection::Id collectionId(const QString &remoteItemId)
0030 {
0031     // [CollectionId]::[RemoteCollectionId]::[Offset]
0032     const QStringList lst = remoteItemId.split(QStringLiteral("::"));
0033     Q_ASSERT(lst.size() == 3);
0034     return lst.first().toLongLong();
0035 }
0036 
0037 static QString mboxFile(const QString &remoteItemId)
0038 {
0039     // [CollectionId]::[RemoteCollectionId]::[Offset]
0040     const QStringList lst = remoteItemId.split(QStringLiteral("::"));
0041     Q_ASSERT(lst.size() == 3);
0042     return lst.at(1);
0043 }
0044 
0045 static quint64 itemOffset(const QString &remoteItemId)
0046 {
0047     // [CollectionId]::[RemoteCollectionId]::[Offset]
0048     const QStringList lst = remoteItemId.split(QStringLiteral("::"));
0049     Q_ASSERT(lst.size() == 3);
0050     return lst.last().toULongLong();
0051 }
0052 
0053 MboxResource::MboxResource(const QString &id)
0054     : SingleFileResource<Settings>(id)
0055 {
0056     new SettingsAdaptor(mSettings);
0057     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), mSettings, QDBusConnection::ExportAdaptors);
0058 
0059     const QStringList mimeTypes{QStringLiteral("message/rfc822")};
0060     setSupportedMimetypes(mimeTypes, QStringLiteral("message-rfc822"));
0061     // Register the list of deleted items as an attribute of the collection.
0062     AttributeFactory::registerAttribute<DeletedItemsAttribute>();
0063     setName(mSettings->displayName());
0064 }
0065 
0066 MboxResource::~MboxResource()
0067 {
0068     delete mMBox;
0069 }
0070 
0071 Collection MboxResource::rootCollection() const
0072 {
0073     // Maildir only has a single collection so we treat it as an inbox
0074     auto col = SingleFileResource<Settings>::rootCollection();
0075     col.attribute<Akonadi::SpecialCollectionAttribute>(Akonadi::Collection::AddIfMissing)->setCollectionType("inbox");
0076     return col;
0077 }
0078 
0079 void MboxResource::retrieveItems(const Akonadi::Collection &col)
0080 {
0081     Q_UNUSED(col)
0082     if (!mMBox) {
0083         cancelTask();
0084         return;
0085     }
0086     if (mMBox->fileName().isEmpty()) {
0087         Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured."));
0088         return;
0089     }
0090 
0091     reloadFile();
0092 
0093     KMBox::MBoxEntry::List entryList;
0094     if (const auto attr = col.attribute<DeletedItemsAttribute>()) {
0095         entryList = mMBox->entries(attr->deletedItemEntries());
0096     } else { // No deleted items (yet)
0097         entryList = mMBox->entries();
0098     }
0099     mMBox->lock(); // Lock the file so that it doesn't get locked for every
0100     // readEntryHeaders() call.
0101 
0102     Item::List items;
0103     const QString colId = QString::number(col.id());
0104     const QString colRid = col.remoteId();
0105     double count = 1;
0106     const int entryListSize(entryList.size());
0107     items.reserve(entryListSize);
0108     for (const KMBox::MBoxEntry &entry : std::as_const(entryList)) {
0109         // TODO: Use cache policy to see what actually has to been set as payload.
0110         //       Currently most views need a minimal amount of information so the
0111         //       Items get Envelopes as payload.
0112         auto mail = new KMime::Message();
0113         mail->setHead(KMime::CRLFtoLF(mMBox->readMessageHeaders(entry)));
0114         mail->parse();
0115 
0116         Item item;
0117         item.setRemoteId(colId + QLatin1StringView("::") + colRid + QLatin1StringView("::") + QString::number(entry.messageOffset()));
0118         item.setMimeType(QStringLiteral("message/rfc822"));
0119         item.setSize(entry.messageSize());
0120         item.setPayload(KMime::Message::Ptr(mail));
0121         Akonadi::MessageFlags::copyMessageFlags(*mail, item);
0122         Q_EMIT percent(count++ / entryListSize);
0123         items << item;
0124     }
0125 
0126     mMBox->unlock(); // Now we have the items, unlock
0127 
0128     itemsRetrieved(items);
0129 }
0130 
0131 bool MboxResource::retrieveItems(const Akonadi::Item::List &items, const QSet<QByteArray> &parts)
0132 {
0133     Q_UNUSED(parts)
0134 
0135     if (!mMBox) {
0136         Q_EMIT error(i18n("MBox not loaded."));
0137         return false;
0138     }
0139     if (mMBox->fileName().isEmpty()) {
0140         Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured."));
0141         return false;
0142     }
0143 
0144     Akonadi::Item::List rv;
0145     rv.reserve(items.count());
0146     for (const auto &item : items) {
0147         const QString rid = item.remoteId();
0148         const quint64 offset = itemOffset(rid);
0149         KMime::Message *mail = mMBox->readMessage(KMBox::MBoxEntry(offset));
0150         if (!mail) {
0151             Q_EMIT error(i18n("Failed to read message with uid '%1'.", rid));
0152             return false;
0153         }
0154 
0155         Item i(item);
0156         i.setPayload(KMime::Message::Ptr(mail));
0157         Akonadi::MessageFlags::copyMessageFlags(*mail, i);
0158         rv.push_back(i);
0159     }
0160     itemsRetrieved(rv);
0161     return true;
0162 }
0163 
0164 void MboxResource::aboutToQuit()
0165 {
0166     if (!mSettings->readOnly()) {
0167         writeFile();
0168     }
0169     mSettings->save();
0170 }
0171 
0172 void MboxResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
0173 {
0174     if (!mMBox) {
0175         cancelTask(i18n("MBox not loaded."));
0176         return;
0177     }
0178     if (mMBox->fileName().isEmpty()) {
0179         Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured."));
0180         return;
0181     }
0182 
0183     // we can only deal with mail
0184     if (!item.hasPayload<KMime::Message::Ptr>()) {
0185         cancelTask(i18n("Only email messages can be added to the MBox resource."));
0186         return;
0187     }
0188 
0189     const KMBox::MBoxEntry entry = mMBox->appendMessage(item.payload<KMime::Message::Ptr>());
0190     if (!entry.isValid()) {
0191         cancelTask(i18n("Mail message not added to the MBox."));
0192         return;
0193     }
0194 
0195     scheduleWrite();
0196     const QString rid =
0197         QString::number(collection.id()) + QLatin1StringView("::") + collection.remoteId() + QLatin1StringView("::") + QString::number(entry.messageOffset());
0198 
0199     Item i(item);
0200     i.setRemoteId(rid);
0201 
0202     changeCommitted(i);
0203 }
0204 
0205 void MboxResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts)
0206 {
0207     if (parts.contains("PLD:RFC822")) {
0208         qCDebug(MBOXRESOURCE_LOG) << itemOffset(item.remoteId());
0209         // Only complete messages can be stored in a MBox file. Because all messages
0210         // are stored in one single file we do an ItemDelete and an ItemCreate to
0211         // prevent that whole file must been rewritten.
0212         auto fetchJob = new CollectionFetchJob(Collection(collectionId(item.remoteId())), CollectionFetchJob::Base);
0213 
0214         connect(fetchJob, &CollectionFetchJob::result, this, &MboxResource::onCollectionFetch);
0215 
0216         mCurrentItemDeletions.insert(fetchJob, item);
0217 
0218         fetchJob->start();
0219         return;
0220     }
0221 
0222     changeProcessed();
0223 }
0224 
0225 void MboxResource::itemRemoved(const Akonadi::Item &item)
0226 {
0227     auto fetchJob = new CollectionFetchJob(Collection(collectionId(item.remoteId())), CollectionFetchJob::Base);
0228 
0229     if (!fetchJob->exec()) {
0230         cancelTask(i18n("Could not fetch the collection: %1", fetchJob->errorString()));
0231         return;
0232     }
0233 
0234     Q_ASSERT(fetchJob->collections().size() == 1);
0235     Collection mboxCollection = fetchJob->collections().at(0);
0236     auto attr = mboxCollection.attribute<DeletedItemsAttribute>(Akonadi::Collection::AddIfMissing);
0237 
0238     if (mSettings->compactFrequency() == Settings::per_x_messages && mSettings->messageCount() == static_cast<uint>(attr->offsetCount() + 1)) {
0239         qCDebug(MBOXRESOURCE_LOG) << "Compacting mbox file";
0240         mMBox->purge(attr->deletedItemEntries() << KMBox::MBoxEntry(itemOffset(item.remoteId())));
0241         scheduleWrite();
0242         mboxCollection.removeAttribute<DeletedItemsAttribute>();
0243     } else {
0244         attr->addDeletedItemOffset(itemOffset(item.remoteId()));
0245     }
0246 
0247     auto modifyJob = new CollectionModifyJob(mboxCollection);
0248     if (!modifyJob->exec()) {
0249         cancelTask(modifyJob->errorString());
0250         return;
0251     }
0252 
0253     changeProcessed();
0254 }
0255 
0256 void MboxResource::handleHashChange()
0257 {
0258     Q_EMIT warning(
0259         i18n("The MBox file was changed by another program. "
0260              "A copy of the new file was made and pending changes "
0261              "are appended to that copy. To prevent this from happening "
0262              "use locking and make sure that all programs accessing the mbox "
0263              "use the same locking method."));
0264 }
0265 
0266 bool MboxResource::readFromFile(const QString &fileName)
0267 {
0268     delete mMBox;
0269     mMBox = new KMBox::MBox();
0270 
0271     switch (mSettings->lockfileMethod()) {
0272     case Settings::procmail:
0273         mMBox->setLockType(KMBox::MBox::ProcmailLockfile);
0274         mMBox->setLockFile(mSettings->lockfile());
0275         break;
0276     case Settings::mutt_dotlock:
0277         mMBox->setLockType(KMBox::MBox::MuttDotlock);
0278         break;
0279     case Settings::mutt_dotlock_privileged:
0280         mMBox->setLockType(KMBox::MBox::MuttDotlockPrivileged);
0281         break;
0282     }
0283 
0284     return mMBox->load(QUrl::fromLocalFile(fileName).toLocalFile());
0285 }
0286 
0287 bool MboxResource::writeToFile(const QString &fileName)
0288 {
0289     if (!mMBox->save(fileName)) {
0290         Q_EMIT error(i18n("Failed to save mbox file to %1", fileName));
0291         return false;
0292     }
0293 
0294     // HACK: When writeToFile is called with another file than with which the mbox
0295     // was loaded we assume that a backup is made as result of the fileChanged slot
0296     // in SingleFileResourceBase. The problem is that SingleFileResource assumes that
0297     // the implementing resources can save/retrieve the data from before the file
0298     // change we have a problem at this point in the mbox resource. Therefore we
0299     // copy the original file and append pending changes to it but also add an extra
0300     // '\n' to make sure that the hashes differ and the user gets notified. Normally
0301     // if this happens the user should make use of locking in all applications that
0302     // use the mbox file.
0303     if (fileName != mMBox->fileName()) {
0304         QFile file(fileName);
0305         file.open(QIODevice::WriteOnly);
0306         file.seek(file.size());
0307         file.write("\n");
0308     }
0309 
0310     return true;
0311 }
0312 
0313 /// Private slots
0314 
0315 void MboxResource::onCollectionFetch(KJob *job)
0316 {
0317     Q_ASSERT(mCurrentItemDeletions.contains(job));
0318     const Item item = mCurrentItemDeletions.take(job);
0319 
0320     if (job->error()) {
0321         cancelTask(job->errorString());
0322         return;
0323     }
0324 
0325     auto fetchJob = dynamic_cast<CollectionFetchJob *>(job);
0326     Q_ASSERT(fetchJob);
0327     Q_ASSERT(fetchJob->collections().size() == 1);
0328 
0329     Collection mboxCollection = fetchJob->collections().at(0);
0330     auto attr = mboxCollection.attribute<DeletedItemsAttribute>(Akonadi::Collection::AddIfMissing);
0331     attr->addDeletedItemOffset(itemOffset(item.remoteId()));
0332 
0333     auto modifyJob = new CollectionModifyJob(mboxCollection);
0334     mCurrentItemDeletions.insert(modifyJob, item);
0335     connect(modifyJob, &CollectionModifyJob::result, this, &MboxResource::onCollectionModify);
0336     modifyJob->start();
0337 }
0338 
0339 void MboxResource::onCollectionModify(KJob *job)
0340 {
0341     Q_ASSERT(mCurrentItemDeletions.contains(job));
0342     const Item item = mCurrentItemDeletions.take(job);
0343 
0344     if (job->error()) {
0345         // Failed to store the offset of a deleted item in the DeletedItemsAttribute
0346         // of the collection. In this case we shouldn't try to store the modified
0347         // item.
0348         cancelTask(
0349             i18n("Failed to update the changed item because the old item "
0350                  "could not be deleted Reason: %1",
0351                  job->errorString()));
0352         return;
0353     }
0354 
0355     Collection c(collectionId(item.remoteId()));
0356     c.setRemoteId(mboxFile(item.remoteId()));
0357 
0358     itemAdded(item, c);
0359 }
0360 
0361 AKONADI_RESOURCE_MAIN(MboxResource)
0362 
0363 #include "moc_mboxresource.cpp"