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"