import { randAlphaNumeric, randEmail } from "@ngneat/falso" // import { createHash } from "crypto"; // change the number depending on the version of the extension, this is to prevent conflicts with breaking changes const PREFIX = "aliasaddon_2" export interface Alias { // id that mailcow assigns alias id: number // domain of alias domain: string targetAddress: string address: string active: boolean created: number // modified: Date // hash of site siteHash: string } enum GenerationMethod { RandomCharacters = 0, RandomName = 1 // WebsiteURL = 2 } export interface Settings { host?: string apiKey?: string forwardAddress?: string aliasDomain: string | null generationMethod: GenerationMethod } interface FetchAliasData { id: number domain: string // private comment includes the site hash and will start with aliasextension_ if it was generated by this extension private_comment: string | null // target address goto: string // alias address address: string active: number active_int: number created: string // modified will be null if the alias has never been modified modified: string | null } export async function fetchDomains( settings: Required ): Promise { const controller = new AbortController() const id = setTimeout(() => controller.abort(), 5000) const data: { domain_name: string aliases_left: number active: number active_int: number }[] = await ( await fetch(`${settings.host}/api/v1/get/domain/all`, { headers: { "X-API-Key": settings.apiKey }, signal: controller.signal }) ).json() clearTimeout(id) return data .filter((domain) => domain.active === 1 && domain.aliases_left > 0) .map((domain) => domain.domain_name) } export async function fetchAliases( settings: Required ): Promise { const data: FetchAliasData[] = await ( await fetch(`${settings.host}/api/v1/get/alias/all`, { headers: { "X-API-Key": settings.apiKey } }) ).json() return data .filter( // make sure alias is active and was generated by this extension (alias) => alias.private_comment && alias.private_comment.startsWith(PREFIX) ) .map((alias) => { // format: prefix, version, hash, timestamp const info = alias.private_comment!.split("_") return { id: alias.id, domain: alias.domain, targetAddress: alias.goto, address: alias.address, active: alias.active === 1, created: parseInt(info[3]), // modified: alias.modified ? new Date(`${alias.modified}.000Z`) : null, siteHash: info[2] } }) } export async function generateAlias( settings: Required, hostname?: string ): Promise { const address = generateEmail(settings, hostname) // const hash = await generateHash(hostname ?? "no") const hash = hostname ?? "nohostname" // although mailcow has its own date, the format they use sucks const createdAt = Date.now() const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 5000) // first in msg array should be "alias_added", second is the address, third is the id as a string const data = await ( await fetch(`${settings.host}/api/v1/add/alias`, { headers: { "Content-Type": "application/json", "X-API-Key": settings.apiKey }, method: "POST", body: JSON.stringify({ address, goto: settings.forwardAddress, active: "1", private_comment: `${PREFIX}_${hash}_${createdAt}` }), signal: controller.signal }) ).json() clearTimeout(timeoutId) if (data[0].type !== "success") { throw new Error("Failed to generate alias") } return { id: parseInt(data[0].msg[2]), domain: settings.aliasDomain!, targetAddress: settings.apiKey, address, active: true, // mailcow returns dates in weird format so not using them (https://github.com/mailcow/mailcow-dockerized/issues/4876) created: createdAt, // modified: null, siteHash: hash } } export async function updateAlias( id: number, settings: Settings, active: 0 | 1 ): Promise { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 5000) const data = await ( await fetch(`${settings.host}/api/v1/edit/alias/${id}`, { headers: { "Content-Type": "application/json", "X-API-Key": settings.apiKey! }, method: "POST", body: JSON.stringify({ attr: { active: active.toString() }, items: [id] }), signal: controller.signal }) ).json() clearTimeout(timeoutId) return data[0].type === "success" } export async function deleteAlias( id: number, settings: Settings ): Promise { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 5000) const data = await ( await fetch(`${settings.host}/api/v1/delete/alias`, { headers: { "Content-Type": "application/json", "X-API-Key": settings.apiKey! }, method: "POST", body: JSON.stringify([id]), signal: controller.signal }) ).json() clearTimeout(timeoutId) return data[0].type === "success" } // export function generateHash(data: string) { // return createHash("sha256").update(data).digest("hex"); // } export async function generateHash(data: string) { return Array.from( new Uint8Array( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data)) ) ) .map((bytes) => bytes.toString(16).padStart(2, "0")) .join("") } export function generateEmail( settings: Required, hostname?: string ): string { switch (settings.generationMethod) { case GenerationMethod.RandomCharacters: return `${randAlphaNumeric({ length: 16 }).join("")}@${ settings.aliasDomain }` case GenerationMethod.RandomName: console.log(settings) if (!settings.aliasDomain) { return randEmail({ provider: "example", suffix: "com" }) } const domain = settings.aliasDomain.split(".") const suffix = domain.pop() const provider = domain.join(".") return randEmail({ provider, suffix }) // case GenerationMethod.WebsiteURL: // return `${hostname.replace(".", "_")}_${faker.random.numeric(3)}@${ // settings.aliasDomain // }` } }