MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - data - ContactStore.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 94.6 % 242 229
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 19 19
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 54.1 % 666 360

             Branch data     Line data    Source code
       1                 :             : #include "ContactStore.h"
       2                 :             : #include "DatabaseSecurity.h"
       3                 :             : #include "DatabaseSync.h"
       4                 :             : 
       5                 :             : #include <QLoggingCategory>
       6                 :             : #include <QSqlError>
       7                 :             : #include <QSqlQuery>
       8                 :             : #include <QUuid>
       9                 :             : 
      10   [ +  +  +  -  :          21 : Q_LOGGING_CATEGORY(lcContactStore, "mailjd.contactstore")
             +  -  -  - ]
      11                 :             : 
      12         [ +  - ]:         121 : ContactStore::ContactStore(QObject *parent) : QObject(parent) {}
      13                 :             : 
      14                 :          80 : ContactStore::~ContactStore() { close(); }
      15                 :             : 
      16                 :         120 : bool ContactStore::open(const QString &dbPath) {
      17   [ +  -  +  + ]:         120 :   if (!DatabaseSecurity::preparePath(dbPath)) {
      18   [ +  -  +  -  :           2 :     qCWarning(lcContactStore)
                   +  + ]
      19   [ +  -  +  - ]:           1 :         << "Failed to create private contacts DB:" << dbPath;
      20                 :           1 :     return false;
      21                 :             :   }
      22                 :             :   m_connectionName =
      23   [ +  -  +  -  :         238 :       QStringLiteral("contacts_") + QUuid::createUuid().toString();
                   +  - ]
      24   [ +  -  +  - ]:         238 :   m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
      25         [ +  - ]:         119 :   m_db.setDatabaseName(dbPath);
      26                 :             : 
      27   [ +  -  +  + ]:         119 :   if (!m_db.open()) {
      28   [ +  -  +  -  :           2 :     qCWarning(lcContactStore)
                   +  + ]
      29   [ +  -  +  -  :           1 :         << "Failed to open contacts DB:" << m_db.lastError().text();
             +  -  +  - ]
      30                 :           1 :     return false;
      31                 :             :   }
      32   [ +  -  -  + ]:         118 :   if (!DatabaseSecurity::restrictExistingFile(dbPath)) {
      33   [ #  #  #  #  :           0 :     qCWarning(lcContactStore)
                   #  # ]
      34   [ #  #  #  # ]:           0 :         << "Failed to restrict contacts DB permissions:" << dbPath;
      35         [ #  # ]:           0 :     m_db.close();
      36                 :           0 :     return false;
      37                 :             :   }
      38                 :             : 
      39                 :             :   // WAL mode for better performance
      40         [ +  - ]:         118 :   QSqlQuery pragma(m_db);
      41         [ +  - ]:         118 :   pragma.exec(QStringLiteral("PRAGMA journal_mode=WAL"));
      42         [ +  - ]:         118 :   pragma.exec(QStringLiteral("PRAGMA synchronous=NORMAL"));
      43                 :             :   // T-619/SEC-19: Prevent SQLITE_BUSY errors during concurrent access
      44         [ +  - ]:         118 :   pragma.exec(QStringLiteral("PRAGMA busy_timeout=5000"));
      45                 :             : 
      46         [ +  - ]:         118 :   return createSchema();
      47                 :         118 : }
      48                 :             : 
      49                 :          98 : void ContactStore::close() {
      50         [ +  + ]:          98 :   if (m_db.isOpen()) {
      51                 :          68 :     m_db.close();
      52                 :             :   }
      53                 :             :   // T-402/Bug 20: Release reference before removeDatabase
      54   [ +  -  +  - ]:          98 :   m_db = QSqlDatabase();
      55         [ +  + ]:          98 :   if (!m_connectionName.isEmpty()) {
      56                 :          69 :     QSqlDatabase::removeDatabase(m_connectionName);
      57                 :          69 :     m_connectionName.clear();
      58                 :             :   }
      59                 :          98 : }
      60                 :             : 
      61                 :          51 : bool ContactStore::isOpen() const { return m_db.isOpen(); }
      62                 :             : 
      63                 :         118 : bool ContactStore::createSchema() {
      64         [ +  - ]:         118 :   QSqlQuery q(m_db);
      65         [ +  - ]:         118 :   bool ok = q.exec(QStringLiteral(
      66                 :             :       "CREATE TABLE IF NOT EXISTS contacts ("
      67                 :             :       "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
      68                 :             :       "  display_name TEXT NOT NULL DEFAULT '',"
      69                 :             :       "  email TEXT NOT NULL,"
      70                 :             :       "  source TEXT NOT NULL DEFAULT 'local',"
      71                 :             :       "  carddav_uid TEXT DEFAULT '',"
      72                 :             :       "  carddav_etag TEXT DEFAULT '',"
      73                 :             :       "  last_used TEXT,"
      74                 :             :       "  use_count INTEGER NOT NULL DEFAULT 0,"
      75                 :             :       "  created_at TEXT NOT NULL DEFAULT (datetime('now')),"
      76                 :             :       "  updated_at TEXT NOT NULL DEFAULT (datetime('now'))"
      77                 :             :       ")"));
      78         [ +  + ]:         118 :   if (!ok) {
      79   [ +  -  +  -  :           2 :     qCWarning(lcContactStore)
                   +  + ]
      80   [ +  -  +  -  :           1 :         << "Failed to create contacts table:" << q.lastError().text();
             +  -  +  - ]
      81                 :           1 :     return false;
      82                 :             :   }
      83                 :             : 
      84         [ +  - ]:         117 :   QSqlQuery schemaQuery(m_db);
      85         [ +  - ]:         117 :   schemaQuery.prepare(QStringLiteral(
      86                 :             :       "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'contacts'"));
      87                 :             :   const bool needsLegacyMigration =
      88   [ +  -  +  -  :         351 :       schemaQuery.exec() && schemaQuery.next() &&
             +  -  +  - ]
      89   [ +  -  +  -  :         351 :       schemaQuery.value(0).toString().contains(
          +  -  +  +  +  
             -  -  -  -  
                      - ]
      90   [ +  -  +  -  :         234 :           QStringLiteral("UNIQUE(email, source)"));
          +  -  -  -  -  
                      - ]
      91         [ +  - ]:         117 :   schemaQuery.finish();
      92         [ +  + ]:         117 :   if (needsLegacyMigration) {
      93   [ +  -  +  -  :          10 :     qCInfo(lcContactStore)
                   +  + ]
      94         [ +  - ]:           5 :         << "Migrating contacts schema away from global email/source uniqueness";
      95   [ +  -  -  + ]:           5 :     if (!m_db.transaction()) {
      96   [ #  #  #  #  :           0 :       qCWarning(lcContactStore)
                   #  # ]
      97         [ #  # ]:           0 :           << "Failed to start contacts migration transaction:"
      98   [ #  #  #  #  :           0 :           << m_db.lastError().text();
                   #  # ]
      99                 :           3 :       return false;
     100                 :             :     }
     101         [ +  - ]:           5 :     QSqlQuery migrate(m_db);
     102   [ +  -  +  +  :          10 :     if (!migrate.exec(QStringLiteral("ALTER TABLE contacts RENAME TO contacts_old")) ||
             -  -  -  - ]
     103   [ +  -  +  -  :           9 :         !migrate.exec(QStringLiteral(
          +  +  +  -  -  
                -  -  - ]
     104                 :             :             "CREATE TABLE contacts ("
     105                 :             :             "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
     106                 :             :             "  display_name TEXT NOT NULL DEFAULT '',"
     107                 :             :             "  email TEXT NOT NULL,"
     108                 :             :             "  source TEXT NOT NULL DEFAULT 'local',"
     109                 :             :             "  carddav_uid TEXT DEFAULT '',"
     110                 :             :             "  carddav_etag TEXT DEFAULT '',"
     111                 :             :             "  last_used TEXT,"
     112                 :             :             "  use_count INTEGER NOT NULL DEFAULT 0,"
     113                 :             :             "  created_at TEXT NOT NULL DEFAULT (datetime('now')),"
     114                 :             :             "  updated_at TEXT NOT NULL DEFAULT (datetime('now'))"
     115                 :           8 :             ")")) ||
     116   [ +  -  +  +  :           9 :         !migrate.exec(QStringLiteral(
          +  +  +  +  -  
                -  -  - ]
     117                 :             :             "INSERT INTO contacts (id, display_name, email, source, carddav_uid, "
     118                 :             :             "carddav_etag, last_used, use_count, created_at, updated_at) "
     119                 :             :             "SELECT id, display_name, email, source, carddav_uid, carddav_etag, "
     120   [ +  -  +  + ]:          14 :             "last_used, use_count, created_at, updated_at FROM contacts_old")) ||
     121   [ +  -  -  +  :           7 :         !migrate.exec(QStringLiteral("DROP TABLE contacts_old"))) {
          +  +  +  +  +  
             +  -  -  -  
                      - ]
     122   [ +  -  +  -  :           6 :       qCWarning(lcContactStore)
                   +  + ]
     123         [ +  - ]:           3 :           << "Failed to migrate contacts uniqueness schema:"
     124   [ +  -  +  -  :           3 :           << migrate.lastError().text();
                   +  - ]
     125         [ +  - ]:           3 :       m_db.rollback();
     126                 :           3 :       return false;
     127                 :             :     }
     128   [ +  -  -  + ]:           2 :     if (!m_db.commit()) {
     129   [ #  #  #  #  :           0 :       qCWarning(lcContactStore)
                   #  # ]
     130         [ #  # ]:           0 :           << "Failed to commit contacts uniqueness migration:"
     131   [ #  #  #  #  :           0 :           << m_db.lastError().text();
                   #  # ]
     132         [ #  # ]:           0 :       m_db.rollback();
     133                 :           0 :       return false;
     134                 :             :     }
     135         [ +  + ]:           5 :   }
     136                 :             : 
     137         [ +  - ]:         114 :   q.exec(QStringLiteral(
     138                 :             :       "CREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email)"));
     139         [ +  - ]:         114 :   q.exec(QStringLiteral(
     140                 :             :       "CREATE INDEX IF NOT EXISTS idx_contacts_last_used "
     141                 :             :       "ON contacts(last_used DESC)"));
     142         [ +  - ]:         114 :   q.exec(QStringLiteral(
     143                 :             :       "CREATE INDEX IF NOT EXISTS idx_contacts_carddav_uid "
     144                 :             :       "ON contacts(carddav_uid)"));
     145         [ +  - ]:         114 :   q.exec(QStringLiteral(
     146                 :             :       "CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_local_email_unique "
     147                 :             :       "ON contacts(email, source) WHERE source = 'local'"));
     148         [ +  - ]:         114 :   q.exec(QStringLiteral(
     149                 :             :       "CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_carddav_identity_unique "
     150                 :             :       "ON contacts(carddav_uid, email, source) WHERE source = 'carddav'"));
     151                 :             : 
     152                 :         114 :   return true;
     153                 :         118 : }
     154                 :             : 
     155                 :         108 : Contact ContactStore::contactFromQuery(const QSqlQuery &q) const {
     156                 :         108 :   Contact c;
     157   [ +  -  +  - ]:         108 :   c.id = q.value(0).toLongLong();
     158   [ +  -  +  - ]:         108 :   c.displayName = q.value(1).toString();
     159   [ +  -  +  - ]:         108 :   c.email = q.value(2).toString();
     160   [ +  -  +  - ]:         108 :   c.source = q.value(3).toString();
     161   [ +  -  +  - ]:         108 :   c.cardDavUid = q.value(4).toString();
     162   [ +  -  +  - ]:         108 :   c.cardDavEtag = q.value(5).toString();
     163   [ +  -  +  -  :         108 :   c.lastUsed = QDateTime::fromString(q.value(6).toString(), Qt::ISODate);
                   +  - ]
     164   [ +  -  +  - ]:         108 :   c.useCount = q.value(7).toInt();
     165   [ +  -  +  -  :         108 :   c.createdAt = QDateTime::fromString(q.value(8).toString(), Qt::ISODate);
                   +  - ]
     166   [ +  -  +  -  :         108 :   c.updatedAt = QDateTime::fromString(q.value(9).toString(), Qt::ISODate);
                   +  - ]
     167                 :         108 :   return c;
     168                 :           0 : }
     169                 :             : 
     170                 :             : // --- CRUD ---
     171                 :             : 
     172                 :          81 : qint64 ContactStore::addContact(const Contact &contact) {
     173         [ +  - ]:          81 :   QSqlQuery q(m_db);
     174         [ +  - ]:          81 :   q.prepare(QStringLiteral(
     175                 :             :       "INSERT INTO contacts (display_name, email, source, carddav_uid, "
     176                 :             :       "carddav_etag, last_used, use_count) "
     177                 :             :       "VALUES (?, ?, ?, ?, ?, ?, ?)"));
     178         [ +  - ]:          81 :   q.addBindValue(contact.displayName);
     179   [ +  -  +  - ]:          81 :   q.addBindValue(contact.email.toLower());
     180   [ +  +  +  -  :         162 :   q.addBindValue(contact.source.isEmpty() ? QStringLiteral("local")
             +  +  -  - ]
     181                 :          58 :                                           : contact.source);
     182         [ +  - ]:          81 :   q.addBindValue(contact.cardDavUid);
     183         [ +  - ]:          81 :   q.addBindValue(contact.cardDavEtag);
     184   [ +  -  +  - ]:         162 :   q.addBindValue(contact.lastUsed.isValid()
     185   [ +  +  +  -  :         162 :                      ? contact.lastUsed.toString(Qt::ISODate)
             +  +  -  - ]
     186                 :             :                      : QVariant());
     187         [ +  - ]:          81 :   q.addBindValue(contact.useCount);
     188                 :             : 
     189   [ +  -  +  + ]:          81 :   if (!q.exec()) {
     190   [ +  -  +  -  :           8 :     qCWarning(lcContactStore)
                   +  + ]
     191   [ +  -  +  -  :           4 :         << "addContact failed:" << q.lastError().text();
             +  -  +  - ]
     192                 :           4 :     return -1;
     193                 :             :   }
     194   [ +  -  +  - ]:          77 :   return q.lastInsertId().toLongLong();
     195                 :          81 : }
     196                 :             : 
     197                 :           6 : bool ContactStore::updateContact(const Contact &contact) {
     198         [ +  - ]:           6 :   QSqlQuery q(m_db);
     199         [ +  - ]:           6 :   q.prepare(QStringLiteral(
     200                 :             :       "UPDATE contacts SET display_name = ?, email = ?, source = ?, "
     201                 :             :       "carddav_uid = ?, carddav_etag = ?, last_used = ?, use_count = ?, "
     202                 :             :       "updated_at = datetime('now') WHERE id = ?"));
     203         [ +  - ]:           6 :   q.addBindValue(contact.displayName);
     204   [ +  -  +  - ]:           6 :   q.addBindValue(contact.email.toLower());
     205         [ +  - ]:           6 :   q.addBindValue(contact.source);
     206         [ +  - ]:           6 :   q.addBindValue(contact.cardDavUid);
     207         [ +  - ]:           6 :   q.addBindValue(contact.cardDavEtag);
     208   [ +  -  +  - ]:          12 :   q.addBindValue(contact.lastUsed.isValid()
     209   [ +  +  +  -  :          12 :                      ? contact.lastUsed.toString(Qt::ISODate)
             +  +  -  - ]
     210                 :             :                      : QVariant());
     211         [ +  - ]:           6 :   q.addBindValue(contact.useCount);
     212         [ +  - ]:           6 :   q.addBindValue(contact.id);
     213                 :             : 
     214   [ +  -  +  + ]:           6 :   if (!q.exec()) {
     215   [ +  -  +  -  :           2 :     qCWarning(lcContactStore)
                   +  + ]
     216   [ +  -  +  -  :           1 :         << "updateContact failed:" << q.lastError().text();
             +  -  +  - ]
     217                 :           1 :     return false;
     218                 :             :   }
     219         [ +  - ]:           5 :   return q.numRowsAffected() > 0;
     220                 :           6 : }
     221                 :             : 
     222                 :          10 : bool ContactStore::removeContact(qint64 id) {
     223         [ +  - ]:          10 :   QSqlQuery q(m_db);
     224         [ +  - ]:          10 :   q.prepare(QStringLiteral("DELETE FROM contacts WHERE id = ?"));
     225         [ +  - ]:          10 :   q.addBindValue(id);
     226   [ +  -  +  + ]:          10 :   if (!q.exec()) {
     227   [ +  -  +  -  :           2 :     qCWarning(lcContactStore)
                   +  + ]
     228   [ +  -  +  -  :           1 :         << "removeContact failed:" << q.lastError().text();
             +  -  +  - ]
     229                 :           1 :     return false;
     230                 :             :   }
     231         [ +  - ]:           9 :   return q.numRowsAffected() > 0;
     232                 :          10 : }
     233                 :             : 
     234                 :          11 : std::optional<Contact> ContactStore::contactById(qint64 id) const {
     235         [ +  - ]:          11 :   QSqlQuery q(m_db);
     236         [ +  - ]:          11 :   q.prepare(QStringLiteral(
     237                 :             :       "SELECT id, display_name, email, source, carddav_uid, carddav_etag, "
     238                 :             :       "last_used, use_count, created_at, updated_at "
     239                 :             :       "FROM contacts WHERE id = ?"));
     240         [ +  - ]:          11 :   q.addBindValue(id);
     241   [ +  -  +  +  :          11 :   if (q.exec() && q.next()) {
          +  -  +  +  +  
                      + ]
     242         [ +  - ]:           7 :     return contactFromQuery(q);
     243                 :             :   }
     244                 :           4 :   return std::nullopt;
     245                 :          11 : }
     246                 :             : 
     247                 :          13 : std::optional<Contact> ContactStore::contactByEmail(
     248                 :             :     const QString &email) const {
     249         [ +  - ]:          13 :   QSqlQuery q(m_db);
     250         [ +  - ]:          13 :   q.prepare(QStringLiteral(
     251                 :             :       "SELECT id, display_name, email, source, carddav_uid, carddav_etag, "
     252                 :             :       "last_used, use_count, created_at, updated_at "
     253                 :             :       "FROM contacts WHERE email = ? ORDER BY use_count DESC LIMIT 1"));
     254   [ +  -  +  - ]:          13 :   q.addBindValue(email.toLower());
     255   [ +  -  +  +  :          13 :   if (q.exec() && q.next()) {
          +  -  +  +  +  
                      + ]
     256         [ +  - ]:          11 :     return contactFromQuery(q);
     257                 :             :   }
     258                 :           2 :   return std::nullopt;
     259                 :          13 : }
     260                 :             : 
     261                 :          27 : QList<Contact> ContactStore::allContacts() const {
     262                 :          27 :   QList<Contact> result;
     263         [ +  - ]:          27 :   QSqlQuery q(m_db);
     264         [ +  - ]:          27 :   q.exec(QStringLiteral(
     265                 :             :       "SELECT id, display_name, email, source, carddav_uid, carddav_etag, "
     266                 :             :       "last_used, use_count, created_at, updated_at "
     267                 :             :       "FROM contacts ORDER BY display_name COLLATE NOCASE"));
     268   [ +  -  +  + ]:          55 :   while (q.next()) {
     269   [ +  -  +  - ]:          28 :     result.append(contactFromQuery(q));
     270                 :             :   }
     271                 :          27 :   return result;
     272                 :          27 : }
     273                 :             : 
     274                 :             : // --- Search & Suggestions ---
     275                 :             : 
     276                 :          61 : QList<Contact> ContactStore::search(const QString &query, int limit) const {
     277                 :          61 :   QList<Contact> result;
     278   [ +  -  +  + ]:          61 :   if (query.trimmed().isEmpty())
     279                 :           7 :     return result;
     280                 :             : 
     281         [ +  - ]:          54 :   QSqlQuery q(m_db);
     282         [ +  - ]:          54 :   q.prepare(QStringLiteral(
     283                 :             :       "SELECT id, display_name, email, source, carddav_uid, carddav_etag, "
     284                 :             :       "last_used, use_count, created_at, updated_at "
     285                 :             :       "FROM contacts "
     286                 :             :       "WHERE display_name LIKE ? OR email LIKE ? "
     287                 :             :       "ORDER BY use_count DESC, last_used DESC "
     288                 :             :       "LIMIT ?"));
     289   [ +  -  +  - ]:         108 :   QString pattern = QStringLiteral("%%1%").arg(query.trimmed());
     290         [ +  - ]:          54 :   q.addBindValue(pattern);
     291         [ +  - ]:          54 :   q.addBindValue(pattern);
     292         [ +  - ]:          54 :   q.addBindValue(limit);
     293                 :             : 
     294   [ +  -  +  + ]:          54 :   if (q.exec()) {
     295   [ +  -  +  + ]:          93 :     while (q.next()) {
     296   [ +  -  +  - ]:          40 :       result.append(contactFromQuery(q));
     297                 :             :     }
     298                 :             :   }
     299                 :          54 :   return result;
     300                 :          54 : }
     301                 :             : 
     302                 :             : // --- Usage Tracking ---
     303                 :             : 
     304                 :          15 : void ContactStore::recordUsage(const QString &email,
     305                 :             :                                const QString &displayName) {
     306   [ +  -  +  - ]:          15 :   QString lowerEmail = email.toLower().trimmed();
     307         [ +  + ]:          15 :   if (lowerEmail.isEmpty())
     308                 :           2 :     return;
     309                 :             : 
     310                 :             :   // Try to update existing contact
     311         [ +  - ]:          13 :   QSqlQuery q(m_db);
     312         [ +  - ]:          13 :   q.prepare(QStringLiteral(
     313                 :             :       "UPDATE contacts SET use_count = use_count + 1, "
     314                 :             :       "last_used = datetime('now'), updated_at = datetime('now'), "
     315                 :             :       "display_name = CASE WHEN display_name = '' OR display_name = email "
     316                 :             :       "  THEN ? ELSE display_name END "
     317                 :             :       "WHERE email = ?"));
     318   [ +  -  +  - ]:          13 :   q.addBindValue(displayName.trimmed());
     319         [ +  - ]:          13 :   q.addBindValue(lowerEmail);
     320                 :             : 
     321   [ +  -  +  +  :          13 :   if (q.exec() && q.numRowsAffected() > 0) {
          +  -  +  +  +  
                      + ]
     322                 :           5 :     return; // Updated existing
     323                 :             :   }
     324                 :             : 
     325                 :             :   // Insert new local contact
     326                 :           8 :   Contact c;
     327   [ +  -  +  +  :          16 :   c.displayName = displayName.trimmed().isEmpty() ? lowerEmail
                   +  - ]
     328                 :           8 :                                                   : displayName.trimmed();
     329                 :           8 :   c.email = lowerEmail;
     330                 :           8 :   c.source = QStringLiteral("local");
     331                 :           8 :   c.useCount = 1;
     332         [ +  - ]:           8 :   c.lastUsed = QDateTime::currentDateTime();
     333         [ +  - ]:           8 :   addContact(c);
     334   [ +  +  +  + ]:          20 : }
     335                 :             : 
     336                 :             : // --- CardDAV Sync Helpers ---
     337                 :             : 
     338                 :          21 : QList<Contact> ContactStore::cardDavContacts() const {
     339                 :          21 :   QList<Contact> result;
     340         [ +  - ]:          21 :   QSqlQuery q(m_db);
     341         [ +  - ]:          21 :   q.exec(QStringLiteral(
     342                 :             :       "SELECT id, display_name, email, source, carddav_uid, carddav_etag, "
     343                 :             :       "last_used, use_count, created_at, updated_at "
     344                 :             :       "FROM contacts WHERE source = 'carddav' "
     345                 :             :       "ORDER BY display_name COLLATE NOCASE"));
     346   [ +  -  +  + ]:          43 :   while (q.next()) {
     347   [ +  -  +  - ]:          22 :     result.append(contactFromQuery(q));
     348                 :             :   }
     349                 :          21 :   return result;
     350                 :          21 : }
     351                 :             : 
     352                 :          24 : void ContactStore::upsertCardDavContact(const Contact &contact) {
     353         [ +  - ]:          24 :   QSqlQuery q(m_db);
     354                 :             :   // Try update first by CardDAV identity and email. A single vCard UID can
     355                 :             :   // legitimately contain multiple EMAIL entries, so email is part of the row
     356                 :             :   // identity here.
     357         [ +  - ]:          24 :   q.prepare(QStringLiteral(
     358                 :             :       "UPDATE contacts SET display_name = ?, email = ?, "
     359                 :             :       "carddav_etag = ?, updated_at = datetime('now') "
     360                 :             :       "WHERE carddav_uid = ? AND email = ? AND source = 'carddav'"));
     361         [ +  - ]:          24 :   q.addBindValue(contact.displayName);
     362   [ +  -  +  - ]:          24 :   q.addBindValue(contact.email.toLower());
     363         [ +  - ]:          24 :   q.addBindValue(contact.cardDavEtag);
     364         [ +  - ]:          24 :   q.addBindValue(contact.cardDavUid);
     365   [ +  -  +  - ]:          24 :   q.addBindValue(contact.email.toLower());
     366                 :             : 
     367   [ +  -  +  +  :          24 :   if (q.exec() && q.numRowsAffected() > 0) {
          +  -  +  +  +  
                      + ]
     368                 :           4 :     return; // Updated
     369                 :             :   }
     370                 :             : 
     371                 :             :   // Insert new
     372                 :          20 :   Contact c = contact;
     373                 :          20 :   c.source = QStringLiteral("carddav");
     374         [ +  - ]:          20 :   addContact(c);
     375         [ +  + ]:          24 : }
     376                 :             : 
     377                 :          10 : void ContactStore::removeStaleCardDavContacts(
     378                 :             :     const QStringList &activeUids, bool allowEmpty) {
     379                 :             :   // T-623/FUNC-12: Guard against empty list — a network error returning an
     380                 :             :   // empty list should NOT delete all CardDAV contacts.
     381         [ +  + ]:          10 :   if (activeUids.isEmpty()) {
     382         [ +  + ]:           5 :     if (allowEmpty) {
     383         [ +  - ]:           3 :       QSqlQuery q(m_db);
     384   [ +  -  +  + ]:           3 :       if (!q.exec(QStringLiteral(
     385                 :             :               "DELETE FROM contacts WHERE source = 'carddav'"))) {
     386   [ +  -  +  -  :           2 :         qCWarning(lcContactStore)
                   +  + ]
     387         [ +  - ]:           1 :             << "removeStaleCardDavContacts failed:"
     388   [ +  -  +  -  :           1 :             << q.lastError().text();
                   +  - ]
     389                 :             :       }
     390                 :           3 :       return;
     391                 :           3 :     }
     392   [ +  -  +  -  :           4 :     qCWarning(lcContactStore)
                   +  + ]
     393                 :             :         << "removeStaleCardDavContacts: activeUids is empty — skipping"
     394         [ +  - ]:           2 :            " to prevent accidental deletion of all contacts";
     395                 :           2 :     return;
     396                 :             :   }
     397                 :             : 
     398                 :           5 :   QString error;
     399         [ +  + ]:          10 :   if (!DatabaseSync::removeRowsMissingFromUidSet(
     400         [ +  - ]:           5 :           m_db,
     401                 :          10 :           QStringLiteral(
     402                 :             :               "DELETE FROM contacts WHERE source = 'carddav' "
     403                 :             :               "AND NOT EXISTS "
     404                 :             :               "(SELECT 1 FROM mailjd_active_sync_uids active "
     405                 :             :               "WHERE active.uid = contacts.carddav_uid)"),
     406                 :             :           {}, activeUids, &error)) {
     407   [ +  -  +  -  :           2 :     qCWarning(lcContactStore)
                   +  + ]
     408   [ +  -  +  - ]:           1 :         << "removeStaleCardDavContacts failed:" << error;
     409                 :             :   }
     410                 :           5 : }
        

Generated by: LCOV version 2.0-1