class CalInvite::Providers::Ics
Generic ICS provider for generating standard iCalendar (.ics) files. This provider generates ICS files that are compatible with most calendar applications. Supports all-day events, regular events, and multi-day sessions with proper timezone handling.
This is the single RFC 5545 generator for the gem: {Ical} and {IcsContent} are subclasses kept for their public names and produce identical output.
@example Creating a regular event ICS file event = CalInvite::Event.new( title: “Team Meeting”, start_time: Time.now, end_time: Time.now + 3600, timezone: ‘America/New_York’ ) ics = CalInvite::Providers::Ics.new(event) ics_content = ics.generate
@example Creating an all-day event ICS file event = CalInvite::Event.new( title: “Company Holiday”, all_day: true, start_time: Date.today, end_time: Date.today + 1 ) ics_content = CalInvite::Providers::Ics.new(event).generate
@example Creating a multi-day event ICS file event = CalInvite::Event.new( title: “Conference”, multi_day_sessions: [ { start_time: Time.parse(“2024-04-01 09:00”), end_time: Time.parse(“2024-04-01 17:00”) }, { start_time: Time.parse(“2024-04-02 09:00”), end_time: Time.parse(“2024-04-02 17:00”) } ] ) ics_content = CalInvite::Providers::Ics.new(event).generate
Constants
- MAX_LINE_OCTETS
-
Maximum content line length in octets, excluding the CRLF (RFC 5545 §3.1).
Public Instance Methods
Source
# File lib/cal_invite/providers/ics.rb 46 def generate 47 calendar_lines = [ 48 "BEGIN:VCALENDAR", 49 "VERSION:2.0", 50 "PRODID:-//CalInvite//EN", 51 "CALSCALE:GREGORIAN", 52 "METHOD:#{method_value}" 53 ] 54 55 calendar_lines << "X-WR-CALNAME:#{escape_text(event.calendar_name)}" if event.calendar_name 56 57 unless event.all_day 58 calendar_lines.concat(vtimezone_lines || []) 59 end 60 61 if event.all_day 62 calendar_lines.concat(generate_all_day_event) 63 elsif event.multi_day_sessions.any? 64 event.multi_day_sessions.each do |session| 65 calendar_lines.concat(generate_vevent(session[:start_time], session[:end_time])) 66 end 67 else 68 calendar_lines.concat(generate_vevent(event.start_time, event.end_time)) 69 end 70 71 calendar_lines << "END:VCALENDAR" 72 ensure_single_line_values!(calendar_lines) 73 calendar_lines.map { |line| fold_line(line) }.join("\r\n") 74 end
Generates the complete ICS calendar content with proper calendar properties. Handles all event types: all-day, regular, and multi-day sessions.
@return [String] The complete ICS calendar content in iCalendar format
Private Instance Methods
Source
# File lib/cal_invite/providers/ics.rb 158 def add_optional_fields(vevent) 159 description_parts = [] 160 description_parts << format_description if format_description 161 if description_parts.any? 162 vevent << "DESCRIPTION:#{escape_text(description_parts.join('\n\n'))}" 163 end 164 165 if location = format_location 166 vevent << "LOCATION:#{escape_text(location)}" 167 end 168 169 if url = format_url 170 # URL is a URI value, not TEXT: RFC 5545 escaping would corrupt it. 171 vevent << "URL:#{url}" 172 end 173 174 vevent << geo_line if geo_line 175 176 if organizer = organizer_line 177 vevent << organizer 178 end 179 180 attendees_list.each { |attendee| vevent << attendee_line(attendee) } 181 182 vevent << rrule_line if rrule_line 183 vevent << sequence_line 184 vevent << status_line 185 vevent << transp_line 186 vevent << busystatus_line 187 vevent << class_line 188 vevent.concat(importance_lines) 189 vevent << disallow_counter_line if disallow_counter_line 190 vevent.concat(valarm_lines) 191 end
Adds optional fields to the event component if they exist. Handles description, location, URL, geo, organizer, attendees, recurrence, visibility/busy status, and reminders.
@param vevent [Array<String>] The current event lines array @return [void]
Source
# File lib/cal_invite/providers/ics.rb 215 def date_time_line(name, time) 216 if CalInvite::IcalTimezone.utc?(event.timezone) 217 "#{name}:#{format_timestamp(time)}" 218 else 219 "#{name};TZID=#{event.timezone}:#{format_local_timestamp(time)}" 220 end 221 end
Builds a DTSTART/DTEND line: a Z-suffixed UTC timestamp for UTC events, otherwise local wall-clock time referencing the event’s VTIMEZONE.
@param name [String] The property name (“DTSTART” or “DTEND”) @param time [Time] The time to format (UTC) @return [String] The content line
# File lib/cal_invite/providers/ics.rb 106 def ensure_single_line_values!(lines) 107 line = lines.find { |l| l.match?(/[\r\n]/) } 108 return unless line 109 110 raise ArgumentError, "#{line[/\A[A-Z-]+/]} value must not contain line breaks" 111 end
Last line of defense against content-line injection: a CR or LF inside a value would end the line and let the rest of the value be parsed as new properties/components. TEXT values are escaped before reaching here, so this only fires for unescaped values (UID, URL, RRULE, emails, …).
@param lines [Array<String>] The content lines about to be joined @raise [ArgumentError] if any line contains a CR or LF @return [void]
Source
# File lib/cal_invite/providers/ics.rb 238 def escape_text(text) 239 return '' if text.nil? 240 241 text.to_s.gsub(/\r\n|[\r\n\\;,]/) { |match| match.match?(/[\r\n]/) ? '\\n' : "\\#{match}" } 242 end
Escapes a TEXT value per RFC 5545 §3.3.11: backslash, semicolon and comma are backslash-escaped, and any line break (CRLF, CR or LF) becomes \n.
@param text [String, nil] The text to escape @return [String] The escaped text, or empty string if input was nil
Source
# File lib/cal_invite/providers/ics.rb 87 def fold_line(line) 88 return line if line.bytesize <= MAX_LINE_OCTETS 89 90 segments = [+""] 91 line.each_char do |char| 92 segments << +" " if segments.last.bytesize + char.bytesize > MAX_LINE_OCTETS 93 segments.last << char 94 end 95 segments.join("\r\n") 96 end
Folds a content line longer than 75 octets into CRLF + single-space continuation lines (RFC 5545 §3.1). Splits only between characters, so a multi-byte UTF-8 character is never cut in half.
@param line [String] A single unfolded content line @return [String] The folded line (unchanged if already short enough)
Source
# File lib/cal_invite/providers/ics.rb 205 def format_date(time) 206 time.strftime("%Y%m%d") 207 end
Formats a time object as a date string in iCalendar format.
@param time [Time] The time to format @return [String] The formatted date (YYYYMMDD)
# File lib/cal_invite/providers/ics.rb 229 def format_local_timestamp(time) 230 local_wall_time(time).strftime("%Y%m%dT%H%M%S") 231 end
Formats a time object as a local timestamp in iCalendar format. Converts from UTC to the event’s timezone; times are expected to be in UTC already.
@param time [Time] The time to format @return [String] The formatted local time (YYYYMMDDTHHmmSS)
Source
# File lib/cal_invite/providers/ics.rb 197 def format_timestamp(time) 198 time.utc.strftime("%Y%m%dT%H%M%SZ") 199 end
Formats a time object as an UTC timestamp in iCalendar format.
@param time [Time] The time to format @return [String] The formatted UTC timestamp (YYYYMMDDTHHmmSSZ)
Source
# File lib/cal_invite/providers/ics.rb 117 def generate_all_day_event 118 start_date = event.start_time || Time.now 119 end_date = event.end_time || (start_date + 86400) 120 vevent = [ 121 "BEGIN:VEVENT", 122 "UID:#{event.uid}", 123 "DTSTAMP:#{format_timestamp(Time.now.utc)}", 124 "DTSTART;VALUE=DATE:#{format_date(start_date)}", 125 "DTEND;VALUE=DATE:#{format_date(end_date)}", 126 "SUMMARY:#{escape_text(event.title)}" 127 ] 128 add_optional_fields(vevent) 129 vevent << "END:VEVENT" 130 vevent 131 end
Generates an all-day event component (VEVENT) with appropriate formatting. Defaults to today (and a one-day span) when start/end times are omitted.
@return [Array<String>] Array of iCalendar format lines for the all-day event
# File lib/cal_invite/providers/ics.rb 138 def generate_vevent(start_time, end_time) 139 vevent = [ 140 "BEGIN:VEVENT", 141 "UID:#{event.uid}", 142 "DTSTAMP:#{format_timestamp(Time.now.utc)}", 143 date_time_line("DTSTART", start_time), 144 date_time_line("DTEND", end_time), 145 "SUMMARY:#{escape_text(event.title)}" 146 ] 147 add_optional_fields(vevent) 148 vevent << "END:VEVENT" 149 vevent 150 end
Generates a regular or multi-day session event component (VEVENT).
@param start_time [Time] The event start time @param end_time [Time] The event end time @return [Array<String>] Array of iCalendar format lines for the event