feat: comprehensive Wino Mail updates

- ReaderView: complete rewrite with CSS styling, headers, attachments, zoom, find, dark mode, image blocking
- Categories: DAO + UI tree widget with colors, nested categories, assignment
- Rules engine: DAO + evaluation + actions (move, mark read, flag, delete, category, forward)
- Templates: DAO + editor + manager with variables support
- Signatures: DAO + manager + auto-per-account + manual selector in compose
- ComposeView: signature combo, template selector, auto-signature on new email
- MainWindow: frameless with rounded corners, 1px border, resize from edges, no status bar
- Database: new tables (Category, MailCategory, Rule, Template, Signature) with indexes
This commit is contained in:
2026-08-23 14:49:58 +02:00
parent 5fcedfa129
commit 0bb8738607
7399 changed files with 44711 additions and 209 deletions
+254
View File
@@ -0,0 +1,254 @@
#include "categorydao.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
#include <QDateTime>
bool CategoryDao::insert(Category& cat)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Category (accountId, name, color, parentCategoryId, sortOrder, createdAt) "
"VALUES (:accountId, :name, :color, :parentCategoryId, :sortOrder, :createdAt)"
);
query.bindValue(":accountId", cat.accountId >= 0 ? cat.accountId : QVariant());
query.bindValue(":name", cat.name);
query.bindValue(":color", cat.color.name());
query.bindValue(":parentCategoryId", cat.parentCategoryId >= 0 ? cat.parentCategoryId : QVariant());
query.bindValue(":sortOrder", cat.sortOrder);
query.bindValue(":createdAt", QDateTime::currentDateTime());
if (!query.exec()) {
qWarning() << "Failed to insert category:" << query.lastError().text();
return false;
}
cat.id = query.lastInsertId().toLongLong();
return true;
}
bool CategoryDao::update(const Category& cat)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Category SET "
"accountId = :accountId, "
"name = :name, "
"color = :color, "
"parentCategoryId = :parentCategoryId, "
"sortOrder = :sortOrder "
"WHERE id = :id"
);
query.bindValue(":id", cat.id);
query.bindValue(":accountId", cat.accountId >= 0 ? cat.accountId : QVariant());
query.bindValue(":name", cat.name);
query.bindValue(":color", cat.color.name()); // Use default hex format
query.bindValue(":parentCategoryId", cat.parentCategoryId >= 0 ? cat.parentCategoryId : QVariant());
query.bindValue(":sortOrder", cat.sortOrder);
if (!query.exec()) {
qWarning() << "Failed to update category:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
bool CategoryDao::remove(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Category WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete category:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
std::optional<Category> CategoryDao::findById(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec() || !query.next()) {
return std::nullopt;
}
Category cat;
cat.id = query.value(0).toLongLong();
cat.accountId = query.value(1).toLongLong();
cat.name = query.value(2).toString();
cat.color = QColor(query.value(3).toString());
cat.parentCategoryId = query.value(4).toLongLong();
cat.sortOrder = query.value(5).toInt();
cat.createdAt = query.value(6).toDateTime();
return cat;
}
QVector<Category> CategoryDao::findByAccount(qint64 accountId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (accountId >= 0) {
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE accountId = :accountId OR accountId IS NULL ORDER BY sortOrder, name");
query.bindValue(":accountId", accountId);
} else {
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE accountId IS NULL ORDER BY sortOrder, name");
}
QVector<Category> cats;
if (query.exec()) {
while (query.next()) {
Category cat;
cat.id = query.value(0).toLongLong();
cat.accountId = query.value(1).toLongLong();
cat.name = query.value(2).toString();
cat.color = QColor(query.value(3).toString());
cat.parentCategoryId = query.value(4).toLongLong();
cat.sortOrder = query.value(5).toInt();
cat.createdAt = query.value(6).toDateTime();
cats.append(cat);
}
}
return cats;
}
QVector<Category> CategoryDao::findGlobal()
{
return findByAccount(-1);
}
QVector<Category> CategoryDao::findAll()
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category ORDER BY accountId, sortOrder, name");
QVector<Category> cats;
if (query.exec()) {
while (query.next()) {
Category cat;
cat.id = query.value(0).toLongLong();
cat.accountId = query.value(1).toLongLong();
cat.name = query.value(2).toString();
cat.color = QColor(query.value(3).toString());
cat.parentCategoryId = query.value(4).toLongLong();
cat.sortOrder = query.value(5).toInt();
cat.createdAt = query.value(6).toDateTime();
cats.append(cat);
}
}
return cats;
}
QVector<Category> CategoryDao::findChildren(qint64 parentId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE parentCategoryId = :parentId ORDER BY sortOrder, name");
query.bindValue(":parentId", parentId);
QVector<Category> cats;
if (query.exec()) {
while (query.next()) {
Category cat;
cat.id = query.value(0).toLongLong();
cat.accountId = query.value(1).toLongLong();
cat.name = query.value(2).toString();
cat.color = QColor(query.value(3).toString());
cat.parentCategoryId = query.value(4).toLongLong();
cat.sortOrder = query.value(5).toInt();
cat.createdAt = query.value(6).toDateTime();
cats.append(cat);
}
}
return cats;
}
bool CategoryDao::assignToMail(qint64 mailCopyId, qint64 categoryId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT OR IGNORE INTO MailCategory (mailCopyId, categoryId, assignedAt) "
"VALUES (:mailCopyId, :categoryId, :assignedAt)"
);
query.bindValue(":mailCopyId", mailCopyId);
query.bindValue(":categoryId", categoryId);
query.bindValue(":assignedAt", QDateTime::currentDateTime());
if (!query.exec()) {
qWarning() << "Failed to assign category to mail:" << query.lastError().text();
return false;
}
return true;
}
bool CategoryDao::removeFromMail(qint64 mailCopyId, qint64 categoryId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCategory WHERE mailCopyId = :mailCopyId AND categoryId = :categoryId");
query.bindValue(":mailCopyId", mailCopyId);
query.bindValue(":categoryId", categoryId);
if (!query.exec()) {
qWarning() << "Failed to remove category from mail:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
QVector<qint64> CategoryDao::categoriesForMail(qint64 mailCopyId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT categoryId FROM MailCategory WHERE mailCopyId = :mailCopyId");
query.bindValue(":mailCopyId", mailCopyId);
QVector<qint64> ids;
if (query.exec()) {
while (query.next()) {
ids.append(query.value(0).toLongLong());
}
}
return ids;
}
QVector<Category> CategoryDao::fullCategoriesForMail(qint64 mailCopyId)
{
QVector<qint64> ids = categoriesForMail(mailCopyId);
QVector<Category> cats;
for (qint64 id : ids) {
if (auto cat = findById(id)) {
cats.append(*cat);
}
}
return cats;
}
bool CategoryDao::reorder(const QVector<qint64>& idsInOrder)
{
QSqlDatabase db = DatabaseManager::instance().database();
db.transaction();
QSqlQuery query(db);
query.prepare("UPDATE Category SET sortOrder = :order WHERE id = :id");
for (int i = 0; i < idsInOrder.size(); ++i) {
query.bindValue(":id", idsInOrder[i]);
query.bindValue(":order", i);
if (!query.exec()) {
db.rollback();
qWarning() << "Failed to reorder category:" << query.lastError().text();
return false;
}
}
return db.commit();
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "../databasemanager.h"
#include <QVector>
#include <optional>
#include <QColor>
#include <QDateTime>
struct Category
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QColor color{QColor("#1976D2")};
qint64 parentCategoryId{-1};
int sortOrder{0};
QDateTime createdAt;
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
};
class CategoryDao
{
public:
static bool insert(Category& cat);
static bool update(const Category& cat);
static bool remove(qint64 id);
static std::optional<Category> findById(qint64 id);
static QVector<Category> findByAccount(qint64 accountId); // accountId = -1 for global
static QVector<Category> findGlobal();
static QVector<Category> findAll();
static QVector<Category> findChildren(qint64 parentId);
static bool assignToMail(qint64 mailCopyId, qint64 categoryId);
static bool removeFromMail(qint64 mailCopyId, qint64 categoryId);
static QVector<qint64> categoriesForMail(qint64 mailCopyId);
static QVector<Category> fullCategoriesForMail(qint64 mailCopyId);
static bool reorder(const QVector<qint64>& idsInOrder);
};
+352
View File
@@ -0,0 +1,352 @@
#include "ruledao.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
bool RuleCondition::match(const QString& text) const
{
QString haystack = caseSensitive ? text : text.toLower();
QString needle = caseSensitive ? value : value.toLower();
switch (op) {
case Contains: return haystack.contains(needle);
case NotContains: return !haystack.contains(needle);
case Equals: return haystack == needle;
case NotEquals: return haystack != needle;
case StartsWith: return haystack.startsWith(needle);
case EndsWith: return haystack.endsWith(needle);
case Regex: return QRegularExpression(value, caseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption).match(text).hasMatch();
default: return false;
}
}
QJsonObject RuleCondition::toJson() const
{
QJsonObject obj;
obj["field"] = static_cast<int>(field);
obj["op"] = static_cast<int>(op);
obj["value"] = value;
obj["caseSensitive"] = caseSensitive;
return obj;
}
RuleCondition RuleCondition::fromJson(const QJsonObject& obj)
{
RuleCondition cond;
cond.field = static_cast<Field>(obj["field"].toInt());
cond.op = static_cast<Operator>(obj["op"].toInt());
cond.value = obj["value"].toString();
cond.caseSensitive = obj["caseSensitive"].toBool();
return cond;
}
QJsonObject RuleAction::toJson() const
{
QJsonObject obj;
obj["type"] = static_cast<int>(type);
obj["value"] = value;
return obj;
}
RuleAction RuleAction::fromJson(const QJsonObject& obj)
{
RuleAction act;
act.type = static_cast<Type>(obj["type"].toInt());
act.value = obj["value"].toString();
return act;
}
QJsonObject Rule::toJson() const
{
QJsonObject obj;
obj["id"] = static_cast<qint64>(id);
obj["accountId"] = accountId >= 0 ? static_cast<qint64>(accountId) : QJsonValue::Null;
obj["name"] = name;
obj["description"] = description;
obj["enabled"] = enabled;
obj["priority"] = priority;
obj["matchAll"] = matchAll;
QJsonArray condArray;
for (const auto& c : conditions) condArray.append(c.toJson());
obj["conditions"] = condArray;
QJsonArray actArray;
for (const auto& a : actions) actArray.append(a.toJson());
obj["actions"] = actArray;
obj["createdAt"] = createdAt.toString(Qt::ISODate);
obj["updatedAt"] = updatedAt.toString(Qt::ISODate);
obj["lastRun"] = lastRun.isValid() ? QJsonValue(lastRun.toString(Qt::ISODate)) : QJsonValue::Null;
obj["runCount"] = runCount;
return obj;
}
Rule Rule::fromJson(const QJsonObject& obj)
{
Rule rule;
rule.id = obj["id"].toVariant().toLongLong();
rule.accountId = obj["accountId"].isNull() ? -1 : obj["accountId"].toVariant().toLongLong();
rule.name = obj["name"].toString();
rule.description = obj["description"].toString();
rule.enabled = obj["enabled"].toBool();
rule.priority = obj["priority"].toInt();
rule.matchAll = obj["matchAll"].toBool();
QJsonArray condArray = obj["conditions"].toArray();
for (const auto& v : condArray) rule.conditions.append(RuleCondition::fromJson(v.toObject()));
QJsonArray actArray = obj["actions"].toArray();
for (const auto& v : actArray) rule.actions.append(RuleAction::fromJson(v.toObject()));
rule.createdAt = QDateTime::fromString(obj["createdAt"].toString(), Qt::ISODate);
rule.updatedAt = QDateTime::fromString(obj["updatedAt"].toString(), Qt::ISODate);
rule.lastRun = obj["lastRun"].isNull() ? QDateTime() : QDateTime::fromString(obj["lastRun"].toString(), Qt::ISODate);
rule.runCount = obj["runCount"].toVariant().toLongLong();
return rule;
}
bool RuleDao::insert(Rule& rule)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Rule (accountId, name, description, enabled, priority, matchAll, conditions, actions, createdAt, updatedAt, lastRun, runCount) "
"VALUES (:accountId, :name, :description, :enabled, :priority, :matchAll, :conditions, :actions, :createdAt, :updatedAt, :lastRun, :runCount)"
);
query.bindValue(":accountId", rule.accountId >= 0 ? rule.accountId : QVariant());
query.bindValue(":name", rule.name);
query.bindValue(":description", rule.description);
query.bindValue(":enabled", rule.enabled ? 1 : 0);
query.bindValue(":priority", rule.priority);
query.bindValue(":matchAll", rule.matchAll ? 1 : 0);
QJsonObject json = rule.toJson();
QJsonArray condArray = json["conditions"].toArray();
QJsonArray actArray = json["actions"].toArray();
QJsonDocument condDoc(condArray);
QJsonDocument actDoc(actArray);
query.bindValue(":conditions", condDoc.toJson(QJsonDocument::Compact));
query.bindValue(":actions", actDoc.toJson(QJsonDocument::Compact));
query.bindValue(":createdAt", QDateTime::currentDateTime());
query.bindValue(":updatedAt", QDateTime::currentDateTime());
query.bindValue(":lastRun", QVariant());
query.bindValue(":runCount", 0);
if (!query.exec()) {
qWarning() << "Failed to insert rule:" << query.lastError().text();
return false;
}
rule.id = query.lastInsertId().toLongLong();
return true;
}
bool RuleDao::update(const Rule& rule)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Rule SET "
"accountId = :accountId, "
"name = :name, "
"description = :description, "
"enabled = :enabled, "
"priority = :priority, "
"matchAll = :matchAll, "
"conditions = :conditions, "
"actions = :actions, "
"updatedAt = :updatedAt "
"WHERE id = :id"
);
query.bindValue(":id", rule.id);
query.bindValue(":accountId", rule.accountId >= 0 ? rule.accountId : QVariant());
query.bindValue(":name", rule.name);
query.bindValue(":description", rule.description);
query.bindValue(":enabled", rule.enabled ? 1 : 0);
query.bindValue(":priority", rule.priority);
query.bindValue(":matchAll", rule.matchAll ? 1 : 0);
QJsonObject json = rule.toJson();
QJsonArray condArray = json["conditions"].toArray();
QJsonArray actArray = json["actions"].toArray();
QJsonDocument condDoc(condArray);
QJsonDocument actDoc(actArray);
query.bindValue(":conditions", condDoc.toJson(QJsonDocument::Compact));
query.bindValue(":actions", actDoc.toJson(QJsonDocument::Compact));
query.bindValue(":updatedAt", QDateTime::currentDateTime());
if (!query.exec()) {
qWarning() << "Failed to update rule:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
bool RuleDao::remove(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Rule WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete rule:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
std::optional<Rule> RuleDao::findById(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, description, enabled, priority, matchAll, conditions, actions, createdAt, updatedAt, lastRun, runCount FROM Rule WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec() || !query.next()) {
return std::nullopt;
}
Rule rule;
rule.id = query.value(0).toLongLong();
rule.accountId = query.value(1).isNull() ? -1 : query.value(1).toLongLong();
rule.name = query.value(2).toString();
rule.description = query.value(3).toString();
rule.enabled = query.value(4).toBool();
rule.priority = query.value(5).toInt();
rule.matchAll = query.value(6).toBool();
QJsonDocument condDoc = QJsonDocument::fromJson(query.value(7).toByteArray());
QJsonArray condArray = condDoc.array();
for (const auto& v : condArray) rule.conditions.append(RuleCondition::fromJson(v.toObject()));
QJsonDocument actDoc = QJsonDocument::fromJson(query.value(8).toByteArray());
QJsonArray actArray = actDoc.array();
for (const auto& v : actArray) rule.actions.append(RuleAction::fromJson(v.toObject()));
rule.createdAt = QDateTime::fromString(query.value(9).toString(), Qt::ISODate);
rule.updatedAt = QDateTime::fromString(query.value(10).toString(), Qt::ISODate);
rule.lastRun = query.value(11).isNull() ? QDateTime() : QDateTime::fromString(query.value(11).toString(), Qt::ISODate);
rule.runCount = query.value(12).toLongLong();
return rule;
}
QVector<Rule> RuleDao::findByAccount(qint64 accountId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (accountId >= 0) {
query.prepare("SELECT id, accountId, name, description, enabled, priority, matchAll, conditions, actions, createdAt, updatedAt, lastRun, runCount FROM Rule WHERE accountId = :accountId OR accountId IS NULL ORDER BY priority, name");
query.bindValue(":accountId", accountId);
} else {
query.prepare("SELECT id, accountId, name, description, enabled, priority, matchAll, conditions, actions, createdAt, updatedAt, lastRun, runCount FROM Rule WHERE accountId IS NULL ORDER BY priority, name");
}
QVector<Rule> rules;
if (query.exec()) {
while (query.next()) {
Rule rule;
rule.id = query.value(0).toLongLong();
rule.accountId = query.value(1).isNull() ? -1 : query.value(1).toLongLong();
rule.name = query.value(2).toString();
rule.description = query.value(3).toString();
rule.enabled = query.value(4).toBool();
rule.priority = query.value(5).toInt();
rule.matchAll = query.value(6).toBool();
QJsonDocument condDoc = QJsonDocument::fromJson(query.value(7).toByteArray());
QJsonArray condArray = condDoc.array();
for (const auto& v : condArray) rule.conditions.append(RuleCondition::fromJson(v.toObject()));
QJsonDocument actDoc = QJsonDocument::fromJson(query.value(8).toByteArray());
QJsonArray actArray = actDoc.array();
for (const auto& v : actArray) rule.actions.append(RuleAction::fromJson(v.toObject()));
rule.createdAt = QDateTime::fromString(query.value(9).toString(), Qt::ISODate);
rule.updatedAt = QDateTime::fromString(query.value(10).toString(), Qt::ISODate);
rule.lastRun = query.value(11).isNull() ? QDateTime() : QDateTime::fromString(query.value(11).toString(), Qt::ISODate);
rule.runCount = query.value(12).toLongLong();
rules.append(rule);
}
}
return rules;
}
QVector<Rule> RuleDao::findGlobal()
{
return findByAccount(-1);
}
QVector<Rule> RuleDao::findAllEnabled()
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, description, enabled, priority, matchAll, conditions, actions, createdAt, updatedAt, lastRun, runCount FROM Rule WHERE enabled = 1 ORDER BY priority, name");
QVector<Rule> rules;
if (query.exec()) {
while (query.next()) {
Rule rule;
rule.id = query.value(0).toLongLong();
rule.accountId = query.value(1).isNull() ? -1 : query.value(1).toLongLong();
rule.name = query.value(2).toString();
rule.description = query.value(3).toString();
rule.enabled = query.value(4).toBool();
rule.priority = query.value(5).toInt();
rule.matchAll = query.value(6).toBool();
QJsonDocument condDoc = QJsonDocument::fromJson(query.value(7).toByteArray());
QJsonArray condArray = condDoc.array();
for (const auto& v : condArray) rule.conditions.append(RuleCondition::fromJson(v.toObject()));
QJsonDocument actDoc = QJsonDocument::fromJson(query.value(8).toByteArray());
QJsonArray actArray = actDoc.array();
for (const auto& v : actArray) rule.actions.append(RuleAction::fromJson(v.toObject()));
rule.createdAt = QDateTime::fromString(query.value(9).toString(), Qt::ISODate);
rule.updatedAt = QDateTime::fromString(query.value(10).toString(), Qt::ISODate);
rule.lastRun = query.value(11).isNull() ? QDateTime() : QDateTime::fromString(query.value(11).toString(), Qt::ISODate);
rule.runCount = query.value(12).toLongLong();
rules.append(rule);
}
}
return rules;
}
bool RuleDao::setEnabled(qint64 id, bool enabled)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("UPDATE Rule SET enabled = :enabled, updatedAt = :updatedAt WHERE id = :id");
query.bindValue(":id", id);
query.bindValue(":enabled", enabled ? 1 : 0);
query.bindValue(":updatedAt", QDateTime::currentDateTime());
if (!query.exec()) {
qWarning() << "Failed to set rule enabled:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
bool RuleDao::updateLastRun(qint64 id, const QDateTime& when, qint64 runCount)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("UPDATE Rule SET lastRun = :lastRun, runCount = :runCount WHERE id = :id");
query.bindValue(":id", id);
query.bindValue(":lastRun", when);
query.bindValue(":runCount", runCount);
if (!query.exec()) {
qWarning() << "Failed to update rule last run:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include "../databasemanager.h"
#include <QVector>
#include <optional>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
struct RuleCondition
{
enum Field { From, To, Cc, Subject, Body, HasAttachment, Size, Date, Flagged, Read };
enum Operator { Contains, NotContains, Equals, NotEquals, StartsWith, EndsWith, Regex, GreaterThan, LessThan, Before, After };
Field field{From};
Operator op{Contains};
QString value;
bool caseSensitive{false};
bool match(const QString& text) const;
QJsonObject toJson() const;
static RuleCondition fromJson(const QJsonObject& obj);
};
struct RuleAction
{
enum Type {
MoveToFolder, // value = folderId
MarkAsRead, // value = "true"/"false"
MarkAsFlagged, // value = "true"/"false"
Delete, // no value
AssignCategory, // value = categoryId
RemoveCategory, // value = categoryId
ForwardTo, // value = email address
ReplyWithTemplate, // value = templateId
SetPriority, // value = "high"/"normal"/"low"
StopProcessing // no value (stop evaluating more rules)
};
Type type{MoveToFolder};
QString value;
QJsonObject toJson() const;
static RuleAction fromJson(const QJsonObject& obj);
};
struct Rule
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QString description;
bool enabled{true};
int priority{100}; // lower = higher priority
bool matchAll{true}; // true = AND, false = OR
QVector<RuleCondition> conditions;
QVector<RuleAction> actions;
QDateTime createdAt;
QDateTime updatedAt;
QDateTime lastRun;
qint64 runCount{0};
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
QJsonObject toJson() const;
static Rule fromJson(const QJsonObject& obj);
};
class RuleDao
{
public:
static bool insert(Rule& rule);
static bool update(const Rule& rule);
static bool remove(qint64 id);
static std::optional<Rule> findById(qint64 id);
static QVector<Rule> findByAccount(qint64 accountId); // accountId = -1 for global
static QVector<Rule> findGlobal();
static QVector<Rule> findAllEnabled();
static bool setEnabled(qint64 id, bool enabled);
static bool updateLastRun(qint64 id, const QDateTime& when, qint64 runCount);
};
+387
View File
@@ -0,0 +1,387 @@
#include "templatedao.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
bool TemplateDao::insert(Template& tmpl)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Template (accountId, name, subject, bodyHtml, bodyText, variables, isDefault, createdAt, updatedAt) "
"VALUES (:accountId, :name, :subject, :bodyHtml, :bodyText, :variables, :isDefault, :createdAt, :updatedAt)"
);
query.bindValue(":accountId", tmpl.accountId >= 0 ? tmpl.accountId : QVariant());
query.bindValue(":name", tmpl.name);
query.bindValue(":subject", tmpl.subject);
query.bindValue(":bodyHtml", tmpl.bodyHtml);
query.bindValue(":bodyText", tmpl.bodyText);
QJsonArray varsArray;
for (const TemplateVariable& var : tmpl.variables) {
QJsonObject varObj;
varObj["name"] = var.name;
varObj["label"] = var.label;
varObj["defaultValue"] = var.defaultValue;
varObj["type"] = var.type;
varObj["options"] = QJsonArray::fromStringList(var.options);
varsArray.append(varObj);
}
QJsonDocument varsDoc(varsArray);
query.bindValue(":variables", varsDoc.toJson(QJsonDocument::Compact));
query.bindValue(":isDefault", tmpl.isDefault ? 1 : 0);
query.bindValue(":createdAt", QDateTime::currentDateTime());
query.bindValue(":updatedAt", QDateTime::currentDateTime());
if (!query.exec()) {
qWarning() << "Failed to insert template:" << query.lastError().text();
return false;
}
tmpl.id = query.lastInsertId().toLongLong();
return true;
}
bool TemplateDao::update(const Template& tmpl)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Template SET "
"accountId = :accountId, "
"name = :name, "
"subject = :subject, "
"bodyHtml = :bodyHtml, "
"bodyText = :bodyText, "
"variables = :variables, "
"isDefault = :isDefault, "
"updatedAt = :updatedAt "
"WHERE id = :id"
);
query.bindValue(":id", tmpl.id);
query.bindValue(":accountId", tmpl.accountId >= 0 ? tmpl.accountId : QVariant());
query.bindValue(":name", tmpl.name);
query.bindValue(":subject", tmpl.subject);
query.bindValue(":bodyHtml", tmpl.bodyHtml);
query.bindValue(":bodyText", tmpl.bodyText);
QJsonArray varsArray;
for (const TemplateVariable& var : tmpl.variables) {
QJsonObject varObj;
varObj["name"] = var.name;
varObj["label"] = var.label;
varObj["defaultValue"] = var.defaultValue;
varObj["type"] = var.type;
varObj["options"] = QJsonArray::fromStringList(var.options);
varsArray.append(varObj);
}
QJsonDocument varsDoc(varsArray);
query.bindValue(":variables", varsDoc.toJson(QJsonDocument::Compact));
query.bindValue(":isDefault", tmpl.isDefault ? 1 : 0);
query.bindValue(":updatedAt", QDateTime::currentDateTime());
if (!query.exec()) {
qWarning() << "Failed to update template:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
bool TemplateDao::remove(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Template WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete template:" << query.lastError().text();
return false;
}
return query.numRowsAffected() > 0;
}
std::optional<Template> TemplateDao::findById(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, subject, bodyHtml, bodyText, variables, isDefault, createdAt, updatedAt FROM Template WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec() || !query.next()) {
return std::nullopt;
}
Template tmpl;
tmpl.id = query.value(0).toLongLong();
tmpl.accountId = query.value(1).isNull() ? -1 : query.value(1).toLongLong();
tmpl.name = query.value(2).toString();
tmpl.subject = query.value(3).toString();
tmpl.bodyHtml = query.value(4).toString();
tmpl.bodyText = query.value(5).toString();
QJsonDocument varsDoc = QJsonDocument::fromJson(query.value(6).toByteArray());
QJsonArray varsArray = varsDoc.array();
for (const auto& v : varsArray) {
QJsonObject obj = v.toObject();
TemplateVariable var;
var.name = obj["name"].toString();
var.label = obj["label"].toString();
var.defaultValue = obj["defaultValue"].toString();
var.type = obj["type"].toString();
QJsonArray optsArray = obj["options"].toArray();
for (const auto& opt : optsArray) {
var.options.append(opt.toString());
}
tmpl.variables.append(var);
}
tmpl.isDefault = query.value(7).toBool();
tmpl.createdAt = QDateTime::fromString(query.value(8).toString(), Qt::ISODate);
tmpl.updatedAt = QDateTime::fromString(query.value(9).toString(), Qt::ISODate);
return tmpl;
}
QVector<Template> TemplateDao::findByAccount(qint64 accountId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (accountId >= 0) {
query.prepare("SELECT id, accountId, name, subject, bodyHtml, bodyText, variables, isDefault, createdAt, updatedAt FROM Template WHERE accountId = :accountId OR accountId IS NULL ORDER BY isDefault DESC, name");
query.bindValue(":accountId", accountId);
} else {
query.prepare("SELECT id, accountId, name, subject, bodyHtml, bodyText, variables, isDefault, createdAt, updatedAt FROM Template WHERE accountId IS NULL ORDER BY isDefault DESC, name");
}
QVector<Template> templates;
if (query.exec()) {
while (query.next()) {
Template tmpl;
tmpl.id = query.value(0).toLongLong();
tmpl.accountId = query.value(1).isNull() ? -1 : query.value(1).toLongLong();
tmpl.name = query.value(2).toString();
tmpl.subject = query.value(3).toString();
tmpl.bodyHtml = query.value(4).toString();
tmpl.bodyText = query.value(5).toString();
QJsonDocument varsDoc = QJsonDocument::fromJson(query.value(6).toByteArray());
QJsonArray varsArray = varsDoc.array();
for (const auto& v : varsArray) {
QJsonObject obj = v.toObject();
TemplateVariable var;
var.name = obj["name"].toString();
var.label = obj["label"].toString();
var.defaultValue = obj["defaultValue"].toString();
var.type = obj["type"].toString();
QJsonArray optsArray = obj["options"].toArray();
for (const auto& opt : optsArray) {
var.options.append(opt.toString());
}
tmpl.variables.append(var);
}
tmpl.isDefault = query.value(7).toBool();
tmpl.createdAt = QDateTime::fromString(query.value(8).toString(), Qt::ISODate);
tmpl.updatedAt = QDateTime::fromString(query.value(9).toString(), Qt::ISODate);
templates.append(tmpl);
}
}
return templates;
}
QVector<Template> TemplateDao::findGlobal()
{
return findByAccount(-1);
}
QVector<Template> TemplateDao::findAll()
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, subject, bodyHtml, bodyText, variables, isDefault, createdAt, updatedAt FROM Template ORDER BY accountId, isDefault DESC, name");
QVector<Template> templates;
if (query.exec()) {
while (query.next()) {
Template tmpl;
tmpl.id = query.value(0).toLongLong();
tmpl.accountId = query.value(1).isNull() ? -1 : query.value(1).toLongLong();
tmpl.name = query.value(2).toString();
tmpl.subject = query.value(3).toString();
tmpl.bodyHtml = query.value(4).toString();
tmpl.bodyText = query.value(5).toString();
QJsonDocument varsDoc = QJsonDocument::fromJson(query.value(6).toByteArray());
QJsonArray varsArray = varsDoc.array();
for (const auto& v : varsArray) {
QJsonObject obj = v.toObject();
TemplateVariable var;
var.name = obj["name"].toString();
var.label = obj["label"].toString();
var.defaultValue = obj["defaultValue"].toString();
var.type = obj["type"].toString();
QJsonArray optsArray = obj["options"].toArray();
for (const auto& opt : optsArray) {
var.options.append(opt.toString());
}
tmpl.variables.append(var);
}
tmpl.isDefault = query.value(7).toBool();
tmpl.createdAt = QDateTime::fromString(query.value(8).toString(), Qt::ISODate);
tmpl.updatedAt = QDateTime::fromString(query.value(9).toString(), Qt::ISODate);
templates.append(tmpl);
}
}
return templates;
}
std::optional<Template> TemplateDao::findDefault(qint64 accountId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (accountId >= 0) {
query.prepare("SELECT id, accountId, name, subject, bodyHtml, bodyText, variables, isDefault, createdAt, updatedAt FROM Template WHERE (accountId = :accountId OR accountId IS NULL) AND isDefault = 1 LIMIT 1");
query.bindValue(":accountId", accountId);
} else {
query.prepare("SELECT id, accountId, name, subject, bodyHtml, bodyText, variables, isDefault, createdAt, updatedAt FROM Template WHERE accountId IS NULL AND isDefault = 1 LIMIT 1");
}
if (!query.exec() || !query.next()) {
return std::nullopt;
}
Template tmpl;
tmpl.id = query.value(0).toLongLong();
tmpl.accountId = query.value(1).isNull() ? -1 : query.value(1).toLongLong();
tmpl.name = query.value(2).toString();
tmpl.subject = query.value(3).toString();
tmpl.bodyHtml = query.value(4).toString();
tmpl.bodyText = query.value(5).toString();
QJsonDocument varsDoc = QJsonDocument::fromJson(query.value(6).toByteArray());
QJsonArray varsArray = varsDoc.array();
for (const auto& v : varsArray) {
QJsonObject obj = v.toObject();
TemplateVariable var;
var.name = obj["name"].toString();
var.label = obj["label"].toString();
var.defaultValue = obj["defaultValue"].toString();
var.type = obj["type"].toString();
QJsonArray optsArray = obj["options"].toArray();
for (const auto& opt : optsArray) {
var.options.append(opt.toString());
}
tmpl.variables.append(var);
}
tmpl.isDefault = query.value(7).toBool();
tmpl.createdAt = QDateTime::fromString(query.value(8).toString(), Qt::ISODate);
tmpl.updatedAt = QDateTime::fromString(query.value(9).toString(), Qt::ISODate);
return tmpl;
}
bool TemplateDao::setDefault(qint64 id, bool isDefault)
{
QSqlDatabase db = DatabaseManager::instance().database();
db.transaction();
QSqlQuery query(db);
// First, unset any existing default for this account
if (isDefault) {
auto tmplOpt = findById(id);
if (tmplOpt.has_value()) {
qint64 accId = tmplOpt->accountId;
if (accId >= 0) {
query.prepare("UPDATE Template SET isDefault = 0, updatedAt = :updatedAt WHERE accountId = :accountId AND isDefault = 1");
query.bindValue(":accountId", accId);
} else {
query.prepare("UPDATE Template SET isDefault = 0, updatedAt = :updatedAt WHERE accountId IS NULL AND isDefault = 1");
}
query.bindValue(":updatedAt", QDateTime::currentDateTime());
if (!query.exec()) {
db.rollback();
qWarning() << "Failed to unset previous default:" << query.lastError().text();
return false;
}
}
}
// Set new default
query.prepare("UPDATE Template SET isDefault = :isDefault, updatedAt = :updatedAt WHERE id = :id");
query.bindValue(":id", id);
query.bindValue(":isDefault", isDefault ? 1 : 0);
query.bindValue(":updatedAt", QDateTime::currentDateTime());
if (!query.exec()) {
db.rollback();
qWarning() << "Failed to set default template:" << query.lastError().text();
return false;
}
return db.commit();
}
QJsonObject Template::toJson() const
{
QJsonObject obj;
obj["id"] = static_cast<qint64>(id);
obj["accountId"] = accountId >= 0 ? static_cast<qint64>(accountId) : QJsonValue::Null;
obj["name"] = name;
obj["subject"] = subject;
obj["bodyHtml"] = bodyHtml;
obj["bodyText"] = bodyText;
QJsonArray varsArray;
for (const TemplateVariable& var : variables) {
QJsonObject varObj;
varObj["name"] = var.name;
varObj["label"] = var.label;
varObj["defaultValue"] = var.defaultValue;
varObj["type"] = var.type;
varObj["options"] = QJsonArray::fromStringList(var.options);
varsArray.append(varObj);
}
obj["variables"] = varsArray;
obj["isDefault"] = isDefault;
obj["createdAt"] = createdAt.toString(Qt::ISODate);
obj["updatedAt"] = updatedAt.toString(Qt::ISODate);
return obj;
}
Template Template::fromJson(const QJsonObject& obj)
{
Template tmpl;
tmpl.id = obj["id"].toVariant().toLongLong();
tmpl.accountId = obj["accountId"].isNull() ? -1 : obj["accountId"].toVariant().toLongLong();
tmpl.name = obj["name"].toString();
tmpl.subject = obj["subject"].toString();
tmpl.bodyHtml = obj["bodyHtml"].toString();
tmpl.bodyText = obj["bodyText"].toString();
QJsonArray varsArray = obj["variables"].toArray();
for (const auto& v : varsArray) {
QJsonObject varObj = v.toObject();
TemplateVariable var;
var.name = varObj["name"].toString();
var.label = varObj["label"].toString();
var.defaultValue = varObj["defaultValue"].toString();
var.type = varObj["type"].toString();
QJsonArray optsArray = varObj["options"].toArray();
for (const auto& opt : optsArray) {
var.options.append(opt.toString());
}
tmpl.variables.append(var);
}
tmpl.isDefault = obj["isDefault"].toBool();
tmpl.createdAt = QDateTime::fromString(obj["createdAt"].toString(), Qt::ISODate);
tmpl.updatedAt = QDateTime::fromString(obj["updatedAt"].toString(), Qt::ISODate);
return tmpl;
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "../databasemanager.h"
#include <QVector>
#include <optional>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
struct TemplateVariable
{
QString name;
QString label;
QString defaultValue;
QString type; // "text", "email", "date", "select", "textarea"
QStringList options; // for select type
};
struct Template
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QString subject;
QString bodyHtml;
QString bodyText;
QVector<TemplateVariable> variables;
bool isDefault{false};
QDateTime createdAt;
QDateTime updatedAt;
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
QJsonObject toJson() const;
static Template fromJson(const QJsonObject& obj);
};
class TemplateDao
{
public:
static bool insert(Template& tmpl);
static bool update(const Template& tmpl);
static bool remove(qint64 id);
static std::optional<Template> findById(qint64 id);
static QVector<Template> findByAccount(qint64 accountId); // accountId = -1 for global
static QVector<Template> findGlobal();
static QVector<Template> findAll();
static std::optional<Template> findDefault(qint64 accountId);
static bool setDefault(qint64 id, bool isDefault);
};
+79
View File
@@ -146,6 +146,78 @@ bool DatabaseManager::initialize(const QString& databasePath)
return false;
}
// ===== Categories table =====
if (!query.exec(
"CREATE TABLE IF NOT EXISTS Category ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"accountId INTEGER, " // NULL = global category, otherwise per-account
"name TEXT NOT NULL, "
"color TEXT NOT NULL DEFAULT '#1976D2', " // hex color
"parentCategoryId INTEGER, " // for nested categories
"sortOrder INTEGER DEFAULT 0, "
"createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, "
"FOREIGN KEY(accountId) REFERENCES Account(id) ON DELETE CASCADE, "
"FOREIGN KEY(parentCategoryId) REFERENCES Category(id) ON DELETE SET NULL"
");")) {
qWarning() << "Failed to create Category table:" << query.lastError().text();
return false;
}
// ===== MailCategory (many-to-many) table =====
if (!query.exec(
"CREATE TABLE IF NOT EXISTS MailCategory ("
"mailCopyId INTEGER NOT NULL, "
"categoryId INTEGER NOT NULL, "
"assignedAt DATETIME DEFAULT CURRENT_TIMESTAMP, "
"PRIMARY KEY (mailCopyId, categoryId), "
"FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id) ON DELETE CASCADE, "
"FOREIGN KEY(categoryId) REFERENCES Category(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create MailCategory table:" << query.lastError().text();
return false;
}
// ===== Rules table =====
if (!query.exec(
"CREATE TABLE IF NOT EXISTS Rule ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"accountId INTEGER, " // NULL = global rule
"name TEXT NOT NULL, "
"description TEXT, "
"enabled BOOLEAN DEFAULT 1, "
"priority INTEGER DEFAULT 100, " // lower = higher priority
"matchAll BOOLEAN DEFAULT 1, " // 1 = AND, 0 = OR
"conditions TEXT NOT NULL, " // JSON array of conditions
"actions TEXT NOT NULL, " // JSON array of actions
"createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, "
"updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, "
"lastRun DATETIME, "
"runCount INTEGER DEFAULT 0, "
"FOREIGN KEY(accountId) REFERENCES Account(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create Rule table:" << query.lastError().text();
return false;
}
// ===== Templates table =====
if (!query.exec(
"CREATE TABLE IF NOT EXISTS Template ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"accountId INTEGER, " // NULL = global template
"name TEXT NOT NULL, "
"subject TEXT, "
"bodyHtml TEXT, "
"bodyText TEXT, "
"variables TEXT, " // JSON array of variable definitions {name, label, defaultValue, type}
"isDefault BOOLEAN DEFAULT 0, "
"createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, "
"updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, "
"FOREIGN KEY(accountId) REFERENCES Account(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create Template table:" << query.lastError().text();
return false;
}
// Existing installations were created before storedPath was introduced.
// SQLite reports an error when the column already exists, which is safe to
// ignore here because the CREATE TABLE above covers new databases.
@@ -159,6 +231,13 @@ bool DatabaseManager::initialize(const QString& databasePath)
query.exec("ALTER TABLE Attachment ADD COLUMN storedPath TEXT");
query.exec("CREATE INDEX IF NOT EXISTS idx_mailcopy_folder_uid ON MailCopy(folderId, uid)");
query.exec("CREATE INDEX IF NOT EXISTS idx_attachment_mailcopy ON Attachment(mailCopyId)");
query.exec("CREATE INDEX IF NOT EXISTS idx_category_account ON Category(accountId)");
query.exec("CREATE INDEX IF NOT EXISTS idx_mailcategory_mail ON MailCategory(mailCopyId)");
query.exec("CREATE INDEX IF NOT EXISTS idx_mailcategory_category ON MailCategory(categoryId)");
query.exec("CREATE INDEX IF NOT EXISTS idx_rule_account ON Rule(accountId)");
query.exec("CREATE INDEX IF NOT EXISTS idx_rule_enabled ON Rule(enabled)");
query.exec("CREATE INDEX IF NOT EXISTS idx_template_account ON Template(accountId)");
query.exec("CREATE INDEX IF NOT EXISTS idx_template_default ON Template(isDefault)");
m_initialized = true;
qDebug() << "Database initialized at:" << dbPath;