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