From 4139df4a13631baea9616b96d358ec55be778052 Mon Sep 17 00:00:00 2001 From: kei Date: Thu, 11 Jun 2026 05:50:56 +0200 Subject: [PATCH 1/2] test(mail): cover recipient message column Add coverage for showing a recipient address column in regular and indexed message lists. Covers #754. --- src/app/common/messagelist.spec.ts | 73 +++++++++++++++++++++ src/app/xapian/searchmessagedisplay.spec.ts | 62 +++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/app/common/messagelist.spec.ts create mode 100644 src/app/xapian/searchmessagedisplay.spec.ts diff --git a/src/app/common/messagelist.spec.ts b/src/app/common/messagelist.spec.ts new file mode 100644 index 000000000..9b0c8d2f8 --- /dev/null +++ b/src/app/common/messagelist.spec.ts @@ -0,0 +1,73 @@ +// --------- BEGIN RUNBOX LICENSE --------- +// Copyright (C) 2016-2022 Runbox Solutions AS (runbox.com). +// +// This file is part of Runbox 7. +// +// Runbox 7 is free software: You can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// Runbox 7 is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Runbox 7. If not, see . +// ---------- END RUNBOX LICENSE ---------- + +import { MailAddressInfo } from './mailaddressinfo'; +import { MessageList } from './messagelist'; + +describe('MessageList', () => { + const makeMessage = (to: MailAddressInfo[], cc: MailAddressInfo[] = []) => ({ + id: 1, + messageDate: new Date('2026-01-01T12:00:00Z'), + from: [new MailAddressInfo('Sender', 'sender@example.com')], + to, + cc, + subject: 'Subject', + plaintext: '', + size: 100, + attachment: false, + answeredFlag: false, + flaggedFlag: false + }); + + it('adds a recipient address column when enabled for incoming folders', () => { + const rows = new MessageList([ + makeMessage([ + new MailAddressInfo('Alias', 'alias+shopping@runbox.com') + ], [ + new MailAddressInfo('Copy', 'copy@example.com') + ]) + ]); + + const columns = rows.getCanvasTableColumns({ + selectedFolder: 'Inbox', + displayRecipientColumn: true + }); + const recipientColumn = columns.find((column) => column.cacheKey === 'recipient'); + + expect(recipientColumn).toBeTruthy(); + expect(columns.findIndex((column) => column.cacheKey === 'recipient')).toBeLessThan( + columns.findIndex((column) => column.cacheKey === 'subject') + ); + expect(recipientColumn.getValue(0)).toBe('alias+shopping@runbox.com, copy@example.com'); + }); + + it('does not add a duplicate recipient column for Sent folders', () => { + const rows = new MessageList([ + makeMessage([new MailAddressInfo('Recipient', 'recipient@example.com')]) + ]); + + const columns = rows.getCanvasTableColumns({ + selectedFolder: 'Sent', + displayRecipientColumn: true + }); + + expect(columns.find((column) => column.cacheKey === 'recipient')).toBeUndefined(); + expect(columns.find((column) => column.cacheKey === 'from').name).toBe('To'); + }); +}); diff --git a/src/app/xapian/searchmessagedisplay.spec.ts b/src/app/xapian/searchmessagedisplay.spec.ts new file mode 100644 index 000000000..cfd5b6bc2 --- /dev/null +++ b/src/app/xapian/searchmessagedisplay.spec.ts @@ -0,0 +1,62 @@ +// --------- BEGIN RUNBOX LICENSE --------- +// Copyright (C) 2016-2022 Runbox Solutions AS (runbox.com). +// +// This file is part of Runbox 7. +// +// Runbox 7 is free software: You can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// Runbox 7 is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Runbox 7. If not, see . +// ---------- END RUNBOX LICENSE ---------- + +import { SearchMessageDisplay } from './searchmessagedisplay'; + +describe('SearchMessageDisplay', () => { + const makeDisplay = () => new SearchMessageDisplay({ + getDocData: () => ({ + from: 'Sender', + subject: 'Subject', + recipients: ['alias@runbox.com', 'alias+tag@runbox.com'], + textcontent: '', + attachment: false, + answered: false, + flagged: false + }) + }, [[42]]); + + it('adds a recipient address column for indexed incoming results', () => { + const columns = makeDisplay().getCanvasTableColumns({ + selectedFolder: 'Inbox', + displayFolderColumn: false, + displayRecipientColumn: true, + viewmode: 'messages' + }); + const recipientColumn = columns.find((column) => column.cacheKey === 'recipient'); + + expect(recipientColumn).toBeTruthy(); + expect(columns.findIndex((column) => column.cacheKey === 'recipient')).toBeLessThan( + columns.findIndex((column) => column.cacheKey === 'subject') + ); + expect(recipientColumn.getValue(0)).toBe('alias@runbox.com, alias+tag@runbox.com'); + }); + + it('keeps Sent folder results on the existing To column when not showing all folders', () => { + const columns = makeDisplay().getCanvasTableColumns({ + selectedFolder: 'Sent', + displayFolderColumn: false, + displayRecipientColumn: true, + viewmode: 'messages' + }); + + expect(columns.find((column) => column.cacheKey === 'recipient')).toBeUndefined(); + expect(columns.find((column) => column.cacheKey === 'from').name).toBe('To'); + }); +}); From 03be47ee9c0b9ba20481bf0c4e9f875745ffc0c9 Mon Sep 17 00:00:00 2001 From: kei Date: Thu, 11 Jun 2026 05:51:06 +0200 Subject: [PATCH 2/2] feat(mail): add recipient address column Add a persisted view option that shows recipient addresses in the message list for incoming folders. Keep Sent folders on their existing To column, and keep wrapped rows anchored to the Subject column when the optional column is inserted. Closes #754. --- src/app/app.component.html | 11 ++++++++++ src/app/app.component.ts | 15 +++++++++++-- src/app/common/messagelist.ts | 29 ++++++++++++++++++++++++++ src/app/xapian/searchmessagedisplay.ts | 23 ++++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index ec21e8698..adc6fbdbc 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -347,6 +347,17 @@

