Repository: adamgibbons/ics
Pull request #260 (2023-12-13 16:43:51)

Revert "Allow `organiser` to contain comma, ensure that new lines in values don't break output, and various other tweaks"
Reverts adamgibbons/ics#252 (tests failing)


Patch:
diff --git a/README.md b/README.md
index e992877..d77840f 100644
--- a/README.md
+++ b/README.md
@@ -121,20 +121,18 @@ console.log(value)
 // VERSION:2.0
 // CALSCALE:GREGORIAN
 // PRODID:adamgibbons/ics
-// METHOD:PUBLISH
-// X-PUBLISHED-TTL:PT1H
 // BEGIN:VEVENT
-// UID:pP83XzQPo5RlvjDCMIINs
+// UID:mPfHOGi_sif_xO493Mgi6
 // SUMMARY:Lunch
-// DTSTAMP:20230917T142209Z
-// DTSTART:20180115T121500Z
+// DTSTAMP:20180210T093900Z
+// DTSTART:20180115T191500Z
 // DURATION:PT45M
 // END:VEVENT
 // BEGIN:VEVENT
-// UID:gy5vfUVv6wjyBeNkkFmBX
+// UID:ho-KcKyhNaQVDqJCcGfXD
 // SUMMARY:Dinner
-// DTSTAMP:20230917T142209Z
-// DTSTART:20180115T121500Z
+// DTSTAMP:20180210T093900Z
+// DTSTART:20180115T191500Z
 // DURATION:PT1H30M
 // END:VEVENT
 // END:VCALENDAR
@@ -147,8 +145,8 @@ let moment = require("moment")
 let events = []
 let alarms = []
 
