gnus-icalendar.el 41 KB

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