Adding PGPType to Msg as preparation to support PGP/MIME in go-mail-middleware

This commit is contained in:
Winni Neessen 2023-01-31 18:35:48 +01:00
parent cfaeb51bef
commit dab860834a
Signed by: wneessen
GPG key ID: 5F3AF39B820C119D
6 changed files with 141 additions and 14 deletions

View file

@ -141,6 +141,8 @@ const (
TypeTextPlain ContentType = "text/plain" TypeTextPlain ContentType = "text/plain"
TypeTextHTML ContentType = "text/html" TypeTextHTML ContentType = "text/html"
TypeAppOctetStream ContentType = "application/octet-stream" TypeAppOctetStream ContentType = "application/octet-stream"
TypePGPSignature ContentType = "application/pgp-signature"
TypePGPEncrypted ContentType = "application/pgp-encrypted"
) )
// List of MIMETypes // List of MIMETypes

39
file.go
View file

@ -14,8 +14,11 @@ type FileOption func(*File)
// File is an attachment or embedded file of the Msg // File is an attachment or embedded file of the Msg
type File struct { type File struct {
Name string ContentType ContentType
Desc string
Enc Encoding
Header textproto.MIMEHeader Header textproto.MIMEHeader
Name string
Writer func(w io.Writer) (int64, error) Writer func(w io.Writer) (int64, error)
} }
@ -26,11 +29,45 @@ func WithFileName(n string) FileOption {
} }
} }
// WithFileDescription sets an optional file description of the File that will be
// added as Content-Description part
func WithFileDescription(d string) FileOption {
return func(f *File) {
f.Desc = d
}
}
// WithFileEncoding sets the encoding of the File. By default we should always use
// Base64 encoding but there might be exceptions, where this might come handy.
// Please note that quoted-printable should never be used for attachments/embeds. If this
// is provided as argument, the function will automatically override back to Base64
func WithFileEncoding(e Encoding) FileOption {
return func(f *File) {
if e == EncodingQP {
return
}
f.Enc = e
}
}
// WithFileContentType sets the content type of the File.
// By default go-mail will try to guess the file type and its corresponding
// content type and fall back to application/octet-stream if the file type
// could not be guessed. In some cases, however, it might be needed to force
// this to a specific type. For such situations this override method can
// be used
func WithFileContentType(t ContentType) FileOption {
return func(f *File) {
f.ContentType = t
}
}
// setHeader sets header fields to a File // setHeader sets header fields to a File
func (f *File) setHeader(h Header, v string) { func (f *File) setHeader(h Header, v string) {
f.Header.Set(string(h), v) f.Header.Set(string(h), v)
} }
// getHeader return header fields of a File
func (f *File) getHeader(h Header) (string, bool) { func (f *File) getHeader(h Header) (string, bool) {
v := f.Header.Get(string(h)) v := f.Header.Get(string(h))
return v, v != "" return v, v != ""

View file

@ -15,6 +15,9 @@ type Importance int
// List of common generic header field names // List of common generic header field names
const ( const (
// HeaderContentDescription is the "Content-Description" header
HeaderContentDescription Header = "Content-Description"
// HeaderContentDisposition is the "Content-Disposition" header // HeaderContentDisposition is the "Content-Disposition" header
HeaderContentDisposition Header = "Content-Disposition" HeaderContentDisposition Header = "Content-Disposition"

55
msg.go
View file

@ -41,6 +41,18 @@ const (
errParseMailAddr = "failed to parse mail address %q: %w" errParseMailAddr = "failed to parse mail address %q: %w"
) )
const (
// NoPGP indicates that a message should not be treated as PGP encrypted
// or signed and is the default value for a message
NoPGP PGPType = iota
// PGPEncrypt indicates that a message should be treated as PGP encrypted
// This works closely together with the corresponding go-mail-middleware
PGPEncrypt
// PGPSignature indicates that a message should be treated as PGP signed
// This works closely together with the corresponding go-mail-middleware
PGPSignature
)
// MiddlewareType is the type description of the Middleware and needs to be returned // MiddlewareType is the type description of the Middleware and needs to be returned
// in the Middleware interface by the Type method // in the Middleware interface by the Type method
type MiddlewareType string type MiddlewareType string
@ -51,6 +63,10 @@ type Middleware interface {
Type() MiddlewareType Type() MiddlewareType
} }
// PGPType is a type alias for a int representing a type of PGP encryption
// or signature
type PGPType int
// Msg is the mail message struct // Msg is the mail message struct
type Msg struct { type Msg struct {
// addrHeader is a slice of strings that the different mail AddrHeader fields // addrHeader is a slice of strings that the different mail AddrHeader fields
@ -77,10 +93,8 @@ type Msg struct {
// genHeader is a slice of strings that the different generic mail Header fields // genHeader is a slice of strings that the different generic mail Header fields
genHeader map[Header][]string genHeader map[Header][]string
// preformHeader is a slice of strings that the different generic mail Header fields // middlewares is the list of middlewares to apply to the Msg before sending in FIFO order
// of which content is already preformated and will not be affected by the automatic line middlewares []Middleware
// breaks
preformHeader map[Header]string
// mimever represents the MIME version // mimever represents the MIME version
mimever MIMEVersion mimever MIMEVersion
@ -88,8 +102,14 @@ type Msg struct {
// parts represent the different parts of the Msg // parts represent the different parts of the Msg
parts []*Part parts []*Part
// middlewares is the list of middlewares to apply to the Msg before sending in FIFO order // preformHeader is a slice of strings that the different generic mail Header fields
middlewares []Middleware // of which content is already preformated and will not be affected by the automatic line
// breaks
preformHeader map[Header]string
// pgptype indicates that a message has a PGPType assigned and therefore will generate
// different Content-Type settings in the msgWriter
pgptype PGPType
// sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation // sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation
sendError error sendError error
@ -161,6 +181,13 @@ func WithMiddleware(mw Middleware) MsgOption {
} }
} }
// WithPGPType overrides the default PGPType of the message
func WithPGPType(t PGPType) MsgOption {
return func(m *Msg) {
m.pgptype = t
}
}
// SetCharset sets the encoding charset of the Msg // SetCharset sets the encoding charset of the Msg
func (m *Msg) SetCharset(c Charset) { func (m *Msg) SetCharset(c Charset) {
m.charset = c m.charset = c
@ -182,6 +209,11 @@ func (m *Msg) SetMIMEVersion(mv MIMEVersion) {
m.mimever = mv m.mimever = mv
} }
// SetPGPType sets the PGPType of the Msg
func (m *Msg) SetPGPType(t PGPType) {
m.pgptype = t
}
// Encoding returns the currently set encoding of the Msg // Encoding returns the currently set encoding of the Msg
func (m *Msg) Encoding() string { func (m *Msg) Encoding() string {
return m.encoding.String() return m.encoding.String()
@ -1005,17 +1037,22 @@ func (m *Msg) hasAlt() bool {
c++ c++
} }
} }
return c > 1 return c > 1 && m.pgptype == 0
} }
// hasMixed returns true if the Msg has mixed parts // hasMixed returns true if the Msg has mixed parts
func (m *Msg) hasMixed() bool { func (m *Msg) hasMixed() bool {
return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1 return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1)
} }
// hasRelated returns true if the Msg has related parts // hasRelated returns true if the Msg has related parts
func (m *Msg) hasRelated() bool { func (m *Msg) hasRelated() bool {
return (len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1 return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1)
}
// hasPGPType returns true if the Msg should be treated as PGP encoded message
func (m *Msg) hasPGPType() bool {
return m.pgptype > 0
} }
// newPart returns a new Part for the Msg // newPart returns a new Part for the Msg

