1 ;;; gnus-icalendar.el --- reply to iCalendar meeting requests
3 ;; Copyright (C) 2013-2015 Free Software Foundation, Inc.
5 ;; Author: Jan Tatarik <Jan.Tatarik@gmail.com>
6 ;; Keywords: mail, icalendar, org
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
24 ;; (require 'gnus-icalendar)
25 ;; (gnus-icalendar-setup)
27 ;; to enable optional iCalendar->Org sync functionality
28 ;; NOTE: both the capture file and the headline(s) inside must already exist
29 ;; (setq gnus-icalendar-org-capture-file "~/org/notes.org")
30 ;; (setq gnus-icalendar-org-capture-headline '("Calendar"))
31 ;; (gnus-icalendar-org-setup)
43 (eval-when-compile (require 'cl))
45 (defun gnus-icalendar-find-if (pred seq)
48 (when (funcall pred (car seq))
49 (throw 'found (car seq)))
56 (defclass gnus-icalendar-event ()
57 ((organizer :initarg :organizer
58 :accessor gnus-icalendar-event:organizer
60 :type (or null string))
61 (summary :initarg :summary
62 :accessor gnus-icalendar-event:summary
64 :type (or null string))
65 (description :initarg :description
66 :accessor gnus-icalendar-event:description
68 :type (or null string))
69 (location :initarg :location
70 :accessor gnus-icalendar-event:location
72 :type (or null string))
73 (start-time :initarg :start-time
74 :accessor gnus-icalendar-event:start-time
77 (end-time :initarg :end-time
78 :accessor gnus-icalendar-event:end-time
81 (recur :initarg :recur
82 :accessor gnus-icalendar-event:recur
84 :type (or null string))
86 :accessor gnus-icalendar-event:uid
88 (method :initarg :method
89 :accessor gnus-icalendar-event:method
91 :type (or null string))
93 :accessor gnus-icalendar-event:rsvp
95 :type (or null boolean))
96 (participation-type :initarg :participation-type
97 :accessor gnus-icalendar-event:participation-type
98 :initform 'non-participant
100 (req-participants :initarg :req-participants
101 :accessor gnus-icalendar-event:req-participants
104 (opt-participants :initarg :opt-participants
105 :accessor gnus-icalendar-event:opt-participants
108 "generic iCalendar Event class")
110 (defclass gnus-icalendar-event-request (gnus-icalendar-event)
112 "iCalendar class for REQUEST events")
114 (defclass gnus-icalendar-event-cancel (gnus-icalendar-event)
116 "iCalendar class for CANCEL events")
118 (defclass gnus-icalendar-event-reply (gnus-icalendar-event)
120 "iCalendar class for REPLY events")
122 (defmethod gnus-icalendar-event:recurring-p ((event gnus-icalendar-event))
123 "Return t if EVENT is recurring."
124 (not (null (gnus-icalendar-event:recur event))))
126 (defmethod gnus-icalendar-event:recurring-freq ((event gnus-icalendar-event))
127 "Return recurring frequency of EVENT."
128 (let ((rrule (gnus-icalendar-event:recur event)))
129 (string-match "FREQ=\\([[:alpha:]]+\\)" rrule)
130 (match-string 1 rrule)))
132 (defmethod gnus-icalendar-event:recurring-interval ((event gnus-icalendar-event))
133 "Return recurring interval of EVENT."
134 (let ((rrule (gnus-icalendar-event:recur event))
135 (default-interval 1))
137 (string-match "INTERVAL=\\([[:digit:]]+\\)" rrule)
138 (or (match-string 1 rrule)
141 (defmethod gnus-icalendar-event:start ((event gnus-icalendar-event))
142 (format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:start-time event)))
144 (defun gnus-icalendar-event--decode-datefield (event field zone-map)
145 (let* ((dtdate (icalendar--get-event-property event field))
146 (dtdate-zone (icalendar--find-time-zone
147 (icalendar--get-event-property-attributes
148 event field) zone-map))
149 (dtdate-dec (icalendar--decode-isodatetime dtdate nil dtdate-zone)))
150 (apply 'encode-time dtdate-dec)))
152 (defun gnus-icalendar-event--find-attendee (ical name-or-email)
153 (let* ((event (car (icalendar--all-events ical)))
154 (event-props (caddr event)))
155 (gmm-labels ((attendee-name (att) (plist-get (cadr att) 'CN))
156 (attendee-email (att)
157 (replace-regexp-in-string "^.*MAILTO:" "" (caddr att)))
158 (attendee-prop-matches-p (prop)
159 (and (eq (car prop) 'ATTENDEE)
160 (or (member (attendee-name prop) name-or-email)
161 (let ((att-email (attendee-email prop)))
162 (gnus-icalendar-find-if (lambda (email)
163 (string-match email att-email))
166 (gnus-icalendar-find-if #'attendee-prop-matches-p event-props))))
168 (defun gnus-icalendar-event--get-attendee-names (ical)
169 (let* ((event (car (icalendar--all-events ical)))
170 (attendee-props (gnus-remove-if-not
171 (lambda (p) (eq (car p) 'ATTENDEE))
174 (gmm-labels ((attendee-role (prop) (plist-get (cadr prop) 'ROLE))
175 (attendee-name (prop)
176 (or (plist-get (cadr prop) 'CN)
177 (replace-regexp-in-string "^.*MAILTO:" "" (caddr prop))))
178 (attendees-by-type (type)
180 (lambda (p) (string= (attendee-role p) type))
182 (attendee-names-by-type (type)
183 (mapcar #'attendee-name (attendees-by-type type))))
186 (attendee-names-by-type "REQ-PARTICIPANT")
187 (attendee-names-by-type "OPT-PARTICIPANT")))))
189 (defun gnus-icalendar-event-from-ical (ical &optional attendee-name-or-email)
190 (let* ((event (car (icalendar--all-events ical)))
191 (organizer (replace-regexp-in-string
193 (or (icalendar--get-event-property event 'ORGANIZER) "")))
194 (prop-map '((summary . SUMMARY)
195 (description . DESCRIPTION)
196 (location . LOCATION)
199 (method (caddr (assoc 'METHOD (caddr (car (nreverse ical))))))
200 (attendee (when attendee-name-or-email
201 (gnus-icalendar-event--find-attendee ical attendee-name-or-email)))
202 (attendee-names (gnus-icalendar-event--get-attendee-names ical))
203 (role (plist-get (cadr attendee) 'ROLE))
204 (participation-type (pcase role
205 ("REQ-PARTICIPANT" 'required)
206 ("OPT-PARTICIPANT" 'optional)
207 (_ 'non-participant)))
208 (zone-map (icalendar--convert-all-timezones ical))
209 (args (list :method method
211 :start-time (gnus-icalendar-event--decode-datefield event 'DTSTART zone-map)
212 :end-time (gnus-icalendar-event--decode-datefield event 'DTEND zone-map)
213 :rsvp (string= (plist-get (cadr attendee) 'RSVP) "TRUE")
214 :participation-type participation-type
215 :req-participants (car attendee-names)
216 :opt-participants (cadr attendee-names)))
218 ((string= method "REQUEST") 'gnus-icalendar-event-request)
219 ((string= method "CANCEL") 'gnus-icalendar-event-cancel)
220 ((string= method "REPLY") 'gnus-icalendar-event-reply)
221 (t 'gnus-icalendar-event))))
223 (gmm-labels ((map-property (prop)
224 (let ((value (icalendar--get-event-property event prop)))
226 ;; ugly, but cannot get
227 ;;replace-regexp-in-string work with "\\" as
228 ;;REP, plus we should also handle "\\;"
229 (replace-regexp-in-string
231 (replace-regexp-in-string
232 "\\\\n" "\n" (substring-no-properties value))))))
233 (accumulate-args (mapping)
234 (destructuring-bind (slot . ical-property) mapping
235 (setq args (append (list
236 (intern (concat ":" (symbol-name slot)))
237 (map-property ical-property))
240 (mapc #'accumulate-args prop-map)
241 (apply 'make-instance event-class args))))
243 (defun gnus-icalendar-event-from-buffer (buf &optional attendee-name-or-email)
244 "Parse RFC5545 iCalendar in buffer BUF and return an event object.
246 Return a gnus-icalendar-event object representing the first event