| 1 | import * as gmailpostmastertools from "@distilled.cloud/gcp/gmailpostmastertools_v2"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Stream from "effect/Stream"; |
| 4 | import { createPhysicalName } from "../../PhysicalName.ts"; |
| 5 | |
| 6 | export const DEFAULT_DOMAIN_SUFFIX = "example.com"; |
| 7 | export const DEFAULT_EMAIL_DOMAIN = "example.com"; |
| 8 | export const DEFAULT_PERMISSION = "READER"; |
| 9 | export const DOMAIN_PREFIX = "domains/"; |
| 10 | export const USERS_SEGMENT = "/users/"; |
| 11 | |
| 12 | const emptyList = <A>() => Effect.succeed([] as A[]); |
| 13 | |
| 14 | export const lastSegment = (value: string) => { |
| 15 | const trimmed = value.replace(/\/+$/, ""); |
| 16 | const parts = trimmed.split("/"); |
| 17 | return parts[parts.length - 1] || trimmed; |
| 18 | }; |
| 19 | |
| 20 | export const sameText = (left: string | undefined, right: string | undefined) => |
| 21 | (left ?? "") === (right ?? ""); |
| 22 | |
| 23 | export const normalize = (value: string | undefined) => |
| 24 | (value ?? "").trim().toLowerCase(); |
| 25 | |
| 26 | export const toDomainName = (value: string) => { |
| 27 | const trimmed = value.trim(); |
| 28 | if (trimmed.length === 0) return ""; |
| 29 | if (trimmed.startsWith(DOMAIN_PREFIX)) return trimmed; |
| 30 | return `${DOMAIN_PREFIX}${trimmed}`; |
| 31 | }; |
| 32 | |
| 33 | export const domainIdOf = (value: string | undefined) => { |
| 34 | const trimmed = (value ?? "").trim(); |
| 35 | if (trimmed.length === 0) return ""; |
| 36 | if (trimmed.startsWith(DOMAIN_PREFIX)) { |
| 37 | return trimmed.slice(DOMAIN_PREFIX.length).split("/")[0] ?? trimmed; |
| 38 | } |
| 39 | return trimmed; |
| 40 | }; |
| 41 | |
| 42 | export const userIdOf = (value: string | undefined) => { |
| 43 | if (value === undefined || value.length === 0) return ""; |
| 44 | const usersAt = value.indexOf(USERS_SEGMENT); |
| 45 | if (usersAt >= 0) return value.slice(usersAt + USERS_SEGMENT.length); |
| 46 | if (value.startsWith("users/")) return value.slice("users/".length); |
| 47 | return lastSegment(value); |
| 48 | }; |
| 49 | |
| 50 | export const toUserName = (parent: string, userId: string) => { |
| 51 | if (userId.startsWith(DOMAIN_PREFIX) && userId.includes(USERS_SEGMENT)) { |
| 52 | return userId; |
| 53 | } |
| 54 | const domain = toDomainName(parent); |
| 55 | const user = userId.startsWith("users/") |
| 56 | ? userId.slice("users/".length) |
| 57 | : userIdOf(userId) || userId; |
| 58 | if (domain.length === 0 || user.length === 0) return ""; |
| 59 | return `${domain}${USERS_SEGMENT}${user}`; |
| 60 | }; |
| 61 | |
| 62 | export const parentOfUserName = (name: string) => { |
| 63 | const usersAt = name.indexOf(USERS_SEGMENT); |
| 64 | if (usersAt > 0) return name.slice(0, usersAt); |
| 65 | return ""; |
| 66 | }; |
| 67 | |
| 68 | export const userEmailOf = (user: gmailpostmastertools.User) => |
| 69 | user.user ?? userIdOf(user.name); |
| 70 | |
| 71 | export const fullUserName = ( |
| 72 | parent: string, |
| 73 | user: gmailpostmastertools.User, |
| 74 | ) => { |
| 75 | const name = user.name ?? ""; |
| 76 | if (name.startsWith(DOMAIN_PREFIX) && name.includes(USERS_SEGMENT)) { |
| 77 | return name; |
| 78 | } |
| 79 | return toUserName(parent, userEmailOf(user) || name); |
| 80 | }; |
| 81 | |
| 82 | const dnsLabel = (value: string, maxLength: number) => { |
| 83 | let label = /^[a-z]/.test(value) ? value : `a${value}`; |
| 84 | label = label.replace(/^-+|-+$/g, ""); |
| 85 | if (!label.startsWith("alchemy-")) { |
| 86 | label = `alchemy-${label}`; |
| 87 | } |
| 88 | return label.slice(0, maxLength).replace(/-+$/g, ""); |
| 89 | }; |
| 90 | |
| 91 | export const isAlchemyDomain = (name: string | undefined) => { |
| 92 | const id = domainIdOf(name).toLowerCase(); |
| 93 | const first = id.split(".")[0] ?? ""; |
| 94 | return first.startsWith("alchemy-") || id.includes("alchemy-"); |
| 95 | }; |
| 96 | |
| 97 | export const isAlchemyEmail = (email: string | undefined) => { |
| 98 | const value = normalize(email); |
| 99 | const local = value.split("@")[0] ?? ""; |
| 100 | return local.startsWith("alchemy-") || local.includes("+alchemy-"); |
| 101 | }; |
| 102 | |
| 103 | export const isPatchablePermission = (permission: string | undefined) => |
| 104 | permission === "READER" || permission === "ADMIN"; |
| 105 | |
| 106 | export const toGeneratedDomainId = ( |
| 107 | id: string, |
| 108 | requested: string | undefined, |
| 109 | existing: string | undefined, |
| 110 | ) => |
| 111 | Effect.gen(function* () { |
| 112 | if (requested !== undefined && requested.length > 0) { |
| 113 | return requested.toLowerCase(); |
| 114 | } |
| 115 | if (existing !== undefined && existing.length > 0) { |
| 116 | return existing.toLowerCase(); |
| 117 | } |
| 118 | const generated = yield* createPhysicalName({ |
| 119 | id, |
| 120 | maxLength: 48, |
| 121 | lowercase: true, |
| 122 | }); |
| 123 | const label = dnsLabel(generated, 63); |
| 124 | return `${label}.${DEFAULT_DOMAIN_SUFFIX}`; |
| 125 | }); |
| 126 | |
| 127 | export const toGeneratedUserId = ( |
| 128 | id: string, |
| 129 | requested: string | undefined, |
| 130 | existing: string | undefined, |
| 131 | ) => |
| 132 | Effect.gen(function* () { |
| 133 | if (requested !== undefined && requested.length > 0) { |
| 134 | return requested.toLowerCase(); |
| 135 | } |
| 136 | if (existing !== undefined && existing.length > 0) { |
| 137 | return existing.toLowerCase(); |
| 138 | } |
| 139 | const generated = yield* createPhysicalName({ |
| 140 | id, |
| 141 | maxLength: 40, |
| 142 | lowercase: true, |
| 143 | }); |
| 144 | const local = dnsLabel(generated, 64); |
| 145 | return `${local}@${DEFAULT_EMAIL_DOMAIN}`; |
| 146 | }); |
| 147 | |
| 148 | export const catchMissing = <A, E extends { readonly _tag: string }, R>( |
| 149 | effect: Effect.Effect<A, E, R>, |
| 150 | ) => |
| 151 | effect.pipe( |
| 152 | Effect.catchIf( |
| 153 | (error): error is E & { readonly _tag: "NotFound" } => |
| 154 | error._tag === "NotFound", |
| 155 | () => Effect.succeed(undefined), |
| 156 | ), |
| 157 | ); |
| 158 | |
| 159 | export const ignoreMissing = <E extends { readonly _tag: string }, R>( |
| 160 | effect: Effect.Effect<unknown, E, R>, |
| 161 | ) => |
| 162 | effect.pipe( |
| 163 | Effect.catchIf( |
| 164 | (error): error is E & { readonly _tag: "NotFound" } => |
| 165 | error._tag === "NotFound", |
| 166 | () => Effect.void, |
| 167 | ), |
| 168 | ); |
| 169 | |
| 170 | export const getDomain = (name: string) => |
| 171 | name.length === 0 |
| 172 | ? Effect.succeed(undefined) |
| 173 | : catchMissing( |
| 174 | gmailpostmastertools.getDomains({ name: toDomainName(name) }), |
| 175 | ); |
| 176 | |
| 177 | export const getUser = (name: string) => |
| 178 | name.length === 0 |
| 179 | ? Effect.succeed(undefined) |
| 180 | : catchMissing(gmailpostmastertools.getDomainsUsers({ name })); |
| 181 | |
| 182 | export const listDomains = () => |
| 183 | gmailpostmastertools.listDomains.pages({ pageSize: 200 }).pipe( |
| 184 | Stream.flatMap((page) => Stream.fromIterable(page.domains ?? [])), |
| 185 | Stream.runCollect, |
| 186 | Effect.map((chunk) => Array.from(chunk)), |
| 187 | Effect.catchTag("NotFound", () => emptyList<gmailpostmastertools.Domain>()), |
| 188 | ); |
| 189 | |
| 190 | export const listUsers = (parent: string) => |
| 191 | parent.length === 0 |
| 192 | ? emptyList<gmailpostmastertools.User>() |
| 193 | : gmailpostmastertools.listDomainsUsers |
| 194 | .pages({ parent: toDomainName(parent), pageSize: 200 }) |
| 195 | .pipe( |
| 196 | Stream.flatMap((page) => Stream.fromIterable(page.users ?? [])), |
| 197 | Stream.runCollect, |
| 198 | Effect.map((chunk) => Array.from(chunk)), |
| 199 | Effect.catchTag("NotFound", () => |
| 200 | emptyList<gmailpostmastertools.User>(), |
| 201 | ), |
| 202 | ); |
| 203 | |
| 204 | export const listOwnedDomains = () => |
| 205 | listDomains().pipe( |
| 206 | Effect.map((domains) => |
| 207 | domains.filter((domain) => isAlchemyDomain(domain.name)), |
| 208 | ), |
| 209 | ); |
| 210 | |
| 211 | export const listOwnedUsers = () => |
| 212 | Effect.gen(function* () { |
| 213 | const domains = yield* listDomains(); |
| 214 | const users = yield* Effect.forEach( |
| 215 | domains.filter((domain) => (domain.name ?? "").length > 0), |
| 216 | (domain) => listUsers(domain.name ?? ""), |
| 217 | { concurrency: 4 }, |
| 218 | ); |
| 219 | return users.flat().filter((user) => isAlchemyEmail(userEmailOf(user))); |
| 220 | }); |
| 221 | |
| 222 | export const findUser = (parent: string, userId: string, name: string) => |
| 223 | Effect.gen(function* () { |
| 224 | const existing = yield* getUser(name || toUserName(parent, userId)); |
| 225 | if (existing !== undefined) return existing; |
| 226 | if (userId.length === 0 && name.length === 0) return undefined; |
| 227 | const users = yield* listUsers(parent); |
| 228 | const wanted = normalize(userId || userIdOf(name)); |
| 229 | return users.find( |
| 230 | (user) => |
| 231 | normalize(userEmailOf(user)) === wanted || |
| 232 | normalize(user.name) === normalize(name), |
| 233 | ); |
| 234 | }); |