MailWidget.cpp 24 KB

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