#!/bin/bash # Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail # Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing mail stack (isync, msmtp, notmuch, alot, w3m)..." # isync/mbsync: IMAP sync daemon; msmtp: SMTP sender; notmuch: fast mail indexer; # alot: terminal mail UI built on notmuch; w3m: renders HTML mail parts as plain text. sudo pacman -S --noconfirm --needed isync msmtp notmuch alot w3m # In unattended installs (answerfile mode / no TTY) there is no operator to answer # the account prompts below, so install the tools and stop cleanly here; the user # configures their mail account after first boot. if [[ "${MARCHY_UNATTENDED:-0}" == "1" || ! -t 0 ]]; then skip "Unattended mode — mail stack installed; configure accounts after first boot." exit 0 fi # ── Credentials ─────────────────────────────────────────────────────────────── # Collect all account details interactively before writing any config files, # so the user can review/abort cleanly before any files are touched. read -rp "Full name : " FULL_NAME read -rp "Email address : " EMAIL read -rp "IMAP host (e.g. mail.example.com) : " IMAP_HOST # Default IMAP port to 993 (IMAPS/TLS); user may override for non-standard setups. read -rp "IMAP port [993] : " IMAP_PORT; IMAP_PORT="${IMAP_PORT:-993}" # Default IMAP username to the email address, which most providers require. read -rp "IMAP username [$EMAIL] : " IMAP_USER; IMAP_USER="${IMAP_USER:-$EMAIL}" # -s suppresses echo so the password is not visible; trailing `echo` restores the newline. read -rsp "IMAP password : " IMAP_PASS; echo read -rp "SMTP host (e.g. mail.example.com) : " SMTP_HOST # Default SMTP port to 587 (STARTTLS submission); use 465 for implicit TLS. read -rp "SMTP port [587] : " SMTP_PORT; SMTP_PORT="${SMTP_PORT:-587}" # Root of the local Maildir tree; mbsync creates per-folder subdirectories here. MAILDIR="$HOME/Mail" mkdir -p "$MAILDIR" # ── mbsync ──────────────────────────────────────────────────────────────────── log "Writing ~/.mbsyncrc..." # Heredoc writes the full mbsync config in one shot using the variables collected # above. Overwriting on each run means re-running the script updates credentials. cat > ~/.mbsyncrc << EOF IMAPAccount main Host $IMAP_HOST Port $IMAP_PORT User $IMAP_USER Pass $IMAP_PASS SSLType IMAPS # Trust the system CA bundle rather than pinning a specific server certificate. CertificateFile /etc/ssl/certs/ca-certificates.crt IMAPStore main-remote Account main MaildirStore main-local # SubFolders Verbatim: mirror the IMAP folder hierarchy exactly as subdirectories. SubFolders Verbatim Path $MAILDIR/ Inbox $MAILDIR/INBOX Channel main Far :main-remote: Near :main-local: # Sync all IMAP folders; replace * with a list to restrict to specific folders. Patterns * # Create Both: automatically create missing folders on either side. Create Both # SyncState *: store .mbsyncstate alongside each Maildir folder for portability. SyncState * # Expunge Both: propagate deletions bidirectionally so remote deletes appear locally. Expunge Both EOF # 600 permissions keep the plaintext password private from other local users. chmod 600 ~/.mbsyncrc # ── msmtp ───────────────────────────────────────────────────────────────────── log "Writing ~/.msmtprc..." # msmtp acts as a drop-in sendmail replacement; alot invokes it via the # sendmail_command field in the alot account block written further below. cat > ~/.msmtprc << EOF defaults tls on tls_trust_file /etc/ssl/certs/ca-certificates.crt # Log every sent message with a timestamp to aid debugging delivery failures. logfile ~/.msmtp.log account main host $SMTP_HOST port $SMTP_PORT auth on user $IMAP_USER password $IMAP_PASS from $EMAIL # Make "main" the account used when msmtp is called without an explicit -a flag. account default : main EOF # Same 600 restriction as mbsyncrc — this file contains a plaintext SMTP password. chmod 600 ~/.msmtprc # ── notmuch ─────────────────────────────────────────────────────────────────── log "Configuring notmuch..." # notmuch config set writes to ~/.notmuch-config; each call is idempotent # (overwrites the existing key) so re-running the script is safe. notmuch config set user.name "$FULL_NAME" notmuch config set user.email "$EMAIL" # Point notmuch at the Maildir root so `notmuch new` scans the right location. notmuch config set database.path "$MAILDIR" # Synchronise notmuch tags with Maildir flags (Seen, Replied, etc.) bidirectionally. notmuch config set maildir.synchronize_flags true # Tag every newly indexed message as unread and inbox by default. notmuch config set new.tags "unread;inbox" # post-new hook: tag sent mail, remove inbox from trash # notmuch executes this script automatically after `notmuch new` indexes messages. mkdir -p "$MAILDIR/.notmuch/hooks" cat > "$MAILDIR/.notmuch/hooks/post-new" << 'EOF' #!/bin/bash # Apply folder-based tags and strip inbox/unread from non-inbox folders. notmuch tag +sent -inbox -- folder:Sent notmuch tag +trash -inbox -unread -- folder:Trash notmuch tag +draft -inbox -- folder:Drafts notmuch tag +spam -inbox -unread -- folder:Spam folder:Junk EOF # The hook must be executable or notmuch will silently skip it. chmod +x "$MAILDIR/.notmuch/hooks/post-new" # ── alot ────────────────────────────────────────────────────────────────────── # The bindings section lives in ~/Dotfiles/alot/config (symlinked by shell-setup.sh). # Write only the account block, which contains machine-specific paths/identity. log "Writing account details into ~/Dotfiles/alot/config..." ALOT_CFG="$HOME/Dotfiles/alot/config" # Use an inline Python script to splice the [accounts] block into the existing # config file. sed struggles with multi-line replacements; Python's re.sub with # re.DOTALL handles the entire block atomically and overwrites any prior values. # Replace the [[main]] account block in-place (sed removes old block, cat appends new one) python3 - "$ALOT_CFG" "$FULL_NAME" "$EMAIL" "$MAILDIR" << 'PYEOF' import sys, re path, name, email, maildir = sys.argv[1:] block = f"""[accounts] [[main]] realname = {name} address = {email} sendmail_command = msmtp -a main sent_box = maildir://{maildir}/Sent draft_box = maildir://{maildir}/Drafts """ with open(path) as f: text = f.read() # Replace from [accounts] up to the next top-level section or end-of-file, # ensuring any previous account block is cleanly overwritten on re-runs. text = re.sub(r'\[accounts\].*?(?=\n\[|\Z)', block, text, flags=re.DOTALL) with open(path, 'w') as f: f.write(text) PYEOF # ── mailcap (HTML email rendering via w3m) ──────────────────────────────────── log "Writing ~/.mailcap..." # grep -qxF checks for an exact full-line match to avoid appending duplicates # on repeated runs; the || only appends the entry when it is absent. # w3m -dump converts HTML to plain text; copiousoutput tells mail clients # to page the result rather than try to display it inline. grep -qxF "text/html; w3m -dump -o document_charset=%{charset} '%s'; nametemplate=%s.html; copiousoutput" ~/.mailcap 2>/dev/null \ || echo "text/html; w3m -dump -o document_charset=%{charset} '%s'; nametemplate=%s.html; copiousoutput" >> ~/.mailcap # ── systemd timer for periodic sync ─────────────────────────────────────────── # This whole block is best-effort: when the installer runs from a bare TTY or a # chroot there is no per-user systemd session bus (and ~/.config may not yet be # owned by the invoking user), so `systemctl --user` and even the mkdir can fail. # None of that should abort the module — the mail config is already in place and # the timer can be (re)created on first graphical/login session. We therefore # warn and continue instead of letting `set -e` kill the install. log "Installing mbsync systemd user timer (every 5 min)..." # User units live under ~/.config/systemd/user/; --user scope requires no root # and units only run while the user session is active. if mkdir -p ~/.config/systemd/user 2>/dev/null; then # Type=oneshot: systemd marks the service done as soon as ExecStart exits, # appropriate for a batch sync command that runs and terminates. # After=network-online.target: prevents sync attempts before a route is available. cat > ~/.config/systemd/user/mbsync.service << EOF [Unit] Description=Sync mail with mbsync After=network-online.target [Service] Type=oneshot ExecStart=/usr/bin/mbsync -a # Re-index immediately after each sync so alot reflects new messages right away. ExecStartPost=/usr/bin/notmuch new EOF # OnBootSec=2min: delay the first run by 2 min to avoid startup congestion. # OnUnitActiveSec=5min: repeat 5 min after the previous run finishes. cat > ~/.config/systemd/user/mbsync.timer << EOF [Unit] Description=Run mbsync every 5 minutes [Timer] OnBootSec=2min OnUnitActiveSec=5min Unit=mbsync.service [Install] WantedBy=timers.target EOF # Reload the user manager so it picks up the newly written unit files. # A successful daemon-reload means a user session bus is available; only then # is it worth trying to enable/start the timer. Guard both so the absence of # a session during install is reported, not fatal. if systemctl --user daemon-reload 2>/dev/null; then # enable makes the timer survive reboots; --now also starts it now. systemctl --user enable --now mbsync.timer 2>/dev/null \ || warn "Could not enable mbsync.timer now — run: systemctl --user enable --now mbsync.timer" else warn "No user systemd session detected — enable the timer after login with: systemctl --user enable --now mbsync.timer" fi else warn "Could not create ~/.config/systemd/user — skipping the mbsync timer (set it up after first login)." fi # ── initial sync ────────────────────────────────────────────────────────────── # Also best-effort: with placeholder credentials or no network at install time # the first sync is expected to fail. Warn rather than abort so the module still # reports success — the timer (or a manual `mbsync -a`) will sync once the user # has filled in real account details. log "Running initial mail sync..." # -a syncs all configured channels; may take several minutes on a large mailbox. if mbsync -a; then # Index the freshly downloaded messages so alot can display them immediately. notmuch new || warn "notmuch new failed — index later with: notmuch new" else warn "Initial mail sync failed (credentials/network not ready) — run 'mbsync -a && notmuch new' once configured." fi log "Mail setup complete. Syncs automatically every 5 min via systemd timer." log "Open alot with: alot | Check timer: systemctl --user status mbsync.timer"