source: branches/working-0709/ccl/cocoa-ide/hemlock/src/archive/mh.lisp

Last change on this file was 6567, checked in by Gary Byers, 18 years ago

Move lots of (currently unused, often unlikely to ever be used) stuff to an
archive directory.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 122.6 KB
Line 
1;;; -*- Package: Hemlock; Log: hemlock.log -*-
2;;;
3;;; **********************************************************************
4;;; This code was written as part of the CMU Common Lisp project at
5;;; Carnegie Mellon University, and has been placed in the public domain.
6;;;
7#+CMU (ext:file-comment
8 "$Header$")
9;;;
10;;; **********************************************************************
11;;;
12;;; This is a mailer interface to MH.
13;;;
14;;; Written by Bill Chiles.
15;;;
16
17(in-package :hemlock)
18
19
20
21
22;;;; General stuff.
23
24(defvar *new-mail-buffer* nil)
25
26(defvar *mh-utility-bit-bucket* (make-broadcast-stream))
27
28
29(defattribute "Digit"
30 "This is just a (mod 2) attribute for base 10 digit characters.")
31;;;
32(dotimes (i 10)
33 (setf (character-attribute :digit (digit-char i)) 1))
34
35
36(defmacro number-string (number)
37 `(let ((*print-base* 10))
38 (prin1-to-string ,number)))
39
40
41(defmacro do-headers-buffers ((buffer-var folder &optional hinfo-var)
42 &rest forms)
43 "The Forms are evaluated with Buffer-Var bound to each buffer containing
44 headers lines for folder. Optionally Hinfo-Var is bound to the
45 headers-information structure."
46 (let ((folder-var (gensym))
47 (hinfo (gensym)))
48 `(let ((,folder-var ,folder))
49 (declare (simple-string ,folder-var))
50 (dolist (,buffer-var *buffer-list*)
51 (when (hemlock-bound-p 'headers-information :buffer ,buffer-var)
52 (let ((,hinfo (variable-value 'headers-information
53 :buffer ,buffer-var)))
54 (when (string= (the simple-string (headers-info-folder ,hinfo))
55 ,folder-var)
56 ,@(if hinfo-var
57 `((let ((,hinfo-var ,hinfo))
58 ,@forms))
59 forms))))))))
60
61(defmacro do-headers-lines ((hbuffer &key line-var mark-var) &rest forms)
62 "Forms are evaluated for each non-blank line. When supplied Line-Var and
63 Mark-Var are to the line and a :left-inserting mark at the beginning of the
64 line. This works with DELETE-HEADERS-BUFFER-LINE, but one should be careful
65 using this to modify the hbuffer."
66 (let ((line-var (or line-var (gensym)))
67 (mark-var (or mark-var (gensym)))
68 (id (gensym)))
69 `(with-mark ((,mark-var (buffer-point ,hbuffer) :left-inserting))
70 (buffer-start ,mark-var)
71 (loop
72 (let* ((,line-var (mark-line ,mark-var))
73 (,id (line-message-id ,line-var)))
74 (unless (blank-line-p ,line-var)
75 ,@forms)
76 (if (or (not (eq ,line-var (mark-line ,mark-var)))
77 (string/= ,id (line-message-id ,line-var)))
78 (line-start ,mark-var)
79 (unless (line-offset ,mark-var 1 0) (return))))))))
80
81(defmacro with-headers-mark ((mark-var hbuffer msg) &rest forms)
82 "Forms are executed with Mark-Var bound to a :left-inserting mark at the
83 beginning of the headers line representing msg. If no such line exists,
84 no execution occurs."
85 (let ((line (gensym)))
86 `(do-headers-lines (,hbuffer :line-var ,line :mark-var ,mark-var)
87 (when (string= (the simple-string (line-message-id ,line))
88 (the simple-string ,msg))
89 ,@forms
90 (return)))))
91
92
93
94
95;;;; Headers Mode.
96
97(defmode "Headers" :major-p t)
98
99(defhvar "Headers Information"
100 "This holds the information about the current headers buffer."
101 :value nil)
102
103(defstruct (headers-info (:print-function print-headers-info))
104 buffer ;Buffer for these headers.
105 folder ;String name of folder with leading MH "+".
106 msg-seq ;MH sequence of messages in buffer.
107 msg-strings ;List of strings representing msg-seq.
108 other-msg-bufs ;List of message buffers referencing this headers buffer.
109 draft-bufs ;List of draft buffers referencing this headers buffer.
110 msg-buffer)
111
112(defun print-headers-info (obj str n)
113 (declare (ignore n))
114 (format str "#<Headers Info ~S>" (headers-info-folder obj)))
115
116(defmacro line-message-deleted (line)
117 `(getf (line-plist ,line) 'mh-msg-deleted))
118
119(defmacro line-message-id (line)
120 `(getf (line-plist ,line) 'mh-msg-id))
121
122(defun headers-current-message (hinfo)
123 (let* ((point (buffer-point (headers-info-buffer hinfo)))
124 (line (mark-line point)))
125 (unless (blank-line-p line)
126 (values (line-message-id line)
127 (copy-mark point)))))
128
129(defcommand "Message Headers" (p)
130 "Prompts for a folder and messages, displaying headers in a buffer in the
131 current window. With an argument, prompt for a pick expression."
132 "Show some headers."
133 (let ((folder (prompt-for-folder)))
134 (new-message-headers
135 folder
136 (prompt-for-message :prompt (if p
137 "MH messages to pick from: "
138 "MH messages: ")
139 :folder folder
140 :messages "all")
141 p)))
142
143(defcommand "Pick Headers" (p)
144 "Further narrow the selection of this folders headers.
145 Prompts for a pick expression to pick over the headers in the current
146 buffer. Entering an empty expression displays all the headers for that
147 folder."
148 "Prompts for a pick expression to pick over the headers in the current
149 buffer."
150 (declare (ignore p))
151 (let ((hinfo (value headers-information)))
152 (unless hinfo
153 (editor-error "Pick Headers only works in a headers buffer."))
154 (pick-message-headers hinfo)))
155
156;;; PICK-MESSAGE-HEADERS picks messages from info's messages based on an
157;;; expression provided by the user. If the expression is empty, we do
158;;; headers on all the messages in folder. The buffer's name is changed to
159;;; reflect the messages picked over and the expression used.
160;;;
161(defun pick-message-headers (hinfo)
162 (let ((folder (headers-info-folder hinfo))
163 (msgs (headers-info-msg-strings hinfo)))
164 (multiple-value-bind (pick user-pick)
165 (prompt-for-pick-expression)
166 (let* ((hbuffer (headers-info-buffer hinfo))
167 (new-mail-buf-p (eq hbuffer *new-mail-buffer*))
168 (region (cond (pick
169 (message-headers-to-region
170 folder (pick-messages folder msgs pick)))
171 (new-mail-buf-p
172 (maybe-get-new-mail-msg-hdrs folder))
173 (t (message-headers-to-region folder
174 (list "all"))))))
175 (with-writable-buffer (hbuffer)
176 (revamp-headers-buffer hbuffer hinfo)
177 (when region (insert-message-headers hbuffer hinfo region)))
178 (setf (buffer-modified hbuffer) nil)
179 (buffer-start (buffer-point hbuffer))
180 (setf (buffer-name hbuffer)
181 (cond (pick (format nil "Headers ~A ~A ~A" folder msgs user-pick))
182 (new-mail-buf-p (format nil "Unseen Headers ~A" folder))
183 (t (format nil "Headers ~A (all)" folder))))))))
184
185;;; NEW-MESSAGE-HEADERS picks over msgs if pickp is non-nil, or it just scans
186;;; msgs. It is important to pick and get the message headers region before
187;;; making the buffer and info structures since PICK-MESSAGES and
188;;; MESSAGE-HEADERS-TO-REGION will call EDITOR-ERROR if they fail. The buffer
189;;; name is chosen based on folder, msgs, and an optional pick expression.
190;;;
191(defun new-message-headers (folder msgs &optional pickp)
192 (multiple-value-bind (pick-exp user-pick)
193 (if pickp (prompt-for-pick-expression))
194 (let* ((pick (if pick-exp (pick-messages folder msgs pick-exp)))
195 (region (message-headers-to-region folder (or pick msgs)))
196 (hbuffer (maybe-make-mh-buffer (format nil "Headers ~A ~A~:[~; ~S~]"
197 folder msgs pick user-pick)
198 :headers))
199 (hinfo (make-headers-info :buffer hbuffer :folder folder)))
200 (insert-message-headers hbuffer hinfo region)
201 (defhvar "Headers Information"
202 "This holds the information about the current headers buffer."
203 :value hinfo :buffer hbuffer)
204 (setf (buffer-modified hbuffer) nil)
205 (setf (buffer-writable hbuffer) nil)
206 (buffer-start (buffer-point hbuffer))
207 (change-to-buffer hbuffer))))
208
209(defhvar "MH Scan Line Form"
210 "This is a pathname of a file containing an MH format expression for headers
211 lines."
212 :value (pathname "library:mh-scan"))
213
214;;; MESSAGE-HEADERS-TO-REGION uses the MH "scan" utility output headers into
215;;; buffer for folder and msgs.
216;;;
217;;; (value fill-column) should really be done as if the buffer were current,
218;;; but Hemlock doesn't let you do this without the buffer being current.
219;;;
220(defun message-headers-to-region (folder msgs &optional width)
221 (let ((region (make-empty-region)))
222 (with-output-to-mark (*standard-output* (region-end region) :full)
223 (mh "scan"
224 `(,folder ,@msgs
225 "-form" ,(namestring (truename (value mh-scan-line-form)))
226 "-width" ,(number-string (or width (value fill-column)))
227 "-noheader")))
228 region))
229
230(defun insert-message-headers (hbuffer hinfo region)
231 (ninsert-region (buffer-point hbuffer) region)
232 (let ((seq (set-message-headers-ids hbuffer :return-seq)))
233 (setf (headers-info-msg-seq hinfo) seq)
234 (setf (headers-info-msg-strings hinfo) (mh-sequence-strings seq)))
235 (when (value virtual-message-deletion)
236 (note-deleted-headers hbuffer
237 (mh-sequence-list (headers-info-folder hinfo)
238 "hemlockdeleted"))))
239
240(defun set-message-headers-ids (hbuffer &optional return-seq)
241 (let ((msgs nil))
242 (do-headers-lines (hbuffer :line-var line)
243 (let* ((line-str (line-string line))
244 (num (parse-integer line-str :junk-allowed t)))
245 (declare (simple-string line-str))
246 (unless num
247 (editor-error "MH scan lines must contain the message id as the ~
248 first thing on the line for the Hemlock interface."))
249 (setf (line-message-id line) (number-string num))
250 (when return-seq (setf msgs (mh-sequence-insert num msgs)))))
251 msgs))
252
253(defun note-deleted-headers (hbuffer deleted-seq)
254 (when deleted-seq
255 (do-headers-lines (hbuffer :line-var line :mark-var hmark)
256 (if (mh-sequence-member-p (line-message-id line) deleted-seq)
257 (note-deleted-message-at-mark hmark)
258 (setf (line-message-deleted line) nil)))))
259
260;;; PICK-MESSAGES -- Internal Interface.
261;;;
262;;; This takes a folder (with a + in front of the name), messages to pick
263;;; over, and an MH pick expression (in the form returned by
264;;; PROMPT-FOR-PICK-EXPRESSION). Sequence is an MH sequence to set to exactly
265;;; those messages chosen by the pick when zerop is non-nil; when zerop is nil,
266;;; pick adds the messages to the sequence along with whatever messages were
267;;; already in the sequence. This returns a list of message specifications.
268;;;
269(defun pick-messages (folder msgs expression &optional sequence (zerop t))
270 (let* ((temp (with-output-to-string (*standard-output*)
271 (unless
272 ;; If someone bound *signal-mh-errors* to nil around this
273 ;; function, MH pick outputs bogus messages (for example,
274 ;; "0"), and MH would return without calling EDITOR-ERROR.
275 (mh "pick" `(,folder
276 ,@msgs
277 ,@(if sequence `("-sequence" ,sequence))
278 ,@(if zerop '("-zero"))
279 "-list" ; -list must follow -sequence.
280 ,@expression))
281 (return-from pick-messages nil))))
282 (len (length temp))
283 (start 0)
284 (result nil))
285 (declare (simple-string temp))
286 (loop
287 (let ((end (position #\newline temp :start start :test #'char=)))
288 (cond ((not end)
289 (return (nreverse (cons (subseq temp start) result))))
290 ((= start end)
291 (return (nreverse result)))
292 (t
293 (push (subseq temp start end) result)
294 (when (>= (setf start (1+ end)) len)
295 (return (nreverse result)))))))))
296
297
298(defcommand "Delete Headers Buffer and Message Buffers" (p &optional buffer)
299 "Prompts for a headers buffer to delete along with its associated message
300 buffers. Any associated draft buffers are left alone, but their associated
301 message buffers will be deleted."
302 "Deletes the current headers buffer and its associated message buffers."
303 (declare (ignore p))
304 (let* ((default (cond ((value headers-information) (current-buffer))
305 ((value message-information) (value headers-buffer))))
306 (buffer (or buffer
307 (prompt-for-buffer :default default
308 :default-string
309 (if default (buffer-name default))))))
310 (unless (hemlock-bound-p 'headers-information :buffer buffer)
311 (editor-error "Not a headers buffer -- ~A" (buffer-name buffer)))
312 (let* ((hinfo (variable-value 'headers-information :buffer buffer))
313 ;; Copy list since buffer cleanup hook is destructive.
314 (other-bufs (copy-list (headers-info-other-msg-bufs hinfo)))
315 (msg-buf (headers-info-msg-buffer hinfo)))
316 (when msg-buf (delete-buffer-if-possible msg-buf))
317 (dolist (b other-bufs) (delete-buffer-if-possible b))
318 (delete-buffer-if-possible (headers-info-buffer hinfo)))))
319
320(defhvar "Expunge Messages Confirm"
321 "When set (the default), \"Expunge Messages\" and \"Quit Headers\" will ask
322 for confirmation before expunging messages and packing the folder's message
323 id's."
324 :value t)
325
326(defhvar "Temporary Draft Folder"
327 "This is the folder name where MH fcc: messages are kept that are intended
328 to be deleted and expunged when messages are expunged for any other
329 folder -- \"Expunge Messages\" and \"Quit Headers\"."
330 :value nil)
331
332;;; "Quit Headers" doesn't expunge or compact unless there is a deleted
333;;; sequence. This collapses other headers buffers into the same folder
334;;; differently than "Expunge Messages" since the latter assumes there will
335;;; always be one remaining headers buffer. This command folds all headers
336;;; buffers into the folder that are not the current buffer or the new mail
337;;; buffer into one buffer. When the current buffer is the new mail buffer
338;;; we do not check for more unseen headers since we are about to delete
339;;; the buffer anyway. The other headers buffers must be deleted before
340;;; making the new one due to aliasing the buffer structure and
341;;; MAYBE-MAKE-MH-BUFFER.
342;;;
343(defcommand "Quit Headers" (p)
344 "Quit headers buffer possibly expunging deleted messages.
345 This affects the current headers buffer. When there are deleted messages
346 the user is asked for confirmation on expunging the messages and packing the
347 folder's message id's. Then the buffer and all its associated message
348 buffers are deleted. Setting \"Quit Headers Confirm\" to nil inhibits
349 prompting. When \"Temporary Draft Folder\" is bound, this folder's messages
350 are deleted and expunged."
351 "This affects the current headers buffer. When there are deleted messages
352 the user is asked for confirmation on expunging the messages and packing
353 the folder. Then the buffer and all its associated message buffers are
354 deleted."
355 (declare (ignore p))
356 (let* ((hinfo (value headers-information))
357 (minfo (value message-information))
358 (hdrs-buf (cond (hinfo (current-buffer))
359 (minfo (value headers-buffer)))))
360 (unless hdrs-buf
361 (editor-error "Not in or associated with any headers buffer."))
362 (let* ((folder (cond (hinfo (headers-info-folder hinfo))
363 (minfo (message-info-folder minfo))))
364 (deleted-seq (mh-sequence-list folder "hemlockdeleted")))
365 (when (and deleted-seq
366 (or (not (value expunge-messages-confirm))
367 (prompt-for-y-or-n
368 :prompt (list "Expunge messages and pack folder ~A? "
369 folder)
370 :default t
371 :default-string "Y")))
372 (message "Deleting messages ...")
373 (mh "rmm" (list folder "hemlockdeleted"))
374 (let ((*standard-output* *mh-utility-bit-bucket*))
375 (message "Compacting folder ...")
376 (mh "folder" (list folder "-fast" "-pack")))
377 (message "Maintaining consistency ...")
378 (let (hbufs)
379 (declare (list hbufs))
380 (do-headers-buffers (b folder)
381 (unless (or (eq b hdrs-buf) (eq b *new-mail-buffer*))
382 (push b hbufs)))
383 (dolist (b hbufs)
384 (delete-headers-buffer-and-message-buffers-command nil b))
385 (when hbufs
386 (new-message-headers folder (list "all"))))
387 (expunge-messages-fix-draft-buffers folder)
388 (unless (eq hdrs-buf *new-mail-buffer*)
389 (expunge-messages-fix-unseen-headers folder))
390 (delete-and-expunge-temp-drafts)))
391 (delete-headers-buffer-and-message-buffers-command nil hdrs-buf)))
392
393;;; DELETE-AND-EXPUNGE-TEMP-DRAFTS deletes all the messages in the
394;;; temporary draft folder if there is one defined. Any headers buffers
395;;; into this folder are deleted with their message buffers. We have to
396;;; create a list of buffers to delete since buffer deletion destructively
397;;; modifies the same list DO-HEADERS-BUFFERS uses. "rmm" is run without
398;;; error reporting since it signals an error if there are no messages to
399;;; delete. This function must return; for example, "Quit Headers" would
400;;; not complete successfully if this ended up calling EDITOR-ERROR.
401;;;
402(defun delete-and-expunge-temp-drafts ()
403 (let ((temp-draft-folder (value temporary-draft-folder)))
404 (when temp-draft-folder
405 (setf temp-draft-folder (coerce-folder-name temp-draft-folder))
406 (message "Deleting and expunging temporary drafts ...")
407 (when (mh "rmm" (list temp-draft-folder "all") :errorp nil)
408 (let (hdrs)
409 (declare (list hdrs))
410 (do-headers-buffers (b temp-draft-folder)
411 (push b hdrs))
412 (dolist (b hdrs)
413 (delete-headers-buffer-and-message-buffers-command nil b)))))))
414
415
416
417
418;;;; Message Mode.
419
420(defmode "Message" :major-p t)
421
422(defhvar "Message Information"
423 "This holds the information about the current message buffer."
424 :value nil)
425
426(defstruct message/draft-info
427 headers-mark) ;Mark pointing to a headers line in a headers buffer.
428
429(defstruct (message-info (:include message/draft-info)
430 (:print-function print-message-info))
431 folder ;String name of folder with leading MH "+".
432 msgs ;List of strings representing messages to be shown.
433 draft-buf ;Possible draft buffer reference.
434 keep) ;Whether message buffer may be re-used.
435
436(defun print-message-info (obj str n)
437 (declare (ignore n))
438 (format str "#<Message Info ~S ~S>"
439 (message-info-folder obj) (message-info-msgs obj)))
440
441
442(defcommand "Next Message" (p)
443 "Show the next message.
444 When in a message buffer, shows the next message in the associated headers
445 buffer. When in a headers buffer, moves point down a line and shows that
446 message."
447 "When in a message buffer, shows the next message in the associated headers
448 buffer. When in a headers buffer, moves point down a line and shows that
449 message."
450 (declare (ignore p))
451 (show-message-offset 1))
452
453(defcommand "Previous Message" (p)
454 "Show the previous message.
455 When in a message buffer, shows the previous message in the associated
456 headers buffer. When in a headers buffer, moves point up a line and shows
457 that message."
458 "When in a message buffer, shows the previous message in the associated
459 headers buffer. When in a headers buffer, moves point up a line and
460 shows that message."
461 (declare (ignore p))
462 (show-message-offset -1))
463
464(defcommand "Next Undeleted Message" (p)
465 "Show the next undeleted message.
466 When in a message buffer, shows the next undeleted message in the associated
467 headers buffer. When in a headers buffer, moves point down to a line
468 without a deleted message and shows that message."
469 "When in a message buffer, shows the next undeleted message in the associated
470 headers buffer. When in a headers buffer, moves point down to a line without
471 a deleted message and shows that message."
472 (declare (ignore p))
473 (show-message-offset 1 :undeleted))
474
475(defcommand "Previous Undeleted Message" (p)
476 "Show the previous undeleted message.
477 When in a message buffer, shows the previous undeleted message in the
478 associated headers buffer. When in a headers buffer, moves point up a line
479 without a deleted message and shows that message."
480 "When in a message buffer, shows the previous undeleted message in the
481 associated headers buffer. When in a headers buffer, moves point up a line
482 without a deleted message and shows that message."
483 (declare (ignore p))
484 (show-message-offset -1 :undeleted))
485
486(defun show-message-offset (offset &optional undeleted)
487 (let ((minfo (value message-information)))
488 (cond
489 ((not minfo)
490 (let ((hinfo (value headers-information)))
491 (unless hinfo (editor-error "Not in a message or headers buffer."))
492 (show-message-offset-hdrs-buf hinfo offset undeleted)))
493 ((message-info-keep minfo)
494 (let ((hbuf (value headers-buffer)))
495 (unless hbuf (editor-error "Not associated with a headers buffer."))
496 (let ((hinfo (variable-value 'headers-information :buffer hbuf))
497 (point (buffer-point hbuf)))
498 (move-mark point (message-info-headers-mark minfo))
499 (show-message-offset-hdrs-buf hinfo offset undeleted))))
500 (t
501 (show-message-offset-msg-buf minfo offset undeleted)))))
502
503(defun show-message-offset-hdrs-buf (hinfo offset undeleted)
504 (unless hinfo (editor-error "Not in a message or headers buffer."))
505 (unless (show-message-offset-mark (buffer-point (headers-info-buffer hinfo))
506 offset undeleted)
507 (editor-error "No ~:[previous~;next~] ~:[~;undeleted ~]message."
508 (plusp offset) undeleted))
509 (show-headers-message hinfo))
510
511(defun show-message-offset-msg-buf (minfo offset undeleted)
512 (let ((msg-mark (message-info-headers-mark minfo)))
513 (unless msg-mark (editor-error "Not associated with a headers buffer."))
514 (unless (show-message-offset-mark msg-mark offset undeleted)
515 (let ((hbuf (value headers-buffer))
516 (mbuf (current-buffer)))
517 (setf (current-buffer) hbuf)
518 (setf (window-buffer (current-window)) hbuf)
519 (delete-buffer-if-possible mbuf))
520 (editor-error "No ~:[previous~;next~] ~:[~;undeleted ~]message."
521 (plusp offset) undeleted))
522 (move-mark (buffer-point (line-buffer (mark-line msg-mark))) msg-mark)
523 (let* ((next-msg (line-message-id (mark-line msg-mark)))
524 (folder (message-info-folder minfo))
525 (mbuffer (current-buffer)))
526 (with-writable-buffer (mbuffer)
527 (delete-region (buffer-region mbuffer))
528 (setf (buffer-name mbuffer) (get-storable-msg-buf-name folder next-msg))
529 (setf (message-info-msgs minfo) next-msg)
530 (read-mh-file (merge-pathnames next-msg
531 (merge-relative-pathnames
532 (strip-folder-name folder)
533 (mh-directory-pathname)))
534 mbuffer)
535 (let ((unseen-seq (mh-profile-component "unseen-sequence")))
536 (when unseen-seq
537 (mark-one-message folder next-msg unseen-seq :delete))))))
538 (let ((dbuffer (message-info-draft-buf minfo)))
539 (when dbuffer
540 (delete-variable 'message-buffer :buffer dbuffer)
541 (setf (message-info-draft-buf minfo) nil))))
542
543(defun get-storable-msg-buf-name (folder msg)
544 (let ((name (format nil "Message ~A ~A" folder msg)))
545 (if (not (getstring name *buffer-names*))
546 name
547 (let ((n 2))
548 (loop
549 (setf name (format nil "Message ~A ~A copy ~D" folder msg n))
550 (unless (getstring name *buffer-names*)
551 (return name))
552 (incf n))))))
553
554(defun show-message-offset-mark (msg-mark offset undeleted)
555 (with-mark ((temp msg-mark))
556 (let ((winp
557 (cond (undeleted
558 (loop
559 (unless (and (line-offset temp offset 0)
560 (not (blank-line-p (mark-line temp))))
561 (return nil))
562 (unless (line-message-deleted (mark-line temp))
563 (return t))))
564 ((and (line-offset temp offset 0)
565 (not (blank-line-p (mark-line temp)))))
566 (t nil))))
567 (if winp (move-mark msg-mark temp)))))
568
569
570(defcommand "Show Message" (p)
571 "Shows the current message.
572 Prompts for a folder and message(s), displaying this in the current window.
573 When invoked in a headers buffer, shows the message on the current line."
574 "Show a message."
575 (declare (ignore p))
576 (let ((hinfo (value headers-information)))
577 (if hinfo
578 (show-headers-message hinfo)
579 (let ((folder (prompt-for-folder)))
580 (show-prompted-message folder (prompt-for-message :folder folder))))))
581
582;;; SHOW-HEADERS-MESSAGE shows the current message for hinfo. If there is a
583;;; main message buffer, clobber it, and we don't have to deal with kept
584;;; messages or draft associations since those operations should have moved
585;;; the message buffer into the others list. Remove the message from the
586;;; unseen sequence, and make sure the message buffer is displayed in some
587;;; window.
588;;;
589(defun show-headers-message (hinfo)
590 (multiple-value-bind (cur-msg cur-mark)
591 (headers-current-message hinfo)
592 (unless cur-msg (editor-error "Not on a header line."))
593 (let* ((mbuffer (headers-info-msg-buffer hinfo))
594 (folder (headers-info-folder hinfo))
595 (buf-name (get-storable-msg-buf-name folder cur-msg))
596 (writable nil))
597 (cond (mbuffer
598 (setf (buffer-name mbuffer) buf-name)
599 (setf writable (buffer-writable mbuffer))
600 (setf (buffer-writable mbuffer) t)
601 (delete-region (buffer-region mbuffer))
602 (let ((minfo (variable-value 'message-information :buffer mbuffer)))
603 (move-mark (message-info-headers-mark minfo) cur-mark)
604 (delete-mark cur-mark)
605 (setf (message-info-msgs minfo) cur-msg)))
606 (t (setf mbuffer (maybe-make-mh-buffer buf-name :message))
607 (setf (headers-info-msg-buffer hinfo) mbuffer)
608 (defhvar "Message Information"
609 "This holds the information about the current headers buffer."
610 :value (make-message-info :folder folder
611 :msgs cur-msg
612 :headers-mark cur-mark)
613 :buffer mbuffer)
614 (defhvar "Headers Buffer"
615 "This is bound in message and draft buffers to their
616 associated headers buffer."
617 :value (headers-info-buffer hinfo) :buffer mbuffer)))
618 (read-mh-file (merge-pathnames
619 cur-msg
620 (merge-relative-pathnames (strip-folder-name folder)
621 (mh-directory-pathname)))
622 mbuffer)
623 (setf (buffer-writable mbuffer) writable)
624 (let ((unseen-seq (mh-profile-component "unseen-sequence")))
625 (when unseen-seq (mark-one-message folder cur-msg unseen-seq :delete)))
626 (get-message-buffer-window mbuffer))))
627
628;;; SHOW-PROMPTED-MESSAGE takes an arbitrary message spec and blasts those
629;;; messages into a message buffer. First we pick the message to get them
630;;; individually specified as normalized message ID's -- all integers and
631;;; no funny names such as "last".
632;;;
633(defun show-prompted-message (folder msgs)
634 (let* ((msgs (pick-messages folder msgs nil))
635 (mbuffer (maybe-make-mh-buffer (format nil "Message ~A ~A" folder msgs)
636 :message)))
637 (defhvar "Message Information"
638 "This holds the information about the current headers buffer."
639 :value (make-message-info :folder folder :msgs msgs)
640 :buffer mbuffer)
641 (let ((*standard-output* (make-hemlock-output-stream (buffer-point mbuffer)
642 :full)))
643 (mh "show" `(,folder ,@msgs "-noshowproc" "-noheader"))
644 (setf (buffer-modified mbuffer) nil))
645 (buffer-start (buffer-point mbuffer))
646 (setf (buffer-writable mbuffer) nil)
647 (get-message-buffer-window mbuffer)))
648
649;;; GET-MESSAGE-BUFFER-WINDOW currently just changes to buffer, unless buffer
650;;; has any windows, in which case it uses the first one. It could prompt for
651;;; a window, split the current window, split the current window or use the
652;;; next one if there is one, funcall an Hvar. It could take a couple
653;;; arguments to control its behaviour. Whatever.
654;;;
655(defun get-message-buffer-window (mbuffer)
656 (let ((wins (buffer-windows mbuffer)))
657 (cond (wins
658 (setf (current-buffer) mbuffer)
659 (setf (current-window) (car wins)))
660 (t (change-to-buffer mbuffer)))))
661
662
663(defhvar "Scroll Message Showing Next"
664 "When this is set, \"Scroll Message\" shows the next message when the end
665 of the current message is visible."
666 :value t)
667
668(defcommand "Scroll Message" (p)
669 "Scroll the current window down through the current message.
670 If the end of the message is visible, then show the next undeleted message
671 if \"Scroll Message Showing Next\" is non-nil."
672 "Scroll the current window down through the current message."
673 (if (and (not p)
674 (displayed-p (buffer-end-mark (current-buffer)) (current-window))
675 (value scroll-message-showing-next))
676 (show-message-offset 1 :undeleted)
677 (scroll-window-down-command p)))
678
679
680(defcommand "Keep Message" (p)
681 "Keeps the current message buffer from being re-used. Also, if the buffer
682 would be deleted due to a draft completion, it will not be."
683 "Keeps the current message buffer from being re-used. Also, if the buffer
684 would be deleted due to a draft completion, it will not be."
685 (declare (ignore p))
686 (let ((minfo (value message-information)))
687 (unless minfo (editor-error "Not in a message buffer."))
688 (let ((hbuf (value headers-buffer)))
689 (when hbuf
690 (let ((mbuf (current-buffer))
691 (hinfo (variable-value 'headers-information :buffer hbuf)))
692 (when (eq (headers-info-msg-buffer hinfo) mbuf)
693 (setf (headers-info-msg-buffer hinfo) nil)
694 (push mbuf (headers-info-other-msg-bufs hinfo))))))
695 (setf (message-info-keep minfo) t)))
696
697(defcommand "Edit Message Buffer" (p)
698 "Recursively edit message buffer.
699 Puts the current message buffer into \"Text\" mode allowing modifications in
700 a recursive edit. While in this state, the buffer is associated with the
701 pathname of the message, so saving the file is possible."
702 "Puts the current message buffer into \"Text\" mode allowing modifications in
703 a recursive edit. While in this state, the buffer is associated with the
704 pathname of the message, so saving the file is possible."
705 (declare (ignore p))
706 (let* ((minfo (value message-information)))
707 (unless minfo (editor-error "Not in a message buffer."))
708 (let* ((msgs (message-info-msgs minfo))
709 (mbuf (current-buffer))
710 (mbuf-name (buffer-name mbuf))
711 (writable (buffer-writable mbuf))
712 (abortp t))
713 (when (consp msgs)
714 (editor-error
715 "There appears to be more than one message in this buffer."))
716 (unwind-protect
717 (progn
718 (setf (buffer-writable mbuf) t)
719 (setf (buffer-pathname mbuf)
720 (merge-pathnames
721 msgs
722 (merge-relative-pathnames
723 (strip-folder-name (message-info-folder minfo))
724 (mh-directory-pathname))))
725 (setf (buffer-major-mode mbuf) "Text")
726 (do-recursive-edit)
727 (setf abortp nil))
728 (when (and (not abortp)
729 (buffer-modified mbuf)
730 (prompt-for-y-or-n
731 :prompt "Message buffer modified, save it? "
732 :default t))
733 (save-file-command nil mbuf))
734 (setf (buffer-modified mbuf) nil)
735 ;; "Save File", which the user may have used, changes the buffer's name.
736 (unless (getstring mbuf-name *buffer-names*)
737 (setf (buffer-name mbuf) mbuf-name))
738 (setf (buffer-writable mbuf) writable)
739 (setf (buffer-pathname mbuf) nil)
740 (setf (buffer-major-mode mbuf) "Message")))))
741
742
743
744
745;;;; Draft Mode.
746
747(defmode "Draft")
748
749(defhvar "Draft Information"
750 "This holds the information about the current draft buffer."
751 :value nil)
752
753(defstruct (draft-info (:include message/draft-info)
754 (:print-function print-draft-info))
755 folder ;String name of draft folder with leading MH "+".
756 message ;String id of draft folder message.
757 pathname ;Pathname of draft in the draft folder directory.
758 delivered ;This is set when the draft was really sent.
759 replied-to-folder ;Folder of message draft is in reply to.
760 replied-to-msg) ;Message draft is in reply to.
761
762(defun print-draft-info (obj str n)
763 (declare (ignore n))
764 (format str "#<Draft Info ~A>" (draft-info-message obj)))
765
766
767(defhvar "Reply to Message Prefix Action"
768 "This is one of :cc-all, :no-cc-all, or nil. When an argument is supplied to
769 \"Reply to Message\", this value determines how arguments passed to the
770 MH utility."
771 :value nil)
772
773(defcommand "Reply to Message" (p)
774 "Sets up a draft in reply to the current message.
775 Prompts for a folder and message to reply to. When in a headers buffer,
776 replies to the message on the current line. When in a message buffer,
777 replies to that message. With an argument, regard \"Reply to Message Prefix
778 Action\" for carbon copy arguments to the MH utility."
779 "Prompts for a folder and message to reply to. When in a headers buffer,
780 replies to the message on the current line. When in a message buffer,
781 replies to that message."
782 (let ((hinfo (value headers-information))
783 (minfo (value message-information)))
784 (cond (hinfo
785 (multiple-value-bind (cur-msg cur-mark)
786 (headers-current-message hinfo)
787 (unless cur-msg (editor-error "Not on a header line."))
788 (setup-reply-draft (headers-info-folder hinfo)
789 cur-msg hinfo cur-mark p)))
790 (minfo
791 (setup-message-buffer-draft (current-buffer) minfo :reply p))
792 (t
793 (let ((folder (prompt-for-folder)))
794 (setup-reply-draft folder
795 (car (prompt-for-message :folder folder))
796 nil nil p))))))
797
798;;; SETUP-REPLY-DRAFT takes a folder and msg to draft a reply to. Optionally,
799;;; a headers buffer and mark are associated with the draft. First, the draft
800;;; buffer is associated with the headers buffer if there is one. Then the
801;;; message buffer is created and associated with the drafter buffer and
802;;; headers buffer. Argument may be used to pass in the argument from the
803;;; command.
804;;;
805(defun setup-reply-draft (folder msg &optional hinfo hmark argument)
806 (let* ((dbuffer (sub-setup-message-draft
807 "repl" :end-of-buffer
808 `(,folder ,msg
809 ,@(if argument
810 (case (value reply-to-message-prefix-action)
811 (:no-cc-all '("-nocc" "all"))
812 (:cc-all '("-cc" "all")))))))
813 (dinfo (variable-value 'draft-information :buffer dbuffer))
814 (h-buf (if hinfo (headers-info-buffer hinfo))))
815 (setf (draft-info-replied-to-folder dinfo) folder)
816 (setf (draft-info-replied-to-msg dinfo) msg)
817 (when h-buf
818 (defhvar "Headers Buffer"
819 "This is bound in message and draft buffers to their associated
820 headers buffer."
821 :value h-buf :buffer dbuffer)
822 (setf (draft-info-headers-mark dinfo) hmark)
823 (push dbuffer (headers-info-draft-bufs hinfo)))
824 (let ((msg-buf (maybe-make-mh-buffer (format nil "Message ~A ~A" folder msg)
825 :message)))
826 (defhvar "Message Information"
827 "This holds the information about the current headers buffer."
828 :value (make-message-info :folder folder :msgs msg
829 :headers-mark
830 (if h-buf (copy-mark hmark) hmark)
831 :draft-buf dbuffer)
832 :buffer msg-buf)
833 (when h-buf
834 (defhvar "Headers Buffer"
835 "This is bound in message and draft buffers to their associated
836 headers buffer."
837 :value h-buf :buffer msg-buf)
838 (push msg-buf (headers-info-other-msg-bufs hinfo)))
839 (read-mh-file (merge-pathnames
840 msg
841 (merge-relative-pathnames (strip-folder-name folder)
842 (mh-directory-pathname)))
843 msg-buf)
844 (setf (buffer-writable msg-buf) nil)
845 (defhvar "Message Buffer"
846 "This is bound in draft buffers to their associated message buffer."
847 :value msg-buf :buffer dbuffer))
848 (get-draft-buffer-window dbuffer)))
849
850
851(defcommand "Forward Message" (p)
852 "Forward current message.
853 Prompts for a folder and message to forward. When in a headers buffer,
854 forwards the message on the current line. When in a message buffer,
855 forwards that message."
856 "Prompts for a folder and message to reply to. When in a headers buffer,
857 replies to the message on the current line. When in a message buffer,
858 replies to that message."
859 (declare (ignore p))
860 (let ((hinfo (value headers-information))
861 (minfo (value message-information)))
862 (cond (hinfo
863 (multiple-value-bind (cur-msg cur-mark)
864 (headers-current-message hinfo)
865 (unless cur-msg (editor-error "Not on a header line."))
866 (setup-forward-draft (headers-info-folder hinfo)
867 cur-msg hinfo cur-mark)))
868 (minfo
869 (setup-message-buffer-draft (current-buffer) minfo :forward))
870 (t
871 (let ((folder (prompt-for-folder)))
872 (setup-forward-draft folder
873 (car (prompt-for-message :folder folder))))))))
874
875;;; SETUP-FORWARD-DRAFT sets up a draft forwarding folder's msg. When there
876;;; is a headers buffer involved (hinfo and hmark), the draft is associated
877;;; with it.
878;;;
879;;; This function is like SETUP-REPLY-DRAFT (in addition to "forw" and
880;;; :to-field), but it does not setup a message buffer. If this is added as
881;;; something forward drafts want, then SETUP-REPLY-DRAFT should be
882;;; parameterized and renamed.
883;;;
884(defun setup-forward-draft (folder msg &optional hinfo hmark)
885 (let* ((dbuffer (sub-setup-message-draft "forw" :to-field
886 (list folder msg)))
887 (dinfo (variable-value 'draft-information :buffer dbuffer))
888 (h-buf (if hinfo (headers-info-buffer hinfo))))
889 (when h-buf
890 (defhvar "Headers Buffer"
891 "This is bound in message and draft buffers to their associated
892 headers buffer."
893 :value h-buf :buffer dbuffer)
894 (setf (draft-info-headers-mark dinfo) hmark)
895 (push dbuffer (headers-info-draft-bufs hinfo)))
896 (get-draft-buffer-window dbuffer)))
897
898
899(defcommand "Send Message" (p)
900 "Setup a draft buffer.
901 Setup a draft buffer, reserving a draft folder message. When invoked in a
902 headers buffer, the current message is available in an associated message
903 buffer."
904 "Setup a draft buffer, reserving a draft folder message. When invoked in
905 a headers buffer, the current message is available in an associated
906 message buffer."
907 (declare (ignore p))
908 (let ((hinfo (value headers-information))
909 (minfo (value message-information)))
910 (cond (hinfo (setup-headers-message-draft hinfo))
911 (minfo (setup-message-buffer-draft (current-buffer) minfo :compose))
912 (t (setup-message-draft)))))
913
914(defun setup-message-draft ()
915 (get-draft-buffer-window (sub-setup-message-draft "comp" :to-field)))
916
917;;; SETUP-HEADERS-MESSAGE-DRAFT sets up a draft buffer associated with a
918;;; headers buffer and a message buffer. The headers current message is
919;;; inserted in the message buffer which is also associated with the headers
920;;; buffer. The draft buffer is associated with the message buffer.
921;;;
922(defun setup-headers-message-draft (hinfo)
923 (multiple-value-bind (cur-msg cur-mark)
924 (headers-current-message hinfo)
925 (unless cur-msg (message "Draft not associated with any message."))
926 (let* ((dbuffer (sub-setup-message-draft "comp" :to-field))
927 (dinfo (variable-value 'draft-information :buffer dbuffer))
928 (h-buf (headers-info-buffer hinfo)))
929 (when cur-msg
930 (defhvar "Headers Buffer"
931 "This is bound in message and draft buffers to their associated headers
932 buffer."
933 :value h-buf :buffer dbuffer)
934 (push dbuffer (headers-info-draft-bufs hinfo)))
935 (when cur-msg
936 (setf (draft-info-headers-mark dinfo) cur-mark)
937 (let* ((folder (headers-info-folder hinfo))
938 (msg-buf (maybe-make-mh-buffer
939 (format nil "Message ~A ~A" folder cur-msg)
940 :message)))
941 (defhvar "Message Information"
942 "This holds the information about the current headers buffer."
943 :value (make-message-info :folder folder :msgs cur-msg
944 :headers-mark (copy-mark cur-mark)
945 :draft-buf dbuffer)
946 :buffer msg-buf)
947 (defhvar "Headers Buffer"
948 "This is bound in message and draft buffers to their associated
949 headers buffer."
950 :value h-buf :buffer msg-buf)
951 (push msg-buf (headers-info-other-msg-bufs hinfo))
952 (read-mh-file (merge-pathnames
953 cur-msg
954 (merge-relative-pathnames (strip-folder-name folder)
955 (mh-directory-pathname)))
956 msg-buf)
957 (setf (buffer-writable msg-buf) nil)
958 (defhvar "Message Buffer"
959 "This is bound in draft buffers to their associated message buffer."
960 :value msg-buf :buffer dbuffer)))
961 (get-draft-buffer-window dbuffer))))
962
963;;; SETUP-MESSAGE-BUFFER-DRAFT takes a message buffer and its message
964;;; information. A draft buffer is created according to type, and the two
965;;; buffers are associated. Any previous association of the message buffer and
966;;; a draft buffer is dropped. Any association between the message buffer and
967;;; a headers buffer is propagated to the draft buffer, and if the message
968;;; buffer is the headers buffer's main message buffer, it is moved to "other"
969;;; status. Argument may be used to pass in the argument from the command.
970;;;
971(defun setup-message-buffer-draft (msg-buf minfo type &optional argument)
972 (let* ((msgs (message-info-msgs minfo))
973 (cur-msg (if (consp msgs) (car msgs) msgs))
974 (folder (message-info-folder minfo))
975 (dbuffer
976 (ecase type
977 (:reply
978 (sub-setup-message-draft
979 "repl" :end-of-buffer
980 `(,folder ,cur-msg
981 ,@(if argument
982 (case (value reply-to-message-prefix-action)
983 (:no-cc-all '("-nocc" "all"))
984 (:cc-all '("-cc" "all")))))))
985 (:compose
986 (sub-setup-message-draft "comp" :to-field))
987 (:forward
988 (sub-setup-message-draft "forw" :to-field
989 (list folder cur-msg)))))
990 (dinfo (variable-value 'draft-information :buffer dbuffer)))
991 (when (message-info-draft-buf minfo)
992 (delete-variable 'message-buffer :buffer (message-info-draft-buf minfo)))
993 (setf (message-info-draft-buf minfo) dbuffer)
994 (when (eq type :reply)
995 (setf (draft-info-replied-to-folder dinfo) folder)
996 (setf (draft-info-replied-to-msg dinfo) cur-msg))
997 (when (hemlock-bound-p 'headers-buffer :buffer msg-buf)
998 (let* ((hbuf (variable-value 'headers-buffer :buffer msg-buf))
999 (hinfo (variable-value 'headers-information :buffer hbuf)))
1000 (defhvar "Headers Buffer"
1001 "This is bound in message and draft buffers to their associated
1002 headers buffer."
1003 :value hbuf :buffer dbuffer)
1004 (setf (draft-info-headers-mark dinfo)
1005 (copy-mark (message-info-headers-mark minfo)))
1006 (push dbuffer (headers-info-draft-bufs hinfo))
1007 (when (eq (headers-info-msg-buffer hinfo) msg-buf)
1008 (setf (headers-info-msg-buffer hinfo) nil)
1009 (push msg-buf (headers-info-other-msg-bufs hinfo)))))
1010 (defhvar "Message Buffer"
1011 "This is bound in draft buffers to their associated message buffer."
1012 :value msg-buf :buffer dbuffer)
1013 (get-draft-buffer-window dbuffer)))
1014
1015(defvar *draft-to-pattern*
1016 (new-search-pattern :string-insensitive :forward "To:"))
1017
1018(defun sub-setup-message-draft (utility point-action &optional args)
1019 (mh utility `(,@args "-nowhatnowproc"))
1020 (let* ((folder (mh-draft-folder))
1021 (draft-msg (mh-current-message folder))
1022 (msg-pn (merge-pathnames draft-msg (mh-draft-folder-pathname)))
1023 (dbuffer (maybe-make-mh-buffer (format nil "Draft ~A" draft-msg)
1024 :draft)))
1025 (read-mh-file msg-pn dbuffer)
1026 (setf (buffer-pathname dbuffer) msg-pn)
1027 (defhvar "Draft Information"
1028 "This holds the information about the current draft buffer."
1029 :value (make-draft-info :folder (coerce-folder-name folder)
1030 :message draft-msg
1031 :pathname msg-pn)
1032 :buffer dbuffer)
1033 (let ((point (buffer-point dbuffer)))
1034 (ecase point-action
1035 (:to-field
1036 (when (find-pattern point *draft-to-pattern*)
1037 (line-end point)))
1038 (:end-of-buffer (buffer-end point))))
1039 dbuffer))
1040
1041(defun read-mh-file (pathname buffer)
1042 (unless (probe-file pathname)
1043 (editor-error "No such message -- ~A" (namestring pathname)))
1044 (read-file pathname (buffer-point buffer))
1045 (setf (buffer-write-date buffer) (file-write-date pathname))
1046 (buffer-start (buffer-point buffer))
1047 (setf (buffer-modified buffer) nil))
1048
1049
1050(defvar *draft-buffer-window-fun* 'change-to-buffer
1051 "This is called by GET-DRAFT-BUFFER-WINDOW to display a new draft buffer.
1052 The default is CHANGE-TO-BUFFER which uses the current window.")
1053
1054;;; GET-DRAFT-BUFFER-WINDOW is called to display a new draft buffer.
1055;;;
1056(defun get-draft-buffer-window (dbuffer)
1057 (funcall *draft-buffer-window-fun* dbuffer))
1058
1059
1060(defcommand "Reply to Message in Other Window" (p)
1061 "Reply to message, creating another window for draft buffer.
1062 Prompts for a folder and message to reply to. When in a headers buffer,
1063 replies to the message on the current line. When in a message buffer,
1064 replies to that message. The current window is split displaying the draft
1065 buffer in the new window and the message buffer in the current."
1066 "Prompts for a folder and message to reply to. When in a headers buffer,
1067 replies to the message on the current line. When in a message buffer,
1068 replies to that message."
1069 (let ((*draft-buffer-window-fun* #'draft-buffer-in-other-window))
1070 (reply-to-message-command p)))
1071
1072(defun draft-buffer-in-other-window (dbuffer)
1073 (when (hemlock-bound-p 'message-buffer :buffer dbuffer)
1074 (let ((mbuf (variable-value 'message-buffer :buffer dbuffer)))
1075 (when (not (eq (current-buffer) mbuf))
1076 (change-to-buffer mbuf))))
1077 (setf (current-buffer) dbuffer)
1078 (setf (current-window) (make-window (buffer-start-mark dbuffer)))
1079 (defhvar "Split Window Draft"
1080 "Indicates window needs to be cleaned up for draft."
1081 :value t :buffer dbuffer))
1082
1083(defhvar "Deliver Message Confirm"
1084 "When set, \"Deliver Message\" will ask for confirmation before sending the
1085 draft. This is off by default since \"Deliver Message\" is not bound to
1086 any key by default."
1087 :value t)
1088
1089(defcommand "Deliver Message" (p)
1090 "Save and deliver the current draft buffer.
1091 When in a draft buffer, this saves the file and uses SEND to deliver the
1092 draft. Otherwise, this prompts for a draft message id, invoking SEND."
1093 "When in a draft buffer, this saves the file and uses SEND to deliver the
1094 draft. Otherwise, this prompts for a draft message id, invoking SEND."
1095 (declare (ignore p))
1096 (let ((dinfo (value draft-information)))
1097 (cond (dinfo
1098 (deliver-draft-buffer-message dinfo))
1099 (t
1100 (let* ((folder (coerce-folder-name (mh-draft-folder)))
1101 (msg (prompt-for-message :folder folder)))
1102 (mh "send" `("-draftfolder" ,folder "-draftmessage" ,@msg)))))))
1103
1104(defun deliver-draft-buffer-message (dinfo)
1105 (when (draft-info-delivered dinfo)
1106 (editor-error "This draft has already been delivered."))
1107 (when (or (not (value deliver-message-confirm))
1108 (prompt-for-y-or-n :prompt "Deliver message? " :default t))
1109 (let ((dbuffer (current-buffer)))
1110 (when (buffer-modified dbuffer)
1111 (write-buffer-file dbuffer (buffer-pathname dbuffer)))
1112 (message "Delivering draft ...")
1113 (mh "send" `("-draftfolder" ,(draft-info-folder dinfo)
1114 "-draftmessage" ,(draft-info-message dinfo)))
1115 (setf (draft-info-delivered dinfo) t)
1116 (let ((replied-folder (draft-info-replied-to-folder dinfo))
1117 (replied-msg (draft-info-replied-to-msg dinfo)))
1118 (when replied-folder
1119 (message "Annotating message being replied to ...")
1120 (mh "anno" `(,replied-folder ,replied-msg "-component" "replied"))
1121 (do-headers-buffers (hbuf replied-folder)
1122 (with-headers-mark (hmark hbuf replied-msg)
1123 (mark-to-note-replied-msg hmark)
1124 (with-writable-buffer (hbuf)
1125 (setf (next-character hmark) #\A))))
1126 (dolist (b *buffer-list*)
1127 (when (and (hemlock-bound-p 'message-information :buffer b)
1128 (buffer-modeline-field-p b :replied-to-message))
1129 (dolist (w (buffer-windows b))
1130 (update-modeline-field b w :replied-to-message))))))
1131 (maybe-delete-extra-draft-window dbuffer (current-window))
1132 (let ((mbuf (value message-buffer)))
1133 (when (and mbuf
1134 (not (hemlock-bound-p 'netnews-message-info :buffer mbuf)))
1135 (let ((minfo (variable-value 'message-information :buffer mbuf)))
1136 (when (and minfo (not (message-info-keep minfo)))
1137 (delete-buffer-if-possible mbuf)))))
1138 (delete-buffer-if-possible dbuffer))))
1139
1140(defcommand "Delete Draft and Buffer" (p)
1141 "Delete the current draft and associated message and buffer."
1142 "Delete the current draft and associated message and buffer."
1143 (declare (ignore p))
1144 (let ((dinfo (value draft-information))
1145 (dbuffer (current-buffer)))
1146 (unless dinfo (editor-error "No draft associated with buffer."))
1147 (maybe-delete-extra-draft-window dbuffer (current-window))
1148 (delete-file (draft-info-pathname dinfo))
1149 (let ((mbuf (value message-buffer)))
1150 (when (and mbuf
1151 (not (hemlock-bound-p 'netnews-message-info :buffer mbuf)))
1152 (let ((minfo (variable-value 'message-information :buffer mbuf)))
1153 (when (and minfo (not (message-info-keep minfo)))
1154 (delete-buffer-if-possible mbuf)))))
1155 (delete-buffer-if-possible dbuffer)))
1156
1157;;; MAYBE-DELETE-EXTRA-DRAFT-WINDOW -- Internal.
1158;;;
1159;;; This takes a draft buffer and a window into it that should not be deleted.
1160;;; If "Split Window Draft" is bound in the buffer, and there are at least two
1161;;; windows in dbuffer-window's group, then we delete some window. Blow away
1162;;; the variable, so we don't think this is still a split window draft buffer.
1163;;;
1164(defun maybe-delete-extra-draft-window (dbuffer dbuffer-window)
1165 (when (and (hemlock-bound-p 'split-window-draft :buffer dbuffer)
1166 ;; Since we know bitmap devices have window groups, this loop is
1167 ;; more correct than testing the length of *window-list* and
1168 ;; accounting for *echo-area-window* being in there.
1169 (do ((start dbuffer-window)
1170 (count 1 (1+ count))
1171 (w (next-window dbuffer-window) (next-window w)))
1172 ((eq start w) (> count 1))))
1173 (delete-window (next-window dbuffer-window))
1174 (delete-variable 'split-window-draft :buffer dbuffer)))
1175
1176(defcommand "Remail Message" (p)
1177 "Prompts for a folder and message to remail. Prompts for a resend-to
1178 address string and resend-cc address string. When in a headers buffer,
1179 remails the message on the current line. When in a message buffer,
1180 remails that message."
1181 "Prompts for a folder and message to remail. Prompts for a resend-to
1182 address string and resend-cc address string. When in a headers buffer,
1183 remails the message on the current line. When in a message buffer,
1184 remails that message."
1185 (declare (ignore p))
1186 (let ((hinfo (value headers-information))
1187 (minfo (value message-information)))
1188 (cond (hinfo
1189 (multiple-value-bind (cur-msg cur-mark)
1190 (headers-current-message hinfo)
1191 (unless cur-msg (editor-error "Not on a header line."))
1192 (delete-mark cur-mark)
1193 (remail-message (headers-info-folder hinfo) cur-msg
1194 (prompt-for-string :prompt "Resend To: ")
1195 (prompt-for-string :prompt "Resend Cc: "))))
1196 (minfo
1197 (remail-message (message-info-folder minfo)
1198 (message-info-msgs minfo)
1199 (prompt-for-string :prompt "Resend To: ")
1200 (prompt-for-string :prompt "Resend Cc: ")))
1201 (t
1202 (let ((folder (prompt-for-folder)))
1203 (remail-message folder
1204 (car (prompt-for-message :folder folder))
1205 (prompt-for-string :prompt "Resend To: ")
1206 (prompt-for-string :prompt "Resend Cc: "))))))
1207 (message "Message remailed."))
1208
1209
1210;;; REMAIL-MESSAGE claims a draft folder message with "dist". This is then
1211;;; sucked into a buffer and modified by inserting the supplied addresses.
1212;;; "send" is used to deliver the draft, but it requires certain evironment
1213;;; variables to make it do the right thing. "mhdist" says the draft is only
1214;;; remailing information, and "mhaltmsg" is the message to send. "mhannotate"
1215;;; must be set due to a bug in MH's "send"; it will not notice the "mhdist"
1216;;; flag unless there is some message to be annotated. This command does not
1217;;; provide for annotation of the remailed message.
1218;;;
1219(defun remail-message (folder msg resend-to resend-cc)
1220 (mh "dist" `(,folder ,msg "-nowhatnowproc"))
1221 (let* ((draft-folder (mh-draft-folder))
1222 (draft-msg (mh-current-message draft-folder)))
1223 (setup-remail-draft-message draft-msg resend-to resend-cc)
1224 (mh "send" `("-draftfolder" ,draft-folder "-draftmessage" ,draft-msg)
1225 :environment
1226 `((:|mhdist| . "1")
1227 (:|mhannotate| . "1")
1228 (:|mhaltmsg| . ,(namestring
1229 (merge-pathnames msg (merge-relative-pathnames
1230 (strip-folder-name folder)
1231 (mh-directory-pathname)))))))))
1232
1233;;; SETUP-REMAIL-DRAFT-MESSAGE takes a draft folder and message that have been
1234;;; created with the MH "dist" utility. A buffer is created with this
1235;;; message's pathname, searching for "resent-to:" and "resent-cc:", filling in
1236;;; the supplied argument values. After writing out the results, the buffer
1237;;; is deleted.
1238;;;
1239(defvar *draft-resent-to-pattern*
1240 (new-search-pattern :string-insensitive :forward "resent-to:"))
1241(defvar *draft-resent-cc-pattern*
1242 (new-search-pattern :string-insensitive :forward "resent-cc:"))
1243
1244(defun setup-remail-draft-message (msg resend-to resend-cc)
1245 (let* ((msg-pn (merge-pathnames msg (mh-draft-folder-pathname)))
1246 (dbuffer (maybe-make-mh-buffer (format nil "Draft ~A" msg)
1247 :draft))
1248 (point (buffer-point dbuffer)))
1249 (read-mh-file msg-pn dbuffer)
1250 (when (find-pattern point *draft-resent-to-pattern*)
1251 (line-end point)
1252 (insert-string point resend-to))
1253 (buffer-start point)
1254 (when (find-pattern point *draft-resent-cc-pattern*)
1255 (line-end point)
1256 (insert-string point resend-cc))
1257 (write-file (buffer-region dbuffer) msg-pn :keep-backup nil)
1258 ;; The draft buffer delete hook expects this to be bound.
1259 (defhvar "Draft Information"
1260 "This holds the information about the current draft buffer."
1261 :value :ignore
1262 :buffer dbuffer)
1263 (delete-buffer dbuffer)))
1264
1265
1266
1267
1268;;;; Message and Draft Stuff.
1269
1270(defhvar "Headers Buffer"
1271 "This is bound in message and draft buffers to their associated headers
1272 buffer."
1273 :value nil)
1274
1275(defcommand "Goto Headers Buffer" (p)
1276 "Selects associated headers buffer if it exists.
1277 The headers buffer's point is moved to the appropriate line, pushing a
1278 buffer mark where point was."
1279 "Selects associated headers buffer if it exists."
1280 (declare (ignore p))
1281 (let ((h-buf (value headers-buffer)))
1282 (unless h-buf (editor-error "No associated headers buffer."))
1283 (let ((info (or (value message-information) (value draft-information))))
1284 (change-to-buffer h-buf)
1285 (push-buffer-mark (copy-mark (current-point)))
1286 (move-mark (current-point) (message/draft-info-headers-mark info)))))
1287
1288(defhvar "Message Buffer"
1289 "This is bound in draft buffers to their associated message buffer."
1290 :value nil)
1291
1292(defcommand "Goto Message Buffer" (p)
1293 "Selects associated message buffer if it exists."
1294 "Selects associated message buffer if it exists."
1295 (declare (ignore p))
1296 (let ((msg-buf (value message-buffer)))
1297 (unless msg-buf (editor-error "No associated message buffer."))
1298 (change-to-buffer msg-buf)))
1299
1300
1301(defhvar "Message Insertion Prefix"
1302 "This is a fill prefix that is used when inserting text from a message buffer
1303 into a draft buffer by \"Insert Message Region\". It defaults to three
1304 spaces."
1305 :value " ")
1306
1307(defhvar "Message Insertion Column"
1308 "This is a fill column that is used when inserting text from a message buffer
1309 into a draft buffer by \"Insert Message Region\"."
1310 :value 75)
1311
1312(defcommand "Insert Message Region" (p)
1313 "Copy the current region into the associated draft or post buffer. When
1314 in a message buffer that has an associated draft or post buffer, the
1315 current active region is copied into the draft or post buffer. It is
1316 filled using \"Message Insertion Prefix\" and \"Message Insertion
1317 Column\". If an argument is supplied, the filling is inhibited.
1318 If both a draft buffer and post buffer are associated with this, then it
1319 is inserted into the draft buffer."
1320 "When in a message buffer that has an associated draft or post buffer,
1321 the current active region is copied into the post or draft buffer. It is
1322 filled using \"Message Insertion Prefix\" and \"Message Insertion
1323 Column\". If an argument is supplied, the filling is inhibited."
1324 (let* ((minfo (value message-information))
1325 (nm-info (if (hemlock-bound-p 'netnews-message-info)
1326 (value netnews-message-info)))
1327 (post-buffer (and nm-info (nm-info-post-buffer nm-info)))
1328 (post-info (and post-buffer
1329 (variable-value 'post-info :buffer post-buffer)))
1330 dbuf kind)
1331 (cond (minfo
1332 (setf kind :mail)
1333 (setf dbuf (message-info-draft-buf minfo)))
1334 (nm-info
1335 (setf kind :netnews)
1336 (setf dbuf (or (nm-info-draft-buffer nm-info)
1337 (nm-info-post-buffer nm-info))))
1338 (t (editor-error "Not in a netnews message or message buffer.")))
1339 (unless dbuf
1340 (editor-error "Message buffer not associated with any draft or post ~
1341 buffer."))
1342 (let* ((region (copy-region (current-region)))
1343 (dbuf-point (buffer-point dbuf))
1344 (dbuf-mark (copy-mark dbuf-point)))
1345 (cond ((and (eq kind :mail)
1346 (hemlock-bound-p 'split-window-draft :buffer dbuf)
1347 (> (length (the list *window-list*)) 2)
1348 (buffer-windows dbuf))
1349 (setf (current-buffer) dbuf
1350 (current-window) (car (buffer-windows dbuf))))
1351 ((and (eq kind :netnews)
1352 (and (member (post-info-message-window post-info)
1353 *window-list*)
1354 (member (post-info-reply-window post-info)
1355 *window-list*)))
1356 (setf (current-buffer) dbuf
1357 (current-window) (post-info-reply-window post-info)))
1358 (t (change-to-buffer dbuf)))
1359 (push-buffer-mark dbuf-mark)
1360 (ninsert-region dbuf-point region)
1361 (unless p
1362 (fill-region-by-paragraphs (region dbuf-mark dbuf-point)
1363 (value message-insertion-prefix)
1364 (value message-insertion-column)))))
1365 (setf (last-command-type) :ephemerally-active))
1366
1367
1368(defhvar "Message Buffer Insertion Prefix"
1369 "This is a line prefix that is inserted at the beginning of every line in
1370 a message buffer when inserting those lines into a draft buffer with
1371 \"Insert Message Buffer\". It defaults to four spaces."
1372 :value " ")
1373
1374(defcommand "Insert Message Buffer" (p)
1375 "Insert entire (associated) message buffer into (associated) draft or
1376 post buffer. When in a draft or post buffer with an associated message
1377 buffer, or when in a message buffer that has an associated draft or post
1378 buffer, the message buffer is inserted into the draft buffer. When
1379 there are both an associated draft and post buffer, the text is inserted
1380 into the draft buffer. Each inserted line is modified by prefixing it
1381 with \"Message Buffer Insertion Prefix\". If an argument is supplied
1382 the prefixing is inhibited."
1383 "When in a draft or post buffer with an associated message buffer, or
1384 when in a message buffer that has an associated draft or post buffer, the
1385 message buffer is inserted into the draft buffer. Each inserted line is
1386 modified by prefixing it with \"Message Buffer Insertion Prefix\". If an
1387 argument is supplied the prefixing is inhibited."
1388 (let ((minfo (value message-information))
1389 (dinfo (value draft-information))
1390 mbuf dbuf message-kind)
1391 (cond (minfo
1392 (setf message-kind :mail)
1393 (setf dbuf (message-info-draft-buf minfo))
1394 (unless dbuf
1395 (editor-error
1396 "Message buffer not associated with any draft buffer."))
1397 (setf mbuf (current-buffer))
1398 (change-to-buffer dbuf))
1399 (dinfo
1400 (setf message-kind :mail)
1401 (setf mbuf (value message-buffer))
1402 (unless mbuf
1403 (editor-error
1404 "Draft buffer not associated with any message buffer."))
1405 (setf dbuf (current-buffer)))
1406 ((hemlock-bound-p 'netnews-message-info)
1407 (setf message-kind :netnews)
1408 (setf mbuf (current-buffer))
1409 (let ((nm-info (value netnews-message-info)))
1410 (setf dbuf (or (nm-info-draft-buffer nm-info)
1411 (nm-info-post-buffer nm-info)))
1412 (unless dbuf
1413 (editor-error "Message buffer not associated with any draft ~
1414 or post buffer.")))
1415 (change-to-buffer dbuf))
1416 ((hemlock-bound-p 'post-info)
1417 (setf message-kind :netnews)
1418 (let ((post-info (value post-info)))
1419 (setf mbuf (post-info-message-buffer post-info))
1420 (unless mbuf
1421 (editor-error "Post buffer not associated with any message ~
1422 buffer.")))
1423 (setf dbuf (current-buffer)))
1424 (t (editor-error "Not in a draft, message, news-message, or post ~
1425 buffer.")))
1426 (let* ((dbuf-point (buffer-point dbuf))
1427 (dbuf-mark (copy-mark dbuf-point)))
1428 (push-buffer-mark dbuf-mark)
1429 (insert-region dbuf-point (buffer-region mbuf))
1430 (unless p
1431 (let ((prefix (value message-buffer-insertion-prefix)))
1432 (with-mark ((temp dbuf-mark :left-inserting))
1433 (loop
1434 (when (mark>= temp dbuf-point) (return))
1435 (insert-string temp prefix)
1436 (unless (line-offset temp 1 0) (return)))))))
1437 (ecase message-kind
1438 (:mail
1439 (insert-message-buffer-cleanup-split-draft dbuf mbuf))
1440 (:netnews
1441 (nn-reply-cleanup-split-windows dbuf))))
1442 (setf (last-command-type) :ephemerally-active))
1443
1444;;; INSERT-MESSAGE-BUFFER-CLEANUP-SPLIT-DRAFT tries to delete an extra window
1445;;; due to "Reply to Message in Other Window". Since we just inserted the
1446;;; message buffer in the draft buffer, we don't need the other window into
1447;;; the message buffer.
1448;;;
1449(defun insert-message-buffer-cleanup-split-draft (dbuf mbuf)
1450 (when (and (hemlock-bound-p 'split-window-draft :buffer dbuf)
1451 (> (length (the list *window-list*)) 2))
1452 (let ((win (car (buffer-windows mbuf))))
1453 (cond
1454 (win
1455 (when (eq win (current-window))
1456 (let ((dwin (car (buffer-windows dbuf))))
1457 (unless dwin
1458 (editor-error "Couldn't fix windows for split window draft."))
1459 (setf (current-buffer) dbuf)
1460 (setf (current-window) dwin)))
1461 (delete-window win))
1462 (t ;; This happens when invoked with the message buffer current.
1463 (let ((dwins (buffer-windows dbuf)))
1464 (when (> (length (the list dwins)) 1)
1465 (delete-window (find-if #'(lambda (w)
1466 (not (eq w (current-window))))
1467 dwins)))))))
1468 (delete-variable 'split-window-draft :buffer dbuf)))
1469
1470
1471;;; CLEANUP-MESSAGE-BUFFER is called when a buffer gets deleted. It cleans
1472;;; up references to a message buffer.
1473;;;
1474(defun cleanup-message-buffer (buffer)
1475 (let ((minfo (variable-value 'message-information :buffer buffer)))
1476 (when (hemlock-bound-p 'headers-buffer :buffer buffer)
1477 (let* ((hinfo (variable-value 'headers-information
1478 :buffer (variable-value 'headers-buffer
1479 :buffer buffer)))
1480 (msg-buf (headers-info-msg-buffer hinfo)))
1481 (if (eq msg-buf buffer)
1482 (setf (headers-info-msg-buffer hinfo) nil)
1483 (setf (headers-info-other-msg-bufs hinfo)
1484 (delete buffer (headers-info-other-msg-bufs hinfo)
1485 :test #'eq))))
1486 (delete-mark (message-info-headers-mark minfo))
1487 ;;
1488 ;; Do this for MAYBE-MAKE-MH-BUFFER since it isn't necessary for GC.
1489 (delete-variable 'headers-buffer :buffer buffer))
1490 (when (message-info-draft-buf minfo)
1491 (delete-variable 'message-buffer
1492 :buffer (message-info-draft-buf minfo)))))
1493
1494;;; CLEANUP-DRAFT-BUFFER is called when a buffer gets deleted. It cleans
1495;;; up references to a draft buffer.
1496;;;
1497(defun cleanup-draft-buffer (buffer)
1498 (let ((dinfo (variable-value 'draft-information :buffer buffer)))
1499 (when (hemlock-bound-p 'headers-buffer :buffer buffer)
1500 (let* ((hinfo (variable-value 'headers-information
1501 :buffer (variable-value 'headers-buffer
1502 :buffer buffer))))
1503 (setf (headers-info-draft-bufs hinfo)
1504 (delete buffer (headers-info-draft-bufs hinfo) :test #'eq))
1505 (delete-mark (draft-info-headers-mark dinfo))))
1506 (when (hemlock-bound-p 'message-buffer :buffer buffer)
1507 (setf (message-info-draft-buf
1508 (variable-value 'message-information
1509 :buffer (variable-value 'message-buffer
1510 :buffer buffer)))
1511 nil))))
1512
1513;;; CLEANUP-HEADERS-BUFFER is called when a buffer gets deleted. It cleans
1514;;; up references to a headers buffer.
1515;;;
1516(defun cleanup-headers-buffer (buffer)
1517 (let* ((hinfo (variable-value 'headers-information :buffer buffer))
1518 (msg-buf (headers-info-msg-buffer hinfo)))
1519 (when msg-buf
1520 (cleanup-headers-reference
1521 msg-buf (variable-value 'message-information :buffer msg-buf)))
1522 (dolist (b (headers-info-other-msg-bufs hinfo))
1523 (cleanup-headers-reference
1524 b (variable-value 'message-information :buffer b)))
1525 (dolist (b (headers-info-draft-bufs hinfo))
1526 (cleanup-headers-reference
1527 b (variable-value 'draft-information :buffer b)))))
1528
1529(defun cleanup-headers-reference (buffer info)
1530 (delete-mark (message/draft-info-headers-mark info))
1531 (setf (message/draft-info-headers-mark info) nil)
1532 (delete-variable 'headers-buffer :buffer buffer)
1533 (when (typep info 'draft-info)
1534 (setf (draft-info-replied-to-folder info) nil)
1535 (setf (draft-info-replied-to-msg info) nil)))
1536
1537;;; REVAMP-HEADERS-BUFFER cleans up a headers buffer for immediate re-use.
1538;;; After deleting the buffer's region, there will be one line in the buffer
1539;;; because of how Hemlock regions work, so we have to delete that line's
1540;;; plist. Then we clean up any references to the buffer and delete the
1541;;; main message buffer. The other message buffers are left alone assuming
1542;;; they are on the "others" list because they are being used in some
1543;;; particular way (for example, a draft buffer refers to one or the user has
1544;;; kept it). Then some slots of the info structure are set to nil.
1545;;;
1546(defun revamp-headers-buffer (hbuffer hinfo)
1547 (delete-region (buffer-region hbuffer))
1548 (setf (line-plist (mark-line (buffer-point hbuffer))) nil)
1549 (let ((msg-buf (headers-info-msg-buffer hinfo)))
1550 ;; Deleting the buffer sets the slot to nil.
1551 (when msg-buf (delete-buffer-if-possible msg-buf))
1552 (cleanup-headers-buffer hbuffer))
1553 (setf (headers-info-other-msg-bufs hinfo) nil)
1554 (setf (headers-info-draft-bufs hinfo) nil)
1555 (setf (headers-info-msg-seq hinfo) nil)
1556 (setf (headers-info-msg-strings hinfo) nil))
1557
1558
1559
1560
1561;;;; Incorporating new mail.
1562
1563(defhvar "New Mail Folder"
1564 "This is the folder new mail is incorporated into."
1565 :value "+inbox")
1566
1567(defcommand "Incorporate New Mail" (p)
1568 "Incorporates new mail into \"New Mail Folder\", displaying INC output in
1569 a pop-up window."
1570 "Incorporates new mail into \"New Mail Folder\", displaying INC output in
1571 a pop-up window."
1572 (declare (ignore p))
1573 (with-pop-up-display (s)
1574 (incorporate-new-mail s)))
1575
1576(defhvar "Unseen Headers Message Spec"
1577 "This is an MH message spec suitable any message prompt. It is used to
1578 supply headers for the unseen headers buffer, in addition to the
1579 unseen-sequence name that is taken from the user's MH profile, when
1580 incorporating new mail and after expunging. This value is a string."
1581 :value nil)
1582
1583(defcommand "Incorporate and Read New Mail" (p)
1584 "Incorporates new mail and generates a headers buffer.
1585 Incorporates new mail into \"New Mail Folder\", and creates a headers buffer
1586 with the new messages. To use this, you must define an unseen- sequence in
1587 your profile. Each time this is invoked the unseen-sequence is SCAN'ed, and
1588 the headers buffer's contents are replaced."
1589 "Incorporates new mail into \"New Mail Folder\", and creates a headers
1590 buffer with the new messages. This buffer will be appended to with
1591 successive uses of this command."
1592 (declare (ignore p))
1593 (let ((unseen-seq (mh-profile-component "unseen-sequence")))
1594 (unless unseen-seq
1595 (editor-error "No unseen-sequence defined in MH profile."))
1596 (incorporate-new-mail)
1597 (let* ((folder (value new-mail-folder))
1598 ;; Stash current message before fetching unseen headers.
1599 (cur-msg (mh-current-message folder))
1600 (region (get-new-mail-msg-hdrs folder unseen-seq)))
1601 ;; Fetch message headers before possibly making buffer in case we error.
1602 (when (not (and *new-mail-buffer*
1603 (member *new-mail-buffer* *buffer-list* :test #'eq)))
1604 (let ((name (format nil "Unseen Headers ~A" folder)))
1605 (when (getstring name *buffer-names*)
1606 (editor-error "There already is a buffer named ~S!" name))
1607 (setf *new-mail-buffer*
1608 (make-buffer name :modes (list "Headers")
1609 :delete-hook '(new-mail-buf-delete-hook)))
1610 (setf (buffer-writable *new-mail-buffer*) nil)))
1611 (cond ((hemlock-bound-p 'headers-information
1612 :buffer *new-mail-buffer*)
1613 (let ((hinfo (variable-value 'headers-information
1614 :buffer *new-mail-buffer*)))
1615 (unless (string= (headers-info-folder hinfo) folder)
1616 (editor-error
1617 "An unseen headers buffer already exists but into another ~
1618 folder. Your mail has already been incorporated into the ~
1619 specified folder."))
1620 (with-writable-buffer (*new-mail-buffer*)
1621 (revamp-headers-buffer *new-mail-buffer* hinfo))
1622 ;; Restore the name in case someone used "Pick Headers".
1623 (setf (buffer-name *new-mail-buffer*)
1624 (format nil "Unseen Headers ~A" folder))
1625 (insert-new-mail-message-headers hinfo region cur-msg)))
1626 (t
1627 (let ((hinfo (make-headers-info :buffer *new-mail-buffer*
1628 :folder folder)))
1629 (defhvar "Headers Information"
1630 "This holds the information about the current headers buffer."
1631 :value hinfo :buffer *new-mail-buffer*)
1632 (insert-new-mail-message-headers hinfo region cur-msg)))))))
1633
1634;;; NEW-MAIL-BUF-DELETE-HOOK is invoked whenever the new mail buffer is
1635;;; deleted.
1636;;;
1637(defun new-mail-buf-delete-hook (buffer)
1638 (declare (ignore buffer))
1639 (setf *new-mail-buffer* nil))
1640
1641;;; GET-NEW-MAIL-MSG-HDRS takes a folder and the unseen-sequence name. It
1642;;; returns a region with the unseen message headers and any headers due to
1643;;; the "Unseen Headers Message Spec" variable.
1644;;;
1645(defun get-new-mail-msg-hdrs (folder unseen-seq)
1646 (let* ((unseen-headers-message-spec (value unseen-headers-message-spec))
1647 (other-msgs (if unseen-headers-message-spec
1648 (breakup-message-spec
1649 (string-trim '(#\space #\tab)
1650 unseen-headers-message-spec))))
1651 (msg-spec (cond ((null other-msgs)
1652 (list unseen-seq))
1653 ((member unseen-seq other-msgs :test #'string=)
1654 other-msgs)
1655 (t (cons unseen-seq other-msgs)))))
1656 (message-headers-to-region folder msg-spec)))
1657
1658;;; INSERT-NEW-MAIL-MESSAGE-HEADERS inserts region in the new mail buffer.
1659;;; Then we look for the header line with cur-msg id, moving point there.
1660;;; There may have been unseen messages before incorporating new mail, and
1661;;; cur-msg should be the first new message. Then we either switch to the
1662;;; new mail headers, or show the current message.
1663;;;
1664(defun insert-new-mail-message-headers (hinfo region cur-msg)
1665 (declare (simple-string cur-msg))
1666 (with-writable-buffer (*new-mail-buffer*)
1667 (insert-message-headers *new-mail-buffer* hinfo region))
1668 (let ((point (buffer-point *new-mail-buffer*)))
1669 (buffer-start point)
1670 (with-headers-mark (cur-mark *new-mail-buffer* cur-msg)
1671 (move-mark point cur-mark)))
1672 (change-to-buffer *new-mail-buffer*))
1673
1674
1675(defhvar "Incorporate New Mail Hook"
1676 "Functions on this hook are invoked immediately after new mail is
1677 incorporated."
1678 :value nil)
1679
1680(defun incorporate-new-mail (&optional stream)
1681 "Incorporates new mail, passing INC's output to stream. When stream is
1682 nil, output is flushed."
1683 (unless (new-mail-p) (editor-error "No new mail."))
1684 (let ((args `(,(coerce-folder-name (value new-mail-folder))
1685 ,@(if stream nil '("-silent"))
1686 "-form" ,(namestring (truename (value mh-scan-line-form)))
1687 "-width" ,(number-string (value fill-column)))))
1688 (message "Incorporating new mail ...")
1689 (mh "inc" args))
1690 (when (value incorporate-new-mail-hook)
1691 (message "Invoking new mail hooks ..."))
1692 (invoke-hook incorporate-new-mail-hook))
1693
1694
1695
1696
1697;;;; Deletion.
1698
1699(defhvar "Virtual Message Deletion"
1700 "When set, \"Delete Message\" merely MARK's a message into the
1701 \"hemlockdeleted\" sequence; otherwise, RMM is invoked."
1702 :value t)
1703
1704(defcommand "Delete Message and Show Next" (p)
1705 "Delete message and show next undeleted message.
1706 This command is only valid in a headers buffer or a message buffer
1707 associated with some headers buffer. The current message is deleted, and
1708 the next undeleted one is shown."
1709 "Delete the current message and show the next undeleted one."
1710 (declare (ignore p))
1711 (let ((hinfo (value headers-information))
1712 (minfo (value message-information)))
1713 (cond (hinfo
1714 (multiple-value-bind (cur-msg cur-mark)
1715 (headers-current-message hinfo)
1716 (unless cur-msg (editor-error "Not on a header line."))
1717 (delete-mark cur-mark)
1718 (delete-message (headers-info-folder hinfo) cur-msg)))
1719 (minfo
1720 (delete-message (message-info-folder minfo)
1721 (message-info-msgs minfo)))
1722 (t
1723 (editor-error "Not in a headers or message buffer."))))
1724 (show-message-offset 1 :undeleted))
1725
1726(defcommand "Delete Message and Down Line" (p)
1727 "Deletes the current message, moving point to the next line.
1728 When in a headers buffer, deletes the message on the current line. Then it
1729 moves point to the next non-blank line."
1730 "Deletes current message and moves point down a line."
1731 (declare (ignore p))
1732 (let ((hinfo (value headers-information)))
1733 (unless hinfo (editor-error "Not in a headers buffer."))
1734 (multiple-value-bind (cur-msg cur-mark)
1735 (headers-current-message hinfo)
1736 (unless cur-msg (editor-error "Not on a header line."))
1737 (delete-message (headers-info-folder hinfo) cur-msg)
1738 (when (line-offset cur-mark 1)
1739 (unless (blank-line-p (mark-line cur-mark))
1740 (move-mark (current-point) cur-mark)))
1741 (delete-mark cur-mark))))
1742
1743;;; "Delete Message" unlike "Headers Delete Message" cannot know for sure
1744;;; which message id's have been deleted, so when virtual message deletion
1745;;; is not used, we cannot use DELETE-HEADERS-BUFFER-LINE to keep headers
1746;;; buffers consistent. However, the message id's in the buffer (if deleted)
1747;;; will generate MH errors if operations are attempted with them, and
1748;;; if the user ever packs the folder with "Expunge Messages", the headers
1749;;; buffer will be updated.
1750;;;
1751(defcommand "Delete Message" (p)
1752 "Prompts for a folder, messages to delete, and pick expression. When in
1753 a headers buffer into the same folder specified, the messages prompt
1754 defaults to those messages in the buffer; \"all\" may be entered if this is
1755 not what is desired. When \"Virtual Message Deletion\" is set, messages are
1756 only MARK'ed for deletion. See \"Expunge Messages\". When this feature is
1757 not used, headers and message buffers message id's my not be consistent
1758 with MH."
1759 "Prompts for a folder and message to delete. When \"Virtual Message
1760 Deletion\" is set, messages are only MARK'ed for deletion. See \"Expunge
1761 Messages\"."
1762 (declare (ignore p))
1763 (let* ((folder (prompt-for-folder))
1764 (hinfo (value headers-information))
1765 (temp-msgs (prompt-for-message
1766 :folder folder
1767 :messages
1768 (if (and hinfo
1769 (string= folder
1770 (the simple-string
1771 (headers-info-folder hinfo))))
1772 (headers-info-msg-strings hinfo))
1773 :prompt "MH messages to pick from: "))
1774 (pick-exp (prompt-for-pick-expression))
1775 (msgs (pick-messages folder temp-msgs pick-exp))
1776 (virtually (value virtual-message-deletion)))
1777 (declare (simple-string folder))
1778 (if virtually
1779 (mh "mark" `(,folder ,@msgs "-sequence" "hemlockdeleted" "-add"))
1780 (mh "rmm" `(,folder ,@msgs)))
1781 (if virtually
1782 (let ((deleted-seq (mh-sequence-list folder "hemlockdeleted")))
1783 (when deleted-seq
1784 (do-headers-buffers (hbuf folder)
1785 (with-writable-buffer (hbuf)
1786 (note-deleted-headers hbuf deleted-seq)))))
1787 (do-headers-buffers (hbuf folder hinfo)
1788 (do-headers-lines (hbuf :line-var line :mark-var hmark)
1789 (when (member (line-message-id line) msgs :test #'string=)
1790 (delete-headers-buffer-line hinfo hmark)))))))
1791
1792(defcommand "Headers Delete Message" (p)
1793 "Delete current message.
1794 When in a headers buffer, deletes the message on the current line. When
1795 in a message buffer, deletes that message. When \"Virtual Message
1796 Deletion\" is set, messages are only MARK'ed for deletion. See \"Expunge
1797 Messages\"."
1798 "When in a headers buffer, deletes the message on the current line. When
1799 in a message buffer, deletes that message. When \"Virtual Message
1800 Deletion\" is set, messages are only MARK'ed for deletion. See \"Expunge
1801 Messages\"."
1802 (declare (ignore p))
1803 (let ((hinfo (value headers-information))
1804 (minfo (value message-information)))
1805 (cond (hinfo
1806 (multiple-value-bind (cur-msg cur-mark)
1807 (headers-current-message hinfo)
1808 (unless cur-msg (editor-error "Not on a header line."))
1809 (delete-mark cur-mark)
1810 (delete-message (headers-info-folder hinfo) cur-msg)))
1811 (minfo
1812 (let ((msgs (message-info-msgs minfo)))
1813 (delete-message (message-info-folder minfo)
1814 (if (consp msgs) (car msgs) msgs)))
1815 (message "Message deleted."))
1816 (t (editor-error "Not in a headers or message buffer.")))))
1817
1818;;; DELETE-MESSAGE takes a folder and message id and either flags this message
1819;;; for deletion or deletes it. All headers buffers into folder are updated,
1820;;; either by flagging a headers line or deleting it.
1821;;;
1822(defun delete-message (folder msg)
1823 (cond ((value virtual-message-deletion)
1824 (mark-one-message folder msg "hemlockdeleted" :add)
1825 (do-headers-buffers (hbuf folder)
1826 (with-headers-mark (hmark hbuf msg)
1827 (with-writable-buffer (hbuf)
1828 (note-deleted-message-at-mark hmark)))))
1829 (t (mh "rmm" (list folder msg))
1830 (do-headers-buffers (hbuf folder hinfo)
1831 (with-headers-mark (hmark hbuf msg)
1832 (delete-headers-buffer-line hinfo hmark)))))
1833 (dolist (b *buffer-list*)
1834 (when (and (hemlock-bound-p 'message-information :buffer b)
1835 (buffer-modeline-field-p b :deleted-message))
1836 (dolist (w (buffer-windows b))
1837 (update-modeline-field b w :deleted-message)))))
1838
1839;;; NOTE-DELETED-MESSAGE-AT-MARK takes a mark at the beginning of a valid
1840;;; headers line, sticks a "D" on the line, and frobs the line's deleted
1841;;; property. This assumes the headers buffer is modifiable.
1842;;;
1843(defun note-deleted-message-at-mark (mark)
1844 (find-attribute mark :digit)
1845 (find-attribute mark :digit #'zerop)
1846 (character-offset mark 2)
1847 (setf (next-character mark) #\D)
1848 (setf (line-message-deleted (mark-line mark)) t))
1849
1850;;; DELETE-HEADERS-BUFFER-LINE takes a headers information and a mark on the
1851;;; line to be deleted. Before deleting the line, we check to see if any
1852;;; message or draft buffers refer to the buffer because of the line. Due
1853;;; to how regions are deleted, line plists get messed up, so they have to
1854;;; be regenerated. We regenerate them for the whole buffer, so we don't have
1855;;; to hack the code to know which lines got messed up.
1856;;;
1857(defun delete-headers-buffer-line (hinfo hmark)
1858 (delete-headers-line-references hinfo hmark)
1859 (let ((id (line-message-id (mark-line hmark)))
1860 (hbuf (headers-info-buffer hinfo)))
1861 (with-writable-buffer (hbuf)
1862 (with-mark ((end (line-start hmark) :left-inserting))
1863 (unless (line-offset end 1 0) (buffer-end end))
1864 (delete-region (region hmark end))))
1865 (let ((seq (mh-sequence-delete id (headers-info-msg-seq hinfo))))
1866 (setf (headers-info-msg-seq hinfo) seq)
1867 (setf (headers-info-msg-strings hinfo) (mh-sequence-strings seq)))
1868 (set-message-headers-ids hbuf)
1869 (when (value virtual-message-deletion)
1870 (let ((deleted-seq (mh-sequence-list (headers-info-folder hinfo)
1871 "hemlockdeleted")))
1872 (do-headers-lines (hbuf :line-var line)
1873 (setf (line-message-deleted line)
1874 (mh-sequence-member-p (line-message-id line) deleted-seq)))))))
1875
1876
1877;;; DELETE-HEADERS-LINE-REFERENCES removes any message buffer or draft buffer
1878;;; pointers to a headers buffer or marks into the headers buffer. Currently
1879;;; message buffers and draft buffers are identified differently for no good
1880;;; reason; probably message buffers should be located in the same way draft
1881;;; buffers are. Also, we currently assume only one of other-msg-bufs could
1882;;; refer to the line (similarly for draft-bufs), but this might be bug
1883;;; prone. The message buffer case couldn't happen since the buffer name
1884;;; would cause MAYBE-MAKE-MH-BUFFER to re-use the buffer, but you could reply
1885;;; to the same message twice simultaneously.
1886;;;
1887(defun delete-headers-line-references (hinfo hmark)
1888 (let ((msg-id (line-message-id (mark-line hmark)))
1889 (main-msg-buf (headers-info-msg-buffer hinfo)))
1890 (declare (simple-string msg-id))
1891 (when main-msg-buf
1892 (let ((minfo (variable-value 'message-information :buffer main-msg-buf)))
1893 (when (string= (the simple-string (message-info-msgs minfo))
1894 msg-id)
1895 (cond ((message-info-draft-buf minfo)
1896 (cleanup-headers-reference main-msg-buf minfo)
1897 (setf (headers-info-msg-buffer hinfo) nil))
1898 (t (delete-buffer-if-possible main-msg-buf))))))
1899 (dolist (mbuf (headers-info-other-msg-bufs hinfo))
1900 (let ((minfo (variable-value 'message-information :buffer mbuf)))
1901 (when (string= (the simple-string (message-info-msgs minfo))
1902 msg-id)
1903 (cond ((message-info-draft-buf minfo)
1904 (cleanup-headers-reference mbuf minfo)
1905 (setf (headers-info-other-msg-bufs hinfo)
1906 (delete mbuf (headers-info-other-msg-bufs hinfo)
1907 :test #'eq)))
1908 (t (delete-buffer-if-possible mbuf)))
1909 (return)))))
1910 (dolist (dbuf (headers-info-draft-bufs hinfo))
1911 (let ((dinfo (variable-value 'draft-information :buffer dbuf)))
1912 (when (same-line-p (draft-info-headers-mark dinfo) hmark)
1913 (cleanup-headers-reference dbuf dinfo)
1914 (setf (headers-info-draft-bufs hinfo)
1915 (delete dbuf (headers-info-draft-bufs hinfo) :test #'eq))
1916 (return)))))
1917
1918
1919(defcommand "Undelete Message" (p)
1920 "Prompts for a folder, messages to undelete, and pick expression. When in
1921 a headers buffer into the same folder specified, the messages prompt
1922 defaults to those messages in the buffer; \"all\" may be entered if this is
1923 not what is desired. This command is only meaningful if you have
1924 \"Virtual Message Deletion\" set."
1925 "Prompts for a folder, messages to undelete, and pick expression. When in
1926 a headers buffer into the same folder specified, the messages prompt
1927 defaults to those messages in the buffer; \"all\" may be entered if this is
1928 not what is desired. This command is only meaningful if you have
1929 \"Virtual Message Deletion\" set."
1930 (declare (ignore p))
1931 (unless (value virtual-message-deletion)
1932 (editor-error "You don't use virtual message deletion."))
1933 (let* ((folder (prompt-for-folder))
1934 (hinfo (value headers-information))
1935 (temp-msgs (prompt-for-message
1936 :folder folder
1937 :messages
1938 (if (and hinfo
1939 (string= folder
1940 (the simple-string
1941 (headers-info-folder hinfo))))
1942 (headers-info-msg-strings hinfo))
1943 :prompt "MH messages to pick from: "))
1944 (pick-exp (prompt-for-pick-expression))
1945 (msgs (if pick-exp
1946 (or (pick-messages folder temp-msgs pick-exp) temp-msgs)
1947 temp-msgs)))
1948 (declare (simple-string folder))
1949 (mh "mark" `(,folder ,@msgs "-sequence" "hemlockdeleted" "-delete"))
1950 (let ((deleted-seq (mh-sequence-list folder "hemlockdeleted")))
1951 (do-headers-buffers (hbuf folder)
1952 (with-writable-buffer (hbuf)
1953 (do-headers-lines (hbuf :line-var line :mark-var hmark)
1954 (when (and (line-message-deleted line)
1955 (not (mh-sequence-member-p (line-message-id line)
1956 deleted-seq)))
1957 (note-undeleted-message-at-mark hmark))))))))
1958
1959(defcommand "Headers Undelete Message" (p)
1960 "Undelete the current message.
1961 When in a headers buffer, undeletes the message on the current line. When
1962 in a message buffer, undeletes that message. This command is only
1963 meaningful if you have \"Virtual Message Deletion\" set."
1964 "When in a headers buffer, undeletes the message on the current line. When
1965 in a message buffer, undeletes that message. This command is only
1966 meaningful if you have \"Virtual Message Deletion\" set."
1967 (declare (ignore p))
1968 (unless (value virtual-message-deletion)
1969 (editor-error "You don't use virtual message deletion."))
1970 (let ((hinfo (value headers-information))
1971 (minfo (value message-information)))
1972 (cond (hinfo
1973 (multiple-value-bind (cur-msg cur-mark)
1974 (headers-current-message hinfo)
1975 (unless cur-msg (editor-error "Not on a header line."))
1976 (delete-mark cur-mark)
1977 (undelete-message (headers-info-folder hinfo) cur-msg)))
1978 (minfo
1979 (undelete-message (message-info-folder minfo)
1980 (message-info-msgs minfo))
1981 (message "Message undeleted."))
1982 (t (editor-error "Not in a headers or message buffer.")))))
1983
1984;;; UNDELETE-MESSAGE takes a folder and a message id. All headers buffers into
1985;;; folder are updated.
1986;;;
1987(defun undelete-message (folder msg)
1988 (mark-one-message folder msg "hemlockdeleted" :delete)
1989 (do-headers-buffers (hbuf folder)
1990 (with-headers-mark (hmark hbuf msg)
1991 (with-writable-buffer (hbuf)
1992 (note-undeleted-message-at-mark hmark))))
1993 (dolist (b *buffer-list*)
1994 (when (and (hemlock-bound-p 'message-information :buffer b)
1995 (buffer-modeline-field-p b :deleted-message))
1996 (dolist (w (buffer-windows b))
1997 (update-modeline-field b w :deleted-message)))))
1998
1999;;; NOTE-UNDELETED-MESSAGE-AT-MARK takes a mark at the beginning of a valid
2000;;; headers line, sticks a space on the line in place of a "D", and frobs the
2001;;; line's deleted property. This assumes the headers buffer is modifiable.
2002;;;
2003(defun note-undeleted-message-at-mark (hmark)
2004 (find-attribute hmark :digit)
2005 (find-attribute hmark :digit #'zerop)
2006 (character-offset hmark 2)
2007 (setf (next-character hmark) #\space)
2008 (setf (line-message-deleted (mark-line hmark)) nil))
2009
2010
2011(defcommand "Expunge Messages" (p)
2012 "Expunges messages marked for deletion.
2013 This command prompts for a folder, invoking RMM on the \"hemlockdeleted\"
2014 sequence after asking the user for confirmation. Setting \"Quit Headers
2015 Confirm\" to nil inhibits prompting. The folder's message id's are packed
2016 with FOLDER -pack. When in a headers buffer, uses that folder. When in a
2017 message buffer, uses its folder, updating any associated headers buffer.
2018 When \"Temporary Draft Folder\" is bound, this folder's messages are deleted
2019 and expunged."
2020 "Prompts for a folder, invoking RMM on the \"hemlockdeleted\" sequence and
2021 packing the message id's with FOLDER -pack. When in a headers buffer,
2022 uses that folder."
2023 (declare (ignore p))
2024 (let* ((hinfo (value headers-information))
2025 (minfo (value message-information))
2026 (folder (cond (hinfo (headers-info-folder hinfo))
2027 (minfo (message-info-folder minfo))
2028 (t (prompt-for-folder))))
2029 (deleted-seq (mh-sequence-list folder "hemlockdeleted")))
2030 ;;
2031 ;; Delete the messages if there are any.
2032 ;; This deletes "hemlockdeleted" from sequence file; we don't have to.
2033 (when (and deleted-seq
2034 (or (not (value expunge-messages-confirm))
2035 (prompt-for-y-or-n
2036 :prompt (list "Expunge messages and pack folder ~A? "
2037 folder)
2038 :default t
2039 :default-string "Y")))
2040 (message "Deleting messages ...")
2041 (mh "rmm" (list folder "hemlockdeleted"))
2042 ;;
2043 ;; Compact the message id's after deletion.
2044 (let ((*standard-output* *mh-utility-bit-bucket*))
2045 (message "Compacting folder ...")
2046 (mh "folder" (list folder "-fast" "-pack")))
2047 ;;
2048 ;; Do a bunch of consistency maintenance.
2049 (let ((new-buf-p (eq (current-buffer) *new-mail-buffer*)))
2050 (message "Maintaining consistency ...")
2051 (expunge-messages-fold-headers-buffers folder)
2052 (expunge-messages-fix-draft-buffers folder)
2053 (expunge-messages-fix-unseen-headers folder)
2054 (when new-buf-p (change-to-buffer *new-mail-buffer*)))
2055 (delete-and-expunge-temp-drafts))))
2056
2057;;; EXPUNGE-MESSAGES-FOLD-HEADERS-BUFFERS deletes all headers buffers into the
2058;;; compacted folder. We can only update the headers buffers by installing all
2059;;; headers, so there may as well be only one such buffer. First we get a list
2060;;; of the buffers since DO-HEADERS-BUFFERS is trying to iterate over a list
2061;;; being destructively modified by buffer deletions.
2062;;;
2063(defun expunge-messages-fold-headers-buffers (folder)
2064 (let (hbufs)
2065 (declare (list hbufs))
2066 (do-headers-buffers (b folder)
2067 (unless (eq b *new-mail-buffer*)
2068 (push b hbufs)))
2069 (unless (zerop (length hbufs))
2070 (dolist (b hbufs)
2071 (delete-headers-buffer-and-message-buffers-command nil b))
2072 (new-message-headers folder (list "all")))))
2073
2074;;; EXPUNGE-MESSAGES-FIX-DRAFT-BUFFERS finds any draft buffer that was set up
2075;;; as a reply to some message in folder, removing this relationship in case
2076;;; that message id does not exist after expunge folder compaction.
2077;;;
2078(defun expunge-messages-fix-draft-buffers (folder)
2079 (declare (simple-string folder))
2080 (dolist (b *buffer-list*)
2081 (when (hemlock-bound-p 'draft-information :buffer b)
2082 (let* ((dinfo (variable-value 'draft-information :buffer b))
2083 (reply-folder (draft-info-replied-to-folder dinfo)))
2084 (when (and reply-folder
2085 (string= (the simple-string reply-folder) folder))
2086 (setf (draft-info-replied-to-folder dinfo) nil)
2087 (setf (draft-info-replied-to-msg dinfo) nil))))))
2088
2089;;; EXPUNGE-MESSAGES-FIX-UNSEEN-HEADERS specially handles the unseen headers
2090;;; buffer apart from the other headers buffers into the same folder when
2091;;; messages have been expunged. We must delete the associated message buffers
2092;;; since REVAMP-HEADERS-BUFFER does not, and these potentially reference bad
2093;;; message id's. When doing this we must copy the other-msg-bufs list since
2094;;; the delete buffer cleanup hook for them is destructive. Then we check for
2095;;; more unseen messages.
2096;;;
2097(defun expunge-messages-fix-unseen-headers (folder)
2098 (declare (simple-string folder))
2099 (when *new-mail-buffer*
2100 (let ((hinfo (variable-value 'headers-information
2101 :buffer *new-mail-buffer*)))
2102 (when (string= (the simple-string (headers-info-folder hinfo))
2103 folder)
2104 (let ((other-bufs (copy-list (headers-info-other-msg-bufs hinfo))))
2105 (dolist (b other-bufs) (delete-buffer-if-possible b)))
2106 (with-writable-buffer (*new-mail-buffer*)
2107 (revamp-headers-buffer *new-mail-buffer* hinfo)
2108 ;; Restore the name in case someone used "Pick Headers".
2109 (setf (buffer-name *new-mail-buffer*)
2110 (format nil "Unseen Headers ~A" folder))
2111 (let ((region (maybe-get-new-mail-msg-hdrs folder)))
2112 (when region
2113 (insert-message-headers *new-mail-buffer* hinfo region))))))))
2114
2115;;; MAYBE-GET-NEW-MAIL-MSG-HDRS returns a region suitable for a new mail buffer
2116;;; or nil. Folder is probed for unseen headers, and if there are some, then
2117;;; we call GET-NEW-MAIL-MSG-HDRS which also uses "Unseen Headers Message Spec".
2118;;; If there are no unseen headers, we only look for "Unseen Headers Message
2119;;; Spec" messages. We go through these contortions to keep MH from outputting
2120;;; errors.
2121;;;
2122(defun maybe-get-new-mail-msg-hdrs (folder)
2123 (let ((unseen-seq-name (mh-profile-component "unseen-sequence")))
2124 (multiple-value-bind (unseen-seq foundp)
2125 (mh-sequence-list folder unseen-seq-name)
2126 (if (and foundp unseen-seq)
2127 (get-new-mail-msg-hdrs folder unseen-seq-name)
2128 (let ((spec (value unseen-headers-message-spec)))
2129 (when spec
2130 (message-headers-to-region
2131 folder
2132 (breakup-message-spec (string-trim '(#\space #\tab) spec)))))))))
2133
2134
2135
2136
2137;;;; Folders.
2138
2139(defvar *folder-name-table* nil)
2140
2141(defun check-folder-name-table ()
2142 (unless *folder-name-table*
2143 (message "Finding folder names ...")
2144 (setf *folder-name-table* (make-string-table))
2145 (let* ((output (with-output-to-string (*standard-output*)
2146 (mh "folders" '("-fast"))))
2147 (length (length output))
2148 (start 0))
2149 (declare (simple-string output))
2150 (loop
2151 (when (> start length) (return))
2152 (let ((nl (position #\newline output :start start)))
2153 (unless nl (return))
2154 (unless (= start nl)
2155 (setf (getstring (subseq output start nl) *folder-name-table*) t))
2156 (setf start (1+ nl)))))))
2157
2158(defcommand "List Folders" (p)
2159 "Pop up a list of folders at top-level."
2160 "Pop up a list of folders at top-level."
2161 (declare (ignore p))
2162 (check-folder-name-table)
2163 (with-pop-up-display (s)
2164 (do-strings (f ignore *folder-name-table*)
2165 (declare (ignore ignore))
2166 (write-line f s))))
2167
2168(defcommand "Create Folder" (p)
2169 "Creates a folder. If the folder already exists, an error is signaled."
2170 "Creates a folder. If the folder already exists, an error is signaled."
2171 (declare (ignore p))
2172 (let ((folder (prompt-for-folder :must-exist nil)))
2173 (when (folder-existsp folder)
2174 (editor-error "Folder already exists -- ~S!" folder))
2175 (create-folder folder)))
2176
2177(defcommand "Delete Folder" (p)
2178 "Prompts for a folder and uses RMF to delete it."
2179 "Prompts for a folder and uses RMF to delete it."
2180 (declare (ignore p))
2181 (let* ((folder (prompt-for-folder))
2182 (*standard-output* *mh-utility-bit-bucket*))
2183 (mh "rmf" (list folder))
2184 ;; RMF doesn't recognize this documented switch.
2185 ;; "-nointeractive"))))
2186 (check-folder-name-table)
2187 (delete-string (strip-folder-name folder) *folder-name-table*)))
2188
2189
2190(defvar *refile-default-destination* nil)
2191
2192(defcommand "Refile Message" (p)
2193 "Prompts for a source folder, messages, pick expression, and a destination
2194 folder to refile the messages."
2195 "Prompts for a source folder, messages, pick expression, and a destination
2196 folder to refile the messages."
2197 (declare (ignore p))
2198 (let* ((src-folder (prompt-for-folder :prompt "Source folder: "))
2199 (hinfo (value headers-information))
2200 (temp-msgs (prompt-for-message
2201 :folder src-folder
2202 :messages
2203 (if (and hinfo
2204 (string= src-folder
2205 (the simple-string
2206 (headers-info-folder hinfo))))
2207 (headers-info-msg-strings hinfo))
2208 :prompt "MH messages to pick from: "))
2209 (pick-exp (prompt-for-pick-expression))
2210 ;; Return pick result or temp-msgs individually specified in a list.
2211 (msgs (pick-messages src-folder temp-msgs pick-exp)))
2212 (declare (simple-string src-folder))
2213 (refile-message src-folder msgs
2214 (prompt-for-folder :must-exist nil
2215 :prompt "Destination folder: "
2216 :default *refile-default-destination*))))
2217
2218(defcommand "Headers Refile Message" (p)
2219 "Refile the current message.
2220 When in a headers buffer, refiles the message on the current line, and when
2221 in a message buffer, refiles that message, prompting for a destination
2222 folder."
2223 "When in a headers buffer, refiles the message on the current line, and when
2224 in a message buffer, refiles that message, prompting for a destination
2225 folder."
2226 (declare (ignore p))
2227 (let ((hinfo (value headers-information))
2228 (minfo (value message-information)))
2229 (cond (hinfo
2230 (multiple-value-bind (cur-msg cur-mark)
2231 (headers-current-message hinfo)
2232 (unless cur-msg (editor-error "Not on a header line."))
2233 (delete-mark cur-mark)
2234 (refile-message (headers-info-folder hinfo) cur-msg
2235 (prompt-for-folder
2236 :must-exist nil
2237 :prompt "Destination folder: "
2238 :default *refile-default-destination*))))
2239 (minfo
2240 (refile-message
2241 (message-info-folder minfo) (message-info-msgs minfo)
2242 (prompt-for-folder :must-exist nil
2243 :prompt "Destination folder: "
2244 :default *refile-default-destination*))
2245 (message "Message refiled."))
2246 (t
2247 (editor-error "Not in a headers or message buffer.")))))
2248
2249;;; REFILE-MESSAGE refiles msg from src-folder to dst-folder. If dst-buffer
2250;;; doesn't exist, the user is prompted for creating it. All headers buffers
2251;;; concerning src-folder are updated. When msg is a list, we did a general
2252;;; message prompt, and we cannot know which headers lines to delete.
2253;;;
2254(defun refile-message (src-folder msg dst-folder)
2255 (unless (folder-existsp dst-folder)
2256 (cond ((prompt-for-y-or-n
2257 :prompt "Destination folder doesn't exist. Create it? "
2258 :default t :default-string "Y")
2259 (create-folder dst-folder))
2260 (t (editor-error "Not refiling message."))))
2261 (mh "refile" `(,@(if (listp msg) msg (list msg))
2262 "-src" ,src-folder ,dst-folder))
2263 (setf *refile-default-destination* (strip-folder-name dst-folder))
2264 (if (listp msg)
2265 (do-headers-buffers (hbuf src-folder hinfo)
2266 (do-headers-lines (hbuf :line-var line :mark-var hmark)
2267 (when (member (line-message-id line) msg :test #'string=)
2268 (delete-headers-buffer-line hinfo hmark))))
2269 (do-headers-buffers (hbuf src-folder hinfo)
2270 (with-headers-mark (hmark hbuf msg)
2271 (delete-headers-buffer-line hinfo hmark)))))
2272
2273
2274
2275
2276;;;; Miscellaneous commands.
2277
2278(defcommand "Mark Message" (p)
2279 "Prompts for a folder, message, and sequence. By default the message is
2280 added, but if an argument is supplied, the message is deleted. When in
2281 a headers buffer or message buffer, only a sequence is prompted for."
2282 "Prompts for a folder, message, and sequence. By default the message is
2283 added, but if an argument is supplied, the message is deleted. When in
2284 a headers buffer or message buffer, only a sequence is prompted for."
2285 (let* ((hinfo (value headers-information))
2286 (minfo (value message-information)))
2287 (cond (hinfo
2288 (multiple-value-bind (cur-msg cur-mark)
2289 (headers-current-message hinfo)
2290 (unless cur-msg (editor-error "Not on a header line."))
2291 (delete-mark cur-mark)
2292 (let ((seq-name (prompt-for-string :prompt "Sequence name: "
2293 :trim t)))
2294 (declare (simple-string seq-name))
2295 (when (string= "" seq-name)
2296 (editor-error "Sequence name cannot be empty."))
2297 (mark-one-message (headers-info-folder hinfo)
2298 cur-msg seq-name (if p :delete :add)))))
2299 (minfo
2300 (let ((msgs (message-info-msgs minfo))
2301 (seq-name (prompt-for-string :prompt "Sequence name: "
2302 :trim t)))
2303 (declare (simple-string seq-name))
2304 (when (string= "" seq-name)
2305 (editor-error "Sequence name cannot be empty."))
2306 (mark-one-message (message-info-folder minfo)
2307 (if (consp msgs) (car msgs) msgs)
2308 seq-name (if p :delete :add))))
2309 (t
2310 (let ((folder (prompt-for-folder))
2311 (seq-name (prompt-for-string :prompt "Sequence name: "
2312 :trim t)))
2313 (declare (simple-string seq-name))
2314 (when (string= "" seq-name)
2315 (editor-error "Sequence name cannot be empty."))
2316 (mh "mark" `(,folder ,@(prompt-for-message :folder folder)
2317 "-sequence" ,seq-name
2318 ,(if p "-delete" "-add"))))))))
2319
2320
2321(defcommand "List Mail Buffers" (p)
2322 "Show a list of all mail associated buffers.
2323 If the buffer has an associated message buffer, it is displayed to the right
2324 of the buffer name. If there is no message buffer, but the buffer is
2325 associated with a headers buffer, then it is displayed. If the buffer is
2326 modified then a * is displayed before the name."
2327 "Display the names of all buffers in a with-random-typeout window."
2328 (declare (ignore p))
2329 (let ((buffers nil))
2330 (declare (list buffers))
2331 (do-strings (n b *buffer-names*)
2332 (declare (ignore n))
2333 (unless (eq b *echo-area-buffer*)
2334 (cond ((hemlock-bound-p 'message-buffer :buffer b)
2335 ;; Catches draft buffers associated with message buffers first.
2336 (push (cons b (variable-value 'message-buffer :buffer b))
2337 buffers))
2338 ((hemlock-bound-p 'headers-buffer :buffer b)
2339 ;; Then draft or message buffers associated with headers buffers.
2340 (push (cons b (variable-value 'headers-buffer :buffer b))
2341 buffers))
2342 ((or (hemlock-bound-p 'draft-information :buffer b)
2343 (hemlock-bound-p 'message-information :buffer b)
2344 (hemlock-bound-p 'headers-information :buffer b))
2345 (push b buffers)))))
2346 (with-pop-up-display (s :height (length buffers))
2347 (dolist (ele (nreverse buffers))
2348 (let* ((association (if (consp ele) (cdr ele)))
2349 (b (if association (car ele) ele))
2350 (buffer-pathname (buffer-pathname b))
2351 (buffer-name (buffer-name b)))
2352 (write-char (if (buffer-modified b) #\* #\space) s)
2353 (if buffer-pathname
2354 (format s "~A ~A~:[~;~50T~:*~A~]~%"
2355 (file-namestring buffer-pathname)
2356 (directory-namestring buffer-pathname)
2357 (if association (buffer-name association)))
2358 (format s "~A~:[~;~50T~:*~A~]~%"
2359 buffer-name
2360 (if association (buffer-name association)))))))))
2361
2362
2363(defcommand "Message Help" (p)
2364 "Show this help."
2365 "Show this help."
2366 (declare (ignore p))
2367 (describe-mode-command nil "Message"))
2368
2369(defcommand "Headers Help" (p)
2370 "Show this help."
2371 "Show this help."
2372 (declare (ignore p))
2373 (describe-mode-command nil "Headers"))
2374
2375(defcommand "Draft Help" (p)
2376 "Show this help."
2377 "Show this help."
2378 (declare (ignore p))
2379 (describe-mode-command nil "Draft"))
2380
2381
2382
2383
2384;;;; Prompting.
2385
2386;;; Folder prompting.
2387;;;
2388
2389(defun prompt-for-folder (&key (must-exist t) (prompt "MH Folder: ")
2390 (default (mh-current-folder)))
2391 "Prompts for a folder, using MH's idea of the current folder as a default.
2392 The result will have a leading + in the name."
2393 (check-folder-name-table)
2394 (let ((folder (prompt-for-keyword (list *folder-name-table*)
2395 :must-exist must-exist :prompt prompt
2396 :default default :default-string default
2397 :help "Enter folder name.")))
2398 (declare (simple-string folder))
2399 (when (string= folder "") (editor-error "Must supply folder!"))
2400 (let ((name (coerce-folder-name folder)))
2401 (when (and must-exist (not (folder-existsp name)))
2402 (editor-error "Folder does not exist -- ~S." name))
2403 name)))
2404
2405(defun coerce-folder-name (folder)
2406 (if (char= (schar folder 0) #\+)
2407 folder
2408 (concatenate 'simple-string "+" folder)))
2409
2410(defun strip-folder-name (folder)
2411 (if (char= (schar folder 0) #\+)
2412 (subseq folder 1)
2413 folder))
2414
2415
2416;;; Message prompting.
2417;;;
2418
2419(defun prompt-for-message (&key (folder (mh-current-folder))
2420 (prompt "MH messages: ")
2421 messages)
2422 "Prompts for a message spec, using messages as a default. If messages is
2423 not supplied, then the current message for folder is used. The result is
2424 a list of strings which are the message ids, intervals, and/or sequence
2425 names the user entered."
2426 (let* ((cur-msg (cond ((not messages) (mh-current-message folder))
2427 ((stringp messages) messages)
2428 ((consp messages)
2429 (if (= (length (the list messages)) 1)
2430 (car messages)
2431 (format nil "~{~A~^ ~}" messages))))))
2432 (breakup-message-spec (prompt-for-string :prompt prompt
2433 :default cur-msg
2434 :default-string cur-msg
2435 :trim t
2436 :help "Enter MH message id(s)."))))
2437
2438(defun breakup-message-spec (msgs)
2439 (declare (simple-string msgs))
2440 (let ((start 0)
2441 (result nil))
2442 (loop
2443 (let ((end (position #\space msgs :start start :test #'char=)))
2444 (unless end
2445 (return (if (zerop start)
2446 (list msgs)
2447 (nreverse (cons (subseq msgs start) result)))))
2448 (push (subseq msgs start end) result)
2449 (setf start (1+ end))))))
2450
2451
2452;;; PICK expression prompting.
2453;;;
2454
2455(defhvar "MH Lisp Expression"
2456 "When this is set (the default), MH expression prompts are read in a Lisp
2457 syntax. Otherwise, the input is as if it had been entered on a shell
2458 command line."
2459 :value t)
2460
2461;;; This is dynamically bound to nil for argument processing routines.
2462;;;
2463(defvar *pick-expression-strings* nil)
2464
2465(defun prompt-for-pick-expression ()
2466 "Prompts for an MH PICK-like expression that is converted to a list of
2467 strings suitable for EXT:RUN-PROGRAM. As a second value, the user's
2468 expression is as typed in is returned."
2469 (let ((exp (prompt-for-string :prompt "MH expression: "
2470 :help "Expression to PICK over mail messages."
2471 :trim t))
2472 (*pick-expression-strings* nil))
2473 (if (value mh-lisp-expression)
2474 (let ((exp (let ((*package* *keyword-package*))
2475 (read-from-string exp))))
2476 (if exp
2477 (if (consp exp)
2478 (lisp-to-pick-expression exp)
2479 (editor-error "Lisp PICK expressions cannot be atomic."))))
2480 (expand-mh-pick-spec exp))
2481 (values (nreverse *pick-expression-strings*)
2482 exp)))
2483
2484(defun lisp-to-pick-expression (exp)
2485 (ecase (car exp)
2486 (:and (lpe-and/or exp "-and"))
2487 (:or (lpe-and/or exp "-or"))
2488 (:not (push "-not" *pick-expression-strings*)
2489 (let ((nexp (cadr exp)))
2490 (unless (consp nexp) (editor-error "Bad expression -- ~S" nexp))
2491 (lisp-to-pick-expression nexp)))
2492
2493 (:cc (lpe-output-and-go exp "-cc"))
2494 (:date (lpe-output-and-go exp "-date"))
2495 (:from (lpe-output-and-go exp "-from"))
2496 (:search (lpe-output-and-go exp "-search"))
2497 (:subject (lpe-output-and-go exp "-subject"))
2498 (:to (lpe-output-and-go exp "-to"))
2499 (:-- (lpe-output-and-go (cdr exp)
2500 (concatenate 'simple-string
2501 "--" (string (cadr exp)))))
2502
2503 (:before (lpe-after-and-before exp "-before"))
2504 (:after (lpe-after-and-before exp "-after"))
2505 (:datefield (lpe-output-and-go exp "-datefield"))))
2506
2507(defun lpe-after-and-before (exp op)
2508 (let ((operand (cadr exp)))
2509 (when (numberp operand)
2510 (setf (cadr exp)
2511 (if (plusp operand)
2512 (number-string (- operand))
2513 (number-string operand)))))
2514 (lpe-output-and-go exp op))
2515
2516(defun lpe-output-and-go (exp op)
2517 (push op *pick-expression-strings*)
2518 (let ((operand (cadr exp)))
2519 (etypecase operand
2520 (string (push operand *pick-expression-strings*))
2521 (symbol (push (symbol-name operand)
2522 *pick-expression-strings*)))))
2523
2524(defun lpe-and/or (exp op)
2525 (push "-lbrace" *pick-expression-strings*)
2526 (dolist (ele (cdr exp))
2527 (lisp-to-pick-expression ele)
2528 (push op *pick-expression-strings*))
2529 (pop *pick-expression-strings*) ;Clear the extra "-op" arg.
2530 (push "-rbrace" *pick-expression-strings*))
2531
2532;;; EXPAND-MH-PICK-SPEC takes a string of "words" assumed to be separated
2533;;; by single spaces. If a "word" starts with a quotation mark, then
2534;;; everything is grabbed up to the next one and used as a single word.
2535;;; Currently, this does not worry about extra spaces (or tabs) between
2536;;; "words".
2537;;;
2538(defun expand-mh-pick-spec (spec)
2539 (declare (simple-string spec))
2540 (let ((start 0))
2541 (loop
2542 (let ((end (position #\space spec :start start :test #'char=)))
2543 (unless end
2544 (if (zerop start)
2545 (setf *pick-expression-strings* (list spec))
2546 (push (subseq spec start) *pick-expression-strings*))
2547 (return))
2548 (cond ((char= #\" (schar spec start))
2549 (setf end (position #\" spec :start (1+ start) :test #'char=))
2550 (unless end (editor-error "Bad quoting syntax."))
2551 (push (subseq spec (1+ start) end) *pick-expression-strings*)
2552 (setf start (+ end 2)))
2553 (t (push (subseq spec start end) *pick-expression-strings*)
2554 (setf start (1+ end))))))))
2555
2556
2557;;; Password prompting.
2558;;;
2559
2560(defun prompt-for-password (&optional (prompt "Password: "))
2561 "Prompts for password with prompt."
2562 (let ((hi::*parse-verification-function* #'(lambda (string) (list string))))
2563 (let ((hi::*parse-prompt* prompt))
2564 (hi::display-prompt-nicely))
2565 (let ((start-window (current-window)))
2566 (move-mark *parse-starting-mark* (buffer-point *echo-area-buffer*))
2567 (setf (current-window) *echo-area-window*)
2568 (unwind-protect
2569 (use-buffer *echo-area-buffer*
2570 (let ((result ()))
2571 (declare (list result))
2572 (loop
2573 (let ((key-event (get-key-event *editor-input*)))
2574 (ring-pop hi::*key-event-history*)
2575 (cond ((eq key-event #k"return")
2576 (return (prog1 (coerce (nreverse result)
2577 'simple-string)
2578 (fill result nil))))
2579 ((or (eq key-event #k"control-u")
2580 (eq key-event #k"control-U"))
2581 (setf result nil))
2582 (t (push (ext:key-event-char key-event) result)))))))
2583 (setf (current-window) start-window)))))
2584
2585
2586
2587
2588
2589;;;; Making mail buffers.
2590
2591;;; MAYBE-MAKE-MH-BUFFER looks up buffer with name, returning it if it exists
2592;;; after cleaning it up to a state "good as new". Currently, we don't
2593;;; believe it is possible to try to make two draft buffers with the same name
2594;;; since that would mean that composition, draft folder interaction, and
2595;;; draft folder current message didn't do what we expected -- or some user
2596;;; was modifying the draft folder in some evil way.
2597;;;
2598(defun maybe-make-mh-buffer (name use)
2599 (let ((buf (getstring name *buffer-names*)))
2600 (cond ((not buf)
2601 (ecase use
2602 (:headers (make-buffer name
2603 :modes '("Headers")
2604 :delete-hook '(cleanup-headers-buffer)))
2605
2606 (:message
2607 (make-buffer name :modes '("Message")
2608 :modeline-fields
2609 (value default-message-modeline-fields)
2610 :delete-hook '(cleanup-message-buffer)))
2611
2612 (:draft
2613 (let ((buf (make-buffer
2614 name :delete-hook '(cleanup-draft-buffer))))
2615 (setf (buffer-minor-mode buf "Draft") t)
2616 buf))))
2617 ((hemlock-bound-p 'headers-information :buffer buf)
2618 (setf (buffer-writable buf) t)
2619 (delete-region (buffer-region buf))
2620 (cleanup-headers-buffer buf)
2621 (delete-variable 'headers-information :buffer buf)
2622 buf)
2623 ((hemlock-bound-p 'message-information :buffer buf)
2624 (setf (buffer-writable buf) t)
2625 (delete-region (buffer-region buf))
2626 (cleanup-message-buffer buf)
2627 (delete-variable 'message-information :buffer buf)
2628 buf)
2629 ((hemlock-bound-p 'draft-information :buffer buf)
2630 (error "Attempt to create multiple draft buffers to same draft ~
2631 folder message -- ~S"
2632 name)))))
2633
2634
2635
2636;;;; Message buffer modeline fields.
2637
2638(make-modeline-field
2639 :name :deleted-message :width 2
2640 :function
2641 #'(lambda (buffer window)
2642 "Returns \"D \" when message in buffer is deleted."
2643 (declare (ignore window))
2644 (let* ((minfo (variable-value 'message-information :buffer buffer))
2645 (hmark (message-info-headers-mark minfo)))
2646 (cond ((not hmark)
2647 (let ((msgs (message-info-msgs minfo)))
2648 (if (and (value virtual-message-deletion)
2649 (mh-sequence-member-p
2650 (if (consp msgs) (car msgs) msgs)
2651 (mh-sequence-list (message-info-folder minfo)
2652 "hemlockdeleted")))
2653 "D "
2654 "")))
2655 ((line-message-deleted (mark-line hmark))
2656 "D ")
2657 (t "")))))
2658
2659(make-modeline-field
2660 :name :replied-to-message :width 1
2661 :function
2662 #'(lambda (buffer window)
2663 "Returns \"A\" when message in buffer is deleted."
2664 (declare (ignore window))
2665 (let* ((minfo (variable-value 'message-information :buffer buffer))
2666 (hmark (message-info-headers-mark minfo)))
2667 (cond ((not hmark)
2668 ;; Could do something nasty here to figure out the right value.
2669 "")
2670 (t
2671 (mark-to-note-replied-msg hmark)
2672 (if (char= (next-character hmark) #\A)
2673 "A"
2674 ""))))))
2675
2676;;; MARK-TO-NOTE-REPLIED-MSG moves the headers-buffer mark to a line position
2677;;; suitable for checking or setting the next character with respect to noting
2678;;; that a message has been replied to.
2679;;;
2680(defun mark-to-note-replied-msg (hmark)
2681 (line-start hmark)
2682 (find-attribute hmark :digit)
2683 (find-attribute hmark :digit #'zerop)
2684 (character-offset hmark 1))
2685
2686
2687(defhvar "Default Message Modeline Fields"
2688 "This is the default list of modeline-field objects for message buffers."
2689 :value
2690 (list (modeline-field :hemlock-literal) (modeline-field :package)
2691 (modeline-field :modes) (modeline-field :buffer-name)
2692 (modeline-field :replied-to-message) (modeline-field :deleted-message)
2693 (modeline-field :buffer-pathname) (modeline-field :modifiedp)))
2694
2695
2696
2697
2698;;;; MH interface.
2699
2700;;; Running an MH utility.
2701;;;
2702
2703(defhvar "MH Utility Pathname"
2704 "MH utility names are merged with this. The default is
2705 \"/usr/misc/.mh/bin/\"."
2706 :value (pathname "/usr/misc/.mh/bin/"))
2707
2708(defvar *signal-mh-errors* t
2709 "This is the default value for whether MH signals errors. It is useful to
2710 bind this to nil when using PICK-MESSAGES with the \"Incorporate New Mail
2711 Hook\".")
2712
2713(defvar *mh-error-output* (make-string-output-stream))
2714
2715(defun mh (utility args &key (errorp *signal-mh-errors*) environment)
2716 "Runs the MH utility with the list of args (suitable for EXT:RUN-PROGRAM),
2717 outputting to *standard-output*. Environment is a list of strings
2718 appended with ext:*environment-list*. This returns t, unless there is
2719 an error. When errorp, this reports any MH errors in the echo area as
2720 an editor error, and this does not return; otherwise, nil and the error
2721 output from the MH utility are returned."
2722 (fresh-line)
2723 (let* ((utility
2724 (namestring
2725 (or (probe-file (merge-pathnames utility
2726 (value mh-utility-pathname)))
2727 utility)))
2728 (proc (ext:run-program
2729 utility args
2730 :output *standard-output*
2731 :error *mh-error-output*
2732 :env (append environment ext:*environment-list*))))
2733 (fresh-line)
2734 (ext:process-close proc)
2735 (cond ((zerop (ext:process-exit-code proc))
2736 (values t nil))
2737 (errorp
2738 (editor-error "MH Error -- ~A"
2739 (get-output-stream-string *mh-error-output*)))
2740 (t (values nil (get-output-stream-string *mh-error-output*))))))
2741
2742
2743
2744;;; Draft folder name and pathname.
2745;;;
2746
2747(defun mh-draft-folder ()
2748 (let ((drafts (mh-profile-component "draft-folder")))
2749 (unless drafts
2750 (error "There must be a draft-folder component in your profile."))
2751 drafts))
2752
2753(defun mh-draft-folder-pathname ()
2754 "Returns the pathname of the MH draft folder directory."
2755 (let ((drafts (mh-profile-component "draft-folder")))
2756 (unless drafts
2757 (error "There must be a draft-folder component in your profile."))
2758 (merge-relative-pathnames drafts (mh-directory-pathname))))
2759
2760
2761;;; Current folder name.
2762;;;
2763
2764(defun mh-current-folder ()
2765 "Returns the current MH folder from the context file."
2766 (mh-profile-component "current-folder" (mh-context-pathname)))
2767
2768
2769;;; Current message name.
2770;;;
2771
2772(defun mh-current-message (folder)
2773 "Returns the current MH message from the folder's sequence file."
2774 (declare (simple-string folder))
2775 (let ((folder (strip-folder-name folder)))
2776 (mh-profile-component
2777 "cur"
2778 (merge-pathnames ".mh_sequences"
2779 (merge-relative-pathnames folder
2780 (mh-directory-pathname))))))
2781
2782
2783;;; Context pathname.
2784;;;
2785
2786(defvar *mh-context-pathname* nil)
2787
2788(defun mh-context-pathname ()
2789 "Returns the pathname of the MH context file."
2790 (or *mh-context-pathname*
2791 (setf *mh-context-pathname*
2792 (merge-pathnames (or (mh-profile-component "context") "context")
2793 (mh-directory-pathname)))))
2794
2795
2796;;; MH directory pathname.
2797;;;
2798
2799(defvar *mh-directory-pathname* nil)
2800
2801;;; MH-DIRECTORY-PATHNAME fetches the "path" MH component and bashes it
2802;;; appropriately to get an absolute directory pathname.
2803;;;
2804(defun mh-directory-pathname ()
2805 "Returns the pathname of the MH directory."
2806 (if *mh-directory-pathname*
2807 *mh-directory-pathname*
2808 (let ((path (mh-profile-component "path")))
2809 (unless path (error "MH profile does not contain a Path component."))
2810 (setf *mh-directory-pathname*
2811 (truename (merge-relative-pathnames path
2812 (user-homedir-pathname)))))))
2813
2814;;; Profile components.
2815;;;
2816
2817(defun mh-profile-component (name &optional (pathname (mh-profile-pathname))
2818 (error-on-open t))
2819 "Returns the trimmed string value for the MH profile component name. If
2820 the component is not present, nil is returned. This may be used on MH
2821 context and sequence files as well due to their having the same format.
2822 Error-on-open indicates that errors generated by OPEN should not be ignored,
2823 which is the default. When opening a sequence file, it is better to supply
2824 this as nil since the file may not exist or be readable in another user's
2825 MH folder, and returning nil meaning the sequence could not be found is just
2826 as useful."
2827 (with-open-stream (s (if error-on-open
2828 (open pathname)
2829 (ignore-errors (open pathname))))
2830 (if s
2831 (loop
2832 (multiple-value-bind (line eofp) (read-line s nil :eof)
2833 (when (eq line :eof) (return nil))
2834 (let ((colon (position #\: (the simple-string line) :test #'char=)))
2835 (unless colon
2836 (error "Bad record ~S in file ~S." line (namestring pathname)))
2837 (when (string-equal name line :end2 colon)
2838 (return (string-trim '(#\space #\tab)
2839 (subseq line (1+ colon))))))
2840 (when eofp (return nil)))))))
2841
2842
2843;;; Profile pathname.
2844;;;
2845
2846(defvar *mh-profile-pathname* nil)
2847
2848(defun mh-profile-pathname ()
2849 "Returns the pathname of the MH profile."
2850 (or *mh-profile-pathname*
2851 (setf *mh-profile-pathname*
2852 (merge-pathnames (or (cdr (assoc :mh ext:*environment-list*))
2853 ".mh_profile")
2854 (truename (user-homedir-pathname))))))
2855
2856
2857
2858
2859;;;; Sequence handling.
2860
2861(declaim (optimize (speed 2))); byte compile off
2862
2863(defun mark-one-message (folder msg sequence add-or-delete)
2864 "Msg is added or deleted to the sequence named sequence in the folder's
2865 \".mh_sequence\" file. Add-or-delete is either :add or :delete."
2866 (let ((seq-list (mh-sequence-list folder sequence)))
2867 (ecase add-or-delete
2868 (:add
2869 (write-mh-sequence folder sequence (mh-sequence-insert msg seq-list)))
2870 (:delete
2871 (when (mh-sequence-member-p msg seq-list)
2872 (write-mh-sequence folder sequence
2873 (mh-sequence-delete msg seq-list)))))))
2874
2875
2876(defun mh-sequence-list (folder name)
2877 "Returns a list representing the messages and ranges of id's for the
2878 sequence name in folder from the \".mh_sequences\" file. A second value
2879 is returned indicating whether the sequence was found or not."
2880 (declare (simple-string folder))
2881 (let* ((folder (strip-folder-name folder))
2882 (seq-string (mh-profile-component
2883 name
2884 (merge-pathnames ".mh_sequences"
2885 (merge-relative-pathnames
2886 folder (mh-directory-pathname)))
2887 nil)))
2888 (if (not seq-string)
2889 (values nil nil)
2890 (let ((length (length (the simple-string seq-string)))
2891 (result ())
2892 (intervalp nil)
2893 (start 0))
2894 (declare (fixnum length start))
2895 (loop
2896 (multiple-value-bind (msg index)
2897 (parse-integer seq-string
2898 :start start :end length
2899 :junk-allowed t)
2900 (unless msg (return))
2901 (cond ((or (= index length)
2902 (char/= (schar seq-string index) #\-))
2903 (if intervalp
2904 (setf (cdar result) msg)
2905 (push (cons msg msg) result))
2906 (setf intervalp nil)
2907 (setf start index))
2908 (t
2909 (push (cons msg nil) result)
2910 (setf intervalp t)
2911 (setf start (1+ index)))))
2912 (when (>= start length) (return)))
2913 (values (nreverse result) t)))))
2914
2915(defun write-mh-sequence (folder name seq-list)
2916 "Writes seq-list to folder's \".mh_sequences\" file. If seq-list is nil,
2917 the sequence is removed from the file."
2918 (declare (simple-string folder))
2919 (let* ((folder (strip-folder-name folder))
2920 (input (merge-pathnames ".mh_sequences"
2921 (merge-relative-pathnames
2922 folder (mh-directory-pathname))))
2923 (input-dir (pathname (directory-namestring input)))
2924 (output (loop (let* ((sym (gensym))
2925 (f (merge-pathnames
2926 (format nil "sequence-file-~A.tmp" sym)
2927 input-dir)))
2928 (unless (probe-file f) (return f)))))
2929 (found nil))
2930 (cond ((not (hemlock-ext:file-writable output))
2931 (loud-message "Cannot write sequence temp file ~A.~%~
2932 Aborting output of ~S sequence."
2933 name (namestring output)))
2934 (t
2935 (with-open-file (in input)
2936 (with-open-file (out output :direction :output)
2937 (loop
2938 (multiple-value-bind (line eofp) (read-line in nil :eof)
2939 (when (eq line :eof)
2940 (return nil))
2941 (let ((colon (position #\: (the simple-string line)
2942 :test #'char=)))
2943 (unless colon
2944 (error "Bad record ~S in file ~S."
2945 line (namestring input)))
2946 (cond ((and (not found) (string-equal name line
2947 :end2 colon))
2948 (sub-write-mh-sequence
2949 out (subseq line 0 colon) seq-list)
2950 (setf found t))
2951 (t (write-line line out))))
2952 (when eofp (return))))
2953 (unless found
2954 (fresh-line out)
2955 (sub-write-mh-sequence out name seq-list))))
2956 (hacking-rename-file output input)))))
2957
2958(defun sub-write-mh-sequence (stream name seq-list)
2959 (when seq-list
2960 (write-string name stream)
2961 (write-char #\: stream)
2962 (let ((*print-base* 10))
2963 (dolist (range seq-list)
2964 (write-char #\space stream)
2965 (let ((low (car range))
2966 (high (cdr range)))
2967 (declare (fixnum low high))
2968 (cond ((= low high)
2969 (prin1 low stream))
2970 (t (prin1 low stream)
2971 (write-char #\- stream)
2972 (prin1 high stream))))))
2973 (terpri stream)))
2974
2975
2976;;; MH-SEQUENCE-< keeps SORT from consing rest args when FUNCALL'ing #'<.
2977;;;
2978(defun mh-sequence-< (x y)
2979 (< x y))
2980
2981(defun mh-sequence-insert (item seq-list)
2982 "Inserts item into an mh sequence list. Item can be a string (\"23\"),
2983 number (23), or a cons of two numbers ((23 . 23) or (3 . 5))."
2984 (let ((range (typecase item
2985 (string (let ((id (parse-integer item)))
2986 (cons id id)))
2987 (cons item)
2988 (number (cons item item)))))
2989 (cond (seq-list
2990 (setf seq-list (sort (cons range seq-list)
2991 #'mh-sequence-< :key #'car))
2992 (coelesce-mh-sequence-ranges seq-list))
2993 (t (list range)))))
2994
2995(defun coelesce-mh-sequence-ranges (seq-list)
2996 (when seq-list
2997 (let* ((current seq-list)
2998 (next (cdr seq-list))
2999 (current-range (car current))
3000 (current-end (cdr current-range)))
3001 (declare (fixnum current-end))
3002 (loop
3003 (unless next
3004 (setf (cdr current-range) current-end)
3005 (setf (cdr current) nil)
3006 (return))
3007 (let* ((next-range (car next))
3008 (next-start (car next-range))
3009 (next-end (cdr next-range)))
3010 (declare (fixnum next-start next-end))
3011 (cond ((<= (1- next-start) current-end)
3012 ;;
3013 ;; Extend the current range since the next one overlaps.
3014 (when (> next-end current-end)
3015 (setf current-end next-end)))
3016 (t
3017 ;;
3018 ;; Update the current range since the next one doesn't overlap.
3019 (setf (cdr current-range) current-end)
3020 ;;
3021 ;; Make the next range succeed current. Then make it current.
3022 (setf (cdr current) next)
3023 (setf current next)
3024 (setf current-range next-range)
3025 (setf current-end next-end))))
3026 (setf next (cdr next))))
3027 seq-list))
3028
3029
3030(defun mh-sequence-delete (item seq-list)
3031 "Inserts item into an mh sequence list. Item can be a string (\"23\"),
3032 number (23), or a cons of two numbers ((23 . 23) or (3 . 5))."
3033 (let ((range (typecase item
3034 (string (let ((id (parse-integer item)))
3035 (cons id id)))
3036 (cons item)
3037 (number (cons item item)))))
3038 (when seq-list
3039 (do ((id (car range) (1+ id))
3040 (end (cdr range)))
3041 ((> id end))
3042 (setf seq-list (sub-mh-sequence-delete id seq-list)))
3043 seq-list)))
3044
3045(defun sub-mh-sequence-delete (id seq-list)
3046 (do ((prev nil seq)
3047 (seq seq-list (cdr seq)))
3048 ((null seq))
3049 (let* ((range (car seq))
3050 (low (car range))
3051 (high (cdr range)))
3052 (cond ((> id high))
3053 ((< id low)
3054 (return))
3055 ((= id low)
3056 (cond ((/= low high)
3057 (setf (car range) (1+ id)))
3058 (prev
3059 (setf (cdr prev) (cdr seq)))
3060 (t (setf seq-list (cdr seq-list))))
3061 (return))
3062 ((= id high)
3063 (setf (cdr range) (1- id))
3064 (return))
3065 ((< low id high)
3066 (setf (cdr range) (1- id))
3067 (setf (cdr seq) (cons (cons (1+ id) high) (cdr seq)))
3068 (return)))))
3069 seq-list)
3070
3071
3072(defun mh-sequence-member-p (item seq-list)
3073 "Returns to or nil whether item is in the mh sequence list. Item can be a
3074 string (\"23\") or a number (23)."
3075 (let ((id (typecase item
3076 (string (parse-integer item))
3077 (number item))))
3078 (dolist (range seq-list nil)
3079 (let ((low (car range))
3080 (high (cdr range)))
3081 (when (<= low id high) (return t))))))
3082
3083
3084(defun mh-sequence-strings (seq-list)
3085 "Returns a list of strings representing the ranges and messages id's in
3086 seq-list."
3087 (let ((result nil))
3088 (dolist (range seq-list)
3089 (let ((low (car range))
3090 (high (cdr range)))
3091 (if (= low high)
3092 (push (number-string low) result)
3093 (push (format nil "~D-~D" low high) result))))
3094 (nreverse result)))
3095
3096(declaim (optimize (speed 0))); byte compile again.
3097
3098
3099;;;; CMU Common Lisp support.
3100
3101;;; HACKING-RENAME-FILE renames old to new. This is used instead of Common
3102;;; Lisp's RENAME-FILE because it merges new pathname with old pathname,
3103;;; which loses when old has a name and type, and new has only a type (a
3104;;; Unix-oid "dot" file).
3105;;;
3106(defun hacking-rename-file (old new)
3107 (let ((ses-name1 (namestring old))
3108 (ses-name2 (namestring new)))
3109 (multiple-value-bind (res err) (unix:unix-rename ses-name1 ses-name2)
3110 (unless res
3111 (error "Failed to rename ~A to ~A: ~A."
3112 ses-name1 ses-name2 (unix:get-unix-error-msg err))))))
3113
3114
3115;;; Folder existence and creation.
3116;;;
3117
3118(defun folder-existsp (folder)
3119 "Returns t if the directory for folder exists. Folder is a simple-string
3120 specifying a folder name relative to the MH mail directoy."
3121 (declare (simple-string folder))
3122 (let* ((folder (strip-folder-name folder))
3123 (pathname (merge-relative-pathnames folder (mh-directory-pathname)))
3124 (pf (probe-file pathname)))
3125 (and pf
3126 (null (pathname-name pf))
3127 (null (pathname-type pf)))))
3128
3129(defun create-folder (folder)
3130 "Creates folder directory with default protection #o711 but considers the
3131 MH profile for the \"Folder-Protect\" component. Folder is a simple-string
3132 specifying a folder name relative to the MH mail directory."
3133 (declare (simple-string folder))
3134 (let* ((folder (strip-folder-name folder))
3135 (pathname (merge-relative-pathnames folder (mh-directory-pathname)))
3136 (ses-name (namestring pathname))
3137 (length-1 (1- (length ses-name)))
3138 (name (if (= (position #\/ ses-name :test #'char= :from-end t)
3139 length-1)
3140 (subseq ses-name 0 (1- (length ses-name)))
3141 ses-name))
3142 (protection (mh-profile-component "folder-protect")))
3143 (when protection
3144 (setf protection
3145 (parse-integer protection :radix 8 :junk-allowed t)))
3146 (multiple-value-bind (winp err)
3147 (unix:unix-mkdir name (or protection #o711))
3148 (unless winp
3149 (error "Couldn't make directory ~S: ~A"
3150 name
3151 (unix:get-unix-error-msg err)))
3152 (check-folder-name-table)
3153 (setf (getstring folder *folder-name-table*) t))))
3154
3155
3156;;; Checking for mail.
3157;;;
3158
3159(defvar *mailbox* nil)
3160
3161(defun new-mail-p ()
3162 (unless *mailbox*
3163 (setf *mailbox*
3164 (probe-file (or (cdr (assoc :mail ext:*environment-list*))
3165 (cdr (assoc :maildrop ext:*environment-list*))
3166 (mh-profile-component "MailDrop")
3167 (merge-pathnames
3168 (cdr (assoc :user ext:*environment-list*))
3169 "/usr/spool/mail/")))))
3170 (when *mailbox*
3171 (multiple-value-bind (success dev ino mode nlink uid gid rdev size
3172 atime)
3173 (unix:unix-stat (namestring *mailbox*))
3174 (declare (ignore dev ino nlink uid gid rdev atime))
3175 (and success
3176 (plusp (logand unix:s-ifreg mode))
3177 (not (zerop size))))))
3178
3179
3180
Note: See TracBrowser for help on using the repository browser.