]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog-comment-add/mail.go
2a8bf45b3e03c16803a5e973645a289835ef80e8
[sgblog.git] / cmd / sgblog-comment-add / mail.go
1 /*
2 SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
3 Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU Affero General Public License as
7 published by the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU Affero General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package main
19
20 import (
21         "bytes"
22         "encoding/base64"
23         "errors"
24         "io"
25         "mime"
26         "mime/multipart"
27         "mime/quotedprintable"
28         "net/mail"
29         "strings"
30 )
31
32 const (
33         CT  = "Content-Type"
34         CTE = "Content-Transfer-Encoding"
35         TP  = "text/plain"
36 )
37
38 func processTP(ct, cte string, body io.Reader) (io.Reader, error) {
39         _, params, err := mime.ParseMediaType(ct)
40         if err != nil {
41                 return nil, err
42         }
43         if c := strings.ToLower(params["charset"]); !(c == "" ||
44                 c == "utf-8" ||
45                 c == "iso-8859-1" ||
46                 c == "us-ascii") {
47                 return nil, errors.New("only utf-8/iso-8859-1/us-ascii charsets supported")
48         }
49         switch cte {
50         case "quoted-printable":
51                 return quotedprintable.NewReader(body), nil
52         case "base64":
53                 return base64.NewDecoder(base64.StdEncoding, body), nil
54         }
55         return body, nil
56 }
57
58 func parseEmail(msg *mail.Message) (subj string, body io.Reader, err error) {
59         subj = msg.Header.Get("Subject")
60         if subj == "" {
61                 err = errors.New("no Subject")
62                 return
63         }
64         subj, err = new(mime.WordDecoder).DecodeHeader(subj)
65         if err != nil {
66                 return
67         }
68         ct := msg.Header.Get(CT)
69         if ct == "" {
70                 ct = "text/plain"
71         }
72         if strings.HasPrefix(ct, TP) {
73                 body, err = processTP(ct, msg.Header.Get(CTE), msg.Body)
74                 return
75         }
76         ct, params, err := mime.ParseMediaType(ct)
77         if ct != "multipart/signed" {
78                 err = errors.New("only text/plain and multipart/signed+text/plain Content-Type supported")
79                 return
80         }
81         boundary := params["boundary"]
82         if len(boundary) == 0 {
83                 err = errors.New("no boundary string")
84                 return
85         }
86         data, err := io.ReadAll(msg.Body)
87         if err != nil {
88                 return
89         }
90         boundaryIdx := bytes.Index(data, []byte("--"+boundary))
91         if boundaryIdx == -1 {
92                 err = errors.New("no boundary found")
93                 return
94         }
95         mpr := multipart.NewReader(bytes.NewReader(data[boundaryIdx:]), boundary)
96         var part *multipart.Part
97         for {
98                 part, err = mpr.NextPart()
99                 if err != nil {
100                         if err == io.EOF {
101                                 break
102                         }
103                         return
104                 }
105                 ct = part.Header.Get(CT)
106                 if strings.HasPrefix(ct, TP) {
107                         body, err = processTP(ct, part.Header.Get(CTE), part)
108                         return
109                 }
110                 if strings.HasPrefix(ct, "multipart/mixed") {
111                         ct, params, err = mime.ParseMediaType(ct)
112                         boundary = params["boundary"]
113                         if len(boundary) == 0 {
114                                 err = errors.New("no boundary string")
115                                 return
116                         }
117                         mpr := multipart.NewReader(part, boundary)
118                         for {
119                                 part, err = mpr.NextPart()
120                                 if err != nil {
121                                         if err == io.EOF {
122                                                 break
123                                         }
124                                         return
125                                 }
126                                 ct = part.Header.Get(CT)
127                                 if strings.HasPrefix(ct, TP) {
128                                         body, err = processTP(ct, part.Header.Get(CTE), part)
129                                         return
130                                 }
131                         }
132                 }
133         }
134         err = errors.New("no text/plain part found")
135         return
136 }