From mboxrd@z Thu Jan 1 00:00:00 1970 X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on inbox.vuxu.org X-Spam-Level: X-Spam-Status: No, score=-2.1 required=5.0 tests=DKIM_INVALID,DKIM_SIGNED, RCVD_IN_DNSWL_MED autolearn=ham autolearn_force=no version=3.4.4 Received: (qmail 13382 invoked from network); 14 Jun 2020 18:58:20 -0000 Received: from lists1.math.uh.edu (129.7.128.208) by inbox.vuxu.org with ESMTPUTF8; 14 Jun 2020 18:58:20 -0000 Received: from localhost ([127.0.0.1] helo=lists.math.uh.edu) by lists1.math.uh.edu with smtp (Exim 4.92.3) (envelope-from ) id 1jkXp8-0002th-3L; Sun, 14 Jun 2020 13:57:42 -0500 Received: from mx2.math.uh.edu ([129.7.128.33]) by lists1.math.uh.edu with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92.3) (envelope-from ) id 1jk4Cs-0003LX-3B for ding@lists.math.uh.edu; Sat, 13 Jun 2020 06:20:14 -0500 Received: from quimby.gnus.org ([95.216.78.240]) by mx2.math.uh.edu with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.93) (envelope-from ) id 1jk4Co-00BGV2-Vi for ding@lists.math.uh.edu; Sat, 13 Jun 2020 06:20:13 -0500 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnus.org; s=20200322; h=Content-Type:Mime-Version:Message-ID:Date:Subject:From:To: Sender:Reply-To:Cc:Content-Transfer-Encoding:Content-ID:Content-Description: Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID: In-Reply-To:References:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=G7g/8R2ULMTQ39p/oPGCyzCW1QPT6Tg/UfYdmSghQIg=; b=Z2iJBDoaSd2Yq6WtB/4Cb5JugT prGxczuFzACSymp5Q8CqxtbwGWUzB/rc33JGyHJaXJc+LlayRpqxWpg3Cq1cpfkKaPs+DKwcpp0xD jiKSaQZhSTPC8s9EILvb+Z9NfjvALLaz4RnAuczt9yuU9GaHCrMoG8pTv9hy6ZUqKeno=; Received: from ciao.gmane.io ([159.69.161.202]) by quimby.gnus.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1jk4Ch-0006tb-Ap for ding@gnus.org; Sat, 13 Jun 2020 13:20:06 +0200 Received: from list by ciao.gmane.io with local (Exim 4.92) (envelope-from ) id 1jk4Cg-000IGi-0a for ding@gnus.org; Sat, 13 Jun 2020 13:20:02 +0200 X-Injected-Via-Gmane: http://gmane.org/ To: ding@gnus.org From: Ferdinand Pieper Subject: [RFC] [WIP] Creating icalendar event invitations Date: Sat, 13 Jun 2020 12:57:17 +0200 Message-ID: <87k10bp71u.fsf@ims.uni-hannover.de> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/26.3 (gnu/linux) Cancel-Lock: sha1:2wlYRSX34woE8cRB5sl7Ms6Q+QQ= List-ID: Precedence: bulk --=-=-= Content-Type: text/plain; format=flowed Hi all, gnus has good support to accept/decline icalendar invitations with gnus-icalendar.el. I'm looking to extend this to also allow the creation of new invitations from within gnus. Below is a first rough implementation which I for now kept in a separate file from gnus-icalendar.el, but the goal would be for the code to also live in gnus-icalendar.el. Most of the code is still very rough and the implementation is not handling all cases correctly. I'm mainly sharing it now to ask you whether this is a welcome addition to gnus-icalendar and to ask for thoughts on the general implementation, before I spend more time to polish the code. How it works: The code adds functions to create RFC5545 compliant VCALENDAR and VEVENT parts from gnus-icalendar-event objects and also a function to create a event invitation based on the current message buffer. To create the gnus-icalendar-event the mail addresses in the To header are converted to required participants, addresses in Cc are interpreted as optional participants and the subject and message body are added as summary and description of the event. The date range and location have to be entered interactively for now. Please let me know if you have any other ideas on how users could create new events. Next step will be to polish the code so that the basic functionality of creating simple one-shot event invitations works reliably. Long term goals include handling request responses and supporting a wider range of the RFC5545 spec. Thanks! --=-=-= Content-Type: application/emacs-lisp Content-Disposition: attachment; filename=gnus-icalendar-request.el Content-Transfer-Encoding: quoted-printable (defun gnus-icalendar-event--format-attendee (attendee role) (when (member role '("req" "opt")) (format "ATTENDEE;PARTSTAT=3DNEEDS-ACTION;ROLE=3D%s-PARTICIPANT;RSVP=3D= TRUE:mailto:%s" (upcase role) attendee))) (defun gnus-icalendar-event--create-attendee-list (req opt) (concat (mapconcat (lambda (req) (gnus-icalendar-event--format-attendee req "req= ")) req "\n") (when opt "\n" (mapconcat (lambda (opt) (gnus-icalendar-event--format-attendee opt "o= pt")) opt "\n")))) (defun gnus-icalendar-event--ical-from-event (event) (with-slots (summary description location organizer recur uid start-time = end-time req-participants opt-participants) event (let ((dtstamp (format-time-string "DTSTAMP:%Y%m%dT%H%M%SZ" nil t)) ;; = current UTC time (summary (format "SUMMARY:%s" summary)) (description (when (and (stringp description) (not (string-empty-= p description))) (format "DESCRIPTION:%s" (with-temp-buffer (insert description) (beginning-of-buffer) (while (re-search-forward "\n" nil t) (replace-match "\\n" t t)) (buffer-string))))) ;; TODO: How to do t= his properly? (dtstart (format-time-string "DTSTART:%Y%m%dT%H%M%SZ" start-time = t)) ;; start-time in UTC (dtend (format-time-string "DTEND:%Y%m%dT%H%M%SZ" end-time t)) ;;= end-time in UTC (attendee (gnus-icalendar-event--create-attendee-list req-partici= pants opt-participants)) (location (when (and (stringp location) (not (string-empty-p loca= tion))) (format "LOCATION:%s" location))) (organizer (format "ORGANIZER:mailto:%s" organizer)) (uid (format "UID:%s" uid)) (sequence "SEQUENCE:0") ;; TODO: Consider follow-up event modific= ations. ;; TODO: handle recur ) (with-temp-buffer (insert (mapconcat #'identity (list "BEGIN:VEVENT" dtstamp dtstart dtend summary description attendee location organizer uid sequence "END:VEVENT") "\n")) (flush-lines "^$" (point-min) (point-max)) (buffer-string))))) ;; Vcalendar creation ;; I have not yet found a good way to create vtimezone accurately from ;; scratch. For now hardcoded for CET/CEST and crude general ;; implementation below. (defvar gnus-icalendar-vtimezone-times '(CEST "BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=3DYEARLY;BYDAY=3D-1SU;BYMONTH=3D3 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=3DYEARLY;BYDAY=3D-1SU;BYMONTH=3D10 END:STANDARD" CET "BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=3DYEARLY;BYDAY=3D-1SU;BYMONTH=3D3 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=3DYEARLY;BYDAY=3D-1SU;BYMONTH=3D10 END:STANDARD") "Timezone information about standard and daylight savings time used in VC= ALENDAR parts.") (defun gnus-icalendar--default-vtimezone (&optional zone) "Return default VTIMEZONE information for the current time zone or ZONE i= f provided." (let ((time-zone (current-time-zone nil zone))) (format "BEGIN:STANDARD DTSTART:%s TZOFFSETTO:%s TZOFFSETFROM:+0000 TZNAME:%s END:STANDARD" (format-time-string "%Y%m%dT%H%M%S" 0) ;; set effective timezon= e start date to epoch (format-time-string "%z" (current-time) time-zone) ;; time zone= offset (cadr time-zone) ))) (defun gnus-icalendar--build-vcalendar-from-vevent (event) "Create VCALENDAR part with VEVENT part EVENT." (let* ((time-zone (cadr (current-time-zone))) (vtimezone (mapconcat #'identity `("BEGIN:VTIMEZONE" ,(format "TZID:%s" time-zone) ,(or (plist-get gnus-icalendar-= vtimezone-times (intern time-zone)) (gnus-icalendar--default-v= timezone)) "END:VTIMEZONE") "\n"))) (mapconcat #'identity `("BEGIN:VCALENDAR" "PRODID:Gnus" "VERSION:2.0" "METHOD:REQUEST" ,vtimezone ,event "END:VCALENDAR") "\n"))) (defun gnus-icalendar-event-message-insert-request (event) "Insert text/calendar part into message with request for VEVENT specified in EVENT." (when (provided-mode-derived-p major-mode 'message-mode) (mml-insert-part "text/calendar; method=3D\"REQUEST\"; charset=3DUTF-8") (insert (gnus-icalendar--build-vcalendar-from-vevent (gnus-icalendar-event--ical-from-event event))))) (defun gnus-icalendar-event-from-message-and-insert (&optional date locatio= n) "Create a event request based on the current message. Direct recipients of the message (in To header) are interpreted as required participants. Recipients in Cc are optional participants. The From header is always converted to the event organizer. Message subject is interpreted as summary and message body (if existant) as description. Time and date of the event can be provided as org formatted date range (only with time for now) or will be asked for if nil. Same for location." (interactive) (if (not message-draft-article) ;; internally set by message-mode (message "Not in a message draft") (unless (or date (featurep 'org)) (error "Timestamp creation requires org. Please load org or provide a= org-styled date range")) (message-check-recipients) ;; check for bogus recipients (let* ((date (or date (with-temp-buffer (org-time-stamp nil) (buffer-string)))) (start-time (org-timestamp-to-time (org-timestamp-from-string date) nil)) (end-time (org-timestamp-to-time ;; set end-time if input was a = time-range (org-timestamp-from-string date) t)) (end-time (if (equal end-time start-time) ;; ask for end-time if= previous input was not a range (org-read-date nil t nil "End time:" start-time) end-time)) ;; TODO: better differentiate date-time ranges and date (whole-d= ay) ranges (uid (if (featurep 'org-id) (org-id-uuid) (format "%s@%s" (number-to-string (abs (random))) (md5 (format "%s%s%s%s" (emacs-pid) user-full-name user-mail-address (system-name)))))) (recur nil) ;; TODO (location (or location (read-string "Event location: "))) (description (when (message-goto-body) (buffer-substring (point) (point-max)))) (summary (save-restriction (message-narrow-to-headers) (message-fetch-field "Subject"))) (organizer (caar (mail-header-parse-addresses (save-restriction (message-narrow-to-headers) (message-fetch-field "From"))))) ;; TODO inse= rt common name for "name " addresses (rsvp nil) ;; TODO (participation-type 'non-participant) ;; TODO (req-participants (mapcar #'car (mail-header-parse-addresses (save-restriction (message-narrow-to-headers) (message-fetch-field "To"))))) (opt-participants (mapcar #'car (mail-header-parse-addresses (save-restriction (message-narrow-to-headers) (message-fetch-field "Cc"))))) (event (gnus-icalendar-event-request :uid uid :recur recur :location location :description description :summary summary :method "REQUEST" :organizer organizer :start-time start-time :end-time end-time :rsvp rsvp :participation-type partici= pation-type :req-participants req-parti= cipants :opt-participants opt-parti= cipants))) (message-goto-body) (delete-region (point) (point-max)) (when (not (string-empty-p description)) (mml-insert-multipart "mixed") (mml-insert-part "text/plain") (insert description "\n") (re-search-forward "<#/part>\n")) (gnus-icalendar-event-message-insert-request event)))) --=-=-=--