MailImporter

filterimporterakonadi.cpp
1/*
2 SPDX-FileCopyrightText: 2017-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "filterimporterakonadi.h"
8#include "mailimporterakonadi_debug.h"
9#include <Akonadi/CollectionCreateJob>
10#include <Akonadi/CollectionFetchJob>
11#include <Akonadi/Item>
12#include <Akonadi/ItemCreateJob>
13#include <Akonadi/ItemFetchJob>
14#include <Akonadi/ItemFetchScope>
15#include <Akonadi/MessageFlags>
16#include <Akonadi/MessageParts>
17#include <KLocalizedString>
18#include <MailImporter/FilterInfo>
19#include <QFile>
20#include <QScopedPointer>
21#include <QUrl>
22
23using namespace MailImporter;
24
25FilterImporterAkonadi::FilterImporterAkonadi(MailImporter::FilterInfo *info)
26 : MailImporter::FilterImporterBase(info)
27{
28}
29
30FilterImporterAkonadi::~FilterImporterAkonadi()
31{
32}
33
34void FilterImporterAkonadi::clear()
35{
36 mMessageFolderMessageIDMap.clear();
37 mMessageFolderCollectionMap.clear();
38 mCountDuplicates = 0;
39}
40
41Akonadi::MessageStatus FilterImporterAkonadi::convertToAkonadiMessageStatus(const MailImporter::MessageStatus &status)
42{
43 Akonadi::MessageStatus akonadiStatus;
44 if (status.isDeleted()) {
45 akonadiStatus.setDeleted(true);
46 }
47 if (status.isRead()) {
48 akonadiStatus.setRead(true);
49 }
50 if (status.isForwarded()) {
51 akonadiStatus.setForwarded(true);
52 }
53 if (status.isReplied()) {
54 akonadiStatus.setReplied(true);
55 }
56 return akonadiStatus;
57}
58
59Akonadi::Collection FilterImporterAkonadi::rootCollection() const
60{
61 return mRootCollection;
62}
63
64void FilterImporterAkonadi::setRootCollection(const Akonadi::Collection &collection)
65{
66 mRootCollection = collection;
67}
68
69QString FilterImporterAkonadi::topLevelFolder() const
70{
71 return mRootCollection.name();
72}
73
74bool FilterImporterAkonadi::importMessage(const QString &folderName,
75 const QString &msgPath,
76 bool duplicateCheck,
77 const MailImporter::MessageStatus &mailImporterstatus)
78{
79 const Akonadi::MessageStatus status = convertToAkonadiMessageStatus(mailImporterstatus);
80 QString messageID;
81 // Create the mail folder (if not already created).
82 Akonadi::Collection mailFolder = parseFolderString(folderName);
83 QUrl msgUrl = QUrl::fromLocalFile(msgPath);
84 if (!msgUrl.isEmpty() && msgUrl.isLocalFile()) {
85 QFile f(msgUrl.toLocalFile());
86 QByteArray msgText;
87 if (f.open(QIODevice::ReadOnly)) {
88 msgText = f.readAll();
89 f.close();
90 } else {
91 qCWarning(MAILIMPORTERAKONADI_LOG) << "Failed to read temporary file: " << f.errorString();
92 }
93 if (msgText.isEmpty()) {
94 mInfo->addErrorLogEntry(i18n("Error: failed to read temporary file at %1", msgPath));
95 return false;
96 }
97
98 // Construct a message.
99 KMime::Message::Ptr newMessage(new KMime::Message());
100 newMessage->setContent(msgText);
101 newMessage->parse();
102
103 if (duplicateCheck) {
104 // Get the messageID.
105 const KMime::Headers::Base *messageIDHeader = newMessage->messageID(false);
106 if (messageIDHeader) {
107 messageID = messageIDHeader->asUnicodeString();
108 }
109
110 if (!messageID.isEmpty()) {
111 // Check for duplicate.
112 if (checkForDuplicates(messageID, mailFolder, folderName)) {
113 mCountDuplicates++;
114 return false;
115 }
116 }
117 }
118
119 // Add it to the collection.
120 if (mailFolder.isValid()) {
121 addAkonadiMessage(mailFolder, newMessage, status);
122 } else {
123 mInfo->alert(i18n("<b>Warning:</b> Got a bad message folder, adding to root folder."));
124 addAkonadiMessage(rootCollection(), newMessage, status);
125 }
126 } else {
127 qCWarning(MAILIMPORTERAKONADI_LOG) << "Url is not temporary file: " << msgUrl;
128 }
129 return true;
130}
131
132Akonadi::Collection FilterImporterAkonadi::parseFolderString(const QString &folderParseString)
133{
134 // Return an already created collection:
135 const Akonadi::Collection col = mMessageFolderCollectionMap.value(folderParseString);
136 if (col.isValid()) {
137 return col;
138 }
139
140 // The folder hasn't yet been created, create it now.
141 const QStringList folderList = folderParseString.split(QLatin1Char('/'), Qt::SkipEmptyParts);
142 bool isFirst = true;
143 QString folderBuilder;
144 Akonadi::Collection lastCollection;
145
146 // Create each folder on the folder list and add it the map.
147 for (const QString &folder : folderList) {
148 if (isFirst) {
149 mMessageFolderCollectionMap[folder] = addSubCollection(rootCollection(), folder);
150 folderBuilder = folder;
151 lastCollection = mMessageFolderCollectionMap[folder];
152 isFirst = false;
153 } else {
154 folderBuilder += QLatin1Char('/') + folder;
155 mMessageFolderCollectionMap[folderBuilder] = addSubCollection(lastCollection, folder);
156 lastCollection = mMessageFolderCollectionMap[folderBuilder];
157 }
158 }
159
160 return lastCollection;
161}
162
163Akonadi::Collection FilterImporterAkonadi::addSubCollection(const Akonadi::Collection &baseCollection, const QString &newCollectionPathName)
164{
165 // Ensure that the collection doesn't already exist, if it does just return it.
166 auto fetchJob = new Akonadi::CollectionFetchJob(baseCollection, Akonadi::CollectionFetchJob::FirstLevel);
167 if (!fetchJob->exec()) {
168 mInfo->alert(i18n("<b>Warning:</b> Could not check that the folder already exists. Reason: %1", fetchJob->errorString()));
169 return Akonadi::Collection();
170 }
171 const Akonadi::Collection::List lstCols = fetchJob->collections();
172 for (const Akonadi::Collection &subCollection : lstCols) {
173 if (subCollection.name() == newCollectionPathName) {
174 return subCollection;
175 }
176 }
177
178 // The subCollection doesn't exist, create a new one
179 Akonadi::Collection newSubCollection;
180 newSubCollection.setParentCollection(baseCollection);
181 newSubCollection.setName(newCollectionPathName);
182
183 auto job = new Akonadi::CollectionCreateJob(newSubCollection);
184 if (!job->exec()) {
185 mInfo->alert(i18n("<b>Error:</b> Could not create folder. Reason: %1", job->errorString()));
186 return Akonadi::Collection();
187 }
188 // Return the newly created collection
189 Akonadi::Collection collection = job->collection();
190 return collection;
191}
192
193bool FilterImporterAkonadi::checkForDuplicates(const QString &msgID, const Akonadi::Collection &msgCollection, const QString &messageFolder)
194{
195 bool folderFound = false;
196
197 // Check if the contents of this collection have already been found.
198 QMultiMap<QString, QString>::const_iterator end(mMessageFolderMessageIDMap.constEnd());
199 for (QMultiMap<QString, QString>::const_iterator it = mMessageFolderMessageIDMap.constBegin(); it != end; ++it) {
200 if (it.key() == messageFolder) {
201 folderFound = true;
202 break;
203 }
204 }
205
206 if (!folderFound) {
207 // Populate the map with message IDs that are in that collection.
208 if (msgCollection.isValid()) {
209 Akonadi::ItemFetchJob job(msgCollection);
210 job.fetchScope().fetchPayloadPart(Akonadi::MessagePart::Header);
211 if (!job.exec()) {
212 mInfo->addInfoLogEntry(
213 i18n("<b>Warning:</b> Could not fetch mail in folder %1. Reason: %2"
214 " You may have duplicate messages.",
215 messageFolder,
216 job.errorString()));
217 } else {
218 const Akonadi::Item::List items = job.items();
219 for (const Akonadi::Item &messageItem : items) {
220 if (!messageItem.isValid()) {
221 mInfo->addInfoLogEntry(i18n("<b>Warning:</b> Got an invalid message in folder %1.", messageFolder));
222 } else {
223 if (!messageItem.hasPayload<KMime::Message::Ptr>()) {
224 continue;
225 }
226 const auto message = messageItem.payload<KMime::Message::Ptr>();
227 const KMime::Headers::Base *messageID = message->messageID(false);
228 if (messageID) {
229 if (!messageID->isEmpty()) {
230 mMessageFolderMessageIDMap.insert(messageFolder, messageID->asUnicodeString());
231 }
232 }
233 }
234 }
235 }
236 }
237 }
238
239 // Check if this message has a duplicate
240 QMultiMap<QString, QString>::const_iterator endMsgID(mMessageFolderMessageIDMap.constEnd());
241 for (QMultiMap<QString, QString>::const_iterator it = mMessageFolderMessageIDMap.constBegin(); it != endMsgID; ++it) {
242 if (it.key() == messageFolder && it.value() == msgID) {
243 return true;
244 }
245 }
246
247 // The message isn't a duplicate, but add it to the map for checking in the future.
248 mMessageFolderMessageIDMap.insert(messageFolder, msgID);
249 return false;
250}
251
252bool FilterImporterAkonadi::addAkonadiMessage(const Akonadi::Collection &collection, const KMime::Message::Ptr &message, Akonadi::MessageStatus status)
253{
254 Akonadi::Item item;
255
256 item.setMimeType(QStringLiteral("message/rfc822"));
257
258 if (status.isOfUnknownStatus()) {
259 KMime::Headers::Base *statusHeaders = message->headerByType("X-Status");
260 if (statusHeaders) {
261 if (!statusHeaders->isEmpty()) {
262 status.setStatusFromStr(statusHeaders->asUnicodeString());
263 item.setFlags(status.statusFlags());
264 }
265 }
266 } else {
267 item.setFlags(status.statusFlags());
268 }
269
271 item.setPayload<KMime::Message::Ptr>(message);
273 job->setAutoDelete(false);
274 if (!job->exec()) {
275 mInfo->alert(i18n("<b>Error:</b> Could not add message to folder %1. Reason: %2", collection.name(), job->errorString()));
276 return false;
277 }
278 return true;
279}
280
281void FilterImporterAkonadi::clearCountDuplicate()
282{
283 mCountDuplicates = 0;
284}
285
286int FilterImporterAkonadi::countDuplicates() const
287{
288 return mCountDuplicates;
289}
290
291bool FilterImporterAkonadi::importMessage(const KArchiveFile *file, const QString &folderPath, int &nbTotal, int &fileDone)
292{
293 const Akonadi::Collection collection = parseFolderString(folderPath);
294 if (!collection.isValid()) {
295 mInfo->addErrorLogEntry(i18n("Unable to retrieve folder for folder path %1.", folderPath));
296 return false;
297 }
298
299 KMime::Message::Ptr newMessage(new KMime::Message());
300 newMessage->setContent(file->data());
301 newMessage->parse();
302
303 if (mInfo->removeDupMessage()) {
304 KMime::Headers::MessageID *messageId = newMessage->messageID(false);
305 if (messageId) {
306 const QString messageIdStr = messageId->asUnicodeString();
307 if (!messageIdStr.isEmpty()) {
308 if (checkForDuplicates(messageIdStr, collection, folderPath)) {
309 nbTotal--;
310 return true;
311 }
312 }
313 }
314 }
315
316 const bool result = addAkonadiMessage(collection, newMessage, Akonadi::MessageStatus());
317 if (result) {
318 fileDone++;
319 }
320 return result;
321}
void setParentCollection(const Collection &parent)
bool isValid() const
void setName(const QString &name)
QString name() const
void setPayload(const T &p)
void setMimeType(const QString &mimeType)
void setFlags(const Flags &flags)
void setRead(bool read=true)
void setForwarded(bool forwarded=true)
void setDeleted(bool deleted=true)
void setReplied(bool replied=true)
virtual QByteArray data() const
virtual QString asUnicodeString() const=0
virtual bool isEmpty() const=0
QString asUnicodeString() const override
The FilterImporterBase class.
The FilterInfo class.
Definition filterinfo.h:21
The MessageStatus class.
Q_SCRIPTABLE CaptureState status()
QString i18n(const char *text, const TYPE &arg...)
AKONADI_MIME_EXPORT void copyMessageFlags(KMime::Message &from, Akonadi::Item &to)
AKONADI_MIME_EXPORT const char Header[]
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
bool isEmpty() const const
void clear()
T value(const Key &key, const T &defaultValue) const const
void clear()
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator insert(const Key &key, const T &value)
bool isEmpty() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
SkipEmptyParts
QUrl fromLocalFile(const QString &localFile)
bool isEmpty() const const
bool isLocalFile() const const
QString toLocalFile() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:08:31 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.