Compare commits
10 Commits
eb50aec6dc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 58fd5c4d50 | |||
| 3837b42437 | |||
| dce89a0380 | |||
| 483b0bbb8c | |||
| b9ed4072f2 | |||
| 4f384a2c79 | |||
| cc21b2b1dc | |||
| 1d070f1f05 | |||
| 6600f2e9c8 | |||
| 67869d7316 |
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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,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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Executable
+29
@@ -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
|
||||||
@@ -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
@@ -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下)。
|
||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Submodule config/wallpaper/Pictures/backgrounds updated: abde275209...2382384128
+4
-2
@@ -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
-28
@@ -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
|
||||||
@@ -124,9 +124,7 @@
|
|||||||
它的作用是解压一个只含有一个顶层目录的 tarball,cd 进入解压后得到的目录,生成一个 shell,并在这个 shell 退出时清理先前解压得到的文件。
|
它的作用是解压一个只含有一个顶层目录的 tarball,cd 进入解压后得到的目录,生成一个 shell,并在这个 shell 退出时清理先前解压得到的文件。
|
||||||
|
|
||||||
另外还有对于在 LFS 与 BLFS 中编译包的一些通用建议:
|
另外还有对于在 LFS 与 BLFS 中编译包的一些通用建议:
|
||||||
|
|
||||||
1. 通常来说,应该(或者说请务必)在编译和安装一个包后完全删除它的目录,仅有少数例外:
|
1. 通常来说,应该(或者说请务必)在编译和安装一个包后完全删除它的目录,仅有少数例外:
|
||||||
|
|
||||||
- linux(保留构建树可以缩短重新构建耗时,或至少保留 config 便于复原配置)
|
- linux(保留构建树可以缩短重新构建耗时,或至少保留 config 便于复原配置)
|
||||||
- blfs-bootscripts(在 BLFS 中)
|
- blfs-bootscripts(在 BLFS 中)
|
||||||
|
|
||||||
@@ -135,7 +133,6 @@
|
|||||||
2. 通常来说,建议将所有相关的 tarball 和 patch 下载在同一个目录中,并且将 tarball 也解压在这个目录中。如果目录层级与该默认情况不一致的话必须修改 LFS 书中提供的命令中对应的相对路径。
|
2. 通常来说,建议将所有相关的 tarball 和 patch 下载在同一个目录中,并且将 tarball 也解压在这个目录中。如果目录层级与该默认情况不一致的话必须修改 LFS 书中提供的命令中对应的相对路径。
|
||||||
|
|
||||||
3. 如果和我一样使用 UEFI 引导,那么大概率将会在 [8.64. GRUB-2.12](https://www.linuxfromscratch.org/lfs/view/stable/chapter08/grub.html) 第一次接触 BLFS。和 LFS 不同,BLFS 中大多数包都是可选的,一个包可能会依赖其他包,这些依赖分为三个层级:
|
3. 如果和我一样使用 UEFI 引导,那么大概率将会在 [8.64. GRUB-2.12](https://www.linuxfromscratch.org/lfs/view/stable/chapter08/grub.html) 第一次接触 BLFS。和 LFS 不同,BLFS 中大多数包都是可选的,一个包可能会依赖其他包,这些依赖分为三个层级:
|
||||||
|
|
||||||
- Required
|
- Required
|
||||||
- Recommended
|
- Recommended
|
||||||
- Optional
|
- Optional
|
||||||
@@ -148,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
|
||||||
@@ -196,20 +206,15 @@
|
|||||||
|
|
||||||
它的作用是自动挂载一系列虚拟文件系统,同时在退出 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。为避免过早地陷入依赖地狱,建议仅按照顺序安装以下包:
|
||||||
|
|
||||||
1. [efivar-39](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/efivar.html)
|
1. [efivar-39](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/efivar.html)
|
||||||
2. [Popt-1.19](https://www.linuxfromscratch.org/blfs/view/12.4/general/popt.html)
|
2. [Popt-1.19](https://www.linuxfromscratch.org/blfs/view/12.4/general/popt.html)
|
||||||
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)
|
||||||
|
|
||||||
@@ -222,7 +227,6 @@
|
|||||||
- [10.3. Linux-6.16.1](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/kernel.html)
|
- [10.3. Linux-6.16.1](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/kernel.html)
|
||||||
|
|
||||||
内核配置是整本 LFS 书中最具有挑战的环节。对此我可以总结出几点建议:
|
内核配置是整本 LFS 书中最具有挑战的环节。对此我可以总结出几点建议:
|
||||||
|
|
||||||
- 复刻并裁剪现有配置
|
- 复刻并裁剪现有配置
|
||||||
|
|
||||||
如果将要使用正在构建的 LFS 系统的机器和 Host 完全相同,并且内核版本相同或相近,可以将 Host 现在运行的内核的配置文件搬过来,同时仅启用当前 Host 加载的内核模块,这将极大地减小配置难度和缩短构建耗时:
|
如果将要使用正在构建的 LFS 系统的机器和 Host 完全相同,并且内核版本相同或相近,可以将 Host 现在运行的内核的配置文件搬过来,同时仅启用当前 Host 加载的内核模块,这将极大地减小配置难度和缩短构建耗时:
|
||||||
@@ -249,7 +253,9 @@
|
|||||||
|
|
||||||
如果内核版本不一致,会在 `make localmodconfig` 时出现很多交互选项,建议全部保持默认,或使用 `make olddefconfig` 来自动处理。
|
如果内核版本不一致,会在 `make localmodconfig` 时出现很多交互选项,建议全部保持默认,或使用 `make olddefconfig` 来自动处理。
|
||||||
|
|
||||||
在此之后,仍建议按照 LFS 书中的指示检查和调整配置选项。
|
对此需要特别注意,大多数成熟发行版的内核都是在有 initrd 的前提下配置、编译和使用的,而在 LFS 中直到 BLFS 才会涉及到 initrd。因跨度过大,不建议直接从这里跳到 BLFS 中配置 initrd。因此需要针对这点手动做一些调整,否则可能无法启动。具体修改方向在之后会提到。
|
||||||
|
|
||||||
|
在此之后,仍建议(或者说请务必)按照 LFS 书中的指示检查和调整配置选项。
|
||||||
|
|
||||||
- 参考 Host 的内核配置
|
- 参考 Host 的内核配置
|
||||||
|
|
||||||
@@ -265,20 +271,22 @@
|
|||||||
|
|
||||||
- `<*>` or `<M>`
|
- `<*>` or `<M>`
|
||||||
|
|
||||||
对于大多数选项,建议选择模块化(M)而非内置(\*)。这将显著减少内核体积,并且提高灵活性。但有几种情况例外,例如:
|
对于大多数选项,建议选择模块化(M)而非内置(\*)。这将显著减少内核体积,并且提高灵活性。但在**没有配置 initrd**(这将在 BLFS 中涉及),有几种情况例外,例如:
|
||||||
|
- 存储总线和控制器驱动,如 `CONFIG_BLK_DEV_NVME`;
|
||||||
|
- RootFS 所需驱动,如 `CONFIG_EXT4_FS`,`CONFIG_BTRFS_FS`;
|
||||||
|
- 如果全盘加密,输入密码(显然)需要键盘或其他输入设备的驱动;
|
||||||
|
- 基础显示驱动,如 `CONFIG_VT`,`CONFIG_VT_CONSOLE` 等。
|
||||||
|
|
||||||
- 引导相关的选项(例如 EFI 支持)必须内置,否则无法引导。
|
总之,在挂载 RootFS 之前需要的,以及挂载 RootFS 需要的驱动需要内置。
|
||||||
- 一些必要的文件系统(例如 ext4)建议内置,否则可能无法挂载根文件系统。
|
|
||||||
|
|
||||||
也有必须模块化的情况,例如:
|
也有在**没有 initrd** 的情况下必须模块化的情况,例如:
|
||||||
|
- 需要外部固件支持的驱动程序(如 `i915`),除非使用 initrd 或通过 `CONFIG_EXTRA_FIRMWARE` 将固件也内置到内核中。
|
||||||
- 需要固件支持的驱动程序(如 i915),除非使用 initrd 或将固件内置到内核中。
|
|
||||||
|
|
||||||
- [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]
|
||||||
>
|
>
|
||||||
@@ -297,7 +305,6 @@
|
|||||||
BLFS 并不像 LFS 那样有线性的章节顺序,但仍建议先顺序阅读直到 [After LFS Configuration Issues](https://www.linuxfromscratch.org/blfs/view/stable/postlfs/config.html) 章节**结束**再按自己的需要安装各种包。
|
BLFS 并不像 LFS 那样有线性的章节顺序,但仍建议先顺序阅读直到 [After LFS Configuration Issues](https://www.linuxfromscratch.org/blfs/view/stable/postlfs/config.html) 章节**结束**再按自己的需要安装各种包。
|
||||||
|
|
||||||
- `su: must be run from a terminal`
|
- `su: must be run from a terminal`
|
||||||
|
|
||||||
1. What?
|
1. What?
|
||||||
|
|
||||||
这通常发生在 chroot 后使用 `su` 切换到普通用户,再使用 `su` 试图切换回 root 时。
|
这通常发生在 chroot 后使用 `su` 切换到普通用户,再使用 `su` 试图切换回 root 时。
|
||||||
@@ -344,14 +351,14 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
其中:
|
其中:
|
||||||
|
|
||||||
- `-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 相关的话题。算个小坑?大概。
|
||||||
@@ -437,8 +444,24 @@
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
原因是源文件显式包含了多余的 moc 文件,和自动生成的元对象代码冲突,导致重复定义。出问题的源文件有两个,可以通过以下命令修复:
|
原因是源文件显式 include 了多余的 moc 文件,和自动生成的相同作用的代码冲突,导致重复定义。出问题的源文件有两个,可以通过以下命令修复:
|
||||||
|
|
||||||
```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)
|
||||||
|
|||||||
+440
-51
@@ -4,15 +4,136 @@
|
|||||||
>
|
>
|
||||||
> 仅记录我的折腾过程, 并非指南, 并非推荐, 并非技术文档.
|
> 仅记录我的折腾过程, 并非指南, 并非推荐, 并非技术文档.
|
||||||
|
|
||||||
## 要做什么
|
## 目录
|
||||||
|
|
||||||
|
- [目录](#目录)
|
||||||
|
- [概览](#概览)
|
||||||
|
- [一些术语和缩写](#一些术语和缩写)
|
||||||
|
- [要做什么](#要做什么)
|
||||||
|
- [需要什么](#需要什么)
|
||||||
|
- [前置准备](#前置准备)
|
||||||
|
- [放开那个端口!](#放开那个端口)
|
||||||
|
- [注册 SMTP 中继服务](#注册-smtp-中继服务)
|
||||||
|
- [配置 DNS 和 rDNS](#配置-dns-和-rdns)
|
||||||
|
- [在 DNS 服务商处](#在-dns-服务商处)
|
||||||
|
- [在云服务器商处](#在云服务器商处)
|
||||||
|
- [在服务器上](#在服务器上)
|
||||||
|
- [配置邮件服务器](#配置邮件服务器)
|
||||||
|
- [搞定 SSL](#搞定-ssl)
|
||||||
|
- [创建 compose.yaml](#创建-composeyaml)
|
||||||
|
- [创建邮箱账号](#创建邮箱账号)
|
||||||
|
- [配置 SPF](#配置-spf)
|
||||||
|
- [配置 DKIM](#配置-dkim)
|
||||||
|
- [配置 DMARC](#配置-dmarc)
|
||||||
|
- [启动!](#启动)
|
||||||
|
- [Rspamd Web UI](#rspamd-web-ui)
|
||||||
|
- [配置邮件客户端](#配置邮件客户端)
|
||||||
|
- [Thunderbird](#thunderbird)
|
||||||
|
- [Extra Notes](#extra-notes)
|
||||||
|
- [exim4: 我呢?](#exim4-我呢)
|
||||||
|
- [Catch'em All!](#catchem-all)
|
||||||
|
- [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. 在自己的服务器上配置邮件服务器, 直接接收邮件;
|
||||||
|
|
||||||
2. 使用 SMTP 中继服务发送邮件;
|
2. 配置邮件服务器通过 SMTP 中继服务发送邮件;
|
||||||
|
|
||||||
|
> 或者也可以让邮件服务器直接发信, 但要额外做好以下准备:
|
||||||
|
>
|
||||||
|
> - 出入站均畅通无阻的 25 端口
|
||||||
|
>
|
||||||
|
> 大多数云服务商默认封锁 25 端口的出站流量, 需要联系或付费解封.
|
||||||
|
>
|
||||||
|
> - 良好的 IP 声誉
|
||||||
|
>
|
||||||
|
> 可以通过 [mxtoolbox](https://mxtoolbox.com) 或类似网站检查 IP 是否在任意黑名单中. 如果在的话也不是没救, 可以查查对应黑名单的影响范围, 有些其实根本无人在意.
|
||||||
|
>
|
||||||
|
> - 绝对完善的安全策略
|
||||||
|
>
|
||||||
|
> 很多对于使用 SMTP 中继服务时可选的配置在自己发信的场景下是基本要求, 例如 rDNS, DKIM 等. 可以通过 [mail-tester.com](https://www.mail-tester.com) 或 [Hardenize](https://www.hardenize.com/) 等网站检查配置是否完善.
|
||||||
|
>
|
||||||
|
> - 耗费数周乃至数月培养 IP 声誉的时间与物质成本
|
||||||
|
>
|
||||||
|
> 多少沾点玄学. 举个例子, 在这个过程中需要持续向多个公共邮箱平台的多个邮箱发送**有效**邮件, 什么样的邮件是"有效"的? 不知道. 发送多少封才能被认为"无害"? 也不知道. 发送到多少封会被认为"滥用"? 不知道. 不同平台具体采取什么样的评估标准? 还是不知道. 对于纯良的个人邮局来说这无疑是最大的挑战
|
||||||
|
>
|
||||||
|
> - IP 长期保活的持久战准备
|
||||||
|
>
|
||||||
|
> 和上面一点一样也是黑箱. 即使有良好的初始声誉, 如果后续不进行长期维护仍然可能会回到垃圾箱. 这不是短期努力可以解决的, 需要长期坚持.
|
||||||
|
>
|
||||||
|
> 如果确实觉得以上这些都不算问题, 那么可以忽略下文中所有有关 SMTP 中继相关的内容. 其实就本文包含的步骤而言区别并没有很大, 下文中相关的地方会以引用的形式进行标注.
|
||||||
|
|
||||||
3. 配置 SPF/DKIM/DMARC/MTA-STS 等等.
|
3. 配置 SPF/DKIM/DMARC/MTA-STS 等等.
|
||||||
|
|
||||||
## 需要什么
|
### 需要什么
|
||||||
|
|
||||||
1. 一个中意的域名.
|
1. 一个中意的域名.
|
||||||
|
|
||||||
@@ -25,36 +146,32 @@
|
|||||||
|
|
||||||
3. SMTP 中继服务.
|
3. SMTP 中继服务.
|
||||||
|
|
||||||
也就是帮你发邮件的服务商, 详见后续章节.
|
也就是帮忙发邮件的服务商, 详见后续章节.
|
||||||
|
|
||||||
> 或者也可以自己发, 但要额外做好以下准备:
|
|
||||||
>
|
|
||||||
> - 出入站均畅通无阻的 25 端口
|
|
||||||
> - 良好的 IP 声誉
|
|
||||||
> - 绝对完善的安全策略
|
|
||||||
> - 耗费数周乃至数月培养 IP 声誉的时间与物质成本
|
|
||||||
> - 为 IP 长期保活的持久战准备
|
|
||||||
>
|
|
||||||
> 如果确实觉得没问题, 那么可以忽略下文中所有有关 SMTP 中继相关的内容, 就本文包含的内容而言步骤区别并没有很大.
|
|
||||||
|
|
||||||
4. 一个拥有公网 IP 和充足空闲资源的服务器, 并且(至少)需要开通以下 TCP 端口:
|
4. 一个拥有公网 IP 和充足空闲资源的服务器, 并且(至少)需要开通以下 TCP 端口:
|
||||||
|
|
||||||
| 端口 | 用途 | 出站 | 入站 | 说明 |
|
| 端口 | 用途 | 出站 | 入站 | 说明 |
|
||||||
| ---- | --------------- | ---- | ---- | --------------------------------------------------- |
|
| ---- | --------------- | ---- | ---- | --------------------------------------------------- |
|
||||||
| 25 | SMTP | ❌ | ✅ | 核心传输端口. 如果使用 SMTP 中继服务, 则不需要出站 |
|
| 25 | SMTP | ⚠️ | ✅ | 核心传输端口. 如果使用 SMTP 中继服务, 则出站可选 |
|
||||||
| 993 | IMAPS | ❌ | ✅ | 用于邮件客户端收信 |
|
| 993 | IMAPS | ❌ | ✅ | 用于邮件客户端收信 |
|
||||||
| 587 | SMTP Submission | ✅ | ✅ | 支持 STARTTLS. 如果使用 SMTP 中继服务, 则也需要出站 |
|
| 587 | SMTP Submission | ⚠️ | ✅ | 支持 STARTTLS. 如果使用 SMTP 中继服务, 则也需要出站 |
|
||||||
| 465 | SMTPS | ❌ | ✅ | 支持 Implicit SSL/TLS. |
|
| 465 | SMTPS | ❌ | ✅ | 支持 Implicit SSL/TLS |
|
||||||
|
|
||||||
很多云服务商会默认屏蔽 25 端口的出站方向流量, 但这对于使用 SMTP 中继服务的场景来说并不重要, 因为发信时直接连接收件方服务器的并非自己的服务器.
|
很多云服务商会默认屏蔽 25 端口的出站方向流量, 但这对于使用 SMTP 中继服务的场景来说并不重要, 因为发信时直接连接收件方服务器的并非自己的服务器.
|
||||||
|
|
||||||
同时, 最好支持 rDNS, 也就是把 IP 反解析到域名. IPv4 和 IPv6 用到哪个就配置哪个, 都用得到就都配置.
|
同时, 最好支持 rDNS, 也就是把 IP 反解析到域名. IPv4 和 IPv6 用到哪个就配置哪个, 都用得到就都配置.
|
||||||
|
|
||||||
> 如果不使用 SMTP 中继服务, 则必须配置 rDNS
|
> 如果选择直接发信不使用 SMTP 中继服务, 则必须配置 rDNS
|
||||||
|
|
||||||
下文中将使用 `1.14.5.14` 作为服务器的公网 IP 地址.
|
下文中将使用 `1.14.5.14` 作为服务器的公网 IP 地址.
|
||||||
|
|
||||||
## 放开那个 25 端口!
|
> [!IMPORTANT]
|
||||||
|
>
|
||||||
|
> 在网络工程和状态防火墙的语境中, 当提到"封锁端口 X 的出站流量"时, 通常指的是"禁止从本地服务器发起到外部服务器的连接请求, 目标端口为 X". 相反, "封锁端口 X 的入站流量"则意味着"禁止外部服务器发起到本地服务器的连接请求, 目标端口为 X". 策略中用于识别流量的是目标端口, 而非源端口. 例如即使"25 端口的出站流量被封", 如果强行用 25 端口连接外部服务器的其他开放端口如 80, 这种连接请求仍然是允许的. 当然策略中也可以通过源端口进行过滤, 不过这么做通常意义不大.
|
||||||
|
|
||||||
|
## 前置准备
|
||||||
|
|
||||||
|
### 放开那个端口!
|
||||||
|
|
||||||
1. 检测 25 端口是否真的开放:
|
1. 检测 25 端口是否真的开放:
|
||||||
|
|
||||||
@@ -92,7 +209,7 @@
|
|||||||
|
|
||||||
2. 解决占用:
|
2. 解决占用:
|
||||||
|
|
||||||
我的服务器是 Debian 13 系统, 默认使用 `exim4` 作为邮件传输代理(MTA). 它监听 127.0.0.1:25, 因此除了在防火墙里放行 25 端口外, 还需要禁用 `exim4`.
|
我的服务器是 Debian 13 系统, 默认使用 `exim4` 作为邮件传输代理(MTA). 不出意外的话 25 端口已经在它手里攥了很久了. 因此除了在防火墙里放行 25 端口外, 还需要禁用 `exim4`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl disable --now exim4
|
sudo systemctl disable --now exim4
|
||||||
@@ -100,9 +217,11 @@
|
|||||||
|
|
||||||
如果确实需要系统内部通信, 例如 `cron` 发送邮件通知, 可以安装 `ssmtp` 或 `msmtp` 之类的轻量级 MTA, 参见 [后续章节](#exim4-我呢).
|
如果确实需要系统内部通信, 例如 `cron` 发送邮件通知, 可以安装 `ssmtp` 或 `msmtp` 之类的轻量级 MTA, 参见 [后续章节](#exim4-我呢).
|
||||||
|
|
||||||
## 注册 SMTP 中继服务
|
### 注册 SMTP 中继服务
|
||||||
|
|
||||||
此类服务可以大致理解为"帮你发邮件的中介", 他们有一大堆 IP 地址, 这些地址的声誉都不错, 因此用他们发信的话, 邮件更容易送达收件箱而不是自动进入垃圾箱. 并且也可以避免 25 端口出站被封的问题.
|
> 如果选择直接发信不使用 SMTP 中继服务, 则**跳过**本节内容.
|
||||||
|
|
||||||
|
此类服务可以大致理解为"帮忙发邮件的中介", 他们通常有很多 IP 地址, 这些地址的声誉都不错, 配置也很完善, 因此用他们发信的话, 邮件更容易送达收件箱而不是自动被扔进垃圾箱. 并且这也可以避免 25 端口出站被封的问题.
|
||||||
|
|
||||||
我此次用的是 [Resend](https://resend.com), 其他类似服务还有 Amazon SES, Mailgun 等等.
|
我此次用的是 [Resend](https://resend.com), 其他类似服务还有 Amazon SES, Mailgun 等等.
|
||||||
|
|
||||||
@@ -129,7 +248,17 @@
|
|||||||
- 值: `mail.domain.tld`
|
- 值: `mail.domain.tld`
|
||||||
- 优先级: `10`
|
- 优先级: `10`
|
||||||
|
|
||||||
这将会是邮件的接收和发送服务器.
|
这将会是邮件的接收和发送服务器的域名.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> 如果乐意的话可以把收信域名, 发信域名, 乃至退信域名等等都拆分开来配置, 但这超过了本文的讨论范围且配置大同小异, 因此不做另外说明 ~~主要是懒~~ :)
|
||||||
|
>
|
||||||
|
> 在使用 SMTP 中继服务的情况下, 这个域名只作为收信服务器存在, 发信时使用的域名通常是 SMTP 服务商提供的子域名.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> 优先级数值越小, 优先级越高. 如果同一个域名下有多个 MX 记录, 那么邮件服务器会优先选择优先级最高的记录进行连接.
|
||||||
|
|
||||||
- A 记录:
|
- A 记录:
|
||||||
- 主机名: `mail`
|
- 主机名: `mail`
|
||||||
@@ -137,11 +266,13 @@
|
|||||||
|
|
||||||
指向邮件服务器的公网 IP 地址.
|
指向邮件服务器的公网 IP 地址.
|
||||||
|
|
||||||
其他记录会在启动邮件服务器后配置.
|
其他记录将会在启动邮件服务器后配置.
|
||||||
|
|
||||||
### 在云服务器商处
|
### 在云服务器商处
|
||||||
|
|
||||||
将服务器 ip 的 rDNS 设置为 `mail.domain.tld`. 虽然未来主要使用 Resend 发信, 但是收信时一些发信方也可能会检查 rDNS, 因此最好设置正确, 有备无患.
|
将服务器 ip 的 rDNS 设置为 `mail.domain.tld`. 这对于使用 SMTP 中继服务的场景来说几乎毫无作用, 但是收信时一些发信方也可能会检查 rDNS, 因此最好设置正确, 有备无患.
|
||||||
|
|
||||||
|
> 如果选择直接发信不使用 SMTP 中继服务, 则**必须**配置 rDNS
|
||||||
|
|
||||||
### 在服务器上
|
### 在服务器上
|
||||||
|
|
||||||
@@ -166,12 +297,12 @@
|
|||||||
|
|
||||||
或者也可以让 `docker-mailserver` 自己申请证书, 但是我的服务器的 `80` 和 `443` 端口都是 `openresty` 的, 并且恰好有合适的证书, 不想折腾了.
|
或者也可以让 `docker-mailserver` 自己申请证书, 但是我的服务器的 `80` 和 `443` 端口都是 `openresty` 的, 并且恰好有合适的证书, 不想折腾了.
|
||||||
|
|
||||||
### 创建 `compose.yaml`
|
### 创建 compose.yaml
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mailserver:
|
mailserver:
|
||||||
image: docker.io/mailserver/docker-mailserver:latest
|
image: docker.io/mailserver/docker-mailserver:15.1.0
|
||||||
container_name: mailserver
|
container_name: mailserver
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
@@ -181,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
|
||||||
@@ -203,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
|
||||||
# 与下方挂载路径对应
|
# 与下方挂载路径对应
|
||||||
@@ -228,11 +361,12 @@ services:
|
|||||||
以上配置中**必须**修改的地方有:
|
以上配置中**必须**修改的地方有:
|
||||||
|
|
||||||
- `domain.tld`: 替换为真实域名.
|
- `domain.tld`: 替换为真实域名.
|
||||||
|
- `resend`: 替换为在 SMTP 服务商处创建的真实用户名.
|
||||||
- `res_some_random_api_key`: 替换为在 SMTP 服务商处创建的 SMTP 凭据或 API Key.
|
- `res_some_random_api_key`: 替换为在 SMTP 服务商处创建的 SMTP 凭据或 API Key.
|
||||||
|
|
||||||
一些 environment 的解释:
|
一些 environment 的解释:
|
||||||
|
|
||||||
- 如果机器性能孱弱或很在意占用的资源, 可以关掉 [Raspamd](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/rspamd/) 使用老东西:
|
- 如果机器性能孱弱或很在意占用的资源, 可以关掉 [Rspamd](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/rspamd/) 使用老东西:
|
||||||
- `ENABLE_AMAVIS=1`
|
- `ENABLE_AMAVIS=1`
|
||||||
- `ENABLE_OPENDKIM=1`
|
- `ENABLE_OPENDKIM=1`
|
||||||
- `ENABLE_OPENDMARC=1`
|
- `ENABLE_OPENDMARC=1`
|
||||||
@@ -241,15 +375,9 @@ services:
|
|||||||
- `ENABLE_RSPAMD=0`
|
- `ENABLE_RSPAMD=0`
|
||||||
- `RSPAMD_LEARN=0`
|
- `RSPAMD_LEARN=0`
|
||||||
|
|
||||||
- 如果不需要 SMTP 中继服务, 可以删除以下环境变量:
|
- `RSPAMD_GREYLISTING=0`
|
||||||
- `DEFAULT_RELAY_HOST=[smtp.resend.com]:587`
|
|
||||||
- `RELAY_USER=resend`
|
|
||||||
- `RELAY_PASSWORD=res_some_random_api_key`
|
|
||||||
|
|
||||||
- 如果垃圾邮件实在太多:
|
启用后, 在第一次接受陌生人邮件时拒绝, 要求对方重试, 这样可以有效减少垃圾邮件, 但会显著增加延迟.
|
||||||
- `RSPAMD_GREYLISTING=1`
|
|
||||||
|
|
||||||
在第一次接受陌生人邮件时拒绝, 要求对方重试, 这样可以有效减少垃圾邮件, 但会显著增加延迟.
|
|
||||||
|
|
||||||
- `ENABLE_MTA_STS=1`:
|
- `ENABLE_MTA_STS=1`:
|
||||||
|
|
||||||
@@ -265,7 +393,17 @@ services:
|
|||||||
|
|
||||||
- `POSTFIX_INET_PROTOCOLS=ipv4`:
|
- `POSTFIX_INET_PROTOCOLS=ipv4`:
|
||||||
|
|
||||||
如果服务器的 IPv6 配置不完善, 可以强制 Postfix 仅使用 IPv4.
|
如果服务器的 IPv6 配置不完善, 可以强制 Postfix 仅使用 IPv4. 反之也可以只支持 IPv6.
|
||||||
|
|
||||||
|
- `ENABLE_SRS=1`:
|
||||||
|
|
||||||
|
启用 SRS 用于转发场景, 详见 [后续章节](#转发到其他域).
|
||||||
|
|
||||||
|
> 如果选择直接发信不使用 SMTP 中继服务, **必须**删除以下环境变量:
|
||||||
|
>
|
||||||
|
> - `DEFAULT_RELAY_HOST=[smtp.resend.com]:587`
|
||||||
|
> - `RELAY_USER=resend`
|
||||||
|
> - `RELAY_PASSWORD=res_some_random_api_key`
|
||||||
|
|
||||||
如果要进行进一步配置, 必须先启动容器. 此时会报错, 因为还没有创建邮箱账号. 但不用管, 先让它跑着.
|
如果要进行进一步配置, 必须先启动容器. 此时会报错, 因为还没有创建邮箱账号. 但不用管, 先让它跑着.
|
||||||
|
|
||||||
@@ -289,24 +427,39 @@ 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 mx include:resend.com -all` (假设使用 Resend 作为 SMTP 服务商)
|
- 值: `v=spf1 -all` (假设使用 Resend 作为 SMTP 服务商)
|
||||||
|
|
||||||
解释:
|
解释:
|
||||||
- `v=spf1`: 指定 SPF 版本.
|
- `v=spf1`: 指定 SPF 版本.
|
||||||
- `mx`: 允许通过 MX 记录指定的服务器发送邮件.
|
- `include:resend.com`: 仅允许 Resend 的服务器以此域名的名义发送邮件. 因为 Resend 并不会通过根域名发送邮件, 所以这条记录严格来说可以省略.
|
||||||
- `include:resend.com`: 允许 Resend 的服务器发送邮件.
|
|
||||||
- `-all`: 硬失败, 未授权的服务器发送邮件时拒绝. 因为发行渠道只有 Resend, 所以这样设置是合理的. 如果希望软失败, 即接受但标记为可疑, 可以使用 `~all`.
|
- `-all`: 硬失败, 未授权的服务器发送邮件时拒绝. 因为发行渠道只有 Resend, 所以这样设置是合理的. 如果希望软失败, 即接受但标记为可疑, 可以使用 `~all`.
|
||||||
|
|
||||||
|
> 如果选择直接发信不使用 SMTP 中继服务, 则**需要**将 `include:resend.com` 替换为自己的邮件服务器的 IP 地址或域名, 例如:
|
||||||
|
>
|
||||||
|
> `v=spf1 a:mail.domain.tld -all`
|
||||||
|
>
|
||||||
|
> 详细语法参见 [SPF 规范](https://datatracker.ietf.org/doc/html/rfc7208).
|
||||||
|
|
||||||
### 配置 DKIM
|
### 配置 DKIM
|
||||||
|
|
||||||
DKIM (DomainKeys Identified Mail) 用于验证邮件的完整性和真实性.
|
DKIM (DomainKeys Identified Mail) 用于验证邮件的完整性和真实性.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> 对于使用 SMTP 中继服务的场景, DKIM 通常由服务商负责配置和签署, 本地 DKIM 在中继场景下不一定会被最终保留或使用, 其价值更多在于内部一致性或未来切换为直连发信的准备.
|
||||||
|
|
||||||
|
> 如果选择直接发信而非使用 SMTP 中继服务, 则本节内容是**必须的**.
|
||||||
|
|
||||||
1. 生成 DKIM 密钥:
|
1. 生成 DKIM 密钥:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -333,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 服务商处添加以下记录:
|
||||||
|
|
||||||
@@ -397,7 +550,9 @@ docker compose up -d
|
|||||||
|
|
||||||
## 配置邮件客户端
|
## 配置邮件客户端
|
||||||
|
|
||||||
我并非 TUI 重度爱好者, 日常用 Thunderbird 当客户端.
|
我并非 TUI 重度爱好者, 日常用 Thunderbird 当客户端, 因此这里只涉及这一种客户端的配置方法, 当然其他的也大同小异.
|
||||||
|
|
||||||
|
### Thunderbird
|
||||||
|
|
||||||
1. 在添加邮箱的第一个页面, 点击 `MANUAL CONFIGURATION`.
|
1. 在添加邮箱的第一个页面, 点击 `MANUAL CONFIGURATION`.
|
||||||
|
|
||||||
@@ -428,7 +583,11 @@ docker compose up -d
|
|||||||
|
|
||||||
现在已经可以试着和其他邮箱互发邮件了!
|
现在已经可以试着和其他邮箱互发邮件了!
|
||||||
|
|
||||||
## exim4: 我呢?
|
## Extra Notes
|
||||||
|
|
||||||
|
本节包含一些额外的说明和可选配置.
|
||||||
|
|
||||||
|
### exim4: 我呢?
|
||||||
|
|
||||||
这个, 不需要了. 既然已经有了邮箱服务, 那么继续使用重量级的 `exim4` 就没什么意义了.
|
这个, 不需要了. 既然已经有了邮箱服务, 那么继续使用重量级的 `exim4` 就没什么意义了.
|
||||||
|
|
||||||
@@ -502,9 +661,13 @@ docker compose up -d
|
|||||||
echo "This is a test email from the system." | mail -s "Test Email" root
|
echo "This is a test email from the system." | mail -s "Test Email" root
|
||||||
```
|
```
|
||||||
|
|
||||||
如果一切正常, 你应该会在前面配置的邮箱客户端中收到这封测试邮件.
|
如果一切正常, 应该会在前面配置的邮箱客户端中收到这封测试邮件.
|
||||||
|
|
||||||
## Catch'em All!
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> 此时可将 msmtp 看作 mailserver 的客户端, 因此 `/etc/msmtprc` 中配置的邮件服务器并不一定要部署在本机, 甚至可以是公共邮箱服务.
|
||||||
|
|
||||||
|
### Catch'em All!
|
||||||
|
|
||||||
如果希望接收发往不存在邮箱地址的邮件, 可以启用 Catch-all 功能. 方法也很简单, 使用 alias 即可:
|
如果希望接收发往不存在邮箱地址的邮件, 可以启用 Catch-all 功能. 方法也很简单, 使用 alias 即可:
|
||||||
|
|
||||||
@@ -514,7 +677,11 @@ docker exec -it mailserver setup alias add @domain.tld me@domain.tld
|
|||||||
|
|
||||||
将其中 `me@domain.tld` 替换为希望接收这些邮件的真实邮箱地址即可.
|
将其中 `me@domain.tld` 替换为希望接收这些邮件的真实邮箱地址即可.
|
||||||
|
|
||||||
## 更进一步
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> 完全按照上述步骤配置通配符邮箱别名可能会导致后续添加其他邮箱时邮件仍然发到上面指定的 catch-all 邮箱而不是新创建的邮箱. 更好的实践是用到什么邮箱名再创建对应的别名.
|
||||||
|
|
||||||
|
### MTA-STS
|
||||||
|
|
||||||
**MTA-STS** (Mail Transfer Agent Strict Transport Security) 通过强制要求发送方使用加密连接发送邮件防止中间人攻击, 对个人邮箱来讲~~看起来其实没啥大用但总归~~是个加分项, 并且确实会让邮箱服务变得更酷.
|
**MTA-STS** (Mail Transfer Agent Strict Transport Security) 通过强制要求发送方使用加密连接发送邮件防止中间人攻击, 对个人邮箱来讲~~看起来其实没啥大用但总归~~是个加分项, 并且确实会让邮箱服务变得更酷.
|
||||||
|
|
||||||
@@ -583,14 +750,236 @@ 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) 或类似工具进行自动化处理.
|
||||||
|
|
||||||
|
### 邮件传输链路
|
||||||
|
|
||||||
|
> 一些关于服务器位置描述的解释:
|
||||||
|
>
|
||||||
|
> - `Local`: 自建邮件服务器.
|
||||||
|
> - `Source`: 外部邮件服务器, 发送方.
|
||||||
|
> - `Destination`: 外部邮件服务器, 接收方.
|
||||||
|
> - `Relay`: SMTP 中继服务.
|
||||||
|
|
||||||
|
以本文配置为例:
|
||||||
|
|
||||||
|
- 收信
|
||||||
|
1. 外部投递 (Source MTA to Local MTA):
|
||||||
|
- 连接方: 外部邮件服务器.
|
||||||
|
|
||||||
|
- 被连接方: 自建邮件服务器公网 IP.
|
||||||
|
|
||||||
|
- 目标端口: 25.
|
||||||
|
|
||||||
|
- 说明: 标准的 SMTP 传输. 这也是为什么需要开放 25 端口入站.
|
||||||
|
|
||||||
|
2. 本地分发 (Local MTA to Local MDA):
|
||||||
|
- 连接方: 自建邮件服务器.
|
||||||
|
|
||||||
|
- 被连接方: 自建邮件服务器本地存储.
|
||||||
|
|
||||||
|
- 端口: 本地通信, 无需端口.
|
||||||
|
|
||||||
|
- 说明: Postfix (MTA) 接收邮件后,转交给 Dovecot (MDA) 将邮件存入磁盘.
|
||||||
|
|
||||||
|
3. 客户端拉取 (MUA to Local MDA):
|
||||||
|
- 连接方: 邮件客户端 (例如 Thunderbird).
|
||||||
|
|
||||||
|
- 被连接方: 自建邮件服务器公网 IP.
|
||||||
|
|
||||||
|
- 目标端口: 993 (IMAPS).
|
||||||
|
|
||||||
|
- 说明: 使用 IMAPS 拉取邮件.
|
||||||
|
|
||||||
|
- 发信
|
||||||
|
1. 客户端提交 (MUA to Local MSA):
|
||||||
|
- 连接方: 邮件客户端 (例如 Thunderbird).
|
||||||
|
|
||||||
|
- 被连接方: 自建邮件服务器公网 IP.
|
||||||
|
|
||||||
|
- 目标端口: 587 (STARTTLS) 或 465 (SSL/TLS).
|
||||||
|
|
||||||
|
- 说明: 把邮件上传到服务器队列中.
|
||||||
|
|
||||||
|
2. 中继转发 (Local MTA to Relay MTA):
|
||||||
|
- 连接方: 自建邮件服务器.
|
||||||
|
|
||||||
|
- 被连接方: SMTP 中继服务.
|
||||||
|
|
||||||
|
- 目标端口: 587 (STARTTLS).
|
||||||
|
|
||||||
|
- 说明: 自建邮件服务器根据 `DEFAULT_RELAY_HOST` 配置, 验证账号密码后将邮件转交给中继商.
|
||||||
|
|
||||||
|
3. 最终投递 (Relay MTA to Destination MTA):
|
||||||
|
- 连接方: SMTP 中继服务.
|
||||||
|
|
||||||
|
- 被连接方: 外部邮件服务器.
|
||||||
|
|
||||||
|
- 目标端口: 25.
|
||||||
|
|
||||||
|
- 说明: 这一步由中继商完成. 部分客户端可能会显示代发信息, 具体取决于中继实现与客户端策略.
|
||||||
|
|
||||||
|
### 备份与恢复
|
||||||
|
|
||||||
|
> [!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 需要花费的时间成本乃至金钱成本远超我的想象, 二是无论如何邮件内容也会被收件方的平台以算法评估一遍的事实彻底击碎了对于"隐私"乌托邦最后的妄想. 即使有这么多复杂的安全措施, 补齐了传输过程中的每一个可能的安全漏洞, 但真正和我点对点沟通的从来都只是靠着一条条既定规则维系的平台, 而不是我在写下收件地址时心中所想的一个个鲜活的人.
|
||||||
|
|
||||||
那么做这一切的目的到底是什么呢? 或许也只剩"酷"了吧, 除此之外我唯一能想到的好处就是在仅需要邮箱就能注册账号的平台注册无穷无尽的小号.
|
那么做这一切的目的到底是什么呢? 或许也只剩"酷"了吧, 除此之外我唯一能想到的好处就是在仅需要邮箱就能注册账号的平台注册无穷无尽的小号.
|
||||||
|
|
||||||
Anyway, 确实是我拥有域名和服务器以来部署过最复杂, 折腾时间最久, 也可能是最无用的服务. 仅论过程还是很有意思的, 至少能让我理解现代电子邮箱系统到底是如何运作的. ~~闲的没事的话~~还是很值得试一试的 :)
|
Anyway, 确实是我拥有域名和服务器以来部署过的最复杂, 折腾时间最久, 也可能是最无用的服务. 仅论过程还是很有意思的, 至少能让我理解现代电子邮箱系统到底是如何运作的. ~~闲的没事的话~~还是很值得试一试的 :)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
de 布局太全能了
|
||||||
|
|
||||||
<img src="https://upload.wikimedia.org/wikipedia/commons/3/3e/KB_Germany_Linux.svg" alt="Deutsche Tastaturbelegung unter Linux"/>
|
<img src="https://upload.wikimedia.org/wikipedia/commons/3/3e/KB_Germany_Linux.svg" alt="Deutsche Tastaturbelegung unter Linux"/>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+3
-3
@@ -6,11 +6,11 @@
|
|||||||
>
|
>
|
||||||
> sddm: `cachyos-v3/sddm 0.21.0-8`
|
> sddm: `cachyos-v3/sddm 0.21.0-8`
|
||||||
|
|
||||||
SDDM shows a blank screen (only with mouse cursor and tty cursor) after booting into graphical target, but starts normally after restarting the SDDM service.
|
SDDM shows a blank screen (possibly only with mouse cursor and tty cursor) after booting into graphical target, but starts normally after restarting the SDDM service.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
SDDM starts before the NVIDIA driver is fully initialized, causing the greeter session to fail to display properly.
|
SDDM started before the NVIDIA driver is fully initialized, causing the greeter session to fail to display anything.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>evidence</summary>
|
<summary>evidence</summary>
|
||||||
@@ -114,7 +114,7 @@ sddm[1150]: Greeter session started successfully
|
|||||||
|
|
||||||
## How
|
## How
|
||||||
|
|
||||||
The key is to avoid timing race between NVIDIA initialization and SDDM start. There are multiple ways to achieve this, two for example:
|
The key is to avoid timing race between NVIDIA initialization and SDDM startup. There are multiple ways to achieve this, here are two:
|
||||||
|
|
||||||
### Early KMS:
|
### Early KMS:
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -206,8 +206,20 @@ mail_success
|
|||||||
|
|
||||||
- `--fga 0`: Disable "film grain analysis" (default disabled). fga in current version of VVenC (1.13.1) is kinda buggy and may lead to crashes in CQP mode. Enable with caution.
|
- `--fga 0`: Disable "film grain analysis" (default disabled). fga in current version of VVenC (1.13.1) is kinda buggy and may lead to crashes in CQP mode. Enable with caution.
|
||||||
|
|
||||||
- `--maxrate` or `-m` can be used to set a maximum bitrate. However, `--qpa 1` is required for this to take effect. With these two parameters enabled, this certain pattern is theoretically called **CQF** (Constant Quality Factor) and is noticeably different from CQP.
|
- `--maxrate` or `-m` can be used to set a maximum bitrate. However, `--qpa 1` is required for this to take effect. With these two parameters enabled, this certain pattern of rc theoretically becomes **CQF** (Constant Quality Factor) and behaves noticeably differently comparing to CQP.
|
||||||
|
|
||||||
|
## Extra notes
|
||||||
|
|
||||||
|
- Remux 266 bare stream into MKV:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.266 -c:v copy -bsf:v setts=ts='N*(1001/24000)/TB' output.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
The `bsf:v setts` part is necessary to set correct timestamps, replace `1001/24000` with the reciprocal of the actual frame rate.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [Usage - fraunhoferhhi/vvenc Wiki](https://github.com/fraunhoferhhi/vvenc/wiki/Usage)
|
- [Usage - fraunhoferhhi/vvenc Wiki](https://github.com/fraunhoferhhi/vvenc/wiki/Usage)
|
||||||
|
|
||||||
|
- [setts - FFmpeg Bitstream Filters Documentation](https://ffmpeg.org/ffmpeg-bitstream-filters.html#setts)
|
||||||
|
|||||||
Reference in New Issue
Block a user