;;; -*- Mode:Emacs-Lisp -*-

;;; gnus-mark.el v1.6
;;; Operating on more than one news article at a time.
;;; Created:  28-Jun-91 by Jamie Zawinski <jwz@lucid.com>
;;; Modified: 28-Jun-91 by Sebastian Kremer <sk@thp.Uni-Koeln.DE>
;;; Modified: 01-Dec-91 by Jamie Zawinski <jwz@lucid.com>
;;; Modified: 05-Dec-91 by Paul D. Smith <paul_smith@dg.com>
;;; Modified: 28-Nov-92 by A1C Tim Miller <tjm@hrt213.brooks.af.mil>
;;; Modified: 10-Jun-93 by Vivek Khera <khera@cs.duke.edu> (GNUS 3.15 fixes)
;;; Modified: 15-Sep-93 by Jamie Zawinski <jwz@lucid.com> (article saving)
;;;
;;; typing `@' in the subject buffer will mark the current article with
;;; an `@'.  After marking more than one article this way, you can use one
;;; of the commands in this file on all of them at once.
;;;
;;; `M-@' will prompt you for a regular expression, and will mark all f
;;; articles which match.
;;;
;;; `^U@' will prompt you for a mark-character to use other than `@'.
;;;
;;; To unmark an article, use `u', `d', or `^U M-@ SPC RET'.
;;;
;;; `F' (gnus-forward-marked-articles) will put you in a send-mail buffer 
;;; along with the contents of all of the marked articles in RFC-944 digest
;;; format, suitable for later explosion with any reasonable mail reader.
;;;
;;; `M-x gnus-uudecode-marked-messages' (M-x gnus-uu RET works) will strip the
;;; junk from the beginning and end of the marked articles, concatenate them
;;; together, and pipe the result through uudecode.  If the resultant file is
;;; a tar file and/or is compressed, this command offers to unpack/uncompress
;;; as well.  See also the variables gnus-uudecode-file-mode, 
;;; gnus-uudecode-auto-chmod, and gnus-uudecode-auto-touch.  If the first
;;; marked message is not the first part of the uuencoded file, or if the last
;;; marked message is not the last part of the uuencoded file, it complains.
;;; However, it's not possible to tell if the middle parts are out of order,
;;; so make sure you use ^C^S^S to get the articles in the right order first.
;;; It also complains about obviously-corrupted files.
;;;
;;; `M-x gnus-unshar-marked-articles' (M-x gnus-un RET works) will strip the 
;;; junk from the beginning and end of the marked articles, and run each of
;;; them through sh in turn.  This doesn't work on shar files that don't 
;;; begin with "#!".
;;;
;;; Both of the above commands prompt you for a directory in which to do the
;;; dirty work.  If the directory you specify doesn't exist, you have the
;;; option of creating it.
;;;
;;; `C-o' and `o' (`gnus-summary-save-in-mail' and `gnus-summary-save-article')
;;; will operate on the marked articles, assuming you are using one of the 
;;; standard functions for `gnus-default-article-saver', those being
;;; `gnus-summary-save-in-rmail', `gnus-summary-save-in-mail',
;;; `gnus-summary-save-in-folder', and `gnus-summary-save-in-file'.  If you 
;;; use a different function here, it should be pretty obvious from reading
;;; the code how to convert it to operate on the marked articles.
;;;
;;; When saving articles, the variable `gnus-save-marked-in-same-file' controls
;;; whether to prompt for the file/folder intowhich each article should be 
;;; written.  If t, you will be asked where to save them once, and all 
;;; messages will be saved to the same place.  If nil, you will be prompted
;;; for each article.
;;
;; LCD Archive Entry:
;; gnus-mark|Jamie Zawinski|jwz@lucid.com
;; |Operate on more than one news article at a time
;; |93-09-15|1.6|~/misc/gnus-mark.el.Z|

(require 'gnus)

(define-key gnus-summary-mode-map "@" 'gnus-summary-mark-article)
(define-key gnus-summary-mode-map "\M-@" 'gnus-summary-mark-regexp)

(substitute-key-definition 'gnus-summary-mail-forward
			   'gnus-forward-marked-articles
			   gnus-summary-mode-map)
;(define-key gnus-summary-mode-map "\C-F" 'gnus-forward-marked-articles)

;;; See also gnus-uudecode-marked-messages and gnus-unshar-marked-articles.

(defvar gnus-default-mark-char ?@
  "*Character used to mark articles for later commands in GNUS.")

(defun gnus-summary-mark-article (mark)
"Mark the current article for later commands.
This marker comes from variable `gnus-default-mark-char'.
You can change this variable by giving a prefix argument to this command,
in which case you will be prompted for the character to use."
  (interactive (list (if current-prefix-arg
			 (let ((cursor-in-echo-area t))
			   (message "Mark message with: ")
			   (setq gnus-default-mark-char (read-char)))
			 gnus-default-mark-char)))
  (or (eq (current-buffer) (get-buffer gnus-summary-buffer))
      (error "not in summary buffer"))
  (gnus-summary-mark-as-read nil gnus-default-mark-char)
  (gnus-summary-next-subject 1 nil))

;; Actually, gnus-kill should have an interactive spec!
(defun gnus-summary-mark-regexp (regexp &optional marker)
  "Mark all articles with subjects matching REGEXP.
With a prefix ARG, prompt for the marker.  Type RET immediately to
mark them as unread or enter SPC RET to remove all kinds of marks."
  (interactive
   (list (read-string "Mark (regexp): ")
	 (if current-prefix-arg
	     (read-string
	      "Mark with char (RET to mark as unread, SPC RET to remove existing markers): "))))
  (setq marker (or marker (char-to-string gnus-default-mark-char)))
  (gnus-kill "Subject" regexp
	     (if (equal "" marker)
		 '(gnus-summary-mark-as-unread)
	       (list 'gnus-summary-mark-as-read nil marker))
	     ;; overwrite existing marks:
	     t))

(defun gnus-summary-mark-map-articles (mark function)
  (save-excursion
    (set-buffer gnus-summary-buffer)
    (let ((str (concat "^"
		       (regexp-quote (make-string 1 mark))
		       "[^-0-9]*\\([-0-9]+\\):"))
	  got-one)
      (save-excursion
	(goto-char (point-min))
	(while (not (eobp))
	  (if (looking-at str)
	      (progn
		(setq got-one t)
		(save-excursion
		  (funcall function
		    (gnus-find-header-by-number gnus-newsgroup-headers 
		      (string-to-int
			(buffer-substring
			  (match-beginning 1) (match-end 1))))))))
	  (forward-line 1)))
      (cond ((not got-one)
	     (let ((article (gnus-summary-article-number)))
	       (if (or (null gnus-current-article)
		       (/= article gnus-current-article))
		   ;; Selected subject is different from current article's.
		   (gnus-summary-display-article article))
	       (funcall function 
			(gnus-find-header-by-number gnus-newsgroup-headers 
						    article)))))
      )))


;;; simpler, more specific to gnus-mark version of shell-command

(defun gnus-mark-shell-command (start end command erase)
  "Execute string COMMAND in inferior shell with region as input.
Display output (if any) in temp buffer interactively.
If ERASE is non-nil the buffer is erased, otherwise the output is
appended to the end of the buffer."
    (let ((buffer (get-buffer-create "*Shell Command Output*"))
	  (orig-buffer (current-buffer)))
      (set-buffer buffer)
      (if erase
	  (erase-buffer)
	(goto-char (point-max)))
      (set-buffer orig-buffer)
      (if (eq buffer orig-buffer)
	  (setq start 1 end 1))
      (display-buffer buffer)
      (bury-buffer buffer)
      (call-process-region start end shell-file-name
			   nil buffer t "-c" command)))


;;; RFC944 forwarding of multiple messages

(defun gnus-forward-marked-articles ()
  "Forward the marked messages to another user, RFC944 style."
  (interactive)
  (let (subj p
	(state 'first)
	tmp-buf)
    (unwind-protect
	(progn
	  (setq tmp-buf (get-buffer-create "*gnus-forward-tmp*"))
	  (save-excursion (set-buffer tmp-buf) (erase-buffer))
	  (gnus-summary-mark-map-articles
	   gnus-default-mark-char
	   (function (lambda (msg)
	     (if (eq state 'first) (setq state t) (setq state nil))
	     (message "Snarfing article %s..." (aref msg 0))
	     (if (eq gnus-current-article (aref msg 0))
		 (gnus-summary-mark-as-read)
	       (gnus-summary-display-article (aref msg 0)))
	     (set-buffer gnus-article-buffer)
	     (widen)
	     (set-buffer tmp-buf)
	     (goto-char (point-max))
	     (if subj
		 (insert "----------\n")
	       (setq subj (aref msg 1)))
	     (setq p (point))
	     (insert-buffer gnus-article-buffer)
	     (goto-char p)
	     (while (re-search-forward "^-" nil t)
	       (insert " -"))
	     )))
	  (mail nil nil (concat "[Fwd: " subj "]"))
	  (save-excursion
	    (goto-char (point-max))
	    (insert (if state
			"---------- Begin forwarded message\n"
		      "---------- Begin digest\n"))
	    (insert-buffer tmp-buf)
	    (goto-char (point-max))
	    (insert (if state
			"\n---------- End forwarded message\n"
		      "\n---------- End digest\n"))))
      ;; protected
      (and tmp-buf (kill-buffer tmp-buf)))))


;;; reading a directory name, and offering to create if it doesn't exist.

(defun gnus-mark-read-directory (prompt &optional default-dir)
  (let ((dir
	 (read-file-name prompt
			 (or default-dir default-directory)
			 (or default-dir default-directory))))
    (if (string-match "/$" dir)
	(setq dir (substring dir 0 (match-beginning 0))))
    (setq dir
      (cond ((file-directory-p dir) dir)
	    ((file-exists-p dir)
	     (ding)
	     (message "%s exists and is not a directory!" dir)
	     (sleep-for 2)
	     (gnus-mark-read-directory prompt dir))
	    ((y-or-n-p (format "directory %s doesn't exist, create it? " dir))
	     (make-directory dir)
	     dir)
	    (t (gnus-mark-read-directory prompt dir))))
    (if (string-match "/$" dir)
	dir
      (concat dir "/"))))


;;; uudecode

(defconst gnus-uudecode-begin-pattern
    "^begin[ \t]+\\([0-9][0-9][0-9][0-9]?\\)[ \t]+\\([^ \t\n]*\\)$")

(defconst gnus-uudecode-body-pattern
    "^M.............................................................?$")

(defconst gnus-uudecode-begin-or-body-pattern
    (concat "\\(" gnus-uudecode-begin-pattern "\\|"
	    gnus-uudecode-body-pattern "\\)"))

(defvar gnus-uudecode-file-mode "644"
  "*If non-nil, this overrides the mode specified in the `begin' line of
a uuencoded file being unpacked by vm-uudecode.  This should be a string,
which is the mode desired in octal.")

(defvar gnus-uudecode-auto-chmod "u+w"
  "*If non-nil, then when gnus is untarring a file for you, it will
apply this chmod modifier to each of the unpacked files.  This should be
a string like \"u+w\".")

(defvar gnus-uudecode-auto-touch t
  "*If non-nil, then when vm-uudecode is untarring a file for you, it will
cause the write-date of each of the unpacked files to be the current time.
Normally tar unpacks files with the time at which they are packed; this can
cause your `make' commands to fail if you are installing a new version of
a package which you have modified.")

(defvar gnus-uudecode-picture-pattern "\\.\\(gif\\|p[bgp]m\\|rast\\|pic\\|jpg\\|tiff?\\)$"
  "*If non-nil, this should be a pattern which matches files which are 
images.  When gnus-uudecode-marked-articles creates a file which matches
this pattern, it will ask you if you want to look at it now.  If so, it
invokes gnus-uudecode-picture-viewer with the filename as an argument.
After doing this, it asks you if you want to keep the picture or delete it.")

(defvar gnus-uudecode-picture-viewer "xv"
  "*The picture viewer that gnus-uudecode-marked-articles uses.  See doc of
variable gnus-uudecode-picture-pattern.")

(defvar gnus-uudecode-default-directory nil "*")

(defun gnus-uudecode-marked-articles (directory)
  "Strip the junk from the beginning and end of the marked articles, 
concatenate them together, and pipe the result through uudecode.  If
the resultant file is a tar file and/or is compressed, this command
offers to unpack/uncompress as well.  See also the variables
gnus-uudecode-file-mode, gnus-uudecode-auto-chmod, and
 gnus-uudecode-auto-touch."
  (interactive (list (gnus-mark-read-directory "uudecode in directory: "
		       gnus-uudecode-default-directory)))
  (setq gnus-uudecode-default-directory directory)
  (let ((state 'first)
	tmp-buf
	name)
    (unwind-protect
      (progn
       (setq tmp-buf  (get-buffer-create "*gnus-uudecode-tmp*"))
       (save-excursion (set-buffer tmp-buf) (erase-buffer))
       (gnus-summary-mark-map-articles
	gnus-default-mark-char
	(function (lambda (msg)
	  (message "Snarfing article %s..." (aref msg 0))
	  (if (eq state 'last)
	      (error "articles out of order: articles follow `end' line."))
	  (if (eq gnus-current-article (aref msg 0))
	      (gnus-summary-mark-as-read)
	    (gnus-summary-display-article (aref msg 0)))
	  (set-buffer gnus-article-buffer)
	  (widen)
	  (set-buffer tmp-buf)
	  (goto-char (point-max))
	  (let ((p (point))
		(case-fold-search nil))
	    (insert-buffer gnus-article-buffer)
	    (goto-char p)
	    ;; Some MSDOS losers post uuencoded articles with CRLF.
	    (while (search-forward "\r\n" nil t)
	      (forward-char -1)
	      (delete-char -1))
	    (goto-char p)
	    (cond
	     ((eq state 'first)
	      (or (re-search-forward gnus-uudecode-begin-pattern nil t)
		  (error "couldn't find `begin' line in first article."))
	      ;; I'd like to second-guess the losers who use mixed-case
	      ;; and upper-case filenames, but this trashes trailing ".Z"
	      ;;(downcase-region (match-beginning 2) (match-end 2))
	      (setq name (buffer-substring (match-beginning 2) (match-end 2)))
	      ;; don't tolerate bogus umasks.
	      (if gnus-uudecode-file-mode
		  (progn
		    (goto-char (match-beginning 1))
		    (delete-region (match-beginning 1) (match-end 1))
		    (insert gnus-uudecode-file-mode)))
	      (setq state 'middle))
	     (t
	      (or (re-search-forward gnus-uudecode-begin-or-body-pattern nil t)
		  (error "couldn't find beginning of data."))))
	    (beginning-of-line)
	    (delete-region p (point))
	    (let (c tmp)
	      ;; This could be sped up a lot, but then we'd lose the
	      ;; error checking it does; maybe that's ok.
	      (while (progn
		       (forward-line)
		       (setq c (- (following-char) ? ))
		       (end-of-line)
		       (setq tmp (/ (1- (current-column)) 4))
		       (beginning-of-line)
		       (= (+ tmp (+ tmp tmp)) c))
		)
	      ;; Slack.
	      (setq p (point))
	      (if (or (looking-at "end\n")
		      (progn (forward-line 1) (looking-at "end\n"))
		      (progn (forward-line 1) (looking-at "end\n"))
		      (progn (forward-line 1) (looking-at "end\n")))
		  (progn
		    (forward-line 1)
		    (setq state 'last))
		(goto-char p))
	      )
	    (delete-region (point) (point-max))))))
       (or (eq state 'last) (error "no `end' line in last article."))
       (set-buffer tmp-buf)
       (let* ((base-file (file-name-nondirectory name))
	      (final-file (concat directory base-file))
	      (command (concat "cd " directory " ; uudecode"))
	      tar-p)
	 (cond ((string-match "\\.tar\\.\\(Z\\|z\\|gz\\)$" base-file)
		(if (y-or-n-p "uncompress/untar? ")
		    (setq command (concat command " && zcat "
					  base-file " | tar -vxf -")
			  final-file nil
			  tar-p t)))
	       ((string-match "\\.tar$" base-file)
		(if (y-or-n-p "untar? ")
		    (setq command (concat command " && tar -vxf " base-file)
			  final-file nil
			  tar-p t)))
	       ((string-match "\\.\\(Z\\|z\\|gz\\)$" base-file)
		(if (y-or-n-p "uncompress? ")
		    (setq command (concat command " ; uncompress " base-file)
			  final-file (substring base-file 0
						(match-beginning 0))))))
	 (let ((str (concat "executing \"" command "\" ...")))
	   (message str)
	   (gnus-mark-shell-command (point-min) (point-max) command t)
;	   (if final-file
;	      (dired-add-entry-all-buffers directory
;	     (file-name-nondirectory final-file)))
	   (message (concat str " done.")))
	 (cond
	  (tar-p
	   (set-buffer (get-buffer "*Shell Command Output*"))
	   (let ((all (concat command "\n" (buffer-string)))
		 files files-str)
	     (goto-char (point-min))
	     (while (not (eobp))
	       (if (looking-at "^x \\([^,\n]+\\), ")
		   (setq files (cons (buffer-substring
				      (match-beginning 1) (match-end 1))
				     files)))
	       (forward-line 1))
	     (setq files (nreverse files)
		   files-str (mapconcat 'identity files " "))
	     (cond
	      (files
	       (cond
		(gnus-uudecode-auto-chmod
		 (setq command (concat "cd " directory " ; chmod "
				       gnus-uudecode-auto-chmod " " files-str))
		 (gnus-mark-shell-command (point) (point) command nil)
		 (setq all (concat all "\n" command "\n" (buffer-string)))))
	       (cond
		(gnus-uudecode-auto-touch
		 (setq command (concat "cd " directory " ; touch " files-str))
		 (gnus-mark-shell-command (point) (point) command nil)
		 (setq all (concat all "\n" command "\n" (buffer-string)))))
	      (goto-char (point-min))
	      (insert all "\n")
;	      (mapcar (function (lambda (x)
;			(dired-add-entry-all-buffers directory x)))
;		      files)
	      ))))
	  (t
	   (message "wrote file %s" final-file)
	   (let ((case-fold-search t))
	     (cond ((null gnus-uudecode-picture-pattern) nil)
		   ((and (string-match gnus-uudecode-picture-pattern
				       final-file)
			 (y-or-n-p
			   (format "look at the picture in %s? " final-file)))
		    (gnus-mark-shell-command (point) (point)
		      (if (string-match (regexp-quote directory) final-file)
			  (concat "cd " directory " ; "
                                  gnus-uudecode-picture-viewer " "
				  (substring final-file (match-end 0)))
			(concat gnus-uudecode-picture-viewer " " final-file))
		      nil)
		    (if (y-or-n-p (format "delete file %s? " final-file))
			(progn
			  (delete-file final-file)
			  (message "%s deleted." final-file))
			)
		    (display-buffer "*Article*")))))
	  )))
      ;; protected
      (and tmp-buf (kill-buffer tmp-buf)))))


;;; shar (ack pffleughhh barf)

(defvar gnus-unshar-program "/bin/sh"
  "*The program to use to unshar files; you might want to use something
that is less of a gaping security hole than /bin/sh.")

(defvar gnus-unshar-default-directory nil "*")

(defun gnus-unshar-marked-articles (directory)
  "For each of the marked articles, strip the junk from the beginning and end
and then run the result through gnus-unshar-program (typically /bin/sh.)"
  (interactive (list (gnus-mark-read-directory
		       "unshar in directory: " gnus-unshar-default-directory)))
  (setq gnus-unshar-default-directory directory)
  (let (tmp-buf
	(command (concat "cd " directory " ; " gnus-unshar-program)))
    (unwind-protect
      (progn
       (if (setq tmp-buf (get-buffer "*Shell Command Output*"))
	   (save-excursion
	     (set-buffer tmp-buf)
	     (erase-buffer)))
       (setq tmp-buf (get-buffer-create "*gnus-unshar-tmp*"))
       (gnus-summary-mark-map-articles
	gnus-default-mark-char
	(function (lambda (msg)
	  (message "Snarfing article %s..." (aref msg 0))
	  (if (eq gnus-current-article (aref msg 0))
	      (gnus-summary-mark-as-read)
	    (gnus-summary-display-article (aref msg 0)))
	  (set-buffer gnus-article-buffer)
	  (widen)
	  (set-buffer tmp-buf)
	  (erase-buffer)
	  (insert-buffer gnus-article-buffer)
	  (or (re-search-forward "^#!" nil t)
	      (re-search-forward "^: This is a shar archive" nil t)
	      (re-search-forward "^# This is a shell archive" nil t)
	      (re-search-forward "^# type \"sh file -c\"." nil t)
	      (re-search-forward "^#!" nil nil)) ; for the error message
	  (beginning-of-line)
	  (delete-region (point-min) (point))
	  (goto-char (point-max))
	  ;; what kind of shithead has a signature after a shar file?
	  (if (re-search-backward "^--" nil t)
	      (delete-region (point) (point-max)))
	  (message "unsharing article %s..." (aref msg 0))
	  (gnus-mark-shell-command (point-min) (point-max) command nil)
	  (message "unsharing article %s...done." (aref msg 0))
	  ))))
      ;; protected
      (kill-buffer tmp-buf)
;      (if (y-or-n-p "Display *Article* buffer? ")
;	  (display-buffer "*Article*"))
      )))


;;; This code encapsulates the definitions of the standard gnus-save-in-*
;;; functions to operate on the marked articles.

(defvar gnus-save-marked-in-same-file t
  "*When saving multiple marked articles, whether to prompt each time.
If t, you will be asked where to save them once, and all messages will
be saved there.  If nil, you will be prompted for each article.")

(defvar inside-gnus-save-marked-articles-mapper)
(defun gnus-save-marked-articles-mapper (saver filename var)
  (let* ((count 0)
	 (fn (function (lambda (msg)
			(if filename
			    (funcall saver filename)
			  (call-interactively saver)
			  (if gnus-save-marked-in-same-file
			      (setq filename (symbol-value var))))
			(setq count (1+ count))))))
    (if (and (boundp 'inside-gnus-save-marked-articles-mapper)
	     inside-gnus-save-marked-articles-mapper)
	(funcall fn nil)
      (let ((inside-gnus-save-marked-articles-mapper t))
	  (gnus-summary-mark-map-articles gnus-default-mark-char fn)
	  (if (> count 0)
	      (message "%s"
		       (concat (format "Saved %d article%s"
				       count (if (= count 1) "" "s"))
			       (if gnus-save-marked-in-same-file
				   (format " to %s" filename)))))))))


(defvar gm-orig-gnus-summary-save-in-rmail
  (symbol-function 'gnus-summary-save-in-rmail))

(defun gnus-summary-save-in-rmail (&optional filename)
  "Append the marked articles to an Rmail file.
Optional argument FILENAME specifies file name.
Directory to save to is default to `gnus-article-save-directory' which
is initialized from the SAVEDIR environment variable."
  (interactive)
  (gnus-save-marked-articles-mapper gm-orig-gnus-summary-save-in-rmail
				    filename 'gnus-newsgroup-last-rmail))

(defvar gm-orig-gnus-summary-save-in-mail
  (symbol-function 'gnus-summary-save-in-mail))

(defun gnus-summary-save-in-mail (&optional filename)
  "Append the marked articles to a Unix mail file.
Optional argument FILENAME specifies file name.
Directory to save to is default to `gnus-article-save-directory' which
is initialized from the SAVEDIR environment variable."
  (interactive)
  (gnus-save-marked-articles-mapper gm-orig-gnus-summary-save-in-mail
				    filename 'gnus-newsgroup-last-mail))

(defvar gm-orig-gnus-summary-save-in-file
  (symbol-function 'gnus-summary-save-in-file))

(defun gnus-summary-save-in-file (&optional filename)
  "Append the marked articles to a file.
Optional argument FILENAME specifies file name.
Directory to save to is default to `gnus-article-save-directory' which
is initialized from the SAVEDIR environment variable."
  (interactive)
  (gnus-save-marked-articles-mapper gm-orig-gnus-summary-save-in-file
				    filename 'gnus-newsgroup-last-file))

(defvar gm-orig-gnus-summary-save-in-folder
  (symbol-function 'gnus-summary-save-in-folder))

(defun gnus-summary-save-in-folder (&optional folder)
  "Save the marked articles to a MH folder (using `rcvstore' in MH library).
Optional argument FOLDER specifies folder name."
  (interactive)
  (gnus-save-marked-articles-mapper gm-orig-gnus-summary-save-in-folder
				    folder 'gnus-newsgroup-last-folder))

(provide 'gnus-mark)
