1 /*
2     sieve.cpp
3 
4     SPDX-FileCopyrightText: 2001 Hamish Rodda <meddie@yoyo.cc.monash.edu.au>
5 
6     SPDX-License-Identifier: GPL-2.0-only
7 */
8 
9 /**
10  * Portions adapted from the SMTP ioslave.
11  * SPDX-FileCopyrightText: 2000, 2001 Alex Zepeda <jazepeda@pacbell.net>
12  * SPDX-FileCopyrightText: 2001 Michael Häckel <Michael@Haeckel.Net>
13  *
14  * Policy: the function where the error occurs calls error(). A result of
15  * false, where it signifies an error, thus doesn't need to call error() itself.
16  */
17 
18 #include "sieve.h"
19 #include "../common.h"
20 #include "sieve_debug.h"
21 
22 extern "C" {
23 #include <sasl/sasl.h>
24 }
25 
26 #include <QRegularExpression>
27 #include <QSslSocket>
28 #include <QUrlQuery>
29 
30 #include <KLocalizedString>
31 #include <KMessageBox>
32 #include <QApplication>
33 #include <QUrl>
34 #include <cassert>
35 #include <sys/stat.h>
36 namespace
37 {
returnEndLine()38 auto returnEndLine()
39 {
40     return Qt::endl;
41 }
42 }
43 #define ksDebug qCDebug(SIEVE_LOG)
44 
45 #define SIEVE_DEFAULT_PORT 2000
46 
47 static const sasl_callback_t callbacks[] = {{SASL_CB_ECHOPROMPT, nullptr, nullptr},
48                                             {SASL_CB_NOECHOPROMPT, nullptr, nullptr},
49                                             {SASL_CB_GETREALM, nullptr, nullptr},
50                                             {SASL_CB_USER, nullptr, nullptr},
51                                             {SASL_CB_AUTHNAME, nullptr, nullptr},
52                                             {SASL_CB_PASS, nullptr, nullptr},
53                                             {SASL_CB_CANON_USER, nullptr, nullptr},
54                                             {SASL_CB_LIST_END, nullptr, nullptr}};
55 
56 static const unsigned int SIEVE_DEFAULT_RECIEVE_BUFFER = 512;
57 
58 // Pseudo plugin class to embed meta data
59 class KIOPluginForMetaData : public QObject
60 {
61     Q_OBJECT
62     Q_PLUGIN_METADATA(IID "org.kde.kio.slave.sieve" FILE "sieve.json")
63 };
64 
65 using namespace KIO;
66 extern "C" {
kdemain(int argc,char ** argv)67 Q_DECL_EXPORT int kdemain(int argc, char **argv)
68 {
69     QApplication app(argc, argv);
70     app.setApplicationName(QStringLiteral("kio_sieve"));
71 
72     ksDebug << "*** Starting kio_sieve " << returnEndLine();
73 
74     if (argc != 4) {
75         ksDebug << "Usage: kio_sieve protocol domain-socket1 domain-socket2" << returnEndLine();
76         return -1;
77     }
78 
79     if (!initSASL()) {
80         ::exit(-1);
81     }
82 
83     kio_sieveProtocol slave(argv[2], argv[3]);
84     slave.dispatchLoop();
85 
86     sasl_done();
87 
88     ksDebug << "*** kio_sieve Done" << returnEndLine();
89     return 0;
90 }
91 }
92 
93 /* ---------------------------------------------------------------------------------- */
kio_sieveResponse()94 kio_sieveResponse::kio_sieveResponse()
95 {
96     clear();
97 }
98 
99 /* ---------------------------------------------------------------------------------- */
getType() const100 const uint &kio_sieveResponse::getType() const
101 {
102     return rType;
103 }
104 
105 /* ---------------------------------------------------------------------------------- */
getQuantity() const106 uint kio_sieveResponse::getQuantity() const
107 {
108     return quantity;
109 }
110 
111 /* ---------------------------------------------------------------------------------- */
getAction() const112 const QByteArray &kio_sieveResponse::getAction() const
113 {
114     return key;
115 }
116 
117 /* ---------------------------------------------------------------------------------- */
getKey() const118 const QByteArray &kio_sieveResponse::getKey() const
119 {
120     return key;
121 }
122 
123 /* ---------------------------------------------------------------------------------- */
getVal() const124 const QByteArray &kio_sieveResponse::getVal() const
125 {
126     return val;
127 }
128 
129 /* ---------------------------------------------------------------------------------- */
getExtra() const130 const QByteArray &kio_sieveResponse::getExtra() const
131 {
132     return extra;
133 }
134 
135 /* ---------------------------------------------------------------------------------- */
setQuantity(uint newQty)136 void kio_sieveResponse::setQuantity(uint newQty)
137 {
138     rType = QUANTITY;
139     quantity = newQty;
140 }
141 
142 /* ---------------------------------------------------------------------------------- */
setAction(const QByteArray & newAction)143 void kio_sieveResponse::setAction(const QByteArray &newAction)
144 {
145     rType = ACTION;
146     key = newAction;
147 }
148 
149 /* ---------------------------------------------------------------------------------- */
setKey(const QByteArray & newKey)150 void kio_sieveResponse::setKey(const QByteArray &newKey)
151 {
152     rType = KEY_VAL_PAIR;
153     key = newKey;
154 }
155 
156 /* ---------------------------------------------------------------------------------- */
setVal(const QByteArray & newVal)157 void kio_sieveResponse::setVal(const QByteArray &newVal)
158 {
159     val = newVal;
160 }
161 
162 /* ---------------------------------------------------------------------------------- */
setExtra(const QByteArray & newExtra)163 void kio_sieveResponse::setExtra(const QByteArray &newExtra)
164 {
165     extra = newExtra;
166 }
167 
168 /* ---------------------------------------------------------------------------------- */
clear()169 void kio_sieveResponse::clear()
170 {
171     rType = NONE;
172     extra = key = val = QByteArray();
173     quantity = 0;
174 }
175 
176 /* ---------------------------------------------------------------------------------- */
kio_sieveProtocol(const QByteArray & pool_socket,const QByteArray & app_socket)177 kio_sieveProtocol::kio_sieveProtocol(const QByteArray &pool_socket, const QByteArray &app_socket)
178     : TCPSlaveBase("sieve", pool_socket, app_socket, false)
179     , m_connMode(NORMAL)
180     , m_supportsTLS(false)
181     , m_shouldBeConnected(false)
182     , m_allowUnencrypted(false)
183     , m_port(SIEVE_DEFAULT_PORT)
184 {
185 }
186 
187 /* ---------------------------------------------------------------------------------- */
~kio_sieveProtocol()188 kio_sieveProtocol::~kio_sieveProtocol()
189 {
190     if (isConnected()) {
191         disconnect();
192     }
193 }
194 
195 /* ---------------------------------------------------------------------------------- */
setHost(const QString & host,quint16 port,const QString & user,const QString & pass)196 void kio_sieveProtocol::setHost(const QString &host, quint16 port, const QString &user, const QString &pass)
197 {
198     if (isConnected() && (m_sServer != host || m_port != port || m_sUser != user || m_sPass != pass)) {
199         disconnect();
200     }
201     m_sServer = host;
202     m_port = port ? port : SIEVE_DEFAULT_PORT;
203     m_sUser = user;
204     m_sPass = pass;
205     m_supportsTLS = false;
206 }
207 
208 /* ---------------------------------------------------------------------------------- */
openConnection()209 void kio_sieveProtocol::openConnection()
210 {
211     m_connMode = CONNECTION_ORIENTED;
212     connect();
213 }
214 
parseCapabilities(bool requestCapabilities)215 bool kio_sieveProtocol::parseCapabilities(bool requestCapabilities /* = false*/)
216 {
217     ksDebug << returnEndLine();
218 
219     // Setup...
220     bool ret = false;
221 
222     if (requestCapabilities) {
223         sendData("CAPABILITY");
224     }
225 
226     while (receiveData()) {
227         ksDebug << "Looping receive" << returnEndLine();
228 
229         if (r.getType() == kio_sieveResponse::ACTION) {
230             if (r.getAction().toLower().contains("ok")) {
231                 ksDebug << "Sieve server ready & awaiting authentication." << returnEndLine();
232                 break;
233             } else {
234                 ksDebug << "Unknown action " << r.getAction() << "." << returnEndLine();
235             }
236         } else if (r.getKey() == "IMPLEMENTATION") {
237             ksDebug << "Connected to Sieve server: " << r.getVal() << returnEndLine();
238             ret = true;
239             setMetaData(QStringLiteral("implementation"), QLatin1String(r.getVal()));
240             m_implementation = QLatin1String(r.getVal());
241         } else if (r.getKey() == "SASL") {
242             // Save list of available SASL methods
243             const QString val = QLatin1String(r.getVal());
244             m_sasl_caps = val.split(QLatin1Char(' '));
245             ksDebug << "Server SASL authentication methods: " << m_sasl_caps.join(QLatin1String(", ")) << returnEndLine();
246             setMetaData(QStringLiteral("saslMethods"), QLatin1String(r.getVal()));
247         } else if (r.getKey() == "SIEVE") {
248             // Save script capabilities; report back as meta data:
249             const QString val = QLatin1String(r.getVal());
250             ksDebug << "Server script capabilities: " << val.split(QLatin1Char(' ')).join(QLatin1String(", ")) << returnEndLine();
251             setMetaData(QStringLiteral("sieveExtensions"), QLatin1String(r.getVal()));
252         } else if (r.getKey() == "STARTTLS") {
253             // The server supports TLS
254             ksDebug << "Server supports TLS" << returnEndLine();
255             m_supportsTLS = true;
256             setMetaData(QStringLiteral("tlsSupported"), QStringLiteral("true"));
257         } else {
258             ksDebug << "Unrecognised key " << r.getKey() << returnEndLine();
259         }
260     }
261 
262     if (!m_supportsTLS) {
263         setMetaData(QStringLiteral("tlsSupported"), QStringLiteral("false"));
264     }
265 
266     return ret;
267 }
268 
269 /* ---------------------------------------------------------------------------------- */
270 /**
271  * Checks if connection parameters have changed.
272  * If it it, close the current connection
273  */
changeCheck(const QUrl & url)274 void kio_sieveProtocol::changeCheck(const QUrl &url)
275 {
276     QString auth;
277 
278     // Check the SASL auth mechanism in the 'sasl' metadata...
279     if (!metaData(QStringLiteral("sasl")).isEmpty()) {
280         auth = metaData(QStringLiteral("sasl")).toUpper();
281     } else {
282         // ... and if not found, check the x-mech=AUTH query part of the url.
283         QString query = url.query();
284         if (query.startsWith(QLatin1Char('?'))) {
285             query.remove(0, 1);
286         }
287         QStringList q = query.split(QLatin1Char(','));
288 
289         for (QStringList::iterator it = q.begin(), end(q.end()); it != end; ++it) {
290             if (((*it).section(QLatin1Char('='), 0, 0)).toLower() == QLatin1String("x-mech")) {
291                 auth = ((*it).section(QLatin1Char('='), 1)).toUpper();
292                 break;
293             }
294         }
295     }
296     ksDebug << "auth: " << auth << " m_sAuth: " << m_sAuth << returnEndLine();
297     if (m_sAuth != auth) {
298         m_sAuth = auth;
299         if (isConnected()) {
300             disconnect();
301         }
302     }
303     // For TLS, only disconnect if we are unencrypted and are
304     // no longer allowed (otherwise, it's still fine):
305     const bool allowUnencryptedNow = QUrlQuery(url).queryItemValue(QStringLiteral("x-allow-unencrypted")) == QLatin1String("true");
306     if (m_allowUnencrypted && !allowUnencryptedNow) {
307         if (isConnected()) {
308             disconnect();
309         }
310     }
311     m_allowUnencrypted = allowUnencryptedNow;
312 }
313 
314 /* ---------------------------------------------------------------------------------- */
315 /**
316  * Connects to the server.
317  * returns false and calls error() if an error occurred.
318  */
connect(bool useTLSIfAvailable)319 bool kio_sieveProtocol::connect(bool useTLSIfAvailable)
320 {
321     ksDebug << returnEndLine();
322 
323     if (isConnected()) {
324         return true;
325     }
326 
327     infoMessage(i18n("Connecting to %1...", m_sServer));
328 
329     if (m_connMode == CONNECTION_ORIENTED && m_shouldBeConnected) {
330         error(ERR_CONNECTION_BROKEN, i18n("The connection to the server was lost."));
331         return false;
332     }
333 
334     setBlocking(true);
335 
336     if (!connectToHost(QStringLiteral("sieve"), m_sServer, m_port)) {
337         return false;
338     }
339 
340     if (!parseCapabilities()) {
341         disconnectFromHost();
342         error(ERR_UNSUPPORTED_PROTOCOL, i18n("Server identification failed."));
343         return false;
344     }
345 
346     // Attempt to start TLS
347     if (!m_allowUnencrypted && !QSslSocket::supportsSsl()) {
348         error(ERR_SLAVE_DEFINED, i18n("Can not use TLS since the underlying Qt library does not support it."));
349         disconnect();
350         return false;
351     }
352 
353     if (!m_allowUnencrypted && useTLSIfAvailable && QSslSocket::supportsSsl() && !m_supportsTLS
354         && messageBox(WarningContinueCancel,
355                       i18n("TLS encryption was requested, but your Sieve server does not advertise TLS in its capabilities.\n"
356                            "You can choose to try to initiate TLS negotiations nonetheless, or cancel the operation."),
357                       i18n("Server Does Not Advertise TLS"),
358                       i18n("&Start TLS nonetheless"),
359                       i18n("&Cancel"))
360             != KMessageBox::Continue) {
361         error(ERR_USER_CANCELED, i18n("TLS encryption requested, but not supported by server."));
362         disconnect();
363         return false;
364     }
365 
366     // FIXME find a test server and test that this works
367     if (useTLSIfAvailable && m_supportsTLS && QSslSocket::supportsSsl()) {
368         sendData("STARTTLS");
369         if (operationSuccessful()) {
370             ksDebug << "TLS has been accepted. Starting TLS..." << returnEndLine() << "WARNING this is untested and may fail.";
371             if (startSsl()) {
372                 ksDebug << "TLS enabled successfully." << returnEndLine();
373                 // reparse capabilities:
374                 parseCapabilities(requestCapabilitiesAfterStartTLS());
375             } else {
376                 ksDebug << "TLS initiation failed.";
377                 if (m_allowUnencrypted) {
378                     disconnect(true);
379                     return connect(false);
380                 }
381                 messageBox(Information,
382                            i18n("Your Sieve server claims to support TLS, "
383                                 "but negotiation was unsuccessful."),
384                            i18n("Connection Failed"));
385                 disconnect(true);
386                 return false;
387             }
388         } else if (!m_allowUnencrypted) {
389             ksDebug << "Server incapable of TLS.";
390             disconnect();
391             error(ERR_SLAVE_DEFINED,
392                   i18n("The server does not seem to support TLS. "
393                        "Disable TLS if you want to connect without encryption."));
394             return false;
395         } else {
396             ksDebug << "Server incapable of TLS. Transmitted documents will be unencrypted." << returnEndLine();
397         }
398     } else {
399         ksDebug << "We are incapable of TLS. Transmitted documents will be unencrypted." << returnEndLine();
400     }
401 
402     assert(m_allowUnencrypted || isUsingSsl());
403 
404     infoMessage(i18n("Authenticating user..."));
405     if (!authenticate()) {
406         disconnect();
407         error(ERR_CANNOT_AUTHENTICATE, i18n("Authentication failed."));
408         return false;
409     }
410 
411     m_shouldBeConnected = true;
412     return true;
413 }
414 
415 /* ---------------------------------------------------------------------------------- */
closeConnection()416 void kio_sieveProtocol::closeConnection()
417 {
418     m_connMode = CONNECTION_ORIENTED;
419     disconnect();
420 }
421 
422 /* ---------------------------------------------------------------------------------- */
disconnect(bool forcibly)423 void kio_sieveProtocol::disconnect(bool forcibly)
424 {
425     if (!forcibly) {
426         sendData("LOGOUT");
427 
428         if (!operationSuccessful()) {
429             ksDebug << "Server did not logout cleanly." << returnEndLine();
430         }
431     }
432 
433     disconnectFromHost();
434     m_shouldBeConnected = false;
435 }
436 
437 /* ---------------------------------------------------------------------------------- */
438 /*void kio_sieveProtocol::slave_status()
439 {
440     slaveStatus(isConnected() ? m_sServer : "", isConnected());
441 
442     finished();
443 }*/
444 
445 /* ---------------------------------------------------------------------------------- */
special(const QByteArray & data)446 void kio_sieveProtocol::special(const QByteArray &data)
447 {
448     int tmp;
449     QDataStream stream(data);
450     QUrl url;
451 
452     stream >> tmp;
453 
454     switch (tmp) {
455     case 1:
456         stream >> url;
457         if (!activate(url)) {
458             return;
459         }
460         break;
461     case 2:
462         if (!deactivate()) {
463             return;
464         }
465         break;
466     case 3:
467         parseCapabilities(true);
468         break;
469     }
470 
471     infoMessage(i18nc("special command completed", "Done."));
472 
473     finished();
474 }
475 
476 /* ---------------------------------------------------------------------------------- */
activate(const QUrl & url)477 bool kio_sieveProtocol::activate(const QUrl &url)
478 {
479     changeCheck(url);
480     if (!connect()) {
481         return false;
482     }
483 
484     infoMessage(i18n("Activating script..."));
485 
486     QString filename = url.fileName();
487 
488     if (filename.isEmpty()) {
489         error(ERR_DOES_NOT_EXIST, url.toDisplayString());
490         return false;
491     }
492 
493     if (!sendData("SETACTIVE \"" + filename.toUtf8() + "\"")) {
494         return false;
495     }
496 
497     if (operationSuccessful()) {
498         ksDebug << "Script activation complete." << returnEndLine();
499         return true;
500     } else {
501         error(ERR_INTERNAL_SERVER, i18n("There was an error activating the script."));
502         return false;
503     }
504 }
505 
506 /* ---------------------------------------------------------------------------------- */
deactivate()507 bool kio_sieveProtocol::deactivate()
508 {
509     if (!connect()) {
510         return false;
511     }
512 
513     if (!sendData("SETACTIVE \"\"")) {
514         return false;
515     }
516 
517     if (operationSuccessful()) {
518         ksDebug << "Script deactivation complete." << returnEndLine();
519         return true;
520     } else {
521         error(ERR_INTERNAL_SERVER, i18n("There was an error deactivating the script."));
522         return false;
523     }
524 }
525 
append_lf2crlf(QByteArray & out,const QByteArray & in)526 static void append_lf2crlf(QByteArray &out, const QByteArray &in)
527 {
528     if (in.isEmpty()) {
529         return;
530     }
531     const unsigned int oldOutSize = out.size();
532     out.resize(oldOutSize + 2 * in.size());
533     const char *s = in.begin();
534     const char *const end = in.end();
535     char *d = out.begin() + oldOutSize;
536     char last = '\0';
537     while (s < end) {
538         if (*s == '\n' && last != '\r') {
539             *d++ = '\r';
540         }
541         *d++ = last = *s++;
542     }
543     out.resize(d - out.begin());
544 }
545 
put(const QUrl & url,int,KIO::JobFlags)546 void kio_sieveProtocol::put(const QUrl &url, int /*permissions*/, KIO::JobFlags)
547 {
548     changeCheck(url);
549     if (!connect()) {
550         return;
551     }
552 
553     infoMessage(i18n("Sending data..."));
554 
555     QString filename = url.fileName();
556 
557     if (filename.isEmpty()) {
558         error(ERR_MALFORMED_URL, url.toDisplayString());
559         return;
560     }
561 
562     QByteArray data;
563     for (;;) {
564         dataReq();
565         QByteArray buffer;
566         const int newSize = readData(buffer);
567         append_lf2crlf(data, buffer);
568         if (newSize < 0) {
569             // read error: network in unknown state so disconnect
570             error(ERR_CANNOT_READ, i18n("KIO data supply error."));
571             return;
572         }
573         if (newSize == 0) {
574             break;
575         }
576     }
577 
578     // script size
579     int bufLen = (int)data.size();
580     totalSize(bufLen);
581 
582     // timsieved 1.1.0:
583     // C: HAVESPACE "rejected" 74
584     // S: NO "Number expected"
585     // C: HAVESPACE 74
586     // S: NO "Missing script name"
587     // S: HAVESPACE "rejected" "74"
588     // C: NO "Number expected"
589     // => broken, we can't use it :-(
590     // (will be fixed in Cyrus 2.1.10)
591 
592     if (!sendData("PUTSCRIPT \"" + filename.toUtf8() + "\" {" + QByteArray::number(bufLen) + "+}")) {
593         return;
594     }
595 
596     // atEnd() lies so the code below doesn't work.
597     /*if (!atEnd()) {
598         // We are not expecting any data here, so if the server has responded
599         // with anything but OK we treat it as an error.
600         char * buf = new char[2];
601         while (!atEnd()) {
602                 ksDebug << "Reading..." << returnEndLine();
603                 read(buf, 1);
604                 ksDebug << "Trailing [" << buf[0] << "]" << returnEndLine();
605         }
606         ksDebug << "End of data." << returnEndLine();
607         delete[] buf;
608 
609         if (!operationSuccessful()) {
610                 error(ERR_UNSUPPORTED_PROTOCOL, i18n("A protocol error occurred "
611                                         "while trying to negotiate script uploading.\n"
612                                         "The server responded:\n%1")
613                                                 .arg(r.getAction().right(r.getAction().length() - 3)));
614                 return;
615         }
616     }*/
617 
618     // upload data to the server
619     if (write(data.constData(), bufLen) != bufLen) {
620         error(ERR_CANNOT_WRITE, i18n("Network error."));
621         disconnect(true);
622         return;
623     }
624 
625     // finishing CR/LF
626     if (!sendData("")) {
627         return;
628     }
629 
630     processedSize(bufLen);
631 
632     infoMessage(i18n("Verifying upload completion..."));
633 
634     if (operationSuccessful()) {
635         ksDebug << "Script upload complete." << returnEndLine();
636     } else {
637         /* The managesieve server parses received scripts and rejects
638          * scripts which are not syntactically correct. Here we expect
639          * to receive a message detailing the error (only the first
640          * error is reported. */
641         if (r.getAction().length() > 3) {
642             // make a copy of the extra info
643             QByteArray extra = r.getAction().right(r.getAction().length() - 3);
644 
645             // send the extra message off for re-processing
646             receiveData(false, extra);
647 
648             if (r.getType() == kio_sieveResponse::QUANTITY) {
649                 // length of the error message
650                 uint len = r.getQuantity();
651 
652                 QByteArray errmsg(len, 0);
653 
654                 read(errmsg.data(), len);
655 
656                 error(ERR_INTERNAL_SERVER,
657                       i18n("The script did not upload successfully.\n"
658                            "This is probably due to errors in the script.\n"
659                            "The server responded:\n%1",
660                            QString::fromLatin1(errmsg.data(), errmsg.size())));
661 
662                 // clear the rest of the incoming data
663                 receiveData();
664             } else if (r.getType() == kio_sieveResponse::KEY_VAL_PAIR) {
665                 error(ERR_INTERNAL_SERVER,
666                       i18n("The script did not upload successfully.\n"
667                            "This is probably due to errors in the script.\n"
668                            "The server responded:\n%1",
669                            QString::fromUtf8(r.getKey())));
670             } else {
671                 error(ERR_INTERNAL_SERVER,
672                       i18n("The script did not upload successfully.\n"
673                            "The script may contain errors."));
674             }
675         } else {
676             error(ERR_INTERNAL_SERVER,
677                   i18n("The script did not upload successfully.\n"
678                        "The script may contain errors."));
679         }
680     }
681 
682     // if ( permissions != -1 )
683     //        chmod( url, permissions );
684 
685     infoMessage(i18nc("data upload complete", "Done."));
686 
687     finished();
688 }
689 
inplace_crlf2lf(QByteArray & in)690 static void inplace_crlf2lf(QByteArray &in)
691 {
692     if (in.isEmpty()) {
693         return;
694     }
695     QByteArray &out = in; // inplace
696     const char *s = in.begin();
697     const char *const end = in.end();
698     char *d = out.begin();
699     char last = '\0';
700     while (s < end) {
701         if (*s == '\n' && last == '\r') {
702             --d;
703         }
704         *d++ = last = *s++;
705     }
706     out.resize(d - out.begin());
707 }
708 
709 /* ---------------------------------------------------------------------------------- */
get(const QUrl & url)710 void kio_sieveProtocol::get(const QUrl &url)
711 {
712     changeCheck(url);
713     if (!connect()) {
714         return;
715     }
716 
717     infoMessage(i18n("Retrieving data..."));
718 
719     QString filename = url.fileName();
720 
721     if (filename.isEmpty()) {
722         error(ERR_MALFORMED_URL, url.toDisplayString());
723         return;
724     }
725 
726     // SlaveBase::mimetype( QString("text/plain") ); // "application/sieve");
727 
728     if (!sendData("GETSCRIPT \"" + filename.toUtf8() + "\"")) {
729         return;
730     }
731 
732     if (receiveData() && r.getType() == kio_sieveResponse::QUANTITY) {
733         // determine script size
734         ssize_t total_len = r.getQuantity();
735         totalSize(total_len);
736 
737         ssize_t recv_len = 0;
738         do {
739             // wait for data...
740             if (!waitForResponse(600)) {
741                 error(KIO::ERR_SERVER_TIMEOUT, m_sServer);
742                 disconnect(true);
743                 return;
744             }
745 
746             // ...read data...
747             // Only read as much as we need, otherwise we slurp in the OK that
748             // operationSuccessful() is expecting below.
749             QByteArray dat(qMin(total_len - recv_len, ssize_t(64 * 1024)), '\0');
750             ssize_t this_recv_len = read(dat.data(), dat.size());
751 
752             if (this_recv_len < 1 && !isConnected()) {
753                 error(KIO::ERR_CONNECTION_BROKEN, m_sServer);
754                 disconnect(true);
755                 return;
756             }
757 
758             dat.resize(this_recv_len);
759             inplace_crlf2lf(dat);
760             // send data to slaveinterface
761             data(dat);
762 
763             recv_len += this_recv_len;
764             processedSize(recv_len);
765         } while (recv_len < total_len);
766 
767         infoMessage(i18n("Finishing up..."));
768         data(QByteArray());
769 
770         if (operationSuccessful()) {
771             ksDebug << "Script retrieval complete." << returnEndLine();
772         } else {
773             ksDebug << "Script retrieval failed." << returnEndLine();
774         }
775     } else {
776         error(ERR_UNSUPPORTED_PROTOCOL,
777               i18n("A protocol error occurred "
778                    "while trying to negotiate script downloading."));
779         return;
780     }
781 
782     infoMessage(i18nc("data retrieval complete", "Done."));
783     finished();
784 }
785 
del(const QUrl & url,bool isfile)786 void kio_sieveProtocol::del(const QUrl &url, bool isfile)
787 {
788     if (!isfile) {
789         error(ERR_INTERNAL, i18n("Folders are not supported."));
790         return;
791     }
792 
793     changeCheck(url);
794     if (!connect()) {
795         return;
796     }
797 
798     infoMessage(i18n("Deleting file..."));
799 
800     QString filename = url.fileName();
801 
802     if (filename.isEmpty()) {
803         error(ERR_MALFORMED_URL, url.toDisplayString());
804         return;
805     }
806 
807     if (!sendData("DELETESCRIPT \"" + filename.toUtf8() + "\"")) {
808         return;
809     }
810 
811     if (operationSuccessful()) {
812         ksDebug << "Script deletion successful." << returnEndLine();
813     } else {
814         error(ERR_INTERNAL_SERVER, i18n("The server would not delete the file."));
815         return;
816     }
817 
818     infoMessage(i18nc("file removal complete", "Done."));
819 
820     finished();
821 }
822 
chmod(const QUrl & url,int permissions)823 void kio_sieveProtocol::chmod(const QUrl &url, int permissions)
824 {
825     switch (permissions) {
826     case 0700: // activate
827         activate(url);
828         break;
829     case 0600: // deactivate
830         deactivate();
831         break;
832     default: // unsupported
833         error(ERR_CANNOT_CHMOD, i18n("Cannot chmod to anything but 0700 (active) or 0600 (inactive script)."));
834         return;
835     }
836 
837     finished();
838 }
839 
urlStat(const QUrl & url)840 void kio_sieveProtocol::urlStat(const QUrl &url)
841 {
842     changeCheck(url);
843     if (!connect()) {
844         return;
845     }
846 
847     UDSEntry entry;
848 
849     QString filename = url.fileName();
850 
851     if (filename.isEmpty()) {
852         entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral("/"));
853 
854         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
855 
856         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700);
857 
858         statEntry(entry);
859     } else {
860         if (!sendData("LISTSCRIPTS")) {
861             return;
862         }
863 
864         while (receiveData()) {
865             if (r.getType() == kio_sieveResponse::ACTION) {
866                 if (r.getAction().toLower().count("ok") == 1) {
867                     // Script list completed
868                     break;
869                 }
870             } else {
871                 if (filename == QString::fromUtf8(r.getKey())) {
872                     entry.clear();
873 
874                     entry.fastInsert(KIO::UDSEntry::UDS_NAME, QString::fromUtf8(r.getKey()));
875 
876                     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
877 
878                     if (r.getExtra() == "ACTIVE") {
879                         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700);
880                     } else {
881                         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0600);
882                     }
883 
884                     entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/sieve"));
885 
886                     // setMetaData("active", (r.getExtra() == "ACTIVE") ? "yes" : "no");
887 
888                     statEntry(entry);
889                     // cannot break here because we need to clear
890                     // the rest of the incoming data.
891                 }
892             }
893         }
894     }
895 
896     finished();
897 }
898 
listDir(const QUrl & url)899 void kio_sieveProtocol::listDir(const QUrl &url)
900 {
901     changeCheck(url);
902     if (!connect()) {
903         return;
904     }
905 
906     if (!sendData("LISTSCRIPTS")) {
907         return;
908     }
909 
910     UDSEntry entry;
911 
912     while (receiveData()) {
913         if (r.getType() == kio_sieveResponse::ACTION) {
914             if (r.getAction().toLower().count("ok") == 1) {
915                 // Script list completed.
916                 break;
917             }
918         } else {
919             entry.clear();
920             entry.fastInsert(KIO::UDSEntry::UDS_NAME, QString::fromUtf8(r.getKey()));
921 
922             entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
923 
924             if (r.getExtra() == "ACTIVE") {
925                 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700); // mark exec'able
926             } else {
927                 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0600);
928             }
929 
930             entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/sieve"));
931 
932             // asetMetaData("active", (r.getExtra() == "ACTIVE") ? "true" : "false");
933 
934             ksDebug << "Listing script " << r.getKey() << returnEndLine();
935             listEntry(entry);
936         }
937     }
938 
939     finished();
940 }
941 
942 /* ---------------------------------------------------------------------------------- */
saslInteract(void * in,AuthInfo & ai)943 bool kio_sieveProtocol::saslInteract(void *in, AuthInfo &ai)
944 {
945     ksDebug << "sasl_interact" << returnEndLine();
946     auto *interact = (sasl_interact_t *)in;
947 
948     // some mechanisms do not require username && pass, so it doesn't need a popup
949     // window for getting this info
950     for (; interact->id != SASL_CB_LIST_END; interact++) {
951         if (interact->id == SASL_CB_AUTHNAME || interact->id == SASL_CB_PASS) {
952             if (m_sUser.isEmpty() || m_sPass.isEmpty()) {
953                 const int errorCode = openPasswordDialogV2(ai);
954                 if (errorCode) {
955                     // calling error() below is wrong for two reasons:
956                     // - ERR_ABORTED is too harsh
957                     // - higher layers already call error() and that can't happen twice.
958                     // error(ERR_ABORTED, i18n("No authentication details supplied."));
959                     error(errorCode, QString());
960                     return false;
961                 }
962                 m_sUser = ai.username;
963                 m_sPass = ai.password;
964             }
965             break;
966         }
967     }
968 
969     interact = (sasl_interact_t *)in;
970     while (interact->id != SASL_CB_LIST_END) {
971         ksDebug << "SASL_INTERACT id: " << interact->id << returnEndLine();
972         switch (interact->id) {
973         case SASL_CB_USER:
974         case SASL_CB_AUTHNAME:
975             ksDebug << "SASL_CB_[AUTHNAME|USER]: '" << m_sUser << "'" << returnEndLine();
976             interact->result = strdup(m_sUser.toUtf8().constData());
977             interact->len = strlen((const char *)interact->result);
978             break;
979         case SASL_CB_PASS:
980             ksDebug << "SASL_CB_PASS: [hidden] " << returnEndLine();
981             interact->result = strdup(m_sPass.toUtf8().constData());
982             interact->len = strlen((const char *)interact->result);
983             break;
984         default:
985             interact->result = nullptr;
986             interact->len = 0;
987             break;
988         }
989         interact++;
990     }
991     return true;
992 }
993 
994 #define SASLERROR error(ERR_CANNOT_AUTHENTICATE, i18n("An error occurred during authentication: %1", QString::fromUtf8(sasl_errdetail(conn))));
995 
authenticate()996 bool kio_sieveProtocol::authenticate()
997 {
998     int result;
999     sasl_conn_t *conn = nullptr;
1000     sasl_interact_t *client_interact = nullptr;
1001     const char *out = nullptr;
1002     uint outlen;
1003     const char *mechusing = nullptr;
1004     QByteArray challenge;
1005 
1006     /* Retrieve authentication details from user.
1007      * Note: should this require realm as well as user & pass details
1008      * before it automatically skips the prompt?
1009      * Note2: encoding issues with PLAIN login? */
1010     AuthInfo ai;
1011     ai.url.setScheme(QStringLiteral("sieve"));
1012     ai.url.setHost(m_sServer);
1013     ai.url.setPort(m_port);
1014     ai.username = m_sUser;
1015     ai.password = m_sPass;
1016     ai.keepPassword = true;
1017     ai.caption = i18n("Sieve Authentication Details");
1018     ai.comment = i18n(
1019         "Please enter your authentication details for your sieve account "
1020         "(usually the same as your email password):");
1021 
1022     result = sasl_client_new("sieve", m_sServer.toLatin1().constData(), nullptr, nullptr, callbacks, 0, &conn);
1023     if (result != SASL_OK) {
1024         ksDebug << "sasl_client_new failed with: " << result << returnEndLine();
1025         SASLERROR
1026         return false;
1027     }
1028 
1029     QStringList strList;
1030     //    strList.append("NTLM");
1031 
1032     if (!m_sAuth.isEmpty()) {
1033         strList.append(m_sAuth);
1034     } else {
1035         strList = m_sasl_caps;
1036     }
1037 
1038     do {
1039         result = sasl_client_start(conn, strList.join(QLatin1Char(' ')).toLatin1().constData(), &client_interact, &out, &outlen, &mechusing);
1040 
1041         if (result == SASL_INTERACT) {
1042             if (!saslInteract(client_interact, ai)) {
1043                 sasl_dispose(&conn);
1044                 return false;
1045             }
1046         }
1047     } while (result == SASL_INTERACT);
1048 
1049     if (result != SASL_CONTINUE && result != SASL_OK) {
1050         ksDebug << "sasl_client_start failed with: " << result << returnEndLine();
1051         SASLERROR
1052         sasl_dispose(&conn);
1053         return false;
1054     }
1055 
1056     ksDebug << "Preferred authentication method is " << mechusing << "." << returnEndLine();
1057 
1058     QString firstCommand = QLatin1String("AUTHENTICATE \"") + QString::fromLatin1(mechusing) + QLatin1String("\"");
1059     challenge = QByteArray::fromRawData(out, outlen).toBase64();
1060     if (!challenge.isEmpty()) {
1061         firstCommand += QLatin1String(" \"");
1062         firstCommand += QString::fromLatin1(challenge.data(), challenge.size());
1063         firstCommand += QLatin1Char('\"');
1064     }
1065 
1066     if (!sendData(firstCommand.toLatin1())) {
1067         return false;
1068     }
1069 
1070     do {
1071         receiveData();
1072 
1073         if (operationResult() != OTHER) {
1074             break;
1075         }
1076 
1077         ksDebug << "Challenge len  " << r.getQuantity() << returnEndLine();
1078 
1079         if (r.getType() != kio_sieveResponse::QUANTITY) {
1080             sasl_dispose(&conn);
1081             error(ERR_UNSUPPORTED_PROTOCOL, QString::fromLatin1(mechusing));
1082             return false;
1083         }
1084 
1085         int qty = r.getQuantity();
1086 
1087         receiveData();
1088 
1089         if (r.getType() != kio_sieveResponse::ACTION && r.getAction().length() != qty) {
1090             sasl_dispose(&conn);
1091             error(ERR_UNSUPPORTED_PROTOCOL,
1092                   i18n("A protocol error occurred during authentication.\n"
1093                        "Choose a different authentication method to %1.",
1094                        QLatin1String(mechusing)));
1095             return false;
1096         }
1097         challenge = QByteArray::fromBase64(QByteArray::fromRawData(r.getAction().data(), qty));
1098         //        ksDebug << "S:  [" << r.getAction() << "]." << returnEndLine();
1099 
1100         do {
1101             result = sasl_client_step(conn, challenge.isEmpty() ? nullptr : challenge.data(), challenge.size(), &client_interact, &out, &outlen);
1102 
1103             if (result == SASL_INTERACT) {
1104                 if (!saslInteract(client_interact, ai)) {
1105                     sasl_dispose(&conn);
1106                     return false;
1107                 }
1108             }
1109         } while (result == SASL_INTERACT);
1110 
1111         ksDebug << "sasl_client_step: " << result << returnEndLine();
1112         if (result != SASL_CONTINUE && result != SASL_OK) {
1113             ksDebug << "sasl_client_step failed with: " << result << returnEndLine();
1114             SASLERROR
1115             sasl_dispose(&conn);
1116             return false;
1117         }
1118 
1119         sendData('\"' + QByteArray::fromRawData(out, outlen).toBase64() + '\"');
1120         //    ksDebug << "C-1:  [" << out << "]." << returnEndLine();
1121     } while (true);
1122 
1123     ksDebug << "Challenges finished." << returnEndLine();
1124     sasl_dispose(&conn);
1125 
1126     if (operationResult() == OK) {
1127         // Authentication succeeded.
1128         return true;
1129     } else {
1130         // Authentication failed.
1131         error(ERR_CANNOT_AUTHENTICATE,
1132               i18n("Authentication failed.\nMost likely the password is wrong.\nThe server responded:\n%1", QString::fromLatin1(r.getAction())));
1133         return false;
1134     }
1135 }
1136 
1137 /* --------------------------------------------------------------------------- */
mimetype(const QUrl & url)1138 void kio_sieveProtocol::mimetype(const QUrl &url)
1139 {
1140     ksDebug << "Requesting mimetype for " << url.toDisplayString() << returnEndLine();
1141 
1142     if (url.fileName().isEmpty()) {
1143         mimeType(QStringLiteral("inode/directory"));
1144     } else {
1145         mimeType(QStringLiteral("application/sieve"));
1146     }
1147 
1148     finished();
1149 }
1150 
1151 /* --------------------------------------------------------------------------- */
sendData(const QByteArray & data)1152 bool kio_sieveProtocol::sendData(const QByteArray &data)
1153 {
1154     QByteArray write_buf = data + "\r\n";
1155 
1156     // ksDebug << "C: " << data << returnEndLine();
1157 
1158     // Write the command
1159     ssize_t write_buf_len = write_buf.length();
1160     if (write(write_buf.data(), write_buf_len) != write_buf_len) {
1161         error(ERR_CANNOT_WRITE, i18n("Network error."));
1162         disconnect(true);
1163         return false;
1164     }
1165 
1166     return true;
1167 }
1168 
1169 /* --------------------------------------------------------------------------- */
receiveData(bool waitForData,const QByteArray & reparse)1170 bool kio_sieveProtocol::receiveData(bool waitForData, const QByteArray &reparse)
1171 {
1172     QByteArray interpret;
1173     int start;
1174     int end;
1175 
1176     if (reparse.isEmpty()) {
1177         if (!waitForData) {
1178             // is there data waiting?
1179             if (atEnd()) {
1180                 return false;
1181             }
1182         }
1183 
1184         // read data from the server
1185         char buffer[SIEVE_DEFAULT_RECIEVE_BUFFER];
1186         const ssize_t numRead = readLine(buffer, SIEVE_DEFAULT_RECIEVE_BUFFER - 1);
1187         if (numRead < 0) {
1188             return false;
1189         }
1190         buffer[SIEVE_DEFAULT_RECIEVE_BUFFER - 1] = '\0';
1191 
1192         // strip LF/CR
1193         interpret = QByteArray(buffer, qstrlen(buffer) - 2);
1194     } else {
1195         interpret = reparse;
1196     }
1197 
1198     r.clear();
1199 
1200     // ksDebug << "S: " << interpret << returnEndLine();
1201 
1202     switch (interpret[0]) {
1203     case '{': {
1204         // expecting {quantity}
1205         start = 0;
1206         end = interpret.indexOf("+}", start + 1);
1207         // some older versions of Cyrus enclose the literal size just in { } instead of { +}
1208         if (end == -1) {
1209             end = interpret.indexOf('}', start + 1);
1210         }
1211 
1212         bool ok = false;
1213         r.setQuantity(interpret.mid(start + 1, end - start - 1).toUInt(&ok));
1214         if (!ok) {
1215             disconnect();
1216             error(ERR_INTERNAL_SERVER, i18n("A protocol error occurred."));
1217             return false;
1218         }
1219 
1220         return true;
1221     }
1222     case '"':
1223         // expecting "key" "value" pairs
1224         break;
1225     default:
1226         // expecting single string
1227         r.setAction(interpret);
1228         return true;
1229     }
1230 
1231     start = 0;
1232 
1233     end = interpret.indexOf('"', start + 1);
1234     if (end == -1) {
1235         ksDebug << "Possible insufficient buffer size." << returnEndLine();
1236         r.setKey(interpret.right(interpret.length() - start));
1237         return true;
1238     }
1239 
1240     r.setKey(interpret.mid(start + 1, end - start - 1));
1241 
1242     start = interpret.indexOf('"', end + 1);
1243     if (start == -1) {
1244         if ((int)interpret.length() > end) {
1245             // skip " and space
1246             r.setExtra(interpret.right(interpret.length() - end - 2));
1247         }
1248 
1249         return true;
1250     }
1251 
1252     end = interpret.indexOf('"', start + 1);
1253     if (end == -1) {
1254         ksDebug << "Possible insufficient buffer size." << returnEndLine();
1255         r.setVal(interpret.right(interpret.length() - start));
1256         return true;
1257     }
1258 
1259     r.setVal(interpret.mid(start + 1, end - start - 1));
1260     return true;
1261 }
1262 
operationSuccessful()1263 bool kio_sieveProtocol::operationSuccessful()
1264 {
1265     while (receiveData(true)) {
1266         if (r.getType() == kio_sieveResponse::ACTION) {
1267             QByteArray response = r.getAction().left(2);
1268             if (response == "OK") {
1269                 return true;
1270             } else if (response == "NO") {
1271                 return false;
1272             }
1273         }
1274     }
1275     return false;
1276 }
1277 
operationResult()1278 int kio_sieveProtocol::operationResult()
1279 {
1280     if (r.getType() == kio_sieveResponse::ACTION) {
1281         QByteArray response = r.getAction().left(2);
1282         if (response == "OK") {
1283             return OK;
1284         } else if (response == "NO") {
1285             return NO;
1286         } else if (response == "BY" /*E*/) {
1287             return BYE;
1288         }
1289     }
1290 
1291     return OTHER;
1292 }
1293 
requestCapabilitiesAfterStartTLS() const1294 bool kio_sieveProtocol::requestCapabilitiesAfterStartTLS() const
1295 {
1296     // Cyrus didn't send CAPABILITIES after STARTTLS until 2.3.11, which is
1297     // not standard conform, but we need to support that anyway.
1298     // m_implementation looks like this 'Cyrus timsieved v2.2.12' for Cyrus btw.
1299     QRegularExpression regExp(QStringLiteral("Cyrus\\stimsieved\\sv(\\d+)\\.(\\d+)\\.(\\d+)([-\\w]*)"), QRegularExpression::CaseInsensitiveOption);
1300     QRegularExpressionMatch match = regExp.match(m_implementation);
1301     if (match.hasMatch()) {
1302         const int major = match.captured(1).toInt();
1303         const int minor = match.captured(2).toInt();
1304         const int patch = match.captured(3).toInt();
1305         const QString vendor = match.captured(4);
1306         if (major < 2 || (major == 2 && (minor < 3 || (minor == 3 && patch < 11))) || (vendor == QLatin1String("-kolab-nocaps"))) {
1307             ksDebug << " kio_sieveProtocol::requestCapabilitiesAfterStartTLS : Enabling compat mode for Cyrus < 2.3.11 or Cyrus marked as \"kolab-nocaps\""
1308                     << returnEndLine();
1309             return true;
1310         }
1311     }
1312     return false;
1313 }
1314 #include "sieve.moc"
1315