modernize: Use ranged-for loops in some obvious cases
[quassel.git] / src / core / corenetwork.cpp
index 552662a..29f10f0 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2014 by the Quassel Project                        *
+ *   Copyright (C) 2005-2018 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
-#include <QHostInfo>
-
 #include "corenetwork.h"
 
+#include <QDebug>
+#include <QHostInfo>
+
 #include "core.h"
 #include "coreidentity.h"
 #include "corenetworkconfig.h"
 #include "coreuserinputhandler.h"
 #include "networkevent.h"
 
-INIT_SYNCABLE_OBJECT(CoreNetwork)
+// IRCv3 capabilities
+#include "irccap.h"
+
 CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
     : Network(networkid, session),
     _coreSession(session),
     _userInputHandler(new CoreUserInputHandler(this)),
     _autoReconnectCount(0),
     _quitRequested(false),
+    _disconnectExpected(false),
 
     _previousConnectionAttemptFailed(false),
     _lastUsedServerIndex(0),
 
-    _lastPingTime(0),
-    _pingCount(0),
-    _sendPings(false),
     _requestedUserModes('-')
 {
+    // Check if raw IRC logging is enabled
+    _debugLogRawIrc = (Quassel::isOptionSet("debug-irc") || Quassel::isOptionSet("debug-irc-id"));
+    _debugLogRawNetId = Quassel::optionValue("debug-irc-id").toInt();
+
     _autoReconnectTimer.setSingleShot(true);
     connect(&_socketCloseTimer, SIGNAL(timeout()), this, SLOT(socketCloseTimeout()));
 
@@ -59,6 +64,11 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
         _channelKeys[chan.toLower()] = channels[chan];
     }
 
+    QHash<QString, QByteArray> bufferCiphers = coreSession()->bufferCiphers(networkId());
+    foreach(QString buffer, bufferCiphers.keys()) {
+        storeChannelCipherKey(buffer.toLower(), bufferCiphers[buffer]);
+    }
+
     connect(networkConfig(), SIGNAL(pingTimeoutEnabledSet(bool)), SLOT(enablePingTimeout(bool)));
     connect(networkConfig(), SIGNAL(pingIntervalSet(int)), SLOT(setPingInterval(int)));
     connect(networkConfig(), SIGNAL(autoWhoEnabledSet(bool)), SLOT(setAutoWhoEnabled(bool)));
@@ -68,10 +78,9 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
     connect(&_autoReconnectTimer, SIGNAL(timeout()), this, SLOT(doAutoReconnect()));
     connect(&_autoWhoTimer, SIGNAL(timeout()), this, SLOT(sendAutoWho()));
     connect(&_autoWhoCycleTimer, SIGNAL(timeout()), this, SLOT(startAutoWhoCycle()));
-    connect(&_tokenBucketTimer, SIGNAL(timeout()), this, SLOT(fillBucketAndProcessQueue()));
+    connect(&_tokenBucketTimer, SIGNAL(timeout()), this, SLOT(checkTokenBucket()));
 
     connect(&socket, SIGNAL(connected()), this, SLOT(socketInitialized()));
-    connect(&socket, SIGNAL(disconnected()), this, SLOT(socketDisconnected()));
     connect(&socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError)));
     connect(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(socketStateChanged(QAbstractSocket::SocketState)));
     connect(&socket, SIGNAL(readyRead()), this, SLOT(socketHasData()));
@@ -81,19 +90,58 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
 #endif
     connect(this, SIGNAL(newEvent(Event *)), coreSession()->eventManager(), SLOT(postEvent(Event *)));
 
+    // Custom rate limiting
+    // These react to the user changing settings in the client
+    connect(this, SIGNAL(useCustomMessageRateSet(bool)), SLOT(updateRateLimiting()));
+    connect(this, SIGNAL(messageRateBurstSizeSet(quint32)), SLOT(updateRateLimiting()));
+    connect(this, SIGNAL(messageRateDelaySet(quint32)), SLOT(updateRateLimiting()));
+    connect(this, SIGNAL(unlimitedMessageRateSet(bool)), SLOT(updateRateLimiting()));
+
+    // IRCv3 capability handling
+    // These react to CAP messages from the server
+    connect(this, SIGNAL(capAdded(QString)), this, SLOT(serverCapAdded(QString)));
+    connect(this, SIGNAL(capAcknowledged(QString)), this, SLOT(serverCapAcknowledged(QString)));
+    connect(this, SIGNAL(capRemoved(QString)), this, SLOT(serverCapRemoved(QString)));
+
     if (Quassel::isOptionSet("oidentd")) {
-        connect(this, SIGNAL(socketConnected(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Core::instance()->oidentdConfigGenerator(), SLOT(addSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Qt::BlockingQueuedConnection);
-        connect(this, SIGNAL(socketDisconnected(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Core::instance()->oidentdConfigGenerator(), SLOT(removeSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)));
+        connect(this, SIGNAL(socketInitialized(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)),
+                Core::instance()->oidentdConfigGenerator(), SLOT(addSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)), Qt::BlockingQueuedConnection);
+        connect(this, SIGNAL(socketDisconnected(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)),
+                Core::instance()->oidentdConfigGenerator(), SLOT(removeSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)));
+    }
+
+    if (Quassel::isOptionSet("ident-daemon")) {
+        connect(this, SIGNAL(socketInitialized(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)),
+                Core::instance()->identServer(), SLOT(addSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)), Qt::BlockingQueuedConnection);
+        connect(this, SIGNAL(socketDisconnected(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)),
+                Core::instance()->identServer(), SLOT(removeSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)));
     }
 }
 
 
 CoreNetwork::~CoreNetwork()
 {
-    if (connectionState() != Disconnected && connectionState() != Network::Reconnecting)
-        disconnectFromIrc(false);  // clean up, but this does not count as requested disconnect!
-    disconnect(&socket, 0, this, 0); // this keeps the socket from triggering events during clean up
-    delete _userInputHandler;
+    // Ensure we don't get any more signals from the socket while shutting down
+    disconnect(&socket, nullptr, this, nullptr);
+    if (!forceDisconnect()) {
+        qWarning() << QString{"Could not disconnect from network %1 (network ID: %2, user ID: %3)"}
+                      .arg(networkName()).arg(networkId().toInt()).arg(userId().toInt());
+    }
+}
+
+
+bool CoreNetwork::forceDisconnect(int msecs)
+{
+    if (socket.state() == QAbstractSocket::UnconnectedState) {
+        // Socket already disconnected.
+        return true;
+    }
+    // Request a socket-level disconnect if not already happened
+    socket.disconnectFromHost();
+    if (socket.state() != QAbstractSocket::UnconnectedState) {
+        return socket.waitForDisconnected(msecs);
+    }
+    return true;
 }
 
 
