Skip to content

Commit ad26fd2

Browse files
committed
webmail: add button to mark a mailbox and its children as read
this sets the seen flag on all messages in the mailbox and its children.
1 parent c8fd9ca commit ad26fd2

File tree

9 files changed

+134
-1
lines changed

9 files changed

+134
-1
lines changed

webmail/api.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,16 @@ func (Webmail) FlagsClear(ctx context.Context, messageIDs []int64, flaglist []st
11951195
xops.MessageFlagsClear(ctx, log, acc, messageIDs, flaglist)
11961196
}
11971197

1198+
// MailboxesMarkRead marks all messages in mailboxes as read. Child mailboxes are
1199+
// not automatically included, they must explicitly be included in the list of IDs.
1200+
func (Webmail) MailboxesMarkRead(ctx context.Context, mailboxIDs []int64) {
1201+
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
1202+
acc := reqInfo.Account
1203+
log := reqInfo.Log
1204+
1205+
xops.MailboxesMarkRead(ctx, log, acc, mailboxIDs)
1206+
}
1207+
11981208
// MailboxCreate creates a new mailbox.
11991209
func (Webmail) MailboxCreate(ctx context.Context, name string) {
12001210
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)

webmail/api.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,20 @@
247247
],
248248
"Returns": []
249249
},
250+
{
251+
"Name": "MailboxesMarkRead",
252+
"Docs": "MailboxesMarkRead marks all messages in mailboxes as read. Child mailboxes are\nnot automatically included, they must explicitly be included in the list of IDs.",
253+
"Params": [
254+
{
255+
"Name": "mailboxIDs",
256+
"Typewords": [
257+
"[]",
258+
"int64"
259+
]
260+
}
261+
],
262+
"Returns": []
263+
},
250264
{
251265
"Name": "MailboxCreate",
252266
"Docs": "MailboxCreate creates a new mailbox.",

webmail/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,16 @@ export class Client {
875875
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
876876
}
877877

878+
// MailboxesMarkRead marks all messages in mailboxes as read. Child mailboxes are
879+
// not automatically included, they must explicitly be included in the list of IDs.
880+
async MailboxesMarkRead(mailboxIDs: number[] | null): Promise<void> {
881+
const fn: string = "MailboxesMarkRead"
882+
const paramTypes: string[][] = [["[]","int64"]]
883+
const returnTypes: string[][] = []
884+
const params: any[] = [mailboxIDs]
885+
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
886+
}
887+
878888
// MailboxCreate creates a new mailbox.
879889
async MailboxCreate(name: string): Promise<void> {
880890
const fn: string = "MailboxCreate"

webmail/api_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ func TestAPI(t *testing.T) {
227227
api.MailboxSetSpecialUse(ctx, store.Mailbox{ID: sent.ID, SpecialUse: store.SpecialUse{Sent: true}}) // Sent, for sending mail later.
228228
tneedError(t, func() { api.MailboxSetSpecialUse(ctx, store.Mailbox{ID: 0}) })
229229

230+
// MailboxesMarkRead
231+
api.FlagsClear(ctx, []int64{inboxText.ID, inboxMinimal.ID}, []string{`\seen`})
232+
api.MailboxesMarkRead(ctx, []int64{inbox.ID, archive.ID, sent.ID})
233+
tneedError(t, func() { api.MailboxesMarkRead(ctx, []int64{inbox.ID + 999}) }) // Does not exist.
234+
230235
// MailboxRename
231236
api.MailboxRename(ctx, testbox1.ID, "Testbox2")
232237
api.MailboxRename(ctx, testbox1.ID, "Test/A/B/Box1")

webmail/msg.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,15 @@ var api;
556556
const params = [messageIDs, flaglist];
557557
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
558558
}
559+
// MailboxesMarkRead marks all messages in mailboxes as read. Child mailboxes are
560+
// not automatically included, they must explicitly be included in the list of IDs.
561+
async MailboxesMarkRead(mailboxIDs) {
562+
const fn = "MailboxesMarkRead";
563+
const paramTypes = [["[]", "int64"]];
564+
const returnTypes = [];
565+
const params = [mailboxIDs];
566+
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
567+
}
559568
// MailboxCreate creates a new mailbox.
560569
async MailboxCreate(name) {
561570
const fn = "MailboxCreate";

webmail/text.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,15 @@ var api;
556556
const params = [messageIDs, flaglist];
557557
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
558558
}
559+
// MailboxesMarkRead marks all messages in mailboxes as read. Child mailboxes are
560+
// not automatically included, they must explicitly be included in the list of IDs.
561+
async MailboxesMarkRead(mailboxIDs) {
562+
const fn = "MailboxesMarkRead";
563+
const paramTypes = [["[]", "int64"]];
564+
const returnTypes = [];
565+
const params = [mailboxIDs];
566+
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
567+
}
559568
// MailboxCreate creates a new mailbox.
560569
async MailboxCreate(name) {
561570
const fn = "MailboxCreate";

webmail/webmail.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,15 @@ var api;
556556
const params = [messageIDs, flaglist];
557557
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
558558
}
559+
// MailboxesMarkRead marks all messages in mailboxes as read. Child mailboxes are
560+
// not automatically included, they must explicitly be included in the list of IDs.
561+
async MailboxesMarkRead(mailboxIDs) {
562+
const fn = "MailboxesMarkRead";
563+
const paramTypes = [["[]", "int64"]];
564+
const returnTypes = [];
565+
const params = [mailboxIDs];
566+
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
567+
}
559568
// MailboxCreate creates a new mailbox.
560569
async MailboxCreate(name) {
561570
const fn = "MailboxCreate";
@@ -5486,7 +5495,11 @@ const newMailboxView = (xmb, mailboxlistView, otherMailbox) => {
54865495
let actionBtn;
54875496
const cmdOpenActions = async () => {
54885497
const trashmb = mailboxlistView.mailboxes().find(mb => mb.Trash);
5489-
const remove = popover(actionBtn, { transparent: true }, dom.div(style({ display: 'flex', flexDirection: 'column', gap: '.5ex' }), dom.div(dom.clickbutton('Move to trash', attr.title('Move mailbox, its messages and its mailboxes to the trash.'), async function click() {
5498+
const remove = popover(actionBtn, { transparent: true }, dom.div(style({ display: 'flex', flexDirection: 'column', gap: '.5ex' }), dom.div(dom.clickbutton('Mark as read', attr.title('Mark all messages in the mailbox and its sub mailboxes as read.'), async function click() {
5499+
remove();
5500+
const mailboxIDs = [mbv.mailbox.ID, ...mailboxlistView.mailboxes().filter(mb => mb.Name.startsWith(mbv.mailbox.Name + '/')).map(mb => mb.ID)];
5501+
await withStatus('Marking mailboxes as read', client.MailboxesMarkRead(mailboxIDs));
5502+
})), dom.div(dom.clickbutton('Move to trash', attr.title('Move mailbox, its messages and its mailboxes to the trash.'), async function click() {
54905503
if (!trashmb) {
54915504
window.alert('No mailbox configured for trash yet.');
54925505
return;

webmail/webmail.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5149,6 +5149,13 @@ const newMailboxView = (xmb: api.Mailbox, mailboxlistView: MailboxlistView, othe
51495149

51505150
const remove = popover(actionBtn, {transparent: true},
51515151
dom.div(style({display: 'flex', flexDirection: 'column', gap: '.5ex'}),
5152+
dom.div(
5153+
dom.clickbutton('Mark as read', attr.title('Mark all messages in the mailbox and its sub mailboxes as read.'), async function click() {
5154+
remove()
5155+
const mailboxIDs = [mbv.mailbox.ID, ...mailboxlistView.mailboxes().filter(mb => mb.Name.startsWith(mbv.mailbox.Name+'/')).map(mb => mb.ID)]
5156+
await withStatus('Marking mailboxes as read', client.MailboxesMarkRead(mailboxIDs))
5157+
}),
5158+
),
51525159
dom.div(
51535160
dom.clickbutton('Move to trash', attr.title('Move mailbox, its messages and its mailboxes to the trash.'), async function click() {
51545161
if (!trashmb) {

webops/xops.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,62 @@ func (x XOps) MessageFlagsClear(ctx context.Context, log mlog.Log, acc *store.Ac
287287
})
288288
}
289289

290+
// MailboxesMarkRead updates all messages in the referenced mailboxes as seen when
291+
// they aren't yet. The mailboxes are updated with their unread messages counts,
292+
// and the changes are propagated.
293+
func (x XOps) MailboxesMarkRead(ctx context.Context, log mlog.Log, acc *store.Account, mailboxIDs []int64) {
294+
acc.WithRLock(func() {
295+
var changes []store.Change
296+
297+
x.DBWrite(ctx, acc, func(tx *bstore.Tx) {
298+
var modseq store.ModSeq
299+
300+
// Note: we don't need to retrain, changing the "seen" flag is not relevant.
301+
302+
for _, mbID := range mailboxIDs {
303+
mb := x.mailboxID(ctx, tx, mbID)
304+
305+
// Find messages to update.
306+
q := bstore.QueryTx[store.Message](tx)
307+
q.FilterNonzero(store.Message{MailboxID: mb.ID})
308+
q.FilterEqual("Seen", false)
309+
q.FilterEqual("Expunged", false)
310+
q.SortAsc("UID")
311+
var have bool
312+
err := q.ForEach(func(m store.Message) error {
313+
have = true // We need to update mailbox.
314+
315+
oflags := m.Flags
316+
mb.Sub(m.MailboxCounts())
317+
m.Seen = true
318+
mb.Add(m.MailboxCounts())
319+
320+
if modseq == 0 {
321+
var err error
322+
modseq, err = acc.NextModSeq(tx)
323+
x.Checkf(ctx, err, "assigning next modseq")
324+
}
325+
m.ModSeq = modseq
326+
err := tx.Update(&m)
327+
x.Checkf(ctx, err, "updating message")
328+
329+
changes = append(changes, m.ChangeFlags(oflags))
330+
return nil
331+
})
332+
x.Checkf(ctx, err, "listing messages to mark as read")
333+
334+
if have {
335+
err := tx.Update(&mb)
336+
x.Checkf(ctx, err, "updating mailbox")
337+
changes = append(changes, mb.ChangeCounts())
338+
}
339+
}
340+
})
341+
342+
store.BroadcastChanges(acc, changes)
343+
})
344+
}
345+
290346
// MessageMove moves messages to the mailbox represented by mailboxName, or to mailboxID if mailboxName is empty.
291347
func (x XOps) MessageMove(ctx context.Context, log mlog.Log, acc *store.Account, messageIDs []int64, mailboxName string, mailboxID int64) {
292348
acc.WithRLock(func() {

0 commit comments

Comments
 (0)