@end defvar
@defvar eudc-inline-expansion-format
-This variable lets you control exactly what is inserted into the buffer
-upon an inline expansion request. It is a list whose first element is a
-string passed to @code{format}. Remaining elements are symbols
-corresponding to directory attribute names. The corresponding attribute
-values are passed as additional arguments to @code{format}. Default is
-@code{("%s %s <%s>" firstname name email)}.
+This variable lets you control exactly what is inserted into the
+buffer upon an inline expansion request. It can be set to @code{nil},
+to a function, or to a list. Default is @code{nil}.
+
+When the value is a list, the first element is a string passed to
+@code{format}. Remaining elements are symbols corresponding to
+directory attribute names. The corresponding attribute values are
+passed as additional arguments to @code{format}.
+
+When the value is @code{nil}, the expansion result will be formatted
+according to @url{https://datatracker.ietf.org/doc/html/rfc5322, RFC
+5322}. The @var{phrase} part will be formatted as ``firstname name'',
+quoting the result if necessary. No @var{comment} part will be added
+in this case. This will produce any of the default formats
+@center @var{address}
+@center @var{first} @code{<}@var{address}@code{>}
+@center @var{last} @code{<}@var{address}@code{>
+@center @var{first} @var{last} @code{<}@var{address}@code{>}
+depending on whether a first and/or last name are returned by the
+query, or not.
+
+When the value is a function, the expansion result will be formatted
+according to @url{https://datatracker.ietf.org/doc/html/rfc5322, RFC
+5322}, and the referenced function is called to format the
+@var{phrase}, and @var{comment} parts, respectively. The formatted
+@var{phrase} part will be quoted if necessary. Thus one can produce
+any of the formats:
+@center @var{address}
+@center @var{phrase} @code{<}@var{address}@code{>}
+@center @var{address} @code{(}@var{comment}@code{)}
+@center @var{phrase} @code{<}@var{address}@code{>} @code{(}@var{comment}@code{)}
+
+Email address specifications, as are generated by inline expansion,
+need to comply with RFC 5322 in order to be useful in email
+messages. When an invalid address specification is present in an email
+message header, the message is likely to be rejected by a receiving
+MTA. It is hence recommended to switch old configurations, which use
+a list value, to the new @code{nil}, or function value type since it
+ensures that the inserted address specifications will be in line with
+@url{https://datatracker.ietf.org/doc/html/rfc5322, RFC 5322}. At
+minimum, and to achieve the same semantics as with the old list
+default value, this variable should now be set to @code{nil}:
+@lisp
+(customize-set-variable 'eudc-inline-expansion-format nil)
+@end lisp
+
+A function value can for example be used to get @emph{``last, first
+<address>''} instead of the default @emph{``first last <address>''}:
+@lisp
+(defun my-phrase-last-comma-first (search-res-alist)
+ (let* (phrase
+ (my-attrs (eudc-translate-attribute-list '(firstname name)))
+ (first-name (cdr (assq (nth 0 my-attrs) search-res-alist)))
+ (last-name (cdr (assq (nth 1 my-attrs) search-res-alist)))
+ (comment nil))
+ (setq phrase (concat last-name ", " first-name))
+ (cons phrase comment)))
+
+(customize-set-variable 'eudc-inline-expansion-format
+ #'my-phrase-last-comma-first)
+@end lisp
+To set the @var{comment} part, too, instead of @code{nil} as in this
+example, also provide a string as the @code{cdr} of the @code{cons}
+being returned. Do not include any double quotes in the @var{phrase}
+part, as they are added automatically if needed. Neither include
+parentheses in the @var{comment} part as they, too, are added
+automatically.
@end defvar
@defvar eudc-multiple-match-handling-method
return any. This is useful for example, if one wants to search LDAP
for a name that happens to match a contact in one's BBDB.
++++
+*** New behaviour and default for option 'eudc-inline-expansion-format'
+EUDC inline expansion result formatting defaulted to
+
+ '("%s %s <%s>" firstname name email)
+
+Since email address specifications need to comply with RFC 5322 in
+order to be useful in messages, there was a risk to produce syntax
+which was standard with RFC 822, but is marked as obsolete syntax by
+its successor RFC 5322. Also, the first and last name part was never
+enclosed in double quotes, potentially producing invalid address
+specifications, which may be rejected by a receiving MTA. Thus, this
+variable can now additionally be set to nil (the new default), or a
+function. In both cases, the formatted result will be in compliance
+with RFC 5322. When set to nil, a default format very similar to the
+old default will be produced. When set to a function, that function
+is called, and the returned values are used to populate the phrase and
+comment parts (see RFC 5322 for definitions). In both cases, the
+phrase part will be automatically quoted if necessary.
+
** eww/shr
+++
:type 'boolean
:version "25.1")
-(defcustom eudc-inline-expansion-format '("%s %s <%s>" firstname name email)
- "A list specifying the format of the expansion of inline queries.
-This variable controls what `eudc-expand-inline' actually inserts in
-the buffer. First element is a string passed to `format'. Remaining
-elements are symbols indicating attribute names; the corresponding values
-are passed as additional arguments to `format'."
- :type '(list
- (string :tag "Format String")
- (repeat :inline t
- :tag "Attributes"
- (choice
- :tag "Attribute"
- (const :menu-tag "First Name" :tag "First Name" firstname)
- (const :menu-tag "Surname" :tag "Surname" name)
- (const :menu-tag "Email Address" :tag "Email Address" email)
- (const :menu-tag "Phone" :tag "Phone" phone)
- (symbol :menu-tag "Other")
- (symbol :tag "Attribute name"))))
- :version "25.1")
+(defcustom eudc-inline-expansion-format nil
+ "Specify the format of the expansion of inline queries.
+This variable controls what `eudc-expand-inline' actually inserts
+in the buffer. It is either a list, or a function.
+
+When set to a list, the expansion result will be formatted
+according to the first element of the list, a string, which is
+passed as the first argument to `format'. The remaining elements
+of the list are symbols indicating attribute names; the
+corresponding values are passed as additional arguments to
+`format'.
+
+When set to nil, the expansion result will be formatted using
+`eudc-rfc5322-make-address', and the PHRASE part will be
+formatted according to \"firstname name\", quoting the result if
+necessary. No COMMENT will be added in this case.
+
+When set to a function, the expansion result will be formatted
+using `eudc-rfc5322-make-address', and the referenced function is
+used to format the PHRASE, and COMMENT parts, respectively. It
+receives a single argument, which is an alist of
+protocol-specific attributes describing the recipient. To access
+the alist elements using generic EUDC attribute names, such as
+for example name, or email, use `eudc-translate-attribute-list'.
+The function should return a list, which should contain two
+elements. If the first element is a string, it will be used as
+the PHRASE part, quoting it if necessary. If the second element
+is a string, it will be used as the COMMENT part, unless it
+contains characters not allowed in the COMMENT part by RFC 5322,
+in which case the COMMENT part will be omitted."
+ :type '(choice (const :tag "RFC 5322 formatted \"first last <address>\"" nil)
+ (function :tag "RFC 5322 phrase/comment formatting function")
+ (list :tag "Format string (deprecated)"
+ (string :tag "Format String")
+ (repeat :inline t
+ :tag "Attributes"
+ (choice
+ :tag "Attribute"
+ (const :menu-tag "First Name" :tag "First Name" firstname)
+ (const :menu-tag "Surname" :tag "Surname" name)
+ (const :menu-tag "Email Address" :tag "Email Address" email)
+ (const :menu-tag "Phone" :tag "Phone" phone)
+ (symbol :menu-tag "Other")
+ (symbol :tag "Attribute name")))))
+ :version "29.1")
(defcustom eudc-inline-expansion-servers 'server-then-hotlist
"Which servers to contact for the expansion of inline queries.
newtext)))
(concat rtn-str (substring str start))))
+
+(defconst eudc-rfc5322-atext-token "[:alpha:][:digit:]!#$%&'*+/=?^_`{|}~-"
+ "Printable US-ASCII characters not including specials. Used for atoms.")
+
+(defconst eudc-rfc5322-wsp-token " \t"
+ "Non-folding white space.")
+
+(defconst eudc-rfc5322-fwsp-token
+ (concat eudc-rfc5322-wsp-token "\n")
+ "Folding white space.")
+
+(defconst eudc-rfc5322-cctext-token "\u005D-\u007E\u002A-\u005B\u0021-\u0027"
+ "Printable US-ASCII characters not including '(', ')', or '\\'.")
+
+(defun eudc-rfc5322-quote-phrase (string)
+ "Quote STRING if it needs quoting as a phrase in a header."
+ (if (string-match
+ (concat "[^" eudc-rfc5322-wsp-token eudc-rfc5322-atext-token "]")
+ string)
+ (concat "\"" string "\"")
+ string))
+
+(defun eudc-rfc5322-valid-comment-p (string)
+ "Check if STRING can be used as comment in a header."
+ (if (string-match
+ (concat "[^" eudc-rfc5322-cctext-token eudc-rfc5322-fwsp-token "]")
+ string)
+ nil
+ t))
+
+(defun eudc-rfc5322-make-address (address &optional firstname name comment)
+ "Create a valid address specification according to RFC5322.
+RFC5322 address specifications are used in message header fields
+to indicate senders and recipients of messages. They generally
+have one of the forms:
+
+ADDRESS
+ADDRESS (COMMENT)
+PHRASE <ADDRESS>
+PHRASE <ADDRESS> (COMMENT)
+
+The arguments FIRSTNAME and NAME are combined to form PHRASE.
+PHRASE is enclosed in double quotes if necessary.
+
+COMMENT is omitted if it contains any symbols outside the
+permitted set `eudc-rfc5322-cctext-token'."
+ (if (and address
+ (not (string-blank-p address)))
+ (let ((result address)
+ (name-given (and name
+ (not (string-blank-p name))))
+ (firstname-given (and firstname
+ (not (string-blank-p firstname))))
+ (valid-comment-given (and comment
+ (not (string-blank-p comment))
+ (eudc-rfc5322-valid-comment-p comment))))
+ (if (or name-given firstname-given)
+ (let ((phrase (string-trim (concat firstname " " name))))
+ (setq result
+ (concat
+ (eudc-rfc5322-quote-phrase phrase)
+ " <" result ">"))))
+ (if valid-comment-given
+ (setq result
+ (concat result " (" comment ")")))
+ result)
+ ;; nil or empty address, nothing to return
+ nil))
+
;;}}}
;;{{{ Server and Protocol Variable Routines
((eq eudc-multiple-match-handling-method 'abort)
(error "There is more than one match for the query"))))))
+;;;###autoload
+(defun eudc-format-inline-expansion-result (res query-attrs)
+ "Format a query result according to `eudc-inline-expansion-format'."
+ (cond
+ ;; format string
+ ((consp eudc-inline-expansion-format)
+ (string-trim (apply #'format
+ (car eudc-inline-expansion-format)
+ (mapcar
+ (lambda (field)
+ (or (cdr (assq field res))
+ ""))
+ (eudc-translate-attribute-list
+ (cdr eudc-inline-expansion-format))))))
+
+ ;; formatting function
+ ((functionp eudc-inline-expansion-format)
+ (let ((addr (cdr (assq (nth 2 query-attrs) res)))
+ (ucontent (funcall eudc-inline-expansion-format res)))
+ (if (and ucontent
+ (listp ucontent))
+ (let* ((phrase (car ucontent))
+ (comment (cadr ucontent))
+ (phrase-given
+ (and phrase
+ (stringp phrase)
+ (not (string-blank-p phrase))))
+ (valid-comment-given
+ (and comment
+ (stringp comment)
+ (not (string-blank-p comment))
+ (eudc-rfc5322-valid-comment-p
+ comment))))
+ (eudc-rfc5322-make-address
+ addr nil
+ (if phrase-given phrase nil)
+ (if valid-comment-given comment nil)))
+ (progn
+ (error "Error: the function referenced by \
+`eudc-inline-expansion-format' is expected to return a list.")
+ nil))))
+
+ ;; fallback behaviour (nil function, or non-matching type)
+ (t
+ (let ((fname (cdr (assq (nth 0 query-attrs) res)))
+ (lname (cdr (assq (nth 1 query-attrs) res)))
+ (addr (cdr (assq (nth 2 query-attrs) res))))
+ (eudc-rfc5322-make-address addr fname lname)))))
+
;;;###autoload
(defun eudc-query-with-words (query-words &optional try-all-servers)
"Query the directory server, and return the matching responses.
individual QUERY-WORDS with directory attribute names.
After querying the server for the given string, the expansion
specified by `eudc-inline-expansion-format' is applied to the
-matches before returning them.inserted in the buffer at point.
+matches before returning them.
Multiple servers can be tried with the same query until one finds a match,
see `eudc-inline-expansion-servers'. When TRY-ALL-SERVERS is non-nil,
keep collecting results from subsequent servers after the first match."
(unwind-protect
(cl-flet
((run-query
- (query-formats)
- (let ((response
- (eudc-query
- (eudc-format-query query-words (car query-formats))
- (eudc-translate-attribute-list
- (cdr eudc-inline-expansion-format)))))
- (when response
- ;; Process response through eudc-inline-expansion-format.
- (dolist (r response)
- (let ((response-string
- (apply #'format
- (car eudc-inline-expansion-format)
- (mapcar
- (lambda (field)
- (or (cdr (assq field r))
- ""))
- (eudc-translate-attribute-list
- (cdr eudc-inline-expansion-format))))))
- (if (> (length response-string) 0)
- (push response-string response-strings))))
- (when (not try-all-servers)
- (throw 'found nil))))))
+ (query-formats)
+ (let* ((query-attrs (eudc-translate-attribute-list
+ (if (consp eudc-inline-expansion-format)
+ (cdr eudc-inline-expansion-format)
+ '(firstname name email))))
+ (response
+ (eudc-query
+ (eudc-format-query query-words (car query-formats))
+ query-attrs)))
+ (when response
+ ;; Format response.
+ (dolist (r response)
+ (let ((response-string
+ (eudc-format-inline-expansion-result r query-attrs)))
+ (if response-string
+ (cl-pushnew response-string response-strings
+ :test #'equal))))
+ (when (not try-all-servers)
+ (throw 'found nil))))))
(catch 'found
;; Loop on the servers.
(dolist (server servers)