;; This module implements the mail-reading window as a unit. The ;; unit is instantiated once for each window. ;; General notes: ;; ;; * Always use `as-background' when communicating with the ;; server. That way, the user can kill the window if necessary. ;; use `enable-main-frame' for the first argument to `as-background'. ;; The `as-background' function is defined in "utilr.ss". ;; (module readr mzscheme (require mzlib/unit mzlib/class mzlib/file mred/mred-sig framework mzlib/process) (require mzlib/string mzlib/list mzlib/thread "spell.ss") (require "sirmails.ss") (require "pref.ss") (require net/imap-sig net/smtp-sig net/head-sig net/base64-sig net/mime-sig net/qp-sig browser/htmltext) (require mrlib/hierlist/hierlist-sig) (require net/sendurl) (require openssl/mzssl) ;; Constant for messages without a title: (define no-subject-string "") (provide read@) (define-unit read@ (import sirmail:options^ sirmail:environment^ sirmail:utils^ sirmail:send^ mred^ imap^ smtp^ head^ base64^ (prefix mime: mime^) qp^ hierlist^) (export sirmail:read^) ;; This will be set to the frame object (define main-frame #f) (define done? #f) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Error Handling ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; It's possible that SirMail can't even start with ;; the default preference values. This flag lets us ;; give the user a chance. (define got-started? #f) (define (show-error x) (show-error-message-box x main-frame) (when (not got-started?) (when (eq? 'yes (confirm-box "Startup Error" (string-append "Looks like you didn't even get started. " "Set preferences (so you're ready to try again)?") #f '(app))) (show-pref-dialog)))) (uncaught-exception-handler (lambda (x) (show-error x) ((error-escape-handler)))) ;; Install std bindings global for file dialog, etc. (let ([km (make-object keymap%)]) (add-text-keymap-functions km) (keymap:setup-global km) (let ([f (current-text-keymap-initializer)]) (current-text-keymap-initializer (lambda (k) (send k chain-to-keymap km #f) (f k))))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Mailbox List ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; In case this is the first run... (unless (directory-exists? (LOCAL-DIR)) (make-directory (LOCAL-DIR))) ;; The "mailboxes" file tells us where to find local copies ;; of the mailbox content (define mailboxes (with-handlers ([void (lambda (x) '(("Inbox" #"inbox")))]) (with-input-from-file (build-path (LOCAL-DIR) "mailboxes") read))) (unless (assoc mailbox-name mailboxes) (error 'sirmail "No local mapping for mailbox: ~a" mailbox-name)) (define (string/bytes->path s) (if (string? s) (string->path s) (bytes->path s))) ;; find the mailbox for this window: (define mailbox-dir (build-path (LOCAL-DIR) (string/bytes->path (cadr (assoc mailbox-name mailboxes))))) (unless (directory-exists? mailbox-dir) (make-directory mailbox-dir)) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Message data structure ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; We use a lists so they can be easily read and written ;; type message = (list ... message attributes, see selectors below ...) (define uid-validity #f) ;; mailbox : (listof message) ;; mailboxes holds the list of messages reflected in the top list ;; in the GUI. When modifying this value (usually indirectly), use ;; `header-chganging-action'. Mutate the variable, but not the list! (define mailbox (let* ([mailbox-file (build-path mailbox-dir "mailbox")] [l (with-handlers ([void (lambda (x) (message-box "SirMail" (format "error reading mailbox ~s, ~a\n" mailbox-file (exn-message x))) null)]) (with-input-from-file mailbox-file read))]) (when (eof-object? l) (message-box "SirMail" (format "mailbox ~s was eof\n" mailbox-file)) (set! l '())) ;; If the file's list start with an integer, that's ;; the uidvalidity value. Otherwise, for backward ;; compatibility, we allow the case that it wasn't ;; recorded. (let ([l (if (and (pair? l) (or (not (car l)) (integer? (car l)))) (begin (set! uid-validity (car l)) (cdr l)) l)]) ;; Convert each entry to a vector: (map list->vector l)))) (define mailbox-ht #f) (define (rebuild-mailbox-table!) (set! mailbox-ht (make-hash-table 'equal)) (for-each (lambda (m) (hash-table-put! mailbox-ht (vector-ref m 0) m)) mailbox)) (rebuild-mailbox-table!) (define (find-message id) (hash-table-get mailbox-ht id (lambda () #f))) (define (message-uid m) (vector-ref m 0)) (define (message-position m) (vector-ref m 1)) (define (message-downloaded? m) (vector-ref m 2)) (define (message-from m) (vector-ref m 3)) (define (message-subject m) (vector-ref m 4)) (define (message-flags m) (vector-ref m 5)) (define (message-size m) ;; For backward compatibility: (if ((vector-length m) . < . 7) #f (vector-ref m 6))) (define (set-message-position! m v) (vector-set! m 1 v)) (define (set-message-downloaded?! m v) (vector-set! m 2 v)) (define (set-message-flags! m v) (vector-set! m 5 v)) (define (message-marked? m) (memq 'marked (message-flags m))) (define (write-mailbox) (status "Saving mailbox information...") (with-output-to-file (build-path mailbox-dir "mailbox") (lambda () (write (cons uid-validity (map vector->list mailbox)))) 'truncate)) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Connection ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-values (connect disconnect force-disconnect) (let ([connection #f] [connection-custodian #f] [message-count 0]) (values (letrec ([connect (case-lambda [() (connect 'reuse)] [(mode) (connect 'reuse void void)] [(mode break-bad break-ok) (define (with-disconnect-handler thunk) (with-handlers ([void (lambda (exn) (force-disconnect) (status "") (raise exn))]) (break-ok) (begin0 (thunk) (break-bad)))) (if connection ;; Already Connected (cond [(eq? mode 'reselect) (let-values ([(count new) (with-disconnect-handler (lambda () (imap-noop connection)))]) (check-validity (or (imap-uidvalidity connection) 0) void) (values connection (imap-messages connection) (imap-new? connection)))] [(eq? mode 'check-new) (let-values ([(count new) (with-disconnect-handler (lambda () (imap-noop connection)))]) (values connection message-count (imap-new? connection)))] [else (values connection message-count (imap-new? connection))]) ;; New connection (begin (let ([pw (or (get-PASSWORD) (let ([p (get-pw-from-user (USERNAME) main-frame)]) (unless p (raise-user-error 'connect "connection canceled")) p))]) (let*-values ([(imap count new) (let-values ([(server port-no) (parse-server-name (IMAP-SERVER) (if (get-pref 'sirmail:use-ssl?) 993 143))]) (set! connection-custodian (make-custodian)) (parameterize ([current-custodian connection-custodian]) (with-disconnect-handler (lambda () (if (get-pref 'sirmail:use-ssl?) (let ([c (ssl-make-client-context)]) (let ([cert (get-pref 'sirmail:server-certificate)]) (when cert (ssl-set-verify! c #t) (ssl-load-verify-root-certificates! c cert))) (let-values ([(in out) (ssl-connect server port-no c)]) (imap-connect* in out (USERNAME) pw mailbox-name))) (parameterize ([imap-port-number port-no]) (imap-connect server (USERNAME) pw mailbox-name)))))))]) (unless (get-PASSWORD) (set-PASSWORD pw)) (status "(Connected, ~a messages)" count) (with-disconnect-handler (lambda () (check-validity (or (imap-uidvalidity imap) 0) (lambda () (imap-disconnect imap))))) (set! connection imap) (set! message-count count) (send disconnected-msg show #f) (values imap count (imap-new? imap))))))])]) connect) (lambda () (when connection (status "Disconnecting...") (as-background enable-main-frame (lambda (break-bad break-ok) (with-handlers ([void (lambda (exn) (force-disconnect/status) (raise exn))]) (break-ok) (imap-disconnect connection))) close-frame) (status "") (set! connection #f))) (lambda () (custodian-shutdown-all connection-custodian) (set! connection #f))))) (define (force-disconnect/status) (force-disconnect) (send disconnected-msg show #t) (set! initialized? #f) (set! continue? #f) (status "")) (define (check-validity v cleanup) (when (and uid-validity (not (= uid-validity v)) (pair? mailbox)) ;; This is really very unlikely, but we checked ;; to guard against disaster. (cleanup) (raise-user-error 'connect "UID validity changed, ~a -> ~a! SirMail can't handle it." uid-validity v)) (set! uid-validity v)) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Mailbox Actions (indepdent of the GUI) ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define initialized? #f) (define new-messages? #f) (define current-count 0) (define continue? #f) (define (initialized count) (set! initialized? #t) (set! continue? #t) (set! new-messages? #f) (set! current-count count) (hide-new-mail-msg)) ;; Syncs `mailbox' with the server (define (update-local break-bad break-ok) (status "Updating ~a from ~a..." mailbox-name (IMAP-SERVER)) (let-values ([(imap count new?) (connect 'reselect break-bad break-ok)]) (imap-reset-new! imap) (start-biff) (status "Getting message ids...") (let* ([positions (if continue? (let ([p (length mailbox)]) (map (lambda (i) (+ i p)) (enumerate (- count p)))) (enumerate count))] [data (with-handlers ([void (lambda (exn) (force-disconnect/status) (raise exn))]) (break-ok) (begin0 (imap-get-messages imap positions '(uid)) (break-bad)))] [uids (map car data)] [curr-uids (map (lambda (m) (vector-ref m 0)) mailbox)] [deleted (if continue? null (remove* uids curr-uids))] [position-uids (map cons uids positions)] [new (if continue? position-uids (remove* curr-uids position-uids (lambda (a b) (equal? a (car b)))))]) (status "~a deleted, ~a locally new" (length deleted) (length new)) (unless (null? new) (status "Getting new headers...")) (let* ([new-data (with-handlers ([void (lambda (exn) (force-disconnect/status) (raise exn))]) (break-ok) (begin0 (imap-get-messages imap (map cdr new) '(header size)) (break-bad)))] [new-headers (map car new-data)] [new-sizes (map cadr new-data)] [new-uid/size-map (map cons (map car new) new-sizes)]) (if (and (null? deleted) (null? new)) (begin (initialized count) (status "No new messages") #f) (begin (unless (null? deleted) (status "Deleting local messages...") (for-each (lambda (uid) (with-handlers ([void void]) (let ([path (build-path mailbox-dir (format "~a" uid))]) (delete-file path) (let ([body (string-append path "body")]) (when (file-exists? body) (delete-file body)))))) deleted)) (unless (null? new-headers) (status "Saving new headers...") (for-each (lambda (position-uid header) (with-output-to-file (build-path mailbox-dir (format "~a" (car position-uid))) (lambda () (display header)) 'truncate)) new new-headers)) (set! mailbox (append (if continue? mailbox null) (map (lambda (uid pos) (let ([old (ormap (lambda (m) (and (equal? uid (message-uid m)) m)) mailbox)]) (list->vector `(,uid ,pos ,(if old (message-downloaded? old) #f) ,(if old (message-from old) (extract-field "From" (get-header uid))) ,(if old (message-subject old) (extract-field "Subject" (get-header uid))) ,(if old (message-flags old) null) ,(if old (message-size old) (let ([new (assoc uid new-uid/size-map)]) (if new (cdr new) 0))))))) uids positions))) (rebuild-mailbox-table!) (write-mailbox) (initialized count) (display-message-count (length mailbox)) (let ([len (length new-headers)]) (status "Got ~a new message~a" len (if (= 1 len) "" "s"))) #t)))))) (define (check-for-new break-bad break-ok) (status "Checking ~a at ~a..." mailbox-name (IMAP-SERVER)) (let-values ([(imap count new?) (connect 'check-new break-bad break-ok)]) (set! new-messages? new?)) (if new-messages? (begin (show-new-mail-msg) (status "New mail") #t) (begin (hide-new-mail-msg) (status "No new mail") #f)) new-messages?) ;; gets cached header (define (get-header uid) (let ([file (build-path mailbox-dir (format "~a" uid))]) (with-input-from-file file (lambda () (bytes->string/latin-1 (read-bytes (file-size file))))))) ;; gets cached body or downloads from server (and caches) (define (get-body uid break-bad break-ok) (let ([v (find-message uid)] [file (build-path mailbox-dir (format "~abody" uid))]) (when (not v) (error 'internal-error "unknown message: ~a" uid)) (unless (message-downloaded? v) (status "Getting message ~a..." uid) (let ([size (message-size v)] [warn-size (WARN-DOWNLOAD-SIZE)]) (when (and size warn-size (> size warn-size)) (unless (eq? 'yes (confirm-box "Large Message" (format "The message is ~s bytes.~nReally download?" size) main-frame)) (status "") (raise-user-error "download aborted")))) (let*-values ([(imap count new?) (connect 'reuse break-bad break-ok)]) (let ([body (with-handlers ([void (lambda (exn) (force-disconnect/status) (raise exn))]) (break-ok) (begin0 (let ([reply (imap-get-messages imap (list (message-position v)) '(uid body))]) (if (equal? (caar reply) (message-uid v)) (cadar reply) (raise-user-error (string-append "server UID does not match local UID; " "update the message list and try again")))) (break-bad)))]) (status "Saving message ~a..." uid) (with-output-to-file file (lambda () (write-bytes body)) 'truncate) (set-message-downloaded?! v #t) (write-mailbox)))) (begin0 (with-input-from-file file (lambda () (read-bytes (file-size file)))) (status "")))) ;; Checks that `mailbox' is synced with the server (define (check-positions imap msgs) (status "Checking message mapping...") (let ([ids (imap-get-messages imap (map message-position msgs) '(uid))]) (unless (equal? (map car ids) (map message-uid msgs)) (raise-user-error 'position-check "server's position->id mapping doesn't match local copy. server: ~s local: ~s" (map car ids) (map message-uid msgs))))) (define (remove-delete-flags imap) (status "Removing old delete flags...") (imap-store imap '- (map message-position mailbox) (list (symbol->imap-flag 'deleted)))) ;; purge-messages : (listof messages) -> void (define (purge-messages marked bad-break break-ok) (unless (null? marked) (let-values ([(imap count new?) (connect)]) (with-handlers ([void (lambda (exn) (force-disconnect/status) (raise exn))]) (break-ok) (check-positions imap marked) (remove-delete-flags imap) (status "Deleting marked messages...") (imap-store imap '+ (map message-position marked) (list (symbol->imap-flag 'deleted))) (imap-expunge imap) (unless (equal? (imap-get-expunges imap) (map message-position marked)) (error "expunge notification list doesn't match expunge request")) (bad-break)) (set! mailbox (filter (lambda (m) (not (memq m marked))) mailbox)) (rebuild-mailbox-table!) (let loop ([l mailbox][p 1]) (unless (null? l) (set-message-position! (car l) p) (loop (cdr l) (add1 p)))) (write-mailbox) (let* ([problems null] [try-delete (lambda (f) (with-handlers ([void (lambda (x) (set! problems (cons x problems)))]) (delete-file f)))]) (for-each (lambda (m) (let ([uid (message-uid m)]) (try-delete (build-path mailbox-dir (format "~a" uid))) (when (message-downloaded? m) (try-delete (build-path mailbox-dir (format "~abody" uid)))))) marked) (unless (null? problems) (message-box "Warning" (apply string-append "There we problems deleting some local files:" (map (lambda (x) (string-append (string #\newline) (if (exn? x) (exn-message x) ""))) problems)) main-frame)) (display-message-count (length mailbox)) (status "Messages deleted"))))) ;; purge-marked : -> void ;; purges the marked mailbox messages. (define (purge-marked bad-break break-ok) (let* ([marked (filter message-marked? mailbox)]) (purge-messages marked bad-break break-ok))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GUI: Message List Tools ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define FROM-WIDTH 150) (define SUBJECT-WIDTH 300) (define UID-WIDTH 150) ;; update-frame-width : -> void ;; updates the green line's width ;; preferences's value of sirmail:frame-width must be ;; up to date before calling this function (define (update-frame-width) (let* ([goofy-margin 15] [calc-w (- (get-pref 'sirmail:frame-width) goofy-margin)]) (set! FROM-WIDTH (quotient calc-w 4)) (set! UID-WIDTH (quotient calc-w 5)) (set! SUBJECT-WIDTH (- calc-w FROM-WIDTH UID-WIDTH))) (when (object? sorting-from-snip) (send sorting-from-snip set-min-width FROM-WIDTH) (send sorting-from-snip set-max-width FROM-WIDTH) (send sorting-uid-snip set-min-width UID-WIDTH) (send sorting-uid-snip set-max-width UID-WIDTH) (send sorting-subject-snip set-min-width SUBJECT-WIDTH) (send sorting-subject-snip set-max-width SUBJECT-WIDTH)) (when (object? header-list) (let ([e (send header-list get-editor)]) (send e begin-edit-sequence) (for-each (lambda (item) (let* ([e (send item get-editor)] [line-snip (let loop ([s (send e find-first-snip)]) (cond [(not s) #f] [(is-a? s line-snip%) s] [else (loop (send s next))]))]) (send line-snip set-width (+ FROM-WIDTH SUBJECT-WIDTH UID-WIDTH)))) (send header-list get-items)) (send e end-edit-sequence)))) (update-frame-width) (define unselected-delta (make-object style-delta% 'change-normal-color)) (define selected-delta (make-object style-delta%)) (send selected-delta set-delta-foreground "BLUE") (define unread-delta (make-object style-delta% 'change-bold)) (define read-delta (make-object style-delta% 'change-weight 'normal)) (define marked-delta (make-object style-delta% 'change-italic)) (define unmarked-delta (make-object style-delta% 'change-style 'normal)) (define red-delta (make-object style-delta%)) (send red-delta set-delta-foreground "red") (define green-delta (make-object style-delta%)) (send green-delta set-delta-foreground "green") ;; url-delta : style-delta ;; this is used to higlight urls in the editor window (define url-delta (make-object style-delta% 'change-underline #t)) (send url-delta set-delta-foreground "blue") (define (apply-style i delta) (let ([e (send i get-editor)]) (send e change-style delta 0 (send e last-position)))) (define (set-standard-style t s e) (send t change-style (send (send t get-style-list) find-named-style "Standard") s e)) (define current-selected #f) (define (set-current-selected i) (unless (eq? current-selected i) (let ([e (send header-list get-editor)]) (send e begin-edit-sequence) (when current-selected (apply-style current-selected unselected-delta)) (set! current-selected i) (when i (apply-style i selected-delta) ; In case we downloaded it just now: (apply-style i read-delta)) (send e end-edit-sequence)))) (define vertical-line-snipclass (make-object (class snip-class% () (define/override (read s) (make-object vertical-line-snip%)) (super-instantiate ())))) (send vertical-line-snipclass set-version 1) (send vertical-line-snipclass set-classname "sirmail:vertical-line%") (send (get-the-snip-class-list) add vertical-line-snipclass) (define body-pen (send the-pen-list find-or-create-pen "blue" 0 'solid)) (define body-brush (send the-brush-list find-or-create-brush "WHITE" 'solid)) (define vertical-line-snip% (class snip% (inherit set-snipclass get-style get-admin) (field [width 15] [height 10]) [define/override get-extent (lambda (dc x y w-box h-box descent-box space-box lspace-box rspace-box) (for-each (lambda (box) (when box (set-box! box 0))) (list w-box h-box lspace-box rspace-box)) (let ([old-font (send dc get-font)]) (send dc set-font (send (get-style) get-font)) (let-values ([(w h descent ascent) (send dc get-text-extent "yxX")]) (when w-box (set-box! w-box width)) ;; add one here because I know the descent for the entire ;; line is going to be one more than the descent of the font. (when descent-box (set-box! descent-box (+ descent 1))) (when space-box (set-box! space-box ascent)) (let ([text (and (get-admin) (send (get-admin) get-editor))]) ;; add 2 here because I know lines are two pixels taller ;; than the font. How do I know? I just know. (set! height (+ h 2)) (when h-box (set-box! h-box (+ h 2))) (send dc set-font old-font)))))] [define/override draw (lambda (dc x y left top right bottom dx dy draw-caret) (let ([orig-pen (send dc get-pen)] [orig-brush (send dc get-brush)]) (send dc set-pen body-pen) (send dc set-brush body-brush) (send dc draw-line (+ x (quotient width 2)) y (+ x (quotient width 2)) (+ y (- height 1))) (send dc set-pen orig-pen) (send dc set-brush orig-brush)))] [define/override write (lambda (s) (void))] [define/override copy (lambda () (let ([s (make-object vertical-line-snip%)]) (send s set-style (get-style)) s))] (super-instantiate ()) (set-snipclass vertical-line-snipclass))) (define common-style-list #f) (define (single-style t) ;; Commented out for now: '(if common-style-list (send t set-style-list common-style-list) (set! common-style-list (send t get-style-list))) t) (define (make-field w) (let ([m (instantiate editor-snip% () (editor (single-style (let ([e (make-object text% 0.0)]) (send e set-keymap #f) (send e set-max-undo-history 0) e))) (with-border? #f) (top-margin 1) (top-inset 1) (bottom-margin 1) (bottom-inset 1) (min-width w) (max-width w))]) (send m set-flags (remove 'handles-events (send m get-flags))) m)) (define first-gap 35) (define second-gap 15) (define line-space 8) (define extra-height 2) (define left-edge-space 2) (define line-snip% (class snip% (init-field from subject uid) (define/override (draw dc x y left top bottom right dx dy draw-caret) (let ([w (get-width)]) (let-values ([(_1 h _2 _3) (send dc get-text-extent "yX")]) (let* ([old-clip (send dc get-clipping-region)] [new-clip #f] [set-clip (lambda (x y w h) (if old-clip (begin (send dc set-clipping-region #f) (unless new-clip (set! new-clip (make-object region% dc))) (send new-clip set-rectangle x y w h) (send new-clip intersect old-clip) (send dc set-clipping-region new-clip)) (send dc set-clipping-rect x y w h)))]) (set-clip x y (+ FROM-WIDTH (/ first-gap 2) (- line-space)) h) (send dc draw-text from (+ x left-edge-space) y #t) (set-clip (+ x FROM-WIDTH (/ first-gap 2) line-space) y (+ SUBJECT-WIDTH (/ second-gap 2) (- line-space)) h) (send dc draw-text subject (+ x FROM-WIDTH (/ first-gap 2) line-space) y #t) (send dc set-clipping-region old-clip) (send dc draw-text uid (+ x FROM-WIDTH first-gap SUBJECT-WIDTH (/ second-gap 2) line-space) y #t)) (let ([p (send dc get-pen)]) (send dc set-pen body-pen) (send dc draw-line (+ x FROM-WIDTH (/ first-gap 2)) y (+ x FROM-WIDTH (/ first-gap 2)) (+ y h extra-height)) (send dc draw-line (+ x FROM-WIDTH first-gap SUBJECT-WIDTH (/ second-gap 2)) y (+ x FROM-WIDTH first-gap SUBJECT-WIDTH (/ second-gap 2)) (+ y h extra-height)) (send dc set-pen p))))) (inherit get-style) (define/override (get-extent dc x y wb hb db sb lb rb) (let-values ([(w h d s) (send dc get-text-extent "yX" (send (get-style) get-font))]) (set-box/f! hb (+ extra-height h)) (set-box/f! wb (get-width)) (set-box/f! db d) (set-box/f! sb s) (set-box/f! lb 2) (set-box/f! rb 0))) (inherit get-admin) (field [width 500]) (define/public (set-width w) (let ([admin (get-admin)]) (when admin (send admin resized this #t))) (set! width w)) (define/private (get-width) width) (super-new))) (define (set-box/f! b v) (when (box? b) (set-box! b v))) (define (add-message m) (let* ([i (send header-list new-item)] [e (send i get-editor)] [one-line (lambda (s) (regexp-replace* #rx"[ \r\n\t]+" s " "))] [snip (new line-snip% (from (one-line (or (parse-encoded (message-from m)) ""))) (subject (one-line (or (parse-encoded (message-subject m)) no-subject-string))) (uid (format "~a" (message-uid m))))] [before (send e last-position)]) (send e begin-edit-sequence) (send i user-data (message-uid m)) (send e set-line-spacing 0) (send snip set-width (+ FROM-WIDTH SUBJECT-WIDTH UID-WIDTH)) (send e insert snip) (unless (message-downloaded? m) (send e change-style unread-delta before (+ before 1))) (when (memq 'marked (message-flags m)) (send e change-style marked-delta before (+ before 1))) (send e end-edit-sequence) i)) (define display-text% (html-text-mixin (text:foreground-color-mixin text:standard-style-list%))) ;; Class for the panel that has columns titles and ;; supports clicks to change the sort order (define sorting-list% (class hierarchical-list% (inherit get-editor selectable set-no-sublists) (define/private (find-sorting-key evt) (let loop ([editor (get-editor)]) (when editor (let ([xb (box (send evt get-x))] [yb (box (send evt get-y))]) (send editor global-to-local xb yb) (let* ([pos (send editor find-position (unbox xb) (unbox yb))] [snip (send editor find-snip pos 'after-or-none)]) (cond [(eq? snip sorting-from-snip) 'from] [(eq? snip sorting-subject-snip) 'subject] [(eq? snip sorting-uid-snip) 'uid] [(is-a? snip editor-snip%) (loop (send snip get-editor))] [else #f])))))) (define tracking #f) (define tracking-on? #f) (define/override (on-event evt) (cond [(send evt button-down?) (set! tracking (find-sorting-key evt)) (if tracking (begin (set! tracking-on? #t) (reset-sorting-tracking) (set-sorting-tracking tracking)) (begin (set! tracking-on? #f) (reset-sorting-tracking)))] [(and tracking (send evt button-up?)) (let ([sorting-key (find-sorting-key evt)] [was-tracking tracking]) (set! tracking #f) (set! tracking-on? #f) (and (eq? sorting-key was-tracking) (case sorting-key [(from) (sort-by-sender)] [(subject) (sort-by-subject)] [(uid) (sort-by-order-received)]))) (reset-sorting-tracking)] [(and tracking (send evt dragging?)) (let ([sorting-key (find-sorting-key evt)]) (if (eq? sorting-key tracking) (unless tracking-on? (set! tracking-on? #t) (reset-sorting-tracking) (set-sorting-tracking tracking)) (when tracking-on? (set! tracking-on? #f) (reset-sorting-tracking))))] [tracking (set! tracking #f) (set! tracking-on? #f) (reset-sorting-tracking)])) (super-new (style '(hide-hscroll hide-vscroll))) (set-no-sublists #t) (selectable #f))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GUI: Frame, Menus, & Key Bindings ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Message display modes (define show-full-headers? #f) (define quote-in-reply? #t) (define mime-mode? #t) (define no-mime-inline? #f) (define html-mode? #t) (define img-mode? #f) (define prefer-text? (get-pref 'sirmail:prefer-text)) (define global-keymap (make-object keymap%)) (send global-keymap add-function "new-mailer" (lambda (w e) (start-new-mailer #f "" "" "" "" "" null))) (send global-keymap add-function "disconnect" (lambda (w e) (disconnect) (force-disconnect/status))) (send global-keymap add-function "get-new-mail" (lambda (w e) (get-new-mail))) (send global-keymap add-function "archive-current" (lambda (w e) (send header-list archive-current-message))) (send global-keymap add-function "prev-msg" (lambda (w e) (send header-list select-prev))) (send global-keymap add-function "next-msg" (lambda (w e) (send header-list select-next))) (send global-keymap add-function "mark-msg" (lambda (w e) (send header-list mark-message #t))) (send global-keymap add-function "unmark-msg" (lambda (w e) (send header-list unmark-message #t))) (send global-keymap add-function "hit-msg" (lambda (w e) (send header-list hit))) (send global-keymap add-function "scroll-down" (lambda (w e) (if (send header-list selected-hit?) (let*-values ([(e) (send message get-editor)] [(x y) (send e editor-location-to-dc-location 0 0)]) (send e move-position 'down #f 'page) (let*-values ([(x2 y2) (send e editor-location-to-dc-location 0 0)]) (when (= y y2) (let ([current (send header-list get-selected)]) (send header-list select-next) (unless (eq? current (send header-list get-selected)) (send header-list hit)))))) (send header-list hit)))) (send global-keymap add-function "scroll-up" (lambda (w e) (when (send header-list selected-hit?) (let ([e (send message get-editor)]) (send e move-position 'up #f 'page))))) (send global-keymap add-function "rewind-msg" (lambda (w e) (send header-list rewind-selected))) (send global-keymap add-function "forward-msg" (lambda (w e) (send header-list forward-selected))) (send global-keymap add-function "purge" (lambda (w e) (purge-marked/update-headers))) (send global-keymap add-function "gc" (lambda (w e) (collect-garbage) (collect-garbage) (dump-memory-stats))) (send global-keymap add-function "show-memory-graph" (lambda (w e) (show-memory-graph))) (send global-keymap map-function ":m" "new-mailer") (send global-keymap map-function ":g" "get-new-mail") (send global-keymap map-function ":a" "archive-current") (send global-keymap map-function ":i" "disconnect") (send global-keymap map-function ":n" "next-msg") (send global-keymap map-function ":p" "prev-msg") (send global-keymap map-function ":return" "hit-msg") (send global-keymap map-function ":d" "mark-msg") (send global-keymap map-function ":u" "unmark-msg") (send global-keymap map-function ":space" "scroll-down") (send global-keymap map-function ":b" "scroll-up") (send global-keymap map-function "#" "purge") (send global-keymap map-function "!" "gc") (send global-keymap map-function ":z" "show-memory-graph") (send global-keymap map-function ":m:left" "rewind-msg") (send global-keymap map-function ":d:left" "rewind-msg") (send global-keymap map-function ":m:right" "forward-msg") (send global-keymap map-function ":d:right" "forward-msg") (define icon (make-object bitmap% (build-path (collection-path "sirmail") "postmark.bmp"))) (define icon-mask (make-object bitmap% (build-path (collection-path "sirmail") "postmark-mask.xbm"))) (unless (and (send icon ok?) (send icon-mask ok?)) (set! icon #f)) (define sm-super-frame% (frame:standard-menus-mixin frame:basic%)) (define sm-frame% (class sm-super-frame% (inherit get-menu-bar set-icon) (define/override (file-menu:create-new?) #f) (define/override (file-menu:create-open?) #f) (define/override (file-menu:create-open-recent?) #f) ;; -------------------- File Menu -------------------- (define/override (file-menu:between-save-as-and-print file-menu) (make-object menu-item% "&Get New Mail" file-menu (lambda (i e) (get-new-mail)) #\g) (make-object menu-item% "&Download All" file-menu (lambda (i e) (download-all)) #\l) (make-object menu-item% "Archive Message" file-menu (lambda (i e) (send header-list archive-current-message))) (make-object separator-menu-item% file-menu) (make-object menu-item% "&Open Folders List" file-menu (lambda (x1 x2) (open-folders-window))) (make-object separator-menu-item% file-menu) (make-object menu-item% "&New Message" file-menu (lambda (i e) (start-new-mailer #f "" "" "" "" "" null)) #\m) (make-object menu-item% "&Resume Message..." file-menu (lambda (i e) (let ([file (get-file "Select message to resume" main-frame)]) (when file (start-new-mailer file "" "" "" "" "" null))))) (instantiate menu-item% () (label "Send Queued Messages") (parent file-menu) (demand-callback (lambda (menu-item) (send menu-item enable (enqueued-messages?)))) (callback (lambda (i e) (send-queued-messages)))) (make-object separator-menu-item% file-menu) (make-object menu-item% "&Save Message As..." file-menu (lambda (i e) (let ([f (put-file "Save message to" main-frame)]) (when f (send (send message get-editor) save-file f 'text)))))) (define/override (file-menu:create-print?) #t) (define/override (file-menu:print-callback i e) (send (send message get-editor) print)) (define/override (file-menu:between-print-and-close file-menu) (make-object separator-menu-item% file-menu) (make-object menu-item% "D&isconnect" file-menu (lambda (i e) (disconnect) (force-disconnect/status)) #\i)) (define/override (file-menu:close-callback i e) (send main-frame on-close)) (define/override (file-menu:create-quit?) #f) ;; -------------------- Help Menu -------------------- (define/override (help-menu:after-about menu) (make-object menu-item% "&Help" menu (lambda (i e) (let* ([f (instantiate frame% ("Help") [width 500] [height 300])] [e (make-object text%)] [c (make-object editor-canvas% f e)]) (send e load-file (build-path (collection-path "sirmail") "doc.txt")) (send f show #t)))) (super help-menu:after-about menu)) ;; -------------------- Misc. -------------------- (inherit get-edit-target-object) [define/override on-size (lambda (w h) (put-pref 'sirmail:frame-width w) (put-pref 'sirmail:frame-height h) (update-frame-width) (super on-size w h))] [define/augment can-close? (lambda () (and (send (get-menu-bar) is-enabled?) (inner #t can-close?)))] [define/augment on-close (lambda () (logout) (set! done? #t) (inner (void) on-close))] [define/override on-subwindow-char (lambda (w e) (or (and (send (send main-frame get-menu-bar) is-enabled?) (or (send global-keymap handle-key-event w e) (and (eq? #\tab (send e get-key-code)) (member w (list header-list message)) (send (if (eq? w message) header-list message) focus)))) (super on-subwindow-char w e)))] (super-instantiate ()) (when icon (set-icon icon icon-mask 'both)))) ;; -------------------- Frame Creation -------------------- (set! main-frame (make-object sm-frame% mailbox-name #f (get-pref 'sirmail:frame-width) (get-pref 'sirmail:frame-height))) (define mb (send main-frame get-menu-bar)) ;; -------------------- Message Menu -------------------- (define msg-menu (make-object menu% "&Message" mb)) (make-object menu-item% "&Reply" msg-menu (lambda (i e) (do-reply #f quote-in-reply?)) #\R) (make-object menu-item% "&Follow Up" msg-menu (lambda (i e) (do-reply #t quote-in-reply?)) #\t) (make-object menu-item% "F&orward" msg-menu (lambda (i e) (do-forward)) #\W) (send (make-object checkable-menu-item% "&Quote Original" msg-menu (lambda (item e) (set! quote-in-reply? (send item is-checked?)))) check #t) (make-object separator-menu-item% msg-menu) (make-object menu-item% "&Mark Selected" msg-menu (lambda (i e) (send header-list mark-message #t)) #\D) (make-object menu-item% "&Unmark Selected" msg-menu (lambda (i e) (send header-list unmark-message #t)) #\U) (define (mark-all mark? between?) (let* ([marked-uids (map message-uid (filter (if mark? (lambda (x) (not (message-marked? x))) message-marked?) mailbox))] [items (send header-list get-items)] [selected (send header-list get-selected)]) (for-each (lambda (i) (when (member (send i user-data) marked-uids) (send i select #t) (if mark? (send header-list mark-message #f) (send header-list unmark-message #f)))) (if between? (let ([drop-some (lambda (items) (let loop ([items items]) (if (null? items) null (if (message-marked? (find-message (send (car items) user-data))) items (loop (cdr items))))))]) (reverse (drop-some (reverse (drop-some items))))) items)) (write-mailbox) (status "~aarked all" (if mark? "M" "Unm")) (if selected (send selected select #t) (send (send header-list get-selected) select #f)))) (make-object menu-item% "Mark All" msg-menu (lambda (i e) (mark-all #t #f))) (make-object menu-item% "Unmark All" msg-menu (lambda (i e) (mark-all #f #f))) (make-object menu-item% "Mark All Between Marked" msg-menu (lambda (i e) (mark-all #t #t))) (make-object separator-menu-item% msg-menu) (make-object menu-item% "&Delete Marked" msg-menu (lambda (i e) (when (eq? 'yes (confirm-box "Delete Marked?" "Really delete the marked messages?" main-frame)) (purge-marked/update-headers)))) (make-object (class menu-item% (inherit enable set-label) (define/override (on-demand) (let ([folder (get-active-folder)]) (enable folder) (when folder (set-label (format "&Copy Marked to ~a" folder))))) (super-instantiate ())) "&Copy Marked to Selected Folder" msg-menu (lambda x (let ([mbox (get-active-folder)]) (if mbox (copy-marked-to mbox) (bell))))) (make-object separator-menu-item% msg-menu) (define sort-menu (make-object menu% "&Sort" msg-menu)) (let ([m (make-object menu% "Decode" msg-menu)]) (letrec ([switch (lambda (item e) (if (send item is-checked?) (begin ;; Disable others: (send raw check (eq? raw item)) (send mime-lite check (eq? mime-lite item)) (send mime check (eq? mime item)) (send html check (eq? html item)) (send img check (eq? img item)) ;; Update flags (set! mime-mode? (or (send mime is-checked?) (send mime-lite is-checked?) (send html is-checked?) (send img is-checked?))) (set! no-mime-inline? (or (send mime-lite is-checked?))) (set! html-mode? (or (send html is-checked?) (send img is-checked?))) (set! img-mode? (send img is-checked?)) ;; Re-decode (redisplay-current)) ;; Turn it back on (send item check #t)))] [raw (make-object checkable-menu-item% "&Raw" m switch)] [mime-lite (make-object checkable-menu-item% "MIME &without Inline" m switch)] [mime (make-object checkable-menu-item% "&MIME" m switch)] [html (make-object checkable-menu-item% "MIME and &HTML" m switch)] [img (make-object checkable-menu-item% "MIME, HTML, and &Images" m switch)]) (send (if (and mime-mode? html-mode?) html (if mime-mode? mime raw)) check #t) (make-object separator-menu-item% m) (send (make-object checkable-menu-item% "Prefer &Text" m (lambda (i e) (put-pref 'sirmail:prefer-text (send i is-checked?)) (set! prefer-text? (send i is-checked?)) (redisplay-current))) check prefer-text?))) (define wrap-lines-item (make-object checkable-menu-item% "&Wrap Lines" msg-menu (lambda (item e) (put-pref 'sirmail:wrap-lines (send item is-checked?)) (send (send message get-editor) auto-wrap (send item is-checked?))))) (make-object checkable-menu-item% "&View Full Header" msg-menu (lambda (i e) (set! show-full-headers? (send i is-checked?)) (redisplay-current))) (make-object menu-item% "by From" sort-menu (lambda (i e) (sort-by-sender))) (make-object menu-item% "by Subject" sort-menu (lambda (i e) (sort-by-subject))) (make-object menu-item% "by Date" sort-menu (lambda (i e) (sort-by-date))) (make-object menu-item% "by Order Received" sort-menu (lambda (i e) (sort-by-order-received))) (make-object menu-item% "by Size" sort-menu (lambda (i e) (sort-by-size))) (make-object menu-item% "by Header Field..." sort-menu (lambda (i e) (sort-by-header-field))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GUI: Message List ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define drag-cursor (make-object cursor% 'hand)) (define plain-cursor (make-object cursor% 'arrow)) (define arrow+watch-cursor (make-object cursor% 'arrow+watch)) (define header-list% (class hierarchical-list% (inherit get-items show-focus set-cursor select) (field [selected #f]) (define/public (mark marked? update?) (when selected (let* ([uid (send selected user-data)] [m (find-message uid)] [flags (message-flags m)]) (unless (eq? (not marked?) (not (memq 'marked flags))) (set-message-flags! m (if marked? (cons 'marked flags) (remq 'marked flags))) (when update? (write-mailbox)) (apply-style selected (if marked? marked-delta unmarked-delta)) (when update? (status "~aarked" (if marked? "M" "Unm"))))))) (define/public (archive-current-message) (when selected (let ([archive-mailbox (ARCHIVE-MAILBOX)]) (when archive-mailbox (let* ([uid (send selected user-data)] [item (find-message uid)]) (header-changing-action #f (lambda () (as-background enable-main-frame (lambda (bad-break break-ok) (with-handlers ([void no-status-handler]) (copy-messages-to (list item) archive-mailbox) (purge-messages (list item) bad-break break-ok))) close-frame)))))))) (define/public (hit) (when selected (on-double-select selected))) (define/public (mark-message update?) (mark #t update?)) (define/public (unmark-message update?) (mark #f update?)) (define/public (selected-hit?) (eq? selected current-selected)) (define/override (on-select i) (set! selected i)) ;; -------------------- Message selection -------------------- (define past-selected null) (define future-selected null) (define/private (push-selected uid) (unless (and (pair? past-selected) (equal? uid (car past-selected))) (set! future-selected (remove uid future-selected)) (set! past-selected (cons uid (remove uid past-selected))))) (define/public (rewind-selected) (when (pair? past-selected) (set! future-selected (cons (car past-selected) future-selected)) (set! past-selected (cdr past-selected))) (unless (pair? past-selected) (set! past-selected (reverse future-selected)) (set! future-selected null)) (set! past-selected (select-from-stack past-selected))) (define/public (forward-selected) (unless (pair? future-selected) (set! future-selected (reverse past-selected)) (set! past-selected null)) (set! future-selected (select-from-stack future-selected)) (when (pair? future-selected) (set! past-selected (cons (car future-selected) past-selected)) (set! future-selected (cdr future-selected)))) (define (select-from-stack selected) (if (pair? selected) (let* ([uid (car selected)] [i (ormap (lambda (i) (and (equal? uid (send i user-data)) i)) (send header-list get-items))]) (if i (begin (select i) (do-double-select i #f) selected) (select-from-stack (cdr selected)))) null)) (define/override (on-double-select i) (do-double-select i #t)) (define/private (do-double-select i push?) (let ([e (send message get-editor)] [uid (send i user-data)]) (dynamic-wind (lambda () (send e lock #f) (send e begin-edit-sequence)) (lambda () (send e erase) (set-current-selected #f) (let* ([h (get-header uid)] [small-h (get-viewable-headers h)]) (send e insert (string-crlf->lf small-h) 0 'same #f) ;; Do the body (possibly mime) (let ([body (as-background enable-main-frame (lambda (break-bad break-ok) (get-body uid break-bad break-ok)) close-frame)] [insert (lambda (body delta) (let ([start (send e last-position)]) (send e set-position start) (send e insert body start 'same #f) (let ([end (send e last-position)]) (delta e start end))))]) (when push? (push-selected uid)) (parse-and-insert-body h body e insert 78 img-mode?))) (send e set-position 0) (set-current-selected i)) (lambda () (send e end-edit-sequence) (send e lock #t))))) ;; -------------------- Message drag'n'drop -------------------- (inherit get-editor client->screen) (field (dragging-item #f) (dragging-title #f) (last-status #f) (drag-start-x 0) (drag-start-y 0)) (define/override (on-event evt) (cond [(send evt button-down?) (when dragging-item (status "") (send (get-editor) set-cursor plain-cursor) (set! dragging-item #f)) (let ([text (get-editor)]) (when text (let ([xb (box (send evt get-x))] [yb (box (send evt get-y))]) (send text global-to-local xb yb) (let* ([pos (send text find-position (unbox xb) (unbox yb))] [snip (send text find-snip pos 'after-or-none)] [item (and (is-a? snip hierarchical-item-snip%) (send snip get-item))]) (set! dragging-title "???") (set! dragging-item item) (set! drag-start-x (send evt get-x)) (set! drag-start-y (send evt get-y)) (when dragging-item (let* ([ud (send dragging-item user-data)] [message (find-message ud)] [cap-length 50]) (when message (let ([title (message-subject message)]) (cond [(not title) (set! dragging-title no-subject-string)] [((string-length title) . <= . cap-length) (set! dragging-title title)] [else (set! dragging-title (string-append (substring title 0 (- cap-length 3)) "..."))])))))))))] [(send evt dragging?) (when dragging-item (when (or ((abs (- (send evt get-x) drag-start-x)) . > . 5) ((abs (- (send evt get-y) drag-start-y)) . > . 5)) (send (get-editor) set-cursor drag-cursor)) (let-values ([(gx gy) (client->screen (send evt get-x) (send evt get-y))]) (let ([mailbox-name (send-message-to-window gx gy (list gx gy))]) (if (string? mailbox-name) (status "Move message \"~a\" to ~a" dragging-title mailbox-name) (status "")))))] [(send evt button-up?) (when dragging-item (send (get-editor) set-cursor plain-cursor) (let-values ([(gx gy) (client->screen (send evt get-x) (send evt get-y))] [(ditem) dragging-item]) (set! dragging-item #f) (let ([mailbox-name (send-message-to-window gx gy (list gx gy))]) (if (bytes? mailbox-name) (let* ([user-data (send ditem user-data)] [item (find-message user-data)]) (when item (header-changing-action #f (lambda () (as-background enable-main-frame (lambda (bad-break break-ok) (with-handlers ([void no-status-handler]) (void) (copy-messages-to (list item) mailbox-name) (purge-messages (list item) bad-break break-ok))) close-frame))))) (status "")))))] [else (when (and dragging-item (not (and (or (send evt leaving?) (send evt entering?)) (or (send evt get-left-down) (send evt get-middle-down) (send evt get-right-down))))) (set! dragging-item #f) (send (get-editor) set-cursor plain-cursor) (status ""))]) (super on-event evt)) (super-new (style '(no-hscroll))) (show-focus #t))) ;; header-changing-action: bool thunk -> thunk-result ;; Use this function to bracket operations that change ;; `mailbox'. It will use before an after values to update ;; the message list. (define (header-changing-action downloads? go) (let ([old-mailbox mailbox]) (dynamic-wind void go (lambda () (let ([items (send header-list get-items)] [selected (send header-list get-selected)] [need-del-selection? #f] [set-selection? #f]) (send (send header-list get-editor) begin-edit-sequence) (for-each (lambda (i) (let ([a (find-message (send i user-data))]) (if a (begin ; Message still here (when (and downloads? (message-downloaded? a)) (apply-style i read-delta)) (when need-del-selection? (set! need-del-selection? #f) (send i select #t))) (begin ; Message gone (when (eq? i selected) (set! need-del-selection? #t)) (when (eq? i current-selected) (let ([e (send message get-editor)]) (send e begin-edit-sequence) (send e lock #f) (send e erase) (send e lock #t) (send e end-edit-sequence)) (set-current-selected #f)) (send header-list delete-item i))))) items) (let ([old-ids (make-hash-table 'equal)]) (for-each (lambda (m) (hash-table-put! old-ids (message-uid m) #t)) old-mailbox) (for-each (lambda (m) (unless (hash-table-get old-ids (message-uid m) #f) (let ([i (add-message m)]) (unless set-selection? (set! set-selection? #t) (send i select #t) (send i scroll-to))))) mailbox)) (send (send header-list get-editor) end-edit-sequence)))))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GUI: Message Operations ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Closes connections and terminates the window (define (logout) (with-handlers ([void (lambda (x) (show-error x) (when (eq? 'yes (confirm-box "Error" "There was an error disconnecting. Exit anyway?" main-frame)) (exit-sirmail "read-window exception (1)") (send main-frame show #f)))]) (disconnect) (when biff (send biff stop)) (exit-sirmail "read-window exception (2)") (send main-frame show #f))) (define (get-new-mail) (with-handlers ([void (lambda (x) (status "") (if (send disconnected-msg is-shown?) (raise x) (begin (show-error x) (when (exn:fail:network? x) (when (eq? 'yes (confirm-box "Error" (format "There was an communication error.~nClose the connection?") main-frame)) (force-disconnect/status))))))]) (header-changing-action #f (lambda () (as-background enable-main-frame (lambda (break-bad break-ok) (when (or (not initialized?) (check-for-new break-bad break-ok)) (update-local break-bad break-ok))) close-frame))))) (define (purge-marked/update-headers) (header-changing-action #f (lambda () (as-background enable-main-frame (lambda (break-bad break-ok) (with-handlers ([void no-status-handler]) (purge-marked break-bad break-ok))) close-frame)))) (define (copy-marked-to dest-mailbox-name) (let* ([marked (filter message-marked? mailbox)]) (as-background enable-main-frame (lambda (break-bad break-ok) (copy-messages-to marked dest-mailbox-name)) close-frame))) (define (copy-messages-to marked dest-mailbox-name) (unless (null? marked) (let-values ([(imap count new?) (connect)]) (check-positions imap marked) (status "Copying messages to ~a..." dest-mailbox-name) (imap-copy imap (map message-position marked) dest-mailbox-name) (status "Copied to ~a" dest-mailbox-name)))) (define (auto-file) (as-background enable-main-frame (lambda (break-bad break-ok) (break-ok) (map (lambda (auto) (let* ([dest-mailbox-name (car auto)] [fields (map car (cadr auto))] [val-rxs (map string->regexp (map cadr (cadr auto)))]) (with-handlers ([void no-status-handler]) (break-ok) (status "Finding ~a messages..." dest-mailbox-name) (let ([file-msgs (filter (lambda (m) (and (not (message-marked? m)) (let ([h (get-header (message-uid m))]) (ormap (lambda (field val-rx) (let ([v (extract-field field h)]) (and v (regexp-match val-rx v)))) fields val-rxs)))) mailbox)]) (unless (null? file-msgs) (status "Filing to ~a..." dest-mailbox-name) (break-bad) (let-values ([(imap count new?) (connect)]) (status (format "Filing to ~a..." dest-mailbox-name)) ; Copy messages for filing: (imap-copy imap (map message-position file-msgs) dest-mailbox-name) ; Mark them (let the user delete) (for-each (lambda (m) (set-message-flags! m (cons 'marked (message-flags m))) (let ([i (let ([items (send header-list get-items)] [uid (message-uid m)]) (ormap (lambda (i) (and (eq? (send i user-data) uid) i)) items))]) (apply-style i marked-delta))) file-msgs) (write-mailbox))))))) (AUTO-FILE-TABLE))) close-frame) (status "Auto file done")) (define (download-all) (get-new-mail) (header-changing-action #t (lambda () (as-background enable-main-frame (lambda (break-bad break-ok) (with-handlers ([exn:break? (lambda (x) "")]) (break-ok) (with-handlers ([exn:break? (lambda (x) (void))]) (for-each (lambda (message) (let ([uid (message-uid message)]) (break-bad) (get-body uid break-bad break-ok) (break-ok))) mailbox)))) close-frame)))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GUI: Rest of Frame ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define sizing-panel (make-object panel:vertical-dragable% (send main-frame get-area-container))) (define top-half (make-object vertical-panel% sizing-panel)) (define button-panel (new horizontal-panel% (parent top-half) (stretchable-height #f))) (define sorting-list (instantiate sorting-list% () (parent top-half) (stretchable-height #f) (vertical-inset 1))) (define header-list (make-object header-list% top-half)) (send (send header-list get-editor) set-line-spacing 0) (define message (new canvas:color% [parent sizing-panel] [style '(auto-hscroll)])) (send header-list min-height 20) (send header-list stretchable-height #t) (send header-list set-no-sublists #t) (send main-frame reflow-container) (with-handlers ([void void]) (send sizing-panel set-percentages (list 1/3 2/3))) (let ([e (make-object display-text%)]) ((current-text-keymap-initializer) (send e get-keymap)) (send e set-max-undo-history 0) (send message set-editor e) (make-fixed-width message e #f #f) (let ([b (make-object bitmap% (build-path (collection-path "icons") "return.xbm") 'xbm)]) (when (send b ok?) (send e set-autowrap-bitmap b))) (send e lock #t)) (when (get-pref 'sirmail:wrap-lines) (send wrap-lines-item check #t) (send (send message get-editor) auto-wrap #t)) ;; enable-main-frame - use with `as-background' (define can-poll? #t) (define (enable-main-frame on? refocus break-proc) (let ([w (send main-frame get-focus-window)]) (set! can-poll? on?) (send sorting-list enable on?) (send header-list enable on?) (send message enable on?) (let* ([cursor (if on? plain-cursor arrow+watch-cursor)]) (send main-frame set-cursor cursor) (send (send sorting-list get-editor) set-cursor cursor #t) (send (send header-list get-editor) set-cursor cursor #t) (send (send message get-editor) set-cursor (if on? #f cursor) #t)) (send (send main-frame get-menu-bar) enable on?) (set! cancel-button-todo break-proc) (send cancel-button enable (not on?)) (when (and on? refocus) (send refocus focus)) w)) (define (close-frame) (send main-frame show #f)) (define no-status-handler (lambda (x) (status "") (raise x))) (define disable-button-panel (make-object horizontal-panel% button-panel)) (define mailbox-message (make-object message% (format "~a: XXXXX" mailbox-name) disable-button-panel)) (define (display-message-count n) (send mailbox-message set-label (format "~a: ~a" mailbox-name n))) (display-message-count (length mailbox)) (define new-mail-message% (class canvas% (inherit get-dc get-client-size get-parent horiz-margin vert-margin) (init-field font) (define message "<>") (define/override (on-paint) (let ([dc (get-dc)]) (send dc set-font font) (let-values ([(w h) (get-client-size)] [(tw th ta td) (send dc get-text-extent message)]) (send dc set-pen (send the-pen-list find-or-create-pen (get-panel-background) 1 'transparent)) (send dc set-brush (send the-brush-list find-or-create-brush (get-panel-background) 'panel)) (send dc draw-rectangle 0 0 w h) (send dc draw-text message (- (/ w 2) (/ tw 2)) (- (/ h 2) (/ th 2)) #t)))) (define/public (set-message n) (set! message (cond [(get-pref 'sirmail:always-happy) "New Mail!"] [(n . <= . 50) "New Mail!"] [(n . <= . 200) "New Mail"] [else "New Mail!@#$%"])) (update-min-width)) (inherit min-width) (define/private (update-min-width) (let-values ([(w h d s) (send (get-dc) get-text-extent message font)]) (min-width (inexact->exact (ceiling w))))) (super-new (style '(transparent))) (update-min-width) (inherit stretchable-width) (horiz-margin 2) (vert-margin 2) (stretchable-width #f))) (define-values (show-new-mail-msg hide-new-mail-msg disconnected-msg enqueued-msg) (let* ([font (make-object font% (send normal-control-font get-point-size) 'system 'normal 'bold)]) (let ([spacer (make-object message% " " disable-button-panel)] [m (make-object new-mail-message% font disable-button-panel)] [d (new message% [label "Disconnected"] [parent disable-button-panel] [font font])] [e-msg (new message% [label "Mail Enqueued"] [parent disable-button-panel] [font font])]) (send m show #f) (send e-msg show #f) (values (lambda () (send m set-message (length mailbox)) (send m show #t)) (lambda () (send m show #f)) d e-msg)))) (thread (lambda () (let loop () (unless (and (object? main-frame) (send main-frame is-shown?) (procedure? enqueued-messages?)) (sleep 1/2) (loop))) (let loop () (when (send main-frame is-shown?) (send enqueued-msg show (enqueued-messages?)) (sleep 1/2) (loop))))) ;; Optional GC icon (lots of work for this little thing!) (when (get-pref 'sirmail:show-gc-icon) (let* ([gif (make-object bitmap% (build-path (collection-path "icons") "recycle.png"))] [w (send gif get-width)] [h (send gif get-height)] [scale 1] [recycle-bm (make-object bitmap% (quotient w scale) (quotient h scale))] [dc (make-object bitmap-dc% recycle-bm)]) (send dc set-scale (/ 1 scale) (/ 1 scale)) (send dc draw-bitmap gif 0 0) (send dc set-bitmap #f) (let* ([w (send recycle-bm get-width)] [h (send recycle-bm get-height)] [canvas (instantiate canvas% (button-panel) [stretchable-width #f] [stretchable-height #f] [style '(border)])] [empty-bm (make-object bitmap% w h)] [dc (make-object bitmap-dc% empty-bm)]) (send canvas min-client-width w) (send canvas min-client-height h) (send dc clear) (send dc set-bitmap #f) (register-collecting-blit canvas 0 0 w h recycle-bm empty-bm 0 0 0 0)))) (define cancel-button (make-object button% "Stop" button-panel (lambda (b e) (cancel-button-todo)))) (define cancel-button-todo void) (send cancel-button enable #f) ;; -------------------- Status Line -------------------- (define last-status "") (define status-sema (make-semaphore 1)) (define (status . args) (semaphore-wait status-sema) (let ([s (apply format args)]) (unless (equal? s last-status) (set! last-status s) (update-status-text))) (semaphore-post status-sema)) ;; update-status-text : -> void ;; =any thread= (define (update-status-text) (let ([mem-str (if (and vsz rss) (format "(mz: ~a vsz: ~a rss: ~a vocab: ~a)" (format-number (quotient (current-memory-use) 1024)) vsz rss (word-count)) (format "(mz: ~a vocab: ~a)" (format-number (quotient (current-memory-use) 1024)) (word-count)))]) (send main-frame set-status-text (if (equal? last-status "") mem-str (string-append last-status " " mem-str))))) (thread (lambda () (let loop () (semaphore-wait status-sema) (when (object? main-frame) (update-status-text)) (semaphore-post status-sema) (sleep 5) (unless done? (loop))))) (define vsz #f) (define rss #f) (define (start-vsz/rss-thread) (thread (lambda () (define (get-numbers) (with-handlers ([exn:fail? (lambda (x) #f)]) (let ([re:nums #rx"[^ \t]*[ \t]*[^ \t]*[ \t]*[^ \t]*[ \t]*[^ \t]*[ \t]*([0-9]*)[ \t]*([0-9]*)[ \t]*"]) (let ([m (regexp-match re:nums (get-lines))]) (and m (map string->number (cdr m))))))) (define command "ps wwaux | grep SirMail | grep -v grep") (define (get-lines) (let ([p (open-output-string)]) (parameterize ([current-output-port p] [current-input-port (open-input-string "")]) (system command)) (get-output-string p))) (let loop () (let ([v (get-numbers)]) (when (and v (send main-frame is-shown?)) (set! vsz (format-number (car v))) (set! rss (format-number (cadr v))) (sleep 10) (loop))))))) ;; copied from framerok/private/frame.sss -- be sure to propogate fixes.... ;; or establish single point of control. (define (format-number n) (if n (let loop ([n n]) (cond [(<= n 1000) (number->string n)] [else (string-append (loop (quotient n 1000)) "," (pad-to-3 (modulo n 1000)))])) "???")) (define (pad-to-3 n) (cond [(<= n 9) (format "00~a" n)] [(<= n 99) (format "0~a" n)] [else (number->string n)])) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GUI: Sorting ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (send sorting-list min-height 5) (define sorting-text (send (send sorting-list new-item) get-editor)) (define sorting-text-from (make-object text%)) (send sorting-text-from insert "From") (define sorting-text-subject (make-object text%)) (send sorting-text-subject insert "Subject") (define sorting-text-uid (make-object text%)) (send sorting-text-uid insert "UID") (define (add-sorting-es text width) (let ([es (instantiate editor-snip% () (with-border? #f) (editor text) (top-margin 1) (top-inset 1) (bottom-margin 1) (bottom-inset 1))]) (send sorting-text insert es) (send es set-flags (remove 'handles-events (send es get-flags))) es)) (send sorting-list set-line-count 1) (define sorting-from-snip (add-sorting-es sorting-text-from FROM-WIDTH)) (send sorting-text insert (make-object vertical-line-snip%)) (define sorting-subject-snip (add-sorting-es sorting-text-subject SUBJECT-WIDTH)) (send sorting-text insert (make-object vertical-line-snip%)) (define sorting-uid-snip (add-sorting-es sorting-text-uid UID-WIDTH)) (when (AUTO-FILE-TABLE) (make-object separator-menu-item% msg-menu) (make-object menu-item% "Auto File" msg-menu (lambda (i e) (auto-file)))) (define (redisplay-current) (when current-selected (send header-list on-double-select current-selected))) (define (sort-by-date) (sort-by-fields (list (list "date" date-cmp))) (reset-sorting-text-styles)) (define (sort-by-sender) (sort-by fromseconds -- too expensive. (define (date-cmp aid bid a b) (define (month->number mon) (string-lowercase! mon) (case (string->symbol mon) [(jan) 1] [(feb) 2] [(mar) 3] [(apr) 4] [(may) 5] [(jun) 6] [(jul) 7] [(aug) 8] [(sep) 9] [(oct) 10] [(nov) 11] [(dec) 12])) (define (pairwise-cmp l1 l2) (cond [(and (null? l1) (null? l2)) 'same] [(or (null? l1) (null? l2)) (error 'pairwise-cmp "internal error; date lists mismatched")] [(= (car l1) (car l2)) (pairwise-cmp (cdr l1) (cdr l2))] [else (< (car l1) (car l2))])) (define (get-date a) (let* ([m (regexp-match re:date a)]) (if m (let* ([datel (cdr m)] [day (string->number (first datel))] [month (month->number (second datel))] [year (string->number (third datel))] [hours (string->number (fourth datel))] [minutes (string->number (fifth datel))] [seconds (string->number (sixth datel))]) (list year month day hours minutes seconds)) (list 0 0 0 0 0 0)))) (pairwise-cmp (get-date a) (get-date b))) (define re:quote "[\"<>]") ;; from boolean ;; compares messages by from lines, defaults to uid if froms are equal. (define (from string (define (get-address msg) (let ([frm (message-from msg)]) (if frm (hash-table-get address-memo-table frm (lambda () (let ([res (with-handlers ([exn:fail? (lambda (x) "")]) (regexp-replace* re:quote (car (extract-addresses frm 'address)) ""))]) (hash-table-put! address-memo-table frm res) res))) ""))) ;; get-address : message -> string (define address-memo-table (make-hash-table 'equal)) (define ((field boolean ;; compares messages by subject lines, defaults to uid if subjects are equal. (define (subject boolean (define (string-cmp/default-uid str-a str-b a b) (if (string-locale-ci=? str-a str-b) (< (message-uid a) (message-uid b)) (string-locale-ci bool ;; returns true if there are messages to send (define (enqueued-messages?) (not (= 0 (length (directory-list queue-directory))))) ;; send-queued-messsages : -> void ;; sends the files queued in `queue-directory' (define (send-queued-messages) (for-each send-queued-message (directory-list queue-directory))) ;; send-queued-message : string -> void ;; sends the email message in `filename' by opening a window and sending it a message (define (send-queued-message filename) (start-new-window (lambda () (let ([full-filename (build-path queue-directory filename)]) (send (new-mailer full-filename "" "" "" "" "" null (length mailbox)) send-message) (delete-file full-filename))))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Message Parsing ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define get-viewable-headers (lambda (h) ((if mime-mode? parse-encoded values) (if show-full-headers? h (let loop ([l (reverse (MESSAGE-FIELDS-TO-SHOW))] [small-h empty-header]) (if (null? l) small-h (let ([v (extract-field (car l) h)]) (if v (loop (cdr l) (insert-field (car l) v small-h)) (loop (cdr l) small-h))))))))) (define (parse-and-insert-body header body text-obj insert sep-width img-mode?) (define (insert-separator) (insert (format "\n~a\n" (make-string sep-width #\-)) (lambda (t s e) (send t change-style green-delta (add1 s) (sub1 e))))) (if mime-mode? (let mime-loop ([msg (with-handlers ([exn:fail? (lambda (x) (mime:make-message #f (mime:make-entity 'text 'plain 'charset 'encoding (mime:make-disposition 'error 'filename 'creation 'modification 'read 'size 'params) 'params 'id 'description 'other 'fields null (lambda (o) (fprintf o "MIME error: ~a" (if (exn? x) (exn-message x) x)))) #f))]) (mime:mime-analyze (bytes-append (string->bytes/latin-1 header (char->integer #\?)) body)))] [skip-headers? #t]) (let* ([ent (mime:message-entity msg)] [slurp-stream (lambda (ent o) (with-handlers ([exn:fail? (lambda (x) (fprintf o "~n[decode error: ~a]~n" (if (exn? x) (exn-message x) x)))]) ((mime:entity-body ent) o)))] [slurp (lambda (ent) (let ([o (open-output-bytes)]) (slurp-stream ent o) (get-output-bytes o)))] [generic (lambda (ent) (let ([fn (parse-encoded (or (let ([disp (mime:entity-disposition ent)]) (and (not (equal? "" (mime:disposition-filename disp))) (mime:disposition-filename disp))) (let ([l (mime:entity-params ent)]) (let ([a (assoc "name" l)]) (and a (cdr a))))))] [sz (mime:disposition-size (mime:entity-disposition ent))] [content #f]) (let ([to-file (lambda (fn) (as-background enable-main-frame (lambda (break-bad break-ok) (break-ok) (let ([v (slurp ent)]) (break-bad) (unless content (set! content v))) (break-ok) (with-output-to-file fn (lambda () (write-bytes content)) 'truncate/replace)) close-frame))]) (insert-separator) (insert (format "[~a/~a~a~a]" (mime:entity-type ent) (mime:entity-subtype ent) (if fn (format " \"~a\"" fn) "") (if sz (format " ~a bytes" sz) "")) (lambda (t s e) (send t set-clickback s e (lambda (a b c) (let ([fn (put-file "Save Attachement As" main-frame #f fn)]) (when fn (to-file fn)))) #f #f) (send t change-style url-delta s e))) (when (eq? (system-type) 'macosx) (when fn (let ([and-open (lambda (dir) (let ([safer-fn (normalize-path (build-path (find-system-path 'home-dir) dir (regexp-replace* #rx"[/\"|:<>\\]" fn "-")))]) (insert " " set-standard-style) (insert (format "[~~/~a & open]" dir) (lambda (t s e) (send t set-clickback s e (lambda (a b c) (to-file safer-fn) (parameterize ([current-input-port (open-input-string "")]) (system* "/usr/bin/open" (path->string safer-fn)))) #f #f) (send t change-style url-delta s e)))))]) (and-open "Desktop") (and-open "Temp"))))) (insert "\n" set-standard-style) (lambda () (unless content (set! content (slurp ent))) content)))]) (case (mime:entity-type ent) [(text) (let ([disp (mime:disposition-type (mime:entity-disposition ent))]) (cond [(or (eq? disp 'error) (and (eq? disp 'inline) (not no-mime-inline?))) (cond [(and html-mode? (eq? 'html (mime:entity-subtype ent))) ;; If no text-obj supplied, make a temporary one for rendering: (let ([target (or text-obj (make-object display-text%))]) (as-background enable-main-frame (lambda (break-bad break-ok) (break-ok) (with-handlers ([void no-status-handler]) (status "Rendering HTML...") (let-values ([(in out) (make-pipe)]) (slurp-stream ent out) (close-output-port out) (render-html-to-text in target img-mode? #f)) (status ""))) close-frame) (unless text-obj ;; Copy text in target to `insert': (insert (send target get-text) void)))] [else (let-values ([(bytes->string done) (cond [(and mime-mode? (string? (mime:entity-charset ent)) (string-ci=? "UTF-8" (mime:entity-charset ent))) (values bytes->string/utf-8 void)] [(and mime-mode? (string? (mime:entity-charset ent)) (bytes-open-converter (generalize-encoding (mime:entity-charset ent)) "UTF-8")) => (lambda (c) (values (lambda (s alt) (let loop ([l null][start 0]) (let-values ([(r got status) (bytes-convert c s start)]) (case status [(complete) (bytes->string/utf-8 (apply bytes-append (reverse (cons r l))) alt)] [(aborts) (loop (list* #"?" r l) (+ start got))] [(error) (loop (list* #"?" r l) (+ start got 1))])))) (lambda () (bytes-close-converter c))))] [else (values bytes->string/latin-1 void)])]) (dynamic-wind void (lambda () (insert (bytes->string (crlf->lf/preserve-last (slurp ent)) #\?) (lambda (t s e) (when (SHOW-URLS) (hilite-urls t s e)) ;;(handle-formatting e) ; too slow (if (eq? disp 'error) (send t change-style red-delta s e))))) done))])] [else (generic ent)]))] [(image) (let ([get (generic ent)]) (let ([tmp-file (make-temporary-file "sirmail-mime-image-~a")]) (call-with-output-file tmp-file (lambda (port) (write-bytes (get) port)) 'truncate) (unless no-mime-inline? (let ([bitmap (make-object bitmap% tmp-file)]) (when (send bitmap ok?) (insert (make-object image-snip% bitmap) void) (insert "\n" void)) (delete-file tmp-file)))))] [(message) (insert-separator) (unless (or skip-headers? (null? (mime:message-fields msg))) (insert (string-crlf->lf (get-viewable-headers (let loop ([l (mime:message-fields msg)]) (if (null? l) crlf (string-append (car l) crlf (loop (cdr l))))))) void)) (map (lambda (x) (mime-loop x #f)) (mime:entity-parts ent))] [(multipart) (cond [(and (eq? 'alternative (mime:entity-subtype ent)) (= 2 (length (mime:entity-parts ent))) (andmap (lambda (m) (eq? 'text (mime:entity-type (mime:message-entity m)))) (mime:entity-parts ent)) (let ([l (map (lambda (m) (mime:entity-subtype (mime:message-entity m))) (mime:entity-parts ent))]) (and (member l '((plain html) (html plain) (plain enriched) (enriched plain))) l))) => (lambda (l) (let ([pos (if (eq? (car l) 'plain) (if prefer-text? 0 1) (if prefer-text? 1 0))]) (mime-loop (list-ref (mime:entity-parts ent) pos) #f)))] [else (map (lambda (x) (mime-loop x #f)) (mime:entity-parts ent))])] [else (generic ent)]))) ;; Non-mime mode: (insert (bytes->string/latin-1 (crlf->lf body)) void))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Biff ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define biff% (class timer% (inherit stop) (define/override (notify) (when can-poll? (unless (send disconnected-msg is-shown?) (with-handlers ([void (lambda (x) (stop) (force-disconnect/status) (status "Error connecting: ~s" (if (exn? x) (exn-message x) x)))]) (let ([old-new-messages? new-messages?]) (as-background enable-main-frame (lambda (break-bad break-ok) (check-for-new break-bad break-ok)) close-frame) (when (and new-messages? (not (eq? old-new-messages? new-messages?))) (bell))))))) (super-instantiate ()))) (define biff (if (BIFF-DELAY) (make-object biff%) #f)) (define (start-biff) (when biff (send biff start (* 1000 (BIFF-DELAY))))) (start-biff) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Mail Sending ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Mail-sending window is implemented in sendr.ss. This is ;; the set-up for opening such a window. (define my-address (with-handlers ([void (lambda (x) "")]) (car (extract-addresses (MAIL-FROM) 'address)))) (define my-username-@ (let ([m (regexp-match "^([^@]*)@" my-address)]) (if m (cadr m) (string-append my-address "@")))) (define (not-me? name-addr-full) (let ([addr (cadr name-addr-full)]) (cond [(string-ci=? addr my-address) #f] [(and (SELF-ADDRESSES) (member addr (SELF-ADDRESSES))) #f] [(and (> (string-length addr) (string-length my-username-@)) (string-ci=? my-username-@ (substring addr 0 (string-length my-username-@)))) (eq? (message-box "Identity?" (format "Are you ~a?" (caddr name-addr-full)) main-frame '(yes-no)) 'no)] [else #t]))) (define (do-reply follow-up? quote-msg?) (define selected (send header-list get-selected)) (unless selected (bell)) (when selected (unless (eq? selected current-selected) (send header-list on-double-select selected)) (unless (eq? selected current-selected) (bell)) (when (eq? selected current-selected) (let* ([uid (send selected user-data)] [h (get-header uid)] [rendered-body (let ([e (send message get-editor)] [start (string-length (string-crlf->lf (get-viewable-headers h)))]) (send e get-text start 'eof #t #t))]) (start-new-mailer #f (parse-encoded (or (extract-field "Reply-To" h) (extract-field "From" h) "")) (if follow-up? (let ([to (parse-encoded (extract-field "To" h))] [cc (parse-encoded (extract-field "CC" h))]) (if (or to cc) (let ([to (map caddr (filter not-me? (append (if to (extract-addresses to 'all) null) (if cc (extract-addresses cc 'all) null))))]) (if (null? to) "" (assemble-address-field to))) "")) "") (let ([s (parse-encoded (extract-field "Subject" h))]) (cond [(not s) ""] [(regexp-match #rx"^[Rr][Ee][(]([0-9]+)[)]:(.*)$" s) ;; Other mailer is counting replies. We'll count, too. => (lambda (m) (format "~a(~a):~a" (substring s 0 2) (add1 (string->number (caddr m))) (cadddr m)))] [(regexp-match "^[Rr][Ee]:" s) s] [(regexp-match "^[Aa][Nn][Tt][Ww][Oo][Rr][Tt]:" s) s] [else (string-append "Re: " s)])) (let ([id (extract-field "Message-Id" h)] [refs (extract-field "References" h)]) (format "~a~a" (if id (format "In-Reply-To: ~a\r\n" id) "") (if (or refs id) (format "References: ~a\r\n" (cond [(and refs id) (format "~a\r\n\t\t~a" refs id)] [else (or refs id)])) ""))) (if quote-in-reply? (let ([date (parse-encoded (extract-field "Date" h))] [name (with-handlers ([exn:fail? (lambda (x) #f)]) (let ([from (parse-encoded (extract-field "From" h))]) (car (extract-addresses from 'name))))]) (string-append (cond [(and date name) (format "At ~a, ~a wrote:\r" date name)] [name (format "Quoting ~a:\r" name)] [else (format "Quoting :\r")]) "> " (regexp-replace #rx"(?:\n> )*$" (regexp-replace* #rx"\n" rendered-body "\n> ") ""))) "") null))))) (define (do-forward) (define selected (send header-list get-selected)) (unless selected (bell)) (when selected (let* ([uid (send selected user-data)] [h (get-header uid)] [body (get-body uid void void)]) (start-new-mailer #f "" "" (let ([s (extract-field "Subject" h)]) (if (and s (not (regexp-match "^[Ff][Ww][Dd]:" s))) (string-append "Fwd: " s) (or s "Fwd"))) "" "" (list (make-enclosure "Forwarded Message" (insert-field "Content-Type" "message/rfc822" (insert-field "Content-Transfer-Encoding" "8bit" (insert-field "Content-Disposition" "inline" empty-header))) (lambda () (split-crlf (bytes-append (string->bytes/latin-1 h (char->integer #\?)) body))))))))) (define (start-new-mailer file to cc subject other-headers body enclosures) (start-new-window (lambda () (new-mailer file to cc subject other-headers body enclosures (length mailbox))))) (define (start-new-mailer/send-message file to cc subject other-headers body enclosures) (start-new-window (lambda () (send (new-mailer file to cc subject other-headers body enclosures (length mailbox)) send-message)))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Misc Formatting ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; handle-formatting : text -> void (define (handle-formatting e) (let loop ([line (send e last-line)]) (unless (zero? line) (for-each (lambda (regexp/action) (handle-single-line/formatting (car regexp/action) (cadr regexp/action) e line)) regexps/actions) (loop (- line 1))))) (define (handle-single-line/formatting regexp action e line) (let ([start (send e line-start-position line)] [end (send e line-end-position line)]) (let loop ([string (send e get-text start end #f)] [line-offset 0]) (cond [(regexp-match-positions regexp string) => (lambda (m) (let ([before (cadr m)] [during (caddr m)] [after (cadddr m)]) (action e (+ line-offset start (car during)) (+ line-offset start (cdr during))) (loop (substring string (car before) (cdr before)) (+ (car before) line-offset)) (loop (substring string (car after) (cdr after)) (+ (car after) line-offset))))] [else (void)])))) ;; emoticon-path (may not exist) (define emoticon-path (build-path (collection-path "sirmail") "emoticon")) ;; emoticon : string string -> (listof (list regexp (text number number -> void))) (define (emoticon img . icons) (let ([snip (make-object image-snip% (build-path emoticon-path img))]) (map (lambda (icon) (list (regexp (string-append "(.*)(" (quote-regexp-chars icon) ")(.*)")) (lambda (e start end) (send e insert (send snip copy) start end)))) icons))) (define (quote-regexp-chars str) (apply string (let loop ([chars (string->list str)]) (cond [(null? chars) null] [else (let ([char (car chars)]) (if (memq char regexp-special-chars) (list* #\\ char (loop (cdr chars))) (cons char (loop (cdr chars)))))])))) (define regexp-special-chars (string->list "()*+?[].^\\|")) ;; all regexps must have three parenthesized sub-expressions ;; the first is unmatched text before the regexp, the second ;; is the matched tetx and the third is unmatched text after the regexp. (define regexps/actions (list* (list (regexp "(.*)( \\*([^\\*]*)\\* )(.*)") (lambda (e start end) (send e change-style bold-style-delta start end))) (list (regexp "(.*) _([^_]*)_ (.*)") (lambda (e start end) (send e change-style italic-style-delta start end))) (if (directory-exists? emoticon-path) (append (emoticon "bigsmile.gif" ":D" ":-D") (emoticon "cry.gif" ":')" ":'-)") (emoticon "happy.gif" ":)" ":-)" ":>" ":->" "<-:" "<:" "(-:" "(:") (emoticon "kiss.gif" "*:" ":*") (emoticon "sad.gif" ":(" ":-(" ":<" ":-<" ">-:" ">:" "):" ")-:") (emoticon "tongue.gif" ":P" ":-P") (emoticon "wink.gif" ";)" ";-)" ";>" ";->")) null))) (define bold-style-delta (make-object style-delta% 'change-bold)) (define italic-style-delta (make-object style-delta% 'change-italic)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Mailbox memory graph (from messages in this mailbox) ;; (define (show-memory-graph) ;; grab the current value of the mailbox (let ([mailbox mailbox] [mbox-eventspace (current-eventspace)]) (status "Collecting memory graph record...") ;; ht : [symbol -o> (listof (cons seconds bytes))] (parameterize ([current-eventspace (make-eventspace)]) (queue-callback (lambda () (let ([ht (make-hash-table)]) (let loop ([mailbox mailbox]) (cond [(empty? mailbox) (void)] [else (let* ([message (car mailbox)] [uid (message-uid message)] [header (get-header uid)] [key (string->symbol (format "~a" (extract-field "X-Mailer" header)))] [uptime-str (extract-field "X-Uptime" header)]) (when uptime-str (let ([uptime (parse-uptime uptime-str)]) (when uptime (hash-table-put! ht key (cons uptime (hash-table-get ht key (lambda () '())))))))) (loop (cdr mailbox))])) (let ([info (sort (hash-table-map ht (lambda (x y) (list (symbol->string x) y))) (lambda (x y) (string<=? (car x) (car y))))]) (parameterize ([current-eventspace mbox-eventspace]) (queue-callback (lambda () (status "Showing graph")))) (make-memory-graph-window info)))))))) ;; eventspace: graph eventspace (define (parse-uptime str) (let* ([sep-bytes (regexp-match #rx"([0-9,]*) bytes" str)] [bytes (and sep-bytes (string->number (regexp-replace* #rx"," (cadr sep-bytes) "")))] [seconds (cond [(regexp-match day-hour-regexp str) => (combine (* 24 60 60) (* 60 60))] [(regexp-match hour-minute-regexp str) => (combine (* 60 60) 60)] [(regexp-match minute-second-regexp str) => (combine 60 1)] [else #f])]) (if (and bytes seconds) (cons seconds bytes) #f))) ;; eventspace: graph eventspace (define (combine m1 m2) (lambda (match) (let ([first (cadr match)] [second (caddr match)]) (+ (* (string->number first) m1) (* (string->number second) m2))))) ;; info : (listof (list string (listof (cons number number)))) -> void ;; eventspace: new graph eventspace (define (make-memory-graph-window info) (define frame (new frame:basic% (label "Memory Histogram") (width 500) (height 600))) (define canvas (new canvas% (paint-callback (lambda (c dc) (draw-graph dc text))) (parent (send frame get-area-container)))) (define text (new text%)) (define editor-canvas (new editor-canvas% (parent (send frame get-area-container)) (editor text) (stretchable-height #f) (line-count 6))) (define colors '("Green" "DarkOliveGreen" "ForestGreen" "MediumTurquoise" "SteelBlue" "Teal" "CadetBlue" "Indigo" "Purple" "Fuchsia" "Black" "DarkRed" "HotPink" "OrangeRed" "SaddleBrown")) (define original-colors colors) (define (draw-graph dc text) (let ([max-x 0] [max-y 0] [left-scale 0]) (for-each (lambda (key-pairs) (for-each (lambda (pair) (set! max-x (max (car pair) max-x)) (set! max-y (max (cdr pair) max-y))) (cadr key-pairs))) info) (let-values ([(cw ch) (send canvas get-client-size)]) (let* ([text-height (let-values ([(w h _1 _2) (send dc get-text-extent "9")]) h)] [draw-y-label (lambda (frac) (let ([str (format "~a" (quotient (* frac max-y) (* 1024 1024)))] [y (max (* ch (- 1 frac)) text-height)]) (let-values ([(w h _1 _2) (send dc get-text-extent str)]) (set! left-scale (max left-scale w)) (send dc draw-line 0 (+ y (floor (/ h 2))) cw (+ y (floor (/ h 2)))) (send dc draw-text str 0 y))))]) (draw-y-label 1) (draw-y-label 3/4) (draw-y-label 1/2) (draw-y-label 1/4) (draw-y-label 0) (send dc draw-line left-scale 0 left-scale ch))) (set! colors original-colors) (for-each (lambda (key-pairs) (let ([key (car key-pairs)] [pairs (cadr key-pairs)]) (set! colors (cdr colors)) (send dc set-pen (send the-pen-list find-or-create-pen (car colors) 1 'solid)) (send dc set-brush (send the-brush-list find-or-create-brush (car colors) 'solid)) (for-each (lambda (pair) (plot-pair dc (car pair) (cdr pair) left-scale max-x max-y)) pairs))) info))) (define (plot-pair dc x y left-scale max-x max-y) (let-values ([(cw ch) (send canvas get-client-size)]) (let* ([w (- cw left-scale)] [h ch] [dc-x (+ left-scale (* x (/ w max-x)))] [dc-y (- ch (* y (/ h max-y)))]) (send dc draw-rectangle dc-x dc-y 3 3)))) (send text begin-edit-sequence) (for-each (lambda (key-pairs) (let ([key (car key-pairs)] [pairs (cadr key-pairs)]) (set! colors (cdr colors)) (let ([before (send text last-position)]) (send text insert (format "~a msgs ~a ~a" (length pairs) key (car colors))) (let ([after (send text last-position)]) (send text insert "\n") (let ([sd (make-object style-delta%)]) (send sd set-delta-foreground (car colors)) (send text change-style sd before after)))))) info) (send text end-edit-sequence) ; (set-cdr! (last-pair colors) colors) ;; FIXME: need a cyclic list (send frame show #t)) (define (make-and-regexp first second) (regexp (format "([0-9]+) ~as? and ([0-9]+) ~as?" first second))) (define day-hour-regexp (make-and-regexp "day" "hour")) (define hour-minute-regexp (make-and-regexp "hour" "minute")) (define minute-second-regexp (make-and-regexp "minute" "second")) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Hiliting URLS ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; hilite-urls : text -> void ;; highligts all of the urls (strings beginning with `http:', `https:' or `ftp:') ;; in the buffer to be able to click on them. (define (hilite-urls e start end) (define (hilite-urls/prefix prefix) (let loop ([pos start]) (when (< pos end) (let ([start-pos (send e find-string prefix 'forward pos 'eof #t #f)]) (when start-pos (let ([eou-pos (let loop ([eou-pos start-pos]) (cond [(= eou-pos (send e last-position)) eou-pos] [(char-whitespace? (send e get-character eou-pos)) ;; Back up past ., ,, >, ", and ): (let loop ([eou-pos eou-pos]) (if (memq (send e get-character (sub1 eou-pos)) '(#\" #\. #\, #\> #\))) (loop (sub1 eou-pos)) eou-pos))] [else (loop (+ eou-pos 1))]))]) (send e change-style url-delta start-pos eou-pos) (send e set-clickback start-pos eou-pos (lambda (e start-pos eou-pos) (send-url (send e get-text start-pos eou-pos)))) (loop eou-pos))))))) (hilite-urls/prefix "http:") (hilite-urls/prefix "https:") (hilite-urls/prefix "ftp:"))))