Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async Implementation #31

Open
leleftheriades opened this issue Apr 27, 2020 · 5 comments
Open

Async Implementation #31

leleftheriades opened this issue Apr 27, 2020 · 5 comments

Comments

@leleftheriades
Copy link

leleftheriades commented Apr 27, 2020

I am wondering if an async api will be provided.
Initially I thought it was supported because I saw a connect_async() but then I realized that is just a synchronous method with timeout support.
We would like to use this library because we liked the decoupled design but we need to use an async api for email sending

@karastojko
Copy link
Owner

Yes, only the synchronous method is currently available. I intend to add the asynchronous support because I also need it. Before that I have few defects to fix and to finish some IMAP stuff.

@xiexin0606
Copy link

Yes, only the synchronous method is currently available. I intend to add the asynchronous support because I also need it. Before that I have few defects to fix and to finish some IMAP stuff.

Hi, does this is in the plan?

@tsurumi-yizhou
Copy link
Contributor

What type do you like? coroutine? callback?

@karastojko
Copy link
Owner

I am starting to analyze how to do it (asynchronous methods). Still, I cannot give the precise schedule, due to bugs that are meant to be resolved in the meantime. But definitely, that is the next feature to be added.

@leleftheriades
Copy link
Author

leleftheriades commented Jun 16, 2023

Here is a sample utility class that can help you get inspired with the smtp dialog. It demonstrates how do do custom errors for the async api, as well as how to do timeouts. (I wrote it in notepad so I haven't checked if it works)

struct Reply{
    int code;
    std::string content;
};

Custom error like boost does it:

enum class SmtpError{
    success,
    timeout,
    readError,
    parseError
};

class SmtpErrorCategory : public std::error_category{
    public:
    const char* name() const noexcept override{
        return "SMTP error";        
    }

    std::string message(int value) const noexcept override{
        switch (static_cast<SmtpError>(value)){
        case SmtpError::success: return "Success";
        case SmtpError::timeout: return "Timeout";
        case SmtpError::readError: return "Read error";
        case SmtpError::parseError: return "Parse error";
        default: return "Unknown error";
        }        
    }
};

inline std::error_code make_error_code(SmtpError ec){
    static SmtpErrorCategory category;
    return std::error_code(static_cast<int>(ec), category);
}

Decorator that wraps a socket or ssl socket that provides interface for sending and receiving SMTP messages with timeout support. The idea is that you start both a timer and an async_read. if timer finishes first, you close the socket, raise a flag that timer finished first and call the completion callback with error. if the read finishes first, it will schedule another read if it's muli-reply, or if it is the last reply it will cancel the timeout timer and increase the currentOperationId, such that even if timer completes, it will realize that the read already succeded and won't close the socket

//socket_t can be boost::asio::ip::tcp::socket or boost::asio::ssl::stream<boost::asio::ip::tcp::socket>
//timer_t can be boost::asio::deadline_timer
template<typename socket_t, typename timer_t>
class SmtpTransaction{

public:    
    SmtpTransaction(socket_t& socket, timer_t& timeoutTimer);
    void async_receive(std::chrono::seconds timeout, std::function<void(std::error_code ec, std::vector<Reply> replies)>&& completionHandler);
    void async_send_and_receive(std::string&& command, std::chrono::seconds timeout, std::function<void(std::error_code ec, std::vector<Reply> replies)>&& completionHandler);
    
private:
    std::string sendBuffer;
    std::vector<Reply> replies;
    long long currentOperationId;
    bool timeoutTriggered;
    boost::asio::streambuf readBuffer;
    std::istream readBufferInputStream;   
    std::function<void(std::error_code ec, std::vector<Reply> replies)>&& completionHandler;
    socket_t& socket;
    timer_t& timeoutTimer;
}

Sample implementation for async_receive with timeouts and parsing for multiple replies

SmtpTransaction::SmtpTransaction(socket_t& socket, timer_t& timeoutTimer)
: socket(socket)
, timeoutTimer(timeoutTimer)
, currentOperationId(0)
, readBufferInputStream(&readBuffer)
{
}


void SmtpTransaction::async_receive(std::chrono::seconds timeout, std::function<void(std::error_code ec, std::vector<Reply> replies)>&& completionHandler){    
    this->completionHandler = std::move(completionHandler);
    
    timeoutTimer.expires_from_now(timeout);
    timeoutTimer.async_wait([idForWhichTimeoutWasPlaced(currentOperationId), this](const boost::system::error_code& error){
        if (error == boost::asio::error::operation_aborted)
            return;
    
        if (currentOperationId > idForWhichTimeoutWasPlaced)
            return;
            
        boost::system::error_code closeErrorCode;
        socket.close(closeErrorCode);
        
        timeoutTriggered = true;
        
        completionHandler(make_error_code(SmtpError::timeout), std::move(replies));
    });

    receiveNextLine();
}

void receiveNextLine(){    
    socket.async_read_until(readBuffer, "\n", [this](const boost::system::error_code& error, size_t bytes){
        if (timeoutTriggered)
            return;        
        
        if (error){
            ++currentOperationId;
            timeoutTimer.cancel();            
        
            boost::system::error_code closeErrorCode;
            socket.close(closeErrorCode);
            
            completionHandler(make_error_code(SmtpError::readError), std::move(replies));
            return;
        }
        
        std::string line;
        std::getline(readBufferInputStream, line, '\n');
        boost::algorithm::trim_if(line, boost::algorithm::is_any_of("\r\n"));

        try {
            int code = stoi(line.substr(0, 3));
            bool hasMoreReplies = line.size() > 3 && line.at(3) == '-';
            std::string content = line.size() > 4 ? line.substr(4) : "";

            replies.push_back(Reply{
                .code = code,
                .content = content
            });

            if (hasMoreReplies){
                receiveNextLine();                
            }else{            
                ++currentOperationId;
                timeoutTimer.cancel();
                
                completionHandler(make_error_code(SmtpError::success), std::move(replies));
            }        
            
        } catch (const std::exception&)
        {
            ++currentOperationId;
            timeoutTimer.cancel();
        
            boost::system::error_code closeErrorCode;
            socket.close(closeErrorCode);
            
            completionHandler(make_error_code(SmtpError::parseError), std::move(replies));            
            return;
        }
            
    });    
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants