Skip to content

Commit

Permalink
Merge pull request #378 from wneessen/feature/quicksend
Browse files Browse the repository at this point in the history
Add QuickSend feature
  • Loading branch information
wneessen authored Nov 19, 2024
2 parents 25a0fb2 + f05654d commit 9410381
Show file tree
Hide file tree
Showing 4 changed files with 510 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .golangci.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,10 @@ text = "G505:"
linters = ["gosec"]
path = "smtp/smtp_test.go"
text = "G505:"

## These are tests which intentionally do not need any TLS settings
[[issues.exclude-rules]]
linters = ["gosec"]
path = "quicksend_test.go"
text = "G402:"

23 changes: 23 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3665,6 +3665,8 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "

// serverProps represents the configuration properties for the SMTP server.
type serverProps struct {
BufferMutex sync.RWMutex
EchoBuffer io.Writer
FailOnAuth bool
FailOnDataInit bool
FailOnDataClose bool
Expand Down Expand Up @@ -3754,6 +3756,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
if err != nil {
t.Logf("failed to write line: %s", err)
}
if props.EchoBuffer != nil {
props.BufferMutex.Lock()
if _, berr := props.EchoBuffer.Write([]byte(data + "\r\n")); berr != nil {
t.Errorf("failed write to echo buffer: %s", berr)
}
props.BufferMutex.Unlock()
}
_ = writer.Flush()
}
writeOK := func() {
Expand All @@ -3770,6 +3779,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
break
}
time.Sleep(time.Millisecond)
if props.EchoBuffer != nil {
props.BufferMutex.Lock()
if _, berr := props.EchoBuffer.Write([]byte(data)); berr != nil {
t.Errorf("failed write to echo buffer: %s", berr)
}
props.BufferMutex.Unlock()
}

var datastring string
data = strings.TrimSpace(data)
Expand Down Expand Up @@ -3830,6 +3846,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
t.Logf("failed to read data from connection: %s", derr)
break
}
if props.EchoBuffer != nil {
props.BufferMutex.Lock()
if _, berr := props.EchoBuffer.Write([]byte(ddata)); berr != nil {
t.Errorf("failed write to echo buffer: %s", berr)
}
props.BufferMutex.Unlock()
}
ddata = strings.TrimSpace(ddata)
if ddata == "." {
if props.FailOnDataClose {
Expand Down
112 changes: 112 additions & 0 deletions quicksend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT

package mail

import (
"bytes"
"crypto/tls"
"fmt"
"net"
"strconv"
)

type AuthData struct {
Auth bool
Username string
Password string
}

var testHookTLSConfig func() *tls.Config // nil, except for tests

// QuickSend is an all-in-one method for quickly sending simple text mails in go-mail.
//
// This method will create a new client that connects to the server at addr, switches to TLS if possible,
// authenticates with the optional AuthData provided in auth and create a new simple Msg with the provided
// subject string and message bytes as body. The message will be sent using from as sender address and will
// be delivered to every address in rcpts. QuickSend will always send as text/plain ContentType.
//
// For the SMTP authentication, if auth is not nil and AuthData.Auth is set to true, it will try to
// autodiscover the best SMTP authentication mechanism supported by the server. If auth is set to true
// but autodiscover is not able to find a suitable authentication mechanism or if the authentication
// fails, the mail delivery will fail completely.
//
// The content parameter should be an RFC 822-style email body. The lines of content should be CRLF terminated.
//
// Parameters:
// - addr: The hostname and port of the mail server, it must include a port, as in "mail.example.com:smtp".
// - auth: A AuthData pointer. If nil or if AuthData.Auth is set to false, not SMTP authentication will be performed.
// - from: The from address of the sender as string.
// - rcpts: A slice of strings of receipient addresses.
// - subject: The subject line as string.
// - content: A byte slice of the mail content
// - opts: Optional parameters for customizing the body part.
//
// Returns:
// - A pointer to the generated Msg.
// - An error if any step in the process of mail generation or delivery failed.
func QuickSend(addr string, auth *AuthData, from string, rcpts []string, subject string, content []byte) (*Msg, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("failed to split host and port from address: %w", err)
}
portnum, err := strconv.Atoi(port)
if err != nil {
return nil, fmt.Errorf("failed to convert port to int: %w", err)
}
client, err := NewClient(host, WithPort(portnum), WithTLSPolicy(TLSOpportunistic))
if err != nil {
return nil, fmt.Errorf("failed to create new client: %w", err)
}

if auth != nil && auth.Auth {
client.SetSMTPAuth(SMTPAuthAutoDiscover)
client.SetUsername(auth.Username)
client.SetPassword(auth.Password)
}

tlsConfig := client.tlsconfig
if testHookTLSConfig != nil {
tlsConfig = testHookTLSConfig()
}
if err = client.SetTLSConfig(tlsConfig); err != nil {
return nil, fmt.Errorf("failed to set TLS config: %w", err)
}

message := NewMsg()
if err = message.From(from); err != nil {
return nil, fmt.Errorf("failed to set MAIL FROM address: %w", err)
}
if err = message.To(rcpts...); err != nil {
return nil, fmt.Errorf("failed to set RCPT TO address: %w", err)
}
message.Subject(subject)
buffer := bytes.NewBuffer(content)
writeFunc := writeFuncFromBuffer(buffer)
message.SetBodyWriter(TypeTextPlain, writeFunc)

if err = client.DialAndSend(message); err != nil {
return nil, fmt.Errorf("failed to dial and send message: %w", err)
}
return message, nil
}

// NewAuthData creates a new AuthData instance with the provided username and password.
//
// This function initializes an AuthData struct with authentication enabled and sets the
// username and password fields.
//
// Parameters:
// - user: The username for authentication.
// - pass: The password for authentication.
//
// Returns:
// - A pointer to the initialized AuthData instance.
func NewAuthData(user, pass string) *AuthData {
return &AuthData{
Auth: true,
Username: user,
Password: pass,
}
}
Loading

0 comments on commit 9410381

Please sign in to comment.