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-datetimesrc/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-textsrc/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
;; Timestampstest/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))