Commit5e5698e9Recorded21 Mar 2026Repositorysigil-caldav

Add attendee parameter support to iCalendar and CalDAV event creation

Message

- Attendees are now parsed as dicts with email, name (CN), partstat, rsvp, and role fields instead of plain email strings - ical-generate emits ATTENDEE lines with parameters when attendees are dicts, plain strings still supported for backward compat - caldav-event-create! accepts organizer: and attendees: keywords - Export set-ical-event-organizer!, set-ical-event-attendees!, ical-attendee helper, and ical-attendee-email accessor

Changed
 src/sigil/caldav.sgl       |  2 ++
 src/sigil/caldav/event.sgl | 11 ++++++++---
 src/sigil/caldav/ical.sgl  | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 test/test-caldav.sgl       | 21 ++++++++++++++++++++-
 4 files changed, 105 insertions(+), 7 deletions(-)
Diff
src/sigil/caldav.sglmodified
@@ -56,7 +56,9 @@
56
set-ical-event-summary! set-ical-event-description!
57
set-ical-event-dtstart! set-ical-event-dtend!
58
set-ical-event-location! set-ical-event-status!
+59
set-ical-event-organizer! set-ical-event-attendees!
60
set-ical-event-url! set-ical-event-rrule!
+61
ical-attendee ical-attendee-email
62
ical-parse ical-generate
63
generate-event-uid
64
parse-ical-datetime format-ical-datetime
src/sigil/caldav/event.sglmodified
@@ -178,12 +178,15 @@
178
(location #f)
179
(status #f)
180
(all-day? #f)
181
(rrule #f)))
+181
(rrule #f)
+182
(organizer #f)
+183
(attendees '())))
184
(: caldav-client? (calendar-href: string?) (summary: string?)
185
(dtstart: number?) (dtend: (maybe number?))
186
(description: (maybe string?)) (location: (maybe string?))
187
(status: (maybe string?)) (all-day?: boolean?)
186
(rrule: (maybe string?)) -> ical-event?)
+188
(rrule: (maybe string?)) (organizer: (maybe string?))
+189
(attendees: list?) -> ical-event?)
190
(unless calendar-href
191
(error "caldav-event-create!: calendar-href: is required"))
192
(unless dtstart
@@ -199,7 +202,9 @@
202
location: location
203
status: status
204
all-day?: all-day?
202
rrule: rrule))
+205
rrule: rrule
+206
organizer: organizer
+207
attendees: attendees))
208
(ical-text (ical-generate event))
209
(event-url (string-append calendar-href uid ".ics"))
210
(response (caldav-put client event-url ical-text
src/sigil/caldav/ical.sglmodified
@@ -24,8 +24,13 @@
24
set-ical-event-summary! set-ical-event-description!
25
set-ical-event-dtstart! set-ical-event-dtend!
26
set-ical-event-location! set-ical-event-status!
+27
set-ical-event-organizer! set-ical-event-attendees!
28
set-ical-event-url! set-ical-event-rrule!
29
+30
;; Attendee helpers
+31
ical-attendee
+32
ical-attendee-email
+33
34
;; Parsing and generation
35
ical-parse
36
ical-generate
@@ -271,6 +276,55 @@
276
(substring value 7 (string-length value))
277
value))
278
+279
;;; Create an attendee dict for use in ical-event attendees lists.
+280
;;;
+281
;;; ```scheme
+282
;;; (ical-attendee "[email protected]")
+283
;;; (ical-attendee "[email protected]" name: "Bob Smith")
+284
;;; ```
+285
(define (ical-attendee email (keys: (name #f) (partstat "NEEDS-ACTION") (rsvp #t) (role "REQ-PARTICIPANT")))
+286
(: string? (name: (maybe string?)) (partstat: string?) (rsvp: boolean?) (role: string?) -> dict?)
+287
(let ((d (dict email: email partstat: partstat rsvp: rsvp role: role)))
+288
(if name (dict-set d name: name) d)))
+289
+290
;; Get the email from an attendee (string or dict)
+291
(define (ical-attendee-email att)
+292
(if (string? att) att (dict-ref att email:)))
+293
+294
;; Parse iCal parameter string into an alist of key-value pairs.
+295
;; Input: "CN=Bob Smith;PARTSTAT=NEEDS-ACTION;RSVP=TRUE"
+296
(define (parse-ical-params params-str)
+297
(if (or (not params-str) (string=? params-str ""))
+298
'()
+299
(let ((parts (string-split params-str ";")))
+300
(filter-map
+301
(lambda (part)
+302
(let ((eq-pos (string-find part "=")))
+303
(if eq-pos
+304
(cons (string-upcase (substring part 0 eq-pos))
+305
(substring part (+ eq-pos 1) (string-length part)))
+306
#f)))
+307
parts))))
+308
+309
;; Build an attendee dict from parsed iCal params and mailto value
+310
(define (build-attendee params value)
+311
(let* ((email (parse-cal-address value))
+312
(param-alist (parse-ical-params params))
+313
(cn (let ((p (assoc "CN" param-alist)))
+314
(if p (cdr p) #f)))
+315
(partstat (let ((p (assoc "PARTSTAT" param-alist)))
+316
(if p (cdr p) #f)))
+317
(rsvp (let ((p (assoc "RSVP" param-alist)))
+318
(if p (equal? (string-upcase (cdr p)) "TRUE") #f)))
+319
(role (let ((p (assoc "ROLE" param-alist)))
+320
(if p (cdr p) #f)))
+321
(d (dict email: email)))
+322
(let* ((d (if cn (dict-set d name: cn) d))
+323
(d (if partstat (dict-set d partstat: partstat) d))
+324
(d (if rsvp (dict-set d rsvp: rsvp) d))
+325
(d (if role (dict-set d role: role) d)))
+326
d)))
+327
328
;;; Parse an iCalendar string into a list of ical-event structs.
329
;;;
330
;;; Extracts all VEVENT components from the VCALENDAR. Properties
@@ -337,7 +391,7 @@
391
(set-ical-event-organizer! ev (parse-cal-address value)))
392
((equal? name "ATTENDEE")
393
(set-ical-event-attendees! ev
340
(cons (parse-cal-address value)
+394
(cons (build-attendee params value)
395
(ical-event-attendees ev))))
396
((equal? name "CREATED")
397
(set-ical-event-created! ev (parse-ical-datetime value)))
@@ -419,8 +473,26 @@
473
(string-append "mailto:" (ical-event-organizer event)))))
474
(for-each
475
(lambda (attendee)
422
(add! (emit-line "ATTENDEE"
423
(string-append "mailto:" attendee))))
+476
(if (string? attendee)
+477
;; Plain email string (backward compat)
+478
(add! (emit-line "ATTENDEE"
+479
(string-append "mailto:" attendee)))
+480
;; Dict with parameters
+481
(let* ((email (dict-ref attendee email:))
+482
(params '())
+483
(_ (let ((v (dict-ref attendee role: #f)))
+484
(when v (set! params (cons (string-append "ROLE=" v) params)))))
+485
(_ (let ((v (dict-ref attendee rsvp: #f)))
+486
(when v (set! params (cons "RSVP=TRUE" params)))))
+487
(_ (let ((v (dict-ref attendee partstat: #f)))
+488
(when v (set! params (cons (string-append "PARTSTAT=" v) params)))))
+489
(_ (let ((v (dict-ref attendee name: #f)))
+490
(when v (set! params (cons (string-append "CN=" v) params)))))
+491
(prop-name (if (null? params)
+492
"ATTENDEE"
+493
(string-append "ATTENDEE;" (string-join params ";")))))
+494
(add! (emit-line prop-name
+495
(string-append "mailto:" email))))))
496
(ical-event-attendees event))
497
498
;; Timestamps
test/test-caldav.sglmodified
@@ -93,7 +93,9 @@
93
(assert-equal "Conference Room B" (ical-event-location ev))
94
(assert-equal "CONFIRMED" (ical-event-status ev))
95
(assert-equal "[email protected]" (ical-event-organizer ev))
96
(assert-equal '("[email protected]" "[email protected]") (ical-event-attendees ev))
+96
(assert-equal 2 (length (ical-event-attendees ev)))
+97
(assert-equal "[email protected]" (ical-attendee-email (car (ical-event-attendees ev))))
+98
(assert-equal "[email protected]" (ical-attendee-email (cadr (ical-event-attendees ev))))
99
(assert-true (ical-event-created ev))
100
(assert-true (ical-event-last-modified ev))))
101
@@ -170,6 +172,23 @@
172
(assert-true (string-contains? text "ORGANIZER:mailto:[email protected]"))
173
(assert-true (string-contains? text "ATTENDEE:mailto:[email protected]"))))
174
+175
(test "generates attendee with parameters from dict"
+176
(let* ((att (ical-attendee "[email protected]" name: "Bob"))
+177
(ev (ical-event uid: "att-params-1"
+178
summary: "Meeting"
+179
dtstart: (parse-ical-datetime "20240315T090000Z")
+180
organizer: "[email protected]"
+181
attendees: (list att)))
+182
(text (ical-generate ev)))
+183
;; Check params are present (line may be folded at 75 chars)
+184
(assert-true (string-contains? text "ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION"))
+185
(assert-true (string-contains? text "RSVP=TRUE"))
+186
;; Round-trip: verify attendee preserved through parse
+187
(let* ((events (ical-parse text))
+188
(ev2 (car events))
+189
(att2 (car (ical-event-attendees ev2))))
+190
(assert-equal "[email protected]" (ical-attendee-email att2)))))
+191
192
(test "round-trip: parse then generate preserves key fields"
193
(let* ((events (ical-parse simple-ical))
194
(ev (car events))