feat: EmailTreeModel grouping modes + MailListView integration

- EmailTreeModel: 4 grouping modes
  * NoGrouping - flat list
  * GroupByDate - chronological groups (Hoy, Ayer, Esta semana...)
  * GroupByThread - conversations with expandable threads (ThreadId from References/In-Reply-To/Message-ID)
  * GroupBySender - groups by sender email/name

- EmailTreeModel thread support:
  * ThreadItem type with expand/collapse
  * ThreadId extraction via ThreadUtils (References > In-Reply-To > Message-ID)
  * Double-click thread node toggles expansion
  * Threads sorted by newest email first, emails within thread oldest first

- EmailTreeModel sender grouping:
  * Groups by sender name/email
  * Senders sorted by newest email first
  * Emails within group newest first

- MailListView: Added 'Agrupar por remitente' to group-by combo
- MailListView: Double-click thread node toggles expansion (not open mail)
- ThreadUtils: parseReferencesHeader, extractThreadId, generateThreadId
This commit is contained in:
2026-08-25 18:53:51 +02:00
parent fa699b5399
commit 6e6fdf9c21
75 changed files with 25631 additions and 12574 deletions
+34 -7
View File
@@ -28,8 +28,8 @@ bool MailItemDao::insert(MailItem& item)
QSqlQuery query(db);
query.prepare(
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr)"
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr, threadId, inReplyTo, \"references\", isPinned) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr, :threadId, :inReplyTo, :references, :isPinned)"
);
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId().isEmpty() ? QVariant() : QVariant(item.messageId()));
@@ -47,6 +47,10 @@ bool MailItemDao::insert(MailItem& item)
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
query.bindValue(":threadId", item.threadId().isEmpty() ? QVariant() : QVariant(item.threadId()));
query.bindValue(":inReplyTo", item.inReplyTo().isEmpty() ? QVariant() : QVariant(item.inReplyTo()));
query.bindValue(":references", item.references().join(" "));
query.bindValue(":isPinned", item.isPinned() ? 1 : 0);
if (!query.exec()) {
qWarning() << "Failed to insert mail item:" << query.lastError().text();
@@ -82,7 +86,11 @@ bool MailItemDao::update(const MailItem& item)
"bodyHtml = :bodyHtml, "
"toAddr = :toAddr, "
"ccAddr = :ccAddr, "
"bccAddr = :bccAddr "
"bccAddr = :bccAddr, "
"threadId = :threadId, "
"inReplyTo = :inReplyTo, "
"\"references\" = :references, "
"isPinned = :isPinned "
"WHERE id = :id"
);
query.bindValue(":id", item.id());
@@ -102,6 +110,10 @@ bool MailItemDao::update(const MailItem& item)
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
query.bindValue(":threadId", item.threadId().isEmpty() ? QVariant() : QVariant(item.threadId()));
query.bindValue(":inReplyTo", item.inReplyTo().isEmpty() ? QVariant() : QVariant(item.inReplyTo()));
query.bindValue(":references", item.references().join(" "));
query.bindValue(":isPinned", item.isPinned() ? 1 : 0);
if (!query.exec()) {
qWarning() << "Failed to update mail item:" << query.lastError().text();
@@ -128,7 +140,7 @@ std::optional<MailItem> MailItemDao::findById(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE id = :id");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr, threadId, inReplyTo, \"references\", isPinned FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
@@ -154,6 +166,11 @@ std::optional<MailItem> MailItemDao::findById(qint64 id)
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
item.setThreadId(query.value(17).toString());
item.setInReplyTo(query.value(18).toString());
QString refs = query.value(19).toString();
if (!refs.isEmpty()) item.setReferences(refs.split(" ", Qt::SkipEmptyParts));
item.setPinned(query.value(20).toBool());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
@@ -167,7 +184,7 @@ QVector<MailItem> MailItemDao::findAll()
QVector<MailItem> items;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy")) {
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr, threadId, inReplyTo, \"references\", isPinned FROM MailCopy")) {
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
return items;
}
@@ -190,6 +207,11 @@ QVector<MailItem> MailItemDao::findAll()
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
item.setThreadId(query.value(17).toString());
item.setInReplyTo(query.value(18).toString());
QString refs = query.value(19).toString();
if (!refs.isEmpty()) item.setReferences(refs.split(" ", Qt::SkipEmptyParts));
item.setPinned(query.value(20).toBool());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
@@ -203,7 +225,7 @@ QVector<MailItem> MailItemDao::findByFolderId(int folderId)
QVector<MailItem> items;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr, threadId, inReplyTo, \"references\", isPinned FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
@@ -229,6 +251,11 @@ QVector<MailItem> MailItemDao::findByFolderId(int folderId)
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
item.setThreadId(query.value(17).toString());
item.setInReplyTo(query.value(18).toString());
QString refs = query.value(19).toString();
if (!refs.isEmpty()) item.setReferences(refs.split(" ", Qt::SkipEmptyParts));
item.setPinned(query.value(20).toBool());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
@@ -242,7 +269,7 @@ QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 since
QVector<MailItem> items;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr, threadId, inReplyTo, \"references\", isPinned FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid");
query.bindValue(":folderId", folderId);
query.bindValue(":sinceUid", sinceUid);
+4
View File
@@ -124,6 +124,10 @@ bool DatabaseManager::initialize(const QString& databasePath)
"toAddr TEXT, "
"ccAddr TEXT, "
"bccAddr TEXT, "
"threadId TEXT, "
"inReplyTo TEXT, "
"\"references\" TEXT, "
"isPinned BOOLEAN DEFAULT 0, "
"FOREIGN KEY(folderId) REFERENCES Folder(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create MailCopy table:" << query.lastError().text();