Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3909,6 +3909,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."
Expand Down
10 changes: 9 additions & 1 deletion apps/cli/src/locales/en/messages.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ο»Ώ{
{
"bitwarden": {
"message": "Bitwarden"
},
Expand Down Expand Up @@ -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."
},
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3406,6 +3406,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."
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -9062,6 +9062,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."
Expand Down
2 changes: 1 addition & 1 deletion libs/common/src/tools/extension/vendor/fastmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ export const FastmailExtensions: ExtensionMetadata[] = [
selfHost: "maybe",
baseUrl: "https://api.fastmail.com",
},
requestedFields: [Field.token],
requestedFields: [Field.token, Field.prefix],
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
></button>
</bit-form-field>
}
@if (displayPrefix) {
<bit-form-control>
<input bitCheckbox type="checkbox" formControlName="prefix" (change)="save('prefix')" />
<bit-label>{{ "forwarderWebsitePrefix" | i18n }}</bit-label>
<bit-hint>{{ "forwarderWebsitePrefixHint" | i18n }}</bit-hint>
</bit-form-control>
}
@if (displayBaseUrl) {
<bit-form-field disableMargin>
<bit-label>{{ "selfHostBaseUrl" | i18n }}</bit-label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
AriaDisableDirective,
TooltipDirective,
BitIconButtonComponent,
CheckboxModule,
} from "@bitwarden/components";
import {
CredentialGeneratorService,
Expand All @@ -31,6 +32,7 @@ const Controls = Object.freeze({
domain: "domain",
token: "token",
baseUrl: "baseUrl",
prefix: "prefix",
});

/** Options group for forwarder integrations */
Expand All @@ -45,6 +47,7 @@ const Controls = Object.freeze({
AriaDisableDirective,
TooltipDirective,
BitIconButtonComponent,
CheckboxModule,
JslibModule,
I18nPipe,
],
Expand Down Expand Up @@ -90,6 +93,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
[Controls.domain]: [""],
[Controls.token]: [""],
[Controls.baseUrl]: [""],
[Controls.prefix]: [false],
});

private vendor = new ReplaySubject<VendorId>(1);
Expand All @@ -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);
});
Expand All @@ -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
Expand Down Expand Up @@ -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);
});
}

Expand All @@ -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<void>();

Expand Down
42 changes: 42 additions & 0 deletions libs/tools/generator/core/src/engine/forwarder-context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,48 @@ describe("ForwarderContext", () => {
});
});

describe("prefixEnabled", () => {
it("returns true when prefix is set to the 'website' sentinel", () => {
const settings = mock<EmailPrefixSettings & ApiSettings>({ prefix: "website" });
const config = mock<ForwarderConfiguration<typeof settings>>();
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<EmailPrefixSettings & ApiSettings>({ prefix });
const config = mock<ForwarderConfiguration<typeof settings>>();
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<ForwarderConfiguration<typeof settings>>();
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<EmailPrefixSettings & ApiSettings>({ prefix: "some-other-value" });
const config = mock<ForwarderConfiguration<typeof settings>>();
const context = new ForwarderContext(config, settings, i18n);

const result = context.prefixEnabled();

expect(result).toBe(false);
});
});

describe("missingAccountIdCause", () => {
it("returns the cause", () => {
const settings = mock<EmailDomainSettings & ApiSettings>();
Expand Down
9 changes: 9 additions & 0 deletions libs/tools/generator/core/src/engine/forwarder-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ export class ForwarderContext<Settings extends ApiSettings> 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
Expand Down
28 changes: 27 additions & 1 deletion libs/tools/generator/core/src/integration/fastmail.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,41 @@ 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);
const methodCall = result.methodCalls[0][1];

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");
});

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("");
});
});

Expand Down
9 changes: 8 additions & 1 deletion libs/tools/generator/core/src/integration/fastmail.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -71,7 +72,13 @@ const createForwardingEmail = Object.freeze({
state: "enabled",
description: "",
forDomain: context.website(request),
emailPrefix: "",
emailPrefix: context.prefixEnabled()
? (Utils.getDomain(context.website(request)) ?? "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/_+/g, "_")
.substring(0, 64)
: "",
Comment thread
bmbitwarden marked this conversation as resolved.
},
},
},
Expand Down
Loading