invite/src/lib/emailList.ts
chris 2c78112a4f Initial commit: E-Invite, a self-hosted Evite-lite
Docker-ready Next.js + Prisma/Postgres e-invitation app: themed invite
pages, RSVP, comments, share links, email + SMS invites with
reply-to-RSVP-by-text, co-hosts, photo dropbox, and browser push
notifications.
2026-07-11 10:11:12 -04:00

78 lines
2.0 KiB
TypeScript

import { z } from 'zod';
import { looksLikeEmail, normalizePhoneNumber } from '@/lib/phone';
const emailSchema = z.string().trim().email();
export type ParsedEmailList = {
validEmails: string[];
skipped: number;
};
/**
* Parses a free-form textarea of guest emails (comma and/or newline
* separated) into a deduplicated, lowercased list of valid addresses, plus
* a count of entries that didn't parse as a valid email.
*/
export function parseEmailList(raw: string): ParsedEmailList {
const rawEntries = raw
.split(/[\n,]/)
.map((entry) => entry.trim())
.filter(Boolean);
const validEmails = new Set<string>();
let skipped = 0;
for (const entry of rawEntries) {
const result = emailSchema.safeParse(entry);
if (result.success) {
validEmails.add(result.data.toLowerCase());
} else {
skipped++;
}
}
return { validEmails: [...validEmails], skipped };
}
export type ParsedGuestContactList = {
emails: string[];
phones: string[];
skipped: number;
};
/**
* Like parseEmailList, but for a guest list textarea that accepts a mix of
* emails and phone numbers - each entry is routed by whether it contains an
* '@', then validated/normalized accordingly. Anything that fails either
* check is counted as skipped rather than rejecting the whole batch.
*/
export function parseGuestContactList(raw: string): ParsedGuestContactList {
const rawEntries = raw
.split(/[\n,]/)
.map((entry) => entry.trim())
.filter(Boolean);
const emails = new Set<string>();
const phones = new Set<string>();
let skipped = 0;
for (const entry of rawEntries) {
if (looksLikeEmail(entry)) {
const result = emailSchema.safeParse(entry);
if (result.success) {
emails.add(result.data.toLowerCase());
} else {
skipped++;
}
} else {
const normalized = normalizePhoneNumber(entry);
if (normalized) {
phones.add(normalized);
} else {
skipped++;
}
}
}
return { emails: [...emails], phones: [...phones], skipped };
}