@@ -139,6 +187,14 @@ QByteArray CoreNetwork::userEncode(const QString &userNick, const QString &strin
 
 void CoreNetwork::connectToIrc(bool reconnecting)
 {
+    if (_shuttingDown) {
+        return;
+    }
+
+    if (Core::instance()->identServer()) {
+        _socketId = Core::instance()->identServer()->addWaitingSocket();
+    }
+
     if (!reconnecting && useAutoReconnect() && _autoReconnectCount == 0) {
         _autoReconnectTimer.setInterval(autoReconnectInterval() * 1000);
         if (unlimitedReconnectRetries())
@@ -159,18 +215,29 @@ void CoreNetwork::connectToIrc(bool reconnecting)
     // cleaning up old quit reason
     _quitReason.clear();
 
+    // Reset capability negotiation tracking, also handling server changes during reconnect
+    _capsQueuedIndividual.clear();
+    _capsQueuedBundled.clear();
+    clearCaps();
+    _capNegotiationActive = false;
+    _capInitialNegotiationEnded = false;
+
     // use a random server?
     if (useRandomServer()) {
         _lastUsedServerIndex = qrand() % serverList().size();
     }
     else if (_previousConnectionAttemptFailed) {
         // cycle to next server if previous connection attempt failed
+        _previousConnectionAttemptFailed = false;
         displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Connection failed. Cycling to next Server"));
         if (++_lastUsedServerIndex >= serverList().size()) {
             _lastUsedServerIndex = 0;
         }
     }
-    _previousConnectionAttemptFailed = false;
+    else {
+        // Start out with the top server in the list
+        _lastUsedServerIndex = 0;
+    }
 
     Server server = usedServer();
     displayStatusMsg(tr("Connecting to %1:%2...").arg(server.host).arg(server.port));
@@ -186,10 +253,16 @@ void CoreNetwork::connectToIrc(bool reconnecting)
 
     enablePingTimeout();
 
+    // Reset tracking for valid timestamps in PONG replies
+    setPongTimestampValid(false);
+
     // Qt caches DNS entries for a minute, resulting in round-robin (e.g. for chat.freenode.net) not working if several users
     // connect at a similar time. QHostInfo::fromName(), however, always performs a fresh lookup, overwriting the cache entry.
-    QHostInfo::fromName(server.host);
-
+    if (! server.useProxy) {
+        //Avoid hostname lookups when a proxy is specified. The lookups won't use the proxy and may therefore leak the DNS
+        //hostname of the server. Qt's DNS cache also isn't used by the proxy so we don't need to refresh the entry.
+        QHostInfo::fromName(server.host);
+    }
 #ifdef HAVE_SSL
     if (server.useSsl) {
         CoreIdentity *identity = identityPtr();
@@ -210,6 +283,8 @@ void CoreNetwork::connectToIrc(bool reconnecting)
 
 void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool withReconnect)
 {
+    // Disconnecting from the network, should expect a socket close or error
+    _disconnectExpected = true;
     _quitRequested = requested; // see socketDisconnected();
     if (!withReconnect) {
         _autoReconnectTimer.stop();
@@ -232,37 +307,67 @@ void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool
         _quitReason = reason;
 
     displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason));
-    switch (socket.state()) {
-    case QAbstractSocket::ConnectedState:
-        userInputHandler()->issueQuit(_quitReason);
-        if (requested || withReconnect) {
-            // the irc server has 10 seconds to close the socket
+    if (socket.state() == QAbstractSocket::UnconnectedState) {
+        socketDisconnected();
+    }
+    else {
+        if (socket.state() == QAbstractSocket::ConnectedState) {
+            // If shutting down, prioritize the QUIT command
+            userInputHandler()->issueQuit(_quitReason, _shuttingDown);
+        }
+        else {
+            socket.close();
+        }
+        if (socket.state() != QAbstractSocket::UnconnectedState) {
+            // Wait for up to 10 seconds for the socket to close cleanly, then it will be forcefully aborted
             _socketCloseTimer.start(10000);
-            break;
         }
-    default:
-        socket.close();
-        socketDisconnected();
     }
 }
 
 
+void CoreNetwork::socketCloseTimeout()
+{
+    qWarning() << QString{"Timed out quitting network %1 (network ID: %2, user ID: %3)"}
+                  .arg(networkName()).arg(networkId().toInt()).arg(userId().toInt());
+    socket.abort();
+}
+
+
+void CoreNetwork::shutdown()
+{
+    _shuttingDown = true;
+    disconnectFromIrc(false, {}, false);
+}
+
+
 void CoreNetwork::userInput(BufferInfo buf, QString msg)
 {
     userInputHandler()->handleUserInput(buf, msg);
 }
 
 
-void CoreNetwork::putRawLine(QByteArray s)
+void CoreNetwork::putRawLine(const QByteArray s, const bool prepend)
 {
-    if (_tokenBucket > 0)
+    if (_tokenBucket > 0 || (_skipMessageRates && _msgQueue.size() == 0)) {
+        // If there's tokens remaining, ...
+        // Or rate limits don't apply AND no messages are in queue (to prevent out-of-order), ...
+        // Send the message now.
         writeToSocket(s);
-    else
-        _msgQueue.append(s);
+    } else {
+        // Otherwise, queue the message for later
+        if (prepend) {
+            // Jump to the start, skipping other messages
+            _msgQueue.prepend(s);
+        } else {
+            // Add to back, waiting in order
+            _msgQueue.append(s);
+        }
+    }
 }
 
 
-void CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> &params, const QByteArray &prefix)
+void CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> &params, const QByteArray &prefix, const bool prepend)
 {
     QByteArray msg;
 
@@ -279,13 +384,23 @@ void CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> &params, co
         msg += params[i];
     }
 
-    putRawLine(msg);
+    putRawLine(msg, prepend);
+}
+
+
+void CoreNetwork::putCmd(const QString &cmd, const QList<QList<QByteArray>> &params, const QByteArray &prefix, const bool prependAll)
+{
+    QListIterator<QList<QByteArray>> i(params);
+    while (i.hasNext()) {
+        QList<QByteArray> msg = i.next();
+        putCmd(cmd, msg, prefix, prependAll);
+    }
 }
 
 
 void CoreNetwork::setChannelJoined(const QString &channel)
 {
-    _autoWhoQueue.prepend(channel.toLower()); // prepend so this new chan is the first to be checked
+    queueAutoWhoOneshot(channel); // check this new channel first
 
     Core::setChannelPersistent(userId(), networkId(), channel, true);
     Core::setPersistentChannelKey(userId(), networkId(), channel, _channelKeys[channel.toLower()]);
@@ -323,32 +438,32 @@ void CoreNetwork::removeChannelKey(const QString &channel)
 Cipher *CoreNetwork::cipher(const QString &target)
 {
     if (target.isEmpty())
-        return 0;
+        return nullptr;
 
     if (!Cipher::neededFeaturesAvailable())
-        return 0;
+        return nullptr;
 
-    CoreIrcChannel *channel = qobject_cast<CoreIrcChannel *>(ircChannel(target));
+    auto *channel = qobject_cast<CoreIrcChannel *>(ircChannel(target));
     if (channel) {
         return channel->cipher();
     }
-    CoreIrcUser *user = qobject_cast<CoreIrcUser *>(ircUser(target));
+    auto *user = qobject_cast<CoreIrcUser *>(ircUser(target));
     if (user) {
         return user->cipher();
     } else if (!isChannelName(target)) {
         return qobject_cast<CoreIrcUser*>(newIrcUser(target))->cipher();
     }
-    return 0;
+    return nullptr;
 }
 
 
 QByteArray CoreNetwork::cipherKey(const QString &target) const
 {
-    CoreIrcChannel *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+    auto *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
     if (c)
         return c->cipher()->key();
 
-    CoreIrcUser *u = qobject_cast<CoreIrcUser*>(ircUser(target));
+    auto *u = qobject_cast<CoreIrcUser*>(ircUser(target));
     if (u)
         return u->cipher()->key();
 
@@ -358,18 +473,20 @@ QByteArray CoreNetwork::cipherKey(const QString &target) const
 
 void CoreNetwork::setCipherKey(const QString &target, const QByteArray &key)
 {
-    CoreIrcChannel *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+    auto *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
     if (c) {
         c->setEncrypted(c->cipher()->setKey(key));
+        coreSession()->setBufferCipher(networkId(), target, key);
         return;
     }
 
-    CoreIrcUser *u = qobject_cast<CoreIrcUser*>(ircUser(target));
+    auto *u = qobject_cast<CoreIrcUser*>(ircUser(target));
     if (!u && !isChannelName(target))
         u = qobject_cast<CoreIrcUser*>(newIrcUser(target));
 
     if (u) {
         u->setEncrypted(u->cipher()->setKey(key));
+        coreSession()->setBufferCipher(networkId(), target, key);
         return;
     }
 }
@@ -377,10 +494,10 @@ void CoreNetwork::setCipherKey(const QString &target, const QByteArray &key)
 
 bool CoreNetwork::cipherUsesCBC(const QString &target)
 {
-    CoreIrcChannel *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+    auto *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
     if (c)
         return c->cipher()->usesCBC();
-    CoreIrcUser *u = qobject_cast<CoreIrcUser*>(ircUser(target));
+    auto *u = qobject_cast<CoreIrcUser*>(ircUser(target));
     if (u)
         return u->cipher()->usesCBC();
 
@@ -388,13 +505,13 @@ bool CoreNetwork::cipherUsesCBC(const QString &target)
 }
 #endif /* HAVE_QCA2 */
 
-bool CoreNetwork::setAutoWhoDone(const QString &channel)
+bool CoreNetwork::setAutoWhoDone(const QString &name)
 {
-    QString chan = channel.toLower();
-    if (_autoWhoPending.value(chan, 0) <= 0)
+    QString chanOrNick = name.toLower();
+    if (_autoWhoPending.value(chanOrNick, 0) <= 0)
         return false;
-    if (--_autoWhoPending[chan] <= 0)
-        _autoWhoPending.remove(chan);
+    if (--_autoWhoPending[chanOrNick] <= 0)
+        _autoWhoPending.remove(chanOrNick);
     return true;
 }
 
@@ -416,11 +533,7 @@ void CoreNetwork::socketHasData()
         else if (s.endsWith("\n"))
             s.chop(1);
         NetworkDataEvent *event = new NetworkDataEvent(EventManager::NetworkIncoming, this, s);
-#if QT_VERSION >= 0x040700
         event->setTimestamp(QDateTime::currentDateTimeUtc());
-#else
-        event->setTimestamp(QDateTime::currentDateTime().toUTC());
-#endif
         emit newEvent(event);
     }
 }
@@ -428,8 +541,10 @@ void CoreNetwork::socketHasData()
 
 void CoreNetwork::socketError(QAbstractSocket::SocketError error)
 {
-    if (_quitRequested && error == QAbstractSocket::RemoteHostClosedError)
+    // Ignore socket closed errors if expected
+    if (_disconnectExpected && error == QAbstractSocket::RemoteHostClosedError) {
         return;
+    }
 
     _previousConnectionAttemptFailed = true;
     qWarning() << qPrintable(tr("Could not connect to %1 (%2)").arg(networkName(), socket.errorString()));
@@ -450,29 +565,41 @@ void CoreNetwork::socketInitialized()
         disconnectFromIrc();
         return;
     }
-    
-    emit socketConnected(identity, localAddress(), localPort(), peerAddress(), peerPort());
-    
+
     Server server = usedServer();
+
 #ifdef HAVE_SSL
-    if (server.useSsl && !socket.isEncrypted())
+    // Non-SSL connections enter here only once, always emit socketInitialized(...) in these cases
+    // SSL connections call socketInitialized() twice, only emit socketInitialized(...) on the first (not yet encrypted) run
+    if (!server.useSsl || !socket.isEncrypted()) {
+        emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort(), _socketId);
+    }
+
+    if (server.useSsl && !socket.isEncrypted()) {
+        // We'll finish setup once we're encrypted, and called again
         return;
-#endif
-#if QT_VERSION >= 0x040600
-    socket.setSocketOption(QAbstractSocket::KeepAliveOption, true);
+    }
+#else
+    emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort(), _socketId);
 #endif
 
