diff --git a/README.md b/README.md
index d5650cf..34d69d6 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,8 @@
LanQin Email 是一个自建邮箱 Webmail 全栈方案:前端使用 React + TypeScript + shadcn/ui,后端使用 Go + SQLite,部署时可用单容器集成 API、Web、Nginx、Postfix、Dovecot、Rspamd。
+交流群组:[Telegram 群组](https://t.me/+EhII7MSyi3QwNDQ5)
+
## 功能特性
- **Webmail 客户端**:多邮箱切换、文件夹、邮件读写、草稿、定时发送、附件、搜索、标签、星标、移动/删除、已读/未读。
@@ -21,6 +23,16 @@ LanQin Email 是一个自建邮箱 Webmail 全栈方案:前端使用 React + T
- **邮件服务栈**:Postfix 投递、Dovecot IMAP/POP3、Rspamd 反垃圾与 DKIM 签名、Maildir 到 SQLite 同步。
- **部署友好**:默认 all-in-one 单容器,也提供多容器 stack 方便调试 Postfix/Dovecot/Rspamd。
+## 界面预览
+
+| Webmail 邮件阅读与列表 | 写邮件 · 富文本编辑工具栏 |
+| --- | --- |
+|  |  |
+| 多邮箱切换、文件夹、搜索、标签、星标与邮件阅读面板。 | 富文本工具栏支持字体、标题、加粗、斜体、下划线、颜色、高亮、列表、对齐、引用、代码块、附件、表情与定时发送。 |
+| 管理后台 · 系统概览 | 第三方客户端配置 |
+|  |  |
+| 管理用户、权限组、域名、邮箱、别名、系统设置与发送审计。 | 一键查看 IMAP / POP3 / SMTP 服务器、端口、安全方式与账号信息。 |
+
## 目录结构
```text
@@ -204,3 +216,17 @@ docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
## License
[MIT](./LICENSE)
+
+
+## Star 趋势
+
+
+
+
+
+
+
+
+
+友情链接:[LINUX DO](https://linux.do/) —— 新的理想型社区
+
diff --git a/apps/api/internal/app/maildir_ownership_other.go b/apps/api/internal/app/maildir_ownership_other.go
new file mode 100644
index 0000000..261ecc4
--- /dev/null
+++ b/apps/api/internal/app/maildir_ownership_other.go
@@ -0,0 +1,7 @@
+//go:build !unix
+
+package app
+
+func applyMaildirOwnership(path string) error {
+ return nil
+}
diff --git a/apps/api/internal/app/maildir_ownership_unix.go b/apps/api/internal/app/maildir_ownership_unix.go
new file mode 100644
index 0000000..3010df2
--- /dev/null
+++ b/apps/api/internal/app/maildir_ownership_unix.go
@@ -0,0 +1,17 @@
+//go:build unix
+
+package app
+
+import "os"
+
+const (
+ maildirOwnerUID = 5000
+ maildirOwnerGID = 5000
+)
+
+func applyMaildirOwnership(path string) error {
+ if os.Geteuid() != 0 {
+ return nil
+ }
+ return os.Chown(path, maildirOwnerUID, maildirOwnerGID)
+}
diff --git a/apps/api/internal/app/maildir_write.go b/apps/api/internal/app/maildir_write.go
index c26d08e..4b7fba3 100644
--- a/apps/api/internal/app/maildir_write.go
+++ b/apps/api/internal/app/maildir_write.go
@@ -120,7 +120,7 @@ func (a *App) writeRawMessageToMaildirFolder(ctx context.Context, messageID, fol
if strings.EqualFold(folderName, "Inbox") && !state.IsRead {
subdir = "new"
}
- if err := ensureMaildirFolderDirs(folderBase); err != nil {
+ if err := ensureMaildirFolderDirs(base, folderBase); err != nil {
return err
}
filename := maildirFilename(messageID, state.MessageID)
@@ -130,6 +130,10 @@ func (a *App) writeRawMessageToMaildirFolder(ctx context.Context, messageID, fol
if err := os.WriteFile(tmpPath, raw, 0o600); err != nil {
return err
}
+ if err := applyMaildirOwnership(tmpPath); err != nil {
+ _ = os.Remove(tmpPath)
+ return err
+ }
if err := os.Rename(tmpPath, finalPath); err != nil {
_ = os.Remove(tmpPath)
return err
@@ -213,7 +217,7 @@ func (a *App) moveMessageMaildir(ctx context.Context, messageID, targetFolderID
}
base := filepath.Join(strings.TrimSpace(a.cfg.MaildirRoot), mb.Domain, mb.LocalPart, "Maildir")
folderBase := maildirFolderPath(base, folderName)
- if err := ensureMaildirFolderDirs(folderBase); err != nil {
+ if err := ensureMaildirFolderDirs(base, folderBase); err != nil {
return err
}
subdir := "cur"
@@ -225,6 +229,9 @@ func (a *App) moveMessageMaildir(ctx context.Context, messageID, targetFolderID
if err := os.Rename(state.RawPath, targetPath); err != nil {
return err
}
+ if err := applyMaildirOwnership(targetPath); err != nil {
+ return err
+ }
}
if state.FolderID != "" && state.FolderID != targetFolderID {
if _, err := a.bumpFolderModSeq(ctx, state.FolderID); err != nil {
@@ -318,6 +325,9 @@ func (a *App) updateMessageMaildirFlags(ctx context.Context, messageID string, r
if err := os.Rename(state.RawPath, targetPath); err != nil {
return err
}
+ if err := applyMaildirOwnership(targetPath); err != nil {
+ return err
+ }
modSeq, err := a.bumpFolderModSeq(ctx, state.FolderID)
if err != nil {
return err
@@ -481,15 +491,33 @@ func (a *App) pathIsUnderMaildirRoot(path string) (bool, error) {
return rel != "." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != "..", nil
}
-func ensureMaildirFolderDirs(folderBase string) error {
+func ensureMaildirFolderDirs(base, folderBase string) error {
for _, sub := range []string{"tmp", "new", "cur"} {
if err := os.MkdirAll(filepath.Join(folderBase, sub), 0o755); err != nil {
return err
}
}
+ for _, dir := range maildirOwnershipDirs(base, folderBase) {
+ if err := applyMaildirOwnership(dir); err != nil {
+ return err
+ }
+ }
return nil
}
+func maildirOwnershipDirs(base, folderBase string) []string {
+ dirs := []string{
+ filepath.Dir(filepath.Dir(base)),
+ filepath.Dir(base),
+ base,
+ }
+ if filepath.Clean(folderBase) != filepath.Clean(base) {
+ dirs = append(dirs, folderBase)
+ }
+ dirs = append(dirs, filepath.Join(folderBase, "tmp"), filepath.Join(folderBase, "new"), filepath.Join(folderBase, "cur"))
+ return dirs
+}
+
func maildirFilename(messageID, headerMessageID string) string {
base := strings.TrimSpace(headerMessageID)
if base == "" {
diff --git a/apps/web/src/pages/admin.tsx b/apps/web/src/pages/admin.tsx
index cce0cef..c6fc09d 100644
--- a/apps/web/src/pages/admin.tsx
+++ b/apps/web/src/pages/admin.tsx
@@ -52,6 +52,7 @@ const sectionPermissions: Record = {
settings: ["admin.settings.view", "admin.templates.view"],
}
const projectRepositoryUrl = "https://github.com/LanQin996/LanQin-Email"
+const projectTelegramUrl = "https://t.me/+EhII7MSyi3QwNDQ5"
const projectTag = import.meta.env.VITE_APP_VERSION || ""
const projectReleaseUrl = import.meta.env.VITE_RELEASE_URL || (projectTag ? `${projectRepositoryUrl}/releases/tag/${projectTag}` : "")
const defaultPermissionLimits: PermissionLimits = { maxAttachmentMb: 25, smtpDailyLimit: 200, smtpMinuteLimit: 20, imapMinuteLimit: 200, pop3MinuteLimit: 150 }
@@ -1387,6 +1388,12 @@ function AboutProjectCard() {
Issues
+
diff --git a/docs/screenshots/admin-preview.png b/docs/screenshots/admin-preview.png
new file mode 100644
index 0000000..a6a4682
Binary files /dev/null and b/docs/screenshots/admin-preview.png differ
diff --git a/docs/screenshots/client-preview.png b/docs/screenshots/client-preview.png
new file mode 100644
index 0000000..7e4b807
Binary files /dev/null and b/docs/screenshots/client-preview.png differ
diff --git a/docs/screenshots/compose-preview.png b/docs/screenshots/compose-preview.png
new file mode 100644
index 0000000..2fd4403
Binary files /dev/null and b/docs/screenshots/compose-preview.png differ
diff --git a/docs/screenshots/mail-preview.png b/docs/screenshots/mail-preview.png
new file mode 100644
index 0000000..46c781d
Binary files /dev/null and b/docs/screenshots/mail-preview.png differ