From: Gnus developers Date: Thu, 1 Aug 2013 22:58:40 +0000 (+0000) Subject: Merge changes made in Gnus master X-Git-Tag: emacs-24.3.90~173^2^2~42^2~45^2~387^2~1688^2~40 X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=89cccc2f3dc32af8e3cb047edb9c2d4df55ca962;p=emacs.git Merge changes made in Gnus master 2013-08-01 Lars Magne Ingebrigtsen * gnus.texi (Basic Usage): Mention that warp means jump here. (The notmuch Engine): Mention notmuch. 2013-08-01 Lars Magne Ingebrigtsen * message.el (message-ignored-news-headers): Delete X-Gnus-Delayed before sending. * dgnushack.el (dgnushack-compile): Add a temporary check for gnus-icalendar. * mm-decode.el (mm-command-output): New face. (mm-display-external): Use it. 2013-08-01 Kan-Ru Chen (陳侃如) (tiny change) * nnmbox.el (nnmbox-request-article): Don't change point. 2013-08-01 Lars Magne Ingebrigtsen * gnus-icalendar.el (gnus-icalendar-event:inline-reply-buttons): Include `handle' parameter. 2013-08-01 Jan Tatarik * gnus-icalendar.el: New file. 2013-08-01 Lars Magne Ingebrigtsen * gnus-int.el (gnus-warp-to-article): Mention that warp means jump. * gnus-uu.el (gnus-uu-mark-thread, gnus-uu-unmark-thread): Work with dummy roots, too. 2013-08-01 David Edmondson * mml2015.el (mml2015-epg-key-image-to-string): Protect against bugging out on ttys. 2013-08-01 Lars Magne Ingebrigtsen * gnus-start.el (gnus-dribble-save): Only save the dribble file if it's not empty. --- diff --git a/doc/misc/ChangeLog b/doc/misc/ChangeLog index 9b45ac06f4c..7f5c70e07e3 100644 --- a/doc/misc/ChangeLog +++ b/doc/misc/ChangeLog @@ -1,3 +1,8 @@ +2013-08-01 Lars Magne Ingebrigtsen + + * gnus.texi (Basic Usage): Mention that warp means jump here. + (The notmuch Engine): Mention notmuch. + 2013-07-30 Tassilo Horn * gnus.texi (Sorting the Summary Buffer): Document new defcustom diff --git a/doc/misc/gnus.texi b/doc/misc/gnus.texi index 808bd2b114b..4edc1d62f1a 100644 --- a/doc/misc/gnus.texi +++ b/doc/misc/gnus.texi @@ -21109,17 +21109,17 @@ the articles that match this query, and takes you to a summary buffer showing these articles. Articles may then be read, moved and deleted using the usual commands. -The @code{nnir} group made in this way is an @code{ephemeral} group, and -some changes are not permanent: aside from reading, moving, and +The @code{nnir} group made in this way is an @code{ephemeral} group, +and some changes are not permanent: aside from reading, moving, and deleting, you can't act on the original article. But there is an -alternative: you can @emph{warp} to the original group for the article -on the current line with @kbd{A W}, aka +alternative: you can @emph{warp} (i.e., jump) to the original group +for the article on the current line with @kbd{A W}, aka @code{gnus-warp-to-article}. Even better, the function -@code{gnus-summary-refer-thread}, bound by default in summary buffers to -@kbd{A T}, will first warp to the original group before it works its -magic and includes all the articles in the thread. From here you can -read, move and delete articles, but also copy them, alter article marks, -whatever. Go nuts. +@code{gnus-summary-refer-thread}, bound by default in summary buffers +to @kbd{A T}, will first warp to the original group before it works +its magic and includes all the articles in the thread. From here you +can read, move and delete articles, but also copy them, alter article +marks, whatever. Go nuts. You say you want to search more than just the group on the current line? No problem: just process-mark the groups you want to search. You want @@ -21161,6 +21161,7 @@ query language anyway. * The swish++ Engine:: Swish++ configuration and usage. * The swish-e Engine:: Swish-e configuration and usage. * The namazu Engine:: Namazu configuration and usage. +* The notmuch Engine:: Notmuch configuration and usage. * The hyrex Engine:: Hyrex configuration and usage. * Customizations:: User customizable settings. @end menu @@ -21390,6 +21391,26 @@ mknmz --mailnews ~/Mail/archive/ ~/Mail/mail/ ~/Mail/lists/ For maximum searching efficiency you might want to have a cron job run this command periodically, say every four hours. + +@node The notmuch Engine +@subsubsection The notmuch Engine + +@table @code +@item nnir-notmuch-program +The name of the notmuch search executable. Defaults to +@samp{notmuch}. + +@item nnir-notmuch-additional-switches +A list of strings, to be given as additional arguments to notmuch. + +@item nnir-notmuch-remove-prefix +The prefix to remove from each file name returned by notmuch in order +to get a group name (albeit with @samp{/} instead of @samp{.}). This +is a regular expression. + +@end table + + @node The hyrex Engine @subsubsection The hyrex Engine This engine is obsolete. diff --git a/lisp/gnus/ChangeLog b/lisp/gnus/ChangeLog index dd00eebe6f3..069935b4406 100644 --- a/lisp/gnus/ChangeLog +++ b/lisp/gnus/ChangeLog @@ -1,5 +1,44 @@ 2013-08-01 Lars Magne Ingebrigtsen + * message.el (message-ignored-news-headers): Delete X-Gnus-Delayed + before sending. + + * dgnushack.el (dgnushack-compile): Add a temporary check for + gnus-icalendar. + + * mm-decode.el (mm-command-output): New face. + (mm-display-external): Use it. + +2013-08-01 Kan-Ru Chen (陳侃如) (tiny change) + + * nnmbox.el (nnmbox-request-article): Don't change point. + +2013-08-01 Lars Magne Ingebrigtsen + + * gnus-icalendar.el (gnus-icalendar-event:inline-reply-buttons): + Include `handle' parameter. + +2013-08-01 Jan Tatarik + + * gnus-icalendar.el: New file. + +2013-08-01 Lars Magne Ingebrigtsen + + * gnus-int.el (gnus-warp-to-article): Mention that warp means jump. + + * gnus-uu.el (gnus-uu-mark-thread, gnus-uu-unmark-thread): Work with + dummy roots, too. + +2013-08-01 David Edmondson + + * mml2015.el (mml2015-epg-key-image-to-string): Protect against bugging + out on ttys. + +2013-08-01 Lars Magne Ingebrigtsen + + * gnus-start.el (gnus-dribble-save): Only save the dribble file if it's + not empty. + * nnrss.el (nnrss-discover-feed): Indent. 2013-08-01 Katsumi Yamaoka diff --git a/lisp/gnus/gnus-icalendar.el b/lisp/gnus/gnus-icalendar.el new file mode 100644 index 00000000000..0286fd5dd89 --- /dev/null +++ b/lisp/gnus/gnus-icalendar.el @@ -0,0 +1,837 @@ +;;; gnus-icalendar.el --- reply to iCalendar meeting requests + +;; Copyright (C) 2013 Free Software Foundation, Inc. + +;; Author: Jan Tatarik +;; Keywords: mail, icalendar, org + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; To install: +;; (require 'gnus-icalendar) +;; (gnus-icalendar-setup) + +;; to enable optional iCalendar->Org sync functionality +;; NOTE: both the capture file and the headline(s) inside must already exist +;; (setq gnus-icalendar-org-capture-file "~/org/notes.org") +;; (setq gnus-icalendar-org-capture-headline '("Calendar")) +;; (gnus-icalendar-org-setup) + + +;;; Code: + +(require 'icalendar) +(require 'eieio) +(require 'mm-decode) +(require 'gnus-sum) + +(eval-when-compile (require 'cl)) + +(defun gnus-icalendar-find-if (pred seq) + (catch 'found + (while seq + (when (funcall pred (car seq)) + (throw 'found (car seq))) + (pop seq)))) + +;;; +;;; ical-event +;;; + +(defclass gnus-icalendar-event () + ((organizer :initarg :organizer + :accessor gnus-icalendar-event:organizer + :initform "" + :type (or null string)) + (summary :initarg :summary + :accessor gnus-icalendar-event:summary + :initform "" + :type (or null string)) + (description :initarg :description + :accessor gnus-icalendar-event:description + :initform "" + :type (or null string)) + (location :initarg :location + :accessor gnus-icalendar-event:location + :initform "" + :type (or null string)) + (start :initarg :start + :accessor gnus-icalendar-event:start + :initform "" + :type (or null string)) + (end :initarg :end + :accessor gnus-icalendar-event:end + :initform "" + :type (or null string)) + (recur :initarg :recur + :accessor gnus-icalendar-event:recur + :initform "" + :type (or null string)) + (uid :initarg :uid + :accessor gnus-icalendar-event:uid + :type string) + (method :initarg :method + :accessor gnus-icalendar-event:method + :initform "PUBLISH" + :type (or null string)) + (rsvp :initarg :rsvp + :accessor gnus-icalendar-event:rsvp + :initform nil + :type (or null boolean))) + "generic iCalendar Event class") + +(defclass gnus-icalendar-event-request (gnus-icalendar-event) + nil + "iCalendar class for REQUEST events") + +(defclass gnus-icalendar-event-cancel (gnus-icalendar-event) + nil + "iCalendar class for CANCEL events") + +(defclass gnus-icalendar-event-reply (gnus-icalendar-event) + nil + "iCalendar class for REPLY events") + +(defmethod gnus-icalendar-event:recurring-p ((event gnus-icalendar-event)) + "Return t if EVENT is recurring." + (not (null (gnus-icalendar-event:recur event)))) + +(defmethod gnus-icalendar-event:recurring-freq ((event gnus-icalendar-event)) + "Return recurring frequency of EVENT." + (let ((rrule (gnus-icalendar-event:recur event))) + (string-match "FREQ=\\([[:alpha:]]+\\)" rrule) + (match-string 1 rrule))) + +(defmethod gnus-icalendar-event:recurring-interval ((event gnus-icalendar-event)) + "Return recurring interval of EVENT." + (let ((rrule (gnus-icalendar-event:recur event)) + (default-interval 1)) + + (string-match "INTERVAL=\\([[:digit:]]+\\)" rrule) + (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: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 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)))) + +(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)) + (attendee-email (att) + (replace-regexp-in-string "^.*MAILTO:" "" (caddr att))) + (attendee-prop-matches-p (prop) + (and (eq (car prop) 'ATTENDEE) + (or (member (attendee-name prop) name-or-email) + (let ((att-email (attendee-email prop))) + (gnus-icalendar-find-if (lambda (email) + (string-match email att-email)) + name-or-email)))))) + + (gnus-icalendar-find-if #'attendee-prop-matches-p event-props)))) + + +(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) ""))) + (prop-map '((summary . SUMMARY) + (description . DESCRIPTION) + (location . LOCATION) + (recur . RRULE) + (uid . UID))) + (method (caddr (assoc 'METHOD (caddr (car (nreverse ical)))))) + (attendee (when attendee-name-or-email + (gnus-icalendar-event--find-attendee ical attendee-name-or-email))) + (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) + :rsvp (string= (plist-get (cadr attendee) 'RSVP) + "TRUE"))) + (event-class (pcase method + ("REQUEST" 'gnus-icalendar-event-request) + ("CANCEL" 'gnus-icalendar-event-cancel) + ("REPLY" 'gnus-icalendar-event-reply) + (_ 'gnus-icalendar-event)))) + + (labels ((map-property (prop) + (let ((value (icalendar--get-event-property event prop))) + (when value + ;; ugly, but cannot get + ;;replace-regexp-in-string work with "\\" as + ;;REP, plus we should also handle "\\;" + (replace-regexp-in-string + "\\\\," "," + (replace-regexp-in-string + "\\\\n" "\n" (substring-no-properties value)))))) + (accumulate-args (mapping) + (destructuring-bind (slot . ical-property) mapping + (setq args (append (list + (intern (concat ":" (symbol-name slot))) + (map-property ical-property)) + args))))) + + (mapc #'accumulate-args prop-map) + (apply 'make-instance event-class args)))) + +(defun gnus-icalendar-event-from-buffer (buf &optional attendee-name-or-email) + "Parse RFC5545 iCalendar in buffer BUF and return an event object. + +Return a gnus-icalendar-event object representing the first event +contained in the invitation. Return nil for calendars without an event entry. + +ATTENDEE-NAME-OR-EMAIL is a list of strings that will be matched +against the event's attendee names and emails. Invitation rsvp +status will be retrieved from the first matching attendee record." + (let ((ical (with-current-buffer (icalendar--get-unfolded-buffer (get-buffer buf)) + (goto-char (point-min)) + (icalendar--read-element nil nil)))) + + (when ical + (gnus-icalendar-event-from-ical ical attendee-name-or-email)))) + +;;; +;;; gnus-icalendar-event-reply +;;; + +(defun gnus-icalendar-event--build-reply-event-body (ical-request status identities) + (let ((summary-status (capitalize (symbol-name status))) + (attendee-status (upcase (symbol-name status))) + reply-event-lines) + (labels ((update-summary (line) + (if (string-match "^[^:]+:" line) + (replace-match (format "\\&%s: " summary-status) t nil line) + line)) + (update-dtstamp () + (format-time-string "DTSTAMP:%Y%m%dT%H%M%SZ" nil t)) + (attendee-matches-identity (line) + (gnus-icalendar-find-if (lambda (name) (string-match-p name line)) + identities)) + (update-attendee-status (line) + (when (and (attendee-matches-identity line) + (string-match "\\(PARTSTAT=\\)[^;]+" line)) + (replace-match (format "\\1%s" attendee-status) t nil line))) + (process-event-line (line) + (when (string-match "^\\([^;:]+\\)" line) + (let* ((key (match-string 0 line)) + ;; NOTE: not all of the below fields are mandatory, + ;; but they are often present in other clients' + ;; replies. Can be helpful for debugging, too. + (new-line (pcase key + ("ATTENDEE" (update-attendee-status line)) + ("SUMMARY" (update-summary line)) + ("DTSTAMP" (update-dtstamp)) + ((or "ORGANIZER" "DTSTART" "DTEND" + "LOCATION" "DURATION" "SEQUENCE" + "RECURRENCE-ID" "UID") line) + (_ nil)))) + (when new-line + (push new-line reply-event-lines)))))) + + (mapc #'process-event-line (split-string ical-request "\n")) + + (unless (gnus-icalendar-find-if (lambda (x) (string-match "^ATTENDEE" x)) + reply-event-lines) + (error "Could not find an event attendee matching given identity")) + + (mapconcat #'identity `("BEGIN:VEVENT" + ,@(nreverse reply-event-lines) + "END:VEVENT") + "\n")))) + +(defun gnus-icalendar-event-reply-from-buffer (buf status identities) + "Build a calendar event reply for request contained in BUF. +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) + (save-excursion + (let ((block-start-re (format "^BEGIN:%s" blockname)) + (block-end-re (format "^END:%s" blockname)) + start) + (when (re-search-forward block-start-re nil t) + (setq start (line-beginning-position)) + (re-search-forward block-end-re) + (buffer-substring-no-properties start (line-end-position))))))) + + (let (zone event) + (with-current-buffer (icalendar--get-unfolded-buffer (get-buffer buf)) + (goto-char (point-min)) + (setq zone (extract-block "VTIMEZONE") + event (extract-block "VEVENT"))) + + (when event + (let ((contents (list "BEGIN:VCALENDAR" + "METHOD:REPLY" + "PRODID:Gnus" + "VERSION:2.0" + zone + (gnus-icalendar-event--build-reply-event-body event status identities) + "END:VCALENDAR"))) + + (mapconcat #'identity (delq nil contents) "\n")))))) + +;;; +;;; gnus-icalendar-org +;;; +;;; TODO: this is an optional feature, and it's only available with org-mode +;;; 7+, so will need to properly handle emacsen with no/outdated org-mode + +(require 'org) +(require 'org-capture) + +(defgroup gnus-icalendar-org nil + "Settings for Calendar Event gnus/org integration." + :group 'gnus-icalendar + :prefix "gnus-icalendar-org-") + +(defcustom gnus-icalendar-org-capture-file nil + "Target Org file for storing captured calendar events." + :type 'file + :group 'gnus-icalendar-org) + +(defcustom gnus-icalendar-org-capture-headline nil + "Target outline in `gnus-icalendar-org-capture-file' for storing captured events." + :type '(repeat string) + :group 'gnus-icalendar-org) + +(defcustom gnus-icalendar-org-template-name "used by gnus-icalendar-org" + "Org-mode template name." + :type '(string) + :group 'gnus-icalendar-org) + +(defcustom gnus-icalendar-org-template-key "#" + "Org-mode template hotkey." + :type '(string) + :group 'gnus-icalendar-org) + +(defvar gnus-icalendar-org-enabled-p nil) + + +(defmethod gnus-icalendar-event:org-repeat ((event gnus-icalendar-event)) + "Return `org-mode' timestamp repeater string for recurring EVENT. +Return nil for non-recurring EVENT." + (when (gnus-icalendar-event:recurring-p event) + (let* ((freq-map '(("HOURLY" . "h") + ("DAILY" . "d") + ("WEEKLY" . "w") + ("MONTHLY" . "m") + ("YEARLY" . "y"))) + (org-freq (cdr (assoc (gnus-icalendar-event:recurring-freq event) freq-map)))) + + (when org-freq + (format "+%s%s" (gnus-icalendar-event:recurring-interval event) org-freq))))) + +(defmethod gnus-icalendar-event:org-timestamp ((event gnus-icalendar-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)) + (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)))) + +;; TODO: make the template customizable +(defmethod gnus-icalendar-event->org-entry ((event gnus-icalendar-event) reply-status) + "Return string with new `org-mode' entry describing EVENT." + (with-temp-buffer + (org-mode) + (with-slots (organizer summary description location + recur uid) event + (let* ((reply (if reply-status (capitalize (symbol-name reply-status)) + "Not replied yet")) + (props `(("ICAL_EVENT" . "t") + ("ID" . ,uid) + ("DT" . ,(gnus-icalendar-event:org-timestamp event)) + ("ORGANIZER" . ,(gnus-icalendar-event:organizer event)) + ("LOCATION" . ,(gnus-icalendar-event:location event)) + ("RRULE" . ,(gnus-icalendar-event:recur event)) + ("REPLY" . ,reply)))) + + (insert (format "* %s (%s)\n\n" summary location)) + (mapc (lambda (prop) + (org-entry-put (point) (car prop) (cdr prop))) + props)) + + (when description + (save-restriction + (narrow-to-region (point) (point)) + (insert description) + (indent-region (point-min) (point-max) 2) + (fill-region (point-min) (point-max)))) + + (buffer-string)))) + +(defun gnus-icalendar--deactivate-org-timestamp (ts) + (replace-regexp-in-string "[<>]" + (lambda (m) (pcase m ("<" "[") (">" "]"))) + ts)) + +(defun gnus-icalendar-find-org-event-file (event &optional org-file) + "Return the name of the file containing EVENT org entry. +Return nil when not found. + +All org agenda files are searched for the EVENT entry. When +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 + ((find-event-in (file) + (org-check-agenda-file file) + (with-current-buffer (find-file-noselect file) + (let ((event-pos (org-find-entry-with-id uid))) + (when (and event-pos + (string= (cdr (assoc "ICAL_EVENT" (org-entry-properties event-pos))) + "t")) + (throw 'found file)))))) + + (gnus-icalendar-find-if #'find-event-in files)))) + + +(defun gnus-icalendar--show-org-event (event &optional org-file) + (let ((file (gnus-icalendar-find-org-event-file event org-file))) + (when file + (switch-to-buffer (find-file file)) + (goto-char (org-find-entry-with-id (gnus-icalendar-event:uid event))) + (org-show-entry)))) + + +(defun gnus-icalendar--update-org-event (event reply-status &optional org-file) + (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 + (let ((event-pos (org-find-entry-with-id uid))) + (when event-pos + (goto-char event-pos) + + ;; update the headline, keep todo, priority and tags, if any + (save-excursion + (let* ((priority (org-entry-get (point) "PRIORITY")) + (headline (delq nil (list + (org-entry-get (point) "TODO") + (when priority (format "[#%s]" priority)) + (format "%s (%s)" summary location) + (org-entry-get (point) "TAGS"))))) + + (re-search-forward "^\\*+ " (line-end-position)) + (delete-region (point) (line-end-position)) + (insert (mapconcat #'identity headline " ")))) + + ;; update props and description + (let ((entry-end (org-entry-end-position)) + (entry-outline-level (org-outline-level))) + + ;; delete body of the entry, leave org drawers intact + (save-restriction + (org-narrow-to-element) + (goto-char entry-end) + (re-search-backward "^[\t ]*:END:") + (forward-line) + (delete-region (point) entry-end)) + + ;; put new event description in the entry body + (when description + (save-restriction + (narrow-to-region (point) (point)) + (insert "\n" (replace-regexp-in-string "[\n]+$" "\n" description) "\n") + (indent-region (point-min) (point-max) (1+ entry-outline-level)) + (fill-region (point-min) (point-max)))) + + ;; update entry properties + (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 "RRULE" recur) + (when reply-status (org-entry-put event-pos "REPLY" + (capitalize (symbol-name reply-status)))) + (save-buffer))))))))) + + +(defun gnus-icalendar--cancel-org-event (event &optional org-file) + (let ((file (gnus-icalendar-find-org-event-file event org-file))) + (when file + (with-current-buffer (find-file-noselect file) + (let ((event-pos (org-find-entry-with-id (gnus-icalendar-event:uid event)))) + (when event-pos + (let ((ts (org-entry-get event-pos "DT"))) + (when ts + (org-entry-put event-pos "DT" (gnus-icalendar--deactivate-org-timestamp ts)) + (save-buffer))))))))) + + +(defun gnus-icalendar--get-org-event-reply-status (event &optional org-file) + (let ((file (gnus-icalendar-find-org-event-file event org-file))) + (when file + (save-excursion + (with-current-buffer (find-file-noselect file) + (let ((event-pos (org-find-entry-with-id (gnus-icalendar-event:uid event)))) + (org-entry-get event-pos "REPLY"))))))) + + +(defun gnus-icalendar-insinuate-org-templates () + (unless (gnus-icalendar-find-if (lambda (x) (string= (cadr x) gnus-icalendar-org-template-name)) + org-capture-templates) + (setq org-capture-templates + (append `((,gnus-icalendar-org-template-key + ,gnus-icalendar-org-template-name + entry + (file+olp ,gnus-icalendar-org-capture-file ,@gnus-icalendar-org-capture-headline) + "%i" + :immediate-finish t)) + org-capture-templates)) + + ;; hide the template from interactive template selection list + ;; (org-capture) + ;; NOTE: doesn't work when capturing from string + ;; (when (boundp 'org-capture-templates-contexts) + ;; (push `(,gnus-icalendar-org-template-key "" ((in-mode . "gnus-article-mode"))) + ;; org-capture-templates-contexts)) + )) + +(defun gnus-icalendar:org-event-save (event reply-status) + (with-temp-buffer + (org-capture-string (gnus-icalendar-event->org-entry event reply-status) + gnus-icalendar-org-template-key))) + +(defun gnus-icalendar-show-org-agenda (event) + (let* ((time-delta (time-subtract (gnus-icalendar-event:end-time event) + (gnus-icalendar-event:start-time event))) + (duration-days (1+ (/ (+ (* (car time-delta) (expt 2 16)) + (cadr time-delta)) + 86400)))) + + (org-agenda-list nil (gnus-icalendar-event:start event) duration-days))) + +(defmethod gnus-icalendar-event:sync-to-org ((event gnus-icalendar-event-request) reply-status) + (if (gnus-icalendar-find-org-event-file event) + (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)) + (when (gnus-icalendar-find-org-event-file event) + (gnus-icalendar--cancel-org-event event))) + +(defun gnus-icalendar-org-setup () + (if (and gnus-icalendar-org-capture-file gnus-icalendar-org-capture-headline) + (progn + (gnus-icalendar-insinuate-org-templates) + (setq gnus-icalendar-org-enabled-p t)) + (message "Cannot enable Calendar->Org: missing capture file, headline"))) + +;;; +;;; gnus-icalendar +;;; + +(defgroup gnus-icalendar nil + "Settings for inline display of iCalendar invitations." + :group 'gnus-article + :prefix "gnus-icalendar-") + +(defcustom gnus-icalendar-reply-bufname "*CAL*" + "Buffer used for building iCalendar invitation reply." + :type '(string) + :group 'gnus-icalendar) + +(make-variable-buffer-local + (defvar gnus-icalendar-reply-status nil)) + +(make-variable-buffer-local + (defvar gnus-icalendar-event nil)) + +(make-variable-buffer-local + (defvar gnus-icalendar-handle nil)) + +(defvar gnus-icalendar-identities + (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)))) + +;; 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) + (format "%-12s%s" + (propertize (concat (car x) ":") 'face 'bold) + (cadr x)))) + + (with-slots (organizer summary description location recur uid method rsvp) event + (let ((headers `(("Summary" ,summary) + ("Location" ,location) + ("Time" ,(gnus-icalendar-event:org-timestamp event)) + ("Organizer" ,organizer) + ("Method" ,method)))) + + (when (and (not (gnus-icalendar-event-reply-p event)) rsvp) + (setq headers (append headers + `(("Status" ,(or reply-status "Not replied yet")))))) + + (concat + (mapconcat #'format-header headers "\n") + "\n\n" + description))))) + +(defmacro gnus-icalendar-with-decoded-handle (handle &rest body) + "Execute BODY in buffer containing the decoded contents of HANDLE." + (let ((charset (make-symbol "charset"))) + `(let ((,charset (cdr (assoc 'charset (mm-handle-type ,handle))))) + (with-temp-buffer + (mm-insert-part ,handle) + (when (string= ,charset "utf-8") + (mm-decode-coding-region (point-min) (point-max) 'utf-8)) + + ,@body)))) + + +(defun gnus-icalendar-event-from-handle (handle &optional attendee-name-or-email) + (gnus-icalendar-with-decoded-handle handle + (gnus-icalendar-event-from-buffer (current-buffer) attendee-name-or-email))) + +(defun gnus-icalendar-insert-button (text callback data) + ;; FIXME: the gnus-mime-button-map keymap does not make sense for this kind + ;; of button. + (let ((start (point))) + (gnus-add-text-properties + start + (progn + (insert "[ " text " ]") + (point)) + `(gnus-callback + ,callback + keymap ,gnus-mime-button-map + face ,gnus-article-button-face + gnus-data ,data)) + (widget-convert-button 'link start (point) + :action 'gnus-widget-press-button + :button-keymap gnus-widget-button-keymap))) + +(defun gnus-icalendar-send-buffer-by-mail (buffer-name subject) + (let ((message-signature nil)) + (with-current-buffer gnus-summary-buffer + (gnus-summary-reply) + (message-goto-body) + (mml-insert-multipart "alternative") + (mml-insert-empty-tag 'part 'type "text/plain") + (mml-attach-buffer buffer-name "text/calendar; method=REPLY; charset=UTF-8") + (message-goto-subject) + (delete-region (line-beginning-position) (line-end-position)) + (insert "Subject: " subject) + (message-send-and-exit)))) + +(defun gnus-icalendar-reply (data) + (let* ((handle (car data)) + (status (cadr data)) + (event (caddr data)) + (reply (gnus-icalendar-with-decoded-handle handle + (gnus-icalendar-event-reply-from-buffer + (current-buffer) status gnus-icalendar-identities)))) + + (when reply + (flet ((fold-icalendar-buffer () + (goto-char (point-min)) + (while (re-search-forward "^\\(.\\{72\\}\\)\\(.+\\)$" nil t) + (replace-match "\\1\n \\2") + (goto-char (line-beginning-position))))) + (let ((subject (concat (capitalize (symbol-name status)) + ": " (gnus-icalendar-event:summary event)))) + + (with-current-buffer (get-buffer-create gnus-icalendar-reply-bufname) + (delete-region (point-min) (point-max)) + (insert reply) + (fold-icalendar-buffer) + (gnus-icalendar-send-buffer-by-mail (buffer-name) subject)) + + ;; Back in article buffer + (setq-local gnus-icalendar-reply-status status) + (when gnus-icalendar-org-enabled-p + (gnus-icalendar--update-org-event event status) + ;; refresh article buffer to update the reply status + (with-current-buffer gnus-summary-buffer + (gnus-summary-show-article)))))))) + +(defun gnus-icalendar-sync-event-to-org (event) + (gnus-icalendar-event:sync-to-org event gnus-icalendar-reply-status)) + +(defmethod gnus-icalendar-event:inline-reply-buttons ((event gnus-icalendar-event) handle) + (when (gnus-icalendar-event:rsvp event) + `(("Accept" gnus-icalendar-reply (,handle accepted ,event)) + ("Tentative" gnus-icalendar-reply (,handle tentative ,event)) + ("Decline" gnus-icalendar-reply (,handle declined ,event))))) + +(defmethod gnus-icalendar-event:inline-reply-buttons ((event gnus-icalendar-event-reply) handle) + "No buttons for REPLY events." + nil) + +(defmethod gnus-icalendar-event:inline-reply-status ((event gnus-icalendar-event)) + (or (when gnus-icalendar-org-enabled-p + (gnus-icalendar--get-org-event-reply-status event)) + "Not replied yet")) + +(defmethod gnus-icalendar-event:inline-reply-status ((event gnus-icalendar-event-reply)) + "No reply status for REPLY events." + nil) + + +(defmethod gnus-icalendar-event:inline-org-buttons ((event gnus-icalendar-event)) + (let* ((org-entry-exists-p (gnus-icalendar-find-org-event-file event)) + (export-button-text (if org-entry-exists-p "Update Org Entry" "Export to Org"))) + + (delq nil (list + `("Show Agenda" gnus-icalendar-show-org-agenda ,event) + (when (gnus-icalendar-event-request-p event) + `(,export-button-text 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) + (when buttons + (mapc (lambda (x) + (apply 'gnus-icalendar-insert-button x) + (insert " ")) + buttons) + (insert "\n\n")))) + + (insert-button-group + (gnus-icalendar-event:inline-reply-buttons event handle)) + + (when gnus-icalendar-org-enabled-p + (insert-button-group (gnus-icalendar-event:inline-org-buttons event))) + + (setq gnus-icalendar-event event + gnus-icalendar-handle handle) + + (insert (gnus-icalendar-event->gnus-calendar + event + (gnus-icalendar-event:inline-reply-status event))))))) + +(defun gnus-icalendar-save-part (handle) + (let (event) + (when (and (equal (car (mm-handle-type handle)) "text/calendar") + (setq event (gnus-icalendar-event-from-handle handle gnus-icalendar-identities))) + + (gnus-icalendar-event:sync-to-org event)))) + + +(defun gnus-icalendar-save-event () + "Save the Calendar event in the text/calendar part under point." + (interactive) + (gnus-article-check-buffer) + (let ((data (get-text-property (point) 'gnus-data))) + (when data + (gnus-icalendar-save-part data)))) + +(defun gnus-icalendar-reply-accept () + "Accept invitation in the current article." + (interactive) + (with-current-buffer gnus-article-buffer + (gnus-icalendar-reply (list gnus-icalendar-handle 'accepted gnus-icalendar-event)) + (setq-local gnus-icalendar-reply-status 'accepted))) + +(defun gnus-icalendar-reply-tentative () + "Send tentative response to invitation in the current article." + (interactive) + (with-current-buffer gnus-article-buffer + (gnus-icalendar-reply (list gnus-icalendar-handle 'tentative gnus-icalendar-event)) + (setq-local gnus-icalendar-reply-status 'tentative))) + +(defun gnus-icalendar-reply-decline () + "Decline invitation in the current article." + (interactive) + (with-current-buffer gnus-article-buffer + (gnus-icalendar-reply (list gnus-icalendar-handle 'declined gnus-icalendar-event)) + (setq-local gnus-icalendar-reply-status 'declined))) + +(defun gnus-icalendar-event-export () + "Export calendar event to `org-mode', or update existing agenda entry." + (interactive) + (with-current-buffer gnus-article-buffer + (gnus-icalendar-sync-event-to-org gnus-icalendar-event)) + ;; refresh article buffer in case the reply had been sent before initial org + ;; export + (with-current-buffer gnus-summary-buffer + (gnus-summary-show-article))) + +(defun gnus-icalendar-event-show () + "Display `org-mode' agenda entry related to the calendar event." + (interactive) + (gnus-icalendar--show-org-event + (with-current-buffer gnus-article-buffer + gnus-icalendar-event))) + +(defun gnus-icalendar-event-check-agenda () + "Display `org-mode' agenda for days between event start and end dates." + (interactive) + (gnus-icalendar-show-org-agenda + (with-current-buffer gnus-article-buffer gnus-icalendar-event))) + +(defun gnus-icalendar-setup () + (add-to-list 'mm-inlined-types "text/calendar") + (add-to-list 'mm-automatic-display "text/calendar") + (add-to-list 'mm-inline-media-tests '("text/calendar" gnus-icalendar-mm-inline identity)) + + (gnus-define-keys (gnus-summary-calendar-map "i" gnus-summary-mode-map) + "a" gnus-icalendar-reply-accept + "t" gnus-icalendar-reply-tentative + "d" gnus-icalendar-reply-decline + "c" gnus-icalendar-event-check-agenda + "e" gnus-icalendar-event-export + "s" gnus-icalendar-event-show) + + (require 'gnus-art) + (add-to-list 'gnus-mime-action-alist + (cons "save calendar event" 'gnus-icalendar-save-event) + t)) + +(provide 'gnus-icalendar) + +;;; gnus-icalendar.el ends here diff --git a/lisp/gnus/gnus-int.el b/lisp/gnus/gnus-int.el index 2378b598eeb..6aa874f0347 100644 --- a/lisp/gnus/gnus-int.el +++ b/lisp/gnus/gnus-int.el @@ -582,8 +582,8 @@ This is the string that Gnus uses to identify the group." (gnus-group-method group))) (defun gnus-warp-to-article () - "Warps from an article in a virtual group to the article in its -real group. Does nothing on a real group." + "Jump from an article in a virtual group to the article in its real group. +Does nothing in a real group." (interactive) (when (gnus-virtual-group-p gnus-newsgroup-name) (let ((gnus-command-method diff --git a/lisp/gnus/gnus-start.el b/lisp/gnus/gnus-start.el index e27fb522b86..9f3f469ad43 100644 --- a/lisp/gnus/gnus-start.el +++ b/lisp/gnus/gnus-start.el @@ -944,7 +944,8 @@ If REGEXP is given, lines that match it will be deleted." (when (and gnus-dribble-buffer (buffer-name gnus-dribble-buffer)) (with-current-buffer gnus-dribble-buffer - (save-buffer)))) + (when (> (buffer-size) 0) + (save-buffer))))) (defun gnus-dribble-clear () (when (gnus-buffer-exists-p gnus-dribble-buffer) diff --git a/lisp/gnus/gnus-uu.el b/lisp/gnus/gnus-uu.el index c50dcde0034..16ed4f17801 100644 --- a/lisp/gnus/gnus-uu.el +++ b/lisp/gnus/gnus-uu.el @@ -640,7 +640,7 @@ When called interactively, prompt for REGEXP." (let ((level (gnus-summary-thread-level))) (while (and (gnus-summary-set-process-mark (gnus-summary-article-number)) - (zerop (gnus-summary-next-subject 1 nil t)) + (zerop (forward-line 1)) (> (gnus-summary-thread-level) level))))) (gnus-summary-position-point)) @@ -650,7 +650,7 @@ When called interactively, prompt for REGEXP." (let ((level (gnus-summary-thread-level))) (while (and (gnus-summary-remove-process-mark (gnus-summary-article-number)) - (zerop (gnus-summary-next-subject 1)) + (zerop (forward-line 1)) (> (gnus-summary-thread-level) level)))) (gnus-summary-position-point)) diff --git a/lisp/gnus/message.el b/lisp/gnus/message.el index b35eb9dca12..d6d6b3f8bed 100644 --- a/lisp/gnus/message.el +++ b/lisp/gnus/message.el @@ -264,7 +264,7 @@ This is a list of regexps and regexp matches." :type 'sexp) (defcustom message-ignored-news-headers - "^NNTP-Posting-Host:\\|^Xref:\\|^[BGF]cc:\\|^Resent-Fcc:\\|^X-Draft-From:\\|^X-Gnus-Agent-Meta-Information:\\|^X-Message-SMTP-Method:" + "^NNTP-Posting-Host:\\|^Xref:\\|^[BGF]cc:\\|^Resent-Fcc:\\|^X-Draft-From:\\|^X-Gnus-Agent-Meta-Information:\\|^X-Message-SMTP-Method:\\|^X-Gnus-Delayed:" "*Regexp of headers to be removed unconditionally before posting." :group 'message-news :group 'message-headers diff --git a/lisp/gnus/mm-decode.el b/lisp/gnus/mm-decode.el index 98be1c5def2..7274708f014 100644 --- a/lisp/gnus/mm-decode.el +++ b/lisp/gnus/mm-decode.el @@ -63,6 +63,18 @@ :group 'news :group 'multimedia) +(defface mm-command-output + '((((class color) + (background dark)) + (:foreground "ForestGreen")) + (((class color) + (background light)) + (:foreground "red3")) + (t + (:italic t))) + "Face used for displaying output from commands." + :group 'mime-display) + ;;; Convenience macros. (defmacro mm-handle-buffer (handle) @@ -983,9 +995,12 @@ external if displayed external." (let ((buffer-read-only nil) (point (point))) (forward-line 2) - (mm-insert-inline - handle (with-current-buffer buffer - (buffer-string))) + (let ((start (point))) + (mm-insert-inline + handle (with-current-buffer buffer + (buffer-string))) + (put-text-property start (point) + 'face 'mm-command-output)) (goto-char point)))) (when (buffer-live-p buffer) (kill-buffer buffer))) diff --git a/lisp/gnus/mml2015.el b/lisp/gnus/mml2015.el index 2c2187a5f8d..3efa5c23bb3 100644 --- a/lisp/gnus/mml2015.el +++ b/lisp/gnus/mml2015.el @@ -885,17 +885,19 @@ If set, it overrides the setting of `mml2015-sign-with-sender'." (defun mml2015-epg-key-image-to-string (key-id) "Return a string with the image of a key, if any" - (let* ((result "") - (key-image (mml2015-epg-key-image key-id))) - (when key-image - (setq result " ") - (put-text-property - 1 2 'display - (gnus-rescale-image key-image - (cons mml2015-maximum-key-image-dimension - mml2015-maximum-key-image-dimension)) - result)) - result)) + (let ((key-image (mml2015-epg-key-image key-id))) + (if (not key-image) + "" + (condition-case error + (let ((result " ")) + (put-text-property + 1 2 'display + (gnus-rescale-image key-image + (cons mml2015-maximum-key-image-dimension + mml2015-maximum-key-image-dimension)) + result) + result) + (error ""))))) (defun mml2015-epg-signature-to-string (signature) (concat (epg-signature-to-string signature) diff --git a/lisp/gnus/nnmbox.el b/lisp/gnus/nnmbox.el index 3228eacdd0a..c605541e7f1 100644 --- a/lisp/gnus/nnmbox.el +++ b/lisp/gnus/nnmbox.el @@ -148,28 +148,29 @@ (deffoo nnmbox-request-article (article &optional newsgroup server buffer) (nnmbox-possibly-change-newsgroup newsgroup server) (with-current-buffer nnmbox-mbox-buffer - (when (nnmbox-find-article article) - (let (start stop) - (re-search-backward (concat "^" message-unix-mail-delimiter) nil t) - (setq start (point)) - (forward-line 1) - (setq stop (if (re-search-forward (concat "^" - message-unix-mail-delimiter) - nil 'move) - (match-beginning 0) - (point))) - (let ((nntp-server-buffer (or buffer nntp-server-buffer))) - (set-buffer nntp-server-buffer) - (erase-buffer) - (insert-buffer-substring nnmbox-mbox-buffer start stop) - (goto-char (point-min)) - (while (looking-at "From ") - (delete-char 5) - (insert "X-From-Line: ") - (forward-line 1)) - (if (numberp article) - (cons nnmbox-current-group article) - (nnmbox-article-group-number nil))))))) + (save-excursion + (when (nnmbox-find-article article) + (let (start stop) + (re-search-backward (concat "^" message-unix-mail-delimiter) nil t) + (setq start (point)) + (forward-line 1) + (setq stop (if (re-search-forward (concat "^" + message-unix-mail-delimiter) + nil 'move) + (match-beginning 0) + (point))) + (let ((nntp-server-buffer (or buffer nntp-server-buffer))) + (set-buffer nntp-server-buffer) + (erase-buffer) + (insert-buffer-substring nnmbox-mbox-buffer start stop) + (goto-char (point-min)) + (while (looking-at "From ") + (delete-char 5) + (insert "X-From-Line: ") + (forward-line 1)) + (if (numberp article) + (cons nnmbox-current-group article) + (nnmbox-article-group-number nil)))))))) (deffoo nnmbox-request-group (group &optional server dont-check info) (nnmbox-possibly-change-newsgroup nil server) @@ -255,14 +256,14 @@ (if (setq is-old (nnmail-expired-article-p newsgroup - (buffer-substring - (point) (progn (end-of-line) (point))) force)) + (buffer-substring (point) (line-end-position)) + force)) (progn (unless (eq nnmail-expiry-target 'delete) (with-temp-buffer (nnmbox-request-article (car articles) - newsgroup server - (current-buffer)) + newsgroup server + (current-buffer)) (let ((nnml-current-directory nil)) (nnmail-expiry-target-group nnmail-expiry-target newsgroup)))