Serenity Operating System
1/*
2 * Copyright (c) 2021, Kyle Pereira <hey@xylepereira.me>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <LibCore/ArgsParser.h>
8#include <LibCore/DeprecatedFile.h>
9#include <LibCore/EventLoop.h>
10#include <LibCore/GetPassword.h>
11#include <LibIMAP/Client.h>
12#include <LibMain/Main.h>
13
14ErrorOr<int> serenity_main(Main::Arguments arguments)
15{
16 if (pledge("stdio inet tty rpath unix", nullptr) < 0) {
17 perror("pledge");
18 return 1;
19 }
20
21 DeprecatedString host;
22 int port;
23 bool tls { false };
24
25 DeprecatedString username;
26 Core::SecretString password;
27
28 bool interactive_password;
29
30 Core::ArgsParser args_parser;
31 args_parser.add_option(interactive_password, "Prompt for password with getpass", "interactive", 'i');
32 args_parser.add_option(tls, "Connect with TLS (IMAPS)", "secure", 's');
33 args_parser.add_positional_argument(host, "IMAP host", "host");
34 args_parser.add_positional_argument(port, "Port to connect to", "port");
35 args_parser.add_positional_argument(username, "Username", "username");
36 args_parser.parse(arguments);
37
38 if (interactive_password) {
39 password = TRY(Core::get_password());
40 } else {
41 auto standard_input = Core::DeprecatedFile::standard_input();
42 password = Core::SecretString::take_ownership(standard_input->read_all());
43 }
44
45 Core::EventLoop loop;
46 auto client = TRY(tls ? IMAP::Client::connect_tls(host, port) : IMAP::Client::connect_plaintext(host, port));
47 TRY(client->connection_promise()->await());
48
49 auto response = TRY(client->login(username, password.view())->await()).release_value();
50 outln("[LOGIN] Login response: {}", response.response_text());
51
52 response = move(TRY(client->send_simple_command(IMAP::CommandType::Capability)->await()).value().get<IMAP::SolidResponse>());
53 outln("[CAPABILITY] First capability: {}", response.data().capabilities().first());
54 bool idle_supported = !response.data().capabilities().find_if([](auto capability) { return capability.equals_ignoring_ascii_case("IDLE"sv); }).is_end();
55
56 response = TRY(client->list(""sv, "*"sv)->await()).release_value();
57 outln("[LIST] First mailbox: {}", response.data().list_items().first().name);
58
59 auto mailbox = "Inbox"sv;
60 response = TRY(client->select(mailbox)->await()).release_value();
61 outln("[SELECT] Select response: {}", response.response_text());
62
63 auto message = Message {
64 "From: John Doe <jdoe@machine.example>\r\n"
65 "To: Mary Smith <mary@example.net>\r\n"
66 "Subject: Saying Hello\r\n"
67 "Date: Fri, 21 Nov 1997 09:55:06 -0600\r\n"
68 "Message-ID: <1234@local.machine.example>\r\n"
69 "\r\n"
70 "This is a message just to say hello.\r\n"
71 "So, \"Hello\"."
72 };
73 auto promise = client->append("INBOX"sv, move(message));
74 response = TRY(promise->await()).release_value();
75 outln("[APPEND] Response: {}", response.response_text());
76
77 Vector<IMAP::SearchKey> keys;
78 keys.append(IMAP::SearchKey {
79 IMAP::SearchKey::From { "jdoe@machine.example" } });
80 keys.append(IMAP::SearchKey {
81 IMAP::SearchKey::Subject { "Saying Hello" } });
82 response = TRY(client->search({}, move(keys), false)->await()).release_value();
83
84 Vector<unsigned> search_results = move(response.data().search_results());
85 auto added_message = search_results.first();
86 outln("[SEARCH] Number of results: {}", search_results.size());
87
88 response = TRY(client->status("INBOX"sv, { IMAP::StatusItemType::Recent, IMAP::StatusItemType::Messages })->await()).release_value();
89 outln("[STATUS] Recent items: {}", response.data().status_item().get(IMAP::StatusItemType::Recent));
90
91 for (auto item : search_results) {
92 // clang-format off
93 // clang formats this very badly
94 auto fetch_command = IMAP::FetchCommand {
95 .sequence_set = { { (int)item, (int)item } },
96 .data_items = {
97 IMAP::FetchCommand::DataItem {
98 .type = IMAP::FetchCommand::DataItemType::BodyStructure
99 },
100 IMAP::FetchCommand::DataItem {
101 .type = IMAP::FetchCommand::DataItemType::BodySection,
102 .section = IMAP::FetchCommand::DataItem::Section {
103 .type = IMAP::FetchCommand::DataItem::SectionType::HeaderFields,
104 .headers = { { "Subject" } }
105 }
106 },
107 IMAP::FetchCommand::DataItem {
108 .type = IMAP::FetchCommand::DataItemType::BodySection,
109 .section = IMAP::FetchCommand::DataItem::Section {
110 .type = IMAP::FetchCommand::DataItem::SectionType::Parts,
111 .parts = { { 1 } }
112 },
113 .partial_fetch = true,
114 .start = 0,
115 .octets = 8192
116 }
117 }
118 };
119 // clang-format on
120
121 auto fetch_response = TRY(client->fetch(fetch_command, false)->await()).release_value();
122 outln("[FETCH] Subject of search result: {}",
123 fetch_response.data()
124 .fetch_data()
125 .first()
126 .get<IMAP::FetchResponseData>()
127 .body_data()
128 .find_if([](Tuple<IMAP::FetchCommand::DataItem, Optional<DeprecatedString>>& data) {
129 const auto data_item = data.get<0>();
130 return data_item.section.has_value() && data_item.section->type == IMAP::FetchCommand::DataItem::SectionType::HeaderFields;
131 })
132 ->get<1>()
133 .value());
134 }
135
136 // FIXME: There is a discrepancy between IMAP::Sequence wanting signed ints
137 // and IMAP search results returning unsigned ones. Find which one is
138 // more correct and fix this.
139 response = TRY(client->store(IMAP::StoreMethod::Add, { static_cast<int>(added_message), static_cast<int>(added_message) }, false, { "\\Deleted" }, false)->await()).release_value();
140 outln("[STORE] Store response: {}", response.response_text());
141
142 response = move(TRY(client->send_simple_command(IMAP::CommandType::Expunge)->await()).release_value().get<IMAP::SolidResponse>());
143 outln("[EXPUNGE] Number of expunged entries: {}", response.data().expunged().size());
144
145 if (idle_supported) {
146 VERIFY(TRY(client->idle()->await()).has_value());
147 sleep(3);
148 response = TRY(client->finish_idle()->await()).release_value();
149 outln("[IDLE] Idle response: {}", response.response_text());
150 } else {
151 outln("[IDLE] Skipped. No IDLE support.");
152 }
153
154 response = move(TRY(client->send_simple_command(IMAP::CommandType::Logout)->await()).release_value().get<IMAP::SolidResponse>());
155 outln("[LOGOUT] Bye: {}", response.data().bye_message().value());
156
157 client->close();
158
159 return 0;
160}