No Message Selected

Inline previews + + + + Recipient column + + column.cacheKey === 'subject'); + const defaultSelectedColumn = subjectColumnIndex > -1 ? subjectColumnIndex : 3; + this.canvastable.rowWrapModeWrapColumn = defaultSelectedColumn; + this.canvastable.rowWrapModeDefaultSelectedColumn = defaultSelectedColumn; this.autoAdjustColumnWidths(); } diff --git a/src/app/common/messagelist.ts b/src/app/common/messagelist.ts index dd7e50249..3f7e1e02d 100644 --- a/src/app/common/messagelist.ts +++ b/src/app/common/messagelist.ts @@ -19,6 +19,7 @@ import { MessageDisplay } from '../common/messagedisplay'; import { MessageInfo } from './messageinfo'; +import { MailAddressInfo } from './mailaddressinfo'; import { MessageTableRowTool} from '../messagetable/messagetablerow'; import { CanvasTableColumn } from '../canvastable/canvastablecolumn'; @@ -62,6 +63,22 @@ export class MessageList extends MessageDisplay { ''; } + getRecipientColumnValueForRow(rowIndex: number): string { + const rowobj = this.rows[rowIndex]; + return this.formatRecipientAddresses([].concat(rowobj.to || [], rowobj.cc || [])); + } + + private formatRecipientAddresses(recipients: MailAddressInfo[]): string { + return recipients + .map((mailAddr) => mailAddr.address || mailAddr.nameAndAddress || '') + .filter((address) => address.length > 0) + .join(', '); + } + + private shouldShowRecipientColumn(app: { displayRecipientColumn: boolean; selectedFolder: string }): boolean { + return app.displayRecipientColumn && app.selectedFolder.indexOf('Sent') !== 0; + } + // filter visible rows by whatever options the frontend has filterBy(options: Map) { this.rows = this._rows; @@ -155,6 +172,18 @@ export class MessageList extends MessageDisplay { } ]; + if (this.shouldShowRecipientColumn(app)) { + columns.splice(3, 0, { + name: 'Recipient', + cacheKey: 'recipient', + sortColumn: null, + rowWrapModeHidden: true, + getValue: (rowIndex: number): string => this.getRecipientColumnValueForRow(rowIndex), + draggable: true, + width: 220 + }); + } + return columns; } } diff --git a/src/app/xapian/searchmessagedisplay.ts b/src/app/xapian/searchmessagedisplay.ts index 8bd686cb7..021cddc3a 100644 --- a/src/app/xapian/searchmessagedisplay.ts +++ b/src/app/xapian/searchmessagedisplay.ts @@ -54,6 +54,17 @@ export class SearchMessageDisplay extends MessageDisplay { filterBy(options: Map) { } + getRecipientColumnValueForRow(rowIndex: number): string { + return this.searchService.getDocData(this.getRowId(rowIndex)).recipients.join(', '); + } + + private shouldShowRecipientColumn( + app: { displayRecipientColumn: boolean; displayFolderColumn: boolean; selectedFolder: string } + ): boolean { + return app.displayRecipientColumn + && (app.displayFolderColumn || app.selectedFolder.indexOf('Sent') !== 0); + } + // columns // app is a Component (currently) public getCanvasTableColumns(app: any): CanvasTableColumn[] { @@ -107,6 +118,18 @@ export class SearchMessageDisplay extends MessageDisplay { } ]; + if (this.shouldShowRecipientColumn(app)) { + columns.splice(3, 0, { + name: 'Recipient', + draggable: true, + cacheKey: 'recipient', + sortColumn: null, + rowWrapModeHidden: true, + getValue: (rowIndex): string => this.getRecipientColumnValueForRow(rowIndex), + width: 220 + }); + } + if (app.viewmode === 'conversations') { // Array containing row (conversation) objects waiting to be counted let currentCountObject = null;