aboutsummaryrefslogtreecommitdiffstats
path: root/src/types.h
blob: 409ce790402a1f2f652ff013f643ff4da46b4f23 (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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*
 * 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.
 */

#pragma once

#include <QDateTime>
#include <QMetaType>
#include <QString>
#include <QStringList>

struct ThreadSummary
{
    QString threadId;
    QString subject;
    QString authors;
    QDateTime date;
    int totalCount = 0;
    int matchedCount = 0;
    QStringList tags;

    /// The thread's FIRST message, which is the one the root card stands for.
    ///
    /// Carried by the query itself rather than learned when the thread is
    /// expanded. That timing was item 66: until a thread had been opened the
    /// model did not know this id, so clicking an unexpanded root fell through
    /// to rendering the whole conversation, and the identical click behaved
    /// differently afterwards.
    ///
    /// Unlike `recipients` below, this is free. It comes from
    /// notmuch_thread_get_toplevel_messages, which reads the INDEX, not the
    /// message files: measured indistinguishable from not collecting it at all
    /// over a 36,615-thread database. Do not move it behind a flag by analogy
    /// with recipients; the two have nothing in common but their position here.
    QString firstMessageId;

    /// The tags of that ONE message, as opposed to `tags` above, which is
    /// notmuch's union over the whole thread.
    ///
    /// A card stands for one message but sits above a conversation, and shows
    /// both: its own tags at full size, the thread's others smaller (item
    /// 111). Without this the split is unknown until the row is opened and the
    /// message loads, so every chip renders as the card's own and then shrinks
    /// on selection, which is what the user reported.
    ///
    /// Free, for the same reason `firstMessageId` is: the walk that finds that
    /// message is already happening and this reads the INDEX, not the message
    /// file. Do not move it behind a flag by analogy with `recipients`.
    QStringList firstMessageTags;

    /// Who the thread's messages were sent TO, summarised for one line.
    ///
    /// Empty unless the query asked for it, and that is a performance
    /// contract rather than a default: To is NOT served from notmuch's index,
    /// so filling this reads every message file. Measured at 8.7 ms per thread,
    /// which is 38 seconds over a 4411-thread inbox and 663 ms over a
    /// 601-thread sent view. Only a Sent query asks.
    ///
    /// Shown in the sender's place there, where `authors` is the user on every
    /// row and carries nothing.
    QString recipients;

    bool isUnread() const { return tags.contains(QStringLiteral("unread")); }
    bool isFlagged() const { return tags.contains(QStringLiteral("flagged")); }
    /// True when this message was forwarded. The Maildir "P" (passed) flag,
    /// which notmuch translates to a tag under maildir.synchronize_flags.
    /// Item 68 measured the whole database: nothing derives this from a
    /// subject line, so a "Fwd:" subject with no flag is correctly unmarked.
    bool isPassed() const { return tags.contains(QStringLiteral("passed")); }

    /// True when this message was replied to. The Maildir "R" flag.
    bool isReplied() const { return tags.contains(QStringLiteral("replied")); }
    bool isDeleted() const { return tags.contains(QStringLiteral("deleted")); }
    bool isSpam() const { return tags.contains(QStringLiteral("spam")); }

    /// notmuch applies "attachment" itself while indexing, so this needs no
    /// MIME parsing and no extra worker query: the tag is already in tags.
    bool hasAttachment() const
    {
        return tags.contains(QStringLiteral("attachment"));
    }

    /// True while the thread is tagged for removal. notmuch deletes nothing
    /// itself: the tag marks the thread for whatever the user's sync script
    /// does next, so the row has to show it is on its way out.
    bool isDoomed() const { return isDeleted() || isSpam(); }
};

struct MessageRef
{
    QString messageId;
    QString filePath;
    QStringList tags;

    /// True when the message itself matched the user's query, as opposed to
    /// being pulled in only because a sibling in its thread matched. Drives
    /// whether it renders expanded or as a stub.
    bool matched = true;

    /// For the message pane header's flagged mark (item 70). The tags are
    /// already carried, so this is the same predicate MessageNode and
    /// ThreadSummary offer rather than new state.
    bool isFlagged() const { return tags.contains(QStringLiteral("flagged")); }
};

/// One message as a row in the thread list.
///
/// Separate from MessageRef, which exists for RENDERING a thread and carries
/// only what the message pane needs. A row has to be drawn without opening the
/// message at all, so the display facts live here.
struct MessageNode
{
    QString messageId;
    QString threadId;  ///< The thread this message belongs to.
    QString from;
    QString subject;
    QDateTime date;
    QStringList tags;
    QString filePath;

    /// Reply depth within the thread. 0 is the thread's first message, which
    /// occupies the ROOT row rather than a child row: the user's model is
    /// "N replies", so a thread of 7 shows 1 root and 6 descendants.
    int depth = 0;

    bool isUnread() const { return tags.contains(QStringLiteral("unread")); }
    bool isFlagged() const { return tags.contains(QStringLiteral("flagged")); }
    /// True when this message was forwarded. The Maildir "P" (passed) flag,
    /// which notmuch translates to a tag under maildir.synchronize_flags.
    /// Item 68 measured the whole database: nothing derives this from a
    /// subject line, so a "Fwd:" subject with no flag is correctly unmarked.
    bool isPassed() const { return tags.contains(QStringLiteral("passed")); }

    /// True when this message was replied to. The Maildir "R" flag.
    bool isReplied() const { return tags.contains(QStringLiteral("replied")); }

    /// notmuch applies "attachment" while indexing, so this needs no MIME
    /// parsing, exactly as on ThreadSummary.
    bool hasAttachment() const
    {
        return tags.contains(QStringLiteral("attachment"));
    }

    bool isDeleted() const { return tags.contains(QStringLiteral("deleted")); }
    bool isSpam() const { return tags.contains(QStringLiteral("spam")); }

    /// True while the message is tagged for removal, exactly as the thread
    /// predicate of the same name. A reply carries its own fate: a
    /// message-scoped Delete tags one message, and the reply's row is the only
    /// place the user can see that happen.
    bool isDoomed() const { return isDeleted() || isSpam(); }
};

/// What an action is about to touch, resolved from the selection.
///
/// Exists because the thread list holds two kinds of row since item 20, so a
/// keypress alone no longer says whether it hit one message or seven. Actions
/// take one of these rather than a bare list of thread ids, and the status bar
/// reports it: this project's answer to that ambiguity is to make the scope
/// visible, not to add a confirmation dialog. See CLAUDE.md on why.
struct ActionScope
{
    QStringList threadIds;   ///< Whole threads to act on.
    QStringList messageIds;  ///< Individual messages to act on.

    /// Messages the action will touch in total, for the status bar. A whole
    /// thread contributes all of its messages, a message row contributes one.
    int messageCount = 0;

    /// True when any whole thread is in scope, which drives the
    /// "(whole thread)" suffix in the status bar.
    bool wholeThread = false;

    bool isEmpty() const
    {
        return threadIds.isEmpty() && messageIds.isEmpty();
    }
};

/// One tag mutation, kept so it can be inverted for undo.
struct TagChange
{
    QStringList messageIds;
    QStringList added;
    QStringList removed;
    QString description;  ///< Shown in the undo action's text.

    TagChange inverted() const
    {
        return TagChange{ messageIds, removed, added,
                          QStringLiteral("Undo %1").arg(description) };
    }
};

/// Database-level facts for the Maildir overview.
///
/// Every field is -1 until answered, so a dialog opened against a database that
/// cannot be read shows "unknown" rather than a confident zero. A zero is a
/// claim, and "no mail at all" is exactly the wrong thing to tell someone whose
/// index failed to open.
struct DatabaseStats
{
    int messages = -1;  ///< Every message notmuch has indexed.
    int threads = -1;   ///< Every thread. Differs from messages by reply depth.
    int tags = -1;      ///< Distinct tag names in the database.
};

Q_DECLARE_METATYPE(ThreadSummary)
Q_DECLARE_METATYPE(MessageRef)
Q_DECLARE_METATYPE(MessageNode)
Q_DECLARE_METATYPE(TagChange)
Q_DECLARE_METATYPE(DatabaseStats)