ultisuite-client/lib/contacts/export-contacts.ts
R3D347HR4Y 77f99d8d8a hehe
2026-05-19 00:48:20 +02:00

119 lines
4.0 KiB
TypeScript

import { fullContactDisplayName, type FullContact } from "./types"
function escapeVCardValue(value: string): string {
return value
.replace(/\\/g, "\\\\")
.replace(/;/g, "\\;")
.replace(/,/g, "\\,")
.replace(/\n/g, "\\n")
}
function escapeCsvField(value: string): string {
if (/[",\n\r]/.test(value)) {
return `"${value.replace(/"/g, '""')}"`
}
return value
}
function sanitizeFilename(name: string): string {
return name.replace(/[/\\?%*:|"<>]/g, "-").trim() || "contact"
}
export function contactToVCard(contact: FullContact): string {
const lines: string[] = ["BEGIN:VCARD", "VERSION:3.0"]
const last = contact.lastName ?? ""
const first = contact.firstName ?? ""
const middle = contact.middleName ?? ""
const prefix = contact.namePrefix ?? ""
const suffix = contact.nameSuffix ?? ""
lines.push(
`N:${escapeVCardValue(last)};${escapeVCardValue(first)};${escapeVCardValue(middle)};${escapeVCardValue(prefix)};${escapeVCardValue(suffix)}`
)
const fn = fullContactDisplayName(contact) || contact.emails[0]?.value || contact.phones[0]?.value
if (fn) lines.push(`FN:${escapeVCardValue(fn)}`)
for (const e of contact.emails) {
if (e.value.trim()) {
lines.push(`EMAIL;TYPE=${escapeVCardValue(e.label || "INTERNET")}:${escapeVCardValue(e.value.trim())}`)
}
}
for (const p of contact.phones) {
if (p.value.trim()) {
lines.push(`TEL;TYPE=${escapeVCardValue(p.label || "VOICE")}:${escapeVCardValue(p.value.trim())}`)
}
}
if (contact.company?.trim()) {
const org = contact.jobTitle?.trim()
? `${escapeVCardValue(contact.company.trim())};${escapeVCardValue(contact.jobTitle.trim())}`
: escapeVCardValue(contact.company.trim())
lines.push(`ORG:${org}`)
} else if (contact.jobTitle?.trim()) {
lines.push(`TITLE:${escapeVCardValue(contact.jobTitle.trim())}`)
}
if (contact.department?.trim()) {
lines.push(`X-ABLabel:${escapeVCardValue(contact.department.trim())}`)
}
if (contact.birthday?.month && contact.birthday?.day) {
const y = contact.birthday.year ?? 1900
const m = String(contact.birthday.month).padStart(2, "0")
const d = String(contact.birthday.day).padStart(2, "0")
lines.push(`BDAY:${y}${m}${d}`)
}
if (contact.notes?.trim()) {
lines.push(`NOTE:${escapeVCardValue(contact.notes.trim())}`)
}
lines.push("END:VCARD")
return lines.join("\r\n")
}
export function contactsToVCard(contacts: FullContact[]): string {
return contacts.map(contactToVCard).join("\r\n")
}
export function contactsToCsv(contacts: FullContact[]): string {
const header = ["Name", "Email", "Phone", "Company", "Job Title", "Notes"]
const rows = contacts.map((c) => {
const name = fullContactDisplayName(c)
const email = c.emails.map((e) => e.value).join("; ")
const phone = c.phones.map((p) => p.value).join("; ")
const company = c.company ?? ""
const jobTitle = c.jobTitle ?? ""
const notes = c.notes ?? ""
return [name, email, phone, company, jobTitle, notes].map(escapeCsvField).join(",")
})
return [header.join(","), ...rows].join("\r\n")
}
export function downloadTextFile(content: string, filename: string, mimeType: string): void {
const blob = new Blob([content], { type: mimeType })
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
export function downloadContactVCard(contact: FullContact): void {
const base = sanitizeFilename(
fullContactDisplayName(contact) || contact.emails[0]?.value || "contact"
)
downloadTextFile(contactToVCard(contact), `${base}.vcf`, "text/vcard;charset=utf-8")
}
export function downloadContactsVCard(contacts: FullContact[], filename = "contacts.vcf"): void {
downloadTextFile(contactsToVCard(contacts), filename, "text/vcard;charset=utf-8")
}
export function downloadContactsCsv(contacts: FullContact[], filename = "contacts.csv"): void {
downloadTextFile(contactsToCsv(contacts), filename, "text/csv;charset=utf-8")
}