Compare commits

..

7 Commits

Author SHA1 Message Date
58fd5c4d50 update cliphist viewer 2026-02-04 16:09:27 +01:00
3837b42437 fix: missing icon in hyprlock 2026-02-03 09:37:04 +01:00
dce89a0380 update memo & more 2026-02-01 19:00:52 +01:00
483b0bbb8c add prime-toggle script 2026-01-27 12:46:21 +01:00
b9ed4072f2 minor 2026-01-24 23:16:59 +01:00
4f384a2c79 update mail-service.md, terminology clarification 2026-01-24 06:17:00 +01:00
cc21b2b1dc update mail-service.md 2026-01-22 06:20:50 +01:00
16 changed files with 781 additions and 168 deletions
+9 -8
View File
@@ -8,17 +8,18 @@ $entry_border_color = rgba(3B3B3B64)
$entry_color = rgba(FFFFFFFF) $entry_color = rgba(FFFFFFFF)
$font_family = Noto Sans $font_family = Noto Sans
$font_family_clock = Font Awesome 6 Free $font_family_clock = Font Awesome 6 Free
$font_material_symbols = Material Symbols Rounded $font_family_nerd = Symbols Nerd Font
$background = ~/Pictures/backgrounds/miku-space.jpg
# workaround: https://github.com/hyprwm/hyprlock/issues/825 animation {
animation = fade, 0 animation = fade, 0
# another wierd bug that only happens in hybrid mode :/ }
background { background {
# color = rgba(041011FF) # color = rgba(041011FF)
# color = rgba(000000FF) # color = rgba(000000FF)
# path = {{ SWWW_WALL }} # path = {{ SWWW_WALL }}
path = ~/Pictures/backgrounds/miku-space.jpg path = $background
# blur_size = 5 # blur_size = 5
# blur_passes = 4 # blur_passes = 4
} }
@@ -68,12 +69,12 @@ label { # Greeting
} }
label { # lock icon label { # lock icon
monitor = monitor =
text = lock text = 󰌾
shadow_passes = 1 shadow_passes = 1
shadow_boost = 0.5 shadow_boost = 0.5
color = $text_color color = $text_color
font_size = 21 font_size = 21
font_family = $font_material_symbols font_family = $font_family_nerd
position = 100, 100 position = 100, 100
halign = left halign = left
@@ -88,7 +89,7 @@ label { # "locked" text
font_size = 14 font_size = 14
font_family = $font_family font_family = $font_family
position = 140, 105 position = 140, 100
halign = left halign = left
valign = bottom valign = bottom
} }
+1
View File
@@ -3,6 +3,7 @@ include "config/monitors.kdl"
include "config/styles.kdl" include "config/styles.kdl"
include "config/execs.kdl" include "config/execs.kdl"
include "config/envs.kdl" include "config/envs.kdl"
include "config/prime.kdl"
include "config/rules.kdl" include "config/rules.kdl"
include "config/binds.kdl" include "config/binds.kdl"
include "config/misc.kdl" include "config/misc.kdl"
+1 -1
View File
@@ -36,7 +36,7 @@ binds {
Alt+Space { spawn-sh "pkill -x rofi || rofi -show drun"; } Alt+Space { spawn-sh "pkill -x rofi || rofi -show drun"; }
// Actions // Actions
Mod+V { spawn-sh "pkill -x rofi || rofi-cliphist"; } Mod+V { spawn-sh "pkill -x shorin-cliphist || ghostty -e shorin-cliphist"; }
Mod+Period { spawn-sh "pkill -x rofi || rofi-emoji"; } Mod+Period { spawn-sh "pkill -x rofi || rofi-emoji"; }
Ctrl+Alt+Delete { spawn-sh "pkill -x wlogout || wlogout -p layer-shell"; } Ctrl+Alt+Delete { spawn-sh "pkill -x wlogout || wlogout -p layer-shell"; }
Print { spawn "niri" "msg" "action" "screenshot-screen"; } Print { spawn "niri" "msg" "action" "screenshot-screen"; }
+2 -3
View File
@@ -13,10 +13,9 @@ environment {
// Nvidia // Nvidia
LIBVA_DRIVER_NAME "nvidia" LIBVA_DRIVER_NAME "nvidia"
__GLX_VENDOR_LIBRARY_NAME "nvidia"
NVD_BACKEND "nvidia" NVD_BACKEND "nvidia"
GBM_BACKEND "nvidia-drm"; GBM_BACKEND "nvidia-drm"
WLR_NO_HARDWARE_CURSORS "1"; WLR_NO_HARDWARE_CURSORS "1"
// Fix Swing // Fix Swing
_JAVA_AWT_WM_NONREPARENTING "1" _JAVA_AWT_WM_NONREPARENTING "1"
+1 -1
View File
@@ -1,7 +1,7 @@
screenshot-path "~/Pictures/Screenshots/niri_screenshot_%Y-%m-%d_%H-%M-%S.png" screenshot-path "~/Pictures/Screenshots/niri_screenshot_%Y-%m-%d_%H-%M-%S.png"
debug { debug {
render-drm-device "/dev/dri/renderD129" render-drm-device "/dev/dri/renderD128"
} }
// gestures { // gestures {
@@ -0,0 +1,5 @@
environment {
__NV_PRIME_RENDER_OFFLOAD "1"
__VK_LAYER_NV_optimus "NVIDIA_only"
__GLX_VENDOR_LIBRARY_NAME "nvidia"
}
+43 -37
View File
@@ -11,12 +11,13 @@
# - change-colortheme (from scripts/change-colortheme) # - change-colortheme (from scripts/change-colortheme)
# - flock (usually part of util-linux) # - flock (usually part of util-linux)
set -euo pipefail
trap 'echo $LINENO: $BASH_COMMAND' ERR
# Lock # Lock
exec {LOCK_FD}>/tmp/"$(basename "$0")".lock || { exec {LOCK_FD}>/tmp/"$(basename "$0")".lock
echo "Failed to open lock file"
exit 1
}
flock -n "$LOCK_FD" || { flock -n "$LOCK_FD" || {
echo "Another instance is running. Exiting." echo "Another instance is running. Exiting."
@@ -26,7 +27,7 @@ flock -n "$LOCK_FD" || {
# Open a file selection dialog if no argument is provided # Open a file selection dialog if no argument is provided
if [ -z "$1" ]; then if [ -z "${1-}" ]; then
image=$(zenity --file-selection --title="Open File" --file-filter="*.jpg *.jpeg *.png *.webp *.bmp *.jfif *.tiff *.avif *.heic *.heif") image=$(zenity --file-selection --title="Open File" --file-filter="*.jpg *.jpeg *.png *.webp *.bmp *.jfif *.tiff *.avif *.heic *.heif")
else else
image="$1" image="$1"
@@ -37,8 +38,8 @@ fi
# Obtain screen resolution # Obtain screen resolution
screen_width=$2 screen_width=${2-}
screen_height=$3 screen_height=${3-}
[ -z "$screen_width" ] && { [ -z "$screen_width" ] && {
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
@@ -56,8 +57,9 @@ screen_height=$3
fi fi
} }
[ -z "$screen_width" ] && screen_width=2560 ## Default to 2k
[ -z "$screen_height" ] && screen_height=1440 screen_width=${screen_width:-2560}
screen_height=${screen_height:-1440}
# $HOME/.config/wallpaper-chooser/config.json: # $HOME/.config/wallpaper-chooser/config.json:
# ```json # ```json
@@ -71,37 +73,43 @@ touch "$image" 2>/dev/null || true # ignore errors
# Copy image to local wallpaper directory # Copy image to local wallpaper directory
## Format of current and cached wallpaper
wallpaper_ext="png" wallpaper_ext="png"
## Generate a random name for the current wallpaper
set +o pipefail # SIGPIPE is expected here
random_name=$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 16) random_name=$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 16)
set -o pipefail
## Directory to store current wallpaper
current_dir="$HOME/.local/share/wallpaper/current" current_dir="$HOME/.local/share/wallpaper/current"
## Path to current wallpaper image
wallpaper_image="$current_dir/wallpaper-${random_name}.${wallpaper_ext}" wallpaper_image="$current_dir/wallpaper-${random_name}.${wallpaper_ext}"
mkdir -p "$current_dir" || { mkdir -p "$current_dir"
echo "Could not create directory $current_dir"
exit 1
}
temp_img=$(mktemp --suffix=."$wallpaper_ext") || exit 1 ## Batch copy using a temporary file to avoid incomplete file being used
temp_img=$(mktemp --suffix=."$wallpaper_ext")
trap 'rm -f "$temp_img"' EXIT trap 'rm -f "$temp_img"' EXIT
magick "$image" -resize "${screen_width}x${screen_height}^" -gravity center -extent "${screen_width}x${screen_height}" "$temp_img" || { magick "$image" -resize "${screen_width}x${screen_height}^" -gravity center -extent "${screen_width}x${screen_height}" "$temp_img"
echo "Could not resize and crop image" cp "$temp_img" "$wallpaper_image"
exit 1
} ## Generate hash for caching,
cp "$temp_img" "$wallpaper_image" || exit 1 ## based on content of the source image and resolution of the resized image
hash="$(md5sum "$image" | awk '{print $1}')-${screen_width}x${screen_height}" hash="$(md5sum "$image" | awk '{print $1}')-${screen_width}x${screen_height}"
# Clean up old wallpapers # Clean up old wallpapers in the same directory of current wallpaper.
# Only keep the newly added one so the wallpaper-daemon can pick it up directly
find "$current_dir" -type f -name "wallpaper-*" ! -name "$(basename "$wallpaper_image")" -delete find "$current_dir" -type f -name "wallpaper-*" ! -name "$(basename "$wallpaper_image")" -delete
# Generate blurred wallpaper # Generate blurred wallpaper
## Similarly, store blurred version of current wallpaper in separate directory
blur_dir="$HOME/.local/share/wallpaper/blurred" blur_dir="$HOME/.local/share/wallpaper/blurred"
## Directory to cache blurred wallpapers, so that we don't need to regenerate
## them every time when switching wallpapers. This makes it possible to have
## an auto-played slideshow with blurred wallpapers without noticeable delay.
blur_cache_dir="$HOME/.local/share/wallpaper/blurred-cache" blur_cache_dir="$HOME/.local/share/wallpaper/blurred-cache"
mkdir -p "$blur_dir" "$blur_cache_dir" || { mkdir -p "$blur_dir" "$blur_cache_dir"
echo "Could not create cache directory"
exit 1
}
blurred_image="$blur_dir/blurred-${random_name}.${wallpaper_ext}" blurred_image="$blur_dir/blurred-${random_name}.${wallpaper_ext}"
blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}" blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
@@ -109,6 +117,14 @@ blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
( (
# notify-send -a "change-wallpaper" "Generating Blurred Wallpaper" "This may take a few seconds..." # notify-send -a "change-wallpaper" "Generating Blurred Wallpaper" "This may take a few seconds..."
function apply_blured {
find "$blur_dir" -type f -name "blurred-*" ! -name "$(basename "$blurred_image")" -delete
if [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 >/dev/null 2>/dev/null
fi
notify-send -a "change-wallpaper" "Blurred Wallpaper Applied" "$blurred_image" -i "$blurred_image"
}
### Check if cached blurred image exists ### Check if cached blurred image exists
if [ -f "$blurred_cache_image" ]; then if [ -f "$blurred_cache_image" ]; then
# sleep 1 # Some ugly workaround # sleep 1 # Some ugly workaround
@@ -116,11 +132,7 @@ blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
echo "Could not copy cached blurred image" echo "Could not copy cached blurred image"
# exit 1 # Non-critical error # exit 1 # Non-critical error
else else
find "$blur_dir" -type f -name "blurred-*" ! -name "$(basename "$blurred_image")" -delete apply_blured
if [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 >/dev/null 2>/dev/null
fi
notify-send -a "change-wallpaper" "Blurred Wallpaper From Cache" "$blurred_image" -i "$blurred_image"
exit 0 exit 0
fi fi
fi fi
@@ -133,7 +145,7 @@ blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
printf "%.2f", s printf "%.2f", s
}') }')
### use a temporary file to avoid incomplete file being used ### Batch processing using a temporary file to avoid incomplete file being used
temp_blurred=$(mktemp --suffix=."$wallpaper_ext") || exit 1 temp_blurred=$(mktemp --suffix=."$wallpaper_ext") || exit 1
trap 'rm -f "${temp_blurred}"' EXIT trap 'rm -f "${temp_blurred}"' EXIT
magick "$wallpaper_image" -blur 0x"$sigma" "$temp_blurred" || { magick "$wallpaper_image" -blur 0x"$sigma" "$temp_blurred" || {
@@ -146,18 +158,12 @@ blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
exit 1 exit 1
} }
find "$blur_dir" -type f -name "blurred-*" ! -name "$(basename "$blurred_image")" -delete
cp -f "$blurred_image" "$blurred_cache_image" || { cp -f "$blurred_image" "$blurred_cache_image" || {
echo "Could not cache blurred image" echo "Could not cache blurred image"
# exit 1 # Non-critical error # exit 1 # Non-critical error
} }
if [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then apply_blured
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 >/dev/null 2>/dev/null
fi
notify-send -a "change-wallpaper" "Blurred Wallpaper Generated" "$blurred_image" -i "$blurred_image"
) & ) &
# Apply wallpaper # Apply wallpaper
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
[[ "$XDG_CURRENT_DESKTOP" != "niri" ]] && exit 1
if grep -q 'prefer_order=(nvidia intel)' "$HOME/.local/snippets/set_display"; then
sed -i 's/prefer_order=(nvidia intel)/prefer_order=(intel nvidia)/' "$HOME/.local/snippets/set_display"
echo "" >"$HOME/.config/niri/config/prime.kdl"
echo "Disabled global Nvidia Prime offloading."
else
sed -i 's/prefer_order=(intel nvidia)/prefer_order=(nvidia intel)/' "$HOME/.local/snippets/set_display"
cat >"$HOME/.config/niri/config/prime.kdl" <<EOF
environment {
__NV_PRIME_RENDER_OFFLOAD "1"
__VK_LAYER_NV_optimus "NVIDIA_only"
__GLX_VENDOR_LIBRARY_NAME "nvidia"
}
EOF
echo "Enabled global Nvidia Prime offloading."
fi
# Restart session
printf "Session restart is required to apply changes.\nDo it now? (Y/n): "
read -r answer
if [[ "$answer" =~ ^[Yy]$ || -z "$answer" ]]; then
niri msg action quit
fi
+27 -4
View File
@@ -1,7 +1,30 @@
#!/bin/sh #!/usr/bin/env bash
# Description: # Description:
# Quick snippet for cliphist + rofi + wl-copy # ~~Quick~~ snippet for cliphist + rofi + wl-copy
cliphist list | rofi -dmenu -config ~/.config/rofi/dmenu.rasi -display-columns 2 -i | \ tmp_dir="/tmp/cliphist"
cliphist decode | wl-copy trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir"
read -r -d '' prog <<EOF
/^[0-9]+\s<meta http-equiv=/ { next }
match(\$0, /^([0-9]+)\s(\[\[\s)?binary.*(jpg|jpeg|png|bmp)/, grp) {
system("echo " grp[1] "\\\\\t | cliphist decode >$tmp_dir/"grp[1]"."grp[3])
print \$0"\0icon\x1f$tmp_dir/"grp[1]"."grp[3]
next
}
1
EOF
# Pipeline logic:
# 1. cliphist list: gives "ID <tab> Content"
# 2. gawk: adds icon paths
# 3. rofi -dmenu: shows list, hides column 1 (ID), returns "ID <tab> Content" on select
# 4. cliphist decode: reads ID from line, gets original content
result=$(cliphist list | gawk "$prog" | rofi -dmenu -display-columns 2 -config "$HOME/.config/rofi/dmenu.rasi" -show-icons -p "Clipboard")
if [[ -n "$result" ]]; then
echo "$result" | cliphist decode | wl-copy
fi
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env bash
# shellcheck disable=SC2016
# Credit: https://github.com/SHORiN-KiWATA/shorinclip
#
# MIT License
#
# Copyright (c) 2026 shorinkiwata
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
set -euo pipefail
CACHE_DIR=$(mktemp -d)
export CACHE_DIR
trap 'rm -rf "$CACHE_DIR"' EXIT
# 颜色变量
export C_TERTIARY='\x1b[1;35m'
export C_PRIMARY='\x1b[1;34m'
export C_CYAN='\x1b[1;36m'
export C_RESET='\x1b[0m'
# 过滤无法复制粘贴的信息
filter_clip_list() {
grep -vP "(?s)(?i)\t.*<html.*\[表情\]"
}
export -f filter_clip_list
# 美化列表
format_clip_list() {
sed -E \
-e "s/(\t).*\.(mp4|mkv|webm|avi|mov|flv|wmv)$/\1${C_TERTIARY}[VIDEO]File.\2${C_RESET}/" \
-e "s/(\t)file:\/\/.*\.(mp4|mkv|webm|avi|mov|flv|wmv)$/\1${C_TERTIARY}[VIDEO]Url.\2${C_RESET}/" \
-e "s/(\t).*src=\"file:\/\/.*[qQ][qQ].*/\1${C_PRIMARY}[IMG_HTML]QQ${C_RESET}/" \
-e "s/(\t)file:\/\/.*xwechat.*temp.*/\1${C_PRIMARY}[IMG]WeChat${C_RESET}/" \
-e "s/(\t)file:\/\/.*\.gif$/\1${C_PRIMARY}[IMG]Url.gif${C_RESET}/" \
-e "s/(\t)file:\/\/.*\.(png|jpg|jpeg|webp|bmp)$/\1${C_TERTIARY}[IMG]Url.\2${C_RESET}/" \
-e "s/(\t)file:\/\/.*/\1${C_CYAN}[URL]File${C_RESET}/" \
-e "s/(\t)\/.*\.gif$/\1${C_PRIMARY}[IMG]Path.gif${C_RESET}/" \
-e "s/(\t)\/.*\.(png|jpg|jpeg|webp|bmp)$/\1${C_TERTIARY}[IMG]Path.\2${C_RESET}/" \
-e "s/\[\[ binary data .* (png|jpg|jpeg|gif|webp) .*\]\]/${C_TERTIARY}[IMG]Bin.\1${C_RESET}/" \
-e "s/\[\[ binary data .* \]\]/${C_CYAN}[BINARY]${C_RESET}/"
}
export -f format_clip_list
# 导入fzf前的处理,添加序号
add_num() {
awk -F '\t' '{printf "%s\t\x1b[90m%-2d \x1b[0m%s\n", $1, NR, $2}'
}
export -f add_num
# 复制前的处理,对不同数据采取不同的处理方式
copy_selection() {
#获取fzf选择的项目
local input="$1"
# 获取decode内容
local decoded
decoded=$(echo "$input" | cliphist decode)
# 获取mime类型
local mime
mime=$(echo "$input" | cliphist decode | file -b --mime-type -)
# Python 工具:用于 URL 编码 (处理路径中的中文和空格)
url_encode() {
python -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))" "$1"
}
# 1. 二进制图片 -> 直接复制图片数据
if [[ "$mime" =~ image ]]; then
# 直接复制
echo "$input" | cliphist decode | wl-copy
# 2. QQ HTML -> 提取路径 -> 存入缓存 -> 复制 file:// 缓存路径
elif [ "$mime" = "text/html" ]; then
local qq_src
qq_src=$(echo "$decoded" | grep -oP "^<img src=\"file://\K[^\"]+")
if [ -f "$qq_src" ]; then
# url编码处理
local encoded_path
encoded_path=$(url_encode "$qq_src")
# 拼接头并复制
echo "file://$encoded_path" | wl-copy --type text/uri-list
fi
# 4. file:// 协议路径 -> 直接复制为文件链接
elif [[ "$decoded" == file://* ]]; then
echo "$decoded" | wl-copy --type text/uri-list
# 3. 绝对路径 -> 转 file:// -> 复制为URL链接
elif [[ "$decoded" == /* ]] && [ -e "$decoded" ]; then
local encoded_path
encoded_path=$(url_encode "$decoded")
echo "file://$encoded_path" | wl-copy --type text/uri-list
# 5. 其他 -> 普通文本复制
else
echo "$input" | cliphist decode | wl-copy
fi
}
export -f copy_selection
# 自动刷新机制
FZF_PORT=$(shuf -i 10000-60000 -n 1)
RELOAD_CMD="cliphist list | format_clip_list | add_num"
wl-paste --watch bash -c "curl -s -X POST -d 'reload($RELOAD_CMD)' http://localhost:$FZF_PORT" >/dev/null 2>&1 &
WATCH_PID=$!
trap 'kill $WATCH_PID 2>/dev/null' EXIT
# 读取窗口大小避免窗口过小的时候就打开fzf导致菜单错位
# 50次循环超时
wait_timeout=50
# 循环检测终端的长和宽
while [[ $(tput cols) -lt 35 || $(tput lines) -lt 25 ]]; do
echo "Waiting for terminal resize to at least 35x25..."
sleep 1
# 每循环一次减少超时时间
((wait_timeout--))
[ "$wait_timeout" -eq 0 ] && {
echo "Timeout waiting for terminal resize. Exiting."
exit 1
}
done
#=== FZF 主程序 ===
cliphist list | format_clip_list | add_num | fzf \
--ansi \
--listen "$FZF_PORT" \
--bind "ctrl-r:reload($RELOAD_CMD)" \
--bind "ctrl-x:execute-silent(bash -c 'cliphist delete <<< \"\$1\"' -- {})+reload($RELOAD_CMD)" \
--prompt="󰅍 > " \
--header='CTRL-X: Delete | CTRL-R: Reload | ENTER: Copy' \
--color='header:italic:yellow,prompt:blue,pointer:blue' \
--info=hidden \
--no-sort \
--layout=reverse \
--with-nth 2.. \
--delimiter '\t' \
--preview-window=down:50% \
--preview '
# 获取当期剪贴版项目的id
id=$(echo {} | cut -f1)
content=$(echo {} | cut -f2-)
# 通过decode数据获取mimetype
mimeType=$(echo {} | cliphist decode | file -b --mime-type -)
# 获取文件后缀名
ext=$(echo $mimeType | awk -F"/" "{print \$2}")
# 通过mimetype判断数据/类型
# 如果是二进制图片
if [[ $mimeType =~ image ]]; then
# 生成缓存文件
img_hash=$(echo {} | cliphist decode | md5sum | cut -d" " -f1)
cache_file="$CACHE_DIR/$img_hash.$ext"
echo {} | cliphist decode > "$cache_file"
kitty +kitten icat --clear --image-id=10 --transfer-mode=file \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$cache_file" </dev/tty
# 如果是qq复制出来的html链接
elif [ "$mimeType" = "text/html" ] && echo "$content" | grep -q QQ; then
# 获取html数据中的图片路径
qq_img_file=$(echo {} | cliphist decode | grep -oP "^<img src=\"file://\K[^\"]+")
# 生成缓存文件
#qq_ext="${qq_img_file##*.}"
#qq_img_cache_file=$CACHE_DIR/$id.$qq_ext
#cp $qq_img_file $qq_img_cache_file
# 把路径传给kitty +kitten icat预览
if [ -f "$qq_img_file" ]; then
kitty +kitten icat --transfer-mode=file --clear --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$qq_img_file" </dev/tty
else
echo "$qq_img_file does not exsist."
fi
# 如果是复制文件获取的绝对路径
elif path=$(echo {} | cliphist decode) && [[ "$path" == /* ]]; then
# 获取路径代表的文件的mimetype
path_mime=$(file -b --mime-type "$path")
# 如果是图片的话
if [[ $path_mime =~ image ]]; then
kitty +kitten icat --transfer-mode=file --clear --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$path" </dev/tty
# 如果是视频的话
elif [[ "$path_mime" =~ video ]]; then
# 计算路径哈希值避免重复生成视频缩略图
video_hash=$(echo "$path" | md5sum | cut -d" " -f1)
thumb_file="$CACHE_DIR/$video_hash.png"
# 如果缩略图不存在
if [ ! -f "$thumb_file" ]; then
# 是否安装了缩略图软件
if command -v ffmpegthumbnailer &>/dev/null;then
ffmpegthumbnailer -i "$path" -o "$thumb_file" -s 480 -t 0 >/dev/null 2>&1
else
echo "ffmpegthumbnailer not installed."
fi
fi
# 如果缩略图文件存在且大小不为零
if [ -s "$thumb_file" ]; then
kitty +kitten icat --transfer-mode=file --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@${FZF_PREVIEW_LEFT}x${FZF_PREVIEW_TOP}" \
"$thumb_file" </dev/tty
else
echo "Video: $path (No thumbnail)"
fi
else
echo "$path does not exsist."
fi
# 如果是file:///开头的协议路径
elif decoded=$(echo {} | cliphist decode) && [[ "$decoded" == file://* ]]; then
# 获取文件路径
raw_path="${decoded#file://}"
raw_path=$(echo "$raw_path" | python -c "import sys, urllib.parse; print(urllib.parse.unquote(sys.stdin.read().strip()))")
mime_raw_path=$(file -b --mime-type "$raw_path")
# 如果是图片的话
if [[ $mime_raw_path =~ image ]]; then
kitty +kitten icat --transfer-mode=file --clear --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$raw_path" </dev/tty
# 如果是视频的话
elif [[ $mime_raw_path =~ video ]]; then
video_hash=$(echo "$raw_path" | md5sum | cut -d" " -f1)
raw_thumb_file="$CACHE_DIR/$video_hash.png"
# 如果文件不存在的话
if [ ! -f "$raw_thumb_file" ] ; then
#检测是否安装了ffmpeg缩略图软件
if command -v ffmpegthumbnailer >/dev/null 2>&1; then
ffmpegthumbnailer -i "$raw_path" -o "$raw_thumb_file" -s 480 -t 0 >/dev/null 2>&1
else
echo "ffmpegthumbnailer not installed"
fi
fi
# 如果缩略图文件存在且大小不为零
if [ -s "$raw_thumb_file" ]; then
kitty +kitten icat --transfer-mode=file --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@${FZF_PREVIEW_LEFT}x${FZF_PREVIEW_TOP}" \
"$raw_thumb_file" </dev/tty
else
echo "Video: $raw_path (No thumbnail)"
fi
fi
# 其他数据
else
kitty +kitten icat --clear --silent --transfer-mode=file > /dev/tty 2>/dev/null
# 直接预览
echo "[$mimeType]"
echo ---
echo {} | cliphist decode
fi
' \
--bind "enter:execute-silent(bash -c 'copy_selection \"\$1\"' -- {})+accept"
# 粘贴已经绑定到了fzf的enter键脚本会自动退出必须写在fzf的按键绑定否则wl-copy会导致浮动终端卡住niri下
+53 -53
View File
@@ -9,7 +9,7 @@
# Constants # Constants
niri_config_file="$HOME/.config/niri/config/misc.kdl" niri_config_file="$HOME/.config/niri/config/misc.kdl"
prefer_order=(amd nvidia intel) # AMD -> Nvidia -> Intel prefer_order=(nvidia intel)
# Get vendor and path of each GPU # Get vendor and path of each GPU
default_card_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)" default_card_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)"
@@ -24,49 +24,49 @@ nvidia_render_path=""
amd_render_path="" amd_render_path=""
for link in /dev/dri/by-path/*-card; do for link in /dev/dri/by-path/*-card; do
[[ -e "$link" ]] || continue [[ -e "$link" ]] || continue
card="$(readlink -f "$link")" card="$(readlink -f "$link")"
vfile="/sys/class/drm/$(basename "$card")/device/vendor" vfile="/sys/class/drm/$(basename "$card")/device/vendor"
[[ -r "$vfile" ]] || continue [[ -r "$vfile" ]] || continue
vendor="$(cat "$vfile")" vendor="$(cat "$vfile")"
case "$vendor" in case "$vendor" in
0x10de) nvidia_card_path="$card" ;; 0x10de) nvidia_card_path="$card" ;;
0x8086) intel_card_path="$card" ;; 0x8086) intel_card_path="$card" ;;
0x1002) amd_card_path="$card" ;; 0x1002) amd_card_path="$card" ;;
esac esac
done done
for link in /dev/dri/by-path/*-render; do for link in /dev/dri/by-path/*-render; do
[[ -e "$link" ]] || continue [[ -e "$link" ]] || continue
render="$(readlink -f "$link")" render="$(readlink -f "$link")"
vfile="/sys/class/drm/$(basename "$render")/device/vendor" vfile="/sys/class/drm/$(basename "$render")/device/vendor"
[[ -r "$vfile" ]] || continue [[ -r "$vfile" ]] || continue
vendor="$(cat "$vfile")" vendor="$(cat "$vfile")"
case "$vendor" in case "$vendor" in
0x10de) nvidia_render_path="$render" ;; 0x10de) nvidia_render_path="$render" ;;
0x8086) intel_render_path="$render" ;; 0x8086) intel_render_path="$render" ;;
0x1002) amd_render_path="$render" ;; 0x1002) amd_render_path="$render" ;;
esac esac
done done
# Specify device for brightnessctl # Specify device for brightnessctl
# Only tested on my laptop with Intel iGPU & Nvidia dGPU # Only tested on my laptop with Intel iGPU & Nvidia dGPU
BRIGHTNESSCTL_DEVICE="auto" BRIGHTNESSCTL_DEVICE="auto"
if [[ -n "$intel_card_path" ]]; then if [[ -n "$intel_card_path" ]]; then
BRIGHTNESSCTL_DEVICE="intel_backlight" BRIGHTNESSCTL_DEVICE="intel_backlight"
elif [[ -n "$nvidia_card_path" ]]; then elif [[ -n "$nvidia_card_path" ]]; then
BRIGHTNESSCTL_DEVICE="nvidia_0" BRIGHTNESSCTL_DEVICE="nvidia_0"
fi fi
export BRIGHTNESSCTL_DEVICE export BRIGHTNESSCTL_DEVICE
# AQ_DRM_DEVICES allows multiple entries separated by colon # AQ_DRM_DEVICES allows multiple entries separated by colon
devices="" devices=""
for who in "${prefer_order[@]}"; do for who in "${prefer_order[@]}"; do
case "$who" in case "$who" in
nvidia) [[ -n "$nvidia_card_path" ]] && devices="${devices:+$devices:}$nvidia_card_path" ;; nvidia) [[ -n "$nvidia_card_path" ]] && devices="${devices:+$devices:}$nvidia_card_path" ;;
intel) [[ -n "$intel_card_path" ]] && devices="${devices:+$devices:}$intel_card_path" ;; intel) [[ -n "$intel_card_path" ]] && devices="${devices:+$devices:}$intel_card_path" ;;
amd) [[ -n "$amd_card_path" ]] && devices="${devices:+$devices:}$amd_card_path" ;; amd) [[ -n "$amd_card_path" ]] && devices="${devices:+$devices:}$amd_card_path" ;;
esac esac
done done
HYPR_AQ_DRM_DEVICES="${devices:-$default_card_path}" HYPR_AQ_DRM_DEVICES="${devices:-$default_card_path}"
export HYPR_AQ_DRM_DEVICES export HYPR_AQ_DRM_DEVICES
@@ -74,37 +74,37 @@ export HYPR_AQ_DRM_DEVICES
# But niri only supports choosing one preferred render device # But niri only supports choosing one preferred render device
primary_render_device="$default_render_path" primary_render_device="$default_render_path"
for who in "${prefer_order[@]}"; do for who in "${prefer_order[@]}"; do
case "$who" in case "$who" in
nvidia) [[ -n "$nvidia_render_path" ]] && { nvidia) [[ -n "$nvidia_render_path" ]] && {
primary_render_device="$nvidia_render_path" primary_render_device="$nvidia_render_path"
break break
} ;; } ;;
intel) [[ -n "$intel_render_path" ]] && { intel) [[ -n "$intel_render_path" ]] && {
primary_render_device="$intel_render_path" primary_render_device="$intel_render_path"
break break
} ;; } ;;
amd) [[ -n "$amd_render_path" ]] && { amd) [[ -n "$amd_render_path" ]] && {
primary_render_device="$amd_render_path" primary_render_device="$amd_render_path"
break break
} ;; } ;;
esac esac
done done
# Update niri config # Update niri config
function update_niri_config() { function update_niri_config() {
local config_file="$1" local config_file="$1"
local device_path="$2" local device_path="$2"
[[ -f "$config_file" ]] || return [[ -f "$config_file" ]] || return
if grep -qE '^\s*render-drm-device\s+"[^"]+"' "$config_file"; then if grep -qE '^\s*render-drm-device\s+"[^"]+"' "$config_file"; then
local current local current
current="$(grep -E '^\s*render-drm-device\s+"[^"]+"' "$config_file" | sed -E 's/^\s*render-drm-device\s+"([^"]+)".*/\1/')" current="$(grep -E '^\s*render-drm-device\s+"[^"]+"' "$config_file" | sed -E 's/^\s*render-drm-device\s+"([^"]+)".*/\1/')"
[[ "$current" == "$device_path" ]] && return [[ "$current" == "$device_path" ]] && return
sed -i -E "s|^(\s*render-drm-device\s+)\"[^\"]+\"|\1\"$device_path\"|" "$config_file" sed -i -E "s|^(\s*render-drm-device\s+)\"[^\"]+\"|\1\"$device_path\"|" "$config_file"
else else
printf '\ndebug {\nrender-drm-device "%s"\n}\n' "$device_path" >>"$config_file" printf '\ndebug {\nrender-drm-device "%s"\n}\n' "$device_path" >>"$config_file"
fi fi
} }
update_niri_config "$niri_config_file" "$primary_render_device" update_niri_config "$niri_config_file" "$primary_render_device"
@@ -123,6 +123,15 @@ end
# journalctl # journalctl
alias jctl="journalctl -p 3 -xb" alias jctl="journalctl -p 3 -xb"
# nohup
function nh
nohup $argv >/dev/null 2>&1 & disown
end
# ffmpeg
alias ffmpeg="ffmpeg -hide_banner -nostdin"
alias ffprobe="ffprobe -hide_banner"
# git # git
if type -q git if type -q git
function gcp function gcp
+4 -2
View File
@@ -46,7 +46,7 @@ ExecStart=/usr/bin/kmscon --vt=%I --seats=seat0 --no-switchvt
```conf ```conf
login=/bin/login -p -f kolkas login=/bin/login -p -f kolkas
font-name=MesloLGM Nerd Font Mono, Maple Mono NF CN font-name=Maple Mono NF CN
font-size=14 font-size=14
``` ```
@@ -93,7 +93,9 @@ login=/usr/bin/su - kolkas -s /usr/bin/fish --login
### 关于字体的补充说明 ### 关于字体的补充说明
- kmscon 上 2 字符宽的 nerd font 图标会被裁剪至 1 字符宽。可以使用带有 Mono 后缀的 Meslo 系字体作为首选字体,它的图标字符只有 1 字符宽。 - ~~kmscon 上 2 字符宽的 nerd font 图标会被裁剪至 1 字符宽。可以使用带有 Mono 后缀的 Meslo 系字体作为首选字体,它的图标字符只有 1 字符宽。~~
EDIT: kmscon 9.2.0 版本已修复该问题。
- [Archwiki - KMSCON](https://wiki.archlinux.org/title/KMSCON) 上提供了另一种更改字体的方法,即修改 fontconfig 配置,具体做法为在 monospace 字体族中前置添加字体。但这会影响所有使用 fontconfig 和 monospace 字体族的程序,个人认为并非首选。 - [Archwiki - KMSCON](https://wiki.archlinux.org/title/KMSCON) 上提供了另一种更改字体的方法,即修改 fontconfig 配置,具体做法为在 monospace 字体族中前置添加字体。但这会影响所有使用 fontconfig 和 monospace 字体族的程序,个人认为并非首选。
+51 -19
View File
@@ -14,7 +14,7 @@
- [2.7. Mounting the New Partition](https://www.linuxfromscratch.org/lfs/view/stable/chapter02/mounting.html) - [2.7. Mounting the New Partition](https://www.linuxfromscratch.org/lfs/view/stable/chapter02/mounting.html)
可以用一个小脚本挂载分区,推荐使用文件系统 UUID 而非诸如 `/dev/sdc1` 这样的绝对路径: 可以用一个小脚本挂载分区。对于指定分区的方式,推荐使用文件系统 UUID 而非诸如 `/dev/sdc1` 这样的绝对路径:
```bash ```bash
#!/bin/bash #!/bin/bash
@@ -145,7 +145,20 @@
- [7.4. Entering the Chroot Environment](https://www.linuxfromscratch.org/lfs/view/stable/chapter07/chroot.html) - [7.4. Entering the Chroot Environment](https://www.linuxfromscratch.org/lfs/view/stable/chapter07/chroot.html)
对于 chroot 也可以使用一个小脚本显著地改善体验 如果想要使用 `arch-install-scripts` 提供的 `arch-chroot` 脚本偷懒,则必须确保环境变量的清洗与重新设置,至少应确保 `PATH`, `MAKEFLAGS` 和 `TESTSUITEFLAGS` 被正确设置
```bash
arch-chroot "$LFS" /usr/bin/env -i \
HOME=/root \
TERM="$TERM" \
PS1='(lfs chroot) \u:\w\$ ' \
PATH=/usr/local/bin:/usr/bin:/usr/sbin \
MAKEFLAGS="-j$(nproc)" \
TESTSUITEFLAGS="-j$(nproc)" \
/bin/bash --login
```
或者完全按照 LFS 书中的 chroot 步骤编写一个小脚本:
```bash ```bash
#!/bin/bash #!/bin/bash
@@ -193,10 +206,6 @@
它的作用是自动挂载一系列虚拟文件系统,同时在退出 chroot 时自动清理。 它的作用是自动挂载一系列虚拟文件系统,同时在退出 chroot 时自动清理。
> [!IMPORTANT]
>
> 此处并不建议使用 `arch-install-scripts` 提供的 `arch-chroot` 偷懒,除非明确知道自己在做什么。
- [8.64. GRUB-2.12](https://www.linuxfromscratch.org/lfs/view/stable/chapter08/grub.html) - [8.64. GRUB-2.12](https://www.linuxfromscratch.org/lfs/view/stable/chapter08/grub.html)
对于 UEFI 引导的系统,此时需要跳转 BLFS 安装 GRUB。为避免过早地陷入依赖地狱建议仅按照顺序安装以下包 对于 UEFI 引导的系统,此时需要跳转 BLFS 安装 GRUB。为避免过早地陷入依赖地狱建议仅按照顺序安装以下包
@@ -205,7 +214,7 @@
3. [efibootmgr-18](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/efibootmgr.html) 3. [efibootmgr-18](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/efibootmgr.html)
4. [GRUB-2.12 for EFI](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/grub-efi.html) 4. [GRUB-2.12 for EFI](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/grub-efi.html)
这对于引导系统来说已经足够用了。如果需要的话可以之后再补上文档等其他附加选项 这对于引导系统来说已经足够用了。如果需要的话可以之后再补上文档等其他附加依赖重新构建安装
- [10.2. Creating the /etc/fstab File](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/fstab.html) - [10.2. Creating the /etc/fstab File](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/fstab.html)
@@ -244,7 +253,9 @@
如果内核版本不一致,会在 `make localmodconfig` 时出现很多交互选项,建议全部保持默认,或使用 `make olddefconfig` 来自动处理。 如果内核版本不一致,会在 `make localmodconfig` 时出现很多交互选项,建议全部保持默认,或使用 `make olddefconfig` 来自动处理。
在此之后,仍建议按照 LFS 书中的指示检查和调整配置选项 对此需要特别注意,大多数成熟发行版的内核都是在有 initrd 的前提下配置、编译和使用的,而在 LFS 中直到 BLFS 才会涉及到 initrd。因跨度过大不建议直接从这里跳到 BLFS 中配置 initrd。因此需要针对这点手动做一些调整否则可能无法启动。具体修改方向在之后会提到
在此之后,仍建议(或者说请务必)按照 LFS 书中的指示检查和调整配置选项。
- 参考 Host 的内核配置 - 参考 Host 的内核配置
@@ -260,18 +271,22 @@
- `<*>` or `<M>` - `<*>` or `<M>`
对于大多数选项建议选择模块化M而非内置\*)。这将显著减少内核体积,并且提高灵活性。但有几种情况例外,例如: 对于大多数选项建议选择模块化M而非内置\*)。这将显著减少内核体积,并且提高灵活性。但在**没有配置 initrd**(这将在 BLFS 中涉及),有几种情况例外,例如:
- 引导相关的选项(例如 EFI 支持)必须内置,否则无法引导。 - 存储总线和控制器驱动,如 `CONFIG_BLK_DEV_NVME`
- 一些必要的文件系统(例如 ext4建议内置否则可能无法挂载根文件系统。 - RootFS 所需驱动,如 `CONFIG_EXT4_FS``CONFIG_BTRFS_FS`
- 如果全盘加密,输入密码(显然)需要键盘或其他输入设备的驱动;
- 基础显示驱动,如 `CONFIG_VT``CONFIG_VT_CONSOLE` 等。
也有必须模块化的情况,例如: 总之,在挂载 RootFS 之前需要的,以及挂载 RootFS 需要的驱动需要内置。
- 需要固件支持的驱动程序(如 i915除非使用 initrd 或将固件内置到内核中。
也有在**没有 initrd** 的情况下必须模块化的情况,例如:
- 需要外部固件支持的驱动程序(如 `i915`),除非使用 initrd 或通过 `CONFIG_EXTRA_FIRMWARE` 将固件也内置到内核中。
- [10.4. Using GRUB to Set Up the Boot Process](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/grub.html) - [10.4. Using GRUB to Set Up the Boot Process](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/grub.html)
强烈建议使用 PARTUUID 和 UUID 替代传统的 `/dev/sdXN` 设备路径以及 `(hdM,N)` 来指定 `/boot` 分区和根分区。 强烈建议使用 PARTUUID 和 UUID 替代传统的 `/dev/sdXN` 设备路径以及 `(hdM,N)` 来指定 `/boot` 分区和根分区。
另外,如果将外置存储设备(如 USB 硬盘)作为根文件系统,建议在 GRUB 配置中添加 `rootdelay=10` 或 `rootwait` 参数以防止启动时找不到根文件系统 另外,如果将外置存储设备(如 USB 硬盘)上的分区作为 RootFS ,建议在 GRUB 配置中添加 `rootdelay=10` 或 `rootwait` 参数以防止启动时找不到 RootFS
> [!NOTE] > [!NOTE]
> >
@@ -337,12 +352,13 @@
其中: 其中:
- `-D gallium-drivers=iris,llvmpipe` - `-D gallium-drivers=iris,llvmpipe`
- 不包含 NVIDIA 相关的参数,因为 NVIDIA 专有驱动自带完整的 OpenGL 支持,不需要 Mesa 提供 - 不包含 NVIDIA 相关的参数,因为 NVIDIA 专有驱动自带完整的 OpenGL 实现,不需要 Mesa 提供
- 同时启用 llvmpipe 用于 OpenGL 上下文中的软件渲染以防万一。 - `iris` 用于现代 Intel 显卡。对于较新Gen 8 及更新)的硬件,`crocus`(适用于 Gen 4 到 Gen 7.5)和 `i915`(更老)用户态 OpenGL 驱动已被废弃,不应再使用。注意此处的 `i915` 用户态驱动和内核中的 `i915` 模块是不同的东西;
- `iris` 用于 Intel 显卡。对于较新Gen 8 及更新)的硬件,`crocus`(适用于 Gen 4 到 Gen 7和 `i915`(更老)用户态 OpenGL 驱动已被废弃,不应再使用。注意此处的 i915 和内核中的 i915 内核驱动是不同的东西 - 启用 `llvmpipe` 用于 OpenGL 上下文中的软件渲染以防万一
- `-D vulkan-drivers=intel,swrast` - `-D vulkan-drivers=intel,swrast`
- 启用 Intel iGPU 的 Vulkan 支持。 - 不用管 NVIDIA原因同上
- 同时启用 Vulkan 上下文中的软件光栅化驱动 swrast 以防万一。 - 启用 Intel iGPU 的 Vulkan 支持;
- 启用 Vulkan 上下文中的软件渲染驱动“软件光栅化器” `swrast` 以防万一。注意这里的 `swrast` 实际指 `lavapipe`,和被废弃的 gallium `swrast` 驱动是不同的东西。
- `-D glvnd=enabled`:启用 GLVND 支持以便和 NVIDIA 专有驱动兼容。libglvnd 需要[在 GLFS 书中安装](https://glfs-book.github.io/glfs/shareddeps/libglvnd.html)。 - `-D glvnd=enabled`:启用 GLVND 支持以便和 NVIDIA 专有驱动兼容。libglvnd 需要[在 GLFS 书中安装](https://glfs-book.github.io/glfs/shareddeps/libglvnd.html)。
> 虽然 GLFS 中的 libglvnd 章节在开头处提到了 `If you've come here from the BLFS Mesa page, ...`,但实际上 BLFS 中的 Mesa 章节并没有提到 libglvnd 和除 nouveau 外与 NVIDIA 相关的话题。算个小坑?大概。 > 虽然 GLFS 中的 libglvnd 章节在开头处提到了 `If you've come here from the BLFS Mesa page, ...`,但实际上 BLFS 中的 Mesa 章节并没有提到 libglvnd 和除 nouveau 外与 NVIDIA 相关的话题。算个小坑?大概。
@@ -433,3 +449,19 @@
```bash ```bash
sed -i -E 's|\s*#include "moc_.*|// &|g' qtpositioning/src/plugins/position/geoclue2/qgeopositioninfosourcefactory_geoclue2.cpp qtpositioning/src/plugins/position/geoclue2/qgeopositioninfosource_geoclue2.cpp sed -i -E 's|\s*#include "moc_.*|// &|g' qtpositioning/src/plugins/position/geoclue2/qgeopositioninfosourcefactory_geoclue2.cpp qtpositioning/src/plugins/position/geoclue2/qgeopositioninfosource_geoclue2.cpp
``` ```
## References
- [Welcome - Linux From Scratch](https://www.linuxfromscratch.org/)
- [Linux From Scratch - Version 12.4 ](https://www.linuxfromscratch.org/lfs/view/stable/)
- [Linux From Scratch - 版本 12.4-中文翻译版](https://lfs.xry111.site/zh_CN/12.4/)
- [Beyond Linux® From Scratch (System V Edition) - Version 12.4](https://www.linuxfromscratch.org/blfs/view/stable/)
- [meson.build - Mesa/mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/meson.build)
- [Intel - Gentoo wiki](https://wiki.gentoo.org/wiki/Intel)
- [LFS btw](https://io.uyani.de/s/dg7FbrQefPf8sJq)
+276 -39
View File
@@ -7,10 +7,13 @@
## 目录 ## 目录
- [目录](#目录) - [目录](#目录)
- [要做什么](#要做什么) - [概览](#概览)
- [需要什么](#需要什么) - [一些术语和缩写](#一些术语和缩写)
- [放开那个端口!](#放开那个端口) - [要做什么](#要做什么)
- [注册 SMTP 中继服务](#注册-smtp-中继服务) - [需要什么](#需要什么)
- [前置准备](#前置准备)
- [放开那个端口!](#放开那个端口)
- [注册 SMTP 中继服务](#注册-smtp-中继服务)
- [配置 DNS 和 rDNS](#配置-dns-和-rdns) - [配置 DNS 和 rDNS](#配置-dns-和-rdns)
- [在 DNS 服务商处](#在-dns-服务商处) - [在 DNS 服务商处](#在-dns-服务商处)
- [在云服务器商处](#在云服务器商处) - [在云服务器商处](#在云服务器商处)
@@ -25,17 +28,80 @@
- [启动!](#启动) - [启动!](#启动)
- [Rspamd Web UI](#rspamd-web-ui) - [Rspamd Web UI](#rspamd-web-ui)
- [配置邮件客户端](#配置邮件客户端) - [配置邮件客户端](#配置邮件客户端)
- [exim4: 我呢?](#exim4-我呢) - [Thunderbird](#thunderbird)
- [Catch'em All!](#catchem-all)
- [Extra Notes](#extra-notes) - [Extra Notes](#extra-notes)
- [exim4: 我呢?](#exim4-我呢)
- [Catch'em All!](#catchem-all)
- [MTA-STS](#mta-sts) - [MTA-STS](#mta-sts)
- [DMARC Alignment](#dmarc-alignment)
- [转发到其他域](#转发到其他域)
- [查看报告](#查看报告) - [查看报告](#查看报告)
- [邮件传输链路](#邮件传输链路) - [邮件传输链路](#邮件传输链路)
- [为什么要做自建邮局?](#为什么要做自建邮局) - [备份与恢复](#备份与恢复)
- [测试工具](#测试工具)
- [这一切到底是为什么?](#这一切到底是为什么)
## 要做什么 ## 概览
一个概览. ### 一些术语和缩写
> 看不懂没关系, 遇到了再回来翻就好 :)
- M\*A
| 简写 | 全称 | 中文名称 | 说明 |
| ---- | --------------------- | ------------ | ---------------------------------------------------------- |
| MTA | Mail Transfer Agent | 邮件传输代理 | 负责在邮件服务器之间传输邮件. 例如 Postfix. |
| MDA | Mail Delivery Agent | 邮件投递代理 | 负责将邮件存储到用户邮箱中. 例如 Dovecot. |
| MSA | Mail Submission Agent | 邮件提交代理 | 负责接收来自邮件客户端的邮件并将其传递给 MTA. 例如 Postfix |
| MUA | Mail User Agent | 邮件用户代理 | 即邮件客户端. 例如 Thunderbird |
- 协议
| 简写 | 全称 | 中文名称 | 说明 |
| --------------- | -------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| SMTP | Simple Mail Transfer Protocol | 简单邮件传输协议 | 用于 MTA 之间的服务器通信. 通常运行在 25 端口. |
| SMTP Submission | SMTP Submission | SMTP 提交协议 | 用于 MUA 向 MSA 提交邮件. 通常运行在 587 端口, 支持 `STARTTLS` 加密通信. |
| SMTPS | SMTP over SSL/TLS | 基于 SSL/TLS 的 SMTP | 用于 MUA 提交邮件给 MSA, 使用独立端口(465)进行加密通信, 支持 `Implicit TLS`, 与 `STARTTLS` 不同的是其在连接建立时便立即进行 SSL/TLS 握手, 降低中间人攻击的风险. |
| IMAP | Internet Message Access Protocol | 互联网消息访问协议 | 用于 MUA 从 MDA 拉取邮件. |
| IMAPS | IMAP over SSL/TLS | 基于 SSL/TLS 的 IMAP | 使用独立端口(993)进行加密通信. |
| POP3 | Post Office Protocol version 3 | 邮局协议版本 3 | 另一种用于 MUA 接收邮件的协议, 不同的是一旦某个 MUA 取走邮件, 该邮件会在服务器上删除, 其他 MUA 将不再能收到这封邮件. 本文不会涉及此协议. |
| LMTP | Local Mail Transfer Protocol | 本地邮件传输协议 | 简化的 SMTP, 用于 MTA 在服务器内部将邮件传递给 MDA. |
- 安全策略
| 简写 | 全称 | 中文名称 | 说明 |
| --------------- | --------------------------------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------- |
| rDNS | Reverse DNS | 反向域名解析 | 将 IP 地址映射回域名的特殊 DNS 记录. 多个 ip 可以同时映射到同一个域名, 但一个 ip 只能映射到一个域名. |
| PTR | Pointer Record | 指针记录 | rDNS 使用的 DNS 记录类型. |
| SPF | Sender Policy Framework | 发件人策略框架 | 用于防止伪造发件人的技术. |
| DKIM | DomainKeys Identified Mail | 域名密钥识别邮件 | 用于验证邮件的完整性和真实性的技术. |
| DMARC | Domain-based Message Authentication, Reporting, and Conformance | 基于域的消息认证, 报告和一致性 | 用于指定邮件在目标服务器的处理策略. |
| DMARC Alignment | DMARC Alignment | DMARC 对齐 | 用于确保发件人域名与 SPF 和 DKIM 认证域名一致的策略. |
| MTA-STS | Mail Transfer Agent Strict Transport Security | 邮件传输代理严格传输安全 | 用于防止中间人攻击的技术. |
| TLS-RPT | Transport Layer Security Reporting | 传输层安全报告 | 一方向另一方反馈 TLS 相关报告的方式. |
| SRS | Sender Rewriting Scheme | 发件人重写方案 | 用于确保在邮件转发后保持 SPF 通过的技术, 通过改写 Envelope From 地址实现. |
| ARC | Authenticated Received Chain | 认证接收链 | 用于在邮件转发过程中保持认证信息的技术. |
- 邮件数据
| 名称 | 中文名称 | 说明 |
| -------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Envelope From | 信封发件人 | 邮件传输过程中使用的发件人地址. 通常在接受到的邮件头部中以 `Return-Path` 字段显示. |
| (Header) From | 邮件头部发件人 | 邮件中显示的发件人地址, 也是在邮件客户端中能清晰看到的最醒目的发件人地址. |
| Return-Path | 退信路径 | 邮件头部中的一个字段, 用于指定邮件的回执地址, 通常由接受方 MTA 根据 Envelope sender 生成. |
| DKIM-Signature | DKIM 签名 | 邮件头部中的一个字段, 包含用于验证邮件完整性和真实性的签名信息. |
| Content-Type | 内容类型 | 邮件头部中的一个字段, 用于指定邮件内容的格式和编码方式. 文本为主的邮件通常使用 `text/plain; charset="UTF-8"``text/html; charset="UTF-8"`, 二者在收件方的垃圾邮件过滤策略中可能会有不同的检测标准和权重. |
- 反垃圾与声誉
| 简写 | 全称 | 中文名称 | 说明 |
| ---- | ------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------- |
| RBL | Real-time Blackhole List | 实时黑洞列表 | 由第三方维护的黑名单数据库. |
| - | Greylisting | 灰名单 | 一种反垃圾邮件技术, 通过拒绝第一次发送的邮件并要求重试来过滤垃圾邮件. |
| BIMI | Brand Indicators for Message Identification | 邮件识别的品牌指示符 | 允许在收件人的列表页显示自定义品牌 Logo 的标准. ~~因为我没有品牌也没有 Logo 所以~~本文不会涉及. |
### 要做什么
1. 在自己的服务器上配置邮件服务器, 直接接收邮件; 1. 在自己的服务器上配置邮件服务器, 直接接收邮件;
@@ -61,13 +127,13 @@
> >
> - IP 长期保活的持久战准备 > - IP 长期保活的持久战准备
> >
> 及时在上面一点中成功培养了良好的初始声誉, 如果后续不进行长期维护仍然可能会回到垃圾箱. 这不是短期努力可以解决的, 需要长期坚持. > 上面一点一样也是黑箱. 即使有良好的初始声誉, 如果后续不进行长期维护仍然可能会回到垃圾箱. 这不是短期努力可以解决的, 需要长期坚持.
> >
> 如果确实觉得问题, 那么可以忽略下文中所有有关 SMTP 中继相关的内容. 其实就本文包含的步骤而言区别并没有很大, 下文中相关的地方会以引用的形式进行标注. > 如果确实觉得以上这些都不算问题, 那么可以忽略下文中所有有关 SMTP 中继相关的内容. 其实就本文包含的步骤而言区别并没有很大, 下文中相关的地方会以引用的形式进行标注.
3. 配置 SPF/DKIM/DMARC/MTA-STS 等等. 3. 配置 SPF/DKIM/DMARC/MTA-STS 等等.
## 需要什么 ### 需要什么
1. 一个中意的域名. 1. 一个中意的域名.
@@ -99,7 +165,13 @@
下文中将使用 `1.14.5.14` 作为服务器的公网 IP 地址. 下文中将使用 `1.14.5.14` 作为服务器的公网 IP 地址.
## 放开那个端口! > [!IMPORTANT]
>
> 在网络工程和状态防火墙的语境中, 当提到"封锁端口 X 的出站流量"时, 通常指的是"禁止从本地服务器发起到外部服务器的连接请求, 目标端口为 X". 相反, "封锁端口 X 的入站流量"则意味着"禁止外部服务器发起到本地服务器的连接请求, 目标端口为 X". 策略中用于识别流量的是目标端口, 而非源端口. 例如即使"25 端口的出站流量被封", 如果强行用 25 端口连接外部服务器的其他开放端口如 80, 这种连接请求仍然是允许的. 当然策略中也可以通过源端口进行过滤, 不过这么做通常意义不大.
## 前置准备
### 放开那个端口!
1. 检测 25 端口是否真的开放: 1. 检测 25 端口是否真的开放:
@@ -145,7 +217,7 @@
如果确实需要系统内部通信, 例如 `cron` 发送邮件通知, 可以安装 `ssmtp` 或 `msmtp` 之类的轻量级 MTA, 参见 [后续章节](#exim4-我呢). 如果确实需要系统内部通信, 例如 `cron` 发送邮件通知, 可以安装 `ssmtp` 或 `msmtp` 之类的轻量级 MTA, 参见 [后续章节](#exim4-我呢).
## 注册 SMTP 中继服务 ### 注册 SMTP 中继服务
> 如果选择直接发信不使用 SMTP 中继服务, 则**跳过**本节内容. > 如果选择直接发信不使用 SMTP 中继服务, 则**跳过**本节内容.
@@ -181,6 +253,12 @@
> [!TIP] > [!TIP]
> >
> 如果乐意的话可以把收信域名, 发信域名, 乃至退信域名等等都拆分开来配置, 但这超过了本文的讨论范围且配置大同小异, 因此不做另外说明 ~~主要是懒~~ :) > 如果乐意的话可以把收信域名, 发信域名, 乃至退信域名等等都拆分开来配置, 但这超过了本文的讨论范围且配置大同小异, 因此不做另外说明 ~~主要是懒~~ :)
>
> 在使用 SMTP 中继服务的情况下, 这个域名只作为收信服务器存在, 发信时使用的域名通常是 SMTP 服务商提供的子域名.
> [!NOTE]
>
> 优先级数值越小, 优先级越高. 如果同一个域名下有多个 MX 记录, 那么邮件服务器会优先选择优先级最高的记录进行连接.
- A 记录: - A 记录:
- 主机名: `mail` - 主机名: `mail`
@@ -192,7 +270,7 @@
### 在云服务器商处 ### 在云服务器商处
将服务器 ip 的 rDNS 设置为 `mail.domain.tld`. 虽然未来主要使用 Resend 发信, 但是收信时一些发信方也可能会检查 rDNS, 因此最好设置正确, 有备无患. 将服务器 ip 的 rDNS 设置为 `mail.domain.tld`. 这对于使用 SMTP 中继服务的场景来说几乎毫无作用, 但是收信时一些发信方也可能会检查 rDNS, 因此最好设置正确, 有备无患.
> 如果选择直接发信不使用 SMTP 中继服务, 则**必须**配置 rDNS > 如果选择直接发信不使用 SMTP 中继服务, 则**必须**配置 rDNS
@@ -234,9 +312,9 @@ services:
ports: ports:
- '25:25' # SMTP - '25:25' # SMTP
- '143:143' # IMAP - '143:143' # IMAP
- '993:993' # IMAPS
- '587:587' # STARTTLS - '587:587' # STARTTLS
- '465:465' # SMTPS - '465:465' # SMTPS
- '993:993' # IMAPS
- '127.0.0.1:11334:11334' # Raspamd Web UI - '127.0.0.1:11334:11334' # Raspamd Web UI
environment: environment:
- DMS_DEBUG=0 - DMS_DEBUG=0
@@ -256,6 +334,8 @@ services:
- SPOOF_PROTECTION=1 - SPOOF_PROTECTION=1
# 启用 MTA-STS # 启用 MTA-STS
# - ENABLE_MTM_STS=1 # - ENABLE_MTM_STS=1
# 启用 SRS
# - ENABLE_SRS=1
# 使用自定义证书 # 使用自定义证书
- SSL_TYPE=manual - SSL_TYPE=manual
# 与下方挂载路径对应 # 与下方挂载路径对应
@@ -295,10 +375,9 @@ services:
- `ENABLE_RSPAMD=0` - `ENABLE_RSPAMD=0`
- `RSPAMD_LEARN=0` - `RSPAMD_LEARN=0`
- 如果垃圾邮件实在太多: - `RSPAMD_GREYLISTING=0`
- `RSPAMD_GREYLISTING=1`
在第一次接受陌生人邮件时拒绝, 要求对方重试, 这样可以有效减少垃圾邮件, 但会显著增加延迟. 启用后, 在第一次接受陌生人邮件时拒绝, 要求对方重试, 这样可以有效减少垃圾邮件, 但会显著增加延迟.
- `ENABLE_MTA_STS=1`: - `ENABLE_MTA_STS=1`:
@@ -316,6 +395,10 @@ services:
如果服务器的 IPv6 配置不完善, 可以强制 Postfix 仅使用 IPv4. 反之也可以只支持 IPv6. 如果服务器的 IPv6 配置不完善, 可以强制 Postfix 仅使用 IPv4. 反之也可以只支持 IPv6.
- `ENABLE_SRS=1`:
启用 SRS 用于转发场景, 详见 [后续章节](#转发到其他域).
> 如果选择直接发信不使用 SMTP 中继服务, **必须**删除以下环境变量: > 如果选择直接发信不使用 SMTP 中继服务, **必须**删除以下环境变量:
> >
> - `DEFAULT_RELAY_HOST=[smtp.resend.com]:587` > - `DEFAULT_RELAY_HOST=[smtp.resend.com]:587`
@@ -344,17 +427,21 @@ docker exec -it mailserver setup email add me@domain.tld <密码>
### 配置 SPF ### 配置 SPF
SPF 记录用于指定哪些服务器被允许代表该域名发送邮件. SPF 记录用于指定哪些服务器被允许代表该域名发送邮件. 它会检查信件 "Envelope sender" (也就是 `Return-Path` 头部) 中的域名是否与对应域名的 SPF 记录匹配, 从而防止伪造发件人.
> [!IMPORTANT]
>
> 对于使用 SMTP 中继服务的场景, SPF 记录仍然是**必须的**. 即使正常发信时收件方检查的是子域名如 `send.domain.tld` 的 SPF 记录, 但设置根域名 `domain.tld` 的 SPF 记录仍然是良好的实践, 可以防止伪造发件人.
在 DNS 服务商处添加以下记录: 在 DNS 服务商处添加以下记录:
- SPF 记录 (TXT 记录): - SPF 记录 (TXT 记录):
- 主机名: `@` - 主机名: `@`
- 值: `v=spf1 include:resend.com -all` (假设使用 Resend 作为 SMTP 服务商) - 值: `v=spf1 -all` (假设使用 Resend 作为 SMTP 服务商)
解释: 解释:
- `v=spf1`: 指定 SPF 版本. - `v=spf1`: 指定 SPF 版本.
- `include:resend.com`: 仅允许 Resend 的服务器以此域名的名义发送邮件. - `include:resend.com`: 仅允许 Resend 的服务器以此域名的名义发送邮件. 因为 Resend 并不会通过根域名发送邮件, 所以这条记录严格来说可以省略.
- `-all`: 硬失败, 未授权的服务器发送邮件时拒绝. 因为发行渠道只有 Resend, 所以这样设置是合理的. 如果希望软失败, 即接受但标记为可疑, 可以使用 `~all`. - `-all`: 硬失败, 未授权的服务器发送邮件时拒绝. 因为发行渠道只有 Resend, 所以这样设置是合理的. 如果希望软失败, 即接受但标记为可疑, 可以使用 `~all`.
> 如果选择直接发信不使用 SMTP 中继服务, 则**需要**将 `include:resend.com` 替换为自己的邮件服务器的 IP 地址或域名, 例如: > 如果选择直接发信不使用 SMTP 中继服务, 则**需要**将 `include:resend.com` 替换为自己的邮件服务器的 IP 地址或域名, 例如:
@@ -365,7 +452,11 @@ SPF 记录用于指定哪些服务器被允许代表该域名发送邮件.
### 配置 DKIM ### 配置 DKIM
DKIM (DomainKeys Identified Mail) 用于验证邮件的完整性和真实性. 对于使用 SMTP 中继服务的场景, DKIM 通常由服务商负责配置和签署, 但仍推荐在自建服务器侧进行签名以保证邮件从源头开始的完整性. DKIM (DomainKeys Identified Mail) 用于验证邮件的完整性和真实性.
> [!NOTE]
>
> 对于使用 SMTP 中继服务的场景, DKIM 通常由服务商负责配置和签署, 本地 DKIM 在中继场景下不一定会被最终保留或使用, 其价值更多在于内部一致性或未来切换为直连发信的准备.
> 如果选择直接发信而非使用 SMTP 中继服务, 则本节内容是**必须的**. > 如果选择直接发信而非使用 SMTP 中继服务, 则本节内容是**必须的**.
@@ -395,7 +486,7 @@ DKIM (DomainKeys Identified Mail) 用于验证邮件的完整性和真实性.
### 配置 DMARC ### 配置 DMARC
DMARC 记录用于指定邮件接收方如何处理未通过 SPF 或 DKIM 检查的邮件. DMARC (Domain-based Message Authentication, Reporting, and Conformance) 用于指定邮件的处理策略. 更多有关 DMARC Alignment 的内容参见 [后续章节](#dmarc-alignment).
在 DNS 服务商处添加以下记录: 在 DNS 服务商处添加以下记录:
@@ -459,7 +550,9 @@ docker compose up -d
## 配置邮件客户端 ## 配置邮件客户端
我并非 TUI 重度爱好者, 日常用 Thunderbird 当客户端. 这里只涉及这一种客户端的配置方法, 当然其他的也大同小异. 我并非 TUI 重度爱好者, 日常用 Thunderbird 当客户端, 因此这里只涉及这一种客户端的配置方法, 当然其他的也大同小异.
### Thunderbird
1. 在添加邮箱的第一个页面, 点击 `MANUAL CONFIGURATION`. 1. 在添加邮箱的第一个页面, 点击 `MANUAL CONFIGURATION`.
@@ -490,7 +583,11 @@ docker compose up -d
现在已经可以试着和其他邮箱互发邮件了! 现在已经可以试着和其他邮箱互发邮件了!
## exim4: 我呢? ## Extra Notes
本节包含一些额外的说明和可选配置.
### exim4: 我呢?
这个, 不需要了. 既然已经有了邮箱服务, 那么继续使用重量级的 `exim4` 就没什么意义了. 这个, 不需要了. 既然已经有了邮箱服务, 那么继续使用重量级的 `exim4` 就没什么意义了.
@@ -570,7 +667,7 @@ docker compose up -d
> >
> 此时可将 msmtp 看作 mailserver 的客户端, 因此 `/etc/msmtprc` 中配置的邮件服务器并不一定要部署在本机, 甚至可以是公共邮箱服务. > 此时可将 msmtp 看作 mailserver 的客户端, 因此 `/etc/msmtprc` 中配置的邮件服务器并不一定要部署在本机, 甚至可以是公共邮箱服务.
## Catch'em All! ### Catch'em All!
如果希望接收发往不存在邮箱地址的邮件, 可以启用 Catch-all 功能. 方法也很简单, 使用 alias 即可: 如果希望接收发往不存在邮箱地址的邮件, 可以启用 Catch-all 功能. 方法也很简单, 使用 alias 即可:
@@ -584,8 +681,6 @@ docker exec -it mailserver setup alias add @domain.tld me@domain.tld
> >
> 完全按照上述步骤配置通配符邮箱别名可能会导致后续添加其他邮箱时邮件仍然发到上面指定的 catch-all 邮箱而不是新创建的邮箱. 更好的实践是用到什么邮箱名再创建对应的别名. > 完全按照上述步骤配置通配符邮箱别名可能会导致后续添加其他邮箱时邮件仍然发到上面指定的 catch-all 邮箱而不是新创建的邮箱. 更好的实践是用到什么邮箱名再创建对应的别名.
## Extra Notes
### MTA-STS ### MTA-STS
**MTA-STS** (Mail Transfer Agent Strict Transport Security) 通过强制要求发送方使用加密连接发送邮件防止中间人攻击, 对个人邮箱来讲~~看起来其实没啥大用但总归~~是个加分项, 并且确实会让邮箱服务变得更酷. **MTA-STS** (Mail Transfer Agent Strict Transport Security) 通过强制要求发送方使用加密连接发送邮件防止中间人攻击, 对个人邮箱来讲~~看起来其实没啥大用但总归~~是个加分项, 并且确实会让邮箱服务变得更酷.
@@ -655,20 +750,136 @@ docker exec -it mailserver setup alias add @domain.tld me@domain.tld
- 从支持 MTA-STS 的邮箱服务商 (例如 Gmail) 发送测试邮件, 查看 Postfix 日志来验证入站时 TLS 握手等流程是否符合预期. 至于 MTA-STS 是否真的生效, 可以稍后查看 TLS-RPT 报告邮箱收到的相关报告来确认. - 从支持 MTA-STS 的邮箱服务商 (例如 Gmail) 发送测试邮件, 查看 Postfix 日志来验证入站时 TLS 握手等流程是否符合预期. 至于 MTA-STS 是否真的生效, 可以稍后查看 TLS-RPT 报告邮箱收到的相关报告来确认.
### DMARC Alignment
这是一种收信方采取的安全策略, 用于确保发件人的身份与邮件头部的 `From` 地址一致.
- 两种模式
1. Relaxed (默认)
只要域名相同或子域名关系即可通过.
2. Strict
必须完全相同才可通过.
可以在 DMARC 记录中通过 `adkim` 和 `aspf` 标签指定 DKIM 和 SPF 的 Alignment 模式, 例如:
```plain
v=DMARC1; p=none; adkim=r; aspf=r;
```
指定了 DKIM 和 SPF 都使用 Relaxed 模式.
使用 SMTP 中继服务时, **必须**使用 Relaxed 模式, 原因后续会说明.
- 两个步骤
1. SPF Alignment
匹配 `Return-Path` 地址的域名和 `From` 头部的域名是否相同或子域名关系.
2. DKIM Alignment
匹配 DKIM 签名中 `d=` 标签的域名和 `From` 头部的域名是否相同或子域名关系.
二者只要有一个通过即可满足 DMARC Alignment 要求.
- SMTP 中继发信时如何工作
- 对于 SPF Alignment
以 Resend 为例, 其发出的邮件中 `Return-Path` 地址虽然会被改写, 但会被改写为 `<很长一串>@send.domain.tld`, 其域名属于 `domain.tld` 的子域名, 因此在 Relaxed 模式下可以通过 SPF Alignment 检查.
- 对于 DKIM Alignment
以 Resend 为例, 在其让添加进 DNS 的记录中有一条便是选择器为 `resend` 的 DKIM 记录, resend 会使用该密钥对发出的邮件进行签名, 签名中的 `d=` 标签会被设置为 `domain.tld`, 因此可以通过 DKIM Alignment 检查.
### 转发到其他域
> [!CAUTION]
>
> 大多数 SMTP 中继服务采用严格的发件人核验制度, 即发件人的域名必须是经过验证和配置的域名. 因此如果使用类似的服务发信, 那么自动转发到其他域名很有可能是不可能的事情.
>
> 因此本节内容更适合自建邮件服务器并直接发信的场景, 对于没有强商业目的的个人邮箱来说意义不大.
- SRS (Sender Rewriting Scheme)
用于在转发邮件时重写发件人地址, 改写 envelope sender 为自己域名下的地址, 从而避免 SPF 检查失败的问题.
对于 docker-mailserver, 只需要在 `compose.yaml` 中添加 `ENABLE_SRS=1` 环境变量即可. 其他相关配置参见 [官方文档](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#srs-sender-rewriting-scheme).
> 在此处我遇到了一个极其诡异的情况, 简单提一嘴:
>
> - What
>
> 启用 SRS 后, postfix 迟迟无法启动, postsrsd 长时间以 CPU 满负荷持续运行.
>
> - Why
>
> 如果使用 strace 跟踪 postsrsd 进程, 会发现它在不停地尝试关闭一些文件描述符, 类似这样:
>
> ```plain
> close(192479922) = -1 EBADF (Bad file descriptor)
> ```
>
> 原因是 postsrsd 在启动时,为了确保环境干净,会执行"关闭从 3 到 MAX 的所有文件描述符"的操作, 而 MAX 的值通过 `sysconf(_SC_OPEN_MAX)` 或 `getrlimit(RLIMIT_NOFILE)` 获取. 在 Docker 容器中, 这个值可能会非常大, 导致 postsrsd 花费大量时间尝试关闭这些不存在的文件描述符.
>
> - How
>
> 通过在 `compose.yaml` 中添加 `ulimits` 配置, 将 `nofile` 的软限制和硬限制都设置为一个合理的值, 例如 65535:
>
> ```yaml
> services:
> mailserver:
> ...
> ulimits:
> nofile:
> soft: 65535
> hard: 65535
> ```
>
> 之后重建容器即可.
- ARC (Authenticated Received Chain)
用于在邮件转发过程中保留并传递上游服务器对 SPF / DKIM / DMARC 的认证结果, 从而帮助下游服务器在 DMARC 检查失败时判断邮件是否原本是可信的.
- 为什么需要 ARC
SRS 会改写 Return-Path, 在 From 域不属于转发域的情况下这会破坏 SPF 对齐. 如果同时一些受 DKIM 签名中 `h=` 标签保护的 Header (在不知情的情况下)调整顺序/增/删/改, 或邮件内容改变, 则会导致 DKIM 签名失效, 从而导致 DMARC 检查失败. 此时如果启用了 ARC, 则可以通过 ARC 链来证明邮件的真实性.
但同时, ARC 工作的前提条件是转发服务器受目标服务器信任. 这又是一个黑盒, 涉及漫长和玄学的信誉培养过程.
- 如何启用
对于启用 Rspamd 的 docker-mailserver, 需要在 `config/rspamd/override.d/arc.conf` 中添加如下内容:
```plain
sign_local = true;
sign_authenticated = true;
allow_env_sender_mismatch = true;
allow_hdr_from_mismatch = true;
domain {
domain.tld {
path = "/path/to/dkim/private/key.private.txt";
selector = "mail";
}
}
```
替换 `domain.tld` 和 `path` 以及 `selector` 为真实值. Rspamd 中, ARC 通常使用和 DKIM 相同的密钥对邮件进行签名.
`allow_env_sender_mismatch` 和 `allow_hdr_from_mismatch` 用于在转发导致的发件人域名与当前服务器域名不一致时仍然进行签名.
详细配置参见 [Rspamd 文档](https://docs.rspamd.com/modules/arc/).
### 查看报告 ### 查看报告
上述配置中的 DMARC 和 TLS-RPT 都会发送报告到指定邮箱. 可以定期查看这些报告以了解邮件传输的安全状况和潜在问题, 推荐部署 [Parsedmarc](https://github.com/domainaware/parsedmarc) 或类似工具进行自动化处理. 上述配置中的 DMARC 和 TLS-RPT 都会发送报告到指定邮箱. 可以定期查看这些报告以了解邮件传输的安全状况和潜在问题, 推荐部署 [Parsedmarc](https://github.com/domainaware/parsedmarc) 或类似工具进行自动化处理.
### 邮件传输链路 ### 邮件传输链路
> 一些术语简写: > 一些关于服务器位置描述的解释:
>
> - `MTA`: Mail Transfer Agent, 邮件传输代理, 负责在邮件服务器之间传输邮件. 例如 Postfix.
> - `MDA`: Mail Delivery Agent, 邮件投递代理, 负责将邮件存储到用户邮箱中. 例如 Dovecot.
> - `MSA`: Mail Submission Agent, 邮件提交代理, 负责接收来自邮件客户端的邮件并将其传递给 MTA. 例如 Postfix.
> - `MUA`: Mail User Agent, 邮件用户代理, 即邮件客户端. 例如 Thunderbird.
>
> 以及位置:
> >
> - `Local`: 自建邮件服务器. > - `Local`: 自建邮件服务器.
> - `Source`: 外部邮件服务器, 发送方. > - `Source`: 外部邮件服务器, 发送方.
@@ -731,13 +942,39 @@ docker exec -it mailserver setup alias add @domain.tld me@domain.tld
- 目标端口: 25. - 目标端口: 25.
- 说明: 这一步由中继商完成. 收件人会看到类似"由 xxx@send.domain.tld 代发" 的信息. - 说明: 这一步由中继商完成. 部分客户端可能会显示代发信息, 具体取决于中继实现与客户端策略.
### 备份与恢复
> [!CAUTION]
>
> 邮件数据一旦丢失几乎不可恢复, 务必定期离机备份.
本文采用 docker 部署邮件服务器, 容器本身是无状态的, 因此只需要备份挂载的卷即可, 不做过多赘述.
唯一需要特殊说明的是务必保留文件权限和属主信息, 否则恢复后邮件服务器可能无法正常工作.
### 测试工具
一些上文中提到或者没提到的在线测试工具:
- [Mail-Tester](https://www.mail-tester.com): 用于测试邮件是否容易被判定为垃圾邮件.
- [MailGenius](https://www.mailgenius.com/): 比 Mail-Tester 更全面和严格的测试工具.
- [Hardenize](https://www.hardenize.com/): 用于测试域名的安全配置, 其中也包括 DMARC, MTA-STS 等邮件服务器相关的配置.
- [MXToolbox](https://mxtoolbox.com/): 提供多种邮件相关的测试工具, 包括黑名单检查, SMTP 测试等等.
- [GMass](https://www.gmass.co/inbox): 提供了 15 个 Gmail 邮箱用于测试邮件送达率.
- [Google Postmaster Tools](https://postmaster.google.com/v2/sender_compliance): 如果主要收件方是 Gmail 且选择自己发信而不使用 SMTP 中继服务, 可以注册并使用该工具来监控邮件送达情况和声誉. 需要注意的是即使"Compliance status"中全部项为绿色, 也不代表邮件一定不会被扔进垃圾箱, 仅供参考.
--- ---
> What, Why, How. 以上是 What 和 How, 接下来是... > What, Why, How. 以上是 What 和 How, 接下来是...
## 为什么要做自建邮局? ## 这一切到底是为什么?
排除利益驱动, 我能想到的最合适的借口就是"隐私". 可是如果登陆 Resend 的后台看一眼, 就会发现我写的邮件被一封封明晃晃地不加掩饰地放在那里, 所有的内容都以明文的方式被看得一清二楚. 此时和使用公共邮箱服务唯一的区别似乎也就只剩"有一个很酷的后缀"这一点了. 即便真的解决了 25 端口问题 (是的, 写完上述内容的几天后我确实做到了) 从而得以摆脱 SMTP 中继服务, 在惊讶于明明 Mail-Tester 给出了 10/10 的满分评价但还是被 Gmail 扔进垃圾箱的残酷现实之余, 我也意识到自建邮局这条路仅靠热情是绝对走不通的. 一是预热 IP 需要花费的时间成本乃至金钱成本远超我的想象, 二是无论如何邮件内容也会被收件方的平台以算法评估一遍的事实彻底击碎了对于"隐私"乌托邦最后的妄想. 即使有这么多复杂的安全措施, 补齐了传输过程中的每一个可能的安全漏洞, 但真正和我点对点沟通的从来都只是靠着一条条既定规则维系的平台, 而不是我在写下收件地址时心中所想的一个个鲜活的人. 排除利益驱动, 我能想到的最合适的借口就是"隐私". 可是如果登陆 Resend 的后台看一眼, 就会发现我写的邮件被一封封明晃晃地不加掩饰地放在那里, 所有的内容都以明文的方式被看得一清二楚. 此时和使用公共邮箱服务唯一的区别似乎也就只剩"有一个很酷的后缀"这一点了. 即便真的解决了 25 端口问题 (是的, 写完上述内容的几天后我确实做到了) 从而得以摆脱 SMTP 中继服务, 在惊讶于明明 Mail-Tester 给出了 10/10 的满分评价但还是被 Gmail 扔进垃圾箱的残酷现实之余, 我也意识到自建邮局这条路仅靠热情是绝对走不通的. 一是预热 IP 需要花费的时间成本乃至金钱成本远超我的想象, 二是无论如何邮件内容也会被收件方的平台以算法评估一遍的事实彻底击碎了对于"隐私"乌托邦最后的妄想. 即使有这么多复杂的安全措施, 补齐了传输过程中的每一个可能的安全漏洞, 但真正和我点对点沟通的从来都只是靠着一条条既定规则维系的平台, 而不是我在写下收件地址时心中所想的一个个鲜活的人.