+ {t('legacyProtocols.impactNone', 'No account used a legacy mail app in the last 30 days.')}
+
+ );
+ }
+ return (
+
+
+ {t('legacyProtocols.statementTitle', 'Only INBUXA webmail and JMAP apps will work.')}
+
+
+ {scope.kind === 'server'
+ ? t(
+ 'legacyProtocols.statementLead',
+ 'Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone on this server.',
+ )
+ : t(
+ 'legacyProtocols.statementLeadTenant',
+ 'Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {{organization}}.',
+ { organization: scope.organization },
+ )}
+
+
+ -
+ {t(
+ 'legacyProtocols.statementApps',
+ 'Phone and desktop mail apps will stop receiving and sending mail. That’s iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.',
+ )}
+
+ -
+ {t(
+ 'legacyProtocols.statementFilters',
+ 'Filters managed from a mail app (ManageSieve) will stop working. Filters set in INBUXA webmail keep working.',
+ )}
+
+ -
+ {t(
+ 'legacyProtocols.statementUnaffected',
+ 'Incoming mail is not affected. Calendars and contacts are not affected.',
+ )}
+
+ -
+ {t(
+ 'legacyProtocols.statementWebmail',
+ 'People keep full access through INBUXA webmail, which can be installed as an app on phones and computers.',
+ )}
+
+
+ {scope.kind === 'server' && (
+
+ {scope.listeners.length > 0
+ ? t('legacyProtocols.statementPorts', 'The IMAP, POP3 and ManageSieve ports will close: {{list}}.', {
+ list: scope.listeners.map(describeListener).join(', '),
+ })
+ : t(
+ 'legacyProtocols.statementNoPorts',
+ 'No IMAP, POP3 or ManageSieve listeners are configured, so no ports will close.',
+ )}
+
+ )}
+
+ {t(
+ 'legacyProtocols.statementSubmission',
+ 'Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and INBUXA webmail (JMAP) are not affected and cannot be turned off here.',
+ )}
+
+ {scope.kind === 'server' && (
+
+ {t('legacyProtocols.firewallLead', 'This does not change your firewall or port forwarding.')}{' '}
+ {t(
+ 'legacyProtocols.firewallBody',
+ 'INBUXA stops answering on these ports; anything that still routes them to this server — firewall rules, NAT port-forwards, a load balancer or proxy — is yours to reconcile.',
+ )}
+
+ )}
+ {t('legacyProtocols.statementUndo', 'You can turn legacy protocols back on at any time.')}
+
+ );
+}
diff --git a/src/features/hardening/protocolPolicy.test.ts b/src/features/hardening/protocolPolicy.test.ts
index 6bd822f..17456fe 100644
--- a/src/features/hardening/protocolPolicy.test.ts
+++ b/src/features/hardening/protocolPolicy.test.ts
@@ -10,6 +10,7 @@ import {
CONFIRM_PHRASE,
impactEntries,
parsePolicy,
+ parseTenantPolicy,
phraseMatches,
protocolRows,
type ProtocolPolicy,
@@ -120,3 +121,18 @@ describe('the impact panel (LP-15)', () => {
expect(ago(now - 10_000, now, 'en')).toBe('this minute');
});
});
+
+describe("a tenant's switch", () => {
+ it('reads the wire, and tells an older server from nobody', () => {
+ const p = parseTenantPolicy({
+ id: 'b',
+ tenantId: 'b',
+ legacyProtocols: 'disabled',
+ changedAt: 5,
+ recentLegacyUse: [{ accountId: 'c', name: 'u@t.example', protocol: 'imap', lastUsedAt: 9 }],
+ });
+ expect(p).toMatchObject({ id: 'b', legacyProtocols: 'disabled', changedAt: 5 });
+ expect(p.recentLegacyUse).toHaveLength(1);
+ expect(parseTenantPolicy({ id: 'b' })).toMatchObject({ legacyProtocols: 'enabled', recentLegacyUse: null });
+ });
+});
diff --git a/src/features/hardening/protocolPolicy.ts b/src/features/hardening/protocolPolicy.ts
index 126f701..c083b71 100644
--- a/src/features/hardening/protocolPolicy.ts
+++ b/src/features/hardening/protocolPolicy.ts
@@ -256,3 +256,65 @@ export function phraseMatches(typed: string): boolean {
export function describeListener(l: PolicyListener): string {
return l.ports.length > 0 ? `${l.name} (${l.ports.join(', ')})` : l.name;
}
+
+// ---- A tenant's switch: inbuxa:TenantProtocolPolicy (LP-9 to LP-14) ----
+
+const TENANT_OBJECT = 'inbuxa:TenantProtocolPolicy';
+
+export interface TenantPolicy {
+ /** The tenant's id, which is also the policy's. */
+ id: string;
+ legacyProtocols: 'enabled' | 'disabled';
+ /** Milliseconds since the epoch. */
+ changedAt: number | null;
+ /** The tenant's own people who used a legacy mail app lately (LP-15), or null from an older server. */
+ recentLegacyUse: RecentUse[] | null;
+}
+
+export function parseTenantPolicy(raw: Record