View file

@ -100,12 +100,26 @@ func (mw *msgWriter) writeMsg(m *Msg) {
mw.startMP(MIMEAlternative, m.boundary) mw.startMP(MIMEAlternative, m.boundary)
mw.writeString(DoubleNewLine) mw.writeString(DoubleNewLine)
} }
if m.hasPGPType() {
switch m.pgptype {
case PGPEncrypt:
mw.startMP(`encrypted; protocol="application/pgp-encrypted"`, m.boundary)
case PGPSignature:
mw.startMP(`signed; protocol="application/pgp-signature";`, m.boundary)
}
mw.writeString(DoubleNewLine)
mw.startMP("mixed", m.boundary)
}
for _, p := range m.parts { for _, p := range m.parts {
if !p.del { if !p.del {
mw.writePart(p, m.charset) mw.writePart(p, m.charset)
} }
} }
if m.hasPGPType() {
mw.stopMP()
}
if m.hasAlt() { if m.hasAlt() {
mw.stopMP() mw.stopMP()
} }
@ -172,17 +186,30 @@ func (mw *msgWriter) stopMP() {
// addFiles adds the attachments/embeds file content to the mail body // addFiles adds the attachments/embeds file content to the mail body
func (mw *msgWriter) addFiles(fl []*File, a bool) { func (mw *msgWriter) addFiles(fl []*File, a bool) {
for _, f := range fl { for _, f := range fl {
e := EncodingB64
if _, ok := f.getHeader(HeaderContentType); !ok { if _, ok := f.getHeader(HeaderContentType); !ok {
mt := mime.TypeByExtension(filepath.Ext(f.Name)) mt := mime.TypeByExtension(filepath.Ext(f.Name))
if mt == "" { if mt == "" {
mt = "application/octet-stream" mt = "application/octet-stream"
} }
if f.ContentType != "" {
mt = string(f.ContentType)
}
f.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mt, f.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mt,
mw.en.Encode(mw.c.String(), f.Name))) mw.en.Encode(mw.c.String(), f.Name)))
} }
if _, ok := f.getHeader(HeaderContentTransferEnc); !ok { if _, ok := f.getHeader(HeaderContentTransferEnc); !ok {
f.setHeader(HeaderContentTransferEnc, string(EncodingB64)) if f.Enc != "" {
e = f.Enc
}
f.setHeader(HeaderContentTransferEnc, string(e))
}
if f.Desc != "" {
if _, ok := f.getHeader(HeaderContentDescription); !ok {
f.setHeader(HeaderContentDescription, f.Desc)
}
} }
if _, ok := f.getHeader(HeaderContentDisposition); !ok { if _, ok := f.getHeader(HeaderContentDisposition); !ok {
@ -208,7 +235,7 @@ func (mw *msgWriter) addFiles(fl []*File, a bool) {
if mw.d > 0 { if mw.d > 0 {
mw.newPart(f.Header) mw.newPart(f.Header)
} }
mw.writeBody(f.Writer, EncodingB64) mw.writeBody(f.Writer, e)
} }
} }
@ -228,6 +255,9 @@ func (mw *msgWriter) writePart(p *Part, cs Charset) {
} }
if mw.d > 0 { if mw.d > 0 {
mh := textproto.MIMEHeader{} mh := textproto.MIMEHeader{}
if p.desc != "" {
mh.Add(string(HeaderContentDescription), p.desc)
}
mh.Add(string(HeaderContentType), ct) mh.Add(string(HeaderContentType), ct)
mh.Add(string(HeaderContentTransferEnc), cte) mh.Add(string(HeaderContentTransferEnc), cte)
mw.newPart(mh) mw.newPart(mh)

18
part.go
View file

@ -15,6 +15,7 @@ type PartOption func(*Part)
// Part is a part of the Msg // Part is a part of the Msg
type Part struct { type Part struct {
ctype ContentType ctype ContentType
desc string
enc Encoding enc Encoding
del bool del bool
w func(io.Writer) (int64, error) w func(io.Writer) (int64, error)
@ -44,6 +45,11 @@ func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) {
return p.w return p.w
} }
// GetDescription returns the currently set Content-Description of the Part
func (p *Part) GetDescription() string {
return p.desc
}
// SetContent overrides the content of the Part with the given string // SetContent overrides the content of the Part with the given string
func (p *Part) SetContent(c string) { func (p *Part) SetContent(c string) {
buf := bytes.NewBufferString(c) buf := bytes.NewBufferString(c)
@ -60,6 +66,11 @@ func (p *Part) SetEncoding(e Encoding) {
p.enc = e p.enc = e
} }
// SetDescription overrides the Content-Description of the Part
func (p *Part) SetDescription(d string) {
p.desc = d
}
// SetWriteFunc overrides the WriteFunc of the Part // SetWriteFunc overrides the WriteFunc of the Part
func (p *Part) SetWriteFunc(w func(io.Writer) (int64, error)) { func (p *Part) SetWriteFunc(w func(io.Writer) (int64, error)) {
p.w = w p.w = w
@ -77,3 +88,10 @@ func WithPartEncoding(e Encoding) PartOption {
p.enc = e p.enc = e
} }
} }
// WithPartContentDescription overrides the default Part Content-Description
func WithPartContentDescription(d string) PartOption {
return func(p *Part) {
p.desc = d
}
}