-    emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort());
+    socket.setSocketOption(QAbstractSocket::KeepAliveOption, true);
 
-    // TokenBucket to avoid sending too much at once
-    _messageDelay = 2200;  // this seems to be a safe value (2.2 seconds delay)
-    _burstSize = 5;
-    _tokenBucket = _burstSize; // init with a full bucket
-    _tokenBucketTimer.start(_messageDelay);
+    // Update the TokenBucket, force-enabling unlimited message rates for initial registration and
+    // capability negotiation.  networkInitialized() will call updateRateLimiting() without the
+    // force flag to apply user preferences.  When making changes, ensure that this still happens!
+    // As Quassel waits for CAP ACK/NAK and AUTHENTICATE replies, this shouldn't ever fill the IRC
+    // server receive queue and cause a kill.  "Shouldn't" being the operative word; the real world
+    // is a scary place.
+    updateRateLimiting(true);
+    // Fill up the token bucket as we're connecting from scratch
+    resetTokenBucket();
+
+    // Request capabilities as per IRCv3.2 specifications
+    // Older servers should ignore this; newer servers won't downgrade to RFC1459
+    displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Requesting capability list..."));
+    putRawLine(serverEncode(QString("CAP LS 302")));
 
-    if (networkInfo().useSasl) {
-        putRawLine(serverEncode(QString("CAP REQ :sasl")));
-    }
     if (!server.password.isEmpty()) {
         putRawLine(serverEncode(QString("PASS %1").arg(server.password)));
     }
