aboutsummaryrefslogtreecommitdiffstats
path: root/src/maildirname.cpp
blob: 6263aec82710aad7bda1b1379490befae64d972e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/*
 * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
 * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
 *
 * 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 <QCoreApplication>
#include <QDateTime>
#include <QHostInfo>

namespace MaildirName {

/// A fresh Maildir filename for a message being moved between folders,
/// preserving only its `:2,<flags>` 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=<n>` 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);
}

}  // namespace MaildirName