diff options
| author | Paul Nelson | 2026-02-21 17:28:58 +0100 |
|---|---|---|
| committer | Eli Zaretskii | 2026-02-21 19:30:25 +0200 |
| commit | ab4be3cc1ff2fde6b4d9f72d877cc62d6f836d1d (patch) | |
| tree | 3bdf5f3b2af7e33d4e138e48599629e5ffe06d28 | |
| parent | f3555fc846e536cd038f47177356a98a4ee06a20 (diff) | |
| download | emacs-ab4be3cc1ff2fde6b4d9f72d877cc62d6f836d1d.tar.gz emacs-ab4be3cc1ff2fde6b4d9f72d877cc62d6f836d1d.zip | |
Make Rmail summary address display customizable
* lisp/mail/rmailsum.el (rmail-summary-sender-function)
(rmail-summary-recipient-function, rmail-summary-address-width):
New user options.
(rmail-summary--address-display, rmail-summary-name-or-address)
(rmail-summary-recipient-strip-quoted-names)
(rmail-summary-recipient-names): New functions.
(rmail-header-summary): Use them when formatting sender and
recipient fields (bug#80406).
* doc/emacs/rmail.texi (Rmail Make Summary): Document them.
* test/lisp/mail/rmailsum-tests.el: New file.
(rmailsum-tests-name-or-address-prefers-name)
(rmailsum-tests-name-or-address-fallback-to-address)
(rmailsum-tests-recipient-strip-quoted-names-first-line)
(rmailsum-tests-recipient-names-folded)
(rmailsum-tests-recipient-names-fallback-to-address): New tests.
| -rw-r--r-- | doc/emacs/rmail.texi | 16 | ||||
| -rw-r--r-- | etc/NEWS | 6 | ||||
| -rw-r--r-- | lisp/mail/rmailsum.el | 170 | ||||
| -rw-r--r-- | test/lisp/mail/rmailsum-tests.el | 53 |
4 files changed, 206 insertions, 39 deletions
diff --git a/doc/emacs/rmail.texi b/doc/emacs/rmail.texi index 987fe3a55d4..d82cd177fa6 100644 --- a/doc/emacs/rmail.texi +++ b/doc/emacs/rmail.texi | |||
| @@ -1001,6 +1001,22 @@ use for the summary window. The variable | |||
| 1001 | for a message should include the line count of the message. Setting | 1001 | for a message should include the line count of the message. Setting |
| 1002 | this option to @code{nil} might speed up the generation of summaries. | 1002 | this option to @code{nil} might speed up the generation of summaries. |
| 1003 | 1003 | ||
| 1004 | @vindex rmail-summary-sender-function | ||
| 1005 | @vindex rmail-summary-recipient-function | ||
| 1006 | Rmail formats the sender and recipient fields in summary lines using | ||
| 1007 | the options @code{rmail-summary-sender-function} and | ||
| 1008 | @code{rmail-summary-recipient-function}. For senders, the possible | ||
| 1009 | values include showing the address (the default), or showing the sender | ||
| 1010 | name with fallback to the address. For recipients, the possible values | ||
| 1011 | include showing addresses from the first line of the @samp{To:} field | ||
| 1012 | (the default), showing the first recipient name with fallback to | ||
| 1013 | address, or showing all recipient names with fallback to addresses. | ||
| 1014 | Both options can also be set to a custom function. | ||
| 1015 | |||
| 1016 | @vindex rmail-summary-address-width | ||
| 1017 | The option @code{rmail-summary-address-width} controls the width of | ||
| 1018 | those fields. | ||
| 1019 | |||
| 1004 | @node Rmail Summary Edit | 1020 | @node Rmail Summary Edit |
| 1005 | @subsection Editing in Summaries | 1021 | @subsection Editing in Summaries |
| 1006 | 1022 | ||
| @@ -2008,6 +2008,12 @@ to 'imap-open' for 'imap-authenticate' to use, or remove 'plain' from | |||
| 2008 | already set. | 2008 | already set. |
| 2009 | 2009 | ||
| 2010 | +++ | 2010 | +++ |
| 2011 | *** New user options for formatting Rmail summary lines. | ||
| 2012 | 'rmail-summary-sender-function' and 'rmail-summary-recipient-function' | ||
| 2013 | control how the sender/recipient fields are displayed in the summary. | ||
| 2014 | 'rmail-summary-address-width' controls the width of that field. | ||
| 2015 | |||
| 2016 | +++ | ||
| 2011 | *** New user option 'rmail-mime-save-action'. | 2017 | *** New user option 'rmail-mime-save-action'. |
| 2012 | This option specifies an action to take after saving a MIME attachment. | 2018 | This option specifies an action to take after saving a MIME attachment. |
| 2013 | Predefined values include visiting the file in Emacs, jumping to the | 2019 | Predefined values include visiting the file in Emacs, jumping to the |
diff --git a/lisp/mail/rmailsum.el b/lisp/mail/rmailsum.el index 79c2d04ac4f..6af9bb4653a 100644 --- a/lisp/mail/rmailsum.el +++ b/lisp/mail/rmailsum.el | |||
| @@ -50,6 +50,89 @@ Setting this option to nil might speed up the generation of summaries." | |||
| 50 | :type 'boolean | 50 | :type 'boolean |
| 51 | :group 'rmail-summary) | 51 | :group 'rmail-summary) |
| 52 | 52 | ||
| 53 | (defcustom rmail-summary-address-width 25 | ||
| 54 | "Width of the sender/recipient field in Rmail summary lines." | ||
| 55 | :type 'natnum | ||
| 56 | :version "31.1" | ||
| 57 | :group 'rmail-summary) | ||
| 58 | |||
| 59 | (defun rmail-summary--address-display (part) | ||
| 60 | "Return display text for parsed address PART, or nil. | ||
| 61 | Prefer a non-empty real name, falling back to a non-empty email address." | ||
| 62 | (let ((name (and (consp part) (cdr part))) | ||
| 63 | (addr (cond ((consp part) (car part)) | ||
| 64 | ((stringp part) part)))) | ||
| 65 | (setq name (and (stringp name) (string-trim name))) | ||
| 66 | (setq addr (and (stringp addr) (string-trim addr))) | ||
| 67 | (cond ((and name (> (length name) 0)) name) | ||
| 68 | ((and addr (> (length addr) 0)) addr)))) | ||
| 69 | |||
| 70 | (defun rmail-summary-name-or-address (address) | ||
| 71 | "Return the first parsed address in ADDRESS as a display string. | ||
| 72 | Return the first address's real name when it is non-empty; otherwise | ||
| 73 | return its email address when that is non-empty. If no address yields | ||
| 74 | either, return unfolded ADDRESS trimmed of surrounding whitespace." | ||
| 75 | (require 'mail-parse) | ||
| 76 | (let* ((unfolded (replace-regexp-in-string "[\r\n]+[ \t]*" " " address)) | ||
| 77 | (first (car (mail-header-parse-addresses unfolded)))) | ||
| 78 | (or (rmail-summary--address-display first) | ||
| 79 | (string-trim unfolded)))) | ||
| 80 | |||
| 81 | (defun rmail-summary-recipient-strip-quoted-names (recipient) | ||
| 82 | "Strip quoted names from the first line of RECIPIENT field. | ||
| 83 | Applies `mail-strip-quoted-names' to the first physical line of the | ||
| 84 | header field value." | ||
| 85 | (when (string-match "\n" recipient) | ||
| 86 | (setq recipient (substring recipient 0 (match-beginning 0)))) | ||
| 87 | (setq recipient (string-trim-right recipient "[ \t]+")) | ||
| 88 | (mail-strip-quoted-names recipient)) | ||
| 89 | |||
| 90 | (defun rmail-summary-recipient-names (recipient) | ||
| 91 | "Return all parsed addresses in RECIPIENT as display strings. | ||
| 92 | For each parsed address, use its non-empty real name, falling back to | ||
| 93 | its non-empty email address. Skip parsed items with neither, and | ||
| 94 | return the selected values joined by \", \"." | ||
| 95 | (if (and (stringp recipient) (> (length recipient) 0)) | ||
| 96 | (progn | ||
| 97 | (require 'mail-parse) | ||
| 98 | (let* ((unfolded (replace-regexp-in-string "[\r\n]+[ \t]*" " " | ||
| 99 | recipient)) | ||
| 100 | (parts (mail-header-parse-addresses unfolded)) | ||
| 101 | (results nil)) | ||
| 102 | (dolist (part parts) | ||
| 103 | (let ((display (rmail-summary--address-display part))) | ||
| 104 | (when display | ||
| 105 | (push display results)))) | ||
| 106 | (mapconcat #'identity (nreverse results) ", "))) | ||
| 107 | "")) | ||
| 108 | |||
| 109 | (defcustom rmail-summary-sender-function #'mail-strip-quoted-names | ||
| 110 | "Function used to format the sender field in Rmail summary lines. | ||
| 111 | The function is called with the raw contents of the From: field as a | ||
| 112 | string, and should return a string." | ||
| 113 | :type '(choice (function-item :tag "Address" | ||
| 114 | mail-strip-quoted-names) | ||
| 115 | (function-item :tag "Name (fallback to address)" | ||
| 116 | rmail-summary-name-or-address) | ||
| 117 | function) | ||
| 118 | :version "31.1" | ||
| 119 | :group 'rmail-summary) | ||
| 120 | |||
| 121 | (defcustom rmail-summary-recipient-function #'rmail-summary-recipient-strip-quoted-names | ||
| 122 | "Function used to format the recipient field in Rmail summary lines. | ||
| 123 | The function is called with the raw contents of the To: field as a | ||
| 124 | string, and should return a string. When extracting names, fall back to | ||
| 125 | email addresses if no name can be extracted." | ||
| 126 | :type '(choice (function-item :tag "Addresses (first line)" | ||
| 127 | rmail-summary-recipient-strip-quoted-names) | ||
| 128 | (function-item :tag "First recipient name (fallback to address)" | ||
| 129 | rmail-summary-name-or-address) | ||
| 130 | (function-item :tag "All recipient names (fallback to addresses)" | ||
| 131 | rmail-summary-recipient-names) | ||
| 132 | function) | ||
| 133 | :version "31.1" | ||
| 134 | :group 'rmail-summary) | ||
| 135 | |||
| 53 | (defcustom rmail-summary-progressively-narrow nil | 136 | (defcustom rmail-summary-progressively-narrow nil |
| 54 | "Non-nil means progressively narrow the set of messages produced by summary. | 137 | "Non-nil means progressively narrow the set of messages produced by summary. |
| 55 | This enables you to apply the summary criteria on top one another, | 138 | This enables you to apply the summary criteria on top one another, |
| @@ -970,26 +1053,39 @@ the message being processed." | |||
| 970 | (t "??????")))) | 1053 | (t "??????")))) |
| 971 | " " | 1054 | " " |
| 972 | (save-excursion | 1055 | (save-excursion |
| 973 | (let* ((from (and (re-search-forward "^From:[ \t]*" nil t) | 1056 | (let* ((from-raw (and (re-search-forward "^From:[ \t]*" nil t) |
| 974 | (mail-strip-quoted-names | 1057 | (buffer-substring |
| 975 | (buffer-substring | 1058 | (1- (point)) |
| 976 | (1- (point)) | 1059 | ;; Get all the lines of the From field |
| 977 | ;; Get all the lines of the From field | 1060 | ;; so that we get a whole comment if there is one, |
| 978 | ;; so that we get a whole comment if there is one, | 1061 | ;; so that mail-strip-quoted-names can discard it. |
| 979 | ;; so that mail-strip-quoted-names can discard it. | 1062 | (progn |
| 980 | (progn | 1063 | (while (progn (forward-line 1) |
| 981 | (while (progn (forward-line 1) | 1064 | (looking-at "[ \t]"))) |
| 982 | (looking-at "[ \t]"))) | 1065 | ;; Back up over newline, then trailing spaces or tabs |
| 983 | ;; Back up over newline, then trailing spaces or tabs | 1066 | (forward-char -1) |
| 984 | (forward-char -1) | 1067 | (skip-chars-backward " \t") |
| 985 | (skip-chars-backward " \t") | 1068 | (point))))) |
| 986 | (point)))))) | 1069 | ;; FROM is used for self-detection and should be |
| 1070 | ;; independent of display customization via | ||
| 1071 | ;; `rmail-summary-sender-function'. | ||
| 1072 | (from (and from-raw | ||
| 1073 | (mail-strip-quoted-names from-raw))) | ||
| 1074 | (from-display (and from-raw | ||
| 1075 | (funcall rmail-summary-sender-function | ||
| 1076 | from-raw))) | ||
| 1077 | (width rmail-summary-address-width) | ||
| 1078 | (after (min 11 width)) | ||
| 1079 | (before (- width after)) | ||
| 987 | len mch lo newline) | 1080 | len mch lo newline) |
| 988 | ;; If there are multiple lines in FROM, | 1081 | ;; If there are multiple lines in FROM or FROM-DISPLAY, |
| 989 | ;; discard up to the last newline in it. | 1082 | ;; discard up to the last newline in them. |
| 990 | (while (and (stringp from) | 1083 | (while (and (stringp from) |
| 991 | (setq newline (string-search "\n" from))) | 1084 | (setq newline (string-search "\n" from))) |
| 992 | (setq from (substring from (1+ newline)))) | 1085 | (setq from (substring from (1+ newline)))) |
| 1086 | (while (and (stringp from-display) | ||
| 1087 | (setq newline (string-search "\n" from-display))) | ||
| 1088 | (setq from-display (substring from-display (1+ newline)))) | ||
| 993 | (if (or (null from) | 1089 | (if (or (null from) |
| 994 | (string-match | 1090 | (string-match |
| 995 | (or rmail-user-mail-address-regexp | 1091 | (or rmail-user-mail-address-regexp |
| @@ -1004,38 +1100,34 @@ the message being processed." | |||
| 1004 | (goto-char (point-min)) | 1100 | (goto-char (point-min)) |
| 1005 | (if (not (re-search-forward "^To:[ \t]*" nil t)) | 1101 | (if (not (re-search-forward "^To:[ \t]*" nil t)) |
| 1006 | nil | 1102 | nil |
| 1007 | (setq from | 1103 | (setq from-display |
| 1008 | (concat "to: " | 1104 | (concat "to: " |
| 1009 | (mail-strip-quoted-names | 1105 | (funcall rmail-summary-recipient-function |
| 1010 | (buffer-substring | 1106 | (mail-fetch-field "To"))))))) |
| 1011 | (point) | 1107 | (if (null from-display) |
| 1012 | (progn (end-of-line) | 1108 | (make-string width ?\s) |
| 1013 | (skip-chars-backward " \t") | 1109 | ;; We are going to return only `rmail-summary-address-width' characters of the |
| 1014 | (point))))))))) | ||
| 1015 | (if (null from) | ||
| 1016 | " " | ||
| 1017 | ;; We are going to return only 25 characters of the | ||
| 1018 | ;; address, so make sure it is RFC2047 decoded before | 1110 | ;; address, so make sure it is RFC2047 decoded before |
| 1019 | ;; taking its substring. This is important when the address is not on the same line as the name, e.g.: | 1111 | ;; taking its substring. This is important when the address is not on the same line as the name, e.g.: |
| 1020 | ;; To: =?UTF-8?Q?=C5=A0t=C4=9Bp=C3=A1n_?= =?UTF-8?Q?N=C4=9Bmec?= | 1112 | ;; To: =?UTF-8?Q?=C5=A0t=C4=9Bp=C3=A1n_?= =?UTF-8?Q?N=C4=9Bmec?= |
| 1021 | ;; <stepnem@gmail.com> | 1113 | ;; <stepnem@gmail.com> |
| 1022 | (setq from (rfc2047-decode-string from)) | 1114 | (setq from-display (rfc2047-decode-string from-display)) |
| 1023 | ;; We cannot tolerate any leftover newlines in From, | 1115 | ;; We cannot tolerate any leftover newlines in From, |
| 1024 | ;; as that disrupts the rmail-summary display. | 1116 | ;; as that disrupts the rmail-summary display. |
| 1025 | ;; Newlines can be left in From if it was malformed, | 1117 | ;; Newlines can be left in From if it was malformed, |
| 1026 | ;; e.g. had unbalanced quotes. | 1118 | ;; e.g. had unbalanced quotes. |
| 1027 | (setq from (replace-regexp-in-string "\n+" " " from)) | 1119 | (setq from-display (replace-regexp-in-string "\n+" " " from-display)) |
| 1028 | (setq len (length from)) | 1120 | (setq len (length from-display)) |
| 1029 | (setq mch (string-match "[@%]" from)) | 1121 | (setq mch (string-match "[@%]" from-display)) |
| 1030 | (format "%25s" | 1122 | (format (format "%%%ds" width) |
| 1031 | (if (or (not mch) (<= len 25)) | 1123 | (if (or (not mch) (<= len width)) |
| 1032 | (substring from (max 0 (- len 25))) | 1124 | (substring from-display (max 0 (- len width))) |
| 1033 | (substring from | 1125 | (substring from-display |
| 1034 | (setq lo (cond ((< (- mch 14) 0) 0) | 1126 | (setq lo (cond ((< (- mch before) 0) 0) |
| 1035 | ((< len (+ mch 11)) | 1127 | ((< len (+ mch after)) |
| 1036 | (- len 25)) | 1128 | (- len width)) |
| 1037 | (t (- mch 14)))) | 1129 | (t (- mch before)))) |
| 1038 | (min len (+ lo 25))))))))) | 1130 | (min len (+ lo width))))))))) |
| 1039 | (concat (if (re-search-forward "^Subject:" nil t) | 1131 | (concat (if (re-search-forward "^Subject:" nil t) |
| 1040 | (let (pos str) | 1132 | (let (pos str) |
| 1041 | (skip-chars-forward " \t") | 1133 | (skip-chars-forward " \t") |
diff --git a/test/lisp/mail/rmailsum-tests.el b/test/lisp/mail/rmailsum-tests.el new file mode 100644 index 00000000000..fe9d672ba66 --- /dev/null +++ b/test/lisp/mail/rmailsum-tests.el | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | ;;; rmailsum-tests.el --- tests for rmailsum.el -*- lexical-binding: t -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2026 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | ;; GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | ;; it under the terms of the GNU General Public License as published by | ||
| 9 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 10 | ;; (at your option) any later version. | ||
| 11 | |||
| 12 | ;; GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | ;; GNU General Public License for more details. | ||
| 16 | |||
| 17 | ;; You should have received a copy of the GNU General Public License | ||
| 18 | ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. | ||
| 19 | |||
| 20 | ;;; Commentary: | ||
| 21 | |||
| 22 | ;;; Code: | ||
| 23 | |||
| 24 | (require 'ert) | ||
| 25 | (require 'rmailsum) | ||
| 26 | |||
| 27 | (ert-deftest rmailsum-tests-name-or-address-prefers-name () | ||
| 28 | (let ((from "Foo Bar <foo@example.test>")) | ||
| 29 | (should (equal (rmail-summary-name-or-address from) | ||
| 30 | "Foo Bar")))) | ||
| 31 | |||
| 32 | (ert-deftest rmailsum-tests-name-or-address-fallback-to-address () | ||
| 33 | (let ((from "<foo@example.test>")) | ||
| 34 | (should (equal (rmail-summary-name-or-address from) | ||
| 35 | "foo@example.test")))) | ||
| 36 | |||
| 37 | (ert-deftest rmailsum-tests-recipient-strip-quoted-names-first-line () | ||
| 38 | (let ((to "Foo Bar <foo@example.test>,\n Baz Quux <baz@example.test>")) | ||
| 39 | (should (equal (rmail-summary-recipient-strip-quoted-names to) | ||
| 40 | "foo@example.test,")))) | ||
| 41 | |||
| 42 | (ert-deftest rmailsum-tests-recipient-names-folded () | ||
| 43 | (let ((to "Foo Bar <foo@example.test>,\n Baz Quux <baz@example.test>")) | ||
| 44 | (should (equal (rmail-summary-recipient-names to) | ||
| 45 | "Foo Bar, Baz Quux")))) | ||
| 46 | |||
| 47 | (ert-deftest rmailsum-tests-recipient-names-fallback-to-address () | ||
| 48 | (let ((to "Foo Bar <foo@example.test>,\n <baz@example.test>")) | ||
| 49 | (should (equal (rmail-summary-recipient-names to) | ||
| 50 | "Foo Bar, baz@example.test")))) | ||
| 51 | |||
| 52 | (provide 'rmailsum-tests) | ||
| 53 | ;;; rmailsum-tests.el ends here | ||