aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Nelson2026-02-21 17:28:58 +0100
committerEli Zaretskii2026-02-21 19:30:25 +0200
commitab4be3cc1ff2fde6b4d9f72d877cc62d6f836d1d (patch)
tree3bdf5f3b2af7e33d4e138e48599629e5ffe06d28
parentf3555fc846e536cd038f47177356a98a4ee06a20 (diff)
downloademacs-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.texi16
-rw-r--r--etc/NEWS6
-rw-r--r--lisp/mail/rmailsum.el170
-rw-r--r--test/lisp/mail/rmailsum-tests.el53
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
1001for a message should include the line count of the message. Setting 1001for a message should include the line count of the message. Setting
1002this option to @code{nil} might speed up the generation of summaries. 1002this 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
1007the options @code{rmail-summary-sender-function} and
1008@code{rmail-summary-recipient-function}. For senders, the possible
1009values include showing the address (the default), or showing the sender
1010name with fallback to the address. For recipients, the possible values
1011include showing addresses from the first line of the @samp{To:} field
1012(the default), showing the first recipient name with fallback to
1013address, or showing all recipient names with fallback to addresses.
1014Both 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
1018those fields.
1019
1004@node Rmail Summary Edit 1020@node Rmail Summary Edit
1005@subsection Editing in Summaries 1021@subsection Editing in Summaries
1006 1022
diff --git a/etc/NEWS b/etc/NEWS
index 6ea88aca195..d05fed3f8d8 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2008,6 +2008,12 @@ to 'imap-open' for 'imap-authenticate' to use, or remove 'plain' from
2008already set. 2008already set.
2009 2009
2010+++ 2010+++
2011*** New user options for formatting Rmail summary lines.
2012'rmail-summary-sender-function' and 'rmail-summary-recipient-function'
2013control 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'.
2012This option specifies an action to take after saving a MIME attachment. 2018This option specifies an action to take after saving a MIME attachment.
2013Predefined values include visiting the file in Emacs, jumping to the 2019Predefined 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.
61Prefer 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.
72Return the first address's real name when it is non-empty; otherwise
73return its email address when that is non-empty. If no address yields
74either, 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.
83Applies `mail-strip-quoted-names' to the first physical line of the
84header 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.
92For each parsed address, use its non-empty real name, falling back to
93its non-empty email address. Skip parsed items with neither, and
94return 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.
111The function is called with the raw contents of the From: field as a
112string, 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.
123The function is called with the raw contents of the To: field as a
124string, and should return a string. When extracting names, fall back to
125email 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.
55This enables you to apply the summary criteria on top one another, 138This 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