mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-22 18:50:37 +01:00
v0.1.1: Added embeds/attachments
This commit is contained in:
parent
13f4c54a27
commit
1157180369
8 changed files with 271 additions and 60 deletions
|
@ -27,8 +27,8 @@ Some of the features of this library:
|
|||
* [X] RFC5322 compliant mail address validation
|
||||
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, etc.)
|
||||
* [X] Reusing the same SMTP connection to send multiple mails
|
||||
* [X] Support for attachments and inline embeds
|
||||
* [ ] Support for different encodings
|
||||
* [ ] Support for attachments
|
||||
|
||||
## Examples
|
||||
The packageis shipped with example code. Check it out on the packages
|
||||
|
|
|
@ -83,6 +83,10 @@ var (
|
|||
// ErrNoActiveConnection should be used when a method is used that requies a server connection
|
||||
// but is not yet connected
|
||||
ErrNoActiveConnection = errors.New("not connected to SMTP server")
|
||||
|
||||
// ErrServerNoUnencoded should be used when 8BIT encoding is selected for a message, but
|
||||
// the server does not offer 8BITMIME mode
|
||||
ErrServerNoUnencoded = errors.New("message is 8bit unencoded, but server does not support 8BITMIME")
|
||||
)
|
||||
|
||||
// NewClient returns a new Session client object
|
||||
|
@ -281,6 +285,11 @@ func (c *Client) Send(ml ...*Msg) error {
|
|||
return fmt.Errorf("failed to send mail: %w", err)
|
||||
}
|
||||
for _, m := range ml {
|
||||
if m.encoding == NoEncoding {
|
||||
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
|
||||
return ErrServerNoUnencoded
|
||||
}
|
||||
}
|
||||
f, err := m.GetSender(false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
58
cmd/main.go
58
cmd/main.go
|
@ -1,9 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/wneessen/go-mail"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
@ -27,34 +27,46 @@ func main() {
|
|||
fmt.Printf("failed to set TO address: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
m.Subject("This is the Subject with Umlauts: üöäß")
|
||||
m.SetHeader(mail.HeaderContentLang, "de", "en", "fr", "sp", "de", "en", "fr", "sp", "de", "en", "fr",
|
||||
"sp", "de", "en", "fr", "sp")
|
||||
m.SetHeader(mail.HeaderListUnsubscribePost, "üüüüüüüü", "aaaaääää", "ßßßßßßßßß", "XXXXXX", "ZZZZZ", "XXXXXXXX",
|
||||
"äää äää", "YYYYYY", "XXXXXX", "ZZZZZ", "üäö´")
|
||||
m.Subject("This is a mail with attachments")
|
||||
m.SetMessageID()
|
||||
m.SetDate()
|
||||
m.SetBulk()
|
||||
m.SetBodyString(mail.TypeTextPlain, "This should have an attachment.")
|
||||
|
||||
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))
|
||||
f, err := os.Open("/home/wneessen/certs.csv")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create new client: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := c.DialAndSend(m); err != nil {
|
||||
fmt.Printf("failed to dial: %s\n", err)
|
||||
fmt.Printf("failed to open file for reading: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Printf("failed to close file: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
m.AttachReader("certs.csv", f, mail.WithFileName("test.txt"))
|
||||
|
||||
sendMail := flag.Bool("send", false, "wether to send mail or output to STDOUT")
|
||||
flag.Parse()
|
||||
if !*sendMail {
|
||||
_, err := m.Write(os.Stdout)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to write mail: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
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))
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create new client: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := c.DialAndSend(m); err != nil {
|
||||
fmt.Printf("failed to dial: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
33
file.go
Normal file
33
file.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/textproto"
|
||||
)
|
||||
|
||||
// FileOption returns a function that can be used for grouping File options
|
||||
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) error
|
||||
}
|
||||
|
||||
// WithFileName sets the filename of the File
|
||||
func WithFileName(n string) FileOption {
|
||||
return func(f *File) {
|
||||
f.Name = n
|
||||
}
|
||||
}
|
||||
|
||||
// setHeader sets header fields to a File
|
||||
func (f *File) setHeader(h Header, v string) {
|
||||
f.Header.Set(string(h), v)
|
||||
}
|
||||
|
||||
func (f *File) getHeader(h Header) (string, bool) {
|
||||
v := f.Header.Get(string(h))
|
||||
return v, v != ""
|
||||
}
|
|
@ -14,6 +14,9 @@ const (
|
|||
// HeaderContentDisposition is the "Content-Disposition" header
|
||||
HeaderContentDisposition Header = "Content-Disposition"
|
||||
|
||||
// HeaderContentID is the "Content-ID" header
|
||||
HeaderContentID Header = "Content-ID"
|
||||
|
||||
// HeaderContentLang is the "Content-Language" header
|
||||
HeaderContentLang Header = "Content-Language"
|
||||
|
||||
|
|
137
msg.go
137
msg.go
|
@ -9,6 +9,7 @@ import (
|
|||
"mime"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -45,17 +46,13 @@ type Msg struct {
|
|||
|
||||
// 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
|
||||
ctype ContentType
|
||||
enc Encoding
|
||||
}
|
||||
// attachments represent the different attachment File of the Msg
|
||||
attachments []*File
|
||||
|
||||
// PartOption returns a function that can be used for grouping Part options
|
||||
type PartOption func(*Part)
|
||||
// embeds represent the different embedded File of the Msg
|
||||
embeds []*File
|
||||
}
|
||||
|
||||
// MsgOption returns a function that can be used for grouping Msg options
|
||||
type MsgOption func(*Msg)
|
||||
|
@ -372,7 +369,7 @@ func (m *Msg) SetBodyString(ct ContentType, b string, o ...PartOption) {
|
|||
|
||||
// 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 := m.newPart(ct, o...)
|
||||
p.w = w
|
||||
m.parts = []*Part{p}
|
||||
}
|
||||
|
@ -389,11 +386,31 @@ func (m *Msg) AddAlternativeString(ct ContentType, b string, o ...PartOption) {
|
|||
|
||||
// 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 := m.newPart(ct, o...)
|
||||
p.w = w
|
||||
m.parts = append(m.parts, p)
|
||||
}
|
||||
|
||||
// AttachFile adds an attachment File to the Msg
|
||||
func (m *Msg) AttachFile(n string, o ...FileOption) {
|
||||
m.attachments = m.appendFile(m.attachments, fileFromFS(n), o...)
|
||||
}
|
||||
|
||||
// AttachReader adds an attachment File via io.Reader to the Msg
|
||||
func (m *Msg) AttachReader(n string, r io.Reader, o ...FileOption) {
|
||||
m.attachments = m.appendFile(m.attachments, fileFromReader(n, r), o...)
|
||||
}
|
||||
|
||||
// EmbedFile adds an embedded File to the Msg
|
||||
func (m *Msg) EmbedFile(n string, o ...FileOption) {
|
||||
m.embeds = m.appendFile(m.embeds, fileFromFS(n), o...)
|
||||
}
|
||||
|
||||
// EmbedReader adds an embedded File from an io.Reader to the Msg
|
||||
func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) {
|
||||
m.embeds = m.appendFile(m.embeds, fileFromReader(n, r), o...)
|
||||
}
|
||||
|
||||
// Write writes the formated Msg into a give io.Writer
|
||||
func (m *Msg) Write(w io.Writer) (int64, error) {
|
||||
mw := &msgWriter{w: w}
|
||||
|
@ -401,8 +418,46 @@ 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 {
|
||||
// appendFile adds a File to the Msg (as attachment or embed)
|
||||
func (m *Msg) appendFile(c []*File, f *File, o ...FileOption) []*File {
|
||||
// Override defaults with optionally provided FileOption functions
|
||||
for _, co := range o {
|
||||
if co == nil {
|
||||
continue
|
||||
}
|
||||
co(f)
|
||||
}
|
||||
|
||||
if c == nil {
|
||||
return []*File{f}
|
||||
}
|
||||
|
||||
return append(c, f)
|
||||
}
|
||||
|
||||
// encodeString encodes a string based on the configured message encoder and the corresponding
|
||||
// charset for the Msg
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// newPart returns a new Part for the Msg
|
||||
func (m *Msg) newPart(ct ContentType, o ...PartOption) *Part {
|
||||
p := &Part{
|
||||
ctype: ct,
|
||||
enc: m.encoding,
|
||||
|
@ -419,23 +474,44 @@ func (m *Msg) NewPart(ct ContentType, o ...PartOption) *Part {
|
|||
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() {
|
||||
m.encoder = getEncoder(m.encoding)
|
||||
}
|
||||
|
||||
// fileFromFS returns a File pointer from a given file in the system's file system
|
||||
func fileFromFS(n string) *File {
|
||||
return &File{
|
||||
Name: filepath.Base(n),
|
||||
Header: make(map[string][]string),
|
||||
Writer: func(w io.Writer) error {
|
||||
h, err := os.Open(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, h); err != nil {
|
||||
_ = h.Close()
|
||||
return fmt.Errorf("failed to copy file to io.Writer: %w", err)
|
||||
}
|
||||
return h.Close()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// fileFromReader returns a File pointer from a given io.Reader
|
||||
func fileFromReader(n string, r io.Reader) *File {
|
||||
return &File{
|
||||
Name: filepath.Base(n),
|
||||
Header: make(map[string][]string),
|
||||
Writer: func(w io.Writer) error {
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
|
||||
func getEncoder(e Encoding) mime.WordEncoder {
|
||||
switch e {
|
||||
|
@ -447,14 +523,3 @@ func getEncoder(e Encoding) mime.WordEncoder {
|
|||
return mime.QEncoding
|
||||
}
|
||||
}
|
||||
|
||||
// encodeString encodes a string based on the configured message encoder and the corresponding
|
||||
// charset for the Msg
|
||||
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
|
||||
}
|
||||
|
|
64
msgwriter.go
64
msgwriter.go
|
@ -4,9 +4,11 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/textproto"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -55,16 +57,37 @@ func (mw *msgWriter) writeMsg(m *Msg) {
|
|||
}
|
||||
mw.writeHeader(HeaderMIMEVersion, string(m.mimever))
|
||||
|
||||
if m.hasMixed() {
|
||||
mw.startMP("mixed", m.boundary)
|
||||
mw.writeString("\r\n\r\n")
|
||||
}
|
||||
if m.hasRelated() {
|
||||
mw.startMP("related", m.boundary)
|
||||
mw.writeString("\r\n\r\n")
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
// Add embeds
|
||||
mw.addFiles(m.embeds, false)
|
||||
if m.hasRelated() {
|
||||
mw.stopMP()
|
||||
}
|
||||
|
||||
// Add attachments
|
||||
mw.addFiles(m.attachments, true)
|
||||
if m.hasMixed() {
|
||||
mw.stopMP()
|
||||
}
|
||||
}
|
||||
|
||||
// startMP writes a multipart beginning
|
||||
|
@ -94,6 +117,47 @@ func (mw *msgWriter) stopMP() {
|
|||
}
|
||||
}
|
||||
|
||||
// addFiles adds the attachments/embeds file content to the mail body
|
||||
func (mw *msgWriter) addFiles(fl []*File, a bool) {
|
||||
for _, f := range fl {
|
||||
if _, ok := f.getHeader(HeaderContentType); !ok {
|
||||
mt := mime.TypeByExtension(filepath.Ext(f.Name))
|
||||
if mt == "" {
|
||||
mt = "application/octet-stream"
|
||||
}
|
||||
f.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mt, f.Name))
|
||||
}
|
||||
|
||||
if _, ok := f.getHeader(HeaderContentTransferEnc); !ok {
|
||||
f.setHeader(HeaderContentTransferEnc, string(EncodingB64))
|
||||
}
|
||||
|
||||
if _, ok := f.getHeader(HeaderContentDisposition); !ok {
|
||||
d := "inline"
|
||||
if a {
|
||||
d = "attachment"
|
||||
}
|
||||
f.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, d, f.Name))
|
||||
}
|
||||
|
||||
if !a {
|
||||
if _, ok := f.getHeader(HeaderContentID); !ok {
|
||||
f.setHeader(HeaderContentID, fmt.Sprintf("<%s>", f.Name))
|
||||
}
|
||||
}
|
||||
if mw.d == 0 {
|
||||
for h, v := range f.Header {
|
||||
mw.writeHeader(Header(h), v...)
|
||||
}
|
||||
mw.writeString("\r\n")
|
||||
}
|
||||
if mw.d > 0 {
|
||||
mw.newPart(f.Header)
|
||||
}
|
||||
mw.writeBody(f.Writer, EncodingB64)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
25
part.go
Normal file
25
part.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package mail
|
||||
|
||||
import "io"
|
||||
|
||||
// PartOption returns a function that can be used for grouping Part options
|
||||
type PartOption func(*Part)
|
||||
|
||||
// Part is a part of the Msg
|
||||
type Part struct {
|
||||
ctype ContentType
|
||||
enc Encoding
|
||||
w func(io.Writer) error
|
||||
}
|
||||
|
||||
// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message
|
||||
func (p *Part) SetEncoding(e Encoding) {
|
||||
p.enc = e
|
||||
}
|
||||
|
||||
// WithPartEncoding overrides the default Part encoding
|
||||
func WithPartEncoding(e Encoding) PartOption {
|
||||
return func(p *Part) {
|
||||
p.enc = e
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue