Update gnus-icalendar to use new iCalendar library

This change updates gnus-icalendar.el to use the new iCalendar
library instead of obsolete functions from icalendar.el.
* lisp/gnus/gnus-icalendar.el
(gnus-icalendar-event)
(gnus-icalendar-event:recurring-p)
(gnus-icalendar-event:recurring-interval)
(gnus-icalendar-event:recurring-days)
(gnus-icalendar-event--find-attendee)
(gnus-icalendar-event-from-ical)
(gnus-icalendar-event-from-buffer)
(gnus-icalendar-event--build-reply)
(gnus-icalendar-event-reply-from-buffer)
(gnus-icalendar-event:org-repeat): Reimplement using new iCalendar functions.
(gnus-icalendar-event--attendees-by-type): Rename from
`gnus-icalendar-event--get-attendee-names'.
(gnus-icalendar-event--build-reply): Rename from
`gnus-icalendar-event--build-reply-event-body'.
(gnus-icalendar--format-participant-list): Expect list of
`icalendar-attendee's.  Add docstring.
(Bug#80426)

* test/lisp/gnus/gnus-icalendar-tests.el: Update tests.
This commit is contained in:
Richard Lawrence
2025-12-16 10:39:19 +01:00
committed by Eli Zaretskii
parent f6c1421d1b
commit 5346417d16
2 changed files with 270 additions and 270 deletions

View File

@@ -36,6 +36,10 @@
;;; Code:
(require 'icalendar)
(require 'icalendar-parser)
(eval-when-compile (require 'icalendar-macs))
(require 'icalendar-ast)
(require 'icalendar-utils)
(require 'eieio)
(require 'gmm-utils)
(require 'mm-decode)
@@ -82,8 +86,8 @@
:type (or null t))
(recur :initarg :recur
:accessor gnus-icalendar-event:recur
:initform ""
:type (or null string))
:initform nil
:type (or null list))
(uid :initarg :uid
:accessor gnus-icalendar-event:uid
:type string)
@@ -127,295 +131,212 @@
(cl-defmethod gnus-icalendar-event:recurring-freq ((event gnus-icalendar-event))
"Return recurring frequency of EVENT."
(let ((rrule (gnus-icalendar-event:recur event)))
(string-match "FREQ=\\([[:alpha:]]+\\)" rrule)
(match-string 1 rrule)))
(ical:recur-freq (gnus-icalendar-event:recur event)))
(cl-defmethod gnus-icalendar-event:recurring-interval ((event gnus-icalendar-event))
"Return recurring interval of EVENT."
(let ((rrule (gnus-icalendar-event:recur event))
(default-interval "1"))
(if (string-match "INTERVAL=\\([[:digit:]]+\\)" rrule)
(match-string 1 rrule)
default-interval)))
(ical:recur-interval-size (gnus-icalendar-event:recur event)))
(cl-defmethod gnus-icalendar-event:recurring-days ((event gnus-icalendar-event))
"Return, when available, the week day numbers on which the EVENT recurs."
(let ((rrule (gnus-icalendar-event:recur event))
(weekday-map '(("SU" . 0)
("MO" . 1)
("TU" . 2)
("WE" . 3)
("TH" . 4)
("FR" . 5)
("SA" . 6))))
(when (and rrule (string-match "BYDAY=\\([^;]+\\)" rrule))
(let ((bydays (split-string (match-string 1 rrule) ",")))
(seq-map
(lambda (x) (cdr (assoc x weekday-map)))
(seq-filter (lambda (x) (string-match "^[A-Z]\\{2\\}$" x)) bydays))))))
(let ((rrule (gnus-icalendar-event:recur event)))
(when rrule
(mapcar (lambda (el) (if (consp el) (car el) el))
(ical:recur-by* 'BYDAY rrule)))))
(cl-defmethod gnus-icalendar-event:start ((event gnus-icalendar-event))
(format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:start-time event)))
(defun gnus-icalendar-event--decode-datefield (event field zone-map)
(let* ((dtdate (icalendar--get-event-property event field))
(dtdate-zone (icalendar--find-time-zone
(icalendar--get-event-property-attributes
event field) zone-map))
(dtdate-dec (icalendar--decode-isodatetime dtdate nil dtdate-zone)))
(when dtdate-dec (encode-time dtdate-dec))))
(defun gnus-icalendar-event--find-attendee (attendees ids)
"Return the first `icalendar-attendee' in ATTENDEES matching IDS.
IDS should be a list of strings. The first attendee is returned whose
name (as `icalendar-cnparam') or email address (without \"mailto:\")
is a member of IDS."
(catch 'found
(dolist (attendee attendees)
(ical:with-property attendee ((ical:cnparam :value name))
(let ((email (ical:strip-mailto value)))
(when (or (member name ids)
(member email ids))
(throw 'found attendee)))))))
(defun gnus-icalendar-event--find-attendee (ical name-or-email)
(let* ((event (car (icalendar--all-events ical)))
(event-props (caddr event)))
(cl-labels ((attendee-name (att) (plist-get (cadr att) 'CN))
(attendee-email
(att)
(replace-regexp-in-string "^.*MAILTO:" "" (caddr att)))
(attendee-prop-matches-p
(prop)
(and (eq (car prop) 'ATTENDEE)
(or (member (attendee-name prop) name-or-email)
(let ((att-email (attendee-email prop)))
(gnus-icalendar-find-if
(lambda (str-or-fun)
(if (functionp str-or-fun)
(funcall str-or-fun att-email)
(string-match str-or-fun att-email)))
name-or-email))))))
(gnus-icalendar-find-if #'attendee-prop-matches-p event-props))))
(defun gnus-icalendar-event--attendees-by-type (attendees)
"Return lists of required and optional participants in ATTENDEES.
ATTENDEES must be a list of `icalendar-attendee' nodes. The returned
list has the form (REQUIRED OPTIONAL), where each is a list of
`icalendar-attendee' nodes."
(let (required optional)
(dolist (attendee attendees)
(ical:with-property attendee ((ical:roleparam :value role))
(when (or (null role) ; "REQ-PARTICIPANT" is the default
(equal role "REQ-PARTICIPANT"))
(push attendee required))
(when (equal role "OPT-PARTICIPANT")
(push attendee optional))))
(list (nreverse required)
(nreverse optional))))
(defun gnus-icalendar-event--get-attendee-names (ical)
(let* ((event (car (icalendar--all-events ical)))
(attendee-props (seq-filter
(lambda (p) (eq (car p) 'ATTENDEE))
(caddr event))))
(defun gnus-icalendar-event-from-ical (vcalendar &optional ids)
"Initialize an event instance with the first `icalendar-vevent' in VCALENDAR.
IDS should be a list of strings representing names and email addresses
by which to identify an `icalendar-attendee' in the event as the
recipient."
(ical:with-component vcalendar
((ical:vevent vevent)
(ical:method :value method))
(ical:with-component vevent
((ical:organizer :value organizer)
(ical:attendee :all attendees)
(ical:summary :value summary)
(ical:description :value description)
(ical:dtstart :value dtstart)
(ical:dtend :value dtend)
(ical:location :value location)
(ical:rrule :value rrule)
(ical:uid :value uid))
(cl-labels
((attendee-role (prop)
;; RFC5546: default ROLE is REQ-PARTICIPANT
(and prop
(or (plist-get (cadr prop) 'ROLE)
"REQ-PARTICIPANT")))
(attendee-name
(prop)
(or (plist-get (cadr prop) 'CN)
(replace-regexp-in-string "^.*MAILTO:" "" (caddr prop))))
(attendees-by-type (type)
(seq-filter
(lambda (p) (string= (attendee-role p) type))
attendee-props))
(attendee-names-by-type
(type)
(mapcar #'attendee-name (attendees-by-type type))))
(list
(attendee-names-by-type "REQ-PARTICIPANT")
(attendee-names-by-type "OPT-PARTICIPANT")))))
(defun gnus-icalendar-event-from-ical (ical &optional attendee-name-or-email)
(let* ((event (car (icalendar--all-events ical)))
(organizer (replace-regexp-in-string
"^.*MAILTO:" ""
(or (icalendar--get-event-property event 'ORGANIZER) "")))
(prop-map '((summary . SUMMARY)
(description . DESCRIPTION)
(location . LOCATION)
(recur . RRULE)
(uid . UID)))
(method (caddr (assoc 'METHOD (caddr (car (nreverse ical))))))
(attendee (when attendee-name-or-email
(gnus-icalendar-event--find-attendee
ical attendee-name-or-email)))
(attendee-names (gnus-icalendar-event--get-attendee-names ical))
(let* ((attendee (when ids (gnus-icalendar-event--find-attendee attendees ids)))
(rsvp-p (ical:with-param-of attendee 'ical:rsvpparam))
;; RFC5546: default ROLE is REQ-PARTICIPANT
(role (and attendee
(or (plist-get (cadr attendee) 'ROLE)
"REQ-PARTICIPANT")))
(role (when attendee
(or (ical:with-param-of attendee 'ical:roleparam)
"REQ-PARTICIPANT")))
(participation-type (pcase role
("REQ-PARTICIPANT" 'required)
("OPT-PARTICIPANT" 'optional)
(_ 'non-participant)))
(zone-map (icalendar--convert-all-timezones ical))
(req/opt (gnus-icalendar-event--attendees-by-type attendees))
(args
(list :method method
:organizer organizer
:start-time (gnus-icalendar-event--decode-datefield
event 'DTSTART zone-map)
:end-time (gnus-icalendar-event--decode-datefield
event 'DTEND zone-map)
:rsvp (string= (plist-get (cadr attendee) 'RSVP) "TRUE")
:organizer (when organizer (ical:strip-mailto organizer))
:summary summary
:description description
:location location
:recur rrule
:start-time (encode-time dtstart)
:end-time (encode-time dtend)
:rsvp rsvp-p
:participation-type participation-type
:req-participants (car attendee-names)
:opt-participants (cadr attendee-names)))
(event-class
(cond
((string= method "REQUEST") 'gnus-icalendar-event-request)
((string= method "CANCEL") 'gnus-icalendar-event-cancel)
((string= method "REPLY") 'gnus-icalendar-event-reply)
(t 'gnus-icalendar-event))))
(cl-labels
((map-property
(prop)
(let ((value (icalendar--get-event-property event prop)))
(when value
;; ugly, but cannot get
;;replace-regexp-in-string work with "\\" as
;;REP, plus we should also handle "\\;"
(string-replace
"\\," ","
(string-replace
"\\n" "\n" (substring-no-properties value))))))
(accumulate-args
(mapping)
(cl-destructuring-bind (slot . ical-property) mapping
(setq args (append (list
(intern (concat ":" (symbol-name slot)))
(map-property ical-property))
args)))))
(mapc #'accumulate-args prop-map)
(apply
#'make-instance
event-class
(cl-loop for slot in (eieio-class-slots event-class)
for keyword = (intern
(format ":%s" (eieio-slot-descriptor-name slot)))
when (plist-member args keyword)
append (list keyword
(if (eq keyword :uid)
;; The UID has to be a string.
(or (plist-get args keyword) "")
(plist-get args keyword))))))))
:req-participants (car req/opt)
:opt-participants (cadr req/opt)
:uid (or uid ""))) ; UID must be a string
(event-class (pcase method
("REQUEST" 'gnus-icalendar-event-request)
("CANCEL" 'gnus-icalendar-event-cancel)
("REPLY" 'gnus-icalendar-event-reply)
(_ 'gnus-icalendar-event))))
;; Initialize and return the instance:
(apply
#'make-instance
event-class
(cl-loop for slot in (eieio-class-slots event-class)
for keyword = (intern
(format ":%s" (eieio-slot-descriptor-name slot)))
when (plist-member args keyword)
append (list keyword (plist-get args keyword))))))))
(defun gnus-icalendar-event-from-buffer (buf &optional attendee-name-or-email)
(defun gnus-icalendar-event-from-buffer (buf &optional ids)
"Parse RFC5545 iCalendar in buffer BUF and return an event object.
Return a gnus-icalendar-event object representing the first event
contained in the invitation. Return nil for calendars without an
event entry.
ATTENDEE-NAME-OR-EMAIL is a list of strings that will be matched
against the event's attendee names and emails. Invitation rsvp
status will be retrieved from the first matching attendee record."
(let ((ical (with-current-buffer (icalendar--get-unfolded-buffer (get-buffer buf))
(goto-char (point-min))
(icalendar--read-element nil nil))))
(when ical
(gnus-icalendar-event-from-ical ical attendee-name-or-email))))
IDS is a list of strings that identify the recipient
`icalendar-attendee' by name or email address. Invitation rsvp status
will be retrieved from the first matching attendee record."
(let ((vcalendar (ical:parse buf)))
(when vcalendar
(gnus-icalendar-event-from-ical vcalendar ids))))
;;;
;;; gnus-icalendar-event-reply
;;;
(defun gnus-icalendar-event--build-reply-event-body (ical-request status identities &optional comment)
(defun gnus-icalendar-event--build-reply (vcalendar status ids &optional comment)
"Return an `icalendar-vcalendar' based on VCALENDAR with updated STATUS.
STATUS should one of \\='accepted, \\='declined, or \\='tentative. The
recipient whose participation status is updated to STATUS is identified
in EVENT by finding an `icalendar-attendee' whose name or email address
matches one of the strings in IDS. If no such attendee is found, a new
`icalendar-attendee' is added from the values of `user-mail-address' and
`user-full-name'. COMMENT, if provided, will be added as an
`icalendar-comment' to the returned event."
(let ((summary-status (capitalize (symbol-name status)))
(attendee-status (upcase (symbol-name status)))
reply-event-lines)
(cl-labels
((update-summary
(line)
(if (string-match "^[^:]+:" line)
(replace-match (format "\\&%s: " summary-status) t nil line)
line))
(update-comment
(line)
(if comment (format "COMMENT:%s" comment)
line))
(update-dtstamp ()
(format-time-string "DTSTAMP:%Y%m%dT%H%M%SZ" nil t))
(attendee-matches-identity
(line)
(gnus-icalendar-find-if (lambda (name) (string-match-p name line))
identities))
(update-attendee-status
(line)
(when (and (attendee-matches-identity line)
(string-match "\\(PARTSTAT=\\)[^;]+" line))
(replace-match (format "\\1%s" attendee-status) t nil line)))
(process-event-line
(line)
(when (string-match "^\\([^;:]+\\)" line)
(let* ((key (match-string 0 line))
;; NOTE: not all of the below fields are mandatory,
;; but they are often present in other clients'
;; replies. Can be helpful for debugging, too.
(new-line
(cond
((string= key "ATTENDEE") (update-attendee-status line))
((string= key "SUMMARY") (update-summary line))
((string= key "COMMENT") (update-comment line))
((string= key "DTSTAMP") (update-dtstamp))
((member key '("ORGANIZER" "DTSTART" "DTEND"
"LOCATION" "DURATION" "SEQUENCE"
"RECURRENCE-ID" "UID"))
line)
(t nil))))
(when new-line
(push new-line reply-event-lines))))))
recipient)
(ical:with-component vcalendar
((ical:vtimezone :all tz-nodes)
(ical:vevent :first vevent))
(ical:with-component vevent
((ical:summary :value summary)
(ical:attendee :all attendees)
(ical:uid :value uid)
(ical:comment :value old-comment)
;; The nodes below are copied unchanged to the reply. Not all
;; of them are mandatory, but they are often present in other
;; clients' replies. Can be helpful for debugging, too.
(ical:organizer :first organizer-node)
(ical:dtstart :first dtstart-node)
(ical:dtend :first dtend-node)
(ical:duration :first duration-node)
(ical:location :first location-node)
(ical:sequence :first sequence-node)
(ical:recurrence-id :first recid-node))
(mapc #'process-event-line (split-string ical-request "\n"))
(setq recipient (gnus-icalendar-event--find-attendee attendees ids))
(if recipient
(ical:with-property recipient
((ical:partstatparam :first partstat-node))
(ical:ast-node-set-value partstat-node attendee-status))
;; RFC5546 refers to uninvited attendees as "party crashers".
;; This situation is common if the invitation is sent to a group
;; of people via a mailing list.
(lwarn 'gnus-icalendar :warning
"Could not find a matching event attendee; creating new.")
(setq recipient
(ical:make-property ical:attendee
(concat "mailto:" user-mail-address)
(ical:partstatparam attendee-status)
(ical:cnparam user-full-name)))
(push recipient attendees))
;; RFC5546 refers to uninvited attendees as "party crashers".
;; This situation is common if the invitation is sent to a group
;; of people via a mailing list.
(unless (gnus-icalendar-find-if (lambda (x) (string-match "^ATTENDEE" x))
reply-event-lines)
(lwarn 'gnus-icalendar :warning
"Could not find an event attendee matching given identity")
(push (format "ATTENDEE;RSVP=TRUE;PARTSTAT=%s;CN=%s:MAILTO:%s"
attendee-status user-full-name user-mail-address)
reply-event-lines))
;; Build the reply:
(ical:make-vcalendar
(ical:method "REPLY")
(@ tz-nodes)
(ical:vevent
(ical:uid uid)
recid-node
sequence-node
organizer-node
dtstart-node
dtend-node
duration-node
location-node
(ical:summary
(if (string-match "^[^:]+:" summary)
(replace-match (format "\\&%s: " summary-status) t nil summary)
summary))
(ical:comment (or comment old-comment))
(@ attendees)))))))
;; add comment line if not existing
(when (and comment
(not (gnus-icalendar-find-if
(lambda (x)
(string-match "^COMMENT" x))
reply-event-lines)))
(push (format "COMMENT:%s" comment) reply-event-lines))
(mapconcat #'identity `("BEGIN:VEVENT"
,@(nreverse reply-event-lines)
"END:VEVENT")
"\n"))))
(defun gnus-icalendar-event-reply-from-buffer (buf status identities &optional comment)
(defun gnus-icalendar-event-reply-from-buffer (buf status ids
&optional comment)
"Build a calendar event reply for request contained in BUF.
The reply will have STATUS (`accepted', `tentative' or `declined').
The reply will be composed for attendees matching any entry
on the IDENTITIES list.
Optional argument COMMENT will be placed in the comment field of the
reply.
"
(cl-labels
((extract-block
(blockname)
(save-excursion
(let ((block-start-re (format "^BEGIN:%s" blockname))
(block-end-re (format "^END:%s" blockname))
start)
(when (re-search-forward block-start-re nil t)
(setq start (line-beginning-position))
(re-search-forward block-end-re)
(buffer-substring-no-properties start (line-end-position)))))))
(let (zone event)
(with-current-buffer (icalendar--get-unfolded-buffer (get-buffer buf))
(goto-char (point-min))
(setq zone (extract-block "VTIMEZONE")
event (extract-block "VEVENT")))
(when event
(let ((contents (list "BEGIN:VCALENDAR"
"METHOD:REPLY"
"PRODID:Gnus"
"VERSION:2.0"
zone
(gnus-icalendar-event--build-reply-event-body event status identities comment)
"END:VCALENDAR")))
(mapconcat #'identity (delq nil contents) "\n"))))))
The reply will have STATUS (`accepted', `tentative' or `declined'). The
reply will be composed for attendees matching any entry in the
IDS list. Optional argument COMMENT will be placed in the
comment field of the reply."
(let (vcalendar reply)
(with-current-buffer (ical:unfolded-buffer-from-buffer (get-buffer buf))
(setq vcalendar (ical:parse))
(unless vcalendar
(error "Could not parse invitation; see buffer %s"
(buffer-name (ical:error-buffer))))
(setq reply
(gnus-icalendar-event--build-reply vcalendar status ids comment))
(ical:print-calendar-node reply))))
;;;
;;; gnus-icalendar-org
@@ -455,15 +376,17 @@ reply.
"Return `org-mode' timestamp repeater string for recurring EVENT.
Return nil for non-recurring EVENT."
(when (gnus-icalendar-event:recurring-p event)
(let* ((freq-map '(("HOURLY" . "h")
("DAILY" . "d")
("WEEKLY" . "w")
("MONTHLY" . "m")
("YEARLY" . "y")))
(org-freq (cdr (assoc (gnus-icalendar-event:recurring-freq event) freq-map))))
(let* ((freq-map '((HOURLY . "h")
(DAILY . "d")
(WEEKLY . "w")
(MONTHLY . "m")
(YEARLY . "y")))
(org-freq
(alist-get (gnus-icalendar-event:recurring-freq event) freq-map))
(interval-size (gnus-icalendar-event:recurring-interval event)))
(when org-freq
(format "+%s%s" (gnus-icalendar-event:recurring-interval event) org-freq)))))
(format "+%d%s" interval-size org-freq)))))
(defun gnus-icalendar--find-day (start-date end-date day)
(let ((time-1-day 86400))
@@ -550,7 +473,18 @@ Return nil for non-recurring EVENT."
(defun gnus-icalendar--format-participant-list (participants)
(mapconcat #'identity participants ", "))
"Format PARTICIPANTS as a comma-separated list.
Each `icalendar-attendee' in PARTICIPANTS will be represented like
A. Person <a.person@example.domain>
or simply: <a.person@example.domain>, if no `icalendar-cnparam' is present."
(mapconcat
(lambda (attendee)
(ical:with-property attendee ((ical:cnparam :value cn))
(if cn
(format "%s <%s>" cn value)
(format "<%s>" value))))
participants ", "))
;; TODO: make the template customizable
(cl-defmethod gnus-icalendar-event->org-entry ((event gnus-icalendar-event) reply-status)
@@ -1110,3 +1044,7 @@ means prompt for a comment to include in the reply."
(provide 'gnus-icalendar)
;;; gnus-icalendar.el ends here
;; Local Variables:
;; read-symbol-shorthands: (("ical:" . "icalendar-"))
;; End:

View File

@@ -35,7 +35,7 @@
(let (event)
(with-temp-buffer
(insert ical-string)
(setq event (gnus-icalendar-event-from-buffer (buffer-name) participant)))
(setq event (gnus-icalendar-event-from-buffer (current-buffer) participant)))
event))
(ert-deftest gnus-icalendar-parse ()
@@ -94,7 +94,8 @@ END:VCALENDAR
(setenv "TZ" "CET-1CEST,M3.5.0/2,M10.5.0/3")
(should (eq (eieio-object-class event) 'gnus-icalendar-event-request))
(should (not (gnus-icalendar-event:recurring-p event)))
(should (string= (gnus-icalendar-event:start event) "2020-12-08 15:00"))
(should (equal (gnus-icalendar-event:start event)
"2020-12-08 15:00"))
(with-slots (organizer summary description location end-time uid rsvp participation-type) event
(should (string= organizer "anoncompany.com_3bm6fh805bme9uoeliqcle1sag@group.calendar.google.com"))
(should (string= summary "Townhall | All Company Meeting"))
@@ -106,9 +107,20 @@ END:VCALENDAR
(should (eq participation-type 'non-participant))))
(setenv "TZ" tz))))
(defun gnus-icalendar-at/@ ()
"Replace \" <at> \" with \"@\" before parsing."
(goto-char (point-min))
(while (re-search-forward " <at> " nil t)
(replace-match "@")))
;; FIXME: is "icalendary" (not "icalendar") intentional, here and below?
(ert-deftest gnus-icalendary-byday ()
""
(let ((tz (getenv "TZ"))
(let* ((tz (getenv "TZ"))
(icalendar-pre-parsing-hook
;; clean up " <at> " addresses so the parser doesn't choke...
;; FIXME: can we just change the test data, or is this a real example?
'(gnus-icalendar-at/@))
(event (gnus-icalendar-tests--get-ical-event "\
BEGIN:VCALENDAR
PRODID:Zimbra-Calendar-Provider
@@ -138,8 +150,8 @@ SUMMARY:appointment every weekday\\, start jul 24\\, 2020\\, end aug 24\\, 2020
ATTENDEE;CN=Mark Hershberger;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP
=TRUE:mailto:hexmode <at> gmail.com
ORGANIZER;CN=Mark A. Hershberger:mailto:mah <at> nichework.com
DTSTART;TZID=\"America/New_York\":20200724T090000
DTEND;TZID=\"America/New_York\":20200724T093000
DTSTART;TZID=America/New_York:20200724T090000
DTEND;TZID=America/New_York:20200724T093000
STATUS:CONFIRMED
CLASS:PUBLIC
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
@@ -163,10 +175,12 @@ END:VCALENDAR" (list "Mark Hershberger"))))
(setenv "TZ" "CET-1CEST,M3.5.0/2,M10.5.0/3")
(should (eq (eieio-object-class event) 'gnus-icalendar-event-request))
(should (gnus-icalendar-event:recurring-p event))
(should (string= (gnus-icalendar-event:recurring-interval event) "1"))
(should (= 1 (gnus-icalendar-event:recurring-interval event)))
(should (string= (gnus-icalendar-event:start event) "2020-07-24 15:00"))
(with-slots (organizer summary description location end-time uid rsvp participation-type) event
(should (string= organizer "mah <at> nichework.com"))
(should (string= organizer
(replace-regexp-in-string " <at> " "@"
"mah <at> nichework.com")))
(should (string= summary "appointment every weekday, start jul 24, 2020, end aug 24, 2020"))
(should (string= description "The following is a new meeting request:"))
(should (null location))
@@ -236,7 +250,7 @@ END:VCALENDAR" (list "participant@anoncompany.com"))))
(setenv "TZ" "CET-1CEST,M3.5.0/2,M10.5.0/3")
(should (eq (eieio-object-class event) 'gnus-icalendar-event-request))
(should (gnus-icalendar-event:recurring-p event))
(should (string= (gnus-icalendar-event:recurring-interval event) "1"))
(should (= 1 (gnus-icalendar-event:recurring-interval event)))
(should (string= (gnus-icalendar-event:start event) "2020-09-15 14:00"))
(with-slots (organizer summary description location end-time uid rsvp participation-type) event
(should (string= organizer "anon@anoncompany.com"))
@@ -258,6 +272,29 @@ END:VCALENDAR" (list "participant@anoncompany.com"))))
(ert-deftest gnus-icalendar-accept-with-comment ()
""
(let ((event "\
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20200915T140000
DTEND;TZID=Europe/Berlin:20200915T143000
@@ -275,7 +312,8 @@ SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Casual coffee talk
TRANSP:OPAQUE
END:VEVENT")
END:VEVENT
END:VCALENDAR")
(icalendar-identities '("participant@anoncompany.com")))
(let* ((reply (with-temp-buffer
(insert event)
@@ -292,6 +330,29 @@ END:VEVENT")
(ert-deftest gnus-icalendar-decline-without-changing-comment ()
""
(let ((event "\
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20200915T140000
DTEND;TZID=Europe/Berlin:20200915T143000
@@ -310,7 +371,8 @@ SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Casual coffee talk
TRANSP:OPAQUE
END:VEVENT")
END:VEVENT
END:VCALENDAR")
(icalendar-identities '("participant@anoncompany.com")))
(let* ((reply (with-temp-buffer
(insert event)