@@ -484,8 +611,11 @@ void CoreNetwork::socketInitialized()
     else {
         nick = identity->nicks()[0];
     }
-    putRawLine(serverEncode(QString("NICK :%1").arg(nick)));
-    putRawLine(serverEncode(QString("USER %1 8 * :%2").arg(identity->ident(), identity->realName())));
+    putRawLine(serverEncode(QString("NICK %1").arg(nick)));
+    // Only allow strict-compliant idents when strict mode is enabled
+    putRawLine(serverEncode(QString("USER %1 8 * :%2").arg(
+                                coreSession()->strictCompliantIdent(identity),
+                                identity->realName())));
 }
 
 
@@ -511,7 +641,9 @@ void CoreNetwork::socketDisconnected()
 
     setConnected(false);
     emit disconnected(networkId());
-    emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort());
+    emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort(), _socketId);
+    // Reset disconnect expectations
+    _disconnectExpected = false;
     if (_quitRequested) {
         _quitRequested = false;
         setConnectionState(Network::Disconnected);
@@ -533,6 +665,7 @@ void CoreNetwork::socketStateChanged(QAbstractSocket::SocketState socketState)
     switch (socketState) {
     case QAbstractSocket::UnconnectedState:
         state = Network::Disconnected;
+        socketDisconnected();
         break;
     case QAbstractSocket::HostLookupState:
     case QAbstractSocket::ConnectingState:
@@ -555,8 +688,13 @@ void CoreNetwork::networkInitialized()
 {
     setConnectionState(Network::Initialized);
     setConnected(true);
+    _disconnectExpected = false;
     _quitRequested = false;
 
+    // Update the TokenBucket with specified rate-limiting settings, removing the force-unlimited
+    // flag used for initial registration and capability negotiation.
+    updateRateLimiting();
+
     if (useAutoReconnect()) {
         // reset counter
         _autoReconnectCount = unlimitedReconnectRetries() ? -1 : autoReconnectRetries();
@@ -564,8 +702,11 @@ void CoreNetwork::networkInitialized()
 
     // restore away state
     QString awayMsg = Core::awayMessage(userId(), networkId());
-    if (!awayMsg.isEmpty())
-        userInputHandler()->handleAway(BufferInfo(), Core::awayMessage(userId(), networkId()));
+    if (!awayMsg.isEmpty()) {
+        // Don't re-apply any timestamp formatting in order to preserve escaped percent signs, e.g.
+        // '%%%%%%%%' -> '%%%%'  If processed again, it'd result in '%%'.
+        userInputHandler()->handleAway(BufferInfo(), awayMsg, true);
+    }
 
     sendPerform();
 
@@ -673,20 +814,20 @@ void CoreNetwork::updateIssuedModes(const QString &requestedModes)
     QString removeModes;
     bool addMode = true;
 
-    for (int i = 0; i < requestedModes.length(); i++) {
-        if (requestedModes[i] == '+') {
+    for (auto requestedMode : requestedModes) {
+        if (requestedMode == '+') {
             addMode = true;
             continue;
         }
-        if (requestedModes[i] == '-') {
+        if (requestedMode == '-') {
             addMode = false;
             continue;
         }
         if (addMode) {
-            addModes += requestedModes[i];
+            addModes += requestedMode;
         }
         else {
-            removeModes += requestedModes[i];
+            removeModes += requestedMode;
         }
     }
 
@@ -799,12 +940,17 @@ void CoreNetwork::doAutoReconnect()
 
 void CoreNetwork::sendPing()
 {
-    uint now = QDateTime::currentDateTime().toTime_t();
+    qint64 now = QDateTime::currentDateTime().toMSecsSinceEpoch();
     if (_pingCount != 0) {
         qDebug() << "UserId:" << userId() << "Network:" << networkName() << "missed" << _pingCount << "pings."
                  << "BA:" << socket.bytesAvailable() << "BTW:" << socket.bytesToWrite();
     }
-    if ((int)_pingCount >= networkConfig()->maxPingCount() && now - _lastPingTime <= (uint)(_pingTimer.interval() / 1000) + 1) {
+    if ((int)_pingCount >= networkConfig()->maxPingCount()
+            && (now - _lastPingTime) <= (_pingTimer.interval() + (1 * 1000))) {
+        // In transitioning to 64-bit time, the interval no longer needs converted down to seconds.
+        // However, to reduce the risk of breaking things by changing past behavior, we still allow
+        // up to 1 second missed instead of enforcing a stricter 1 millisecond allowance.
+        //
         // the second check compares the actual elapsed time since the last ping and the pingTimer interval
         // if the interval is shorter then the actual elapsed time it means that this thread was somehow blocked
         // and unable to even handle a ping answer. So we ignore those misses.
@@ -814,8 +960,12 @@ void CoreNetwork::sendPing()
         _lastPingTime = now;
         _pingCount++;
         // Don't send pings until the network is initialized
-        if(_sendPings)
+        if(_sendPings) {
+            // Mark as waiting for a reply
+            _pongReplyPending = true;
+            // Send default timestamp ping
             userInputHandler()->handlePing(BufferInfo(), QString());
+        }
     }
 }
 
@@ -826,6 +976,7 @@ void CoreNetwork::enablePingTimeout(bool enable)
         disablePingTimeout();
     else {
         resetPingTimeout();
+        resetPongReplyPending();
         if (networkConfig()->pingTimeoutEnabled())
             _pingTimer.start();
     }
@@ -837,6 +988,7 @@ void CoreNetwork::disablePingTimeout()
     _pingTimer.stop();
     _sendPings = false;
     resetPingTimeout();
+    resetPongReplyPending();
 }
 
 
@@ -846,6 +998,316 @@ void CoreNetwork::setPingInterval(int interval)
 }
 
 
+void CoreNetwork::setPongTimestampValid(bool validTimestamp)
+{
+    _pongTimestampValid = validTimestamp;
+}
+
+
+/******** Custom Rate Limiting ********/
+
+void CoreNetwork::updateRateLimiting(const bool forceUnlimited)
+{
+    // Verify and apply custom rate limiting options, always resetting the delay and burst size
+    // (safe-guarding against accidentally starting the timer), but don't reset the token bucket as
+    // this may be called while connected to a server.
+
+    if (useCustomMessageRate() || forceUnlimited) {
+        // Custom message rates enabled, or chosen by means of forcing unlimited.  Let's go for it!
+
+        _messageDelay = messageRateDelay();
+
+        _burstSize = messageRateBurstSize();
+        if (_burstSize < 1) {
+            qWarning() << "Invalid messageRateBurstSize data, cannot have zero message burst size!"
+                       << _burstSize;
+            // Can't go slower than one message at a time
+            _burstSize = 1;
+        }
+
+        if (_tokenBucket > _burstSize) {
+            // Don't let the token bucket exceed the maximum
+            _tokenBucket = _burstSize;
+            // To fill up the token bucket, use resetRateLimiting().  Don't do that here, otherwise
+            // changing the rate-limit settings while connected to a server will incorrectly reset
+            // the token bucket.
+        }
+
+        // Toggle the timer according to whether or not rate limiting is enabled
+        // If we're here, either useCustomMessageRate or forceUnlimited is true.  Thus, the logic is
+        // _skipMessageRates = ((useCustomMessageRate && unlimitedMessageRate) || forceUnlimited)
+        // Override user preferences if called with force unlimited, only used during connect.
+        _skipMessageRates = (unlimitedMessageRate() || forceUnlimited);
+        if (_skipMessageRates) {
+            // If the message queue already contains messages, they need sent before disabling the
+            // timer.  Set the timer to a rapid pace and let it disable itself.
+            if (_msgQueue.size() > 0) {
+                qDebug() << "Outgoing message queue contains messages while disabling rate "
+                            "limiting.  Sending remaining queued messages...";
+                // Promptly run the timer again to clear the messages.  Rate limiting is disabled,
+                // so nothing should cause this to block.. in theory.  However, don't directly call
+                // fillBucketAndProcessQueue() in order to keep it on a separate thread.
+                //
+                // TODO If testing shows this isn't needed, it can be simplified to a direct call.
+                // Hesitant to change it without a wide variety of situations to verify behavior.
+                _tokenBucketTimer.start(100);
+            } else {
+                // No rate limiting, disable the timer
+                _tokenBucketTimer.stop();
+            }
+        } else {
+            // Rate limiting enabled, enable the timer
+            _tokenBucketTimer.start(_messageDelay);
+        }
+    } else {
+        // Custom message rates disabled.  Go for the default.
+
+        _skipMessageRates = false;  // Enable rate-limiting by default
+        _messageDelay = 2200;       // This seems to be a safe value (2.2 seconds delay)
+        _burstSize = 5;             // 5 messages at once
+        if (_tokenBucket > _burstSize) {
+            // TokenBucket to avoid sending too much at once.  Don't let the token bucket exceed the
+            // maximum.
+            _tokenBucket = _burstSize;
+            // To fill up the token bucket, use resetRateLimiting().  Don't do that here, otherwise
+            // changing the rate-limit settings while connected to a server will incorrectly reset
+            // the token bucket.
+        }
+        // Rate limiting enabled, enable the timer
+        _tokenBucketTimer.start(_messageDelay);
+    }
+}
+
+void CoreNetwork::resetTokenBucket()
+{
+    // Fill up the token bucket to the maximum
+    _tokenBucket = _burstSize;
+}
+
+
+/******** IRCv3 Capability Negotiation ********/
+
+void CoreNetwork::serverCapAdded(const QString &capability)
+{
+    // Check if it's a known capability; if so, add it to the list
+    // Handle special cases first
+    if (capability == IrcCap::SASL) {
+        // Only request SASL if it's enabled
+        if (networkInfo().useSasl)
+            queueCap(capability);
+    } else if (IrcCap::knownCaps.contains(capability)) {
+        // Handling for general known capabilities
+        queueCap(capability);
+    }
+}
+
+void CoreNetwork::serverCapAcknowledged(const QString &capability)
+{
+    // This may be called multiple times in certain situations.
+
+    // Handle core-side configuration
+    if (capability == IrcCap::AWAY_NOTIFY) {
+        // away-notify enabled, stop the autoWho timers, handle manually
+        setAutoWhoEnabled(false);
+    }
+
+    // Handle capabilities that require further messages sent to the IRC server
+    // If you change this list, ALSO change the list in CoreNetwork::capsRequiringServerMessages
+    if (capability == IrcCap::SASL) {
+        // If SASL mechanisms specified, limit to what's accepted for authentication
+        // if the current identity has a cert set, use SASL EXTERNAL
+        // FIXME use event
+#ifdef HAVE_SSL
+        if (!identityPtr()->sslCert().isNull()) {
+            if (saslMaybeSupports(IrcCap::SaslMech::EXTERNAL)) {
+                // EXTERNAL authentication supported, send request
+                putRawLine(serverEncode("AUTHENTICATE EXTERNAL"));
+            } else {
+                displayMsg(Message::Error, BufferInfo::StatusBuffer, "",
+                           tr("SASL EXTERNAL authentication not supported"));
+                sendNextCap();
+            }
+        } else {
+#endif
+            if (saslMaybeSupports(IrcCap::SaslMech::PLAIN)) {
+                // PLAIN authentication supported, send request
+                // Only working with PLAIN atm, blowfish later
+                putRawLine(serverEncode("AUTHENTICATE PLAIN"));
+            } else {
+                displayMsg(Message::Error, BufferInfo::StatusBuffer, "",
+                           tr("SASL PLAIN authentication not supported"));
+                sendNextCap();
+            }
+#ifdef HAVE_SSL
+        }
+#endif
+    }
+}
+
+void CoreNetwork::serverCapRemoved(const QString &capability)
+{
+    // This may be called multiple times in certain situations.
+
+    // Handle special cases here
+    if (capability == IrcCap::AWAY_NOTIFY) {
+        // away-notify disabled, enable autoWho according to configuration
+        setAutoWhoEnabled(networkConfig()->autoWhoEnabled());
+    }
+}
+
+void CoreNetwork::queueCap(const QString &capability)
+{
+    // IRCv3 specs all use lowercase capability names
+    QString _capLowercase = capability.toLower();
+
+    if(capsRequiringConfiguration.contains(_capLowercase)) {
+        // The capability requires additional configuration before being acknowledged (e.g. SASL),
+        // so we should negotiate it separately from all other capabilities.  Otherwise new
+        // capabilities will be requested while still configuring the previous one.
+        if (!_capsQueuedIndividual.contains(_capLowercase)) {
+            _capsQueuedIndividual.append(_capLowercase);
+        }
+    } else {
+        // The capability doesn't need any special configuration, so it should be safe to try
+        // bundling together with others.  "Should" being the imperative word, as IRC servers can do
+        // anything.
+        if (!_capsQueuedBundled.contains(_capLowercase)) {
+            _capsQueuedBundled.append(_capLowercase);
+        }
+    }
+}
+
+QString CoreNetwork::takeQueuedCaps()
+{
+    // Clear the record of the most recently negotiated capability bundle.  Does nothing if the list
+    // is empty.
+    _capsQueuedLastBundle.clear();
+
+    // First, negotiate all the standalone capabilities that require additional configuration.
+    if (!_capsQueuedIndividual.empty()) {
+        // We have an individual capability available.  Take the first and pass it back.
+        return _capsQueuedIndividual.takeFirst();
+    } else if (!_capsQueuedBundled.empty()) {
+        // We have capabilities available that can be grouped.  Try to fit in as many as within the
+        // maximum length.
+        // See CoreNetwork::maxCapRequestLength
+
+        // Response must have at least one capability regardless of max length for anything to
+        // happen.
+        QString capBundle = _capsQueuedBundled.takeFirst();
+        QString nextCap("");
+        while (!_capsQueuedBundled.empty()) {
+            // As long as capabilities remain, get the next...
+            nextCap = _capsQueuedBundled.first();
+            if ((capBundle.length() + 1 + nextCap.length()) <= maxCapRequestLength) {
+                // [capability + 1 for a space + this new capability] fit within length limits
+                // Add it to formatted list
+                capBundle.append(" " + nextCap);
+                // Add it to most recent bundle of requested capabilities (simplifies retry logic)
+                _capsQueuedLastBundle.append(nextCap);
+                // Then remove it from the queue
+                _capsQueuedBundled.removeFirst();
+            } else {
+                // We've reached the length limit for a single capability request, stop adding more
+                break;
+            }
+        }
+        // Return this space-separated set of capabilities, removing any extra spaces
+        return capBundle.trimmed();
+    } else {
+        // No capabilities left to negotiate, return an empty string.
+        return QString();
+    }
+}
+
+void CoreNetwork::retryCapsIndividually()
+{
+    // The most recent set of capabilities got denied by the IRC server.  As we don't know what got
+    // denied, try each capability individually.
+    if (_capsQueuedLastBundle.empty()) {
+        // No most recently tried capability set, just return.
+        return;
+        // Note: there's little point in retrying individually requested caps during negotiation.
+        // We know the individual capability was the one that failed, and it's not likely it'll
+        // suddenly start working within a few seconds.  'cap-notify' provides a better system for
+        // handling capability removal and addition.
+    }
+
+    // This should be fairly rare, e.g. services restarting during negotiation, so simplicity wins
+    // over efficiency.  If this becomes an issue, implement a binary splicing system instead,
+    // keeping track of which halves of the group fail, dividing the set each time.
+
+    // Add most recently tried capability set to individual list, re-requesting them one at a time
+    _capsQueuedIndividual.append(_capsQueuedLastBundle);
+    // Warn of this issue to explain the slower login.  Servers usually shouldn't trigger this.
+    displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
+               tr("Could not negotiate some capabilities, retrying individually (%1)...")
+               .arg(_capsQueuedLastBundle.join(", ")));
+    // Capabilities are already removed from the capability bundle queue via takeQueuedCaps(), no
+    // need to remove them here.
+    // Clear the most recently tried set to reduce risk that mistakes elsewhere causes retrying
+    // indefinitely.
+    _capsQueuedLastBundle.clear();
+}
+
+void CoreNetwork::beginCapNegotiation()
+{
+    // Don't begin negotiation if no capabilities are queued to request
+    if (!capNegotiationInProgress()) {
+        // If the server doesn't have any capabilities, but supports CAP LS, continue on with the
+        // normal connection.
+        displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("No capabilities available"));
+        endCapNegotiation();
+        return;
+    }
+
+    _capNegotiationActive = true;
+    displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
+               tr("Ready to negotiate (found: %1)").arg(caps().join(", ")));
+
+    // Build a list of queued capabilities, starting with individual, then bundled, only adding the
+    // comma separator between the two if needed (both individual and bundled caps exist).
+    QString queuedCapsDisplay =
+            _capsQueuedIndividual.join(", ")
+            + ((!_capsQueuedIndividual.empty() && !_capsQueuedBundled.empty()) ? ", " : "")
+            + _capsQueuedBundled.join(", ");
+    displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
+               tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay));
+
+    sendNextCap();
+}
+
+void CoreNetwork::sendNextCap()
+{
+    if (capNegotiationInProgress()) {
+        // Request the next set of capabilities and remove them from the list
+        putRawLine(serverEncode(QString("CAP REQ :%1").arg(takeQueuedCaps())));
+    } else {
+        // No pending desired capabilities, capability negotiation finished
+        // If SASL requested but not available, print a warning
+        if (networkInfo().useSasl && !capEnabled(IrcCap::SASL))
+            displayMsg(Message::Error, BufferInfo::StatusBuffer, "",
+                       tr("SASL authentication currently not supported by server"));
+
+        if (_capNegotiationActive) {
+            displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
+                   tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", ")));
+            _capNegotiationActive = false;
+        }
+
+        endCapNegotiation();
+    }
+}
+
+void CoreNetwork::endCapNegotiation()
+{
+    // If nick registration is already complete, CAP END is not required
+    if (!_capInitialNegotiationEnded) {
+        putRawLine(serverEncode(QString("CAP END")));
+        _capInitialNegotiationEnded = true;
+    }
+}
+
 /******** AutoWHO ********/
 
 void CoreNetwork::startAutoWhoCycle()
