+/*
+SGBlog -- Git-based CGI blogging engine
+Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+package main
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "io"
+ "io/ioutil"
+ "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 := 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
+ }
+ words := strings.Fields(subj)
+ for i, word := range words {
+ if strings.HasPrefix(word, "=?") && strings.HasSuffix(word, "?=") {
+ word, err = new(mime.WordDecoder).Decode(word)
+ if err != nil {
+ return
+ }
+ words[i] = word
+ }
+ }
+ subj = strings.Join(words, " ")
+
+ 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 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 := ioutil.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") {
+ ct, params, err = mime.ParseMediaType(ct)
+ 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
+}