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:
parent
904322e754
commit
8c6061fc4a
8 changed files with 644 additions and 0 deletions
|
@ -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)
|
||||
|
|
6
Userland/Libraries/LibIMAP/CMakeLists.txt
Normal file
6
Userland/Libraries/LibIMAP/CMakeLists.txt
Normal 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)
|
204
Userland/Libraries/LibIMAP/Client.cpp
Normal file
204
Userland/Libraries/LibIMAP/Client.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
57
Userland/Libraries/LibIMAP/Client.h
Normal file
57
Userland/Libraries/LibIMAP/Client.h
Normal 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();
|
||||
};
|
||||
}
|
11
Userland/Libraries/LibIMAP/Objects.cpp
Normal file
11
Userland/Libraries/LibIMAP/Objects.cpp
Normal 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 {
|
||||
|
||||
}
|
158
Userland/Libraries/LibIMAP/Objects.h
Normal file
158
Userland/Libraries/LibIMAP/Objects.h
Normal 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;
|
||||
};
|
163
Userland/Libraries/LibIMAP/Parser.cpp
Normal file
163
Userland/Libraries/LibIMAP/Parser.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
44
Userland/Libraries/LibIMAP/Parser.h
Normal file
44
Userland/Libraries/LibIMAP/Parser.h
Normal 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);
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue