*** empty log message ***
[gnus] / lisp / article.el
1 ;;; article.el --- article treatment functions
2 ;; Copyright (C) 1996 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
5 ;; Keywords: news
6
7 ;; This file is part of GNU Emacs.
8
9 ;; GNU Emacs is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 2, or (at your option)
12 ;; any later version.
13
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ;; GNU General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
21 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
22 ;; Boston, MA 02111-1307, USA.
23
24 ;;; Commentary:
25
26 ;;; Code:
27
28 (require 'nnheader)
29 (require 'gnus-util)
30 (require 'message)
31
32 (defvar gnus-ignored-headers
33    "^Path:\\|^Posting-Version:\\|^Article-I.D.:\\|^Expires:\\|^Date-Received:\\|^References:\\|^Control:\\|^Xref:\\|^Lines:\\|^Posted:\\|^Relay-Version:\\|^Message-ID:\\|^Nf-ID:\\|^Nf-From:\\|^Approved:\\|^Sender:\\|^Received:\\|^Mail-from:"
34   "*All headers that match this regexp will be hidden.
35 This variable can also be a list of regexps of headers to be ignored.
36 If `article-visible-headers' is non-nil, this variable will be ignored.")
37
38 (defvar gnus-visible-headers "^From:\\|^Newsgroups:\\|^Subject:\\|^Date:\\|^Followup-To:\\|^Reply-To:\\|^Organization:\\|^Summary:\\|^Keywords:\\|^To:\\|^Cc:\\|^Posted-To:\\|^Mail-Copies-To:\\|^Apparently-To:\\|^Gnus-Warning:\\|^Resent-"
39   "*All headers that do not match this regexp will be hidden.
40 This variable can also be a list of regexp of headers to remain visible.
41 If this variable is non-nil, `article-ignored-headers' will be ignored.")
42
43 (defvar gnus-sorted-header-list
44   '("^From:" "^Subject:" "^Summary:" "^Keywords:" "^Newsgroups:" "^To:"
45     "^Cc:" "^Date:" "^Organization:")
46   "*This variable is a list of regular expressions.
47 If it is non-nil, headers that match the regular expressions will
48 be placed first in the article buffer in the sequence specified by
49 this list.")
50
51 (defvar gnus-boring-article-headers
52   '(empty followup-to reply-to)
53   "*Headers that are only to be displayed if they have interesting data.
54 Possible values in this list are `empty', `newsgroups', `followup-to',
55 `reply-to', and `date'.")
56
57 (defvar gnus-signature-separator '("^-- $" "^-- *$")
58   "Regexp matching signature separator.
59 This can also be a list of regexps.  In that case, it will be checked
60 from head to tail looking for a separator.  Searches will be done from
61 the end of the buffer.")
62
63 (defvar gnus-signature-limit nil
64   "Provide a limit to what is considered a signature.
65 If it is a number, no signature may not be longer (in characters) than
66 that number.  If it is a floating point number, no signature may be
67 longer (in lines) than that number.  If it is a function, the function
68 will be called without any parameters, and if it returns nil, there is
69 no signature in the buffer.  If it is a string, it will be used as a
70 regexp.  If it matches, the text in question is not a signature.")
71
72 (defvar gnus-hidden-properties '(invisible t intangible t)
73   "Property list to use for hiding text.")
74
75 (defvar gnus-article-x-face-command
76   "{ echo '/* Width=48, Height=48 */'; uncompface; } | icontopbm | xv -quit -"
77   "String or function to be executed to display an X-Face header.
78 If it is a string, the command will be executed in a sub-shell
79 asynchronously.  The compressed face will be piped to this command.")
80
81 (defvar gnus-article-x-face-too-ugly nil
82   "Regexp matching posters whose face shouldn't be shown automatically.")
83
84 (defvar gnus-emphasis-alist
85   '(("_\\(\\w+\\)_" 0 1 'underline)
86     ("\\W\\(/\\(\\w+\\)/\\)\\W" 1 2 'italic)
87     ("\\(_\\*\\|\\*_\\)\\(\\w+\\)\\(_\\*\\|\\*_\\)" 0 2 'bold-underline)
88     ("\\*\\(\\w+\\)\\*" 0 1 'bold))
89   "Alist that says how to fontify certain phrases.
90 Each item looks like this:
91
92   (\"_\\\\([[\\w+\\\\)_\" 0 1 'underline)
93
94 The first element is a regular expression to be matched.  The second
95 is a number that says what regular expression grouping used to find
96 the entire emphasized word.  The third is a number that says what
97 regexp grouping should be displayed and highlighted.  The fourth
98 is the face used for highlighting.")
99
100 (eval-and-compile
101   (autoload 'hexl-hex-string-to-integer "hexl")
102   (autoload 'timezone-make-date-arpa-standard "timezone")
103   (autoload 'mail-extract-address-components "mail-extr"))
104
105 ;;; Internal variables.
106
107 (defvar gnus-inhibit-hiding nil)
108 (defvar gnus-newsgroup-name)
109
110 (defsubst article-hide-text (b e props)
111   "Set text PROPS on the B to E region, extending `intangible' 1 past B."
112   (add-text-properties b e props)
113   (when (memq 'intangible props)
114     (put-text-property 
115      (max (1- b) (point-min))
116      b 'intangible (cddr (memq 'intangible props)))))
117
118 (defsubst article-unhide-text (b e)
119   "Remove hidden text properties from region between B and E."
120   (remove-text-properties b e gnus-hidden-properties)
121   (when (memq 'intangible gnus-hidden-properties)
122     (put-text-property (max (1- b) (point-min))
123                             b 'intangible nil)))
124
125 (defun article-hide-text-type (b e type)
126   "Hide text of TYPE between B and E."
127   (article-hide-text
128    b e (cons 'article-type (cons type gnus-hidden-properties))))
129
130 (defun article-unhide-text-type (b e type)
131   "Hide text of TYPE between B and E."
132   (remove-text-properties
133    b e (cons 'article-type (cons type gnus-hidden-properties)))
134   (when (memq 'intangible gnus-hidden-properties)
135     (put-text-property (max (1- b) (point-min))
136                             b 'intangible nil)))
137
138 (defsubst article-header-rank ()
139   "Give the rank of the string HEADER as given by `article-sorted-header-list'."
140   (let ((list gnus-sorted-header-list)
141         (i 0))
142     (while list
143       (when (looking-at (car list))
144         (setq list nil))
145       (setq list (cdr list))
146       (incf i))
147     i))
148
149 (defun article-hide-headers (&optional arg delete)
150   "Toggle whether to hide unwanted headers and possibly sort them as well.
151 If given a negative prefix, always show; if given a positive prefix,
152 always hide."
153   (interactive (article-hidden-arg))
154   (if (article-check-hidden-text 'headers arg)
155       ;; Show boring headers as well.
156       (article-show-hidden-text 'boring-headers)
157     ;; This function might be inhibited.
158     (unless gnus-inhibit-hiding
159       (save-excursion
160         (save-restriction
161           (let ((buffer-read-only nil)
162                 (props (nconc (list 'article-type 'headers)
163                               gnus-hidden-properties))
164                 (max (1+ (length gnus-sorted-header-list)))
165                 (ignored (when (not (stringp gnus-visible-headers))
166                            (cond ((stringp gnus-ignored-headers)
167                                   gnus-ignored-headers)
168                                  ((listp gnus-ignored-headers)
169                                   (mapconcat 'identity gnus-ignored-headers
170                                              "\\|")))))
171                 (visible
172                  (cond ((stringp gnus-visible-headers)
173                         gnus-visible-headers)
174                        ((and gnus-visible-headers
175                              (listp gnus-visible-headers))
176                         (mapconcat 'identity gnus-visible-headers "\\|"))))
177                 (inhibit-point-motion-hooks t)
178                 want-list beg)
179             ;; First we narrow to just the headers.
180             (widen)
181             (goto-char (point-min))
182             ;; Hide any "From " lines at the beginning of (mail) articles.
183             (while (looking-at "From ")
184               (forward-line 1))
185             (unless (bobp)
186               (if delete
187                   (delete-region (point-min) (point))
188                 (article-hide-text (point-min) (point) props)))
189             ;; Then treat the rest of the header lines.
190             (narrow-to-region
191              (point)
192              (progn (search-forward "\n\n" nil t) (forward-line -1) (point)))
193             ;; Then we use the two regular expressions
194             ;; `gnus-ignored-headers' and `gnus-visible-headers' to
195             ;; select which header lines is to remain visible in the
196             ;; article buffer.
197             (goto-char (point-min))
198             (while (re-search-forward "^[^ \t]*:" nil t)
199               (beginning-of-line)
200               ;; Mark the rank of the header.
201               (put-text-property 
202                (point) (1+ (point)) 'message-rank
203                (if (or (and visible (looking-at visible))
204                        (and ignored
205                             (not (looking-at ignored))))
206                    (article-header-rank) 
207                  (+ 2 max)))
208               (forward-line 1))
209             (message-sort-headers-1)
210             (when (setq beg (text-property-any 
211                              (point-min) (point-max) 'message-rank (+ 2 max)))
212               ;; We make the unwanted headers invisible.
213               (if delete
214                   (delete-region beg (point-max))
215                 ;; Suggested by Sudish Joseph <joseph@cis.ohio-state.edu>.
216                 (article-hide-text-type beg (point-max) 'headers))
217               ;; Work around XEmacs lossage.
218               (put-text-property (point-min) beg 'invisible nil))))))))
219
220 (defun article-hide-boring-headers (&optional arg)
221   "Toggle hiding of headers that aren't very interesting.
222 If given a negative prefix, always show; if given a positive prefix,
223 always hide."
224   (interactive (article-hidden-arg))
225   (unless (article-check-hidden-text 'boring-headers arg)
226     (save-excursion
227       (save-restriction
228         (let ((buffer-read-only nil)
229               (list gnus-boring-article-headers)
230               (inhibit-point-motion-hooks t)
231               elem)
232           (nnheader-narrow-to-headers)
233           (while list
234             (setq elem (pop list))
235             (goto-char (point-min))
236             (cond
237              ;; Hide empty headers.
238              ((eq elem 'empty)
239               (while (re-search-forward "^[^:]+:[ \t]*\n[^ \t]" nil t)
240                 (forward-line -1)
241                 (article-hide-text-type
242                  (progn (beginning-of-line) (point))
243                  (progn 
244                    (end-of-line)
245                    (if (re-search-forward "^[^ \t]" nil t)
246                        (match-beginning 0)
247                      (point-max)))
248                  'boring-headers)))
249              ;; Hide boring Newsgroups header.
250              ((eq elem 'newsgroups)
251               (when (equal (gnus-fetch-field "newsgroups")
252                            (gnus-group-real-name
253                             (if (boundp 'gnus-newsgroup-name)
254                                 gnus-newsgroup-name
255                               "")))
256                 (article-hide-header "newsgroups")))
257              ((eq elem 'followup-to)
258               (when (equal (message-fetch-field "followup-to")
259                            (message-fetch-field "newsgroups"))
260                 (article-hide-header "followup-to")))
261              ((eq elem 'reply-to)
262               (let ((from (message-fetch-field "from"))
263                     (reply-to (message-fetch-field "reply-to")))
264                 (when (and
265                        from reply-to
266                        (equal 
267                         (nth 1 (mail-extract-address-components from))
268                         (nth 1 (mail-extract-address-components reply-to))))
269                   (article-hide-header "reply-to"))))
270              ((eq elem 'date)
271               (let ((date (message-fetch-field "date")))
272                 (when (and date
273                            (< (gnus-days-between date (current-time-string))
274                               4))
275                   (article-hide-header "date")))))))))))
276
277 (defun article-hide-header (header)
278   (save-excursion
279     (goto-char (point-min))
280     (when (re-search-forward (concat "^" header ":") nil t)
281       (article-hide-text-type
282        (progn (beginning-of-line) (point))
283        (progn 
284          (end-of-line)
285          (if (re-search-forward "^[^ \t]" nil t)
286              (match-beginning 0)
287            (point-max)))
288        'boring-headers))))
289
290 ;; Written by Per Abrahamsen <amanda@iesd.auc.dk>.
291 (defun article-treat-overstrike ()
292   "Translate overstrikes into bold text."
293   (interactive)
294   (save-excursion
295     (let ((buffer-read-only nil))
296       (while (search-forward "\b" nil t)
297         (let ((next (following-char))
298               (previous (char-after (- (point) 2))))
299           ;; We do the boldification/underlining by hiding the
300           ;; overstrikes and putting the proper text property
301           ;; on the letters.
302           (cond 
303            ((eq next previous)
304             (article-hide-text-type (- (point) 2) (point) 'overstrike)
305             (put-text-property (point) (1+ (point)) 'face 'bold))
306            ((eq next ?_)
307             (article-hide-text-type (1- (point)) (1+ (point)) 'overstrike)
308             (put-text-property
309              (- (point) 2) (1- (point)) 'face 'underline))
310            ((eq previous ?_)
311             (article-hide-text-type (- (point) 2) (point) 'overstrike)
312             (put-text-property
313              (point) (1+ (point)) 'face 'underline))))))))
314
315 (defun article-fill ()
316   "Format too long lines."
317   (interactive)
318   (save-excursion
319     (let ((buffer-read-only nil))
320       (widen)
321       (goto-char (point-min))
322       (search-forward "\n\n" nil t)
323       (end-of-line 1)
324       (let ((paragraph-start "^[>|#:<;* ]*[ \t]*$")
325             (adaptive-fill-regexp "[ \t]*\\([|#:<;>*]+ *\\)?")
326             (adaptive-fill-mode t))
327         (while (not (eobp))
328           (and (>= (current-column) (min fill-column (window-width)))
329                (/= (preceding-char) ?:)
330                (fill-paragraph nil))
331           (end-of-line 2))))))
332
333 (defun article-remove-cr ()
334   "Remove carriage returns from an article."
335   (interactive)
336   (save-excursion
337     (let ((buffer-read-only nil))
338       (goto-char (point-min))
339       (while (search-forward "\r" nil t)
340         (replace-match "" t t)))))
341
342 (defun article-remove-trailing-blank-lines ()
343   "Remove all trailing blank lines from the article."
344   (interactive)
345   (save-excursion
346     (let ((buffer-read-only nil))
347       (goto-char (point-max))
348       (delete-region
349        (point)
350        (progn
351          (while (looking-at "^[ \t]*$")
352            (forward-line -1))
353          (forward-line 1)
354          (point))))))
355
356 (defun article-display-x-face (&optional force)
357   "Look for an X-Face header and display it if present."
358   (interactive (list 'force))
359   (save-excursion
360     ;; Delete the old process, if any.
361     (when (process-status "article-x-face")
362       (delete-process "article-x-face"))
363     (let ((inhibit-point-motion-hooks t)
364           (case-fold-search nil)
365           from)
366       (save-restriction
367         (nnheader-narrow-to-headers)
368         (setq from (message-fetch-field "from"))
369         (goto-char (point-min))
370         (when (and gnus-article-x-face-command
371                    (or force
372                        ;; Check whether this face is censored.
373                        (not gnus-article-x-face-too-ugly)
374                        (and gnus-article-x-face-too-ugly from
375                             (not (string-match gnus-article-x-face-too-ugly
376                                                from))))
377                    ;; Has to be present.
378                    (re-search-forward "^X-Face: " nil t))
379           ;; We now have the area of the buffer where the X-Face is stored.
380           (let ((beg (point))
381                 (end (1- (re-search-forward "^\\($\\|[^ \t]\\)" nil t))))
382             ;; We display the face.
383             (if (symbolp gnus-article-x-face-command)
384                 ;; The command is a lisp function, so we call it.
385                 (if (gnus-functionp gnus-article-x-face-command)
386                     (funcall gnus-article-x-face-command beg end)
387                   (error "%s is not a function" gnus-article-x-face-command))
388               ;; The command is a string, so we interpret the command
389               ;; as a, well, command, and fork it off.
390               (let ((process-connection-type nil))
391                 (process-kill-without-query
392                  (start-process
393                   "article-x-face" nil shell-file-name shell-command-switch
394                   gnus-article-x-face-command))
395                 (process-send-region "article-x-face" beg end)
396                 (process-send-eof "article-x-face")))))))))
397
398 (defun article-decode-rfc1522 ()
399   "Hack to remove QP encoding from headers."
400   (let ((case-fold-search t)
401         (inhibit-point-motion-hooks t)
402         (buffer-read-only nil)
403         string)
404     (save-restriction
405       (narrow-to-region
406        (goto-char (point-min))
407        (or (search-forward "\n\n" nil t) (point-max)))
408
409       (goto-char (point-min))
410       (while (re-search-forward 
411               "=\\?iso-8859-1\\?q\\?\\([^?\t\n]*\\)\\?=" nil t)
412         (setq string (match-string 1))
413         (narrow-to-region (match-beginning 0) (match-end 0))
414         (delete-region (point-min) (point-max))
415         (insert string)
416         (article-mime-decode-quoted-printable
417          (goto-char (point-min)) (point-max))
418         (subst-char-in-region (point-min) (point-max) ?_ ? )
419         (widen)
420         (goto-char (point-min))))))
421
422 (defun article-de-quoted-unreadable (&optional force)
423   "Do a naive translation of a quoted-printable-encoded article.
424 This is in no way, shape or form meant as a replacement for real MIME
425 processing, but is simply a stop-gap measure until MIME support is
426 written.
427 If FORCE, decode the article whether it is marked as quoted-printable
428 or not."
429   (interactive (list 'force))
430   (save-excursion
431     (let ((case-fold-search t)
432           (buffer-read-only nil)
433           (type (gnus-fetch-field "content-transfer-encoding")))
434       (article-decode-rfc1522)
435       (when (or force
436                 (and type (string-match "quoted-printable" (downcase type))))
437         (goto-char (point-min))
438         (search-forward "\n\n" nil 'move)
439         (article-mime-decode-quoted-printable (point) (point-max))))))
440
441 (defun article-mime-decode-quoted-printable (from to)
442   "Decode Quoted-Printable in the region between FROM and TO."
443   (interactive "r")
444   (goto-char from)
445   (while (search-forward "=" to t)
446     (cond ((eq (following-char) ?\n)
447            (delete-char -1)
448            (delete-char 1))
449           ((looking-at "[0-9A-F][0-9A-F]")
450            (subst-char-in-region
451             (1- (point)) (point) ?=
452             (hexl-hex-string-to-integer
453              (buffer-substring (point) (+ 2 (point)))))
454            (delete-char 2))
455           ((looking-at "=")
456            (delete-char 1))
457           ((gnus-message 3 "Malformed MIME quoted-printable message")))))
458
459 (defun article-hide-pgp (&optional arg)
460   "Toggle hiding of any PGP headers and signatures in the current article.
461 If given a negative prefix, always show; if given a positive prefix,
462 always hide."
463   (interactive (article-hidden-arg))
464   (unless (article-check-hidden-text 'pgp arg)
465     (save-excursion
466       (let (buffer-read-only beg end)
467         (widen)
468         (goto-char (point-min))
469         ;; Hide the "header".
470         (and (search-forward "\n-----BEGIN PGP SIGNED MESSAGE-----\n" nil t)
471              (article-hide-text-type (match-beginning 0) (match-end 0) 'pgp))
472         (setq beg (point))
473         ;; Hide the actual signature.
474         (and (search-forward "\n-----BEGIN PGP SIGNATURE-----\n" nil t)
475              (setq end (1+ (match-beginning 0)))
476              (article-hide-text-type
477               end
478               (if (search-forward "\n-----END PGP SIGNATURE-----\n" nil t)
479                   (match-end 0)
480                 ;; Perhaps we shouldn't hide to the end of the buffer
481                 ;; if there is no end to the signature?
482                 (point-max))
483               'pgp))
484         ;; Hide "- " PGP quotation markers.
485         (when (and beg end)
486           (narrow-to-region beg end)
487           (goto-char (point-min))
488           (while (re-search-forward "^- " nil t)
489             (article-hide-text-type (match-beginning 0) (match-end 0) 'pgp))
490           (widen))))))
491
492 (defun article-hide-pem (&optional arg)
493   "Toggle hiding of any PEM headers and signatures in the current article.
494 If given a negative prefix, always show; if given a positive prefix,
495 always hide."
496   (interactive (article-hidden-arg))
497   (unless (article-check-hidden-text 'pem arg)
498     (save-excursion
499       (let (buffer-read-only end)
500         (widen)
501         (goto-char (point-min))
502         ;; hide the horrendously ugly "header".
503         (and (search-forward "\n-----BEGIN PRIVACY-ENHANCED MESSAGE-----\n"
504                              nil
505                              t)
506              (setq end (1+ (match-beginning 0)))
507              (article-hide-text-type
508               end
509               (if (search-forward "\n\n" nil t)
510                   (match-end 0)
511                 (point-max))
512               'pem))
513         ;; hide the trailer as well
514         (and (search-forward "\n-----END PRIVACY-ENHANCED MESSAGE-----\n"
515                              nil
516                              t)
517              (article-hide-text-type
518               (match-beginning 0) (match-end 0) 'pem))))))
519
520 (defun article-hide-signature (&optional arg)
521   "Hide the signature in the current article.
522 If given a negative prefix, always show; if given a positive prefix,
523 always hide."
524   (interactive (article-hidden-arg))
525   (unless (article-check-hidden-text 'signature arg)
526     (save-excursion
527       (save-restriction
528         (let ((buffer-read-only nil))
529           (when (article-narrow-to-signature)
530             (article-hide-text-type (point-min) (point-max) 'signature)))))))
531
532 (defun article-strip-leading-blank-lines ()
533   "Remove all blank lines from the beginning of the article."
534   (interactive)
535   (save-excursion
536     (let (buffer-read-only)
537       (goto-char (point-min))
538       (when (search-forward "\n\n" nil t)
539         (while (looking-at "[ \t]$")
540           (gnus-delete-line))))))
541
542 (defun article-strip-multiple-blank-lines ()
543   "Replace consequtive blank lines with one empty line."
544   (interactive)
545   (save-excursion
546     (let (buffer-read-only)
547       ;; First make all blank lines empty.
548       (goto-char (point-min))
549       (while (re-search-forward "^[ \t]+$" nil t)
550         (replace-match "" nil t))
551       ;; Then replace multiple empty lines with a single empty line.
552       (goto-char (point-min))
553       (while (re-search-forward "\n\n+" nil t)
554         (replace-match "\n" nil t)))))
555
556 (defun article-strip-blank-lines ()
557   "Strip leading, trailing and multiple blank lines."
558   (interactive)
559   (article-strip-leading-blank-lines)
560   (article-remove-trailing-blank-lines)
561   (article-strip-multiple-blank-lines))
562
563 (defvar mime::preview/content-list)
564 (defvar mime::preview-content-info/point-min)
565 (defun article-narrow-to-signature ()
566   "Narrow to the signature."
567   (widen)
568   (when (and (boundp 'mime::preview/content-list)
569              mime::preview/content-list)
570     ;; We have a MIMEish article, so we use the MIME data to narrow.
571     (let ((pcinfo (car (last mime::preview/content-list))))
572       (condition-case ()
573           (narrow-to-region
574            (funcall (intern "mime::preview-content-info/point-min") pcinfo)
575            (point-max))
576         (error nil))))
577   
578   (when (article-search-signature)
579     (forward-line 1)
580     ;; Check whether we have some limits to what we consider
581     ;; to be a signature.
582     (let ((limits (if (listp gnus-signature-limit) gnus-signature-limit
583                    (list gnus-signature-limit)))
584           limit limited)
585       (while (setq limit (pop limits))
586         (if (or (and (integerp limit)
587                      (< (- (point-max) (point)) limit))
588                 (and (floatp limit)
589                      (< (count-lines (point) (point-max)) limit))
590                 (and (gnus-functionp limit)
591                      (funcall limit))
592                 (and (stringp limit)
593                      (not (re-search-forward limit nil t))))
594             () ; This limit did not succeed.
595           (setq limited t
596                 limits nil)))
597       (unless limited
598         (narrow-to-region (point) (point-max))
599         t))))
600
601 (defun article-search-signature ()
602   "Search the current buffer for the signature separator.
603 Put point at the beginning of the signature separator."
604   (let ((cur (point)))
605     (goto-char (point-max))
606     (if (if (stringp gnus-signature-separator)
607             (re-search-backward gnus-signature-separator nil t)
608           (let ((seps gnus-signature-separator))
609             (while (and seps
610                         (not (re-search-backward (car seps) nil t)))
611               (pop seps))
612             seps))
613         t
614       (goto-char cur)
615       nil)))
616
617 (defun article-hidden-arg ()
618   "Return the current prefix arg as a number, or 0 if no prefix."
619   (list (if current-prefix-arg
620             (prefix-numeric-value current-prefix-arg)
621           0)))
622
623 (defun article-check-hidden-text (type arg)
624   "Return nil if hiding is necessary.
625 Arg can be nil or a number.  Nil and positive means hide, negative
626 means show, 0 means toggle."
627   (save-excursion
628     (let ((hide (article-hidden-text-p type)))
629       (cond
630        ((or (null arg)
631             (> arg 0))
632         nil)
633        ((< arg 0)
634         (article-show-hidden-text type))
635        (t
636         (if (eq hide 'hidden)
637             (article-show-hidden-text type)
638           nil))))))
639
640 (defun article-hidden-text-p (type)
641   "Say whether the current buffer contains hidden text of type TYPE."
642   (let ((pos (text-property-any (point-min) (point-max) 'article-type type)))
643     (when pos
644       (if (get-text-property pos 'invisible)
645           'hidden
646         'shown))))
647
648 (defun article-show-hidden-text (type &optional hide)
649   "Show all hidden text of type TYPE.
650 If HIDE, hide the text instead."
651   (save-excursion
652     (let ((buffer-read-only nil)
653           (inhibit-point-motion-hooks t)
654           (beg (point-min)))
655       (while (gnus-goto-char (text-property-any
656                               beg (point-max) 'article-type type))
657         (setq beg (point))
658         (forward-char)
659         (if hide
660             (article-hide-text beg (point) gnus-hidden-properties)
661           (article-unhide-text beg (point)))
662         (setq beg (point)))
663       t)))
664
665 (defvar article-time-units
666   `((year . ,(* 365.25 24 60 60))
667     (week . ,(* 7 24 60 60))
668     (day . ,(* 24 60 60))
669     (hour . ,(* 60 60))
670     (minute . 60)
671     (second . 1))
672   "Mapping from time units to seconds.")
673
674 (defun article-date-ut (&optional type highlight header)
675   "Convert DATE date to universal time in the current article.
676 If TYPE is `local', convert to local time; if it is `lapsed', output
677 how much time has lapsed since DATE."
678   (interactive (list 'ut t))
679   (let* ((header (or header (message-fetch-field "date") ""))
680          (date (if (vectorp header) (mail-header-date header)
681                  header))
682          (date-regexp "^Date: \\|^X-Sent: ")
683          (now (current-time))
684          (inhibit-point-motion-hooks t)
685          bface eface)
686     (when (and date (not (string= date "")))
687       (save-excursion
688         (save-restriction
689           (nnheader-narrow-to-headers)
690           (let ((buffer-read-only nil))
691             ;; Delete any old Date headers.
692             (if (re-search-forward date-regexp nil t)
693                 (progn
694                   (setq bface (get-text-property (gnus-point-at-bol) 'face)
695                         eface (get-text-property (1- (gnus-point-at-eol))
696                                                  'face))
697                   (message-remove-header date-regexp t)
698                   (beginning-of-line))
699               (goto-char (point-max)))
700             (insert (article-make-date-line date type))
701             ;; Do highlighting.
702             (forward-line -1)
703             (when (looking-at "\\([^:]+\\): *\\(.*\\)$")
704               (put-text-property (match-beginning 1) (match-end 1)
705                                  'face bface)
706               (put-text-property (match-beginning 2) (match-end 2)
707                                  'face eface))))))))
708
709 (defun article-make-date-line (date type)
710   "Return a DATE line of TYPE."
711   (cond
712    ;; Convert to the local timezone.  We have to slap a
713    ;; `condition-case' round the calls to the timezone
714    ;; functions since they aren't particularly resistant to
715    ;; buggy dates.
716    ((eq type 'local)
717     (concat "Date: " (condition-case ()
718                          (timezone-make-date-arpa-standard date)
719                        (error date))
720             "\n"))
721    ;; Convert to Universal Time.
722    ((eq type 'ut)
723     (concat "Date: "
724             (condition-case ()
725                 (timezone-make-date-arpa-standard date nil "UT")
726               (error date))
727             "\n"))
728    ;; Get the original date from the article.
729    ((eq type 'original)
730     (concat "Date: " date "\n"))
731    ;; Do an X-Sent lapsed format.
732    ((eq type 'lapsed)
733     ;; If the date is seriously mangled, the timezone
734     ;; functions are liable to bug out, so we condition-case
735     ;; the entire thing.
736     (let* ((now (current-time))
737            (real-time
738             (condition-case ()
739                 (gnus-time-minus
740                  (gnus-encode-date
741                   (timezone-make-date-arpa-standard
742                    (current-time-string now)
743                    (current-time-zone now) "UT"))
744                  (gnus-encode-date
745                   (timezone-make-date-arpa-standard
746                    date nil "UT")))
747               (error nil)))
748            (real-sec (and real-time
749                           (+ (* (float (car real-time)) 65536)
750                              (cadr real-time))))
751            (sec (and real-time (abs real-sec)))
752            num prev)
753       (cond
754        ((null real-time)
755         "X-Sent: Unknown\n")
756        ((zerop sec)
757         "X-Sent: Now\n")
758        (t
759         (concat
760          "X-Sent: "
761          ;; This is a bit convoluted, but basically we go
762          ;; through the time units for years, weeks, etc,
763          ;; and divide things to see whether that results
764          ;; in positive answers.
765          (mapconcat
766           (lambda (unit)
767             (if (zerop (setq num (ffloor (/ sec (cdr unit)))))
768                 ;; The (remaining) seconds are too few to
769                 ;; be divided into this time unit.
770                 ""
771               ;; It's big enough, so we output it.
772               (setq sec (- sec (* num (cdr unit))))
773               (prog1
774                   (concat (if prev ", " "") (int-to-string
775                                              (floor num))
776                           " " (symbol-name (car unit)) 
777                           (if (> num 1) "s" ""))
778                 (setq prev t))))
779           article-time-units "")
780          ;; If dates are odd, then it might appear like the
781          ;; article was sent in the future.
782          (if (> real-sec 0)
783              " ago\n"
784            " in the future\n"))))))
785    (t
786     (error "Unknown conversion type: %s" type))))
787
788 (defun article-date-local (&optional highlight)
789   "Convert the current article date to the local timezone."
790   (interactive (list t))
791   (article-date-ut 'local highlight))
792
793 (defun article-date-original (&optional highlight)
794   "Convert the current article date to what it was originally.
795 This is only useful if you have used some other date conversion
796 function and want to see what the date was before converting."
797   (interactive (list t))
798   (article-date-ut 'original highlight))
799
800 (defun article-date-lapsed (&optional highlight)
801   "Convert the current article date to time lapsed since it was sent."
802   (interactive (list t))
803   (article-date-ut 'lapsed highlight))
804
805 (defun article-show-all ()
806   "Show all hidden text in the article buffer."
807   (interactive)
808   (save-excursion
809     (let ((buffer-read-only nil))
810       (article-unhide-text (point-min) (point-max)))))
811
812 (defun article-emphasize (&optional arg)
813   "Empasize text according to `gnus-emphasis-alist'."
814   (interactive (article-hidden-arg))
815   (unless (article-check-hidden-text 'emphasis arg)
816     (save-excursion
817       (let ((alist gnus-emphasis-alist)
818             (buffer-read-only nil)
819             (props (append '(article-type emphasis)
820                            gnus-hidden-properties))
821             regexp elem beg invisible visible face)
822         (goto-char (point-min))
823         (search-forward "\n\n" nil t)
824         (setq beg (point))
825         (while (setq elem (pop alist))
826           (goto-char beg)
827           (setq regexp (car elem)
828                 invisible (nth 1 elem)
829                 visible (nth 2 elem)
830                 face (nth 3 elem))
831           (while (re-search-forward regexp nil t)
832             (article-hide-text
833              (match-beginning invisible) (match-end invisible) props)
834             (article-unhide-text-type
835              (match-beginning visible) (match-end visible) 'emphasis)
836             (put-text-property 
837              (match-beginning visible) (match-end visible)
838              'face face)))))))
839
840 (provide 'article)
841
842 ;;; article.el ends here