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