@vlayer/sdk 0.1.0-nightly-20241204-02d2f89 → 0.1.0-nightly-20241205-223f353
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/email/parseEmail.d.ts +4 -3
- package/dist/api/email/preverify.d.ts +2 -0
- package/dist/api/email/preverify.js +25 -3
- package/dist/api/email/preverify.test.js +89 -5
- package/dist/api/utils/prefixAllButNthSubstring.d.ts +1 -0
- package/dist/api/utils/prefixAllButNthSubstring.js +8 -0
- package/dist/api/utils/prefixAllButNthSubstring.test.d.ts +1 -0
- package/dist/api/utils/prefixAllButNthSubstring.test.js +14 -0
- package/package.json +1 -1
@@ -2,9 +2,10 @@ import { Email } from "postal-mime";
|
|
2
2
|
export declare class DkimParsingError extends Error {
|
3
3
|
constructor(message: string);
|
4
4
|
}
|
5
|
-
export
|
6
|
-
export declare function getDkimSigners(mail: Email): {
|
5
|
+
export interface DkimDomainSelector {
|
7
6
|
domain: string;
|
8
7
|
selector: string;
|
9
|
-
}
|
8
|
+
}
|
9
|
+
export declare function parseEmail(mime: string): Promise<Email>;
|
10
|
+
export declare function getDkimSigners(mail: Email): DkimDomainSelector[];
|
10
11
|
export declare function parseParams(str: string): Record<string, string>;
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { DkimDomainSelector } from "./parseEmail.js";
|
2
|
+
export declare function findIndicesOfMatchingDomains(signers: DkimDomainSelector[], expectedOrigin: string): number[];
|
1
3
|
export declare function preverifyEmail(mimeEmail: string): Promise<{
|
2
4
|
email: string;
|
3
5
|
dnsRecords: string[];
|
@@ -1,13 +1,35 @@
|
|
1
|
-
import {
|
1
|
+
import { getDkimSigners, parseEmail } from "./parseEmail.js";
|
2
2
|
import { resolveDkimDns } from "./dnsResolver.js";
|
3
|
+
import { prefixAllButNthSubstring } from "../utils/prefixAllButNthSubstring.js";
|
4
|
+
export function findIndicesOfMatchingDomains(signers, expectedOrigin) {
|
5
|
+
return signers
|
6
|
+
.map(({ domain }) => expectedOrigin.endsWith(domain))
|
7
|
+
.map((isMatch, index) => (isMatch ? index : -1))
|
8
|
+
.filter((index) => index !== -1);
|
9
|
+
}
|
10
|
+
function requireSameOrigin(mimeEmail, signers, fromAddress) {
|
11
|
+
const matchingIndices = findIndicesOfMatchingDomains(signers, fromAddress);
|
12
|
+
if (matchingIndices.length != 1) {
|
13
|
+
throw new Error(`Found ${matchingIndices.length} DKIM headers matching the sender domain`);
|
14
|
+
}
|
15
|
+
const [matchingIndex] = matchingIndices;
|
16
|
+
return [
|
17
|
+
prefixAllButNthSubstring(mimeEmail, /^\s*dkim-signature/gim, signers.length, matchingIndex),
|
18
|
+
[signers[matchingIndex]],
|
19
|
+
];
|
20
|
+
}
|
3
21
|
export async function preverifyEmail(mimeEmail) {
|
4
22
|
const parsedEmail = await parseEmail(mimeEmail);
|
5
|
-
|
23
|
+
let signers = getDkimSigners(parsedEmail);
|
24
|
+
const fromAddress = parsedEmail.from.address;
|
25
|
+
if (!fromAddress) {
|
26
|
+
throw new Error("No from address found");
|
27
|
+
}
|
6
28
|
if (signers.length === 0) {
|
7
29
|
throw new Error("No DKIM header found");
|
8
30
|
}
|
9
31
|
if (signers.length > 1) {
|
10
|
-
|
32
|
+
[mimeEmail, signers] = requireSameOrigin(mimeEmail, signers, fromAddress);
|
11
33
|
}
|
12
34
|
const [{ domain, selector }] = signers;
|
13
35
|
const dnsRecord = await resolveDkimDns(domain, selector);
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
2
|
-
import { preverifyEmail } from "./preverify.js";
|
3
2
|
import { readFile } from "../../testHelpers/readFile.js";
|
3
|
+
import { findIndicesOfMatchingDomains, preverifyEmail } from "./preverify.js";
|
4
|
+
const rawEmail = readFile("./src/api/email/testdata/test_email.txt");
|
4
5
|
describe("Preverify email: integration", () => {
|
5
6
|
test("adds dns record to email mime", async () => {
|
6
|
-
const rawEmail = readFile("./src/api/email/testdata/test_email.txt");
|
7
7
|
const preverifiedEmail = await preverifyEmail(rawEmail);
|
8
8
|
expect(preverifiedEmail).toMatchObject({
|
9
9
|
email: rawEmail,
|
@@ -18,8 +18,92 @@ describe("Preverify email: integration", () => {
|
|
18
18
|
const emailWithNoDkimHeader = readFile("./src/api/email/testdata/test_email_unknown_domain.txt");
|
19
19
|
await expect(preverifyEmail(emailWithNoDkimHeader)).rejects.toThrow();
|
20
20
|
});
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
describe("multiple DKIM headers", () => {
|
22
|
+
function addDkimWithDomain(domain, email) {
|
23
|
+
return `DKIM-Signature: v=1; a=rsa-sha256; d=${domain};
|
24
|
+
s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
|
25
|
+
${email}`;
|
26
|
+
}
|
27
|
+
function addFakeDkimWithDomain(domain, email) {
|
28
|
+
return `X-${addDkimWithDomain(domain, email)}`;
|
29
|
+
}
|
30
|
+
test("looks for DKIM header with the domain matching the sender and removes all other DKIM headers", async () => {
|
31
|
+
const emailWithAddedHeaders = [
|
32
|
+
"example.com",
|
33
|
+
"hello.kitty",
|
34
|
+
"google.com",
|
35
|
+
].reduce((email, domain) => addDkimWithDomain(domain, email), rawEmail);
|
36
|
+
const email = await preverifyEmail(emailWithAddedHeaders);
|
37
|
+
expect(email.email
|
38
|
+
.startsWith(`X-DKIM-Signature: v=1; a=rsa-sha256; d=google.com;
|
39
|
+
s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
|
40
|
+
X-DKIM-Signature: v=1; a=rsa-sha256; d=hello.kitty;
|
41
|
+
s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
|
42
|
+
DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
|
43
|
+
s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
|
44
|
+
X-DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
|
45
|
+
c=simple/simple; d=google.com;`)).toBeTruthy();
|
46
|
+
expect(email.dnsRecords).toStrictEqual(["v=DKIM1; p="]);
|
47
|
+
});
|
48
|
+
test("throws error if no DNS record domain matches the sender", async () => {
|
49
|
+
const emailWithAddedHeaders = addDkimWithDomain("otherdomain.com", rawEmail);
|
50
|
+
await expect(preverifyEmail(emailWithAddedHeaders)).rejects.toThrow("Found 0 DKIM headers matching the sender domain");
|
51
|
+
});
|
52
|
+
test("throws error if multiple DNS record domains match the sender", async () => {
|
53
|
+
let emailWithAddedHeaders = addDkimWithDomain("example.com", rawEmail);
|
54
|
+
emailWithAddedHeaders = addDkimWithDomain("example.com", emailWithAddedHeaders);
|
55
|
+
await expect(preverifyEmail(emailWithAddedHeaders)).rejects.toThrow("Found 2 DKIM headers matching the sender domain");
|
56
|
+
});
|
57
|
+
test("ignores x-dkim-signature headers", async () => {
|
58
|
+
const emailWithPrefixedDkim = addDkimWithDomain("example.com", addFakeDkimWithDomain("example.com", rawEmail));
|
59
|
+
const email = await preverifyEmail(emailWithPrefixedDkim);
|
60
|
+
expect(email.email
|
61
|
+
.startsWith(`DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
|
62
|
+
s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
|
63
|
+
X-DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
|
64
|
+
s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
|
65
|
+
X-DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
|
66
|
+
c=simple/simple; d=google.com;`)).toBeTruthy();
|
67
|
+
});
|
68
|
+
test("ignores dkim-signature somewhere inside a header", async () => {
|
69
|
+
const headerWithDkim = `WTF-IS-THIS-HEADER: DKIM-SIGNATURE;`;
|
70
|
+
const emailWithDkimInHeader = `${headerWithDkim}\n${addDkimWithDomain("example.com", rawEmail)}`;
|
71
|
+
const email = await preverifyEmail(emailWithDkimInHeader);
|
72
|
+
expect(email.email.startsWith(`WTF-IS-THIS-HEADER: DKIM-SIGNATURE;
|
73
|
+
DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
|
74
|
+
s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
|
75
|
+
X-DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
|
76
|
+
c=simple/simple; d=google.com;`)).toBeTruthy();
|
77
|
+
});
|
78
|
+
test("ignores dkim-signature somewhere inside a body", async () => {
|
79
|
+
const emailWithAddedDkim = addDkimWithDomain("example.com", rawEmail);
|
80
|
+
const emailWithDkimsInBody = `${emailWithAddedDkim}\ndkim-signature dkim-signature\r\ndkim-signature`;
|
81
|
+
const email = await preverifyEmail(emailWithDkimsInBody);
|
82
|
+
expect(email.email
|
83
|
+
.startsWith(`DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
|
84
|
+
s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
|
85
|
+
X-DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
|
86
|
+
c=simple/simple; d=google.com;`)).toBeTruthy();
|
87
|
+
expect(email.email.endsWith(`\ndkim-signature dkim-signature\r\ndkim-signature`)).toBeTruthy();
|
88
|
+
});
|
89
|
+
});
|
90
|
+
});
|
91
|
+
describe("findIndicesOfMatchingDomains", () => {
|
92
|
+
test("returns indices of matching domains", () => {
|
93
|
+
const signers = [
|
94
|
+
{ domain: "example.com", selector: "selector1" },
|
95
|
+
{ domain: "other.other", selector: "selector2" },
|
96
|
+
{ domain: "example.com", selector: "selector3" },
|
97
|
+
];
|
98
|
+
expect(findIndicesOfMatchingDomains(signers, "example.com")).toStrictEqual([
|
99
|
+
0, 2,
|
100
|
+
]);
|
101
|
+
});
|
102
|
+
test("returns empty array if no matching domains", () => {
|
103
|
+
const signers = [
|
104
|
+
{ domain: "example.com", selector: "selector1" },
|
105
|
+
{ domain: "example.org", selector: "selector2" },
|
106
|
+
];
|
107
|
+
expect(findIndicesOfMatchingDomains(signers, "other.other")).toStrictEqual([]);
|
24
108
|
});
|
25
109
|
});
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare function prefixAllButNthSubstring(str: string, pattern: RegExp, substringsCount: number, skippedIndex: number): string;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
2
|
+
import { prefixAllButNthSubstring } from "./prefixAllButNthSubstring.js";
|
3
|
+
describe("prefixAllButNthSubstring", () => {
|
4
|
+
test("adds 'X-' prefix to all matches except n-th (indexed from 0)", () => {
|
5
|
+
const str = "abc 123 abc 456 abc 789";
|
6
|
+
expect(prefixAllButNthSubstring(str, /abc/gi, 3, 0)).toBe("abc 123 X-abc 456 X-abc 789");
|
7
|
+
expect(prefixAllButNthSubstring(str, /abc/gi, 3, 1)).toBe("X-abc 123 abc 456 X-abc 789");
|
8
|
+
expect(prefixAllButNthSubstring(str, /abc/gi, 3, 2)).toBe("X-abc 123 X-abc 456 abc 789");
|
9
|
+
});
|
10
|
+
test("does not add prefix to substrings past total substring count", () => {
|
11
|
+
const str = "abc 123 abc 456 abc 789 abc abc";
|
12
|
+
expect(prefixAllButNthSubstring(str, /abc/gi, 3, 1)).toBe("X-abc 123 abc 456 X-abc 789 abc abc");
|
13
|
+
});
|
14
|
+
});
|
package/package.json
CHANGED