From 28a67f2d5b22a54ff9734ff039fec85dd2d47852 Mon Sep 17 00:00:00 2001 From: Jens Luedicke Date: Fri, 5 Sep 2025 14:45:24 +0200 Subject: [PATCH] Add mu4e mail reader configuartion --- init.el | 41 ++-- mu4e-config.el | 528 ++++++++++++++++++++++++++++++++++++++++++++++++ process-mail.sh | 125 ++++++++++++ 3 files changed, 682 insertions(+), 12 deletions(-) create mode 100644 mu4e-config.el create mode 100755 process-mail.sh diff --git a/init.el b/init.el index e1b0606..4312076 100644 --- a/init.el +++ b/init.el @@ -200,24 +200,24 @@ ;; Configure VC backend for Git (setq vc-handled-backends '(Git)) (setq vc-git-diff-switches '("--histogram")) - + ;; IMPORTANT: Tell diff-hl to use VC backend, not Magit (setq diff-hl-reference-revision nil) ; Use working tree, not index (setq diff-hl-disable-on-remote nil) ; Work even on remote files - + ;; Make diff-hl use the left fringe (setq diff-hl-side 'left) - + ;; Ensure diff-hl draws in fringe, not margin (setq diff-hl-draw-borders nil) (setq diff-hl-margin-mode nil) - + ;; Set diff-hl fringe bitmaps (ensure they're visible) (setq diff-hl-fringe-bmp-function 'diff-hl-fringe-bmp-from-type) - + ;; Enable flydiff for real-time updates using VC (diff-hl-flydiff-mode 1) - + ;; Update immediately when visiting a file (setq diff-hl-flydiff-delay 0.3) @@ -229,12 +229,12 @@ ;; Enable globally (global-diff-hl-mode 1) - + ;; Explicitly disable Magit integration in base config ;; (Magit hooks will only be added when dev-mode is enabled) (setq diff-hl-disable-on-remote nil) - ;; Manual refresh command + ;; Manual refresh command (defun diff-hl-refresh () "Manually refresh diff-hl indicators in all buffers." (interactive) @@ -242,7 +242,7 @@ (with-current-buffer buf (when diff-hl-mode (diff-hl-update))))) - + ;; Troubleshooting function (defun diff-hl-diagnose () "Diagnose diff-hl issues." @@ -261,7 +261,7 @@ (push (format "diff-hl-reference-revision: %s" diff-hl-reference-revision) diagnosis) (push (format "Magit loaded: %s" (if (fboundp 'magit-status) "yes" "no")) diagnosis) (message (mapconcat 'identity (nreverse diagnosis) "\n")))) - + ;; Force VC to refresh its state (defun diff-hl-force-vc-refresh () "Force VC to refresh state and then update diff-hl." @@ -270,7 +270,7 @@ (vc-refresh-state) (diff-hl-update) (message "VC state refreshed and diff-hl updated"))) - + ;; Disable Magit integration if causing issues (defun diff-hl-disable-magit () "Disable Magit integration with diff-hl." @@ -279,7 +279,7 @@ (remove-hook 'magit-pre-refresh-hook 'diff-hl-magit-pre-refresh) (remove-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh) (message "Magit integration with diff-hl disabled. Using pure VC backend.")) - + ;; Ensure we're using VC backend (defun diff-hl-ensure-vc-backend () "Ensure diff-hl is using VC backend." @@ -708,6 +708,13 @@ (when (file-exists-p elfeed-config) (load-file elfeed-config))) + ;; Reload mu4e config if it exists + (let ((mu4e-config (expand-file-name "mu4e-config.el" user-emacs-directory))) + (when (file-exists-p mu4e-config) + (condition-case err + (load-file mu4e-config) + (error + (message "mu4e config available but mu4e not installed"))))) (message "Emacs configuration fully reloaded!")) @@ -722,6 +729,16 @@ (load-file elfeed-config) (message "Elfeed RSS reader configuration loaded."))) +;;; Email Configuration (mu4e) +(let ((mu4e-config (expand-file-name "mu4e-config.el" user-emacs-directory))) + (when (file-exists-p mu4e-config) + (condition-case err + (progn + (load-file mu4e-config) + (message "mu4e email configuration loaded.")) + (error + (message "mu4e configuration available but mu4e not installed. Install mu4e package to enable email."))))) + ;;; Development Mode Information (defun show-dev-mode-info () "Show information about development mode." diff --git a/mu4e-config.el b/mu4e-config.el new file mode 100644 index 0000000..3dab993 --- /dev/null +++ b/mu4e-config.el @@ -0,0 +1,528 @@ +;;; mu4e-config.el --- mu4e email configuration -*- lexical-binding: t; -*- + +;; mu4e should already be loaded from .emacs before this file is loaded +;; If not loaded, try to load it +(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e/") +(require 'mu4e) + +;; HTML rendering configuration for mu4e 1.12 + +;; Use shr as the default renderer +(setq mm-text-html-renderer 'shr) + +;; Configure shr for better plain text display +;; (setq shr-use-colors nil) ; No colors +;; (setq shr-use-fonts nil) ; No variable fonts +;; (setq shr-width 80) ; 80 column width +(setq shr-bullet "• ") ; Nice bullet + +;; Create a custom command to view HTML with pandoc +(defun mu4e-view-html-with-pandoc () + "View the HTML part of the current message using pandoc." + (interactive) + (let ((msg (mu4e-message-at-point))) + (unless msg + (mu4e-error "No message at point")) + (let* ((path (mu4e-message-field msg :path)) + (pandoc-buffer "*mu4e-pandoc*") + (temp-dir (make-temp-file "mu4e-extract" t))) + (if path + (progn + (with-current-buffer (get-buffer-create pandoc-buffer) + (erase-buffer) + (insert "=== HTML Email Rendered with Pandoc ===\n\n") + ;; Extract all parts + (call-process "mu" nil nil nil + "extract" "--save-all" + (format "--target-dir=%s" temp-dir) path) + ;; Find HTML files and convert them + (let ((html-files (directory-files temp-dir t "\\.html?$"))) + (if html-files + (dolist (html-file html-files) + (insert (shell-command-to-string + (format "pandoc -f html -t markdown --wrap=auto --columns=80 '%s' 2>/dev/null" + html-file)))) + ;; No HTML files found, try extracting from message directly + (let ((raw-msg (shell-command-to-string (format "cat '%s'" path)))) + ;; Look for HTML content between boundaries + (if (string-match "Content-Type: text/html" raw-msg) + (progn + (insert "Converting HTML content...\n\n") + (let ((temp-html (make-temp-file "mu4e-msg" nil ".html"))) + (with-temp-file temp-html + (insert raw-msg)) + (insert (shell-command-to-string + (format "cat '%s' | sed -n '/Content-Type: text\\/html/,/^--/p' | sed '1,/^$/d' | sed '/^--/,$d' | pandoc -f html -t markdown --wrap=auto --columns=80 2>/dev/null" + temp-html))) + (delete-file temp-html))) + (insert "No HTML content found in this message.\n"))))) + ;; Clean up temp directory + (delete-directory temp-dir t) + (goto-char (point-min)) + (view-mode) + (display-buffer (current-buffer)))) + (mu4e-warn "Cannot access message file"))))) + +;; Simpler approach: Add action to view HTML with pandoc +(add-to-list 'mu4e-view-actions + '("pandoc" . (lambda (msg) + (let* ((path (mu4e-message-field msg :path)) + (pandoc-buffer "*mu4e-pandoc*")) + (when path + (with-current-buffer (get-buffer-create pandoc-buffer) + (erase-buffer) + (insert "=== HTML Email Rendered with Pandoc ===\n\n") + (let ((html-content + (shell-command-to-string + (format "mu view '%s' 2>/dev/null | awk '/text\\/html/{flag=1; next} flag && /^--/{exit} flag' | pandoc -f html -t markdown --wrap=auto --columns=80 2>/dev/null" path)))) + (if (and html-content (> (length html-content) 0)) + (insert html-content) + ;; If no HTML part found, show message + (insert "No HTML content found in this message.\n\n") + (insert (shell-command-to-string + (format "mu view '%s'" path))))) + (goto-char (point-min)) + (view-mode) + (display-buffer (current-buffer))))))) + t) + +;; Add keybinding for pandoc view +(with-eval-after-load 'mu4e-view + (define-key mu4e-view-mode-map (kbd "H") 'mu4e-view-html-with-pandoc)) + +(message "mu4e: HTML rendering configured. Press 'H' in message view to render with pandoc.") + +;; Basic mu4e settings +(setq mu4e-maildir "~/Maildir" + ;; Use our processing script instead of plain mbsync + ;; This will sync mail and fix List-Id headers + mu4e-get-mail-command (expand-file-name "process-mail.sh" user-emacs-directory) + mu4e-update-interval 300 ; Update every 5 minutes + mu4e-compose-signature-auto-include nil + mu4e-view-show-images t + mu4e-view-show-addresses t + mu4e-change-filenames-when-moving t ; Needed for mbsync + mu4e-index-cleanup t ; Clean up after moving + mu4e-index-lazy-check nil ; Don't be lazy about indexing + mu4e-hide-index-messages t) ; Hide indexing messages to avoid errors + +;; Function to get current context's maildir prefix +(defun mu4e-current-context-maildir-prefix () + "Get the maildir prefix for the current context." + (if mu4e-context-current + (let ((context-name (mu4e-context-name mu4e-context-current))) + (format "maildir:/%s/*" context-name)) + "")) + +;; Bookmarks (shortcuts to common searches) +;; Use setq to define the complete list at once +(setq mu4e-bookmarks + '(;; Basic views - work in current context + (:name "Unread messages" + :query "flag:unread AND NOT flag:trashed" + :key ?u) + (:name "Today's messages" + :query "date:today..now" + :key ?t) + (:name "Last 7 days" + :query "date:7d..now" + :key ?w) + + ;; Smart mailboxes - search across all contexts + (:name "📌 Important" + :query "(flag:flagged OR prio:high OR from:/boss|manager|ceo|director|important/ OR subject:/urgent|important|critical|asap/) AND NOT flag:trashed" + :key ?i) + + (:name "📰 Newsletters" + :query "(from:/newsletter|news|digest|update|weekly|daily|monthly|bulletin|announcement/ OR subject:/newsletter|digest|update|weekly|edition/) AND NOT flag:trashed AND NOT flag:flagged" + :key ?n) + + (:name "🛍️ Purchases & Orders" + :query "(from:/amazon|ebay|paypal|stripe|shopify|order|store|shop|invoice|receipt/ OR subject:/order|invoice|receipt|purchase|payment|confirmation|shipping|delivery|tracking/) AND NOT flag:trashed" + :key ?p) + + (:name "📎 Attachments" + :query "flag:attach AND NOT flag:trashed" + :key ?a) + + (:name "🎫 Travel & Tickets" + :query "(from:/airline|hotel|booking|expedia|airbnb|uber|lyft|train|eventbrite|ticketmaster/ OR subject:/booking|reservation|ticket|flight|itinerary|confirmation/) AND NOT flag:trashed" + :key ?v) + + (:name "💰 Finance & Banking" + :query "(from:/bank|credit|visa|mastercard|amex|insurance|tax|accountant/ OR subject:/statement|balance|transaction|payment/) AND NOT flag:trashed" + :key ?f) + + (:name "👥 Social & Forums" + :query "(from:/facebook|twitter|linkedin|instagram|reddit|github|gitlab|discourse|forum/ OR subject:/commented|replied|mentioned|tagged|followed/) AND NOT flag:trashed" + :key ?s) + + ;; Mailing Lists (Personal context) + (:name "📋 All Mailing Lists" + :query "maildir:/Personal/Lists AND NOT flag:trashed" + :key ?L) + + (:name "📋 C++ std-discussion" + :query "list:std-discussion.lists.isocpp.org AND NOT flag:trashed" + :key ?C) + + (:name "📋 Qt Interest" + :query "list:interest.qt-project.org AND NOT flag:trashed" + :key ?q) + + (:name "📋 Boost" + :query "list:boost.lists.boost.org AND NOT flag:trashed" + :key ?b) + + (:name "📋 GCC" + :query "list:gcc.gnu.gcc.org AND NOT flag:trashed" + :key ?G) + + (:name "📋 LKML" + :query "list:linux-kernel.vger.kernel.org AND NOT flag:trashed" + :key ?K))) + +(setq mu4e-maildir-shortcuts + '(("/Personal/INBOX" . ?i) + ("/Personal/Sent" . ?s) + ("/Personal/Trash" . ?t) + ("/Personal/Drafts" . ?d) + ("/Personal/Spam " . ?S) + ("/Personal/Archive" . ?a) + ("/Personal/Lists" . ?l) + ("/IONOS/INBOX" . ?I) + ("/IONOS/Sent" . ?S) + ("/IONOS/Trash" . ?T) + ("/IONOS/Drafts" . ?D) + ("/IONOS/Archive" . ?A))) + +;; UI Configuration - use default headers for now +;; (setq mu4e-headers-fields +;; '((:human-date . 12) +;; (:flags . 6) +;; (:from-or-to . 22) +;; (:subject))) + +;; Make mu4e respect the current color theme +(setq mu4e-view-use-gnus t) ; Use Gnus article mode for better theme support +(setq shr-use-colors nil) ; Let the theme handle colors in HTML emails + +;; Use full window for reading emails +(setq mu4e-split-view nil) ; Don't split, use full window for message view + +;; Make headers/search view respect theme +(setq mu4e-headers-unread-face 'bold) ; Use theme's bold face instead of custom color +(setq mu4e-headers-highlight-face 'highlight) ; Use theme's highlight face +(setq mu4e-headers-flagged-face 'font-lock-warning-face) ; Use theme's warning face + +;; Enable inline images +(setq mu4e-view-show-images t) +(when (fboundp 'imagemagick-register-types) + (imagemagick-register-types)) + +;; Colorize inline patches in emails +(require 'diff-mode) + +(defun mu4e-colorize-patch () + "Colorize patches in mu4e view buffers." + (save-excursion + (goto-char (point-min)) + ;; Look for patch sections (starting with diff, ---, or @@) + (while (re-search-forward "^\\(diff \\|--- \\|\\+\\+\\+ \\|@@ \\)" nil t) + (let ((patch-start (match-beginning 0))) + ;; Find the end of the patch + (if (re-search-forward "^[^-+@ \t]" nil t) + (backward-char) + (goto-char (point-max))) + (let ((patch-end (point)) + (inhibit-read-only t)) + ;; Apply diff-mode font-lock to the patch region + (add-text-properties patch-start patch-end + '(face nil)) ; Reset face first + (save-restriction + (narrow-to-region patch-start patch-end) + (diff-mode) + (font-lock-fontify-region patch-start patch-end) + (widen)))))) + ;; Also colorize simple diff lines + (save-excursion + (goto-char (point-min)) + (while (re-search-forward "^\\(-.*\\)$" nil t) + (let ((inhibit-read-only t)) + (add-face-text-property (match-beginning 1) (match-end 1) + 'diff-removed))) + (goto-char (point-min)) + (while (re-search-forward "^\\(\\+.*\\)$" nil t) + (let ((inhibit-read-only t)) + (add-face-text-property (match-beginning 1) (match-end 1) + 'diff-added))) + (goto-char (point-min)) + (while (re-search-forward "^\\(@@.*@@\\).*$" nil t) + (let ((inhibit-read-only t)) + (add-face-text-property (match-beginning 1) (match-end 1) + 'diff-hunk-header))))) + +;; Hook to colorize patches when viewing messages +(add-hook 'mu4e-view-mode-hook 'mu4e-colorize-patch) + +;; For Gnus article mode (when mu4e-view-use-gnus is t) +(with-eval-after-load 'gnus-art + (add-hook 'gnus-article-mode-hook 'mu4e-colorize-patch)) + +;; Use mu4e for composing email +(setq mail-user-agent 'mu4e-user-agent) + +;; Keybindings +(global-set-key (kbd "C-x m") 'mu4e) +(global-set-key (kbd "C-x M") 'mu4e-compose-new) + +;; Double-check pandoc is available and being used +(if (executable-find "pandoc") + (message "Pandoc found at: %s" (executable-find "pandoc")) + (message "WARNING: Pandoc not found!")) + +;; Prefer plain text when available, but show HTML when it's the only option +(setq mu4e-view-prefer-html nil) + +;; Actions +(add-to-list 'mu4e-view-actions + '("ViewInBrowser" . mu4e-action-view-in-browser) t) + +;; Don't keep message buffers around +(setq message-kill-buffer-on-exit t) + +;; For Proton Bridge: Comment out custom marks for now to avoid errors +;; The default behavior will be: +;; d - move to trash +;; D - delete permanently +;; r - refile/archive + +;; If you want to customize deletion behavior, uncomment and adjust: +;; (with-eval-after-load 'mu4e +;; ;; Make 'd' archive instead of trash +;; (setf (alist-get 'trash mu4e-marks) +;; (list :char '("d" . "▼") +;; :prompt "archive" +;; :dyn-target (lambda (target msg) +;; (mu4e-get-refile-folder msg)) +;; :action (lambda (docid msg target) +;; (mu4e--server-move docid +;; (mu4e--mark-check-target target) +;; "-N"))))) + +;; Test function to verify pandoc is working +(defun mu4e-test-pandoc () + "Test if pandoc is being used for HTML rendering." + (interactive) + (let ((test-html "

Test Header

This is a test paragraph with emphasis.

") + (temp-file (make-temp-file "mu4e-pandoc-test" nil ".html"))) + (with-temp-file temp-file + (insert test-html)) + (message "Testing pandoc with command: %s" mu4e-html2text-command) + (message "Input HTML:\n%s" test-html) + (message "Pandoc output:\n%s" + (shell-command-to-string + (format "%s < %s" mu4e-html2text-command temp-file))) + (delete-file temp-file))) + +;; Mailing lists configuration +(setq mu4e-mailing-lists + '((:list-id "linux-kernel.vger.linux.org" :name "linux-kernel") + (:list-id "std-discussion.lists.isocpp.org" :name "std-discussion") + (:list-id "gcc.gnu.gcc.org" :name "gnu-gcc") + (:list-id "interest.qt-project.org" :name "Qt-Interest") + (:list-id "boost.lists.boost.org" :name "Boost") + (:list-id "boost-announce.lists.boost.org" :name "Boost-Announce") + (:list-id "boost-interest.lists.boost.org" :name "Boost-Interest"))) + +;; Signature +(setq mu4e-compose-signature + "Jens") + +;; SMTP Configuration for sending mail +(require 'smtpmail) +(setq message-send-mail-function 'smtpmail-send-it) + +;; Default SMTP settings (will be overridden by context) +(setq smtpmail-stream-type 'starttls + smtpmail-smtp-service 587 + smtpmail-debug-info t + smtpmail-debug-verb t + smtpmail-auth-credentials "~/.authinfo") + +;; Configure context policy +(setq mu4e-context-policy 'pick-first + mu4e-compose-context-policy 'ask-if-none) + +;; Function to set SMTP parameters based on From address +(defun my-mu4e-set-smtp-params () + "Set SMTP parameters based on the From address." + (let ((from (message-field-value "From"))) + (cond + ;; Personal account - also handles alias addresses @luedicke.me and @luedicke.xyz + ((or (string-match "@luedicke\\.me" from) + (string-match "@luedicke\\.xyz" from)) + (message "Setting SMTP for Personal account (Proton Bridge)...") + (setq smtpmail-smtp-server "127.0.0.1" + smtpmail-smtp-service 1025 + smtpmail-stream-type 'starttls + smtpmail-smtp-user "jens@luedicke.me" ; Always use main account for auth + smtpmail-auth-credentials "~/.authinfo" + smtpmail-smtp-timeout 30)) ; 30 second timeout + ((string-match "jens@luedicke.cloud" from) + (message "Setting SMTP for IONOS account...") + (setq smtpmail-smtp-server "smtp.ionos.de" + smtpmail-smtp-service 587 + smtpmail-stream-type 'starttls + smtpmail-smtp-user "jens@luedicke.cloud" + smtpmail-auth-credentials "~/.authinfo" + smtpmail-smtp-timeout 30)) ; 30 second timeout + (t + (error "Unknown sender address: %s" from))))) + +;; Hook to set SMTP params before sending +(add-hook 'message-send-hook 'my-mu4e-set-smtp-params) + +;; Alias email addresses configuration +;; Define your email aliases here +(setq my-email-aliases + '(("std-discussion@luedicke.xyz" . "std-discussion.lists.isocpp.org") + ("gnu-gcc@luedicke.xyz" . "gcc.gcc.gnu.org") + ("qt-interest@luedicke.xyz" . "interest.qt-project.org") + ("boost@luedicke.xyz" . "boost.lists.boost.org") + ("jens@luedicke.me" . "linux-kernel.vger.kernel.org"))) + +;; Variable to store the desired From address +(defvar my-mu4e-reply-address nil + "Stores the email address to use for replies to mailing lists.") + +;; Function to determine the reply address based on recipient +(defun my-mu4e-set-from-address () + "Set the From address based on the original recipient. +If the message was sent to one of our aliases (via mailing list), +use that alias as the From address." + (setq my-mu4e-reply-address nil) ; Reset first + (let ((msg mu4e-compose-parent-message)) + (when msg + (let ((list-id (mu4e-message-field msg :list)) + (to (mu4e-message-field msg :to)) + (cc (mu4e-message-field msg :cc))) + ;; Check if this message came from a mailing list we have an alias for + (dolist (alias-pair my-email-aliases) + (when (and list-id + (string-match-p (regexp-quote (cdr alias-pair)) list-id) + (not my-mu4e-reply-address)) + ;; Store the alias to use + (setq my-mu4e-reply-address (car alias-pair)) + (message "Will use alias address: %s" my-mu4e-reply-address))))))) + +;; Function to actually set the From header +(defun my-mu4e-compose-set-from () + "Set the From address in the compose buffer." + (when my-mu4e-reply-address + ;; For aliases, we need to keep the authenticated address in From + ;; but add a Reply-To with the alias address + (save-excursion + ;; Keep the main address in From (for SMTP authentication) + (message-remove-header "From") + (message-add-header (format "From: %s <%s>" user-full-name "jens@luedicke.me")) + + ;; Add Reply-To with the alias address so replies come back to the right address + (message-remove-header "Reply-To") + (message-add-header (format "Reply-To: %s <%s>" user-full-name my-mu4e-reply-address)) + + ;; Optionally add a comment in the From field to show which list this is for + ;; This helps you see which alias you're using + (goto-char (point-min)) + (when (re-search-forward "^From: \\(.*\\) <\\(.*\\)>$" nil t) + (replace-match (format "From: %s (via %s) <%s>" + user-full-name + my-mu4e-reply-address + "jens@luedicke.me")))) + (message "Using Reply-To address: %s" my-mu4e-reply-address))) + +;; Hook to set the From address when composing replies +(add-hook 'mu4e-compose-pre-hook 'my-mu4e-set-from-address) +;; Run after a short delay to ensure context switching is complete +(add-hook 'mu4e-compose-mode-hook + (lambda () + (run-at-time 0.1 nil 'my-mu4e-compose-set-from))) + +;; Update contexts to include SMTP settings +(setq mu4e-contexts + `(,(make-mu4e-context + :name "Personal" + :match-func (lambda (msg) + (when msg + (string-prefix-p "/Personal" (mu4e-message-field msg :maildir)))) + :vars '((user-mail-address . "jens@luedicke.me") + (user-full-name . "Jens Luedicke") + (mu4e-drafts-folder . "/Personal/Drafts") + (mu4e-sent-folder . "/Personal/Sent") + (mu4e-trash-folder . "/Personal/Trash") + (mu4e-refile-folder . "/Personal/Archive"))) + ,(make-mu4e-context + :name "IONOS" + :match-func (lambda (msg) + (when msg + (string-prefix-p "/IONOS" (mu4e-message-field msg :maildir)))) + :vars '((user-mail-address . "jens@luedicke.cloud") + (user-full-name . "Jens Luedicke") + (mu4e-drafts-folder . "/IONOS/Drafts") + (mu4e-sent-folder . "/IONOS/Sent") + (mu4e-trash-folder . "/IONOS/Trash") + (mu4e-refile-folder . "/IONOS/Archive"))))) + +;; Optional: Auto-filing rules for incoming mail +;; Uncomment and customize these to automatically move messages to folders +;; (setq mu4e-headers-auto-update t) ; Auto-update headers buffer + +;; Example auto-filing with mu4e-marks +;; This runs when indexing new mail +(defun my-mu4e-auto-file () + "Auto-file certain messages to specific folders." + (when (mu4e-message-field mu4e-compose-parent-message :subject) + (let ((subject (mu4e-message-field mu4e-compose-parent-message :subject)) + (from (mu4e-message-field mu4e-compose-parent-message :from))) + + ;; Auto-file newsletters + (when (or (string-match-p "newsletter\\|digest\\|weekly" subject) + (string-match-p "noreply\\|no-reply\\|newsletter" (car from))) + (mu4e-message-field mu4e-compose-parent-message :maildir "/Newsletters")) + + ;; Auto-file purchase receipts + (when (string-match-p "order\\|receipt\\|invoice\\|purchase" subject) + (mu4e-message-field mu4e-compose-parent-message :maildir "/Purchases"))))) + +;; Hook to run auto-filing after updating +;; (add-hook 'mu4e-index-updated-hook 'my-mu4e-auto-file) + +;; Custom search query functions for advanced users +(defun mu4e-search-important () + "Search for important messages." + (interactive) + (mu4e-search "(flag:flagged OR prio:high OR from:/boss|manager|ceo|director/) AND NOT flag:trashed")) + +(defun mu4e-search-newsletters () + "Search for newsletters." + (interactive) + (mu4e-search "(list:/.+/ OR from:/newsletter|news|digest/ OR body:/unsubscribe/) AND NOT flag:trashed")) + +(defun mu4e-search-purchases () + "Search for purchase-related emails." + (interactive) + (mu4e-search "(from:/amazon|ebay|paypal|order|shop/ OR subject:/order|invoice|receipt|purchase/) AND NOT flag:trashed")) + +;; Customization tips: +;; 1. To add more bookmarks, add entries to mu4e-bookmarks above +;; 2. To customize search patterns, modify the :query strings +;; 3. To change keyboard shortcuts, modify the :key values +;; 4. To add sender-specific rules, add from:/sender@domain/ patterns +;; 5. To exclude certain messages, add AND NOT conditions + +;; VIP sender list example (uncomment and customize): +;; (setq my-vip-senders '("boss@company.com" "important@client.com")) +;; Then use in queries: (member-if (lambda (vip) (string-match-p vip from)) my-vip-senders) + +(provide 'mu4e-config) +;;; mu4e-config.el ends here diff --git a/process-mail.sh b/process-mail.sh new file mode 100755 index 0000000..10a7f66 --- /dev/null +++ b/process-mail.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# Simplified mail processing script +# Now that Proton handles filtering server-side, we just sync and optionally archive + +# Configuration +MAILDIR="$HOME/Maildir" +ARCHIVE_LISTS=true # Set to false if you don't want local date-based archives + +# Function to create date-based archive of Lists folder +archive_lists() { + if [ "$ARCHIVE_LISTS" != "true" ]; then + return + fi + + echo "Creating local date-based archives of mailing lists..." + + local year=$(date +%Y) + local month=$(date +%m) + local day=$(date +%d) + + # Process messages in Lists folder + if [ -d "$MAILDIR/Personal/Lists" ]; then + find "$MAILDIR/Personal/Lists"/{new,cur} -type f 2>/dev/null | while read -r msg; do + # Determine which list this belongs to based on List-Id header + LIST_NAME="" + + if grep -q "List-Id:.*linux-kernel\.vger\.kernel\.org" "$msg" 2>/dev/null; then + LIST_NAME="linux-kernel" + elif grep -q "List-Id:.*std-discussion\.lists\.isocpp\.org" "$msg" 2>/dev/null; then + LIST_NAME="std-discussion" + elif grep -q "List-Id:.*gcc\.gnu\.gcc\.org" "$msg" 2>/dev/null; then + LIST_NAME="gcc" + elif grep -q "List-Id:.*interest\.qt-project\.org" "$msg" 2>/dev/null; then + LIST_NAME="qt-interest" + elif grep -q "List-Id:.*boost-announce\.lists\.boost\.org" "$msg" 2>/dev/null; then + LIST_NAME="boost-announce" + elif grep -q "List-Id:.*boost-interest\.lists\.boost\.org" "$msg" 2>/dev/null; then + LIST_NAME="boost-interest" + elif grep -q "List-Id:.*boost\.lists\.boost\.org" "$msg" 2>/dev/null; then + LIST_NAME="boost" + elif grep -q "List-Id:" "$msg" 2>/dev/null; then + # Extract list name from List-Id header + LIST_NAME=$(grep "List-Id:" "$msg" | sed -n 's/.*<\([^.]*\)\..*/\1/p' | head -1) + if [ -z "$LIST_NAME" ]; then + LIST_NAME="misc" + fi + fi + + # If we identified a list, create archive copy + if [ -n "$LIST_NAME" ]; then + ARCHIVE_DIR="$MAILDIR/Personal/Archive/Lists/$LIST_NAME/$year/$month/$day" + mkdir -p "$ARCHIVE_DIR"/{new,cur,tmp} + + # Determine target directory (new or cur) + if [[ "$msg" == */new/* ]]; then + TARGET_DIR="$ARCHIVE_DIR/new" + else + TARGET_DIR="$ARCHIVE_DIR/cur" + fi + + # Copy if not already archived + BASENAME=$(basename "$msg") + if [ ! -f "$TARGET_DIR/$BASENAME" ]; then + cp "$msg" "$TARGET_DIR/" + fi + fi + done + fi +} + +# Main processing +echo "==========================================" +echo "Mail Processing Started: $(date)" +echo "==========================================" + +# Step 1: Sync mail with mbsync +echo "" +echo "Step 1: Syncing mail with mbsync..." +mbsync -a +SYNC_STATUS=$? + +if [ $SYNC_STATUS -ne 0 ]; then + echo "Note: mbsync returned status $SYNC_STATUS" + echo "This can be normal if some folders are not yet created on the server." +fi + +# Step 2: Create local archives (optional) +if [ "$ARCHIVE_LISTS" = "true" ]; then + echo "" + echo "Step 2: Creating local date-based archives..." + archive_lists +else + echo "" + echo "Step 2: Skipping local archives (disabled)" +fi + +# Step 3: Update mu index +echo "" +echo "Step 3: Updating mu index..." +mu index --quiet + +# Step 4: Show summary +echo "" +echo "==========================================" +echo "Mail Processing Complete: $(date)" +echo "==========================================" + +# Show folder statistics +echo "" +echo "Mail folder summary:" +echo " INBOX: $(find "$MAILDIR/Personal/INBOX" -type f 2>/dev/null | wc -l) messages" +echo " Lists: $(find "$MAILDIR/Personal/Lists" -type f 2>/dev/null | wc -l) messages" + +if [ "$ARCHIVE_LISTS" = "true" ] && [ -d "$MAILDIR/Personal/Archive/Lists" ]; then + echo "" + echo "Local archives:" + find "$MAILDIR/Personal/Archive/Lists" -maxdepth 1 -type d -name "[a-z]*" 2>/dev/null | sort | while read -r dir; do + count=$(find "$dir" -type f 2>/dev/null | wc -l) + if [ $count -gt 0 ]; then + printf " %-20s %d messages\n" "$(basename "$dir"):" "$count" + fi + done +fi + +exit 0 \ No newline at end of file