mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-22 10:40:39 +01:00
Implemented MIME multipart handling for alternative content
This commit is contained in:
parent
a85b761f43
commit
8c804ec573
9 changed files with 303 additions and 38 deletions
|
@ -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.
|
||||
|
|
14
client.go
14
client.go
|
@ -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)
|
||||
|
|
13
cmd/main.go
13
cmd/main.go
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"github.com/wneessen/go-mail"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
@ -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 <wn@neessen.cloud>"
|
||||
toa := "Winfried Neessen <wn@neessen.net>"
|
||||
|
@ -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))
|
||||
|
@ -48,4 +56,5 @@ func main() {
|
|||
fmt.Printf("failed to dial: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
33
encoding.go
33
encoding.go
|
@ -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 (
|
||||
|
@ -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)
|
||||
|
|
7
go.mod
7
go.mod
|
@ -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
go.sum
6
go.sum
|
@ -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=
|
|
@ -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"
|
||||
|
||||
|
|
131
msg.go
131
msg.go
|
@ -1,6 +1,7 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
charset: CharsetUTF8,
|
||||
genHeader: make(map[Header][]string),
|
||||
addrHeader: make(map[AddrHeader][]*mail.Address),
|
||||
charset: CharsetUTF8,
|
||||
encoding: EncodingQP,
|
||||
genHeader: make(map[Header][]string),
|
||||
mimever: Mime10,
|
||||
}
|
||||
|
||||
// Override defaults with optionally provided MsgOption functions
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -294,6 +333,40 @@ 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}
|
||||
|
@ -301,15 +374,50 @@ func (m *Msg) Write(w io.Writer) (int64, error) {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
128
msgwriter.go
128
msgwriter.go
|
@ -1,7 +1,12 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -11,17 +16,29 @@ const MaxHeaderLength = 76
|
|||
|
||||
// msgWriter handles the I/O to the io.WriteCloser of the SMTP client
|
||||
type msgWriter struct {
|
||||
w io.Writer
|
||||
n int64
|
||||
//writers [3]*multipart.Writer
|
||||
//partWriter io.Writer
|
||||
//depth uint8
|
||||
d int8
|
||||
err error
|
||||
mpw [3]*multipart.Writer
|
||||
n int64
|
||||
pw io.Writer
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface for msgWriter
|
||||
func (mw *msgWriter) Write(p []byte) (int, error) {
|
||||
if mw.err != nil {
|
||||
return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err)
|
||||
}
|
||||
|
||||
var n int
|
||||
n, mw.err = mw.w.Write(p)
|
||||
mw.n += int64(n)
|
||||
return n, mw.err
|
||||
}
|
||||
|
||||
// writeMsg formats the message and sends it to its io.Writer
|
||||
func (mw *msgWriter) writeMsg(m *Msg) {
|
||||
if _, ok := m.genHeader["Date"]; !ok {
|
||||
if _, ok := m.genHeader[HeaderDate]; !ok {
|
||||
m.SetDate()
|
||||
}
|
||||
for k, v := range m.genHeader {
|
||||
|
@ -36,8 +53,72 @@ func (mw *msgWriter) writeMsg(m *Msg) {
|
|||
mw.writeHeader(Header(t), v...)
|
||||
}
|
||||
}
|
||||
mw.writeString("\r\n")
|
||||
mw.writeString("This is a test mail")
|
||||
mw.writeHeader(HeaderMIMEVersion, string(m.mimever))
|
||||
|
||||
if m.hasAlt() {
|
||||
mw.startMP(MIMEAlternative, m.boundary)
|
||||
mw.writeString("\r\n\r\n")
|
||||
}
|
||||
for _, p := range m.parts {
|
||||
mw.writePart(p, m.charset)
|
||||
}
|
||||
if m.hasAlt() {
|
||||
mw.stopMP()
|
||||
}
|
||||
}
|
||||
|
||||
// startMP writes a multipart beginning
|
||||
func (mw *msgWriter) startMP(mt MIMEType, b string) {
|
||||
mp := multipart.NewWriter(mw)
|
||||
if b != "" {
|
||||
mw.err = mp.SetBoundary(b)
|
||||
}
|
||||
|
||||
ct := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mt, mp.Boundary())
|
||||
mw.mpw[mw.d] = mp
|
||||
|
||||
if mw.d == 0 {
|
||||
mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, ct))
|
||||
}
|
||||
if mw.d > 0 {
|
||||
mw.newPart(map[string][]string{"Content-Type": {ct}})
|
||||
}
|
||||
mw.d++
|
||||
}
|
||||
|
||||
// stopMP closes the multipart
|
||||
func (mw *msgWriter) stopMP() {
|
||||
if mw.d > 0 {
|
||||
mw.err = mw.mpw[mw.d-1].Close()
|
||||
mw.d--
|
||||
}
|
||||
}
|
||||
|
||||
// newPart creates a new MIME multipart io.Writer and sets the partwriter to it
|
||||
func (mw *msgWriter) newPart(h map[string][]string) {
|
||||
mw.pw, mw.err = mw.mpw[mw.d-1].CreatePart(h)
|
||||
}
|
||||
|
||||
// writePart writes the corresponding part to the Msg body
|
||||
func (mw *msgWriter) writePart(p *Part, cs Charset) {
|
||||
mh := textproto.MIMEHeader{}
|
||||
mh.Add(string(HeaderContentType), fmt.Sprintf("%s; charset=%s",
|
||||
p.ctype, cs))
|
||||
mh.Add(string(HeaderContentTransferEnc), string(p.enc))
|
||||
if mw.d > 0 {
|
||||
mw.newPart(mh)
|
||||
}
|
||||
mw.writeBody(p.w, p.enc)
|
||||
}
|
||||
|
||||
// writeString writes a string into the msgWriter's io.Writer interface
|
||||
func (mw *msgWriter) writeString(s string) {
|
||||
if mw.err != nil {
|
||||
return
|
||||
}
|
||||
var n int
|
||||
n, mw.err = io.WriteString(mw.w, s)
|
||||
mw.n += int64(n)
|
||||
}
|
||||
|
||||
// writeHeader writes a header into the msgWriter's io.Writer
|
||||
|
@ -85,12 +166,29 @@ func (mw *msgWriter) writeHeader(k Header, v ...string) {
|
|||
mw.writeString("\r\n")
|
||||
}
|
||||
|
||||
// writeString writes a string into the msgWriter's io.Writer interface
|
||||
func (mw *msgWriter) writeString(s string) {
|
||||
if mw.err != nil {
|
||||
return
|
||||
// writeBody writes an io.Reader into an io.Writer using provided Encoding
|
||||
func (mw *msgWriter) writeBody(f func(io.Writer) error, e Encoding) {
|
||||
var w io.Writer
|
||||
var ew io.WriteCloser
|
||||
if mw.d == 0 {
|
||||
w = mw.w
|
||||
}
|
||||
var n int
|
||||
n, mw.err = io.WriteString(mw.w, s)
|
||||
mw.n += int64(n)
|
||||
if mw.d > 0 {
|
||||
w = mw.pw
|
||||
}
|
||||
|
||||
switch e {
|
||||
case EncodingQP:
|
||||
ew = quotedprintable.NewWriter(w)
|
||||
case EncodingB64:
|
||||
ew = base64.NewEncoder(base64.StdEncoding, w)
|
||||
case NoEncoding:
|
||||
mw.err = f(w)
|
||||
return
|
||||
default:
|
||||
ew = quotedprintable.NewWriter(w)
|
||||
}
|
||||
|
||||
mw.err = f(ew)
|
||||
mw.err = ew.Close()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue