;;; -*- lexical-binding: t -*- ;;; Externalities. ;; user-full-name from /etc/passwd; set with chfn(1). ;; user-mail-address from EMAIL variable; set with ~/.profile, ;; ~/.xsessionrc, DE's convention-du-jour. ;; ~/.authinfo.gpg: ;; machine imap.gmail.com login LOGIN password PASSWORD port 993 ;; machine smtp.gmail.com login LOGIN password PASSWORD port 587 ;;; Þe Olde Setq. (setq gnus-select-method '(nnimap "gmail" ;; TODO: try auth-source-xoauth2-plugin: ;; (nnimap-authenticator xoauth2) (nnimap-address "imap.gmail.com") (nnimap-server-port 993) (nnmail-expiry-target "nnimap+gmail:[Gmail]/Trash") (nnmail-expiry-wait immediate)) gnus-secondary-select-methods '((nntp "archive.lwn.net") (nntp "news.gmane.io")) smtpmail-smtp-server "smtp.gmail.com" smtpmail-smtp-service 587 ;; Archival of sent messages. gnus-gcc-mark-as-read t ;; The next setting makes the previous one useless; keeping both ;; for now because I'm not sure which I'll settle for. gnus-message-archive-group nil ;; Groups. gnus-group-uncollapsed-levels 2 ;; Summary. gnus-auto-center-summary nil gnus-prompt-before-saving t gnus-summary-line-format "%*%U%R %-11,11&user-date; %B%-23,23f %s\n" gnus-summary-dummy-line-format " ╭ %S\n" gnus-summary-make-false-root 'dummy gnus-sum-thread-tree-root "╭ " gnus-sum-thread-tree-false-root "┬ " gnus-sum-thread-tree-single-indent " " gnus-sum-thread-tree-indent " " gnus-sum-thread-tree-single-leaf "╰► " gnus-sum-thread-tree-leaf-with-other "├► " gnus-sum-thread-tree-vertical "│" gnus-thread-sort-functions '(gnus-thread-sort-by-number (not gnus-thread-sort-by-most-recent-date)) gnus-user-date-format-alist '(((gnus-seconds-today) . " %H:%M") ((+ 86400 (gnus-seconds-today)) . "Yestd %H:%M") ((* 6 86400) . " %a %H:%M") ((gnus-seconds-month) . "%a %d") ((gnus-seconds-year) . "%b %d") (t . "%F")) ;; Articles. gnus-cite-parse-max-size nil gnus-header-face-alist '(("From" nil gnus-header-from) ("Subject" nil gnus-header-subject) ("Date" nil eighters-date) ("Newsgroups:.*," nil gnus-header-newsgroups) ("" gnus-header-name gnus-header-content)) gnus-sorted-header-list (list ;; What, when. "^Subject:" "^Summary:" "^Keywords:" "^Date:" ;; Who. "^From:" "^Organization:" "^Followup-To:" "^To:" "^Cc:" "^Newsgroups:") gnus-treat-display-smileys nil ;; Do not fill anything; let visual-line-mode wrap text. ;;; NB: for format=flowed, there is no setting to say "un-fill ;;; flowed lines", so we *enable* filling, setting an absurd ;;; line length limit, in order to un-fill flowed lines. fill-flowed-display-column most-positive-fixnum mm-fill-flowed t ;;; More long-line-folding settings. gnus-article-unfold-long-headers t gnus-treat-fill-article nil gnus-treat-fill-long-lines nil gnus-treat-fold-headers nil) ;;; Window configurations. (defvar my/gnus-side-by-side-threshold 160) (gnus-add-configuration '(article (if (>= (frame-width) my/gnus-side-by-side-threshold) '(horizontal 1.0 (summary 1.0 point) (article 80)) '(vertical 1.0 (summary 0.25 point) (article 1.0))))) (dolist (buf-name '(forward reply reply-yank)) (gnus-add-configuration `(,buf-name (if (>= (frame-width) my/gnus-side-by-side-threshold) '(vertical 1.0 (summary 0.25) (horizontal 1.0 (article 0.5) (message 1.0 point))) '(vertical 1.0 (summary 0.2) (article 0.2) (message 1.0 point)))))) ;;; Summary tweaks. (defun my/gnus-toggle-article-wrap () (interactive) (with-current-buffer gnus-article-buffer (visual-line-mode 'toggle))) (defun my/gnus-summary-tweak-keys () (keymap-local-set "C-c d v" 'my/gnus-toggle-article-wrap)) (add-hook 'gnus-summary-mode-hook 'my/gnus-summary-tweak-keys) ;; message-subject-re-regexp is used both in Gnus summary buffers to ;; detect and elide similar subjects in a thread, and by message mode ;; when replying, to determine what to strip from the subject. ;; ;; Some MUAs add cruft to the subject, turning "Re: bug#123: foobar" ;; into "RE: [External] : Re: bug#1234: foobar", which Debbugs will ;; then turn into "bug#1234: [External] : Re: bug#1234: foobar". ;; ;; The only way I can find to tell the Gnus summary code to ;; canonicalize all that cruft away is by tweaking this regexp, but ;; setting its global value causes message-mode to elide stuff it ;; shouldn't when crafting subjects. Therefore, chase down the best ;; Gnus hook for the job, and set the regexp locally. (defun my/gnus-reply-prefixes () (mapcan (lambda (prefix) (list prefix (upcase prefix) (capitalize prefix))) '("re" "aw" "sv" "fw" "fwd"))) (setq my/gnus-summary-normalize-subject (rx-to-string `(seq bol (+ (or (seq word-start (or ,@(my/gnus-reply-prefixes)) word-end) (seq "bug#" (+ digit)) (seq "[" (or "External" "SPAM UNSURE") "]")) (? (* space) ":") (* space))))) (add-hook 'gnus-summary-generate-hook (lambda () (setq-local message-subject-re-regexp my/gnus-summary-normalize-subject))) (let* ((initials (mapconcat (lambda (s) (substring s 0 1)) (split-string user-full-name) nil)) (sent-prefix (format "%s → " initials))) (setq gnus-summary-to-prefix sent-prefix gnus-summary-newsgroup-prefix sent-prefix)) ;;; Article tweaks. (defun my/gnus-article-eschew-tables () ;; I set shr-fill-text to nil because I prefer letting ;; visual-line-mode manage wrapping. Unfortunately, many HTML ;; emails rely on s for layouts, and rendering can get ugly. ;; Work around this by treating
& children as any other ;;
. (make-local-variable 'shr-external-rendering-functions) (pcase-dolist (`(,tag . ,shr-function) '((table . shr-tag-div) (thead . shr-tag-div) (tbody . shr-tag-div) (tr . shr-tag-ul) (th . shr-tag-li) (td . shr-tag-li))) (setf (alist-get tag shr-external-rendering-functions) shr-function))) (defun my/gnus-article-has-html () ;; Hard to tell the difference between ;; * the variable `gnus-article-mime-handles', ;; * the function `gnus-article-mime-handles', ;; * the variable `gnus-article-mime-handle-alist'. ;; ;; Stealing debbugs.el's patch-finding logic. (seq-some (lambda (handle) (string= (mm-handle-media-type (cdr handle)) "text/html")) (gnus-article-mime-handles))) (defun my/gnus-article-should-wrap () (save-excursion (message-goto-body) (let ((should-wrap nil) (has-html (my/gnus-article-has-html))) (while-let (((not should-wrap)) ((not (eobp))) (current-line (thing-at-point 'line))) (setq should-wrap (and ;; The line is bigger than the target width. (> (length current-line) (window-width (get-buffer-window gnus-article-buffer))) ;; The line is not boring (citation, diff addition/removal). (not (string-match-p "\\`[>+-]" current-line)) ;; Lines that start with spaces are boring, except in ;; HTML parts: those are choked with
tags that ;; shr left-pads with spaces. ;; NB: HAS-HTML is a naive heuristic: we are assuming ;; that "any text/html part is present" means "we are ;; looking at this text/html part". (or (not (string-match-p "\\` " current-line)) has-html))) (forward-line)) should-wrap))) (defun my/gnus-article-wrap-maybe () ;; Enable visual-line-mode when it helps, i.e. when the message has ;; long lines that are not part of citations nor patches. (with-current-buffer gnus-article-buffer (visual-line-mode (unless (my/gnus-article-should-wrap) -1)))) ;; Article setup is tricky. In order, `gnus-article-prepare' ;; ;; (1) calls `gnus-article-setup-buffer', which ;; (a) calls `gnus-article-mode', which runs ;; gnus-article-mode-hook, ;; (b) sets truncate-lines from gnus-article-truncate-lines, ;; ;; (2) calls `gnus-display-mime', which may end up calling `mm-shr'; ;; this can call `shr-tag-table', which turns truncate-lines on ;; unconditionally. ;; ;; (3) runs gnus-article-prepare-hook. ;; ;; Gnus will only run (1a) once, and skip that step when it re-uses ;; the same *Article* buffer for subsequent articles. So for our ;; purposes, we need to ;; ;; (Ⅰ) hack the shr rendering functions in mode-hook, before `mm-shr' ;; gets to work. ;; (Ⅱ) call `visual-line-mode' (if needed) in prepare-hook, after ;; truncate-lines has been set. (add-hook 'gnus-article-mode-hook 'my/gnus-article-eschew-tables) (add-hook 'gnus-article-prepare-hook 'my/gnus-article-wrap-maybe) ;;; MIME display. (defun my/mm-display-markdown-inline (handle) (mm-display-inline-fontify handle 'markdown-mode)) (with-eval-after-load 'mm-decode ;; bug-gnu-emacs: (setf (alist-get "text/markdown" mm-inline-media-tests nil nil 'equal) '(my/mm-display-markdown-inline))) ;;; Key bindings. ;; ;; m compose ;; ;; Group buffer: ;; ;; L list all groups ;; RET view unread mail in group ;; C-u RET view all mail in group ;; g refresh ;; G G search group ;; ;; Summary buffer: ;; ;; B m move message to group ;; / N fetch new ;; M-g refresh (expire, move, fetch new, show unread) ;; C-u M-g refresh (expire, move, fetch new, show all) ;; C-u g show raw, undecoded message source; g to decode ;; T h collapse (hide) thread ;; T s expand (show) thread ;; T k, C-M-k mark thread as read ;; M-1 T k mark thread as unread ;; r reply ;; R reply (quoting) ;; S w reply-all ;; S W reply-all (quoting) ;; C-c C-f forward ;; d mark read ;; M-u clear marks (≡ mark unread) ;; E expire ;; # toggle mark for next action ;; M-#, M P u unmark for next action ;; ;; Draft summary buffer: ;; ;; D e edit draft ;; ;; Article buffer: ;; ;; o save attachment at point ;; K b add button for inlined MIME part ;; ;; Composing: ;; ;; C-c C-c send ;; C-c C-a attach ;; C-c C-f s change the subject (append "was:") ;; ;;; FAQ. ;; ;; - how to see *all mails*, not just unread? ;; - C-u RET ;; ;; - how to do something on a bunch of mail matching a pattern? ;; - M P R ; mark all mails with subjects matching regexp ;; - M-& ; do on all marked mails ;; ;; - how to delete mail? ;; - E to mark as expired ;; - C-u M-g to refresh ;; ;; - how to remove groups deleted on the IMAP server? ;; - b to iterate over "bogus" groups and remove them ;; ;; - how to list most-recent mails on top? ;; - cf. gnus-thread-sort-functions ;; ;; - how to close a mail without going back to the group list? ;; - = to make summary full-screen ;; ;; - how to get contact completion? ;; - install ebdb from GNU ELPA ;; - or just use message-mail-alias-type ;; ;; - how to refresh? ;; - summary buffer: ;; - / N (fetch new) ;; - M-g (expire, move, fetch & redisplay) ;; - group buffer: g ;; ;; - what do all those letters mean? ;; (info "(gnus) Marking Articles") ;; - O old ≡ read during previous session ;; - R just read ;; - r manually marked as read ;; - A answered ;; - E expirable ;; - G cancelled (e.g. moved somewhere else) ;; - . unseen ;; ;; - how to subscribe to mailing lists? ;; - to browse an NNTP server, either ;; - hit B in the group buffer, then nntp *some server* ;; - or add (nntp "*some server*") to gnus-secondary-methods ;; - over the list: u ;; ;;; TODO. ;; ;; - gnus-summary-line-format (📎 for attachments) ;; ;; - how to archive mails and news locally? ;; ;; - describe-key is mostly useless in article mode: ;; > X runs the command gnus-article-read-summary-keys ;; ;; - detect possibly missing attachments from keywords