MailWidget.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. /*
  2. * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
  3. * Copyright (c) 2021, Undefine <cqundefine@gmail.com>
  4. * Copyright (c) 2022, the SerenityOS developers.
  5. *
  6. * SPDX-License-Identifier: BSD-2-Clause
  7. */
  8. #include "MailWidget.h"
  9. #include <AK/Base64.h>
  10. #include <AK/GenericLexer.h>
  11. #include <Applications/Mail/MailWindowGML.h>
  12. #include <LibConfig/Client.h>
  13. #include <LibDesktop/Launcher.h>
  14. #include <LibGUI/Action.h>
  15. #include <LibGUI/Clipboard.h>
  16. #include <LibGUI/Menu.h>
  17. #include <LibGUI/MessageBox.h>
  18. #include <LibGUI/PasswordInputDialog.h>
  19. #include <LibGUI/Statusbar.h>
  20. #include <LibGUI/TableView.h>
  21. #include <LibGUI/TreeView.h>
  22. #include <LibIMAP/QuotedPrintable.h>
  23. MailWidget::MailWidget()
  24. {
  25. load_from_gml(mail_window_gml);
  26. m_mailbox_list = *find_descendant_of_type_named<GUI::TreeView>("mailbox_list");
  27. m_individual_mailbox_view = *find_descendant_of_type_named<GUI::TableView>("individual_mailbox_view");
  28. m_web_view = *find_descendant_of_type_named<Web::OutOfProcessWebView>("web_view");
  29. m_statusbar = *find_descendant_of_type_named<GUI::Statusbar>("statusbar");
  30. m_mailbox_list->on_selection_change = [this] {
  31. selected_mailbox();
  32. };
  33. m_individual_mailbox_view->on_selection_change = [this] {
  34. selected_email_to_load();
  35. };
  36. m_web_view->on_link_click = [this](auto& url, auto&, unsigned) {
  37. if (!Desktop::Launcher::open(url)) {
  38. GUI::MessageBox::show(
  39. window(),
  40. String::formatted("The link to '{}' could not be opened.", url),
  41. "Failed to open link",
  42. GUI::MessageBox::Type::Error);
  43. }
  44. };
  45. m_web_view->on_link_middle_click = [this](auto& url, auto& target, unsigned modifiers) {
  46. m_web_view->on_link_click(url, target, modifiers);
  47. };
  48. m_web_view->on_link_hover = [this](auto& url) {
  49. if (url.is_valid())
  50. m_statusbar->set_text(url.to_string());
  51. else
  52. m_statusbar->set_text("");
  53. };
  54. m_link_context_menu = GUI::Menu::construct();
  55. auto link_default_action = GUI::Action::create("&Open in Browser", [this](auto&) {
  56. m_web_view->on_link_click(m_link_context_menu_url, "", 0);
  57. });
  58. m_link_context_menu->add_action(link_default_action);
  59. m_link_context_menu_default_action = link_default_action;
  60. m_link_context_menu->add_separator();
  61. m_link_context_menu->add_action(GUI::Action::create("&Copy URL", [this](auto&) {
  62. GUI::Clipboard::the().set_plain_text(m_link_context_menu_url.to_string());
  63. }));
  64. m_web_view->on_link_context_menu_request = [this](auto& url, auto& screen_position) {
  65. m_link_context_menu_url = url;
  66. m_link_context_menu->popup(screen_position, m_link_context_menu_default_action);
  67. };
  68. m_image_context_menu = GUI::Menu::construct();
  69. m_image_context_menu->add_action(GUI::Action::create("&Copy Image", [this](auto&) {
  70. if (m_image_context_menu_bitmap.is_valid())
  71. GUI::Clipboard::the().set_bitmap(*m_image_context_menu_bitmap.bitmap());
  72. }));
  73. m_image_context_menu->add_action(GUI::Action::create("Copy Image &URL", [this](auto&) {
  74. GUI::Clipboard::the().set_plain_text(m_image_context_menu_url.to_string());
  75. }));
  76. m_image_context_menu->add_separator();
  77. m_image_context_menu->add_action(GUI::Action::create("&Open Image in Browser", [this](auto&) {
  78. m_web_view->on_link_click(m_image_context_menu_url, "", 0);
  79. }));
  80. m_web_view->on_image_context_menu_request = [this](auto& image_url, auto& screen_position, Gfx::ShareableBitmap const& shareable_bitmap) {
  81. m_image_context_menu_url = image_url;
  82. m_image_context_menu_bitmap = shareable_bitmap;
  83. m_image_context_menu->popup(screen_position);
  84. };
  85. }
  86. bool MailWidget::connect_and_login()
  87. {
  88. auto server = Config::read_string("Mail", "Connection", "Server", {});
  89. if (server.is_empty()) {
  90. auto result = GUI::MessageBox::show(window(), "Mail has no servers configured. Do you want configure them now?", "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::YesNo);
  91. if (result == GUI::MessageBox::ExecResult::Yes)
  92. Desktop::Launcher::open(URL::create_with_file_protocol("/bin/MailSettings"), "/bin/MailSettings");
  93. return false;
  94. }
  95. // Assume TLS by default, which is on port 993.
  96. auto port = Config::read_i32("Mail", "Connection", "Port", 993);
  97. auto tls = Config::read_bool("Mail", "Connection", "TLS", true);
  98. auto username = Config::read_string("Mail", "User", "Username", {});
  99. if (username.is_empty()) {
  100. GUI::MessageBox::show_error(window(), "Mail has no username configured. Refer to the Mail(1) man page for more information.");
  101. return false;
  102. }
  103. auto password = Config::read_string("Mail", "User", "Password", {});
  104. while (password.is_empty()) {
  105. if (GUI::PasswordInputDialog::show(window(), password, "Login", server, username) != GUI::Dialog::ExecResult::OK)
  106. return false;
  107. }
  108. auto maybe_imap_client = tls ? IMAP::Client::connect_tls(server, port) : IMAP::Client::connect_plaintext(server, port);
  109. if (maybe_imap_client.is_error()) {
  110. GUI::MessageBox::show_error(window(), String::formatted("Failed to connect to '{}:{}' over {}: {}", server, port, tls ? "TLS" : "Plaintext", maybe_imap_client.error()));
  111. return false;
  112. }
  113. m_imap_client = maybe_imap_client.release_value();
  114. auto connection_promise = m_imap_client->connection_promise();
  115. VERIFY(!connection_promise.is_null());
  116. connection_promise->await();
  117. auto response = m_imap_client->login(username, password)->await().release_value();
  118. if (response.status() != IMAP::ResponseStatus::OK) {
  119. dbgln("Failed to login. The server says: '{}'", response.response_text());
  120. GUI::MessageBox::show_error(window(), String::formatted("Failed to login. The server says: '{}'", response.response_text()));
  121. return false;
  122. }
  123. response = m_imap_client->list("", "*")->await().release_value();
  124. if (response.status() != IMAP::ResponseStatus::OK) {
  125. dbgln("Failed to retrieve mailboxes. The server says: '{}'", response.response_text());
  126. GUI::MessageBox::show_error(window(), String::formatted("Failed to retrieve mailboxes. The server says: '{}'", response.response_text()));
  127. return false;
  128. }
  129. auto& list_items = response.data().list_items();
  130. m_account_holder = AccountHolder::create();
  131. m_account_holder->add_account_with_name_and_mailboxes(username, move(list_items));
  132. m_mailbox_list->set_model(m_account_holder->mailbox_tree_model());
  133. m_mailbox_list->expand_tree();
  134. return true;
  135. }
  136. void MailWidget::on_window_close()
  137. {
  138. auto response = move(m_imap_client->send_simple_command(IMAP::CommandType::Logout)->await().release_value().get<IMAP::SolidResponse>());
  139. VERIFY(response.status() == IMAP::ResponseStatus::OK);
  140. m_imap_client->close();
  141. }
  142. IMAP::MultiPartBodyStructureData const* MailWidget::look_for_alternative_body_structure(IMAP::MultiPartBodyStructureData const& current_body_structure, Vector<u32>& position_stack) const
  143. {
  144. if (current_body_structure.media_type.equals_ignoring_case("ALTERNATIVE"))
  145. return &current_body_structure;
  146. u32 structure_index = 1;
  147. for (auto& structure : current_body_structure.bodies) {
  148. if (structure->data().has<IMAP::BodyStructureData>()) {
  149. ++structure_index;
  150. continue;
  151. }
  152. position_stack.append(structure_index);
  153. auto* potential_alternative_structure = look_for_alternative_body_structure(structure->data().get<IMAP::MultiPartBodyStructureData>(), position_stack);
  154. if (potential_alternative_structure)
  155. return potential_alternative_structure;
  156. position_stack.take_last();
  157. ++structure_index;
  158. }
  159. return nullptr;
  160. }
  161. Vector<MailWidget::Alternative> MailWidget::get_alternatives(IMAP::MultiPartBodyStructureData const& multi_part_body_structure_data) const
  162. {
  163. Vector<u32> position_stack;
  164. auto* alternative_body_structure = look_for_alternative_body_structure(multi_part_body_structure_data, position_stack);
  165. if (!alternative_body_structure)
  166. return {};
  167. Vector<MailWidget::Alternative> alternatives;
  168. alternatives.ensure_capacity(alternative_body_structure->bodies.size());
  169. int alternative_index = 1;
  170. for (auto& alternative_body : alternative_body_structure->bodies) {
  171. VERIFY(alternative_body->data().has<IMAP::BodyStructureData>());
  172. position_stack.append(alternative_index);
  173. MailWidget::Alternative alternative = {
  174. .body_structure = alternative_body->data().get<IMAP::BodyStructureData>(),
  175. .position = position_stack,
  176. };
  177. alternatives.append(alternative);
  178. position_stack.take_last();
  179. ++alternative_index;
  180. }
  181. return alternatives;
  182. }
  183. bool MailWidget::is_supported_alternative(Alternative const& alternative) const
  184. {
  185. return alternative.body_structure.type.equals_ignoring_case("text") && (alternative.body_structure.subtype.equals_ignoring_case("plain") || alternative.body_structure.subtype.equals_ignoring_case("html"));
  186. }
  187. void MailWidget::selected_mailbox()
  188. {
  189. m_individual_mailbox_view->set_model(InboxModel::create({}));
  190. auto const& index = m_mailbox_list->selection().first();
  191. if (!index.is_valid())
  192. return;
  193. auto& base_node = *static_cast<BaseNode*>(index.internal_data());
  194. if (is<AccountNode>(base_node)) {
  195. // FIXME: Do something when clicking on an account node.
  196. return;
  197. }
  198. auto& mailbox_node = verify_cast<MailboxNode>(base_node);
  199. auto& mailbox = mailbox_node.mailbox();
  200. // FIXME: It would be better if we didn't allow the user to click on this mailbox node at all.
  201. if (mailbox.flags & (unsigned)IMAP::MailboxFlag::NoSelect)
  202. return;
  203. auto response = m_imap_client->select(mailbox.name)->await().release_value();
  204. if (response.status() != IMAP::ResponseStatus::OK) {
  205. dbgln("Failed to select mailbox. The server says: '{}'", response.response_text());
  206. GUI::MessageBox::show_error(window(), String::formatted("Failed to select mailbox. The server says: '{}'", response.response_text()));
  207. return;
  208. }
  209. if (response.data().exists() == 0) {
  210. // No mail in this mailbox, return.
  211. return;
  212. }
  213. auto fetch_command = IMAP::FetchCommand {
  214. // Mail will always be numbered from 1 up to the number of mail items that exist, which is specified in the select response with "EXISTS".
  215. .sequence_set = { { 1, (int)response.data().exists() } },
  216. .data_items = {
  217. IMAP::FetchCommand::DataItem {
  218. .type = IMAP::FetchCommand::DataItemType::BodySection,
  219. .section = IMAP::FetchCommand::DataItem::Section {
  220. .type = IMAP::FetchCommand::DataItem::SectionType::HeaderFields,
  221. .headers = { { "Subject", "From" } },
  222. },
  223. },
  224. },
  225. };
  226. auto fetch_response = m_imap_client->fetch(fetch_command, false)->await().release_value();
  227. if (response.status() != IMAP::ResponseStatus::OK) {
  228. dbgln("Failed to retrieve subject/from for e-mails. The server says: '{}'", response.response_text());
  229. GUI::MessageBox::show_error(window(), String::formatted("Failed to retrieve e-mails. The server says: '{}'", response.response_text()));
  230. return;
  231. }
  232. Vector<InboxEntry> active_inbox_entries;
  233. for (auto& fetch_data : fetch_response.data().fetch_data()) {
  234. auto& response_data = fetch_data.get<IMAP::FetchResponseData>();
  235. auto& body_data = response_data.body_data();
  236. auto data_item_has_header = [](IMAP::FetchCommand::DataItem const& data_item, String const& search_header) {
  237. if (!data_item.section.has_value())
  238. return false;
  239. if (data_item.section->type != IMAP::FetchCommand::DataItem::SectionType::HeaderFields)
  240. return false;
  241. if (!data_item.section->headers.has_value())
  242. return false;
  243. auto header_iterator = data_item.section->headers->find_if([&search_header](auto& header) {
  244. return header.equals_ignoring_case(search_header);
  245. });
  246. return header_iterator != data_item.section->headers->end();
  247. };
  248. auto subject_iterator = body_data.find_if([&data_item_has_header](Tuple<IMAP::FetchCommand::DataItem, Optional<String>>& data) {
  249. auto const data_item = data.get<0>();
  250. return data_item_has_header(data_item, "Subject");
  251. });
  252. VERIFY(subject_iterator != body_data.end());
  253. auto from_iterator = body_data.find_if([&data_item_has_header](Tuple<IMAP::FetchCommand::DataItem, Optional<String>>& data) {
  254. auto const data_item = data.get<0>();
  255. return data_item_has_header(data_item, "From");
  256. });
  257. VERIFY(from_iterator != body_data.end());
  258. // FIXME: All of the following doesn't really follow RFC 2822: https://datatracker.ietf.org/doc/html/rfc2822
  259. auto parse_and_unfold = [](String const& value) {
  260. GenericLexer lexer(value);
  261. StringBuilder builder;
  262. // There will be a space at the start of the value, which should be ignored.
  263. VERIFY(lexer.consume_specific(' '));
  264. while (!lexer.is_eof()) {
  265. auto current_line = lexer.consume_while([](char c) {
  266. return c != '\r';
  267. });
  268. builder.append(current_line);
  269. bool consumed_end_of_line = lexer.consume_specific("\r\n");
  270. VERIFY(consumed_end_of_line);
  271. // If CRLF are immediately followed by WSP (which is either ' ' or '\t'), then it is not the end of the header and is instead just a wrap.
  272. // If it's not followed by WSP, then it is the end of the header.
  273. // https://datatracker.ietf.org/doc/html/rfc2822#section-2.2.3
  274. if (lexer.is_eof() || (lexer.peek() != ' ' && lexer.peek() != '\t'))
  275. break;
  276. }
  277. return builder.to_string();
  278. };
  279. auto& subject_iterator_value = subject_iterator->get<1>().value();
  280. auto subject_index = subject_iterator_value.find("Subject:");
  281. String subject;
  282. if (subject_index.has_value()) {
  283. auto potential_subject = subject_iterator_value.substring(subject_index.value());
  284. auto subject_parts = potential_subject.split_limit(':', 2);
  285. subject = parse_and_unfold(subject_parts.last());
  286. }
  287. if (subject.is_empty())
  288. subject = "(no subject)";
  289. auto& from_iterator_value = from_iterator->get<1>().value();
  290. auto from_index = from_iterator_value.find("From:");
  291. VERIFY(from_index.has_value());
  292. auto potential_from = from_iterator_value.substring(from_index.value());
  293. auto from_parts = potential_from.split_limit(':', 2);
  294. auto from = parse_and_unfold(from_parts.last());
  295. InboxEntry inbox_entry { from, subject };
  296. active_inbox_entries.append(inbox_entry);
  297. }
  298. m_individual_mailbox_view->set_model(InboxModel::create(move(active_inbox_entries)));
  299. }
  300. void MailWidget::selected_email_to_load()
  301. {
  302. auto const& index = m_individual_mailbox_view->selection().first();
  303. if (!index.is_valid())
  304. return;
  305. // IMAP is 1-based.
  306. int id_of_email_to_load = index.row() + 1;
  307. auto fetch_command = IMAP::FetchCommand {
  308. .sequence_set = { { id_of_email_to_load, id_of_email_to_load } },
  309. .data_items = {
  310. IMAP::FetchCommand::DataItem {
  311. .type = IMAP::FetchCommand::DataItemType::BodyStructure,
  312. },
  313. },
  314. };
  315. auto fetch_response = m_imap_client->fetch(fetch_command, false)->await().release_value();
  316. if (fetch_response.status() != IMAP::ResponseStatus::OK) {
  317. dbgln("Failed to retrieve the body structure of the selected e-mail. The server says: '{}'", fetch_response.response_text());
  318. GUI::MessageBox::show_error(window(), String::formatted("Failed to retrieve the selected e-mail. The server says: '{}'", fetch_response.response_text()));
  319. return;
  320. }
  321. Vector<u32> selected_alternative_position;
  322. String selected_alternative_encoding;
  323. auto& response_data = fetch_response.data().fetch_data().last().get<IMAP::FetchResponseData>();
  324. response_data.body_structure().data().visit(
  325. [&](IMAP::BodyStructureData const& data) {
  326. // The message will be in the first position.
  327. selected_alternative_position.append(1);
  328. selected_alternative_encoding = data.encoding;
  329. },
  330. [&](IMAP::MultiPartBodyStructureData const& data) {
  331. auto alternatives = get_alternatives(data);
  332. if (alternatives.is_empty()) {
  333. dbgln("No alternatives. The server said: '{}'", fetch_response.response_text());
  334. GUI::MessageBox::show_error(window(), "The server sent no message to display.");
  335. return;
  336. }
  337. // We can choose whichever alternative we want. In general, we should choose the last alternative that know we can display.
  338. // RFC 2046 Section 5.1.4 https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.4
  339. auto chosen_alternative = alternatives.last_matching([this](auto& alternative) {
  340. return is_supported_alternative(alternative);
  341. });
  342. if (!chosen_alternative.has_value()) {
  343. GUI::MessageBox::show(window(), "Displaying this type of e-mail is currently unsupported.", "Unsupported", GUI::MessageBox::Type::Information);
  344. return;
  345. }
  346. selected_alternative_position = chosen_alternative->position;
  347. selected_alternative_encoding = chosen_alternative->body_structure.encoding;
  348. });
  349. if (selected_alternative_position.is_empty()) {
  350. // An error occurred above, return.
  351. return;
  352. }
  353. fetch_command = IMAP::FetchCommand {
  354. .sequence_set { { id_of_email_to_load, id_of_email_to_load } },
  355. .data_items = {
  356. IMAP::FetchCommand::DataItem {
  357. .type = IMAP::FetchCommand::DataItemType::BodySection,
  358. .section = IMAP::FetchCommand::DataItem::Section {
  359. .type = IMAP::FetchCommand::DataItem::SectionType::Parts,
  360. .parts = selected_alternative_position,
  361. },
  362. .partial_fetch = false,
  363. },
  364. },
  365. };
  366. fetch_response = m_imap_client->fetch(fetch_command, false)->await().release_value();
  367. if (fetch_response.status() != IMAP::ResponseStatus::OK) {
  368. dbgln("Failed to retrieve the body of the selected e-mail. The server says: '{}'", fetch_response.response_text());
  369. GUI::MessageBox::show_error(window(), String::formatted("Failed to retrieve the selected e-mail. The server says: '{}'", fetch_response.response_text()));
  370. return;
  371. }
  372. auto& fetch_data = fetch_response.data().fetch_data();
  373. if (fetch_data.is_empty()) {
  374. dbgln("The server sent no fetch data.");
  375. GUI::MessageBox::show_error(window(), "The server sent no data.");
  376. return;
  377. }
  378. auto& fetch_response_data = fetch_data.last().get<IMAP::FetchResponseData>();
  379. if (!fetch_response_data.contains_response_type(IMAP::FetchResponseType::Body)) {
  380. GUI::MessageBox::show_error(window(), "The server sent no body.");
  381. return;
  382. }
  383. auto& body_data = fetch_response_data.body_data();
  384. auto body_text_part_iterator = body_data.find_if([](Tuple<IMAP::FetchCommand::DataItem, Optional<String>>& data) {
  385. const auto data_item = data.get<0>();
  386. return data_item.section.has_value() && data_item.section->type == IMAP::FetchCommand::DataItem::SectionType::Parts;
  387. });
  388. VERIFY(body_text_part_iterator != body_data.end());
  389. auto& encoded_data = body_text_part_iterator->get<1>().value();
  390. String decoded_data;
  391. // FIXME: String uses char internally, so 8bit shouldn't be stored in it.
  392. // However, it works for now.
  393. if (selected_alternative_encoding.equals_ignoring_case("7bit") || selected_alternative_encoding.equals_ignoring_case("8bit")) {
  394. decoded_data = encoded_data;
  395. } else if (selected_alternative_encoding.equals_ignoring_case("base64")) {
  396. auto decoded_base64 = decode_base64(encoded_data);
  397. if (!decoded_base64.is_error())
  398. decoded_data = decoded_base64.release_value();
  399. } else if (selected_alternative_encoding.equals_ignoring_case("quoted-printable")) {
  400. decoded_data = IMAP::decode_quoted_printable(encoded_data);
  401. } else {
  402. dbgln("Mail: Unimplemented decoder for encoding: {}", selected_alternative_encoding);
  403. GUI::MessageBox::show(window(), String::formatted("The e-mail encoding '{}' is currently unsupported.", selected_alternative_encoding), "Unsupported", GUI::MessageBox::Type::Information);
  404. return;
  405. }
  406. // FIXME: I'm not sure what the URL should be. Just use the default URL "about:blank".
  407. // FIXME: It would be nice if we could pass over the charset.
  408. m_web_view->load_html(decoded_data, "about:blank");
  409. }