Skip to content

Commit

Permalink
Implemented MIME multipart handling for alternative content
Browse files Browse the repository at this point in the history
  • Loading branch information
wneessen committed Mar 13, 2022
1 parent a85b761 commit 8c804ec
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 36 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ The main idea of this library was to provide a simple interface to sending mails
my [JS-Mailer](https://github.com/wneessen/js-mailer) project. It quickly evolved into a
full-fledged mail library.

Parts (especially the msgWriter) of this library have been ported from the [GoMail](https://github.com/go-mail/mail)
which seems to not be maintained anymore.

**This library is "WIP" an should not be considered "production ready", yet.**

go-mail follows idiomatic Go style and best practice. It's only dependency is the Go Standard Library.
Expand Down
14 changes: 4 additions & 10 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,10 @@ func (c *Client) Send(ml ...*Msg) error {
return fmt.Errorf("sending RCPT TO command failed: %w", err)
}
}
w := os.Stderr

/*
w, err := c.sc.Data()
if err != nil {
return fmt.Errorf("sending DATA command failed: %w", err)
}
*/

w, err := c.sc.Data()
if err != nil {
return fmt.Errorf("sending DATA command failed: %w", err)
}
_, err = m.Write(w)
if err != nil {
return fmt.Errorf("sending mail content failed: %w", err)
Expand Down
13 changes: 11 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"github.com/wneessen/go-mail"
"io"
"os"
)

Expand All @@ -12,8 +13,6 @@ func main() {
fmt.Printf("$TEST_HOST env variable cannot be empty\n")
os.Exit(1)
}
tu := os.Getenv("TEST_USER")
tp := os.Getenv("TEST_PASS")

fa := "Winni Neessen <[email protected]>"
toa := "Winfried Neessen <[email protected]>"
Expand All @@ -37,6 +36,15 @@ func main() {
m.SetDate()
m.SetBulk()

m.SetBodyWriter(mail.TypeTextPlain, func(fw io.Writer) error {
_, err := io.WriteString(fw, "This is a writer test")
return err
})
m.AddAlternativeString(mail.TypeTextHTML, "This is HTML content")
//m.Write(os.Stdout)

tu := os.Getenv("TEST_USER")
tp := os.Getenv("TEST_PASS")
c, err := mail.NewClient(th, mail.WithTLSPolicy(mail.TLSMandatory),
mail.WithSMTPAuth(mail.SMTPAuthLogin), mail.WithUsername(tu),
mail.WithPassword(tp))
Expand All @@ -48,4 +56,5 @@ func main() {
fmt.Printf("failed to dial: %s\n", err)
os.Exit(1)
}

}
33 changes: 31 additions & 2 deletions encoding.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package mail

// Charset represents a character set for the encoding
type Charset string

// ContentType represents a content type for the Msg
type ContentType string

// Encoding represents a MIME encoding scheme like quoted-printable or Base64.
type Encoding string

// Charset represents a character set for the encodingA
type Charset string
// MIMEVersion represents the MIME version for the mail
type MIMEVersion string

// MIMEType represents the MIME type for the mail
type MIMEType string

// List of supported encodings
const (
Expand Down Expand Up @@ -117,6 +126,26 @@ const (
CharsetGBK Charset = "GBK"
)

// List of MIME versions
const (
//Mime10 is the MIME Version 1.0
Mime10 MIMEVersion = "1.0"
)

// List of common content types
const (
TypeTextPlain ContentType = "text/plain"
TypeTextHTML ContentType = "text/html"
TypeAppOctetStream ContentType = "application/octet-stream"
)

// List of MIMETypes
const (
MIMEAlternative MIMEType = "alternative"
MIMEMixed MIMEType = "mixed"
MIMERelated MIMEType = "related"
)

// String is a standard method to convert an Encoding into a printable format
func (e Encoding) String() string {
return string(e)
Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
module github.com/wneessen/go-mail

go 1.17

require github.com/go-mail/mail/v2 v2.3.0

require (
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/go-mail/mail/v2 v2.3.0 h1:wha99yf2v3cpUzD1V9ujP404Jbw2uEvs+rBJybkdYcw=
github.com/go-mail/mail/v2 v2.3.0/go.mod h1:oE2UK8qebZAjjV1ZYUpY7FPnbi/kIU53l1dmqPRb4go=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
6 changes: 6 additions & 0 deletions header.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ type AddrHeader string

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

// HeaderContentLang is the "Content-Language" header
HeaderContentLang Header = "Content-Language"

// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header
HeaderContentTransferEnc Header = "Content-Transfer-Encoding"

// HeaderContentType is the "Content-Type" header
HeaderContentType Header = "Content-Type"

Expand Down
129 changes: 121 additions & 8 deletions msg.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mail

import (
"bytes"
"errors"
"fmt"
"io"
Expand All @@ -21,6 +22,12 @@ var (

// Msg is the mail message struct
type Msg struct {
// addrHeader is a slice of strings that the different mail AddrHeader fields
addrHeader map[AddrHeader][]*mail.Address

// boundary is the MIME content boundary
boundary string

// charset represents the charset of the mail (defaults to UTF-8)
charset Charset

Expand All @@ -33,20 +40,35 @@ type Msg struct {
// genHeader is a slice of strings that the different generic mail Header fields
genHeader map[Header][]string

// addrHeader is a slice of strings that the different mail AddrHeader fields
addrHeader map[AddrHeader][]*mail.Address
// mimever represents the MIME version
mimever MIMEVersion

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

// Part is a part of the Msg
type Part struct {
w func(io.Writer) error
x io.Writer
ctype ContentType
enc Encoding
}

// PartOption returns a function that can be used for grouping Part options
type PartOption func(*Part)

// MsgOption returns a function that can be used for grouping Msg options
type MsgOption func(*Msg)

// NewMsg returns a new Msg pointer
func NewMsg(o ...MsgOption) *Msg {
m := &Msg{
encoding: EncodingQP,
addrHeader: make(map[AddrHeader][]*mail.Address),
charset: CharsetUTF8,
encoding: EncodingQP,
genHeader: make(map[Header][]string),
addrHeader: make(map[AddrHeader][]*mail.Address),
mimever: Mime10,
}

// Override defaults with optionally provided MsgOption functions
Expand Down Expand Up @@ -77,6 +99,13 @@ func WithEncoding(e Encoding) MsgOption {
}
}

// WithMIMEVersion overrides the default MIME version
func WithMIMEVersion(mv MIMEVersion) MsgOption {
return func(m *Msg) {
m.mimever = mv
}
}

// SetCharset sets the encoding charset of the Msg
func (m *Msg) SetCharset(c Charset) {
m.charset = c
Expand All @@ -87,6 +116,16 @@ func (m *Msg) SetEncoding(e Encoding) {
m.encoding = e
}

// SetBoundary sets the boundary of the Msg
func (m *Msg) SetBoundary(b string) {
m.boundary = b
}

// SetMIMEVersion sets the MIME version of the Msg
func (m *Msg) SetMIMEVersion(mv MIMEVersion) {
m.mimever = mv
}

// Encoding returns the currently set encoding of the Msg
func (m *Msg) Encoding() string {
return m.encoding.String()
Expand Down Expand Up @@ -294,22 +333,91 @@ func (m *Msg) GetRecipients() ([]string, error) {
return rl, nil
}

// SetBodyString sets the body of the message.
func (m *Msg) SetBodyString(ct ContentType, b string, o ...PartOption) {
buf := bytes.NewBufferString(b)
w := func(w io.Writer) error {
_, err := io.Copy(w, buf)
return err
}
m.SetBodyWriter(ct, w, o...)
}

// SetBodyWriter sets the body of the message.
func (m *Msg) SetBodyWriter(ct ContentType, w func(io.Writer) error, o ...PartOption) {
p := m.NewPart(ct, o...)
p.w = w
m.parts = []*Part{p}
}

// AddAlternativeString sets the alternative body of the message.
func (m *Msg) AddAlternativeString(ct ContentType, b string, o ...PartOption) {
buf := bytes.NewBufferString(b)
w := func(w io.Writer) error {
_, err := io.Copy(w, buf)
return err
}
m.AddAlternativeWriter(ct, w, o...)
}

// AddAlternativeWriter sets the body of the message.
func (m *Msg) AddAlternativeWriter(ct ContentType, w func(io.Writer) error, o ...PartOption) {
p := m.NewPart(ct, o...)
p.w = w
m.parts = append(m.parts, p)
}

// Write writes the formated Msg into a give io.Writer
func (m *Msg) Write(w io.Writer) (int64, error) {
mw := &msgWriter{w: w}
mw.writeMsg(m)
return mw.n, mw.err
}

// NewPart returns a new Part for the Msg
func (m *Msg) NewPart(ct ContentType, o ...PartOption) *Part {
p := &Part{
ctype: ct,
enc: m.encoding,
}

// Override defaults with optionally provided MsgOption functions
for _, co := range o {
if co == nil {
continue
}
co(p)
}

return p
}

// WithPartEncoding overrides the default Part encoding
func WithPartEncoding(e Encoding) PartOption {
return func(p *Part) {
p.enc = e
}
}

// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message
func (p *Part) SetEncoding(e Encoding) {
p.enc = e
}

// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func (m *Msg) setEncoder() {
switch m.encoding {
m.encoder = getEncoder(m.encoding)
}

// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func getEncoder(e Encoding) mime.WordEncoder {
switch e {
case EncodingQP:
m.encoder = mime.QEncoding
return mime.QEncoding
case EncodingB64:
m.encoder = mime.BEncoding
return mime.BEncoding
default:
m.encoder = mime.QEncoding
return mime.QEncoding
}
}

Expand All @@ -318,3 +426,8 @@ func (m *Msg) setEncoder() {
func (m *Msg) encodeString(s string) string {
return m.encoder.Encode(string(m.charset), s)
}

// hasAlt returns true if the Msg has more than one part
func (m *Msg) hasAlt() bool {
return len(m.parts) > 1
}
Loading

0 comments on commit 8c804ec

Please sign in to comment.