racket/collects/sirmail/pref.rkt

553 lines
19 KiB
Racket

(module pref mzscheme
(require mzlib/class
framework
mred
mzlib/list
mzlib/string
mzlib/etc
net/head)
;; IMPORTANT! All preferences operations outside this
;; file should go through the following exports.
;; DO NOT use preferences:... elsewhere.
(provide get-pref put-pref
show-pref-dialog
add-preferences-menu-items)
(define (string-or-false? x) (or (not x) (string? x)))
(define (ip-string? x) (and (string? x)
(positive? (string-length x))))
(define (abs-path-or-false? x)
(or (not x)
(and (path? x) (absolute-path? x))))
(define (un/marshall-path pref)
(preferences:set-un/marshall pref
(lambda (x)
(if (path? x)
(path->bytes x)
x))
(lambda (x)
(cond
[(bytes? x) (bytes->path x) ]
[(not x) x]
[else 'badvalue]))))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Preference Definitions ;;
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(preferences:set-default 'sirmail:mail-from "SirMail User <sirmail@racket-lang.org>" string?)
(preferences:set-default 'sirmail:username "username" string?)
(preferences:set-default 'sirmail:password #f string-or-false?)
(preferences:set-default 'sirmail:default-to-domain "racket-lang.org" ip-string?)
(preferences:set-default 'sirmail:imap-server "imap.racket-lang.org" ip-string?)
(preferences:set-default 'sirmail:use-ssl? #f boolean?)
(preferences:set-default 'sirmail:server-certificate #f abs-path-or-false?)
(preferences:set-default 'sirmail:smtp-server "sendmail.racket-lang.org" ip-string?)
(preferences:set-default 'sirmail:local-directory
(build-path (find-system-path 'home-dir)
"SirMail")
(lambda (x)
(and (path? x)
(absolute-path? x))))
(un/marshall-path 'sirmail:local-directory)
(preferences:set-default 'sirmail:sent-directory
(build-path (find-system-path 'home-dir)
"SentMail")
(lambda (x)
(or (not x)
(and (path? x)
(absolute-path? x)))))
(un/marshall-path 'sirmail:sent-directory)
(preferences:set-default 'sirmail:root-mailbox-folder #f string-or-false?)
(preferences:set-default 'sirmail:archive-mailbox-folder #f string-or-false?)
(preferences:set-default 'sirmail:initial-sort 'id
(lambda (x) (memq x '(id date subject from))))
(preferences:set-default 'sirmail:biff-delay
60
(lambda (x)
(or (not x)
(and (number? x)
(exact? x)
(integer? x)
(positive? x)))))
(preferences:set-default 'sirmail:warn-download-size 32000
(lambda (x) (or (not x) (and (number? x) (real? x)))))
(preferences:set-default 'sirmail:external-composer 'xemacs
(lambda (x) (memq x '(xemacs gnu-emacs))))
(preferences:set-default 'sirmail:use-extenal-composer? #f boolean?)
(preferences:set-default 'sirmail:show-urls? #t boolean?)
(preferences:set-default 'sirmail:show-gc-icon #f boolean?)
(preferences:set-default 'sirmail:always-happy #f boolean?)
(preferences:set-default 'sirmail:wrap-lines #f boolean?)
(preferences:set-default 'sirmail:prefer-text #t boolean?)
(preferences:set-default 'sirmail:aliases-file
(build-path (find-system-path 'home-dir) ".sirmail.aliases")
abs-path-or-false?)
(un/marshall-path 'sirmail:aliases-file)
(preferences:set-default 'sirmail:auto-file-table-file (build-path (find-system-path 'home-dir) ".sirmail.auto-file")
abs-path-or-false?)
(un/marshall-path 'sirmail:auto-file-table-file)
(preferences:set-default 'sirmail:self-addresses null
(lambda (x) (and (list? x) (andmap string? x))))
(preferences:set-default 'sirmail:fields-to-show '("From" "To" "CC" "Subject" "Date" "X-Mailer" "X-Uptime")
(lambda (x) (and (list? x) (andmap string? x))))
(preferences:set-default 'sirmail:bcc #f
(λ (x) (or (not x) (string? x))))
(let ([fw 560]
[fh 600])
(let-values ([(display-width display-height) (get-display-size)])
(preferences:set-default 'sirmail:frame-width
(min display-height fh)
(lambda (x) (and (number? x) (<= 0 x 32768))))
(preferences:set-default 'sirmail:frame-height
(min display-width fw)
(lambda (x) (and (number? x) (<= 0 x 32768))))))
(define (xywh-okay? n)
(and (number? n)
(<= 0 n 10000)))
(preferences:set-default 'sirmail:folder-window-w 200 xywh-okay?)
(preferences:set-default 'sirmail:folder-window-h 400 xywh-okay?)
(preferences:set-default 'sirmail:folder-window-x 0 xywh-okay?)
(preferences:set-default 'sirmail:folder-window-y 0 xywh-okay?)
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Preference Manager ;;
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define prefs-eventspace (make-eventspace))
(define (in-preferences-eventspace thunk)
(let ([val #f]
[s (make-semaphore)])
(parameterize ([current-eventspace prefs-eventspace])
(queue-callback
(lambda ()
(with-handlers ([void (lambda (x)
;; Assume all raised values are exns
(set! val x))])
(set! val (thunk)))
(semaphore-post s))))
(semaphore-wait s)
(if (exn? val)
(raise val)
val)))
(define (get-pref id)
(in-preferences-eventspace (lambda ()
(preferences:get id))))
(define (put-pref id val)
(in-preferences-eventspace (lambda ()
(preferences:set id val))))
(define (add-preferences-menu-items edit-menu)
(make-object separator-menu-item% edit-menu)
(make-object menu-item% "Preferences" edit-menu
(lambda (x y) (in-preferences-eventspace preferences:show-dialog))))
(define (show-pref-dialog)
(in-preferences-eventspace
(lambda ()
(preferences:show-dialog)
(yield 'wait))))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Preference Dialog ;;
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define needs-check null)
(define (set-hilite e on?)
(send e change-style
(send (make-object style-delta%) set-delta-background (if on? "yellow" "white"))
0 (send e last-position)))
;; make-text-field : string panel number symbol boolean
;; ((union #f string) (union #f top-level-window<%>) string -> boolean)
;; (any -> string)
;; (string -> any)
;; [ (union #f string) ]
;; -> void
;; sets up a text field for a preference
;; The 3rd-to-last argument checks the validity of the field content.If
;; a string is provided, then a top-level-window<%> is also provded, and
;; the checking function should tell the user why the field-value string
;; is bad if it is bad.
;; the last two arguments convert between the string representation (as shown in the text field)
;; and the preferences's actual Scheme value.
(define make-text-field
(opt-lambda (label panel width-num pref optional? check-value val->str str->val [post-label #f])
(define p0 (and (or optional? post-label)
(instantiate horizontal-panel% (panel) [stretchable-height #f])))
(define e (and optional?
(make-object check-box% label p0
(lambda (c e)
(let ([on? (send c get-value)])
(send t enable on?)
(if on?
(t-cb t e)
(begin
;; remove all need-check registrations, if any:
(let loop ()
(let ([a (assq t needs-check)])
(when a
(set! needs-check (remq a needs-check))
(loop))))
(preferences:set pref #f))))))))
(define t-cb (lambda (t e)
(let* ([s (send t get-value)])
(if (check-value #f #f s)
(preferences:set pref (str->val s))
(begin
(set! needs-check (cons (list t label check-value) needs-check))
(set-hilite (send t get-editor) #t))))))
(define t (make-object text-field%
(if optional? #f label)
(or p0 panel)
t-cb
(make-string width-num #\X)))
(when post-label
(send t stretchable-width #f)
(make-object message% post-label p0))
(send t set-value (let ([v (preferences:get pref)])
(if v
(val->str v)
"")))
(when optional?
(send e set-value (preferences:get pref)))
(when e
(send t enable (send e get-value)))
(preferences:add-callback pref (lambda (name val)
(set-hilite (send t get-editor) #f)
(when e
(send e set-value val)
(send t enable val))
(when val
(let ([sval (val->str val)])
(unless (equal? sval (send t get-value))
(send t set-value sval))))))
(or p0 t)))
(define (check-unsaved-pref?)
(and (andmap (lambda (a)
((caddr a) (cadr a) (send (car a) get-top-level-window) (send (car a) get-value)))
needs-check)
(begin
(set! needs-check null)
#t)))
(define make-file/directory-button
(lambda (dir? button-label parent pref enabler)
(define p0 (and enabler
(instantiate horizontal-panel% (parent) [stretchable-height #f])))
(define e (and enabler
(make-object check-box% enabler p0
(lambda (c e)
(let ([on? (send c get-value)])
(send p enable on?)
(preferences:set
pref
(and on?
(string->path (send field get-value)))))))))
(define p (instantiate horizontal-panel% ((or p0 parent))
[stretchable-height #f]))
(define (set-it v)
(preferences:set pref v))
(define field (make-object text-field% button-label p
;; For now, just counteract edits:
(lambda (t e)
(send field set-value (path->string (preferences:get pref))))
(path->string
(or (preferences:get pref)
(current-directory)))))
(when e
(send e set-value (preferences:get pref))
(send p enable (send e get-value)))
(preferences:add-callback pref (lambda (name val)
(when e
(send e set-value val)
(send p enable val))
(when val
(send field set-value (path->string val)))))
(make-object button% "Set..." p (lambda (b e)
(let ([v ((if dir? get-directory get-file)
(or enabler button-label))])
(when v
(set-it v)))))
p0))
(define make-boolean
(opt-lambda (label p pref [extra-action void])
(define c
(make-object check-box% label p (lambda (c e)
(let ([v (send c get-value)])
(extra-action v)
(preferences:set pref v)))))
(send c set-value (preferences:get pref))
(preferences:add-callback pref (lambda (name val)
(send c set-value val)))))
(define (is-host-address? s)
(regexp-match "^([-a-zA-Z0-9]+[.])*[-a-zA-Z0-9]+$" s))
(define (is-host-address+port? s)
(or (is-host-address? s)
(let ([m (regexp-match "^(.*):([0-9]+)$" s)])
(and m
(<= 1 (string->number (caddr m)) 65535)
(is-host-address? (cadr m))))))
(define (is-host-address+port+user? s)
(or (is-host-address+port? s)
(let ([m (regexp-match "^(?:[-+a-zA-Z0-9_.]+)@(.*)$" s)])
(and m
(is-host-address+port? (cadr m))))))
(define (is-host-address+port+user+type? s)
(or (is-host-address+port+user? s)
(let ([m (regexp-match "^(?:ssl|tcp):(.*)$" s)])
(and m
(is-host-address+port+user? (cadr m))))))
(define (is-host-address+port+user+type-list? s)
(let ([l (regexp-split ", *" s)])
(andmap is-host-address+port+user+type? l)))
(define (check-address ok? who tl s port-ok? multi?)
(or (ok? s)
(begin
(when who
(message-box
"Preference Error"
(format (string-append
"The ~a value must be a~a host IP address~a~a~a.\n"
"An IP address is an string containing a combination of "
"period (.), dash (-), A-Z, a-Z, and 0-9. "
"Also the period cannot appear at the very beginning or end.\n"
"~a"
"You provided\n\n ~a\n\nwhich is not legal.")
who
(if multi? " comma-separated list of" "")
(if multi? " es" "")
(if (and multi? port-ok?) " each" "")
(if port-ok? " with an optional port number" "")
(if port-ok?
(string-append
"An optional port number is specified by adding a "
"colon (:) followed by a number between 1 and 65535.\n")
"")
s)
tl
'(ok stop)))
#f)))
(define (check-host-address who tl s)
(check-address is-host-address? who tl s #f #f))
(define (check-host-address/port who tl s)
(check-address is-host-address+port? who tl s #t #f))
(define (check-host-address/port/user/type/multi who tl s)
(check-address is-host-address+port+user+type-list? who tl s #t #t))
;; check-biff-delay : (union #f string) (union #f parent) string -> boolean
;; checks to see if the string in the biff delay field makes
;; sense as an exact integer between 1 and 3600
(define (check-biff-delay who tl s)
(let ([n (string->number s)])
(or (and (number? n)
(integer? n)
(exact? n)
(<= 1 n 3600))
(begin
(when who
(message-box
"Preference Error"
(format (string-append
"The biff delay must be an exact integer between 1 and 3600.\n"
"You provided:\n"
" ~a")
s)
tl
'(ok stop)))
#f))))
;; check-message-size : (union #f string) (union #f parent) string -> boolean
;; checks to see if the string in the download-max-size field makes
;; sense as an exact positive integer
(define (check-message-size who tl s)
(let ([n (string->number s)])
(or (and (number? n)
(integer? n)
(exact? n)
(positive? n))
(begin
(when who
(message-box
"Preference Error"
(format (string-append
"The message size must be an exact, positive integer.\n"
"You provided:\n"
" ~a")
s)
tl
'(ok stop)))
#f))))
(define (check-user-address who tl s)
(with-handlers ([exn:fail?
(lambda (x)
(when who
(message-box
"Preference Error"
(format "The ~a value you provided is not a legal mail address: ~a"
who s)
tl
'(ok stop)))
#f)])
(unless (= 1 (length (extract-addresses s 'all)))
(error "multiple addresses"))
#t))
(define (check-simple-user-address who tl s)
(and (check-user-address who tl s)
(car (extract-addresses s 'address))))
(define (check-id who tl s) #t)
(define (make-text-list label parent pref check-item)
(let ([p (make-object group-box-panel% label parent)])
(define l (make-object list-box% #f (or (preferences:get pref) null) p
(lambda (l e)
(send delete enable (pair? (send l get-selections))))
'(multiple)))
(define hp (instantiate horizontal-panel% (p)
[stretchable-height #f]
[alignment '(center center)]))
(define add (make-object button% "Add" hp (lambda (b e)
(let loop ([init ""])
(let ([v (get-text-from-user (format "Add to ~a" label)
(format "Add to ~a" label)
(send parent get-top-level-window)
init)])
(when v
(let ([revised (check-item (format "item for ~a" label)
(send b get-top-level-window) v)])
(if revised
(begin
(send l append (if (string? revised) revised v))
(set-prefs))
(loop v)))))))))
(define delete (make-object button% "Delete" hp (lambda (b e)
(let ([d (send l get-selections)])
(for-each (lambda (i)
(send l delete i))
(sort d >))
(set-prefs)))))
(define (set-prefs)
(send delete enable (pair? (send l get-selections)))
(preferences:set
pref
(let ([n (send l get-number)])
(let loop ([i 0])
(if (= i n)
null
(cons (send l get-string i)
(loop (add1 i))))))))
(send delete enable #f)
(preferences:add-callback pref (lambda (name val)
(send l clear)
(for-each (lambda (i)
(send l append i))
val)
(send delete enable (pair? (send l get-selections)))))))
(define (make-addresses-preferences-panel parent)
(let ([p (instantiate vertical-panel% (parent))])
(make-text-field "Mail From" p 20 'sirmail:mail-from #f check-user-address (lambda (x) x) (lambda (x) x))
(make-text-field "SMTP Server" p 20 'sirmail:smtp-server #f check-host-address/port/user/type/multi
(lambda (x) x) (lambda (x) x))
(make-file/directory-button #t #f p
'sirmail:sent-directory
"Save Sent Files")
(make-text-field "Default \"To\" domain" p 20 'sirmail:default-to-domain #f check-host-address (lambda (x) x) (lambda (x) x))
(make-text-field "BCC line" p 20 'sirmail:bcc #t void (lambda (x) x) (lambda (x) x))
(make-file/directory-button #f #f p
'sirmail:aliases-file
"Aliases File")
(make-text-list "Self Addresses" p 'sirmail:self-addresses check-simple-user-address)
(make-boolean "Enable compose-with-Emacs" p 'sirmail:use-extenal-composer?)
p))
(define (make-mbox-preferences-panel parent)
(let ([p (instantiate vertical-panel% (parent)
(alignment '(left center)))])
(make-text-field "Username" p 10 'sirmail:username #f check-id (lambda (x) x) (lambda (x) x))
(let ([sp (instantiate group-box-panel% ("IMAP Server" p)
[alignment '(left center)])]
[cert #f])
(make-text-field "Server" sp 20 'sirmail:imap-server #f check-host-address/port (lambda (x) x) (lambda (x) x))
(make-boolean "Encrypt connection using SSL" sp 'sirmail:use-ssl?
(lambda (on?) (send cert enable on?)))
(set! cert (make-file/directory-button #f #f sp
'sirmail:server-certificate
"Verify SSL with certificates"))
(make-text-field "Archive folder" sp 20 'sirmail:archive-mailbox-folder #t void (lambda (x) x) (lambda (x) x))
(make-text-field "Folder list root" sp 20 'sirmail:root-mailbox-folder #t void (lambda (x) x) (lambda (x) x))
(send cert enable (preferences:get 'sirmail:use-ssl?)))
(make-file/directory-button #t "Local directory" p
'sirmail:local-directory
#f)
(make-text-field "Check mail every" p 5 'sirmail:biff-delay #t check-biff-delay number->string string->number
"seconds")
(make-text-field "Verify download of messages larger than" p 10
'sirmail:warn-download-size #t
check-message-size number->string string->number
"bytes")
(make-file/directory-button #f #f p
'sirmail:auto-file-table-file
"Auto-file table file")
(make-boolean "Show GC icon" p 'sirmail:show-gc-icon)
(make-boolean "Always happy to get mail" p 'sirmail:always-happy)
(make-text-list "Shown Header Fields" p 'sirmail:fields-to-show void)
p))
(in-preferences-eventspace
(lambda ()
(preferences:add-panel "Reading" make-mbox-preferences-panel)
(preferences:add-panel "Sending" make-addresses-preferences-panel)
(preferences:add-editor-checkbox-panel)
(preferences:add-can-close-dialog-callback check-unsaved-pref?))))