@@ -857,6 +1319,19 @@ void CoreNetwork::startAutoWhoCycle()
     _autoWhoQueue = channels();
 }
 
+void CoreNetwork::queueAutoWhoOneshot(const QString &name)
+{
+    // Prepend so these new channels/nicks are the first to be checked
+    // Don't allow duplicates
+    if (!_autoWhoQueue.contains(name.toLower())) {
+        _autoWhoQueue.prepend(name.toLower());
+    }
+    if (capEnabled(IrcCap::AWAY_NOTIFY)) {
+        // When away-notify is active, the timer's stopped.  Start a new cycle to who this channel.
+        setAutoWhoEnabled(true);
+    }
+}
+
 
 void CoreNetwork::setAutoWhoDelay(int delay)
 {
@@ -888,19 +1363,60 @@ void CoreNetwork::sendAutoWho()
         return;
 
     while (!_autoWhoQueue.isEmpty()) {
-        QString chan = _autoWhoQueue.takeFirst();
-        IrcChannel *ircchan = ircChannel(chan);
-        if (!ircchan) continue;
-        if (networkConfig()->autoWhoNickLimit() > 0 && ircchan->ircUsers().count() >= networkConfig()->autoWhoNickLimit())
+        QString chanOrNick = _autoWhoQueue.takeFirst();
+        // Check if it's a known channel or nick
+        IrcChannel *ircchan = ircChannel(chanOrNick);
+        IrcUser *ircuser = ircUser(chanOrNick);
+        if (ircchan) {
+            // Apply channel limiting rules
+            // If using away-notify, don't impose channel size limits in order to capture away
+            // state of everyone.  Auto-who won't run on a timer so network impact is minimal.
+            if (networkConfig()->autoWhoNickLimit() > 0
+                && ircchan->ircUsers().count() >= networkConfig()->autoWhoNickLimit()
+                && !capEnabled(IrcCap::AWAY_NOTIFY))
+                continue;
+            _autoWhoPending[chanOrNick.toLower()]++;
+        } else if (ircuser) {
+            // Checking a nick, add it to the pending list
+            _autoWhoPending[ircuser->nick().toLower()]++;
+        } else {
+            // Not a channel or a nick, skip it
+            qDebug() << "Skipping who polling of unknown channel or nick" << chanOrNick;
             continue;
-        _autoWhoPending[chan]++;
-        putRawLine("WHO " + serverEncode(chan));
+        }
+        if (supports("WHOX")) {
+            // Use WHO extended to poll away users and/or user accounts
+            // Explicitly only match on nickname ("n"), don't rely on server defaults
+            //
+            // WHO <nickname> n%chtsunfra,<unique_number>
+            //
+            // See http://faerion.sourceforge.net/doc/irc/whox.var
+            // And https://github.com/quakenet/snircd/blob/master/doc/readme.who
+            // And https://github.com/hexchat/hexchat/blob/57478b65758e6b697b1d82ce21075e74aa475efc/src/common/proto-irc.c#L752
+            putRawLine(serverEncode(QString("WHO %1 n%chtsunfra,%2")
+                                    .arg(serverEncode(chanOrNick),
+                                         QString::number(IrcCap::ACCOUNT_NOTIFY_WHOX_NUM))));
+        } else {
+            // Fall back to normal WHO
+            //
+            // Note: According to RFC 1459, "WHO <phrase>" can fall back to searching realname,
+            // hostmask, etc.  There's nothing we can do about that :(
+            //
+            // See https://tools.ietf.org/html/rfc1459#section-4.5.1
+            putRawLine(serverEncode(QString("WHO %1").arg(chanOrNick)));
+        }
         break;
     }
-    if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive()) {
+
+    if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive()
+        && !capEnabled(IrcCap::AWAY_NOTIFY)) {
         // Timer was stopped, means a new cycle is due immediately
+        // Don't run a new cycle if using away-notify; server will notify as appropriate
         _autoWhoCycleTimer.start();
         startAutoWhoCycle();
+    } else if (capEnabled(IrcCap::AWAY_NOTIFY) && _autoWhoCycleTimer.isActive()) {
+        // Don't run another who cycle if away-notify is enabled
+        _autoWhoCycleTimer.stop();
     }
 }
 
