@bobfrankston/mailx-types 0.1.5 → 0.1.6
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/build-rules.js +33 -0
- package/contact-rules.d.ts +11 -0
- package/contact-rules.js +13 -0
- package/contact-rules.jsonc +28 -0
- package/groups.d.ts +38 -0
- package/groups.js +99 -0
- package/index.d.ts +3 -0
- package/index.js +9 -0
- package/package.json +5 -1
package/build-rules.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generates contact-rules.ts from contact-rules.jsonc.
|
|
4
|
+
*
|
|
5
|
+
* The .jsonc is the source-of-truth (editable, comments, versioned in
|
|
6
|
+
* source). The .ts is what mailx-store and mailx-store-web import — Android
|
|
7
|
+
* has no `fs` to parse JSONC at runtime, and inlining the parsed object
|
|
8
|
+
* sidesteps that constraint cleanly.
|
|
9
|
+
*
|
|
10
|
+
* Run via `npm run build` (prebuild hook) or directly: `node build-rules.js`.
|
|
11
|
+
* Both .jsonc and generated .ts are checked in; the .ts is regenerated on
|
|
12
|
+
* every build so its mtime is deterministic relative to the .jsonc.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
const __dirname = import.meta.dirname;
|
|
18
|
+
const srcPath = path.join(__dirname, "contact-rules.jsonc");
|
|
19
|
+
const outPath = path.join(__dirname, "contact-rules.ts");
|
|
20
|
+
|
|
21
|
+
const raw = fs.readFileSync(srcPath, "utf-8");
|
|
22
|
+
// Loose JSONC handler: strip // line comments and trailing commas. Avoids
|
|
23
|
+
// pulling in jsonc-parser as a dep for what's effectively config.
|
|
24
|
+
const stripped = raw
|
|
25
|
+
.replace(/^\s*\/\/.*$/gm, "")
|
|
26
|
+
.replace(/,(\s*[}\]])/g, "$1");
|
|
27
|
+
const obj = JSON.parse(stripped);
|
|
28
|
+
|
|
29
|
+
const banner = "// AUTO-GENERATED from contact-rules.jsonc — do not edit.\n" +
|
|
30
|
+
"// Edit the .jsonc and run `node build-rules.js` (or `npm run build`).\n\n";
|
|
31
|
+
const ts = banner + `export const CONTACT_RULES = ${JSON.stringify(obj, null, 4)} as const;\n`;
|
|
32
|
+
fs.writeFileSync(outPath, ts);
|
|
33
|
+
console.log(`[mailx-types] regenerated ${path.basename(outPath)}`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const CONTACT_RULES: {
|
|
2
|
+
readonly rulesVersion: "v3-domain-oneoff";
|
|
3
|
+
readonly junk: {
|
|
4
|
+
readonly localExact: "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$";
|
|
5
|
+
readonly localSuffix: "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$";
|
|
6
|
+
readonly localPrefix: "^(no-?reply|noreply|do-?not-?reply|donotreply|notifications?|alerts?|bounces?|mailer)[-_+]";
|
|
7
|
+
readonly localOneoff: "^[0-9a-f]{4}\\.[0-9a-f]{4}(\\.[0-9a-z]{6})?$";
|
|
8
|
+
readonly domain: "^(txt\\.voice\\.google\\.com|reply\\.facebook\\.com|reply\\.linkedin\\.com)$";
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=contact-rules.d.ts.map
|
package/contact-rules.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// AUTO-GENERATED from contact-rules.jsonc — do not edit.
|
|
2
|
+
// Edit the .jsonc and run `node build-rules.js` (or `npm run build`).
|
|
3
|
+
export const CONTACT_RULES = {
|
|
4
|
+
"rulesVersion": "v3-domain-oneoff",
|
|
5
|
+
"junk": {
|
|
6
|
+
"localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$",
|
|
7
|
+
"localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$",
|
|
8
|
+
"localPrefix": "^(no-?reply|noreply|do-?not-?reply|donotreply|notifications?|alerts?|bounces?|mailer)[-_+]",
|
|
9
|
+
"localOneoff": "^[0-9a-f]{4}\\.[0-9a-f]{4}(\\.[0-9a-z]{6})?$",
|
|
10
|
+
"domain": "^(txt\\.voice\\.google\\.com|reply\\.facebook\\.com|reply\\.linkedin\\.com)$"
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=contact-rules.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// mailx — global contact-filter rules (data, NOT user prefs).
|
|
2
|
+
//
|
|
3
|
+
// Edit the regex strings below; run `node build-rules.js` from this package
|
|
4
|
+
// (or `npm run build`) to regenerate `contact-rules.ts`, which is what
|
|
5
|
+
// mailx-store and mailx-store-web import. The .ts export is generated;
|
|
6
|
+
// the .jsonc you're reading is the source of truth.
|
|
7
|
+
//
|
|
8
|
+
// rulesVersion: bump whenever the patterns tighten. The contacts table
|
|
9
|
+
// runs a one-shot purge keyed off this string so historical rows under
|
|
10
|
+
// older rules get cleaned up exactly once per device.
|
|
11
|
+
//
|
|
12
|
+
// Rule shape:
|
|
13
|
+
// localExact — whole local-part match (e.g. "noreply", "mailer-daemon")
|
|
14
|
+
// localSuffix — local-part ends with (e.g. "-bounces", "+bounces")
|
|
15
|
+
// localPrefix — local-part starts with + separator (e.g. "noreply-<rand>")
|
|
16
|
+
// localOneoff — full-shape match for per-message gateway addresses
|
|
17
|
+
// (e.g. "16179691997.15082868100.nfblcbll1x")
|
|
18
|
+
// domain — entire domain match (Google Voice SMS, reply.fb.com)
|
|
19
|
+
{
|
|
20
|
+
"rulesVersion": "v3-domain-oneoff",
|
|
21
|
+
"junk": {
|
|
22
|
+
"localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$",
|
|
23
|
+
"localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$",
|
|
24
|
+
"localPrefix": "^(no-?reply|noreply|do-?not-?reply|donotreply|notifications?|alerts?|bounces?|mailer)[-_+]",
|
|
25
|
+
"localOneoff": "^[0-9a-f]{4,}\\.[0-9a-f]{4,}(\\.[0-9a-z]{6,})?$",
|
|
26
|
+
"domain": "^(txt\\.voice\\.google\\.com|reply\\.facebook\\.com|reply\\.linkedin\\.com)$"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/groups.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group expansion — turns a recipient string with group names into a flat
|
|
3
|
+
* deduplicated address list. Shared between desktop (mailx-service) and
|
|
4
|
+
* Android (mailx-store-web) so send paths produce the same result.
|
|
5
|
+
*
|
|
6
|
+
* Group source: contacts.jsonc → groups: Record<string, string[]>. Each
|
|
7
|
+
* value is an array of either email addresses ("a@b" or "Name <a@b>") or
|
|
8
|
+
* names of other groups (recursive aliasing). Cycles are detected; depth
|
|
9
|
+
* is capped at 10 nested levels.
|
|
10
|
+
*/
|
|
11
|
+
/** Raw recipient string from a To/Cc/Bcc field; may contain group names. */
|
|
12
|
+
export type RecipientToken = string;
|
|
13
|
+
/** Map of group name → member list (each entry: address or another group name). */
|
|
14
|
+
export type GroupMap = Record<string, string[]>;
|
|
15
|
+
/** Result of expanding a recipient string. */
|
|
16
|
+
export interface ExpansionResult {
|
|
17
|
+
/** Flat unique address list (preserving order of first occurrence). */
|
|
18
|
+
addresses: string[];
|
|
19
|
+
/** Group names that couldn't be resolved (typo / cycle) — surface in UI. */
|
|
20
|
+
unresolved: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Split a comma-or-semicolon recipient string into tokens. Respects
|
|
23
|
+
* angle-bracket address blocks ("Name, Suffix <a@b>") so a comma inside
|
|
24
|
+
* the display name doesn't split the entry. */
|
|
25
|
+
export declare function splitRecipients(raw: string): RecipientToken[];
|
|
26
|
+
/** Quick test: does the token look like an email address (with or without
|
|
27
|
+
* a display name)? Anything containing "@" with non-whitespace on both
|
|
28
|
+
* sides counts. */
|
|
29
|
+
export declare function isAddressToken(token: string): boolean;
|
|
30
|
+
/** Extract the bare email address from a token. Returns the original token
|
|
31
|
+
* lowercased if it has no angle-bracket form. */
|
|
32
|
+
export declare function extractAddress(token: string): string;
|
|
33
|
+
/** Expand a recipient string by resolving group names against `groups`.
|
|
34
|
+
* Group names take precedence: if a token matches a group name AND looks
|
|
35
|
+
* like an address, it's treated as a group. (In practice no one names a
|
|
36
|
+
* group "x@y.com", so this is rarely a real conflict.) */
|
|
37
|
+
export declare function expandRecipients(raw: string, groups: GroupMap): ExpansionResult;
|
|
38
|
+
//# sourceMappingURL=groups.d.ts.map
|
package/groups.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group expansion — turns a recipient string with group names into a flat
|
|
3
|
+
* deduplicated address list. Shared between desktop (mailx-service) and
|
|
4
|
+
* Android (mailx-store-web) so send paths produce the same result.
|
|
5
|
+
*
|
|
6
|
+
* Group source: contacts.jsonc → groups: Record<string, string[]>. Each
|
|
7
|
+
* value is an array of either email addresses ("a@b" or "Name <a@b>") or
|
|
8
|
+
* names of other groups (recursive aliasing). Cycles are detected; depth
|
|
9
|
+
* is capped at 10 nested levels.
|
|
10
|
+
*/
|
|
11
|
+
/** Split a comma-or-semicolon recipient string into tokens. Respects
|
|
12
|
+
* angle-bracket address blocks ("Name, Suffix <a@b>") so a comma inside
|
|
13
|
+
* the display name doesn't split the entry. */
|
|
14
|
+
export function splitRecipients(raw) {
|
|
15
|
+
const out = [];
|
|
16
|
+
let buf = "";
|
|
17
|
+
let depth = 0;
|
|
18
|
+
for (let i = 0; i < raw.length; i++) {
|
|
19
|
+
const c = raw[i];
|
|
20
|
+
if (c === "<")
|
|
21
|
+
depth++;
|
|
22
|
+
else if (c === ">")
|
|
23
|
+
depth = Math.max(0, depth - 1);
|
|
24
|
+
if ((c === "," || c === ";") && depth === 0) {
|
|
25
|
+
const t = buf.trim();
|
|
26
|
+
if (t)
|
|
27
|
+
out.push(t);
|
|
28
|
+
buf = "";
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
buf += c;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const t = buf.trim();
|
|
35
|
+
if (t)
|
|
36
|
+
out.push(t);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** Quick test: does the token look like an email address (with or without
|
|
40
|
+
* a display name)? Anything containing "@" with non-whitespace on both
|
|
41
|
+
* sides counts. */
|
|
42
|
+
export function isAddressToken(token) {
|
|
43
|
+
return /^[^@\s][^@]*@[^@\s]+(\s|$)|<[^>]+@[^>]+>/.test(token) || /^[^\s<>@]+@[^\s<>@]+$/.test(token);
|
|
44
|
+
}
|
|
45
|
+
/** Extract the bare email address from a token. Returns the original token
|
|
46
|
+
* lowercased if it has no angle-bracket form. */
|
|
47
|
+
export function extractAddress(token) {
|
|
48
|
+
const m = token.match(/<([^>]+)>/);
|
|
49
|
+
if (m)
|
|
50
|
+
return m[1].trim().toLowerCase();
|
|
51
|
+
return token.trim().toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
/** Expand a recipient string by resolving group names against `groups`.
|
|
54
|
+
* Group names take precedence: if a token matches a group name AND looks
|
|
55
|
+
* like an address, it's treated as a group. (In practice no one names a
|
|
56
|
+
* group "x@y.com", so this is rarely a real conflict.) */
|
|
57
|
+
export function expandRecipients(raw, groups) {
|
|
58
|
+
const tokens = splitRecipients(raw);
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const addresses = [];
|
|
61
|
+
const unresolved = [];
|
|
62
|
+
const visit = (token, depth, visited) => {
|
|
63
|
+
if (depth > 10) {
|
|
64
|
+
unresolved.push(`${token} (depth limit)`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Group name match — case-insensitive
|
|
68
|
+
const groupKey = Object.keys(groups).find(k => k.toLowerCase() === token.trim().toLowerCase());
|
|
69
|
+
if (groupKey) {
|
|
70
|
+
if (visited.has(groupKey.toLowerCase())) {
|
|
71
|
+
unresolved.push(`${groupKey} (cycle)`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const next = new Set(visited);
|
|
75
|
+
next.add(groupKey.toLowerCase());
|
|
76
|
+
for (const member of groups[groupKey] || []) {
|
|
77
|
+
visit(member, depth + 1, next);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Address — keep the original (with display name if present), but
|
|
82
|
+
// dedupe by lowercased bare address.
|
|
83
|
+
if (isAddressToken(token)) {
|
|
84
|
+
const key = extractAddress(token);
|
|
85
|
+
if (!seen.has(key)) {
|
|
86
|
+
seen.add(key);
|
|
87
|
+
addresses.push(token.trim());
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Neither group nor address — flag.
|
|
92
|
+
unresolved.push(token);
|
|
93
|
+
};
|
|
94
|
+
for (const t of tokens) {
|
|
95
|
+
visit(t, 0, new Set());
|
|
96
|
+
}
|
|
97
|
+
return { addresses, unresolved };
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=groups.js.map
|
package/index.d.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Shared type definitions for the mailx email client.
|
|
4
4
|
* This is the contract between client and server.
|
|
5
5
|
*/
|
|
6
|
+
export { CONTACT_RULES } from "./contact-rules.js";
|
|
7
|
+
export { expandRecipients, splitRecipients, isAddressToken, extractAddress, } from "./groups.js";
|
|
8
|
+
export type { GroupMap, RecipientToken, ExpansionResult } from "./groups.js";
|
|
6
9
|
/** Supported authentication methods */
|
|
7
10
|
export type AuthMethod = "password" | "oauth2";
|
|
8
11
|
/** Mail account configuration */
|
package/index.js
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
* Shared type definitions for the mailx email client.
|
|
4
4
|
* This is the contract between client and server.
|
|
5
5
|
*/
|
|
6
|
+
// Generated rule data — both desktop store and Android store import via
|
|
7
|
+
// this barrel so a single source-of-truth (contact-rules.jsonc) drives
|
|
8
|
+
// junk-contact filtering on every platform.
|
|
9
|
+
export { CONTACT_RULES } from "./contact-rules.js";
|
|
10
|
+
// Group-name expansion for recipient fields. Lets users type a group name
|
|
11
|
+
// (e.g. "family") in To/Cc/Bcc and have it expand to the address list at
|
|
12
|
+
// send time. Both desktop and Android send paths consume this expander
|
|
13
|
+
// against contacts.jsonc → groups.
|
|
14
|
+
export { expandRecipients, splitRecipients, isAddressToken, extractAddress, } from "./groups.js";
|
|
6
15
|
// ── Shared Utilities ──
|
|
7
16
|
// Pure functions used by both desktop (mailx-service) and Android (web-service).
|
|
8
17
|
// Kept here to avoid duplication — both platforms import from mailx-types.
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-types",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
+
"prebuild": "node build-rules.js",
|
|
8
9
|
"build": "tsc",
|
|
9
10
|
"release": "npmglobalize"
|
|
10
11
|
},
|
|
@@ -12,5 +13,8 @@
|
|
|
12
13
|
"repository": {
|
|
13
14
|
"type": "git",
|
|
14
15
|
"url": "https://github.com/BobFrankston/mailx-types.git"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
15
19
|
}
|
|
16
20
|
}
|