Module: Pikuri::Thunderbird::IcsEvent
- Defined in:
- lib/pikuri/thunderbird/ics_event.rb
Overview
Builds the iCalendar (+.ics+) text that CalendarCreate stages for
Thunderbird's import wizard. The text is the injection surface — the same
role MailtoUri plays for compose — because a raw newline in a text field
would let an attacker-authored description close DESCRIPTION and inject
its own ATTENDEE line or a second VEVENT. So every text value is
RFC-5545-escaped (++, ;, ,, newline) and control bytes are stripped
before it joins a property line:
IcsEvent.build(title: 'Q2 review', start: Time.parse('2026-07-20 14:00'))
# BEGIN:VCALENDAR … BEGIN:VEVENT
# SUMMARY:Q2 review
# DTSTART:20260720T120000Z ← host-local 14:00 emitted as UTC
# DTEND:20260720T130000Z ← default +1h
# END:VEVENT END:VCALENDAR
Time & all-day encoding
Timed events emit +DTSTART+/+DTEND+ as UTC (+…Z+): the caller's Time is
taken at face value and converted with #utc, so a bare "2pm" the user
typed is 2pm in the host zone — the same "host zone == user zone"
heuristic Calendar/DateHelpers already read events back with. An
explicit offset in the input (e.g. "15:00 +0900") is honored too —
DateHelpers.parse_time keeps it and #utc maps it to the right instant,
so start and finish may legitimately sit in different zones. All-day events
emit VALUE=DATE with an iCal exclusive DTEND (a single-day event's
DTEND is the next day), so finish is the user-intuitive last day.
Immutable.
Constant Summary collapse
- PRODID =
Returns product id line value; identifies pikuri as the writer.
'-//pikuri//thunderbird//EN'- CONTROL =
C0/C1 control bytes (except the ones escape turns into
\n) — invalid in an ICS value and a structure-injection vector; stripped outright.A
String#deletebyte set, not a regexp: escape strips these from the binary copy of the value, keeping the strip byte-wise (every byte here is ASCII, so it can never bite into a multibyte sequence) without a +/n+-flagged regexp meeting a UTF-8 string. Ruby warns on that pairing ("historical binary regexp match"), and an interpreter warning goes to the raw$stderr, bypassingPikuri.log_ioand painting over a TUI's screen — one accented character in a title was enough. Dropping the/ninstead would have made the strip character-wise and raiseArgumentErroron invalid UTF-8, which CalendarCreate then reports as a bad date. "\x00-\x08\x0b\x0c\x0e-\x1f\x7f"
Class Method Summary collapse
-
.build(title:, start:, finish: nil, all_day: false, location: nil, description: nil, uid: "#{SecureRandom.uuid}@pikuri", dtstamp: Time.now) ⇒ String
Build the
.icsdocument for one event.
Class Method Details
.build(title:, start:, finish: nil, all_day: false, location: nil, description: nil, uid: "#{SecureRandom.uuid}@pikuri", dtstamp: Time.now) ⇒ String
Build the .ics document for one event.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/pikuri/thunderbird/ics_event.rb', line 68 def self.build(title:, start:, finish: nil, all_day: false, location: nil, description: nil, uid: "#{SecureRandom.uuid}@pikuri", dtstamp: Time.now) lines = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', "PRODID:#{PRODID}", 'METHOD:PUBLISH', 'BEGIN:VEVENT', "UID:#{escape(uid)}", "DTSTAMP:#{utc_stamp(dtstamp)}", *date_lines(start, finish, all_day), "SUMMARY:#{escape(title)}" ] lines << "LOCATION:#{escape(location)}" if present?(location) lines << "DESCRIPTION:#{escape(description)}" if present?(description) lines += %w[END:VEVENT END:VCALENDAR] # CRLF line endings + a trailing CRLF, per RFC 5545 §3.1. "#{lines.join("\r\n")}\r\n" end |