/* * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs * Copyright (C) 2026 Danilo M. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "maildirname.h" #include #include #include #include #include namespace MaildirName { /// A fresh Maildir filename for a message being moved between folders, /// preserving only its `:2,` suffix. /// /// mbsync's manual is explicit about why this exists, under "the more /// efficient default UID mapping scheme": "it is important that the MUA /// renames files when moving them between Maildir folders", and "the general /// expectation is that a completely new filename is generated as if the /// message was new". /// /// The `,U=` infix mbsync writes is its per-folder IMAP UID. Carrying it /// into another folder makes it a claim about a folder the file is no longer /// in; moving a message out and back then reinserts a UID the server has /// since reassigned, and mbsync refuses the folder with `Maildir error: /// duplicate UID`. Measured on real mail, four collisions in one folder from /// a single move-and-restore. /// /// The FLAGS are kept, deliberately, and that is not a contradiction of /// "as if the message was new". They record seen, flagged and replied, and /// `maildir.synchronize_flags` is true, so notmuch reads them back as tags: /// dropping them would mark every deleted message unread and lose Important /// on the way to the trash. Only the unique part is regenerated. QString fresh(const QString &oldName) { // The `:2,` suffix, when there is one. `info` is everything from the // separator on, so an empty-flag `:2,` is preserved as faithfully as // `:2,FS`. QString info; const int sep = oldName.indexOf(QStringLiteral(":2,")); if (sep >= 0) info = oldName.mid(sep); // The conventional left-to-right unique part: time, a per-process counter, // the pid, the host. The counter is what makes two messages moved in the // same second distinct, which a timestamp alone does not guarantee. static quint64 counter = 0; const qint64 now = QDateTime::currentSecsSinceEpoch(); const QString host = QHostInfo::localHostName().isEmpty() ? QStringLiteral("localhost") : QHostInfo::localHostName(); return QStringLiteral("%1.M%2P%3Q%4.%5%6") .arg(now) .arg(QDateTime::currentMSecsSinceEpoch() % 1000) .arg(QCoreApplication::applicationPid()) .arg(++counter) // A `/` or a `:` in a hostname would break the path or the flag // separator. Neither is legal in a hostname, so this is belt and // braces rather than a known case. .arg(QString(host).replace(QLatin1Char('/'), QLatin1Char('_')) .replace(QLatin1Char(':'), QLatin1Char('_'))) .arg(info); } QString resolveRenamed(const QString &path) { if (path.isEmpty()) return QString(); // The ordinary case, and the overwhelmingly common one: nothing was // renamed. One stat, then out. if (QFileInfo::exists(path)) return path; const QFileInfo info(path); const QString name = info.fileName(); // The unique part mbsync preserves. `:2,D` becomes // `,U=5:2,D`, so the stem ends at whichever of `,` or `:` comes // first. A name carrying neither is all stem. int cut = name.size(); for (const QChar separator : { QLatin1Char(','), QLatin1Char(':') }) { const int at = name.indexOf(separator); if (at >= 0 && at < cut) cut = at; } const QString stem = name.left(cut); if (stem.isEmpty()) return QString(); // One directory, never a recursive walk: a rename keeps the file where it // was, and a file that changed FOLDERS is a different question that only // the message id can answer (see NotmuchWorker::moveMessages(), item 162). const QDir dir(info.absolutePath()); if (!dir.exists()) return QString(); QString found; const QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); for (const QFileInfo &entry : entries) { const QString candidate = entry.fileName(); // Anchored on the stem AND on what follows it, so `...Q2` cannot match // `...Q23`: the next character must begin the infix or the flags. if (!candidate.startsWith(stem)) continue; const QString rest = candidate.mid(stem.size()); if (!rest.isEmpty() && !rest.startsWith(QLatin1Char(',')) && !rest.startsWith(QLatin1Char(':'))) { continue; } // Two files sharing a stem cannot happen in a correct Maildir. Refuse // rather than guess: the caller reports "gone", which is honest, where // a guess could open, move or delete the wrong message. if (!found.isEmpty()) return QString(); found = entry.absoluteFilePath(); } return found; } } // namespace MaildirName