1 /*
2  * SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
3  *
4  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5  *
6  */
7 
8 #include "searchtask.h"
9 #include "imapresource_debug.h"
10 #include <Akonadi/KMime/MessageFlags>
11 #include <Akonadi/SearchQuery>
12 #include <KIMAP/SearchJob>
13 #include <KIMAP/SelectJob>
14 #include <KIMAP/Session>
15 #include <KLocalizedString>
Q_DECLARE_METATYPE(KIMAP::Session *)16 Q_DECLARE_METATYPE(KIMAP::Session *)
17 
18 SearchTask::SearchTask(const ResourceStateInterface::Ptr &state, const QString &query, QObject *parent)
19     : ResourceTask(ResourceTask::DeferIfNoSession, state, parent)
20     , m_query(query)
21 {
22 }
23 
~SearchTask()24 SearchTask::~SearchTask()
25 {
26 }
27 
doStart(KIMAP::Session * session)28 void SearchTask::doStart(KIMAP::Session *session)
29 {
30     qCDebug(IMAPRESOURCE_LOG) << collection().remoteId();
31 
32     const QString mailbox = mailBoxForCollection(collection());
33     if (session->selectedMailBox() == mailbox) {
34         doSearch(session);
35         return;
36     }
37 
38     auto select = new KIMAP::SelectJob(session);
39     select->setMailBox(mailbox);
40     connect(select, &KJob::finished, this, &SearchTask::onSelectDone);
41     select->start();
42 }
43 
onSelectDone(KJob * job)44 void SearchTask::onSelectDone(KJob *job)
45 {
46     if (job->error()) {
47         searchFinished(QVector<qint64>());
48         cancelTask(job->errorText());
49         return;
50     }
51 
52     doSearch(qobject_cast<KIMAP::SelectJob *>(job)->session());
53 }
54 
mapRelation(Akonadi::SearchTerm::Relation relation)55 static KIMAP::Term::Relation mapRelation(Akonadi::SearchTerm::Relation relation)
56 {
57     if (relation == Akonadi::SearchTerm::RelAnd) {
58         return KIMAP::Term::And;
59     }
60     return KIMAP::Term::Or;
61 }
62 
recursiveEmailTermMapping(const Akonadi::SearchTerm & term)63 static KIMAP::Term recursiveEmailTermMapping(const Akonadi::SearchTerm &term)
64 {
65     if (!term.subTerms().isEmpty()) {
66         QVector<KIMAP::Term> subterms;
67         const QList<Akonadi::SearchTerm> lstSearchTermsList = term.subTerms();
68         for (const Akonadi::SearchTerm &subterm : lstSearchTermsList) {
69             const KIMAP::Term newTerm = recursiveEmailTermMapping(subterm);
70             if (!newTerm.isNull()) {
71                 subterms << newTerm;
72             }
73         }
74         return KIMAP::Term(mapRelation(term.relation()), subterms);
75     } else {
76         const Akonadi::EmailSearchTerm::EmailSearchField field = Akonadi::EmailSearchTerm::fromKey(term.key());
77         switch (field) {
78         case Akonadi::EmailSearchTerm::Message:
79             return KIMAP::Term(KIMAP::Term::Text, term.value().toString()).setNegated(term.isNegated());
80         case Akonadi::EmailSearchTerm::Body:
81             return KIMAP::Term(KIMAP::Term::Body, term.value().toString()).setNegated(term.isNegated());
82         case Akonadi::EmailSearchTerm::Headers:
83             // FIXME
84             //                 return KIMAP::Term(KIMAP::Term::Header, term.value()).setNegated(term.isNegated());
85             break;
86         case Akonadi::EmailSearchTerm::ByteSize: {
87             int value = term.value().toInt();
88             switch (term.condition()) {
89             case Akonadi::SearchTerm::CondGreaterOrEqual:
90                 value--;
91                 Q_FALLTHROUGH();
92             case Akonadi::SearchTerm::CondGreaterThan:
93                 return KIMAP::Term(KIMAP::Term::Larger, value).setNegated(term.isNegated());
94             case Akonadi::SearchTerm::CondLessOrEqual:
95                 value++;
96                 Q_FALLTHROUGH();
97             case Akonadi::SearchTerm::CondLessThan:
98                 return KIMAP::Term(KIMAP::Term::Smaller, value).setNegated(term.isNegated());
99             case Akonadi::SearchTerm::CondEqual:
100                 return KIMAP::Term(KIMAP::Term::And,
101                                    QVector<KIMAP::Term>() << KIMAP::Term(KIMAP::Term::Smaller, value + 1) << KIMAP::Term(KIMAP::Term::Larger, value + 1))
102                     .setNegated(term.isNegated());
103             case Akonadi::SearchTerm::CondContains:
104                 qCDebug(IMAPRESOURCE_LOG) << " invalid condition for ByteSize";
105                 break;
106             }
107             break;
108         }
109         case Akonadi::EmailSearchTerm::HeaderOnlyDate:
110         case Akonadi::EmailSearchTerm::HeaderDate: {
111             QDate value = term.value().toDateTime().date();
112             switch (term.condition()) {
113             case Akonadi::SearchTerm::CondGreaterOrEqual:
114                 value = value.addDays(-1);
115                 Q_FALLTHROUGH();
116             case Akonadi::SearchTerm::CondGreaterThan:
117                 return KIMAP::Term(KIMAP::Term::SentSince, value).setNegated(term.isNegated());
118             case Akonadi::SearchTerm::CondLessOrEqual:
119                 value = value.addDays(1);
120                 Q_FALLTHROUGH();
121             case Akonadi::SearchTerm::CondLessThan:
122                 return KIMAP::Term(KIMAP::Term::SentBefore, value).setNegated(term.isNegated());
123             case Akonadi::SearchTerm::CondEqual:
124                 return KIMAP::Term(KIMAP::Term::SentOn, value).setNegated(term.isNegated());
125             case Akonadi::SearchTerm::CondContains:
126                 qCDebug(IMAPRESOURCE_LOG) << " invalid condition for Date";
127                 return KIMAP::Term();
128             default:
129                 qCWarning(IMAPRESOURCE_LOG) << "unknown term for date" << term.key();
130                 return KIMAP::Term();
131             }
132         }
133         case Akonadi::EmailSearchTerm::Subject:
134             return KIMAP::Term(KIMAP::Term::Subject, term.value().toString()).setNegated(term.isNegated());
135         case Akonadi::EmailSearchTerm::HeaderFrom:
136             return KIMAP::Term(KIMAP::Term::From, term.value().toString()).setNegated(term.isNegated());
137         case Akonadi::EmailSearchTerm::HeaderTo:
138             return KIMAP::Term(KIMAP::Term::To, term.value().toString()).setNegated(term.isNegated());
139         case Akonadi::EmailSearchTerm::HeaderCC:
140             return KIMAP::Term(KIMAP::Term::Cc, term.value().toString()).setNegated(term.isNegated());
141         case Akonadi::EmailSearchTerm::HeaderBCC:
142             return KIMAP::Term(KIMAP::Term::Bcc, term.value().toString()).setNegated(term.isNegated());
143         case Akonadi::EmailSearchTerm::MessageStatus: {
144             const QString termStr = term.value().toString();
145             if (termStr == QString::fromLatin1(Akonadi::MessageFlags::Flagged)) {
146                 return KIMAP::Term(KIMAP::Term::Flagged).setNegated(term.isNegated());
147             }
148             if (termStr == QString::fromLatin1(Akonadi::MessageFlags::Deleted)) {
149                 return KIMAP::Term(KIMAP::Term::Deleted).setNegated(term.isNegated());
150             }
151             if (termStr == QString::fromLatin1(Akonadi::MessageFlags::Replied)) {
152                 return KIMAP::Term(KIMAP::Term::Answered).setNegated(term.isNegated());
153             }
154             if (termStr == QString::fromLatin1(Akonadi::MessageFlags::Seen)) {
155                 return KIMAP::Term(KIMAP::Term::Seen).setNegated(term.isNegated());
156             }
157             break;
158         }
159         case Akonadi::EmailSearchTerm::MessageTag:
160             break;
161         case Akonadi::EmailSearchTerm::HeaderReplyTo:
162             break;
163         case Akonadi::EmailSearchTerm::HeaderOrganization:
164             break;
165         case Akonadi::EmailSearchTerm::HeaderListId:
166             break;
167         case Akonadi::EmailSearchTerm::HeaderResentFrom:
168             break;
169         case Akonadi::EmailSearchTerm::HeaderXLoop:
170             break;
171         case Akonadi::EmailSearchTerm::HeaderXMailingList:
172             break;
173         case Akonadi::EmailSearchTerm::HeaderXSpamFlag:
174             break;
175         case Akonadi::EmailSearchTerm::Unknown:
176         default:
177             qCWarning(IMAPRESOURCE_LOG) << "unknown term " << term.key();
178         }
179     }
180     return KIMAP::Term();
181 }
182 
doSearch(KIMAP::Session * session)183 void SearchTask::doSearch(KIMAP::Session *session)
184 {
185     qCDebug(IMAPRESOURCE_LOG) << m_query;
186 
187     Akonadi::SearchQuery query = Akonadi::SearchQuery::fromJSON(m_query.toLatin1());
188     auto searchJob = new KIMAP::SearchJob(session);
189     searchJob->setUidBased(true);
190 
191     KIMAP::Term term = recursiveEmailTermMapping(query.term());
192     if (term.isNull()) {
193         qCWarning(IMAPRESOURCE_LOG) << "failed to translate query " << m_query;
194         searchFinished(QVector<qint64>());
195         cancelTask(i18n("Invalid search"));
196         return;
197     }
198     searchJob->setTerm(term);
199 
200     connect(searchJob, &KJob::finished, this, &SearchTask::onSearchDone);
201     searchJob->start();
202 }
203 
onSearchDone(KJob * job)204 void SearchTask::onSearchDone(KJob *job)
205 {
206     if (job->error()) {
207         qCWarning(IMAPRESOURCE_LOG) << "Failed to execute search " << job->errorString();
208         qCDebug(IMAPRESOURCE_LOG) << m_query;
209         searchFinished(QVector<qint64>());
210         cancelTask(job->errorString());
211         return;
212     }
213 
214     auto searchJob = qobject_cast<KIMAP::SearchJob *>(job);
215     const QVector<qint64> result = searchJob->results();
216     qCDebug(IMAPRESOURCE_LOG) << result.count() << "matches";
217 
218     searchFinished(result);
219     taskDone();
220 }
221