X-Git-Url: http://cgit.sxemacs.org/?a=blobdiff_plain;f=lisp%2Fgnus-icalendar.el;h=56c56f3dd466975da7b23edd257058ff63fb8bfd;hb=cf9036b65fbe4cadd057d7449381b54292772caf;hp=a946a586033906b32305d5e4b292cb2bc0e9dec3;hpb=baf05b14d1562dbcef68a2e409e7053a58c11273;p=gnus diff --git a/lisp/gnus-icalendar.el b/lisp/gnus-icalendar.el index a946a5860..56c56f3dd 100644 --- a/lisp/gnus-icalendar.el +++ b/lisp/gnus-icalendar.el @@ -35,6 +35,7 @@ (require 'icalendar) (require 'eieio) +(require 'gmm-utils) (require 'mm-decode) (require 'gnus-sum) @@ -68,14 +69,14 @@ :accessor gnus-icalendar-event:location :initform "" :type (or null string)) - (start :initarg :start - :accessor gnus-icalendar-event:start + (start-time :initarg :start-time + :accessor gnus-icalendar-event:start-time :initform "" - :type (or null string)) - (end :initarg :end - :accessor gnus-icalendar-event:end + :type (or null t)) + (end-time :initarg :end-time + :accessor gnus-icalendar-event:end-time :initform "" - :type (or null string)) + :type (or null t)) (recur :initarg :recur :accessor gnus-icalendar-event:recur :initform "" @@ -90,7 +91,19 @@ (rsvp :initarg :rsvp :accessor gnus-icalendar-event:rsvp :initform nil - :type (or null boolean))) + :type (or null boolean)) + (participation-required :initarg :participation-required + :accessor gnus-icalendar-event:participation-required + :initform t + :type (or null boolean)) + (req-participants :initarg :req-participants + :accessor gnus-icalendar-event:req-participants + :initform nil + :type (or null t)) + (opt-participants :initarg :opt-participants + :accessor gnus-icalendar-event:opt-participants + :initform nil + :type (or null t))) "generic iCalendar Event class") (defclass gnus-icalendar-event-request (gnus-icalendar-event) @@ -124,32 +137,20 @@ (or (match-string 1 rrule) default-interval))) -(defmethod gnus-icalendar-event:start-time ((event gnus-icalendar-event)) - "Return time value of the EVENT start date." - (date-to-time (gnus-icalendar-event:start event))) +(defmethod gnus-icalendar-event:start ((event gnus-icalendar-event)) + (format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:start-time event))) -(defmethod gnus-icalendar-event:end-time ((event gnus-icalendar-event)) - "Return time value of the EVENT end date." - (date-to-time (gnus-icalendar-event:end event))) +(defun gnus-icalendar-event--decode-datefield (ical field) + (let* ((date (icalendar--get-event-property ical field)) + (date-props (icalendar--get-event-property-attributes ical field)) + (tz (plist-get date-props 'TZID))) - -(defun gnus-icalendar-event--decode-datefield (ical field zone-map &optional date-style) - (let* ((calendar-date-style (or date-style 'european)) - (date (icalendar--get-event-property ical field)) - (date-zone (icalendar--find-time-zone - (icalendar--get-event-property-attributes - ical field) - zone-map)) - (date-decoded (icalendar--decode-isodatetime date nil date-zone))) - - (concat (icalendar--datetime-to-iso-date date-decoded "-") - " " - (icalendar--datetime-to-colontime date-decoded)))) + (date-to-time (timezone-make-date-arpa-standard date nil tz)))) (defun gnus-icalendar-event--find-attendee (ical name-or-email) (let* ((event (car (icalendar--all-events ical))) (event-props (caddr event))) - (labels ((attendee-name (att) (plist-get (cadr att) 'CN)) + (gmm-labels ((attendee-name (att) (plist-get (cadr att) 'CN)) (attendee-email (att) (replace-regexp-in-string "^.*MAILTO:" "" (caddr att))) (attendee-prop-matches-p (prop) @@ -162,10 +163,27 @@ (gnus-icalendar-find-if #'attendee-prop-matches-p event-props)))) +(defun gnus-icalendar-event--get-attendee-names (ical) + (let* ((event (car (icalendar--all-events ical))) + (attendee-props (gnus-remove-if-not + (lambda (p) (eq (car p) 'ATTENDEE)) + (caddr event)))) + + (gmm-labels ((attendee-role (prop) (plist-get (cadr prop) 'ROLE)) + (attendee-name (prop) (plist-get (cadr prop) 'CN)) + (attendees-by-type (type) + (gnus-remove-if-not + (lambda (p) (string= (attendee-role p) type)) + attendee-props)) + (attendee-names-by-type (type) + (mapcar #'attendee-name (attendees-by-type type)))) + + (list + (attendee-names-by-type "REQ-PARTICIPANT") + (attendee-names-by-type "OPT-PARTICIPANT"))))) (defun gnus-icalendar-event-from-ical (ical &optional attendee-name-or-email) (let* ((event (car (icalendar--all-events ical))) - (zone-map (icalendar--convert-all-timezones ical)) (organizer (replace-regexp-in-string "^.*MAILTO:" "" (or (icalendar--get-event-property event 'ORGANIZER) ""))) @@ -177,19 +195,24 @@ (method (caddr (assoc 'METHOD (caddr (car (nreverse ical)))))) (attendee (when attendee-name-or-email (gnus-icalendar-event--find-attendee ical attendee-name-or-email))) + (attendee-names (gnus-icalendar-event--get-attendee-names ical)) (args (list :method method :organizer organizer - :start (gnus-icalendar-event--decode-datefield event 'DTSTART zone-map) - :end (gnus-icalendar-event--decode-datefield event 'DTEND zone-map) + :start-time (gnus-icalendar-event--decode-datefield event 'DTSTART) + :end-time (gnus-icalendar-event--decode-datefield event 'DTEND) :rsvp (string= (plist-get (cadr attendee) 'RSVP) - "TRUE"))) + "TRUE") + :participation-required (string= (plist-get (cadr attendee) 'ROLE) + "REQ-PARTICIPANT") + :req-participants (cdar attendee-names) + :opt-participants (cadr attendee-names))) (event-class (cond ((string= method "REQUEST") 'gnus-icalendar-event-request) ((string= method "CANCEL") 'gnus-icalendar-event-cancel) ((string= method "REPLY") 'gnus-icalendar-event-reply) (t 'gnus-icalendar-event)))) - (labels ((map-property (prop) + (gmm-labels ((map-property (prop) (let ((value (icalendar--get-event-property event prop))) (when value ;; ugly, but cannot get @@ -233,7 +256,7 @@ status will be retrieved from the first matching attendee record." (let ((summary-status (capitalize (symbol-name status))) (attendee-status (upcase (symbol-name status))) reply-event-lines) - (labels ((update-summary (line) + (gmm-labels ((update-summary (line) (if (string-match "^[^:]+:" line) (replace-match (format "\\&%s: " summary-status) t nil line) line)) @@ -257,9 +280,9 @@ status will be retrieved from the first matching attendee record." ((string= key "ATTENDEE") (update-attendee-status line)) ((string= key "SUMMARY") (update-summary line)) ((string= key "DTSTAMP") (update-dtstamp)) - ((find key '("ORGANIZER" "DTSTART" "DTEND" - "LOCATION" "DURATION" "SEQUENCE" - "RECURRENCE-ID" "UID")) line) + ((member key '("ORGANIZER" "DTSTART" "DTEND" + "LOCATION" "DURATION" "SEQUENCE" + "RECURRENCE-ID" "UID")) line) (t nil)))) (when new-line (push new-line reply-event-lines)))))) @@ -280,7 +303,7 @@ status will be retrieved from the first matching attendee record." The reply will have STATUS (`accepted', `tentative' or `declined'). The reply will be composed for attendees matching any entry on the IDENTITIES list." - (flet ((extract-block (blockname) + (gmm-labels ((extract-block (blockname) (save-excursion (let ((block-start-re (format "^BEGIN:%s" blockname)) (block-end-re (format "^END:%s" blockname)) @@ -362,16 +385,57 @@ Return nil for non-recurring EVENT." "Build `org-mode' timestamp from EVENT start/end dates and recurrence info." (let* ((start (gnus-icalendar-event:start-time event)) (end (gnus-icalendar-event:end-time event)) - (start-date (format-time-string "%Y-%m-%d %a" start t)) - (start-time (format-time-string "%H:%M" start t)) - (end-date (format-time-string "%Y-%m-%d %a" end t)) - (end-time (format-time-string "%H:%M" end t)) + (start-date (format-time-string "%Y-%m-%d %a" start)) + (start-time (format-time-string "%H:%M" start)) + (start-at-midnight (string= start-time "00:00")) + (end-date (format-time-string "%Y-%m-%d %a" end)) + (end-time (format-time-string "%H:%M" end)) + (end-at-midnight (string= end-time "00:00")) + (start-end-date-diff (/ (float-time (time-subtract + (date-to-time end-date) + (date-to-time start-date))) + 86400)) (org-repeat (gnus-icalendar-event:org-repeat event)) - (repeat (if org-repeat (concat " " org-repeat) ""))) - - (if (equal start-date end-date) - (format "<%s %s-%s%s>" start-date start-time end-time repeat) - (format "<%s %s>--<%s %s>" start-date start-time end-date end-time)))) + (repeat (if org-repeat (concat " " org-repeat) "")) + (time-1-day '(0 86400))) + + ;; NOTE: special care is needed with appointments ending at midnight + ;; (typically all-day events): the end time has to be changed to 23:59 to + ;; prevent org agenda showing the event on one additional day + (cond + ;; start/end midnight + ;; A 0:0 - A+1 0:0 -> A + ;; A 0:0 - A+n 0:0 -> A - A+n-1 + ((and start-at-midnight end-at-midnight) (if (> start-end-date-diff 1) + (let ((end-ts (format-time-string "%Y-%m-%d %a" (time-subtract end time-1-day)))) + (format "<%s>--<%s>" start-date end-ts)) + (format "<%s%s>" start-date repeat))) + ;; end midnight + ;; A .:. - A+1 0:0 -> A .:.-23:59 + ;; A .:. - A+n 0:0 -> A .:. - A_n-1 + (end-at-midnight (if (= start-end-date-diff 1) + (format "<%s %s-23:59%s>" start-date start-time repeat) + (let ((end-ts (format-time-string "%Y-%m-%d %a" (time-subtract end time-1-day)))) + (format "<%s %s>--<%s>" start-date start-time end-ts)))) + ;; start midnight + ;; A 0:0 - A .:. -> A 0:0-.:. (default 1) + ;; A 0:0 - A+n .:. -> A - A+n .:. + ((and start-at-midnight + (plusp start-end-date-diff)) (format "<%s>--<%s %s>" start-date end-date end-time)) + ;; default + ;; A .:. - A .:. -> A .:.-.:. + ;; A .:. - B .:. + ((zerop start-end-date-diff) (format "<%s %s-%s%s>" start-date start-time end-time repeat)) + (t (format "<%s %s>--<%s %s>" start-date start-time end-date end-time))))) + +(defun gnus-icalendar--format-summary-line (summary &optional location) + (if location + (format "%s (%s)" summary location) + (format "%s" summary))) + + +(defun gnus-icalendar--format-participant-list (participants) + (mapconcat #'identity participants ", ")) ;; TODO: make the template customizable (defmethod gnus-icalendar-event->org-entry ((event gnus-icalendar-event) reply-status) @@ -387,10 +451,14 @@ Return nil for non-recurring EVENT." ("DT" . ,(gnus-icalendar-event:org-timestamp event)) ("ORGANIZER" . ,(gnus-icalendar-event:organizer event)) ("LOCATION" . ,(gnus-icalendar-event:location event)) + ("PARTICIPATION_REQUIRED" . ,(when (gnus-icalendar-event:participation-required event) "t")) + ("REQ_PARTICIPANTS" . ,(gnus-icalendar--format-participant-list (gnus-icalendar-event:req-participants event))) + ("OPT_PARTICIPANTS" . ,(gnus-icalendar--format-participant-list (gnus-icalendar-event:opt-participants event))) ("RRULE" . ,(gnus-icalendar-event:recur event)) ("REPLY" . ,reply)))) - (insert (format "* %s (%s)\n\n" summary location)) + (insert (format "* %s\n\n" + (gnus-icalendar--format-summary-line summary location))) (mapc (lambda (prop) (org-entry-put (point) (car prop) (cdr prop))) props)) @@ -419,7 +487,7 @@ the optional ORG-FILE argument is specified, only that one file is searched." (let ((uid (gnus-icalendar-event:uid event)) (files (or org-file (org-agenda-files t 'ifmode)))) - (flet + (gmm-labels ((find-event-in (file) (org-check-agenda-file file) (with-current-buffer (find-file-noselect file) @@ -444,7 +512,8 @@ is searched." (let ((file (gnus-icalendar-find-org-event-file event org-file))) (when file (with-current-buffer (find-file-noselect file) - (with-slots (uid summary description organizer location recur) event + (with-slots (uid summary description organizer location recur + participation-required req-participants opt-participants) event (let ((event-pos (org-find-entry-with-id uid))) (when event-pos (goto-char event-pos) @@ -455,7 +524,7 @@ is searched." (headline (delq nil (list (org-entry-get (point) "TODO") (when priority (format "[#%s]" priority)) - (format "%s (%s)" summary location) + (gnus-icalendar--format-summary-line summary location) (org-entry-get (point) "TAGS"))))) (re-search-forward "^\\*+ " (line-end-position)) @@ -486,6 +555,9 @@ is searched." (org-entry-put event-pos "DT" (gnus-icalendar-event:org-timestamp event)) (org-entry-put event-pos "ORGANIZER" organizer) (org-entry-put event-pos "LOCATION" location) + (org-entry-put event-pos "PARTICIPATION_REQUIRED" (when participation-required "t")) + (org-entry-put event-pos "REQ_PARTICIPANTS" (gnus-icalendar--format-participant-list req-participants)) + (org-entry-put event-pos "OPT_PARTICIPANTS" (gnus-icalendar--format-participant-list opt-participants)) (org-entry-put event-pos "RRULE" recur) (when reply-status (org-entry-put event-pos "REPLY" (capitalize (symbol-name reply-status)))) @@ -552,7 +624,7 @@ is searched." (gnus-icalendar--update-org-event event reply-status) (gnus-icalendar:org-event-save event reply-status))) -(defmethod gnus-icalendar-event:sync-to-org ((event gnus-icalendar-event-cancel)) +(defmethod gnus-icalendar-event:sync-to-org ((event gnus-icalendar-event-cancel) reply-status) (when (gnus-icalendar-find-org-event-file event) (gnus-icalendar--cancel-org-event event))) @@ -577,6 +649,22 @@ is searched." :type '(string) :group 'gnus-icalendar) +(defcustom gnus-icalendar-additional-identities nil + "We need to know your identity to make replies to calendar requests work. + +Gnus will only offer you the Accept/Tentative/Decline buttons for +calendar events if any of your identities matches at least one +RSVP participant. + +Your identity is guessed automatically from the variables `user-full-name', +`user-mail-address', and `gnus-ignored-from-addresses'. + +If you need even more aliases you can define them here. It really +only makes sense to define names or email addresses." + + :type '(repeat string) + :group 'gnus-icalendar) + (make-variable-buffer-local (defvar gnus-icalendar-reply-status nil)) @@ -590,22 +678,25 @@ is searched." (apply #'append (mapcar (lambda (x) (if (listp x) x (list x))) (list user-full-name (regexp-quote user-mail-address) - ; NOTE: this one can be a list - gnus-ignored-from-addresses)))) + ; NOTE: these can be lists + gnus-ignored-from-addresses ; already regexp-quoted + (mapcar #'regexp-quote gnus-icalendar-additional-identities))))) ;; TODO: make the template customizable (defmethod gnus-icalendar-event->gnus-calendar ((event gnus-icalendar-event) &optional reply-status) "Format an overview of EVENT details." - (flet ((format-header (x) + (gmm-labels ((format-header (x) (format "%-12s%s" (propertize (concat (car x) ":") 'face 'bold) (cadr x)))) - (with-slots (organizer summary description location recur uid method rsvp) event + (with-slots (organizer summary description location recur uid + method rsvp participation-required) event (let ((headers `(("Summary" ,summary) - ("Location" ,location) + ("Location" ,(or location "")) ("Time" ,(gnus-icalendar-event:org-timestamp event)) ("Organizer" ,organizer) + ("Attendance" ,(if participation-required "Required" "Optional")) ("Method" ,method)))) (when (and (not (gnus-icalendar-event-reply-p event)) rsvp) @@ -673,7 +764,7 @@ is searched." (current-buffer) status gnus-icalendar-identities)))) (when reply - (flet ((fold-icalendar-buffer () + (gmm-labels ((fold-icalendar-buffer () (goto-char (point-min)) (while (re-search-forward "^\\(.\\{72\\}\\)\\(.+\\)$" nil t) (replace-match "\\1\n \\2") @@ -729,13 +820,25 @@ is searched." (when org-entry-exists-p `("Show Org Entry" gnus-icalendar--show-org-event ,event)))))) + +(defmethod gnus-icalendar-event:inline-org-buttons ((event gnus-icalendar-event-cancel)) + (let ((org-entry-exists-p (gnus-icalendar-find-org-event-file event))) + + (delq nil (list + `("Show Agenda" gnus-icalendar-show-org-agenda ,event) + (when org-entry-exists-p + `("Update Org Entry" gnus-icalendar-sync-event-to-org ,event)) + (when org-entry-exists-p + `("Show Org Entry" gnus-icalendar--show-org-event ,event)))))) + + (defun gnus-icalendar-mm-inline (handle) (let ((event (gnus-icalendar-event-from-handle handle gnus-icalendar-identities))) (setq gnus-icalendar-reply-status nil) (when event - (flet ((insert-button-group (buttons) + (gmm-labels ((insert-button-group (buttons) (when buttons (mapc (lambda (x) (apply 'gnus-icalendar-insert-button x) @@ -816,6 +919,8 @@ is searched." (gnus-icalendar-show-org-agenda (with-current-buffer gnus-article-buffer gnus-icalendar-event))) +(defvar gnus-mime-action-alist) ; gnus-art + (defun gnus-icalendar-setup () (add-to-list 'mm-inlined-types "text/calendar") (add-to-list 'mm-automatic-display "text/calendar")