* calendar/todo-mode.el: Fix two bugs.
[bpt/emacs.git] / lisp / mail / mail-utils.el
CommitLineData
6594deb0
ER
1;;; mail-utils.el --- utility functions used both by rmail and rnews
2
ba318903 3;; Copyright (C) 1985, 2001-2014 Free Software Foundation, Inc.
9750e079 4
34dc21db 5;; Maintainer: emacs-devel@gnu.org
fd7fa35a 6;; Keywords: mail, news
e5167999 7
a2535589
JA
8;; This file is part of GNU Emacs.
9
b1fc2b50 10;; GNU Emacs is free software: you can redistribute it and/or modify
a2535589 11;; it under the terms of the GNU General Public License as published by
b1fc2b50
GM
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
a2535589
JA
14
15;; GNU Emacs is distributed in the hope that it will be useful,
16;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
b1fc2b50 21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
a2535589 22
e41b2db1
ER
23;;; Commentary:
24
ca10c3d1 25;; Utility functions for mail and netnews handling. These handle fine
e41b2db1
ER
26;; points of header parsing.
27
e5167999 28;;; Code:
a2535589 29
73fa8346 30;;;###autoload
7dbed484
GM
31(defcustom mail-use-rfc822 nil
32 "If non-nil, use a full, hairy RFC822 parser on mail addresses.
73fa8346 33Otherwise, (the default) use a smaller, somewhat faster, and
7aa122f4
SE
34often correct parser."
35 :type 'boolean
36 :group 'mail)
a2535589 37
38a71655
CY
38;;;###autoload
39(defcustom mail-dont-reply-to-names nil
40 "Regexp specifying addresses to prune from a reply message.
41If this is nil, it is set the first time you compose a reply, to
42a value which excludes your own email address.
43
44Matching addresses are excluded from the CC field in replies, and
45also the To field, unless this would leave an empty To field."
46 :type '(choice regexp (const :tag "Your Name" nil))
47 :group 'mail)
48
996792b1
RS
49;; Returns t if file FILE is an Rmail file.
50;;;###autoload
51(defun mail-file-babyl-p (file)
7dbed484
GM
52 "Return non-nil if FILE is a Babyl file."
53 (with-temp-buffer
54 (insert-file-contents file nil 0 100)
55 (looking-at "BABYL OPTIONS:")))
996792b1 56
a2535589
JA
57(defun mail-string-delete (string start end)
58 "Returns a string containing all of STRING except the part
59from START (inclusive) to END (exclusive)."
60 (if (null end) (substring string 0 start)
61 (concat (substring string 0 start)
62 (substring string end nil))))
63
60ba61bb 64;;;###autoload
1d6a4283 65(defun mail-quote-printable (string &optional wrapper)
607e8555
RS
66 "Convert a string to the \"quoted printable\" Q encoding if necessary.
67If the string contains only ASCII characters and no troublesome ones,
68we return it unconverted.
69
1d6a4283 70If the optional argument WRAPPER is non-nil,
444dd0b5 71we add the wrapper characters =?ISO-8859-1?Q?....?=."
1d6a4283
RS
72 (let ((i 0) (result ""))
73 (save-match-data
607e8555
RS
74 (while (or (string-match "[?=\"]" string i)
75 (string-match "[^\000-\177]" string i))
1d6a4283
RS
76 (setq result
77 (concat result (substring string i (match-beginning 0))
78 (upcase (format "=%02x"
79 (aref string (match-beginning 0))))))
80 (setq i (match-end 0)))
81 (if wrapper
444dd0b5 82 (concat "=?ISO-8859-1?Q?"
1d6a4283 83 result (substring string i)
444dd0b5 84 "?=")
1d6a4283
RS
85 (concat result (substring string i))))))
86
367aa646
RS
87;;;###autoload
88(defun mail-quote-printable-region (beg end &optional wrapper)
89 "Convert the region to the \"quoted printable\" Q encoding.
90If the optional argument WRAPPER is non-nil,
91we add the wrapper characters =?ISO-8859-1?Q?....?=."
92 (interactive "r\nP")
93 (save-match-data
94 (save-excursion
95 (goto-char beg)
96 (save-restriction
97 (narrow-to-region beg end)
98 (while (re-search-forward "[?=\"\200-\377]" nil t)
99 (replace-match (upcase (format "=%02x" (preceding-char)))
100 t t))
101 (when wrapper
102 (goto-char beg)
103 (insert "=?ISO-8859-1?Q?")
104 (goto-char end)
105 (insert "?="))))))
106
1d6a4283 107(defun mail-unquote-printable-hexdigit (char)
1c81a393 108 (setq char (upcase char))
1d6a4283
RS
109 (if (>= char ?A)
110 (+ (- char ?A) 10)
111 (- char ?0)))
112
60ba61bb 113;;;###autoload
1d6a4283
RS
114(defun mail-unquote-printable (string &optional wrapper)
115 "Undo the \"quoted printable\" encoding.
116If the optional argument WRAPPER is non-nil,
444dd0b5 117we expect to find and remove the wrapper characters =?ISO-8859-1?Q?....?=."
1d6a4283
RS
118 (save-match-data
119 (and wrapper
444dd0b5 120 (string-match "\\`=\\?ISO-8859-1\\?Q\\?\\([^?]*\\)\\?" string)
1d6a4283 121 (setq string (match-string 1 string)))
60ba61bb
KH
122 (let ((i 0) strings)
123 (while (string-match "=\\(..\\|\n\\)" string i)
124 (setq strings (cons (substring string i (match-beginning 0)) strings))
125 (unless (= (aref string (match-beginning 1)) ?\n)
126 (setq strings
127 (cons (make-string 1
1d6a4283
RS
128 (+ (* 16 (mail-unquote-printable-hexdigit
129 (aref string (match-beginning 1))))
130 (mail-unquote-printable-hexdigit
60ba61bb
KH
131 (aref string (1+ (match-beginning 1))))))
132 strings)))
1d6a4283 133 (setq i (match-end 0)))
60ba61bb
KH
134 (apply 'concat (nreverse (cons (substring string i) strings))))))
135
4d01b827 136;; FIXME Gnus for some reason has `quoted-printable-decode-region' in qp.el.
60ba61bb 137;;;###autoload
926f2004
RS
138(defun mail-unquote-printable-region (beg end &optional wrapper noerror
139 unibyte)
60ba61bb
KH
140 "Undo the \"quoted printable\" encoding in buffer from BEG to END.
141If the optional argument WRAPPER is non-nil,
1c81a393 142we expect to find and remove the wrapper characters =?ISO-8859-1?Q?....?=.
4d01b827
GM
143On encountering malformed quoted-printable text, exits with an error,
144unless NOERROR is non-nil, in which case it continues, and returns nil
145when finished. Returns non-nil on successful completion.
926f2004
RS
146If UNIBYTE is non-nil, insert converted characters as unibyte.
147That is useful if you are going to character code decoding afterward,
148as Rmail does."
c893016b
SM
149 ;; FIXME: `unibyte' should always be non-nil, and the iso-latin-1
150 ;; specific handling should be removed (or moved elsewhere and generalized).
60ba61bb 151 (interactive "r\nP")
1c81a393
RS
152 (let (failed)
153 (save-match-data
154 (save-excursion
155 (save-restriction
156 (narrow-to-region beg end)
157 (goto-char (point-min))
158 (when (and wrapper
159 (looking-at "\\`=\\?ISO-8859-1\\?Q\\?\\([^?]*\\)\\?"))
160 (delete-region (match-end 1) end)
161 (delete-region (point) (match-beginning 1)))
162 (while (re-search-forward "=\\(\\([0-9A-F][0-9A-F]\\)\\|[=\n]\\|..\\)" nil t)
163 (goto-char (match-end 0))
164 (cond ((= (char-after (match-beginning 1)) ?\n)
165 (replace-match ""))
166 ((= (char-after (match-beginning 1)) ?=)
167 (replace-match "="))
168 ((match-beginning 2)
926f2004
RS
169 (let ((char (+ (* 16 (mail-unquote-printable-hexdigit
170 (char-after (match-beginning 2))))
171 (mail-unquote-printable-hexdigit
172 (char-after (1+ (match-beginning 2)))))))
173 (if unibyte
174 (progn
175 (replace-match "")
f45de83b
EZ
176 ;; insert-byte will insert this as a
177 ;; corresponding eight-bit character.
178 (insert-byte char 1))
926f2004 179 (replace-match (make-string 1 char) t t))))
1c81a393
RS
180 (noerror
181 (setq failed t))
182 (t
183 (error "Malformed MIME quoted-printable message"))))
184 (not failed))))))
1d6a4283 185
f440830d 186(autoload 'rfc822-addresses "rfc822")
7fab5ded 187
a2535589
JA
188(defun mail-strip-quoted-names (address)
189 "Delete comments and quoted strings in an address list ADDRESS.
190Also delete leading/trailing whitespace and replace FOO <BAR> with just BAR.
191Return a modified address list."
f440830d 192 (when address
10a4c11f 193 (if mail-use-rfc822
f440830d 194 (mapconcat 'identity (rfc822-addresses address) ", ")
10a4c11f 195 (let (pos)
a2535589 196
b49a2dbf
SM
197 ;; Strip comments.
198 (while (setq pos (string-match
199 "[ \t]*(\\([^()\\]\\|\\\\.\\|\\\\\n\\)*)"
200 address))
201 (setq address (replace-match "" nil nil address 0)))
202
203 ;; strip surrounding whitespace
204 (string-match "\\`[ \t\n]*" address)
205 (setq address (substring address
206 (match-end 0)
207 (string-match "[ \t\n]*\\'" address
208 (match-end 0))))
209
210 ;; strip `quoted' names (This is supposed to hack `"Foo Bar" <bar@host>')
211 (setq pos 0)
212 (while (setq pos (string-match
82d22193 213 "\\([ \t]?\\)\\([ \t]*\"\\([^\"\\]\\|\\\\.\\|\\\\\n\\)*\"[ \t\n]*\\)"
10a4c11f 214 address pos))
b49a2dbf
SM
215 ;; If the next thing is "@", we have "foo bar"@host. Leave it.
216 (if (and (> (length address) (match-end 0))
217 (= (aref address (match-end 0)) ?@))
218 (setq pos (match-end 0))
219 ;; Otherwise discard the "..." part.
220 (setq address (replace-match "" nil nil address 2))))
221 ;; If this address contains <...>, replace it with just
222 ;; the part between the <...>.
223 (while (setq pos (string-match "\\(,\\s-*\\|\\`\\)\\([^,]*<\\([^>,:]*\\)>[^,]*\\)\\(\\s-*,\\|\\'\\)"
224 address))
225 (setq address (replace-match (match-string 3 address)
226 nil 'literal address 2)))
227 address))))
a2535589 228
38a71655 229(defun mail-dont-reply-to (destinations)
bb0974cf 230 "Prune addresses from DESTINATIONS, a list of recipient addresses.
38a71655
CY
231Remove all addresses matching `mail-dont-reply-to-names' from the
232comma-separated list, and return the pruned list."
4d01b827
GM
233 ;; FIXME this (setting a user option the first time a command is used)
234 ;; is somewhat strange. Normally one would never set the option,
235 ;; but instead fall back to the default so long as it was nil.
236 ;; Or just set the default directly in the defcustom.
38a71655
CY
237 (if (null mail-dont-reply-to-names)
238 (setq mail-dont-reply-to-names
239 (concat
240 ;; `rmail-default-dont-reply-to-names' is obsolete.
2dd12e7f 241 (if (bound-and-true-p rmail-default-dont-reply-to-names)
38a71655
CY
242 (concat rmail-default-dont-reply-to-names "\\|")
243 "")
244 (if (and user-mail-address
245 (not (equal user-mail-address user-login-name)))
246 ;; Anchor the login name and email address so that we
247 ;; don't match substrings: if the login name is
248 ;; "foo", we shouldn't match "barfoo@baz.com".
249 (concat "\\`"
250 (regexp-quote user-mail-address)
251 "\\'\\|")
252 "")
253 (concat "\\`" (regexp-quote user-login-name) "@"))))
bb0974cf
PR
254 ;; Split up DESTINATIONS and match each element separately.
255 (let ((start-pos 0) (cur-pos 0)
256 (case-fold-search t))
257 (while start-pos
258 (setq cur-pos (string-match "[,\"]" destinations cur-pos))
259 (if (and cur-pos (equal (match-string 0 destinations) "\""))
260 ;; Search for matching quote.
261 (let ((next-pos (string-match "\"" destinations (1+ cur-pos))))
262 (if next-pos
263 (setq cur-pos (1+ next-pos))
82d22193
RS
264 ;; If the open-quote has no close-quote,
265 ;; delete the open-quote to get something well-defined.
266 ;; This case is not valid, but it can happen if things
267 ;; are weird elsewhere.
bb0974cf
PR
268 (setq destinations (concat (substring destinations 0 cur-pos)
269 (substring destinations (1+ cur-pos))))
270 (setq cur-pos start-pos)))
271 (let* ((address (substring destinations start-pos cur-pos))
272 (naked-address (mail-strip-quoted-names address)))
38a71655 273 (if (string-match mail-dont-reply-to-names naked-address)
bb0974cf 274 (setq destinations (concat (substring destinations 0 start-pos)
a1506d29 275 (and cur-pos (substring destinations
bb0974cf
PR
276 (1+ cur-pos))))
277 cur-pos start-pos)
278 (setq cur-pos (and cur-pos (1+ cur-pos))
279 start-pos cur-pos))))))
280 ;; get rid of any trailing commas
7fab5ded
PR
281 (let ((pos (string-match "[ ,\t\n]*\\'" destinations)))
282 (if pos
283 (setq destinations (substring destinations 0 pos))))
bb0974cf
PR
284 ;; remove leading spaces. they bother me.
285 (if (string-match "\\(\\s \\|,\\)*" destinations)
286 (substring destinations (match-end 0))
287 destinations))
a0b796e3 288
38a71655
CY
289;; Legacy name
290(define-obsolete-function-alias 'rmail-dont-reply-to 'mail-dont-reply-to "24.1")
291
a2535589 292\f
9b9f9c9d 293;;;###autoload
b7bf1cef 294(defun mail-fetch-field (field-name &optional last all list)
da427818 295 "Return the value of the header field whose type is FIELD-NAME.
da427818 296If second arg LAST is non-nil, use the last field of type FIELD-NAME.
b7bf1cef 297If third arg ALL is non-nil, concatenate all such fields with commas between.
78f086e4 298If 4th arg LIST is non-nil, return a list of all such fields.
869692c0
GM
299The buffer should be narrowed to just the header, else false
300matches may be returned from the message body."
a2535589
JA
301 (save-excursion
302 (goto-char (point-min))
303 (let ((case-fold-search t)
304 (name (concat "^" (regexp-quote field-name) "[ \t]*:[ \t]*")))
b7bf1cef
RS
305 (if (or all list)
306 (let ((value (if all "")))
a2535589
JA
307 (while (re-search-forward name nil t)
308 (let ((opoint (point)))
309 (while (progn (forward-line 1)
310 (looking-at "[ \t]")))
6f759698
RS
311 ;; Back up over newline, then trailing spaces or tabs
312 (forward-char -1)
fb83f550 313 (skip-chars-backward " \t" opoint)
b7bf1cef
RS
314 (if list
315 (setq value (cons (buffer-substring-no-properties
316 opoint (point))
317 value))
318 (setq value (concat value
319 (if (string= value "") "" ", ")
320 (buffer-substring-no-properties
321 opoint (point)))))))
322 (if list
323 value
324 (and (not (string= value "")) value)))
a2535589
JA
325 (if (re-search-forward name nil t)
326 (progn
327 (if last (while (re-search-forward name nil t)))
328 (let ((opoint (point)))
329 (while (progn (forward-line 1)
330 (looking-at "[ \t]")))
6f759698
RS
331 ;; Back up over newline, then trailing spaces or tabs
332 (forward-char -1)
fb83f550 333 (skip-chars-backward " \t" opoint)
b94f28ee 334 (buffer-substring-no-properties opoint (point)))))))))
a2535589
JA
335\f
336;; Parse a list of tokens separated by commas.
337;; It runs from point to the end of the visible part of the buffer.
338;; Whitespace before or after tokens is ignored,
339;; but whitespace within tokens is kept.
340(defun mail-parse-comma-list ()
341 (let (accumulated
342 beg)
869fc1d9 343 (skip-chars-forward " \t\n")
a2535589
JA
344 (while (not (eobp))
345 (setq beg (point))
346 (skip-chars-forward "^,")
869fc1d9 347 (skip-chars-backward " \t\n")
a2535589 348 (setq accumulated
698f40ca 349 (cons (buffer-substring-no-properties beg (point))
a2535589
JA
350 accumulated))
351 (skip-chars-forward "^,")
869fc1d9 352 (skip-chars-forward ", \t\n"))
a2535589
JA
353 accumulated))
354
355(defun mail-comma-list-regexp (labels)
356 (let (pos)
357 (setq pos (or (string-match "[^ \t]" labels) 0))
358 ;; Remove leading and trailing whitespace.
359 (setq labels (substring labels pos (string-match "[ \t]*$" labels pos)))
360 ;; Change each comma to \|, and flush surrounding whitespace.
361 (while (setq pos (string-match "[ \t]*,[ \t]*" labels))
362 (setq labels
363 (concat (substring labels 0 pos)
364 "\\|"
365 (substring labels (match-end 0))))))
366 labels)
e6a4a267
RS
367\f
368(defun mail-rfc822-time-zone (time)
369 (let* ((sec (or (car (current-time-zone time)) 0))
370 (absmin (/ (abs sec) 60)))
371 (format "%c%02d%02d" (if (< sec 0) ?- ?+) (/ absmin 60) (% absmin 60))))
372
373(defun mail-rfc822-date ()
374 (let* ((time (current-time))
375 (s (current-time-string time)))
376 (string-match "[^ ]+ +\\([^ ]+\\) +\\([^ ]+\\) \\([^ ]+\\) \\([^ ]+\\)" s)
377 (concat (substring s (match-beginning 2) (match-end 2)) " "
378 (substring s (match-beginning 1) (match-end 1)) " "
379 (substring s (match-beginning 4) (match-end 4)) " "
380 (substring s (match-beginning 3) (match-end 3)) " "
381 (mail-rfc822-time-zone time))))
49116ac0 382
1bfadfb2
GM
383(defun mail-mbox-from ()
384 "Return an mbox \"From \" line for the current message.
385The buffer should be narrowed to just the header."
8de31eec
GM
386 (let* ((from (mail-strip-quoted-names (or (mail-fetch-field "from")
387 (mail-fetch-field "really-from")
388 (mail-fetch-field "sender")
389 (mail-fetch-field "return-path")
390 "unknown")))
391 (date (mail-fetch-field "date"))
392 ;; A From: header can contain multiple addresses, a "From "
393 ;; line must contain only one. (Bug#7760)
394 ;; See eg RFC 5322, 3.6.2. Originator Fields.
395 (end (string-match "[ \t]*[,\n]" from)))
396 (format "From %s %s\n" (if end
397 (substring from 0 end)
398 from)
1bfadfb2
GM
399 (or (and date
400 (ignore-errors
401 (current-time-string (date-to-time date))))
402 (current-time-string)))))
403
49116ac0
JB
404(provide 'mail-utils)
405
6594deb0 406;;; mail-utils.el ends here