diff options
author | x-yl <kylepereira@mail.com> | 2021-06-01 17:21:01 +0400 |
---|---|---|
committer | Ali Mohammad Pur <Ali.mpfard@gmail.com> | 2021-06-11 23:58:28 +0430 |
commit | 8c6061fc4af79984e2c9fbbb2300567a9f5ebbfe (patch) | |
tree | 0dd70e8c5bf45482b79e3db002c56a524d02cbca /Userland/Libraries | |
parent | 904322e75400eabe00f60d74f2c08d03cdb9d3a2 (diff) | |
download | serenity-8c6061fc4af79984e2c9fbbb2300567a9f5ebbfe.zip |
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.
Diffstat (limited to 'Userland/Libraries')
-rw-r--r-- | Userland/Libraries/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibIMAP/CMakeLists.txt | 6 | ||||
-rw-r--r-- | Userland/Libraries/LibIMAP/Client.cpp | 204 | ||||
-rw-r--r-- | Userland/Libraries/LibIMAP/Client.h | 57 | ||||
-rw-r--r-- | Userland/Libraries/LibIMAP/Objects.cpp | 11 | ||||
-rw-r--r-- | Userland/Libraries/LibIMAP/Objects.h | 158 | ||||
-rw-r--r-- | Userland/Libraries/LibIMAP/Parser.cpp | 163 | ||||
-rw-r--r-- | Userland/Libraries/LibIMAP/Parser.h | 44 |
8 files changed, 644 insertions, 0 deletions
diff --git a/Userland/Libraries/CMakeLists.txt b/Userland/Libraries/CMakeLists.txt index 5a7543656a..a51cf9b709 100644 --- a/Userland/Libraries/CMakeLists.txt +++ b/Userland/Libraries/CMakeLists.txt @@ -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) diff --git a/Userland/Libraries/LibIMAP/CMakeLists.txt b/Userland/Libraries/LibIMAP/CMakeLists.txt new file mode 100644 index 0000000000..95fd92eec1 --- /dev/null +++ b/Userland/Libraries/LibIMAP/CMakeLists.txt @@ -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) diff --git a/Userland/Libraries/LibIMAP/Client.cpp b/Userland/Libraries/LibIMAP/Client.cpp new file mode 100644 index 0000000000..160b51131c --- /dev/null +++ b/Userland/Libraries/LibIMAP/Client.cpp @@ -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(); + } +} +} diff --git a/Userland/Libraries/LibIMAP/Client.h b/Userland/Libraries/LibIMAP/Client.h new file mode 100644 index 0000000000..1481d92eb7 --- /dev/null +++ b/Userland/Libraries/LibIMAP/Client.h @@ -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(); +}; +} diff --git a/Userland/Libraries/LibIMAP/Objects.cpp b/Userland/Libraries/LibIMAP/Objects.cpp new file mode 100644 index 0000000000..11c8222c06 --- /dev/null +++ b/Userland/Libraries/LibIMAP/Objects.cpp @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2021, Kyle Pereira <hey@xylepereira.me> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibIMAP/Objects.h> + +namespace IMAP { + +} diff --git a/Userland/Libraries/LibIMAP/Objects.h b/Userland/Libraries/LibIMAP/Objects.h new file mode 100644 index 0000000000..037b9387b5 --- /dev/null +++ b/Userland/Libraries/LibIMAP/Objects.h @@ -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; +}; diff --git a/Userland/Libraries/LibIMAP/Parser.cpp b/Userland/Libraries/LibIMAP/Parser.cpp new file mode 100644 index 0000000000..1c872e242a --- /dev/null +++ b/Userland/Libraries/LibIMAP/Parser.cpp @@ -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); +} + +} diff --git a/Userland/Libraries/LibIMAP/Parser.h b/Userland/Libraries/LibIMAP/Parser.h new file mode 100644 index 0000000000..9a4ee07e5e --- /dev/null +++ b/Userland/Libraries/LibIMAP/Parser.h @@ -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); +}; +} |