feat(mail): implement UTF-8 mojibake repair functionality
Some checks failed
CI / Go tests (push) Has been cancelled
CI / Integration tests (push) Has been cancelled
CI / DB migrations (push) Has been cancelled

- Added repairUTF8Mojibake function to fix UTF-8 text misread as Latin-1, addressing common encoding issues in email bodies.
- Enhanced RepairStoredBodies and RepairSnippetWithBodies functions to utilize the new mojibake repair logic.
- Introduced unit tests for mojibake repair functionality to ensure accurate text restoration.
- Updated charset handling in repairLegacyCharsetString to incorporate mojibake repair, improving overall text processing reliability.
This commit is contained in:
R3D347HR4Y 2026-06-18 11:11:36 +02:00
parent 125169edee
commit e6a04fdd31
6 changed files with 180 additions and 64 deletions

View File

@ -44,6 +44,8 @@ DOMAIN={{PUBLIC_HOST}}
# Origine publique dérivée (http:// ou https:// + hôte)
SUITE_ORIGIN=http{{SECURE}}://{{PUBLIC_HOST}}
CLOUDFLARE_TUNNEL_PUBLIC_URL={{SUITE_ORIGIN}}
# X-Forwarded-Proto envoyé aux upstreams (nginx :80 + tunnel → https côté navigateur)
FORWARDED_PROTO=http{{SECURE}}
# -----------------------------------------------------------------------------
# General

View File

@ -10,6 +10,7 @@ services:
- ./nginx/patches/QGuclOcQ.js:/etc/nginx/patches/QGuclOcQ.js:ro
environment:
DOMAIN: ${DOMAIN:-localhost}
FORWARDED_PROTO: ${FORWARDED_PROTO:-http}
MAIL_FRONTEND_UPSTREAM: ${MAIL_FRONTEND_UPSTREAM:-host.docker.internal:3004}
env_file: ../.env.resolved
extra_hosts:

View File

