Repository: sebbo2002/ical-generator

Issue:
Include VTimezone
I would love to see `VTIMEZONE` included in the ical-generator. Some calendars are fine without them, like iCal, and Google, however Outlook requires a `VTIMEZONE` section when a timezone is used, otherwise it won't parse. I imagine there are other calendars that are also sticklers as well.

I imagine this could work by bundling in the [information from here](https://github.com/libical/libical/tree/master/zoneinfo), possibly as a build step, then include it in the ics file when relevant. 

Hints from issue comments:
@apnerve: This would be so much useful!@apnerve: BTW, since `moment-timezone` is already used, the information can be fetched from `moment-timezone` itself as mentioned in this [answer](https://stackoverflow.com/a/54556645/69310) on StackOverflow@Gr8z: Working on this now@sebbo2002: Closing this in favor of #198.@sebbo2002: Okay, everything back to the beginning. I first thought we were going to get this built in based on PR #198, but I see this implementation is based on `moment-timezone`. Which is no longer a dependency.

So we probably need a build step to bundle this information into a file. Or the feature just works if `moment-timezone` is installed and available. Because actually I don't want to hold timezone tables around in my library.@sebbo2002: Maybe just allow to integrate [ical-timezones](https://www.npmjs.com/package/@touch4it/ical-timezones) if required?@adventpit: I would be available to look into this. Do I understand correctly, that you would prefer to leave external dependencies out and have the user put in the according `VTIMEZONE` string from `ical-timezones`?
So an additional method on `ICalCalendar`to put this entire string in, instead of using the timezone information from the existing `ICalCalendar.timezone()` and fetching from `ical-timezones` ourselves?@sebbo2002: @adventpit I would allow the user to give an [ical-timezone](https://www.npmjs.com/package/@touch4it/ical-timezones) object in `timezone()` instead of a string. Just as different date objects are currently allowed. Thus it remains with one method `timezone()`. Also, this would be required for both `ICalCalendar` and `ICalEvent`.@adventpit: Okay, so I might be a bit stupid, because I'm not sure if I quite understand how we'd be doing this.

So working on the premise that [ical-timezones](https://www.npmjs.com/package/@touch4it/ical-timezones) stays external, that means we do not import `ical-timezones`, because we want to be as independent as possible, correct?

So firstly sending in an external object from `ical-timezones`, would require us again to import the package though, because we don't know how the object is built. Theoretically, we could also rebuild it ourselves, but I don't think that is a best practice especially for maintaining. However, there also is no external object on `ical-timezones`, only a string.
Following, this I guess what you're meaning is that we create a new Object, e.g. `ICalVtimezone`, the user can create and put the `VTIMEZONE` string from `ical-timezones` in through `ICalCalendar.timezone()`.

From there on, we'd just include this string, if the input object is `ICalVtimezone`.
Does that approach make sense?

Furthermore, `VTIMEZONE` is intended to allow multiple components of itself inside of a `VCALENDAR`, similar to `VEVENT`. In `VEVENT`, I don't think we'd require any changes, because the `TZID` is already present on `DTSTART` and `DTEND`.
So I think to properly implement `VTIMEZONE`, it would preferably be an array. I suppose you can workaround by just appending `VTIMEZONE` string inside our `ICalVtimezone` object, but that feels hacky imo.@sebbo2002: > So working on the premise that ical-timezones stays external, that means we do not import ical-timezones, because we want to be as independent as possible, correct?

Correct.

> So firstly sending in an external object from ical-timezones, would require us again to import the package though, because we don't know how the object is built.

All right, I had a few errors in thinking in my cursory answer, sorry. It's best to just forget what I said. I'll have to have another look at the whole thing, hopefully I'll get to it at the weekend. I thought it was a small thing when I quickly looked at it, but yes, you're right, it's not that simple.@sebbo2002: Okay, I have now had time after all, here is my suggestion: 😂

- The method `timezone()` of the `ICalCalendar` object is extended so that optionally `ical-timezone` can be passed. Something like `cal.timezone({name: 'Europe/Berlin', generator: tz})`
- The event method remains as it is. A string is assumed with the timezone.
- `ICalCalendar`'s `toString()` method is extended so that `VTIMEZONE`s of the used timezones are generated and inserted in the calendar if the reference to `ical-timezone` was passed accordingly before. Care must be taken that each time zone is inserted only once. If the time zone was not listed, there is no `VTIMEZONE` object in `toString()`'s output

What do you think?.@adventpit: Sorry for the delayed answer.

Yes I think this is more accurate, but I still can't wrap my head around it when it comes to the `ical-timezones` object. When we get `tz` in `cal.timezone({name: 'Europe/Berlin', generator: tz})`, how do me know what it can do without importing the library?@sebbo2002: Just save the reference for later usage, or do I miss anything:

*in timezone()*:
```js
this.icalTimezone = tz; // also validate it with a type checker (is* method)
```

*in toString()*:
```js
if(this.icalTimezone) {
    // use it
}
```

I'll probably get a chance to put some time in here again on Wednesday, so if you want I can have a look at it on Wednesday too. @adventpit: I think it's just me being stupid here. 

I was wondering about the type conversion from the object, because we later want to call on it then to do:
```javascript
tz.getVtimezoneComponent("Europe/Berlin")
```

So would `tz` basically be an `any` object to us or what do we check with the type checker?

I thought of looking into it myself and sending a pull request, once I've understood it completely, but I'm not sure, if I'm ever gonna reach that state xD So sure maybe take a look at it yourself on Wednesday and we can continue from there.

Thanks for your quick replies on the matter, it's a great project you built yourself here!@sebbo2002: Okay, yes, then it's probably better that way. I'll get back to you on Wednesday and maybe bring a solution with me.@sebbo2002: :tada: This issue has been resolved in version 2.0.0-develop.20 :tada:

The release is available on:
- [npm package (@next dist-tag)](https://www.npmjs.com/package/ical-generator/v/2.0.0-develop.20)
- [GitHub release](https://github.com/sebbo2002/ical-generator/releases/tag/v2.0.0-develop.20)
- `v2.0.0-develop.20`

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:@sebbo2002: :tada: This issue has been resolved in version 2.0.0 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/ical-generator/v/2.0.0)
- [GitHub release](https://github.com/sebbo2002/ical-generator/releases/tag/v2.0.0)
- `v2.0.0`

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:@github-actions: This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related issues and reference this one.

Solution patch:
diff --git a/.github/workflows/release-bot.yml b/.github/workflows/release-bot.yml
index 0e6b220..198c36a 100644
--- a/.github/workflows/release-bot.yml
+++ b/.github/workflows/release-bot.yml
@@ -13,8 +13,6 @@ jobs:
     steps:
       - name: ☁️ Checkout Project
         uses: actions/checkout@v2
-        with:
-          fetch-depth: 0
       - name: ☁️ Checkout ReleaseBot
         uses: actions/checkout@v2
         with:
diff --git a/README.md b/README.md
index 6e6fa09..4c8baf5 100755
--- a/README.md
+++ b/README.md
@@ -69,8 +69,29 @@ objects. You can also pass a string which is then passed to javascript's `Date`
 
 It is recommended to use UTC time as far as possible. `ical-generator` will output all time information as UTC time as
 long as no time zone is defined. For day.js, a plugin is necessary for this, which is a prerequisite. If a time zone is
-set, `ical-generator` assumes that the given time matches the time zone. If a `moment-timezone` object or Luxon's
-`setZone` method works, `ical-generator` sets it according to the time zone set in the calendar/event.
+set, `ical-generator` assumes that the given time matches the time zone. If a time zone is used, it is also recommended
+to use a VTimezone generator. Such a function generates a VTimezone entry and returns it. For example, ical-timezones can
+be used for this:
+
+```typescript
+import ical from 'ical-generator';
+import {getVtimezoneComponent} from '@touch4it/ical-timezones';
+
+const cal = new ICalCalendar();
+cal.timezone({
+    name: 'FOO',
+    generator: getVtimezoneComponent
+});
+cal.createEvent({
+    start: new Date(),
+    timezone: 'Europe/London'
+});
+```
+
+If a `moment-timezone` object or Luxon's `setZone` method works, `ical-generator` sets it according to the time zone set
+in the calendar/event.
+
+
 
 
 ## 🚦 Tests
diff --git a/package-lock.json b/package-lock.json
index 5a6e59e..ae31a1b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,7 +5,6 @@
   "requires": true,
   "packages": {
     "": {
-      "name": "ical-generator",
       "version": "v2.0.0-develop",
       "license": "MIT",
       "dependencies": {
@@ -20,6 +19,7 @@
         "@semantic-release/github": "^7.2.0",
         "@semantic-release/npm": "^7.1.0",
         "@semantic-release/release-notes-generator": "^9.0.2",
+        "@touch4it/ical-timezones": "^1.6.0",
         "@types/luxon": "^1.26.3",
         "@types/mocha": "^8.2.2",
         "@types/node": "^14.14.35",
@@ -1521,6 +1521,15 @@
         "node": ">= 6"
       }
     },
+    "node_modules/@touch4it/ical-timezones": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@touch4it/ical-timezones/-/ical-timezones-1.6.0.tgz",
+      "integrity": "sha512-C613mSzA0oZP6qIx/y/SOvxpOAViBurysLTKERfyWDw83y1pLFAIE4H5G6iPamREBaKiZ//Fthk90SDzjKllHQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/@types/aggregate-error": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@types/aggregate-error/-/aggregate-error-1.0.1.tgz",
@@ -5875,7 +5884,181 @@
         "treeverse",
         "validate-npm-package-name",
         "which",
-        "write-file-atomic"
+        "write-file-atomic",
+        "@npmcli/disparity-colors",
+        "@npmcli/git",
+        "@npmcli/installed-package-contents",
+        "@npmcli/map-workspaces",
+        "@npmcli/metavuln-calculator",
+        "@npmcli/move-file",
+        "@npmcli/name-from-folder",
+        "@npmcli/node-gyp",
+        "@npmcli/promise-spawn",
+        "@tootallnate/once",
+        "agent-base",
+        "agentkeepalive",
+        "aggregate-error",
+        "ajv",
+        "ansi-regex",
+        "ansi-styles",
+        "aproba",
+        "are-we-there-yet",
+        "asap",
+        "asn1",
+        "assert-plus",
+        "asynckit",
+        "aws-sign2",
+        "aws4",
+        "balanced-match",
+        "bcrypt-pbkdf",
+        "bin-links",
+        "binary-extensions",
+        "brace-expansion",
+        "builtins",
+        "caseless",
+        "cidr-regex",
+        "clean-stack",
+        "clone",
+        "cmd-shim",
+        "code-point-at",
+        "color-convert",
+        "color-name",
+        "colors",
+        "combined-stream",
+        "common-ancestor-path",
+        "concat-map",
+        "console-control-strings",
+        "core-util-is",
+        "dashdash",
+        "debug",
+        "debuglog",
+        "defaults",
+        "delayed-stream",
+        "delegates",
+        "depd",
+        "dezalgo",
+        "diff",
+        "ecc-jsbn",
+        "emoji-regex",
+        "encoding",
+        "env-paths",
+        "err-code",
+        "extend",
+        "extsprintf",
+        "fast-deep-equal",
+        "fast-json-stable-stringify",
+        "forever-agent",
+        "form-data",
+        "fs-minipass",
+        "fs.realpath",
+        "function-bind",
+        "gauge",
+        "getpass",
+        "har-schema",
+        "har-validator",
+        "has",
+        "has-flag",
+        "has-unicode",
+        "http-cache-semantics",
+        "http-proxy-agent",
+        "http-signature",
+        "https-proxy-agent",
+        "humanize-ms",
+        "iconv-lite",
+        "ignore-walk",
+        "imurmurhash",
+        "indent-string",
+        "infer-owner",
+        "inflight",
+        "inherits",
+        "ip",
+        "ip-regex",
+        "is-core-module",
+        "is-fullwidth-code-point",
+        "is-lambda",
+        "is-typedarray",
+        "isarray",
+        "isexe",
+        "isstream",
+        "jsbn",
+        "json-schema",
+        "json-schema-traverse",
+        "json-stringify-nice",
+        "json-stringify-safe",
+        "jsonparse",
+        "jsprim",
+        "just-diff",
+        "just-diff-apply",
+        "lru-cache",
+        "mime-db",
+        "mime-types",
+        "minimatch",
+        "minipass-collect",
+        "minipass-fetch",
+        "minipass-flush",
+        "minipass-json-stream",
+        "minipass-sized",
+        "minizlib",
+        "mute-stream",
+        "normalize-package-data",
+        "npm-bundled",
+        "npm-install-checks",
+        "npm-normalize-package-bin",
+        "npm-packlist",
+        "number-is-nan",
+        "oauth-sign",
+        "object-assign",
+        "once",
+        "p-map",
+        "path-is-absolute",
+        "path-parse",
+        "performance-now",
+        "process-nextick-args",
+        "promise-all-reject-late",
+        "promise-call-limit",
+        "promise-inflight",
+        "promise-retry",
+        "promzard",
+        "psl",
+        "punycode",
+        "qs",
+        "read-cmd-shim",
+        "readable-stream",
+        "request",
+        "resolve",
+        "retry",
+        "safe-buffer",
+        "safer-buffer",
+        "set-blocking",
+        "signal-exit",
+        "smart-buffer",
+        "socks",
+        "socks-proxy-agent",
+        "spdx-correct",
+        "spdx-exceptions",
+        "spdx-expression-parse",
+        "spdx-license-ids",
+        "sshpk",
+        "string_decoder",
+        "string-width",
+        "stringify-package",
+        "strip-ansi",
+        "supports-color",
+        "tunnel-agent",
+        "tweetnacl",
+        "typedarray-to-buffer",
+        "unique-filename",
+        "unique-slug",
+        "uri-js",
+        "util-deprecate",
+        "uuid",
+        "validate-npm-package-license",
+        "verror",
+        "walk-up-path",
+        "wcwidth",
+        "wide-align",
+        "wrappy",
+        "yallist"
       ],
       "dev": true,
       "dependencies": {
@@ -12635,6 +12818,12 @@
       "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
       "dev": true
     },
+    "@touch4it/ical-timezones": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@touch4it/ical-timezones/-/ical-timezones-1.6.0.tgz",
+      "integrity": "sha512-C613mSzA0oZP6qIx/y/SOvxpOAViBurysLTKERfyWDw83y1pLFAIE4H5G6iPamREBaKiZ//Fthk90SDzjKllHQ==",
+      "dev": true
+    },
     "@types/aggregate-error": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@types/aggregate-error/-/aggregate-error-1.0.1.tgz",
diff --git a/package.json b/package.json
index 36f7d0e..5bd9788 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
     "@semantic-release/github": "^7.2.0",
     "@semantic-release/npm": "^7.1.0",
     "@semantic-release/release-notes-generator": "^9.0.2",
+    "@touch4it/ical-timezones": "^1.6.0",
     "@types/luxon": "^1.26.3",
     "@types/mocha": "^8.2.2",
     "@types/node": "^14.14.35",
@@ -60,6 +61,7 @@
     "typescript": "^4.2.3"
   },
   "peerDependencies": {
+    "@touch4it/ical-timezones": "^1.6.0",
     "@types/luxon": "^1.26.0",
     "@types/mocha": "^8.2.1",
     "@types/node": "^14.14.31",
diff --git a/src/calendar.ts b/src/calendar.ts
index 1c1cee7..0bb91c0 100755
--- a/src/calendar.ts
+++ b/src/calendar.ts
@@ -13,6 +13,7 @@ import ICalEvent, {ICalEventData, ICalEventJSONData} from './event';
 import {writeFile, writeFileSync} from 'fs';
 import {promises as fsPromises} from 'fs';
 import {ServerResponse} from 'http';
+import {ICalTimezone} from './types';
 
 
 export interface ICalCalendarData {
@@ -20,7 +21,7 @@ export interface ICalCalendarData {
     method?: ICalCalendarMethod | null;
     name?: string | null;
     description?: string | null;
-    timezone?: string | null;
+    timezone?: ICalTimezone | string | null;
     url?: string | null;
     scale?: string | null;
     ttl?: number | Duration | null;
@@ -33,7 +34,7 @@ interface ICalCalendarInternalData {
     method: ICalCalendarMethod | null;
     name: string | null;
     description: string | null;
-    timezone: string | null;
+    timezone: ICalTimezone | null;
     url: string | null;
     scale: string | null;
     ttl: number | null;
@@ -290,12 +291,50 @@ export default class ICalCalendar {
      * @since 0.2.0
      */
     timezone(timezone: string | null): this;
-    timezone(timezone?: string | null): this | string | null {
+
+    /**
+     * For the best support of time zones, a VTimezone entry in the calendar is
+     * recommended, which informs the client about the corresponding time zones
+     * (daylight saving time, deviation from UTC, etc.). `ical-generator` itself
+     * does not have a time zone database, so an external generator is needed here.
+     *
+     * A VTimezone generator is a function that takes a time zone as a string and
+     * returns a VTimezone component according to the ical standard. For example,
+     * ical-timezones can be used for this:
+     *
+     * ```typescript
+     * import ical from 'ical-generator';
+     * import {getVtimezoneComponent} from '@touch4it/ical-timezones';
+     *
+     * const cal = new ICalCalendar();
+     * cal.timezone({
+     *     name: 'FOO',
+     *     generator: getVtimezoneComponent
+     * });
+     * cal.createEvent({
+     *     start: new Date(),
+     *     timezone: 'Europe/London'
+     * });
+     * ```
+     *
+     * @since 2.0.0
+     */
+    timezone(timezone: ICalTimezone | string | null): this;
+    timezone(timezone?: ICalTimezone | string | null): this | string | null {
         if (timezone === undefined) {
-            return this.data.timezone;
+            return this.data.timezone?.name || null;
+        }
+
+        if(typeof timezone === 'string') {
+            this.data.timezone = {name: timezone};
+        }
+        else if(timezone === null) {
+            this.data.timezone = null;
+        }
+        else {
+            this.data.timezone = timezone;
         }
 
-        this.data.timezone = timezone ? String(timezone) : null;
         return this;
     }
 
@@ -665,6 +704,7 @@ export default class ICalCalendar {
      */
     toJSON(): ICalCalendarJSONData {
         return Object.assign({}, this.data, {
+            timezone: this.timezone(),
             events: this.data.events.map(event => event.toJSON()),
             x: this.x()
         });
@@ -723,9 +763,30 @@ export default class ICalCalendar {
         }
 
         // Timezone
-        if (this.data.timezone) {
-            g += 'TIMEZONE-ID:' + this.data.timezone + '\r\n';
-            g += 'X-WR-TIMEZONE:' + this.data.timezone + '\r\n';
+        if(this.data.timezone?.generator) {
+            const timezones = [...new Set([
+                this.timezone(),
+                ...this.data.events.map(event => event.timezone())
+            ])].filter(tz => tz !== null && !tz.startsWith('/')) as string[];
+
+            timezones.forEach(tz => {
+                if(!this.data.timezone?.generator) {
+                    return;
+                }
+
+                const s = this.data.timezone.generator(tz);
+                if(!s) {
+                    return;
+                }
+
+                g += s.replace(/\r\n/g, '\n')
+                    .replace(/\n/g, '\r\n')
+                    .trim() + '\r\n';
+            });
+        }
+        if (this.data.timezone?.name) {
+            g += 'TIMEZONE-ID:' + this.data.timezone.name + '\r\n';
+            g += 'X-WR-TIMEZONE:' + this.data.timezone.name + '\r\n';
         }
 
         // TTL
diff --git a/src/index.ts b/src/index.ts
index 38a2715..e575b6f 100755
--- a/src/index.ts
+++ b/src/index.ts
@@ -85,5 +85,6 @@ export {
     ICalOrganizer,
     ICalDescription,
     ICalEventRepeatingFreq,
-    ICalWeekday
+    ICalWeekday,
+    ICalTimezone
 } from './types';
diff --git a/src/tools.ts b/src/tools.ts
index 3f67f38..92cb2e4 100755
--- a/src/tools.ts
+++ b/src/tools.ts
@@ -9,6 +9,10 @@ import type { RRule } from 'rrule';
 import {ICalDateTimeValue, ICalOrganizer} from './types';
 
 export function formatDate (timezone: string | null, d: ICalDateTimeValue, dateonly?: boolean, floating?: boolean): string {
+    if(timezone?.startsWith('/')) {
+        timezone = timezone.substr(1);
+    }
+
     if(typeof d === 'string' || d instanceof Date) {
         const m = new Date(d);
 
@@ -98,7 +102,7 @@ export function formatDateTZ (timezone: string | null, property: string, date: I
         floating = true;
     }
 
-    return property + tzParam + ':' + module.exports.formatDate(timezone, date, false, floating);
+    return property + tzParam + ':' + formatDate(timezone, date, false, floating);
 }
 
 export function escape (str: string | unknown): string {
diff --git a/src/types.ts b/src/types.ts
index 312c5aa..d787e21 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -48,6 +48,11 @@ export interface ICalDescription {
     html?: string;
 }
 
+export interface ICalTimezone {
+    name: string | null;
+    generator?: (timezone: string) => string|null;
+}
+
 export enum ICalEventRepeatingFreq {
     SECONDLY = 'SECONDLY',
     MINUTELY = 'MINUTELY',



Test patch:
diff --git a/test/calendar.ts b/test/calendar.ts
index 9762fbe..4608a07 100644
--- a/test/calendar.ts
+++ b/test/calendar.ts
@@ -8,6 +8,7 @@ import {join} from 'path';
 import {getPortPromise} from 'portfinder';
 import ICalCalendar, {ICalCalendarJSONData, ICalCalendarMethod} from '../src/calendar';
 import ICalEvent from '../src/event';
+import {getVtimezoneComponent} from '@touch4it/ical-timezones';
 
 describe('ical-generator Calendar', function () {
     describe('constructor()', function () {
@@ -188,6 +189,11 @@ describe('ical-generator Calendar', function () {
         it('setter should return this', function () {
             const cal = new ICalCalendar();
             assert.deepStrictEqual(cal, cal.timezone('Europe/Berlin'));
+            assert.deepStrictEqual(cal, cal.timezone(null));
+            assert.deepStrictEqual(cal, cal.timezone({
+                name: 'Europe/Berlin',
+                generator: getVtimezoneComponent
+            }));
         });
 
         it('getter should return value', function () {
@@ -196,17 +202,9 @@ describe('ical-generator Calendar', function () {
 
             cal.timezone(null);
             assert.strictEqual(cal.timezone(), null);
-        });
 
-        it('should make a difference to iCal output', function () {
-            const cal = new ICalCalendar().timezone('Europe/London');
-            cal.createEvent({
-                start: new Date(),
-                end: new Date(new Date().getTime() + 3600000),
-                summary: 'Example Event'
-            });
-
-            assert.strictEqual(cal.timezone(), 'Europe/London');
+            cal.timezone({name: 'Europe/Berlin'});
+            assert.strictEqual(cal.timezone(), 'Europe/Berlin');
         });
     });
 
@@ -645,7 +643,30 @@ describe('ical-generator Calendar', function () {
             assert.ok(cal.toString().indexOf('X-WR-TIMEZONE:TEST') > -1);
         });
 
-        it('should include the timezone', function () {
+        it('should include VTimezone objects if generator was supplied', function () {
+            const cal = new ICalCalendar();
+            cal.timezone({name: 'Europe/Berlin', generator: getVtimezoneComponent});
+            cal.createEvent({
+                start: new Date(),
+                timezone: 'Europe/London'
+            });
+
+            assert.ok(cal.toString().includes('BEGIN:VTIMEZONE\r\n'), 'BEGIN:VTIMEZONE');
+            assert.ok(cal.toString().includes('TZID:Europe/Berlin\r\n'), 'TZID:Europe/Berlin');
+            assert.ok(cal.toString().includes('TZID:Europe/London\r\n'), 'TZID:Europe/London');
+        });
+        it('should also work if VTimezone was not found', function () {
+            const cal = new ICalCalendar();
+            cal.timezone({name: 'FOO', generator: getVtimezoneComponent});
+            assert.ok(!cal.toString().includes('TZID:Foo\r\n'));
+        });
+        it('should ignore global timezone ids', function () {
+            const cal = new ICalCalendar();
+            cal.timezone({name: '/Europe/Berlin', generator: getVtimezoneComponent});
+            assert.ok(!cal.toString().includes('TZID:/Europe/Berlin\r\n'));
+        });
+
+        it('should include the ttl', function () {
             const cal = new ICalCalendar();
             cal.ttl(moment.duration(3, 'days'));
             assert.ok(cal.toString().indexOf('REFRESH-INTERVAL;VALUE=DURATION:P3D') > -1);
diff --git a/test/cases.ts b/test/cases.ts
index b945b4f..da6a012 100644
--- a/test/cases.ts
+++ b/test/cases.ts
@@ -8,6 +8,7 @@ import {ICalEventStatus} from '../src/event';
 import {ICalEventRepeatingFreq, ICalWeekday} from '../src/types';
 import {ICalAttendeeRole, ICalAttendeeStatus, ICalAttendeeType} from '../src/attendee';
 import {ICalAlarmType} from '../src/alarm';
+import {getVtimezoneComponent} from '@touch4it/ical-timezones';
 
 describe('ical-generator Cases', function () {
     it('case #1', async function () {
@@ -79,6 +80,7 @@ describe('ical-generator Cases', function () {
 
     it('case #4 (repeating)', async function () {
         const cal = ical({prodId: '//sebbo.net//ical-generator.tests//EN'});
+        cal.timezone({name: null, generator: getVtimezoneComponent});
         cal.events([
             {
                 id: '1',
@@ -93,9 +95,10 @@ describe('ical-generator Cases', function () {
             },
             {
                 id: '2',
-                start: new Date('Fr Oct 04 2013 22:39:30 UTC'),
-                end: new Date('Fr Oct 06 2013 23:15:00 UTC'),
+                start: new Date('Fr Oct 04 2013 22:39:30'),
+                end: new Date('Fr Oct 06 2013 23:15:00'),
                 stamp: new Date('Fr Oct 04 2013 23:34:53 UTC'),
+                timezone: 'Europe/Berlin',
                 summary: 'repeating by day, twice',
                 repeating: {
                     freq: ICalEventRepeatingFreq.DAILY,
@@ -116,9 +119,13 @@ describe('ical-generator Cases', function () {
             }
         ]);
 
-        const string = cal.toString();
-        assert.strictEqual(string, await fs.readFile(__dirname + '/results/generate_04.ics', 'utf8'));
-        assert.strictEqual(ical(cal.toJSON()).toString(), string);
+        assert.strictEqual(cal.toString(), await fs.readFile(__dirname + '/results/generate_04.ics', 'utf8'), 'first check');
+
+        // Wount be same, as reference to VTimezone generator is not exported
+        // assert.strictEqual(ical(cal.toJSON()).toString(), string);
+
+        cal.timezone(null);
+        assert.strictEqual(ical(cal.toJSON()).toString(), cal.toString(), 'second check');
     });
 
     it('case #5 (floating)', async function () {
diff --git a/test/results/generate_04.ics b/test/results/generate_04.ics
index c4fd0ca..5bd6e56 100755
--- a/test/results/generate_04.ics
+++ b/test/results/generate_04.ics
@@ -1,6 +1,25 @@
 BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//sebbo.net//ical-generator.tests//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
 BEGIN:VEVENT
 UID:1
 SEQUENCE:0
@@ -15,8 +34,8 @@ BEGIN:VEVENT
 UID:2
 SEQUENCE:0
 DTSTAMP:20131004T233453Z
-DTSTART:20131004T223930Z
-DTEND:20131006T231500Z
+DTSTART;TZID=Europe/Berlin:20131004T223930
+DTEND;TZID=Europe/Berlin:20131006T231500
 RRULE:FREQ=DAILY;COUNT=2
 SUMMARY:repeating by day\, twice
 END:VEVENT