File indexing completed on 2024-12-22 04:57:33

0001 /*
0002     SPDX-FileCopyrightText: 2007 Till Adam <adam@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "maildir.h"
0008 #include "keycache.h"
0009 
0010 #include <QDateTime>
0011 #include <QDir>
0012 #include <QFileInfo>
0013 #include <QHostInfo>
0014 #include <QRandomGenerator>
0015 #include <QRegularExpression>
0016 #include <QUuid>
0017 
0018 #include "libmaildir_debug.h"
0019 #include <Akonadi/MessageFlags>
0020 #include <KLocalizedString>
0021 #include <fcntl.h>
0022 
0023 // Define it to get more debug output to expense of operating speed
0024 // #define DEBUG_KEYCACHE_CONSITENCY
0025 
0026 using namespace KPIM;
0027 
0028 static QRegularExpression statusSeparatorRx()
0029 {
0030     static const QRegularExpression expr(QStringLiteral(":|!"));
0031     return expr;
0032 }
0033 
0034 class Maildir::Private
0035 {
0036 public:
0037     Private(const QString &p, bool isRoot)
0038         : path(p)
0039         , hostName(QHostInfo::localHostName())
0040         , isRoot(isRoot)
0041     {
0042         // Cache object is created the first time this runs.
0043         // It will live throughout the lifetime of the application
0044         KeyCache::self()->addKeys(path);
0045     }
0046 
0047     Private(const Private &rhs)
0048         : path(rhs.path)
0049         , hostName(rhs.hostName)
0050         , isRoot(rhs.isRoot)
0051     {
0052     }
0053 
0054     bool operator==(const Private &rhs) const
0055     {
0056         return path == rhs.path;
0057     }
0058 
0059     bool accessIsPossible(bool createMissingFolders = true);
0060     [[nodiscard]] bool canAccess(const QString &path) const;
0061 
0062     [[nodiscard]] QStringList subPaths() const
0063     {
0064         QStringList paths;
0065         paths << path + QLatin1StringView("/cur");
0066         paths << path + QLatin1StringView("/new");
0067         paths << path + QLatin1StringView("/tmp");
0068         return paths;
0069     }
0070 
0071     [[nodiscard]] QStringList listNew() const
0072     {
0073         QDir d(path + QLatin1StringView("/new"));
0074         d.setSorting(QDir::NoSort);
0075         return d.entryList(QDir::Files);
0076     }
0077 
0078     [[nodiscard]] QStringList listCurrent() const
0079     {
0080         QDir d(path + QLatin1StringView("/cur"));
0081         d.setSorting(QDir::NoSort);
0082         return d.entryList(QDir::Files);
0083     }
0084 
0085     [[nodiscard]] QString findRealKey(const QString &key) const
0086     {
0087         KeyCache *keyCache = KeyCache::self();
0088         if (keyCache->isNewKey(path, key)) {
0089 #ifdef DEBUG_KEYCACHE_CONSITENCY
0090             if (!QFile::exists(path + QString::fromLatin1("/new/") + key)) {
0091                 qCDebug(LIBMAILDIR_LOG) << "WARNING: key is in cache, but the file is gone: " << path + QString::fromLatin1("/new/") + key;
0092             }
0093 #endif
0094             return path + QLatin1StringView("/new/") + key;
0095         }
0096         if (keyCache->isCurKey(path, key)) {
0097 #ifdef DEBUG_KEYCACHE_CONSITENCY
0098             if (!QFile::exists(path + QString::fromLatin1("/cur/") + key)) {
0099                 qCDebug(LIBMAILDIR_LOG) << "WARNING: key is in cache, but the file is gone: " << path + QString::fromLatin1("/cur/") + key;
0100             }
0101 #endif
0102             return path + QLatin1StringView("/cur/") + key;
0103         }
0104         QString realKey = path + QLatin1StringView("/new/") + key;
0105 
0106         if (QFileInfo::exists(realKey)) {
0107             keyCache->addNewKey(path, key);
0108         } else { // not in "new", search in "cur"
0109             realKey = path + QLatin1StringView("/cur/") + key;
0110             if (QFileInfo::exists(realKey)) {
0111                 keyCache->addCurKey(path, key);
0112             } else {
0113                 realKey.clear(); // not in "cur" either
0114             }
0115         }
0116 
0117         return realKey;
0118     }
0119 
0120     static QString subDirNameForFolderName(const QString &folderName)
0121     {
0122         return QStringLiteral(".%1.directory").arg(folderName);
0123     }
0124 
0125     [[nodiscard]] QString subDirPath() const
0126     {
0127         QDir dir(path);
0128         return subDirNameForFolderName(dir.dirName());
0129     }
0130 
0131     bool moveAndRename(QDir &dest, const QString &newName)
0132     {
0133         if (!dest.exists()) {
0134             qCDebug(LIBMAILDIR_LOG) << "Destination does not exist";
0135             return false;
0136         }
0137         if (dest.exists(newName) || dest.exists(subDirNameForFolderName(newName))) {
0138             qCDebug(LIBMAILDIR_LOG) << "New name already in use";
0139             return false;
0140         }
0141 
0142         if (!dest.rename(path, newName)) {
0143             qCDebug(LIBMAILDIR_LOG) << "Failed to rename maildir";
0144             return false;
0145         }
0146         const QDir subDirs(Maildir::subDirPathForFolderPath(path));
0147         if (subDirs.exists() && !dest.rename(subDirs.path(), subDirNameForFolderName(newName))) {
0148             qCDebug(LIBMAILDIR_LOG) << "Failed to rename subfolders";
0149             return false;
0150         }
0151 
0152         path = dest.path() + QLatin1Char('/') + newName;
0153         return true;
0154     }
0155 
0156     QString path;
0157     QString hostName;
0158     QString lastError;
0159     bool isRoot;
0160 };
0161 
0162 Maildir::Maildir(const QString &path, bool isRoot)
0163     : d(new Private(path, isRoot))
0164 {
0165 }
0166 
0167 void Maildir::swap(const Maildir &rhs)
0168 {
0169     Private *p = d;
0170     d = new Private(*rhs.d);
0171     delete p;
0172 }
0173 
0174 Maildir::Maildir(const Maildir &rhs)
0175     : d(new Private(*rhs.d))
0176 {
0177 }
0178 
0179 Maildir &Maildir::operator=(const Maildir &rhs)
0180 {
0181     // copy and swap, exception safe, and handles assignment to self
0182     Maildir temp(rhs);
0183     swap(temp);
0184     return *this;
0185 }
0186 
0187 bool Maildir::operator==(const Maildir &rhs) const
0188 {
0189     return *d == *rhs.d;
0190 }
0191 
0192 Maildir::~Maildir()
0193 {
0194     delete d;
0195 }
0196 
0197 bool Maildir::Private::canAccess(const QString &path) const
0198 {
0199     // return access( QFile::encodeName( path ), R_OK | W_OK | X_OK ) != 0;
0200     // FIXME X_OK?
0201     QFileInfo d(path);
0202     return d.isReadable() && d.isWritable();
0203 }
0204 
0205 bool Maildir::Private::accessIsPossible(bool createMissingFolders)
0206 {
0207     QStringList paths = subPaths();
0208 
0209     paths.prepend(path);
0210 
0211     for (const QString &p : std::as_const(paths)) {
0212         if (!QFileInfo::exists(p)) {
0213             if (!createMissingFolders) {
0214                 lastError = i18n("Error opening %1; this folder is missing.", p);
0215                 return false;
0216             }
0217             QDir().mkpath(p);
0218             if (!QFileInfo::exists(p)) {
0219                 lastError = i18n("Error opening %1; this folder is missing.", p);
0220                 return false;
0221             }
0222         }
0223         if (!canAccess(p)) {
0224             lastError = i18n(
0225                 "Error opening %1; either this is not a valid "
0226                 "maildir folder, or you do not have sufficient access permissions.",
0227                 p);
0228             return false;
0229         }
0230     }
0231     return true;
0232 }
0233 
0234 bool Maildir::isValid(bool createMissingFolders) const
0235 {
0236     if (path().isEmpty()) {
0237         return false;
0238     }
0239     if (!d->isRoot) {
0240         if (d->accessIsPossible(createMissingFolders)) {
0241             return true;
0242         }
0243     } else {
0244         const QStringList lstMaildir = subFolderList();
0245         for (const QString &sf : lstMaildir) {
0246             const Maildir subMd = Maildir(path() + QLatin1Char('/') + sf);
0247             if (!subMd.isValid(createMissingFolders)) {
0248                 d->lastError = subMd.lastError();
0249                 return false;
0250             }
0251         }
0252         return true;
0253     }
0254     return false;
0255 }
0256 
0257 bool Maildir::isRoot() const
0258 {
0259     return d->isRoot;
0260 }
0261 
0262 bool Maildir::create()
0263 {
0264     // FIXME: in a failure case, this will leave partially created dirs around
0265     // we should clean them up, but only if they didn't previously existed...
0266     const QStringList lstPath = d->subPaths();
0267     for (const QString &p : lstPath) {
0268         QDir dir(p);
0269         if (!dir.exists(p)) {
0270             if (!dir.mkpath(p)) {
0271                 return false;
0272             }
0273         }
0274     }
0275     return true;
0276 }
0277 
0278 QString Maildir::path() const
0279 {
0280     return d->path;
0281 }
0282 
0283 QString Maildir::name() const
0284 {
0285     const QDir dir(d->path);
0286     return dir.dirName();
0287 }
0288 
0289 QString Maildir::addSubFolder(const QString &path)
0290 {
0291     if (!isValid()) {
0292         return {};
0293     }
0294 
0295     // make the subdir dir
0296     QDir dir(d->path);
0297     if (!d->isRoot) {
0298         dir.cdUp();
0299         if (!dir.exists(d->subDirPath())) {
0300             dir.mkdir(d->subDirPath());
0301         }
0302         dir.cd(d->subDirPath());
0303     }
0304 
0305     const QString fullPath = dir.path() + QLatin1Char('/') + path;
0306     Maildir subdir(fullPath);
0307     if (subdir.create()) {
0308         return fullPath;
0309     }
0310     return {};
0311 }
0312 
0313 bool Maildir::removeSubFolder(const QString &folderName)
0314 {
0315     if (!isValid()) {
0316         return false;
0317     }
0318     QDir dir(d->path);
0319     if (!d->isRoot) {
0320         dir.cdUp();
0321         if (!dir.exists(d->subDirPath())) {
0322             return false;
0323         }
0324         dir.cd(d->subDirPath());
0325     }
0326     if (!dir.exists(folderName)) {
0327         return false;
0328     }
0329 
0330     // remove it recursively
0331     bool result = QDir(dir.absolutePath() + QLatin1Char('/') + folderName).removeRecursively();
0332     QString subfolderName = subDirNameForFolderName(folderName);
0333     if (dir.exists(subfolderName)) {
0334         result &= QDir(dir.absolutePath() + QLatin1Char('/') + subfolderName).removeRecursively();
0335     }
0336     return result;
0337 }
0338 
0339 Maildir Maildir::subFolder(const QString &subFolder) const
0340 {
0341     // make the subdir dir
0342     QDir dir(d->path);
0343     if (!d->isRoot) {
0344         dir.cdUp();
0345         if (dir.exists(d->subDirPath())) {
0346             dir.cd(d->subDirPath());
0347         }
0348     }
0349     return Maildir(dir.path() + QLatin1Char('/') + subFolder);
0350 }
0351 
0352 Maildir Maildir::parent() const
0353 {
0354     if (!isValid() || d->isRoot) {
0355         return Maildir();
0356     }
0357     QDir dir(d->path);
0358     dir.cdUp();
0359     if (!dir.dirName().startsWith(QLatin1Char('.')) || !dir.dirName().endsWith(QLatin1StringView(".directory"))) {
0360         return Maildir();
0361     }
0362     const QString parentName = dir.dirName().mid(1, dir.dirName().size() - 11);
0363     dir.cdUp();
0364     dir.cd(parentName);
0365     return Maildir(dir.path());
0366 }
0367 
0368 QStringList Maildir::entryList() const
0369 {
0370     QStringList result;
0371     if (isValid()) {
0372         result = d->listNew() + d->listCurrent();
0373     }
0374     //  qCDebug(LIBMAILDIR_LOG) <<"Maildir::entryList()" << result;
0375     return result;
0376 }
0377 
0378 QStringList Maildir::listCurrent() const
0379 {
0380     QStringList result;
0381     if (isValid()) {
0382         result = d->listCurrent();
0383     }
0384     return result;
0385 }
0386 
0387 QString Maildir::findRealKey(const QString &key) const
0388 {
0389     return d->findRealKey(key);
0390 }
0391 
0392 QStringList Maildir::listNew() const
0393 {
0394     QStringList result;
0395     if (isValid()) {
0396         result = d->listNew();
0397     }
0398     return result;
0399 }
0400 
0401 QString Maildir::pathToNew() const
0402 {
0403     if (isValid()) {
0404         return d->path + QLatin1StringView("/new");
0405     }
0406     return {};
0407 }
0408 
0409 QString Maildir::pathToCurrent() const
0410 {
0411     if (isValid()) {
0412         return d->path + QLatin1StringView("/cur");
0413     }
0414     return {};
0415 }
0416 
0417 QString Maildir::subDirPath() const
0418 {
0419     QDir dir(d->path);
0420     dir.cdUp();
0421     return dir.path() + QLatin1Char('/') + d->subDirPath();
0422 }
0423 
0424 QStringList Maildir::subFolderList() const
0425 {
0426     QDir dir(d->path);
0427 
0428     // the root maildir has its subfolders directly beneath it
0429     if (!d->isRoot) {
0430         dir.cdUp();
0431         if (!dir.exists(d->subDirPath())) {
0432             return {};
0433         }
0434         dir.cd(d->subDirPath());
0435     }
0436     dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
0437     QStringList entries = dir.entryList();
0438     entries.removeAll(QStringLiteral("cur"));
0439     entries.removeAll(QStringLiteral("new"));
0440     entries.removeAll(QStringLiteral("tmp"));
0441     return entries;
0442 }
0443 
0444 QByteArray Maildir::readEntry(const QString &key) const
0445 {
0446     QByteArray result;
0447 
0448     QString realKey(d->findRealKey(key));
0449     if (realKey.isEmpty()) {
0450         // FIXME error handling?
0451         qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntry unable to find: " << key;
0452         d->lastError = i18n("Cannot locate mail file %1.", key);
0453         return result;
0454     }
0455 
0456     QFile f(realKey);
0457     if (!f.open(QIODevice::ReadOnly)) {
0458         d->lastError = i18n("Cannot open mail file %1.", realKey);
0459         return result;
0460     }
0461 
0462     // FIXME be safer than this
0463     result = f.readAll();
0464 
0465     return result;
0466 }
0467 
0468 qint64 Maildir::size(const QString &key) const
0469 {
0470     QString realKey(d->findRealKey(key));
0471     if (realKey.isEmpty()) {
0472         // FIXME error handling?
0473         qCWarning(LIBMAILDIR_LOG) << "Maildir::size unable to find: " << key;
0474         d->lastError = i18n("Cannot locate mail file %1.", key);
0475         return -1;
0476     }
0477 
0478     const QFileInfo info(realKey);
0479     if (!info.exists()) {
0480         d->lastError = i18n("Cannot open mail file %1.", realKey);
0481         return -1;
0482     }
0483 
0484     return info.size();
0485 }
0486 
0487 QDateTime Maildir::lastModified(const QString &key) const
0488 {
0489     const QString realKey(d->findRealKey(key));
0490     if (realKey.isEmpty()) {
0491         qCWarning(LIBMAILDIR_LOG) << "Maildir::lastModified unable to find: " << key;
0492         d->lastError = i18n("Cannot locate mail file %1.", key);
0493         return {};
0494     }
0495 
0496     const QFileInfo info(realKey);
0497     if (!info.exists()) {
0498         return {};
0499     }
0500 
0501     return info.lastModified();
0502 }
0503 
0504 QByteArray Maildir::readEntryHeadersFromFile(const QString &file) const
0505 {
0506     QByteArray result;
0507 
0508     QFile f(file);
0509     if (!f.open(QIODevice::ReadOnly)) {
0510         // FIXME error handling?
0511         qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntryHeaders unable to find: " << file;
0512         d->lastError = i18n("Cannot locate mail file %1.", file);
0513         return result;
0514     }
0515     f.map(0, qMin((qint64)8000, f.size()));
0516     for (;;) {
0517         QByteArray line = f.readLine();
0518         if (line.isEmpty() || line.startsWith('\n')) {
0519             break;
0520         }
0521         result.append(line);
0522     }
0523     return result;
0524 }
0525 
0526 QByteArray Maildir::readEntryHeaders(const QString &key) const
0527 {
0528     const QString realKey(d->findRealKey(key));
0529     if (realKey.isEmpty()) {
0530         qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntryHeaders unable to find: " << key;
0531         d->lastError = i18n("Cannot locate mail file %1.", key);
0532         return {};
0533     }
0534 
0535     return readEntryHeadersFromFile(realKey);
0536 }
0537 
0538 static QString createUniqueFileName()
0539 {
0540     const qint64 time = QDateTime::currentMSecsSinceEpoch();
0541     const int r = QRandomGenerator::global()->bounded(1000);
0542     const QString identifier = QLatin1StringView("R") + QString::number(r);
0543 
0544     QString fileName = QString::number(time) + QLatin1Char('.') + identifier + QLatin1Char('.');
0545 
0546     return fileName;
0547 }
0548 
0549 bool Maildir::writeEntry(const QString &key, const QByteArray &data)
0550 {
0551     QString realKey(d->findRealKey(key));
0552     if (realKey.isEmpty()) {
0553         // FIXME error handling?
0554         qCWarning(LIBMAILDIR_LOG) << "Maildir::writeEntry unable to find: " << key;
0555         d->lastError = i18n("Cannot locate mail file %1.", key);
0556         return false;
0557     }
0558     QFile f(realKey);
0559     bool result = f.open(QIODevice::WriteOnly);
0560     result = result & (f.write(data) != -1);
0561     f.close();
0562     if (!result) {
0563         d->lastError = i18n("Cannot write to mail file %1.", realKey);
0564         return false;
0565     }
0566     return true;
0567 }
0568 
0569 QString Maildir::addEntry(const QByteArray &data)
0570 {
0571     QString uniqueKey;
0572     QString key;
0573     QString finalKey;
0574     QString curKey;
0575 
0576     // QUuid doesn't return globally unique identifiers, therefore we query until we
0577     // get one that doesn't exists yet
0578     do {
0579         uniqueKey = createUniqueFileName() + d->hostName;
0580         key = d->path + QLatin1StringView("/tmp/") + uniqueKey;
0581         finalKey = d->path + QLatin1StringView("/new/") + uniqueKey;
0582         curKey = d->path + QLatin1StringView("/cur/") + uniqueKey;
0583     } while (QFile::exists(key) || QFile::exists(finalKey) || QFile::exists(curKey));
0584 
0585     QFile f(key);
0586     bool result = f.open(QIODevice::WriteOnly);
0587     result = result & (f.write(data) != -1);
0588     f.close();
0589     if (!result) {
0590         d->lastError = i18n("Cannot write to mail file %1.", key);
0591         return {};
0592     }
0593     /*
0594      * FIXME:
0595      *
0596      * The whole point of the locking free maildir idea is that the moves between
0597      * the internal directories are atomic. Afaik QFile::rename does not guarantee
0598      * that, so this will need to be done properly. - ta
0599      *
0600      * For reference: http://trolltech.com/developer/task-tracker/index_html?method=entry&id=211215
0601      */
0602     if (!f.rename(finalKey)) {
0603         qCWarning(LIBMAILDIR_LOG) << "Maildir: Failed to add entry: " << finalKey << "! Error: " << f.errorString();
0604         d->lastError = i18n("Failed to create mail file %1. The error was: %2", finalKey, f.errorString());
0605         return {};
0606     }
0607     KeyCache *keyCache = KeyCache::self();
0608     keyCache->removeKey(d->path, key); // remove all keys, be it "cur" or "new" first
0609     keyCache->addNewKey(d->path, key); // and add a key for "new", as the mail was moved there
0610     return uniqueKey;
0611 }
0612 
0613 bool Maildir::removeEntry(const QString &key)
0614 {
0615     QString realKey(d->findRealKey(key));
0616     if (realKey.isEmpty()) {
0617         qCWarning(LIBMAILDIR_LOG) << "Maildir::removeEntry unable to find: " << key;
0618         return false;
0619     }
0620     KeyCache *keyCache = KeyCache::self();
0621     keyCache->removeKey(d->path, key);
0622     return QFile::remove(realKey);
0623 }
0624 
0625 QString Maildir::changeEntryFlags(const QString &key, const Akonadi::Item::Flags &flags)
0626 {
0627     QString realKey(d->findRealKey(key));
0628     if (realKey.isEmpty()) {
0629         qCWarning(LIBMAILDIR_LOG) << "Maildir::changeEntryFlags unable to find: " << key;
0630         d->lastError = i18n("Cannot locate mail file %1.", key);
0631         return {};
0632     }
0633 
0634     const QRegularExpression rx = statusSeparatorRx();
0635     QString finalKey = key.left(key.indexOf(rx));
0636 
0637     QStringList mailDirFlags;
0638     for (const Akonadi::Item::Flag &flag : flags) {
0639         if (flag == Akonadi::MessageFlags::Forwarded) {
0640             mailDirFlags << QStringLiteral("P");
0641         } else if (flag == Akonadi::MessageFlags::Replied) {
0642             mailDirFlags << QStringLiteral("R");
0643         } else if (flag == Akonadi::MessageFlags::Seen) {
0644             mailDirFlags << QStringLiteral("S");
0645         } else if (flag == Akonadi::MessageFlags::Deleted) {
0646             mailDirFlags << QStringLiteral("T");
0647         } else if (flag == Akonadi::MessageFlags::Flagged) {
0648             mailDirFlags << QStringLiteral("F");
0649         }
0650     }
0651     mailDirFlags.sort();
0652     if (!mailDirFlags.isEmpty()) {
0653 #ifdef Q_OS_WIN
0654         finalKey.append(QLatin1StringView("!2,") + mailDirFlags.join(QString()));
0655 #else
0656         finalKey.append(QLatin1StringView(":2,") + mailDirFlags.join(QString()));
0657 #endif
0658     }
0659 
0660     const QString newUniqueKey = finalKey; // key without path
0661     finalKey.prepend(d->path + QLatin1StringView("/cur/"));
0662 
0663     if (realKey == finalKey) {
0664         // Somehow it already is named this way (e.g. migration bug -> wrong status in akonadi)
0665         return newUniqueKey;
0666     }
0667 
0668     QFile f(realKey);
0669     if (QFile::exists(finalKey)) {
0670         QFile destFile(finalKey);
0671         QByteArray destContent;
0672         if (destFile.open(QIODevice::ReadOnly)) {
0673             destContent = destFile.readAll();
0674             destFile.close();
0675         }
0676         QByteArray sourceContent;
0677         if (f.open(QIODevice::ReadOnly)) {
0678             sourceContent = f.readAll();
0679             f.close();
0680         }
0681 
0682         if (destContent != sourceContent) {
0683             QString newFinalKey = QLatin1StringView("1-") + newUniqueKey;
0684             int i = 1;
0685             const QString currentPath = d->path + QLatin1StringView("/cur/");
0686             while (QFile::exists(currentPath + newFinalKey)) {
0687                 i++;
0688                 newFinalKey = QString::number(i) + QLatin1Char('-') + newUniqueKey;
0689             }
0690             finalKey = currentPath + newFinalKey;
0691         } else {
0692             QFile::remove(finalKey); // they are the same
0693         }
0694     }
0695 
0696     if (!f.rename(finalKey)) {
0697         qCWarning(LIBMAILDIR_LOG) << "Maildir: Failed to rename entry: " << f.fileName() << " to " << finalKey << "! Error: " << f.errorString();
0698         d->lastError = i18n("Failed to update the file name %1 to %2 on the disk. The error was: %3.", f.fileName(), finalKey, f.errorString());
0699         return {};
0700     }
0701 
0702     KeyCache *keyCache = KeyCache::self();
0703     keyCache->removeKey(d->path, key);
0704     keyCache->addCurKey(d->path, newUniqueKey);
0705 
0706     return newUniqueKey;
0707 }
0708 
0709 Akonadi::Item::Flags Maildir::readEntryFlags(const QString &key) const
0710 {
0711     Akonadi::Item::Flags flags;
0712 
0713     const QRegularExpression rx = statusSeparatorRx();
0714     const int index = key.indexOf(rx);
0715     if (index != -1) {
0716         const QStringView mailDirFlags = QStringView(key).mid(index + 3); // after "(:|!)2,"
0717         const int flagSize(mailDirFlags.size());
0718         for (int i = 0; i < flagSize; ++i) {
0719             const QChar flag = mailDirFlags.at(i);
0720             if (flag == QLatin1Char('P')) {
0721                 flags << Akonadi::MessageFlags::Forwarded;
0722             } else if (flag == QLatin1Char('R')) {
0723                 flags << Akonadi::MessageFlags::Replied;
0724             } else if (flag == QLatin1Char('S')) {
0725                 flags << Akonadi::MessageFlags::Seen;
0726             } else if (flag == QLatin1Char('F')) {
0727                 flags << Akonadi::MessageFlags::Flagged;
0728             }
0729         }
0730     }
0731 
0732     return flags;
0733 }
0734 
0735 bool Maildir::moveTo(const Maildir &newParent)
0736 {
0737     if (d->isRoot) {
0738         return false; // not supported
0739     }
0740 
0741     QDir newDir(newParent.path());
0742     if (!newParent.d->isRoot) {
0743         newDir.cdUp();
0744         if (!newDir.exists(newParent.d->subDirPath())) {
0745             newDir.mkdir(newParent.d->subDirPath());
0746         }
0747         newDir.cd(newParent.d->subDirPath());
0748     }
0749 
0750     QDir currentDir(d->path);
0751     currentDir.cdUp();
0752 
0753     if (newDir == currentDir) {
0754         return true;
0755     }
0756 
0757     return d->moveAndRename(newDir, name());
0758 }
0759 
0760 bool Maildir::rename(const QString &newName)
0761 {
0762     if (name() == newName) {
0763         return true;
0764     }
0765     if (d->isRoot) {
0766         return false; // not (yet) supported
0767     }
0768 
0769     QDir dir(d->path);
0770     dir.cdUp();
0771 
0772     return d->moveAndRename(dir, newName);
0773 }
0774 
0775 QString Maildir::moveEntryTo(const QString &key, const Maildir &destination)
0776 {
0777     const QString realKey(d->findRealKey(key));
0778     if (realKey.isEmpty()) {
0779         qCWarning(LIBMAILDIR_LOG) << "Unable to find: " << key;
0780         d->lastError = i18n("Cannot locate mail file %1.", key);
0781         return {};
0782     }
0783     QFile f(realKey);
0784     // ### is this safe regarding the maildir locking scheme?
0785     const QString targetKey = destination.path() + QStringLiteral("/new/") + key;
0786     if (!f.rename(targetKey)) {
0787         qCDebug(LIBMAILDIR_LOG) << "Failed to rename" << realKey << "to" << targetKey << "! Error: " << f.errorString();
0788         d->lastError = f.errorString();
0789         return {};
0790     }
0791 
0792     KeyCache *keyCache = KeyCache::self();
0793 
0794     keyCache->addNewKey(destination.path(), key);
0795     keyCache->removeKey(d->path, key);
0796 
0797     return key;
0798 }
0799 
0800 QString Maildir::subDirPathForFolderPath(const QString &folderPath)
0801 {
0802     QDir dir(folderPath);
0803     const QString dirName = dir.dirName();
0804     dir.cdUp();
0805     return QFileInfo(dir, Private::subDirNameForFolderName(dirName)).filePath();
0806 }
0807 
0808 QString Maildir::subDirNameForFolderName(const QString &folderName)
0809 {
0810     return Private::subDirNameForFolderName(folderName);
0811 }
0812 
0813 void Maildir::removeCachedKeys(const QStringList &keys)
0814 {
0815     KeyCache *keyCache = KeyCache::self();
0816     for (const QString &key : keys) {
0817         keyCache->removeKey(d->path, key);
0818     }
0819 }
0820 
0821 void Maildir::refreshKeyCache()
0822 {
0823     KeyCache::self()->refreshKeys(d->path);
0824 }
0825 
0826 QString Maildir::lastError() const
0827 {
0828     return d->lastError;
0829 }