mirror of
https://github.com/ChronosX88/yans.git
synced 2024-11-21 11:22:19 +00:00
Implement advanced parsing of envelope (article)
This commit is contained in:
parent
70e75b1c63
commit
537c3abe82
14
go.mod
14
go.mod
@ -11,4 +11,16 @@ require (
|
||||
github.com/pressly/goose/v3 v3.5.0
|
||||
)
|
||||
|
||||
require github.com/pkg/errors v0.9.1 // indirect
|
||||
require (
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 // indirect
|
||||
github.com/jhillyerd/enmime v0.9.3 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.12 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
||||
|
22
go.sum
22
go.sum
@ -17,6 +17,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
||||
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
|
||||
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
|
||||
@ -58,10 +60,13 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 h1:gBeyun7mySAKWg7Fb0GOcv0upX9bdaZScs8QcRo8mEY=
|
||||
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -94,6 +99,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jhillyerd/enmime v0.9.3 h1:XKqSbnX3AV+MbzM2NnhRlO6BBa123sPTdwtGl2pySik=
|
||||
github.com/jhillyerd/enmime v0.9.3/go.mod h1:S5ge4lnv/dDDBbAWwtoOFlj14NHiXdw/EqMB2lJz3b8=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
|
||||
github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
@ -120,6 +129,9 @@ github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
@ -134,6 +146,8 @@ github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXy
|
||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
@ -161,6 +175,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
@ -176,6 +193,8 @@ github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@ -220,6 +239,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -255,6 +276,7 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"net/textproto"
|
||||
"time"
|
||||
)
|
||||
@ -14,5 +15,6 @@ type Article struct {
|
||||
Thread sql.NullString `db:"thread"`
|
||||
|
||||
Header textproto.MIMEHeader `db:"-"`
|
||||
Envelope *enmime.Envelope `db:"-"`
|
||||
ArticleNumber int `db:"-"`
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"github.com/ChronosX88/yans/internal/protocol"
|
||||
"github.com/ChronosX88/yans/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"io"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -304,43 +304,42 @@ func (h *Handler) handlePost(s *Session, command string, arguments []string, id
|
||||
return err
|
||||
}
|
||||
|
||||
headers, err := s.tconn.ReadMIMEHeader()
|
||||
dr := s.tconn.DotReader()
|
||||
|
||||
envelope, err := enmime.ReadEnvelope(dr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// generate message id
|
||||
messageID := fmt.Sprintf("<%s@%s>", uuid.New().String(), h.serverDomain)
|
||||
headers.Set("Message-ID", messageID)
|
||||
envelope.SetHeader("Message-ID", []string{messageID})
|
||||
|
||||
// set path header
|
||||
headers.Set("Path", fmt.Sprintf("%s!not-for-mail", h.serverDomain))
|
||||
envelope.SetHeader("Path", []string{fmt.Sprintf("%s!not-for-mail", h.serverDomain)})
|
||||
|
||||
// set date header
|
||||
if headers.Get("Date") == "" {
|
||||
headers.Set("Date", time.Now().UTC().Format(time.RFC1123Z))
|
||||
}
|
||||
envelope.AddHeader("Date", time.Now().UTC().Format(time.RFC1123Z))
|
||||
|
||||
headerJson, err := json.Marshal(headers)
|
||||
headerJson, err := json.Marshal(envelope.Root.Header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := models.Article{}
|
||||
a.HeaderRaw = string(headerJson)
|
||||
a.Header = headers
|
||||
a.Header = envelope.Root.Header
|
||||
a.Envelope = envelope
|
||||
|
||||
dr := s.tconn.DotReader()
|
||||
// TODO handle multipart message
|
||||
body, err := io.ReadAll(dr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.Body = string(body)
|
||||
a.Body = string(envelope.Text)
|
||||
|
||||
// set thread property
|
||||
if headers.Get("In-Reply-To") != "" {
|
||||
parentMessage, err := h.backend.GetArticle(headers.Get("In-Reply-To"))
|
||||
if envelope.GetHeader("In-Reply-To") != "" {
|
||||
parentMessage, err := h.backend.GetArticle(envelope.GetHeader("In-Reply-To"))
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 441, Message: "no such message you are replying to"}.String())
|
||||
@ -487,18 +486,19 @@ func (h *Handler) handleArticle(s *Session, command string, arguments []string,
|
||||
switch command {
|
||||
case protocol.CommandArticle:
|
||||
{
|
||||
m := utils.NewMessage()
|
||||
for k, v := range a.Header {
|
||||
m.SetHeader(k, v...)
|
||||
}
|
||||
|
||||
m.SetBody("text/plain", a.Body) // FIXME currently only plain text is supported
|
||||
dw := s.tconn.DotWriter()
|
||||
_, err = dw.Write([]byte(protocol.NNTPResponse{Code: 220, Message: fmt.Sprintf("%d %s", num, a.Header.Get("Message-ID"))}.String() + protocol.CRLF))
|
||||
builder := utils.Builder()
|
||||
for k, v := range a.Header {
|
||||
for _, j := range v {
|
||||
builder = builder.Header(k, j)
|
||||
}
|
||||
}
|
||||
builder = builder.Text([]byte(a.Body)) // FIXME currently only plain text is supported
|
||||
p, err := builder.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = m.WriteTo(dw)
|
||||
err = p.Encode(dw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -507,9 +507,15 @@ func (h *Handler) handleArticle(s *Session, command string, arguments []string,
|
||||
}
|
||||
case protocol.CommandHead:
|
||||
{
|
||||
m := utils.NewMessage()
|
||||
builder := utils.Builder()
|
||||
for k, v := range a.Header {
|
||||
m.SetHeader(k, v...)
|
||||
for _, j := range v {
|
||||
builder = builder.Header(k, j)
|
||||
}
|
||||
}
|
||||
p, err := builder.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dw := s.tconn.DotWriter()
|
||||
@ -517,7 +523,7 @@ func (h *Handler) handleArticle(s *Session, command string, arguments []string,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = m.WriteTo(dw)
|
||||
err = p.Encode(dw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -526,12 +532,6 @@ func (h *Handler) handleArticle(s *Session, command string, arguments []string,
|
||||
}
|
||||
case protocol.CommandBody:
|
||||
{
|
||||
m := utils.NewMessage()
|
||||
for k, v := range a.Header {
|
||||
m.SetHeader(k, v...)
|
||||
}
|
||||
|
||||
m.SetBody("text/plain", a.Body) // FIXME currently only plain text is supported
|
||||
dw := s.tconn.DotWriter()
|
||||
|
||||
_, err = dw.Write([]byte(protocol.NNTPResponse{Code: 222, Message: fmt.Sprintf("%d %s", num, a.Header.Get("Message-ID"))}.String() + protocol.CRLF))
|
||||
@ -812,13 +812,22 @@ func (h *Handler) handleOver(s *Session, command string, arguments []string, id
|
||||
dw.Write([]byte(v.Header.Get("References") + " "))
|
||||
|
||||
// count bytes for message
|
||||
m := utils.NewMessage()
|
||||
builder := utils.Builder()
|
||||
for k, v := range v.Header {
|
||||
m.SetHeader(k, v...)
|
||||
for _, j := range v {
|
||||
builder = builder.Header(k, j)
|
||||
}
|
||||
}
|
||||
m.SetBody("text/plain", v.Body) // FIXME currently only plain text is supported
|
||||
builder = builder.Text([]byte(v.Body)) // FIXME currently only plain text is supported
|
||||
b := bytes.NewBuffer([]byte{})
|
||||
m.WriteTo(b)
|
||||
p, err := builder.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.Encode(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytesMetadata := b.Len()
|
||||
linesMetadata := strings.Count(v.Body, "\n")
|
||||
|
226
internal/utils/builder.go
Normal file
226
internal/utils/builder.go
Normal file
@ -0,0 +1,226 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/jhillyerd/enmime"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
cdAttachment = "attachment"
|
||||
cdInline = "inline"
|
||||
|
||||
ctMultipartAltern = "multipart/alternative"
|
||||
ctMultipartMixed = "multipart/mixed"
|
||||
ctMultipartRelated = "multipart/related"
|
||||
ctTextPlain = "text/plain"
|
||||
ctTextHTML = "text/html"
|
||||
|
||||
hnMIMEVersion = "MIME-Version"
|
||||
|
||||
utf8 = "utf-8"
|
||||
)
|
||||
|
||||
// MailBuilder facilitates the easy construction of a MIME message. Each manipulation method
|
||||
// returns a copy of the receiver struct. It can be considered immutable if the caller does not
|
||||
// modify the string and byte slices passed in. Immutability allows the headers or entire message
|
||||
// to be reused across multiple threads.
|
||||
type MailBuilder struct {
|
||||
header textproto.MIMEHeader
|
||||
text, html []byte
|
||||
inlines, attachments []*enmime.Part
|
||||
err error
|
||||
}
|
||||
|
||||
// Builder returns an empty MailBuilder struct.
|
||||
func Builder() MailBuilder {
|
||||
return MailBuilder{}
|
||||
}
|
||||
|
||||
// Error returns the stored error from a file attachment/inline read or nil.
|
||||
func (p MailBuilder) Error() error {
|
||||
return p.err
|
||||
}
|
||||
|
||||
// Header returns a copy of MailBuilder with the specified value added to the named header.
|
||||
func (p MailBuilder) Header(name, value string) MailBuilder {
|
||||
// Copy existing header map
|
||||
h := textproto.MIMEHeader{}
|
||||
for k, v := range p.header {
|
||||
h[k] = v
|
||||
}
|
||||
h.Add(name, value)
|
||||
p.header = h
|
||||
return p
|
||||
}
|
||||
|
||||
// Text returns a copy of MailBuilder that will use the provided bytes for its text/plain Part.
|
||||
func (p MailBuilder) Text(body []byte) MailBuilder {
|
||||
p.text = body
|
||||
return p
|
||||
}
|
||||
|
||||
// HTML returns a copy of MailBuilder that will use the provided bytes for its text/html Part.
|
||||
func (p MailBuilder) HTML(body []byte) MailBuilder {
|
||||
p.html = body
|
||||
return p
|
||||
}
|
||||
|
||||
// AddAttachment returns a copy of MailBuilder that includes the specified attachment.
|
||||
func (p MailBuilder) AddAttachment(b []byte, contentType string, fileName string) MailBuilder {
|
||||
part := enmime.NewPart(contentType)
|
||||
part.Content = b
|
||||
part.FileName = fileName
|
||||
part.Disposition = cdAttachment
|
||||
p.attachments = append(p.attachments, part)
|
||||
return p
|
||||
}
|
||||
|
||||
// AddFileAttachment returns a copy of MailBuilder that includes the specified attachment.
|
||||
// fileName, will be populated from the base name of path. Content type will be detected from the
|
||||
// path extension.
|
||||
func (p MailBuilder) AddFileAttachment(path string) MailBuilder {
|
||||
// Only allow first p.err value
|
||||
if p.err != nil {
|
||||
return p
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
p.err = err
|
||||
return p
|
||||
}
|
||||
b, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
p.err = err
|
||||
return p
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
ctype := mime.TypeByExtension(filepath.Ext(name))
|
||||
return p.AddAttachment(b, ctype, name)
|
||||
}
|
||||
|
||||
// AddInline returns a copy of MailBuilder that includes the specified inline. fileName and
|
||||
// contentID may be left empty.
|
||||
func (p MailBuilder) AddInline(
|
||||
b []byte,
|
||||
contentType string,
|
||||
fileName string,
|
||||
contentID string,
|
||||
) MailBuilder {
|
||||
part := enmime.NewPart(contentType)
|
||||
part.Content = b
|
||||
part.FileName = fileName
|
||||
part.Disposition = cdInline
|
||||
part.ContentID = contentID
|
||||
p.inlines = append(p.inlines, part)
|
||||
return p
|
||||
}
|
||||
|
||||
// AddFileInline returns a copy of MailBuilder that includes the specified inline. fileName and
|
||||
// contentID will be populated from the base name of path. Content type will be detected from the
|
||||
// path extension.
|
||||
func (p MailBuilder) AddFileInline(path string) MailBuilder {
|
||||
// Only allow first p.err value
|
||||
if p.err != nil {
|
||||
return p
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
p.err = err
|
||||
return p
|
||||
}
|
||||
b, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
p.err = err
|
||||
return p
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
ctype := mime.TypeByExtension(filepath.Ext(name))
|
||||
return p.AddInline(b, ctype, name, name)
|
||||
}
|
||||
|
||||
// Build performs some basic validations, then constructs a tree of Part structs from the configured
|
||||
// MailBuilder. It will set the Date header to now if it was not explicitly set.
|
||||
func (p MailBuilder) Build() (*enmime.Part, error) {
|
||||
if p.err != nil {
|
||||
return nil, p.err
|
||||
}
|
||||
// Validations
|
||||
// Fully loaded structure; the presence of text, html, inlines, and attachments will determine
|
||||
// how much is necessary:
|
||||
//
|
||||
// multipart/mixed
|
||||
// |- multipart/related
|
||||
// | |- multipart/alternative
|
||||
// | | |- text/plain
|
||||
// | | `- text/html
|
||||
// | `- inlines..
|
||||
// `- attachments..
|
||||
//
|
||||
// We build this tree starting at the leaves, re-rooting as needed.
|
||||
var root, part *enmime.Part
|
||||
if p.text != nil || p.html == nil {
|
||||
root = enmime.NewPart(ctTextPlain)
|
||||
root.Content = p.text
|
||||
root.Charset = utf8
|
||||
}
|
||||
if p.html != nil {
|
||||
part = enmime.NewPart(ctTextHTML)
|
||||
part.Content = p.html
|
||||
part.Charset = utf8
|
||||
if root == nil {
|
||||
root = part
|
||||
} else {
|
||||
root.NextSibling = part
|
||||
}
|
||||
}
|
||||
if p.text != nil && p.html != nil {
|
||||
// Wrap Text & HTML bodies
|
||||
part = root
|
||||
root = enmime.NewPart(ctMultipartAltern)
|
||||
root.AddChild(part)
|
||||
}
|
||||
if len(p.inlines) > 0 {
|
||||
part = root
|
||||
root = enmime.NewPart(ctMultipartRelated)
|
||||
root.AddChild(part)
|
||||
for _, ip := range p.inlines {
|
||||
// Copy inline Part to isolate mutations
|
||||
part = &enmime.Part{}
|
||||
*part = *ip
|
||||
part.Header = make(textproto.MIMEHeader)
|
||||
root.AddChild(part)
|
||||
}
|
||||
}
|
||||
if len(p.attachments) > 0 {
|
||||
part = root
|
||||
root = enmime.NewPart(ctMultipartMixed)
|
||||
root.AddChild(part)
|
||||
for _, ap := range p.attachments {
|
||||
// Copy attachment Part to isolate mutations
|
||||
part = &enmime.Part{}
|
||||
*part = *ap
|
||||
part.Header = make(textproto.MIMEHeader)
|
||||
root.AddChild(part)
|
||||
}
|
||||
}
|
||||
// Headers
|
||||
h := root.Header
|
||||
h.Set(hnMIMEVersion, "1.0")
|
||||
for k, v := range p.header {
|
||||
for _, s := range v {
|
||||
h.Set(k, s)
|
||||
}
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// Equals uses the reflect package to test two MailBuilder structs for equality, primarily for unit
|
||||
// tests.
|
||||
func (p MailBuilder) Equals(o MailBuilder) bool {
|
||||
return reflect.DeepEqual(p, o)
|
||||
}
|
@ -1,635 +0,0 @@
|
||||
package utils
|
||||
|
||||
// this package has been kindly taken from https://github.com/go-gomail/gomail
|
||||
// licensed under MIT license
|
||||
|
||||
// Copyright (c) 2014 Alexandre Cesaro
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var newQPWriter = quotedprintable.NewWriter
|
||||
|
||||
type mimeEncoder struct {
|
||||
mime.WordEncoder
|
||||
}
|
||||
|
||||
var (
|
||||
bEncoding = mimeEncoder{mime.BEncoding}
|
||||
qEncoding = mimeEncoder{mime.QEncoding}
|
||||
lastIndexByte = strings.LastIndexByte
|
||||
)
|
||||
|
||||
// Message represents an email.
|
||||
type Message struct {
|
||||
header header
|
||||
parts []*part
|
||||
attachments []*file
|
||||
embedded []*file
|
||||
charset string
|
||||
encoding Encoding
|
||||
hEncoder mimeEncoder
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
type header map[string][]string
|
||||
|
||||
type part struct {
|
||||
contentType string
|
||||
copier func(io.Writer) error
|
||||
encoding Encoding
|
||||
}
|
||||
|
||||
// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
|
||||
// by default.
|
||||
func NewMessage(settings ...MessageSetting) *Message {
|
||||
m := &Message{
|
||||
header: make(header),
|
||||
charset: "UTF-8",
|
||||
encoding: QuotedPrintable,
|
||||
}
|
||||
|
||||
m.applySettings(settings)
|
||||
|
||||
if m.encoding == Base64 {
|
||||
m.hEncoder = bEncoding
|
||||
} else {
|
||||
m.hEncoder = qEncoding
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Reset resets the message so it can be reused. The message keeps its previous
|
||||
// settings so it is in the same state that after a call to NewMessage.
|
||||
func (m *Message) Reset() {
|
||||
for k := range m.header {
|
||||
delete(m.header, k)
|
||||
}
|
||||
m.parts = nil
|
||||
m.attachments = nil
|
||||
m.embedded = nil
|
||||
}
|
||||
|
||||
func (m *Message) applySettings(settings []MessageSetting) {
|
||||
for _, s := range settings {
|
||||
s(m)
|
||||
}
|
||||
}
|
||||
|
||||
// A MessageSetting can be used as an argument in NewMessage to configure an
|
||||
// email.
|
||||
type MessageSetting func(m *Message)
|
||||
|
||||
// SetCharset is a message setting to set the charset of the email.
|
||||
func SetCharset(charset string) MessageSetting {
|
||||
return func(m *Message) {
|
||||
m.charset = charset
|
||||
}
|
||||
}
|
||||
|
||||
// SetEncoding is a message setting to set the encoding of the email.
|
||||
func SetEncoding(enc Encoding) MessageSetting {
|
||||
return func(m *Message) {
|
||||
m.encoding = enc
|
||||
}
|
||||
}
|
||||
|
||||
// Encoding represents a MIME encoding scheme like quoted-printable or base64.
|
||||
type Encoding string
|
||||
|
||||
const (
|
||||
// QuotedPrintable represents the quoted-printable encoding as defined in
|
||||
// RFC 2045.
|
||||
QuotedPrintable Encoding = "quoted-printable"
|
||||
// Base64 represents the base64 encoding as defined in RFC 2045.
|
||||
Base64 Encoding = "base64"
|
||||
// Unencoded can be used to avoid encoding the body of an email. The headers
|
||||
// will still be encoded using quoted-printable encoding.
|
||||
Unencoded Encoding = "8bit"
|
||||
)
|
||||
|
||||
// SetHeader sets a value to the given header field.
|
||||
func (m *Message) SetHeader(field string, value ...string) {
|
||||
m.encodeHeader(value)
|
||||
m.header[field] = value
|
||||
}
|
||||
|
||||
func (m *Message) encodeHeader(values []string) {
|
||||
for i := range values {
|
||||
values[i] = m.encodeString(values[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) encodeString(value string) string {
|
||||
return m.hEncoder.Encode(m.charset, value)
|
||||
}
|
||||
|
||||
// SetHeaders sets the message headers.
|
||||
func (m *Message) SetHeaders(h map[string][]string) {
|
||||
for k, v := range h {
|
||||
m.SetHeader(k, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAddressHeader sets an address to the given header field.
|
||||
func (m *Message) SetAddressHeader(field, address, name string) {
|
||||
m.header[field] = []string{m.FormatAddress(address, name)}
|
||||
}
|
||||
|
||||
// FormatAddress formats an address and a name as a valid RFC 5322 address.
|
||||
func (m *Message) FormatAddress(address, name string) string {
|
||||
if name == "" {
|
||||
return address
|
||||
}
|
||||
|
||||
enc := m.encodeString(name)
|
||||
if enc == name {
|
||||
m.buf.WriteByte('"')
|
||||
for i := 0; i < len(name); i++ {
|
||||
b := name[i]
|
||||
if b == '\\' || b == '"' {
|
||||
m.buf.WriteByte('\\')
|
||||
}
|
||||
m.buf.WriteByte(b)
|
||||
}
|
||||
m.buf.WriteByte('"')
|
||||
} else if hasSpecials(name) {
|
||||
m.buf.WriteString(bEncoding.Encode(m.charset, name))
|
||||
} else {
|
||||
m.buf.WriteString(enc)
|
||||
}
|
||||
m.buf.WriteString(" <")
|
||||
m.buf.WriteString(address)
|
||||
m.buf.WriteByte('>')
|
||||
|
||||
addr := m.buf.String()
|
||||
m.buf.Reset()
|
||||
return addr
|
||||
}
|
||||
|
||||
func hasSpecials(text string) bool {
|
||||
for i := 0; i < len(text); i++ {
|
||||
switch c := text[i]; c {
|
||||
case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDateHeader sets a date to the given header field.
|
||||
func (m *Message) SetDateHeader(field string, date time.Time) {
|
||||
m.header[field] = []string{m.FormatDate(date)}
|
||||
}
|
||||
|
||||
// FormatDate formats a date as a valid RFC 5322 date.
|
||||
func (m *Message) FormatDate(date time.Time) string {
|
||||
return date.Format(time.RFC1123Z)
|
||||
}
|
||||
|
||||
// GetHeader gets a header field.
|
||||
func (m *Message) GetHeader(field string) []string {
|
||||
return m.header[field]
|
||||
}
|
||||
|
||||
// SetBody sets the body of the message. It replaces any content previously set
|
||||
// by SetBody, AddAlternative or AddAlternativeWriter.
|
||||
func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
|
||||
m.parts = []*part{m.newPart(contentType, newCopier(body), settings)}
|
||||
}
|
||||
|
||||
// AddAlternative adds an alternative part to the message.
|
||||
//
|
||||
// It is commonly used to send HTML emails that default to the plain text
|
||||
// version for backward compatibility. AddAlternative appends the new part to
|
||||
// the end of the message. So the plain text part should be added before the
|
||||
// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative
|
||||
func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
|
||||
m.AddAlternativeWriter(contentType, newCopier(body), settings...)
|
||||
}
|
||||
|
||||
func newCopier(s string) func(io.Writer) error {
|
||||
return func(w io.Writer) error {
|
||||
_, err := io.WriteString(w, s)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// AddAlternativeWriter adds an alternative part to the message. It can be
|
||||
// useful with the text/template or html/template packages.
|
||||
func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
|
||||
m.parts = append(m.parts, m.newPart(contentType, f, settings))
|
||||
}
|
||||
|
||||
func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part {
|
||||
p := &part{
|
||||
contentType: contentType,
|
||||
copier: f,
|
||||
encoding: m.encoding,
|
||||
}
|
||||
|
||||
for _, s := range settings {
|
||||
s(p)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// A PartSetting can be used as an argument in Message.SetBody,
|
||||
// Message.AddAlternative or Message.AddAlternativeWriter to configure the part
|
||||
// added to a message.
|
||||
type PartSetting func(*part)
|
||||
|
||||
// SetPartEncoding sets the encoding of the part added to the message. By
|
||||
// default, parts use the same encoding than the message.
|
||||
func SetPartEncoding(e Encoding) PartSetting {
|
||||
return PartSetting(func(p *part) {
|
||||
p.encoding = e
|
||||
})
|
||||
}
|
||||
|
||||
type file struct {
|
||||
Name string
|
||||
Header map[string][]string
|
||||
CopyFunc func(w io.Writer) error
|
||||
}
|
||||
|
||||
func (f *file) setHeader(field, value string) {
|
||||
f.Header[field] = []string{value}
|
||||
}
|
||||
|
||||
// A FileSetting can be used as an argument in Message.Attach or Message.Embed.
|
||||
type FileSetting func(*file)
|
||||
|
||||
// SetHeader is a file setting to set the MIME header of the message part that
|
||||
// contains the file content.
|
||||
//
|
||||
// Mandatory headers are automatically added if they are not set when sending
|
||||
// the email.
|
||||
func SetHeader(h map[string][]string) FileSetting {
|
||||
return func(f *file) {
|
||||
for k, v := range h {
|
||||
f.Header[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rename is a file setting to set the name of the attachment if the name is
|
||||
// different than the filename on disk.
|
||||
func Rename(name string) FileSetting {
|
||||
return func(f *file) {
|
||||
f.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// SetCopyFunc is a file setting to replace the function that runs when the
|
||||
// message is sent. It should copy the content of the file to the io.Writer.
|
||||
//
|
||||
// The default copy function opens the file with the given filename, and copy
|
||||
// its content to the io.Writer.
|
||||
func SetCopyFunc(f func(io.Writer) error) FileSetting {
|
||||
return func(fi *file) {
|
||||
fi.CopyFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) appendFile(list []*file, name string, settings []FileSetting) []*file {
|
||||
f := &file{
|
||||
Name: filepath.Base(name),
|
||||
Header: make(map[string][]string),
|
||||
CopyFunc: func(w io.Writer) error {
|
||||
h, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, h); err != nil {
|
||||
h.Close()
|
||||
return err
|
||||
}
|
||||
return h.Close()
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range settings {
|
||||
s(f)
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
return []*file{f}
|
||||
}
|
||||
|
||||
return append(list, f)
|
||||
}
|
||||
|
||||
// Attach attaches the files to the email.
|
||||
func (m *Message) Attach(filename string, settings ...FileSetting) {
|
||||
m.attachments = m.appendFile(m.attachments, filename, settings)
|
||||
}
|
||||
|
||||
// Embed embeds the images to the email.
|
||||
func (m *Message) Embed(filename string, settings ...FileSetting) {
|
||||
m.embedded = m.appendFile(m.embedded, filename, settings)
|
||||
}
|
||||
|
||||
// WriteTo implements io.WriterTo. It dumps the whole message into w.
|
||||
func (m *Message) WriteTo(w io.Writer) (int64, error) {
|
||||
mw := &messageWriter{w: w}
|
||||
mw.writeMessage(m)
|
||||
return mw.n, mw.err
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeMessage(m *Message) {
|
||||
if _, ok := m.header["Mime-Version"]; !ok {
|
||||
w.writeString("Mime-Version: 1.0\r\n")
|
||||
}
|
||||
if _, ok := m.header["Date"]; !ok {
|
||||
w.writeHeader("Date", m.FormatDate(time.Now()))
|
||||
}
|
||||
w.writeHeaders(m.header)
|
||||
|
||||
if m.hasMixedPart() {
|
||||
w.openMultipart("mixed")
|
||||
}
|
||||
|
||||
if m.hasRelatedPart() {
|
||||
w.openMultipart("related")
|
||||
}
|
||||
|
||||
if m.hasAlternativePart() {
|
||||
w.openMultipart("alternative")
|
||||
}
|
||||
for _, part := range m.parts {
|
||||
w.writePart(part, m.charset)
|
||||
}
|
||||
if m.hasAlternativePart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(m.embedded, false)
|
||||
if m.hasRelatedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(m.attachments, true)
|
||||
if m.hasMixedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) hasMixedPart() bool {
|
||||
return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1
|
||||
}
|
||||
|
||||
func (m *Message) hasRelatedPart() bool {
|
||||
return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1
|
||||
}
|
||||
|
||||
func (m *Message) hasAlternativePart() bool {
|
||||
return len(m.parts) > 1
|
||||
}
|
||||
|
||||
type messageWriter struct {
|
||||
w io.Writer
|
||||
n int64
|
||||
writers [3]*multipart.Writer
|
||||
partWriter io.Writer
|
||||
depth uint8
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *messageWriter) openMultipart(mimeType string) {
|
||||
mw := multipart.NewWriter(w)
|
||||
contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
|
||||
w.writers[w.depth] = mw
|
||||
|
||||
if w.depth == 0 {
|
||||
w.writeHeader("Content-Type", contentType)
|
||||
w.writeString("\r\n")
|
||||
} else {
|
||||
w.createPart(map[string][]string{
|
||||
"Content-Type": {contentType},
|
||||
})
|
||||
}
|
||||
w.depth++
|
||||
}
|
||||
|
||||
func (w *messageWriter) createPart(h map[string][]string) {
|
||||
w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h)
|
||||
}
|
||||
|
||||
func (w *messageWriter) closeMultipart() {
|
||||
if w.depth > 0 {
|
||||
w.writers[w.depth-1].Close()
|
||||
w.depth--
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writePart(p *part, charset string) {
|
||||
w.writeHeaders(map[string][]string{
|
||||
"Content-Type": {p.contentType + "; charset=" + charset},
|
||||
"Content-Transfer-Encoding": {string(p.encoding)},
|
||||
})
|
||||
w.writeBody(p.copier, p.encoding)
|
||||
}
|
||||
|
||||
func (w *messageWriter) addFiles(files []*file, isAttachment bool) {
|
||||
for _, f := range files {
|
||||
if _, ok := f.Header["Content-Type"]; !ok {
|
||||
mediaType := mime.TypeByExtension(filepath.Ext(f.Name))
|
||||
if mediaType == "" {
|
||||
mediaType = "application/octet-stream"
|
||||
}
|
||||
f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`)
|
||||
}
|
||||
|
||||
if _, ok := f.Header["Content-Transfer-Encoding"]; !ok {
|
||||
f.setHeader("Content-Transfer-Encoding", string(Base64))
|
||||
}
|
||||
|
||||
if _, ok := f.Header["Content-Disposition"]; !ok {
|
||||
var disp string
|
||||
if isAttachment {
|
||||
disp = "attachment"
|
||||
} else {
|
||||
disp = "inline"
|
||||
}
|
||||
f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`)
|
||||
}
|
||||
|
||||
if !isAttachment {
|
||||
if _, ok := f.Header["Content-ID"]; !ok {
|
||||
f.setHeader("Content-ID", "<"+f.Name+">")
|
||||
}
|
||||
}
|
||||
w.writeHeaders(f.Header)
|
||||
w.writeBody(f.CopyFunc, Base64)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) Write(p []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
var n int
|
||||
n, w.err = w.w.Write(p)
|
||||
w.n += int64(n)
|
||||
return n, w.err
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeString(s string) {
|
||||
n, _ := io.WriteString(w.w, s)
|
||||
w.n += int64(n)
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeader(k string, v ...string) {
|
||||
w.writeString(k)
|
||||
if len(v) == 0 {
|
||||
w.writeString(":\r\n")
|
||||
return
|
||||
}
|
||||
w.writeString(": ")
|
||||
|
||||
// Max header line length is 78 characters in RFC 5322 and 76 characters
|
||||
// in RFC 2047. So for the sake of simplicity we use the 76 characters
|
||||
// limit.
|
||||
charsLeft := 76 - len(k) - len(": ")
|
||||
|
||||
for i, s := range v {
|
||||
// If the line is already too long, insert a newline right away.
|
||||
if charsLeft < 1 {
|
||||
if i == 0 {
|
||||
w.writeString("\r\n ")
|
||||
} else {
|
||||
w.writeString(",\r\n ")
|
||||
}
|
||||
charsLeft = 75
|
||||
} else if i != 0 {
|
||||
w.writeString(", ")
|
||||
charsLeft -= 2
|
||||
}
|
||||
|
||||
// While the header content is too long, fold it by inserting a newline.
|
||||
for len(s) > charsLeft {
|
||||
s = w.writeLine(s, charsLeft)
|
||||
charsLeft = 75
|
||||
}
|
||||
w.writeString(s)
|
||||
if i := lastIndexByte(s, '\n'); i != -1 {
|
||||
charsLeft = 75 - (len(s) - i - 1)
|
||||
} else {
|
||||
charsLeft -= len(s)
|
||||
}
|
||||
}
|
||||
w.writeString("\r\n")
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeLine(s string, charsLeft int) string {
|
||||
// If there is already a newline before the limit. Write the line.
|
||||
if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft {
|
||||
w.writeString(s[:i+1])
|
||||
return s[i+1:]
|
||||
}
|
||||
|
||||
for i := charsLeft - 1; i >= 0; i-- {
|
||||
if s[i] == ' ' {
|
||||
w.writeString(s[:i])
|
||||
w.writeString("\r\n ")
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// We could not insert a newline cleanly so look for a space or a newline
|
||||
// even if it is after the limit.
|
||||
for i := 75; i < len(s); i++ {
|
||||
if s[i] == ' ' {
|
||||
w.writeString(s[:i])
|
||||
w.writeString("\r\n ")
|
||||
return s[i+1:]
|
||||
}
|
||||
if s[i] == '\n' {
|
||||
w.writeString(s[:i+1])
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Too bad, no space or newline in the whole string. Just write everything.
|
||||
w.writeString(s)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeaders(h map[string][]string) {
|
||||
if w.depth == 0 {
|
||||
for k, v := range h {
|
||||
if k != "Bcc" {
|
||||
w.writeHeader(k, v...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.createPart(h)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) {
|
||||
var subWriter io.Writer
|
||||
if w.depth == 0 {
|
||||
w.writeString("\r\n")
|
||||
subWriter = w.w
|
||||
} else {
|
||||
subWriter = w.partWriter
|
||||
}
|
||||
|
||||
if enc == Base64 {
|
||||
wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
|
||||
w.err = f(wc)
|
||||
wc.Close()
|
||||
} else if enc == Unencoded {
|
||||
w.err = f(subWriter)
|
||||
} else {
|
||||
wc := newQPWriter(subWriter)
|
||||
w.err = f(wc)
|
||||
wc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// As required by RFC 2045, 6.7. (page 21) for quoted-printable, and
|
||||
// RFC 2045, 6.8. (page 25) for base64.
|
||||
const maxLineLen = 76
|
||||
|
||||
// base64LineWriter limits text encoded in base64 to 76 characters per line
|
||||
type base64LineWriter struct {
|
||||
w io.Writer
|
||||
lineLen int
|
||||
}
|
||||
|
||||
func newBase64LineWriter(w io.Writer) *base64LineWriter {
|
||||
return &base64LineWriter{w: w}
|
||||
}
|
||||
|
||||
func (w *base64LineWriter) Write(p []byte) (int, error) {
|
||||
n := 0
|
||||
for len(p)+w.lineLen > maxLineLen {
|
||||
w.w.Write(p[:maxLineLen-w.lineLen])
|
||||
w.w.Write([]byte("\r\n"))
|
||||
p = p[maxLineLen-w.lineLen:]
|
||||
n += maxLineLen - w.lineLen
|
||||
w.lineLen = 0
|
||||
}
|
||||
|
||||
w.w.Write(p)
|
||||
w.lineLen += len(p)
|
||||
|
||||
return n + len(p), nil
|
||||
}
|
21
internal/utils/stringutil/addr.go
Normal file
21
internal/utils/stringutil/addr.go
Normal file
@ -0,0 +1,21 @@
|
||||
package stringutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/mail"
|
||||
)
|
||||
|
||||
// JoinAddress formats a slice of Address structs such that they can be used in a To or Cc header.
|
||||
func JoinAddress(addrs []mail.Address) string {
|
||||
if len(addrs) == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
for i, a := range addrs {
|
||||
if i > 0 {
|
||||
_, _ = buf.WriteString(", ")
|
||||
}
|
||||
_, _ = buf.WriteString(a.String())
|
||||
}
|
||||
return buf.String()
|
||||
}
|
37
internal/utils/stringutil/split.go
Normal file
37
internal/utils/stringutil/split.go
Normal file
@ -0,0 +1,37 @@
|
||||
package stringutil
|
||||
|
||||
const escape = '\\'
|
||||
|
||||
// SplitQuoted splits a string, ignoring separators present inside of quoted runs. Separators
|
||||
// cannot be escaped outside of quoted runs, the escaping will be ignored.
|
||||
//
|
||||
// Quotes are preserved in result, but the separators are removed.
|
||||
func SplitQuoted(s string, sep rune, quote rune) []string {
|
||||
a := make([]string, 0, 8)
|
||||
quoted := false
|
||||
escaped := false
|
||||
p := 0
|
||||
for i, c := range s {
|
||||
if c == escape {
|
||||
// Escape can escape itself.
|
||||
escaped = !escaped
|
||||
continue
|
||||
}
|
||||
if c == quote {
|
||||
quoted = !quoted
|
||||
continue
|
||||
}
|
||||
escaped = false
|
||||
if !quoted && c == sep {
|
||||
a = append(a, s[p:i])
|
||||
p = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
if quoted && quote != 0 {
|
||||
// s contained an unterminated quoted-run, re-split without quoting.
|
||||
return SplitQuoted(s, sep, rune(0))
|
||||
}
|
||||
|
||||
return append(a, s[p:])
|
||||
}
|
24
internal/utils/stringutil/uuid.go
Normal file
24
internal/utils/stringutil/uuid.go
Normal file
@ -0,0 +1,24 @@
|
||||
package stringutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var uuidRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
var uuidMutex = &sync.Mutex{}
|
||||
|
||||
// UUID generates a random UUID according to RFC 4122.
|
||||
func UUID() string {
|
||||
uuid := make([]byte, 16)
|
||||
uuidMutex.Lock()
|
||||
_, _ = uuidRand.Read(uuid)
|
||||
uuidMutex.Unlock()
|
||||
// variant bits; see section 4.1.1
|
||||
uuid[8] = uuid[8]&^0xc0 | 0x80
|
||||
// version 4 (pseudo-random); see section 4.1.3
|
||||
uuid[6] = uuid[6]&^0xf0 | 0x40
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
|
||||
}
|
36
internal/utils/stringutil/wrap.go
Normal file
36
internal/utils/stringutil/wrap.go
Normal file
@ -0,0 +1,36 @@
|
||||
package stringutil
|
||||
|
||||
// Wrap builds a byte slice from strs, wrapping on word boundaries before max chars
|
||||
func Wrap(max int, strs ...string) []byte {
|
||||
input := make([]byte, 0)
|
||||
output := make([]byte, 0)
|
||||
for _, s := range strs {
|
||||
input = append(input, []byte(s)...)
|
||||
}
|
||||
if len(input) < max {
|
||||
// Doesn't need to be wrapped
|
||||
return input
|
||||
}
|
||||
ls := -1 // Last seen space index
|
||||
lw := -1 // Last written byte index
|
||||
ll := 0 // Length of current line
|
||||
for i := 0; i < len(input); i++ {
|
||||
ll++
|
||||
switch input[i] {
|
||||
case ' ', '\t':
|
||||
ls = i
|
||||
}
|
||||
if ll >= max {
|
||||
if ls >= 0 {
|
||||
output = append(output, input[lw+1:ls]...)
|
||||
output = append(output, '\r', '\n', ' ')
|
||||
lw = ls // Jump over the space we broke on
|
||||
ll = 1 // Count leading space above
|
||||
// Rewind
|
||||
i = lw + 1
|
||||
ls = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
return append(output, input[lw+1:]...)
|
||||
}
|
Loading…
Reference in New Issue
Block a user