Compare commits

..

3 commits

Author SHA1 Message Date
4e880ab31c
Refactor variable and function names for improved clarity in msg.go
The change updates various variable and function names in msg.go to make the code more intuitive. Updated names better capture what they represent or do, improving code readability and maintainability. This refactor does not impact functionality or logic of the code.
2024-02-24 18:26:30 +01:00
c126670f70
Refactor variable and function names in msgWriter
The commit includes changes such as renaming the variables `wbuf` to `buffer`, `cl` to `charLength` and functions `f` to `writeFunc` in msgWriter. This refactoring makes the code easier to read and understand. All the changes are aimed at enhancing code clarity without altering underlying logic or functionality.
2024-02-24 17:38:42 +01:00
263f6bb3de
Refactor SMTP client code for better readability
The variable names in the code related to the I/O of the SMTP client have been clarified for improved readability and comprehension. For example, unclear variable names like `d` and `w` have been replaced with more meaningful names like `depth` and `writer`. The same naming improvements have also been applied to function parameters. This update aims to enhance code maintenance and simplify future development processes.
2024-02-24 12:43:01 +01:00
3 changed files with 355 additions and 349 deletions

345
msg.go
View file

@ -126,8 +126,8 @@ const SendmailPath = "/usr/sbin/sendmail"
type MsgOption func(*Msg)
// NewMsg returns a new Msg pointer
func NewMsg(o ...MsgOption) *Msg {
m := &Msg{
func NewMsg(opts ...MsgOption) *Msg {
msg := &Msg{
addrHeader: make(map[AddrHeader][]*mail.Address),
charset: CharsetUTF8,
encoding: EncodingQP,
@ -137,17 +137,17 @@ func NewMsg(o ...MsgOption) *Msg {
}
// Override defaults with optionally provided MsgOption functions
for _, co := range o {
if co == nil {
for _, option := range opts {
if option == nil {
continue
}
co(m)
option(msg)
}
// Set the matcing mime.WordEncoder for the Msg
m.setEncoder()
msg.setEncoder()
return m
return msg
}
// WithCharset overrides the default message charset
@ -186,9 +186,9 @@ func WithMiddleware(mw Middleware) MsgOption {
}
// WithPGPType overrides the default PGPType of the message
func WithPGPType(t PGPType) MsgOption {
func WithPGPType(pt PGPType) MsgOption {
return func(m *Msg) {
m.pgptype = t
m.pgptype = pt
}
}
@ -232,20 +232,20 @@ func (m *Msg) Charset() string {
// For adding address headers like "To:" or "From", see SetAddrHeader
//
// Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead
func (m *Msg) SetHeader(h Header, v ...string) {
m.SetGenHeader(h, v...)
func (m *Msg) SetHeader(header Header, values ...string) {
m.SetGenHeader(header, values...)
}
// SetGenHeader sets a generic header field of the Msg
// For adding address headers like "To:" or "From", see SetAddrHeader
func (m *Msg) SetGenHeader(h Header, v ...string) {
func (m *Msg) SetGenHeader(header Header, values ...string) {
if m.genHeader == nil {
m.genHeader = make(map[Header][]string)
}
for i, hv := range v {
v[i] = m.encodeString(hv)
for i, val := range values {
values[i] = m.encodeString(val)
}
m.genHeader[h] = v
m.genHeader[header] = values
}
// SetHeaderPreformatted sets a generic header field of the Msg which content is
@ -253,8 +253,8 @@ func (m *Msg) SetGenHeader(h Header, v ...string) {
//
// Deprecated: This method only exists for compatibility reason. Please use
// SetGenHeaderPreformatted instead
func (m *Msg) SetHeaderPreformatted(h Header, v string) {
m.SetGenHeaderPreformatted(h, v)
func (m *Msg) SetHeaderPreformatted(header Header, value string) {
m.SetGenHeaderPreformatted(header, value)
}
// SetGenHeaderPreformatted sets a generic header field of the Msg which content is
@ -268,206 +268,207 @@ func (m *Msg) SetHeaderPreformatted(h Header, v string) {
// user is respondible for the formating of the message header, go-mail cannot
// guarantee the fully compliance with the RFC 2822. It is recommended to use
// SetGenHeader instead.
func (m *Msg) SetGenHeaderPreformatted(h Header, v string) {
func (m *Msg) SetGenHeaderPreformatted(header Header, value string) {
if m.preformHeader == nil {
m.preformHeader = make(map[Header]string)
}
m.preformHeader[h] = v
m.preformHeader[header] = value
}
// SetAddrHeader sets an address related header field of the Msg
func (m *Msg) SetAddrHeader(h AddrHeader, v ...string) error {
func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error {
if m.addrHeader == nil {
m.addrHeader = make(map[AddrHeader][]*mail.Address)
}
var al []*mail.Address
for _, av := range v {
a, err := mail.ParseAddress(av)
var addresses []*mail.Address
for _, addrVal := range values {
address, err := mail.ParseAddress(addrVal)
if err != nil {
return fmt.Errorf(errParseMailAddr, av, err)
return fmt.Errorf(errParseMailAddr, addrVal, err)
}
al = append(al, a)
addresses = append(addresses, address)
}
switch h {
switch header {
case HeaderFrom:
if len(al) > 0 {
m.addrHeader[h] = []*mail.Address{al[0]}
if len(addresses) > 0 {
m.addrHeader[header] = []*mail.Address{addresses[0]}
}
default:
m.addrHeader[h] = al
m.addrHeader[header] = addresses
}
return nil
}
// SetAddrHeaderIgnoreInvalid sets an address related header field of the Msg and ignores invalid address
// in the validation process
func (m *Msg) SetAddrHeaderIgnoreInvalid(h AddrHeader, v ...string) {
var al []*mail.Address
for _, av := range v {
a, err := mail.ParseAddress(m.encodeString(av))
func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
var addresses []*mail.Address
for _, addrVal := range values {
address, err := mail.ParseAddress(m.encodeString(addrVal))
if err != nil {
continue
}
al = append(al, a)
addresses = append(addresses, address)
}
m.addrHeader[h] = al
m.addrHeader[header] = addresses
}
// EnvelopeFrom takes and validates a given mail address and sets it as envelope "FROM"
// addrHeader of the Msg
func (m *Msg) EnvelopeFrom(f string) error {
return m.SetAddrHeader(HeaderEnvelopeFrom, f)
func (m *Msg) EnvelopeFrom(from string) error {
return m.SetAddrHeader(HeaderEnvelopeFrom, from)
}
// EnvelopeFromFormat takes a name and address, formats them RFC5322 compliant and stores them as
// the envelope FROM address header field
func (m *Msg) EnvelopeFromFormat(n, a string) error {
return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, n, a))
func (m *Msg) EnvelopeFromFormat(name, addr string) error {
return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, name, addr))
}
// From takes and validates a given mail address and sets it as "From" genHeader of the Msg
func (m *Msg) From(f string) error {
return m.SetAddrHeader(HeaderFrom, f)
func (m *Msg) From(from string) error {
return m.SetAddrHeader(HeaderFrom, from)
}
// FromFormat takes a name and address, formats them RFC5322 compliant and stores them as
// the From address header field
func (m *Msg) FromFormat(n, a string) error {
return m.SetAddrHeader(HeaderFrom, fmt.Sprintf(`"%s" <%s>`, n, a))
func (m *Msg) FromFormat(name, addr string) error {
return m.SetAddrHeader(HeaderFrom, fmt.Sprintf(`"%s" <%s>`, name, addr))
}
// To takes and validates a given mail address list sets the To: addresses of the Msg
func (m *Msg) To(t ...string) error {
return m.SetAddrHeader(HeaderTo, t...)
func (m *Msg) To(rcpts ...string) error {
return m.SetAddrHeader(HeaderTo, rcpts...)
}
// AddTo adds an additional address to the To address header field
func (m *Msg) AddTo(t string) error {
return m.addAddr(HeaderTo, t)
func (m *Msg) AddTo(rcpt string) error {
return m.addAddr(HeaderTo, rcpt)
}
// AddToFormat takes a name and address, formats them RFC5322 compliant and stores them as
// as additional To address header field
func (m *Msg) AddToFormat(n, a string) error {
return m.addAddr(HeaderTo, fmt.Sprintf(`"%s" <%s>`, n, a))
func (m *Msg) AddToFormat(name, addr string) error {
return m.addAddr(HeaderTo, fmt.Sprintf(`"%s" <%s>`, name, addr))
}
// ToIgnoreInvalid takes and validates a given mail address list sets the To: addresses of the Msg
// Any provided address that is not RFC5322 compliant, will be ignored
func (m *Msg) ToIgnoreInvalid(t ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderTo, t...)
func (m *Msg) ToIgnoreInvalid(rcpts ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderTo, rcpts...)
}
// ToFromString takes and validates a given string of comma separted
// mail address and sets them as To: addresses of the Msg
func (m *Msg) ToFromString(v string) error {
return m.To(strings.Split(v, ",")...)
func (m *Msg) ToFromString(rcpts string) error {
return m.To(strings.Split(rcpts, ",")...)
}
// Cc takes and validates a given mail address list sets the Cc: addresses of the Msg
func (m *Msg) Cc(c ...string) error {
return m.SetAddrHeader(HeaderCc, c...)
func (m *Msg) Cc(rcpts ...string) error {
return m.SetAddrHeader(HeaderCc, rcpts...)
}
// AddCc adds an additional address to the Cc address header field
func (m *Msg) AddCc(t string) error {
return m.addAddr(HeaderCc, t)
func (m *Msg) AddCc(rcpt string) error {
return m.addAddr(HeaderCc, rcpt)
}
// AddCcFormat takes a name and address, formats them RFC5322 compliant and stores them as
// as additional Cc address header field
func (m *Msg) AddCcFormat(n, a string) error {
return m.addAddr(HeaderCc, fmt.Sprintf(`"%s" <%s>`, n, a))
func (m *Msg) AddCcFormat(name, addr string) error {
return m.addAddr(HeaderCc, fmt.Sprintf(`"%s" <%s>`, name, addr))
}
// CcIgnoreInvalid takes and validates a given mail address list sets the Cc: addresses of the Msg
// Any provided address that is not RFC5322 compliant, will be ignored
func (m *Msg) CcIgnoreInvalid(c ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderCc, c...)
func (m *Msg) CcIgnoreInvalid(rcpts ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderCc, rcpts...)
}
// CcFromString takes and validates a given string of comma separted
// mail address and sets them as Cc: addresses of the Msg
func (m *Msg) CcFromString(v string) error {
return m.Cc(strings.Split(v, ",")...)
func (m *Msg) CcFromString(rcpts string) error {
return m.Cc(strings.Split(rcpts, ",")...)
}
// Bcc takes and validates a given mail address list sets the Bcc: addresses of the Msg
func (m *Msg) Bcc(b ...string) error {
return m.SetAddrHeader(HeaderBcc, b...)
func (m *Msg) Bcc(rcpts ...string) error {
return m.SetAddrHeader(HeaderBcc, rcpts...)
}
// AddBcc adds an additional address to the Bcc address header field
func (m *Msg) AddBcc(t string) error {
return m.addAddr(HeaderBcc, t)
func (m *Msg) AddBcc(rcpt string) error {
return m.addAddr(HeaderBcc, rcpt)
}
// AddBccFormat takes a name and address, formats them RFC5322 compliant and stores them as
// as additional Bcc address header field
func (m *Msg) AddBccFormat(n, a string) error {
return m.addAddr(HeaderBcc, fmt.Sprintf(`"%s" <%s>`, n, a))
func (m *Msg) AddBccFormat(name, addr string) error {
return m.addAddr(HeaderBcc, fmt.Sprintf(`"%s" <%s>`, name, addr))
}
// BccIgnoreInvalid takes and validates a given mail address list sets the Bcc: addresses of the Msg
// Any provided address that is not RFC5322 compliant, will be ignored
func (m *Msg) BccIgnoreInvalid(b ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderBcc, b...)
func (m *Msg) BccIgnoreInvalid(rcpts ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderBcc, rcpts...)
}
// BccFromString takes and validates a given string of comma separted
// mail address and sets them as Bcc: addresses of the Msg
func (m *Msg) BccFromString(v string) error {
return m.Bcc(strings.Split(v, ",")...)
func (m *Msg) BccFromString(rcpts string) error {
return m.Bcc(strings.Split(rcpts, ",")...)
}
// ReplyTo takes and validates a given mail address and sets it as "Reply-To" addrHeader of the Msg
func (m *Msg) ReplyTo(r string) error {
rt, err := mail.ParseAddress(r)
func (m *Msg) ReplyTo(addr string) error {
replyTo, err := mail.ParseAddress(addr)
if err != nil {
return fmt.Errorf("failed to parse reply-to address: %w", err)
}
m.SetGenHeader(HeaderReplyTo, rt.String())
m.SetGenHeader(HeaderReplyTo, replyTo.String())
return nil
}
// ReplyToFormat takes a name and address, formats them RFC5322 compliant and stores them as
// the Reply-To header field
func (m *Msg) ReplyToFormat(n, a string) error {
return m.ReplyTo(fmt.Sprintf(`"%s" <%s>`, n, a))
func (m *Msg) ReplyToFormat(name, addr string) error {
return m.ReplyTo(fmt.Sprintf(`"%s" <%s>`, name, addr))
}
// addAddr adds an additional address to the given addrHeader of the Msg
func (m *Msg) addAddr(h AddrHeader, a string) error {
var al []string
for _, ca := range m.addrHeader[h] {
al = append(al, ca.String())
func (m *Msg) addAddr(header AddrHeader, addr string) error {
var addresses []string
for _, address := range m.addrHeader[header] {
addresses = append(addresses, address.String())
}
al = append(al, a)
return m.SetAddrHeader(h, al...)
addresses = append(addresses, addr)
return m.SetAddrHeader(header, addresses...)
}
// Subject sets the "Subject" header field of the Msg
func (m *Msg) Subject(s string) {
m.SetGenHeader(HeaderSubject, s)
func (m *Msg) Subject(subj string) {
m.SetGenHeader(HeaderSubject, subj)
}
// SetMessageID generates a random message id for the mail
func (m *Msg) SetMessageID() {
hn, err := os.Hostname()
hostname, err := os.Hostname()
if err != nil {
hn = "localhost.localdomain"
hostname = "localhost.localdomain"
}
rn, _ := randNum(100000000)
rm, _ := randNum(10000)
rs, _ := randomStringSecure(17)
pid := os.Getpid() * rm
mid := fmt.Sprintf("%d.%d%d.%s@%s", pid, rn, rm, rs, hn)
m.SetMessageIDWithValue(mid)
randNumPrimary, _ := randNum(100000000)
randNumSecondary, _ := randNum(10000)
randString, _ := randomStringSecure(17)
procID := os.Getpid() * randNumSecondary
messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary,
randString, hostname)
m.SetMessageIDWithValue(messageID)
}
// SetMessageIDWithValue sets the message id for the mail
func (m *Msg) SetMessageIDWithValue(v string) {
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", v))
func (m *Msg) SetMessageIDWithValue(messageID string) {
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID))
}
// SetBulk sets the "Precedence: bulk" and "X-Auto-Response-Suppress: All" genHeaders which are
@ -481,35 +482,35 @@ func (m *Msg) SetBulk() {
// SetDate sets the Date genHeader field to the current time in a valid format
func (m *Msg) SetDate() {
ts := time.Now().Format(time.RFC1123Z)
m.SetGenHeader(HeaderDate, ts)
now := time.Now().Format(time.RFC1123Z)
m.SetGenHeader(HeaderDate, now)
}
// SetDateWithValue sets the Date genHeader field to the provided time in a valid format
func (m *Msg) SetDateWithValue(t time.Time) {
m.SetGenHeader(HeaderDate, t.Format(time.RFC1123Z))
func (m *Msg) SetDateWithValue(timeVal time.Time) {
m.SetGenHeader(HeaderDate, timeVal.Format(time.RFC1123Z))
}
// SetImportance sets the Msg Importance/Priority header to given Importance
func (m *Msg) SetImportance(i Importance) {
if i == ImportanceNormal {
func (m *Msg) SetImportance(importance Importance) {
if importance == ImportanceNormal {
return
}
m.SetGenHeader(HeaderImportance, i.String())
m.SetGenHeader(HeaderPriority, i.NumString())
m.SetGenHeader(HeaderXPriority, i.XPrioString())
m.SetGenHeader(HeaderXMSMailPriority, i.NumString())
m.SetGenHeader(HeaderImportance, importance.String())
m.SetGenHeader(HeaderPriority, importance.NumString())
m.SetGenHeader(HeaderXPriority, importance.XPrioString())
m.SetGenHeader(HeaderXMSMailPriority, importance.NumString())
}
// SetOrganization sets the provided string as Organization header for the Msg
func (m *Msg) SetOrganization(o string) {
m.SetGenHeader(HeaderOrganization, o)
func (m *Msg) SetOrganization(org string) {
m.SetGenHeader(HeaderOrganization, org)
}
// SetUserAgent sets the User-Agent/X-Mailer header for the Msg
func (m *Msg) SetUserAgent(a string) {
m.SetGenHeader(HeaderUserAgent, a)
m.SetGenHeader(HeaderXMailer, a)
func (m *Msg) SetUserAgent(userAgent string) {
m.SetGenHeader(HeaderUserAgent, userAgent)
m.SetGenHeader(HeaderXMailer, userAgent)
}
// IsDelivered will return true if the Msg has been successfully delivered
@ -521,17 +522,17 @@ func (m *Msg) IsDelivered() bool {
// as described in RFC8098. It allows to provide a list recipient addresses.
// Address validation is performed
// See: https://www.rfc-editor.org/rfc/rfc8098.html
func (m *Msg) RequestMDNTo(t ...string) error {
var tl []string
for _, at := range t {
a, err := mail.ParseAddress(at)
func (m *Msg) RequestMDNTo(rcpts ...string) error {
var addresses []string
for _, addrVal := range rcpts {
address, err := mail.ParseAddress(addrVal)
if err != nil {
return fmt.Errorf(errParseMailAddr, at, err)
return fmt.Errorf(errParseMailAddr, addrVal, err)
}
tl = append(tl, a.String())
addresses = append(addresses, address.String())
}
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
m.genHeader[HeaderDispositionNotificationTo] = tl
m.genHeader[HeaderDispositionNotificationTo] = addresses
}
return nil
}
@ -540,77 +541,77 @@ func (m *Msg) RequestMDNTo(t ...string) error {
// as described in RFC8098. It allows to provide a recipient address with name and address and will format
// accordingly. Address validation is performed
// See: https://www.rfc-editor.org/rfc/rfc8098.html
func (m *Msg) RequestMDNToFormat(n, a string) error {
return m.RequestMDNTo(fmt.Sprintf(`%s <%s>`, n, a))
func (m *Msg) RequestMDNToFormat(name, addr string) error {
return m.RequestMDNTo(fmt.Sprintf(`%s <%s>`, name, addr))
}
// RequestMDNAddTo adds an additional recipient to the recipient list of the MDN
func (m *Msg) RequestMDNAddTo(t string) error {
a, err := mail.ParseAddress(t)
func (m *Msg) RequestMDNAddTo(rcpt string) error {
address, err := mail.ParseAddress(rcpt)
if err != nil {
return fmt.Errorf(errParseMailAddr, t, err)
return fmt.Errorf(errParseMailAddr, rcpt, err)
}
var tl []string
tl = append(tl, m.genHeader[HeaderDispositionNotificationTo]...)
tl = append(tl, a.String())
var addresses []string
addresses = append(addresses, m.genHeader[HeaderDispositionNotificationTo]...)
addresses = append(addresses, address.String())
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
m.genHeader[HeaderDispositionNotificationTo] = tl
m.genHeader[HeaderDispositionNotificationTo] = addresses
}
return nil
}
// RequestMDNAddToFormat adds an additional formated recipient to the recipient list of the MDN
func (m *Msg) RequestMDNAddToFormat(n, a string) error {
return m.RequestMDNAddTo(fmt.Sprintf(`"%s" <%s>`, n, a))
func (m *Msg) RequestMDNAddToFormat(name, addr string) error {
return m.RequestMDNAddTo(fmt.Sprintf(`"%s" <%s>`, name, addr))
}
// GetSender returns the currently set envelope FROM address. If no envelope FROM is set it will use
// the first mail body FROM address. If ff is true, it will return the full address string including
// the address name, if set
func (m *Msg) GetSender(ff bool) (string, error) {
f, ok := m.addrHeader[HeaderEnvelopeFrom]
if !ok || len(f) == 0 {
f, ok = m.addrHeader[HeaderFrom]
if !ok || len(f) == 0 {
// the first mail body FROM address. If useFullAddr is true, it will return the full address string
// including the address name, if set
func (m *Msg) GetSender(useFullAddr bool) (string, error) {
from, ok := m.addrHeader[HeaderEnvelopeFrom]
if !ok || len(from) == 0 {
from, ok = m.addrHeader[HeaderFrom]
if !ok || len(from) == 0 {
return "", ErrNoFromAddress
}
}
if ff {
return f[0].String(), nil
if useFullAddr {
return from[0].String(), nil
}
return f[0].Address, nil
return from[0].Address, nil
}
// GetRecipients returns a list of the currently set TO/CC/BCC addresses.
func (m *Msg) GetRecipients() ([]string, error) {
var rl []string
for _, t := range []AddrHeader{HeaderTo, HeaderCc, HeaderBcc} {
al, ok := m.addrHeader[t]
if !ok || len(al) == 0 {
var rcpts []string
for _, addressType := range []AddrHeader{HeaderTo, HeaderCc, HeaderBcc} {
addresses, ok := m.addrHeader[addressType]
if !ok || len(addresses) == 0 {
continue
}
for _, r := range al {
rl = append(rl, r.Address)
for _, r := range addresses {
rcpts = append(rcpts, r.Address)
}
}
if len(rl) <= 0 {
return rl, ErrNoRcptAddresses
if len(rcpts) <= 0 {
return rcpts, ErrNoRcptAddresses
}
return rl, nil
return rcpts, nil
}
// GetAddrHeader returns the content of the requested address header of the Msg
func (m *Msg) GetAddrHeader(h AddrHeader) []*mail.Address {
return m.addrHeader[h]
func (m *Msg) GetAddrHeader(header AddrHeader) []*mail.Address {
return m.addrHeader[header]
}
// GetAddrHeaderString returns the address string of the requested address header of the Msg
func (m *Msg) GetAddrHeaderString(h AddrHeader) []string {
var al []string
for _, mh := range m.addrHeader[h] {
al = append(al, mh.String())
func (m *Msg) GetAddrHeaderString(header AddrHeader) []string {
var addresses []string
for _, mh := range m.addrHeader[header] {
addresses = append(addresses, mh.String())
}
return al
return addresses
}
// GetFrom returns the content of the From address header of the Msg
@ -654,8 +655,8 @@ func (m *Msg) GetBccString() []string {
}
// GetGenHeader returns the content of the requested generic header of the Msg
func (m *Msg) GetGenHeader(h Header) []string {
return m.genHeader[h]
func (m *Msg) GetGenHeader(header Header) []string {
return m.genHeader[header]
}
// GetParts returns the message parts of the Msg
@ -669,8 +670,8 @@ func (m *Msg) GetAttachments() []*File {
}
// SetAttachements sets the attachements of the message.
func (m *Msg) SetAttachements(ff []*File) {
m.attachments = ff
func (m *Msg) SetAttachements(files []*File) {
m.attachments = files
}
// UnsetAllAttachments unset the attachments of the message.
@ -684,8 +685,8 @@ func (m *Msg) GetEmbeds() []*File {
}
// SetEmbeds sets the embeds of the message.
func (m *Msg) SetEmbeds(ff []*File) {
m.embeds = ff
func (m *Msg) SetEmbeds(files []*File) {
m.embeds = files
}
// UnsetAllEmbeds unset the embeds of the message.
@ -700,16 +701,16 @@ func (m *Msg) UnsetAllParts() {
}
// SetBodyString sets the body of the message.
func (m *Msg) SetBodyString(ct ContentType, b string, o ...PartOption) {
buf := bytes.NewBufferString(b)
w := writeFuncFromBuffer(buf)
m.SetBodyWriter(ct, w, o...)
func (m *Msg) SetBodyString(contentType ContentType, content string, opts ...PartOption) {
buffer := bytes.NewBufferString(content)
writeFunc := writeFuncFromBuffer(buffer)
m.SetBodyWriter(contentType, writeFunc, opts...)
}
// SetBodyWriter sets the body of the message.
func (m *Msg) SetBodyWriter(ct ContentType, w func(io.Writer) (int64, error), o ...PartOption) {
p := m.newPart(ct, o...)
p.w = w
func (m *Msg) SetBodyWriter(contentType ContentType, writeFunc func(io.Writer) (int64, error), opts ...PartOption) {
p := m.newPart(contentType, opts...)
p.w = writeFunc
m.parts = []*Part{p}
}
@ -933,9 +934,9 @@ func (m *Msg) applyMiddlewares(ms *Msg) *Msg {
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface
func (m *Msg) WriteTo(w io.Writer) (int64, error) {
mw := &msgWriter{w: w, c: m.charset, en: m.encoder}
mw := &msgWriter{writer: w, charset: m.charset, encoder: m.encoder}
mw.writeMsg(m.applyMiddlewares(m))
return mw.n, mw.err
return mw.bytesWritten, mw.err
}
// WriteToSkipMiddleware writes the formated Msg into a give io.Writer and satisfies
@ -950,10 +951,10 @@ func (m *Msg) WriteToSkipMiddleware(w io.Writer, mt MiddlewareType) (int64, erro
mwl = append(mwl, m.middlewares[i])
}
m.middlewares = mwl
mw := &msgWriter{w: w, c: m.charset, en: m.encoder}
mw := &msgWriter{writer: w, charset: m.charset, encoder: m.encoder}
mw.writeMsg(m.applyMiddlewares(m))
m.middlewares = omwl
return mw.n, mw.err
return mw.bytesWritten, mw.err
}
// Write is an alias method to WriteTo due to compatibility reasons

View file

@ -35,237 +35,241 @@ const DoubleNewLine = "\r\n\r\n"
// msgWriter handles the I/O to the io.WriteCloser of the SMTP client
type msgWriter struct {
c Charset
d int8
en mime.WordEncoder
err error
mpw [3]*multipart.Writer
n int64
pw io.Writer
w io.Writer
bytesWritten int64
charset Charset
depth int8
encoder mime.WordEncoder
err error
multiPartWriter [3]*multipart.Writer
partWriter io.Writer
writer io.Writer
}
// Write implements the io.Writer interface for msgWriter
func (mw *msgWriter) Write(p []byte) (int, error) {
func (mw *msgWriter) Write(payload []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)
n, mw.err = mw.writer.Write(payload)
mw.bytesWritten += int64(n)
return n, mw.err
}
// writeMsg formats the message and sends it to its io.Writer
func (mw *msgWriter) writeMsg(m *Msg) {
m.addDefaultHeader()
m.checkUserAgent()
mw.writeGenHeader(m)
mw.writePreformattedGenHeader(m)
func (mw *msgWriter) writeMsg(msg *Msg) {
msg.addDefaultHeader()
msg.checkUserAgent()
mw.writeGenHeader(msg)
mw.writePreformattedGenHeader(msg)
// Set the FROM header (or envelope FROM if FROM is empty)
hf := true
f, ok := m.addrHeader[HeaderFrom]
if !ok || (len(f) == 0 || f == nil) {
f, ok = m.addrHeader[HeaderEnvelopeFrom]
if !ok || (len(f) == 0 || f == nil) {
hf = false
hasFrom := true
from, ok := msg.addrHeader[HeaderFrom]
if !ok || (len(from) == 0 || from == nil) {
from, ok = msg.addrHeader[HeaderEnvelopeFrom]
if !ok || (len(from) == 0 || from == nil) {
hasFrom = false
}
}
if hf && (len(f) > 0 && f[0] != nil) {
mw.writeHeader(Header(HeaderFrom), f[0].String())
if hasFrom && (len(from) > 0 && from[0] != nil) {
mw.writeHeader(Header(HeaderFrom), from[0].String())
}
// Set the rest of the address headers
for _, t := range []AddrHeader{HeaderTo, HeaderCc} {
if al, ok := m.addrHeader[t]; ok {
var v []string
for _, a := range al {
v = append(v, a.String())
for _, to := range []AddrHeader{HeaderTo, HeaderCc} {
if addresses, ok := msg.addrHeader[to]; ok {
var val []string
for _, addr := range addresses {
val = append(val, addr.String())
}
mw.writeHeader(Header(t), v...)
mw.writeHeader(Header(to), val...)
}
}
if m.hasMixed() {
mw.startMP("mixed", m.boundary)
if msg.hasMixed() {
mw.startMP("mixed", msg.boundary)
mw.writeString(DoubleNewLine)
}
if m.hasRelated() {
mw.startMP("related", m.boundary)
if msg.hasRelated() {
mw.startMP("related", msg.boundary)
mw.writeString(DoubleNewLine)
}
if m.hasAlt() {
mw.startMP(MIMEAlternative, m.boundary)
if msg.hasAlt() {
mw.startMP(MIMEAlternative, msg.boundary)
mw.writeString(DoubleNewLine)
}
if m.hasPGPType() {
switch m.pgptype {
if msg.hasPGPType() {
switch msg.pgptype {
case PGPEncrypt:
mw.startMP(`encrypted; protocol="application/pgp-encrypted"`, m.boundary)
mw.startMP(`encrypted; protocol="application/pgp-encrypted"`,
msg.boundary)
case PGPSignature:
mw.startMP(`signed; protocol="application/pgp-signature";`, m.boundary)
mw.startMP(`signed; protocol="application/pgp-signature";`,
msg.boundary)
default:
}
mw.writeString(DoubleNewLine)
}
for _, p := range m.parts {
if !p.del {
mw.writePart(p, m.charset)
for _, part := range msg.parts {
if !part.del {
mw.writePart(part, msg.charset)
}
}
if m.hasAlt() {
if msg.hasAlt() {
mw.stopMP()
}
// Add embeds
mw.addFiles(m.embeds, false)
if m.hasRelated() {
mw.addFiles(msg.embeds, false)
if msg.hasRelated() {
mw.stopMP()
}
// Add attachments
mw.addFiles(m.attachments, true)
if m.hasMixed() {
mw.addFiles(msg.attachments, true)
if msg.hasMixed() {
mw.stopMP()
}
}
// writeGenHeader writes out all generic headers to the msgWriter
func (mw *msgWriter) writeGenHeader(m *Msg) {
gk := make([]string, 0, len(m.genHeader))
for k := range m.genHeader {
gk = append(gk, string(k))
func (mw *msgWriter) writeGenHeader(msg *Msg) {
keys := make([]string, 0, len(msg.genHeader))
for key := range msg.genHeader {
keys = append(keys, string(key))
}
sort.Strings(gk)
for _, k := range gk {
mw.writeHeader(Header(k), m.genHeader[Header(k)]...)
sort.Strings(keys)
for _, key := range keys {
mw.writeHeader(Header(key), msg.genHeader[Header(key)]...)
}
}
// writePreformatedHeader writes out all preformated generic headers to the msgWriter
func (mw *msgWriter) writePreformattedGenHeader(m *Msg) {
for k, v := range m.preformHeader {
mw.writeString(fmt.Sprintf("%s: %s%s", k, v, SingleNewLine))
func (mw *msgWriter) writePreformattedGenHeader(msg *Msg) {
for key, val := range msg.preformHeader {
mw.writeString(fmt.Sprintf("%s: %s%s", key, val, SingleNewLine))
}
}
// startMP writes a multipart beginning
func (mw *msgWriter) startMP(mt MIMEType, b string) {
mp := multipart.NewWriter(mw)
if b != "" {
mw.err = mp.SetBoundary(b)
func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
multiPartWriter := multipart.NewWriter(mw)
if boundary != "" {
mw.err = multiPartWriter.SetBoundary(boundary)
}
ct := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mt, mp.Boundary())
mw.mpw[mw.d] = mp
contentType := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mimeType,
multiPartWriter.Boundary())
mw.multiPartWriter[mw.depth] = multiPartWriter
if mw.d == 0 {
mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, ct))
if mw.depth == 0 {
mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, contentType))
}
if mw.d > 0 {
mw.newPart(map[string][]string{"Content-Type": {ct}})
if mw.depth > 0 {
mw.newPart(map[string][]string{"Content-Type": {contentType}})
}
mw.d++
mw.depth++
}
// stopMP closes the multipart
func (mw *msgWriter) stopMP() {
if mw.d > 0 {
mw.err = mw.mpw[mw.d-1].Close()
mw.d--
if mw.depth > 0 {
mw.err = mw.multiPartWriter[mw.depth-1].Close()
mw.depth--
}
}
// addFiles adds the attachments/embeds file content to the mail body
func (mw *msgWriter) addFiles(fl []*File, a bool) {
for _, f := range fl {
e := EncodingB64
if _, ok := f.getHeader(HeaderContentType); !ok {
mt := mime.TypeByExtension(filepath.Ext(f.Name))
if mt == "" {
mt = "application/octet-stream"
func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
for _, file := range files {
encoding := EncodingB64
if _, ok := file.getHeader(HeaderContentType); !ok {
mimeType := mime.TypeByExtension(filepath.Ext(file.Name))
if mimeType == "" {
mimeType = "application/octet-stream"
}
if f.ContentType != "" {
mt = string(f.ContentType)
if file.ContentType != "" {
mimeType = string(file.ContentType)
}
f.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mt,
mw.en.Encode(mw.c.String(), f.Name)))
file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType,
mw.encoder.Encode(mw.charset.String(), file.Name)))
}
if _, ok := f.getHeader(HeaderContentTransferEnc); !ok {
if f.Enc != "" {
e = f.Enc
if _, ok := file.getHeader(HeaderContentTransferEnc); !ok {
if file.Enc != "" {
encoding = file.Enc
}
f.setHeader(HeaderContentTransferEnc, string(e))
file.setHeader(HeaderContentTransferEnc, string(encoding))
}
if f.Desc != "" {
if _, ok := f.getHeader(HeaderContentDescription); !ok {
f.setHeader(HeaderContentDescription, f.Desc)
if file.Desc != "" {
if _, ok := file.getHeader(HeaderContentDescription); !ok {
file.setHeader(HeaderContentDescription, file.Desc)
}
}
if _, ok := f.getHeader(HeaderContentDisposition); !ok {
d := "inline"
if a {
d = "attachment"
if _, ok := file.getHeader(HeaderContentDisposition); !ok {
disposition := "inline"
if isAttachment {
disposition = "attachment"
}
f.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, d,
mw.en.Encode(mw.c.String(), f.Name)))
file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`,
disposition, mw.encoder.Encode(mw.charset.String(), file.Name)))
}
if !a {
if _, ok := f.getHeader(HeaderContentID); !ok {
f.setHeader(HeaderContentID, fmt.Sprintf("<%s>", f.Name))
if !isAttachment {
if _, ok := file.getHeader(HeaderContentID); !ok {
file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", file.Name))
}
}
if mw.d == 0 {
for h, v := range f.Header {
mw.writeHeader(Header(h), v...)
if mw.depth == 0 {
for header, val := range file.Header {
mw.writeHeader(Header(header), val...)
}
mw.writeString(SingleNewLine)
}
if mw.d > 0 {
mw.newPart(f.Header)
if mw.depth > 0 {
mw.newPart(file.Header)
}
if mw.err == nil {
mw.writeBody(f.Writer, e)
mw.writeBody(file.Writer, encoding)
}
}
}
// 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)
func (mw *msgWriter) newPart(header map[string][]string) {
mw.partWriter, mw.err = mw.multiPartWriter[mw.depth-1].CreatePart(header)
}
// writePart writes the corresponding part to the Msg body
func (mw *msgWriter) writePart(p *Part, cs Charset) {
pcs := p.cset
if pcs.String() == "" {
pcs = cs
func (mw *msgWriter) writePart(part *Part, charset Charset) {
partCharset := part.cset
if partCharset.String() == "" {
partCharset = charset
}
ct := fmt.Sprintf("%s; charset=%s", p.ctype, pcs)
cte := p.enc.String()
if mw.d == 0 {
mw.writeHeader(HeaderContentType, ct)
mw.writeHeader(HeaderContentTransferEnc, cte)
contentType := fmt.Sprintf("%s; charset=%s", part.ctype, partCharset)
contentTransferEnc := part.enc.String()
if mw.depth == 0 {
mw.writeHeader(HeaderContentType, contentType)
mw.writeHeader(HeaderContentTransferEnc, contentTransferEnc)
mw.writeString(SingleNewLine)
}
if mw.d > 0 {
mh := textproto.MIMEHeader{}
if p.desc != "" {
mh.Add(string(HeaderContentDescription), p.desc)
if mw.depth > 0 {
mimeHeader := textproto.MIMEHeader{}
if part.desc != "" {
mimeHeader.Add(string(HeaderContentDescription), part.desc)
}
mh.Add(string(HeaderContentType), ct)
mh.Add(string(HeaderContentTransferEnc), cte)
mw.newPart(mh)
mimeHeader.Add(string(HeaderContentType), contentType)
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
mw.newPart(mimeHeader)
}
mw.writeBody(p.w, p.enc)
mw.writeBody(part.w, part.enc)
}
// writeString writes a string into the msgWriter's io.Writer interface
@ -274,102 +278,103 @@ func (mw *msgWriter) writeString(s string) {
return
}
var n int
n, mw.err = io.WriteString(mw.w, s)
mw.n += int64(n)
n, mw.err = io.WriteString(mw.writer, s)
mw.bytesWritten += int64(n)
}
// writeHeader writes a header into the msgWriter's io.Writer
func (mw *msgWriter) writeHeader(k Header, vl ...string) {
wbuf := bytes.Buffer{}
cl := MaxHeaderLength - 2
wbuf.WriteString(string(k))
cl -= len(k)
if len(vl) == 0 {
wbuf.WriteString(":\r\n")
func (mw *msgWriter) writeHeader(key Header, values ...string) {
buffer := strings.Builder{}
charLength := MaxHeaderLength - 2
buffer.WriteString(string(key))
charLength -= len(key)
if len(values) == 0 {
buffer.WriteString(":\r\n")
return
}
wbuf.WriteString(": ")
cl -= 2
buffer.WriteString(": ")
charLength -= 2
fs := strings.Join(vl, ", ")
sfs := strings.Split(fs, " ")
for i, v := range sfs {
if cl-len(v) <= 1 {
wbuf.WriteString(fmt.Sprintf("%s ", SingleNewLine))
cl = MaxHeaderLength - 3
fullValueStr := strings.Join(values, ", ")
words := strings.Split(fullValueStr, " ")
for i, val := range words {
if charLength-len(val) <= 1 {
buffer.WriteString(fmt.Sprintf("%s ", SingleNewLine))
charLength = MaxHeaderLength - 3
}
wbuf.WriteString(v)
if i < len(sfs)-1 {
wbuf.WriteString(" ")
cl -= 1
buffer.WriteString(val)
if i < len(words)-1 {
buffer.WriteString(" ")
charLength -= 1
}
cl -= len(v)
charLength -= len(val)
}
bufs := wbuf.String()
bufs = strings.ReplaceAll(bufs, fmt.Sprintf(" %s", SingleNewLine), SingleNewLine)
mw.writeString(bufs)
bufferString := buffer.String()
bufferString = strings.ReplaceAll(bufferString, fmt.Sprintf(" %s", SingleNewLine),
SingleNewLine)
mw.writeString(bufferString)
mw.writeString("\r\n")
}
// writeBody writes an io.Reader into an io.Writer using provided Encoding
func (mw *msgWriter) writeBody(f func(io.Writer) (int64, error), e Encoding) {
var w io.Writer
var ew io.WriteCloser
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
var writer io.Writer
var encodedWriter io.WriteCloser
var n int64
var err error
if mw.d == 0 {
w = mw.w
if mw.depth == 0 {
writer = mw.writer
}
if mw.d > 0 {
w = mw.pw
if mw.depth > 0 {
writer = mw.partWriter
}
wbuf := bytes.Buffer{}
lb := Base64LineBreaker{}
lb.out = &wbuf
writeBuffer := bytes.Buffer{}
lineBreaker := Base64LineBreaker{}
lineBreaker.out = &writeBuffer
switch e {
switch encoding {
case EncodingQP:
ew = quotedprintable.NewWriter(&wbuf)
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
case EncodingB64:
ew = base64.NewEncoder(base64.StdEncoding, &lb)
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
case NoEncoding:
_, err = f(&wbuf)
_, err = writeFunc(&writeBuffer)
if err != nil {
mw.err = fmt.Errorf("bodyWriter function: %w", err)
}
n, err = io.Copy(w, &wbuf)
n, err = io.Copy(writer, &writeBuffer)
if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
}
if mw.d == 0 {
mw.n += n
if mw.depth == 0 {
mw.bytesWritten += n
}
return
default:
ew = quotedprintable.NewWriter(w)
encodedWriter = quotedprintable.NewWriter(writer)
}
_, err = f(ew)
_, err = writeFunc(encodedWriter)
if err != nil {
mw.err = fmt.Errorf("bodyWriter function: %w", err)
}
err = ew.Close()
err = encodedWriter.Close()
if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter close encoded writer: %w", err)
}
err = lb.Close()
err = lineBreaker.Close()
if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter close linebreaker: %w", err)
}
n, err = io.Copy(w, &wbuf)
n, err = io.Copy(writer, &writeBuffer)
if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
}
// Since the part writer uses the WriteTo() method, we don't need to add the
// bytes twice
if mw.d == 0 {
mw.n += n
if mw.depth == 0 {
mw.bytesWritten += n
}
}

View file

@ -28,7 +28,7 @@ func (bw *brokenWriter) Write([]byte) (int, error) {
// TestMsgWriter_Write tests the WriteTo() method of the msgWriter
func TestMsgWriter_Write(t *testing.T) {
bw := &brokenWriter{}
mw := &msgWriter{w: bw, c: CharsetUTF8, en: mime.QEncoding}
mw := &msgWriter{writer: bw, charset: CharsetUTF8, encoder: mime.QEncoding}
_, err := mw.Write([]byte("test"))
if err == nil {
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
@ -55,7 +55,7 @@ func TestMsgWriter_writeMsg(t *testing.T) {
m.SetBodyString(TypeTextPlain, "This is the body")
m.AddAlternativeString(TypeTextHTML, "This is the alternative body")
buf := bytes.Buffer{}
mw := &msgWriter{w: &buf, c: CharsetUTF8, en: mime.QEncoding}
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
mw.writeMsg(m)
ms := buf.String()
@ -134,7 +134,7 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) {
m.Subject("This is a subject")
m.SetBodyString(TypeTextPlain, "This is the body")
buf := bytes.Buffer{}
mw := &msgWriter{w: &buf, c: CharsetUTF8, en: mime.QEncoding}
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
mw.writeMsg(m)
ms := buf.String()
if !strings.Contains(ms, `encrypted; protocol="application/pgp-encrypted"`) {
@ -147,7 +147,7 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) {
m.Subject("This is a subject")
m.SetBodyString(TypeTextPlain, "This is the body")
buf = bytes.Buffer{}
mw = &msgWriter{w: &buf, c: CharsetUTF8, en: mime.QEncoding}
mw = &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
mw.writeMsg(m)
ms = buf.String()
if !strings.Contains(ms, `signed; protocol="application/pgp-signature"`) {