1 /*
2 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
3 SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8 #include "changecollectiontask.h"
9
10 #include <KIMAP/RenameJob>
11 #include <KIMAP/Session>
12 #include <KIMAP/SetAclJob>
13 #include <KIMAP/SetMetaDataJob>
14 #include <KIMAP/SubscribeJob>
15 #include <KIMAP/UnsubscribeJob>
16
17 #include "collectionannotationsattribute.h"
18 #include "imapaclattribute.h"
19 #include "imapquotaattribute.h"
20
21 #include "imapresource_debug.h"
22 #include <KLocalizedString>
23
ChangeCollectionTask(const ResourceStateInterface::Ptr & resource,QObject * parent)24 ChangeCollectionTask::ChangeCollectionTask(const ResourceStateInterface::Ptr &resource, QObject *parent)
25 : ResourceTask(DeferIfNoSession, resource, parent)
26 {
27 }
28
~ChangeCollectionTask()29 ChangeCollectionTask::~ChangeCollectionTask()
30 {
31 }
32
syncEnabledState(bool enable)33 void ChangeCollectionTask::syncEnabledState(bool enable)
34 {
35 m_syncEnabledState = enable;
36 }
37
doStart(KIMAP::Session * session)38 void ChangeCollectionTask::doStart(KIMAP::Session *session)
39 {
40 if (collection().remoteId().isEmpty()) {
41 emitError(i18n("Cannot modify IMAP folder '%1', it does not exist on the server.", collection().name()));
42 changeProcessed();
43 return;
44 }
45
46 m_collection = collection();
47 m_pendingJobs = 0;
48
49 if (parts().contains("AccessRights")) {
50 auto aclAttribute = m_collection.attribute<Akonadi::ImapAclAttribute>();
51
52 if (aclAttribute == nullptr) {
53 emitWarning(i18n("ACLs for '%1' need to be retrieved from the IMAP server first. Skipping ACL change", collection().name()));
54 } else {
55 KIMAP::Acl::Rights imapRights = aclAttribute->rights().value(userName().toUtf8());
56 Akonadi::Collection::Rights newRights = collection().rights();
57
58 if (newRights & Akonadi::Collection::CanChangeItem) {
59 imapRights |= KIMAP::Acl::Write;
60 } else {
61 imapRights &= ~KIMAP::Acl::Write;
62 }
63
64 if (newRights & Akonadi::Collection::CanCreateItem) {
65 imapRights |= KIMAP::Acl::Insert;
66 } else {
67 imapRights &= ~KIMAP::Acl::Insert;
68 }
69
70 if (newRights & Akonadi::Collection::CanDeleteItem) {
71 imapRights |= KIMAP::Acl::DeleteMessage;
72 } else {
73 imapRights &= ~KIMAP::Acl::DeleteMessage;
74 }
75
76 if (newRights & (Akonadi::Collection::CanChangeCollection | Akonadi::Collection::CanCreateCollection)) {
77 imapRights |= KIMAP::Acl::CreateMailbox;
78 imapRights |= KIMAP::Acl::Create;
79 } else {
80 imapRights &= ~KIMAP::Acl::CreateMailbox;
81 imapRights &= ~KIMAP::Acl::Create;
82 }
83
84 if (newRights & Akonadi::Collection::CanDeleteCollection) {
85 imapRights |= KIMAP::Acl::DeleteMailbox;
86 } else {
87 imapRights &= ~KIMAP::Acl::DeleteMailbox;
88 }
89
90 if ((newRights & Akonadi::Collection::CanDeleteItem) && (newRights & Akonadi::Collection::CanDeleteCollection)) {
91 imapRights |= KIMAP::Acl::Delete;
92 } else {
93 imapRights &= ~KIMAP::Acl::Delete;
94 }
95
96 qCDebug(IMAPRESOURCE_LOG) << "imapRights:" << imapRights << "newRights:" << newRights;
97
98 auto job = new KIMAP::SetAclJob(session);
99 job->setMailBox(mailBoxForCollection(collection()));
100 job->setRights(KIMAP::SetAclJob::Change, imapRights);
101 job->setIdentifier(userName().toUtf8());
102
103 connect(job, &KIMAP::SetAclJob::result, this, &ChangeCollectionTask::onSetAclDone);
104
105 job->start();
106
107 m_pendingJobs++;
108 }
109 }
110
111 if (parts().contains("collectionannotations") && serverSupportsAnnotations()) {
112 Akonadi::Collection c = collection();
113 auto annotationsAttribute = c.attribute<Akonadi::CollectionAnnotationsAttribute>();
114
115 if (annotationsAttribute) { // No annotations it seems... server is lying to us?
116 QMap<QByteArray, QByteArray> annotations = annotationsAttribute->annotations();
117 qCDebug(IMAPRESOURCE_LOG) << "All annotations: " << annotations;
118
119 const auto annotationKeys{annotations.keys()};
120 for (const QByteArray &entry : annotationKeys) {
121 auto job = new KIMAP::SetMetaDataJob(session);
122 if (serverCapabilities().contains(QLatin1String("METADATA"))) {
123 job->setServerCapability(KIMAP::MetaDataJobBase::Metadata);
124 } else {
125 job->setServerCapability(KIMAP::MetaDataJobBase::Annotatemore);
126 }
127 job->setMailBox(mailBoxForCollection(collection()));
128
129 if (!entry.startsWith("/shared") && !entry.startsWith("/private")) {
130 // Support for legacy annotations that don't include the prefix
131 job->addMetaData(QByteArray("/shared") + entry, annotations[entry]);
132 } else {
133 job->addMetaData(entry, annotations[entry]);
134 }
135
136 qCDebug(IMAPRESOURCE_LOG) << "Job got entry:" << entry << "value:" << annotations[entry];
137
138 connect(job, &KIMAP::SetMetaDataJob::result, this, &ChangeCollectionTask::onSetMetaDataDone);
139
140 job->start();
141
142 m_pendingJobs++;
143 }
144 }
145 }
146
147 if (parts().contains("imapacl")) {
148 Akonadi::Collection c = collection();
149 auto aclAttribute = c.attribute<Akonadi::ImapAclAttribute>();
150
151 if (aclAttribute) {
152 const QMap<QByteArray, KIMAP::Acl::Rights> rights = aclAttribute->rights();
153 const QMap<QByteArray, KIMAP::Acl::Rights> oldRights = aclAttribute->oldRights();
154 const QList<QByteArray> oldIds = oldRights.keys();
155 const QList<QByteArray> ids = rights.keys();
156
157 // remove all ACL entries that have been deleted
158 for (const QByteArray &oldId : oldIds) {
159 if (!ids.contains(oldId)) {
160 auto job = new KIMAP::SetAclJob(session);
161 job->setMailBox(mailBoxForCollection(collection()));
162 job->setIdentifier(oldId);
163 job->setRights(KIMAP::SetAclJob::Remove, oldRights[oldId]);
164
165 connect(job, &KIMAP::SetAclJob::result, this, &ChangeCollectionTask::onSetAclDone);
166
167 job->start();
168
169 m_pendingJobs++;
170 }
171 }
172
173 for (const QByteArray &id : ids) {
174 auto job = new KIMAP::SetAclJob(session);
175 job->setMailBox(mailBoxForCollection(collection()));
176 job->setIdentifier(id);
177 job->setRights(KIMAP::SetAclJob::Change, rights[id]);
178
179 connect(job, &KIMAP::SetAclJob::result, this, &ChangeCollectionTask::onSetAclDone);
180
181 job->start();
182
183 m_pendingJobs++;
184 }
185 }
186 }
187
188 // Check if we need to rename the mailbox
189 // This one goes last on purpose, we don't want the previous jobs
190 // we triggered to act on the wrong mailbox name
191 if (parts().contains("NAME")) {
192 const QChar separator = separatorCharacter();
193 m_collection.setName(m_collection.name().remove(separator));
194 m_collection.setRemoteId(separator + m_collection.name());
195
196 const QString oldMailBox = mailBoxForCollection(collection());
197 const QString newMailBox = mailBoxForCollection(m_collection);
198
199 if (oldMailBox != newMailBox) {
200 auto renameJob = new KIMAP::RenameJob(session);
201 renameJob->setSourceMailBox(oldMailBox);
202 renameJob->setDestinationMailBox(newMailBox);
203 connect(renameJob, &KIMAP::RenameJob::result, this, &ChangeCollectionTask::onRenameDone);
204
205 renameJob->start();
206
207 m_pendingJobs++;
208 }
209 }
210
211 if (m_syncEnabledState && isSubscriptionEnabled() && parts().contains("ENABLED")) {
212 if (collection().enabled()) {
213 auto job = new KIMAP::SubscribeJob(session);
214 job->setMailBox(mailBoxForCollection(collection()));
215 connect(job, &KIMAP::SubscribeJob::result, this, &ChangeCollectionTask::onSubscribeDone);
216 job->start();
217 } else {
218 auto job = new KIMAP::UnsubscribeJob(session);
219 job->setMailBox(mailBoxForCollection(collection()));
220 connect(job, &KIMAP::UnsubscribeJob::result, this, &ChangeCollectionTask::onSubscribeDone);
221 job->start();
222 }
223 m_pendingJobs++;
224 }
225
226 // we scheduled no change on the server side, probably we got only
227 // unsupported part, so just declare the task done
228 if (m_pendingJobs == 0) {
229 changeCommitted(collection());
230 }
231 }
232
onRenameDone(KJob * job)233 void ChangeCollectionTask::onRenameDone(KJob *job)
234 {
235 if (job->error()) {
236 const QString prevRid = collection().remoteId();
237 Q_ASSERT(!prevRid.isEmpty());
238
239 emitWarning(i18n("Failed to rename the folder, restoring folder list."));
240
241 m_collection.setName(prevRid.mid(1));
242 m_collection.setRemoteId(prevRid);
243
244 endTaskIfNeeded();
245 } else {
246 auto renameJob = static_cast<KIMAP::RenameJob *>(job);
247 auto subscribeJob = new KIMAP::SubscribeJob(renameJob->session());
248 subscribeJob->setMailBox(renameJob->destinationMailBox());
249 connect(subscribeJob, &KIMAP::SubscribeJob::result, this, &ChangeCollectionTask::onSubscribeDone);
250 subscribeJob->start();
251 }
252 }
253
onSubscribeDone(KJob * job)254 void ChangeCollectionTask::onSubscribeDone(KJob *job)
255 {
256 if (job->error() && isSubscriptionEnabled()) {
257 emitWarning(
258 i18n("Failed to subscribe to the renamed folder '%1' on the IMAP server. "
259 "It will disappear on next sync. Use the subscription dialog to overcome that",
260 m_collection.name()));
261 }
262
263 endTaskIfNeeded();
264 }
265
onSetAclDone(KJob * job)266 void ChangeCollectionTask::onSetAclDone(KJob *job)
267 {
268 if (job->error()) {
269 emitWarning(i18n("Failed to write some ACLs for '%1' on the IMAP server. %2", collection().name(), job->errorText()));
270 }
271
272 endTaskIfNeeded();
273 }
274
onSetMetaDataDone(KJob * job)275 void ChangeCollectionTask::onSetMetaDataDone(KJob *job)
276 {
277 if (job->error()) {
278 emitWarning(i18n("Failed to write some annotations for '%1' on the IMAP server. %2", collection().name(), job->errorText()));
279 }
280
281 endTaskIfNeeded();
282 }
283
endTaskIfNeeded()284 void ChangeCollectionTask::endTaskIfNeeded()
285 {
286 if (--m_pendingJobs == 0) {
287 // the others have ended, we're done, the next one can go
288 changeCommitted(m_collection);
289 }
290 }
291