From 5398f09809544c7529e8cabec2ee997b2a4781ae Mon Sep 17 00:00:00 2001 From: Sebastian Pekarek Date: Thu, 29 Feb 2024 21:33:16 +0100 Subject: [PATCH] feat(Alarm): Add support for `email` alarm type In ICalAlarm, the methods `summary()`, `createAttendee()` and `attendees()` have been added to support `EMAIL` alarms in addition to `DISPLAY` and `AUDIO`. close #576 --- src/alarm.ts | 115 +++++++++++++++++++++++++++++++++++++++++--- src/attendee.ts | 23 ++++----- test/alarm.ts | 124 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+), 17 deletions(-) diff --git a/src/alarm.ts b/src/alarm.ts index 066313833..aa3ce0b37 100755 --- a/src/alarm.ts +++ b/src/alarm.ts @@ -8,14 +8,17 @@ import { generateCustomAttributes, checkDate, toDurationString, - toJSON + toJSON, + checkNameAndMail } from './tools.js'; import {ICalDateTimeValue} from './types.js'; +import ICalAttendee, { ICalAttendeeData } from './attendee.js'; export enum ICalAlarmType { display = 'display', - audio = 'audio' + audio = 'audio', + email = 'email' } export const ICalAlarmRelatesTo = { @@ -47,6 +50,8 @@ export interface ICalAlarmBaseData { repeat?: ICalAlarmRepeatData | null; attach?: string | ICalAttachment | null; description?: string | null; + summary?: string | null; + attendees?: ICalAttendee[] | ICalAttendeeData[]; x?: {key: string, value: string}[] | [string, string][] | Record; } @@ -63,6 +68,8 @@ interface ICalInternalAlarmData { interval: number | null; attach: ICalAttachment | null; description: string | null; + summary: string | null; + attendees: ICalAttendee[]; x: [string, string][]; } @@ -74,6 +81,8 @@ export interface ICalAlarmJSONData { interval: number | null; attach: ICalAttachment | null; description: string | null; + summary: string | null; + attendees: ICalAttendee[]; x: {key: string, value: string}[]; } @@ -116,6 +125,8 @@ export default class ICalAlarm { interval: null, attach: null, description: null, + summary: null, + attendees: [], x: [] }; @@ -131,6 +142,8 @@ export default class ICalAlarm { data.repeat && this.repeat(data.repeat); data.attach !== undefined && this.attach(data.attach); data.description !== undefined && this.description(data.description); + data.summary !== undefined && this.summary(data.summary); + data.attendees !== undefined && this.attendees(data.attendees); data.x !== undefined && this.x(data.x); } @@ -467,7 +480,8 @@ export default class ICalAlarm { /** * Get the alarm description. Used to set the alarm message - * if alarm type is display. Defaults to the event's summary. + * if alarm type is `display`. If the alarm type is `email`, it's + * used to set the email body. Defaults to the event's summary. * * @since 0.2.1 */ @@ -475,7 +489,8 @@ export default class ICalAlarm { /** * Set the alarm description. Used to set the alarm message - * if alarm type is display. Defaults to the event's summary. + * if alarm type is `display`. If the alarm type is `email`, it's + * used to set the email body. Defaults to the event's summary. * * @since 0.2.1 */ @@ -494,6 +509,79 @@ export default class ICalAlarm { } + /** + * Get the alarm summary. Used to set the email subject + * if alarm type is `email`. Defaults to the event's summary. + * + * @since 7.0.0 + */ + summary (): string | null; + + /** + * Set the alarm summary. Used to set the email subject + * if alarm type is display. Defaults to the event's summary. + * + * @since 0.2.1 + */ + summary (summary: string | null): this; + summary (summary?: string | null): this | string | null { + if (summary === undefined) { + return this.data.summary; + } + if (!summary) { + this.data.summary = null; + return this; + } + + this.data.summary = summary; + return this; + } + + + /** + * Creates a new {@link ICalAttendee} and returns it. Use options to prefill + * the attendee's attributes. Calling this method without options will create + * an empty attendee. + * + * @since 7.0.0 + */ + createAttendee(data: ICalAttendee | ICalAttendeeData | string): ICalAttendee { + if (data instanceof ICalAttendee) { + this.data.attendees.push(data); + return data; + } + if (typeof data === 'string') { + data = { email: data, ...checkNameAndMail('data', data) }; + } + + const attendee = new ICalAttendee(data, this); + this.data.attendees.push(attendee); + return attendee; + } + + + /** + * Get all attendees + * @since 7.0.0 + */ + attendees(): ICalAttendee[]; + + /** + * Add multiple attendees to your event + * + * @since 7.0.0 + */ + attendees(attendees: (ICalAttendee | ICalAttendeeData | string)[]): this; + attendees(attendees?: (ICalAttendee | ICalAttendeeData | string)[]): this | ICalAttendee[] { + if (!attendees) { + return this.data.attendees; + } + + attendees.forEach(attendee => this.createAttendee(attendee)); + return this; + } + + /** * Set X-* attributes. Woun't filter double attributes, * which are also added by another method (e.g. type), @@ -627,13 +715,28 @@ export default class ICalAlarm { } // DESCRIPTION - if (this.data.type === 'display' && this.data.description) { + if (this.data.type !== 'audio' && this.data.description) { g += 'DESCRIPTION:' + escape(this.data.description, false) + '\r\n'; } - else if (this.data.type === 'display') { + else if (this.data.type !== 'audio') { g += 'DESCRIPTION:' + escape(this.event.summary(), false) + '\r\n'; } + // SUMMARY + if (this.data.type === 'email' && this.data.summary) { + g += 'SUMMARY:' + escape(this.data.summary, false) + '\r\n'; + } + else if (this.data.type === 'email') { + g += 'SUMMARY:' + escape(this.event.summary(), false) + '\r\n'; + } + + // ATTENDEES + if (this.data.type === 'email') { + this.data.attendees.forEach(attendee => { + g += attendee.toString(); + }); + } + // CUSTOM X ATTRIBUTES g += generateCustomAttributes(this.data); diff --git a/src/attendee.ts b/src/attendee.ts index 3f23d78a8..81377b720 100755 --- a/src/attendee.ts +++ b/src/attendee.ts @@ -3,6 +3,7 @@ import {addOrGetCustomAttributes, checkEnum, checkNameAndMail, escape} from './tools.js'; import ICalEvent from './event.js'; +import ICalAlarm from './alarm.js'; interface ICalInternalAttendeeData { @@ -94,16 +95,16 @@ export enum ICalAttendeeType { */ export default class ICalAttendee { private readonly data: ICalInternalAttendeeData; - private readonly event: ICalEvent; + private readonly parent: ICalEvent | ICalAlarm; /** * Constructor of {@link ICalAttendee}. The event reference is * required to query the calendar's timezone when required. * * @param data Attendee Data - * @param event Reference to ICalEvent object + * @param parent Reference to ICalEvent object */ - constructor(data: ICalAttendeeData, event: ICalEvent) { + constructor(data: ICalAttendeeData, parent: ICalEvent | ICalAlarm) { this.data = { name: null, email: '', @@ -117,8 +118,8 @@ export default class ICalAttendee { delegatedFrom: null, x: [] }; - this.event = event; - if (!this.event) { + this.parent = parent; + if (!this.parent) { throw new Error('`event` option required!'); } if (!data.email) { @@ -367,14 +368,14 @@ export default class ICalAttendee { if(typeof delegatedTo === 'string') { this.data.delegatedTo = new ICalAttendee( { email: delegatedTo, ...checkNameAndMail('delegatedTo', delegatedTo) }, - this.event, + this.parent, ); } else if(delegatedTo instanceof ICalAttendee) { this.data.delegatedTo = delegatedTo; } else { - this.data.delegatedTo = new ICalAttendee(delegatedTo, this.event); + this.data.delegatedTo = new ICalAttendee(delegatedTo, this.parent); } this.data.status = ICalAttendeeStatus.DELEGATED; @@ -409,14 +410,14 @@ export default class ICalAttendee { else if(typeof delegatedFrom === 'string') { this.data.delegatedFrom = new ICalAttendee( { email: delegatedFrom, ...checkNameAndMail('delegatedFrom', delegatedFrom) }, - this.event, + this.parent, ); } else if(delegatedFrom instanceof ICalAttendee) { this.data.delegatedFrom = delegatedFrom; } else { - this.data.delegatedFrom = new ICalAttendee(delegatedFrom, this.event); + this.data.delegatedFrom = new ICalAttendee(delegatedFrom, this.parent); } return this; @@ -439,7 +440,7 @@ export default class ICalAttendee { * @since 0.2.0 */ delegatesTo (options: ICalAttendee | ICalAttendeeData | string): ICalAttendee { - const a = options instanceof ICalAttendee ? options : this.event.createAttendee(options); + const a = options instanceof ICalAttendee ? options : this.parent.createAttendee(options); this.delegatedTo(a); a.delegatedFrom(this); return a; @@ -462,7 +463,7 @@ export default class ICalAttendee { * @since 0.2.0 */ delegatesFrom (options: ICalAttendee | ICalAttendeeData | string): ICalAttendee { - const a = options instanceof ICalAttendee ? options : this.event.createAttendee(options); + const a = options instanceof ICalAttendee ? options : this.parent.createAttendee(options); this.delegatedFrom(a); a.delegatedTo(this); return a; diff --git a/test/alarm.ts b/test/alarm.ts index 0d46b076f..a973e8cdf 100644 --- a/test/alarm.ts +++ b/test/alarm.ts @@ -6,6 +6,7 @@ import moment from 'moment-timezone'; import ICalCalendar from '../src/calendar.js'; import ICalEvent from '../src/event.js'; import ICalAlarm, { ICalAlarmRelatesTo, ICalAlarmType } from '../src/alarm.js'; +import ICalAttendee from '../src/attendee.js'; describe('ical-generator Alarm', function () { @@ -630,6 +631,127 @@ describe('ical-generator Alarm', function () { }); }); + describe('summary()', function () { + it('setter should return this', function () { + const a = new ICalAlarm({}, new ICalEvent( + { start: new Date() }, + new ICalCalendar() + )); + + assert.deepStrictEqual(a, a.summary(null)); + assert.deepStrictEqual(a, a.summary('Hey Ho!')); + }); + + it('getter should return value', function () { + const a = new ICalAlarm({}, new ICalEvent( + { start: new Date() }, + new ICalCalendar() + )); + + assert.deepStrictEqual(a.summary(), null); + a.summary('blablabla'); + assert.deepStrictEqual(a.summary(), 'blablabla'); + a.summary(null); + assert.deepStrictEqual(a.summary(), null); + }); + + it('should change something', function () { + const a = new ICalAlarm({ + type: ICalAlarmType.email, + summary: 'Huibuh!' + }, new ICalEvent({ start: new Date() }, new ICalCalendar())); + assert.ok(a.toString().indexOf('\r\nSUMMARY:Huibuh') > -1); + }); + + it('should fallback to event summary', function () { + const a = new ICalAlarm( + { type: ICalAlarmType.email }, + new ICalEvent({ start: new Date(), summary: 'Example Event' }, new ICalCalendar()) + ); + + assert.ok(a.toString().indexOf('\r\nSUMMARY:Example Event') > -1); + }); + }); + + describe('createAttendee()', function () { + it('if Attendee passed, it should add and return it', function () { + const alarm = new ICalEvent({ start: new Date() }, new ICalCalendar()).createAlarm({ + type: ICalAlarmType.email + }); + + const attendee = new ICalAttendee({ email: 'mail@example.com' }, alarm); + assert.strictEqual(alarm.createAttendee(attendee), attendee, 'createAttendee returns attendee'); + assert.deepStrictEqual(alarm.attendees()[0], attendee, 'attendee pushed'); + }); + + it('should return a ICalAttendee instance', function () { + const alarm = new ICalEvent({ start: new Date() }, new ICalCalendar()).createAlarm({ + type: ICalAlarmType.email + });; + + assert.ok(alarm.createAttendee({ email: 'mail@example.com' }) instanceof ICalAttendee); + assert.strictEqual(alarm.attendees.length, 1, 'attendee pushed'); + }); + + it('should accept string', function () { + const alarm = new ICalEvent({ start: new Date() }, new ICalCalendar()).createAlarm({ + type: ICalAlarmType.email + });; + const attendee = alarm.createAttendee('Zac '); + + assert.strictEqual(attendee.name(), 'Zac'); + assert.strictEqual(attendee.email(), 'zac@example.com'); + assert.strictEqual(alarm.attendees().length, 1, 'attendee pushed'); + }); + + it('should throw error when string misformated', function () { + const alarm = new ICalEvent({ start: new Date() }, new ICalCalendar()).createAlarm({ + type: ICalAlarmType.email + });; + assert.throws(function () { + alarm.createAttendee('foo bar'); + }, /isn't formated correctly/); + }); + + it('should accept object', function () { + const alarm = new ICalEvent({ start: new Date() }, new ICalCalendar()).createAlarm({ + type: ICalAlarmType.email + }); + const attendee = alarm.createAttendee({name: 'Zac', email: 'zac@example.com'}); + + assert.strictEqual(attendee.name(), 'Zac'); + assert.strictEqual(attendee.email(), 'zac@example.com'); + assert.strictEqual(alarm.attendees().length, 1, 'attendee pushed'); + assert.ok(alarm.toString().includes('ATTENDEE;ROLE=REQ-PARTICIPANT;CN="Zac":MAILTO:zac@example.com')); + }); + }); + + describe('attendees()', function () { + it('getter should return an array of attendees…', function () { + const alarm = new ICalEvent({ start: new Date() }, new ICalCalendar()).createAlarm({ + type: ICalAlarmType.email + }); + assert.strictEqual(alarm.attendees().length, 0); + + const attendee = alarm.createAttendee({ email: 'mail@example.com' }); + assert.strictEqual(alarm.attendees().length, 1); + assert.deepStrictEqual(alarm.attendees()[0], attendee); + }); + + it('setter should add attendees and return this', function () { + const alarm = new ICalEvent({ start: new Date() }, new ICalCalendar()).createAlarm({ + type: ICalAlarmType.email + }); + const foo = alarm.attendees([ + { name: 'Person A', email: 'a@example.com' }, + { name: 'Person B', email: 'b@example.com' } + ]); + + assert.strictEqual(alarm.attendees().length, 2); + assert.deepStrictEqual(foo, alarm); + }); + }); + describe('x()', function () { it('is there', function () { const a = new ICalAlarm({}, new ICalEvent( @@ -653,10 +775,12 @@ describe('ical-generator Alarm', function () { assert.deepStrictEqual(a.toJSON(), { attach: null, + attendees: [], description: null, relatesTo: null, interval: null, repeat: null, + summary: null, trigger: 120, type: 'display', x: []