/* SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2023 Sergey Matveev This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package main import ( "bytes" "encoding/base64" "errors" "fmt" "io" "mime" "mime/multipart" "mime/quotedprintable" "net/mail" "strings" ) const ( CT = "Content-Type" CTE = "Content-Transfer-Encoding" TP = "text/plain" ) func processTP(ct, cte string, body io.Reader) (io.Reader, error) { _, params, err := mime.ParseMediaType(ct) if err != nil { return nil, err } if c := strings.ToLower(params["charset"]); !(c == "" || c == "utf-8" || c == "iso-8859-1" || c == "us-ascii") { return nil, errors.New("only utf-8/iso-8859-1/us-ascii charsets supported") } switch cte { case "quoted-printable": return quotedprintable.NewReader(body), nil case "base64": return base64.NewDecoder(base64.StdEncoding, body), nil } return body, nil } func parseEmail(msg *mail.Message) (subj string, body io.Reader, err error) { subj = msg.Header.Get("Subject") if subj == "" { err = errors.New("no Subject") return } subj, err = new(mime.WordDecoder).DecodeHeader(subj) if err != nil { return } ct := msg.Header.Get(CT) if ct == "" { ct = "text/plain" } if strings.HasPrefix(ct, TP) { body, err = processTP(ct, msg.Header.Get(CTE), msg.Body) return } ct, params, err := mime.ParseMediaType(ct) if err != nil { err = fmt.Errorf("can not ParseMediaType: %w", err) return } if ct != "multipart/signed" { err = errors.New("only text/plain and multipart/signed+text/plain Content-Type supported") return } boundary := params["boundary"] if len(boundary) == 0 { err = errors.New("no boundary string") return } data, err := io.ReadAll(msg.Body) if err != nil { return } boundaryIdx := bytes.Index(data, []byte("--"+boundary)) if boundaryIdx == -1 { err = errors.New("no boundary found") return } mpr := multipart.NewReader(bytes.NewReader(data[boundaryIdx:]), boundary) var part *multipart.Part for { part, err = mpr.NextPart() if err != nil { if err == io.EOF { break } return } ct = part.Header.Get(CT) if strings.HasPrefix(ct, TP) { body, err = processTP(ct, part.Header.Get(CTE), part) return } if strings.HasPrefix(ct, "multipart/mixed") { _, params, err = mime.ParseMediaType(ct) if err != nil { err = fmt.Errorf("can not ParseMediaType: %w", err) return } boundary = params["boundary"] if len(boundary) == 0 { err = errors.New("no boundary string") return } mpr := multipart.NewReader(part, boundary) for { part, err = mpr.NextPart() if err != nil { if err == io.EOF { break } return } ct = part.Header.Get(CT) if strings.HasPrefix(ct, TP) { body, err = processTP(ct, part.Header.Get(CTE), part) return } } } } err = errors.New("no text/plain part found") return }