MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - service - CardDavClient.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 97.9 % 242 237
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 14 14
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 60.1 % 712 428

             Branch data     Line data    Source code
       1                 :             : #include "CardDavClient.h"
       2                 :             : 
       3                 :             : #include <QDomDocument>
       4                 :             : #include <QLoggingCategory>
       5                 :             : #include <QNetworkAccessManager>
       6                 :             : #include <QNetworkReply>
       7                 :             : #include <QNetworkRequest>
       8                 :             : #include <QRegularExpression>
       9                 :             : 
      10                 :             : #include "DavNetworkLimits.h"
      11                 :             : #include "DavXmlHelper.h"
      12                 :             : 
      13   [ +  +  +  -  :          75 : Q_LOGGING_CATEGORY(lcCardDav, "mailjd.carddav")
             +  -  -  - ]
      14                 :             : 
      15                 :             : using namespace DavXmlHelper;
      16                 :             : 
      17                 :          31 : CardDavClient::CardDavClient(const QString &serverUrl,
      18                 :             :                              const QString &username,
      19                 :          31 :                              const QString &password, QObject *parent)
      20                 :          31 :     : QObject(parent), m_serverUrl(serverUrl), m_username(username),
      21                 :          62 :       m_password(password) {
      22   [ +  -  +  + ]:          31 :   if (m_serverUrl.endsWith('/'))
      23         [ +  - ]:           2 :     m_serverUrl.chop(1);
      24   [ +  -  +  -  :          31 :   m_nam = new QNetworkAccessManager(this);
             -  +  -  - ]
      25         [ +  - ]:          31 :   m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy);
      26                 :          31 : }
      27                 :             : 
      28                 :          21 : void CardDavClient::setNetworkAccessManager(QNetworkAccessManager *nam) {
      29   [ +  +  +  -  :          21 :   if (m_nam && m_nam->parent() == this)
                   +  + ]
      30         [ +  - ]:          20 :     delete m_nam;
      31                 :          21 :   m_nam = nam;
      32         [ +  + ]:          21 :   if (m_nam) {
      33                 :          20 :     m_nam->setParent(this);
      34                 :          20 :     m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy);
      35                 :             :   }
      36                 :          21 : }
      37                 :             : 
      38                 :          21 : QByteArray CardDavClient::authHeader() const {
      39                 :             :   // T-504: Refuse to send credentials over unencrypted connections
      40         [ +  - ]:          21 :   QUrl serverUrl(m_serverUrl);
      41   [ +  -  +  -  :          21 :   if (serverUrl.scheme().toLower() == QLatin1String("http")) {
                   +  + ]
      42   [ +  -  +  -  :           4 :     qCWarning(lcCardDav) << "Refusing Basic Auth over HTTP — use HTTPS";
             +  -  +  + ]
      43                 :           2 :     return {};
      44                 :             :   }
      45                 :             :   return "Basic " +
      46                 :           0 :          QByteArray(
      47   [ +  -  +  -  :          57 :              (m_username + QStringLiteral(":") + m_password).toUtf8())
                   +  - ]
      48   [ +  -  +  - ]:          38 :              .toBase64();
      49                 :          21 : }
      50                 :             : 
      51                 :          32 : static int effectivePort(const QUrl &url) {
      52                 :          32 :   const int port = url.port();
      53         [ +  + ]:          32 :   if (port >= 0)
      54                 :           6 :     return port;
      55         [ +  - ]:          78 :   return url.scheme().compare(QStringLiteral("https"), Qt::CaseInsensitive) == 0
      56         [ +  + ]:          26 :              ? 443
      57                 :          26 :              : 80;
      58                 :             : }
      59                 :             : 
      60                 :          20 : QString CardDavClient::resolveDavUrl(const QString &href) const {
      61         [ +  - ]:          20 :   QUrl server(m_serverUrl);
      62                 :          20 :   QUrl base = server;
      63   [ +  -  +  -  :          20 :   if (!base.path().endsWith(QLatin1Char('/')))
                   +  + ]
      64   [ +  -  +  -  :          38 :     base.setPath(base.path() + QLatin1Char('/'));
                   +  - ]
      65                 :             : 
      66         [ +  - ]:          20 :   QUrl candidate(href);
      67   [ +  -  +  +  :          20 :   QUrl resolved = candidate.isRelative() ? base.resolved(candidate) : candidate;
                   +  - ]
      68         [ +  - ]:          19 :   if (!resolved.isValid() ||
      69   [ +  -  +  -  :          77 :       resolved.scheme().compare(server.scheme(), Qt::CaseInsensitive) != 0 ||
          +  +  +  +  +  
             +  -  -  -  
                      - ]
      70   [ +  -  +  -  :          58 :       resolved.host().compare(server.host(), Qt::CaseInsensitive) != 0 ||
          +  +  +  +  -  
                -  -  - ]
      71   [ +  -  +  -  :          16 :       effectivePort(resolved) != effectivePort(server) ||
                   +  + ]
      72   [ +  -  +  +  :          53 :       !resolved.userName().isEmpty() || !resolved.password().isEmpty()) {
          +  -  +  +  +  
          -  +  +  +  +  
          +  +  +  +  -  
                -  -  - ]
      73   [ +  -  +  -  :          14 :     qCWarning(lcCardDav) << "Rejected cross-origin DAV URL:" << href;
          +  -  +  -  +  
                      + ]
      74                 :           7 :     return {};
      75                 :             :   }
      76                 :             : 
      77         [ +  - ]:          13 :   return resolved.toString();
      78                 :          20 : }
      79                 :             : 
      80                 :           9 : void CardDavClient::discoverAddressBooks() {
      81                 :             :   // T-505: URL-encode username to prevent path traversal
      82                 :           9 :   QString url = m_serverUrl +
      83                 :          18 :       QStringLiteral("/remote.php/dav/addressbooks/users/%1/")
      84   [ +  -  +  -  :           9 :           .arg(QString::fromUtf8(QUrl::toPercentEncoding(m_username)));
             +  -  +  - ]
      85                 :             : 
      86   [ +  -  +  - ]:           9 :   QNetworkRequest request{QUrl(url)};
      87   [ +  -  +  -  :           9 :   request.setRawHeader("Authorization", authHeader());
                   +  - ]
      88   [ +  -  +  -  :           9 :   request.setRawHeader("Depth", "1");
                   +  - ]
      89   [ +  -  +  -  :           9 :   request.setRawHeader("Content-Type", "application/xml; charset=utf-8");
                   +  - ]
      90                 :             : 
      91                 :             :   QByteArray body =
      92                 :             :       "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
      93                 :             :       "<d:propfind xmlns:d=\"DAV:\">"
      94                 :             :       "  <d:prop><d:resourcetype/><d:displayname/></d:prop>"
      95         [ +  - ]:           9 :       "</d:propfind>";
      96                 :             : 
      97   [ +  -  +  - ]:           9 :   auto *reply = m_nam->sendCustomRequest(request, "PROPFIND", body);
      98         [ +  - ]:           9 :   DavNetworkLimits::apply(reply);
      99         [ +  - ]:           9 :   connect(reply, &QNetworkReply::finished, this, [this, reply]() {
     100                 :           9 :     onDiscoverReply(reply);
     101                 :           9 :   });
     102                 :           9 : }
     103                 :             : 
     104                 :           9 : void CardDavClient::onDiscoverReply(QNetworkReply *reply) {
     105         [ +  - ]:           9 :   reply->deleteLater();
     106                 :             : 
     107         [ +  - ]:           9 :   const QString limitError = DavNetworkLimits::failureReason(reply);
     108         [ +  + ]:           9 :   if (!limitError.isEmpty()) {
     109   [ +  -  +  -  :           2 :     qCWarning(lcCardDav) << "PROPFIND failed:" << limitError;
          +  -  +  -  +  
                      + ]
     110         [ +  - ]:           1 :     emit syncFailed(limitError);
     111                 :           1 :     return;
     112                 :             :   }
     113                 :             : 
     114   [ +  -  +  + ]:           8 :   if (reply->error() != QNetworkReply::NoError) {
     115   [ +  -  +  -  :           4 :     qCWarning(lcCardDav) << "PROPFIND failed:" << reply->errorString();
          +  -  +  -  +  
                -  +  + ]
     116   [ +  -  +  - ]:           2 :     emit syncFailed(reply->errorString());
     117                 :           2 :     return;
     118                 :             :   }
     119                 :             : 
     120         [ +  - ]:           6 :   QByteArray data = reply->readAll();
     121         [ +  - ]:           6 :   QDomDocument doc;
     122                 :             :   // T-511: Check XML parse result
     123   [ +  -  +  + ]:           6 :   if (!doc.setContent(data)) {
     124   [ +  -  +  -  :           2 :     qCWarning(lcCardDav) << "Failed to parse XML response";
             +  -  +  + ]
     125         [ +  - ]:           1 :     emit syncFailed(QStringLiteral("Invalid XML response"));
     126                 :           1 :     return;
     127                 :             :   }
     128                 :             : 
     129                 :           5 :   QList<AddressBookInfo> books;
     130                 :             :   QDomNodeList responses = findElementsByLocalNameDoc(doc,
     131         [ +  - ]:           5 :       QStringLiteral("response"));
     132                 :             : 
     133   [ +  -  +  + ]:          11 :   for (int i = 0; i < responses.count(); ++i) {
     134   [ +  -  +  - ]:           6 :     QDomElement resp = responses.at(i).toElement();
     135                 :             : 
     136                 :             :     // Check for addressbook resourcetype
     137                 :           6 :     bool isAddressBook = false;
     138                 :             : 
     139                 :             :     QDomNodeList resourceTypes = findElementsByLocalName(resp,
     140         [ +  - ]:           6 :         QStringLiteral("resourcetype"));
     141                 :             : 
     142   [ +  -  +  + ]:           8 :     for (int j = 0; j < resourceTypes.count(); ++j) {
     143                 :           7 :       QString rtXml;
     144         [ +  - ]:           7 :       QTextStream ts(&rtXml);
     145   [ +  -  +  - ]:           7 :       resourceTypes.at(j).save(ts, 0);
     146   [ +  -  +  + ]:           7 :       if (rtXml.contains(QStringLiteral("addressbook"),
     147                 :             :                          Qt::CaseInsensitive)) {
     148                 :           5 :         isAddressBook = true;
     149                 :           5 :         break;
     150                 :             :       }
     151   [ +  +  +  + ]:          12 :     }
     152                 :             : 
     153         [ +  + ]:           6 :     if (isAddressBook) {
     154                 :             :       // Extract href
     155                 :           5 :       QString href;
     156                 :             :       QDomNodeList hrefs = findElementsByLocalName(resp,
     157         [ +  - ]:           5 :           QStringLiteral("href"));
     158   [ +  -  +  + ]:           5 :       if (!hrefs.isEmpty())
     159   [ +  -  +  -  :           4 :         href = hrefs.at(0).toElement().text();
                   +  - ]
     160                 :             : 
     161                 :             :       // Extract displayname
     162                 :           5 :       QString displayName;
     163                 :             :       QDomNodeList nameNodes = findElementsByLocalName(resp,
     164         [ +  - ]:           5 :           QStringLiteral("displayname"));
     165   [ +  -  +  + ]:           5 :       if (!nameNodes.isEmpty())
     166   [ +  -  +  -  :           4 :         displayName = nameNodes.at(0).toElement().text().trimmed();
             +  -  +  - ]
     167                 :             : 
     168                 :             :       // Fallback: extract name from path
     169         [ +  + ]:           5 :       if (displayName.isEmpty()) {
     170                 :           2 :         displayName = href;
     171   [ +  -  -  + ]:           2 :         if (displayName.endsWith(QLatin1Char('/')))
     172         [ #  # ]:           0 :           displayName.chop(1);
     173                 :             :         displayName =
     174         [ +  - ]:           2 :             displayName.mid(displayName.lastIndexOf(QLatin1Char('/')) + 1);
     175                 :             :       }
     176                 :             : 
     177         [ +  + ]:           5 :       if (!href.isEmpty()) {
     178                 :           4 :         books.append({href, displayName});
     179   [ +  -  +  -  :           8 :         qCDebug(lcCardDav) << "Book:" << displayName << "at" << href;
          +  -  +  -  +  
             -  +  -  +  
                      + ]
     180                 :             :       }
     181                 :           5 :     }
     182                 :           6 :   }
     183                 :             : 
     184   [ +  -  +  -  :          10 :   qCInfo(lcCardDav) << "Discovered" << books.size() << "address books";
          +  -  +  -  +  
                -  +  + ]
     185         [ +  - ]:           5 :   emit addressBooksDiscovered(books);
     186   [ +  -  +  +  :          15 : }
          +  +  +  +  -  
                      - ]
     187                 :             : 
     188                 :          16 : void CardDavClient::syncAddressBook(const QString &bookPath) {
     189         [ +  - ]:          16 :   QString url = resolveDavUrl(bookPath);
     190         [ +  + ]:          16 :   if (url.isEmpty()) {
     191         [ +  - ]:           4 :     emit syncFailed(QStringLiteral("Cross-origin URL rejected"));
     192                 :           4 :     return;
     193                 :             :   }
     194                 :             : 
     195   [ +  -  +  - ]:          12 :   QNetworkRequest request{QUrl(url)};
     196   [ +  -  +  -  :          12 :   request.setRawHeader("Authorization", authHeader());
                   +  - ]
     197   [ +  -  +  -  :          12 :   request.setRawHeader("Depth", "1");
                   +  - ]
     198   [ +  -  +  -  :          12 :   request.setRawHeader("Content-Type", "application/xml; charset=utf-8");
                   +  - ]
     199                 :             : 
     200                 :             :   // REPORT with addressbook-query to fetch all contacts
     201                 :             :   QByteArray body =
     202                 :             :       "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
     203                 :             :       "<card:addressbook-query xmlns:d=\"DAV:\" "
     204                 :             :       "  xmlns:card=\"urn:ietf:params:xml:ns:carddav\">"
     205                 :             :       "  <d:prop>"
     206                 :             :       "    <d:getetag/>"
     207                 :             :       "    <card:address-data/>"
     208                 :             :       "  </d:prop>"
     209         [ +  - ]:          12 :       "</card:addressbook-query>";
     210                 :             : 
     211   [ +  -  +  - ]:          12 :   auto *reply = m_nam->sendCustomRequest(request, "REPORT", body);
     212         [ +  - ]:          12 :   DavNetworkLimits::apply(reply);
     213         [ +  - ]:          12 :   connect(reply, &QNetworkReply::finished, this, [this, reply]() {
     214                 :          12 :     onSyncReply(reply);
     215                 :          12 :   });
     216         [ +  + ]:          16 : }
     217                 :             : 
     218                 :          12 : void CardDavClient::onSyncReply(QNetworkReply *reply) {
     219         [ +  - ]:          12 :   reply->deleteLater();
     220                 :             : 
     221         [ +  - ]:          12 :   const QString limitError = DavNetworkLimits::failureReason(reply);
     222         [ +  + ]:          12 :   if (!limitError.isEmpty()) {
     223   [ +  -  +  -  :           4 :     qCWarning(lcCardDav) << "REPORT failed:" << limitError;
          +  -  +  -  +  
                      + ]
     224         [ +  - ]:           2 :     emit syncFailed(limitError);
     225                 :           2 :     return;
     226                 :             :   }
     227                 :             : 
     228   [ +  -  +  + ]:          10 :   if (reply->error() != QNetworkReply::NoError) {
     229   [ +  -  +  -  :           4 :     qCWarning(lcCardDav) << "REPORT failed:" << reply->errorString();
          +  -  +  -  +  
                -  +  + ]
     230   [ +  -  +  - ]:           2 :     emit syncFailed(reply->errorString());
     231                 :           2 :     return;
     232                 :             :   }
     233                 :             : 
     234         [ +  - ]:           8 :   QByteArray data = reply->readAll();
     235   [ +  -  +  -  :          16 :   qCDebug(lcCardDav) << "REPORT response:" << data.size() << "bytes";
          +  -  +  -  +  
                -  +  + ]
     236                 :             : 
     237                 :           8 :   int skipped = 0;
     238                 :           8 :   QList<Contact> contacts;
     239                 :           8 :   QString parseError;
     240   [ +  -  +  + ]:           8 :   if (!parseVCards(data, &contacts, &skipped, &parseError)) {
     241   [ +  -  +  -  :           2 :     qCWarning(lcCardDav) << "REPORT parse failed:" << parseError;
          +  -  +  -  +  
                      + ]
     242         [ +  - ]:           1 :     emit syncFailed(parseError);
     243                 :           1 :     return;
     244                 :             :   }
     245                 :             : 
     246   [ +  -  +  -  :          14 :   qCInfo(lcCardDav) << "Synced" << contacts.size() << "contacts,"
          +  -  +  -  +  
                -  +  + ]
     247   [ +  -  +  - ]:           7 :                     << skipped << "skipped (no email)";
     248         [ +  - ]:           7 :   emit contactsSynced(contacts, skipped);
     249   [ +  +  +  +  :          15 : }
             +  +  +  + ]
     250                 :             : 
     251                 :           7 : QList<Contact> CardDavClient::parseVCards(const QByteArray &data,
     252                 :             :                                           int *skippedOut) {
     253                 :           7 :   QList<Contact> contacts;
     254                 :           7 :   QString error;
     255         [ +  - ]:           7 :   parseVCards(data, &contacts, skippedOut, &error);
     256                 :           7 :   return contacts;
     257                 :           7 : }
     258                 :             : 
     259                 :          16 : bool CardDavClient::parseVCards(const QByteArray &data,
     260                 :             :                                 QList<Contact> *contacts,
     261                 :             :                                 int *skippedOut,
     262                 :             :                                 QString *error) {
     263         [ +  - ]:          16 :   if (contacts)
     264         [ +  - ]:          16 :     contacts->clear();
     265         [ -  + ]:          16 :   if (!contacts) {
     266         [ #  # ]:           0 :     if (error)
     267                 :           0 :       *error = QStringLiteral("Invalid parser output");
     268                 :           0 :     return false;
     269                 :             :   }
     270         [ +  + ]:          16 :   if (skippedOut)
     271                 :          12 :     *skippedOut = 0;
     272                 :             : 
     273                 :             :   // Parse the multistatus XML to extract vCard data and etags
     274         [ +  - ]:          16 :   QDomDocument doc;
     275                 :             :   // T-511: Check XML parse result
     276   [ +  -  +  + ]:          16 :   if (!doc.setContent(data)) {
     277   [ +  -  +  -  :           4 :     qCWarning(lcCardDav) << "Failed to parse XML response";
             +  -  +  + ]
     278         [ +  - ]:           2 :     if (error)
     279                 :           2 :       *error = QStringLiteral("Invalid XML response");
     280                 :           2 :     return false;
     281                 :             :   }
     282                 :             : 
     283                 :             :   QDomNodeList responses = findElementsByLocalNameDoc(doc,
     284         [ +  - ]:          14 :       QStringLiteral("response"));
     285                 :             : 
     286   [ +  -  +  -  :          28 :   qCDebug(lcCardDav) << "Found" << responses.count()
          +  -  +  -  +  
                -  +  + ]
     287         [ +  - ]:          14 :                      << "response elements in XML";
     288                 :             : 
     289                 :             :   // Regex for unfolding vCard lines (RFC 6350 §3.2: CRLF + space/tab)
     290                 :             :   static QRegularExpression foldingRegex(
     291   [ +  +  +  -  :          17 :       QStringLiteral("\\r?\\n[ \\t]"));
             +  -  -  - ]
     292                 :             : 
     293                 :             :   // Match EMAIL with optional item/group prefix:
     294                 :             :   //   "EMAIL;TYPE=...:addr"
     295                 :             :   //   "item1.EMAIL;TYPE=...:addr"
     296                 :             :   //   "X-GOOGLE-PROPERTY.EMAIL;...:addr"
     297                 :             :   //   etc.
     298                 :             :   static QRegularExpression emailRegex(
     299                 :           6 :       QStringLiteral(R"(^(?:[\w-]+\.)?EMAIL[^:]*:(.+)$)"),
     300                 :             :       QRegularExpression::MultilineOption |
     301   [ +  +  +  -  :          17 :           QRegularExpression::CaseInsensitiveOption);
             +  -  -  - ]
     302                 :             : 
     303                 :             :   static QRegularExpression uidRegex(
     304                 :           6 :       QStringLiteral(R"(^UID[^:]*:(.+)$)"),
     305   [ +  +  +  -  :          20 :       QRegularExpression::MultilineOption);
             +  -  -  - ]
     306                 :             :   static QRegularExpression fnRegex(
     307                 :           6 :       QStringLiteral(R"(^(?:[\w-]+\.)?FN[^:]*:(.+)$)"),
     308   [ +  +  +  -  :          20 :       QRegularExpression::MultilineOption);
             +  -  -  - ]
     309                 :             : 
     310                 :          14 :   int skippedNoVcard = 0;
     311                 :          14 :   int skippedNoEmail = 0;
     312                 :             : 
     313   [ +  -  +  + ]:          29 :   for (int i = 0; i < responses.count(); ++i) {
     314   [ +  -  +  - ]:          15 :     QDomElement resp = responses.at(i).toElement();
     315                 :             : 
     316                 :             :     // Get etag
     317                 :          15 :     QString etag;
     318                 :             :     QDomNodeList etagNodes = findElementsByLocalName(resp,
     319         [ +  - ]:          15 :         QStringLiteral("getetag"));
     320   [ +  -  +  + ]:          15 :     if (!etagNodes.isEmpty())
     321   [ +  -  +  -  :          11 :       etag = etagNodes.at(0).toElement().text();
                   +  - ]
     322                 :             : 
     323                 :             :     // Get address-data (vCard content)
     324                 :          15 :     QString vcard;
     325                 :             :     QDomNodeList addrNodes = findElementsByLocalName(resp,
     326         [ +  - ]:          15 :         QStringLiteral("address-data"));
     327   [ +  -  +  + ]:          15 :     if (addrNodes.isEmpty()) {
     328                 :           1 :       ++skippedNoVcard;
     329                 :           1 :       continue;
     330                 :             :     }
     331   [ +  -  +  -  :          14 :     vcard = addrNodes.at(0).toElement().text();
                   +  - ]
     332                 :             : 
     333         [ +  + ]:          14 :     if (vcard.isEmpty()) {
     334                 :           1 :       ++skippedNoVcard;
     335                 :           1 :       continue;
     336                 :             :     }
     337                 :             : 
     338                 :             :     // Unfold vCard lines (continuation lines start with space/tab)
     339         [ +  - ]:          13 :     vcard.replace(foldingRegex, QString());
     340                 :             : 
     341                 :             :     // Normalize CRLF to LF for consistent regex matching
     342         [ +  - ]:          26 :     vcard.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
     343         [ +  - ]:          13 :     vcard.replace(QLatin1Char('\r'), QLatin1Char('\n'));
     344                 :             : 
     345                 :             :     // Parse the vCard
     346         [ +  - ]:          13 :     auto uidMatch = uidRegex.match(vcard);
     347   [ +  -  +  -  :          34 :     QString uid = uidMatch.hasMatch() ? uidMatch.captured(1).trimmed()
             +  +  -  - ]
     348   [ +  +  +  - ]:          21 :                                       : QString();
     349                 :             : 
     350         [ +  - ]:          13 :     auto fnMatch = fnRegex.match(vcard);
     351                 :             :     QString fn =
     352   [ +  -  +  +  :          13 :         fnMatch.hasMatch() ? fnMatch.captured(1).trimmed() : QString();
          +  -  +  -  +  
                +  -  - ]
     353                 :             : 
     354                 :             :     // Collect all EMAIL entries
     355         [ +  - ]:          13 :     auto emailIt = emailRegex.globalMatch(vcard);
     356                 :          13 :     bool hasEmail = false;
     357   [ +  -  +  + ]:          26 :     while (emailIt.hasNext()) {
     358         [ +  - ]:          13 :       auto match = emailIt.next();
     359   [ +  -  +  -  :          13 :       QString email = match.captured(1).trimmed().toLower();
                   +  - ]
     360         [ +  + ]:          13 :       if (email.isEmpty())
     361                 :           1 :         continue;
     362                 :             : 
     363                 :          12 :       Contact c;
     364         [ +  + ]:          12 :       c.displayName = fn.isEmpty() ? email : fn;
     365                 :          12 :       c.email = email;
     366                 :          12 :       c.source = QStringLiteral("carddav");
     367                 :          12 :       c.cardDavUid = uid;
     368                 :          12 :       c.cardDavEtag = etag;
     369         [ +  - ]:          12 :       contacts->append(c);
     370                 :          12 :       hasEmail = true;
     371   [ +  +  +  + ]:          14 :     }
     372                 :             : 
     373         [ +  + ]:          13 :     if (!hasEmail) {
     374                 :           3 :       ++skippedNoEmail;
     375   [ +  -  +  -  :           6 :       qCDebug(lcCardDav) << "Skipped (no email):" << uid << fn;
          +  -  +  -  +  
                -  +  + ]
     376                 :             :     }
     377   [ +  +  +  +  :          23 :   }
          +  +  +  +  +  
                      + ]
     378                 :             : 
     379   [ +  -  +  -  :          28 :   qCDebug(lcCardDav) << "Parse results: contacts=" << contacts->size()
          +  -  +  -  +  
                      + ]
     380   [ +  -  +  - ]:          14 :                      << "skippedNoVcard=" << skippedNoVcard
     381   [ +  -  +  - ]:          14 :                      << "skippedNoEmail=" << skippedNoEmail
     382   [ +  -  +  -  :          14 :                      << "totalResponses=" << responses.count();
                   +  - ]
     383                 :             : 
     384         [ +  + ]:          14 :   if (skippedOut)
     385                 :          10 :     *skippedOut = skippedNoEmail;
     386                 :             : 
     387                 :          14 :   return true;
     388                 :          16 : }
        

Generated by: LCOV version 2.0-1