ical-generator
Version:
ical-generator is a small piece of code which generates ical calendar files
694 lines (646 loc) • 19.5 kB
text/typescript
'use strict';
import type ICalAlarm from './alarm.ts';
import type ICalEvent from './event.ts';
import {
addOrGetCustomAttributes,
checkEnum,
checkNameAndMail,
escape,
} from './tools.ts';
export enum ICalAttendeeRole {
CHAIR = 'CHAIR',
NON = 'NON-PARTICIPANT',
OPT = 'OPT-PARTICIPANT',
REQ = 'REQ-PARTICIPANT',
}
// ref: https://datatracker.ietf.org/doc/html/rfc6638#section-7.1
export enum ICalAttendeeScheduleAgent {
CLIENT = 'CLIENT',
NONE = 'NONE',
SERVER = 'SERVER',
}
export enum ICalAttendeeStatus {
ACCEPTED = 'ACCEPTED',
DECLINED = 'DECLINED',
DELEGATED = 'DELEGATED',
NEEDSACTION = 'NEEDS-ACTION',
TENTATIVE = 'TENTATIVE',
}
// ref: https://tools.ietf.org/html/rfc2445#section-4.2.3
export enum ICalAttendeeType {
GROUP = 'GROUP',
INDIVIDUAL = 'INDIVIDUAL',
RESOURCE = 'RESOURCE',
ROOM = 'ROOM',
UNKNOWN = 'UNKNOWN',
}
export interface ICalAttendeeData {
delegatedFrom?: ICalAttendee | ICalAttendeeData | null | string;
delegatedTo?: ICalAttendee | ICalAttendeeData | null | string;
delegatesFrom?: ICalAttendee | ICalAttendeeData | null | string;
delegatesTo?: ICalAttendee | ICalAttendeeData | null | string;
email: string;
mailto?: null | string;
name?: null | string;
role?: ICalAttendeeRole;
rsvp?: boolean | null;
scheduleAgent?: ICalAttendeeScheduleAgent | ICalXName | null;
sentBy?: null | string;
status?: ICalAttendeeStatus | null;
type?: ICalAttendeeType | null;
x?:
| [string, string][]
| Record<string, string>
| { key: string; value: string }[];
}
export interface ICalAttendeeJSONData {
delegatedFrom: null | string;
delegatedTo: null | string;
email: string;
mailto: null | string;
name: null | string;
role: ICalAttendeeRole;
rsvp: boolean | null;
sentBy: null | string;
status: ICalAttendeeStatus | null;
type: ICalAttendeeType | null;
x: { key: string; value: string }[];
}
export type ICalXName = `X-${string}`;
interface ICalInternalAttendeeData {
delegatedFrom: ICalAttendee | null;
delegatedTo: ICalAttendee | null;
email: string;
mailto: null | string;
name: null | string;
role: ICalAttendeeRole;
rsvp: boolean | null;
scheduleAgent: ICalAttendeeScheduleAgent | ICalXName | null;
sentBy: null | string;
status: ICalAttendeeStatus | null;
type: ICalAttendeeType | null;
x: [string, string][];
}
/**
* Usually you get an {@link ICalAttendee} object like this:
*
* ```javascript
* import ical from 'ical-generator';
* const calendar = ical();
* const event = calendar.createEvent();
* const attendee = event.createAttendee({ email: 'mail@example.com' });
* ```
*
* You can also use the {@link ICalAttendee} object directly:
*
* ```javascript
* import ical, {ICalAttendee} from 'ical-generator';
* const attendee = new ICalAttendee({ email: 'mail@example.com' });
* event.attendees([attendee]);
* ```
*/
export default class ICalAttendee {
private readonly data: ICalInternalAttendeeData;
private readonly parent: ICalAlarm | ICalEvent;
/**
* Constructor of {@link ICalAttendee}. The event reference is
* required to query the calendar's timezone when required.
*
* @param data Attendee Data
* @param parent Reference to ICalEvent object
*/
constructor(data: ICalAttendeeData, parent: ICalAlarm | ICalEvent) {
this.data = {
delegatedFrom: null,
delegatedTo: null,
email: '',
mailto: null,
name: null,
role: ICalAttendeeRole.REQ,
rsvp: null,
scheduleAgent: null,
sentBy: null,
status: null,
type: null,
x: [],
};
this.parent = parent;
if (!this.parent) {
throw new Error('`event` option required!');
}
if (!data.email) {
throw new Error('No value for `email` in ICalAttendee given!');
}
if (data.name !== undefined) this.name(data.name);
if (data.email !== undefined) this.email(data.email);
if (data.mailto !== undefined) this.mailto(data.mailto);
if (data.sentBy !== undefined) this.sentBy(data.sentBy);
if (data.status !== undefined) this.status(data.status);
if (data.role !== undefined) this.role(data.role);
if (data.rsvp !== undefined) this.rsvp(data.rsvp);
if (data.type !== undefined) this.type(data.type);
if (data.delegatedTo !== undefined) this.delegatedTo(data.delegatedTo);
if (data.delegatedFrom !== undefined)
this.delegatedFrom(data.delegatedFrom);
if (data.delegatesTo) this.delegatesTo(data.delegatesTo);
if (data.delegatesFrom) this.delegatesFrom(data.delegatesFrom);
if (data.scheduleAgent !== undefined)
this.scheduleAgent(data.scheduleAgent);
if (data.x !== undefined) this.x(data.x);
}
/**
* Get the attendee's delegated-from field
* @since 0.2.0
*/
delegatedFrom(): ICalAttendee | null;
/**
* Set the attendee's delegated-from field
*
* Creates a new Attendee if the passed object is not already a
* {@link ICalAttendee} object. Will set the `delegatedTo` and
* `delegatedFrom` attributes.
*
* @param delegatedFrom
*/
delegatedFrom(
delegatedFrom: ICalAttendee | ICalAttendeeData | null | string,
): this;
delegatedFrom(
delegatedFrom?: ICalAttendee | ICalAttendeeData | null | string,
): ICalAttendee | null | this {
if (delegatedFrom === undefined) {
return this.data.delegatedFrom;
}
if (!delegatedFrom) {
this.data.delegatedFrom = null;
} else if (typeof delegatedFrom === 'string') {
this.data.delegatedFrom = new ICalAttendee(
{
email: delegatedFrom,
...checkNameAndMail('delegatedFrom', delegatedFrom),
},
this.parent,
);
} else if (delegatedFrom instanceof ICalAttendee) {
this.data.delegatedFrom = delegatedFrom;
} else {
this.data.delegatedFrom = new ICalAttendee(
delegatedFrom,
this.parent,
);
}
return this;
}
/**
* Get the attendee's delegated-to value.
* @since 0.2.0
*/
delegatedTo(): ICalAttendee | null;
/**
* Set the attendee's delegated-to field.
*
* Creates a new Attendee if the passed object is not already a
* {@link ICalAttendee} object. Will set the `delegatedTo` and
* `delegatedFrom` attributes.
*
* Will also set the `status` to `DELEGATED`, if attribute is set.
*
* ```javascript
* const cal = ical();
* const event = cal.createEvent();
* const attendee = cal.createAttendee();
*
* attendee.delegatesTo({email: 'foo@bar.com', name: 'Foo'});
```
*
* @since 0.2.0
*/
delegatedTo(
delegatedTo: ICalAttendee | ICalAttendeeData | null | string,
): this;
delegatedTo(
delegatedTo?: ICalAttendee | ICalAttendeeData | null | string,
): ICalAttendee | null | this {
if (delegatedTo === undefined) {
return this.data.delegatedTo;
}
if (!delegatedTo) {
this.data.delegatedTo = null;
if (this.data.status === ICalAttendeeStatus.DELEGATED) {
this.data.status = null;
}
return this;
}
if (typeof delegatedTo === 'string') {
this.data.delegatedTo = new ICalAttendee(
{
email: delegatedTo,
...checkNameAndMail('delegatedTo', delegatedTo),
},
this.parent,
);
} else if (delegatedTo instanceof ICalAttendee) {
this.data.delegatedTo = delegatedTo;
} else {
this.data.delegatedTo = new ICalAttendee(delegatedTo, this.parent);
}
this.data.status = ICalAttendeeStatus.DELEGATED;
return this;
}
/**
* Create a new attendee this attendee delegates from and returns
* this new attendee. Creates a new attendee if the passed object
* is not already an {@link ICalAttendee}.
*
* ```javascript
* const cal = ical();
* const event = cal.createEvent();
* const attendee = cal.createAttendee();
*
* attendee.delegatesFrom({email: 'foo@bar.com', name: 'Foo'});
* ```
*
* @since 0.2.0
*/
delegatesFrom(
options: ICalAttendee | ICalAttendeeData | string,
): ICalAttendee {
const a =
options instanceof ICalAttendee
? options
: this.parent.createAttendee(options);
this.delegatedFrom(a);
a.delegatedTo(this);
return a;
}
/**
* Create a new attendee this attendee delegates to and returns
* this new attendee. Creates a new attendee if the passed object
* is not already an {@link ICalAttendee}.
*
* ```javascript
* const cal = ical();
* const event = cal.createEvent();
* const attendee = cal.createAttendee();
*
* attendee.delegatesTo({email: 'foo@bar.com', name: 'Foo'});
* ```
*
* @since 0.2.0
*/
delegatesTo(
options: ICalAttendee | ICalAttendeeData | string,
): ICalAttendee {
const a =
options instanceof ICalAttendee
? options
: this.parent.createAttendee(options);
this.delegatedTo(a);
a.delegatedFrom(this);
return a;
}
/**
* Get the attendee's email address
* @since 0.2.0
*/
email(): string;
/**
* Set the attendee's email address
* @since 0.2.0
*/
email(email: string): this;
email(email?: string): string | this {
if (!email) {
return this.data.email;
}
this.data.email = email;
return this;
}
/**
* Get the attendee's email address
* @since 1.3.0
*/
mailto(): null | string;
/**
* Set the attendee's email address
* @since 1.3.0
*/
mailto(mailto: null | string): this;
mailto(mailto?: null | string): null | string | this {
if (mailto === undefined) {
return this.data.mailto;
}
this.data.mailto = mailto || null;
return this;
}
/**
* Get the attendee's name
* @since 0.2.0
*/
name(): null | string;
/**
* Set the attendee's name
* @since 0.2.0
*/
name(name: null | string): this;
name(name?: null | string): null | string | this {
if (name === undefined) {
return this.data.name;
}
this.data.name = name || null;
return this;
}
/**
* Get attendee's role
* @since 0.2.0
*/
role(): ICalAttendeeRole;
/**
* Set the attendee's role, defaults to `REQ` / `REQ-PARTICIPANT`.
* Checkout {@link ICalAttendeeRole} for available roles.
*
* @since 0.2.0
*/
role(role: ICalAttendeeRole): this;
role(role?: ICalAttendeeRole): ICalAttendeeRole | this {
if (role === undefined) {
return this.data.role;
}
this.data.role = checkEnum(ICalAttendeeRole, role) as ICalAttendeeRole;
return this;
}
/**
* Get attendee's RSVP expectation
* @since 0.2.1
*/
rsvp(): boolean | null;
/**
* Set the attendee's RSVP expectation
* @since 0.2.1
*/
rsvp(rsvp: boolean | null): this;
rsvp(rsvp?: boolean | null): boolean | null | this {
if (rsvp === undefined) {
return this.data.rsvp;
}
if (rsvp === null) {
this.data.rsvp = null;
return this;
}
this.data.rsvp = Boolean(rsvp);
return this;
}
/**
* Get attendee's schedule agent
* @since 9.0.0
*/
scheduleAgent(): ICalAttendeeScheduleAgent | ICalXName;
/**
* Set attendee's schedule agent
* See {@link ICalAttendeeScheduleAgent} for available values.
* You can also use custom X-* values.
*
* @since 9.0.0
*/
scheduleAgent(
scheduleAgent: ICalAttendeeScheduleAgent | ICalXName | null,
): this;
scheduleAgent(
scheduleAgent?: ICalAttendeeScheduleAgent | ICalXName | null,
): ICalAttendeeScheduleAgent | ICalXName | null | this {
if (scheduleAgent === undefined) {
return this.data.scheduleAgent;
}
if (!scheduleAgent) {
this.data.scheduleAgent = null;
return this;
}
if (
typeof scheduleAgent == 'string' &&
scheduleAgent.toUpperCase().startsWith('X-')
) {
this.data.scheduleAgent = scheduleAgent as string as ICalXName;
return this;
}
this.data.scheduleAgent = checkEnum(
ICalAttendeeScheduleAgent,
scheduleAgent,
) as ICalAttendeeScheduleAgent;
return this;
}
/**
* Get the acting user's email adress
* @since 3.3.0
*/
sentBy(): null | string;
/**
* Set the acting user's email adress
* @since 3.3.0
*/
sentBy(email: null | string): this;
sentBy(email?: null | string): null | string | this {
if (!email) {
return this.data.sentBy;
}
this.data.sentBy = email;
return this;
}
/**
* Get attendee's status
* @since 0.2.0
*/
status(): ICalAttendeeStatus | null;
/**
* Set the attendee's status. See {@link ICalAttendeeStatus}
* for available status options.
*
* @since 0.2.0
*/
status(status: ICalAttendeeStatus | null): this;
status(
status?: ICalAttendeeStatus | null,
): ICalAttendeeStatus | null | this {
if (status === undefined) {
return this.data.status;
}
if (!status) {
this.data.status = null;
return this;
}
this.data.status = checkEnum(
ICalAttendeeStatus,
status,
) as ICalAttendeeStatus;
return this;
}
/**
* Return a shallow copy of the attendee's options for JSON stringification.
* Can be used for persistence.
*
* @since 0.2.4
*/
toJSON(): ICalAttendeeJSONData {
return Object.assign({}, this.data, {
delegatedFrom: this.data.delegatedFrom?.email() || null,
delegatedTo: this.data.delegatedTo?.email() || null,
x: this.x(),
});
}
/**
* Return generated attendee as a string.
*
* ```javascript
* console.log(attendee.toString()); // → ATTENDEE;ROLE=…
* ```
*/
toString(): string {
let g = 'ATTENDEE';
if (!this.data.email) {
throw new Error('No value for `email` in ICalAttendee given!');
}
// ROLE
g += ';ROLE=' + this.data.role;
// TYPE
if (this.data.type) {
g += ';CUTYPE=' + this.data.type;
}
// PARTSTAT
if (this.data.status) {
g += ';PARTSTAT=' + this.data.status;
}
// RSVP
if (this.data.rsvp !== null) {
g += ';RSVP=' + this.data.rsvp.toString().toUpperCase();
}
// SENT-BY
if (this.data.sentBy !== null) {
g += ';SENT-BY="mailto:' + this.data.sentBy + '"';
}
// DELEGATED-TO
if (this.data.delegatedTo) {
g += ';DELEGATED-TO="' + this.data.delegatedTo.email() + '"';
}
// DELEGATED-FROM
if (this.data.delegatedFrom) {
g += ';DELEGATED-FROM="' + this.data.delegatedFrom.email() + '"';
}
// CN / Name
if (this.data.name) {
g += ';CN="' + escape(this.data.name, true) + '"';
}
// EMAIL
if (this.data.email && this.data.mailto) {
g += ';EMAIL=' + escape(this.data.email, false);
}
// SCHEDULE-AGENT
if (this.data.scheduleAgent) {
g += ';SCHEDULE-AGENT=' + this.data.scheduleAgent;
}
// CUSTOM X ATTRIBUTES
if (this.data.x.length) {
g +=
';' +
this.data.x
.map(
([key, value]) =>
key.toUpperCase() + '=' + escape(value, false),
)
.join(';');
}
g +=
':MAILTO:' +
escape(this.data.mailto || this.data.email, false) +
'\r\n';
return g;
}
/**
* Get attendee's type (a.k.a. CUTYPE)
* @since 0.2.3
*/
type(): ICalAttendeeType;
/**
* Set attendee's type (a.k.a. CUTYPE).
* See {@link ICalAttendeeType} for available status options.
*
* @since 0.2.3
*/
type(type: ICalAttendeeType | null): this;
type(type?: ICalAttendeeType | null): ICalAttendeeType | null | this {
if (type === undefined) {
return this.data.type;
}
if (!type) {
this.data.type = null;
return this;
}
this.data.type = checkEnum(ICalAttendeeType, type) as ICalAttendeeType;
return this;
}
/**
* Set X-* attributes. Woun't filter double attributes,
* which are also added by another method (e.g. status),
* so these attributes may be inserted twice.
*
* ```javascript
* attendee.x([
* {
* key: "X-MY-CUSTOM-ATTR",
* value: "1337!"
* }
* ]);
*
* attendee.x([
* ["X-MY-CUSTOM-ATTR", "1337!"]
* ]);
*
* attendee.x({
* "X-MY-CUSTOM-ATTR": "1337!"
* });
* ```
*
* @since 1.9.0
*/
x(
keyOrArray:
| [string, string][]
| Record<string, string>
| { key: string; value: string }[],
): this;
/**
* Set a X-* attribute. Woun't filter double attributes,
* which are also added by another method (e.g. status),
* so these attributes may be inserted twice.
*
* ```javascript
* attendee.x("X-MY-CUSTOM-ATTR", "1337!");
* ```
*
* @since 1.9.0
*/
x(keyOrArray: string, value: string): this;
/**
* Get all custom X-* attributes.
* @since 1.9.0
*/
x(): { key: string; value: string }[];
x(
keyOrArray?:
| [string, string][]
| Record<string, string>
| string
| { key: string; value: string }[],
value?: string,
): this | void | { key: string; value: string }[] {
if (keyOrArray === undefined) {
return addOrGetCustomAttributes(this.data);
}
if (typeof keyOrArray === 'string' && typeof value === 'string') {
addOrGetCustomAttributes(this.data, keyOrArray, value);
} else if (typeof keyOrArray === 'object') {
addOrGetCustomAttributes(this.data, keyOrArray);
} else {
throw new Error('Either key or value is not a string!');
}
return this;
}
}