MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.2% 343 / 0 / 364
Functions: 100.0% 23 / 0 / 23
Branches: 74.7% 704 / 0 / 942

presentation/QuickInvitation.cpp
Line Branch Exec Source
1 #include "QuickInvitation.h"
2
3 #include "CalendarEventDetails.h"
4 #include "CalendarLayout.h"
5 #include "app/core/AccountSession.h"
6 #include "app/core/CalendarSyncCoordinator.h"
7 #include "app/core/CalendarSyncStatus.h"
8 #include "app/core/MailSessionCore.h"
9 #include "data/CalendarStore.h"
10 #include "data/ICalendar.h"
11 #include "data/MailCache.h"
12 #include "data/RecurrenceExpansion.h"
13 #include "service/Rfc2822Builder.h"
14 #include "util/SecurityLog.h"
15 #include "util/HostMatch.h"
16
17 #include <QLocale>
18 #include <QLoggingCategory>
19 #include <QSettings>
20 #include <QTimeZone>
21
22
4/7
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 8 taken 32 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
36 Q_LOGGING_CATEGORY(lcInvitation, "mailjd.quick.invitation")
23
24 namespace {
25
26 // PARTSTAT of `email` in a stored VEVENT text ("" when not listed).
27 60 QString statusIn(const QString &veventText, const QString &email) {
28
1/1
✓ Branch 5 → 6 taken 60 times.
60 const ICal::Component vevent = ICal::parseFirst(veventText, QStringLiteral("VEVENT"));
29
4/5
✓ Branch 11 → 12 taken 60 times.
✓ Branch 14 → 15 taken 60 times.
✓ Branch 15 → 16 taken 60 times.
✓ Branch 43 → 17 taken 60 times.
✗ Branch 43 → 44 not taken.
60 for (const ICal::Property &property : vevent.propertiesNamed(QStringLiteral("ATTENDEE")))
30
2/3
✓ Branch 18 → 19 taken 60 times.
✓ Branch 21 → 22 taken 60 times.
✗ Branch 21 → 41 not taken.
60 if (Imip::addressOf(property.value) == email) {
31
2/2
✓ Branch 25 → 26 taken 60 times.
✓ Branch 26 → 27 taken 60 times.
60 const QString status = property.parameter(QStringLiteral("PARTSTAT")).toUpper();
32
4/4
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 35 taken 58 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 37 → 39 taken 58 times.
62 return status.isEmpty() ? QStringLiteral("NEEDS-ACTION") : status;
33 120 }
34 ✗ return {};
35 60 }
36
37 } // namespace
38
39 803 QuickInvitation::QuickInvitation(MailSessionCore &session, Deps deps, QObject *parent)
40 803 : QObject(parent), m_session(session), m_deps(std::move(deps)) {
41
2/2
✓ Branch 11 → 12 taken 802 times.
✓ Branch 11 → 15 taken 1 time.
803 if (auto *sync = m_session.calendarSync())
42
1/1
✓ Branch 12 → 13 taken 802 times.
802 connect(sync, &CalendarSyncCoordinator::storeChanged, this, [this] {
43
2/2
✓ Branch 3 → 4 taken 184 times.
✓ Branch 3 → 5 taken 19 times.
203 if (!m_invitation) return;
44 19 rebuildCard();
45 19 emit changed();
46 });
47 803 }
48
49 166 QString QuickInvitation::replyMode() const {
50
3/3
✓ Branch 2 → 3 taken 166 times.
✓ Branch 11 → 12 taken 166 times.
✓ Branch 12 → 13 taken 166 times.
498 const QString mode = QSettings().value(QStringLiteral("calendar/imipReplies"), QStringLiteral("auto")).toString();
51
6/6
✓ Branch 22 → 23 taken 125 times.
✓ Branch 22 → 26 taken 41 times.
✓ Branch 25 → 26 taken 57 times.
✓ Branch 25 → 27 taken 68 times.
✓ Branch 31 → 32 taken 68 times.
✓ Branch 31 → 33 taken 98 times.
566 return mode == QLatin1String("client") || mode == QLatin1String("server") ? mode : QStringLiteral("auto");
52 166 }
53
54 11 void QuickInvitation::setReplyMode(const QString &mode) {
55
3/3
✓ Branch 2 → 3 taken 11 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 10 times.
11 if (mode == replyMode()) return;
56
2/2
✓ Branch 7 → 8 taken 10 times.
✓ Branch 13 → 14 taken 10 times.
20 QSettings().setValue(QStringLiteral("calendar/imipReplies"), mode);
57
7/7
✓ Branch 18 → 19 taken 10 times.
✓ Branch 22 → 23 taken 10 times.
✓ Branch 23 → 24 taken 10 times.
✓ Branch 24 → 25 taken 10 times.
✓ Branch 25 → 26 taken 10 times.
✓ Branch 30 → 20 taken 10 times.
✓ Branch 30 → 31 taken 10 times.
20 qCInfo(lcInvitation) << "Invitation replies:" << replyMode();
58
2/2
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 34 taken 8 times.
10 if (m_invitation) rebuildCard();
59 10 emit changed();
60 }
61
62 24 void QuickInvitation::showCalendarText(const QString &text) {
63 24 QString error;
64
1/1
✓ Branch 2 → 3 taken 24 times.
24 auto invitation = Imip::parse(text, &error);
65 // A calendar file with several appointments is no invitation: its
66 // attachment offers the import (S93.5.3).
67
2/2
✓ Branch 4 → 5 taken 23 times.
✓ Branch 4 → 25 taken 1 time.
24 if (invitation) {
68
4/4
✓ Branch 6 → 7 taken 23 times.
✓ Branch 7 → 8 taken 23 times.
✓ Branch 23 → 9 taken 25 times.
✓ Branch 23 → 24 taken 21 times.
46 for (const CalendarEvent &event : invitation->events)
69
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 21 taken 23 times.
25 if (event.uid != invitation->uid) {
70 2 error = QStringLiteral("several appointments (calendar file)");
71 2 invitation.reset();
72 2 break;
73 }
74 }
75
2/2
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 40 taken 21 times.
24 if (!invitation) {
76
6/6
✓ Branch 27 → 28 taken 3 times.
✓ Branch 31 → 32 taken 3 times.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 37 → 29 taken 3 times.
✓ Branch 37 → 38 taken 3 times.
6 qCInfo(lcInvitation) << "No invitation card for the calendar attachment:" << error;
77
1/1
✓ Branch 38 → 39 taken 3 times.
3 clear();
78 3 return;
79 }
80 21 m_invitation = std::move(invitation);
81 21 m_replyAutoApplied = false;
82
1/1
✓ Branch 42 → 43 taken 21 times.
21 rebuildCard();
83
3/3
✓ Branch 46 → 47 taken 3 times.
✓ Branch 46 → 48 taken 18 times.
✓ Branch 47 → 48 taken 3 times.
21 if (m_invitation->method == QLatin1String("REPLY")) applyReplyAutomatically();
84
1/1
✓ Branch 48 → 49 taken 21 times.
21 emit changed();
85 27 }
86
87 7 void QuickInvitation::clear() {
88 7 m_invitation.reset();
89 7 m_card.clear();
90 7 emit changed();
91 7 }
92
93 121 std::optional<QuickInvitation::Stored> QuickInvitation::storedCopy() const {
94 121 auto *sync = m_session.calendarSync();
95
7/10
✓ Branch 4 → 5 taken 120 times.
✓ Branch 4 → 11 taken 1 time.
✓ Branch 5 → 6 taken 120 times.
✗ Branch 5 → 11 not taken.
✓ Branch 7 → 8 taken 120 times.
✗ Branch 7 → 11 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 120 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 120 times.
121 if (!m_invitation || !sync || !sync->store() || !sync->store()->isOpen()) return std::nullopt;
96 120 CalendarStore *store = sync->store();
97
5/5
✓ Branch 17 → 18 taken 120 times.
✓ Branch 18 → 19 taken 120 times.
✓ Branch 19 → 20 taken 120 times.
✓ Branch 52 → 21 taken 79 times.
✓ Branch 52 → 53 taken 41 times.
120 for (const auto &[accountId, path] : store->calendarsWithUid(m_invitation->uid)) {
98
1/1
✓ Branch 26 → 27 taken 79 times.
79 auto row = store->occurrenceEvent(m_invitation->uid, accountId, path, m_invitation->event.recurrenceId);
99
1/3
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 36 taken 79 times.
✗ Branch 31 → 32 not taken.
79 if (!row) row = store->occurrenceEvent(m_invitation->uid, accountId, path, {});
100
1/2
✓ Branch 37 → 38 taken 79 times.
✗ Branch 37 → 45 not taken.
79 if (row) return Stored{accountId, path, *row};
101 199 }
102 41 return std::nullopt;
103 }
104
105 207 QStringList QuickInvitation::ownAddresses() const {
106 207 QStringList addresses;
107
3/4
✓ Branch 5 → 6 taken 207 times.
✗ Branch 5 → 11 not taken.
✓ Branch 7 → 8 taken 207 times.
✓ Branch 8 → 9 taken 207 times.
207 if (const auto &primary = m_session.accounts()->primary()) addresses << primary->email.toLower();
108
4/4
✓ Branch 13 → 14 taken 207 times.
✓ Branch 14 → 15 taken 207 times.
✓ Branch 42 → 16 taken 207 times.
✓ Branch 42 → 43 taken 207 times.
414 for (const QVariant &account : m_session.accounts()->availableAccounts()) {
109
4/4
✓ Branch 17 → 18 taken 207 times.
✓ Branch 22 → 23 taken 207 times.
✓ Branch 23 → 24 taken 207 times.
✓ Branch 24 → 25 taken 207 times.
414 const QString email = account.toMap().value(QStringLiteral("email")).toString().toLower();
110
3/7
✓ Branch 32 → 33 taken 207 times.
✗ Branch 32 → 36 not taken.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 207 times.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 207 times.
✗ Branch 38 → 39 not taken.
207 if (!email.isEmpty() && !addresses.contains(email)) addresses << email;
111 414 }
112 207 return addresses;
113 ✗ }
114
115 131 QString QuickInvitation::ownAddress() const {
116
1/1
✓ Branch 2 → 3 taken 131 times.
131 const QStringList addresses = ownAddresses();
117
3/6
✓ Branch 4 → 5 taken 131 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 131 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 131 times.
131 if (!m_invitation || addresses.isEmpty()) return {};
118 // The active account first: it sends the reply.
119
5/7
✓ Branch 15 → 16 taken 131 times.
✓ Branch 18 → 19 taken 131 times.
✓ Branch 18 → 20 taken 131 times.
✓ Branch 20 → 21 taken 109 times.
✓ Branch 20 → 23 taken 22 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
262 if (Imip::attendeeIndex(*m_invitation, {addresses.first()}) >= 0) return addresses.first();
120
1/1
✓ Branch 24 → 25 taken 22 times.
22 const int index = Imip::attendeeIndex(*m_invitation, addresses);
121
1/2
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 29 taken 22 times.
22 return index >= 0 ? m_invitation->attendees.at(index).email : QString();
122 262 }
123
124 109 bool QuickInvitation::serverSends(const QString &accountId) const {
125
1/1
✓ Branch 2 → 3 taken 109 times.
109 const QString mode = replyMode();
126
2/2
✓ Branch 5 → 6 taken 76 times.
✓ Branch 5 → 9 taken 33 times.
109 if (mode != QLatin1String("auto")) return mode == QLatin1String("server");
127
3/5
✓ Branch 10 → 11 taken 33 times.
✗ Branch 10 → 14 not taken.
✓ Branch 11 → 12 taken 33 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 33 times.
33 return !accountId.isEmpty() && CalendarSyncStatus::serverScheduling(accountId);
128 109 }
129
130 101 bool QuickInvitation::resolveCalendar(const QString &key, QString *accountId, QString *path) const {
131
12/20
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 10 taken 93 times.
✓ Branch 8 → 9 taken 8 times.
✓ Branch 9 → 11 taken 8 times.
✓ Branch 11 → 12 taken 8 times.
✓ Branch 11 → 13 taken 93 times.
✓ Branch 13 → 14 taken 8 times.
✓ Branch 13 → 15 taken 93 times.
✓ Branch 15 → 16 taken 8 times.
✓ Branch 15 → 17 taken 93 times.
✓ Branch 17 → 18 taken 8 times.
✓ Branch 17 → 19 taken 93 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
✗ Branch 106 → 107 not taken.
✗ Branch 106 → 108 not taken.
✗ Branch 110 → 111 not taken.
✗ Branch 110 → 112 not taken.
✗ Branch 114 → 115 not taken.
✗ Branch 114 → 116 not taken.
109 const QString wanted = key.isEmpty() ? m_card.value(QStringLiteral("defaultCalendar")).toString() : key;
132
5/5
✓ Branch 19 → 20 taken 101 times.
✓ Branch 20 → 21 taken 101 times.
✓ Branch 21 → 22 taken 101 times.
✓ Branch 92 → 23 taken 193 times.
✓ Branch 92 → 93 taken 2 times.
195 for (const QVariant &value : m_deps.calendarChoices()) {
133
1/1
✓ Branch 24 → 25 taken 193 times.
193 const QVariantMap choice = value.toMap();
134
23/46
✓ Branch 29 → 30 taken 193 times.
✓ Branch 30 → 31 taken 193 times.
✓ Branch 32 → 33 taken 101 times.
✓ Branch 32 → 40 taken 92 times.
✓ Branch 37 → 38 taken 101 times.
✓ Branch 38 → 39 taken 101 times.
✓ Branch 39 → 40 taken 2 times.
✓ Branch 39 → 41 taken 99 times.
✓ Branch 42 → 43 taken 101 times.
✓ Branch 42 → 44 taken 92 times.
✓ Branch 44 → 45 taken 101 times.
✓ Branch 44 → 46 taken 92 times.
✓ Branch 46 → 47 taken 101 times.
✓ Branch 46 → 48 taken 92 times.
✓ Branch 48 → 49 taken 101 times.
✓ Branch 48 → 50 taken 92 times.
✓ Branch 50 → 51 taken 193 times.
✗ Branch 50 → 52 not taken.
✓ Branch 52 → 53 taken 193 times.
✗ Branch 52 → 54 not taken.
✓ Branch 54 → 55 taken 193 times.
✗ Branch 54 → 56 not taken.
✓ Branch 56 → 57 taken 193 times.
✗ Branch 56 → 58 not taken.
✓ Branch 58 → 59 taken 193 times.
✗ Branch 58 → 60 not taken.
✓ Branch 60 → 61 taken 94 times.
✓ Branch 60 → 62 taken 99 times.
✗ Branch 118 → 119 not taken.
✗ Branch 118 → 120 not taken.
✗ Branch 122 → 123 not taken.
✗ Branch 122 → 124 not taken.
✗ Branch 126 → 127 not taken.
✗ Branch 126 → 128 not taken.
✗ Branch 130 → 131 not taken.
✗ Branch 130 → 132 not taken.
✗ Branch 134 → 135 not taken.
✗ Branch 134 → 136 not taken.
✗ Branch 138 → 139 not taken.
✗ Branch 138 → 140 not taken.
✗ Branch 142 → 143 not taken.
✗ Branch 142 → 144 not taken.
✗ Branch 146 → 147 not taken.
✗ Branch 146 → 148 not taken.
✗ Branch 150 → 151 not taken.
✗ Branch 150 → 152 not taken.
487 if (choice.value(QStringLiteral("key")).toString() != wanted || choice.value(QStringLiteral("readOnly")).toBool())
135 94 continue;
136
2/2
✓ Branch 66 → 67 taken 99 times.
✓ Branch 67 → 68 taken 99 times.
198 *accountId = choice.value(QStringLiteral("accountId")).toString();
137
2/2
✓ Branch 78 → 79 taken 99 times.
✓ Branch 79 → 80 taken 99 times.
198 *path = choice.value(QStringLiteral("path")).toString();
138 99 return true;
139 294 }
140 2 return false;
141 101 }
142
143 68 QStringList QuickInvitation::conflicts() const {
144 68 QStringList result;
145 68 auto *sync = m_session.calendarSync();
146 68 const CalendarEvent &event = m_invitation->event;
147
8/11
✓ Branch 4 → 5 taken 68 times.
✗ Branch 4 → 10 not taken.
✓ Branch 6 → 7 taken 68 times.
✗ Branch 6 → 10 not taken.
✓ Branch 7 → 8 taken 64 times.
✓ Branch 7 → 10 taken 4 times.
✓ Branch 8 → 9 taken 64 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 64 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 64 times.
68 if (!sync || !sync->store() || event.allDay || !event.dtStart.isValid()) return result;
148
1/1
✓ Branch 14 → 15 taken 64 times.
64 const QDateTime start = event.dtStart.toLocalTime();
149
3/5
✓ Branch 15 → 16 taken 64 times.
✓ Branch 16 → 17 taken 64 times.
✗ Branch 16 → 18 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 19 → 20 taken 64 times.
64 const QDateTime end = (event.dtEnd.isValid() ? event.dtEnd : event.dtStart.addSecs(3600)).toLocalTime();
150
4/4
✓ Branch 25 → 26 taken 64 times.
✓ Branch 26 → 27 taken 64 times.
✓ Branch 27 → 28 taken 64 times.
✓ Branch 28 → 29 taken 64 times.
128 const QList<CalendarEvent> rows = sync->store()->eventsForDateRange(QDateTime(start.date().addDays(-1), QTime(0, 0)),
151
4/4
✓ Branch 22 → 23 taken 64 times.
✓ Branch 23 → 24 taken 64 times.
✓ Branch 24 → 25 taken 64 times.
✓ Branch 29 → 30 taken 64 times.
192 QDateTime(end.date(), QTime(23, 59, 59)));
152 64 for (const CalendarEvent &other :
153
7/7
✓ Branch 33 → 34 taken 64 times.
✓ Branch 34 → 35 taken 64 times.
✓ Branch 35 → 36 taken 64 times.
✓ Branch 37 → 38 taken 64 times.
✓ Branch 38 → 39 taken 64 times.
✓ Branch 90 → 40 taken 70 times.
✓ Branch 90 → 91 taken 64 times.
198 expandEventsForDisplay(rows, start.date(), end.date(), false, {kDisplayOccurrencesPerSeries, {}})) {
154
7/9
✓ Branch 42 → 43 taken 27 times.
✓ Branch 42 → 46 taken 43 times.
✓ Branch 43 → 44 taken 27 times.
✗ Branch 43 → 46 not taken.
✓ Branch 44 → 45 taken 27 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 27 times.
✓ Branch 48 → 49 taken 43 times.
✓ Branch 48 → 50 taken 27 times.
70 if (other.uid == event.uid || other.allDay || !other.dtStart.isValid()) continue;
155
2/3
✓ Branch 50 → 51 taken 27 times.
✓ Branch 51 → 52 taken 27 times.
✗ Branch 51 → 53 not taken.
27 const QDateTime otherEnd = other.dtEnd.isValid() ? other.dtEnd : other.dtStart;
156
5/8
✓ Branch 55 → 56 taken 27 times.
✓ Branch 57 → 58 taken 27 times.
✗ Branch 57 → 61 not taken.
✓ Branch 58 → 59 taken 27 times.
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 27 times.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 27 times.
27 if (other.dtStart >= end || otherEnd <= start) continue;
157
1/1
✓ Branch 65 → 66 taken 27 times.
27 result << tr("Overlaps with %1 %2–%3")
158
3/3
✓ Branch 68 → 69 taken 27 times.
✓ Branch 69 → 70 taken 27 times.
✓ Branch 70 → 71 taken 27 times.
54 .arg(other.summary, CalendarLayout::timeText(other.dtStart.toLocalTime()),
159
3/3
✓ Branch 66 → 67 taken 27 times.
✓ Branch 67 → 68 taken 27 times.
✓ Branch 71 → 72 taken 27 times.
81 CalendarLayout::timeText(otherEnd.toLocalTime()));
160
1/2
✗ Branch 79 → 80 not taken.
✓ Branch 79 → 81 taken 27 times.
27 if (result.size() == 3) break;
161 91 }
162 64 return result;
163 128 }
164
165 90 QVariantList QuickInvitation::attendeeList(const QString &me) const {
166 90 QVariantList list;
167
2/2
✓ Branch 53 → 5 taken 150 times.
✓ Branch 53 → 54 taken 90 times.
240 for (const Imip::Attendee &attendee : m_invitation->attendees)
168
1/1
✓ Branch 35 → 36 taken 150 times.
450 list.append(QVariantMap{{QStringLiteral("name"), attendee.name.isEmpty() ? attendee.email : attendee.name},
169 300 {QStringLiteral("email"), attendee.email},
170 300 {QStringLiteral("status"), attendee.partstat},
171 300 {QStringLiteral("optional"), attendee.role == QLatin1String("OPT-PARTICIPANT")},
172
2/4
✓ Branch 39 → 40 taken 750 times.
✓ Branch 39 → 41 taken 150 times.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 63 not taken.
1200 {QStringLiteral("me"), attendee.email == me}});
173 90 return list;
174 900 }
175
176 90 void QuickInvitation::rebuildCard() {
177 90 const Imip::Invitation &invitation = *m_invitation;
178 90 const CalendarEvent &event = invitation.event;
179
1/1
✓ Branch 3 → 4 taken 90 times.
90 const auto stored = storedCopy();
180
1/1
✓ Branch 4 → 5 taken 90 times.
90 const QString me = ownAddress();
181 90 QVariantMap card;
182
1/1
✓ Branch 9 → 10 taken 90 times.
180 card[QStringLiteral("method")] = invitation.method;
183
4/4
✓ Branch 15 → 16 taken 4 times.
✓ Branch 15 → 17 taken 86 times.
✓ Branch 16 → 18 taken 4 times.
✓ Branch 22 → 23 taken 90 times.
180 card[QStringLiteral("title")] = event.summary.isEmpty() ? tr("(No title)") : event.summary;
184
2/2
✓ Branch 28 → 29 taken 90 times.
✓ Branch 33 → 34 taken 90 times.
180 card[QStringLiteral("whenText")] = CalendarEventDetails::whenText(event);
185 // The organizer's wall clock when it differs from ours.
186
4/5
✓ Branch 39 → 40 taken 86 times.
✓ Branch 39 → 47 taken 4 times.
✓ Branch 40 → 41 taken 86 times.
✓ Branch 41 → 42 taken 86 times.
✗ Branch 41 → 47 not taken.
176 if (!event.allDay && event.dtStart.timeSpec() == Qt::TimeZone &&
187
9/11
✓ Branch 42 → 43 taken 86 times.
✓ Branch 43 → 44 taken 86 times.
✓ Branch 44 → 45 taken 86 times.
✓ Branch 45 → 46 taken 64 times.
✓ Branch 45 → 47 taken 22 times.
✓ Branch 48 → 49 taken 86 times.
✓ Branch 48 → 50 taken 4 times.
✓ Branch 50 → 51 taken 64 times.
✓ Branch 50 → 73 taken 26 times.
✗ Branch 868 → 869 not taken.
✗ Branch 868 → 870 not taken.
176 event.dtStart.offsetFromUtc() != event.dtStart.toLocalTime().offsetFromUtc())
188
4/4
✓ Branch 51 → 52 taken 64 times.
✓ Branch 55 → 56 taken 64 times.
✓ Branch 56 → 57 taken 64 times.
✓ Branch 61 → 62 taken 64 times.
320 card[QStringLiteral("zoneText")] = tr("%1 in %2").arg(CalendarLayout::timeText(event.dtStart),
189
3/3
✓ Branch 52 → 53 taken 64 times.
✓ Branch 53 → 54 taken 64 times.
✓ Branch 54 → 55 taken 64 times.
320 QString::fromUtf8(event.dtStart.timeZone().id()));
190
1/1
✓ Branch 77 → 78 taken 90 times.
180 card[QStringLiteral("location")] = event.location;
191
1/1
✓ Branch 86 → 87 taken 90 times.
180 card[QStringLiteral("meetingUrl")] = invitation.meetingUrl;
192
1/1
✓ Branch 91 → 92 taken 90 times.
90 const auto meeting = QUrl(invitation.meetingUrl);
193
3/3
✓ Branch 93 → 94 taken 90 times.
✓ Branch 94 → 95 taken 90 times.
✓ Branch 99 → 100 taken 90 times.
180 card[QStringLiteral("meetingHost")] = HostMatch::canonicalName(meeting.host());
194
2/2
✓ Branch 106 → 107 taken 90 times.
✓ Branch 111 → 112 taken 90 times.
180 card[QStringLiteral("meetingKnown")] = HostMatch::isKnownMeetingUrl(meeting);
195
1/1
✓ Branch 127 → 128 taken 90 times.
270 card[QStringLiteral("organizer")] = invitation.organizerName.isEmpty()
196
2/2
✓ Branch 117 → 118 taken 22 times.
✓ Branch 117 → 119 taken 68 times.
180 ? invitation.organizerEmail
197
5/9
✓ Branch 122 → 123 taken 68 times.
✓ Branch 133 → 134 taken 68 times.
✓ Branch 133 → 135 taken 22 times.
✓ Branch 135 → 136 taken 68 times.
✓ Branch 135 → 137 taken 22 times.
✗ Branch 952 → 953 not taken.
✗ Branch 952 → 954 not taken.
✗ Branch 956 → 957 not taken.
✗ Branch 956 → 958 not taken.
360 : QStringLiteral("%1 <%2>").arg(invitation.organizerName, invitation.organizerEmail);
198
2/2
✓ Branch 137 → 138 taken 90 times.
✓ Branch 142 → 143 taken 90 times.
180 card[QStringLiteral("attendees")] = attendeeList(me);
199
1/1
✓ Branch 152 → 153 taken 90 times.
180 card[QStringLiteral("rrule")] = event.rrule;
200
3/3
✓ Branch 157 → 158 taken 90 times.
✓ Branch 158 → 159 taken 90 times.
✓ Branch 163 → 164 taken 90 times.
180 card[QStringLiteral("startIso")] = event.dtStart.toUTC().toString(Qt::ISODate);
201
4/4
✓ Branch 170 → 171 taken 90 times.
✓ Branch 171 → 172 taken 90 times.
✓ Branch 172 → 173 taken 90 times.
✓ Branch 177 → 178 taken 90 times.
180 card[QStringLiteral("dateIso")] = event.dtStart.toLocalTime().date().toString(Qt::ISODate);
202
1/1
✓ Branch 188 → 189 taken 90 times.
180 card[QStringLiteral("uid")] = invitation.uid;
203
1/1
✓ Branch 197 → 198 taken 90 times.
180 card[QStringLiteral("comment")] = invitation.comment;
204
1/1
✓ Branch 207 → 208 taken 90 times.
180 card[QStringLiteral("inCalendar")] = stored.has_value();
205 // F-52: an occurrence-only mail (RECURRENCE-ID) can only merge into a
206 // stored series — without one, the card offers neither the answer nor
207 // the add/remove buttons.
208
1/1
✓ Branch 222 → 223 taken 90 times.
180 card[QStringLiteral("occurrenceSeriesMissing")] =
209
2/5
✓ Branch 212 → 213 taken 90 times.
✗ Branch 213 → 214 not taken.
✓ Branch 213 → 217 taken 90 times.
✗ Branch 215 → 216 not taken.
✗ Branch 215 → 217 not taken.
270 event.recurrenceId.isValid() && !stored.has_value();
210
3/3
✓ Branch 228 → 229 taken 61 times.
✓ Branch 228 → 231 taken 29 times.
✓ Branch 232 → 233 taken 90 times.
90 const auto consistency = Imip::consistency(invitation, stored ? &stored->master : nullptr, m_mailSender);
211 90 QString trust = QStringLiteral("ok");
212
2/2
✓ Branch 237 → 238 taken 9 times.
✓ Branch 237 → 245 taken 81 times.
99 if (!consistency.organizerMatches) trust = QStringLiteral("organizerChanged");
213
4/6
✓ Branch 247 → 248 taken 9 times.
✓ Branch 247 → 250 taken 72 times.
✗ Branch 248 → 249 not taken.
✓ Branch 248 → 250 taken 9 times.
✗ Branch 251 → 252 not taken.
✓ Branch 251 → 259 taken 81 times.
81 else if (invitation.method == QLatin1String("REPLY") && !consistency.replierIsAttendee)
214 ✗ trust = QStringLiteral("replierUnknown");
215
4/4
✓ Branch 261 → 262 taken 9 times.
✓ Branch 261 → 263 taken 72 times.
✓ Branch 264 → 265 taken 27 times.
✓ Branch 264 → 272 taken 54 times.
81 else if (invitation.method == QLatin1String("REPLY") ? !consistency.senderIsReplier : !consistency.senderIsOrganizer)
216 27 trust = QStringLiteral("senderUnknown");
217
1/1
✓ Branch 276 → 277 taken 90 times.
180 card[QStringLiteral("trust")] = trust;
218
4/4
✓ Branch 282 → 283 taken 4 times.
✓ Branch 282 → 284 taken 86 times.
✓ Branch 283 → 285 taken 4 times.
✓ Branch 289 → 290 taken 90 times.
180 card[QStringLiteral("mailSender")] = m_mailSender.isEmpty() ? tr("an unknown sender") : m_mailSender;
219
14/24
✓ Branch 296 → 297 taken 61 times.
✓ Branch 296 → 306 taken 29 times.
✓ Branch 301 → 302 taken 61 times.
✓ Branch 305 → 307 taken 61 times.
✓ Branch 307 → 308 taken 61 times.
✓ Branch 307 → 309 taken 29 times.
✓ Branch 309 → 310 taken 61 times.
✓ Branch 309 → 311 taken 29 times.
✓ Branch 311 → 312 taken 61 times.
✓ Branch 311 → 313 taken 29 times.
✓ Branch 313 → 314 taken 61 times.
✓ Branch 313 → 315 taken 29 times.
✓ Branch 315 → 316 taken 61 times.
✓ Branch 315 → 317 taken 29 times.
✗ Branch 1071 → 1072 not taken.
✗ Branch 1071 → 1073 not taken.
✗ Branch 1075 → 1076 not taken.
✗ Branch 1075 → 1077 not taken.
✗ Branch 1079 → 1080 not taken.
✗ Branch 1079 → 1081 not taken.
✗ Branch 1083 → 1084 not taken.
✗ Branch 1083 → 1085 not taken.
✗ Branch 1087 → 1088 not taken.
✗ Branch 1087 → 1089 not taken.
212 const auto storedOrganizer = stored ? ICal::parseFirst(stored->master.icalRaw, QStringLiteral("VEVENT")).value(QStringLiteral("ORGANIZER")) : QString();
220
2/2
✓ Branch 317 → 318 taken 90 times.
✓ Branch 322 → 323 taken 90 times.
180 card[QStringLiteral("storedOrganizer")] = Imip::addressOf(storedOrganizer);
221
5/5
✓ Branch 329 → 330 taken 61 times.
✓ Branch 329 → 334 taken 29 times.
✓ Branch 332 → 333 taken 25 times.
✓ Branch 332 → 334 taken 36 times.
✓ Branch 339 → 340 taken 90 times.
180 card[QStringLiteral("requiresConfirmation")] = stored && trust != QLatin1String("ok");
222 90 QString state;
223 90 const QString method = invitation.method;
224
5/6
✓ Branch 347 → 348 taken 27 times.
✓ Branch 347 → 351 taken 63 times.
✗ Branch 350 → 351 not taken.
✓ Branch 350 → 352 taken 27 times.
✓ Branch 353 → 354 taken 63 times.
✓ Branch 353 → 399 taken 27 times.
90 if (method == QLatin1String("REQUEST") || method == QLatin1String("ADD")) {
225 63 state = QStringLiteral("new");
226
2/2
✓ Branch 361 → 362 taken 43 times.
✓ Branch 361 → 524 taken 20 times.
63 if (stored) {
227
1/1
✓ Branch 363 → 364 taken 43 times.
43 const int storedSequence = Imip::sequenceOf(stored->master.icalRaw);
228
2/2
✓ Branch 364 → 365 taken 10 times.
✓ Branch 364 → 368 taken 33 times.
139 state = invitation.sequence > storedSequence ? QStringLiteral("updated")
229
8/8
✓ Branch 368 → 369 taken 2 times.
✓ Branch 368 → 372 taken 31 times.
✓ Branch 377 → 378 taken 31 times.
✓ Branch 377 → 379 taken 12 times.
✓ Branch 379 → 380 taken 2 times.
✓ Branch 379 → 381 taken 41 times.
✓ Branch 381 → 382 taken 10 times.
✓ Branch 381 → 383 taken 33 times.
119 : invitation.sequence < storedSequence ? QStringLiteral("outdated") : QStringLiteral("answered");
230
2/2
✓ Branch 385 → 386 taken 10 times.
✓ Branch 385 → 524 taken 33 times.
43 if (state == QLatin1String("updated"))
231
2/2
✓ Branch 387 → 388 taken 10 times.
✓ Branch 392 → 393 taken 10 times.
20 card[QStringLiteral("changes")] = Imip::changedFields(stored->master, event);
232 }
233
2/2
✓ Branch 401 → 402 taken 13 times.
✓ Branch 401 → 409 taken 14 times.
27 } else if (method == QLatin1String("CANCEL")) {
234 13 state = QStringLiteral("cancelled");
235
2/2
✓ Branch 411 → 412 taken 9 times.
✓ Branch 411 → 507 taken 5 times.
14 } else if (method == QLatin1String("REPLY")) {
236 9 state = QStringLiteral("reply");
237
1/2
✓ Branch 419 → 420 taken 9 times.
✗ Branch 419 → 524 not taken.
9 if (!invitation.attendees.isEmpty()) {
238 9 const Imip::Attendee &replier = invitation.attendees.first();
239
2/3
✗ Branch 422 → 423 not taken.
✓ Branch 422 → 424 taken 9 times.
✓ Branch 429 → 430 taken 9 times.
18 card[QStringLiteral("replier")] = replier.name.isEmpty() ? replier.email : replier.name;
240
1/1
✓ Branch 438 → 439 taken 9 times.
18 card[QStringLiteral("replierStatus")] = replier.partstat;
241
1/1
✓ Branch 455 → 456 taken 9 times.
18 card[QStringLiteral("replyApplied")] =
242
5/9
✓ Branch 444 → 445 taken 9 times.
✗ Branch 444 → 450 not taken.
✓ Branch 446 → 447 taken 9 times.
✓ Branch 448 → 449 taken 4 times.
✓ Branch 448 → 450 taken 5 times.
✓ Branch 460 → 461 taken 9 times.
✗ Branch 460 → 462 not taken.
✗ Branch 1151 → 1152 not taken.
✗ Branch 1151 → 1153 not taken.
27 stored && statusIn(stored->master.icalRaw, replier.email) == replier.partstat;
243
1/1
✓ Branch 466 → 467 taken 9 times.
18 card[QStringLiteral("autoApplied")] = m_replyAutoApplied; // S108.8.7 (QuickInvitationMail.cpp)
244
6/8
✓ Branch 471 → 472 taken 2 times.
✓ Branch 471 → 473 taken 7 times.
✓ Branch 472 → 488 taken 2 times.
✓ Branch 476 → 477 taken 7 times.
✗ Branch 477 → 478 not taken.
✓ Branch 477 → 487 taken 7 times.
✗ Branch 485 → 486 not taken.
✓ Branch 492 → 493 taken 9 times.
43 card[QStringLiteral("replyNote")] = m_replyAutoApplied ? tr("calendar updated") : stored && serverSends(stored->accountId)
245
5/22
✓ Branch 474 → 475 taken 7 times.
✗ Branch 474 → 487 not taken.
✗ Branch 482 → 483 not taken.
✗ Branch 483 → 484 not taken.
✗ Branch 484 → 485 not taken.
✗ Branch 484 → 487 not taken.
✗ Branch 498 → 499 not taken.
✓ Branch 498 → 500 taken 9 times.
✗ Branch 500 → 501 not taken.
✓ Branch 500 → 502 taken 9 times.
✗ Branch 502 → 503 not taken.
✓ Branch 502 → 504 taken 9 times.
✗ Branch 504 → 505 not taken.
✓ Branch 504 → 506 taken 9 times.
✗ Branch 1175 → 1176 not taken.
✗ Branch 1175 → 1177 not taken.
✗ Branch 1179 → 1180 not taken.
✗ Branch 1179 → 1181 not taken.
✗ Branch 1183 → 1184 not taken.
✗ Branch 1183 → 1185 not taken.
✗ Branch 1187 → 1188 not taken.
✗ Branch 1187 → 1189 not taken.
43 && card.value(QStringLiteral("replyApplied")).toBool() ? tr("already recorded by your server") : QString();
246 }
247
2/2
✓ Branch 509 → 510 taken 1 time.
✓ Branch 509 → 517 taken 4 times.
5 } else if (method == QLatin1String("COUNTER")) {
248 1 state = QStringLiteral("counter");
249 } else {
250 4 state = QStringLiteral("publish");
251 }
252
1/1
✓ Branch 528 → 529 taken 90 times.
180 card[QStringLiteral("state")] = state;
253
6/6
✓ Branch 534 → 535 taken 61 times.
✓ Branch 534 → 540 taken 29 times.
✓ Branch 536 → 537 taken 48 times.
✓ Branch 536 → 540 taken 13 times.
✓ Branch 538 → 539 taken 48 times.
✓ Branch 552 → 553 taken 90 times.
378 card[QStringLiteral("myStatus")] = stored && !me.isEmpty() ? statusIn(stored->master.icalRaw, me)
254
2/2
✓ Branch 541 → 542 taken 18 times.
✓ Branch 541 → 543 taken 24 times.
42 : me.isEmpty() ? QString()
255
7/13
✓ Branch 545 → 546 taken 24 times.
✓ Branch 558 → 559 taken 24 times.
✓ Branch 558 → 560 taken 66 times.
✓ Branch 560 → 561 taken 24 times.
✓ Branch 560 → 564 taken 66 times.
✓ Branch 562 → 563 taken 24 times.
✓ Branch 562 → 564 taken 24 times.
✗ Branch 1212 → 1213 not taken.
✗ Branch 1212 → 1214 not taken.
✗ Branch 1216 → 1217 not taken.
✗ Branch 1216 → 1220 not taken.
✗ Branch 1218 → 1219 not taken.
✗ Branch 1218 → 1220 not taken.
384 : invitation.attendees.at(Imip::attendeeIndex(invitation, {me})).partstat;
256
1/1
✓ Branch 568 → 569 taken 90 times.
180 card[QStringLiteral("replyAs")] = me;
257 90 const bool hasAccount = m_session.accounts()->primary().has_value();
258
5/7
✓ Branch 576 → 577 taken 90 times.
✗ Branch 576 → 585 not taken.
✓ Branch 578 → 579 taken 72 times.
✓ Branch 578 → 585 taken 18 times.
✓ Branch 580 → 581 taken 72 times.
✗ Branch 580 → 585 not taken.
✓ Branch 590 → 591 taken 90 times.
342 card[QStringLiteral("canReply")] = hasAccount && !me.isEmpty() && !invitation.organizerEmail.isEmpty() &&
259
4/7
✓ Branch 581 → 582 taken 72 times.
✓ Branch 583 → 584 taken 72 times.
✗ Branch 583 → 585 not taken.
✓ Branch 595 → 596 taken 72 times.
✓ Branch 595 → 597 taken 18 times.
✗ Branch 1247 → 1248 not taken.
✗ Branch 1247 → 1249 not taken.
342 !ownAddresses().contains(invitation.organizerEmail);
260
8/8
✓ Branch 599 → 600 taken 81 times.
✓ Branch 599 → 604 taken 9 times.
✓ Branch 602 → 603 taken 68 times.
✓ Branch 602 → 604 taken 13 times.
✓ Branch 605 → 606 taken 68 times.
✓ Branch 605 → 618 taken 22 times.
✓ Branch 606 → 607 taken 68 times.
✓ Branch 611 → 612 taken 68 times.
158 if (state != QLatin1String("reply") && state != QLatin1String("cancelled")) card[QStringLiteral("conflicts")] = conflicts();
261 // Writable calendars; the stored copy's or the last used one is the default.
262 90 QVariantList calendars;
263 90 QString defaultKey;
264 ✗ const QString lastKey = [this] {
265
2/2
✓ Branch 2 → 3 taken 90 times.
✓ Branch 6 → 7 taken 90 times.
180 const QStringList parts = m_deps.lastCalendar().split(QLatin1Char('\n'));
266
3/3
✓ Branch 9 → 10 taken 82 times.
✓ Branch 9 → 13 taken 8 times.
✓ Branch 12 → 14 taken 82 times.
180 return parts.size() == 2 ? calendarSelectionKey(parts.at(0), parts.at(1)) : QString();
267
1/1
✓ Branch 618 → 619 taken 90 times.
180 }();
268
5/5
✓ Branch 619 → 620 taken 90 times.
✓ Branch 620 → 621 taken 90 times.
✓ Branch 621 → 622 taken 90 times.
✓ Branch 734 → 623 taken 172 times.
✓ Branch 734 → 735 taken 90 times.
262 for (const QVariant &value : m_deps.calendarChoices()) {
269
1/1
✓ Branch 624 → 625 taken 172 times.
172 const QVariantMap choice = value.toMap();
270
4/4
✓ Branch 629 → 630 taken 172 times.
✓ Branch 630 → 631 taken 172 times.
✓ Branch 635 → 636 taken 82 times.
✓ Branch 635 → 637 taken 90 times.
344 if (choice.value(QStringLiteral("readOnly")).toBool()) continue;
271
2/2
✓ Branch 644 → 645 taken 90 times.
✓ Branch 666 → 667 taken 90 times.
360 calendars.append(QVariantMap{{QStringLiteral("key"), choice.value(QStringLiteral("key"))},
272
1/1
✓ Branch 653 → 654 taken 90 times.
270 {QStringLiteral("displayName"), choice.value(QStringLiteral("displayName"))},
273
3/5
✓ Branch 662 → 663 taken 90 times.
✓ Branch 670 → 671 taken 270 times.
✓ Branch 670 → 672 taken 90 times.
✗ Branch 1282 → 1283 not taken.
✗ Branch 1282 → 1284 not taken.
540 {QStringLiteral("color"), choice.value(QStringLiteral("color"))}});
274
7/28
✗ Branch 691 → 692 not taken.
✓ Branch 691 → 700 taken 90 times.
✗ Branch 696 → 697 not taken.
✗ Branch 697 → 698 not taken.
✗ Branch 699 → 700 not taken.
✗ Branch 699 → 701 not taken.
✗ Branch 702 → 703 not taken.
✓ Branch 702 → 704 taken 90 times.
✗ Branch 704 → 705 not taken.
✓ Branch 704 → 706 taken 90 times.
✗ Branch 706 → 707 not taken.
✓ Branch 706 → 708 taken 90 times.
✗ Branch 708 → 709 not taken.
✓ Branch 708 → 710 taken 90 times.
✗ Branch 710 → 711 not taken.
✓ Branch 710 → 712 taken 90 times.
✓ Branch 712 → 713 taken 90 times.
✗ Branch 712 → 726 not taken.
✗ Branch 1347 → 1348 not taken.
✗ Branch 1347 → 1349 not taken.
✗ Branch 1351 → 1352 not taken.
✗ Branch 1351 → 1353 not taken.
✗ Branch 1355 → 1356 not taken.
✗ Branch 1355 → 1357 not taken.
✗ Branch 1359 → 1360 not taken.
✗ Branch 1359 → 1361 not taken.
✗ Branch 1363 → 1364 not taken.
✗ Branch 1363 → 1365 not taken.
90 if (defaultKey.isEmpty() || choice.value(QStringLiteral("key")).toString() == lastKey)
275
2/2
✓ Branch 717 → 718 taken 90 times.
✓ Branch 718 → 719 taken 90 times.
180 defaultKey = choice.value(QStringLiteral("key")).toString();
276 262 }
277
2/2
✓ Branch 737 → 738 taken 61 times.
✓ Branch 737 → 787 taken 29 times.
90 if (stored) {
278
1/1
✓ Branch 740 → 741 taken 61 times.
61 defaultKey = calendarSelectionKey(stored->accountId, stored->calendarPath);
279
5/5
✓ Branch 743 → 744 taken 61 times.
✓ Branch 744 → 745 taken 61 times.
✓ Branch 745 → 746 taken 61 times.
✓ Branch 784 → 747 taken 118 times.
✓ Branch 784 → 785 taken 61 times.
179 for (const QVariant &value : m_deps.calendarChoices())
280
5/5
✓ Branch 748 → 749 taken 118 times.
✓ Branch 753 → 754 taken 118 times.
✓ Branch 754 → 755 taken 118 times.
✓ Branch 762 → 763 taken 61 times.
✓ Branch 762 → 782 taken 57 times.
236 if (value.toMap().value(QStringLiteral("key")).toString() == defaultKey)
281
3/3
✓ Branch 763 → 764 taken 61 times.
✓ Branch 768 → 769 taken 61 times.
✓ Branch 772 → 773 taken 61 times.
244 card[QStringLiteral("calendarName")] = value.toMap().value(QStringLiteral("displayName"));
282 }
283
1/1
✓ Branch 791 → 792 taken 90 times.
180 card[QStringLiteral("calendars")] = calendars;
284
1/1
✓ Branch 800 → 801 taken 90 times.
180 card[QStringLiteral("defaultCalendar")] = defaultKey;
285 90 QString accountId, path;
286
6/7
✓ Branch 805 → 806 taken 90 times.
✓ Branch 806 → 807 taken 90 times.
✗ Branch 806 → 810 not taken.
✓ Branch 807 → 808 taken 90 times.
✓ Branch 808 → 809 taken 44 times.
✓ Branch 808 → 810 taken 46 times.
✓ Branch 815 → 816 taken 90 times.
180 card[QStringLiteral("serverSends")] = resolveCalendar(defaultKey, &accountId, &path) && serverSends(accountId);
287 90 m_card = card;
288 474 }
289
290 14 bool QuickInvitation::storeRows(const QString &accountId, const QString &path, const QString &partstat) {
291 14 auto *sync = m_session.calendarSync();
292
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 14 times.
14 if (!sync) return false;
293 14 QString name;
294
1/2
✓ Branch 8 → 9 taken 14 times.
✗ Branch 8 → 11 not taken.
14 if (const auto &primary = m_session.accounts()->primary()) name = primary->name;
295
8/10
✓ Branch 11 → 12 taken 14 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 16 taken 12 times.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 15 → 17 taken 2 times.
✓ Branch 16 → 17 taken 12 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 12 times.
✗ Branch 101 → 102 not taken.
✗ Branch 101 → 103 not taken.
14 const QString me = ownAddress().isEmpty() ? ownAddresses().value(0) : ownAddress();
296
2/2
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 30 taken 12 times.
14 QList<CalendarEvent> rows = me.isEmpty() || partstat.isEmpty()
297
7/14
✓ Branch 21 → 22 taken 14 times.
✗ Branch 21 → 24 not taken.
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 34 taken 12 times.
✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 36 taken 12 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 38 taken 12 times.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 112 → 114 not taken.
✗ Branch 116 → 117 not taken.
✗ Branch 116 → 118 not taken.
30 ? Imip::eventsToStore(*m_invitation, QString(), QString(), QString())
298
2/2
✓ Branch 28 → 29 taken 2 times.
✓ Branch 31 → 32 taken 12 times.
28 : Imip::eventsToStore(*m_invitation, me, name, partstat);
299
2/3
✓ Branch 39 → 40 taken 14 times.
✗ Branch 39 → 42 not taken.
✓ Branch 41 → 43 taken 14 times.
14 const QString color = sync->store() ? sync->store()->calendarColor(accountId, path) : QString();
300
4/4
✓ Branch 43 → 44 taken 14 times.
✓ Branch 44 → 45 taken 14 times.
✓ Branch 52 → 46 taken 14 times.
✓ Branch 52 → 53 taken 14 times.
28 for (CalendarEvent &row : rows) {
301 14 row.accountId = accountId;
302 14 row.calendarPath = path;
303 14 row.color = color;
304 }
305
11/11
✓ Branch 53 → 54 taken 14 times.
✓ Branch 57 → 58 taken 14 times.
✓ Branch 58 → 59 taken 14 times.
✓ Branch 60 → 61 taken 14 times.
✓ Branch 61 → 62 taken 14 times.
✓ Branch 63 → 64 taken 2 times.
✓ Branch 63 → 65 taken 12 times.
✓ Branch 64 → 66 taken 2 times.
✓ Branch 66 → 67 taken 14 times.
✓ Branch 76 → 55 taken 14 times.
✓ Branch 76 → 77 taken 14 times.
28 qCInfo(lcInvitation) << "Storing invitation" << m_invitation->method << "as" << (partstat.isEmpty() ? "-" : partstat)
306
4/4
✓ Branch 67 → 68 taken 14 times.
✓ Branch 68 → 69 taken 14 times.
✓ Branch 69 → 70 taken 14 times.
✓ Branch 71 → 72 taken 14 times.
14 << "in" << path << "rows" << rows.size();
307 // F-52: a mail that carries only an occurrence (RECURRENCE-ID) merges
308 // into the stored series — the master row and its other exceptions
309 // survive an update of one date.
310
2/5
✓ Branch 78 → 79 taken 14 times.
✗ Branch 79 → 80 not taken.
✓ Branch 79 → 87 taken 14 times.
✗ Branch 81 → 82 not taken.
✗ Branch 81 → 87 not taken.
14 if (m_invitation->event.recurrenceId.isValid() && rows.size() >= 1 &&
311
1/5
✗ Branch 84 → 85 not taken.
✗ Branch 85 → 86 not taken.
✗ Branch 85 → 87 not taken.
✗ Branch 88 → 89 not taken.
✓ Branch 88 → 92 taken 14 times.
14 rows.cbegin()->recurrenceId.isValid())
312 ✗ return sync->applyOccurrenceUpdate(m_invitation->uid, accountId, path, rows);
313
1/1
✓ Branch 93 → 94 taken 14 times.
14 return sync->restoreEventSeries(m_invitation->uid, accountId, path, rows);
314 14 }
315
316 30 bool QuickInvitation::admitsMutation(bool confirmMismatch) {
317
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 29 times.
30 if (!m_invitation) return false;
318 29 rebuildCard(); // Re-evaluate if the stored appointment changed since display.
319 // S106-342: a newer revision stored since the card was shown (answered
320 // elsewhere, a sync) is never overwritten by this older invitation — the
321 // "outdated" state was a button-visibility rule only.
322
4/4
✓ Branch 11 → 12 taken 29 times.
✓ Branch 12 → 13 taken 29 times.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 34 taken 28 times.
58 if (m_card.value(QStringLiteral("state")).toString() == QLatin1String("outdated")) {
323
5/5
✓ Branch 20 → 21 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 29 → 22 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
2 qCInfo(lcInvitation) << "Invitation answer refused: a newer revision is stored";
324
2/2
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
1 m_deps.setStatus(tr("A newer version of this invitation is already in the calendar."));
325 1 return false;
326 }
327
12/24
✓ Branch 38 → 39 taken 28 times.
✓ Branch 39 → 40 taken 28 times.
✓ Branch 40 → 41 taken 9 times.
✓ Branch 40 → 42 taken 19 times.
✓ Branch 41 → 42 taken 4 times.
✓ Branch 41 → 43 taken 5 times.
✓ Branch 44 → 45 taken 28 times.
✗ Branch 44 → 46 not taken.
✓ Branch 46 → 47 taken 28 times.
✗ Branch 46 → 48 not taken.
✓ Branch 48 → 49 taken 28 times.
✗ Branch 48 → 50 not taken.
✓ Branch 50 → 51 taken 28 times.
✗ Branch 50 → 52 not taken.
✓ Branch 52 → 53 taken 23 times.
✓ Branch 52 → 54 taken 5 times.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
✗ Branch 103 → 104 not taken.
✗ Branch 103 → 105 not taken.
56 if (!m_card.value(QStringLiteral("requiresConfirmation")).toBool() || confirmMismatch) return true;
328
5/5
✓ Branch 54 → 55 taken 5 times.
✓ Branch 58 → 59 taken 5 times.
✓ Branch 59 → 60 taken 5 times.
✓ Branch 63 → 56 taken 5 times.
✓ Branch 63 → 64 taken 5 times.
10 qCWarning(lcSecurity) << "Calendar mutation requires mismatch confirmation";
329
2/2
✓ Branch 64 → 65 taken 5 times.
✓ Branch 65 → 66 taken 5 times.
5 m_deps.setStatus(tr("The invitation does not match the saved appointment. Confirm this change explicitly."));
330 5 return false;
331 }
332
333 16 bool QuickInvitation::respond(const QString &partstat, const QString &calendarKey, const QString &comment, bool confirmMismatch) {
334
3/3
✓ Branch 2 → 3 taken 16 times.
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 12 times.
16 if (!admitsMutation(confirmMismatch)) return false;
335
1/1
✓ Branch 5 → 6 taken 12 times.
12 const QString status = partstat.toUpper();
336
8/8
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 16 taken 8 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 16 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 2 times.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 11 times.
12 if (status != QLatin1String("ACCEPTED") && status != QLatin1String("TENTATIVE") && status != QLatin1String("DECLINED"))
337 1 return false;
338 11 QString accountId, path;
339
1/1
✓ Branch 19 → 20 taken 11 times.
11 const auto stored = storedCopy();
340
2/2
✓ Branch 21 → 22 taken 3 times.
✓ Branch 21 → 26 taken 8 times.
11 if (stored) {
341 3 accountId = stored->accountId;
342 3 path = stored->calendarPath;
343
3/3
✓ Branch 26 → 27 taken 8 times.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 32 taken 7 times.
8 } else if (!resolveCalendar(calendarKey, &accountId, &path)) {
344
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
1 m_deps.setStatus(tr("Choose a writable calendar for the invitation."));
345 1 return false;
346 }
347 // A declined invitation that was never stored stays out of the calendar.
348
4/4
✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 37 taken 8 times.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 38 taken 1 time.
10 const bool keep = status != QLatin1String("DECLINED") || stored;
349
5/7
✓ Branch 39 → 40 taken 9 times.
✓ Branch 39 → 43 taken 1 time.
✓ Branch 40 → 41 taken 9 times.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 9 times.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 49 taken 10 times.
10 if (keep && !storeRows(accountId, path, status)) {
350 ✗ m_deps.setStatus(tr("The invitation could not be saved to the calendar."));
351 ✗ return false;
352 }
353
1/1
✓ Branch 49 → 50 taken 10 times.
10 const QString me = ownAddress();
354
10/23
✓ Branch 54 → 55 taken 10 times.
✓ Branch 55 → 56 taken 10 times.
✓ Branch 56 → 57 taken 10 times.
✗ Branch 56 → 60 not taken.
✓ Branch 57 → 58 taken 10 times.
✓ Branch 58 → 59 taken 4 times.
✓ Branch 58 → 60 taken 6 times.
✓ Branch 61 → 62 taken 10 times.
✗ Branch 61 → 63 not taken.
✓ Branch 63 → 64 taken 10 times.
✗ Branch 63 → 65 not taken.
✓ Branch 65 → 66 taken 10 times.
✗ Branch 65 → 67 not taken.
✓ Branch 67 → 68 taken 10 times.
✗ Branch 67 → 69 not taken.
✗ Branch 211 → 212 not taken.
✗ Branch 211 → 213 not taken.
✗ Branch 215 → 216 not taken.
✗ Branch 215 → 217 not taken.
✗ Branch 219 → 220 not taken.
✗ Branch 219 → 221 not taken.
✗ Branch 223 → 224 not taken.
✗ Branch 223 → 225 not taken.
20 const bool sendReply = m_card.value(QStringLiteral("canReply")).toBool() && !serverSends(accountId);
355
2/2
✓ Branch 69 → 70 taken 4 times.
✓ Branch 69 → 189 taken 6 times.
10 if (sendReply) {
356 4 const auto &primary = m_session.accounts()->primary();
357 4 Rfc2822Builder builder;
358
1/1
✓ Branch 75 → 76 taken 4 times.
4 builder.setFrom(primary->name, primary->email);
359
3/5
✓ Branch 79 → 80 taken 4 times.
✓ Branch 82 → 83 taken 4 times.
✓ Branch 82 → 84 taken 4 times.
✗ Branch 230 → 231 not taken.
✗ Branch 230 → 232 not taken.
8 builder.setTo({m_invitation->organizerEmail});
360
2/2
✓ Branch 86 → 87 taken 2 times.
✓ Branch 86 → 88 taken 2 times.
4 const QString verb = status == QLatin1String("ACCEPTED") ? tr("Accepted")
361
5/5
✓ Branch 87 → 93 taken 2 times.
✓ Branch 90 → 91 taken 1 time.
✓ Branch 90 → 92 taken 1 time.
✓ Branch 91 → 93 taken 1 time.
✓ Branch 92 → 93 taken 1 time.
4 : status == QLatin1String("TENTATIVE") ? tr("Tentative") : tr("Declined");
362
2/2
✓ Branch 97 → 98 taken 4 times.
✓ Branch 98 → 99 taken 4 times.
4 builder.setSubject(QStringLiteral("%1: %2").arg(verb, m_invitation->event.summary));
363
1/1
✓ Branch 102 → 103 taken 4 times.
8 QString body = tr("%1 has answered the invitation “%2”: %3.")
364
2/3
✗ Branch 106 → 107 not taken.
✓ Branch 106 → 108 taken 4 times.
✓ Branch 110 → 111 taken 4 times.
4 .arg(primary->name.isEmpty() ? me : primary->name, m_invitation->event.summary, verb);
365
6/6
✓ Branch 112 → 113 taken 4 times.
✓ Branch 115 → 116 taken 1 time.
✓ Branch 115 → 127 taken 3 times.
✓ Branch 116 → 117 taken 1 time.
✓ Branch 120 → 121 taken 1 time.
✓ Branch 121 → 122 taken 1 time.
5 if (!comment.trimmed().isEmpty()) body += QStringLiteral("\n\n") + comment.trimmed();
366
1/1
✓ Branch 127 → 128 taken 4 times.
4 builder.setBodyText(body);
367
3/3
✓ Branch 131 → 132 taken 4 times.
✓ Branch 134 → 135 taken 4 times.
✓ Branch 135 → 136 taken 4 times.
4 builder.setCalendarPart(Imip::buildReply(*m_invitation, me, primary->name, status, comment), QStringLiteral("REPLY"));
368
1/1
✓ Branch 141 → 142 taken 4 times.
4 builder.setMessageId();
369
8/10
✓ Branch 143 → 144 taken 4 times.
✓ Branch 146 → 147 taken 4 times.
✓ Branch 147 → 148 taken 4 times.
✓ Branch 151 → 152 taken 4 times.
✓ Branch 154 → 155 taken 4 times.
✓ Branch 154 → 156 taken 4 times.
✓ Branch 159 → 160 taken 1 time.
✓ Branch 159 → 161 taken 3 times.
✗ Branch 285 → 286 not taken.
✗ Branch 285 → 287 not taken.
16 if (!m_deps.send({m_invitation->organizerEmail}, builder.build(Rfc2822Builder::BccHeader::Omit), tr("Reply sent to %1.").arg(m_invitation->organizerEmail)))
370 1 return false;
371
8/8
✓ Branch 161 → 162 taken 3 times.
✓ Branch 165 → 166 taken 3 times.
✓ Branch 166 → 167 taken 3 times.
✓ Branch 167 → 168 taken 3 times.
✓ Branch 168 → 169 taken 3 times.
✓ Branch 170 → 171 taken 3 times.
✓ Branch 174 → 163 taken 3 times.
✓ Branch 174 → 175 taken 3 times.
6 qCInfo(lcInvitation) << "Reply" << status << "sent for" << m_invitation->uid;
372
6/6
✓ Branch 177 → 178 taken 3 times.
✓ Branch 177 → 179 taken 1 time.
✓ Branch 181 → 182 taken 3 times.
✓ Branch 181 → 183 taken 1 time.
✓ Branch 185 → 186 taken 3 times.
✓ Branch 185 → 188 taken 1 time.
6 } else {
373
5/5
✓ Branch 189 → 190 taken 5 times.
✓ Branch 189 → 191 taken 1 time.
✓ Branch 190 → 192 taken 5 times.
✓ Branch 191 → 192 taken 1 time.
✓ Branch 192 → 193 taken 6 times.
6 m_deps.setStatus(keep ? tr("Saved to the calendar.") : tr("Invitation declined."));
374 }
375
1/1
✓ Branch 195 → 196 taken 9 times.
9 rebuildCard();
376
1/1
✓ Branch 196 → 197 taken 9 times.
9 emit changed();
377 9 return true;
378 20 }
379
380 8 bool QuickInvitation::addToCalendar(const QString &calendarKey, bool confirmMismatch) {
381
3/3
✓ Branch 2 → 3 taken 8 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 6 times.
8 if (!admitsMutation(confirmMismatch)) return false;
382 6 QString accountId, path;
383
1/1
✓ Branch 5 → 6 taken 6 times.
6 const auto stored = storedCopy();
384
2/2
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 12 taken 3 times.
6 if (stored) {
385 3 accountId = stored->accountId;
386 3 path = stored->calendarPath;
387
3/3
✓ Branch 12 → 13 taken 3 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 18 taken 2 times.
3 } else if (!resolveCalendar(calendarKey, &accountId, &path)) {
388
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
1 m_deps.setStatus(tr("Choose a writable calendar for the invitation."));
389 1 return false;
390 }
391 // An update keeps the answer given before.
392
1/1
✓ Branch 18 → 19 taken 5 times.
5 const QString me = ownAddress();
393
4/5
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 26 taken 2 times.
✓ Branch 22 → 23 taken 3 times.
✗ Branch 22 → 26 not taken.
✓ Branch 24 → 25 taken 3 times.
5 const QString previous = stored && !me.isEmpty() ? statusIn(stored->master.icalRaw, me) : QString();
394
2/3
✓ Branch 27 → 28 taken 5 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 33 taken 5 times.
5 if (!storeRows(accountId, path, previous)) {
395 ✗ m_deps.setStatus(tr("The invitation could not be saved to the calendar."));
396 ✗ return false;
397 }
398
2/2
✓ Branch 33 → 34 taken 5 times.
✓ Branch 34 → 35 taken 5 times.
5 m_deps.setStatus(tr("Saved to the calendar."));
399
1/1
✓ Branch 36 → 37 taken 5 times.
5 rebuildCard();
400
1/1
✓ Branch 37 → 38 taken 5 times.
5 emit changed();
401 5 return true;
402 6 }
403
404 8 bool QuickInvitation::removeFromCalendar(bool confirmMismatch) {
405 8 auto *sync = m_session.calendarSync();
406
1/1
✓ Branch 3 → 4 taken 8 times.
8 const auto stored = storedCopy();
407
9/10
✓ Branch 5 → 6 taken 7 times.
✓ Branch 5 → 15 taken 1 time.
✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 15 not taken.
✓ Branch 8 → 9 taken 6 times.
✓ Branch 8 → 15 taken 1 time.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 15 taken 2 times.
✓ Branch 17 → 18 taken 5 times.
✓ Branch 17 → 19 taken 3 times.
12 if (!m_invitation || !sync || !stored || m_invitation->method != QLatin1String("CANCEL") ||
408
3/3
✓ Branch 13 → 14 taken 4 times.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 3 times.
9 !admitsMutation(confirmMismatch)) return false;
409 // F-52: a cancelled occurrence leaves the series (its exception row and
410 // an EXDATE on the master); the other dates stay in the calendar.
411
2/3
✓ Branch 20 → 21 taken 3 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 54 taken 3 times.
3 if (m_invitation->event.recurrenceId.isValid()) {
412 ✗ if (!sync->applyOccurrenceCancel(m_invitation->uid, stored->accountId, stored->calendarPath,
413 ✗ m_invitation->event.recurrenceId)) {
414 ✗ m_deps.setStatus(tr("The appointment could not be removed."));
415 ✗ return false;
416 }
417 ✗ qCInfo(lcInvitation) << "Cancelled occurrence removed" << m_invitation->uid
418 ✗ << m_invitation->event.recurrenceId.toString(Qt::ISODate);
419 ✗ m_deps.setStatus(tr("Removed this date from the calendar."));
420 ✗ rebuildCard();
421 ✗ emit changed();
422 ✗ return true;
423 }
424
2/3
✓ Branch 57 → 58 taken 3 times.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 63 taken 3 times.
3 if (!sync->removeEvent(m_invitation->uid, stored->accountId, stored->calendarPath)) {
425 ✗ m_deps.setStatus(tr("The appointment could not be removed."));
426 ✗ return false;
427 }
428
6/6
✓ Branch 63 → 64 taken 3 times.
✓ Branch 67 → 68 taken 3 times.
✓ Branch 68 → 69 taken 3 times.
✓ Branch 70 → 71 taken 3 times.
✓ Branch 74 → 65 taken 3 times.
✓ Branch 74 → 75 taken 3 times.
6 qCInfo(lcInvitation) << "Cancelled appointment removed" << m_invitation->uid;
429
2/2
✓ Branch 75 → 76 taken 3 times.
✓ Branch 76 → 77 taken 3 times.
3 m_deps.setStatus(tr("Removed from the calendar."));
430
1/1
✓ Branch 78 → 79 taken 3 times.
3 rebuildCard();
431
1/1
✓ Branch 79 → 80 taken 3 times.
3 emit changed();
432 3 return true;
433 8 }
434
435 3 bool QuickInvitation::applyReply(bool confirmMismatch) {
436 3 auto *sync = m_session.calendarSync();
437
1/1
✓ Branch 3 → 4 taken 3 times.
3 const auto stored = storedCopy();
438
3/6
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 18 not taken.
✓ Branch 8 → 9 taken 3 times.
✗ Branch 8 → 18 not taken.
✓ Branch 11 → 12 taken 3 times.
✗ Branch 11 → 18 not taken.
6 if (!m_invitation || !sync || !stored || m_invitation->attendees.isEmpty() ||
439
7/9
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 18 not taken.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 18 taken 1 time.
✓ Branch 16 → 17 taken 2 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 2 times.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 2 times.
6 m_invitation->method != QLatin1String("REPLY") || !admitsMutation(confirmMismatch)) return false;
440
1/1
✓ Branch 23 → 24 taken 2 times.
2 const Imip::Attendee &replier = m_invitation->attendees.first();
441 2 CalendarEvent updated = stored->master;
442
1/1
✓ Branch 26 → 27 taken 2 times.
2 const QString patched = Imip::withAttendeeStatus(updated.icalRaw, replier.email, replier.partstat);
443
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 1 time.
2 if (patched.isEmpty()) return false;
444 1 updated.icalRaw = patched;
445
2/3
✓ Branch 31 → 32 taken 1 time.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 1 time.
1 if (!sync->saveEvent(updated, false)) return false;
446
8/8
✓ Branch 34 → 35 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 47 → 36 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
2 qCInfo(lcInvitation) << "Attendee status" << replier.partstat << "applied to" << m_invitation->uid;
447
1/1
✓ Branch 48 → 49 taken 1 time.
1 rebuildCard();
448
1/1
✓ Branch 49 → 50 taken 1 time.
1 emit changed();
449 1 return true;
450 3 }
451