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.
- checksums.yaml +4 -4
- data/AGENTS.md +13 -8
- data/CACHING.md +190 -98
- data/CHANGELOG.md +61 -2
- data/CONFIGURATION.md +25 -29
- data/README.md +132 -190
- data/Rakefile +2 -1
- data/SECURITY.md +39 -10
- data/lib/cal_invite/configuration.rb +20 -11
- data/lib/cal_invite/event.rb +87 -114
- data/lib/cal_invite/ical_timezone.rb +64 -18
- data/lib/cal_invite/providers/base_provider.rb +302 -264
- data/lib/cal_invite/providers/ical.rb +7 -152
- data/lib/cal_invite/providers/ics.rb +68 -13
- data/lib/cal_invite/providers/ics_content.rb +7 -129
- data/lib/cal_invite/providers.rb +8 -0
- data/lib/cal_invite/version.rb +1 -1
- data/lib/cal_invite.rb +1 -8
- metadata +4 -9
- data/.DS_Store +0 -0
- data/CLAUDE.md +0 -49
- data/lib/.DS_Store +0 -0
- data/lib/cal_invite/.DS_Store +0 -0
- data/lib/cal_invite/caching.rb +0 -74
- data/lib/tasks/rdoc.rake +0 -13
|
@@ -1,269 +1,307 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
[
|
|
3
|
+
module CalInvite
|
|
4
|
+
module Providers
|
|
5
|
+
# Base class for calendar providers that implements common functionality
|
|
6
|
+
# and defines the interface that all providers must implement.
|
|
7
|
+
#
|
|
8
|
+
# @abstract Subclass and override {#generate} to implement a calendar provider
|
|
9
|
+
class BaseProvider
|
|
10
|
+
attr_reader :event, :method
|
|
11
|
+
|
|
12
|
+
# Initialize a new calendar provider
|
|
13
|
+
#
|
|
14
|
+
# @param event [CalInvite::Event] The event to generate a calendar URL for
|
|
15
|
+
# @param method [Symbol] The iCalendar METHOD (:publish or :request). Only
|
|
16
|
+
# meaningful to the ics-family providers; URL-based providers ignore it.
|
|
17
|
+
def initialize(event, method: :publish)
|
|
18
|
+
@event = event
|
|
19
|
+
@method = method
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Generate a calendar URL or content for the event.
|
|
23
|
+
# This method must be implemented by all provider subclasses.
|
|
24
|
+
#
|
|
25
|
+
# @abstract
|
|
26
|
+
# @return [String] The generated calendar URL or content
|
|
27
|
+
# @raise [NotImplementedError] if the provider class doesn't implement this method
|
|
28
|
+
def generate
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #generate"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
protected
|
|
33
|
+
|
|
34
|
+
# URL encode a string for use in calendar URLs
|
|
35
|
+
#
|
|
36
|
+
# @param str [#to_s] The string to encode
|
|
37
|
+
# @return [String] The URL encoded string
|
|
38
|
+
def url_encode(str)
|
|
39
|
+
URI.encode_www_form_component(str.to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Format the event description
|
|
43
|
+
# @return [String, nil] The formatted description or nil if no content
|
|
44
|
+
def format_description
|
|
45
|
+
parts = []
|
|
46
|
+
parts << event.description if event.description
|
|
47
|
+
parts << "Notes: #{event.notes}" if event.notes
|
|
48
|
+
parts.join("\n\n")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get just the physical location
|
|
52
|
+
# @return [String, nil] The location or nil if not present
|
|
53
|
+
def format_location
|
|
54
|
+
event.location
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get the URL for virtual meetings
|
|
58
|
+
# @return [String, nil] The URL or nil if not present
|
|
59
|
+
def format_url
|
|
60
|
+
event.url
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Format description including URL if present
|
|
64
|
+
# @return [String, nil] The formatted description with URL
|
|
65
|
+
def format_description_with_url
|
|
66
|
+
parts = []
|
|
67
|
+
parts << format_description if format_description
|
|
68
|
+
parts << "Virtual Meeting URL: #{format_url}" if format_url
|
|
69
|
+
parts.join("\n\n")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_optional_params(params)
|
|
73
|
+
params[:description] = url_encode(format_description_with_url) if format_description || format_url
|
|
74
|
+
params[:location] = url_encode(format_location) if format_location
|
|
75
|
+
|
|
76
|
+
if event.show_attendees && event.attendees&.any?
|
|
77
|
+
params[:attendees] = attendee_emails.join(',')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
params
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get the list of attendees if showing attendees is enabled
|
|
84
|
+
# @return [Array<String, Hash>] The list of attendees (email strings or
|
|
85
|
+
# { email:, name:, partstat: } hashes) or empty array if disabled/none present
|
|
86
|
+
def attendees_list
|
|
87
|
+
return [] unless event.show_attendees && event.attendees&.any?
|
|
88
|
+
event.attendees
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Plain email addresses for all attendees, regardless of whether they were
|
|
92
|
+
# given as strings or { email:, name:, partstat: } hashes.
|
|
93
|
+
# @return [Array<String>]
|
|
94
|
+
def attendee_emails
|
|
95
|
+
attendees_list.map { |attendee| attendee_email(attendee) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @param attendee [String, Hash] An attendee as given in Event#attendees
|
|
99
|
+
# @return [String] The attendee's email address
|
|
100
|
+
def attendee_email(attendee)
|
|
101
|
+
attendee.is_a?(Hash) ? attendee_hash_value(attendee, :email) : attendee.to_s
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Reads `key` from an attendee hash, trying both symbol and string keys.
|
|
105
|
+
# Unlike `attendee[:key] || attendee["key"]`, this doesn't misread an
|
|
106
|
+
# explicit `false` value (e.g. `rsvp: false`) as "not set".
|
|
107
|
+
#
|
|
108
|
+
# @param attendee [Hash] An attendee hash
|
|
109
|
+
# @param key [Symbol] The key to read
|
|
110
|
+
# @return [Object, nil]
|
|
111
|
+
def attendee_hash_value(attendee, key)
|
|
112
|
+
return nil unless attendee.is_a?(Hash)
|
|
113
|
+
return attendee[key] if attendee.key?(key)
|
|
114
|
+
|
|
115
|
+
attendee[key.to_s]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
NO_RSVP_METHODS = %i[reply counter decline_counter].freeze
|
|
119
|
+
|
|
120
|
+
PARTSTAT_VALUES = {
|
|
121
|
+
accepted: "ACCEPTED",
|
|
122
|
+
declined: "DECLINED",
|
|
123
|
+
tentative: "TENTATIVE",
|
|
124
|
+
needs_action: "NEEDS-ACTION",
|
|
125
|
+
delegated: "DELEGATED"
|
|
126
|
+
}.freeze
|
|
127
|
+
|
|
128
|
+
# Formats a full ATTENDEE property line for iCalendar output.
|
|
129
|
+
#
|
|
130
|
+
# @param attendee [String, Hash] An email string, or a hash like
|
|
131
|
+
# { email:, name:, partstat:, rsvp: } for a display name, specific RSVP
|
|
132
|
+
# status, and/or an explicit RSVP override. `rsvp:` is rarely needed —
|
|
133
|
+
# e.g. a "registration confirmed" invite where the recipient is already
|
|
134
|
+
# `partstat: :accepted` and nothing is actually being requested, so
|
|
135
|
+
# `rsvp: false` suppresses `RSVP=TRUE` even under `method: :request`.
|
|
136
|
+
# @return [String] The formatted ATTENDEE line
|
|
137
|
+
def attendee_line(attendee)
|
|
138
|
+
email = attendee_email(attendee)
|
|
139
|
+
name = attendee_hash_value(attendee, :name)
|
|
140
|
+
partstat_key = attendee_hash_value(attendee, :partstat)
|
|
141
|
+
rsvp_override = attendee_hash_value(attendee, :rsvp)
|
|
142
|
+
|
|
143
|
+
cn = name ? ";CN=#{quoted_param(name)}" : ""
|
|
144
|
+
partstat = PARTSTAT_VALUES[partstat_key&.to_sym] || "NEEDS-ACTION"
|
|
145
|
+
# REPLY/COUNTER/DECLINECOUNTER all flow attendee -> organizer; RSVP=TRUE
|
|
146
|
+
# ("please respond") only makes sense on an organizer -> attendee REQUEST.
|
|
147
|
+
rsvp_default = !NO_RSVP_METHODS.include?(method)
|
|
148
|
+
rsvp = (rsvp_override.nil? ? rsvp_default : rsvp_override) ? ";RSVP=TRUE" : ""
|
|
149
|
+
|
|
150
|
+
"ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=#{partstat}#{rsvp}#{cn}:mailto:#{email}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Format the ORGANIZER property for iCalendar output. Like attendees, the
|
|
154
|
+
# organizer may be a plain email string or an { email:, name: } hash.
|
|
155
|
+
# @return [String, nil] The formatted ORGANIZER line, or nil if no organizer is set
|
|
156
|
+
def organizer_line
|
|
157
|
+
organizer = event.organizer
|
|
158
|
+
return nil unless organizer
|
|
159
|
+
|
|
160
|
+
email = attendee_email(organizer)
|
|
161
|
+
return nil if email.nil? || email.empty?
|
|
162
|
+
|
|
163
|
+
name = attendee_hash_value(organizer, :name)
|
|
164
|
+
cn = name ? ";CN=#{quoted_param(name)}" : ""
|
|
165
|
+
"ORGANIZER#{cn}:mailto:#{email}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The value for the calendar-level METHOD property. RFC 5545 spells this
|
|
169
|
+
# method "DECLINECOUNTER" (one word) despite `:decline_counter` reading more
|
|
170
|
+
# naturally as a Ruby symbol.
|
|
171
|
+
# @return [String] e.g. "REQUEST", "DECLINECOUNTER"
|
|
172
|
+
def method_value
|
|
173
|
+
method == :decline_counter ? "DECLINECOUNTER" : method.to_s.upcase
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The STATUS property, driven by the iCalendar METHOD in use.
|
|
177
|
+
# @return [String] "STATUS:CANCELLED" for :cancel, "STATUS:CONFIRMED" otherwise
|
|
178
|
+
def status_line
|
|
179
|
+
method == :cancel ? "STATUS:CANCELLED" : "STATUS:CONFIRMED"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Converts a time to local wall-clock time for the event's timezone, for use in
|
|
183
|
+
# a `DTSTART;TZID=...`/`DTEND;TZID=...` property. Falls back to the time as given
|
|
184
|
+
# (unconverted) when the timezone isn't a TZInfo-recognized identifier (e.g. 'UTC'
|
|
185
|
+
# or a raw offset string), matching each provider's prior behavior for those cases.
|
|
186
|
+
#
|
|
187
|
+
# @param time [Time] The time to convert (interpreted as UTC)
|
|
188
|
+
# @return [Time] The local wall-clock time
|
|
189
|
+
def local_wall_time(time)
|
|
190
|
+
CalInvite::IcalTimezone.local_time(event.timezone, time) || time
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Builds the VTIMEZONE component lines for the event's timezone, if applicable.
|
|
194
|
+
# @return [Array<String>, nil] iCalendar lines, or nil for UTC/unrecognized timezones
|
|
195
|
+
def vtimezone_lines
|
|
196
|
+
CalInvite::IcalTimezone.vtimezone_lines(event.timezone)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Format the GEO property from Event#geo.
|
|
200
|
+
# @return [String, nil] The formatted GEO line, or nil if no geo is set
|
|
201
|
+
# @raise [ArgumentError] if lat/lng aren't numeric
|
|
202
|
+
def geo_line
|
|
203
|
+
return nil unless event.geo
|
|
204
|
+
|
|
205
|
+
lat, lng = event.geo.is_a?(Hash) ? [event.geo[:lat] || event.geo["lat"], event.geo[:lng] || event.geo["lng"]] : event.geo
|
|
206
|
+
return nil unless lat && lng
|
|
207
|
+
|
|
208
|
+
"GEO:#{Float(lat)};#{Float(lng)}"
|
|
209
|
+
rescue TypeError, ArgumentError
|
|
210
|
+
raise ArgumentError, "geo lat/lng must be numeric (got #{event.geo.inspect})"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The SEQUENCE property, from Event#sequence.
|
|
214
|
+
# @return [String] e.g. "SEQUENCE:0"
|
|
215
|
+
# @raise [ArgumentError] if sequence isn't an integer
|
|
216
|
+
def sequence_line
|
|
217
|
+
"SEQUENCE:#{Integer(event.sequence)}"
|
|
218
|
+
rescue TypeError, ArgumentError
|
|
219
|
+
raise ArgumentError, "sequence must be an integer (got #{event.sequence.inspect})"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The TRANSP property, from Event#busy (default true).
|
|
223
|
+
# @return [String] "TRANSP:OPAQUE" (busy) or "TRANSP:TRANSPARENT" (free)
|
|
224
|
+
def transp_line
|
|
225
|
+
event.busy == false ? "TRANSP:TRANSPARENT" : "TRANSP:OPAQUE"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
VISIBILITY_VALUES = %w[PUBLIC PRIVATE CONFIDENTIAL].freeze
|
|
229
|
+
|
|
230
|
+
# The CLASS property, from Event#visibility (default :public).
|
|
231
|
+
# @return [String] e.g. "CLASS:PUBLIC"
|
|
232
|
+
# @raise [ArgumentError] if visibility isn't :public, :private, or :confidential
|
|
233
|
+
def class_line
|
|
234
|
+
value = (event.visibility || :public).to_s.upcase
|
|
235
|
+
unless VISIBILITY_VALUES.include?(value)
|
|
236
|
+
raise ArgumentError, "visibility must be one of :public, :private, :confidential (got #{event.visibility.inspect})"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
"CLASS:#{value}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Quotes a parameter value (e.g. CN) and escapes it per RFC 6868, so a display
|
|
243
|
+
# name can't close the quoted string and inject parameters, or break the line.
|
|
244
|
+
#
|
|
245
|
+
# @param value [#to_s] The raw parameter value
|
|
246
|
+
# @return [String] The quoted, escaped value
|
|
247
|
+
def quoted_param(value)
|
|
248
|
+
escaped = value.to_s.gsub(/\^|"|\r\n|[\r\n]/) { |match| { '^' => '^^', '"' => "^'" }.fetch(match, '^n') }
|
|
249
|
+
%("#{escaped}")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# The RRULE property, from Event#rrule, if set.
|
|
253
|
+
# @return [String, nil] e.g. "RRULE:FREQ=WEEKLY;COUNT=5", or nil if no rrule is set
|
|
254
|
+
def rrule_line
|
|
255
|
+
return nil unless event.rrule
|
|
256
|
+
|
|
257
|
+
value = event.rrule.to_s
|
|
258
|
+
value.start_with?("RRULE:") ? value : "RRULE:#{value}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Builds VALARM sub-components from Event#reminders (minutes-before-start values).
|
|
262
|
+
# @return [Array<String>] iCalendar lines, one VALARM block per reminder, or [] if none
|
|
263
|
+
def valarm_lines
|
|
264
|
+
return [] unless event.reminders&.any?
|
|
265
|
+
|
|
266
|
+
event.reminders.flat_map do |minutes|
|
|
267
|
+
["BEGIN:VALARM", "TRIGGER:-PT#{minutes.to_i}M", "ACTION:DISPLAY", "DESCRIPTION:Reminder", "END:VALARM"]
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
IMPORTANCE_VALUES = { low: ["9", "0"], normal: ["5", "1"], high: ["1", "2"] }.freeze
|
|
272
|
+
|
|
273
|
+
# Standard RFC 5545 PRIORITY plus Outlook's non-standard
|
|
274
|
+
# X-MICROSOFT-CDO-IMPORTANCE, from Event#importance (:low/:normal/:high). Safe
|
|
275
|
+
# to always include: RFC 5545 requires unrecognized X- properties be ignored
|
|
276
|
+
# by compliant parsers, so this has no effect outside Outlook.
|
|
277
|
+
# @return [Array<String>] 0, 1, or 2 lines
|
|
278
|
+
def importance_lines
|
|
279
|
+
values = IMPORTANCE_VALUES[event.importance&.to_sym]
|
|
280
|
+
return [] unless values
|
|
281
|
+
|
|
282
|
+
priority, importance = values
|
|
283
|
+
["PRIORITY:#{priority}", "X-MICROSOFT-CDO-IMPORTANCE:#{importance}"]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Outlook's non-standard X-MICROSOFT-CDO-BUSYSTATUS, mirroring Event#busy.
|
|
287
|
+
# Included alongside the standard TRANSP property since some Outlook versions
|
|
288
|
+
# honor this one more reliably.
|
|
289
|
+
# @return [String] e.g. "X-MICROSOFT-CDO-BUSYSTATUS:BUSY"
|
|
290
|
+
def busystatus_line
|
|
291
|
+
"X-MICROSOFT-CDO-BUSYSTATUS:#{event.busy == false ? "FREE" : "BUSY"}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Outlook's non-standard X-MICROSOFT-DISALLOW-COUNTER, from Event#allow_counter
|
|
295
|
+
# (default true). Set Event#allow_counter = false to hide Outlook's "Propose
|
|
296
|
+
# New Time" action — the practical lever for "prevent attendee-initiated
|
|
297
|
+
# reschedules" on Outlook specifically; other clients don't expose an
|
|
298
|
+
# equivalent control and ignore this property.
|
|
299
|
+
# @return [String, nil] "X-MICROSOFT-DISALLOW-COUNTER:TRUE", or nil if counters are allowed
|
|
300
|
+
def disallow_counter_line
|
|
301
|
+
return nil if event.allow_counter != false
|
|
302
|
+
|
|
303
|
+
"X-MICROSOFT-DISALLOW-COUNTER:TRUE"
|
|
304
|
+
end
|
|
232
305
|
end
|
|
233
306
|
end
|
|
234
|
-
|
|
235
|
-
IMPORTANCE_VALUES = { low: ["9", "0"], normal: ["5", "1"], high: ["1", "2"] }.freeze
|
|
236
|
-
|
|
237
|
-
# Standard RFC 5545 PRIORITY plus Outlook's non-standard
|
|
238
|
-
# X-MICROSOFT-CDO-IMPORTANCE, from Event#importance (:low/:normal/:high). Safe
|
|
239
|
-
# to always include: RFC 5545 requires unrecognized X- properties be ignored
|
|
240
|
-
# by compliant parsers, so this has no effect outside Outlook.
|
|
241
|
-
# @return [Array<String>] 0, 1, or 2 lines
|
|
242
|
-
def importance_lines
|
|
243
|
-
values = IMPORTANCE_VALUES[event.importance&.to_sym]
|
|
244
|
-
return [] unless values
|
|
245
|
-
|
|
246
|
-
priority, importance = values
|
|
247
|
-
["PRIORITY:#{priority}", "X-MICROSOFT-CDO-IMPORTANCE:#{importance}"]
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# Outlook's non-standard X-MICROSOFT-CDO-BUSYSTATUS, mirroring Event#busy.
|
|
251
|
-
# Included alongside the standard TRANSP property since some Outlook versions
|
|
252
|
-
# honor this one more reliably.
|
|
253
|
-
# @return [String] e.g. "X-MICROSOFT-CDO-BUSYSTATUS:BUSY"
|
|
254
|
-
def busystatus_line
|
|
255
|
-
"X-MICROSOFT-CDO-BUSYSTATUS:#{event.busy == false ? "FREE" : "BUSY"}"
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# Outlook's non-standard X-MICROSOFT-DISALLOW-COUNTER, from Event#allow_counter
|
|
259
|
-
# (default true). Set Event#allow_counter = false to hide Outlook's "Propose
|
|
260
|
-
# New Time" action — the practical lever for "prevent attendee-initiated
|
|
261
|
-
# reschedules" on Outlook specifically; other clients don't expose an
|
|
262
|
-
# equivalent control and ignore this property.
|
|
263
|
-
# @return [String, nil] "X-MICROSOFT-DISALLOW-COUNTER:TRUE", or nil if counters are allowed
|
|
264
|
-
def disallow_counter_line
|
|
265
|
-
return nil if event.allow_counter != false
|
|
266
|
-
|
|
267
|
-
"X-MICROSOFT-DISALLOW-COUNTER:TRUE"
|
|
268
|
-
end
|
|
269
307
|
end
|