@@ -908,20 +1424,62 @@ void CoreNetwork::sendAutoWho()
 #ifdef HAVE_SSL
 void CoreNetwork::sslErrors(const QList<QSslError> &sslErrors)
 {
-    Q_UNUSED(sslErrors)
-    socket.ignoreSslErrors();
-    // TODO errorhandling
+    Server server = usedServer();
+    if (server.sslVerify) {
+        // Treat the SSL error as a hard error
+        QString sslErrorMessage = tr("Encrypted connection couldn't be verified, disconnecting "
+                                     "since verification is required");
+        if (!sslErrors.empty()) {
+            // Add the error reason if known
+            sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
+        }
+        displayMsg(Message::Error, BufferInfo::StatusBuffer, "", sslErrorMessage);
+
+        // Disconnect, triggering a reconnect in case it's a temporary issue with certificate
+        // validity, network trouble, etc.
+        disconnectFromIrc(false, QString("Encrypted connection not verified"), true /* withReconnect */);
+    } else {
+        // Treat the SSL error as a warning, continue to connect anyways
+        QString sslErrorMessage = tr("Encrypted connection couldn't be verified, continuing "
+                                     "since verification is not required");
+        if (!sslErrors.empty()) {
+            // Add the error reason if known
+            sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
+        }
+        displayMsg(Message::Info, BufferInfo::StatusBuffer, "", sslErrorMessage);
+
+        // Proceed with the connection
+        socket.ignoreSslErrors();
+    }
 }
 
 
 #endif  // HAVE_SSL
 
