Files
wino-mail-dtkqt/src/db/dao/mailitemdao.cpp
T
javier 4c4fec7a0a feat: responsive compact mail view + folder unread badges
- CompactMailDelegate: card-based view for narrow panels (<420px)
  * Avatar with hash-based color, sender + date (relative format)
  * Subject line with attachment icon
  * Action buttons: flag, delete, category, more (context menu)
  * Unread highlight with light blue background + bold sender
- MailListView: automatic switch between tree/table (wide) and list/cards (narrow)
  * QStackedWidget with QTreeView + QListView sharing same proxy model
  * resizeEvent threshold at 420px
  * Signals forwarded to MainMainWindow for actions
- FolderTreeDelegate: custom paint for folder tree
  * Unread count badge (blue pill) right-aligned on folder nodes
  * Account nodes bold, folder nodes normal weight
  * Proper indentation per depth level
- MailItemDao::countUnreadByFolderId() for real-time unread counts
- MainMainWindow handlers for compact actions (flag, delete, category, more menu)
2026-08-24 23:25:13 +02:00

413 lines
16 KiB
C++

#include <QSqlQuery>
#include <QSqlError>
#include "mailitemdao.h"
#include <QDebug>
#include <optional>
bool MailItemDao::insert(MailItem& item)
{
QSqlDatabase db = DatabaseManager::instance().database();
// IMAP UIDs and provider message IDs are stable across synchronizations.
// Update the existing row instead of creating duplicates on every sync.
if (!item.messageId().isEmpty() || item.uid() > 0) {
QSqlQuery existing(db);
if (!item.messageId().isEmpty()) {
existing.prepare("SELECT id FROM MailCopy WHERE messageId = :messageId LIMIT 1");
existing.bindValue(":messageId", item.messageId());
} else {
existing.prepare("SELECT id FROM MailCopy WHERE folderId = :folderId AND uid = :uid LIMIT 1");
existing.bindValue(":folderId", item.folderId());
existing.bindValue(":uid", item.uid());
}
if (existing.exec() && existing.next()) {
item.setId(existing.value(0).toLongLong());
return update(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)"
);
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId().isEmpty() ? QVariant() : QVariant(item.messageId()));
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
if (!query.exec()) {
qWarning() << "Failed to insert mail item:" << query.lastError().text();
return false;
}
item.setId(query.lastInsertId().toLongLong());
return true;
}
bool MailItemDao::upsert(MailItem& item)
{
return insert(item);
}
bool MailItemDao::update(const MailItem& item)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE MailCopy SET "
"folderId = :folderId, "
"messageId = :messageId, "
"subject = :subject, "
"sender = :sender, "
"recipient = :recipient, "
"date = :date, "
"read = :read, "
"flagged = :flagged, "
"hasAttachment = :hasAttachment, "
"size = :size, "
"fileId = :fileId, "
"uid = :uid, "
"bodyHtml = :bodyHtml, "
"toAddr = :toAddr, "
"ccAddr = :ccAddr, "
"bccAddr = :bccAddr "
"WHERE id = :id"
);
query.bindValue(":id", item.id());
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId().isEmpty() ? QVariant() : QVariant(item.messageId()));
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
if (!query.exec()) {
qWarning() << "Failed to update mail item:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::remove(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete mail item:" << query.lastError().text();
return false;
}
return true;
}
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.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find mail item by id:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
return item;
}
return std::nullopt;
}
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")) {
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
items.append(item);
}
return items;
}
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.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid)
{
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.bindValue(":folderId", folderId);
query.bindValue(":sinceUid", sinceUid);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
items.append(item);
}
return items;
}
std::optional<qint64> MailItemDao::maxUidForFolder(int folderId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get max uid for folder:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
if (query.isNull(0)) {
return std::nullopt;
}
return query.value(0).toLongLong();
}
return std::nullopt;
}
QVector<qint64> MailItemDao::getUidsForFolder(int folderId)
{
QVector<qint64> uids;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get uids for folder:" << query.lastError().text();
return uids;
}
while (query.next()) {
qint64 uid = query.value(0).toLongLong();
uids.append(uid);
}
return uids;
}
QVector<qint64> MailItemDao::uidsForFolder(int folderId)
{
return getUidsForFolder(folderId);
}
std::optional<MailItem> MailItemDao::findByUid(int folderId, qint64 uid)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id FROM MailCopy WHERE folderId = :folderId AND uid = :uid LIMIT 1");
query.bindValue(":folderId", folderId);
query.bindValue(":uid", uid);
if (!query.exec() || !query.next()) return std::nullopt;
return findById(query.value(0).toLongLong());
}
bool MailItemDao::removeByUid(int folderId, qint64 uid)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCopy WHERE folderId = :folderId AND uid = :uid");
query.bindValue(":folderId", folderId);
query.bindValue(":uid", uid);
if (!query.exec()) {
qWarning() << "Failed to delete mail item by UID:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::replaceAttachments(qint64 mailItemId,
const QVector<StoredAttachmentRecord>& attachments)
{
QSqlDatabase db = DatabaseManager::instance().database();
if (!db.transaction()) return false;
QSqlQuery remove(db);
remove.prepare("DELETE FROM Attachment WHERE mailCopyId = :mailCopyId");
remove.bindValue(":mailCopyId", mailItemId);
if (!remove.exec()) { db.rollback(); return false; }
QSqlQuery insert(db);
insert.prepare("INSERT INTO Attachment (mailCopyId, filename, mimeType, size, contentId, storedPath) "
"VALUES (:mailCopyId, :filename, :mimeType, :size, :contentId, :storedPath)");
for (const StoredAttachmentRecord &attachment : attachments) {
insert.bindValue(":mailCopyId", mailItemId);
insert.bindValue(":filename", attachment.fileName);
insert.bindValue(":mimeType", attachment.mimeType);
insert.bindValue(":size", attachment.size);
insert.bindValue(":contentId", attachment.contentId);
insert.bindValue(":storedPath", attachment.storedPath);
if (!insert.exec()) { db.rollback(); return false; }
}
return db.commit();
}
QVector<StoredAttachmentRecord> MailItemDao::attachmentsForMail(qint64 mailItemId)
{
QVector<StoredAttachmentRecord> result;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT filename, mimeType, size, contentId, storedPath "
"FROM Attachment WHERE mailCopyId = :mailCopyId ORDER BY id");
query.bindValue(":mailCopyId", mailItemId);
if (!query.exec()) return result;
while (query.next()) {
StoredAttachmentRecord attachment;
attachment.fileName = query.value(0).toString();
attachment.mimeType = query.value(1).toString();
attachment.size = query.value(2).toLongLong();
attachment.contentId = query.value(3).toString();
attachment.storedPath = query.value(4).toString();
result.append(attachment);
}
return result;
}
int MailItemDao::countUnreadByFolderId(int folderId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT COUNT(*) FROM MailCopy WHERE folderId = :folderId AND read = 0");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to count unread mails:" << query.lastError().text();
return 0;
}
if (query.next()) {
return query.value(0).toInt();
}
return 0;
}