mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
132 lines
6.6 KiB
TypeScript
132 lines
6.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import type { ClassificationLabel } from '@/lib/admin/organisation-policy';
|
||
import { LABEL_PRESETS } from '@/lib/classification/presets';
|
||
import {
|
||
applyLabelPrefix,
|
||
draftLabelChoice,
|
||
draftLabelProperty,
|
||
labelHeaderLines,
|
||
labelHeaderProperties,
|
||
parseSioLabelMarking,
|
||
receivedLabel,
|
||
sioLabelValue,
|
||
textColorOn,
|
||
} from '@/lib/classification/labels';
|
||
|
||
// The headers a classification label writes (SIO-Label per RFC 7444 with
|
||
// RFC 2231 for non-ASCII markings, Sensitivity per RFC 2156, the admin's
|
||
// extra header), reading them back, and the subject prefix without doubling.
|
||
|
||
const generic = LABEL_PRESETS.find((p) => p.id === 'generic')!;
|
||
const german = LABEL_PRESETS.find((p) => p.id === 'german-public-sector')!;
|
||
const byId = (list: ClassificationLabel[], id: string) => list.find((l) => l.id === id)!;
|
||
const confidential = byId(generic.list, 'confidential');
|
||
const vsnfd = byId(german.list, 'vs-nfd');
|
||
|
||
describe('SIO-Label', () => {
|
||
it('writes an ASCII marking quoted, with readable colours', () => {
|
||
expect(sioLabelValue(confidential)).toBe(' marking="CONFIDENTIAL"; fgcolor=#000000; bgcolor=#d97706');
|
||
expect(textColorOn('#1e3a8a')).toBe('#ffffff');
|
||
expect(textColorOn('#fde68a')).toBe('#000000');
|
||
});
|
||
|
||
it('writes a non-ASCII marking with RFC 2231, never an encoded word, folded under 78 columns', () => {
|
||
const value = sioLabelValue(vsnfd);
|
||
expect(value).toContain("marking*=utf-8''VS-NUR%20F%C3%9CR%20DEN%20DIENSTGEBRAUCH;");
|
||
expect(value).not.toContain('=?');
|
||
for (const line of `SIO-Label:${value}`.split('\r\n')) expect(line.length).toBeLessThanOrEqual(78);
|
||
expect(parseSioLabelMarking(value)).toBe('VS-NUR FÜR DEN DIENSTGEBRAUCH');
|
||
});
|
||
|
||
it('continues a long encoded marking over several parameters and reads it back', () => {
|
||
const long = { ...vsnfd, marking: 'ÄÖÜ äöü ß – sehr lange Kennzeichnung für einen Test der Fortsetzung' };
|
||
const value = sioLabelValue(long);
|
||
expect(value).toMatch(/marking\*0\*=utf-8''/);
|
||
expect(value).toMatch(/marking\*1\*=/);
|
||
expect(value).not.toMatch(/%[0-9A-F]?(;|\r|$)/);
|
||
expect(parseSioLabelMarking(value)).toBe(long.marking);
|
||
});
|
||
|
||
it('reads the forms servers hand back: raw, refolded, a token, RFC 2047 inside quotes', () => {
|
||
expect(parseSioLabelMarking(' marking="INTERNAL"; fgcolor=#ffffff; bgcolor=#2563eb')).toBe('INTERNAL');
|
||
expect(parseSioLabelMarking(' marking="STRICTLY\r\n CONFIDENTIAL"; bgcolor=red')).toBe('STRICTLY CONFIDENTIAL');
|
||
expect(parseSioLabelMarking('marking=PUBLIC;fgcolor=black')).toBe('PUBLIC');
|
||
expect(parseSioLabelMarking(' marking="=?UTF-8?B?VlMtTlVSIEbDnFIgREVOIERJRU5TVEdFQlJBVUNI?="')).toBe('VS-NUR FÜR DEN DIENSTGEBRAUCH');
|
||
expect(parseSioLabelMarking(' type=":ess"; label="MQYGASkCAQM="')).toBeNull();
|
||
expect(parseSioLabelMarking(undefined)).toBeNull();
|
||
});
|
||
|
||
it('writes Sensitivity and the extra header only when the label sets them', () => {
|
||
expect(labelHeaderProperties(confidential, 'X-Classification')).toEqual({
|
||
// Without the leading space: the server writes "SIO-Label: " itself.
|
||
'header:SIO-Label:asRaw': 'marking="CONFIDENTIAL"; fgcolor=#000000; bgcolor=#d97706',
|
||
'header:Sensitivity:asText': 'Company-Confidential',
|
||
'header:X-Classification:asText': 'Confidential',
|
||
});
|
||
expect(Object.keys(labelHeaderProperties(byId(generic.list, 'public'), ''))).toEqual(['header:SIO-Label:asRaw']);
|
||
expect(labelHeaderLines(vsnfd, 'X-Classification')).toEqual([
|
||
`SIO-Label:${sioLabelValue(vsnfd)}`,
|
||
'Sensitivity: Company-Confidential',
|
||
'X-Classification: VS-NfD',
|
||
]);
|
||
expect(labelHeaderLines({ ...vsnfd, extraHeaderValue: 'Geheim für alle' }, 'X-Class')[2]).toMatch(/^X-Class: =\?UTF-8\?B\?/);
|
||
});
|
||
});
|
||
|
||
describe('received labels', () => {
|
||
const policy = { list: generic.list, defaultId: 'internal', required: false, extraHeader: 'X-Classification' };
|
||
|
||
it('are matched by marking first, case and spacing aside', () => {
|
||
expect(receivedLabel({ 'SIO-Label': ' marking="confidential "' }, policy)?.id).toBe('confidential');
|
||
expect(receivedLabel({ 'sio-label': [' marking="PUBLIC"'] }, policy)?.id).toBe('public');
|
||
});
|
||
|
||
it('then by the extra header value', () => {
|
||
expect(receivedLabel({ 'X-Classification': ' Strictly confidential' }, policy)?.id).toBe('strictly-confidential');
|
||
});
|
||
|
||
it('show nothing for an unknown marking or without labels', () => {
|
||
expect(receivedLabel({ 'SIO-Label': ' marking="TOP SECRET"' }, policy)).toBeNull();
|
||
expect(receivedLabel({ 'SIO-Label': ' marking="PUBLIC"' }, { ...policy, list: [] })).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('the subject prefix', () => {
|
||
const list = generic.list;
|
||
const strictly = byId(list, 'strictly-confidential');
|
||
|
||
it('is added once', () => {
|
||
expect(applyLabelPrefix('Budget', confidential, list)).toBe('[Confidential] Budget');
|
||
expect(applyLabelPrefix('[Confidential] Budget', confidential, list)).toBe('[Confidential] Budget');
|
||
});
|
||
|
||
it('is not doubled across reply and forward chains', () => {
|
||
expect(applyLabelPrefix('Re: [Confidential] Budget', confidential, list)).toBe('[Confidential] Re: Budget');
|
||
expect(applyLabelPrefix('AW: WG: [Confidential] Re: [Confidential] Budget', confidential, list)).toBe('[Confidential] AW: WG: Re: Budget');
|
||
});
|
||
|
||
it('changes with the label, and goes with no label', () => {
|
||
expect(applyLabelPrefix('Re: [Confidential] Budget', strictly, list)).toBe('[Strictly confidential] Re: Budget');
|
||
expect(applyLabelPrefix('[Confidential] Re: Budget', null, list)).toBe('Re: Budget');
|
||
expect(applyLabelPrefix('Re: Budget', byId(list, 'internal'), list)).toBe('Re: Budget');
|
||
});
|
||
|
||
it('knows prefixes without brackets, like VS-NfD', () => {
|
||
expect(applyLabelPrefix('AW: VS-NfD Lagebericht', vsnfd, german.list)).toBe('VS-NfD AW: Lagebericht');
|
||
expect(applyLabelPrefix('VS-NfD: Lagebericht', vsnfd, german.list)).toBe('VS-NfD Lagebericht');
|
||
// A word that only starts like a prefix stays.
|
||
expect(applyLabelPrefix('VS-NfDx test', vsnfd, german.list)).toBe('VS-NfD VS-NfDx test');
|
||
});
|
||
});
|
||
|
||
describe('drafts', () => {
|
||
it('keep the pick, "none" and nothing apart', () => {
|
||
expect(draftLabelProperty('confidential')).toEqual({ 'header:X-Bulwark-Label:asText': 'confidential' });
|
||
expect(draftLabelProperty(null)).toEqual({ 'header:X-Bulwark-Label:asText': 'none' });
|
||
expect(draftLabelProperty(undefined)).toEqual({});
|
||
expect(draftLabelChoice({ 'X-Bulwark-Label': ' confidential' })).toBe('confidential');
|
||
expect(draftLabelChoice({ 'x-bulwark-label': 'none' })).toBeNull();
|
||
expect(draftLabelChoice({})).toBeUndefined();
|
||
});
|
||
});
|