1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 22:07:35 +00:00

LibIMAP: Add a new IMAP client and support NOOP

A large commit, but sets up the framework for how the IMAP library will
work. Right now only the NOOP command and response is supported.
This commit is contained in:
x-yl 2021-06-01 17:21:01 +04:00 committed by Ali Mohammad Pur
parent 904322e754
commit 8c6061fc4a
8 changed files with 644 additions and 0 deletions

View file

@ -19,6 +19,7 @@ add_subdirectory(LibGfx)
add_subdirectory(LibGL)
add_subdirectory(LibGUI)
add_subdirectory(LibHTTP)
add_subdirectory(LibIMAP)
add_subdirectory(LibImageDecoderClient)
add_subdirectory(LibIPC)
add_subdirectory(LibJS)

View file

@ -0,0 +1,6 @@
set(SOURCES Objects.cpp Client.cpp Parser.cpp)
set(GENERATED_SOURCES)
serenity_lib(LibIMAP imap)
target_link_libraries(LibIMAP LibCore LibTLS)

View file

@ -0,0 +1,204 @@
/*
* Copyright (c) 2021, Kyle Pereira <hey@xylepereira.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIMAP/Client.h>
namespace IMAP {
Client::Client(StringView host, unsigned int port, bool start_with_tls)
: m_host(host)
, m_port(port)
, m_tls(start_with_tls)
, m_parser(Parser())
{
if (start_with_tls) {
m_tls_socket = TLS::TLSv12::construct(nullptr);
m_tls_socket->set_root_certificates(DefaultRootCACertificates::the().certificates());
} else {
m_socket = Core::TCPSocket::construct();
}
}
Optional<RefPtr<Promise<Empty>>> Client::connect()
{
bool success;
if (m_tls) {
success = connect_tls();
} else {
success = connect_plaintext();
}
if (!success)
return {};
m_connect_pending = new Promise<bool> {};
return m_connect_pending;
}
bool Client::connect_tls()
{
m_tls_socket->on_tls_ready_to_read = [&](TLS::TLSv12&) {
on_tls_ready_to_receive();
};
m_tls_socket->on_tls_error = [&](TLS::AlertDescription alert) {
dbgln("failed: {}", alert_name(alert));
};
m_tls_socket->on_tls_connected = [&] {
dbgln("connected");
};
auto success = m_tls_socket->connect(m_host, m_port);
dbgln("connecting to {}:{} {}", m_host, m_port, success);
return success;
}
bool Client::connect_plaintext()
{
m_socket->on_ready_to_read = [&] {
on_ready_to_receive();
};
auto success = m_socket->connect(m_host, m_port);
dbgln("connecting to {}:{} {}", m_host, m_port, success);
return success;
}
void Client::on_tls_ready_to_receive()
{
if (!m_tls_socket->can_read())
return;
auto data = m_tls_socket->read();
if (!data.has_value())
return;
// Once we get server hello we can start sending
if (m_connect_pending) {
m_connect_pending->resolve({});
m_connect_pending.clear();
return;
}
m_buffer += data.value();
if (m_buffer[m_buffer.size() - 1] == '\n') {
// Don't try parsing until we have a complete line.
auto response = m_parser.parse(move(m_buffer), m_expecting_response);
handle_parsed_response(move(response));
m_buffer.clear();
}
}
void Client::on_ready_to_receive()
{
if (!m_socket->can_read())
return;
m_buffer += m_socket->read_all();
// Once we get server hello we can start sending.
if (m_connect_pending) {
m_connect_pending->resolve({});
m_connect_pending.clear();
m_buffer.clear();
return;
}
if (m_buffer[m_buffer.size() - 1] == '\n') {
// Don't try parsing until we have a complete line.
auto response = m_parser.parse(move(m_buffer), m_expecting_response);
handle_parsed_response(move(response));
m_buffer.clear();
}
}
static ReadonlyBytes command_byte_buffer(CommandType command)
{
switch (command) {
case CommandType::Noop:
return "NOOP"sv.bytes();
}
VERIFY_NOT_REACHED();
}
void Client::send_raw(StringView data)
{
if (m_tls) {
m_tls_socket->write(data.bytes());
m_tls_socket->write("\r\n"sv.bytes());
} else {
m_socket->write(data.bytes());
m_socket->write("\r\n"sv.bytes());
}
}
RefPtr<Promise<Optional<Response>>> Client::send_command(Command&& command)
{
m_command_queue.append(move(command));
m_current_command++;
auto promise = Promise<Optional<Response>>::construct();
m_pending_promises.append(promise);
if (m_pending_promises.size() == 1)
send_next_command();
return promise;
}
RefPtr<Promise<Optional<Response>>> Client::send_simple_command(CommandType type)
{
auto command = Command { type, m_current_command, {} };
return send_command(move(command));
}
void Client::handle_parsed_response(ParseStatus&& parse_status)
{
if (!m_expecting_response) {
if (!parse_status.successful) {
dbgln("Parsing failed on unrequested data!");
} else if (parse_status.response.has_value()) {
unrequested_response_callback(move(parse_status.response.value().get<SolidResponse>().data()));
}
} else {
bool should_send_next = false;
if (!parse_status.successful) {
m_expecting_response = false;
m_pending_promises.first()->resolve({});
m_pending_promises.remove(0);
}
if (parse_status.response.has_value()) {
m_expecting_response = false;
should_send_next = parse_status.response->has<SolidResponse>();
m_pending_promises.first()->resolve(move(parse_status.response));
m_pending_promises.remove(0);
}
if (should_send_next && !m_command_queue.is_empty()) {
send_next_command();
}
}
}
void Client::send_next_command()
{
auto command = m_command_queue.take_first();
ByteBuffer buffer;
auto tag = AK::String::formatted("A{} ", m_current_command);
buffer += tag.to_byte_buffer();
auto command_type = command_byte_buffer(command.type);
buffer.append(command_type.data(), command_type.size());
for (auto& arg : command.args) {
buffer.append(" ", 1);
buffer.append(arg.bytes().data(), arg.length());
}
send_raw(buffer);
m_expecting_response = true;
}
void Client::close()
{
if (m_tls) {
m_tls_socket->close();
} else {
m_socket->close();
}
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2021, Kyle Pereira <hey@xylepereira.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <LibIMAP/Parser.h>
#include <LibTLS/TLSv12.h>
namespace IMAP {
class Client {
friend class Parser;
public:
Client(StringView host, unsigned port, bool start_with_tls);
Optional<RefPtr<Promise<Empty>>> connect();
RefPtr<Promise<Optional<Response>>> send_command(Command&&);
RefPtr<Promise<Optional<Response>>> send_simple_command(CommandType);
void send_raw(StringView data);
void close();
Function<void(ResponseData&&)> unrequested_response_callback;
private:
StringView m_host;
unsigned m_port;
RefPtr<Core::Socket> m_socket;
RefPtr<TLS::TLSv12> m_tls_socket;
void on_ready_to_receive();
void on_tls_ready_to_receive();
bool m_tls;
int m_current_command = 1;
bool connect_tls();
bool connect_plaintext();
// Sent but response not received
Vector<RefPtr<Promise<Optional<Response>>>> m_pending_promises;
// Not yet sent
Vector<Command> m_command_queue {};
RefPtr<Promise<bool>> m_connect_pending {};
ByteBuffer m_buffer;
Parser m_parser;
bool m_expecting_response { false };
void handle_parsed_response(ParseStatus&& parse_status);
void send_next_command();
};
}

View file

@ -0,0 +1,11 @@
/*
* Copyright (c) 2021, Kyle Pereira <hey@xylepereira.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIMAP/Objects.h>
namespace IMAP {
}

View file

@ -0,0 +1,158 @@
/*
* Copyright (c) 2021, Kyle Pereira <hey@xylepereira.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Format.h>
#include <AK/Function.h>
#include <AK/Tuple.h>
#include <AK/Variant.h>
#include <LibCore/DateTime.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Object.h>
#include <utility>
namespace IMAP {
enum class CommandType {
Noop,
};
enum class ResponseType : unsigned {
};
class Parser;
struct Command {
public:
CommandType type;
int tag;
Vector<String> args;
};
enum class ResponseStatus {
Bad,
No,
OK,
};
class ResponseData {
public:
[[nodiscard]] unsigned response_type() const
{
return m_response_type;
}
ResponseData()
: m_response_type(0)
{
}
ResponseData(ResponseData&) = delete;
ResponseData(ResponseData&&) = default;
ResponseData& operator=(const ResponseData&) = delete;
ResponseData& operator=(ResponseData&&) = default;
[[nodiscard]] bool contains_response_type(ResponseType response_type) const
{
return (static_cast<unsigned>(response_type) & m_response_type) != 0;
}
void add_response_type(ResponseType response_type)
{
m_response_type = m_response_type | static_cast<unsigned>(response_type);
}
private:
unsigned m_response_type;
};
class SolidResponse {
// Parser is allowed to set up fields
friend class Parser;
public:
ResponseStatus status() { return m_status; }
int tag() const { return m_tag; }
ResponseData& data() { return m_data; }
String response_text() { return m_response_text; };
SolidResponse()
: SolidResponse(ResponseStatus::Bad, -1)
{
}
SolidResponse(ResponseStatus status, int tag)
: m_status(status)
, m_tag(tag)
, m_data(ResponseData())
{
}
private:
ResponseStatus m_status;
String m_response_text;
unsigned m_tag;
ResponseData m_data;
};
struct ContinueRequest {
String data;
};
template<typename Result>
class Promise : public Core::Object {
C_OBJECT(Promise);
private:
Optional<Result> m_pending;
public:
Function<void(Result&)> on_resolved;
void resolve(Result&& result)
{
m_pending = move(result);
if (on_resolved)
on_resolved(m_pending.value());
}
bool is_resolved()
{
return m_pending.has_value();
};
Result await()
{
while (!is_resolved()) {
Core::EventLoop::current().pump();
}
return m_pending.release_value();
}
// Converts a Promise<A> to a Promise<B> using a function func: A -> B
template<typename T>
RefPtr<Promise<T>> map(Function<T(Result&)> func)
{
RefPtr<Promise<T>> new_promise = Promise<T>::construct();
on_resolved = [new_promise, func](Result& result) mutable {
auto t = func(result);
new_promise->resolve(move(t));
};
return new_promise;
}
};
using Response = Variant<SolidResponse, ContinueRequest>;
}
// An RFC 2822 message
// https://datatracker.ietf.org/doc/html/rfc2822
struct Message {
String data;
};

View file

@ -0,0 +1,163 @@
/*
* Copyright (c) 2021, Kyle Pereira <hey@xylepereira.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/CharacterTypes.h>
#include <LibIMAP/Parser.h>
namespace IMAP {
ParseStatus Parser::parse(ByteBuffer&& buffer, bool expecting_tag)
{
if (m_incomplete) {
m_buffer += buffer;
m_incomplete = false;
} else {
m_buffer = move(buffer);
position = 0;
m_response = SolidResponse();
}
if (try_consume("+")) {
consume(" ");
auto data = parse_while([](u8 x) { return x != '\r'; });
consume("\r\n");
return { true, { ContinueRequest { data } } };
}
if (expecting_tag) {
if (at_end()) {
m_incomplete = true;
return { true, {} };
}
parse_response_done();
}
if (m_parsing_failed) {
return { false, {} };
} else {
return { true, { { move(m_response) } } };
}
}
bool Parser::try_consume(StringView x)
{
size_t i = 0;
auto previous_position = position;
while (i < x.length() && !at_end() && to_ascii_lowercase(x[i]) == to_ascii_lowercase(m_buffer[position])) {
i++;
position++;
}
if (i != x.length()) {
// We didn't match the full string.
position = previous_position;
return false;
}
return true;
}
void Parser::parse_response_done()
{
consume("A");
auto tag = parse_number();
consume(" ");
ResponseStatus status = parse_status();
consume(" ");
m_response.m_tag = tag;
m_response.m_status = status;
StringBuilder response_data;
while (!at_end() && m_buffer[position] != '\r') {
response_data.append((char)m_buffer[position]);
position += 1;
}
consume("\r\n");
m_response.m_response_text = response_data.build();
}
void Parser::consume(StringView x)
{
if (!try_consume(x)) {
dbgln("{} not matched at {}, buffer: {}", x, position, StringView(m_buffer.data(), m_buffer.size()));
m_parsing_failed = true;
}
}
Optional<unsigned> Parser::try_parse_number()
{
auto number_matched = 0;
while (!at_end() && 0 <= m_buffer[position] - '0' && m_buffer[position] - '0' <= 9) {
number_matched++;
position++;
}
if (number_matched == 0)
return {};
auto number = StringView(m_buffer.data() + position - number_matched, number_matched);
return number.to_uint();
}
unsigned Parser::parse_number()
{
auto number = try_parse_number();
if (!number.has_value()) {
m_parsing_failed = true;
return -1;
}
return number.value();
}
StringView Parser::parse_atom()
{
auto is_non_atom_char = [](u8 x) {
auto non_atom_chars = { '(', ')', '{', ' ', '%', '*', '"', '\\', ']' };
return AK::find(non_atom_chars.begin(), non_atom_chars.end(), x) != non_atom_chars.end();
};
auto start = position;
auto count = 0;
while (!at_end() && !is_ascii_control(m_buffer[position]) && !is_non_atom_char(m_buffer[position])) {
count++;
position++;
}
return StringView(m_buffer.data() + start, count);
}
ResponseStatus Parser::parse_status()
{
auto atom = parse_atom();
if (atom.matches("OK")) {
return ResponseStatus::OK;
} else if (atom.matches("BAD")) {
return ResponseStatus::Bad;
} else if (atom.matches("NO")) {
return ResponseStatus::No;
}
m_parsing_failed = true;
return ResponseStatus::Bad;
}
StringView Parser::parse_while(Function<bool(u8)> should_consume)
{
int chars = 0;
while (!at_end() && should_consume(m_buffer[position])) {
position++;
chars++;
}
return StringView(m_buffer.data() + position - chars, chars);
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2021, Kyle Pereira <hey@xylepereira.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Result.h>
#include <LibIMAP/Objects.h>
namespace IMAP {
class Client;
struct ParseStatus {
bool successful;
Optional<Response> response;
};
class Parser {
public:
ParseStatus parse(ByteBuffer&& buffer, bool expecting_tag);
private:
// To retain state if parsing is not finished
ByteBuffer m_buffer;
SolidResponse m_response;
unsigned position { 0 };
bool m_incomplete { false };
bool m_parsing_failed { false };
bool try_consume(StringView);
bool at_end() { return position >= m_buffer.size(); };
void parse_response_done();
void consume(StringView x);
unsigned parse_number();
Optional<unsigned> try_parse_number();
void parse_untagged();
StringView parse_atom();
ResponseStatus parse_status();
StringView parse_while(Function<bool(u8)> should_consume);
};
}