import Foundation /// Emits vCard 3.0 for a click-wheel iPod. /// /// Version 3.0 rather than 4.0 deliberately: the click-wheel address book was /// written against 2.1/3.0 and does not recognise 4.0's `VERSION` or its /// property forms. Nothing here emits `PHOTO`, `LOGO`, `SOUND` or `X-` /// properties — the categories that made hand-exported files fail — because /// they are simply never written rather than stripped afterwards. public enum VCardWriter { /// Renders one contact as a complete, CRLF-terminated vCard. public static func makeVCard(for contact: IPodContact) -> String { var builder = ContentLineBuilder() builder.appendVerbatim("BEGIN", "VCARD") builder.appendVerbatim("VERSION", "3.0") // N is ordered family;given;middle;prefix;suffix and is required in 3.0. builder.appendStructured("N", components: [ contact.familyName, contact.givenName, contact.middleName, contact.namePrefix, contact.nameSuffix, ]) builder.append("FN", contact.displayName) if !contact.organization.isEmpty { builder.appendStructured("ORG", components: [contact.organization]) } if !contact.jobTitle.isEmpty { builder.append("TITLE", contact.jobTitle) } for phone in contact.phoneNumbers where !phone.value.isEmpty { let type = phone.label.isEmpty ? "VOICE" : phone.label builder.append("TEL", parameters: [("TYPE", type)], value: phone.value) } for email in contact.emailAddresses where !email.value.isEmpty { let type = email.label.isEmpty ? "INTERNET" : "INTERNET,\(email.label)" builder.append("EMAIL", parameters: [("TYPE", type)], value: email.value) } for address in contact.postalAddresses { // ADR is po-box;extended;street;locality;region;postal-code;country. let components = [ "", "", address.street, address.city, address.state, address.postalCode, address.country, ] guard components.contains(where: { !$0.isEmpty }) else { continue } let value = components.map(ContentLine.escapeText).joined(separator: ";") let name = address.label.isEmpty ? "ADR" : "ADR;TYPE=\(address.label)" builder.appendRawLine("\(name):\(value)") } if let birthday = contact.birthday, let formatted = formatBirthday(birthday) { builder.appendVerbatim("BDAY", formatted) } builder.appendVerbatim("END", "VCARD") return builder.render() } /// Formats a birthday as `YYYY-MM-DD`. /// /// Returns nil for year-less birthdays. Contacts.app allows them, but vCard /// 3.0 has no representation Apple doesn't express through an `X-` parameter /// (`X-APPLE-OMIT-YEAR`), and an invented year would show a wrong age on the /// device — so those birthdays are dropped instead. static func formatBirthday(_ components: DateComponents) -> String? { guard let year = components.year, let month = components.month, let day = components.day, year > 1 else { return nil } return String(format: "%04d-%02d-%02d", year, month, day) } }