Skip to content

Commit

Permalink
Merge pull request #117 from wneessen/feature/107_provide-more-ways-f…
Browse files Browse the repository at this point in the history
…or-middleware-to-interact-with-mail-parts

Provide more ways for middleware to interact with mail parts
  • Loading branch information
wneessen authored Feb 13, 2023
2 parents a119616 + 2c3309f commit d052289
Show file tree
Hide file tree
Showing 10 changed files with 334 additions and 16 deletions.
2 changes: 2 additions & 0 deletions encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ const (
TypeTextPlain ContentType = "text/plain"
TypeTextHTML ContentType = "text/html"
TypeAppOctetStream ContentType = "application/octet-stream"
TypePGPSignature ContentType = "application/pgp-signature"
TypePGPEncrypted ContentType = "application/pgp-encrypted"
)

// List of MIMETypes
Expand Down
43 changes: 40 additions & 3 deletions file.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ type FileOption func(*File)

// File is an attachment or embedded file of the Msg
type File struct {
Name string
Header textproto.MIMEHeader
Writer func(w io.Writer) (int64, error)
ContentType ContentType
Desc string
Enc Encoding
Header textproto.MIMEHeader
Name string
Writer func(w io.Writer) (int64, error)
}

// WithFileName sets the filename of the File
Expand All @@ -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
func (f *File) setHeader(h Header, v string) {
f.Header.Set(string(h), v)
}

// getHeader return header fields of a File
func (f *File) getHeader(h Header) (string, bool) {
v := f.Header.Get(string(h))
return v, v != ""
Expand Down
81 changes: 81 additions & 0 deletions file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,84 @@ func TestFile_SetGetHeader(t *testing.T) {
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "", fi)
}
}

// TestFile_WithFileDescription tests the WithFileDescription option
func TestFile_WithFileDescription(t *testing.T) {
tests := []struct {
name string
desc string
}{
{"File description: test", "test"},
{"File description: empty", ""},
}
for _, tt := range tests {
m := NewMsg()
t.Run(tt.name, func(t *testing.T) {
m.AttachFile("file.go", WithFileDescription(tt.desc))
al := m.GetAttachments()
if len(al) <= 0 {
t.Errorf("AttachFile() failed. Attachment list is empty")
}
a := al[0]
if a.Desc != tt.desc {
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc, a.Desc)
}
})
}
}

// TestFile_WithFileEncoding tests the WithFileEncoding option
func TestFile_WithFileEncoding(t *testing.T) {
tests := []struct {
name string
enc Encoding
want Encoding
}{
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
{"File encoding: Base64", EncodingB64, EncodingB64},
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
}
for _, tt := range tests {
m := NewMsg()
t.Run(tt.name, func(t *testing.T) {
m.AttachFile("file.go", WithFileEncoding(tt.enc))
al := m.GetAttachments()
if len(al) <= 0 {
t.Errorf("AttachFile() failed. Attachment list is empty")
}
a := al[0]
if a.Enc != tt.want {
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.enc, a.Enc)
}
})
}
}

// TestFile_WithFileContentType tests the WithFileContentType option
func TestFile_WithFileContentType(t *testing.T) {
tests := []struct {
name string
ct ContentType
want string
}{
{"File content-type: text/plain", TypeTextPlain, "text/plain"},
{"File content-type: html/html", TypeTextHTML, "text/html"},
{"File content-type: application/octet-stream", TypeAppOctetStream, "application/octet-stream"},
{"File content-type: application/pgp-encrypted", TypePGPEncrypted, "application/pgp-encrypted"},
{"File content-type: application/pgp-signature", TypePGPSignature, "application/pgp-signature"},
}
for _, tt := range tests {
m := NewMsg()
t.Run(tt.name, func(t *testing.T) {
m.AttachFile("file.go", WithFileContentType(tt.ct))
al := m.GetAttachments()
if len(al) <= 0 {
t.Errorf("AttachFile() failed. Attachment list is empty")
}
a := al[0]
if a.ContentType != ContentType(tt.want) {
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.want, a.ContentType)
}
})
}
}
3 changes: 3 additions & 0 deletions header.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ type Importance int

