From a5a8a1def250fd823025cb338510d15987914cef Mon Sep 17 00:00:00 2001 From: Ben McFerren Date: Wed, 24 Jun 2026 12:36:02 -0400 Subject: [PATCH 1/4] PM-38122 resolved fastmail checkbox --- apps/browser/src/_locales/en/messages.json | 8 ++++ apps/cli/src/locales/en/messages.json | 10 ++++- apps/desktop/src/locales/en/messages.json | 8 ++++ apps/web/src/locales/en/messages.json | 8 ++++ .../src/tools/extension/vendor/fastmail.ts | 2 +- .../src/forwarder-settings.component.html | 7 ++++ .../src/forwarder-settings.component.ts | 14 ++++++- .../core/src/engine/forwarder-context.spec.ts | 42 +++++++++++++++++++ .../core/src/engine/forwarder-context.ts | 9 ++++ .../core/src/integration/fastmail.spec.ts | 16 ++++++- .../core/src/integration/fastmail.ts | 2 +- 11 files changed, 120 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 1eff547e60d1..be507bf85c62 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3893,6 +3893,14 @@ } } }, + "forwarderWebsitePrefix": { + "message": "Add website prefix", + "description": "Labels the option to use the current website as a prefix for Fastmail masked email addresses" + }, + "forwarderWebsitePrefixHint": { + "message": "When enabled, the current website's domain will be used as a prefix for the generated email address", + "description": "Hint text for the website prefix option in Fastmail forwarder settings" + }, "hostname": { "message": "Hostname", "description": "Part of a URL." diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index 824b03b99cfa..110dbbc75c02 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -1,4 +1,4 @@ -{ +{ "bitwarden": { "message": "Bitwarden" }, @@ -197,6 +197,14 @@ } } }, + "forwarderWebsitePrefix": { + "message": "Add website prefix", + "description": "Labels the option to use the current website as a prefix for Fastmail masked email addresses" + }, + "forwarderWebsitePrefixHint": { + "message": "When enabled, the current website's domain will be used as a prefix for the generated email address", + "description": "Hint text for the website prefix option in Fastmail forwarder settings" + }, "legacyEncryptionUnsupported": { "message": "Legacy encryption is no longer supported. Please contact support to recover your account." }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index e9f53c731e0c..8ffb26dcff7b 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3390,6 +3390,14 @@ } } }, + "forwarderWebsitePrefix": { + "message": "Add website prefix", + "description": "Labels the option to use the current website as a prefix for Fastmail masked email addresses" + }, + "forwarderWebsitePrefixHint": { + "message": "When enabled, the current website's domain will be used as a prefix for the generated email address", + "description": "Hint text for the website prefix option in Fastmail forwarder settings" + }, "hostname": { "message": "Hostname", "description": "Part of a URL." diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8b08e5f461c7..296872fede68 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8980,6 +8980,14 @@ } } }, + "forwarderWebsitePrefix": { + "message": "Add website prefix", + "description": "Labels the option to use the current website as a prefix for Fastmail masked email addresses" + }, + "forwarderWebsitePrefixHint": { + "message": "When enabled, the current website's domain will be used as a prefix for the generated email address", + "description": "Hint text for the website prefix option in Fastmail forwarder settings" + }, "hostname": { "message": "Hostname", "description": "Part of a URL." diff --git a/libs/common/src/tools/extension/vendor/fastmail.ts b/libs/common/src/tools/extension/vendor/fastmail.ts index e6fb9ec16be5..ecd481c86a0d 100644 --- a/libs/common/src/tools/extension/vendor/fastmail.ts +++ b/libs/common/src/tools/extension/vendor/fastmail.ts @@ -20,6 +20,6 @@ export const FastmailExtensions: ExtensionMetadata[] = [ selfHost: "maybe", baseUrl: "https://api.fastmail.com", }, - requestedFields: [Field.token], + requestedFields: [Field.token, Field.prefix], }, ]; diff --git a/libs/tools/generator/components/src/forwarder-settings.component.html b/libs/tools/generator/components/src/forwarder-settings.component.html index 0d213179c2b0..83ee81bd770c 100644 --- a/libs/tools/generator/components/src/forwarder-settings.component.html +++ b/libs/tools/generator/components/src/forwarder-settings.component.html @@ -25,6 +25,13 @@ > } + @if (displayPrefix) { + + + {{ "forwarderWebsitePrefix" | i18n }} + {{ "forwarderWebsitePrefixHint" | i18n }} + + } @if (displayBaseUrl) { {{ "selfHostBaseUrl" | i18n }} diff --git a/libs/tools/generator/components/src/forwarder-settings.component.ts b/libs/tools/generator/components/src/forwarder-settings.component.ts index 0f1c863b2f6d..7ab5bb08b98d 100644 --- a/libs/tools/generator/components/src/forwarder-settings.component.ts +++ b/libs/tools/generator/components/src/forwarder-settings.component.ts @@ -19,6 +19,7 @@ import { AriaDisableDirective, TooltipDirective, BitIconButtonComponent, + CheckboxModule, } from "@bitwarden/components"; import { CredentialGeneratorService, @@ -31,6 +32,7 @@ const Controls = Object.freeze({ domain: "domain", token: "token", baseUrl: "baseUrl", + prefix: "prefix", }); /** Options group for forwarder integrations */ @@ -45,6 +47,7 @@ const Controls = Object.freeze({ AriaDisableDirective, TooltipDirective, BitIconButtonComponent, + CheckboxModule, JslibModule, I18nPipe, ], @@ -90,6 +93,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy [Controls.domain]: [""], [Controls.token]: [""], [Controls.baseUrl]: [""], + [Controls.prefix]: [false], }); private vendor = new ReplaySubject(1); @@ -105,6 +109,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy this.displayDomain = forwarder.capabilities.fields.includes("domain"); this.displayToken = forwarder.capabilities.fields.includes("token"); this.displayBaseUrl = forwarder.capabilities.fields.includes("baseUrl"); + this.displayPrefix = forwarder.capabilities.fields.includes("prefix"); forwarder$.next(forwarder); }); @@ -116,7 +121,9 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy // bind settings to the reactive form settings$.pipe(switchAll(), takeUntil(this.destroyed$)).subscribe((settings) => { // skips reactive event emissions to break a subscription cycle - this.settings.patchValue(settings as any, { emitEvent: false }); + // convert prefix sentinel string to boolean for the checkbox control + const patchValues = { ...(settings as any), prefix: (settings as any).prefix === "website" }; + this.settings.patchValue(patchValues, { emitEvent: false }); }); // enable requested forwarder inputs @@ -144,7 +151,9 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy this.saveSettings .pipe(withLatestFrom(this.settings.valueChanges, settings$), takeUntil(this.destroyed$)) .subscribe(([, value, settings]) => { - settings.next(value as ForwarderOptions); + // convert prefix boolean back to sentinel string for the settings store + const saveValues = { ...value, prefix: (value as any).prefix ? "website" : "" }; + settings.next(saveValues as ForwarderOptions); }); } @@ -167,6 +176,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy protected displayDomain: boolean = false; protected displayToken: boolean = false; protected displayBaseUrl: boolean = false; + protected displayPrefix: boolean = false; private readonly refresh$ = new Subject(); diff --git a/libs/tools/generator/core/src/engine/forwarder-context.spec.ts b/libs/tools/generator/core/src/engine/forwarder-context.spec.ts index e7d848cfed54..1b17ec48c674 100644 --- a/libs/tools/generator/core/src/engine/forwarder-context.spec.ts +++ b/libs/tools/generator/core/src/engine/forwarder-context.spec.ts @@ -74,6 +74,48 @@ describe("ForwarderContext", () => { }); }); + describe("prefixEnabled", () => { + it("returns true when prefix is set to the 'website' sentinel", () => { + const settings = mock({ prefix: "website" }); + const config = mock>(); + const context = new ForwarderContext(config, settings, i18n); + + const result = context.prefixEnabled(); + + expect(result).toBe(true); + }); + + it.each([[null], [undefined], [""]])("returns false when prefix is %p", (prefix) => { + const settings = mock({ prefix }); + const config = mock>(); + const context = new ForwarderContext(config, settings, i18n); + + const result = context.prefixEnabled(); + + expect(result).toBe(false); + }); + + it("returns false when prefix is not an enumerable member of settings", () => { + const settings = {} as EmailPrefixSettings & ApiSettings; + const config = mock>(); + const context = new ForwarderContext(config, settings, i18n); + + const result = context.prefixEnabled(); + + expect(result).toBe(false); + }); + + it("returns false when prefix is any non-'website' string", () => { + const settings = mock({ prefix: "some-other-value" }); + const config = mock>(); + const context = new ForwarderContext(config, settings, i18n); + + const result = context.prefixEnabled(); + + expect(result).toBe(false); + }); + }); + describe("missingAccountIdCause", () => { it("returns the cause", () => { const settings = mock(); diff --git a/libs/tools/generator/core/src/engine/forwarder-context.ts b/libs/tools/generator/core/src/engine/forwarder-context.ts index 69fc37eafa42..130456d4de01 100644 --- a/libs/tools/generator/core/src/engine/forwarder-context.ts +++ b/libs/tools/generator/core/src/engine/forwarder-context.ts @@ -52,6 +52,15 @@ export class ForwarderContext extends IntegrationC return prefix as any; } + /** check whether the website prefix feature is enabled in the forwarder's settings. + * @returns true when the prefix setting is set to the "website" sentinel value; + * false otherwise. This method never throws. + */ + prefixEnabled(): boolean { + const prefix = "prefix" in this.settings ? (this.settings.prefix ?? "") : ""; + return prefix === "website"; + } + /** look up a localized error message indicating an account id is required * but wasn't found. * @remarks this returns a string instead of throwing it so that the diff --git a/libs/tools/generator/core/src/integration/fastmail.spec.ts b/libs/tools/generator/core/src/integration/fastmail.spec.ts index f7b200a30e02..2704d6f3f51f 100644 --- a/libs/tools/generator/core/src/integration/fastmail.spec.ts +++ b/libs/tools/generator/core/src/integration/fastmail.spec.ts @@ -113,8 +113,9 @@ describe("Fastmail forwarder", () => { }); describe("body", () => { - it("creates a request body", () => { + it("creates a request body without email prefix when prefix is not set", () => { context.website.mockReturnValue("website"); + context.prefixEnabled.mockReturnValue(false); const request = { accountId: "accountId", website: "" }; const result = Fastmail.forwarder.createForwardingEmail.body(request, context); @@ -122,6 +123,19 @@ describe("Fastmail forwarder", () => { expect(methodCall.accountId).toEqual("accountId"); expect(methodCall.create["new-masked-email"].forDomain).toEqual("website"); + expect(methodCall.create["new-masked-email"].emailPrefix).toEqual(""); + }); + + it("creates a request body with website as email prefix when prefix is enabled", () => { + context.website.mockReturnValue("example.com"); + context.prefixEnabled.mockReturnValue(true); + const request = { accountId: "accountId", website: "example.com" }; + + const result = Fastmail.forwarder.createForwardingEmail.body(request, context); + const methodCall = result.methodCalls[0][1]; + + expect(methodCall.accountId).toEqual("accountId"); + expect(methodCall.create["new-masked-email"].emailPrefix).toEqual("example.com"); }); }); diff --git a/libs/tools/generator/core/src/integration/fastmail.ts b/libs/tools/generator/core/src/integration/fastmail.ts index a540807666e2..b7e07b8f4617 100644 --- a/libs/tools/generator/core/src/integration/fastmail.ts +++ b/libs/tools/generator/core/src/integration/fastmail.ts @@ -71,7 +71,7 @@ const createForwardingEmail = Object.freeze({ state: "enabled", description: "", forDomain: context.website(request), - emailPrefix: "", + emailPrefix: context.prefixEnabled() ? context.website(request) : "", }, }, }, From 63663bdf788fc6a079c050c8fc31ff63c9dfa1a0 Mon Sep 17 00:00:00 2001 From: Ben McFerren Date: Fri, 26 Jun 2026 11:41:36 -0400 Subject: [PATCH 2/4] PM-38122 resolved pr comment --- libs/tools/generator/core/src/integration/fastmail.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libs/tools/generator/core/src/integration/fastmail.ts b/libs/tools/generator/core/src/integration/fastmail.ts index b7e07b8f4617..e959da8674ba 100644 --- a/libs/tools/generator/core/src/integration/fastmail.ts +++ b/libs/tools/generator/core/src/integration/fastmail.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GENERATOR_DISK, GENERATOR_MEMORY, @@ -71,7 +72,13 @@ const createForwardingEmail = Object.freeze({ state: "enabled", description: "", forDomain: context.website(request), - emailPrefix: context.prefixEnabled() ? context.website(request) : "", + emailPrefix: context.prefixEnabled() + ? Utils.getDomain(context.website(request)) + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/_+/g, "_") + .substring(0, 64) + : "", }, }, }, From 358117732c356620c98201abfb049af722d8caeb Mon Sep 17 00:00:00 2001 From: Ben McFerren Date: Fri, 26 Jun 2026 12:16:36 -0400 Subject: [PATCH 3/4] PM-38122 resolved test passing related to pr comment --- libs/tools/generator/core/src/integration/fastmail.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tools/generator/core/src/integration/fastmail.spec.ts b/libs/tools/generator/core/src/integration/fastmail.spec.ts index 2704d6f3f51f..0b948351d997 100644 --- a/libs/tools/generator/core/src/integration/fastmail.spec.ts +++ b/libs/tools/generator/core/src/integration/fastmail.spec.ts @@ -135,7 +135,7 @@ describe("Fastmail forwarder", () => { const methodCall = result.methodCalls[0][1]; expect(methodCall.accountId).toEqual("accountId"); - expect(methodCall.create["new-masked-email"].emailPrefix).toEqual("example.com"); + expect(methodCall.create["new-masked-email"].emailPrefix).toEqual("example_com"); }); }); From 375c8410be57c6fe77f4c6a9c1e9a50e9a331f1c Mon Sep 17 00:00:00 2001 From: Ben McFerren Date: Fri, 26 Jun 2026 12:27:58 -0400 Subject: [PATCH 4/4] PM-38112 resolved pr comment --- .../generator/core/src/integration/fastmail.spec.ts | 12 ++++++++++++ .../tools/generator/core/src/integration/fastmail.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/libs/tools/generator/core/src/integration/fastmail.spec.ts b/libs/tools/generator/core/src/integration/fastmail.spec.ts index 0b948351d997..4ece7d5a2672 100644 --- a/libs/tools/generator/core/src/integration/fastmail.spec.ts +++ b/libs/tools/generator/core/src/integration/fastmail.spec.ts @@ -137,6 +137,18 @@ describe("Fastmail forwarder", () => { expect(methodCall.accountId).toEqual("accountId"); expect(methodCall.create["new-masked-email"].emailPrefix).toEqual("example_com"); }); + + it("creates a request body with empty email prefix when prefix is enabled but website is empty", () => { + context.website.mockReturnValue(""); + context.prefixEnabled.mockReturnValue(true); + const request = { accountId: "accountId", website: "" }; + + const result = Fastmail.forwarder.createForwardingEmail.body(request, context); + const methodCall = result.methodCalls[0][1]; + + expect(methodCall.accountId).toEqual("accountId"); + expect(methodCall.create["new-masked-email"].emailPrefix).toEqual(""); + }); }); describe("hasJsonPayload", () => { diff --git a/libs/tools/generator/core/src/integration/fastmail.ts b/libs/tools/generator/core/src/integration/fastmail.ts index e959da8674ba..177b53749f6f 100644 --- a/libs/tools/generator/core/src/integration/fastmail.ts +++ b/libs/tools/generator/core/src/integration/fastmail.ts @@ -73,7 +73,7 @@ const createForwardingEmail = Object.freeze({ description: "", forDomain: context.website(request), emailPrefix: context.prefixEnabled() - ? Utils.getDomain(context.website(request)) + ? (Utils.getDomain(context.website(request)) ?? "") .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/_+/g, "_")