+void CoreNetwork::checkTokenBucket()
+{
+    if (_skipMessageRates) {
+        if (_msgQueue.size() == 0) {
+            // Message queue emptied; stop the timer and bail out
+            _tokenBucketTimer.stop();
+            return;
+        }
+        // Otherwise, we're emptying the queue, continue on as normal
+    }
+
+    // Process whatever messages are pending
+    fillBucketAndProcessQueue();
+}
+
+
 void CoreNetwork::fillBucketAndProcessQueue()
 {
+    // If there's less tokens than burst size, refill the token bucket by 1
     if (_tokenBucket < _burstSize) {
         _tokenBucket++;
     }
 
+    // As long as there's tokens available and messages remaining, sending messages from the queue
     while (_msgQueue.size() > 0 && _tokenBucket > 0) {
         writeToSocket(_msgQueue.takeFirst());
     }
@@ -930,9 +1488,18 @@ void CoreNetwork::fillBucketAndProcessQueue()
 
 void CoreNetwork::writeToSocket(const QByteArray &data)
 {
+    // Log the message if enabled and network ID matches or allows all
+    if (_debugLogRawIrc
+            && (_debugLogRawNetId == -1 || networkId().toInt() == _debugLogRawNetId)) {
+        // Include network ID
+        qDebug() << "IRC net" << networkId() << ">>" << data;
+    }
     socket.write(data);
     socket.write("\r\n");
-    _tokenBucket--;
+    if (!_skipMessageRates) {
+        // Only subtract from the token bucket if message rate limiting is enabled
+        _tokenBucket--;
+    }
 }
 
 
@@ -950,6 +1517,9 @@ Network::Server CoreNetwork::usedServer() const
 
 void CoreNetwork::requestConnect() const
 {
+    if (_shuttingDown) {
+        return;
+    }
     if (connectionState() != Disconnected) {
         qWarning() << "Requesting connect while already being connected!";
         return;
@@ -960,6 +1530,9 @@ void CoreNetwork::requestConnect() const
 
 void CoreNetwork::requestDisconnect() const
 {
+    if (_shuttingDown) {
+        return;
+    }
     if (connectionState() == Disconnected) {
         qWarning() << "Requesting disconnect while not being connected!";
         return;
@@ -985,3 +1558,79 @@ void CoreNetwork::requestSetNetworkInfo(const NetworkInfo &info)
         }
     }
 }
+
+
+QList<QList<QByteArray>> CoreNetwork::splitMessage(const QString &cmd, const QString &message, std::function<QList<QByteArray>(QString &)> cmdGenerator)
+{
+    QString wrkMsg(message);
+    QList<QList<QByteArray>> msgsToSend;
+
+    // do while (wrkMsg.size() > 0)
+    do {
+        // First, check to see if the whole message can be sent at once.  The
+        // cmdGenerator function is passed in by the caller and is used to encode
+        // and encrypt (if applicable) the message, since different callers might
+        // want to use different encoding or encode different values.
+        int splitPos = wrkMsg.size();
+        QList<QByteArray> initialSplitMsgEnc = cmdGenerator(wrkMsg);
+        int initialOverrun = userInputHandler()->lastParamOverrun(cmd, initialSplitMsgEnc);
+
+        if (initialOverrun) {
+            // If the message was too long to be sent, first try splitting it along
+            // word boundaries with QTextBoundaryFinder.
+            QString splitMsg(wrkMsg);
+            QTextBoundaryFinder qtbf(QTextBoundaryFinder::Word, splitMsg);
+            qtbf.setPosition(initialSplitMsgEnc[1].size() - initialOverrun);
+            QList<QByteArray> splitMsgEnc;
+            int overrun = initialOverrun;
+
+            while (overrun) {
+                splitPos = qtbf.toPreviousBoundary();
+
+                // splitPos==-1 means the QTBF couldn't find a split point at all and
+                // splitPos==0 means the QTBF could only find a boundary at the beginning of
+                // the string.  Neither one of these works for us.
+                if (splitPos > 0) {
+                    // If a split point could be found, split the message there, calculate the
+                    // overrun, and continue with the loop.
+                    splitMsg = splitMsg.left(splitPos);
+                    splitMsgEnc = cmdGenerator(splitMsg);
+                    overrun = userInputHandler()->lastParamOverrun(cmd, splitMsgEnc);
+                }
+                else {
+                    // If a split point could not be found (the beginning of the message
+                    // is reached without finding a split point short enough to send) and we
+                    // are still in Word mode, switch to Grapheme mode.  We also need to restore
+                    // the full wrkMsg to splitMsg, since splitMsg may have been cut down during
+                    // the previous attempt to find a split point.
+                    if (qtbf.type() == QTextBoundaryFinder::Word) {
+                        splitMsg = wrkMsg;
+                        splitPos = splitMsg.size();
+                        QTextBoundaryFinder graphemeQtbf(QTextBoundaryFinder::Grapheme, splitMsg);
+                        graphemeQtbf.setPosition(initialSplitMsgEnc[1].size() - initialOverrun);
+                        qtbf = graphemeQtbf;
+                    }
+                    else {
+                        // If the QTBF fails to find a split point in Grapheme mode, we give up.
+                        // This should never happen, but it should be handled anyway.
+                        qWarning() << "Unexpected failure to split message!";
+                        return msgsToSend;
+                    }
+                }
+            }
+
+            // Once a message of sendable length has been found, remove it from the wrkMsg and
+            // add it to the list of messages to be sent.
+            wrkMsg.remove(0, splitPos);
+            msgsToSend.append(splitMsgEnc);
+        }
+        else{
+            // If the entire remaining message is short enough to be sent all at once, remove
+            // it from the wrkMsg and add it to the list of messages to be sent.
+            wrkMsg.remove(0, splitPos);
+            msgsToSend.append(initialSplitMsgEnc);
+        }
+    } while (wrkMsg.size() > 0);
+
+    return msgsToSend;
+}