mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-23 02:50:39 +01:00
Merge branch 'main' into feature/145_add-eml-parser-to-generate-msg-from-eml-files
This commit is contained in:
commit
8b19e497a0
9 changed files with 78 additions and 35 deletions
28
msg.go
28
msg.go
|
@ -792,9 +792,13 @@ func (m *Msg) AttachFile(n string, o ...FileOption) {
|
|||
// 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 recommended to
|
||||
// either use AttachFile or AttachReadSeeker instead
|
||||
func (m *Msg) AttachReader(n string, r io.Reader, o ...FileOption) {
|
||||
f := fileFromReader(n, r)
|
||||
func (m *Msg) AttachReader(n string, r io.Reader, o ...FileOption) error {
|
||||
f, err := fileFromReader(n, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.attachments = m.appendFile(m.attachments, f, o...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg
|
||||
|
@ -851,9 +855,13 @@ func (m *Msg) EmbedFile(n string, o ...FileOption) {
|
|||
// 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 recommended to
|
||||
// either use EmbedFile or EmbedReadSeeker instead
|
||||
func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) {
|
||||
f := fileFromReader(n, r)
|
||||
func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) error {
|
||||
f, err := fileFromReader(n, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.embeds = m.appendFile(m.embeds, f, o...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg
|
||||
|
@ -1216,10 +1224,10 @@ func fileFromFS(n string) *File {
|
|||
}
|
||||
|
||||
// fileFromReader returns a File pointer from a given io.Reader
|
||||
func fileFromReader(n string, r io.Reader) *File {
|
||||
func fileFromReader(n string, r io.Reader) (*File, error) {
|
||||
d, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return &File{}
|
||||
return &File{}, err
|
||||
}
|
||||
br := bytes.NewReader(d)
|
||||
return &File{
|
||||
|
@ -1233,7 +1241,7 @@ func fileFromReader(n string, r io.Reader) *File {
|
|||
_, cerr = br.Seek(0, io.SeekStart)
|
||||
return rb, cerr
|
||||
},
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fileFromReadSeeker returns a File pointer from a given io.ReadSeeker
|
||||
|
@ -1261,8 +1269,7 @@ func fileFromHTMLTemplate(n string, t *ht.Template, d interface{}) (*File, error
|
|||
if err := t.Execute(&buf, d); err != nil {
|
||||
return nil, fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
f := fileFromReader(n, &buf)
|
||||
return f, nil
|
||||
return fileFromReader(n, &buf)
|
||||
}
|
||||
|
||||
// fileFromTextTemplate returns a File pointer form a given text/template.Template
|
||||
|
@ -1274,8 +1281,7 @@ func fileFromTextTemplate(n string, t *tt.Template, d interface{}) (*File, error
|
|||
if err := t.Execute(&buf, d); err != nil {
|
||||
return nil, fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
f := fileFromReader(n, &buf)
|
||||
return f, nil
|
||||
return fileFromReader(n, &buf)
|
||||
}
|
||||
|
||||
// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
|
||||
|
|
20
msg_test.go
20
msg_test.go
|
@ -1705,7 +1705,10 @@ func TestMsg_AttachReader(t *testing.T) {
|
|||
rbuf := bytes.Buffer{}
|
||||
rbuf.WriteString(ts)
|
||||
r := bufio.NewReader(&rbuf)
|
||||
m.AttachReader("testfile.txt", r)
|
||||
if err := m.AttachReader("testfile.txt", r); err != nil {
|
||||
t.Errorf("AttachReader() failed. Expected no error, got: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if len(m.attachments) != 1 {
|
||||
t.Errorf("AttachReader() failed. Number of attachments expected: %d, got: %d", 1,
|
||||
len(m.attachments))
|
||||
|
@ -1845,7 +1848,10 @@ func TestMsg_EmbedReader(t *testing.T) {
|
|||
rbuf := bytes.Buffer{}
|
||||
rbuf.WriteString(ts)
|
||||
r := bufio.NewReader(&rbuf)
|
||||
m.EmbedReader("testfile.txt", r)
|
||||
if err := m.EmbedReader("testfile.txt", r); err != nil {
|
||||
t.Errorf("EmbedReader() failed. Expected no error, got: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if len(m.embeds) != 1 {
|
||||
t.Errorf("EmbedReader() failed. Number of embeds expected: %d, got: %d", 1,
|
||||
len(m.embeds))
|
||||
|
@ -2847,8 +2853,14 @@ func TestMsg_AttachEmbedReader_consecutive(t *testing.T) {
|
|||
ts1 := "This is a test string"
|
||||
ts2 := "Another test string"
|
||||
m := NewMsg()
|
||||
m.AttachReader("attachment.txt", bytes.NewBufferString(ts1))
|
||||
m.EmbedReader("embedded.txt", bytes.NewBufferString(ts2))
|
||||
if err := m.AttachReader("attachment.txt", bytes.NewBufferString(ts1)); err != nil {
|
||||
t.Errorf("AttachReader() failed. Expected no error, got: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if err := m.EmbedReader("embedded.txt", bytes.NewBufferString(ts2)); err != nil {
|
||||
t.Errorf("EmbedReader() failed. Expected no error, got: %s", err.Error())
|
||||
return
|
||||
}
|
||||
obuf1 := &bytes.Buffer{}
|
||||
obuf2 := &bytes.Buffer{}
|
||||
_, err := m.WriteTo(obuf1)
|
||||
|
|
|
@ -27,7 +27,7 @@ type cramMD5Auth struct {
|
|||
username, secret string
|
||||
}
|
||||
|
||||
// CRAMMD5Auth returns an Auth that implements the CRAM-MD5 authentication
|
||||
// CRAMMD5Auth returns an [Auth] that implements the CRAM-MD5 authentication
|
||||
// mechanism as defined in RFC 2195.
|
||||
// The returned Auth uses the given username and secret to authenticate
|
||||
// to the server using the challenge-response mechanism.
|
||||
|
|
|
@ -27,7 +27,7 @@ type cramMD5Auth struct {
|
|||
username, secret string
|
||||
}
|
||||
|
||||
// CRAMMD5Auth returns an Auth that implements the CRAM-MD5 authentication
|
||||
// CRAMMD5Auth returns an [Auth] that implements the CRAM-MD5 authentication
|
||||
// mechanism as defined in RFC 2195.
|
||||
// The returned Auth uses the given username and secret to authenticate
|
||||
// to the server using the challenge-response mechanism.
|
||||
|
|
|
@ -16,14 +16,34 @@ type loginAuth struct {
|
|||
}
|
||||
|
||||
const (
|
||||
// ServerRespUsername represents the "Username:" response by the SMTP server
|
||||
ServerRespUsername = "Username:"
|
||||
// LoginXUsernameChallenge represents the Username Challenge response sent by the SMTP server per the AUTH LOGIN
|
||||
// extension.
|
||||
//
|
||||
// See: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-xlogin/.
|
||||
LoginXUsernameChallenge = "Username:"
|
||||
|
||||
// ServerRespPassword represents the "Password:" response by the SMTP server
|
||||
ServerRespPassword = "Password:"
|
||||
// LoginXPasswordChallenge represents the Password Challenge response sent by the SMTP server per the AUTH LOGIN
|
||||
// extension.
|
||||
//
|
||||
// See: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-xlogin/.
|
||||
LoginXPasswordChallenge = "Password:"
|
||||
|
||||
// LoginXDraftUsernameChallenge represents the Username Challenge response sent by the SMTP server per the IETF
|
||||
// draft AUTH LOGIN extension. It should be noted this extension is an expired draft which was never formally
|
||||
// published and was deprecated in favor of the AUTH PLAIN extension.
|
||||
//
|
||||
// See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00.
|
||||
LoginXDraftUsernameChallenge = "User Name\x00"
|
||||
|
||||
// LoginXDraftPasswordChallenge represents the Password Challenge response sent by the SMTP server per the IETF
|
||||
// draft AUTH LOGIN extension. It should be noted this extension is an expired draft which was never formally
|
||||
// published and was deprecated in favor of the AUTH PLAIN extension.
|
||||
//
|
||||
// See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00.
|
||||
LoginXDraftPasswordChallenge = "Password\x00"
|
||||
)
|
||||
|
||||
// LoginAuth returns an Auth that implements the LOGIN authentication
|
||||
// LoginAuth returns an [Auth] that implements the LOGIN authentication
|
||||
// mechanism as it is used by MS Outlook. The Auth works similar to PLAIN
|
||||
// but instead of sending all in one response, the login is handled within
|
||||
// 3 steps:
|
||||
|
@ -56,9 +76,9 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
|
|||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
switch string(fromServer) {
|
||||
case ServerRespUsername:
|
||||
case LoginXUsernameChallenge, LoginXDraftUsernameChallenge:
|
||||
return []byte(a.username), nil
|
||||
case ServerRespPassword:
|
||||
case LoginXPasswordChallenge, LoginXDraftPasswordChallenge:
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected server response: %s", string(fromServer))
|
||||
|
|
|
@ -23,7 +23,7 @@ type plainAuth struct {
|
|||
host string
|
||||
}
|
||||
|
||||
// PlainAuth returns an Auth that implements the PLAIN authentication
|
||||
// PlainAuth returns an [Auth] that implements the PLAIN authentication
|
||||
// mechanism as defined in RFC 4616. The returned Auth uses the given
|
||||
// username and password to authenticate to host and act as identity.
|
||||
// Usually identity should be the empty string, to act as username.
|
||||
|
|
|
@ -8,6 +8,11 @@ type xoauth2Auth struct {
|
|||
username, token string
|
||||
}
|
||||
|
||||
// XOAuth2Auth returns an [Auth] that implements the XOAuth2 authentication
|
||||
// mechanism as defined in the following specs:
|
||||
//
|
||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
// https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
|
||||
func XOAuth2Auth(username, token string) Auth {
|
||||
return &xoauth2Auth{username, token}
|
||||
}
|
||||
|
|
14
smtp/smtp.go
14
smtp/smtp.go
|
@ -60,7 +60,7 @@ type Client struct {
|
|||
dsnrntype string // dsnrntype defines the recipient notify option in case DSN is enabled
|
||||
}
|
||||
|
||||
// Dial returns a new Client connected to an SMTP server at addr.
|
||||
// Dial returns a new [Client] connected to an SMTP server at addr.
|
||||
// The addr must include a port, as in "mail.example.com:smtp".
|
||||
func Dial(addr string) (*Client, error) {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
|
@ -71,7 +71,7 @@ func Dial(addr string) (*Client, error) {
|
|||
return NewClient(conn, host)
|
||||
}
|
||||
|
||||
// NewClient returns a new Client using an existing connection and host as a
|
||||
// NewClient returns a new [Client] using an existing connection and host as a
|
||||
// server name to be used when authenticating.
|
||||
func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||
text := textproto.NewConn(conn)
|
||||
|
@ -164,7 +164,7 @@ func (c *Client) StartTLS(config *tls.Config) error {
|
|||
}
|
||||
|
||||
// TLSConnectionState returns the client's TLS connection state.
|
||||
// The return values are their zero values if StartTLS did
|
||||
// The return values are their zero values if [Client.StartTLS] did
|
||||
// not succeed.
|
||||
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
|
||||
tc, ok := c.conn.(*tls.Conn)
|
||||
|
@ -247,7 +247,7 @@ func (c *Client) Auth(a Auth) error {
|
|||
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
|
||||
// parameter. If the server supports the SMTPUTF8 extension, Mail adds the
|
||||
// SMTPUTF8 parameter.
|
||||
// This initiates a mail transaction and is followed by one or more Rcpt calls.
|
||||
// This initiates a mail transaction and is followed by one or more [Client.Rcpt] calls.
|
||||
func (c *Client) Mail(from string) error {
|
||||
if err := validateLine(from); err != nil {
|
||||
return err
|
||||
|
@ -273,8 +273,8 @@ func (c *Client) Mail(from string) error {
|
|||
}
|
||||
|
||||
// Rcpt issues a RCPT command to the server using the provided email address.
|
||||
// A call to Rcpt must be preceded by a call to Mail and may be followed by
|
||||
// a Data call or another Rcpt call.
|
||||
// A call to Rcpt must be preceded by a call to [Client.Mail] and may be followed by
|
||||
// a [Client.Data] call or another Rcpt call.
|
||||
func (c *Client) Rcpt(to string) error {
|
||||
if err := validateLine(to); err != nil {
|
||||
return err
|
||||
|
@ -302,7 +302,7 @@ func (d *dataCloser) Close() error {
|
|||
// Data issues a DATA command to the server and returns a writer that
|
||||
// can be used to write the mail headers and body. The caller should
|
||||
// close the writer before calling any more methods on c. A call to
|
||||
// Data must be preceded by one or more calls to Rcpt.
|
||||
// Data must be preceded by one or more calls to [Client.Rcpt].
|
||||
func (c *Client) Data() (io.WriteCloser, error) {
|
||||
_, _, err := c.cmd(354, "DATA")
|
||||
if err != nil {
|
||||
|
|
|
@ -57,10 +57,10 @@ var authTests = []authTest{
|
|||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
[]string{"Username:", "Password:", "Invalid:"},
|
||||
[]string{"Username:", "Password:", "User Name\x00", "Password\x00", "Invalid:"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass", ""},
|
||||
[]bool{false, false, true},
|
||||
[]string{"", "user", "pass", "user", "pass", ""},
|
||||
[]bool{false, false, false, false, true},
|
||||
},
|
||||
{
|
||||
CRAMMD5Auth("user", "pass"),
|
||||
|
|
Loading…
Reference in a new issue