064ba84cadc907ebe31cc83f7610927d50c9315a
[gnus] / lisp / gnus-icalendar.el
1 ;;; gnus-icalendar.el --- reply to iCalendar meeting requests
2
3 ;; Copyright (C) 2013  Free Software Foundation, Inc.
4
5 ;; Author: Jan Tatarik <Jan.Tatarik@gmail.com>
6 ;; Keywords: mail, icalendar, org
7
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.
12
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.
17
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/>.
20
21 ;;; Commentary:
22
23 ;; To install:
24 ;; (require 'gnus-icalendar)
25 ;; (gnus-icalendar-setup)
26
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)
32
33
34 ;;; Code:
35
36 (require 'icalendar)
37 (require 'eieio)
38 (require 'gmm-utils)
39 (require 'mm-decode)
40 (require 'gnus-sum)
41
42 (eval-when-compile (require 'cl))
43
44 (defun gnus-icalendar-find-if (pred seq)
45   (catch 'found
46     (while seq
47       (when (funcall pred (car seq))
48         (throw 'found (car seq)))
49       (pop seq))))
50
51 ;;;
52 ;;; ical-event
53 ;;;
54
55 (defclass gnus-icalendar-event ()
56   ((organizer :initarg :organizer
57               :accessor gnus-icalendar-event:organizer
58               :initform ""
59               :type (or null string))
60    (summary :initarg :summary
61             :accessor gnus-icalendar-event:summary
62             :initform ""
63             :type (or null string))
64    (description :initarg :description
65                 :accessor gnus-icalendar-event:description
66                 :initform ""
67                 :type (or null string))
68    (location :initarg :location
69              :accessor gnus-icalendar-event:location
70              :initform ""
71              :type (or null string))
72    (start-time :initarg :start-time
73           :accessor gnus-icalendar-event:start-time
74           :initform ""
75           :type (or null t))
76    (end-time :initarg :end-time
77         :accessor gnus-icalendar-event:end-time
78         :initform ""
79         :type (or null t))
80    (recur :initarg :recur
81           :accessor gnus-icalendar-event:recur
82           :initform ""
83           :type (or null string))
84    (uid :initarg :uid
85         :accessor gnus-icalendar-event:uid
86         :type string)
87    (method :initarg :method
88            :accessor gnus-icalendar-event:method
89            :initform "PUBLISH"
90            :type (or null string))
91    (rsvp :initarg :rsvp
92          :accessor gnus-icalendar-event:rsvp
93          :initform nil
94          :type (or null boolean)))
95   "generic iCalendar Event class")
96
97 (defclass gnus-icalendar-event-request (gnus-icalendar-event)
98   nil
99   "iCalendar class for REQUEST events")
100
101 (defclass gnus-icalendar-event-cancel (gnus-icalendar-event)
102   nil
103   "iCalendar class for CANCEL events")
104
105 (defclass gnus-icalendar-event-reply (gnus-icalendar-event)
106   nil
107   "iCalendar class for REPLY events")
108
109 (defmethod gnus-icalendar-event:recurring-p ((event gnus-icalendar-event))
110   "Return t if EVENT is recurring."
111   (not (null (gnus-icalendar-event:recur event))))
112
113 (defmethod gnus-icalendar-event:recurring-freq ((event gnus-icalendar-event))
114   "Return recurring frequency of EVENT."
115   (let ((rrule (gnus-icalendar-event:recur event)))
116     (string-match "FREQ=\\([[:alpha:]]+\\)" rrule)
117     (match-string 1 rrule)))
118
119 (defmethod gnus-icalendar-event:recurring-interval ((event gnus-icalendar-event))
120   "Return recurring interval of EVENT."
121   (let ((rrule (gnus-icalendar-event:recur event))
122         (default-interval 1))
123
124     (string-match "INTERVAL=\\([[:digit:]]+\\)" rrule)
125     (or (match-string 1 rrule)
126         default-interval)))
127
128 (defmethod gnus-icalendar-event:start ((event gnus-icalendar-event))
129   (format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:start-time event)))
130
131 (defun gnus-icalendar-event--decode-datefield (ical field)
132   (let* ((date (icalendar--get-event-property ical field))
133          (date-props (icalendar--get-event-property-attributes ical field))
134          (tz (plist-get date-props 'TZID)))
135
136     (date-to-time (timezone-make-date-arpa-standard date nil tz))))
137
138 (defun gnus-icalendar-event--find-attendee (ical name-or-email)
139   (let* ((event (car (icalendar--all-events ical)))
140          (event-props (caddr event)))
141     (gmm-labels ((attendee-name (att) (plist-get (cadr att) 'CN))
142                  (attendee-email (att)
143                    (replace-regexp-in-string "^.*MAILTO:" "" (caddr att)))
144                  (attendee-prop-matches-p (prop)
145                    (and (eq (car prop) 'ATTENDEE)
146                         (or (member (attendee-name prop) name-or-email)
147                             (let ((att-email (attendee-email prop)))
148                               (gnus-icalendar-find-if (lambda (email)
149                                                         (string-match email att-email))
150                                                       name-or-email))))))
151
152       (gnus-icalendar-find-if #'attendee-prop-matches-p event-props))))
153
154
155 (defun gnus-icalendar-event-from-ical (ical &optional attendee-name-or-email)
156   (let* ((event (car (icalendar--all-events ical)))
157          (organizer (replace-regexp-in-string
158                      "^.*MAILTO:" ""
159                      (or (icalendar--get-event-property event 'ORGANIZER) "")))
160          (prop-map '((summary . SUMMARY)
161                      (description . DESCRIPTION)
162                      (location . LOCATION)
163                      (recur . RRULE)
164                      (uid . UID)))
165          (method (caddr (assoc 'METHOD (caddr (car (nreverse ical))))))
166          (attendee (when attendee-name-or-email
167                      (gnus-icalendar-event--find-attendee ical attendee-name-or-email)))
168          (args (list :method method
169                      :organizer organizer
170                      :start-time (gnus-icalendar-event--decode-datefield event 'DTSTART)
171                      :end-time (gnus-icalendar-event--decode-datefield event 'DTEND)
172                      :rsvp (string= (plist-get (cadr attendee) 'RSVP)
173                                     "TRUE")))
174          (event-class (cond
175                        ((string= method "REQUEST") 'gnus-icalendar-event-request)
176                        ((string= method "CANCEL") 'gnus-icalendar-event-cancel)
177                        ((string= method "REPLY") 'gnus-icalendar-event-reply)
178                        (t 'gnus-icalendar-event))))
179
180     (gmm-labels ((map-property (prop)
181                    (let ((value (icalendar--get-event-property event prop)))
182                      (when value
183                        ;; ugly, but cannot get
184                        ;;replace-regexp-in-string work with "\\" as
185                        ;;REP, plus we should also handle "\\;"
186                        (replace-regexp-in-string
187                         "\\\\," ","
188                         (replace-regexp-in-string
189                          "\\\\n" "\n" (substring-no-properties value))))))
190                  (accumulate-args (mapping)
191                    (destructuring-bind (slot . ical-property) mapping
192                      (setq args (append (list
193                                          (intern (concat ":" (symbol-name slot)))
194                                          (map-property ical-property))
195                                         args)))))
196
197       (mapc #'accumulate-args prop-map)
198       (apply 'make-instance event-class args))))
199
200 (defun gnus-icalendar-event-from-buffer (buf &optional attendee-name-or-email)
201   "Parse RFC5545 iCalendar in buffer BUF and return an event object.
202
203 Return a gnus-icalendar-event object representing the first event
204 contained in the invitation. Return nil for calendars without an event entry.
205
206 ATTENDEE-NAME-OR-EMAIL is a list of strings that will be matched
207 against the event's attendee names and emails. Invitation rsvp
208 status will be retrieved from the first matching attendee record."
209   (let ((ical (with-current-buffer (icalendar--get-unfolded-buffer (get-buffer buf))
210                 (goto-char (point-min))
211                 (icalendar--read-element nil nil))))
212
213     (when ical
214       (gnus-icalendar-event-from-ical ical attendee-name-or-email))))
215
216 ;;;
217 ;;; gnus-icalendar-event-reply
218 ;;;
219
220 (defun gnus-icalendar-event--build-reply-event-body (ical-request status identities)
221   (let ((summary-status (capitalize (symbol-name status)))
222         (attendee-status (upcase (symbol-name status)))
223         reply-event-lines)
224     (gmm-labels ((update-summary (line)
225                    (if (string-match "^[^:]+:" line)
226                        (replace-match (format "\\&%s: " summary-status) t nil line)
227                      line))
228                  (update-dtstamp ()
229                    (format-time-string "DTSTAMP:%Y%m%dT%H%M%SZ" nil t))
230                  (attendee-matches-identity (line)
231                    (gnus-icalendar-find-if (lambda (name) (string-match-p name line))
232                                            identities))
233                  (update-attendee-status (line)
234                    (when (and (attendee-matches-identity line)
235                               (string-match "\\(PARTSTAT=\\)[^;]+" line))
236                      (replace-match (format "\\1%s" attendee-status) t nil line)))
237                  (process-event-line (line)
238                    (when (string-match "^\\([^;:]+\\)" line)
239                      (let* ((key (match-string 0 line))
240                             ;; NOTE: not all of the below fields are mandatory,
241                             ;; but they are often present in other clients'
242                             ;; replies. Can be helpful for debugging, too.
243                             (new-line
244                              (cond
245                               ((string= key "ATTENDEE") (update-attendee-status line))
246                               ((string= key "SUMMARY") (update-summary line))
247                               ((string= key "DTSTAMP") (update-dtstamp))
248                               ((member key '("ORGANIZER" "DTSTART" "DTEND"
249                                              "LOCATION" "DURATION" "SEQUENCE"
250                                              "RECURRENCE-ID" "UID")) line)
251                               (t nil))))
252                        (when new-line
253                          (push new-line reply-event-lines))))))
254
255       (mapc #'process-event-line (split-string ical-request "\n"))
256
257       (unless (gnus-icalendar-find-if (lambda (x) (string-match "^ATTENDEE" x))
258                           reply-event-lines)
259         (error "Could not find an event attendee matching given identity"))
260
261       (mapconcat #'identity `("BEGIN:VEVENT"
262                               ,@(nreverse reply-event-lines)
263                               "END:VEVENT")
264                  "\n"))))
265
266 (defun gnus-icalendar-event-reply-from-buffer (buf status identities)
267   "Build a calendar event reply for request contained in BUF.
268 The reply will have STATUS (`accepted', `tentative' or  `declined').
269 The reply will be composed for attendees matching any entry
270 on the IDENTITIES list."
271   (gmm-labels ((extract-block (blockname)
272                (save-excursion
273                  (let ((block-start-re (format "^BEGIN:%s" blockname))
274                        (block-end-re (format "^END:%s" blockname))
275                        start)
276                    (when (re-search-forward block-start-re nil t)
277                      (setq start (line-beginning-position))
278                      (re-search-forward block-end-re)
279                      (buffer-substring-no-properties start (line-end-position)))))))
280
281     (let (zone event)
282       (with-current-buffer (icalendar--get-unfolded-buffer (get-buffer buf))
283         (goto-char (point-min))
284         (setq zone (extract-block "VTIMEZONE")
285               event (extract-block "VEVENT")))
286
287       (when event
288         (let ((contents (list "BEGIN:VCALENDAR"
289                               "METHOD:REPLY"
290                               "PRODID:Gnus"
291                               "VERSION:2.0"
292                               zone
293                               (gnus-icalendar-event--build-reply-event-body event status identities)
294                               "END:VCALENDAR")))
295
296           (mapconcat #'identity (delq nil contents) "\n"))))))
297
298 ;;;
299 ;;; gnus-icalendar-org
300 ;;;
301 ;;; TODO: this is an optional feature, and it's only available with org-mode
302 ;;; 7+, so will need to properly handle emacsen with no/outdated org-mode
303
304 (require 'org)
305 (require 'org-capture)
306
307 (defgroup gnus-icalendar-org nil
308   "Settings for Calendar Event gnus/org integration."
309   :group 'gnus-icalendar
310   :prefix "gnus-icalendar-org-")
311
312 (defcustom gnus-icalendar-org-capture-file nil
313   "Target Org file for storing captured calendar events."
314   :type '(choice (const nil) file)
315   :group 'gnus-icalendar-org)
316
317 (defcustom gnus-icalendar-org-capture-headline nil
318   "Target outline in `gnus-icalendar-org-capture-file' for storing captured events."
319   :type '(repeat string)
320   :group 'gnus-icalendar-org)
321
322 (defcustom gnus-icalendar-org-template-name "used by gnus-icalendar-org"
323   "Org-mode template name."
324   :type '(string)
325   :group 'gnus-icalendar-org)
326
327 (defcustom gnus-icalendar-org-template-key "#"
328   "Org-mode template hotkey."
329   :type '(string)
330   :group 'gnus-icalendar-org)
331
332 (defvar gnus-icalendar-org-enabled-p nil)
333
334
335 (defmethod gnus-icalendar-event:org-repeat ((event gnus-icalendar-event))
336   "Return `org-mode' timestamp repeater string for recurring EVENT.
337 Return nil for non-recurring EVENT."
338   (when (gnus-icalendar-event:recurring-p event)
339     (let* ((freq-map '(("HOURLY" . "h")
340                        ("DAILY" . "d")
341                        ("WEEKLY" . "w")
342                        ("MONTHLY" . "m")
343                        ("YEARLY" . "y")))
344            (org-freq (cdr (assoc (gnus-icalendar-event:recurring-freq event) freq-map))))
345
346       (when org-freq
347         (format "+%s%s" (gnus-icalendar-event:recurring-interval event) org-freq)))))
348
349 (defmethod gnus-icalendar-event:org-timestamp ((event gnus-icalendar-event))
350   "Build `org-mode' timestamp from EVENT start/end dates and recurrence info."
351   (let* ((start (gnus-icalendar-event:start-time event))
352          (end (gnus-icalendar-event:end-time event))
353          (start-date (format-time-string "%Y-%m-%d %a" start))
354          (start-time (format-time-string "%H:%M" start))
355          (end-date (format-time-string "%Y-%m-%d %a" end))
356          (end-time (format-time-string "%H:%M" end))
357          (org-repeat (gnus-icalendar-event:org-repeat event))
358          (repeat (if org-repeat (concat " " org-repeat) "")))
359
360     (if (equal start-date end-date)
361         (format "<%s %s-%s%s>" start-date start-time end-time repeat)
362       (format "<%s %s>--<%s %s>" start-date start-time end-date end-time))))
363
364 (defun gnus-icalendar--format-summary-line (summary &optional location)
365   (if location
366       (format "%s (%s)" summary location)
367     (format "%s" summary)))
368
369 ;; TODO: make the template customizable
370 (defmethod gnus-icalendar-event->org-entry ((event gnus-icalendar-event) reply-status)
371   "Return string with new `org-mode' entry describing EVENT."
372   (with-temp-buffer
373     (org-mode)
374     (with-slots (organizer summary description location
375                            recur uid) event
376       (let* ((reply (if reply-status (capitalize (symbol-name reply-status))
377                       "Not replied yet"))
378              (props `(("ICAL_EVENT" . "t")
379                       ("ID" . ,uid)
380                       ("DT" . ,(gnus-icalendar-event:org-timestamp event))
381                       ("ORGANIZER" . ,(gnus-icalendar-event:organizer event))
382                       ("LOCATION" . ,(gnus-icalendar-event:location event))
383                       ("RRULE" . ,(gnus-icalendar-event:recur event))
384                       ("REPLY" . ,reply))))
385
386         (insert (format "* %s\n\n"
387                         (gnus-icalendar--format-summary-line summary location)))
388         (mapc (lambda (prop)
389                 (org-entry-put (point) (car prop) (cdr prop)))
390               props))
391
392       (when description
393         (save-restriction
394           (narrow-to-region (point) (point))
395           (insert description)
396           (indent-region (point-min) (point-max) 2)
397           (fill-region (point-min) (point-max))))
398
399       (buffer-string))))
400
401 (defun gnus-icalendar--deactivate-org-timestamp (ts)
402   (replace-regexp-in-string "[<>]"
403                             (lambda (m) (cond ((string= m "<") "[")
404                                               ((string= m ">") "]")))
405                             ts))
406
407 (defun gnus-icalendar-find-org-event-file (event &optional org-file)
408   "Return the name of the file containing EVENT org entry.
409 Return nil when not found.
410
411 All org agenda files are searched for the EVENT entry.  When
412 the optional ORG-FILE argument is specified, only that one file
413 is searched."
414   (let ((uid (gnus-icalendar-event:uid event))
415         (files (or org-file (org-agenda-files t 'ifmode))))
416     (gmm-labels
417         ((find-event-in (file)
418            (org-check-agenda-file file)
419            (with-current-buffer (find-file-noselect file)
420              (let ((event-pos (org-find-entry-with-id uid)))
421                (when (and event-pos
422                           (string= (cdr (assoc "ICAL_EVENT" (org-entry-properties event-pos)))
423                                    "t"))
424                  (throw 'found file))))))
425
426       (gnus-icalendar-find-if #'find-event-in files))))
427
428
429 (defun gnus-icalendar--show-org-event (event &optional org-file)
430   (let ((file (gnus-icalendar-find-org-event-file event org-file)))
431     (when file
432       (switch-to-buffer (find-file file))
433       (goto-char (org-find-entry-with-id (gnus-icalendar-event:uid event)))
434       (org-show-entry))))
435
436
437 (defun gnus-icalendar--update-org-event (event reply-status &optional org-file)
438   (let ((file (gnus-icalendar-find-org-event-file event org-file)))
439     (when file
440       (with-current-buffer (find-file-noselect file)
441         (with-slots (uid summary description organizer location recur) event
442           (let ((event-pos (org-find-entry-with-id uid)))
443             (when event-pos
444               (goto-char event-pos)
445
446               ;; update the headline, keep todo, priority and tags, if any
447               (save-excursion
448                 (let* ((priority (org-entry-get (point) "PRIORITY"))
449                        (headline (delq nil (list
450                                             (org-entry-get (point) "TODO")
451                                             (when priority (format "[#%s]" priority))
452                                             (gnus-icalendar--format-summary-line summary location)
453                                             (org-entry-get (point) "TAGS")))))
454
455                   (re-search-forward "^\\*+ " (line-end-position))
456                   (delete-region (point) (line-end-position))
457                   (insert (mapconcat #'identity headline " "))))
458
459               ;; update props and description
460               (let ((entry-end (org-entry-end-position))
461                     (entry-outline-level (org-outline-level)))
462
463                 ;; delete body of the entry, leave org drawers intact
464                 (save-restriction
465                   (org-narrow-to-element)
466                   (goto-char entry-end)
467                   (re-search-backward "^[\t ]*:END:")
468                   (forward-line)
469                   (delete-region (point) entry-end))
470
471                 ;; put new event description in the entry body
472                 (when description
473                   (save-restriction
474                     (narrow-to-region (point) (point))
475                     (insert "\n" (replace-regexp-in-string "[\n]+$" "\n" description) "\n")
476                     (indent-region (point-min) (point-max) (1+ entry-outline-level))
477                     (fill-region (point-min) (point-max))))
478
479                 ;; update entry properties
480                 (org-entry-put event-pos "DT" (gnus-icalendar-event:org-timestamp event))
481                 (org-entry-put event-pos "ORGANIZER" organizer)
482                 (org-entry-put event-pos "LOCATION" location)
483                 (org-entry-put event-pos "RRULE" recur)
484                 (when reply-status (org-entry-put event-pos "REPLY"
485                                                   (capitalize (symbol-name reply-status))))
486                 (save-buffer)))))))))
487
488
489 (defun gnus-icalendar--cancel-org-event (event &optional org-file)
490   (let ((file (gnus-icalendar-find-org-event-file event org-file)))
491     (when file
492       (with-current-buffer (find-file-noselect file)
493         (let ((event-pos (org-find-entry-with-id (gnus-icalendar-event:uid event))))
494           (when event-pos
495             (let ((ts (org-entry-get event-pos "DT")))
496               (when ts
497                 (org-entry-put event-pos "DT" (gnus-icalendar--deactivate-org-timestamp ts))
498                 (save-buffer)))))))))
499
500
501 (defun gnus-icalendar--get-org-event-reply-status (event &optional org-file)
502   (let ((file (gnus-icalendar-find-org-event-file event org-file)))
503     (when file
504       (save-excursion
505         (with-current-buffer (find-file-noselect file)
506           (let ((event-pos (org-find-entry-with-id (gnus-icalendar-event:uid event))))
507             (org-entry-get event-pos "REPLY")))))))
508
509
510 (defun gnus-icalendar-insinuate-org-templates ()
511   (unless (gnus-icalendar-find-if (lambda (x) (string= (cadr x) gnus-icalendar-org-template-name))
512                       org-capture-templates)
513     (setq org-capture-templates
514           (append `((,gnus-icalendar-org-template-key
515                      ,gnus-icalendar-org-template-name
516                      entry
517                      (file+olp ,gnus-icalendar-org-capture-file ,@gnus-icalendar-org-capture-headline)
518                      "%i"
519                      :immediate-finish t))
520                   org-capture-templates))
521
522     ;; hide the template from interactive template selection list
523     ;; (org-capture)
524     ;; NOTE: doesn't work when capturing from string
525     ;; (when (boundp 'org-capture-templates-contexts)
526     ;;   (push `(,gnus-icalendar-org-template-key "" ((in-mode . "gnus-article-mode")))
527     ;;         org-capture-templates-contexts))
528     ))
529
530 (defun gnus-icalendar:org-event-save (event reply-status)
531   (with-temp-buffer
532     (org-capture-string (gnus-icalendar-event->org-entry event reply-status)
533                         gnus-icalendar-org-template-key)))
534
535 (defun gnus-icalendar-show-org-agenda (event)
536   (let* ((time-delta (time-subtract (gnus-icalendar-event:end-time event)
537                                     (gnus-icalendar-event:start-time event)))
538          (duration-days (1+ (/ (+ (* (car time-delta) (expt 2 16))
539                                   (cadr time-delta))
540                                86400))))
541
542     (org-agenda-list nil (gnus-icalendar-event:start event) duration-days)))
543
544 (defmethod gnus-icalendar-event:sync-to-org ((event gnus-icalendar-event-request) reply-status)
545   (if (gnus-icalendar-find-org-event-file event)
546       (gnus-icalendar--update-org-event event reply-status)
547     (gnus-icalendar:org-event-save event reply-status)))
548
549 (defmethod gnus-icalendar-event:sync-to-org ((event gnus-icalendar-event-cancel) reply-status)
550   (when (gnus-icalendar-find-org-event-file event)
551     (gnus-icalendar--cancel-org-event event)))
552
553 (defun gnus-icalendar-org-setup ()
554   (if (and gnus-icalendar-org-capture-file gnus-icalendar-org-capture-headline)
555       (progn
556         (gnus-icalendar-insinuate-org-templates)
557         (setq gnus-icalendar-org-enabled-p t))
558     (message "Cannot enable Calendar->Org: missing capture file, headline")))
559
560 ;;;
561 ;;; gnus-icalendar
562 ;;;
563
564 (defgroup gnus-icalendar nil
565   "Settings for inline display of iCalendar invitations."
566   :group 'gnus-article
567   :prefix "gnus-icalendar-")
568
569 (defcustom gnus-icalendar-reply-bufname "*CAL*"
570   "Buffer used for building iCalendar invitation reply."
571   :type '(string)
572   :group 'gnus-icalendar)
573
574 (make-variable-buffer-local
575  (defvar gnus-icalendar-reply-status nil))
576
577 (make-variable-buffer-local
578  (defvar gnus-icalendar-event nil))
579
580 (make-variable-buffer-local
581  (defvar gnus-icalendar-handle nil))
582
583 (defvar gnus-icalendar-identities
584   (apply #'append
585          (mapcar (lambda (x) (if (listp x) x (list x)))
586                  (list user-full-name (regexp-quote user-mail-address)
587                        ; NOTE: this one can be a list
588                        gnus-ignored-from-addresses))))
589
590 ;; TODO: make the template customizable
591 (defmethod gnus-icalendar-event->gnus-calendar ((event gnus-icalendar-event) &optional reply-status)
592   "Format an overview of EVENT details."
593   (gmm-labels ((format-header (x)
594             (format "%-12s%s"
595                     (propertize (concat (car x) ":") 'face 'bold)
596                     (cadr x))))
597
598     (with-slots (organizer summary description location recur uid method rsvp) event
599       (let ((headers `(("Summary" ,summary)
600                       ("Location" ,(or location ""))
601                       ("Time" ,(gnus-icalendar-event:org-timestamp event))
602                       ("Organizer" ,organizer)
603                       ("Method" ,method))))
604
605        (when (and (not (gnus-icalendar-event-reply-p event)) rsvp)
606          (setq headers (append headers
607                                `(("Status" ,(or reply-status "Not replied yet"))))))
608
609        (concat
610         (mapconcat #'format-header headers "\n")
611         "\n\n"
612         description)))))
613
614 (defmacro gnus-icalendar-with-decoded-handle (handle &rest body)
615   "Execute BODY in buffer containing the decoded contents of HANDLE."
616   (let ((charset (make-symbol "charset")))
617     `(let ((,charset (cdr (assoc 'charset (mm-handle-type ,handle)))))
618        (with-temp-buffer
619          (mm-insert-part ,handle)
620          (when (string= ,charset "utf-8")
621            (mm-decode-coding-region (point-min) (point-max) 'utf-8))
622
623          ,@body))))
624
625
626 (defun gnus-icalendar-event-from-handle (handle &optional attendee-name-or-email)
627   (gnus-icalendar-with-decoded-handle handle
628                        (gnus-icalendar-event-from-buffer (current-buffer) attendee-name-or-email)))
629
630 (defun gnus-icalendar-insert-button (text callback data)
631   ;; FIXME: the gnus-mime-button-map keymap does not make sense for this kind
632   ;; of button.
633   (let ((start (point)))
634     (gnus-add-text-properties
635      start
636      (progn
637        (insert "[ " text " ]")
638        (point))
639      `(gnus-callback
640        ,callback
641        keymap ,gnus-mime-button-map
642        face ,gnus-article-button-face
643        gnus-data ,data))
644     (widget-convert-button 'link start (point)
645                            :action 'gnus-widget-press-button
646                            :button-keymap gnus-widget-button-keymap)))
647
648 (defun gnus-icalendar-send-buffer-by-mail (buffer-name subject)
649   (let ((message-signature nil))
650     (with-current-buffer gnus-summary-buffer
651       (gnus-summary-reply)
652       (message-goto-body)
653       (mml-insert-multipart "alternative")
654       (mml-insert-empty-tag 'part 'type "text/plain")
655       (mml-attach-buffer buffer-name "text/calendar; method=REPLY; charset=UTF-8")
656       (message-goto-subject)
657       (delete-region (line-beginning-position) (line-end-position))
658       (insert "Subject: " subject)
659       (message-send-and-exit))))
660
661 (defun gnus-icalendar-reply (data)
662   (let* ((handle (car data))
663          (status (cadr data))
664          (event (caddr data))
665          (reply (gnus-icalendar-with-decoded-handle handle
666                   (gnus-icalendar-event-reply-from-buffer
667                    (current-buffer) status gnus-icalendar-identities))))
668
669     (when reply
670       (gmm-labels ((fold-icalendar-buffer ()
671                (goto-char (point-min))
672                (while (re-search-forward "^\\(.\\{72\\}\\)\\(.+\\)$" nil t)
673                  (replace-match "\\1\n \\2")
674                  (goto-char (line-beginning-position)))))
675         (let ((subject (concat (capitalize (symbol-name status))
676                                ": " (gnus-icalendar-event:summary event))))
677
678           (with-current-buffer (get-buffer-create gnus-icalendar-reply-bufname)
679             (delete-region (point-min) (point-max))
680             (insert reply)
681             (fold-icalendar-buffer)
682             (gnus-icalendar-send-buffer-by-mail (buffer-name) subject))
683
684           ;; Back in article buffer
685           (setq-local gnus-icalendar-reply-status status)
686           (when gnus-icalendar-org-enabled-p
687             (gnus-icalendar--update-org-event event status)
688             ;; refresh article buffer to update the reply status
689             (with-current-buffer gnus-summary-buffer
690               (gnus-summary-show-article))))))))
691
692 (defun gnus-icalendar-sync-event-to-org (event)
693   (gnus-icalendar-event:sync-to-org event gnus-icalendar-reply-status))
694
695 (defmethod gnus-icalendar-event:inline-reply-buttons ((event gnus-icalendar-event) handle)
696   (when (gnus-icalendar-event:rsvp event)
697     `(("Accept" gnus-icalendar-reply (,handle accepted ,event))
698       ("Tentative" gnus-icalendar-reply (,handle tentative ,event))
699       ("Decline" gnus-icalendar-reply (,handle declined ,event)))))
700
701 (defmethod gnus-icalendar-event:inline-reply-buttons ((event gnus-icalendar-event-reply) handle)
702   "No buttons for REPLY events."
703   nil)
704
705 (defmethod gnus-icalendar-event:inline-reply-status ((event gnus-icalendar-event))
706   (or (when gnus-icalendar-org-enabled-p
707         (gnus-icalendar--get-org-event-reply-status event))
708       "Not replied yet"))
709
710 (defmethod gnus-icalendar-event:inline-reply-status ((event gnus-icalendar-event-reply))
711   "No reply status for REPLY events."
712   nil)
713
714
715 (defmethod gnus-icalendar-event:inline-org-buttons ((event gnus-icalendar-event))
716   (let* ((org-entry-exists-p (gnus-icalendar-find-org-event-file event))
717          (export-button-text (if org-entry-exists-p "Update Org Entry" "Export to Org")))
718
719     (delq nil (list
720                `("Show Agenda" gnus-icalendar-show-org-agenda ,event)
721                (when (gnus-icalendar-event-request-p event)
722                  `(,export-button-text gnus-icalendar-sync-event-to-org ,event))
723                (when org-entry-exists-p
724                  `("Show Org Entry" gnus-icalendar--show-org-event ,event))))))
725
726
727 (defmethod gnus-icalendar-event:inline-org-buttons ((event gnus-icalendar-event-cancel))
728   (let ((org-entry-exists-p (gnus-icalendar-find-org-event-file event)))
729
730     (delq nil (list
731                `("Show Agenda" gnus-icalendar-show-org-agenda ,event)
732                (when org-entry-exists-p
733                  `("Update Org Entry" gnus-icalendar-sync-event-to-org ,event))
734                (when org-entry-exists-p
735                  `("Show Org Entry" gnus-icalendar--show-org-event ,event))))))
736
737
738 (defun gnus-icalendar-mm-inline (handle)
739   (let ((event (gnus-icalendar-event-from-handle handle gnus-icalendar-identities)))
740
741     (setq gnus-icalendar-reply-status nil)
742
743     (when event
744       (gmm-labels ((insert-button-group (buttons)
745                 (when buttons
746                   (mapc (lambda (x)
747                           (apply 'gnus-icalendar-insert-button x)
748                           (insert "    "))
749                         buttons)
750                   (insert "\n\n"))))
751
752         (insert-button-group
753          (gnus-icalendar-event:inline-reply-buttons event handle))
754
755         (when gnus-icalendar-org-enabled-p
756           (insert-button-group (gnus-icalendar-event:inline-org-buttons event)))
757
758         (setq gnus-icalendar-event event
759               gnus-icalendar-handle handle)
760
761         (insert (gnus-icalendar-event->gnus-calendar
762                  event
763                  (gnus-icalendar-event:inline-reply-status event)))))))
764
765 (defun gnus-icalendar-save-part (handle)
766   (let (event)
767     (when (and (equal (car (mm-handle-type handle)) "text/calendar")
768                (setq event (gnus-icalendar-event-from-handle handle gnus-icalendar-identities)))
769
770       (gnus-icalendar-event:sync-to-org event))))
771
772
773 (defun gnus-icalendar-save-event ()
774   "Save the Calendar event in the text/calendar part under point."
775   (interactive)
776   (gnus-article-check-buffer)
777   (let ((data (get-text-property (point) 'gnus-data)))
778     (when data
779       (gnus-icalendar-save-part data))))
780
781 (defun gnus-icalendar-reply-accept ()
782   "Accept invitation in the current article."
783   (interactive)
784   (with-current-buffer gnus-article-buffer
785     (gnus-icalendar-reply (list gnus-icalendar-handle 'accepted gnus-icalendar-event))
786     (setq-local gnus-icalendar-reply-status 'accepted)))
787
788 (defun gnus-icalendar-reply-tentative ()
789   "Send tentative response to invitation in the current article."
790   (interactive)
791   (with-current-buffer gnus-article-buffer
792     (gnus-icalendar-reply (list gnus-icalendar-handle 'tentative gnus-icalendar-event))
793     (setq-local gnus-icalendar-reply-status 'tentative)))
794
795 (defun gnus-icalendar-reply-decline ()
796   "Decline invitation in the current article."
797   (interactive)
798   (with-current-buffer gnus-article-buffer
799     (gnus-icalendar-reply (list gnus-icalendar-handle 'declined gnus-icalendar-event))
800     (setq-local gnus-icalendar-reply-status 'declined)))
801
802 (defun gnus-icalendar-event-export ()
803   "Export calendar event to `org-mode', or update existing agenda entry."
804   (interactive)
805   (with-current-buffer gnus-article-buffer
806     (gnus-icalendar-sync-event-to-org gnus-icalendar-event))
807   ;; refresh article buffer in case the reply had been sent before initial org
808   ;; export
809   (with-current-buffer gnus-summary-buffer
810     (gnus-summary-show-article)))
811
812 (defun gnus-icalendar-event-show ()
813   "Display `org-mode' agenda entry related to the calendar event."
814   (interactive)
815   (gnus-icalendar--show-org-event
816    (with-current-buffer gnus-article-buffer
817      gnus-icalendar-event)))
818
819 (defun gnus-icalendar-event-check-agenda ()
820   "Display `org-mode' agenda for days between event start and end dates."
821   (interactive)
822   (gnus-icalendar-show-org-agenda
823    (with-current-buffer gnus-article-buffer gnus-icalendar-event)))
824
825 (defvar gnus-mime-action-alist)         ; gnus-art
826
827 (defun gnus-icalendar-setup ()
828   (add-to-list 'mm-inlined-types "text/calendar")
829   (add-to-list 'mm-automatic-display "text/calendar")
830   (add-to-list 'mm-inline-media-tests '("text/calendar" gnus-icalendar-mm-inline identity))
831
832   (gnus-define-keys (gnus-summary-calendar-map "i" gnus-summary-mode-map)
833     "a" gnus-icalendar-reply-accept
834     "t" gnus-icalendar-reply-tentative
835     "d" gnus-icalendar-reply-decline
836     "c" gnus-icalendar-event-check-agenda
837     "e" gnus-icalendar-event-export
838     "s" gnus-icalendar-event-show)
839
840   (require 'gnus-art)
841   (add-to-list 'gnus-mime-action-alist
842                (cons "save calendar event" 'gnus-icalendar-save-event)
843                t))
844
845 (provide 'gnus-icalendar)
846
847 ;;; gnus-icalendar.el ends here