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