Mail app: Implement labeling emails

"Labels" (MAIL:label) will be used instead of the currently misused
"Status" (MAIL:status) to allow categorization of emails by the
user. These labels are only used on the locally download emails;
there's no syncing with a mail server.

(See also forum thread "Introducing a new indexed attribute META:tag" at
https://discuss.haiku-os.org/t/introducing-a-new-indexed-attribute-meta-tag.)

* Remove custom statuses from "Close and…" submenu and anything else
  concerning custom statuses.

* Add a "Set label" menu with a submenu to
  - Remove the current mail's label, if it has one
  - Add a new label
  - Choose from a list of labels (first 9 items with ALT+n shortcut)
  - Open the labels folder in Tracker to manage labels
    (delete/rename/etc.)

* Labels are saved as separate files in ~/config/settings/Mail/labels/

* Label files are queries for all files which have that particular
  label in their MAIL:label attribute. This is convenient when
  managing your label, e.g. see if there are any emails with a
  certain label before deleting it.

* Added "Same label" to the "Queries" menu.

* In MenusBeginning() the files in ~/config/settings/Mail/labels/ are
  read and shown with their file name as menu item.

* When an email is opened, its label file is created if it doesn't
  exist already.  That way the Mail app can provide labels that were
  not created with it, but  e.g. by manually filling the attribute in
  Tracker, or via a script etc.

* Related: When populating the labels menu, make sure each label query
  has the correct formula. A user may have created a label by hand or
  duplicated an existing formula, in which case the query formula would
  be missing or wrong.

Setting a label currently isn't supported when composing an email, as
we cannot set an attribute, because an email file isn't created until
it's sent.

Change-Id: I53fb4613ec1d2d96493169e0310c11c5ba0131c2
Reviewed-on: https://review.haiku-os.org/c/haiku/+/10384
Tested-by: Commit checker robot <[email protected]>
Reviewed-by: humdinger humdinger <[email protected]>
This commit is contained in:
Humdinger
2026-03-17 12:52:32 +00:00
committed by humdinger humdinger
parent 640b37b32d
commit e53d371901
9 changed files with 354 additions and 48 deletions
+2 -2
View File
@@ -16,6 +16,7 @@ Application Mail :
Enclosures.cpp
FindWindow.cpp
Header.cpp
LabelWindow.cpp
MailApp.cpp
MailPopUpMenu.cpp
MailSupport.cpp
@@ -26,7 +27,6 @@ Application Mail :
QueryList.cpp
QueryMenu.cpp
Signature.cpp
Status.cpp
Utilities.cpp
WIndex.cpp
Words.cpp
@@ -50,6 +50,7 @@ DoCatalogs Mail :
Enclosures.cpp
FindWindow.cpp
Header.cpp
LabelWindow.cpp
LocalizedFolders.h
MailApp.cpp
MailPopUpMenu.cpp
@@ -57,5 +58,4 @@ DoCatalogs Mail :
Prefs.cpp
Settings.cpp
Signature.cpp
Status.cpp
;
+91
View File
@@ -0,0 +1,91 @@
/*
* Copyright 2026 Haiku, Inc. All Rights Reserved.
* Distributed under the terms of the MIT License.
*/
#include "LabelWindow.h"
#include <Button.h>
#include <Catalog.h>
#include <LayoutBuilder.h>
#include <TextControl.h>
#include "MailSupport.h"
#include "Messages.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "LabelWindow"
enum _messages {
kLabel = 128,
kOK,
kCancel
};
TLabelWindow::TLabelWindow(BRect frame, BMessenger target)
: BWindow(BRect(), "", B_MODAL_WINDOW, B_NOT_RESIZABLE | B_AUTO_UPDATE_SIZE_LIMITS),
fTarget(target)
{
fLabel = new BTextControl("label", B_TRANSLATE("New label:"), "", NULL);
fLabel->SetModificationMessage(new BMessage(kLabel));
// disallow control characters and "/"
for (uint32 i = 0; i < 0x20; ++i)
fLabel->TextView()->DisallowChar(i);
fLabel->TextView()->DisallowChar('/');
fOK = new BButton("ok", B_TRANSLATE("OK"), new BMessage(kOK));
fOK->MakeDefault(true);
fOK->SetEnabled(false);
BButton *cancel = new BButton("cancel", B_TRANSLATE("Cancel"), new BMessage(kCancel));
BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
.SetInsets(B_USE_DEFAULT_SPACING)
.AddGroup(B_HORIZONTAL, 0)
.Add(fLabel)
.End()
.AddStrut(B_USE_SMALL_SPACING)
.AddGroup(B_HORIZONTAL, B_USE_SMALL_SPACING, 0)
.AddStrut(B_USE_SMALL_SPACING)
.Add(cancel)
.Add(fOK);
fLabel->BTextControl::MakeFocus(true);
CenterIn(frame);
Show();
}
TLabelWindow::~TLabelWindow()
{
}
void
TLabelWindow::MessageReceived(BMessage* msg)
{
switch (msg->what) {
case kLabel:
fOK->SetEnabled(true);
break;
case kOK:
{
const char* newLabel = fLabel->Text();
if (create_label_file(newLabel) == B_OK) {
BMessage message(M_SET_LABEL);
message.AddString("label", newLabel);
fTarget.SendMessage(&message);
}
// intentional fall-through
}
case kCancel:
Quit();
break;
}
}
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright 2026 Haiku, Inc. All Rights Reserved.
* Distributed under the terms of the MIT License.
*/
#ifndef _LABELWINDOW_H
#define _LABELWINDOW_H
#include <Messenger.h>
#include <Window.h>
class BTextControl;
class TLabelWindow : public BWindow {
public:
TLabelWindow(BRect frame, BMessenger target);
virtual ~TLabelWindow();
protected:
virtual void MessageReceived(BMessage* message);
private:
BButton* fOK;
BMessenger fTarget;
BTextControl* fLabel;
};
#endif // #ifndef _LABELWINDOW_H
+2 -3
View File
@@ -76,13 +76,13 @@ using namespace BPrivate ;
#include "FieldMsg.h"
#include "FindWindow.h"
#include "Header.h"
#include "LabelWindow.h"
#include "MailSupport.h"
#include "MailWindow.h"
#include "Messages.h"
#include "Prefs.h"
#include "QueryMenu.h"
#include "Signature.h"
#include "Status.h"
#include "String.h"
#include "Utilities.h"
#include "Words.h"
@@ -470,7 +470,7 @@ void
TMailApp::ReadyToRun()
{
// Create needed indices for META:group, META:email, MAIL:draft,
// INDEX_SIGNATURE, INDEX_STATUS on the boot volume
// INDEX_SIGNATURE on the boot volume
BVolume volume;
BVolumeRoster().GetBootVolume(&volume);
@@ -479,7 +479,6 @@ TMailApp::ReadyToRun()
fs_create_index(volume.Device(), "META:email", B_STRING_TYPE, 0);
fs_create_index(volume.Device(), "MAIL:draft", B_INT32_TYPE, 0);
fs_create_index(volume.Device(), INDEX_SIGNATURE, B_STRING_TYPE, 0);
fs_create_index(volume.Device(), INDEX_STATUS, B_STRING_TYPE, 0);
fs_create_index(volume.Device(), B_MAIL_ATTR_FLAGS, B_INT32_TYPE, 0);
// Start people queries
+85
View File
@@ -119,6 +119,91 @@ header_len(BFile *file)
}
status_t
create_label_file(const char* label)
{
status_t result;
BPath path;
find_directory(B_USER_SETTINGS_DIRECTORY, &path, true);
path.Append("Mail");
path.Append("labels");
if (create_directory(path.Path(), 777) != B_OK)
return B_ERROR;
path.Append(label);
BFile file(path.Path(), B_READ_WRITE | B_CREATE_FILE);
if (file.InitCheck() != B_OK)
return B_ERROR;
BString string;
string.SetToFormat(B_MAIL_ATTR_LABEL "==\"%s\"", label);
file.WriteAttrString("_trk/qrystr", &string);
string = "E-mail";
file.WriteAttrString("_trk/qryinitmime", &string);
result = BNodeInfo(&file).SetType("application/x-vnd.Be-query");
return result;
}
int32
add_folder_menu_items(BMenu* menu, const char* folder, uint32 what)
{
BPath path;
BPath label_path;
BDirectory directory;
BEntry entry;
if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK)
return B_ERROR;
path.Append("Mail");
path.Append(folder);
directory.SetTo(path.Path());
if (directory.InitCheck() != B_OK)
return B_ERROR;
// skip existing menu items
int32 index = menu->CountItems();
int32 skipItems = index;
while (directory.GetNextEntry(&entry, true) == B_OK) {
if (entry.InitCheck() != B_OK)
break;
entry.GetPath(&label_path);
const char* label = label_path.Leaf();
// Make sure the label file has the correct query formula,
// in case it wasn't created with the Mail app
BFile file(label_path.Path(), B_READ_WRITE);
if (file.InitCheck() != B_OK)
continue;
BString string;
BString formula;
formula.SetToFormat(B_MAIL_ATTR_LABEL "==\"%s\"", label);
file.ReadAttrString("_trk/qrystr", &string);
if (string != formula) {
file.WriteAttrString("_trk/qrystr", &formula);
string = "E-mail";
file.WriteAttrString("_trk/qryinitmime", &string);
BNodeInfo(&file).SetType("application/x-vnd.Be-query");
}
BMessage* message = new BMessage(what);
message->AddString("label", label);
if (index < 9 + skipItems)
menu->AddItem(new BMenuItem(label, message, '1' + index - skipItems), index);
else
menu->AddItem(new BMenuItem(label, message), index);
index++;
}
return index;
}
int32
add_query_menu_items(BMenu* menu, const char* attribute, uint32 what,
const char* format, bool popup)
+2
View File
@@ -63,6 +63,8 @@ class Words;
int32 header_len(BFile*);
status_t create_label_file(const char* label);
int32 add_folder_menu_items(BMenu* menu, const char* folder, uint32 what);
int32 add_query_menu_items(BMenu* menu, const char* attribute, uint32 what,
const char* format, bool popup = false);
+134 -40
View File
@@ -88,6 +88,7 @@ of their respective holders. All rights reserved.
#include "FieldMsg.h"
#include "FindWindow.h"
#include "Header.h"
#include "LabelWindow.h"
#include "Messages.h"
#include "MailApp.h"
#include "MailPopUpMenu.h"
@@ -96,7 +97,6 @@ of their respective holders. All rights reserved.
#include "QueryMenu.h"
#include "Signature.h"
#include "Settings.h"
#include "Status.h"
#include "String.h"
#include "Utilities.h"
@@ -151,6 +151,7 @@ static const uint32 kByForumlaItem = 'Fbyq';
// taken from src/kits/tracker/FindPanel.h
static const int kCopyBufferSize = 64 * 1024; // 64 KB
static const char* kSameLabelItem = B_TRANSLATE("Same label");
static const char* kSameRecipientItem = B_TRANSLATE("Same recipient");
static const char* kSameSenderItem = B_TRANSLATE("Same sender");
static const char* kSameSubjectItem = B_TRANSLATE("Same subject");
@@ -195,8 +196,10 @@ TMailWindow::TMailWindow(BRect rect, const char* title, TMailApp* app,
fFieldState(0),
fPanel(NULL),
fSaveAddrMenu(NULL),
fLabelMenu(NULL),
fLeaveStatusMenu(NULL),
fEncodingMenu(NULL),
fLabel(NULL),
fZoom(rect),
fEnclosuresView(NULL),
fPrevTrackerPositionSaved(false),
@@ -295,26 +298,7 @@ TMailWindow::TMailWindow(BRect rect, const char* title, TMailApp* app,
AddShortcut('T', B_SHIFT_KEY | B_COMMAND_KEY,
new BMessage(M_DELETE_NEXT));
subMenu->AddSeparatorItem();
subMenu->AddItem(new BMenuItem(B_TRANSLATE("Set to Saved"),
new BMessage(M_CLOSE_SAVED), 'W', B_CONTROL_KEY));
if (add_query_menu_items(subMenu, INDEX_STATUS, M_STATUS,
B_TRANSLATE("Set to %s")) > 0)
subMenu->AddSeparatorItem();
subMenu->AddItem(new BMenuItem(B_TRANSLATE("Set to" B_UTF8_ELLIPSIS),
new BMessage(M_CLOSE_CUSTOM)));
#if 0
subMenu->AddItem(new BMenuItem(new TMenu(
B_TRANSLATE("Set to" B_UTF8_ELLIPSIS), INDEX_STATUS, M_STATUS,
false, false),
new BMessage(M_CLOSE_CUSTOM)));
#endif
menu->AddItem(subMenu);
fLeaveStatusMenu = subMenu;
} else {
menu->AddSeparatorItem();
@@ -322,6 +306,14 @@ TMailWindow::TMailWindow(BRect rect, const char* title, TMailApp* app,
new BMessage(B_CLOSE_REQUESTED), 'W'));
}
file.ReadAttrString(B_MAIL_ATTR_LABEL, &fLabel);
// Add label file for current mail's label, in case it doesn't exist yet.
if (fLabel.Length() > 0)
create_label_file(fLabel);
fLabelMenu = new BMenu(B_TRANSLATE("Set label"));
menu->AddItem(fLabelMenu);
menu->AddSeparatorItem();
menu->AddItem(fPrint = new BMenuItem(
B_TRANSLATE("Page setup" B_UTF8_ELLIPSIS),
@@ -1061,6 +1053,47 @@ TMailWindow::MenusBeginning()
LeaveStatus->SetLabel(label.String());
}
}
// Populate label menu
if (fRef != NULL) {
BFile file(fRef, B_READ_ONLY);
BString currentLabel;
file.ReadAttrString(B_MAIL_ATTR_LABEL, &currentLabel);
BMenuItem* menuItem = new BMenuItem(B_TRANSLATE("Remove label"),
new BMessage(M_REMOVE_LABEL));
fLabelMenu->AddItem(menuItem);
fLabelMenu->AddItem(new BMenuItem(B_TRANSLATE("Label as" B_UTF8_ELLIPSIS),
new BMessage(M_NEW_LABEL)));
fLabelMenu->AddSeparatorItem();
int32 count = fLabelMenu->CountItems();
if (add_folder_menu_items(fLabelMenu, "labels", M_SET_LABEL) > count)
fLabelMenu->AddSeparatorItem();
fLabelMenu->AddItem(new BMenuItem(B_TRANSLATE("Manage labels" B_UTF8_ELLIPSIS),
new BMessage(M_MANAGE_LABELS)));
if (currentLabel.Length() == 0)
menuItem->SetEnabled(false);
else {
menuItem = fLabelMenu->FindItem(currentLabel);
if (menuItem != NULL)
menuItem->SetMarked(true);
}
} else
fLabelMenu->SetEnabled(false);
}
void
TMailWindow::MenusEnded()
{
for (int32 i = fLabelMenu->CountItems(); i > 0; --i)
delete fLabelMenu->RemoveItem(i - 1);
BWindow::MenusEnded();
}
@@ -1356,33 +1389,84 @@ TMailWindow::MessageReceived(BMessage* msg)
fKeepStatusOnClose = true;
PostMessage(B_CLOSE_REQUESTED);
break;
case M_CLOSE_CUSTOM:
if (msg->HasString("status")) {
BMessage message(B_CLOSE_REQUESTED);
message.AddString("status", msg->GetString("status"));
PostMessage(&message);
} else {
BRect r = Frame();
BString string = "could not read";
BNode node(fRef);
if (node.InitCheck() == B_OK)
node.ReadAttrString(B_MAIL_ATTR_STATUS, &string);
new TStatusWindow(r, this, string.String());
case M_REMOVE_LABEL:
{
BNode node(fRef);
if (node.InitCheck() == B_NO_ERROR) {
fLabel = "";
node.RemoveAttr(B_MAIL_ATTR_LABEL);
BMenuItem* item = fLabelMenu->FindMarked();
if (item != NULL)
item->SetMarked(false);
item = fLabelMenu->FindItem(B_TRANSLATE("Remove label"));
if (item != NULL)
item->SetEnabled(false);
item = fQueryMenu->FindItem(kSameLabelItem);
if (item != NULL)
item->SetEnabled(false);
}
break;
case M_STATUS:
}
case M_SET_LABEL:
{
const char* attribute;
if (msg->FindString("attribute", &attribute) != B_OK)
break;
const char* label = msg->FindString("label");
if (label != NULL) {
BNode node(fRef);
if (node.InitCheck() == B_NO_ERROR) {
fLabel = label;
node.RemoveAttr(B_MAIL_ATTR_LABEL);
WriteAttrString(&node, B_MAIL_ATTR_LABEL, label);
BMessage message(B_CLOSE_REQUESTED);
message.AddString("status", attribute);
PostMessage(&message);
BMenuItem* item = fLabelMenu->FindMarked();
if (item != NULL)
item->SetMarked(false);
item = fLabelMenu->FindItem(label);
if (item != NULL) {
item->SetMarked(true);
item = fLabelMenu->FindItem(B_TRANSLATE("Remove label"));
if (item != NULL)
item->SetEnabled(true);
item = fQueryMenu->FindItem(kSameLabelItem);
if (item != NULL)
item->SetEnabled(true);
}
}
}
break;
}
case M_MANAGE_LABELS:
{
BPath path;
find_directory(B_USER_SETTINGS_DIRECTORY, &path);
path.Append("Mail/labels");
BEntry entry(path.Path());
if (!entry.Exists())
create_directory(path.Path(), 0777);
BEntry folderEntry;
if (folderEntry.SetTo(path.Path()) == B_OK
&& folderEntry.Exists()) {
BMessage openFolderCommand(B_REFS_RECEIVED);
BMessenger tracker("application/x-vnd.Be-TRAK");
entry_ref ref;
folderEntry.GetRef(&ref);
openFolderCommand.AddRef("refs", &ref);
tracker.SendMessage(&openFolderCommand);
}
break;
}
case M_NEW_LABEL:
new TLabelWindow(Frame(), this);
break;
case M_HEADER:
{
bool showHeader = !fHeader->IsMarked();
@@ -1716,6 +1800,13 @@ TMailWindow::MessageReceived(BMessage* msg)
}
break;
case M_QUERY_LABEL:
{
if (fLabel != "")
_LaunchQuery(kSameLabelItem, B_MAIL_ATTR_LABEL, fLabel);
break;
}
case M_QUERY_RECIPIENT:
{
BString searchText(fHeaderView->To());
@@ -3120,6 +3211,9 @@ TMailWindow::_RebuildQueryMenu(bool firstTime)
new BMessage(M_QUERY_SENDER)));
fQueryMenu->AddItem(new BMenuItem(kSameSubjectItem,
new BMessage(M_QUERY_SUBJECT)));
BMenuItem* item = new BMenuItem(kSameLabelItem, new BMessage(M_QUERY_LABEL));
fQueryMenu->AddItem(item);
item->SetEnabled(fLabel.Length() > 0 ? true : false);
BPath queryPath;
if (_GetQueryPath(&queryPath) < B_OK)
+3 -1
View File
@@ -80,6 +80,7 @@ public:
virtual void FrameResized(float width, float height);
virtual void MenusBeginning();
virtual void MenusEnded();
virtual void MessageReceived(BMessage*);
virtual bool QuitRequested();
virtual void Show();
@@ -173,10 +174,11 @@ private:
BMenuItem* fDeleteNext;
BMenuItem* fSpelling;
BMenu* fSaveAddrMenu;
BMenu* fLabelMenu;
BMenu* fQueryMenu;
BMenu* fLeaveStatusMenu;
BMenu* fEncodingMenu;
BString fLabel;
struct BitmapItem {
BBitmap* bm;
+5 -2
View File
@@ -88,8 +88,10 @@ enum MENUS {
M_DELETE_NEXT,
M_CLOSE_READ,
M_CLOSE_SAVED,
M_CLOSE_CUSTOM,
M_STATUS,
M_MANAGE_LABELS,
M_NEW_LABEL,
M_REMOVE_LABEL,
M_SET_LABEL,
M_READ_POS,
// edit
@@ -110,6 +112,7 @@ enum MENUS {
M_QUERY_RECIPIENT,
M_QUERY_SENDER,
M_QUERY_SUBJECT,
M_QUERY_LABEL,
// encls
M_ADD,