Merge from emacs--devo--0, gnus--rel--5.10
[gnus] / lisp / message.el
index 3aaa8c2..0a38ec0 100644 (file)
@@ -1,27 +1,25 @@
 ;;; message.el --- composing mail and news messages
 
 ;; Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004,
-;;   2005, 2006, 2007 Free Software Foundation, Inc.
+;;   2005, 2006, 2007, 2008 Free Software Foundation, Inc.
 
 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
 ;; Keywords: mail, news
 
 ;; This file is part of GNU Emacs.
 
-;; GNU Emacs is free software; you can redistribute it and/or modify
+;; GNU Emacs 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, or (at your option)
-;; any later version.
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
 
 ;; GNU Emacs 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
+;; 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 GNU Emacs; see the file COPYING.  If not, write to the
-;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
-;; Boston, MA 02110-1301, USA.
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
 
 ;;; Commentary:
 
 
 ;;; Code:
 
+(eval-and-compile
+  (unless (fboundp 'declare-function) (defmacro declare-function (&rest r))))
 (eval-when-compile
   (require 'cl))
-  
+
 (require 'hashcash)
 (require 'canlock)
 (require 'mailheader)
@@ -413,9 +413,17 @@ for `message-cross-post-insert-note'."
 
 ;;; End of variables adopted from `message-utils.el'.
 
-(defcustom message-signature-separator "^-- *$"
-  "Regexp matching the signature separator."
-  :type 'regexp
+(defcustom message-signature-separator "^-- $"
+  "Regexp matching the signature separator.
+This variable is used to strip off the signature from quoted text
+when `message-cite-function' is
+`message-cite-original-without-signature'.  Most useful values
+are \"^-- $\" (strict) and \"^-- *$\" (loose; allow missing
+whitespace)."
+  :type '(choice (const :tag "strict" "^-- $")
+                (const :tag "loose" "^-- *$")
+                regexp)
+  :version "22.3" ;; Gnus 5.10.12 (changed default)
   :link '(custom-manual "(message)Various Message Variables")
   :group 'message-various)
 
@@ -474,7 +482,7 @@ function
 (defcustom message-kill-buffer-query t
   "*Non-nil means that killing a modified message buffer has to be confirmed.
 This is used by `message-kill-buffer'."
-  :version "23.0" ;; No Gnus
+  :version "23.1" ;; No Gnus
   :group 'message-buffers
   :type 'boolean)
 
@@ -559,7 +567,13 @@ Done before generating the new subject of a forward."
   :link '(custom-manual "(message)Forwarding")
   :type 'boolean)
 
-(defcustom message-ignored-resent-headers "^Return-receipt\\|^X-Gnus\\|^Gnus-Warning:\\|^>?From "
+(defcustom message-ignored-resent-headers
+  ;; `Delivered-To' needs to be removed because some mailers use it to
+  ;; detect loops, so if you resend a message to an address that ultimately
+  ;; comes back to you (e.g. a mailing-list to which you subscribe, in which
+  ;; case you may be removed from the list on the grounds that mail to you
+  ;; bounced with a "mailing loop" error).
+  "^Return-receipt\\|^X-Gnus\\|^Gnus-Warning:\\|^>?From \\|^Delivered-To:"
   "*All headers that match this regexp will be deleted when resending a message."
   :group 'message-interface
   :link '(custom-manual "(message)Resending")
@@ -650,7 +664,8 @@ Valid values include `message-send-mail-with-sendmail'
 `message-send-mail-with-mh', `message-send-mail-with-qmail',
 `message-smtpmail-send-it', `smtpmail-send-it',
 `feedmail-send-it' and `message-send-mail-with-mailclient'.  The
-default is system dependent.
+default is system dependent and determined by the function
+`message-send-mail-function'.
 
 See also `send-mail-function'."
   :type '(radio (function-item message-send-mail-with-sendmail)
@@ -659,11 +674,11 @@ See also `send-mail-function'."
                (function-item message-smtpmail-send-it)
                (function-item smtpmail-send-it)
                (function-item feedmail-send-it)
-               (function :tag "Other")
                (function-item message-send-mail-with-mailclient
                               :tag "Use Mailclient package")
                (function :tag "Other"))
   :group 'message-sending
+  :version "23.1" ;; No Gnus
   :initialize 'custom-initialize-default
   :link '(custom-manual "(message)Mail Variables")
   :group 'message-mail)
@@ -806,7 +821,7 @@ If this is nil, use `user-mail-address'.  If it is the symbol
 (defcustom message-sendmail-extra-arguments nil
   "Additional arguments to `sendmail-program'."
   ;; E.g. '("-a" "account") for msmtp
-  :version "23.0" ;; No Gnus
+  :version "23.1" ;; No Gnus
   :type '(repeat string)
   ;; :link '(custom-manual "(message)Mail Variables")
   :group 'message-sending)
@@ -966,7 +981,7 @@ Please also read the note in the documentation of
                 (const :tag "Include date" "On %a, %b %d %Y, %n wrote:")
                 string)
   :link '(custom-manual "(message)Insertion Variables")
-  :version "23.0" ;; No Gnus
+  :version "23.1" ;; No Gnus
   :group 'message-insertion)
 
 (defcustom message-yank-prefix "> "
@@ -1001,7 +1016,7 @@ Used by `message-yank-original' via `message-yank-cite'."
   :link '(custom-manual "(message)Insertion Variables")
   :type 'integer)
 
-(defcustom message-cite-function 'message-cite-original
+(defcustom message-cite-function 'message-cite-original-without-signature
   "*Function for citing an original message.
 Predefined functions include `message-cite-original' and
 `message-cite-original-without-signature'.
@@ -1011,6 +1026,7 @@ Note that these functions use `mail-citation-hook' if that is non-nil."
                (function-item sc-cite-original)
                (function :tag "Other"))
   :link '(custom-manual "(message)Insertion Variables")
+  :version "22.3" ;; Gnus 5.10.12 (changed default)
   :group 'message-insertion)
 
 (defcustom message-indent-citation-function 'message-indent-citation
@@ -1719,6 +1735,7 @@ You must have the \"hashcash\" binary installed, see `hashcash-path'."
          "^ *--+ +begin message +--+ *$\\|"
          "^ *---+ +Original message follows +---+ *$\\|"
          "^ *---+ +Undelivered message follows +---+ *$\\|"
+         "^------ This is a copy of the message, including all the headers. ------ *$\\|"
          "^|? *---+ +Message text follows: +---+ *|?$")
   "A regexp that matches the separator before the text of a failed message.")
 
@@ -1775,33 +1792,32 @@ You must have the \"hashcash\" binary installed, see `hashcash-path'."
   :group 'message-headers
   :type 'regexp)
 
-(eval-and-compile
-  (autoload 'gnus-alive-p "gnus-util")
-  (autoload 'gnus-delay-article "gnus-delay")
-  (autoload 'gnus-extract-address-components "gnus-util")
-  (autoload 'gnus-find-method-for-group "gnus")
-  (autoload 'gnus-group-decoded-name "gnus-group")
-  (autoload 'gnus-group-name-charset "gnus-group")
-  (autoload 'gnus-group-name-decode "gnus-group")
-  (autoload 'gnus-groups-from-server "gnus")
-  (autoload 'gnus-make-local-hook "gnus-util")
-  (autoload 'gnus-open-server "gnus-int")
-  (autoload 'gnus-output-to-mail "gnus-util")
-  (autoload 'gnus-output-to-rmail "gnus-util")
-  (autoload 'gnus-request-post "gnus-int")
-  (autoload 'gnus-select-frame-set-input-focus "gnus-util")
-  (autoload 'gnus-server-string "gnus")
-  (autoload 'idna-to-ascii "idna")
-  (autoload 'message-setup-toolbar "messagexmas")
-  (autoload 'mh-new-draft-name "mh-comp")
-  (autoload 'mh-send-letter "mh-comp")
-  (autoload 'nndraft-request-associate-buffer "nndraft")
-  (autoload 'nndraft-request-expire-articles "nndraft")
-  (autoload 'nnvirtual-find-group-art "nnvirtual")
-  (autoload 'rmail-dont-reply-to "mail-utils")
-  (autoload 'rmail-msg-is-pruned "rmail")
-  (autoload 'rmail-msg-restore-non-pruned-header "rmail")
-  (autoload 'rmail-output "rmailout"))
+(autoload 'gnus-alive-p "gnus-util")
+(autoload 'gnus-delay-article "gnus-delay")
+(autoload 'gnus-extract-address-components "gnus-util")
+(autoload 'gnus-find-method-for-group "gnus")
+(autoload 'gnus-group-decoded-name "gnus-group")
+(autoload 'gnus-group-name-charset "gnus-group")
+(autoload 'gnus-group-name-decode "gnus-group")
+(autoload 'gnus-groups-from-server "gnus")
+(autoload 'gnus-make-local-hook "gnus-util")
+(autoload 'gnus-open-server "gnus-int")
+(autoload 'gnus-output-to-mail "gnus-util")
+(autoload 'gnus-output-to-rmail "gnus-util")
+(autoload 'gnus-request-post "gnus-int")
+(autoload 'gnus-select-frame-set-input-focus "gnus-util")
+(autoload 'gnus-server-string "gnus")
+(autoload 'idna-to-ascii "idna")
+(autoload 'message-setup-toolbar "messagexmas")
+(autoload 'mh-new-draft-name "mh-comp")
+(autoload 'mh-send-letter "mh-comp")
+(autoload 'nndraft-request-associate-buffer "nndraft")
+(autoload 'nndraft-request-expire-articles "nndraft")
+(autoload 'nnvirtual-find-group-art "nnvirtual")
+(autoload 'rmail-dont-reply-to "mail-utils")
+(autoload 'rmail-msg-is-pruned "rmail")
+(autoload 'rmail-msg-restore-non-pruned-header "rmail")
+(autoload 'rmail-output "rmailout")
 
 \f
 
@@ -2469,15 +2485,28 @@ Point is left at the beginning of the narrowed-to region."
     (kill-region start (point))))
 
 
+(autoload 'Info-goto-node "info")
+(defvar mml2015-use)
+
 (defun message-info (&optional arg)
   "Display the Message manual.
 
-Prefixed with one \\[universal-argument], display the Emacs MIME manual.
-Prefixed with two \\[universal-argument]'s, display the PGG manual."
+Prefixed with one \\[universal-argument], display the Emacs MIME
+manual.  With two \\[universal-argument]'s, display the EasyPG or
+PGG manual, depending on the value of `mml2015-use'."
   (interactive "p")
-  (cond ((eq arg 16) (Info-goto-node "(pgg)Top"))
-       ((eq arg  4) (Info-goto-node "(emacs-mime)Top"))
-       (t           (Info-goto-node "(message)Top"))))
+  ;; Why not `info', which is in loaddefs.el?
+  (Info-goto-node (format "(%s)Top"
+                         (cond ((eq arg 16)
+                                (require 'mml2015)
+                                mml2015-use)
+                               ((eq arg  4) 'emacs-mime)
+                               ;; `booleanp' only available in Emacs 22+
+                               ((and (not (memq arg '(nil t)))
+                                     (symbolp arg))
+                                arg)
+                               (t
+                                'message)))))
 
 \f
 
@@ -3696,6 +3725,8 @@ This function uses `mail-citation-hook' if that is non-nil."
 
 (defvar gnus-extract-address-components)
 
+(autoload 'format-spec "format-spec")
+
 (defun message-insert-formatted-citation-line (&optional from date)
   "Function that inserts a formatted citation line.
 
@@ -4028,11 +4059,32 @@ not have PROP."
        (setq start next)))
     (nreverse regions)))
 
-(defcustom message-bogus-address-regexp nil ;; "noreply\\|nospam\\|invalid"
-  "Regexp of potentially bogus mail addresses."
-  :version "23.0" ;; No Gnus
+(defcustom message-bogus-addresses
+  ;; '("noreply" "nospam" "invalid")
+  '("noreply" "nospam" "invalid" "@@" "[^[:ascii:]].*@" "[ \t]")
+  "List of regexps of potentially bogus mail addresses.
+See `message-check-recipients' how to setup checking.
+
+This list should make it possible to catch typos or warn about
+spam-trap addresses.  It doesn't aim to verify strict RFC
+conformance."
+  :version "23.1" ;; No Gnus
   :group 'message-headers
-  :type 'regexp)
+  :type '(choice
+         (const :tag "None" nil)
+         (list
+          (set :inline t
+               (const "noreply")
+               (const "nospam")
+               (const "invalid")
+               (const :tag "duplicate @" "@@")
+               (const :tag "non-ascii local part" "[^[:ascii:]].*@")
+               ;; Already caught by `message-valid-fqdn-regexp'
+               ;; (const :tag "`_' in domain part" "@.*_")
+               (const :tag "whitespace" "[ \t]"))
+          (repeat :inline t
+                  :tag "Other"
+                  (regexp)))))
 
 (defun message-fix-before-sending ()
   "Do various things to make the message nice before sending it."
@@ -4118,7 +4170,7 @@ not have PROP."
          (forward-char)
          (skip-chars-forward mm-7bit-chars)))))
   (message-check 'bogus-recipient
-    ;; Warn before composing or sending a mail to an invalid address.
+    ;; Warn before sending a mail to an invalid address.
     (message-check-recipients)))
 
 (defun message-bogus-recipient-p (recipients)
@@ -4127,9 +4179,9 @@ not have PROP."
 RECIPIENTS is a mail header.  Return a list of potentially bogus
 addresses.  If none is found, return nil.
 
-An addresses might be bogus if the domain part is not fully
-qualified, see `message-valid-fqdn-regexp', or if it matches
-`message-bogus-address-regexp'."
+An address might be bogus if the domain part is not fully
+qualified, see `message-valid-fqdn-regexp', or if there's a
+matching entry in `message-bogus-addresses'."
   ;; FIXME: How about "foo@subdomain", when the MTA adds ".domain.tld"?
   (let (found)
     (mapc (lambda (address)
@@ -4141,9 +4193,15 @@ qualified, see `message-valid-fqdn-regexp', or if it matches
                      (string-match
                       (concat ".@.*\\("
                               message-valid-fqdn-regexp "\\)\\'") address)))
-                   (and (stringp message-bogus-address-regexp)
-                        (string-match message-bogus-address-regexp address)))
-             (push address found)))
+                   (and message-bogus-addresses
+                        (let ((re
+                               (if (listp message-bogus-addresses)
+                                   (mapconcat 'identity
+                                              message-bogus-addresses
+                                              "\\|")
+                                 message-bogus-addresses)))
+                          (string-match re address))))
+                        (push address found)))
          ;;
          (mail-extract-address-components recipients t))
     found))
@@ -4165,6 +4223,8 @@ This function could be useful in `message-setup-hook'."
                        "Address `%s' might be bogus.  Continue? " bog)))
                 (error "Bogus address."))))))))
 
+(custom-add-option 'message-setup-hook 'message-check-recipients)
+
 (defun message-add-action (action &rest types)
   "Add ACTION to be performed when doing an exit of type TYPES."
   (while types
@@ -4483,7 +4543,7 @@ to find out how to use this."
        (apply
         'call-process-region (point-min) (point-max)
         message-qmail-inject-program nil nil nil
-        ;; qmail-inject's default behaviour is to look for addresses on the
+        ;; qmail-inject's default behavior is to look for addresses on the
         ;; command line; if there're none, it scans the headers.
         ;; yes, it does The Right Thing w.r.t. Resent-To and it's kin.
         ;;
@@ -5044,12 +5104,16 @@ Otherwise, generate and save a value for `canlock-password' first."
    ;; Check the length of the signature.
    (message-check 'signature
      (goto-char (point-max))
-     (if (> (count-lines (point) (point-max)) 5)
-        (y-or-n-p
-         (format
-          "Your .sig is %d lines; it should be max 4.  Really post? "
-          (1- (count-lines (point) (point-max)))))
-       t))
+     (if (not (re-search-backward message-signature-separator nil t))
+        t
+       (if (>= (count-lines (1+ (point-at-eol)) (point-max)) 5)
+          (if (message-gnksa-enable-p 'signature)
+              (y-or-n-p
+               (format "Signature is excessively long (%d lines).  Really post? "
+                       (count-lines (1+ (point-at-eol)) (point-max))))
+            (message "Denied posting -- Excessive signature.")
+            nil)
+        t)))
    ;; Ensure that text follows last quoted portion.
    (message-check 'quoting-style
      (goto-char (point-max))
@@ -5311,19 +5375,18 @@ In posting styles use `(\"Expires\" (make-expires-date 30))'."
                   ;; Quote a string containing non-ASCII characters.
                   ;; It will make the RFC2047 encoder cause an error
                   ;; if there are special characters.
-                  (let ((default-enable-multibyte-characters t))
-                    (with-temp-buffer
-                      (insert (car name))
-                      (goto-char (point-min))
-                      (while (search-forward "\"" nil t)
-                        (when (prog2
-                                  (backward-char)
-                                  (zerop (% (skip-chars-backward "\\\\") 2))
-                                (goto-char (match-beginning 0)))
-                          (insert "\\"))
-                        (forward-char))
-                      ;; Those quotes will be removed by the RFC2047 encoder.
-                      (concat "\"" (buffer-string) "\"")))
+                   (mm-with-multibyte-buffer
+                     (insert (car name))
+                     (goto-char (point-min))
+                     (while (search-forward "\"" nil t)
+                       (when (prog2
+                                 (backward-char)
+                                 (zerop (% (skip-chars-backward "\\\\") 2))
+                               (goto-char (match-beginning 0)))
+                         (insert "\\"))
+                       (forward-char))
+                     ;; Those quotes will be removed by the RFC2047 encoder.
+                     (concat "\"" (buffer-string) "\""))
                 (car name))
             (nth 1 name))
           "'s message of \""
@@ -5868,8 +5931,10 @@ they are."
     (with-temp-buffer
       (insert references)
       (goto-char (point-min))
-      ;; Cons a list of valid references.
-      (while (re-search-forward "<[^>]+>" nil t)
+      ;; Cons a list of valid references.  GNKSA says we must not include MIDs
+      ;; with whitespace or missing brackets (7.a "Does not propagate broken
+      ;; Message-IDs in original References").
+      (while (re-search-forward "<[^ <]+@[^ <]+>" nil t)
        (push (match-string 0) refs))
       (setq refs (nreverse refs)
            count (length refs)))
@@ -5945,7 +6010,7 @@ beginning of header value.  Therefore, repeated calls will toggle point
 between beginning of field and beginning of line."
   (interactive "p")
   (let ((zrs 'zmacs-region-stays))
-    (when (and (interactive-p) (boundp zrs))
+    (when (and (featurep 'xemacs) (interactive-p) (boundp zrs))
       (set zrs t)))
   (if (and message-beginning-of-line
           (message-point-in-header-p))
@@ -6193,11 +6258,12 @@ are not included."
   (save-restriction
     (message-narrow-to-headers)
     (run-hooks 'message-header-setup-hook))
-  (set-buffer-modified-p nil)
   (setq buffer-undo-list nil)
   (when message-generate-hashcash
     ;; Generate hashcash headers for recipients already known
     (mail-add-payment-async))
+  ;; Gnus posting styles are applied via buffer-local `message-setup-hook'
+  ;; values.
   (run-hooks 'message-setup-hook)
   ;; Do this last to give it precedence over posting styles, etc.
   (when (message-mail-p)
@@ -6206,6 +6272,8 @@ are not included."
       (if message-alternative-emails
          (message-use-alternative-email-as-from))))
   (message-position-point)
+  ;; Allow correct handling of `message-checksum' in `message-yank-original':
+  (set-buffer-modified-p nil)
   (undo-boundary))
 
 (defun message-set-auto-save-file-name ()
@@ -6299,6 +6367,29 @@ is a function used to switch to and display the mail buffer."
     (message-setup `((Newsgroups . ,(or newsgroups ""))
                     (Subject . ,(or subject ""))))))
 
+(defun message-alter-recipients-discard-bogus-full-name (addrcell)
+  "Discard mail address in full names.
+When the full name in reply headers contains the mail
+address (e.g. \"foo@bar <foo@bar>\"), discard full name.
+ADDRCELL is a cons cell where the car is the mail address and the
+cdr is the complete address (full name and mail address)."
+  (if (string-match (concat (regexp-quote (car addrcell)) ".*"
+                           (regexp-quote (car addrcell)))
+                   (cdr addrcell))
+      (cons (car addrcell) (car addrcell))
+    addrcell))
+
+(defcustom message-alter-recipients-function nil
+  "Function called to allow alteration of reply header structures.
+It is called in `message-get-reply-headers' for each recipient.
+The function is called with one parameter, a cons cell ..."
+  :type '(choice (const :tag "None" nil)
+                (const :tag "Discard bogus full name"
+                       message-alter-recipients-discard-bogus-full-name)
+                function)
+  :version "23.1" ;; No Gnus
+  :group 'message-headers)
+
 (defun message-get-reply-headers (wide &optional to-address address-headers)
   (let (follow-to mct never-mct to cc author mft recipients extra)
   ;; Find all relevant headers we need.
@@ -6399,7 +6490,11 @@ want to get rid of this query permanently.")))
       (setq recipients
            (mapcar
             (lambda (addr)
-              (cons (downcase (mail-strip-quoted-names addr)) addr))
+              (if message-alter-recipients-function
+                  (funcall message-alter-recipients-function
+                           (cons (downcase (mail-strip-quoted-names addr))
+                                 addr))
+                (cons (downcase (mail-strip-quoted-names addr)) addr)))
             (message-tokenize-header recipients)))
       ;; Remove first duplicates.  (Why not all duplicates?  Is this a bug?)
       (let ((s recipients))
@@ -7011,9 +7106,8 @@ Optional DIGEST will use digest to forward."
       (message-forward-make-body-digest-mime forward-buffer)
     (message-forward-make-body-digest-plain forward-buffer)))
 
-(eval-and-compile
-  (autoload 'mm-uu-dissect-text-parts "mm-uu")
-  (autoload 'mm-uu-dissect "mm-uu"))
+(autoload 'mm-uu-dissect-text-parts "mm-uu")
+(autoload 'mm-uu-dissect "mm-uu")
 
 (defun message-signed-or-encrypted-p (&optional dont-emulate-mime handles)
   "Say whether the current buffer contains signed or encrypted message.
@@ -7349,7 +7443,7 @@ Pre-defined symbols include `message-tool-bar-gnome' and
                 (const :tag "Retro look"  message-tool-bar-retro)
                 (repeat :tag "User defined list" gmm-tool-bar-item)
                 (symbol))
-  :version "23.0" ;; No Gnus
+  :version "23.1" ;; No Gnus
   :initialize 'custom-initialize-default
   :set 'message-tool-bar-update
   :group 'message)
@@ -7378,7 +7472,7 @@ Pre-defined symbols include `message-tool-bar-gnome' and
 
 See `gmm-tool-bar-from-list' for details on the format of the list."
   :type '(repeat gmm-tool-bar-item)
-  :version "23.0" ;; No Gnus
+  :version "23.1" ;; No Gnus
   :initialize 'custom-initialize-default
   :set 'message-tool-bar-update
   :group 'message)
@@ -7398,7 +7492,7 @@ See `gmm-tool-bar-from-list' for details on the format of the list."
 
 See `gmm-tool-bar-from-list' for details on the format of the list."
   :type '(repeat gmm-tool-bar-item)
-  :version "23.0" ;; No Gnus
+  :version "23.1" ;; No Gnus
   :initialize 'custom-initialize-default
   :set 'message-tool-bar-update
   :group 'message)
@@ -7411,7 +7505,7 @@ These items are not displayed on the message mode tool bar.
 
 See `gmm-tool-bar-from-list' for the format of the list."
   :type 'gmm-tool-bar-zap-list
-  :version "23.0" ;; No Gnus
+  :version "23.1" ;; No Gnus
   :initialize 'custom-initialize-default
   :set 'message-tool-bar-update
   :group 'message)
@@ -7474,6 +7568,8 @@ If nil, the function bound in `text-mode-map' or `global-map' is executed."
   :type '(choice (const nil)
                 function))
 
+(declare-function mail-abbrev-in-expansion-header-p "mailabbrev" ())
+
 (defun message-tab ()
   "Complete names according to `message-completion-alist'.
 Execute function specified by `message-tab-body-function' when not in
@@ -7797,6 +7893,148 @@ From headers in the original article."
        (delete-region start end)
        (insert match)))))
 
+;; To send pre-formatted letters like the example below, you can use
+;; `message-send-form-letter':
+;; --8<---------------cut here---------------start------------->8---
+;; To: alice@invalid.invalid
+;; Subject: Verification of your contact information
+;; From: Contact verification <admin@foo.invalid>
+;; --text follows this line--
+;; Hi Alice,
+;; please verify that your contact information is still valid:
+;; Alice A, A avenue 11, 1111 A town, Austria
+;; ----------next form letter message follows this line----------
+;; To: bob@invalid.invalid
+;; Subject: Verification of your contact information
+;; From: Contact verification <admin@foo.invalid>
+;; --text follows this line--
+;; Hi Bob,
+;; please verify that your contact information is still valid:
+;; Bob, B street 22, 22222 Be town, Belgium
+;; ----------next form letter message follows this line----------
+;; To: charlie@invalid.invalid
+;; Subject: Verification of your contact information
+;; From: Contact verification <admin@foo.invalid>
+;; --text follows this line--
+;; Hi Charlie,
+;; please verify that your contact information is still valid:
+;; Charlie Chaplin, C plaza 33, 33333 C town, Chile
+;; --8<---------------cut here---------------end--------------->8---
+
+;; FIXME: What is the most common term (circular letter, form letter, serial
+;; letter, standard letter) for such kind of letter?  See also
+;; <http://en.wikipedia.org/wiki/Form_letter>
+
+;; FIXME: Maybe extent message-mode's font-lock support to recognize
+;; `message-form-letter-separator', i.e. highlight each message like a single
+;; message.
+
+(defcustom message-form-letter-separator
+  "\n----------next form letter message follows this line----------\n"
+  "Separator for `message-send-form-letter'."
+  ;; :group 'message-form-letter
+  :group 'message-various
+  :version "23.1" ;; No Gnus
+  :type 'string)
+
+(defcustom message-send-form-letter-delay 1
+  "Delay in seconds when sending a message with `message-send-form-letter'.
+Only used when `message-send-form-letter' is called with non-nil
+argument `force'."
+  ;; :group 'message-form-letter
+  :group 'message-various
+  :version "23.1" ;; No Gnus
+  :type 'integer)
+
+(defun message-send-form-letter (&optional force)
+  "Sent all form letter messages from current buffer.
+Unless FORCE, prompt before sending.
+
+The messages are separated by `message-form-letter-separator'.
+Header and body are separated by `mail-header-separator'."
+  (interactive "P")
+  (let ((sent 0) (skipped 0)
+       start end text
+       buff
+       to done)
+    (goto-char (point-min))
+    (while (not done)
+      (setq start (point)
+           end (if (search-forward message-form-letter-separator nil t)
+                   (- (point) (length message-form-letter-separator) -1)
+                 (setq done t)
+                 (point-max)))
+      (setq text
+           (buffer-substring-no-properties start end))
+      (setq buff (generate-new-buffer "*mail - form letter*"))
+      (with-current-buffer buff
+       (insert text)
+       (message-mode)
+       (setq to (message-fetch-field "To"))
+       (switch-to-buffer buff)
+       (when force
+         (sit-for message-send-form-letter-delay))
+       (if (or force
+                 (y-or-n-p (format "Send message to `%s'? " to)))
+           (progn
+             (setq sent (1+ sent))
+             (message-send-and-exit))
+         (message (format "Message to `%s' skipped." to))
+         (setq skipped (1+ skipped)))
+       (when (buffer-live-p buff)
+         (kill-buffer buff))))
+    (message "%s message(s) sent, %s skipped." sent skipped)))
+
+(defun message-replace-header (header new-value &optional after force)
+  "Remove HEADER and insert the NEW-VALUE.
+If AFTER, insert after this header.  If FORCE, insert new field
+even if NEW-VALUE is empty."
+  ;; Similar to `nnheader-replace-header' but for message buffers.
+  (save-excursion
+    (save-restriction
+      (message-narrow-to-headers)
+      (message-remove-header header))
+    (when (or force (> (length new-value) 0))
+      (if after
+         (message-position-on-field header after)
+       (message-position-on-field header))
+      (insert new-value))))
+
+(defcustom message-recipients-without-full-name
+  (list "ding@gnus.org"
+       "bugs@gnus.org"
+       "emacs-devel@gnu.org"
+       "emacs-pretest-bug@gnu.org"
+       "bug-gnu-emacs@gnu.org")
+  "Mail addresses that have no full name.
+Used in `message-simplify-recipients'."
+  ;; Maybe the addresses could be extracted from
+  ;; `gnus-parameter-to-list-alist'?
+  :type '(choice (const :tag "None" nil)
+                (repeat string))
+  :version "23.1" ;; No Gnus
+  :group 'message-headers)
+
+(defun message-simplify-recipients ()
+  (interactive)
+  (dolist (hdr '("Cc" "To"))
+    (message-replace-header
+     hdr
+     (mapconcat
+      (lambda (addrcomp)
+       (if (and message-recipients-without-full-name
+                (string-match
+                 (regexp-opt message-recipients-without-full-name)
+                 (cadr addrcomp)))
+           (cadr addrcomp)
+         (if (car addrcomp)
+             (message-make-from (car addrcomp) (cadr addrcomp))
+           (cadr addrcomp))))
+      (when (message-fetch-field hdr)
+       (mail-extract-address-components
+        (message-fetch-field hdr) t))
+      ", "))))
+
 (when (featurep 'xemacs)
   (require 'messagexmas)
   (message-xmas-redefine))