root/src/add-ons/mail_daemon/inbound_protocols/imap/imap_lib/Protocol.cpp
/*
 * Copyright 2010-2016, Haiku Inc. All Rights Reserved.
 * Copyright 2010 Clemens Zeidler. All rights reserved.
 *
 * Distributed under the terms of the MIT License.
 */


#include "Protocol.h"

#include "Commands.h"


#define DEBUG_IMAP_PROTOCOL
#ifdef DEBUG_IMAP_PROTOCOL
#       include <stdio.h>
#       define TRACE(...) printf(__VA_ARGS__)
#else
#       define TRACE(...) ;
#endif


namespace IMAP {


Protocol::Protocol()
        :
        fSocket(NULL),
        fBufferedSocket(NULL),
        fHandlerList(5),
        fCommandID(0),
        fIsConnected(false)
{
}


Protocol::~Protocol()
{
        delete fSocket;
        delete fBufferedSocket;
}


status_t
Protocol::Connect(const BNetworkAddress& address, const char* username,
        const char* password, bool useSSL)
{
        TRACE("Connect\n");
        if (useSSL)
                fSocket = new(std::nothrow) BSecureSocket(address);
        else
                fSocket = new(std::nothrow) BSocket(address);

        if (fSocket == NULL)
                return B_NO_MEMORY;

        status_t status = fSocket->InitCheck();
        if (status != B_OK)
                return status;

        fBufferedSocket = new(std::nothrow) BBufferedDataIO(*fSocket, 32768, false,
                true);
        if (fBufferedSocket == NULL)
                return B_NO_MEMORY;

        TRACE("Login\n");

        fIsConnected = true;

        LoginCommand login(username, password);
        status = ProcessCommand(login);
        if (status != B_OK) {
                _Disconnect();
                return status;
        }

        _ParseCapabilities(login.Capabilities());

        if (fCapabilities.IsEmpty()) {
                CapabilityHandler capabilityHandler;
                status = ProcessCommand(capabilityHandler);
                if (status != B_OK)
                        return status;

                _ParseCapabilities(capabilityHandler.Capabilities());
        }

        if (Capabilities().Contains("ID")) {
                // Get the server's ID into our log
                class IDCommand : public IMAP::Command, public IMAP::Handler {
                public:
                        BString CommandString()
                        {
                                return "ID NIL";
                        }

                        bool HandleUntagged(IMAP::Response& response)
                        {
                                if (response.IsCommand("ID") && response.IsListAt(1)) {
                                        puts("Server:");
                                        ArgumentList& list = response.ListAt(1);
                                        for (int32 i = 0; i < list.CountItems(); i += 2) {
                                                printf("  %s: %s\n",
                                                        list.ItemAt(i)->ToString().String(),
                                                        list.ItemAt(i + 1)->ToString().String());
                                        }
                                        return true;
                                }

                                return false;
                        }
                };
                IDCommand idCommand;
                ProcessCommand(idCommand);
        }
        return B_OK;
}


status_t
Protocol::Disconnect()
{
        if (IsConnected()) {
                RawCommand command("LOGOUT");
                ProcessCommand(command);
        }
        return _Disconnect();
}


bool
Protocol::IsConnected()
{
        return fIsConnected;
}


bool
Protocol::AddHandler(Handler& handler)
{
        return fHandlerList.AddItem(&handler);
}


void
Protocol::RemoveHandler(Handler& handler)
{
        fHandlerList.RemoveItem(&handler);
}


status_t
Protocol::GetFolders(FolderList& folders, BString& separator)
{
        BStringList allFolders;
        status_t status = _GetAllFolders(allFolders);
        if (status != B_OK)
                return status;

        BStringList subscribedFolders;
        status = GetSubscribedFolders(subscribedFolders, separator);
        if (status != B_OK)
                return status;

        for (int32 i = 0; i < allFolders.CountStrings(); i++) {
                FolderEntry entry;
                entry.folder = allFolders.StringAt(i);
                for (int32 j = 0; j < subscribedFolders.CountStrings(); j++) {
                        if (entry.folder == subscribedFolders.StringAt(j)) {
                                entry.subscribed = true;
                                break;
                        }
                }
                folders.push_back(entry);
        }

        // you could be subscribed to a folder which not exist currently, add them:
        for (int32 i = 0; i < subscribedFolders.CountStrings(); i++) {
                bool isInlist = false;
                for (int32 j = 0; j < allFolders.CountStrings(); j++) {
                        if (subscribedFolders.StringAt(i) == allFolders.StringAt(j)) {
                                isInlist = true;
                                break;
                        }
                }
                if (isInlist)
                        continue;

                FolderEntry entry;
                entry.folder = subscribedFolders.StringAt(i);
                entry.subscribed = true;
                folders.push_back(entry);
        }

        return B_OK;
}


status_t
Protocol::GetSubscribedFolders(BStringList& folders, BString& separator)
{
        ListCommand command(NULL, true);
        status_t status = ProcessCommand(command);
        if (status != B_OK)
                return status;

        folders = command.FolderList();
        separator = command.Separator();
        return status;
}


status_t
Protocol::SubscribeFolder(const char* folder)
{
        SubscribeCommand command(folder);
        return ProcessCommand(command);
}


status_t
Protocol::UnsubscribeFolder(const char* folder)
{
        UnsubscribeCommand command(folder);
        return ProcessCommand(command);
}


status_t
Protocol::GetQuota(uint64& used, uint64& total)
{
        if (!Capabilities().Contains("QUOTA"))
                return B_ERROR;

        GetQuotaCommand quotaCommand;
        status_t status = ProcessCommand(quotaCommand);
        if (status != B_OK)
                return status;

        used = quotaCommand.UsedStorage();
        total = quotaCommand.TotalStorage();
        return B_OK;
}


status_t
Protocol::SendCommand(const char* command)
{
        return SendCommand(0, command);
}


status_t
Protocol::SendCommand(int32 id, const char* command)
{
        char buffer[2048];
        int32 length;
        if (id > 0) {
                length = snprintf(buffer, sizeof(buffer), "A%.7" B_PRId32 " %s\r\n",
                        id, command);
        } else
                length = snprintf(buffer, sizeof(buffer), "%s\r\n", command);

        TRACE("C: %s", buffer);

        ssize_t bytesWritten = fSocket->Write(buffer, length);
        if (bytesWritten < 0)
                return bytesWritten;

        return bytesWritten == length ? B_OK : B_ERROR;
}


ssize_t
Protocol::SendData(const char* buffer, uint32 length)
{
        return fSocket->Write(buffer, length);
}


status_t
Protocol::ProcessCommand(Command& command, bigtime_t timeout)
{
        BString commandString = command.CommandString();
        if (commandString.IsEmpty())
                return B_BAD_VALUE;

        Handler* handler = dynamic_cast<Handler*>(&command);
        if (handler != NULL && !AddHandler(*handler))
                return B_NO_MEMORY;

        int32 commandID = NextCommandID();
        status_t status = SendCommand(commandID, commandString.String());
        if (status == B_OK) {
                fOngoingCommands[commandID] = &command;
                status = HandleResponse(&command, timeout);
        }

        if (handler != NULL)
                RemoveHandler(*handler);

        return status;
}


// #pragma mark - protected


status_t
Protocol::HandleResponse(Command* command, bigtime_t timeout,
        bool disconnectOnTimeout)
{
        status_t commandStatus = B_OK;
        IMAP::ResponseParser parser(*fBufferedSocket);
        if (IMAP::LiteralHandler* literalHandler
                        = dynamic_cast<IMAP::LiteralHandler*>(command))
                parser.SetLiteralHandler(literalHandler);

        IMAP::Response response;

        bool done = false;
        while (!done) {
                try {
                        status_t status = parser.NextResponse(response, timeout);
                        if (status != B_OK) {
                                // we might have lost the connection, clear the connection state
                                if (status != B_TIMED_OUT || disconnectOnTimeout)
                                        _Disconnect();

                                return status;
                        }

                        if (response.IsUntagged() || response.IsContinuation()) {
                                bool handled = false;
                                for (int32 i = fHandlerList.CountItems(); i-- > 0;) {
                                        if (fHandlerList.ItemAt(i)->HandleUntagged(response)) {
                                                handled = true;
                                                break;
                                        }
                                }
                                if (!handled)
                                        printf("Unhandled S: %s\n", response.ToString().String());
                        } else {
                                CommandIDMap::iterator found
                                        = fOngoingCommands.find(response.Tag());
                                if (found != fOngoingCommands.end()) {
                                        status_t status = found->second->HandleTagged(response);
                                        if (status != B_OK)
                                                commandStatus = status;

                                        fOngoingCommands.erase(found);
                                } else
                                        printf("Unknown tag S: %s\n", response.ToString().String());
                        }
                } catch (ParseException& exception) {
                        printf("Error during parsing: %s\n", exception.Message());
                } catch (StreamException& exception) {
                        return exception.Status();
                }

                if (fOngoingCommands.size() == 0)
                        done = true;
        }

        return commandStatus;
}


int32
Protocol::NextCommandID()
{
        fCommandID++;
        return fCommandID;
}


// #pragma mark - private


status_t
Protocol::_Disconnect()
{
        fOngoingCommands.clear();
        fIsConnected = false;
        delete fBufferedSocket;
        fBufferedSocket = NULL;
        delete fSocket;
        fSocket = NULL;

        return B_OK;
}


status_t
Protocol::_GetAllFolders(BStringList& folders)
{
        ListCommand command(NULL, false);
        status_t status = ProcessCommand(command);
        if (status != B_OK)
                return status;

        folders = command.FolderList();
        return status;
}


void
Protocol::_ParseCapabilities(const ArgumentList& arguments)
{
        fCapabilities.MakeEmpty();

        for (int32 i = 0; i < arguments.CountItems(); i++) {
                if (StringArgument* argument
                                = dynamic_cast<StringArgument*>(arguments.ItemAt(i)))
                        fCapabilities.AddItem(new StringArgument(*argument));
        }

        TRACE("capabilities: %s\n", fCapabilities.ToString().String());
}


}       // namespace IMAP