package httpapi import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "log" "mime" "net/http" stdmail "net/mail" "strings" "temp_mail/internal/smtpclient" "temp_mail/internal/storage" ) func NewMux(store storage.Store, domain string, smtpClient *smtpclient.Client) http.Handler { mux := http.NewServeMux() // Static files mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) // API mux.HandleFunc("/api/address", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: local := r.URL.Query().Get("local") local = sanitizeLocal(local) created := store.CreateAddress(local) writeJSON(w, map[string]interface{}{ "address": fmt.Sprintf("%s@%s", created, domain), "local": created, "ttl": int(store.TTL().Minutes()), }) default: w.WriteHeader(http.StatusMethodNotAllowed) } }) mux.HandleFunc("/api/messages/", func(w http.ResponseWriter, r *http.Request) { // /api/messages/{local} or /api/messages/{local}/{id} path := strings.TrimPrefix(r.URL.Path, "/api/messages/") parts := strings.Split(path, "/") if parts[0] == "" { http.NotFound(w, r) return } local := sanitizeLocal(parts[0]) if len(parts) == 1 { msgs := store.List(local) writeJSON(w, msgs) return } id := parts[1] msg, ok := store.Get(local, id) if !ok { http.NotFound(w, r) return } switch r.URL.Query().Get("format") { case "raw": w.Header().Set("Content-Type", "message/rfc822") _, _ = w.Write(msg.Raw) default: writeJSON(w, msg) } }) // 发送邮件API mux.HandleFunc("/api/send", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } // 解析请求体 var req struct { From string `json:"from"` // 发件人本地部分(如 "test"),将拼接域名 To []string `json:"to"` // 收件人列表(完整邮箱地址) Subject string `json:"subject"` // 邮件主题 Body string `json:"body"` // 邮件正文 HTML string `json:"html"` // HTML正文(可选) } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) writeJSON(w, map[string]interface{}{ "error": "无效的请求格式", }) return } // 验证必填字段 if req.From == "" { w.WriteHeader(http.StatusBadRequest) writeJSON(w, map[string]interface{}{ "error": "发件人不能为空(请输入您创建的邮箱名称)", }) return } if len(req.To) == 0 { w.WriteHeader(http.StatusBadRequest) writeJSON(w, map[string]interface{}{ "error": "收件人不能为空", }) return } if req.Subject == "" { w.WriteHeader(http.StatusBadRequest) writeJSON(w, map[string]interface{}{ "error": "主题不能为空", }) return } if req.Body == "" && req.HTML == "" { w.WriteHeader(http.StatusBadRequest) writeJSON(w, map[string]interface{}{ "error": "邮件内容不能为空", }) return } // 验证发件人邮箱是否存在 fromLocal := sanitizeLocal(req.From) if fromLocal == "" { w.WriteHeader(http.StatusBadRequest) writeJSON(w, map[string]interface{}{ "error": "发件人不能为空", }) return } // 如果地址不存在,自动创建 if !store.AddressExists(fromLocal) { store.CreateAddress(fromLocal) log.Printf("自动创建发件邮箱: %s@%s", fromLocal, domain) } // 构造完整的发件人地址 fromAddr := fmt.Sprintf("%s@%s", fromLocal, domain) // 发送邮件 msg := smtpclient.Message{ From: fromAddr, To: req.To, Subject: req.Subject, Body: req.Body, HTML: req.HTML, } if err := smtpClient.Send(msg); err != nil { log.Printf("发送邮件失败 (from=%s): %v", fromAddr, err) w.WriteHeader(http.StatusInternalServerError) writeJSON(w, map[string]interface{}{ "error": fmt.Sprintf("发送失败: %v", err), }) return } log.Printf("邮件已发送: from=%s, to=%v, subject=%s", fromAddr, req.To, req.Subject) writeJSON(w, map[string]interface{}{ "success": true, "message": "邮件已发送", "from": fromAddr, }) }) // UI mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(indexHTML)) }) // Message detail page mux.HandleFunc("/view/", func(w http.ResponseWriter, r *http.Request) { // /view/{local}/{id} path := strings.TrimPrefix(r.URL.Path, "/view/") parts := strings.Split(path, "/") if len(parts) < 2 { http.NotFound(w, r) return } local := sanitizeLocal(parts[0]) id := parts[1] msg, ok := store.Get(local, id) if !ok { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(renderMessageDetailPage(msg, local, domain))) }) return mux } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(v); err != nil { log.Printf("writeJSON: %v", err) } } func sanitizeLocal(s string) string { s = strings.ToLower(strings.TrimSpace(s)) s = strings.Trim(s, "@ ") if i := strings.IndexByte(s, '@'); i > 0 { s = s[:i] } if i := strings.IndexByte(s, '+'); i > 0 { s = s[:i] } if s == "" { return s } // allow [a-z0-9-.] b := strings.Builder{} for _, ch := range s { if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == '_' { b.WriteRune(ch) } } return b.String() } func renderMessageDetailPage(msg storage.Message, local, domain string) string { // Parse email content from raw bytes htmlContent, textContent, _ := parseEmailContent(msg.Raw) // Choose which content to display bodyHTML := "" if htmlContent != "" { // 为HTML内容注入暗色主题样式 styledHTML := injectDarkThemeStyles(htmlContent) bodyHTML = fmt.Sprintf(``, escapeHTMLAttr(styledHTML)) } else if textContent != "" { bodyHTML = fmt.Sprintf(`
%s`, escapeHTML(textContent)) } else { bodyHTML = `
SECURE / ANONYMOUS / EPHEMERAL