data/Models.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | #include <optional> | ||
| 3 | |||
| 4 | #include <QDateTime> | ||
| 5 | #include <QString> | ||
| 6 | #include <QStringList> | ||
| 7 | |||
| 8 | // FolderInfo represents a single IMAP mailbox from the LIST response. | ||
| 9 | // Used to populate the FolderTree UI. | ||
| 10 | struct FolderInfo { | ||
| 11 | QString name; // Display name, UTF-8 decoded (e.g. "Entwürfe") | ||
| 12 | QString path; // Raw IMAP path (e.g. "INBOX.Entw&APw-rfe") | ||
| 13 | QStringList flags; // IMAP flags: \HasChildren, \Noselect, \Sent, etc. | ||
| 14 | QString delimiter; // Hierarchy delimiter: "." or "/" | ||
| 15 | |||
| 16 | // Convenience: check for common flags | ||
| 17 | 1 | bool hasChildren() const { | |
| 18 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | return flags.contains("\\HasChildren", Qt::CaseInsensitive); |
| 19 | } | ||
| 20 | 24746 | bool isNoselect() const { | |
| 21 |
4/7✓ Branch 2 → 3 taken 24746 times.
✓ Branch 4 → 5 taken 24651 times.
✓ Branch 4 → 8 taken 95 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 24651 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
|
74143 | return flags.contains("\\Noselect", Qt::CaseInsensitive) || |
| 22 |
4/7✓ Branch 5 → 6 taken 24651 times.
✓ Branch 10 → 11 taken 24651 times.
✓ Branch 10 → 12 taken 95 times.
✓ Branch 12 → 13 taken 24746 times.
✗ Branch 12 → 14 not taken.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
|
74143 | flags.contains("\\NonExistent", Qt::CaseInsensitive); |
| 23 | } | ||
| 24 |
1/1✓ Branch 2 → 3 taken 9285 times.
|
9285 | bool isSent() const { return flags.contains("\\Sent", Qt::CaseInsensitive); } |
| 25 | 9273 | bool isDrafts() const { | |
| 26 |
1/1✓ Branch 2 → 3 taken 9273 times.
|
9273 | return flags.contains("\\Drafts", Qt::CaseInsensitive); |
| 27 | } | ||
| 28 | 9276 | bool isTrash() const { | |
| 29 |
1/1✓ Branch 2 → 3 taken 9276 times.
|
9276 | return flags.contains("\\Trash", Qt::CaseInsensitive); |
| 30 | } | ||
| 31 | 9220 | bool isArchive() const { | |
| 32 |
1/1✓ Branch 2 → 3 taken 9220 times.
|
9220 | return flags.contains("\\Archive", Qt::CaseInsensitive); |
| 33 | } | ||
| 34 |
1/1✓ Branch 2 → 3 taken 9226 times.
|
9226 | bool isJunk() const { return flags.contains("\\Junk", Qt::CaseInsensitive); } |
| 35 | }; | ||
| 36 | |||
| 37 | // Flag bitmask constants for MailHeader::flags. | ||
| 38 | // Values match IMAP system flag semantics. | ||
| 39 | namespace MailFlag { | ||
| 40 | constexpr quint32 None = 0; | ||
| 41 | constexpr quint32 Seen = 1 << 0; // \Seen | ||
| 42 | constexpr quint32 Answered = 1 << 1; // \Answered | ||
| 43 | constexpr quint32 Flagged = 1 << 2; // \Flagged | ||
| 44 | constexpr quint32 Deleted = 1 << 3; // \Deleted | ||
| 45 | constexpr quint32 Draft = 1 << 4; // \Draft | ||
| 46 | } // namespace MailFlag | ||
| 47 | |||
| 48 | // MailHeader represents the envelope data of a single email. | ||
| 49 | // Stored in SQLite cache for offline access and fast listing. | ||
| 50 | struct MailHeader { | ||
| 51 | qint64 uid = 0; // IMAP UID (unique within a folder's UIDVALIDITY) | ||
| 52 | qint64 folderId = 0; // FK to MailCache folders table | ||
| 53 | QString subject; | ||
| 54 | QString from; | ||
| 55 | QString to; // every To address, "Name <a@b>, c@d" (Sprint 95) | ||
| 56 | QString cc; // every Cc address, same form (Sprint 95) | ||
| 57 | QString listId; // List-Id without <> and display name; empty = none | ||
| 58 | // True when to/cc/listId come from a sync since cache schema v24; rows | ||
| 59 | // synced earlier have only the first To address and no Cc/List-Id. | ||
| 60 | bool recipientsKnown = false; | ||
| 61 | QDateTime date; | ||
| 62 | quint32 flags = MailFlag::None; // Bitmask of MailFlag constants | ||
| 63 | qint64 size = 0; // RFC822.SIZE in bytes | ||
| 64 | QStringList labels; // IMAP keywords (non-system flags) | ||
| 65 | bool hasAttachments = false; // Cached: has body attachments? | ||
| 66 | bool isSpam = false; // T-231: X-Spam: Yes header detected | ||
| 67 | // Sprint 105 S105.2.2: the server filter's score as stored at header sync | ||
| 68 | // (spam_score/spam_threshold/spam_tool, schema v25). spamKnown = this | ||
| 69 | // writer read the filter fields; false keeps what a sync stored before. | ||
| 70 | bool spamKnown = false; | ||
| 71 | std::optional<double> spamScore; | ||
| 72 | std::optional<double> spamThreshold; | ||
| 73 | QString spamTool; // "SpamAssassin", "Rspamd" or "" (none) | ||
| 74 | QString messageId; // Message-ID header (without <>) | ||
| 75 | QString inReplyTo; // First In-Reply-To ID (compatibility view) | ||
| 76 | QStringList inReplyToIds; // Ordered canonical In-Reply-To token list | ||
| 77 | QStringList references; // References header (list of message-ids) | ||
| 78 | |||
| 79 | // Flag convenience methods | ||
| 80 | 381009 | bool isSeen() const { return flags & MailFlag::Seen; } | |
| 81 | 53470 | bool isAnswered() const { return flags & MailFlag::Answered; } | |
| 82 | 55563 | bool isFlagged() const { return flags & MailFlag::Flagged; } | |
| 83 | 3 | bool isDraft() const { return flags & MailFlag::Draft; } | |
| 84 | }; | ||
| 85 | |||
| 86 | struct MailIdentity { | ||
| 87 | QString account; | ||
| 88 | qint64 folderId = -1; | ||
| 89 | qint64 uid = -1; | ||
| 90 | |||
| 91 | bool isValid() const { return folderId > 0 && uid >= 0; } | ||
| 92 | }; | ||
| 93 | |||
| 94 | // Attachment metadata for a single email attachment. | ||
| 95 | // BLOB data is loaded on-demand from MailCache (lazy loading). | ||
| 96 | enum class AttachmentPresentation { | ||
| 97 | Regular = 0, // Visible downloadable attachment | ||
| 98 | InlineCandidate = 1, // Eligible for a sanitized cid: image | ||
| 99 | ExplicitAttachmentWithContentId = 2, // CID-bearing but still downloadable | ||
| 100 | RejectedInline = 3, // Unsafe, ambiguous, or over budget | ||
| 101 | }; | ||
| 102 | |||
| 103 | struct Attachment { | ||
| 104 | qint64 id = 0; // DB primary key (0 = not yet stored) | ||
| 105 | QString filename; // e.g. "report.pdf" | ||
| 106 | QString contentType; // e.g. "application/pdf" | ||
| 107 | qint64 size = 0; // Decoded size in bytes | ||
| 108 | QString contentId; // Content-ID for inline attachments (without <>) | ||
| 109 | AttachmentPresentation presentation = AttachmentPresentation::Regular; | ||
| 110 | }; | ||
| 111 | |||
| 112 | // MailBody holds the content of a single email. | ||
| 113 | // textHtml contains decoded HTML if the message had a text/html part. | ||
| 114 | struct MailBody { | ||
| 115 | qint64 uid = 0; | ||
| 116 | QString textPlain; // Decoded text/plain body (UTF-8) | ||
| 117 | QString textHtml; // Decoded text/html body (UTF-8), empty if none | ||
| 118 | QByteArray rawSource; // Original raw MIME source (headers + body) | ||
| 119 | QList<Attachment> attachments; // Attachment metadata (no BLOB data) | ||
| 120 | quint32 cacheUidValidity = 0; // Captured with the cached display snapshot | ||
| 121 | QString cacheAccountId; | ||
| 122 | // P90.8: which variants this body actually carries. A mode projection can | ||
| 123 | // leave one variant legitimately empty ("not loaded") even though the | ||
| 124 | // store has it — the reader must then ask for a reprojection instead of | ||
| 125 | // treating the mail as html-less/plain-less. Full means both variants | ||
| 126 | // were read (or written), so emptiness is authoritative. Stays last so | ||
| 127 | // positional aggregate initialization keeps compiling. | ||
| 128 | enum class Projection { Full, TextOnly, HtmlOnly }; | ||
| 129 | Projection projection = Projection::Full; | ||
| 130 | // Digest of the decoded HTML variant, computed on the read/parse worker | ||
| 131 | // when it fits the native document budget. Empty means unavailable. | ||
| 132 | QByteArray htmlDigest; | ||
| 133 | // F100-56: the spam filter's header lines ("x-spam-status: Yes, score=…"), | ||
| 134 | // extracted on the display read worker (util/SpamHeaders.h). Empty = none. | ||
| 135 | QString spamHeaders; | ||
| 136 | }; | ||
| 137 | |||
| 138 | // Result of an IMAP STATUS command (MESSAGES, UNSEEN, RECENT counts). | ||
| 139 | struct StatusResult { | ||
| 140 | QString folderPath; | ||
| 141 | int messages = 0; | ||
| 142 | int unseen = 0; | ||
| 143 | int recent = 0; | ||
| 144 | // Local steady-clock origin of a successfully completed explicit STATUS. | ||
| 145 | // Zero identifies an unsolicited notification with no causal query boundary. | ||
| 146 | qint64 requestStartedNs = 0; | ||
| 147 | bool hasMessages = true; | ||
| 148 | bool hasUnseen = true; | ||
| 149 | }; | ||
| 150 | |||
| 151 | // Authoritative response to UID FETCH ... FLAGS. Unlike the legacy bitmask | ||
| 152 | // pair this retains IMAP keywords so durable label mutations can be verified. | ||
| 153 | struct ImapFlagSnapshot { | ||
| 154 | qint64 uid = 0; | ||
| 155 | quint32 flags = MailFlag::None; | ||
| 156 | QStringList keywords; | ||
| 157 | }; | ||
| 158 | |||
| 159 | // T-154: Contact entry for the local contact store. | ||
| 160 | // Used for autocomplete, @-mentions, and CardDAV sync. | ||
| 161 | struct Contact { | ||
| 162 | qint64 id = 0; // DB primary key | ||
| 163 | QString displayName; // "Max Mustermann" | ||
| 164 | QString email; // "max@example.com" | ||
| 165 | QString source; // "local" or "carddav" | ||
| 166 | QString cardDavUid; // CardDAV row identity (empty for local contacts) | ||
| 167 | QString cardDavEtag; // CardDAV ETag for sync | ||
| 168 | QString cardDavHref; // CardDAV resource URL for write-back (Q09) | ||
| 169 | QString cardDavVUid; // Original vCard UID property (write-back) | ||
| 170 | QString cardDavAccountId; // Owning DAV account (stable collection identity) | ||
| 171 | QString cardDavBookPath; // Owning address-book collection path | ||
| 172 | QDateTime lastUsed; // Last time used as recipient | ||
| 173 | int useCount = 0; // How often used as recipient | ||
| 174 | QDateTime createdAt; | ||
| 175 | QDateTime updatedAt; | ||
| 176 | }; | ||
| 177 |