cal-invite 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CONFIGURATION.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  Complete reference for every configurable option in CalInvite: `Event` attributes, providers, the `generate_calendar_url` call, ICS/email-invite delivery, and global `CalInvite.configure` settings.
4
4
 
5
- For caching-specific detail, see [CACHING.md](CACHING.md).
6
-
7
5
  ## Table of Contents
8
6
 
9
7
  - [Event attributes](#event-attributes)
@@ -22,23 +20,23 @@ For caching-specific detail, see [CACHING.md](CACHING.md).
22
20
 
23
21
  ## Event attributes
24
22
 
25
- All attributes are set via `CalInvite::Event.new(attributes)` or `event.update_attributes(attributes)`.
23
+ All attributes are set via `CalInvite::Event.new(attributes)` or `event.update_attributes(attributes)` (symbol or string keys). Any key not listed below raises `ArgumentError` (`Unknown attribute(s): ...`); `update_attributes` checks all keys before changing anything.
26
24
 
27
25
  | Attribute | Type | Required | Default | Notes |
28
26
  |------------------------|-----------------|----------------------------------------|---------|-------|
29
27
  | `title` | `String` | yes | — | Non-blank. |
30
28
  | `start_time` | `Time` | yes, unless `all_day` or using `multi_day_sessions` | — | Always pass UTC. |
31
- | `end_time` | `Time` | yes, unless `all_day` or using `multi_day_sessions` | — | Always pass UTC. |
29
+ | `end_time` | `Time` | yes, unless `all_day` or using `multi_day_sessions` | — | Always pass UTC. Must be after `start_time` (`ArgumentError` otherwise); all-day events may end on the same date they start. |
32
30
  | `description` | `String` | no | `nil` | Plain text; combined with `notes` in provider output. |
33
31
  | `location` | `String` | no | `nil` | Physical location only. Kept separate from `url` so each provider formats it correctly. |
34
32
  | `url` | `String` | no | `nil` | Virtual meeting link (Zoom, Meet, Teams, etc). Kept separate from `location`. |
35
33
  | `attendees` | `Array<String, Hash>` | no | `nil` | Email strings, or `{ email:, name:, partstat:, rsvp: }` hashes for a display name (`CN=`), a specific RSVP status, and/or an explicit `RSVP=` override. Only emitted if `show_attendees` is `true`. `partstat` is one of `:accepted`, `:declined`, `:tentative`, `:needs_action` (default), `:delegated`. `rsvp:` defaults to `true` for `:request`/`:cancel`/`:publish` and `false` for `:reply`/`:counter`/`:decline_counter`; set it explicitly to override (e.g. `rsvp: false` on an already-`:accepted` attendee in a registration-confirmation invite — see [Replicating a ticketing-platform confirmation email](#replicating-a-ticketing-platform-confirmation-email)). |
36
34
  | `show_attendees` | `Boolean` | no | `false` | Gate for including `attendees` in generated output. |
37
- | `organizer` | `Hash` | no, but required for `method: :request` | `nil` | `{ name: "Jane Doe", email: "jane@example.com" }`. `name` is optional. See [Email meeting invites](#email-meeting-invites-rsvp-capable). |
38
- | `timezone` | `String` | no | `'UTC'` | Controls display/formatting only — does not affect how `start_time`/`end_time` are interpreted. Any IANA identifier (e.g. `'America/New_York'`) produces a correctly converted `DTSTART;TZID=...` plus a full `VTIMEZONE` block; see [Timezones and VTIMEZONE](#timezones-and-vtimezone). |
35
+ | `organizer` | `String`/`Hash` | no, but required for `method: :request` | `nil` | `"jane@example.com"` or `{ name: "Jane Doe", email: "jane@example.com" }`. `name` is optional. See [Email meeting invites](#email-meeting-invites-rsvp-capable). |
36
+ | `timezone` | `String` | no | `CalInvite.configuration.timezone` (`'UTC'`) | Controls display/formatting only — does not affect how `start_time`/`end_time` are interpreted. Must be `'UTC'` or an IANA identifier (e.g. `'America/New_York'`), which produces a correctly converted `DTSTART;TZID=...` plus a full `VTIMEZONE` block; anything else raises `ArgumentError`. See [Timezones and VTIMEZONE](#timezones-and-vtimezone). |
39
37
  | `notes` | `String` | no | `nil` | Appended to the description. |
40
38
  | `all_day` | `Boolean` | no | `false` | When `true`, `start_time`/`end_time` validation is skipped. |
41
- | `multi_day_sessions` | `Array<Hash>` | no | `[]` | `[{ start_time:, end_time: }, ...]`. Used instead of `start_time`/`end_time` for multi-session events. |
39
+ | `multi_day_sessions` | `Array<Hash>` | no | `[]` | `[{ start_time:, end_time: }, ...]`. Used instead of `start_time`/`end_time` for multi-session events. Each session needs both times, with `end_time` after `start_time`. `:ics`/`:ical` emit one `VEVENT` per session and `:yahoo` one URL per session (newline-separated); `:google`/`:outlook`/`:office365` can only express one range, so they use `start_time`/`end_time`, which default to the earliest session start and latest session end (explicit values win). |
42
40
  | `uid` | `String` | no | randomly generated, memoized per `Event` instance | Stable RFC 5545 identifier. **Must** be reused across calls when you send an update (`:request`) or cancellation (`:cancel`) for a previously sent invite — see [Updating and cancelling invites](#updating-and-cancelling-invites). |
43
41
  | `sequence` | `Integer` | no | `0` | RFC 5545 SEQUENCE. Increment it yourself each time you resend a `:request`/`:cancel` for the same `uid`. |
44
42
  | `geo` | `Array<Float>`, `Hash` | no | `nil` | `[37.4595, -122.1418]` or `{ lat:, lng: }`. Emits `GEO:lat;lng` — lets Apple/Google Maps deep-link from the invite. |
@@ -88,7 +86,7 @@ Pass one of these symbols to `generate_calendar_url`:
88
86
 
89
87
  `CalInvite::Providers::SUPPORTED_PROVIDERS` holds the full list programmatically.
90
88
 
91
- `CalInvite::Providers::IcsContent` is also available directly (not via `generate_calendar_url`) if you want raw `.ics` content without going through `Event#generate_calendar_url`'s caching path — see [ICS content and downloads](#ics-content-and-downloads).
89
+ `CalInvite::Providers::IcsContent` is also available directly (not via `generate_calendar_url`) if you want raw `.ics` content without `Event#generate_calendar_url`'s validation — see [ICS content and downloads](#ics-content-and-downloads).
92
90
 
93
91
  ## `generate_calendar_url`
94
92
 
@@ -98,20 +96,26 @@ event.generate_calendar_url(provider, method: :publish)
98
96
 
99
97
  | Param | Type | Default | Notes |
100
98
  |-----------|----------|------------|-------|
101
- | `provider`| `Symbol` | required | One of the provider symbols above. |
99
+ | `provider`| `Symbol` | required | One of the provider symbols above (a String also works). Anything else raises `ArgumentError` listing the supported providers. `:ics_content` is accepted as a legacy alias of `:ics`. |
102
100
  | `method` | `Symbol` | `:publish` | `:publish`, `:request`, `:cancel`, `:reply`, `:counter`, or `:decline_counter`. Only honored by `:ics`/`:ical` — ignored by URL-based providers. `:request` requires `organizer` to be set on the event; see below. `:cancel` requires reusing the original `uid` — see [Updating and cancelling invites](#updating-and-cancelling-invites). `:reply`/`:counter`/`:decline_counter` omit `RSVP=TRUE` on `ATTENDEE` lines — see [Attendee RSVP status and METHOD:REPLY](#attendee-rsvp-status-and-methodreply) and [Attendee-proposed reschedules (COUNTER)](#attendee-proposed-reschedules-counter). Note: `:decline_counter` renders as `METHOD:DECLINECOUNTER` (one word, per RFC 5545). |
103
101
 
104
- Results are cached (when caching is configured) keyed on all event attributes, `provider`, and `method` together — changing any of them produces a distinct cache entry.
102
+ Output is generated fresh on every call. CalInvite doesn't cache anything: building the output takes microseconds, and a cached `.ics` would keep a stale `DTSTAMP`. If you want caching anyway, do it in your app, keyed on whatever identifies the content and changes with it (more examples, including HTTP caching, in [CACHING.md](CACHING.md)):
103
+
104
+ ```ruby
105
+ Rails.cache.fetch([meeting, meeting.sequence, :ics]) do
106
+ meeting.to_cal_invite_event.generate_calendar_url(:ics, method: :request)
107
+ end
108
+ ```
105
109
 
106
110
  ## ICS content and downloads
107
111
 
108
112
  Two ways to get raw `.ics` content:
109
113
 
110
114
  ```ruby
111
- # Via Event (goes through validation + caching)
115
+ # Via Event (goes through validation)
112
116
  content = event.generate_calendar_url(:ics, method: :publish)
113
117
 
114
- # Directly via the provider (bypasses Event caching)
118
+ # Directly via the provider (skips Event#validate!)
115
119
  content = CalInvite::Providers::Ics.new(event, method: :publish).generate
116
120
  ```
117
121
 
@@ -331,10 +335,8 @@ A working reference implementation of this whole pattern — a `Meeting` model
331
335
  persisting `uid`/`sequence`, and controller actions that send/reschedule/cancel
332
336
  — lives in [`Example/calendar_app`](Example/calendar_app): see
333
337
  `app/models/meeting.rb` and `app/controllers/meetings_controller.rb`. It
334
- depends on the attendee-hash/`uid`/`sequence` features documented here, so it
335
- needs whatever gem version ships those (0.2.0+) — the example app's `Gemfile`
336
- tracks the published gem, so point it at `path: "../.."` locally if you want
337
- to run this demo before that release is out.
338
+ depends on the attendee-hash/`uid`/`sequence` features documented here, which
339
+ shipped in 0.2.0, so it runs against the published gem.
338
340
 
339
341
  ## Tracking RSVPs: what the gem does and doesn't do
340
342
 
@@ -506,20 +508,17 @@ For any `timezone` other than `'UTC'` that's a recognized IANA/Olson identifier
506
508
  timestamps with no `VTIMEZONE` block, which every calendar client understands
507
509
  unambiguously.
508
510
 
509
- If `timezone` isn't a recognized IANA identifier (e.g. a raw offset string like
510
- `'+01:00'`), both providers fall back to treating `start_time`/`end_time` as
511
- already being in that zone's wall-clock time — the pre-existing behavior — since
512
- there's no timezone database entry to convert against or build a `VTIMEZONE`
513
- from.
511
+ Any other value — including raw offset strings like `'+01:00'` — raises
512
+ `ArgumentError` from `Event.new`/`update_attributes`: there's no timezone
513
+ database entry to convert against or build a `VTIMEZONE` from, and emitting a
514
+ `TZID` with no matching `VTIMEZONE` is invalid RFC 5545. Use the IANA name for
515
+ the place instead (e.g. `'Europe/Paris'`), or `'UTC'`.
514
516
 
515
517
  ## Global configuration (`CalInvite.configure`)
516
518
 
517
519
  ```ruby
518
520
  # config/initializers/cal_invite.rb
519
521
  CalInvite.configure do |config|
520
- config.cache_store = Rails.cache # or :memory_store, :null_store, or a custom read/write/delete object
521
- config.cache_prefix = 'my_app_cal_invite'
522
- config.cache_expires_in = 3600 # seconds
523
522
  config.webhook_secret = ENV['CAL_INVITE_WEBHOOK_SECRET']
524
523
  config.timezone = 'UTC'
525
524
  end
@@ -527,10 +526,7 @@ end
527
526
 
528
527
  | Option | Type | Default | Notes |
529
528
  |---------------------|-----------------------------------------|----------------|-------|
530
- | `cache_store` | `:memory_store`, `:null_store`, or any object implementing `read`/`write`/`delete` | `nil` (caching disabled) | See [CACHING.md](CACHING.md) for full detail. |
531
- | `cache_prefix` | `String` | `'cal_invite'` | Namespaces cache keys. |
532
- | `cache_expires_in` | `Integer` (seconds) | `86400` (24h) | |
533
529
  | `webhook_secret` | `String` | `nil` | Reserved for webhook signature verification in consuming apps. |
534
- | `timezone` | `String` | `'UTC'` | Configuration-level default; per-event `timezone` attribute takes precedence for that event's display formatting. |
530
+ | `timezone` | `String` | `'UTC'` | Default `timezone` for events created without one; an explicit per-event `timezone:` wins. Validated like any event timezone, so an unknown value makes `Event.new` raise `ArgumentError`. |
535
531
 
536
- Caching is disabled unless `cache_store` is set — without it, `generate_calendar_url` regenerates output on every call.
532
+ `cache_store`, `cache_prefix` and `cache_expires_in` are deprecated and have no effect. Setting one prints a warning, and they'll be removed in 0.4.0, so delete them from your initializer.
data/README.md CHANGED
@@ -1,268 +1,210 @@
1
- # 📅 CalInvite
1
+ # CalInvite
2
2
 
3
- A Ruby gem for generating calendar invitations across multiple calendar platforms with caching and webhook support.
3
+ ![CalInvite: calendar invitations for Ruby. Google, Outlook, Apple, Yahoo, .ics](assets/social/cal-invite-github.webp)
4
4
 
5
- [![Gem Version](https://badge.fury.io/rb/cal-invite.svg)](https://badge.fury.io/rb/cal-invite)
6
- ![Build Status](https://github.com/DashAPI-ai/cal-invite/actions/workflows/main.yml/badge.svg)
5
+ Generate "add to calendar" links and `.ics` calendar invites from Ruby, for Google Calendar, Outlook, Outlook 365, Yahoo, Apple Calendar, and any client that reads iCalendar files.
7
6
 
7
+ [![Gem Version](https://img.shields.io/gem/v/cal-invite)](https://rubygems.org/gems/cal-invite)
8
+ ![Build Status](https://github.com/DashAPI-ai/cal-invite/actions/workflows/main.yml/badge.svg)
8
9
  ![License](https://img.shields.io/github/license/DashAPI-ai/cal-invite.svg)
9
10
 
10
- ## Compatibility
11
-
12
- - Ruby >= 3.0.0
13
- - Rails 6.0, 6.1, 7.0, 7.1, 8.0, 8.1
11
+ You describe an event once and get a link or `.ics` content for each provider. With `method: :request`, the `.ics` is an RFC 5545 meeting request that Gmail, Outlook and Apple Mail show with Accept/Decline buttons, and later updates or cancellations replace the original invite. Any IANA timezone gets a full `VTIMEZONE` with correctly converted times. Attribute values are escaped, so end-user input can't add properties to an invite.
14
12
 
15
- ## Supported Calendar Platforms
13
+ ## Contents
16
14
 
17
- Direct Integration:
18
- - Apple iCal (with proper timezone support)
19
- - Microsoft Outlook
20
- - Microsoft Outlook 365
21
- - Google Calendar
22
- - Yahoo Calendar
23
- - Standard .ics file generation
24
-
25
- Any calendar application that supports the iCalendar (.ics) standard should work, including but not limited to:
26
- - Proton Calendar
27
- - FastMail Calendar
28
- - Thunderbird Calendar
29
- - Zoho Calendar
30
- - Microsoft Teams Calendar
31
- - Zoom Calendar Integration
15
+ - [Installation](#installation)
16
+ - [Quick start](#quick-start)
17
+ - [Providers](#providers)
18
+ - [More examples](#more-examples)
19
+ - [Serving a downloadable .ics file](#serving-a-downloadable-ics-file)
20
+ - [Sending email invites with RSVP](#sending-email-invites-with-rsvp)
21
+ - [Things to know](#things-to-know)
22
+ - [Upgrading to 0.3.0](#upgrading-to-030)
23
+ - [Documentation](#documentation)
24
+ - [Development](#development)
25
+ - [Sponsor](#sponsor)
32
26
 
33
27
  ## Installation
34
28
 
35
- Add this line to your application's Gemfile:
29
+ CalInvite is published on [RubyGems](https://rubygems.org/gems/cal-invite). It requires Ruby 3.0 or newer. It works with or without Rails, and supports Rails 6.0 through 8.1.
36
30
 
37
31
  ```ruby
38
- gem 'cal-invite'
32
+ # Gemfile
33
+ gem "cal-invite"
39
34
  ```
40
35
 
41
- And then execute:
42
36
  ```bash
43
- $ bundle install
37
+ bundle install
44
38
  ```
45
39
 
46
- Or install it yourself as:
47
- ```bash
48
- $ gem install cal-invite
40
+ ## Quick start
41
+
42
+ ```ruby
43
+ require "cal_invite"
44
+
45
+ event = CalInvite::Event.new(
46
+ title: "Team Meeting",
47
+ start_time: Time.utc(2026, 11, 2, 15, 0), # always pass UTC
48
+ end_time: Time.utc(2026, 11, 2, 16, 0),
49
+ timezone: "America/New_York", # how the time is displayed
50
+ description: "Weekly sync",
51
+ location: "Conference Room A", # physical place
52
+ url: "https://zoom.us/j/123456789" # virtual meeting link
53
+ )
54
+
55
+ event.generate_calendar_url(:google) # => "https://calendar.google.com/calendar/render?action=TEMPLATE&..."
56
+ event.generate_calendar_url(:ics) # => "BEGIN:VCALENDAR\r\n..." (.ics file content)
49
57
  ```
50
58
 
51
- ## Usage
59
+ ## Providers
52
60
 
53
- ### Basic Event Creation
61
+ | Provider | Returns | Notes |
62
+ |--------------|---------|-------|
63
+ | `:google` | URL | Opens Google Calendar's "add event" page. |
64
+ | `:outlook` | URL | Outlook.com (personal accounts). |
65
+ | `:office365` | URL | Outlook on the web for work/school (Microsoft 365) accounts. |
66
+ | `:yahoo` | URL | One URL per session for multi-day events, separated by newlines. |
67
+ | `:ics` | `.ics` content | Standard iCalendar file; works with Apple Calendar, Thunderbird, Proton, Fastmail and others. |
68
+ | `:ical` | `.ics` content | Same content as `:ics`; kept for Apple Calendar naming. |
54
69
 
55
- Important notes:
56
- - Always provide times in UTC
57
- - Use the timezone parameter to specify the display timezone
58
- - Location and URL are handled separately for better calendar integration
70
+ Only `:ics` and `:ical` support the `method:` option (`:publish` by default, also `:request`, `:cancel`, `:reply`, `:counter`, `:decline_counter`). Any other provider name raises `ArgumentError`.
59
71
 
60
- For the full list of `Event` attributes, providers, and configuration options, see [CONFIGURATION.md](CONFIGURATION.md).
72
+ ## More examples
61
73
 
62
74
  ```ruby
63
- # Create an event with physical location
64
- event = CalInvite::Event.new(
65
- title: "Team Meeting",
66
- start_time: Time.current.utc, # Always use UTC times
67
- end_time: Time.current.utc + 2.hours,
68
- description: "Weekly team sync",
69
- location: "Conference Room A", # Physical location
70
- timezone: "America/New_York", # Display timezone
71
- attendees: ["person@example.com"],
72
- show_attendees: true,
73
- notes: "Please bring your laptop"
74
- )
75
-
76
- # Create an event with both physical and virtual locations
77
- event = CalInvite::Event.new(
78
- title: "Hybrid Meeting",
79
- start_time: Time.current.utc,
80
- end_time: Time.current.utc + 2.hours,
81
- description: "Weekly team sync",
82
- location: "Conference Room A", # Physical location
83
- url: "https://zoom.us/j/123456789", # Virtual meeting URL
84
- timezone: "America/New_York",
85
- attendees: ["person@example.com"],
75
+ # Attendees: email strings, or hashes with a display name and RSVP status
76
+ CalInvite::Event.new(
77
+ title: "Planning",
78
+ start_time: Time.utc(2026, 11, 2, 15), end_time: Time.utc(2026, 11, 2, 16),
79
+ attendees: ["bob@example.com", { email: "ada@example.com", name: "Ada", partstat: :accepted }],
86
80
  show_attendees: true
87
81
  )
88
82
 
89
83
  # All-day event
90
- event = CalInvite::Event.new(
91
- title: "Company All-Day Event",
92
- start_time: Date.today.beginning_of_day.utc,
93
- end_time: Date.today.end_of_day.utc,
94
- all_day: true,
95
- timezone: "America/New_York"
84
+ CalInvite::Event.new(title: "Company Offsite", start_time: Time.utc(2026, 12, 4), all_day: true)
85
+
86
+ # Recurring event, with a reminder 15 minutes before
87
+ CalInvite::Event.new(
88
+ title: "Standup",
89
+ start_time: Time.utc(2026, 11, 2, 14), end_time: Time.utc(2026, 11, 2, 14, 15),
90
+ timezone: "Europe/Paris",
91
+ rrule: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
92
+ reminders: [15]
96
93
  )
97
94
 
98
- # Multi-day event
99
- event = CalInvite::Event.new(
95
+ # Multi-day event with separate sessions
96
+ CalInvite::Event.new(
100
97
  title: "Training Workshop",
101
98
  multi_day_sessions: [
102
- {
103
- start_time: Time.parse("2024-03-01 08:00:00 UTC"),
104
- end_time: Time.parse("2024-03-01 17:00:00 UTC")
105
- },
106
- {
107
- start_time: Time.parse("2024-03-02 09:00:00 UTC"),
108
- end_time: Time.parse("2024-03-02 13:00:00 UTC")
109
- }
99
+ { start_time: Time.utc(2026, 3, 1, 13), end_time: Time.utc(2026, 3, 1, 22) },
100
+ { start_time: Time.utc(2026, 3, 2, 14), end_time: Time.utc(2026, 3, 2, 18) }
110
101
  ],
111
- description: "Advanced Ruby Training",
112
- location: "Training Center",
113
- url: "https://zoom.us/j/123456789", # Virtual meeting URL kept separate
114
102
  timezone: "America/New_York",
115
- notes: "Bring your own laptop"
103
+ location: "Training Center"
116
104
  )
117
-
118
- # Generate calendar URLs
119
- ical_url = event.generate_calendar_url(:ical)
120
- google_url = event.generate_calendar_url(:google)
121
- outlook_url = event.generate_calendar_url(:outlook)
122
- outlook365_url = event.generate_calendar_url(:office365)
123
- yahoo_url = event.generate_calendar_url(:yahoo)
124
105
  ```
125
106
 
126
- ### Implementing ICS Downloads
107
+ For a multi-day event, `:ics`, `:ical` and `:yahoo` produce one entry per session. Google and Outlook can only show one range, so they show the whole span, from the first session's start to the last session's end.
127
108
 
128
- To enable ICS file downloads in your application, you'll need to:
109
+ Every attribute (`organizer`, `geo`, `busy`, `visibility`, `importance`, `calendar_name`, ...) is listed in [CONFIGURATION.md](CONFIGURATION.md#event-attributes).
129
110
 
130
- 1. Create an endpoint that will handle the download request
131
- 2. Generate the ICS content
132
- 3. Send the file to the user
133
-
134
- Here's a basic example of the controller logic:
111
+ ## Serving a downloadable .ics file
135
112
 
136
113
  ```ruby
137
- # In your controller action
138
- def download_calendar
139
- event = # ... your event creation logic ...
140
-
141
- content = CalInvite::Providers::Ics.new(event).generate
142
- filename = "#{event.title.downcase.gsub(/[^0-9A-Za-z.\-]/, '_')}_#{Time.now.strftime('%Y%m%d')}.ics"
143
-
144
- send_data(
145
- content,
146
- filename: filename,
147
- type: 'text/calendar; charset=UTF-8',
148
- disposition: 'attachment'
114
+ # app/controllers/events_controller.rb
115
+ def calendar
116
+ event = CalInvite::Event.new(...)
117
+ download = CalInvite::Providers::IcsDownload.wrap_for_download(
118
+ event.generate_calendar_url(:ics), event.title
149
119
  )
120
+
121
+ send_data download[:content],
122
+ type: download[:headers]["Content-Type"],
123
+ filename: "#{CalInvite::Providers::IcsDownload.sanitize_filename(event.title)}.ics",
124
+ disposition: "attachment"
150
125
  end
151
126
  ```
152
127
 
153
- You can implement this in any controller and route that makes sense for your application's architecture.
128
+ ## Sending email invites with RSVP
154
129
 
155
- ### Sending Email Meeting Invites (RSVP-capable)
130
+ A plain `.ics` attachment opens as a file. To have mail clients show it as a meeting invite with Accept/Decline buttons, you need three things: an `organizer`, `method: :request`, and a matching `method=REQUEST` on the attachment's content type.
156
131
 
157
- By default, a `.ics` attachment opens as a file in most mail clients. To get
158
- Gmail/Outlook/Apple Mail to render the message as an invite with Accept/Decline
159
- actions (like Luma or Google Calendar invites do), set an `organizer` on the
160
- event and pass `method: :request` when generating. Full walkthrough and all
161
- options in [CONFIGURATION.md](CONFIGURATION.md#email-meeting-invites-rsvp-capable).
162
-
163
- ### ICS File Generation
164
-
165
- The gem provides two ways to generate ICS files:
166
-
167
- 1. Direct content generation:
168
132
  ```ruby
169
133
  event = CalInvite::Event.new(
170
- title: "Meeting",
171
- start_time: Time.current.utc,
172
- end_time: Time.current.utc + 1.hour,
173
- timezone: "America/New_York"
134
+ title: "Design Review",
135
+ start_time: Time.utc(2026, 11, 2, 15), end_time: Time.utc(2026, 11, 2, 16),
136
+ organizer: { name: "Jane Doe", email: "jane@example.com" },
137
+ attendees: ["guest@example.com"],
138
+ show_attendees: true
174
139
  )
175
140
 
176
- # Generate ICS content
177
- content = CalInvite::Providers::Ics.new(event).generate
141
+ # In your ActionMailer method:
142
+ attachments["invite.ics"] = {
143
+ mime_type: "text/calendar; method=REQUEST",
144
+ content: event.generate_calendar_url(:ics, method: :request)
145
+ }
178
146
  ```
179
147
 
180
- 2. Rails controller integration:
181
- ```ruby
182
- # In your controller
183
- def download_ics
184
- event = CalInvite::Event.new(
185
- title: "Meeting",
186
- start_time: Time.current.utc,
187
- end_time: Time.current.utc + 1.hour,
188
- timezone: "America/New_York"
189
- )
190
-
191
- content = CalInvite::Providers::Ics.new(event).generate
192
- filename = "#{event.title.downcase.gsub(/[^0-9A-Za-z.\-]/, '_')}_#{Time.now.strftime('%Y%m%d')}.ics"
193
-
194
- send_data(
195
- content,
196
- filename: filename,
197
- type: 'text/calendar; charset=UTF-8',
198
- disposition: 'attachment'
199
- )
200
- end
201
- ```
202
-
203
- ### Important Notes
204
-
205
- 1. Time Handling:
206
- - Always provide times in UTC to the Event constructor
207
- - Use the timezone parameter to specify the display timezone
208
- - All-day events should use beginning_of_day.utc and end_of_day.utc
148
+ Save `event.uid`. To reschedule or cancel later, rebuild the event with the same `uid` and a higher `sequence`, then send `method: :request` again (for a reschedule) or `method: :cancel`. See [Updating and cancelling invites](CONFIGURATION.md#updating-and-cancelling-invites). The [example Rails app](Example/calendar_app) has a working implementation.
209
149
 
210
- 2. Location and URL:
211
- - Physical location goes in the `location` parameter
212
- - Virtual meeting URL goes in the `url` parameter
213
- - They are handled separately for better calendar integration
150
+ ## Things to know
214
151
 
215
- 3. ICS Files:
216
- - Both Apple iCal and standard ICS files now properly handle timezones
217
- - Attendees are properly formatted with RSVP options
218
- - Virtual meeting URLs are properly separated from physical locations
152
+ - Pass `start_time` and `end_time` as UTC `Time`s. `timezone` only controls how the time is displayed. It must be `"UTC"` or an IANA name such as `"America/New_York"`, and defaults to `CalInvite.configuration.timezone` (`"UTC"`).
153
+ - Put the physical place in `location` and the video-call link in `url`. Each provider formats them its own way.
154
+ - Invalid input raises `ArgumentError`: a misspelled attribute (`locaton:`), an unknown timezone, an end time before the start time, or an unsupported provider.
155
+ - CalInvite generates links and invites but doesn't receive RSVP replies. It also can't set options that exist only in the Google and Microsoft APIs, such as guest permissions. See [Tracking RSVPs](CONFIGURATION.md#tracking-rsvps-what-the-gem-does-and-doesnt-do).
156
+ - Output is cheap to build, so nothing is cached and it's generated fresh on every call. To cache it in your app, or answer repeat downloads with `304 Not Modified`, see [CACHING.md](CACHING.md).
219
157
 
220
- ## Caching Support
221
-
222
- CalInvite includes built-in caching support to improve performance when generating calendar URLs. To enable caching in your Rails application:
158
+ Global settings:
223
159
 
224
160
  ```ruby
225
161
  # config/initializers/cal_invite.rb
226
162
  CalInvite.configure do |config|
227
- # Use Rails cache by default
228
- config.cache_store = Rails.cache
229
-
230
- # Optional: Set cache prefix
231
- config.cache_prefix = 'my_app_cal_invite'
232
-
233
- # Optional: Set expiration time (in seconds)
234
- config.cache_expires_in = 3600 # 1 hour
163
+ config.timezone = "America/New_York" # default for events that don't set one
235
164
  end
236
165
  ```
237
166
 
238
- For detailed information about configuring caching in Rails applications and available options, see our [Caching Guide](https://github.com/DashAPI-ai/cal-invite/blob/master/CACHING.md). For every other configuration option (`Event` attributes, providers, email invites), see [CONFIGURATION.md](CONFIGURATION.md).
167
+ ## Upgrading to 0.3.0
239
168
 
240
- ## Development
169
+ 0.3.0 is stricter. Unknown attributes, invalid timezones and invalid time ranges now raise `ArgumentError`, where 0.2.x silently produced a broken invite. The gem also no longer loads all of `active_support/core_ext`. In plain Ruby, `1.hour` and `Time.current` no longer work unless you require them yourself.
241
170
 
242
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
171
+ The `cache_store`/`cache_prefix`/`cache_expires_in` settings and the top-level `::BaseProvider` constant are deprecated. They still work, but print a warning, and they'll be removed in 0.4.0. The full list of changes is in [CHANGELOG.md](CHANGELOG.md).
243
172
 
244
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
173
+ ## Documentation
245
174
 
246
- ## Testing
175
+ - [CONFIGURATION.md](CONFIGURATION.md): every attribute, provider and option, plus guides for email invites, updates and cancellations, RSVP status, COUNTER proposals, and timezones.
176
+ - [API reference](https://dashapi-ai.github.io/cal-invite/): generated from the YARD comments (`bundle exec rake rdoc` builds it locally).
177
+ - [CACHING.md](CACHING.md): caching output in plain Ruby, Rails, Sinatra, Hanami and Grape, including HTTP caching for `.ics` feeds.
178
+ - [CHANGELOG.md](CHANGELOG.md): release history.
247
179
 
248
- Add test(s) as necessary.
180
+ ## Development
249
181
 
250
- Run all the tests before submitting: `bundle exec rake test`
182
+ ```bash
183
+ bin/setup # install dependencies
184
+ bundle exec rake test # full suite (+ coverage report in coverage/)
185
+ bundle exec rake test TEST=test/cal_invite/event_test.rb # one file
186
+ bin/console # IRB with the local lib/ loaded
187
+ ```
251
188
 
252
- ## Contributing
189
+ To try unreleased changes in the [example Rails app](Example/calendar_app), switch its Gemfile to the commented-out `gem "cal-invite", path: "../../"` line and run `bundle install`. Switch it back before committing.
253
190
 
254
- Bug reports and pull requests are welcome on GitHub at https://github.com/DashAPI-ai/cal-invite. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/DashAPI-ai/cal-invite/blob/master/CODE_OF_CONDUCT.md).
191
+ To release, bump `lib/cal_invite/version.rb`, update `CHANGELOG.md`, merge to `main`, and push a `vX.Y.Z` tag. CI then runs the tests, publishes the gem to RubyGems and deploys the docs.
255
192
 
256
- ## Documentation
193
+ ## Contributing
257
194
 
258
- The documentation is spread across the README, [CONFIGURATION.md](CONFIGURATION.md), [CACHING.md](CACHING.md), and the doc folder.
195
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/DashAPI-ai/cal-invite). Please add tests for your change and run `bundle exec rake test` before opening a PR. To report a security issue, see [SECURITY.md](SECURITY.md). Everyone taking part in the project is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
259
196
 
260
- The documentation can be generated using `bundle exec rake rdoc`
197
+ ## Sponsor
261
198
 
262
- ## License
199
+ CalInvite is sponsored by [DashAPI](https://dashapi.ai).
263
200
 
264
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
201
+ <a href="https://dashapi.ai">
202
+ <picture>
203
+ <source media="(prefers-color-scheme: dark)" srcset="assets/sponsors/dashapi-logo-dark.svg">
204
+ <img src="assets/sponsors/dashapi-logo-light.svg" alt="DashAPI" width="140">
205
+ </picture>
206
+ </a>
265
207
 
266
- ## Code of Conduct
208
+ ## License
267
209
 
268
- Everyone interacting in the Cal::Invite project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/DashAPI-ai/cal-invite/blob/master/CODE_OF_CONDUCT.md).
210
+ Released under the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -25,7 +25,8 @@ RDoc::Task.new(:rdoc) do |rdoc|
25
25
  rdoc.options << '--exclude' << '^(test|spec|features)/'
26
26
 
27
27
  # Include files to document
28
- rdoc.rdoc_files.include('README.md', 'LICENSE.txt', 'lib/**/*.rb')
28
+ rdoc.rdoc_files.include('README.md', 'CONFIGURATION.md', 'CACHING.md', 'CHANGELOG.md',
29
+ 'SECURITY.md', 'CODE_OF_CONDUCT.md', 'LICENSE.txt', 'lib/**/*.rb')
29
30
  rdoc.rdoc_files.exclude('lib/cal_invite/version.rb')
30
31
  end
31
32
 
data/SECURITY.md CHANGED
@@ -2,20 +2,49 @@
2
2
 
3
3
  ## Supported Versions
4
4
 
5
- Use this section to tell people about which versions of your project are
6
- currently being supported with security updates.
5
+ Security fixes are released for the latest minor version only. Please upgrade to
6
+ the most recent release before reporting.
7
7
 
8
8
  | Version | Supported |
9
9
  | ------- | ------------------ |
10
- | 5.1.x | :white_check_mark: |
11
- | 5.0.x | :x: |
12
- | 4.0.x | :white_check_mark: |
13
- | < 4.0 | :x: |
10
+ | 0.2.x | :white_check_mark: |
11
+ | < 0.2 | :x: |
14
12
 
15
13
  ## Reporting a Vulnerability
16
14
 
17
- Use this section to tell people how to report a vulnerability.
15
+ **Please do not open a public issue for security problems.**
18
16
 
19
- Tell them where to go, how often they can expect to get an update on a
20
- reported vulnerability, what to expect if the vulnerability is accepted or
21
- declined, etc.
17
+ Report vulnerabilities privately through GitHub:
18
+ [Report a vulnerability](https://github.com/DashAPI-ai/cal-invite/security/advisories/new)
19
+ (Security tab → "Report a vulnerability").
20
+
21
+ Please include:
22
+
23
+ - the affected version(s) and provider(s) (`:ics`, `:google`, ...)
24
+ - a minimal `CalInvite::Event` that reproduces the issue, and the output you got
25
+ - the impact you expect (e.g. injected `.ics` properties, broken URL encoding)
26
+
27
+ What to expect:
28
+
29
+ - an acknowledgement within 5 business days;
30
+ - an assessment, and a fix timeline if the report is accepted, within 14 days;
31
+ - a patched release plus a published GitHub Security Advisory (with credit, if
32
+ you'd like it) once a fix is available. If the report is declined, we'll
33
+ explain why.
34
+
35
+ ## Scope
36
+
37
+ CalInvite turns `CalInvite::Event` attributes into calendar URLs and `.ics`
38
+ content. Attribute values are treated as untrusted:
39
+
40
+ - **URL providers** (`:google`, `:outlook`, `:office365`, `:yahoo`) URL-encode
41
+ every value.
42
+ - **`.ics` providers** (`:ics`, `:ical`, `IcsContent`) escape TEXT values per
43
+ RFC 5545, quote and escape parameter values (e.g. `CN=`) per RFC 6868, and raise
44
+ `ArgumentError` rather than emit a value that would break out of its content
45
+ line.
46
+
47
+ Anything that lets attribute values alter the structure of generated output
48
+ (extra properties, components, parameters, or URL query parameters) is in scope.
49
+ How the host application stores, sends, or serves the generated content (email
50
+ delivery, HTTP headers you set yourself, etc.) is out of scope.