-let start = moment().format('YYYY-M-D-H-m').split("-").map((a) => parseInt(a))
-let end = moment().add({'hours':2, "minutes":30}).format("YYYY-M-D-H-m").split("-").map((a) => parseInt(a))
+let start = moment().format('YYYY-M-D-H-m').split("-")
+let end = moment().add({'hours':2, "minutes":30}).format("YYYY-M-D-H-m").split("-")
 
 alarms.push({
   action: 'audio',
@@ -169,28 +167,34 @@ let event = {
   alarms: alarms
 }
 events.push(event)
-console.log(ics.createEvents(events).value)
+console.log(ics.createEvents(events))
 
 // BEGIN:VCALENDAR
 // VERSION:2.0
 // CALSCALE:GREGORIAN
-// PRODID:myCalendarId
+// PRODID:MyCalendarId
 // METHOD:PUBLISH
 // X-PUBLISHED-TTL:PT1H
 // BEGIN:VEVENT
-// UID:123@ics.com
+// UID:123@MyCalendarIdics.com
 // SUMMARY:test here
-// DTSTAMP:20230917T142621Z
-// DTSTART:20230917T152600
-// DTEND:20230917T175600
+// DTSTAMP:20180409T072100Z
+// DTSTART:20180409
+// DTEND:20180409
+// BEGIN:VALARM
+// ACTION:DISPLAY
+// DESCRIPTION:Reminder
+// TRIGGER:-PT2H30M
+// END:VALARM
 // BEGIN:VALARM
 // ACTION:AUDIO
 // REPEAT:2
-// DESCRIPTION:Reminder
 // ATTACH;VALUE=URI:Glass
-// TRIGGER:-PT2H30M\nEND:VALARM
+// TRIGGER:PT2H
+// END:VALARM
 // END:VEVENT
 // END:VCALENDAR
+
 ```
 
 #### Using ESModules & in the browser
@@ -242,17 +246,14 @@ If a callback is provided, returns a Node-style callback.
 
 Object literal containing event information.
 Only the `start` property is required.
-
-Note all date/time fields can be the array form, or a number representing the unix timestamp in milliseconds (e.g. `getTime()` on a `Date`).
-
 The following properties are accepted:
 
 | Property      | Description   | Example  |
 | ------------- | ------------- | ----------
-| start         | **Required**. Date and time at which the event begins. | `[2000, 1, 5, 10, 0]` (January 5, 2000) or a `number`
+| start         | **Required**. Date and time at which the event begins. | `[2000, 1, 5, 10, 0]` (January 5, 2000)
 | startInputType | Type of the date/time data in `start`:<br>`local` (default): passed data is in local time.<br>`utc`: passed data is UTC |
 | startOutputType | Format of the start date/time in the output:<br>`utc` (default): the start date will be sent in UTC format.<br>`local`: the start date will be sent as "floating" (form #1 in [RFC 5545](https://tools.ietf.org/html/rfc5545#section-3.3.5)) |
-| end           | Time at which event ends. *Either* `end` or `duration` is required, but *not* both. | `[2000, 1, 5, 13, 5]` (January 5, 2000 at 1pm) or a `number`
+| end           | Time at which event ends. *Either* `end` or `duration` is required, but *not* both. | `[2000, 1, 5, 13, 5]` (January 5, 2000 at 1pm)
 | endInputType | Type of the date/time data in `end`:<br>`local`: passed data is in local time.<br>`utc`: passed data is UTC.<br>The default is the value of `startInputType` |
 | endOutputType | Format of the start date/time in the output:<br>`utc`: the start date will be sent in UTC format.<br>`local`: the start date will be sent as "floating" (form #1 in [RFC 5545](https://tools.ietf.org/html/rfc5545#section-3.3.5)).<br>The default is the value of `startOutputType` |
 | duration      | How long the event lasts. Object literal having form `{ weeks, days, hours, minutes, seconds }` *Either* `end` or `duration` is required, but *not* both. | `{ hours: 1, minutes: 45 }` (1 hour and 45 minutes)
@@ -270,13 +271,13 @@ The following properties are accepted:
 | uid           | Universal unique id for event, produced by default with `nanoid`.  **Warning:** This value must be **globally unique**.  It is recommended that it follow the [RFC 822 addr-spec](https://www.w3.org/Protocols/rfc822/) (i.e. `localpart@domain`).  Including the `@domain` half is a good way to ensure uniqueness. | `'LZfXLFzPPR4NNrgjlWDxn'`
 | method        | This property defines the iCalendar object method associated with the calendar object. When used in a MIME message entity, the value of this property MUST be the same as the Content-Type "method" parameter value.  If either the "METHOD" property or the Content-Type "method" parameter is specified, then the other MUST also be specified. | `PUBLISH`
 | recurrenceRule        | A recurrence rule, commonly referred to as an RRULE, defines the repeat pattern or rule for to-dos, journal entries and events. If specified, RRULE can be used to compute the recurrence set (the complete set of recurrence instances in a calendar component). You can use a generator like this [one](https://www.textmagic.com/free-tools/rrule-generator). | `FREQ=DAILY`
-| exclusionDates | Array of date-time exceptions for recurring events, to-dos, journal entries, or time zone definitions. | `[[2000, 1, 5, 10, 0], [2000, 2, 5, 10, 0]]` OR `[1694941727477, 1694945327477]`
+| exclusionDates| This property defines the list of DATE-TIME exceptions for recurring events, to-dos, journal entries, or time zone definitions. Uses a comma-delimited list of [Date-Time](https://tools.ietf.org/html/rfc5545#section-3.3.5) values. See [EXDATE spec](https://tools.ietf.org/html/rfc5545#section-3.8.5.1).|`'20230620T131500Z,20230621T131500'` (June 20th, 2023 at 1:15pm UTC and June 21st, 2000 at 1:15pm LOCAL)
 | sequence      | For sending an update for an event (with the same uid), defines the revision sequence number. | `2`
 | busyStatus    | Used to specify busy status for Microsoft applications, like Outlook. See [Microsoft spec](https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/cd68eae7-ed65-4dd3-8ea7-ad585c76c736). | `'BUSY'` OR `'FREE'` OR `'TENTATIVE`' OR `'OOF'`
 | transp        | Used to specify event transparency (does event consume actual time of an individual). Used by Google Calendar to determine if event should change attendees availability to 'Busy' or not. | `'TRANSPARENT'` OR `'OPAQUE'`
 | classification    | This property defines the access classification for a calendar component. See [iCalender spec](https://icalendar.org/iCalendar-RFC-5545/3-8-1-3-classification.html). | `'PUBLIC'` OR `'PRIVATE'` OR `'CONFIDENTIAL`' OR any non-standard string
-| created | Date-time representing event's creation date. Provide a date-time in local time | `[2000, 1, 5, 10, 0]` or a `number`
-| lastModified | Date-time representing date when event was last modified. Provide a date-time in local time | [2000, 1, 5, 10, 0] or a `number`
+| created | Date-time representing event's creation date. Provide a date-time in UTC | [2000, 1, 5, 10, 0] (January 5, 2000 GMT +00:00)
+| lastModified | Date-time representing date when event was last modified. Provide a date-time in UTC | [2000, 1, 5, 10, 0] (January 5, 2000 GMT +00:00)
 | calName       |  Specifies the _calendar_ (not event) name. Used by Apple iCal and Microsoft Outlook; see [Open Specification](https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/1da58449-b97e-46bd-b018-a1ce576f3e6d) | `'Example Calendar'` |
 | htmlContent       | Used to include HTML markup in an event's description. Standard DESCRIPTION tag should contain non-HTML version. | `<!DOCTYPE html><html><body><p>This is<br>test<br>html code.</p></body></html>` |
 
@@ -307,12 +308,10 @@ function (err, value) {
 }
 ```
 
-### `createEvents(events[, headerParams, callback])`
+### `createEvents(events[, callback])`
 
 Generates an iCal-compliant VCALENDAR string with multiple VEVENTS.
 
-`headerParams` may be omitted, and in this case they will be read from the first event.
-
 If a callback is not provided, returns an object having the form `{ error, value }`, where value is an iCal-compliant text string
 if `error` is `null`.
 
diff --git a/index.d.ts b/index.d.ts
index 84a4d3e..8cba459 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,5 +1,3 @@
-export type DateTime = DateArray | number | string;
-
 export type DateArray =
   | [number, number, number, number, number]
   | [number, number, number, number]
@@ -69,20 +67,14 @@ export type Alarm = {
   description?: string;
   summary?: string;
   duration?: DurationObject;
-  trigger?: DurationObject | DateTime;
+  trigger?: DurationObject; // @todo DateArray | DurationObject;
   repeat?: number;
   attachType?: string;
   attach?: string;
 };
 
-export type HeaderAttributes = {
-  productId?: string;
-  method?: string;
-  calName?: string;
-}
-
 export type EventAttributes = {
-  start: DateTime;
+  start: DateArray;
   startInputType?: 'local' | 'utc';
   startOutputType?: 'local' | 'utc';
 
@@ -108,18 +100,18 @@ export type EventAttributes = {
   categories?: string[];
   alarms?: Alarm[];
 
-  productId?: HeaderAttributes['productId'];
+  productId?: string;
   uid?: string;
-  method?: HeaderAttributes['method'];
+  method?: string;
   recurrenceRule?: string;
   exclusionDates?: string;
   sequence?: number;
-  calName?: HeaderAttributes['calName'];
+  calName?: string;
   classification?: classificationType;
-  created?: DateTime;
-  lastModified?: DateTime;
+  created?: DateArray;
+  lastModified?: DateArray;
   htmlContent?: string;
-} & ({ end: DateTime } | { duration: DurationObject });
+} & ({ end: DateArray } | { duration: DurationObject });
 
 export type ReturnObject = { error?: Error; value?: string };
 
@@ -130,7 +122,7 @@ export function createEvent(attributes: EventAttributes, callback: NodeCallback)
 export function createEvent(attributes: EventAttributes): ReturnObject;
 
 export function createEvents(events: EventAttributes[], callback: NodeCallback): void;
-export function createEvents(events: EventAttributes[], headerAttributes?: HeaderAttributes): ReturnObject;
-export function createEvents(events: EventAttributes[], headerAttributes: HeaderAttributes, callback: NodeCallback): void;
+
+export function createEvents(events: EventAttributes[]): ReturnObject;
 
 export function convertTimestampToArray(timestamp: Number, inputType: String): DateArray;
diff --git a/package.json b/package.json
index 96fe028..b73ad99 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ics",
-  "version": "3.6.0",
+  "version": "3.6.1",
   "description": "iCal (ics) file generator",
   "exports": {
     "types": "./index.d.ts",
diff --git a/src/defaults.js b/src/defaults.js
index d453f09..7ee8f46 100644
--- a/src/defaults.js
+++ b/src/defaults.js
@@ -1,12 +1,13 @@
 import { nanoid } from 'nanoid'
+import { formatDate } from './utils'
 
-export const headerDefaults = () => ({
-  productId: 'adamgibbons/ics',
-  method: 'PUBLISH'
-})
-
-export const eventDefaults = () => ({
+const defaults = {
   title: 'Untitled event',
+  productId: 'adamgibbons/ics',
+  method: 'PUBLISH',
   uid: nanoid(),
-  timestamp: Date.now()
-})
+  timestamp: formatDate(null, 'utc'),
+  start: formatDate(null, 'utc')
+}
+
+export default defaults
diff --git a/src/index.js b/src/index.js
index df23c17..8462e4f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,19 +1,56 @@
+import { nanoid }  from 'nanoid'
 import {
-  buildHeader,
   buildEvent,
-  validateHeader,
-  validateHeaderAndEvent,
-  formatHeader,
-  formatEvent,
-  formatFooter,
+  validateEvent,
+  formatEvent
 } from './pipeline'
 
-function buildHeaderAndValidate(header) {
-  return validateHeader(buildHeader(header))
+function assignUniqueId(event) {
+  event.uid = event.uid || nanoid()
+  return event
 }
+function validateAndBuildEvent(event) {
+  return validateEvent(buildEvent(event))
+}
+
+function applyInitialFormatting({ error, value }) {
+  if (error) {
+    return { error, value: null }
+  }
+
+  return { error: null, value: formatEvent(value) }
+}
+
+function reformatEventsByPosition({ error, value }, idx, list) {
+  if (error) return { error, value }
+
+  if (idx === 0) {
+    // beginning of list
+    return { value: value.slice(0, value.indexOf('END:VCALENDAR')), error: null }
+  }
+
+  if (idx === list.length - 1) {
+    // end of list
+    return { value: value.slice(value.indexOf('BEGIN:VEVENT')), error: null}
+  }
+
+  return { error: null, value: value.slice(value.indexOf('BEGIN:VEVENT'), value.indexOf('END:VEVENT') + 12) }
+}
+
+function catenateEvents(accumulator, { error, value }, idx) {
+  if (error) {
+    accumulator.error = error
+    accumulator.value = null
+    return accumulator
+  }
+
+  if (accumulator.value) {
+    accumulator.value = accumulator.value.concat(value)
+    return accumulator
+  }
 
-function buildHeaderAndEventAndValidate(event) {
-  return validateHeaderAndEvent({...buildHeader(event), ...buildEvent(event) })
+  accumulator.value = value
+  return accumulator
 }
 
 export function convertTimestampToArray(timestamp, inputType = 'local') {
@@ -28,51 +65,69 @@ export function convertTimestampToArray(timestamp, inputType = 'local') {
 }
 
 export function createEvent (attributes, cb) {
-  return createEvents([attributes], cb)
-}
+  if (!attributes) { Error('Attributes argument is required') }
 
-export function createEvents (events, headerAttributesOrCb, cb) {
-  const resolvedHeaderAttributes = typeof headerAttributesOrCb === 'object' ? headerAttributesOrCb : {};
-  const resolvedCb = arguments.length === 3 ? cb : (typeof headerAttributesOrCb === 'function' ? headerAttributesOrCb : null);
+  assignUniqueId(attributes)
 
-  const run = () => {
-    if (!events) {
-      return { error: new Error('one argument is required'), value: null }
-    }
+  if (!cb) {
+    // No callback, so return error or value in an object
+    const { error, value } = validateAndBuildEvent(attributes)
+
+    if (error) return { error, value }
 
-    const { error: headerError, value: headerValue } = events.length === 0
-      ? buildHeaderAndValidate(resolvedHeaderAttributes)
-      : buildHeaderAndEventAndValidate({...events[0], ...resolvedHeaderAttributes});
+    let event = ''
 
-    if (headerError) {
-      return {error: headerError, value: null}
+    try {
+      event = formatEvent(value)
+    } catch(error) {
+      return { error, value: null }
     }
 
-    let value = ''
-    value += formatHeader(headerValue)
+    return { error: null, value: event }
+  }
 
-    for (let i = 0; i < events.length; i++) {
-      const { error: eventError, value: eventValue } = buildHeaderAndEventAndValidate(events[i])
-      if (eventError) return {error: eventError, value: null}
+  // Return a node-style callback
+  const { error, value } = validateAndBuildEvent(attributes)
 
-      value += formatEvent(eventValue);
-    }
+  if (error) return cb(error)
+
+  return cb(null, formatEvent(value))
+}
 
-    value += formatFooter();
+export function createEvents (events, cb) {
+  if (!events) {
+    return { error: Error('one argument is required'), value: null }
+  }
 
-    return { error: null, value }
+  if (events.length === 0) {
+    const {error, value: dummy} = createEvent({
+      start: [2000, 10, 5, 5, 0],
+      duration: { hours: 1 }
+    })
+    if (error) return {error, value: null}
+
+    return {
+      error: null,
+      value: (
+        dummy.slice(0, dummy.indexOf('BEGIN:VEVENT')) +
+        dummy.slice(dummy.indexOf('END:VEVENT') + 10 + 2)
+      )
+    }
   }
 
-  let returnValue;
-  try {
-    returnValue = run();
-  } catch (e) {
-    returnValue = { error: e, value: null }
+  if (events.length === 1) {
+    return createEvent(events[0], cb)
   }
 
-  if (!resolvedCb) {
-    return returnValue
+  const { error, value } = events.map(assignUniqueId)
+    .map(validateAndBuildEvent)
+    .map(applyInitialFormatting)
+    .map(reformatEventsByPosition)
+    .reduce(catenateEvents, { error: null, value: null })
+
+  if (!cb) {
+    return { error, value }
   }
 
-  return resolvedCb(returnValue.error, returnValue.value)
+  return cb(error, value)
 }
diff --git a/src/pipeline/build.js b/src/pipeline/build.js
index a9b4ef3..1039c7c 100644
--- a/src/pipeline/build.js
+++ b/src/pipeline/build.js
@@ -1,22 +1,38 @@
-import { headerDefaults, eventDefaults } from "../defaults";
+import defaultAttributes from "../defaults";
 
-function removeUndefined(input) {
-  return Object.entries(input).reduce(
-    (clean, entry) => typeof entry[1] !== 'undefined' ? Object.assign(clean, {[entry[0]]: entry[1]}) : clean,
-    {}
-  )
-}
+export default function buildEvent(attributes = {}) {
+  const {
+    title,
+    productId,
+    method,
+    uid,
+    sequence,
+    start,
+    startType,
+    duration,
+    end,
+    description,
+    url,
+    geo,
+    location,
+    status,
+    categories,
+    organizer,
+    attendees,
+    alarms,
+    recurrenceRule,
+    created,
+    lastModified,
+    calName,
+    htmlContent
+  } = attributes;
 
-export function buildHeader(attributes = {}) {
   // fill in default values where necessary
-  const output = Object.assign({}, headerDefaults(), attributes);
-
-  return removeUndefined(output)
-}
+  const output = Object.assign({}, defaultAttributes, attributes);
 
-export function buildEvent(attributes = {}) {
-  // fill in default values where necessary
-  const output = Object.assign({}, eventDefaults(), attributes);
-
-  return removeUndefined(output)
+  // remove undefined values
+  return Object.entries(output).reduce(
+      (clean, entry) => typeof entry[1] !== 'undefined' ? Object.assign(clean, {[entry[0]]: entry[1]}) : clean,
+      {}
+  )
 }
diff --git a/src/pipeline/format.js b/src/pipeline/format.js
index 02349b9..92d2fa9 100644
--- a/src/pipeline/format.js
+++ b/src/pipeline/format.js
@@ -10,34 +10,12 @@ import {
     formatDuration,
     foldLine
 } from '../utils'
-import encodeNewLines from '../utils/encode-new-lines'
 
-export function formatHeader(attributes = {}) {
+export default function formatEvent(attributes = {}) {
   const {
+    title,
     productId,
     method,
-    calName,
-  } = attributes
-
-  let icsFormat = ''
-  icsFormat += 'BEGIN:VCALENDAR\r\n'
-  icsFormat += 'VERSION:2.0\r\n'
-  icsFormat += 'CALSCALE:GREGORIAN\r\n'
-  icsFormat += foldLine(`PRODID:${encodeNewLines(productId)}`) + '\r\n'
-  icsFormat += foldLine(`METHOD:${encodeNewLines(method)}`) + '\r\n'
-  icsFormat += calName ? (foldLine(`X-WR-CALNAME:${encodeNewLines(calName)}`) + '\r\n') : ''
-  icsFormat += `X-PUBLISHED-TTL:PT1H\r\n`
-
-  return icsFormat
-}
-
-export function formatFooter() {
-  return `END:VCALENDAR\r\n`
-}
-
-export function formatEvent(attributes = {}) {
-  const {
-    title,
     uid,
     sequence,
     timestamp,
@@ -65,53 +43,62 @@ export function formatEvent(attributes = {}) {
     classification,
     created,
     lastModified,
+    calName,
     htmlContent
   } = attributes
 
   let icsFormat = ''
+  icsFormat += 'BEGIN:VCALENDAR\r\n'
+  icsFormat += 'VERSION:2.0\r\n'
+  icsFormat += 'CALSCALE:GREGORIAN\r\n'
+  icsFormat += foldLine(`PRODID:${productId}`) + '\r\n'
+  icsFormat += foldLine(`METHOD:${method}`) + '\r\n'
+  icsFormat += calName ? (foldLine(`X-WR-CALNAME:${calName}`) + '\r\n') : ''
+  icsFormat += `X-PUBLISHED-TTL:PT1H\r\n`
   icsFormat += 'BEGIN:VEVENT\r\n'
-  icsFormat += foldLine(`UID:${encodeNewLines(uid)}`) + '\r\n'
-  icsFormat += title ? foldLine(`SUMMARY:${encodeNewLines(setSummary(title))}`) + '\r\n' : ''
-  icsFormat += foldLine(`DTSTAMP:${encodeNewLines(formatDate(timestamp))}`) + '\r\n'
+  icsFormat += `UID:${uid}\r\n`
+  icsFormat +=  foldLine(`SUMMARY:${title ? setSummary(title) : title}`) + '\r\n'
+  icsFormat += `DTSTAMP:${timestamp}\r\n`
 
   // All day events like anniversaries must be specified as VALUE type DATE
-  icsFormat += foldLine(`DTSTART${start && start.length == 3 ? ";VALUE=DATE" : ""}:${encodeNewLines(formatDate(start, startOutputType || startType, startInputType))}`) + '\r\n'
+  icsFormat += `DTSTART${start && start.length == 3 ? ";VALUE=DATE" : ""}:${formatDate(start, startOutputType || startType, startInputType)}\r\n`
 
   // End is not required for all day events on single days (like anniversaries)
   if (!end || end.length !== 3 || start.length !== end.length || start.some((val, i) => val !== end[i])) {
     if (end) {
-      icsFormat += foldLine(`DTEND${end.length === 3 ? ";VALUE=DATE" : ""}:${encodeNewLines(formatDate(end, endOutputType || startOutputType || startType, endInputType || startInputType))}`) + '\r\n'
+      icsFormat += `DTEND${end.length === 3 ? ";VALUE=DATE" : ""}:${formatDate(end, endOutputType || startOutputType || startType, endInputType || startInputType)}\r\n`;
     }
   }
 
   icsFormat += typeof sequence !== 'undefined' ? (`SEQUENCE:${sequence}\r\n`) : ''
-  icsFormat += description ? (foldLine(`DESCRIPTION:${encodeNewLines(setDescription(description))}`) + '\r\n') : ''
-  icsFormat += url ? (foldLine(`URL:${encodeNewLines(url)}`) + '\r\n') : ''
+  icsFormat += description ? (foldLine(`DESCRIPTION:${setDescription(description)}`) + '\r\n') : ''
+  icsFormat += url ? (foldLine(`URL:${url}`) + '\r\n') : ''
   icsFormat += geo ? (foldLine(`GEO:${setGeolocation(geo)}`) + '\r\n') : ''
-  icsFormat += location ? (foldLine(`LOCATION:${encodeNewLines(setLocation(location))}`) + '\r\n') : ''
-  icsFormat += status ? (foldLine(`STATUS:${encodeNewLines(status)}`) + '\r\n') : ''
-  icsFormat += categories ? (foldLine(`CATEGORIES:${encodeNewLines(categories.join(','))}`) + '\r\n') : ''
+  icsFormat += location ? (foldLine(`LOCATION:${setLocation(location)}`) + '\r\n') : ''
+  icsFormat += status ? (foldLine(`STATUS:${status}`) + '\r\n') : ''
+  icsFormat += categories ? (foldLine(`CATEGORIES:${categories}`) + '\r\n') : ''
   icsFormat += organizer ? (foldLine(`ORGANIZER;${setOrganizer(organizer)}`) + '\r\n') : ''
-  icsFormat += busyStatus ? (foldLine(`X-MICROSOFT-CDO-BUSYSTATUS:${encodeNewLines(busyStatus)}`) + '\r\n') : ''
-  icsFormat += transp ? (foldLine(`TRANSP:${encodeNewLines(transp)}`) + '\r\n') : ''
-  icsFormat += classification ? (foldLine(`CLASS:${encodeNewLines(classification)}`) + '\r\n') : ''
-  icsFormat += created ? ('CREATED:' + encodeNewLines(formatDate(created)) + '\r\n') : ''
-  icsFormat += lastModified ? ('LAST-MODIFIED:' + encodeNewLines(formatDate(lastModified)) + '\r\n') : ''
-  icsFormat += htmlContent ? (foldLine(`X-ALT-DESC;FMTTYPE=text/html:${encodeNewLines(htmlContent)}`) + '\r\n') : ''
+  icsFormat += busyStatus ? (foldLine(`X-MICROSOFT-CDO-BUSYSTATUS:${busyStatus}`) + '\r\n') : ''
+  icsFormat += transp ? (foldLine(`TRANSP:${transp}`) + '\r\n') : ''
+  icsFormat += classification ? (foldLine(`CLASS:${classification}`) + '\r\n') : ''
+  icsFormat += created ? ('CREATED:' + formatDate(created) + '\r\n') : ''
+  icsFormat += lastModified ? ('LAST-MODIFIED:' + formatDate(lastModified) + '\r\n') : ''
+  icsFormat += htmlContent ? (foldLine(`X-ALT-DESC;FMTTYPE=text/html:${htmlContent}`) + '\r\n') : ''
   if (attendees) {
-    attendees.forEach((attendee) => {
-      icsFormat += foldLine(`ATTENDEE;${encodeNewLines(setContact(attendee))}`) + '\r\n'
+    attendees.map(function (attendee) {
+      icsFormat += foldLine(`ATTENDEE;${setContact(attendee)}`) + '\r\n'
     })
   }
-  icsFormat += recurrenceRule ? foldLine(`RRULE:${encodeNewLines(recurrenceRule)}`) + '\r\n' : ''
-  icsFormat += exclusionDates ? foldLine(`EXDATE:${encodeNewLines(exclusionDates.map((a) => formatDate(a)).join(','))}`) + '\r\n': ''
-  icsFormat += duration ? foldLine(`DURATION:${formatDuration(duration)}`) + '\r\n' : ''
+  icsFormat += recurrenceRule ? `RRULE:${recurrenceRule}\r\n` : ''
+  icsFormat += exclusionDates ? `EXDATE:${exclusionDates}\r\n` : ''
+  icsFormat += duration ? `DURATION:${formatDuration(duration)}\r\n` : ''
   if (alarms) {
-    alarms.forEach((alarm) => {
+    alarms.map(function (alarm) {
       icsFormat += setAlarm(alarm)
     })
   }
   icsFormat += `END:VEVENT\r\n`
+  icsFormat += `END:VCALENDAR\r\n`
 
   return icsFormat
 }
diff --git a/src/pipeline/index.js b/src/pipeline/index.js
index a20e5e1..7c0e31f 100644
--- a/src/pipeline/index.js
+++ b/src/pipeline/index.js
@@ -1,13 +1,9 @@
-import { buildHeader, buildEvent } from './build'
-import { formatHeader, formatEvent, formatFooter } from './format'
-import { validateHeader, validateHeaderAndEvent } from './validate'
+import buildEvent from './build'
+import formatEvent from './format'
+import validateEvent from './validate'
 
 export {
-  buildHeader,
   buildEvent,
-  formatHeader,
   formatEvent,
-  formatFooter,
-  validateHeader,
-  validateHeaderAndEvent
+  validateEvent
 }
diff --git a/src/pipeline/validate.js b/src/pipeline/validate.js
index 50d0dba..e169acc 100644
--- a/src/pipeline/validate.js
+++ b/src/pipeline/validate.js
@@ -1,1 +1,3 @@
-export * from '../schema'
+import validate from '../schema'
+
+export default validate
diff --git a/src/schema/index.js b/src/schema/index.js
index 6f8c64e..70458a1 100644
--- a/src/schema/index.js
+++ b/src/schema/index.js
@@ -6,18 +6,7 @@ import * as yup from 'yup'
 // "url must match the following: ...." as opposed to "url must be a valid URL"
 const urlRegex = /^(?:([a-z0-9+.-]+):\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/
 
-const dateTimeSchema = ({ required }) => yup.lazy((value) => {
-  if (typeof value === 'number') {
-    return yup.number().integer().min(0)
-  }
-  if (typeof value === 'string') {
-    return yup.string().required()
-  }
-  if (!required && typeof value === 'undefined') {
-    return yup.mixed().oneOf([undefined])
-  }
-
-  return yup.array().required().min(3).max(7).of(yup.lazy((item, options) => {
+const dateTimeSchema = yup.array().min(3).max(7).of(yup.lazy((item, options) => {
     const itemIndex = options.parent.indexOf(options.value)
     return [
       yup.number().integer(),
@@ -27,8 +16,7 @@ const dateTimeSchema = ({ required }) => yup.lazy((value) => {
       yup.number().integer().min(0).max(60),
       yup.number().integer().min(0).max(60)
     ][itemIndex]
-  }))
-  }
+  })
 )
 
 const durationSchema = yup.object().shape({
@@ -59,7 +47,7 @@ const organizerSchema = yup.object().shape({
 }).noUnknown()
 
 const alarmSchema = yup.object().shape({
-  action: yup.string().matches(/^(audio|display|email)$/).required(),
+  action: yup.string().matches(/audio|display|email/).required(),
   trigger: yup.mixed().required(),
   description: yup.string(),
   duration: durationSchema,
@@ -72,66 +60,49 @@ const alarmSchema = yup.object().shape({
   'iana-prop': yup.mixed()
 }).noUnknown()
 
-const headerShape = {
-  productId: yup.string(),
-  method: yup.string(),
-  calName: yup.string()
-}
-
-const headerSchema = yup.object().shape(headerShape).noUnknown()
-
-const eventShape = {
+const schema = yup.object().shape({
   summary: yup.string(),
-  timestamp: dateTimeSchema({ required: false }),
+  timestamp: yup.mixed(),
   title: yup.string(),
-  uid: yup.string(),
+  productId: yup.string(),
+  method: yup.string(),
+  uid: yup.string().required(),
   sequence: yup.number().integer().max(2_147_483_647),
-  start: dateTimeSchema({ required: true }),
+  start: dateTimeSchema.required(),
   duration: durationSchema,
-  startType: yup.string().matches(/^(utc|local)$/),
-  startInputType: yup.string().matches(/^(utc|local)$/),
-  startOutputType: yup.string().matches(/^(utc|local)$/),
-  end: dateTimeSchema({ required: false }),
-  endInputType: yup.string().matches(/^(utc|local)$/),
-  endOutputType: yup.string().matches(/^(utc|local)$/),
+  startType: yup.string().matches(/utc|local/),
+  startInputType: yup.string().matches(/utc|local/),
+  startOutputType: yup.string().matches(/utc|local/),
+  end: dateTimeSchema,
+  endInputType: yup.string().matches(/utc|local/),
+  endOutputType: yup.string().matches(/utc|local/),
   description: yup.string(),
   url: yup.string().matches(urlRegex),
   geo: yup.object().shape({lat: yup.number(), lon: yup.number()}),
   location: yup.string(),
-  status: yup.string().matches(/^(TENTATIVE|CANCELLED|CONFIRMED)$/i),
+  status: yup.string().matches(/TENTATIVE|CANCELLED|CONFIRMED/i),
   categories: yup.array().of(yup.string()),
   organizer: organizerSchema,
   attendees: yup.array().of(contactSchema),
   alarms: yup.array().of(alarmSchema),
   recurrenceRule: yup.string(),
-  busyStatus: yup.string().matches(/^(TENTATIVE|FREE|BUSY|OOF)$/i),
-  transp: yup.string().matches(/^(TRANSPARENT|OPAQUE)$/i),
+  busyStatus: yup.string().matches(/TENTATIVE|FREE|BUSY|OOF/i),
+  transp: yup.string().matches(/TRANSPARENT|OPAQUE/i),
   classification: yup.string(),
-  created: dateTimeSchema({ required: false }),
-  lastModified: dateTimeSchema({ required: false }),
-  exclusionDates: yup.array().of(dateTimeSchema({ required: true })),
+  created: dateTimeSchema,
+  lastModified: dateTimeSchema,
+  calName: yup.string(),
   htmlContent: yup.string()
-}
-
-const headerAndEventSchema = yup.object().shape({ ...headerShape, ...eventShape }).test('xor', `object should have end or duration (but not both)`, val => {
+}).test('xor', `object should have end or duration (but not both)`, val => {
   const hasEnd = !!val.end
   const hasDuration = !!val.duration
   return ((hasEnd && !hasDuration) || (!hasEnd && hasDuration) || (!hasEnd && !hasDuration))
 }).noUnknown()
 
+export default function validateEvent (candidate) {
 
-export function validateHeader (candidate) {
-  try {
-    const value = headerSchema.validateSync(candidate, {abortEarly: false, strict: true})
-    return {error: null, value}
-  } catch (error) {
-    return {error: Object.assign({}, error), value: undefined}
-  }
-}
-
-export function validateHeaderAndEvent (candidate) {
   try {
-    const value = headerAndEventSchema.validateSync(candidate, {abortEarly: false, strict: true})
+    const value = schema.validateSync(candidate, {abortEarly: false, strict: true})
     return {error: null, value}
   } catch (error) {
     return {error: Object.assign({}, error), value: undefined}
diff --git a/src/utils/encode-new-lines.js b/src/utils/encode-new-lines.js
deleted file mode 100644
index f846d6b..0000000
--- a/src/utils/encode-new-lines.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function encodeNewLines (text) {
-  return text.replace(/\r?\n/gm, "\\n")
-}
diff --git a/src/utils/encode-param-value.js b/src/utils/encode-param-value.js
deleted file mode 100644
index 2233f57..0000000
--- a/src/utils/encode-param-value.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function encodeParamValue (value) {
-  return `"${value.replaceAll('"', '\\"')}"`
-}
diff --git a/src/utils/format-date.js b/src/utils/format-date.js
index 75cc874..8afdf5c 100644
--- a/src/utils/format-date.js
+++ b/src/utils/format-date.js
@@ -1,10 +1,6 @@
 const pad = n => n < 10 ? `0${n}` : `${n}`
 
 export default function formatDate(args = [], outputType = 'utc', inputType = 'local') {
-  if (typeof args === 'string') {
-    return args;
-  }
-  
   if (Array.isArray(args) && args.length === 3) {
     const [year, month, date] = args
     return `${year}${pad(month)}${pad(date)}`
@@ -18,9 +14,6 @@ export default function formatDate(args = [], outputType = 'utc', inputType = 'l
     } else {
       outDate = new Date(Date.UTC(year, month - 1, date, hours, minutes, seconds))
     }
-  } else if (!Array.isArray(args)) {
-    // it's a unix time stamp (ms)
-    outDate = new Date(args);
   }
 
   if (outputType === 'local') {
diff --git a/src/utils/format-text.js b/src/utils/format-text.js
index a2d5aba..99b2d9c 100644
--- a/src/utils/format-text.js
+++ b/src/utils/format-text.js
@@ -4,4 +4,4 @@ export default function formatText (text) {
     .replace(/\r?\n/gm, "\\n")
     .replace(/;/gm, "\\;")
     .replace(/,/gm, "\\,")
-}
+}
\ No newline at end of file
diff --git a/src/utils/index.js b/src/utils/index.js
index 964d4bb..0ecdd04 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -8,7 +8,6 @@ import setSummary from './set-summary'
 import formatDuration from './format-duration'
 import foldLine from './fold-line'
 import setLocation from './set-location'
-import encodeParamValue from './encode-param-value'
 
 export {
   formatDate,
@@ -20,6 +19,5 @@ export {
   setSummary,
   setDescription,
   foldLine,
-  setLocation,
-  encodeParamValue
+  setLocation
 }
diff --git a/src/utils/set-alarm.js b/src/utils/set-alarm.js
index 526f4ea..81f5d8c 100644
--- a/src/utils/set-alarm.js
+++ b/src/utils/set-alarm.js
@@ -1,6 +1,5 @@
 import formatDate from './format-date'
 import foldLine from './fold-line'
-import encodeNewLines from './encode-new-lines'
 
 function setDuration ({
   weeks,
@@ -22,11 +21,11 @@ function setDuration ({
 
 function setTrigger (trigger) {
   let formattedString = ''
-  if(Array.isArray(trigger) || typeof trigger === 'number' || typeof trigger === 'string') {
-    formattedString = `TRIGGER;VALUE=DATE-TIME:${encodeNewLines(formatDate(trigger))}\r\n`
+  if(Array.isArray(trigger)) {
+    formattedString = `TRIGGER;VALUE=DATE-TIME:${formatDate(trigger)}\r\n`
   } else {
     let alert = trigger.before ? '-' : ''
-    formattedString = `TRIGGER:${encodeNewLines(alert+setDuration(trigger))}\r\n`
+    formattedString = `TRIGGER:${alert+setDuration(trigger)}\r\n`
   }
 
   return formattedString
@@ -49,14 +48,14 @@ export default function setAlarm(attributes = {}) {
   } = attributes
 
   let formattedString = 'BEGIN:VALARM\r\n'
-  formattedString += foldLine(`ACTION:${encodeNewLines(setAction(action))}`) + '\r\n'
+  formattedString += foldLine(`ACTION:${setAction(action)}`) + '\r\n'
   formattedString += repeat ? foldLine(`REPEAT:${repeat}`) + '\r\n' : ''
-  formattedString += description ? foldLine(`DESCRIPTION:${encodeNewLines(description)}`) + '\r\n' : ''
+  formattedString += description ? foldLine(`DESCRIPTION:${description}`) + '\r\n' : ''
   formattedString += duration ? foldLine(`DURATION:${setDuration(duration)}`) + '\r\n' : ''
   let attachInfo = attachType ? attachType : 'FMTTYPE=audio/basic'
-  formattedString += attach ? foldLine(encodeNewLines(`ATTACH;${attachInfo}:${attach}`)) + '\r\n' : ''
-  formattedString += trigger ? encodeNewLines(setTrigger(trigger)) : ''
-  formattedString += summary ? (foldLine(`SUMMARY:${encodeNewLines(summary)}`) + '\r\n') : ''
+  formattedString += attach ? foldLine(`ATTACH;${attachInfo}:${attach}`) + '\r\n' : ''
+  formattedString += trigger ? setTrigger(trigger) : ''
+  formattedString += summary ? (foldLine(`SUMMARY:${summary}`) + '\r\n') : ''
   formattedString += 'END:VALARM\r\n'
 
   return formattedString
diff --git a/src/utils/set-contact.js b/src/utils/set-contact.js
index 9f8edea..cee2e09 100644
--- a/src/utils/set-contact.js
+++ b/src/utils/set-contact.js
@@ -1,5 +1,3 @@
-import encodeParamValue from "./encode-param-value";
-
 export default function setContact({ name, email, rsvp, dir, partstat, role, cutype, xNumGuests }) {
   let formattedParts = [];
 
@@ -7,21 +5,21 @@ export default function setContact({ name, email, rsvp, dir, partstat, role, cut
     formattedParts.push(rsvp ? 'RSVP=TRUE' : 'RSVP=FALSE');
   }
   if(cutype){
-    formattedParts.push("CUTYPE=".concat(encodeParamValue(cutype)));
+    formattedParts.push("CUTYPE=".concat(cutype));
   }
   if(xNumGuests !== undefined){
     formattedParts.push(`X-NUM-GUESTS=${xNumGuests}`);
   }
   if(role){
-    formattedParts.push("ROLE=".concat(encodeParamValue(role)));
+    formattedParts.push("ROLE=".concat(role));
   }
   if(partstat){
-    formattedParts.push("PARTSTAT=".concat(encodeParamValue(partstat)));
+    formattedParts.push("PARTSTAT=".concat(partstat));
   }
   if(dir){
-    formattedParts.push("DIR=".concat(encodeParamValue(dir)));
+    formattedParts.push("DIR=".concat(dir));
   }
-  formattedParts.push('CN='.concat((encodeParamValue(name || 'Unnamed attendee'))));
+  formattedParts.push('CN='.concat((name || 'Unnamed attendee')));
 
   var formattedAttendee = formattedParts.join(';').concat(email ? ":mailto:".concat(email) : '');
 
diff --git a/src/utils/set-organizer.js b/src/utils/set-organizer.js
index 01a6f45..26adf00 100644
--- a/src/utils/set-organizer.js
+++ b/src/utils/set-organizer.js
@@ -1,11 +1,9 @@
-import encodeParamValue from "./encode-param-value"
-
 export default function setOrganizer({ name, email, dir, sentBy }) {
   let formattedOrganizer = ''
-  formattedOrganizer += dir ? `DIR=${encodeParamValue(dir)};` : ''
-  formattedOrganizer += sentBy ? `SENT-BY=${encodeParamValue(`MAILTO:${sentBy}`)};` : ''
+  formattedOrganizer += dir ? `DIR="${dir}";` : ''
+  formattedOrganizer += sentBy ? `SENT-BY="MAILTO:${sentBy}";` : ''
   formattedOrganizer += 'CN='
-  formattedOrganizer += encodeParamValue(name || 'Organizer')
+  formattedOrganizer += name || 'Organizer'
   formattedOrganizer += email ? `:MAILTO:${email}` : ''
 
   return formattedOrganizer


Test patch:
diff --git a/test/index.spec.js b/test/index.spec.js
index 6cbec95..2738a46 100644
--- a/test/index.spec.js
+++ b/test/index.spec.js
@@ -75,12 +75,6 @@ describe('ics', () => {
         expect(error).to.be.null
         expect(value).to.contain('BEGIN:VCALENDAR')
       })
-
-      it('support header params', () => {
-        const { error, value } = createEvents([], { calName: 'test' })
-        expect(error).to.be.null
-        expect(value).to.contain('X-WR-CALNAME:test')
-      })
     })
 
     describe('when a callback is provided', () => {
@@ -100,21 +94,13 @@ describe('ics', () => {
         })
       })
 
-      it('returns an iCal string when passed 0 events', (done) => {
+      it('returns an iCal string when passed 0 events', () => {
         createEvents([], (error, value) => {
           expect(error).to.be.null
           expect(value).to.contain('BEGIN:VCALENDAR')
           done()
         })
       })
-
-      it('support header params', (done) => {
-        createEvents([], { calName: 'test' }, (error, value) => {
-          expect(error).to.be.null
-          expect(value).to.contain('X-WR-CALNAME:test')
-          done()
-        })
-      })
     })
   })
-})
+})
\ No newline at end of file
diff --git a/test/pipeline/build.spec.js b/test/pipeline/build.spec.js
index 6ef2d28..cead401 100644
--- a/test/pipeline/build.spec.js
+++ b/test/pipeline/build.spec.js
@@ -1,38 +1,35 @@
 import { expect } from 'chai'
-import { buildHeader, buildEvent } from '../../src/pipeline'
+import { buildEvent } from '../../src/pipeline'
 
-describe('pipeline.buildHeader properties', () => {
-  describe('productId', () => {
+describe('pipeline.build properties', () => {
+  describe('title', () => {
     it('sets a default', () => {
-      const header = buildHeader()
-      expect(header.productId).to.equal('adamgibbons/ics')
+      const event = buildEvent()
+      expect(event.title).to.equal('Untitled event')
     })
-    it('sets a productId', () => {
-      const header = buildHeader({ productId: 'productId' })
-      expect(header.productId).to.equal('productId')
+    it('sets a title', () => {
+      const event = buildEvent({ title: 'Hello event!' })
+      expect(event.title).to.equal('Hello event!')
     })
   })
-  describe('method', () => {
+  describe('productId', () => {
     it('sets a default', () => {
-      const header = buildHeader()
-      expect(header.method).to.equal('PUBLISH')
+      const event = buildEvent()
+      expect(event.productId).to.equal('adamgibbons/ics')
     })
-    it('sets a method', () => {
-      const header = buildHeader({ method: 'method' })
-      expect(header.method).to.equal('method')
+    it('sets a product id', () => {
+      const event = buildEvent({ productId: 'myProductId' })
+      expect(event.productId).to.equal('myProductId')
     })
   })
-})
-
-describe('pipeline.buildEvent properties', () => {
-  describe('title', () => {
+  describe('method', () => {
     it('sets a default', () => {
       const event = buildEvent()
-      expect(event.title).to.equal('Untitled event')
+      expect(event.method).to.equal('PUBLISH')
     })
-    it('sets a title', () => {
-      const event = buildEvent({ title: 'Hello event!' })
-      expect(event.title).to.equal('Hello event!')
+    it('sets a method', () => {
+      const event = buildEvent({ method: 'REQUEST' })
+      expect(event.method).to.equal('REQUEST')
     })
   })
   describe('uid', () => {
diff --git a/test/pipeline/format.spec.js b/test/pipeline/format.spec.js
index 06cb2b6..5bf9928 100644
--- a/test/pipeline/format.spec.js
+++ b/test/pipeline/format.spec.js
@@ -2,41 +2,17 @@ import dayjs from 'dayjs';
 import { expect } from 'chai'
 import {
   formatEvent,
-  buildEvent,
-  formatHeader,
-  buildHeader
+  buildEvent
 } from '../../src/pipeline'
 import {foldLine} from "../../src/utils";
 
-describe('pipeline.formatHeader', () => {
-  it('writes default values when no attributes passed', () => {
-    const header = buildHeader()
-    const formattedHeader = formatHeader(header)
-    expect(formattedHeader).to.contain('BEGIN:VCALENDAR')
-    expect(formattedHeader).to.contain('VERSION:2.0')
-    expect(formattedHeader).to.contain('PRODID:adamgibbons/ics')
-  })
-  it('writes a product id', () => {
-    const header = buildHeader({ productId: 'productId'})
-    const formattedHeader = formatHeader(header)
-    expect(formattedHeader).to.contain('PRODID:productId')
-  })
-  it('writes a method', () => {
-    const header = buildHeader({ method: 'method'})
-    const formattedHeader = formatHeader(header)
-    expect(formattedHeader).to.contain('METHOD:method')
-  })
-  it('writes a calName', () => {
-    const header = buildHeader({ calName: 'calName'})
-    const formattedHeader = formatHeader(header)
-    expect(formattedHeader).to.contain('X-WR-CALNAME:calName')
-  })
-})
-
 describe('pipeline.formatEvent', () => {
   it('writes default values when no attributes passed', () => {
     const event = buildEvent()
     const formattedEvent = formatEvent(event)
+    expect(formattedEvent).to.contain('BEGIN:VCALENDAR')
+    expect(formattedEvent).to.contain('VERSION:2.0')
+    expect(formattedEvent).to.contain('PRODID:adamgibbons/ics')
     expect(formattedEvent).to.contain('BEGIN:VEVENT')
     expect(formattedEvent).to.contain('SUMMARY:Untitled event')
     expect(formattedEvent).to.contain('UID:')
@@ -44,6 +20,7 @@ describe('pipeline.formatEvent', () => {
     expect(formattedEvent).to.contain('DTSTART:')
     expect(formattedEvent).to.contain('DTSTAMP:20')
     expect(formattedEvent).to.contain('END:VEVENT')
+    expect(formattedEvent).to.contain('END:VCALENDAR')
   })
   it('writes a title', () => {
     const event = buildEvent({ title: 'foo bar' })
@@ -123,6 +100,11 @@ describe('pipeline.formatEvent', () => {
     const formattedEvent = formatEvent(event)
     expect(formattedEvent).to.contain('LAST-MODIFIED:20170515')
   })
+  it('writes a cal name', () => {
+    const event = buildEvent({ calName: 'John\'s Calendar' })
+    const formattedEvent = formatEvent(event)
+    expect(formattedEvent).to.contain('X-WR-CALNAME:John\'s Calendar')
+  })
   it('writes a html content and folds correctly', () => {
     const event = buildEvent({ htmlContent: '<!DOCTYPE html><html><body><p>This is<br>test<br>html code.</p></body></html>' })
     const formattedEvent = formatEvent(event)
@@ -193,8 +175,8 @@ describe('pipeline.formatEvent', () => {
       {name: 'Brittany Seaton', email: 'brittany@example.com', rsvp: true }
     ]})
     const formattedEvent = formatEvent(event)
-    expect(formattedEvent).to.contain('ATTENDEE;CN="Adam Gibbons":mailto:adam@example.com')
-    expect(formattedEvent).to.contain('ATTENDEE;RSVP=TRUE;CN="Brittany Seaton":mailto:brittany@example.com')
+    expect(formattedEvent).to.contain('ATTENDEE;CN=Adam Gibbons:mailto:adam@example.com')
+    expect(formattedEvent).to.contain('ATTENDEE;RSVP=TRUE;CN=Brittany Seaton:mailto:brittany@example.com')
   })
   it('writes a busystatus', () => {
     const eventFree = buildEvent({ busyStatus: "FREE" })
@@ -233,34 +215,22 @@ describe('pipeline.formatEvent', () => {
     expect(formattedEventAnyClass).to.contain('CLASS:non-standard-property')
   })
   it('writes an organizer', () => {
-    const formattedEvent = formatEvent({
-      productId: 'productId',
-      method: 'method',
-      uid: 'uid',
-      timestamp: 'timestamp',
-      organizer: {
+    const formattedEvent = formatEvent({ organizer: {
         name: 'Adam Gibbons',
         email: 'adam@example.com',
         dir: 'test-dir-value',
-        sentBy: 'test@example.com',
-      }
-    })
-    expect(formattedEvent).to.contain(foldLine('ORGANIZER;DIR="test-dir-value";SENT-BY="MAILTO:test@example.com";CN="Adam Gibbons":MAILTO:adam@example.com'))
+        sentBy: 'test@example.com'
+      }})
+    expect(formattedEvent).to.contain(foldLine('ORGANIZER;DIR="test-dir-value";SENT-BY="MAILTO:test@example.com";CN=Adam Gibbons:MAILTO:adam@example.com'))
   })
   it('writes an alarm', () => {
-    const formattedEvent = formatEvent({
-      productId: 'productId',
-      method: 'method',
-      uid: 'uid',
-      timestamp: 'timestamp',
-      alarms: [{
-        action: 'audio',
-        trigger: [1997, 2, 17, 1, 30],
-        repeat: 4,
-        duration: { minutes: 15 },
-        attach: 'ftp://example.com/pub/sounds/bell-01.aud'
-      }]
-    })
+    const formattedEvent = formatEvent({ alarms: [{
+      action: 'audio',
+      trigger: [1997, 2, 17, 1, 30],
+      repeat: 4,
+      duration: { minutes: 15 },
+      attach: 'ftp://example.com/pub/sounds/bell-01.aud'
+    }]})
 
     expect(formattedEvent).to.contain('BEGIN:VALARM')
     expect(formattedEvent).to.contain('TRIGGER;VALUE=DATE-TIME:199702')
@@ -274,15 +244,13 @@ describe('pipeline.formatEvent', () => {
     const formattedEvent = formatEvent({
       productId: '*'.repeat(1000),
       method: '*'.repeat(1000),
-      timestamp: '*'.repeat(1000),
-      uid: '*'.repeat(1000),
       title: '*'.repeat(1000),
       description: '*'.repeat(1000),
       url: '*'.repeat(1000),
       geo: '*'.repeat(1000),
       location: '*'.repeat(1000),
       status: '*'.repeat(1000),
-      categories: ['*'.repeat(1000)],
+      categories: '*'.repeat(1000),
       organizer: '*'.repeat(1000),
       attendees: [
         {name: '*'.repeat(1000), email: '*'.repeat(1000)},
@@ -293,38 +261,13 @@ describe('pipeline.formatEvent', () => {
     expect(max).to.be.at.most(75)
   })
   it('writes a recurrence rule', () => {
-    const formattedEvent = formatEvent({
-      productId: 'productId',
-      method: 'method',
-      uid: 'uid',
-      timestamp: 'timestamp',
-      recurrenceRule: 'FREQ=DAILY'
-    })
+    const formattedEvent = formatEvent({ recurrenceRule: 'FREQ=DAILY' })
 
     expect(formattedEvent).to.contain('RRULE:FREQ=DAILY')
   })
   it('writes exception date-time', () => {
-    const date1 = new Date(0);
-    date1.setUTCFullYear(2000);
-    date1.setUTCMonth(6);
-    date1.setUTCDate(20);
-    date1.setUTCHours(2);
-    date1.setUTCMinutes(0);
-    date1.setUTCSeconds(0);
+    const formattedEvent = formatEvent({ exclusionDates: '20000620T010000Z,20000621T010000Z' })
 
-    const date2 = new Date(date1);
-    date2.setUTCDate(21);
-
-    const formattedEvent = formatEvent({
-      productId: 'productId',
-      method: 'method',
-      uid: 'uid',
-      timestamp: 'timestamp',
-      exclusionDates: [
-        [date1.getUTCFullYear(), date1.getUTCMonth(), date1.getUTCDate(), date1.getUTCHours(), date1.getUTCMinutes(), date1.getUTCSeconds()],
-        [date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate(), date2.getUTCHours(), date2.getUTCMinutes(), date2.getUTCSeconds()]
-      ]
-    })
     expect(formattedEvent).to.contain('EXDATE:20000620T010000Z,20000621T010000Z')
   })
 })
diff --git a/test/pipeline/validate.spec.js b/test/pipeline/validate.spec.js
index 665299f..6820428 100644
--- a/test/pipeline/validate.spec.js
+++ b/test/pipeline/validate.spec.js
@@ -1,9 +1,9 @@
 import { expect } from 'chai'
-import { validateHeaderAndEvent } from '../../src/pipeline'
+import { validateEvent } from '../../src/pipeline'
 
-describe('pipeline.validateHeaderAndEvent', () => {
+describe('pipeline.validate', () => {
   it('validates an event', () => {
-    const { error, value } = validateHeaderAndEvent({
+    const { error, value } = validateEvent({
       uid: '1',
       start: [1997, 10, 1, 22, 30],
       duration: { hours: 1 }
@@ -12,7 +12,7 @@ describe('pipeline.validateHeaderAndEvent', () => {
     expect(value.uid).to.equal('1')
   })
   it('returns an error if the sequence number is too long', () => {
-    const { error, value } = validateHeaderAndEvent({
+    const { error, value } = validateEvent({
       uid: '1',
       start: [1997, 10, 1, 22, 30],
       duration: { hours: 1 },
@@ -21,13 +21,13 @@ describe('pipeline.validateHeaderAndEvent', () => {
     expect(error).to.exist
   })
   it('returns undefined when passed no event', () => {
-    const { error, value } = validateHeaderAndEvent()
+    const { error, value } = validateEvent()
     expect(value).to.be.undefined
   })
   it('returns an error when invalid data passed', () => {
-    expect(validateHeaderAndEvent(null).error).to.exist
-    expect(validateHeaderAndEvent(1).error).to.exist
-    expect(validateHeaderAndEvent('foo').error).to.exist
-    expect(validateHeaderAndEvent({}).error).to.exist
+    expect(validateEvent(null).error).to.exist
+    expect(validateEvent(1).error).to.exist
+    expect(validateEvent('foo').error).to.exist
+    expect(validateEvent({}).error).to.exist
   })
-})
+})
\ No newline at end of file
diff --git a/test/schema/index.spec.js b/test/schema/index.spec.js
index e1d68f8..deea7ee 100644
--- a/test/schema/index.spec.js
+++ b/test/schema/index.spec.js
@@ -1,17 +1,22 @@
 import { expect } from 'chai'
-import { validateHeaderAndEvent } from '../../src/schema'
+import validateEvent from '../../src/schema'
 
-describe('.validateHeaderAndEvent', () => {
+describe('.validateEvent', () => {
   describe('must have one and only one occurrence of', () => {
+    it('uid', () => {
+      const {error} = validateEvent({title: 'foo'})
+      expect(error.errors.some(p => p === 'uid is a required field')).to.be.true
+    })
+
     it('start', () => {
-      const {error} = validateHeaderAndEvent({title: 'foo', uid: 'foo'})
+      const {error} = validateEvent({title: 'foo', uid: 'foo'})
       expect(error.errors.some(p => p === 'start is a required field')).to.be.true
     })
   })
 
   describe('must have duration XOR end', () => {
     it('duration and end are not allowed together', () => {
-      const {error, value} = validateHeaderAndEvent({
+      const {error, value} = validateEvent({
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         duration: {hours: 1},
@@ -23,7 +28,7 @@ describe('.validateHeaderAndEvent', () => {
 
   describe('may have one and only one occurrence of', () => {
     it('summary', () => {
-      const {errors} = validateHeaderAndEvent({
+      const {errors} = validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -32,7 +37,7 @@ describe('.validateHeaderAndEvent', () => {
 
       expect(errors.some(p => p.match(/summary must be a `string` type/))).to.be.true
 
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -41,7 +46,7 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('description', () => {
-      const {errors} = validateHeaderAndEvent({
+      const {errors} = validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -49,7 +54,7 @@ describe('.validateHeaderAndEvent', () => {
       }).error
       expect(errors.some(p => p.match(/description must be a `string` type/))).to.be.true
 
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -58,14 +63,14 @@ describe('.validateHeaderAndEvent', () => {
 
     })
     it('url', () => {
-      const {errors} = validateHeaderAndEvent({
+      const {errors} = validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         url: 'abc'
       }).error
       expect(errors.some(p => p.match(/url must/))).to.be.true
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -74,21 +79,21 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('geo', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         geo: 'abc'
       }).error.name === 'ValidationError')
 
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         geo: {lat: 'thing', lon: 32.1},
       }).error.name === 'ValidationError')
 
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -96,7 +101,7 @@ describe('.validateHeaderAndEvent', () => {
       }).value.geo).to.exist
     })
     it('location', () => {
-      const {errors} = validateHeaderAndEvent({
+      const {errors} = validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -104,7 +109,7 @@ describe('.validateHeaderAndEvent', () => {
       }).error
       expect(errors.some(p => p.match(/location must be a `string` type/))).to.be.true
 
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -113,25 +118,25 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('status', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         status: 'tentativo'
       }).error).to.exist
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         status: 'tentative'
       }).value.status).to.equal('tentative')
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         status: 'cancelled'
       }).value.status).to.equal('cancelled')
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -140,7 +145,7 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('categories', () => {
-      const {errors} = validateHeaderAndEvent({
+      const {errors} = validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -149,7 +154,7 @@ describe('.validateHeaderAndEvent', () => {
 
       expect(errors.some(p => p.match(/categories\[0] must be a `string` type/))).to.be.true
 
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -158,14 +163,14 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('organizer', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         organizer: {name: 'Adam', email: 'adam@example.com'}
       }).value.organizer).to.include({name: 'Adam', email: 'adam@example.com'})
 
-      const {errors} = validateHeaderAndEvent({
+      const {errors} = validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -175,7 +180,7 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('attendees', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -184,7 +189,7 @@ describe('.validateHeaderAndEvent', () => {
           {name: 'Brittany', email: 'brittany@example.com'}]
       }).value.attendees).to.be.an('array').that.is.not.empty
 
-      const {errors} = validateHeaderAndEvent({
+      const {errors} = validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -194,7 +199,7 @@ describe('.validateHeaderAndEvent', () => {
       }).error
       expect(errors.some(p => p === 'attendees[0] field has unspecified keys: foo')).to.be.true
 
-      const res = validateHeaderAndEvent({
+      const res = validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -207,7 +212,7 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('created', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -216,7 +221,7 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('transp', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -225,7 +230,7 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('lastModified', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
@@ -234,7 +239,7 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('calName', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         calName: 'John\'s Calendar',
@@ -243,7 +248,7 @@ describe('.validateHeaderAndEvent', () => {
     })
 
     it('htmlContent', () => {
-      expect(validateHeaderAndEvent({
+      expect(validateEvent({
         title: 'foo',
         uid: 'foo',
         htmlContent: '<!DOCTYPE html><html><body><p>This is<br>test<br>html code.</p></body></html>',
@@ -254,7 +259,7 @@ describe('.validateHeaderAndEvent', () => {
 
   describe('may have one or more occurrences of', () => {
     it('alarm component', () => {
-      const event = validateHeaderAndEvent({
+      const event = validateEvent({
         uid: 'foo',
         start: [2018, 12, 1, 10, 30],
         duration: {hours: 1},
diff --git a/test/utils/encode-param-value.spec.js b/test/utils/encode-param-value.spec.js
deleted file mode 100644
index d3b5eff..0000000
--- a/test/utils/encode-param-value.spec.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { expect } from 'chai';
-import { encodeParamValue } from '../../src/utils'
-
-describe('utils.encodeParamValue', () => {
-  it('encodes correctly', () => {
-    expect(encodeParamValue('test')).to.equal(`"test"`)
-    expect(encodeParamValue('a"b"c')).to.equal(`"a\\"b\\"c"`)
-  });
-})
diff --git a/test/utils/format-date.spec.js b/test/utils/format-date.spec.js
index 0a1dbc5..e8e09c1 100644
--- a/test/utils/format-date.spec.js
+++ b/test/utils/format-date.spec.js
@@ -33,10 +33,4 @@ describe('utils.formatDate', () => {
     expect(formatDate([1998, 1, 18, 23, 9, 59], 'local', 'local'))
       .to.equal('19980118T230959')
   })
-  it('sets a UTC date-time when passed a unix timestamp', () => {
-    expect(formatDate(1694940441442)).to.equal('20230917T084721Z')
-  })
-  it('returns a string as is', () => {
-    expect(formatDate('20230917T084721Z')).to.equal('20230917T084721Z')
-  })
 })
diff --git a/test/utils/set-contact.spec.js b/test/utils/set-contact.spec.js
index d683074..3e9fb8d 100644
--- a/test/utils/set-contact.spec.js
+++ b/test/utils/set-contact.spec.js
@@ -5,23 +5,23 @@ describe('utils.setContact', () => {
   it('set a contact with role', () => {
     const contact = { name: 'm-vinc', email: 'vinc@example.com' }
     expect(setContact(contact))
-    .to.equal(`CN="m-vinc":mailto:vinc@example.com`)
+    .to.equal(`CN=m-vinc:mailto:vinc@example.com`)
 
     const contactChair = Object.assign({role: 'CHAIR'}, contact)
     expect(setContact(contactChair))
-    .to.equal(`ROLE="CHAIR";CN="m-vinc":mailto:vinc@example.com`)
+    .to.equal(`ROLE=CHAIR;CN=m-vinc:mailto:vinc@example.com`)
 
     const contactRequired = Object.assign({role: 'REQ-PARTICIPANT', rsvp: true }, contact)
     expect(setContact(contactRequired))
-    .to.equal(`RSVP=TRUE;ROLE="REQ-PARTICIPANT";CN="m-vinc":mailto:vinc@example.com`)
+    .to.equal(`RSVP=TRUE;ROLE=REQ-PARTICIPANT;CN=m-vinc:mailto:vinc@example.com`)
 
     const contactOptional = Object.assign({role: 'OPT-PARTICIPANT', rsvp: false }, contact)
     expect(setContact(contactOptional))
-    .to.equal(`RSVP=FALSE;ROLE="OPT-PARTICIPANT";CN="m-vinc":mailto:vinc@example.com`)
+    .to.equal(`RSVP=FALSE;ROLE=OPT-PARTICIPANT;CN=m-vinc:mailto:vinc@example.com`)
 
     const contactNon = Object.assign({role: 'NON-PARTICIPANT' }, contact)
     expect(setContact(contactNon))
-    .to.equal(`ROLE="NON-PARTICIPANT";CN="m-vinc":mailto:vinc@example.com`)
+    .to.equal(`ROLE=NON-PARTICIPANT;CN=m-vinc:mailto:vinc@example.com`)
   })
   it('set a contact with partstat', () => {
     const contact = { name: 'm-vinc', email: 'vinc@example.com' }
@@ -32,19 +32,19 @@ describe('utils.setContact', () => {
     const contactTentative = Object.assign({contact, partstat: 'TENTATIVE'}, contact)
 
     expect(setContact(contactUndefined))
-    .to.equal('CN="m-vinc":mailto:vinc@example.com')
+    .to.equal('CN=m-vinc:mailto:vinc@example.com')
 
     expect(setContact(contactNeedsAction))
-    .to.equal('PARTSTAT="NEEDS-ACTION";CN="m-vinc":mailto:vinc@example.com')
+    .to.equal('PARTSTAT=NEEDS-ACTION;CN=m-vinc:mailto:vinc@example.com')
 
     expect(setContact(contactDeclined))
-    .to.equal('PARTSTAT="DECLINED";CN="m-vinc":mailto:vinc@example.com')
+    .to.equal('PARTSTAT=DECLINED;CN=m-vinc:mailto:vinc@example.com')
 
     expect(setContact(contactTentative))
-    .to.equal('PARTSTAT="TENTATIVE";CN="m-vinc":mailto:vinc@example.com')
+    .to.equal('PARTSTAT=TENTATIVE;CN=m-vinc:mailto:vinc@example.com')
 
     expect(setContact(contactAccepted))
-    .to.equal('PARTSTAT="ACCEPTED";CN="m-vinc":mailto:vinc@example.com')
+    .to.equal('PARTSTAT=ACCEPTED;CN=m-vinc:mailto:vinc@example.com')
   })
   it('sets a contact and only sets RSVP if specified', () => {
     const contact1 = {
@@ -60,17 +60,17 @@ describe('utils.setContact', () => {
     }
 
     expect(setContact(contact1))
-    .to.equal('CN="Adam Gibbons":mailto:adam@example.com')
+    .to.equal('CN=Adam Gibbons:mailto:adam@example.com')
 
     expect(setContact(contact2))
-    .to.equal('RSVP=TRUE;DIR="https://example.com/contacts/adam";CN="Adam Gibbons":mailto:adam@example.com')
+    .to.equal('RSVP=TRUE;DIR=https://example.com/contacts/adam;CN=Adam Gibbons:mailto:adam@example.com')
   })
   it('set a contact with cutype and guests', () => {
     const contact = { name: 'm-vinc', email: 'vinc@example.com' }
     const contactCuGuests = Object.assign({ cutype: 'INDIVIDUAL', xNumGuests: 0 }, contact)
     const contactString = setContact(contactCuGuests)
 
-    expect(contactString).to.contain('CUTYPE="INDIVIDUAL"')
+    expect(contactString).to.contain('CUTYPE=INDIVIDUAL')
     expect(contactString).to.contain('X-NUM-GUESTS=0')
   })
 })