Merge branch 'stable' into unstable

This commit is contained in:
ChrispyBacon-dev 2026-05-02 10:15:54 +02:00
commit a6789ac194
16 changed files with 724 additions and 145 deletions

View file

@ -38,6 +38,17 @@ All notable changes to this project will be documented in this file.
### Fixed
- **Webmail - Dark Mode:** Placeholder text in the reply composer and input backgrounds in the settings panel were broken in dark mode due to a Vue scoped CSS compilation issue. Fixed throughout.
### Hotfixes (post-release)
- **Inbound - R2 Cron Delivery:** Emails buffered in R2 were not delivered when the EML `To:` header differed from the SMTP envelope recipient (e.g. GitHub mailing list addresses). Mail manager now falls back through payload `resolved_mailbox`, EML delivery headers (`X-GitHub-Recipient-Address`, `Delivered-To`), and the `Received: for <addr>` header.
- **Inbound - Worker `list()` Metadata:** Cloudflare R2 `list()` does not return `customMetadata` unless explicitly requested. Added `include: ['customMetadata']` to the cron sweep so envelope metadata is available on retry.
- **Inbound - R2 Delete Error Handling:** A transient R2 delete failure in the success path caused a false HTTP 500 response despite the message being delivered. Now try/except with a warning log; cron cleans up any orphaned object.
- **Inbound - Empty Message-ID:** Emails with no `Message-ID` header produced an empty string, causing a UNIQUE constraint collision. A UUID fallback is now generated.
- **Inbound - Attachment Filename Safety:** Filenames with path traversal components (`..`) are now stripped via `os.path.basename`. Duplicate filenames within the same message get a numeric suffix instead of overwriting each other on disk.
- **Worker - KV Availability Guard:** Alias KV lookups in the `email()` handler now check `typeof env.QUOTA_KV !== 'undefined'` before use, matching the pattern already applied to quota checks. A missing KV binding previously caused all alias mail to be silently rejected.
- **Worker - Subject Metadata Truncation:** Subject stored in R2 `customMetadata` is now capped at 500 characters to stay within Cloudflare's 2 KB per-object metadata limit.
- **Worker - R2 Object TTL:** The cron sweep now expires and deletes `temp_cache/` objects older than 7 days to prevent unbounded accumulation during persistent misconfigurations.
- **Inbound - Multi-Domain Header Requirement:** When multiple domains are configured, an inbound webhook without the `X-DockFlare-Domain` header now returns 400 instead of silently using a non-deterministic domain config.
## [v3.1.0] - 2026-04-16
> **Cloudflare Context:** Cloudflare's Email Service entered public beta today — the same `send_email` Workers binding that powers DockFlare Mail's outbound relay is now generally available. Read the announcement: [Email for Agents](https://blog.cloudflare.com/email-for-agents/)

View file

@ -14,7 +14,7 @@
<a href="https://github.com/ChrispyBacon-dev/DockFlare/stargazers">
<img src="https://img.shields.io/github/stars/ChrispyBacon-dev/DockFlare?style=for-the-badge" alt="Stars">
</a>
<a href="https://github.com/ChrispyBacon-dev/DockFlare/releases"><img src="https://img.shields.io/badge/Release-v3.1.0-blue.svg?style=for-the-badge" alt="Release"></a>
<a href="https://github.com/ChrispyBacon-dev/DockFlare/releases"><img src="https://img.shields.io/badge/Release-v3.1.1-blue.svg?style=for-the-badge" alt="Release"></a>
<a href="https://hub.docker.com/r/alplat/dockflare"><img src="https://img.shields.io/docker/pulls/alplat/dockflare?style=for-the-badge" alt="Docker Pulls"></a>
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg?style=for-the-badge" alt="Python"></a>
<a href="https://github.com/ChrispyBacon-dev/DockFlare/blob/main/LICENSE.MD"><img src="https://img.shields.io/badge/License-GPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
@ -81,10 +81,14 @@ Detailed architecture guide: [https://dockflare.app/architecture](https://dockfl
### One-Liner Install
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
The script checks prerequisites, creates `~/dockflare/`, writes a production-ready `docker-compose.yml`, and starts all services. Open `http://<your-server-ip>:5000` when it finishes and follow the setup wizard.
The script will guide you through:
1. Choosing an install directory (default: `~/dockflare/`).
2. Choosing a local UI port (default: `5000`).
3. Optionally configuring a Cloudflare Tunnel for DockFlare itself.
4. Optionally enabling the Email profile (dockflare-mail-manager + dockflare-webmail).
For full setup documentation, use the project docs site:
@ -269,6 +273,14 @@ networks:
docker compose up -d
```
The Email Suite is an optional, opt-in feature and will not start by default. To include email services, add the `email` profile:
```bash
docker compose --profile email up -d
```
Email setup and provisioning guide: [Email Overview](https://github.com/ChrispyBacon-dev/DockFlare/blob/stable/dockflare/app/templates/docs/en/Email-Overview.md)
3. Open `http://your-server-ip:5000` and complete the setup wizard.
If you are migrating from older environment-based setups, DockFlare can import existing values during onboarding.

View file

@ -45,9 +45,11 @@ export default {
const allowedRecipients = JSON.parse(env.ALLOWED_RECIPIENTS || '[]');
if (!allowedRecipients.includes(message.to)) {
let aliasRecord = null;
try {
aliasRecord = await env.QUOTA_KV.get('alias::' + message.to, 'json');
} catch (_) {}
if (typeof env.QUOTA_KV !== 'undefined') {
try {
aliasRecord = await env.QUOTA_KV.get('alias::' + message.to, 'json');
} catch (_) {}
}
if (!aliasRecord) {
message.setReject("Recipient not allowed");
@ -81,7 +83,7 @@ export default {
to: message.to,
resolved_mailbox: resolvedMailbox || message.to,
via_alias: resolvedMailbox ? "1" : "0",
subject: message.headers.get("subject") || "",
subject: (message.headers.get("subject") || "").slice(0, 500),
receivedAt: receivedAt
}
});
@ -121,6 +123,8 @@ export default {
async scheduled(event, env, ctx) {
console.log("Cron: scanning R2 temp_cache for buffered emails...");
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
const now = Date.now();
let cursor;
let processed = 0;
let failed = 0;
@ -129,11 +133,19 @@ export default {
const list = await env.EMAIL_BUCKET.list({
prefix: "temp_cache/",
limit: 100,
cursor: cursor
cursor: cursor,
include: ['customMetadata']
});
for (const object of list.objects) {
const r2Key = object.key;
if (object.uploaded && (now - object.uploaded.getTime()) > MAX_AGE_MS) {
console.warn(`Cron: expiring ${r2Key} (age > 7d)`);
try { await env.EMAIL_BUCKET.delete(r2Key); } catch (_) {}
failed++;
continue;
}
const meta = object.customMetadata || {};
const messageId = r2Key.replace("temp_cache/", "").replace(".eml", "");

View file

@ -4,26 +4,22 @@ Die Aaleitig zeigt dr schnäuschti Wäg, wie du DockFlare mit em ghärtete Socke
## Option A — Eizeiler-Installation (Empfohle)
Dr schnäuschti Wäg, DockFlare zum Laufe z bringe, isch dr Installations-Skript uf [dockflare.app](https://dockflare.app):
Dr schnäuschti Wäg, DockFlare zum Laufe z bringe, isch dr interaktivi Installations-Skript uf [dockflare.app](https://dockflare.app):
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
Dr Skript macht Folgendes:
1. Prüeft, ob Docker u Docker Compose verfüegbar si.
2. Erstellt `~/dockflare/` u schrybt dort e `docker-compose.yml` ane.
3. Erstellt s Docker-Netzwerk `cloudflare-net`, falls's no nid existiert.
4. Ladet d Images abe u startet alli Dienscht.
5. Zeigt d lokali URL aa, wenn's fertig isch.
Dr Skript füehrt di dür folgende Schritt:
1. Uswahl vom Installationsverzeuchnis (Standard: `~/dockflare/`).
2. Uswahl vom lokale UI-Port (Standard: `5000`).
3. Optionali Konfiguration vomene Cloudflare-Tunnel für DockFlare.
4. Optionali Aktivierig vom E-Mail-Profil (dockflare-mail-manager + dockflare-webmail).
Dernoo schrybt er d `docker-compose.yml`, lot di se aaluege u fragt vor em Starte nochmal nach.
Sobald alles lauft, mach `http://<your-server-ip>:5000` uf u füehrt di dr Iirichtigsassistent dür.
> **Optionali Yberschribige** — setz Umgebigsvariable vor em Pipe, zum d Installation aapasse:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## Option B — Manuälli Docker-Compose-Irichtig

View file

@ -4,26 +4,22 @@ Diese Anleitung zeigt den schnellsten Weg, um DockFlare mit dem gehärteten Sock
## Option A — Einzeiliges Installations-Skript (Empfohlen)
Der schnellste Weg, DockFlare zum Laufen zu bringen, ist das Installations-Skript unter [dockflare.app](https://dockflare.app):
Der schnellste Weg, DockFlare zum Laufen zu bringen, ist das interaktive Installations-Skript unter [dockflare.app](https://dockflare.app):
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
Das Skript führt folgende Schritte durch:
1. Prüft, ob Docker und Docker Compose verfügbar sind.
2. Erstellt `~/dockflare/` und schreibt dort eine `docker-compose.yml`.
3. Erstellt das Docker-Netzwerk `cloudflare-net`, falls es noch nicht existiert.
4. Lädt die Images herunter und startet alle Dienste.
5. Gibt die lokale URL aus, wenn fertig.
Das Skript führt Sie durch folgende Schritte:
1. Auswahl des Installationsverzeichnisses (Standard: `~/dockflare/`).
2. Auswahl des lokalen UI-Ports (Standard: `5000`).
3. Optional: Konfiguration eines Cloudflare-Tunnels für DockFlare selbst.
4. Optional: Aktivierung des E-Mail-Profils (dockflare-mail-manager + dockflare-webmail).
Anschließend schreibt es die `docker-compose.yml`, ermöglicht eine Überprüfung und fragt vor dem Starten nach.
Nach dem Start öffnen Sie `http://<your-server-ip>:5000` und schließen Sie den Einrichtungsassistenten ab.
> **Optionale Überschreibungen** — Setzen Sie Umgebungsvariablen vor dem Ausführen, um die Installation anzupassen:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## Option B — Manuelle Docker-Compose-Einrichtung

View file

@ -4,26 +4,22 @@ This guide walks through the fastest way to run DockFlare with the hardened sock
## Option A — One-Liner Install (Recommended)
The quickest way to get DockFlare running is the install script hosted at [dockflare.app](https://dockflare.app):
The quickest way to get DockFlare running is the interactive install script hosted at [dockflare.app](https://dockflare.app):
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
The script will:
1. Check that Docker and Docker Compose are available.
2. Create `~/dockflare/` and write a `docker-compose.yml` there.
3. Create the `cloudflare-net` Docker network if it does not exist.
4. Pull the images and start all services.
5. Print the local URL when done.
The script will guide you through:
1. Choosing an install directory (default: `~/dockflare/`).
2. Choosing a local UI port (default: `5000`).
3. Optionally configuring a Cloudflare Tunnel for DockFlare itself.
4. Optionally enabling the Email profile (dockflare-mail-manager + dockflare-webmail).
It then writes `docker-compose.yml`, lets you review it, and asks before pulling and starting the stack.
Once running, open `http://<your-server-ip>:5000` and complete the setup wizard.
> **Optional overrides** — set environment variables before piping to control the install:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## Option B — Manual Docker Compose

View file

@ -4,26 +4,22 @@ Esta guía explica la forma más rápida de ejecutar DockFlare con el socket pro
## Opción A — Instalación en un solo comando (Recomendado)
La forma más rápida de poner en marcha DockFlare es el script de instalación alojado en [dockflare.app](https://dockflare.app):
La forma más rápida de poner en marcha DockFlare es el script de instalación interactivo alojado en [dockflare.app](https://dockflare.app):
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
El script realizará lo siguiente:
1. Comprobar que Docker y Docker Compose están disponibles.
2. Crear `~/dockflare/` y escribir un archivo `docker-compose.yml` allí.
3. Crear la red Docker `cloudflare-net` si no existe.
4. Descargar las imágenes e iniciar todos los servicios.
5. Mostrar la URL local al finalizar.
El script le guiará a través de:
1. Elección del directorio de instalación (predeterminado: `~/dockflare/`).
2. Elección del puerto local de la interfaz (predeterminado: `5000`).
3. Configuración opcional de un túnel de Cloudflare para DockFlare.
4. Activación opcional del perfil de correo electrónico (dockflare-mail-manager + dockflare-webmail).
A continuación, escribe el archivo `docker-compose.yml`, permite revisarlo y pregunta antes de iniciar el stack.
Una vez en ejecución, abra `http://<your-server-ip>:5000` y complete el asistente de configuración.
> **Opciones de personalización** — establezca variables de entorno antes de ejecutar el comando para controlar la instalación:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## Opción B — Docker Compose manual

View file

@ -4,26 +4,22 @@ Ce guide présente le moyen le plus rapide d'exécuter DockFlare avec un socket-
## Option A — Installation en une seule commande (Recommandé)
La façon la plus rapide de démarrer DockFlare est d'utiliser le script d'installation hébergé sur [dockflare.app](https://dockflare.app) :
La façon la plus rapide de démarrer DockFlare est d'utiliser le script d'installation interactif hébergé sur [dockflare.app](https://dockflare.app) :
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
Le script va :
1. Vérifier que Docker et Docker Compose sont disponibles.
2. Créer `~/dockflare/` et y écrire un fichier `docker-compose.yml`.
3. Créer le réseau Docker `cloudflare-net` s'il n'existe pas encore.
4. Télécharger les images et démarrer tous les services.
5. Afficher l'URL locale une fois terminé.
Le script vous guidera à travers :
1. Le choix du répertoire d'installation (par défaut : `~/dockflare/`).
2. Le choix du port local de l'interface (par défaut : `5000`).
3. La configuration optionnelle d'un tunnel Cloudflare pour DockFlare.
4. L'activation optionnelle du profil e-mail (dockflare-mail-manager + dockflare-webmail).
Il génère ensuite le fichier `docker-compose.yml`, vous permet de le vérifier et demande confirmation avant de démarrer le stack.
Une fois démarré, ouvrez `http://<your-server-ip>:5000` et suivez l'assistant de configuration.
> **Substitutions optionnelles** — définissez des variables d'environnement avant la commande pour personnaliser l'installation :
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## Option B — Configuration manuelle avec Docker Compose

View file

@ -4,26 +4,22 @@ Panduan ini menjelaskan cara tercepat untuk menjalankan DockFlare dengan socket
## Opsi A — Instalasi Satu Perintah (Direkomendasikan)
Cara tercepat untuk menjalankan DockFlare adalah menggunakan skrip instalasi yang tersedia di [dockflare.app](https://dockflare.app):
Cara tercepat untuk menjalankan DockFlare adalah menggunakan skrip instalasi interaktif yang tersedia di [dockflare.app](https://dockflare.app):
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
Skrip akan melakukan hal berikut:
1. Memeriksa bahwa Docker dan Docker Compose tersedia.
2. Membuat `~/dockflare/` dan menulis file `docker-compose.yml` di sana.
3. Membuat jaringan Docker `cloudflare-net` jika belum ada.
4. Menarik image dan memulai semua layanan.
5. Menampilkan URL lokal setelah selesai.
Skrip akan memandu Anda melalui:
1. Memilih direktori instalasi (default: `~/dockflare/`).
2. Memilih port UI lokal (default: `5000`).
3. Konfigurasi opsional tunnel Cloudflare untuk DockFlare.
4. Mengaktifkan profil email secara opsional (dockflare-mail-manager + dockflare-webmail).
Kemudian menghasilkan `docker-compose.yml`, memungkinkan Anda meninjaunya, dan meminta konfirmasi sebelum memulai stack.
Setelah berjalan, buka `http://<your-server-ip>:5000` dan selesaikan wizard pengaturan.
> **Opsi kustomisasi** — atur variabel lingkungan sebelum menjalankan perintah untuk mengontrol instalasi:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## Opsi B — Docker Compose Manual

View file

@ -4,26 +4,22 @@ Questa guida illustra il modo più veloce per eseguire DockFlare con un `docker-
## Opzione A — Installazione con un solo comando (Consigliato)
Il modo più rapido per avviare DockFlare è lo script di installazione disponibile su [dockflare.app](https://dockflare.app):
Il modo più rapido per avviare DockFlare è lo script di installazione interattivo disponibile su [dockflare.app](https://dockflare.app):
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
Lo script eseguirà le seguenti operazioni:
1. Verificare che Docker e Docker Compose siano disponibili.
2. Creare `~/dockflare/` e scrivere un file `docker-compose.yml` al suo interno.
3. Creare la rete Docker `cloudflare-net` se non esiste ancora.
4. Scaricare le immagini e avviare tutti i servizi.
5. Stampare l'URL locale al termine.
Lo script ti guiderà attraverso:
1. La scelta della directory di installazione (predefinita: `~/dockflare/`).
2. La scelta della porta locale dell'interfaccia (predefinita: `5000`).
3. La configurazione opzionale di un tunnel Cloudflare per DockFlare.
4. L'attivazione opzionale del profilo e-mail (dockflare-mail-manager + dockflare-webmail).
Genera quindi il file `docker-compose.yml`, ti permette di verificarlo e chiede conferma prima di avviare lo stack.
Una volta avviato, apri `http://<your-server-ip>:5000` e completa la procedura guidata di configurazione.
> **Sostituzioni opzionali** — imposta le variabili d'ambiente prima del pipe per controllare l'installazione:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## Opzione B — Docker Compose manuale

View file

@ -4,26 +4,22 @@
## オプション A — ワンライナーインストール(推奨)
DockFlare を最短で起動するには、[dockflare.app](https://dockflare.app) でホストされているインストールスクリプトを使用します:
DockFlare を最短で起動するには、[dockflare.app](https://dockflare.app) でホストされているインタラクティブなインストールスクリプトを使用します:
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
スクリプトは以下を実行します:
1. Docker と Docker Compose が利用可能かどうかを確認します。
2. `~/dockflare/` を作成し、そこに `docker-compose.yml` を書き込みます。
3. `cloudflare-net` Docker ネットワークが存在しない場合は作成します。
4. イメージをプルしてすべてのサービスを起動します。
5. 完了したらローカル URL を表示します。
スクリプトは以下の手順を案内します:
1. インストールディレクトリの選択(デフォルト:`~/dockflare/`)。
2. ローカル UI ポートの選択(デフォルト:`5000`)。
3. DockFlare 自身の Cloudflare トンネルの設定(オプション)。
4. メールプロファイルの有効化オプションdockflare-mail-manager + dockflare-webmail
その後、`docker-compose.yml` を生成し、確認を求めてからスタックを起動します。
起動後、`http://<your-server-ip>:5000` を開いてセットアップウィザードを完了してください。
> **オプションの上書き設定** — パイプする前に環境変数を設定してインストールをカスタマイズできます:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## オプション B — 手動 Docker Compose

View file

@ -4,26 +4,22 @@ Ten przewodnik pokazuje najszybszy sposób uruchomienia DockFlare z wzmocnionym
## Opcja A — Instalacja jednym poleceniem (Zalecane)
Najszybszym sposobem uruchomienia DockFlare jest skrypt instalacyjny dostępny na [dockflare.app](https://dockflare.app):
Najszybszym sposobem uruchomienia DockFlare jest interaktywny skrypt instalacyjny dostępny na [dockflare.app](https://dockflare.app):
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
Skrypt wykona następujące czynności:
1. Sprawdzi, czy Docker i Docker Compose są dostępne.
2. Utworzy katalog `~/dockflare/` i zapisze tam plik `docker-compose.yml`.
3. Utworzy sieć Docker `cloudflare-net`, jeśli jeszcze nie istnieje.
4. Pobierze obrazy i uruchomi wszystkie usługi.
5. Wyświetli lokalny adres URL po zakończeniu.
Skrypt przeprowadzi Cię przez:
1. Wybór katalogu instalacji (domyślnie: `~/dockflare/`).
2. Wybór lokalnego portu interfejsu (domyślnie: `5000`).
3. Opcjonalną konfigurację tunelu Cloudflare dla DockFlare.
4. Opcjonalne włączenie profilu e-mail (dockflare-mail-manager + dockflare-webmail).
Następnie generuje plik `docker-compose.yml`, umożliwia jego przegląd i pyta o potwierdzenie przed uruchomieniem stacka.
Po uruchomieniu otwórz `http://<your-server-ip>:5000` i ukończ kreator konfiguracji.
> **Opcjonalne parametry** — ustaw zmienne środowiskowe przed uruchomieniem, aby dostosować instalację:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## Opcja B — Ręczna konfiguracja Docker Compose

View file

@ -4,26 +4,22 @@
## 选项 A — 一键安装(推荐)
启动 DockFlare 最快捷的方式是使用托管在 [dockflare.app](https://dockflare.app) 的安装脚本:
启动 DockFlare 最快捷的方式是使用托管在 [dockflare.app](https://dockflare.app) 的交互式安装脚本:
```bash
curl -fsSL https://dockflare.app/install.sh | bash
bash <(curl -fsSL https://dockflare.app/install.sh)
```
脚本将执行以下操作:
1. 检查 Docker 和 Docker Compose 是否可用。
2. 创建 `~/dockflare/` 目录并在其中写入 `docker-compose.yml`
3. 如果 `cloudflare-net` Docker 网络不存在则自动创建。
4. 拉取镜像并启动所有服务。
5. 完成后打印本地访问地址。
脚本将引导您完成以下步骤:
1. 选择安装目录(默认:`~/dockflare/`)。
2. 选择本地 UI 端口(默认:`5000`)。
3. 可选:为 DockFlare 配置 Cloudflare 隧道。
4. 可选启用邮件配置文件dockflare-mail-manager + dockflare-webmail
随后生成 `docker-compose.yml`,允许您查看,并在启动前确认。
启动后,打开 `http://<your-server-ip>:5000` 并完成设置向导。
> **可选覆盖参数** — 在管道命令前设置环境变量以自定义安装行为:
> ```bash
> DOCKFLARE_PORT=8080 DOCKFLARE_DIR=/opt/dockflare curl -fsSL https://dockflare.app/install.sh | bash
> ```
---
## 选项 B — 手动 Docker Compose

543
install.sh Normal file
View file

@ -0,0 +1,543 @@
#!/usr/bin/env bash
set -euo pipefail
# -----------------------------------------------------------------------------
# DockFlare Master — Install Script
# https://dockflare.app
#
# Usage (interactive — recommended):
# bash <(curl -fsSL https://dockflare.app/install.sh)
#
#
# Optional env overrides (non-interactive / pipe mode):
# DOCKFLARE_PORT — host port to expose DockFlare UI on (default: 5000)
# DOCKFLARE_DIR — install directory (default: $HOME/dockflare)
# DOCKFLARE_UID — UID for data volume ownership (default: 65532)
# DOCKFLARE_GID — GID for data volume ownership (default: 65532)
# DOCKFLARE_EMAIL — set to "true" to enable email profile (default: false)
# DOCKFLARE_TLD — base domain e.g. example.com (required if EMAIL=true)
# DOCKFLARE_DOMAIN — DockFlare master domain (default: dockflare.$DOCKFLARE_TLD)
#
# Examples:
# bash <(curl -fsSL https://dockflare.app/install.sh)
# local run
# bash install.sh
# -----------------------------------------------------------------------------
DOCKFLARE_PORT="${DOCKFLARE_PORT:-5000}"
DOCKFLARE_DIR="${DOCKFLARE_DIR:-$HOME/dockflare}"
DOCKFLARE_UID="${DOCKFLARE_UID:-65532}"
DOCKFLARE_GID="${DOCKFLARE_GID:-65532}"
DOCKFLARE_EMAIL="${DOCKFLARE_EMAIL:-false}"
DOCKFLARE_TLD="${DOCKFLARE_TLD:-}"
DOCKFLARE_DOMAIN="${DOCKFLARE_DOMAIN:-}"
BOLD="\033[1m"
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
RED="\033[0;31m"
CYAN="\033[0;36m"
BLUE="\033[0;34m"
ORANGE="\033[0;33m"
RESET="\033[0m"
# Detect interactive mode (stdin is a TTY)
if [ -t 0 ]; then
IS_INTERACTIVE=true
else
IS_INTERACTIVE=false
fi
banner() {
# Split each line at position 35: DOCK (blue) | FLARE (orange)
local lines=(
" ██████╗ ██████╗ ██████╗██╗ ██╗███████╗██╗ █████╗ ██████╗ ███████╗"
" ██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝██╔════╝██║ ██╔══██╗██╔══██╗██╔════╝"
" ██║ ██║██║ ██║██║ █████╔╝ █████╗ ██║ ███████║██████╔╝█████╗ "
" ██║ ██║██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ██╔══██║██╔══██╗██╔══╝ "
" ██████╔╝╚██████╔╝╚██████╗██║ ██╗██║ ███████╗██║ ██║██║ ██║███████╗"
" ╚═════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝"
)
echo ""
for line in "${lines[@]}"; do
echo -e "${BLUE}${BOLD}${line:0:35}${ORANGE}${line:35}${RESET}"
done
echo ""
echo -e " ${BOLD}Master Installer${RESET} · https://dockflare.app"
echo ""
}
info() { echo -e " ${GREEN}${RESET} $*"; }
warn() { echo -e " ${YELLOW}${RESET} $*"; }
error() { echo -e " ${RED}${RESET} $*" >&2; }
section() { echo ""; echo -e " ${BOLD}$*${RESET}"; echo " $(printf '─%.0s' {1..60})"; }
# prompt <var_name> <label> <default>
# Reads user input; falls back to default if empty or non-interactive.
prompt() {
local var_name="$1"
local label="$2"
local default="$3"
local input=""
if [ "$IS_INTERACTIVE" = true ]; then
echo -e "\n ${CYAN}?${RESET} ${label}"
printf " [default: %s]: " "$default"
read -r input </dev/tty
fi
[ -z "$input" ] && input="$default"
printf -v "$var_name" '%s' "$input"
}
# prompt_yn <var_name> <label> <default: y|n>
# Sets named variable to "true" or "false".
prompt_yn() {
local var_name="$1"
local label="$2"
local default="${3:-n}"
local hint input=""
[ "$default" = "y" ] && hint="Y/n" || hint="y/N"
if [ "$IS_INTERACTIVE" = true ]; then
echo -e "\n ${CYAN}?${RESET} ${label}"
printf " [%s]: " "$hint"
read -r input </dev/tty
fi
[ -z "$input" ] && input="$default"
case "$input" in
[Yy]*) printf -v "$var_name" '%s' "true" ;;
*) printf -v "$var_name" '%s' "false" ;;
esac
}
banner
# Stash original defaults so "start over" can reset to them
DOCKFLARE_DIR_ORIG="$DOCKFLARE_DIR"
DOCKFLARE_PORT_ORIG="$DOCKFLARE_PORT"
# -----------------------------------------------------------------------------
# Interactive configuration
# -----------------------------------------------------------------------------
while true; do
if [ "$IS_INTERACTIVE" = true ]; then
section "Configuration"
echo -e " Press ${BOLD}Enter${RESET} to accept each default."
else
section "Configuration (non-interactive — using defaults / env overrides)"
fi
# 1. Install directory
prompt DOCKFLARE_DIR "Where should DockFlare be installed?" "$DOCKFLARE_DIR"
# 2. Port
prompt DOCKFLARE_PORT "Local UI port — DockFlare will be reachable at http://localhost:${DOCKFLARE_PORT} (change if that port is already in use)" "$DOCKFLARE_PORT"
# 3. Email features
EMAIL_ENABLED=false
if [ "$DOCKFLARE_EMAIL" = "true" ]; then
EMAIL_ENABLED=true
else
prompt_yn EMAIL_ENABLED "Enable DockFlare Email features? (dockflare-mail-manager + dockflare-webmail)" "n"
fi
if [ "$EMAIL_ENABLED" = "true" ]; then
# 3a. Base domain
prompt DOCKFLARE_TLD "Enter your base domain (e.g. example.com):" "${DOCKFLARE_TLD:-}"
if [ -z "$DOCKFLARE_TLD" ]; then
error "A base domain is required when Email features are enabled."
exit 1
fi
# 3b. DockFlare master domain
_default_domain="dockflare.${DOCKFLARE_TLD}"
prompt DOCKFLARE_DOMAIN "DockFlare master domain?" "${DOCKFLARE_DOMAIN:-$_default_domain}"
# 3c. Webmail domain
_default_mail="mail.${DOCKFLARE_TLD}"
prompt MAIL_DOMAIN "Webmail domain?" "${_default_mail}"
echo ""
info "Base domain: ${DOCKFLARE_TLD}"
info "DockFlare domain: ${DOCKFLARE_DOMAIN}"
info "Webmail domain: ${MAIL_DOMAIN}"
else
# 4. Cloudflare Tunnel self-expose for DockFlare (email disabled path)
TUNNEL_ENABLED=false
prompt_yn TUNNEL_ENABLED "Expose DockFlare via a Cloudflare Tunnel? (sets up dockflare labels)" "n"
if [ "$TUNNEL_ENABLED" = "true" ]; then
prompt DOCKFLARE_DOMAIN "DockFlare tunnel domain? (e.g. dockflare.example.com)" "${DOCKFLARE_DOMAIN:-}"
if [ -z "$DOCKFLARE_DOMAIN" ]; then
error "A domain is required to set up the Cloudflare Tunnel label."
exit 1
fi
else
# Placeholders kept in compose so the user can manually fill them in later
DOCKFLARE_DOMAIN="dockflare.TLD"
fi
MAIL_DOMAIN="mail.dockflare.TLD"
fi
echo ""
info "Install directory: ${DOCKFLARE_DIR}"
info "UI port: ${DOCKFLARE_PORT}"
info "Email profile: ${EMAIL_ENABLED}"
if [ "$EMAIL_ENABLED" = "false" ]; then
if [ "${TUNNEL_ENABLED:-false}" = "true" ]; then
info "Tunnel domain: ${DOCKFLARE_DOMAIN}"
else
info "Cloudflare Tunnel: disabled (labels left as comments)"
fi
fi
# Summary confirmation — skip loop in non-interactive mode
CONFIG_OK=true
prompt_yn CONFIG_OK "Does this look correct? (No = start over)" "y"
[ "$CONFIG_OK" = "true" ] && break
echo ""
warn "Starting over — your answers have been cleared."
DOCKFLARE_DIR="${DOCKFLARE_DIR_ORIG}"
DOCKFLARE_PORT="${DOCKFLARE_PORT_ORIG}"
DOCKFLARE_TLD=""
DOCKFLARE_DOMAIN=""
done
# -----------------------------------------------------------------------------
# Preflight checks
# -----------------------------------------------------------------------------
section "Checking prerequisites"
if ! command -v docker &>/dev/null; then
error "Docker is not installed. Please install Docker first: https://docs.docker.com/get-docker/"
exit 1
fi
info "Docker found: $(docker --version)"
COMPOSE_CMD=""
if docker compose version &>/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose &>/dev/null; then
COMPOSE_CMD="docker-compose"
else
error "Docker Compose (v2) is not available. Please install it: https://docs.docker.com/compose/install/"
exit 1
fi
info "Docker Compose found: $($COMPOSE_CMD version --short 2>/dev/null || $COMPOSE_CMD version)"
if ! docker info &>/dev/null; then
error "Docker daemon is not running or current user lacks permissions."
error "Try: sudo usermod -aG docker \$USER then log out and back in."
exit 1
fi
info "Docker daemon is running"
# -----------------------------------------------------------------------------
# Install directory
# -----------------------------------------------------------------------------
section "Setting up install directory"
if [ -d "$DOCKFLARE_DIR" ]; then
info "Directory exists: $DOCKFLARE_DIR"
if [ -f "$DOCKFLARE_DIR/docker-compose.yml" ]; then
warn "Existing docker-compose.yml found at ${DOCKFLARE_DIR}/docker-compose.yml"
echo ""
echo -e " How would you like to proceed?"
echo -e " ${BOLD} 1)${RESET} Overwrite the existing file"
echo -e " ${BOLD} 2)${RESET} Rename it (timestamped backup) and continue"
echo -e " ${BOLD} 3)${RESET} Abort — exit so you can review the file manually"
echo ""
COMPOSE_ACTION=""
if [ "$IS_INTERACTIVE" = true ]; then
printf " [1/2/3, default: 1]: "
read -r COMPOSE_ACTION </dev/tty
fi
[ -z "$COMPOSE_ACTION" ] && COMPOSE_ACTION="1"
case "$COMPOSE_ACTION" in
2)
_backup="${DOCKFLARE_DIR}/docker-compose.yml.$(date +%Y-%m-%d_%H%M%S)"
mv "$DOCKFLARE_DIR/docker-compose.yml" "$_backup"
info "Existing file renamed to: $_backup"
;;
3)
echo ""
echo -e " ${YELLOW}Aborted.${RESET} No changes made."
echo -e " Review your file at: ${DOCKFLARE_DIR}/docker-compose.yml"
echo ""
exit 0
;;
*)
info "Existing file will be overwritten."
;;
esac
fi
else
mkdir -p "$DOCKFLARE_DIR"
info "Created directory: $DOCKFLARE_DIR"
fi
# -----------------------------------------------------------------------------
# Docker network
# -----------------------------------------------------------------------------
section "Preparing Docker network"
if docker network inspect cloudflare-net &>/dev/null 2>&1; then
info "Network 'cloudflare-net' already exists"
else
docker network create cloudflare-net
info "Created network: cloudflare-net"
fi
# -----------------------------------------------------------------------------
# Build compose fragments based on configuration
# -----------------------------------------------------------------------------
# Self-expose labels block for the dockflare service.
# The entire labels: key is included or fully commented out to avoid YAML null-mapping errors.
# - Email enabled OR tunnel enabled → labels: uncommented with real domain
# - Neither → entire block commented out (safe placeholder for later)
if [ "$EMAIL_ENABLED" = "true" ] || [ "${TUNNEL_ENABLED:-false}" = "true" ]; then
LABELS_BLOCK="\
labels:
- dockflare.enable=true
- dockflare.hostname=${DOCKFLARE_DOMAIN}
- dockflare.service=http://dockflare:5000
#- dockflare.access.group=YOUR-ACCESS-GROUP-ID"
else
LABELS_BLOCK="\
# -- Cloudflare Tunnel labels (via DockFlare) OPTIONAL --
# Uncomment and replace dockflare.TLD with your domain to expose DockFlare via its own tunnel:
#labels:
# - dockflare.enable=true
# - dockflare.hostname=dockflare.TLD
# - dockflare.service=http://dockflare:5000
# - dockflare.access.group=YOUR-ACCESS-GROUP-ID"
fi
# Webmail label comment — only needed when placeholders are still in place
if [ "$EMAIL_ENABLED" = "true" ]; then
WEBMAIL_LABEL_NOTE=""
else
WEBMAIL_LABEL_NOTE=" # Replace dockflare.TLD / mail.dockflare.TLD with your actual domain"$'\n'
fi
# -----------------------------------------------------------------------------
# Write docker-compose.yml
# -----------------------------------------------------------------------------
section "Writing docker-compose.yml"
cat > "$DOCKFLARE_DIR/docker-compose.yml" <<COMPOSE
version: '3.8'
services:
docker-socket-proxy:
image: tecnativa/docker-socket-proxy:v0.4.1
container_name: docker-socket-proxy
restart: unless-stopped
logging:
driver: "none"
environment:
- DOCKER_HOST=unix:///var/run/docker.sock
- CONTAINERS=1
- EVENTS=1
- NETWORKS=1
- IMAGES=1
- POST=1
- PING=1
- INFO=1
- EXEC=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- dockflare-internal
dockflare-init:
image: alpine:3.20
command: ["sh", "-c", "chown -R ${DOCKFLARE_UID}:${DOCKFLARE_GID} /app/data"]
volumes:
- dockflare_data:/app/data
networks:
- dockflare-internal
restart: "no"
dockflare:
image: alplat/dockflare:stable
container_name: dockflare
restart: unless-stopped
ports:
- "${DOCKFLARE_PORT}:5000" # Optional: comment out once exposed via Cloudflare Tunnel with an Access Policy to restrict access to tunnel-only
${LABELS_BLOCK}
volumes:
- dockflare_data:/app/data
environment:
- REDIS_URL=redis://redis:6379/0
- REDIS_DB_INDEX=0
- DOCKER_HOST=tcp://docker-socket-proxy:2375
#- LOG_LEVEL=DEBUG
depends_on:
docker-socket-proxy:
condition: service_started
dockflare-init:
condition: service_completed_successfully
redis:
condition: service_started
networks:
- cloudflare-net
- dockflare-internal
redis:
image: redis:7-alpine
container_name: dockflare-redis
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no"]
logging:
driver: "none"
volumes:
- dockflare_redis:/data
networks:
- dockflare-internal
dockflare-mail-manager:
image: alplat/dockflare-mail-manager:stable
container_name: dockflare-mail-manager
restart: unless-stopped
profiles: ["email"]
environment:
- DOCKFLARE_MASTER_URL=http://dockflare:5000
- MAIL_DATA_PATH=/data
volumes:
- mail_data:/data
depends_on:
dockflare:
condition: service_started
networks:
- cloudflare-net
- dockflare-internal
dockflare-webmail:
image: alplat/dockflare-webmail:stable
container_name: dockflare-webmail
restart: unless-stopped
profiles: ["email"]
environment:
- DOCKFLARE_MASTER_URL=https://${DOCKFLARE_DOMAIN}
labels:
${WEBMAIL_LABEL_NOTE} - dockflare.enable=true
- dockflare.hostname=${MAIL_DOMAIN}
- dockflare.service=http://dockflare-webmail:80
depends_on:
dockflare-mail-manager:
condition: service_started
networks:
- cloudflare-net
- dockflare-internal
volumes:
dockflare_data:
dockflare_redis:
mail_data:
networks:
cloudflare-net:
name: cloudflare-net
external: true
dockflare-internal:
name: dockflare-internal
COMPOSE
info "docker-compose.yml written to $DOCKFLARE_DIR"
echo ""
echo -e " ${CYAN}Review:${RESET} ${DOCKFLARE_DIR}/docker-compose.yml"
SHOW_COMPOSE=false
prompt_yn SHOW_COMPOSE "Show the generated docker-compose.yml?" "n"
if [ "$SHOW_COMPOSE" = "true" ]; then
echo ""
echo -e " ${BOLD}$(printf '─%.0s' {1..60})${RESET}"
cat "$DOCKFLARE_DIR/docker-compose.yml"
echo -e " ${BOLD}$(printf '─%.0s' {1..60})${RESET}"
echo ""
fi
# -----------------------------------------------------------------------------
# Pull images & start
# -----------------------------------------------------------------------------
PROFILE_FLAGS=""
if [ "$EMAIL_ENABLED" = "true" ]; then
PROFILE_FLAGS="--profile email"
fi
START_NOW=true
prompt_yn START_NOW "Pull images and start DockFlare now?" "y"
if [ "$START_NOW" = "false" ]; then
echo ""
echo -e " ${YELLOW}Skipped. To start manually:${RESET}"
if [ "$EMAIL_ENABLED" = "true" ]; then
echo -e " cd ${DOCKFLARE_DIR} && ${COMPOSE_CMD} --profile email up -d"
else
echo -e " cd ${DOCKFLARE_DIR} && ${COMPOSE_CMD} up -d"
fi
echo ""
echo -e " ${YELLOW}${BOLD}Security recommendation:${RESET}"
echo -e " Once DockFlare is fully configured, it is recommended to remove the exposed port"
echo -e " and restrict access exclusively via a Cloudflare Tunnel with an Access Policy."
echo -e " See the ${BOLD}ports:${RESET} comment in ${DOCKFLARE_DIR}/docker-compose.yml for details."
echo ""
echo -e " ${CYAN}Docs:${RESET} https://dockflare.app/docs"
echo ""
exit 0
fi
section "Pulling images"
$COMPOSE_CMD -f "$DOCKFLARE_DIR/docker-compose.yml" $PROFILE_FLAGS pull
section "Starting DockFlare"
$COMPOSE_CMD -f "$DOCKFLARE_DIR/docker-compose.yml" $PROFILE_FLAGS up -d
# -----------------------------------------------------------------------------
# Done
# -----------------------------------------------------------------------------
LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")
echo ""
echo -e " ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e " ${GREEN}${BOLD} DockFlare is running!${RESET}"
echo -e " ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo ""
echo -e " ${BOLD}Local access:${RESET} http://localhost:${DOCKFLARE_PORT}"
echo -e " ${BOLD}Network access:${RESET} http://${LOCAL_IP}:${DOCKFLARE_PORT}"
echo -e " ${BOLD}Install path:${RESET} ${DOCKFLARE_DIR}"
echo ""
echo -e " ${YELLOW}${BOLD}Next steps:${RESET}"
echo -e " ${YELLOW}1.${RESET} Open the URL above and complete the setup wizard"
echo -e " ${YELLOW}2.${RESET} Add your Cloudflare API credentials"
echo -e " ${YELLOW}3.${RESET} DockFlare will start managing your tunnels automatically"
if [ "$EMAIL_ENABLED" = "true" ]; then
echo ""
echo -e " ${YELLOW}${BOLD}Email profile:${RESET}"
echo -e " ${YELLOW}4.${RESET} DockFlare will be reachable at https://${DOCKFLARE_DOMAIN}"
echo -e " ${YELLOW}5.${RESET} Webmail will be reachable at https://${MAIL_DOMAIN}"
echo -e " ${YELLOW}6.${RESET} Restart if needed: cd ${DOCKFLARE_DIR} && docker compose --profile email up -d"
fi
echo ""
echo -e " ${YELLOW}${BOLD}Security recommendation:${RESET}"
echo -e " Once DockFlare is fully configured, it is recommended to remove the exposed local port"
echo -e " and restrict access exclusively via a Cloudflare Tunnel with an CloudFlare Zero Trust Access Policy."
echo -e " See the ${BOLD}ports:${RESET} comment in ${DOCKFLARE_DIR}/docker-compose.yml for details."
echo ""
echo -e " ${CYAN}Docs:${RESET} https://dockflare.app/docs"
echo ""

View file

@ -184,10 +184,14 @@ def inbound():
return jsonify({"error": "unknown domain"}), 401
secret = domain_cfg['webhook_secret']
else:
cur = get_db().execute("SELECT webhook_secret FROM domain_configs LIMIT 1")
row = cur.fetchone()
secret = row['webhook_secret'] if row else config.WEBHOOK_SECRET
domain_cfg = None
db = get_db()
count = db.execute("SELECT COUNT(*) FROM domain_configs").fetchone()[0]
if count > 1:
log.warning("Inbound webhook: X-DockFlare-Domain header missing with %d domains configured", count)
return jsonify({"error": "domain header required"}), 400
row = db.execute("SELECT * FROM domain_configs LIMIT 1").fetchone()
domain_cfg = row if row else None
secret = domain_cfg['webhook_secret'] if domain_cfg else config.WEBHOOK_SECRET
if not _verify_signature(request, secret):
return jsonify({"error": "invalid signature"}), 401
@ -257,6 +261,12 @@ def inbound():
)
break
if not to_address:
for addr in parsed.get('delivered_to_addresses', []):
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (addr,)).fetchone():
to_address = addr
break
if not to_address and domain_cfg and domain_cfg['catch_all_mailbox']:
catch_all = domain_cfg['catch_all_mailbox']
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (catch_all,)).fetchone():
@ -307,10 +317,21 @@ def inbound():
))
msg_id = cur.lastrowid
used_filenames = set()
for att in parsed['attachments']:
att_dir = os.path.join(config.ATTACHMENTS_PATH, str(msg_id))
os.makedirs(att_dir, exist_ok=True)
safe_filename = att['filename'].replace('/', '_').replace('\\', '_')
raw_name = os.path.basename(att['filename'] or '').replace('/', '_').replace('\\', '_').strip('. ')
safe_filename = raw_name or 'unnamed_attachment'
if safe_filename in used_filenames:
stem, sep, ext = safe_filename.rpartition('.')
base = stem if sep else safe_filename
tail = (sep + ext) if sep else ''
counter = 2
while f"{base}_{counter}{tail}" in used_filenames:
counter += 1
safe_filename = f"{base}_{counter}{tail}"
used_filenames.add(safe_filename)
att_path = os.path.join(att_dir, safe_filename)
with open(att_path, 'wb') as f:
f.write(att['data'])
@ -441,7 +462,10 @@ def inbound():
})
_check_and_send_auto_reply(db, to_address, parsed, domain_cfg)
delete_from_r2(r2_key, domain_cfg)
try:
delete_from_r2(r2_key, domain_cfg)
except Exception:
log.warning("Inbound: R2 delete failed for %s — will clean on next cron", r2_key)
log.info("Inbound delivered: message=%s to=%s db_id=%s",
msg_uuid, to_address, msg_id)

View file

@ -1,6 +1,8 @@
import email
import email.policy
import email.utils
import re
import uuid
from datetime import datetime, timezone
import nh3
@ -21,12 +23,13 @@ _ALLOWED_ATTRIBUTES = {
def parse_eml(eml_bytes):
msg = email.message_from_bytes(eml_bytes, policy=email.policy.default)
parsed = {
'message_id': msg.get('Message-ID', '').strip('<>'),
'message_id': msg.get('Message-ID', '').strip('<>') or str(uuid.uuid4()),
'from_address': '',
'from_name': '',
'to_addresses': [],
'cc_addresses': [],
'bcc_addresses': [],
'delivered_to_addresses': [],
'subject': msg.get('Subject', ''),
'date': msg.get('Date'),
'in_reply_to': msg.get('In-Reply-To', '').strip('<>'),
@ -54,6 +57,20 @@ def parse_eml(eml_bytes):
addr for _, addr in pairs if addr
]
for delivery_header in ['Delivered-To', 'X-Original-To', 'X-Forwarded-To', 'X-GitHub-Recipient-Address', 'destinations']:
for val in msg.get_all(delivery_header) or []:
_, addr = email.utils.parseaddr(str(val))
if addr and addr not in parsed['delivered_to_addresses']:
parsed['delivered_to_addresses'].append(addr)
for received in msg.get_all('Received') or []:
m = re.search(r'\bfor\s+<([^>]+)>', str(received))
if m:
addr = m.group(1).strip()
if addr and addr not in parsed['delivered_to_addresses']:
parsed['delivered_to_addresses'].append(addr)
break
try:
if parsed['date']:
dt = email.utils.parsedate_to_datetime(parsed['date'])