// List of common generic header field names
const (
// HeaderContentDescription is the "Content-Description" header
HeaderContentDescription Header = "Content-Description"

// HeaderContentDisposition is the "Content-Disposition" header
HeaderContentDisposition Header = "Content-Disposition"

Expand Down
59 changes: 48 additions & 11 deletions msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ const (
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
// in the Middleware interface by the Type method
type MiddlewareType string
Expand All @@ -51,6 +63,10 @@ type Middleware interface {
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
type Msg struct {
// addrHeader is a slice of strings that the different mail AddrHeader fields
Expand All @@ -77,19 +93,23 @@ type Msg struct {
// genHeader is a slice of strings that the different generic mail Header fields
genHeader map[Header][]string

// preformHeader is a slice of strings that the different generic mail Header fields
// of which content is already preformated and will not be affected by the automatic line
// breaks
preformHeader map[Header]string
// middlewares is the list of middlewares to apply to the Msg before sending in FIFO order
middlewares []Middleware

// mimever represents the MIME version
mimever MIMEVersion

// parts represent the different parts of the Msg
parts []*Part

// middlewares is the list of middlewares to apply to the Msg before sending in FIFO order
middlewares []Middleware
// preformHeader is a slice of strings that the different generic mail Header fields
// 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 error
Expand Down Expand Up @@ -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
func (m *Msg) SetCharset(c Charset) {
m.charset = c
Expand All @@ -182,6 +209,11 @@ func (m *Msg) SetMIMEVersion(mv MIMEVersion) {
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
func (m *Msg) Encoding() string {
return m.encoding.String()
Expand Down Expand Up @@ -774,10 +806,10 @@ func (m *Msg) EmbedFile(n string, o ...FileOption) {

// EmbedReader adds an embedded File from an io.Reader to the Msg
//
// CAVEAT: For AttachReader to work it has to read all data of the io.Reader
// CAVEAT: For EmbedReader to work it has to read all data of the io.Reader
// into memory first, so it can seek through it. Using larger amounts of
// data on the io.Reader should be avoided. For such, it is recommeded to
// either use AttachFile or AttachReadSeeker instead
// either use EmbedFile or EmbedReadSeeker instead
func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) {
f := fileFromReader(n, r)
m.embeds = m.appendFile(m.embeds, f, o...)
Expand Down Expand Up @@ -1027,17 +1059,22 @@ func (m *Msg) hasAlt() bool {
c++
}
}
return c > 1
return c > 1 && m.pgptype == 0
}

// hasMixed returns true if the Msg has mixed parts
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
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
Expand Down
30 changes: 30 additions & 0 deletions msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,36 @@ func TestNewMsgWithBoundary(t *testing.T) {
}
}

// TestNewMsg_WithPGPType tests WithPGPType option
func TestNewMsg_WithPGPType(t *testing.T) {
tests := []struct {
name string
pt PGPType
hpt bool
}{
{"Not a PGP encoded message", NoPGP, false},
{"PGP encrypted message", PGPEncrypt, true},
{"PGP signed message", PGPSignature, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMsg(WithPGPType(tt.pt))
if m.pgptype != tt.pt {
t.Errorf("WithPGPType() failed. Expected: %d, got: %d", tt.pt, m.pgptype)
}
m.pgptype = 99
m.SetPGPType(tt.pt)
if m.pgptype != tt.pt {
t.Errorf("SetPGPType() failed. Expected: %d, got: %d", tt.pt, m.pgptype)
}
if m.hasPGPType() != tt.hpt {
t.Errorf("hasPGPType() failed. Expected %t, got: %t", tt.hpt, m.hasPGPType())
}
})
}
}

type uppercaseMiddleware struct{}

func (mw uppercaseMiddleware) Handle(m *Msg) *Msg {
Expand Down
Loading

0 comments on commit d052289

Please sign in to comment.