Merge branch 'main' into feature/145_add-eml-parser-to-generate-msg-from-eml-files

This commit is contained in:
Winni Neessen 2024-01-10 11:07:54 +01:00
commit 8b19e497a0
Signed by: wneessen
GPG key ID: 385AC9889632126E
9 changed files with 78 additions and 35 deletions

28
msg.go
View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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.

View file

@ -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))

View file

@ -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.

View file

@ -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}
}

View file

@ -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 {

View file

@ -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"),