]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog-comment-add/mail.go
Comments and refactoring
[sgblog.git] / cmd / sgblog-comment-add / mail.go
1 /*
2 SGBlog -- Git-based CGI blogging engine
3 Copyright (C) 2020 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         "io/ioutil"
26         "mime"
27         "mime/multipart"
28         "mime/quotedprintable"
29         "net/mail"
30         "strings"
31 )
32
33 const (
34         CT  = "Content-Type"
35         CTE = "Content-Transfer-Encoding"
36         TP  = "text/plain"
37 )
38
39 func processTP(ct, cte string, body io.Reader) (io.Reader, error) {
40         _, params, err := mime.ParseMediaType(ct)
41         if err != nil {
42                 return nil, err
43         }
44         if c := params["charset"]; !(c == "" || c == "utf-8" || c == "iso-8859-1" || c == "us-ascii") {
45                 return nil, errors.New("only utf-8/iso-8859-1/us-ascii charsets supported")
46         }
47         switch cte {
48         case "quoted-printable":
49                 return quotedprintable.NewReader(body), nil
50         case "base64":
51                 return base64.NewDecoder(base64.StdEncoding, body), nil
52         }
53         return body, nil
54 }
55
56 func parseEmail(msg *mail.Message) (subj string, body io.Reader, err error) {
57         subj = msg.Header.Get("Subject")
58         if subj == "" {
59                 err = errors.New("no Subject")
60                 return
61         }
62         words := strings.Fields(subj)
63         for i, word := range words {
64                 if strings.HasPrefix(word, "=?") && strings.HasSuffix(word, "?=") {
65                         word, err = new(mime.WordDecoder).Decode(word)
66                         if err != nil {
67                                 return
68                         }
69                         words[i] = word
70                 }
71         }
72         subj = strings.Join(words, " ")
73
74         ct := msg.Header.Get(CT)
75         if ct == "" {
76                 ct = "text/plain"
77         }
78         if strings.HasPrefix(ct, TP) {
79                 body, err = processTP(ct, msg.Header.Get(CTE), msg.Body)
80                 return
81         }
82         ct, params, err := mime.ParseMediaType(ct)
83         if ct != "multipart/signed" {
84                 err = errors.New("only text/plain and multipart/signed+text/plain Content-Type supported")
85                 return
86         }
87         boundary := params["boundary"]
88         if len(boundary) == 0 {
89                 err = errors.New("no boundary string")
90                 return
91         }
92         data, err := ioutil.ReadAll(msg.Body)
93         if err != nil {
94                 return
95         }
96         boundaryIdx := bytes.Index(data, []byte("--"+boundary))
97         if boundaryIdx == -1 {
98                 err = errors.New("no boundary found")
99                 return
100         }
101         mpr := multipart.NewReader(bytes.NewReader(data[boundaryIdx:]), boundary)
102         var part *multipart.Part
103         for {
104                 part, err = mpr.NextPart()
105                 if err != nil {
106                         if err == io.EOF {
107                                 break
108                         }
109                         return
110                 }
111                 ct = part.Header.Get(CT)
112                 if strings.HasPrefix(ct, TP) {
113                         body, err = processTP(ct, part.Header.Get(CTE), part)
114                         return
115                 }
116                 if strings.HasPrefix(ct, "multipart/mixed") {
117                         ct, params, err = mime.ParseMediaType(ct)
118                         boundary = params["boundary"]
119                         if len(boundary) == 0 {
120                                 err = errors.New("no boundary string")
121                                 return
122                         }
123                         mpr := multipart.NewReader(part, boundary)
124                         for {
125                                 part, err = mpr.NextPart()
126                                 if err != nil {
127                                         if err == io.EOF {
128                                                 break
129                                         }
130                                         return
131                                 }
132                                 ct = part.Header.Get(CT)
133                                 if strings.HasPrefix(ct, TP) {
134                                         body, err = processTP(ct, part.Header.Get(CTE), part)
135                                         return
136                                 }
137                         }
138                 }
139         }
140         err = errors.New("no text/plain part found")
141         return
142 }