feat: filter search results by message size

This commit is contained in:
Linus Rath
2026-10-31 05:26:03 +01:00
parent 1342e2a0f8
commit aac8a40367
5 changed files with 317 additions and 3 deletions
+27 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { splitSearchText, textConditions, textFilter } from '../search-utils';
import { sizeFilterBytes, sizePresetOf, splitSearchText, textConditions, textFilter } from '../search-utils';
import { textFilterMatches, type StalwartTextVersion } from './stalwart-text-model';
type Filter = Record<string, unknown>;
@@ -125,3 +125,29 @@ describe('text search results (P1-7)', () => {
expect(hits('0.16', textFilter('project budg*'))).toEqual([]);
});
});
// The size filter (webmail 720e14622): KB bounds as typed, sent in bytes.
describe('sizeFilterBytes', () => {
it('turns a KB value into bytes', () => {
expect(sizeFilterBytes('1024')).toBe(1024 * 1024);
expect(sizeFilterBytes('1.5')).toBe(1536);
});
it('reads an empty, zero, negative or non-numeric value as unset', () => {
for (const value of [undefined, '', '0', '-5', 'abc', 'Infinity']) {
expect(sizeFilterBytes(value)).toBeNull();
}
});
});
describe('sizePresetOf', () => {
it('names the panel choice for the bounds', () => {
expect(sizePresetOf(undefined, undefined)).toBe('any');
expect(sizePresetOf('', '')).toBe('any');
expect(sizePresetOf('1024', undefined)).toBe('mb1');
expect(sizePresetOf('25600', '')).toBe('mb25');
expect(sizePresetOf('2048', undefined)).toBe('custom');
expect(sizePresetOf('1024', '4096')).toBe('custom');
expect(sizePresetOf(undefined, '100')).toBe('custom');
});
});
+27
View File
@@ -1,6 +1,33 @@
// Free-text search conditions for Email/query, ported from the webmail's
// lib/jmap/search-utils.ts (5967a1c4) so both clients send the same filters.
/**
* A KB size field (the filter panel's custom size) as a positive byte count
* for Email/query's minSize / maxSize (RFC 8621 §4.4.1), or null when unset
* or not a positive number (webmail lib/jmap/search-utils.ts, 720e14622).
*/
export function sizeFilterBytes(value: string | undefined): number | null {
const kb = Number(value);
if (!value || !Number.isFinite(kb) || kb <= 0) return null;
return Math.round(kb * 1024);
}
/** The size presets of the filter panel, in MB: "Larger than 1 MB" and so on. */
export const SIZE_PRESETS_MB = [1, 5, 10, 25] as const;
/**
* Which size choice the filter panel shows for the KB bounds: none set, a
* "Larger than N MB" preset, or a custom range.
*/
export function sizePresetOf(minSizeKb: string | undefined, maxSizeKb: string | undefined): string {
if (!minSizeKb && !maxSizeKb) return 'any';
if (!maxSizeKb) {
const mb = SIZE_PRESETS_MB.find((m) => String(m * 1024) === minSizeKb);
if (mb) return `mb${mb}`;
}
return 'custom';
}
/** Filter properties whose value the server reads as free text. */
export type SearchTextProperty = 'text' | 'from' | 'to' | 'subject' | 'body';