mirror of
https://github.com/SerenityOS/serenity
synced 2026-04-25 17:15:42 +02:00
LibIMAP+Mail: Show unseen message count for mailboxes
This PR implements the standard behavior of displaying the mailbox name and parenthesized unseen message count in bold when the unseen message count is greater than zero.
This commit is contained in:
@@ -74,3 +74,25 @@ void AccountHolder::rebuild_tree()
|
||||
{
|
||||
m_mailbox_tree_model->invalidate();
|
||||
}
|
||||
|
||||
void MailboxNode::update_display_name_with_unseen_count()
|
||||
{
|
||||
m_display_name_with_unseen_count = ByteString::formatted("{} ({})", m_display_name, m_unseen_count);
|
||||
}
|
||||
|
||||
void MailboxNode::decrement_unseen_count()
|
||||
{
|
||||
if (m_unseen_count)
|
||||
set_unseen_count(m_unseen_count - 1);
|
||||
}
|
||||
|
||||
void MailboxNode::increment_unseen_count()
|
||||
{
|
||||
set_unseen_count(m_unseen_count + 1);
|
||||
}
|
||||
|
||||
void MailboxNode::set_unseen_count(unsigned unseen_count)
|
||||
{
|
||||
m_unseen_count = unseen_count;
|
||||
update_display_name_with_unseen_count();
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public:
|
||||
AccountNode const& associated_account() const { return m_associated_account; }
|
||||
ByteString const& select_name() const { return m_mailbox.name; }
|
||||
ByteString const& display_name() const { return m_display_name; }
|
||||
ByteString const& display_name_with_unseen_count() const { return m_display_name_with_unseen_count; }
|
||||
IMAP::ListItem const& mailbox() const { return m_mailbox; }
|
||||
|
||||
bool has_parent() const { return m_parent; }
|
||||
@@ -68,6 +69,11 @@ public:
|
||||
Vector<NonnullRefPtr<MailboxNode>> const& children() const { return m_children; }
|
||||
void add_child(NonnullRefPtr<MailboxNode> child) { m_children.append(child); }
|
||||
|
||||
unsigned unseen_count() const { return m_unseen_count; }
|
||||
void decrement_unseen_count();
|
||||
void increment_unseen_count();
|
||||
void set_unseen_count(unsigned unseen_count);
|
||||
|
||||
private:
|
||||
MailboxNode(AccountNode const& associated_account, IMAP::ListItem const& mailbox, ByteString display_name)
|
||||
: m_associated_account(associated_account)
|
||||
@@ -76,9 +82,13 @@ private:
|
||||
{
|
||||
}
|
||||
|
||||
void update_display_name_with_unseen_count();
|
||||
|
||||
AccountNode const& m_associated_account;
|
||||
IMAP::ListItem m_mailbox;
|
||||
ByteString m_display_name;
|
||||
ByteString m_display_name_with_unseen_count;
|
||||
unsigned m_unseen_count;
|
||||
|
||||
Vector<NonnullRefPtr<MailboxNode>> m_children;
|
||||
RefPtr<MailboxNode> m_parent;
|
||||
|
||||
@@ -13,9 +13,14 @@ InboxModel::InboxModel(Vector<InboxEntry> entries)
|
||||
{
|
||||
}
|
||||
|
||||
void InboxModel::set_seen(int row, bool seen)
|
||||
MailStatus InboxModel::mail_status(int row)
|
||||
{
|
||||
m_entries[row].seen = seen;
|
||||
return m_entries[row].status;
|
||||
}
|
||||
|
||||
void InboxModel::set_mail_status(int row, MailStatus status)
|
||||
{
|
||||
m_entries[row].status = status;
|
||||
did_update(DontInvalidateIndices);
|
||||
}
|
||||
|
||||
@@ -53,10 +58,8 @@ GUI::Variant InboxModel::data(GUI::ModelIndex const& index, GUI::ModelRole role)
|
||||
if (index.column() == Column::Date)
|
||||
return Gfx::TextAlignment::CenterRight;
|
||||
}
|
||||
if (role == GUI::ModelRole::Font) {
|
||||
if (!value.seen)
|
||||
return Gfx::FontDatabase::default_font().bold_variant();
|
||||
}
|
||||
if (role == GUI::ModelRole::Font && value.status == MailStatus::Unseen)
|
||||
return Gfx::FontDatabase::default_font().bold_variant();
|
||||
if (role == static_cast<GUI::ModelRole>(InboxModelCustomRole::Sequence))
|
||||
return value.sequence_number;
|
||||
return {};
|
||||
|
||||
@@ -10,12 +10,17 @@
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibIMAP/Objects.h>
|
||||
|
||||
enum class MailStatus {
|
||||
Unseen,
|
||||
Seen,
|
||||
};
|
||||
|
||||
struct InboxEntry {
|
||||
u32 sequence_number;
|
||||
ByteString date;
|
||||
ByteString from;
|
||||
ByteString subject;
|
||||
bool seen;
|
||||
MailStatus status;
|
||||
};
|
||||
|
||||
enum class InboxModelCustomRole {
|
||||
@@ -39,7 +44,8 @@ public:
|
||||
|
||||
virtual ~InboxModel() override = default;
|
||||
|
||||
void set_seen(int row, bool);
|
||||
MailStatus mail_status(int row);
|
||||
void set_mail_status(int row, MailStatus status);
|
||||
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include "MailWidget.h"
|
||||
#include "InboxModel.h"
|
||||
#include <AK/Base64.h>
|
||||
#include <AK/GenericLexer.h>
|
||||
#include <Applications/Mail/MailWindowGML.h>
|
||||
@@ -100,6 +101,31 @@ MailWidget::MailWidget()
|
||||
};
|
||||
}
|
||||
|
||||
MailboxNode* MailWidget::get_mailbox_by_name(ByteString const& username, ByteString const& mailbox_name)
|
||||
{
|
||||
for (auto& account : m_account_holder->accounts()) {
|
||||
if (account->name() == username) {
|
||||
for (auto& mailbox : account->mailboxes()) {
|
||||
if (mailbox->select_name() == mailbox_name)
|
||||
return mailbox;
|
||||
}
|
||||
}
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
ErrorOr<void> MailWidget::refresh_unseen_count_for_mailbox(MailboxNode* mailbox)
|
||||
{
|
||||
auto response = TRY(m_imap_client->status(mailbox->select_name(), { IMAP::StatusItemType::Unseen, IMAP::StatusItemType::Messages })->await());
|
||||
if (response.status() != IMAP::ResponseStatus::OK) {
|
||||
dbgln("Failed to get mailbox status. The server says: '{}'", response.response_text());
|
||||
return {};
|
||||
}
|
||||
if (response.data().status_items().size() > 0)
|
||||
mailbox->set_unseen_count(response.data().status_items()[0].get(IMAP::StatusItemType::Unseen));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<bool> MailWidget::connect_and_login()
|
||||
{
|
||||
auto server = Config::read_string("Mail"sv, "Connection"sv, "Server"sv, {});
|
||||
@@ -150,7 +176,7 @@ ErrorOr<bool> MailWidget::connect_and_login()
|
||||
}
|
||||
|
||||
m_statusbar->set_text("Logged in. Loading mailboxes..."_string);
|
||||
response = TRY(m_imap_client->list(""sv, "*"sv)->await());
|
||||
response = TRY(m_imap_client->list(""sv, "*"sv, true)->await());
|
||||
|
||||
if (response.status() != IMAP::ResponseStatus::OK) {
|
||||
dbgln("Failed to retrieve mailboxes. The server says: '{}'", response.response_text());
|
||||
@@ -160,14 +186,21 @@ ErrorOr<bool> MailWidget::connect_and_login()
|
||||
|
||||
auto& list_items = response.data().list_items();
|
||||
|
||||
m_statusbar->set_text(MUST(String::formatted("Loaded {} mailboxes", list_items.size())));
|
||||
|
||||
m_account_holder = AccountHolder::create();
|
||||
m_account_holder->add_account_with_name_and_mailboxes(username, move(list_items));
|
||||
|
||||
m_statusbar->set_text(MUST(String::formatted("Loaded {} mailboxes", list_items.size())));
|
||||
|
||||
m_mailbox_list->set_model(m_account_holder->mailbox_tree_model());
|
||||
m_mailbox_list->expand_tree();
|
||||
|
||||
auto& status_items = response.data().status_items();
|
||||
|
||||
for (auto& status_item : status_items) {
|
||||
auto mailbox = get_mailbox_by_name(username, status_item.mailbox());
|
||||
mailbox->set_unseen_count(status_item.get(IMAP::StatusItemType::Unseen));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -263,6 +296,8 @@ void MailWidget::selected_mailbox(GUI::ModelIndex const& index)
|
||||
auto& mailbox_node = verify_cast<MailboxNode>(base_node);
|
||||
auto& mailbox = mailbox_node.mailbox();
|
||||
|
||||
m_selected_mailbox_node = mailbox_node;
|
||||
|
||||
// FIXME: It would be better if we didn't allow the user to click on this mailbox node at all.
|
||||
if (mailbox.flags & (unsigned)IMAP::MailboxFlag::NoSelect)
|
||||
return;
|
||||
@@ -350,12 +385,14 @@ void MailWidget::selected_mailbox(GUI::ModelIndex const& index)
|
||||
}
|
||||
ByteString from = sender_builder.to_byte_string();
|
||||
|
||||
InboxEntry inbox_entry { sequence_number, date, from, subject, seen };
|
||||
InboxEntry inbox_entry { sequence_number, date, from, subject, seen ? MailStatus::Seen : MailStatus::Unseen };
|
||||
m_statusbar->set_text(String::formatted("[{}]: Loading entry {}", mailbox.name, ++i).release_value_but_fixme_should_propagate_errors());
|
||||
|
||||
active_inbox_entries.append(inbox_entry);
|
||||
}
|
||||
|
||||
(void)refresh_unseen_count_for_mailbox(m_selected_mailbox_node);
|
||||
|
||||
m_statusbar->set_text(String::formatted("[{}]: Loaded {} entries", mailbox.name, i).release_value_but_fixme_should_propagate_errors());
|
||||
m_mailbox_model = InboxModel::create(move(active_inbox_entries));
|
||||
m_mailbox_sorting_model = MUST(GUI::SortingProxyModel::create(*m_mailbox_model));
|
||||
@@ -467,7 +504,11 @@ void MailWidget::selected_email_to_load(GUI::ModelIndex const& index)
|
||||
auto& fetch_response_data = fetch_data.last().get<IMAP::FetchResponseData>();
|
||||
|
||||
auto seen = !fetch_response_data.flags().find_if([](StringView value) { return value.equals_ignoring_ascii_case("\\Seen"sv); }).is_end();
|
||||
m_mailbox_model->set_seen(index.row(), seen);
|
||||
if (m_mailbox_model->mail_status(index.row()) != (seen ? MailStatus::Seen : MailStatus::Unseen)) {
|
||||
seen ? m_selected_mailbox_node->decrement_unseen_count() : m_selected_mailbox_node->increment_unseen_count();
|
||||
m_mailbox_list->repaint();
|
||||
}
|
||||
m_mailbox_model->set_mail_status(index.row(), seen ? MailStatus::Seen : MailStatus::Unseen);
|
||||
|
||||
if (!fetch_response_data.contains_response_type(IMAP::FetchResponseType::Body)) {
|
||||
GUI::MessageBox::show_error(window(), "The server sent no body."sv);
|
||||
|
||||
@@ -20,12 +20,14 @@ public:
|
||||
virtual ~MailWidget() override = default;
|
||||
|
||||
ErrorOr<bool> connect_and_login();
|
||||
ErrorOr<void> refresh_unseen_count_for_mailbox(MailboxNode* mailbox);
|
||||
|
||||
void on_window_close();
|
||||
|
||||
private:
|
||||
MailWidget();
|
||||
|
||||
MailboxNode* get_mailbox_by_name(ByteString const& username, ByteString const& mailbox_name);
|
||||
void selected_mailbox(GUI::ModelIndex const&);
|
||||
void selected_email_to_load(GUI::ModelIndex const&);
|
||||
|
||||
@@ -46,6 +48,7 @@ private:
|
||||
RefPtr<GUI::SortingProxyModel> m_mailbox_sorting_model;
|
||||
RefPtr<GUI::TableView> m_individual_mailbox_view;
|
||||
RefPtr<WebView::OutOfProcessWebView> m_web_view;
|
||||
RefPtr<MailboxNode> m_selected_mailbox_node;
|
||||
RefPtr<GUI::Statusbar> m_statusbar;
|
||||
|
||||
RefPtr<GUI::Menu> m_link_context_menu;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "MailboxTreeModel.h"
|
||||
#include "AccountHolder.h"
|
||||
#include <LibGfx/Font/FontDatabase.h>
|
||||
|
||||
MailboxTreeModel::MailboxTreeModel(AccountHolder const& account_holder)
|
||||
: m_account_holder(account_holder)
|
||||
@@ -95,7 +96,13 @@ GUI::Variant MailboxTreeModel::data(GUI::ModelIndex const& index, GUI::ModelRole
|
||||
}
|
||||
|
||||
auto& mailbox_node = verify_cast<MailboxNode>(base_node);
|
||||
return mailbox_node.display_name();
|
||||
return mailbox_node.unseen_count() ? mailbox_node.display_name_with_unseen_count() : mailbox_node.display_name();
|
||||
}
|
||||
|
||||
if (role == GUI::ModelRole::Font && is<MailboxNode>(base_node)) {
|
||||
auto& mailbox_node = verify_cast<MailboxNode>(base_node);
|
||||
if (mailbox_node.unseen_count())
|
||||
return Gfx::FontDatabase::default_font().bold_variant();
|
||||
}
|
||||
|
||||
if (role == GUI::ModelRole::Icon) {
|
||||
|
||||
@@ -204,11 +204,12 @@ NonnullRefPtr<Promise<SolidResponse>> Client::login(StringView username, StringV
|
||||
return cast_promise<SolidResponse>(send_command(move(command)));
|
||||
}
|
||||
|
||||
NonnullRefPtr<Promise<SolidResponse>> Client::list(StringView reference_name, StringView mailbox)
|
||||
NonnullRefPtr<Promise<SolidResponse>> Client::list(StringView reference_name, StringView mailbox, bool unseen)
|
||||
{
|
||||
auto command = Command { CommandType::List, m_current_command,
|
||||
{ ByteString::formatted("\"{}\"", reference_name),
|
||||
ByteString::formatted("\"{}\"", mailbox) } };
|
||||
ByteString::formatted("\"{}\"", mailbox),
|
||||
unseen ? "RETURN (STATUS (UNSEEN))" : "" } };
|
||||
return cast_promise<SolidResponse>(send_command(move(command)));
|
||||
}
|
||||
|
||||
@@ -276,8 +277,10 @@ ErrorOr<void> Client::send_next_command()
|
||||
buffer.append(command_type.data(), command_type.size());
|
||||
|
||||
for (auto& arg : command.args) {
|
||||
buffer.append(" ", 1);
|
||||
buffer.append(arg.bytes().data(), arg.length());
|
||||
if (arg.length()) {
|
||||
buffer.append(" ", 1);
|
||||
buffer.append(arg.bytes().data(), arg.length());
|
||||
}
|
||||
}
|
||||
|
||||
TRY(send_raw(buffer));
|
||||
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
NonnullRefPtr<Promise<Response>> send_simple_command(CommandType);
|
||||
ErrorOr<void> send_raw(StringView data);
|
||||
NonnullRefPtr<Promise<SolidResponse>> login(StringView username, StringView password);
|
||||
NonnullRefPtr<Promise<SolidResponse>> list(StringView reference_name, StringView mailbox_name);
|
||||
NonnullRefPtr<Promise<SolidResponse>> list(StringView reference_name, StringView mailbox_name, bool unseen = false);
|
||||
NonnullRefPtr<Promise<SolidResponse>> lsub(StringView reference_name, StringView mailbox_name);
|
||||
NonnullRefPtr<Promise<SolidResponse>> select(StringView string);
|
||||
NonnullRefPtr<Promise<SolidResponse>> examine(StringView string);
|
||||
|
||||
@@ -685,15 +685,16 @@ public:
|
||||
return m_bye_message;
|
||||
}
|
||||
|
||||
void set_status(StatusItem&& status_item)
|
||||
void add_status_item(StatusItem&& item)
|
||||
{
|
||||
add_response_type(ResponseType::Status);
|
||||
m_status_item = move(status_item);
|
||||
m_status_items.append(move(item));
|
||||
}
|
||||
|
||||
StatusItem& status_item()
|
||||
Vector<StatusItem>& status_items()
|
||||
{
|
||||
return m_status_item;
|
||||
VERIFY(contains_response_type(ResponseType::Status));
|
||||
return m_status_items;
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -702,6 +703,7 @@ private:
|
||||
Vector<ByteString> m_capabilities;
|
||||
Vector<ListItem> m_list_items;
|
||||
Vector<ListItem> m_lsub_items;
|
||||
Vector<StatusItem> m_status_items;
|
||||
Vector<unsigned> m_expunged;
|
||||
|
||||
unsigned m_recent {};
|
||||
@@ -715,7 +717,6 @@ private:
|
||||
Vector<Tuple<unsigned, FetchResponseData>> m_fetch_responses;
|
||||
Vector<unsigned> m_search_results;
|
||||
Optional<ByteString> m_bye_message;
|
||||
StatusItem m_status_item;
|
||||
};
|
||||
|
||||
enum class StoreMethod {
|
||||
|
||||
@@ -255,7 +255,7 @@ ErrorOr<void> Parser::parse_untagged()
|
||||
if (!at_end() && m_buffer[m_position] != ')')
|
||||
TRY(consume(" "sv));
|
||||
}
|
||||
m_response.data().set_status(move(status_item));
|
||||
m_response.data().add_status_item(move(status_item));
|
||||
consume_if(" "sv); // Not in the spec but the Outlook server sends a space for some reason.
|
||||
TRY(consume("\r\n"sv));
|
||||
} else {
|
||||
|
||||
@@ -86,7 +86,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||
outln("[SEARCH] Number of results: {}", search_results.size());
|
||||
|
||||
response = TRY(client->status("INBOX"sv, { IMAP::StatusItemType::Recent, IMAP::StatusItemType::Messages })->await());
|
||||
outln("[STATUS] Recent items: {}", response.data().status_item().get(IMAP::StatusItemType::Recent));
|
||||
if (response.data().status_items().size() > 0)
|
||||
outln("[STATUS] Recent items: {}", response.data().status_items()[0].get(IMAP::StatusItemType::Recent));
|
||||
|
||||
for (auto item : search_results) {
|
||||
// clang-format off
|
||||
|
||||
Reference in New Issue
Block a user