summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibIMAP
diff options
context:
space:
mode:
authorx-yl <kylepereira@mail.com>2021-06-01 17:21:01 +0400
committerAli Mohammad Pur <Ali.mpfard@gmail.com>2021-06-11 23:58:28 +0430
commit8c6061fc4af79984e2c9fbbb2300567a9f5ebbfe (patch)
tree0dd70e8c5bf45482b79e3db002c56a524d02cbca /Userland/Libraries/LibIMAP
parent904322e75400eabe00f60d74f2c08d03cdb9d3a2 (diff)
downloadserenity-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/LibIMAP')
-rw-r--r--Userland/Libraries/LibIMAP/CMakeLists.txt6
-rw-r--r--Userland/Libraries/LibIMAP/Client.cpp204
-rw-r--r--Userland/Libraries/LibIMAP/Client.h57
-rw-r--r--Userland/Libraries/LibIMAP/Objects.cpp11
-rw-r--r--Userland/Libraries/LibIMAP/Objects.h158
-rw-r--r--Userland/Libraries/LibIMAP/Parser.cpp163
-rw-r--r--Userland/Libraries/LibIMAP/Parser.h44
7 files changed, 643 insertions, 0 deletions
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);
+};
+}