1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-5843] add forward email forwarder (#7678)

This commit is contained in:
✨ Audrey ✨
2024-01-25 10:22:26 -05:00
committed by GitHub
parent 2c69810460
commit f6da6d637c
2 changed files with 328 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
/**
* include Request in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { Forwarders } from "../options/constants";
import { ForwardEmailForwarder } from "./forward-email";
import { mockApiService, mockI18nService } from "./mocks.jest";
describe("ForwardEmail Forwarder", () => {
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
const apiService = mockApiService(200, {});
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
await expect(
async () =>
await forwarder.generate(null, {
token,
domain: "example.com",
}),
).rejects.toEqual("forwaderInvalidToken");
expect(apiService.nativeFetch).not.toHaveBeenCalled();
expect(i18nService.t).toHaveBeenCalledWith(
"forwaderInvalidToken",
Forwarders.ForwardEmail.name,
);
});
it.each([null, ""])(
"throws an error if the domain is missing (domain = %p)",
async (domain) => {
const apiService = mockApiService(200, {});
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
await expect(
async () =>
await forwarder.generate(null, {
token: "token",
domain,
}),
).rejects.toEqual("forwarderNoDomain");
expect(apiService.nativeFetch).not.toHaveBeenCalled();
expect(i18nService.t).toHaveBeenCalledWith(
"forwarderNoDomain",
Forwarders.ForwardEmail.name,
);
},
);
it.each([
["forwarderGeneratedByWithWebsite", "provided", "bitwarden.com", "bitwarden.com"],
["forwarderGeneratedByWithWebsite", "provided", "httpbin.org", "httpbin.org"],
["forwarderGeneratedBy", "not provided", null, ""],
["forwarderGeneratedBy", "not provided", "", ""],
])(
"describes the website with %p when the website is %s (= %p)",
async (translationKey, _ignored, website, expectedWebsite) => {
const apiService = mockApiService(200, {});
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
await forwarder.generate(website, {
token: "token",
domain: "example.com",
});
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
expect(i18nService.t).toHaveBeenCalledWith(translationKey, expectedWebsite);
},
);
it.each([
["jane.doe@example.com", 201, { name: "jane.doe", domain: { name: "example.com" } }],
["jane.doe@example.com", 201, { name: "jane.doe" }],
["john.doe@example.com", 201, { name: "john.doe", domain: { name: "example.com" } }],
["john.doe@example.com", 201, { name: "john.doe" }],
["jane.doe@example.com", 200, { name: "jane.doe", domain: { name: "example.com" } }],
["jane.doe@example.com", 200, { name: "jane.doe" }],
["john.doe@example.com", 200, { name: "john.doe", domain: { name: "example.com" } }],
["john.doe@example.com", 200, { name: "john.doe" }],
])(
"returns the generated email address (= %p) if the request is successful (status = %p)",
async (email, status, response) => {
const apiService = mockApiService(status, response);
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
const result = await forwarder.generate(null, {
token: "token",
domain: "example.com",
});
expect(result).toEqual(email);
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request));
},
);
it("throws an invalid token error if the request fails with a 401", async () => {
const apiService = mockApiService(401, {});
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
await expect(
async () =>
await forwarder.generate(null, {
token: "token",
domain: "example.com",
}),
).rejects.toEqual("forwaderInvalidToken");
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request));
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
expect(i18nService.t).toHaveBeenNthCalledWith(
2,
"forwaderInvalidToken",
Forwarders.ForwardEmail.name,
undefined,
);
});
it("throws an invalid token error with a message if the request fails with a 401 and message", async () => {
const apiService = mockApiService(401, { message: "A message" });
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
await expect(
async () =>
await forwarder.generate(null, {
token: "token",
domain: "example.com",
}),
).rejects.toEqual("forwaderInvalidTokenWithMessage");
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request));
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
expect(i18nService.t).toHaveBeenNthCalledWith(
2,
"forwaderInvalidTokenWithMessage",
Forwarders.ForwardEmail.name,
"A message",
);
});
it.each([{}, null])(
"throws an unknown error if the request fails and no status (= %p) is provided",
async (json) => {
const apiService = mockApiService(500, json);
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
await expect(
async () =>
await forwarder.generate(null, {
token: "token",
domain: "example.com",
}),
).rejects.toEqual("forwarderUnknownError");
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request));
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
expect(i18nService.t).toHaveBeenNthCalledWith(
2,
"forwarderUnknownError",
Forwarders.ForwardEmail.name,
);
},
);
it.each([
[100, "Continue"],
[202, "Accepted"],
[300, "Multiple Choices"],
[418, "I'm a teapot"],
[500, "Internal Server Error"],
[600, "Unknown Status"],
])(
"throws an error with the status text if the request returns any other status code (= %i) and a status (= %p) is provided",
async (statusCode, message) => {
const apiService = mockApiService(statusCode, { message });
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
await expect(
async () =>
await forwarder.generate(null, {
token: "token",
domain: "example.com",
}),
).rejects.toEqual("forwarderError");
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request));
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
expect(i18nService.t).toHaveBeenNthCalledWith(
2,
"forwarderError",
Forwarders.ForwardEmail.name,
message,
);
},
);
it.each([
[100, "Continue"],
[202, "Accepted"],
[300, "Multiple Choices"],
[418, "I'm a teapot"],
[500, "Internal Server Error"],
[600, "Unknown Status"],
])(
"throws an error with the status text if the request returns any other status code (= %i) and a status (= %p) is provided",
async (statusCode, error) => {
const apiService = mockApiService(statusCode, { error });
const i18nService = mockI18nService();
const forwarder = new ForwardEmailForwarder(apiService, i18nService);
await expect(
async () =>
await forwarder.generate(null, {
token: "token",
domain: "example.com",
}),
).rejects.toEqual("forwarderError");
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request));
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
expect(i18nService.t).toHaveBeenNthCalledWith(
2,
"forwarderError",
Forwarders.ForwardEmail.name,
error,
);
},
);
});
});

View File

@@ -0,0 +1,79 @@
import { ApiService } from "../../../../abstractions/api.service";
import { I18nService } from "../../../../platform/abstractions/i18n.service";
import { Utils } from "../../../../platform/misc/utils";
import { Forwarders } from "../options/constants";
import { EmailDomainOptions, Forwarder, ApiOptions } from "../options/forwarder-options";
/** Generates a forwarding address for Forward Email */
export class ForwardEmailForwarder implements Forwarder {
/** Instantiates the forwarder
* @param apiService used for ajax requests to the forwarding service
* @param i18nService used to look up error strings
*/
constructor(
private apiService: ApiService,
private i18nService: I18nService,
) {}
/** {@link Forwarder.generate} */
async generate(
website: string | null,
options: ApiOptions & EmailDomainOptions,
): Promise<string> {
if (!options.token || options.token === "") {
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.ForwardEmail.name);
throw error;
}
if (!options.domain || options.domain === "") {
const error = this.i18nService.t("forwarderNoDomain", Forwarders.ForwardEmail.name);
throw error;
}
const url = `https://api.forwardemail.net/v1/domains/${options.domain}/aliases`;
const descriptionId =
website && website !== "" ? "forwarderGeneratedByWithWebsite" : "forwarderGeneratedBy";
const description = this.i18nService.t(descriptionId, website ?? "");
const request = new Request(url, {
redirect: "manual",
cache: "no-store",
method: "POST",
headers: new Headers({
Authorization: "Basic " + Utils.fromUtf8ToB64(options.token + ":"),
"Content-Type": "application/json",
}),
body: JSON.stringify({
labels: website,
description,
}),
});
const response = await this.apiService.nativeFetch(request);
const json = await response.json();
if (response.status === 401) {
const messageKey =
"message" in json ? "forwaderInvalidTokenWithMessage" : "forwaderInvalidToken";
const error = this.i18nService.t(messageKey, Forwarders.ForwardEmail.name, json.message);
throw error;
} else if (response.status === 200 || response.status === 201) {
const { name, domain } = await response.json();
const domainPart = domain?.name || options.domain;
return `${name}@${domainPart}`;
} else if (json?.message) {
const error = this.i18nService.t(
"forwarderError",
Forwarders.ForwardEmail.name,
json.message,
);
throw error;
} else if (json?.error) {
const error = this.i18nService.t("forwarderError", Forwarders.ForwardEmail.name, json.error);
throw error;
} else {
const error = this.i18nService.t("forwarderUnknownError", Forwarders.ForwardEmail.name);
throw error;
}
}
}