about summary refs log tree commit diff
path: root/varuga.el
diff options
context:
space:
mode:
authorArun Isaac2024-06-16 18:36:53 +0100
committerArun Isaac2024-06-16 18:38:12 +0100
commit9b252d90af1a58c368d2484a39d87ebf3d25e6d6 (patch)
tree8af3d203e8386740dc11a7bb25447bfb6f4abe6c /varuga.el
downloadvaruga-9b252d90af1a58c368d2484a39d87ebf3d25e6d6.tar.gz
varuga-9b252d90af1a58c368d2484a39d87ebf3d25e6d6.tar.lz
varuga-9b252d90af1a58c368d2484a39d87ebf3d25e6d6.zip
Initial commit
Diffstat (limited to 'varuga.el')
-rw-r--r--varuga.el181
1 files changed, 181 insertions, 0 deletions
diff --git a/varuga.el b/varuga.el
new file mode 100644
index 0000000..d17e1aa
--- /dev/null
+++ b/varuga.el
@@ -0,0 +1,181 @@
+;;; varuga.el --- Send ical calendar invites by email -*- lexical-binding: t -*-
+
+;; Send ical calendar invites by email
+;; Copyright © 2024 by Arun I
+;;
+;; Author: Arun Isaac <arunisaac@systemreboot.net>
+;; Version: 0.1.0
+;; Homepage: https://git.systemreboot.net/varuga
+;; Package-Requires: ((emacs "26.1"))
+
+;; This file is part of varuga.
+
+;; varuga is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; varuga is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with varuga.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Send ical calendar invites using your Emacs mail client.  These
+;; invites are similar to those produced by Google Calendar, Outlook
+;; Calendar, etc. and are compatible with them.
+;;
+;; varuga populates a message mode buffer with an ical MIME part
+;; (using MML, the MIME Meta Language).  It also adds a plain text
+;; part listing the time of the event in various configured timezones.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'message)
+(require 'org)
+(require 'org-duration)
+(require 'org-id)
+(require 'time)
+
+(cl-defstruct (varuga-calendar (:constructor varuga-calendar)
+                               (:copier nil))
+  components)
+
+(cl-defstruct (varuga-calendar-event (:constructor varuga-calendar-event)
+                                     (:copier nil))
+  organizer
+  organizer-email-address
+  time-start
+  time-end
+  summary
+  location)
+
+(defvar varuga-product-identifier
+  "-//systemreboot//varuga//NONSGML v1.0//EN")
+
+(defvar varuga-clock-list
+  (if (listp world-clock-list)
+      world-clock-list
+    zoneinfo-style-world-list))
+
+(defvar varuga-time-format
+  world-clock-time-format)
+
+(defun varuga-insert-calendar-line (key value)
+  "Insert ical calendar line.
+KEY is the name of the ical property and VALUE is its value."
+  ;; Limit content line length to 75 octets as required by RFC 5545.
+  (let ((maximum-octets-per-line 75)
+        (octets-so-far 0))
+    (seq-do (lambda (c)
+              (let* (;; Escape the linefeed character.
+                     (str (if (eql c ?\n)
+                              "\\N"
+                            (string c)))
+                     (next-octets (+ octets-so-far
+                                     (string-bytes str))))
+                (if (< next-octets maximum-octets-per-line)
+                    (progn
+                      (insert str)
+                      (setq octets-so-far next-octets))
+                  (progn
+                    (insert "\r\n ")
+                    ;; Set octets so far to 1 to account for the
+                    ;; folding space.
+                    (setq octets-so-far 1)))))
+            (format "%s:%s"
+                    (upcase (symbol-name key))
+                    value)))
+  (insert "\r\n"))
+
+(defun varuga-format-time-string (time)
+  "Format TIME to ical specification."
+  (format-time-string "%Y%m%dT%H%M%SZ" time t))
+
+(defun varuga-insert-calendar-event (event)
+  "Insert ical calendar EVENT at point."
+  (varuga-insert-calendar-line 'begin "VEVENT")
+  (varuga-insert-calendar-line 'uid (org-id-uuid))
+  (varuga-insert-calendar-line 'organizer
+                               (format "CN=%s:MAILTO:%s"
+                                       (varuga-calendar-event-organizer event)
+                                       (varuga-calendar-event-organizer-email-address event)))
+  (varuga-insert-calendar-line 'dtstart
+                               (varuga-format-time-string
+                                (varuga-calendar-event-time-start event)))
+  (varuga-insert-calendar-line 'dtend
+                               (varuga-format-time-string
+                                (varuga-calendar-event-time-end event)))
+  (varuga-insert-calendar-line 'summary
+                               (varuga-calendar-event-summary event))
+  (varuga-insert-calendar-line 'location
+                               (varuga-calendar-event-location event))
+  (varuga-insert-calendar-line 'end "VEVENT"))
+
+(defun varuga-insert-calendar (calendar)
+  "Insert ical CALENDAR at point."
+  (varuga-insert-calendar-line 'begin "VCALENDAR")
+  (varuga-insert-calendar-line 'version "2.0")
+  (varuga-insert-calendar-line 'prodid varuga-product-identifier)
+  (seq-do 'varuga-insert-calendar-event
+          (varuga-calendar-components calendar))
+  (varuga-insert-calendar-line 'end "VCALENDAR"))
+
+(defun varuga-invite (summary location when duration)
+  "Insert calendar invitation into current email message buffer.
+SUMMARY is a short description of the event.  LOCATION is the
+location of the event (typically a URI for online meetings).
+WHEN is the encoded time when the event is scheduled.  DURATION
+is the length of the event in minutes."
+  (interactive (list (read-string "Event Summary: ")
+                     (read-string "Location: ")
+                     (org-read-date t t nil "When?")
+                     (org-duration-to-minutes
+                      (read-string "Duration: "))))
+  (save-excursion
+    ;; Fill Subject header.
+    (save-restriction
+      (message-narrow-to-headers)
+      (re-search-forward "^Subject:")
+      (message-narrow-to-field)
+      (end-of-line)
+      ;; TODO: Allow customization of the Subject format.
+      (insert (format "Invitation: %s" summary)))
+    ;; Fill email body.
+    (goto-char (point-max))
+    (insert "<#multipart type=mixed>\n")
+    ;; Add human-readable time in configured timezones. TODO: Allow
+    ;; customization of this format.
+    (insert "<#part type=text/plain>\n\n")
+    (seq-do (pcase-lambda (`(,zone ,place))
+              (insert place)
+              (insert " — ")
+              (insert (let ((system-time-locale "C"))
+                        (format-time-string varuga-time-format when zone)))
+              (insert "\n"))
+            varuga-clock-list)
+    ;; Insert ical part.
+    (insert "<#part type=text/calendar>\n")
+    (varuga-insert-calendar
+     (varuga-calendar
+      :components (list (pcase (mail-extract-address-components
+                                (message-fetch-field "From"))
+                          (`(,organizer ,organizer-email-address)
+                           (varuga-calendar-event
+                            :organizer organizer
+                            :organizer-email-address organizer-email-address
+                            :time-start when
+                            :time-end (time-add when
+                                                (* 60 (org-duration-to-minutes duration)))
+                            :summary summary
+                            :location location))))))
+    (insert "<#/multipart>\n")))
+
+(provide 'varuga)
+
+;;; varuga.el ends here