@ -40,7 +40,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/migration/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -63,7 +63,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/admin/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -86,7 +86,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/drive/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -109,7 +109,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/office/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -132,7 +132,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/richtext/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -155,7 +155,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/ultidraw/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -178,7 +178,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/ai/mcp {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -208,7 +208,7 @@ server {
proxy_set_header X-OpenWebUI-User-Role $http_x_openwebui_user_role;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
@ -242,7 +242,7 @@ server {
proxy_set_header X-OpenWebUI-User-Role $http_x_openwebui_user_role;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
@ -269,7 +269,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/calendar/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -292,7 +292,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/contacts/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -315,7 +315,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/meet/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -338,7 +338,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/photos/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -361,7 +361,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/v1/search {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -384,7 +384,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# OpenWebUI trusted-header signin — ultid injects X-Ulti-User-* (auth_request + POST body deadlocks)
@ -411,7 +411,7 @@ server {
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# --- OpenWebUI API at site root (prod SPA: WEBUI_BASE_URL="" → fetch("/api/…")) ---
@ -425,7 +425,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Accept-Encoding "";
@ -444,7 +444,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400s;
@ -476,7 +476,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# OpenWebUI socket.io (ultid uses /ws without /socket.io)
@ -490,7 +490,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
@ -509,7 +509,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
@ -532,7 +532,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# OpenWebUI Ollama/OpenAI proxy mounts (SPA uses /ollama, /openai at root)
@ -544,7 +544,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /openai/ {
resolver 127.0.0.11 valid=10s ipv6=off;
@ -554,7 +554,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# TipTap / Hocuspocus — proxy WS without redirect (301 breaks upgrade)
@ -570,7 +570,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
@ -584,7 +584,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -597,7 +597,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400;
@ -616,7 +616,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# Public Nextcloud share links → UltiDrive viewer
@ -633,7 +633,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location = /cloud {
@ -649,7 +649,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_read_timeout 300s;
}
@ -664,7 +664,7 @@ server {
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location /ai/ {
@ -685,7 +685,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header X-Forwarded-Prefix /ai;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
@ -775,7 +775,7 @@ server {
proxy_set_header X-Ulti-User-Role $ulti_user_role;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# OpenWebUI API (prefix /ai/api — évite collision avec ultid /api/v1/)
@ -789,7 +789,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
@ -825,7 +825,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header X-Ulti-User-Email $ulti_user_email;
proxy_set_header X-Ulti-User-Name $ulti_user_name;
proxy_set_header X-Ulti-User-Role $ulti_user_role;
@ -859,7 +859,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header X-Ulti-User-Email $ulti_user_email;
proxy_set_header X-Ulti-User-Name $ulti_user_name;
proxy_set_header X-Ulti-User-Role $ulti_user_role;
@ -893,7 +893,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header X-Ulti-User-Email $ulti_user_email;
proxy_set_header X-Ulti-User-Name $ulti_user_name;
proxy_set_header X-Ulti-User-Role $ulti_user_role;
@ -907,7 +907,7 @@ server {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host/office;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
@ -928,7 +928,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -941,7 +941,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -957,7 +957,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -971,7 +971,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /api/auth/ {
@ -982,7 +982,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /mail/ {
@ -993,7 +993,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1007,7 +1007,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1024,7 +1024,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1037,7 +1037,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1050,7 +1050,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1063,7 +1063,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1077,7 +1077,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1091,7 +1091,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1114,7 +1114,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1127,7 +1127,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
@ -1141,7 +1141,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /brand/ {
@ -1150,7 +1150,7 @@ server {
proxy_pass http://$mail_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
location ^~ /mail-backgrounds/ {
@ -1159,7 +1159,7 @@ server {
proxy_pass http://$mail_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# Public assets at repo root (launcher icons, etc.)
@ -1169,7 +1169,7 @@ server {
proxy_pass http://$mail_upstream$request_uri;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# Landing page de la suite (servie par le frontend Next).
@ -1181,7 +1181,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
}
# OpenWebUI SPA routes without /ai prefix (client nav + hard reload in UltiAI iframe).
@ -1212,7 +1212,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}

View File

@ -21,6 +21,8 @@ func RepairStoredBodies(text, html string) (string, string) {
text = decodeBareBase64IfNeeded(text)
html = decodeBareBase64IfNeeded(html)
text = stripPlainTextPreheaderPadding(text)
text = repairUTF8Mojibake(text)
html = repairUTF8Mojibake(html)
return text, html
}
@ -49,7 +51,7 @@ func RepairSnippet(snippet string) string {
// RepairSnippetWithBodies decodes a stored snippet and optionally rebuilds from bodies.
func RepairSnippetWithBodies(snippet, bodyText, bodyHTML string) string {
snippet = stripSnippetMarkup(snippet)
snippet = repairUTF8Mojibake(stripSnippetMarkup(snippet))
if decoded := decodeBareQuotedPrintableIfNeeded(snippet); decoded != snippet {
snippet = stripSnippetMarkup(decoded)
}

View File

@ -2,6 +2,7 @@ package imap
import (
"mime"
"regexp"
"strings"
"unicode/utf8"
@ -11,6 +12,9 @@ import (
"golang.org/x/text/transform"
)
// UTF-8 misread as Latin-1: "réactivité" (U+00C3 U+00A9 …).
var utf8MojibakeRE = regexp.MustCompile(`[\xC2\xC3][\x80-\xBF]`)
func charsetFromContentType(contentType string) string {
if contentType == "" {
return ""
@ -77,10 +81,79 @@ func repairRawBytesToUTF8(data []byte) string {
return strings.ToValidUTF8(string(data), "")
}
// repairLegacyCharsetString fixes text already loaded as a Go string with invalid UTF-8 bytes.
func repairLegacyCharsetString(s string) string {
if s == "" || utf8.ValidString(s) {
func looksLikeUTF8Mojibake(s string) bool {
return utf8MojibakeRE.MatchString(s)
}
// repairUTF8Mojibake fixes UTF-8 text misread as Latin-1 (e.g. "réactivité" → "réactivité").
// Repairs pair-by-pair so mixed/corrupted sequences (e.g. NBSP → space in "Déjà") still partially fix.
func repairUTF8Mojibake(s string) string {
if s == "" || !looksLikeUTF8Mojibake(s) {
return s
}
return repairRawBytesToUTF8([]byte(s))
runes := []rune(s)
var b strings.Builder
b.Grow(len(s))
for i := 0; i < len(runes); i++ {
r := runes[i]
if (r == 0xC2 || r == 0xC3) && i+1 < len(runes) {
next := runes[i+1]
if next >= 0x80 && next <= 0xBF {
seq := []byte{byte(r), byte(next)}
if utf8.Valid(seq) {
decoded, _ := utf8.DecodeRune(seq)
b.WriteRune(decoded)
i++
continue
}
}
}
b.WriteRune(r)
}
out := b.String()
if out == s {
return s
}
return repairLoneMojibakeLeaders(out)
}
func repairLoneMojibakeLeaders(s string) string {
runes := []rune(s)
var b strings.Builder
b.Grow(len(s))
for i := 0; i < len(runes); i++ {
r := runes[i]
if r == 0xC3 || r == 0xC2 {
if i+1 < len(runes) && isLoneMojibakeLeaderBoundary(runes[i+1]) {
if r == 0xC3 {
b.WriteRune('à')
} else {
b.WriteRune('Â')
}
continue
}
}
b.WriteRune(r)
}
return b.String()
}
func isLoneMojibakeLeaderBoundary(r rune) bool {
switch r {
case ' ', '\t', ',', '.', ';', ':', '!', '?':
return true
default:
return false
}
}
// repairLegacyCharsetString fixes text already loaded as a Go string with invalid UTF-8 bytes.
func repairLegacyCharsetString(s string) string {
if s == "" {
return s
}
if !utf8.ValidString(s) {
s = repairRawBytesToUTF8([]byte(s))
}
return repairUTF8Mojibake(s)
}

View File

@ -28,6 +28,44 @@ func TestParseBody_iso88591Charset(t *testing.T) {
}
}
func TestRepairUTF8Mojibake_doubleEncodedFrench(t *testing.T) {
raw := "Si elle bouge, tu perds en réactivité. La NEXOR a été pensée pour ça."
repaired := repairUTF8Mojibake(raw)
want := "Si elle bouge, tu perds en réactivité. La NEXOR a été pensée pour ça."
if repaired != want {
t.Fatalf("repaired = %q, want %q", repaired, want)
}
}
func TestRepairUTF8Mojibake_longMarketingBody(t *testing.T) {
raw := "Si elle bouge, tu perds en réactivité. La NEXOR a été pensée pour ça. Nylon ultra résistant, boucle rapide, compatible 100% F1 et TSI. Sobre, solide, zéro fioritures. Détache la sangle, passe-la dans les passants, c'est prêt. Déjà adoptée par +8 000 pompiers en Europe. Elle était en rupture. Elle est de retour."
repaired := repairUTF8Mojibake(raw)
if strings.Contains(repaired, "Ã") {
t.Fatalf("repaired still has mojibake: %q", repaired)
}
if !strings.Contains(repaired, "réactivité") || !strings.Contains(repaired, "Déjà") {
t.Fatalf("repaired = %q", repaired)
}
}
func TestRepairUTF8Mojibake_leavesValidUTF8Untouched(t *testing.T) {
raw := "Si elle bouge, tu perds en réactivité."
if got := repairUTF8Mojibake(raw); got != raw {
t.Fatalf("repaired = %q, want unchanged", got)
}
}
func TestRepairSnippetWithBodies_mojibakePreview(t *testing.T) {
stored := "Nylon ultra résistant, boucle rapide, zéro fioritures."
got := RepairSnippetWithBodies(stored, "", "")
if strings.Contains(got, "Ã") {
t.Fatalf("snippet = %q, want accents repaired", got)
}
if !strings.Contains(got, "résistant") || !strings.Contains(got, "zéro") {
t.Fatalf("snippet = %q", got)
}
}
func TestRepairLegacyCharsetString_latin1BytesInString(t *testing.T) {
// Simulates DB row stored before charset decode (raw Latin-1 bytes in text column).
raw := string([]byte{0x56, 0x6f, 0x75, 0x73, 0x20, 0x72, 0xe9, 0x75, 0x6e, 0x69, 0x6f, 0x6e})