Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide more ways for middleware to interact with mail parts #117

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