From 7dac169f395becd65ef12a9ff98c6c86f9fb85a9 Mon Sep 17 00:00:00 2001 From: x-yl Date: Fri, 11 Jun 2021 11:12:23 +0400 Subject: [PATCH] Utilities: Add a simple utility to test the IMAP library test-imap is a very simple tool which runs through some of the IMAP commands and makes sure they don't crash. --- Userland/Utilities/CMakeLists.txt | 1 + Userland/Utilities/test-imap.cpp | 161 ++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 Userland/Utilities/test-imap.cpp diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index d811ec22ae..79581146b8 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -52,6 +52,7 @@ target_link_libraries(tar LibArchive LibCompress) target_link_libraries(telws LibProtocol LibLine) target_link_libraries(test-crypto LibCrypto LibTLS LibLine) target_link_libraries(test-fuzz LibCore LibGemini LibGfx LibHTTP LibIPC LibJS LibMarkdown LibShell) +target_link_libraries(test-imap LibIMAP) target_link_libraries(test-pthread LibThreading) target_link_libraries(tt LibPthread) target_link_libraries(unzip LibArchive LibCompress) diff --git a/Userland/Utilities/test-imap.cpp b/Userland/Utilities/test-imap.cpp new file mode 100644 index 0000000000..4619ee7230 --- /dev/null +++ b/Userland/Utilities/test-imap.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2021, Kyle Pereira + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + if (pledge("stdio inet tty rpath unix", nullptr) < 0) { + perror("pledge"); + return 1; + } + + String host; + int port; + bool tls { false }; + + String username; + String password; + + bool interactive_password; + + Core::ArgsParser args_parser; + args_parser.add_option(interactive_password, "Prompt for password with getpass", "interactive", 'i'); + args_parser.add_option(tls, "Connect with TLS (IMAPS)", "secure", 's'); + args_parser.add_positional_argument(host, "IMAP host", "host"); + args_parser.add_positional_argument(port, "Port to connect to", "port"); + args_parser.add_positional_argument(username, "Username", "username"); + args_parser.parse(argc, argv); + + if (interactive_password) { + auto password_or_err = Core::get_password(); + if (password_or_err.is_error()) { + warnln("{}", password_or_err.error().string()); + return 1; + } + password = password_or_err.value(); + } else { + auto standard_input = Core::File::standard_input(); + password = standard_input->read_all(); + } + + Core::EventLoop loop; + auto client = IMAP::Client(host, port, tls); + client.connect().value()->await(); + + auto response = client.login(username, password)->await().release_value(); + outln("[LOGIN] Login response: {}", response.response_text()); + + response = move(client.send_simple_command(IMAP::CommandType::Capability)->await().value().get()); + outln("[CAPABILITY] First capability: {}", response.data().capabilities().first()); + bool idle_supported = !response.data().capabilities().find_if([](auto capability) { return capability.equals_ignoring_case("IDLE"); }).is_end(); + + response = client.list("", "*")->await().release_value(); + outln("[LIST] First mailbox: {}", response.data().list_items().first().name); + + auto mailbox = "Inbox"; + response = client.select(mailbox)->await().release_value(); + outln("[SELECT] Select response: {}", response.response_text()); + + auto message = Message { + "From: John Doe \r\n" + "To: Mary Smith \r\n" + "Subject: Saying Hello\r\n" + "Date: Fri, 21 Nov 1997 09:55:06 -0600\r\n" + "Message-ID: <1234@local.machine.example>\r\n" + "\r\n" + "This is a message just to say hello.\r\n" + "So, \"Hello\"." + }; + auto promise = client.append("INBOX", move(message)); + response = promise->await().release_value(); + outln("[APPEND] Response: {}", response.response_text()); + + Vector keys; + keys.append(IMAP::SearchKey { + IMAP::SearchKey::From { "jdoe@machine.example" } }); + keys.append(IMAP::SearchKey { + IMAP::SearchKey::Subject { "Saying Hello" } }); + response = client.search({}, move(keys), false)->await().release_value(); + + Vector search_results = move(response.data().search_results()); + int added_message = search_results.first(); + outln("[SEARCH] Number of results: {}", search_results.size()); + + response = client.status("INBOX", { IMAP::StatusItemType::Recent, IMAP::StatusItemType::Messages })->await().release_value(); + outln("[STATUS] Recent items: {}", response.data().status_item().get(IMAP::StatusItemType::Recent)); + + for (auto item : search_results) { + // clang-format off + // clang formats this very badly + auto fetch_command = IMAP::FetchCommand { + .sequence_set = { { (int)item, (int)item } }, + .data_items = { + IMAP::FetchCommand::DataItem { + .type = IMAP::FetchCommand::DataItemType::BodyStructure + }, + IMAP::FetchCommand::DataItem { + .type = IMAP::FetchCommand::DataItemType::BodySection, + .section = IMAP::FetchCommand::DataItem::Section { + .type = IMAP::FetchCommand::DataItem::SectionType::HeaderFields, + .headers = { { "Subject" } } + } + }, + IMAP::FetchCommand::DataItem { + .type = IMAP::FetchCommand::DataItemType::BodySection, + .section = IMAP::FetchCommand::DataItem::Section { + .type = IMAP::FetchCommand::DataItem::SectionType::Parts, + .parts = { { 1 } } + }, + .partial_fetch = true, + .start = 0, + .octets = 8192 + } + } + }; + // clang-format on + + auto fetch_response = client.fetch(fetch_command, false)->await().release_value(); + outln("[FETCH] Subject of search result: {}", + fetch_response.data() + .fetch_data() + .first() + .get() + .body_data() + .find_if([](Tuple>& data) { + const auto data_item = data.get<0>(); + return data_item.section.has_value() && data_item.section->type == IMAP::FetchCommand::DataItem::SectionType::HeaderFields; + }) + ->get<1>() + .value()); + } + + response = client.store(IMAP::StoreMethod::Add, { added_message, added_message }, false, { "\\Deleted" }, false)->await().release_value(); + outln("[STORE] Store response: {}", response.response_text()); + + response = move(client.send_simple_command(IMAP::CommandType::Expunge)->await().release_value().get()); + outln("[EXPUNGE] Number of expunged entries: {}", response.data().expunged().size()); + + if (idle_supported) { + VERIFY(client.idle()->await().has_value()); + sleep(3); + response = client.finish_idle()->await().release_value(); + outln("[IDLE] Idle response: {}", response.response_text()); + } else { + outln("[IDLE] Skipped. No IDLE support."); + } + + response = move(client.send_simple_command(IMAP::CommandType::Logout)->await().release_value().get()); + outln("[LOGOUT] Bye: {}", response.data().bye_message().value()); + + client.close(); + + return 0; +}