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