mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 13:50:49 +01:00
Compare commits
62 commits
b9888929f8
...
a94e721161
Author | SHA1 | Date | |
---|---|---|---|
a94e721161 | |||
46ca42e1b7 | |||
f0388ec600 | |||
6ce5c2a860 | |||
5d79ff69c3 | |||
cd90c3ddf3 | |||
e640f2df46 | |||
295155ba67 | |||
0b105048e6 | |||
3333c784a6 | |||
ac7fa5771a | |||
dab9cc947a | |||
2c1082fe42 | |||
3e8706d52e | |||
756269644e | |||
d6426063ba | |||
b4197a136e | |||
01278ccb30 | |||
eafb9cb17e | |||
864c593208 | |||
4890d9130b | |||
b37f8995da | |||
c520925457 | |||
3d5435c138 | |||
1dcdad9da1 | |||
78e2857782 | |||
c186cba2c2 | |||
682f7a6ca5 | |||
cd4c0194dc | |||
a820ba3cee | |||
96466facdd | |||
493f8fc657 | |||
94f47d4369 | |||
476130d6e3 | |||
a0a7f74121 | |||
ecd0bff5ad | |||
5653df373b | |||
869e8db6c5 | |||
fa3c6f956e | |||
159c1bf850 | |||
9163943684 | |||
fbbf17acd0 | |||
48b469faf7 | |||
adcb8ac41d | |||
dfdadc5da2 | |||
8942b08424 | |||
972a3c51c7 | |||
d900f5403e | |||
eeaee3f60a | |||
bae0ac6cde | |||
3e5c93a418 | |||
a34f400a05 | |||
779a3f3942 | |||
6cd3cfd2f7 | |||
aab04672f8 | |||
f7c12d412b | |||
ef3da39840 | |||
92c411454b | |||
59e91eb936 | |||
6a9c8bb56b | |||
ea90352ef4 | |||
e8739b88b0 |
28 changed files with 3959 additions and 852 deletions
|
@ -52,7 +52,7 @@ Here are some highlights of go-mail's featureset:
|
|||
* [X] Support sending mails via a local sendmail command
|
||||
* [X] Support for requestng MDNs (RFC 8098) and DSNs (RFC 1891)
|
||||
* [X] DKIM signature support via [go-mail-middlware](https://github.com/wneessen/go-mail-middleware)
|
||||
* [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces
|
||||
* [X] Message object satisfies `io.WriterTo` and `io.Reader` interfaces
|
||||
* [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed)
|
||||
* [X] Output to file support which allows storing mail messages as e. g. `.eml` files to disk to open them in a MUA
|
||||
* [X] Debug logging of SMTP traffic
|
||||
|
|
98
auth.go
98
auth.go
|
@ -6,69 +6,131 @@ package mail
|
|||
|
||||
import "errors"
|
||||
|
||||
// SMTPAuthType represents a string to any SMTP AUTH type
|
||||
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
|
||||
// mechanism to be used.
|
||||
type SMTPAuthType string
|
||||
|
||||
// Supported SMTP AUTH types
|
||||
const (
|
||||
// SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954
|
||||
// SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954.
|
||||
// https://datatracker.ietf.org/doc/html/rfc4954/
|
||||
//
|
||||
// CRAM-MD5 is not secure by modern standards. The vulnerabilities of MD5 and the lack of
|
||||
// advanced security features make it inappropriate for protecting sensitive communications
|
||||
// today.
|
||||
//
|
||||
// It was recommended to deprecate the standard in 20 November 2008. As an alternative it
|
||||
// recommends e.g. SCRAM or SASL Plain protected by TLS instead.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-sasl-crammd5-to-historic-00.html
|
||||
SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"
|
||||
|
||||
// SMTPAuthLogin is the "LOGIN" SASL authentication mechanism
|
||||
// SMTPAuthCustom is a custom SMTP AUTH mechanism provided by the user. If a user provides
|
||||
// a custom smtp.Auth function to the Client, the Client will its smtpAuthType to this type.
|
||||
//
|
||||
// Do not use this SMTPAuthType without setting a custom smtp.Auth function on the Client.
|
||||
SMTPAuthCustom SMTPAuthType = "CUSTOM"
|
||||
|
||||
// SMTPAuthLogin is the "LOGIN" SASL authentication mechanism. This authentication mechanism
|
||||
// does not have an official RFC that could be followed. There is a spec by Microsoft and an
|
||||
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
|
||||
// automatically matches the MS spec.
|
||||
//
|
||||
// Since the "LOGIN" SASL authentication mechansim transmits the username and password in
|
||||
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||
// connection.
|
||||
//
|
||||
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
||||
|
||||
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
||||
// option and should not be used. Instead, for mail servers that do no support/require
|
||||
// authentication, the Client should not be used with the WithSMTPAuth option
|
||||
// authentication, the Client should not be passed the WithSMTPAuth option at all.
|
||||
SMTPAuthNoAuth SMTPAuthType = ""
|
||||
|
||||
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616
|
||||
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||
//
|
||||
// Since the "PLAIN" SASL authentication mechansim transmits the username and password in
|
||||
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||
// connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
||||
|
||||
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
||||
|
||||
// SMTPAuthSCRAMSHA1 represents the SCRAM-SHA-1 SMTP authentication mechanism
|
||||
// SMTPAuthSCRAMSHA1 is the "SCRAM-SHA-1" SASL authentication mechanism as described in RFC 5802.
|
||||
//
|
||||
// SCRAM-SHA-1 is still considered secure for certain applications, particularly when used as part
|
||||
// of a challenge-response authentication mechanism (as we use it). However, it is generally
|
||||
// recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known
|
||||
// vulnerabilities in other contexts, although it remains effective in HMAC constructions.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc5802
|
||||
SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1"
|
||||
|
||||
// SMTPAuthSCRAMSHA1PLUS represents the "SCRAM-SHA-1-PLUS" authentication mechanism for SMTP.
|
||||
// SMTPAuthSCRAMSHA1PLUS is the "SCRAM-SHA-1-PLUS" SASL authentication mechanism as described in RFC 5802.
|
||||
//
|
||||
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and
|
||||
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
|
||||
// process. Therefore we only allow this mechansim over a TLS secured connection.
|
||||
//
|
||||
// SCRAM-SHA-1-PLUS is still considered secure for certain applications, particularly when used as part
|
||||
// of a challenge-response authentication mechanism (as we use it). However, it is generally
|
||||
// recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known
|
||||
// vulnerabilities in other contexts, although it remains effective in HMAC constructions.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc5802
|
||||
SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS"
|
||||
|
||||
// SMTPAuthSCRAMSHA256 represents the SCRAM-SHA-256 authentication mechanism for SMTP.
|
||||
// SMTPAuthSCRAMSHA256 is the "SCRAM-SHA-256" SASL authentication mechanism as described in RFC 7677.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||
SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256"
|
||||
|
||||
// SMTPAuthSCRAMSHA256PLUS represents the "SCRAM-SHA-256-PLUS" SMTP AUTH type.
|
||||
// SMTPAuthSCRAMSHA256PLUS is the "SCRAM-SHA-256-PLUS" SASL authentication mechanism as described in RFC 7677.
|
||||
//
|
||||
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and
|
||||
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
|
||||
// process. Therefore we only allow this mechansim over a TLS secured connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
|
||||
)
|
||||
|
||||
// SMTP Auth related static errors
|
||||
var (
|
||||
// ErrPlainAuthNotSupported should be used if the target server does not support the "PLAIN" schema
|
||||
// ErrPlainAuthNotSupported is returned when the server does not support the "PLAIN" SMTP
|
||||
// authentication type.
|
||||
ErrPlainAuthNotSupported = errors.New("server does not support SMTP AUTH type: PLAIN")
|
||||
|
||||
// ErrLoginAuthNotSupported should be used if the target server does not support the "LOGIN" schema
|
||||
// ErrLoginAuthNotSupported is returned when the server does not support the "LOGIN" SMTP
|
||||
// authentication type.
|
||||
ErrLoginAuthNotSupported = errors.New("server does not support SMTP AUTH type: LOGIN")
|
||||
|
||||
// ErrCramMD5AuthNotSupported should be used if the target server does not support the "CRAM-MD5" schema
|
||||
// ErrCramMD5AuthNotSupported is returned when the server does not support the "CRAM-MD5" SMTP
|
||||
// authentication type.
|
||||
ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5")
|
||||
|
||||
// ErrXOauth2AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema
|
||||
// ErrXOauth2AuthNotSupported is returned when the server does not support the "XOAUTH2" schema.
|
||||
ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2")
|
||||
|
||||
// ErrSCRAMSHA1AuthNotSupported should be used if the target server does not support the "SCRAM-SHA-1" schema
|
||||
// ErrSCRAMSHA1AuthNotSupported is returned when the server does not support the "SCRAM-SHA-1" SMTP
|
||||
// authentication type.
|
||||
ErrSCRAMSHA1AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1")
|
||||
|
||||
// ErrSCRAMSHA1PLUSAuthNotSupported should be used if the target server does not support the "SCRAM-SHA-1-PLUS" schema
|
||||
// ErrSCRAMSHA1PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-1-PLUS" SMTP
|
||||
// authentication type.
|
||||
ErrSCRAMSHA1PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1-PLUS")
|
||||
|
||||
// ErrSCRAMSHA256AuthNotSupported should be used if the target server does not support the "SCRAM-SHA-256" schema
|
||||
// ErrSCRAMSHA256AuthNotSupported is returned when the server does not support the "SCRAM-SHA-256" SMTP
|
||||
// authentication type.
|
||||
ErrSCRAMSHA256AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256")
|
||||
|
||||
// ErrSCRAMSHA256PLUSAuthNotSupported should be used if the target server does not support the "SCRAM-SHA-256-PLUS" schema
|
||||
// ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP
|
||||
// authentication type.
|
||||
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
||||
)
|
||||
|
|
|
@ -9,21 +9,39 @@ import (
|
|||
"io"
|
||||
)
|
||||
|
||||
// ErrNoOutWriter is an error message that should be used if a Base64LineBreaker has no out io.Writer set
|
||||
// newlineBytes is a byte slice representation of the SingleNewLine constant used for line breaking
|
||||
// in encoding processes.
|
||||
var newlineBytes = []byte(SingleNewLine)
|
||||
|
||||
// ErrNoOutWriter is the error message returned when no io.Writer is set for Base64LineBreaker.
|
||||
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
|
||||
|
||||
// Base64LineBreaker is a io.WriteCloser that writes Base64 encoded data streams
|
||||
// with line breaks at a given line length
|
||||
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
|
||||
// of characters.
|
||||
//
|
||||
// This struct is used to manage base64 encoding while ensuring that new lines are inserted after
|
||||
// reaching a specific line length. It satisfies the io.WriteCloser interface.
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2045 (Base64 and line length limitations)
|
||||
type Base64LineBreaker struct {
|
||||
line [MaxBodyLength]byte
|
||||
used int
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
var newlineBytes = []byte(SingleNewLine)
|
||||
|
||||
// Write writes the data stream and inserts a SingleNewLine when the maximum
|
||||
// line length is reached
|
||||
// Write writes data to the Base64LineBreaker, ensuring lines do not exceed MaxBodyLength.
|
||||
//
|
||||
// This method writes the provided data to the Base64LineBreaker. It ensures that the written
|
||||
// lines do not exceed the MaxBodyLength. If the data exceeds the limit, it handles the
|
||||
// continuation by splitting the data and writing new lines as necessary.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A byte slice containing the data to be written.
|
||||
//
|
||||
// Returns:
|
||||
// - numBytes: The number of bytes written.
|
||||
// - err: An error if one occurred during the write operation.
|
||||
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
||||
if l.out == nil {
|
||||
err = errors.New(ErrNoOutWriter)
|
||||
|
@ -55,8 +73,14 @@ func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
|||
return l.Write(data[excess:])
|
||||
}
|
||||
|
||||
// Close closes the Base64LineBreaker and writes any access data that is still
|
||||
// unwritten in memory
|
||||
// Close finalizes the Base64LineBreaker, writing any remaining buffered data and appending a newline.
|
||||
//
|
||||
// This method ensures that any remaining data in the buffer is written to the output and appends
|
||||
// a newline. It is used to finalize the Base64LineBreaker and should be called when no more data
|
||||
// is expected to be written.
|
||||
//
|
||||
// Returns:
|
||||
// - err: An error if one occurred during the final write operation.
|
||||
func (l *Base64LineBreaker) Close() (err error) {
|
||||
if l.used > 0 {
|
||||
_, err = l.out.Write(l.line[0:l.used])
|
||||
|
|
|
@ -9,7 +9,23 @@ package mail
|
|||
|
||||
import "errors"
|
||||
|
||||
// Send sends out the mail message
|
||||
// Send attempts to send one or more Msg using the Client connection to the SMTP server.
|
||||
// If the Client has no active connection to the server, Send will fail with an error. For each
|
||||
// of the provided Msg, it will associate a SendError with the Msg in case of a transmission
|
||||
// or delivery error.
|
||||
//
|
||||
// This method first checks for an active connection to the SMTP server. If the connection is
|
||||
// not valid, it returns a SendError. It then iterates over the provided messages, attempting
|
||||
// to send each one. If an error occurs during sending, the method records the error and
|
||||
// associates it with the corresponding Msg. If multiple errors are encountered, it aggregates
|
||||
// them into a single SendError to be returned.
|
||||
//
|
||||
// Parameters:
|
||||
// - messages: A variadic list of pointers to Msg objects to be sent.
|
||||
//
|
||||
// Returns:
|
||||
// - An error that represents the sending result, which may include multiple SendErrors if
|
||||
// any occurred; otherwise, returns nil.
|
||||
func (c *Client) Send(messages ...*Msg) error {
|
||||
if err := c.checkConn(); err != nil {
|
||||
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
|
|
|
@ -11,7 +11,21 @@ import (
|
|||
"errors"
|
||||
)
|
||||
|
||||
// Send sends out the mail message
|
||||
// Send attempts to send one or more Msg using the Client connection to the SMTP server.
|
||||
// If the Client has no active connection to the server, Send will fail with an error. For each
|
||||
// of the provided Msg, it will associate a SendError with the Msg in case of a transmission
|
||||
// or delivery error.
|
||||
//
|
||||
// This method first checks for an active connection to the SMTP server. If the connection is
|
||||
// not valid, it returns an error wrapped in a SendError. It then iterates over the provided
|
||||
// messages, attempting to send each one. If an error occurs during sending, the method records
|
||||
// the error and associates it with the corresponding Msg.
|
||||
//
|
||||
// Parameters:
|
||||
// - messages: A variadic list of pointers to Msg objects to be sent.
|
||||
//
|
||||
// Returns:
|
||||
// - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil.
|
||||
func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
||||
if err := c.checkConn(); err != nil {
|
||||
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
|
|
|
@ -483,20 +483,20 @@ func TestWithDSN(t *testing.T) {
|
|||
t.Errorf("failed to create new client: %s", err)
|
||||
return
|
||||
}
|
||||
if !c.dsn {
|
||||
t.Errorf("WithDSN failed. c.dsn expected to be: %t, got: %t", true, c.dsn)
|
||||
if !c.requestDSN {
|
||||
t.Errorf("WithDSN failed. c.requestDSN expected to be: %t, got: %t", true, c.requestDSN)
|
||||
}
|
||||
if c.dsnmrtype != DSNMailReturnFull {
|
||||
t.Errorf("WithDSN failed. c.dsnmrtype expected to be: %s, got: %s", DSNMailReturnFull,
|
||||
c.dsnmrtype)
|
||||
if c.dsnReturnType != DSNMailReturnFull {
|
||||
t.Errorf("WithDSN failed. c.dsnReturnType expected to be: %s, got: %s", DSNMailReturnFull,
|
||||
c.dsnReturnType)
|
||||
}
|
||||
if c.dsnrntype[0] != string(DSNRcptNotifyFailure) {
|
||||
t.Errorf("WithDSN failed. c.dsnrntype[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
|
||||
c.dsnrntype[0])
|
||||
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) {
|
||||
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
|
||||
c.dsnRcptNotifyType[0])
|
||||
}
|
||||
if c.dsnrntype[1] != string(DSNRcptNotifySuccess) {
|
||||
t.Errorf("WithDSN failed. c.dsnrntype[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
|
||||
c.dsnrntype[1])
|
||||
if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) {
|
||||
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
|
||||
c.dsnRcptNotifyType[1])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -519,8 +519,8 @@ func TestWithDSNMailReturnType(t *testing.T) {
|
|||
t.Errorf("failed to create new client: %s", err)
|
||||
return
|
||||
}
|
||||
if string(c.dsnmrtype) != tt.want {
|
||||
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnmrtype))
|
||||
if string(c.dsnReturnType) != tt.want {
|
||||
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnReturnType))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -547,11 +547,11 @@ func TestWithDSNRcptNotifyType(t *testing.T) {
|
|||
t.Errorf("failed to create new client: %s", err)
|
||||
return
|
||||
}
|
||||
if len(c.dsnrntype) <= 0 && !tt.sf {
|
||||
if len(c.dsnRcptNotifyType) <= 0 && !tt.sf {
|
||||
t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none")
|
||||
}
|
||||
if !tt.sf && c.dsnrntype[0] != tt.want {
|
||||
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnrntype[0])
|
||||
if !tt.sf && c.dsnRcptNotifyType[0] != tt.want {
|
||||
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnRcptNotifyType[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -602,6 +602,10 @@ func TestSetSMTPAuthCustom(t *testing.T) {
|
|||
if c.smtpAuth == nil {
|
||||
t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty")
|
||||
}
|
||||
if c.smtpAuthType != SMTPAuthCustom {
|
||||
t.Errorf("failed to set custom SMTP auth method. SMTP Auth type is not custom: %s",
|
||||
c.smtpAuthType)
|
||||
}
|
||||
p, _, err := c.smtpAuth.Start(&si)
|
||||
if err != nil {
|
||||
t.Errorf("SMTP Auth Start() method returned error: %s", err)
|
||||
|
@ -613,6 +617,32 @@ func TestSetSMTPAuthCustom(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestClient_Close_double tests if a close on an already closed connection causes an error.
|
||||
func TestClient_Close_double(t *testing.T) {
|
||||
c, err := getTestConnection(true)
|
||||
if err != nil {
|
||||
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err = c.DialWithContext(ctx); err != nil {
|
||||
t.Errorf("failed to dial with context: %s", err)
|
||||
return
|
||||
}
|
||||
if c.smtpClient == nil {
|
||||
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
||||
return
|
||||
}
|
||||
if !c.smtpClient.HasConnection() {
|
||||
t.Errorf("DialWithContext didn't fail but no connection found.")
|
||||
}
|
||||
if err = c.Close(); err != nil {
|
||||
t.Errorf("failed to close connection: %s", err)
|
||||
}
|
||||
if err = c.Close(); err != nil {
|
||||
t.Errorf("failed 2nd close connection: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_DialWithContext tests the DialWithContext method for the Client object
|
||||
func TestClient_DialWithContext(t *testing.T) {
|
||||
c, err := getTestConnection(true)
|
||||
|
@ -2387,7 +2417,6 @@ func TestXOAuth2OK_faker(t *testing.T) {
|
|||
"250 8BITMIME",
|
||||
"250 OK",
|
||||
"235 2.7.0 Accepted",
|
||||
"250 OK",
|
||||
"221 OK",
|
||||
}
|
||||
var wrote strings.Builder
|
||||
|
@ -2408,10 +2437,10 @@ func TestXOAuth2OK_faker(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("unable to create new client: %v", err)
|
||||
}
|
||||
if err := c.DialWithContext(context.Background()); err != nil {
|
||||
if err = c.DialWithContext(context.Background()); err != nil {
|
||||
t.Fatalf("unexpected dial error: %v", err)
|
||||
}
|
||||
if err := c.Close(); err != nil {
|
||||
if err = c.Close(); err != nil {
|
||||
t.Fatalf("disconnect from test server failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
|
||||
|
@ -2426,7 +2455,6 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
|
|||
"250-AUTH LOGIN PLAIN",
|
||||
"250 8BITMIME",
|
||||
"250 OK",
|
||||
"250 OK",
|
||||
"221 OK",
|
||||
}
|
||||
var wrote strings.Builder
|
||||
|
@ -2445,18 +2473,18 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("unable to create new client: %v", err)
|
||||
}
|
||||
if err := c.DialWithContext(context.Background()); err == nil {
|
||||
if err = c.DialWithContext(context.Background()); err == nil {
|
||||
t.Fatal("expected dial error got nil")
|
||||
} else {
|
||||
if !errors.Is(err, ErrXOauth2AuthNotSupported) {
|
||||
t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err)
|
||||
}
|
||||
}
|
||||
if err := c.Close(); err != nil {
|
||||
if err = c.Close(); err != nil {
|
||||
t.Fatalf("disconnect from test server failed: %v", err)
|
||||
}
|
||||
client := strings.Split(wrote.String(), "\r\n")
|
||||
if len(client) != 5 {
|
||||
if len(client) != 4 {
|
||||
t.Fatalf("unexpected number of client requests got %d; want 5", len(client))
|
||||
}
|
||||
if !strings.HasPrefix(client[0], "EHLO") {
|
||||
|
@ -2465,10 +2493,7 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
|
|||
if client[1] != "NOOP" {
|
||||
t.Fatalf("expected NOOP, got %q", client[1])
|
||||
}
|
||||
if client[2] != "NOOP" {
|
||||
t.Fatalf("expected NOOP, got %q", client[2])
|
||||
}
|
||||
if client[3] != "QUIT" {
|
||||
if client[2] != "QUIT" {
|
||||
t.Fatalf("expected QUIT, got %q", client[3])
|
||||
}
|
||||
}
|
||||
|
|
11
doc.go
11
doc.go
|
@ -2,8 +2,13 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package mail provides a simple and easy way to send mails with Go
|
||||
// Package mail provides an easy to use interface for formating and sending mails. go-mail follows idiomatic Go style
|
||||
// and best practice. It has a small dependency footprint by mainly relying on the Go Standard Library and the Go
|
||||
// extended packages. It combines a lot of functionality from the standard library to give easy and convenient access
|
||||
// to mail and SMTP related tasks. It works like a programatic email client and provides lots of methods and
|
||||
// functionalities you would consider standard in a MUA.
|
||||
package mail
|
||||
|
||||
// VERSION is used in the default user agent string
|
||||
const VERSION = "0.4.4"
|
||||
// VERSION indicates the current version of the package. It is also attached to the default user
|
||||
// agent string.
|
||||
const VERSION = "0.5.0"
|
||||
|
|
205
eml.go
205
eml.go
|
@ -18,14 +18,35 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer
|
||||
// EMLToMsgFromString parses a given EML string and returns a pre-filled Msg pointer.
|
||||
//
|
||||
// This function takes an EML formatted string, converts it into a bytes buffer, and then
|
||||
// calls EMLToMsgFromReader to parse the buffer and create a Msg object. This provides a
|
||||
// convenient way to convert EML strings directly into Msg objects.
|
||||
//
|
||||
// Parameters:
|
||||
// - emlString: A string containing the EML formatted message.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
||||
// fails.
|
||||
func EMLToMsgFromString(emlString string) (*Msg, error) {
|
||||
eb := bytes.NewBufferString(emlString)
|
||||
return EMLToMsgFromReader(eb)
|
||||
}
|
||||
|
||||
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled
|
||||
// Msg pointer
|
||||
// EMLToMsgFromReader parses a reader that holds EML content and returns a pre-filled Msg pointer.
|
||||
//
|
||||
// This function reads EML content from the provided io.Reader and populates a Msg object
|
||||
// with the parsed data. It initializes the Msg and extracts headers and body parts from
|
||||
// the EML content. Any errors encountered during parsing are returned.
|
||||
//
|
||||
// Parameters:
|
||||
// - reader: An io.Reader containing the EML formatted message.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
||||
// fails.
|
||||
func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
||||
msg := &Msg{
|
||||
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
||||
|
@ -46,8 +67,19 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
|||
return msg, nil
|
||||
}
|
||||
|
||||
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a
|
||||
// pre-filled Msg pointer
|
||||
// EMLToMsgFromFile opens and parses a .eml file at a provided file path and returns a
|
||||
// pre-filled Msg pointer.
|
||||
//
|
||||
// This function attempts to read and parse an EML file located at the specified file path.
|
||||
// It initializes a Msg object and populates it with the parsed headers and body. Any errors
|
||||
// encountered during the file operations or parsing are returned.
|
||||
//
|
||||
// Parameters:
|
||||
// - filePath: The path to the .eml file to be parsed.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
||||
// fails.
|
||||
func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
||||
msg := &Msg{
|
||||
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
||||
|
@ -68,7 +100,19 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
|||
return msg, nil
|
||||
}
|
||||
|
||||
// parseEML parses the EML's headers and body and inserts the parsed values into the Msg
|
||||
// parseEML parses the EML's headers and body and inserts the parsed values into the Msg.
|
||||
//
|
||||
// This function extracts relevant header fields and body content from the parsed EML message
|
||||
// and stores them in the provided Msg object. It handles various header types and body
|
||||
// parts, ensuring that the Msg is correctly populated with all necessary information.
|
||||
//
|
||||
// Parameters:
|
||||
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
||||
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
||||
// - msg: A pointer to the Msg object to be populated with the parsed data.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if any issues occur during the parsing process; otherwise, returns nil.
|
||||
func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||
if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
|
||||
return fmt.Errorf("failed to parse EML headers: %w", err)
|
||||
|
@ -79,7 +123,18 @@ func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// readEML opens an EML file and uses net/mail to parse the header and body
|
||||
// readEML opens an EML file and uses net/mail to parse the header and body.
|
||||
//
|
||||
// This function opens the specified EML file for reading and utilizes the net/mail package
|
||||
// to parse the message's headers and body. It returns the parsed message and a buffer
|
||||
// containing the body content, along with any errors encountered during the process.
|
||||
//
|
||||
// Parameters:
|
||||
// - filePath: The path to the EML file to be opened and parsed.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the parsed netmail.Message, a bytes.Buffer containing the body, and an
|
||||
// error if any issues occur during file operations or parsing.
|
||||
func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
||||
fileHandle, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
|
@ -91,7 +146,19 @@ func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
|||
return readEMLFromReader(fileHandle)
|
||||
}
|
||||
|
||||
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader
|
||||
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader.
|
||||
//
|
||||
// This function reads the EML content from the provided io.Reader and uses the net/mail
|
||||
// package to parse the message's headers and body. It returns the parsed netmail.Message
|
||||
// along with a bytes.Buffer containing the body content. Any errors encountered during
|
||||
// the parsing process are returned.
|
||||
//
|
||||
// Parameters:
|
||||
// - reader: An io.Reader containing the EML formatted message.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the parsed netmail.Message, a bytes.Buffer containing the body, and an
|
||||
// error if any issues occur during parsing.
|
||||
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
|
||||
parsedMsg, err := netmail.ReadMessage(reader)
|
||||
if err != nil {
|
||||
|
@ -106,8 +173,18 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error
|
|||
return parsedMsg, &buf, nil
|
||||
}
|
||||
|
||||
// parseEMLHeaders will check the EML headers for the most common headers and set the
|
||||
// according settings in the Msg
|
||||
// parseEMLHeaders parses the EML's headers and populates the Msg with relevant information.
|
||||
//
|
||||
// This function checks the EML headers for common headers and sets the corresponding fields
|
||||
// in the Msg object. It extracts address headers, content types, and other relevant data
|
||||
// for further processing.
|
||||
//
|
||||
// Parameters:
|
||||
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
||||
// - msg: A pointer to the Msg object to be populated with parsed header information.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if parsing the headers fails; otherwise, returns nil.
|
||||
func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
||||
commonHeaders := []Header{
|
||||
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
|
||||
|
@ -175,7 +252,19 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings
|
||||
// parseEMLBodyParts parses the body of an EML based on the different content types and encodings.
|
||||
//
|
||||
// This function examines the content type of the parsed EML message and processes the body
|
||||
// parts accordingly. It handles both plain text and multipart types, ensuring that the
|
||||
// Msg object is populated with the appropriate body content.
|
||||
//
|
||||
// Parameters:
|
||||
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
||||
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
||||
// - msg: A pointer to the Msg object to be populated with the parsed body content.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if any issues occur during the body parsing process; otherwise, returns nil.
|
||||
func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||
// Extract the transfer encoding of the body
|
||||
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
|
||||
|
@ -212,10 +301,24 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseEMLBodyPlain parses the mail body of plain type mails
|
||||
// parseEMLBodyPlain parses the mail body of plain type messages.
|
||||
//
|
||||
// This function handles the parsing of plain text messages based on their encoding. It
|
||||
// identifies the content transfer encoding and decodes the body content accordingly,
|
||||
// storing the result in the provided Msg object.
|
||||
//
|
||||
// Parameters:
|
||||
// - mediatype: The media type of the message (e.g., text/plain).
|
||||
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
||||
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
||||
// - msg: A pointer to the Msg object to be populated with the parsed body content.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if any issues occur during the parsing of the plain body; otherwise, returns nil.
|
||||
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String())
|
||||
// According to RFC2045, if no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
|
||||
// If no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.1
|
||||
if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
|
||||
msg.SetEncoding(EncodingUSASCII)
|
||||
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
||||
|
@ -249,7 +352,20 @@ func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *by
|
|||
return fmt.Errorf("unsupported Content-Transfer-Encoding")
|
||||
}
|
||||
|
||||
// parseEMLMultipart parses a multipart body part of a EML
|
||||
// parseEMLMultipart parses a multipart body part of an EML message.
|
||||
//
|
||||
// This function handles the parsing of multipart messages, extracting the individual parts
|
||||
// and determining their content types. It processes each part according to its content type
|
||||
// and ensures that all relevant data is stored in the Msg object.
|
||||
//
|
||||
// Parameters:
|
||||
// - params: A map containing the parameters from the multipart content type.
|
||||
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
||||
// - msg: A pointer to the Msg object to be populated with the parsed body parts.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if any issues occur during the parsing of the multipart body; otherwise,
|
||||
// returns nil.
|
||||
func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||
boundary, ok := params["boundary"]
|
||||
if !ok {
|
||||
|
@ -349,7 +465,15 @@ ReadNextPart:
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseEMLEncoding parses and determines the encoding of the message
|
||||
// parseEMLEncoding parses and determines the encoding of the message.
|
||||
//
|
||||
// This function extracts the content transfer encoding from the EML headers and sets the
|
||||
// corresponding encoding in the Msg object. It ensures that the correct encoding is used
|
||||
// for further processing of the message content.
|
||||
//
|
||||
// Parameters:
|
||||
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
||||
// - msg: A pointer to the Msg object to be updated with the encoding information.
|
||||
func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
|
||||
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
|
||||
switch {
|
||||
|
@ -363,7 +487,15 @@ func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
|
|||
}
|
||||
}
|
||||
|
||||
// parseEMLContentTypeCharset parses and determines the charset and content type of the message
|
||||
// parseEMLContentTypeCharset parses and determines the charset and content type of the message.
|
||||
//
|
||||
// This function extracts the content type and charset from the EML headers, setting them
|
||||
// appropriately in the Msg object. It ensures that the Msg object is configured with the
|
||||
// correct content type for further processing.
|
||||
//
|
||||
// Parameters:
|
||||
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
||||
// - msg: A pointer to the Msg object to be updated with content type and charset information.
|
||||
func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
|
||||
if value := mailHeader.Get(HeaderContentType.String()); value != "" {
|
||||
contentType, optional := parseMultiPartHeader(value)
|
||||
|
@ -377,7 +509,18 @@ func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
|
|||
}
|
||||
}
|
||||
|
||||
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part
|
||||
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part.
|
||||
//
|
||||
// This function decodes the base64 encoded content of a multipart part and stores the
|
||||
// resulting content in the provided Part object. It handles any errors that occur during
|
||||
// the decoding process.
|
||||
//
|
||||
// Parameters:
|
||||
// - multiPartData: A byte slice containing the base64 encoded data.
|
||||
// - part: A pointer to the Part object where the decoded content will be stored.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if the base64 decoding fails; otherwise, returns nil.
|
||||
func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
|
||||
part.SetEncoding(EncodingB64)
|
||||
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
|
||||
|
@ -388,8 +531,17 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as
|
||||
// separate map
|
||||
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as a map.
|
||||
//
|
||||
// This function splits a multipart header into its main value and any optional parameters,
|
||||
// returning them separately. It helps in processing multipart messages by extracting
|
||||
// relevant information from headers.
|
||||
//
|
||||
// Parameters:
|
||||
// - multiPartHeader: A string representing the multipart header to be parsed.
|
||||
//
|
||||
// Returns:
|
||||
// - The main header value as a string and a map of optional parameters.
|
||||
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
|
||||
optional = make(map[string]string)
|
||||
headerSplit := strings.SplitN(multiPartHeader, ";", 2)
|
||||
|
@ -404,7 +556,20 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s
|
|||
return
|
||||
}
|
||||
|
||||
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed
|
||||
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed.
|
||||
//
|
||||
// This function handles the parsing of multipart sections that are marked as attachments or
|
||||
// embedded content. It processes the content disposition and sets the appropriate fields in
|
||||
// the Msg object based on the parsed data.
|
||||
//
|
||||
// Parameters:
|
||||
// - contentDisposition: A slice of strings containing the content disposition header.
|
||||
// - multiPart: A pointer to the multipart.Part to be parsed.
|
||||
// - msg: A pointer to the Msg object to be populated with the attachment or embed data.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if any issues occur during the parsing of attachments or embeds; otherwise,
|
||||
// returns nil.
|
||||
func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error {
|
||||
cdType, optional := parseMultiPartHeader(contentDisposition[0])
|
||||
filename := "generic.attachment"
|
||||
|
|
165
encoding.go
165
encoding.go
|
@ -4,171 +4,218 @@
|
|||
|
||||
package mail
|
||||
|
||||
// Charset represents a character set for the encoding
|
||||
// Charset is a type wrapper for a string representing different character encodings.
|
||||
type Charset string
|
||||
|
||||
// ContentType represents a content type for the Msg
|
||||
// ContentType is a type wrapper for a string and represents the MIME type of the content being handled.
|
||||
type ContentType string
|
||||
|
||||
// Encoding represents a MIME encoding scheme like quoted-printable or Base64.
|
||||
// Encoding is a type wrapper for a string and represents the type of encoding used for email messages
|
||||
// and/or parts.
|
||||
type Encoding string
|
||||
|
||||
// MIMEVersion represents the MIME version for the mail
|
||||
// MIMEVersion is a type wrapper for a string nad represents the MIME version used in email messages.
|
||||
type MIMEVersion string
|
||||
|
||||
// MIMEType represents the MIME type for the mail
|
||||
// MIMEType is a type wrapper for a string and represents the MIME type for the Msg content or parts.
|
||||
type MIMEType string
|
||||
|
||||
// List of supported encodings
|
||||
const (
|
||||
// EncodingB64 represents the Base64 encoding as specified in RFC 2045.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
|
||||
EncodingB64 Encoding = "base64"
|
||||
|
||||
// EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.7
|
||||
EncodingQP Encoding = "quoted-printable"
|
||||
|
||||
// EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit)
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.7
|
||||
EncodingUSASCII Encoding = "7bit"
|
||||
|
||||
// NoEncoding avoids any character encoding (except of the mail headers)
|
||||
// NoEncoding represents 8-bit encoding for email messages as specified in RFC 6152.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.8
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc6152
|
||||
NoEncoding Encoding = "8bit"
|
||||
)
|
||||
|
||||
// List of common charsets
|
||||
const (
|
||||
// CharsetUTF7 represents the "UTF-7" charset
|
||||
// CharsetUTF7 represents the "UTF-7" charset.
|
||||
CharsetUTF7 Charset = "UTF-7"
|
||||
|
||||
// CharsetUTF8 represents the "UTF-8" charset
|
||||
// CharsetUTF8 represents the "UTF-8" charset.
|
||||
CharsetUTF8 Charset = "UTF-8"
|
||||
|
||||
// CharsetASCII represents the "US-ASCII" charset
|
||||
// CharsetASCII represents the "US-ASCII" charset.
|
||||
CharsetASCII Charset = "US-ASCII"
|
||||
|
||||
// CharsetISO88591 represents the "ISO-8859-1" charset
|
||||
// CharsetISO88591 represents the "ISO-8859-1" charset.
|
||||
CharsetISO88591 Charset = "ISO-8859-1"
|
||||
|
||||
// CharsetISO88592 represents the "ISO-8859-2" charset
|
||||
// CharsetISO88592 represents the "ISO-8859-2" charset.
|
||||
CharsetISO88592 Charset = "ISO-8859-2"
|
||||
|
||||
// CharsetISO88593 represents the "ISO-8859-3" charset
|
||||
// CharsetISO88593 represents the "ISO-8859-3" charset.
|
||||
CharsetISO88593 Charset = "ISO-8859-3"
|
||||
|
||||
// CharsetISO88594 represents the "ISO-8859-4" charset
|
||||
// CharsetISO88594 represents the "ISO-8859-4" charset.
|
||||
CharsetISO88594 Charset = "ISO-8859-4"
|
||||
|
||||
// CharsetISO88595 represents the "ISO-8859-5" charset
|
||||
// CharsetISO88595 represents the "ISO-8859-5" charset.
|
||||
CharsetISO88595 Charset = "ISO-8859-5"
|
||||
|
||||
// CharsetISO88596 represents the "ISO-8859-6" charset
|
||||
// CharsetISO88596 represents the "ISO-8859-6" charset.
|
||||
CharsetISO88596 Charset = "ISO-8859-6"
|
||||
|
||||
// CharsetISO88597 represents the "ISO-8859-7" charset
|
||||
// CharsetISO88597 represents the "ISO-8859-7" charset.
|
||||
CharsetISO88597 Charset = "ISO-8859-7"
|
||||
|
||||
// CharsetISO88599 represents the "ISO-8859-9" charset
|
||||
// CharsetISO88599 represents the "ISO-8859-9" charset.
|
||||
CharsetISO88599 Charset = "ISO-8859-9"
|
||||
|
||||
// CharsetISO885913 represents the "ISO-8859-13" charset
|
||||
// CharsetISO885913 represents the "ISO-8859-13" charset.
|
||||
CharsetISO885913 Charset = "ISO-8859-13"
|
||||
|
||||
// CharsetISO885914 represents the "ISO-8859-14" charset
|
||||
// CharsetISO885914 represents the "ISO-8859-14" charset.
|
||||
CharsetISO885914 Charset = "ISO-8859-14"
|
||||
|
||||
// CharsetISO885915 represents the "ISO-8859-15" charset
|
||||
// CharsetISO885915 represents the "ISO-8859-15" charset.
|
||||
CharsetISO885915 Charset = "ISO-8859-15"
|
||||
|
||||
// CharsetISO885916 represents the "ISO-8859-16" charset
|
||||
// CharsetISO885916 represents the "ISO-8859-16" charset.
|
||||
CharsetISO885916 Charset = "ISO-8859-16"
|
||||
|
||||
// CharsetISO2022JP represents the "ISO-2022-JP" charset
|
||||
// CharsetISO2022JP represents the "ISO-2022-JP" charset.
|
||||
CharsetISO2022JP Charset = "ISO-2022-JP"
|
||||
|
||||
// CharsetISO2022KR represents the "ISO-2022-KR" charset
|
||||
// CharsetISO2022KR represents the "ISO-2022-KR" charset.
|
||||
CharsetISO2022KR Charset = "ISO-2022-KR"
|
||||
|
||||
// CharsetWindows1250 represents the "windows-1250" charset
|
||||
// CharsetWindows1250 represents the "windows-1250" charset.
|
||||
CharsetWindows1250 Charset = "windows-1250"
|
||||
|
||||
// CharsetWindows1251 represents the "windows-1251" charset
|
||||
// CharsetWindows1251 represents the "windows-1251" charset.
|
||||
CharsetWindows1251 Charset = "windows-1251"
|
||||
|
||||
// CharsetWindows1252 represents the "windows-1252" charset
|
||||
// CharsetWindows1252 represents the "windows-1252" charset.
|
||||
CharsetWindows1252 Charset = "windows-1252"
|
||||
|
||||
// CharsetWindows1255 represents the "windows-1255" charset
|
||||
// CharsetWindows1255 represents the "windows-1255" charset.
|
||||
CharsetWindows1255 Charset = "windows-1255"
|
||||
|
||||
// CharsetWindows1256 represents the "windows-1256" charset
|
||||
// CharsetWindows1256 represents the "windows-1256" charset.
|
||||
CharsetWindows1256 Charset = "windows-1256"
|
||||
|
||||
// CharsetKOI8R represents the "KOI8-R" charset
|
||||
// CharsetKOI8R represents the "KOI8-R" charset.
|
||||
CharsetKOI8R Charset = "KOI8-R"
|
||||
|
||||
// CharsetKOI8U represents the "KOI8-U" charset
|
||||
// CharsetKOI8U represents the "KOI8-U" charset.
|
||||
CharsetKOI8U Charset = "KOI8-U"
|
||||
|
||||
// CharsetBig5 represents the "Big5" charset
|
||||
// CharsetBig5 represents the "Big5" charset.
|
||||
CharsetBig5 Charset = "Big5"
|
||||
|
||||
// CharsetGB18030 represents the "GB18030" charset
|
||||
// CharsetGB18030 represents the "GB18030" charset.
|
||||
CharsetGB18030 Charset = "GB18030"
|
||||
|
||||
// CharsetGB2312 represents the "GB2312" charset
|
||||
// CharsetGB2312 represents the "GB2312" charset.
|
||||
CharsetGB2312 Charset = "GB2312"
|
||||
|
||||
// CharsetTIS620 represents the "TIS-620" charset
|
||||
// CharsetTIS620 represents the "TIS-620" charset.
|
||||
CharsetTIS620 Charset = "TIS-620"
|
||||
|
||||
// CharsetEUCKR represents the "EUC-KR" charset
|
||||
// CharsetEUCKR represents the "EUC-KR" charset.
|
||||
CharsetEUCKR Charset = "EUC-KR"
|
||||
|
||||
// CharsetShiftJIS represents the "Shift_JIS" charset
|
||||
// CharsetShiftJIS represents the "Shift_JIS" charset.
|
||||
CharsetShiftJIS Charset = "Shift_JIS"
|
||||
|
||||
// CharsetUnknown represents the "Unknown" charset
|
||||
// CharsetUnknown represents the "Unknown" charset.
|
||||
CharsetUnknown Charset = "Unknown"
|
||||
|
||||
// CharsetGBK represents the "GBK" charset
|
||||
// CharsetGBK represents the "GBK" charset.
|
||||
CharsetGBK Charset = "GBK"
|
||||
)
|
||||
|
||||
// List of MIME versions
|
||||
const (
|
||||
// MIME10 is the MIME Version 1.0
|
||||
MIME10 MIMEVersion = "1.0"
|
||||
)
|
||||
// MIME10 represents the MIME version "1.0" used in email messages.
|
||||
const MIME10 MIMEVersion = "1.0"
|
||||
|
||||
// List of common content types
|
||||
const (
|
||||
TypeAppOctetStream ContentType = "application/octet-stream"
|
||||
// TypeAppOctetStream represents the MIME type for arbitrary binary data.
|
||||
TypeAppOctetStream ContentType = "application/octet-stream"
|
||||
|
||||
// TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative
|
||||
// formats.
|
||||
TypeMultipartAlternative ContentType = "multipart/alternative"
|
||||
TypeMultipartMixed ContentType = "multipart/mixed"
|
||||
TypeMultipartRelated ContentType = "multipart/related"
|
||||
TypePGPSignature ContentType = "application/pgp-signature"
|
||||
TypePGPEncrypted ContentType = "application/pgp-encrypted"
|
||||
TypeTextHTML ContentType = "text/html"
|
||||
TypeTextPlain ContentType = "text/plain"
|
||||
|
||||
// TypeMultipartMixed represents the MIME type for a multipart message containing different parts.
|
||||
TypeMultipartMixed ContentType = "multipart/mixed"
|
||||
|
||||
// TypeMultipartRelated represents the MIME type for a multipart message where each part is a related file
|
||||
// or resource.
|
||||
TypeMultipartRelated ContentType = "multipart/related"
|
||||
|
||||
// TypePGPSignature represents the MIME type for PGP signed messages.
|
||||
TypePGPSignature ContentType = "application/pgp-signature"
|
||||
|
||||
// TypePGPEncrypted represents the MIME type for PGP encrypted messages.
|
||||
TypePGPEncrypted ContentType = "application/pgp-encrypted"
|
||||
|
||||
// TypeTextHTML represents the MIME type for HTML text content.
|
||||
TypeTextHTML ContentType = "text/html"
|
||||
|
||||
// TypeTextPlain represents the MIME type for plain text content.
|
||||
TypeTextPlain ContentType = "text/plain"
|
||||
)
|
||||
|
||||
// List of MIMETypes
|
||||
const (
|
||||
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
|
||||
MIMEAlternative MIMEType = "alternative"
|
||||
MIMEMixed MIMEType = "mixed"
|
||||
MIMERelated MIMEType = "related"
|
||||
|
||||
// MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content.
|
||||
MIMEMixed MIMEType = "mixed"
|
||||
|
||||
// MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities.
|
||||
MIMERelated MIMEType = "related"
|
||||
)
|
||||
|
||||
// String is a standard method to convert an Charset into a printable format
|
||||
// String satisfies the fmt.Stringer interface for the Charset type.
|
||||
// It converts a Charset into a printable format.
|
||||
//
|
||||
// This method returns the string representation of the Charset, allowing it to be easily
|
||||
// printed or logged.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the Charset.
|
||||
func (c Charset) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// String is a standard method to convert an ContentType into a printable format
|
||||
// String satisfies the fmt.Stringer interface for the ContentType type.
|
||||
// It converts a ContentType into a printable format.
|
||||
//
|
||||
// This method returns the string representation of the ContentType, enabling its use
|
||||
// in formatted output such as logging or displaying information to the user.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the ContentType.
|
||||
func (c ContentType) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// String is a standard method to convert an Encoding into a printable format
|
||||
// String satisfies the fmt.Stringer interface for the Encoding type.
|
||||
// It converts an Encoding into a printable format.
|
||||
//
|
||||
// This method returns the string representation of the Encoding, which can be used
|
||||
// for displaying or logging purposes.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the Encoding.
|
||||
func (e Encoding) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
|
97
file.go
97
file.go
|
@ -9,10 +9,15 @@ import (
|
|||
"net/textproto"
|
||||
)
|
||||
|
||||
// FileOption returns a function that can be used for grouping File options
|
||||
// FileOption is a function type used to modify properties of a File
|
||||
type FileOption func(*File)
|
||||
|
||||
// File is an attachment or embedded file of the Msg
|
||||
// File represents a file with properties such as content type, description, encoding, headers, name, and
|
||||
// a writer function.
|
||||
//
|
||||
// This struct can represent either an attachment or an embedded file in a Msg, and it stores relevant
|
||||
// metadata such as content type and encoding, as well as a function to write the file's content to an
|
||||
// io.Writer.
|
||||
type File struct {
|
||||
ContentType ContentType
|
||||
Desc string
|
||||
|
@ -22,32 +27,68 @@ type File struct {
|
|||
Writer func(w io.Writer) (int64, error)
|
||||
}
|
||||
|
||||
// WithFileContentID sets the Content-ID header for the File
|
||||
// WithFileContentID sets the "Content-ID" header in the File's MIME headers to the specified ID.
|
||||
//
|
||||
// This function updates the File's MIME headers by setting the "Content-ID" to the provided string value,
|
||||
// allowing the file to be referenced by this ID within the MIME structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - id: A string representing the content ID to be set in the "Content-ID" header.
|
||||
//
|
||||
// Returns:
|
||||
// - A FileOption function that updates the File's "Content-ID" header.
|
||||
func WithFileContentID(id string) FileOption {
|
||||
return func(f *File) {
|
||||
f.Header.Set(HeaderContentID.String(), id)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFileName sets the filename of the File
|
||||
// WithFileName sets the name of a File to the provided value.
|
||||
//
|
||||
// This function assigns the specified name to the File, updating its Name field.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: A string representing the name to be assigned to the File.
|
||||
//
|
||||
// Returns:
|
||||
// - A FileOption function that sets the File's name.
|
||||
func WithFileName(name string) FileOption {
|
||||
return func(f *File) {
|
||||
f.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// WithFileDescription sets an optional file description of the File that will be
|
||||
// added as Content-Description part
|
||||
// WithFileDescription sets an optional description for the File, which is used in the Content-Description
|
||||
// header of the MIME output.
|
||||
//
|
||||
// This function updates the File's description, allowing an additional text description to be added to
|
||||
// the MIME headers for the file.
|
||||
//
|
||||
// Parameters:
|
||||
// - description: A string representing the description to be set in the Content-Description header.
|
||||
//
|
||||
// Returns:
|
||||
// - A FileOption function that sets the File's description.
|
||||
func WithFileDescription(description string) FileOption {
|
||||
return func(f *File) {
|
||||
f.Desc = description
|
||||
}
|
||||
}
|
||||
|
||||
// WithFileEncoding sets the encoding of the File. By default we should always use
|
||||
// Base64 encoding but there might be exceptions, where this might come handy.
|
||||
// Please note that quoted-printable should never be used for attachments/embeds. If this
|
||||
// is provided as argument, the function will automatically override back to Base64
|
||||
// WithFileEncoding sets the encoding type for a File.
|
||||
//
|
||||
// This function allows the specification of an encoding type for the file, typically used for attachments
|
||||
// or embedded files. By default, Base64 encoding should be used, but this function can override the
|
||||
// default if needed.
|
||||
//
|
||||
// Note: Quoted-printable encoding (EncodingQP) must never be used for attachments or embeds. If EncodingQP
|
||||
// is passed to this function, it will be ignored and the encoding will remain unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - encoding: The Encoding type to be assigned to the File, unless it's EncodingQP.
|
||||
//
|
||||
// Returns:
|
||||
// - A FileOption function that sets the File's encoding.
|
||||
func WithFileEncoding(encoding Encoding) FileOption {
|
||||
return func(f *File) {
|
||||
if encoding == EncodingQP {
|
||||
|
@ -58,23 +99,45 @@ func WithFileEncoding(encoding Encoding) FileOption {
|
|||
}
|
||||
|
||||
// WithFileContentType sets the content type of the File.
|
||||
// By default go-mail will try to guess the file type and its corresponding
|
||||
// content type and fall back to application/octet-stream if the file type
|
||||
// could not be guessed. In some cases, however, it might be needed to force
|
||||
// this to a specific type. For such situations this override method can
|
||||
// be used
|
||||
//
|
||||
// By default, the content type is guessed based on the file type, and if no matching type is identified,
|
||||
// the default "application/octet-stream" is used. This FileOption allows overriding the guessed content
|
||||
// type with a specific one if required.
|
||||
//
|
||||
// Parameters:
|
||||
// - contentType: The ContentType to be assigned to the File.
|
||||
//
|
||||
// Returns:
|
||||
// - A FileOption function that sets the File's content type.
|
||||
func WithFileContentType(contentType ContentType) FileOption {
|
||||
return func(f *File) {
|
||||
f.ContentType = contentType
|
||||
}
|
||||
}
|
||||
|
||||
// setHeader sets header fields to a File
|
||||
// setHeader sets the value of a specified MIME header field for the File.
|
||||
//
|
||||
// This method updates the MIME headers of the File by assigning the provided value to the specified
|
||||
// header field.
|
||||
//
|
||||
// Parameters:
|
||||
// - header: The Header field to be updated.
|
||||
// - value: A string representing the value to be set for the given header.
|
||||
func (f *File) setHeader(header Header, value string) {
|
||||
f.Header.Set(string(header), value)
|
||||
}
|
||||
|
||||
// getHeader return header fields of a File
|
||||
// getHeader retrieves the value of the specified MIME header field.
|
||||
//
|
||||
// This method returns the value of the given header and a boolean indicating whether the header was found
|
||||
// in the File's MIME headers.
|
||||
//
|
||||
// Parameters:
|
||||
// - header: The Header field whose value is to be retrieved.
|
||||
//
|
||||
// Returns:
|
||||
// - A string containing the value of the header.
|
||||
// - A boolean indicating whether the header was present (true) or not (false).
|
||||
func (f *File) getHeader(header Header) (string, bool) {
|
||||
v := f.Header.Get(string(header))
|
||||
return v, v != ""
|
||||
|
|
4
go.mod
4
go.mod
|
@ -7,6 +7,6 @@ module github.com/wneessen/go-mail
|
|||
go 1.16
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/text v0.18.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/text v0.19.0
|
||||
)
|
||||
|
|
12
go.sum
12
go.sum
|
@ -5,8 +5,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
|
@ -37,7 +37,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -46,7 +46,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
@ -55,8 +55,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
|
133
header.go
133
header.go
|
@ -4,129 +4,146 @@
|
|||
|
||||
package mail
|
||||
|
||||
// Header represents a generic mail header field name
|
||||
// Header is a type wrapper for a string and represents email header fields in a Msg.
|
||||
type Header string
|
||||
|
||||
// AddrHeader represents a address related mail Header field name
|
||||
// AddrHeader is a type wrapper for a string and represents email address headers fields in a Msg.
|
||||
type AddrHeader string
|
||||
|
||||
// Importance represents a Importance/Priority value string
|
||||
// Importance is a type wrapper for an int and represents the level of importance or priority for a Msg.
|
||||
type Importance int
|
||||
|
||||
// List of common generic header field names
|
||||
const (
|
||||
// HeaderContentDescription is the "Content-Description" header
|
||||
// HeaderContentDescription is the "Content-Description" header.
|
||||
HeaderContentDescription Header = "Content-Description"
|
||||
|
||||
// HeaderContentDisposition is the "Content-Disposition" header
|
||||
// HeaderContentDisposition is the "Content-Disposition" header.
|
||||
HeaderContentDisposition Header = "Content-Disposition"
|
||||
|
||||
// HeaderContentID is the "Content-ID" header
|
||||
// HeaderContentID is the "Content-ID" header.
|
||||
HeaderContentID Header = "Content-ID"
|
||||
|
||||
// HeaderContentLang is the "Content-Language" header
|
||||
// HeaderContentLang is the "Content-Language" header.
|
||||
HeaderContentLang Header = "Content-Language"
|
||||
|
||||
// HeaderContentLocation is the "Content-Location" header (RFC 2110)
|
||||
// HeaderContentLocation is the "Content-Location" header (RFC 2110).
|
||||
// https://datatracker.ietf.org/doc/html/rfc2110#section-4.3
|
||||
HeaderContentLocation Header = "Content-Location"
|
||||
|
||||
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header
|
||||
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header.
|
||||
HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
|
||||
|
||||
// HeaderContentType is the "Content-Type" header
|
||||
// HeaderContentType is the "Content-Type" header.
|
||||
HeaderContentType Header = "Content-Type"
|
||||
|
||||
// HeaderDate represents the "Date" field
|
||||
// See: https://www.rfc-editor.org/rfc/rfc822#section-5.1
|
||||
// HeaderDate represents the "Date" field.
|
||||
// https://datatracker.ietf.org/doc/html/rfc822#section-5.1
|
||||
HeaderDate Header = "Date"
|
||||
|
||||
// HeaderDispositionNotificationTo is the MDN header as described in RFC8098
|
||||
// See: https://www.rfc-editor.org/rfc/rfc8098.html#section-2.1
|
||||
// HeaderDispositionNotificationTo is the MDN header as described in RFC 8098.
|
||||
// https://datatracker.ietf.org/doc/html/rfc8098#section-2.1
|
||||
HeaderDispositionNotificationTo Header = "Disposition-Notification-To"
|
||||
|
||||
// HeaderImportance represents the "Importance" field
|
||||
// HeaderImportance represents the "Importance" field.
|
||||
HeaderImportance Header = "Importance"
|
||||
|
||||
// HeaderInReplyTo represents the "In-Reply-To" field
|
||||
// HeaderInReplyTo represents the "In-Reply-To" field.
|
||||
HeaderInReplyTo Header = "In-Reply-To"
|
||||
|
||||
// HeaderListUnsubscribe is the "List-Unsubscribe" header field
|
||||
// HeaderListUnsubscribe is the "List-Unsubscribe" header field.
|
||||
HeaderListUnsubscribe Header = "List-Unsubscribe"
|
||||
|
||||
// HeaderListUnsubscribePost is the "List-Unsubscribe-Post" header field
|
||||
// HeaderListUnsubscribePost is the "List-Unsubscribe-Post" header field.
|
||||
HeaderListUnsubscribePost Header = "List-Unsubscribe-Post"
|
||||
|
||||
// HeaderMessageID represents the "Message-ID" field for message identification
|
||||
// See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5
|
||||
// HeaderMessageID represents the "Message-ID" field for message identification.
|
||||
// https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.5
|
||||
HeaderMessageID Header = "Message-ID"
|
||||
|
||||
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045
|
||||
// See: https://datatracker.ietf.org/doc/html/rfc2045#section-4
|
||||
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045.
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-4
|
||||
HeaderMIMEVersion Header = "MIME-Version"
|
||||
|
||||
// HeaderOrganization is the "Organization" header field
|
||||
// HeaderOrganization is the "Organization" header field.
|
||||
HeaderOrganization Header = "Organization"
|
||||
|
||||
// HeaderPrecedence is the "Precedence" header field
|
||||
// HeaderPrecedence is the "Precedence" header field.
|
||||
HeaderPrecedence Header = "Precedence"
|
||||
|
||||
// HeaderPriority represents the "Priority" field
|
||||
// HeaderPriority represents the "Priority" field.
|
||||
HeaderPriority Header = "Priority"
|
||||
|
||||
// HeaderReferences is the "References" header field
|
||||
// HeaderReferences is the "References" header field.
|
||||
HeaderReferences Header = "References"
|
||||
|
||||
// HeaderReplyTo is the "Reply-To" header field
|
||||
// HeaderReplyTo is the "Reply-To" header field.
|
||||
HeaderReplyTo Header = "Reply-To"
|
||||
|
||||
// HeaderSubject is the "Subject" header field
|
||||
// HeaderSubject is the "Subject" header field.
|
||||
HeaderSubject Header = "Subject"
|
||||
|
||||
// HeaderUserAgent is the "User-Agent" header field
|
||||
// HeaderUserAgent is the "User-Agent" header field.
|
||||
HeaderUserAgent Header = "User-Agent"
|
||||
|
||||
// HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field
|
||||
// HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field.
|
||||
HeaderXAutoResponseSuppress Header = "X-Auto-Response-Suppress"
|
||||
|
||||
// HeaderXMailer is the "X-Mailer" header field
|
||||
// HeaderXMailer is the "X-Mailer" header field.
|
||||
HeaderXMailer Header = "X-Mailer"
|
||||
|
||||
// HeaderXMSMailPriority is the "X-MSMail-Priority" header field
|
||||
// HeaderXMSMailPriority is the "X-MSMail-Priority" header field.
|
||||
HeaderXMSMailPriority Header = "X-MSMail-Priority"
|
||||
|
||||
// HeaderXPriority is the "X-Priority" header field
|
||||
// HeaderXPriority is the "X-Priority" header field.
|
||||
HeaderXPriority Header = "X-Priority"
|
||||
)
|
||||
|
||||
// List of common address header field names
|
||||
const (
|
||||
// HeaderBcc is the "Blind Carbon Copy" header field
|
||||
// HeaderBcc is the "Blind Carbon Copy" header field.
|
||||
HeaderBcc AddrHeader = "Bcc"
|
||||
|
||||
// HeaderCc is the "Carbon Copy" header field
|
||||
// HeaderCc is the "Carbon Copy" header field.
|
||||
HeaderCc AddrHeader = "Cc"
|
||||
|
||||
// HeaderEnvelopeFrom is the envelope FROM header field
|
||||
// It's not included in the mail body but only used by the Client for the envelope
|
||||
// HeaderEnvelopeFrom is the envelope FROM header field.
|
||||
//
|
||||
// It is generally not included in the mail body but only used by the Client for the communication with the
|
||||
// SMTP server. If the Msg has no "FROM" address set in the mail body, the msgWriter will try to use the
|
||||
// envelope from address, if this has been set for the Msg.
|
||||
HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom"
|
||||
|
||||
// HeaderFrom is the "From" header field
|
||||
// HeaderFrom is the "From" header field.
|
||||
HeaderFrom AddrHeader = "From"
|
||||
|
||||
// HeaderTo is the "Receipient" header field
|
||||
// HeaderTo is the "Receipient" header field.
|
||||
HeaderTo AddrHeader = "To"
|
||||
)
|
||||
|
||||
// List of Importance values
|
||||
const (
|
||||
// ImportanceLow indicates a low level of importance or priority in a Msg.
|
||||
ImportanceLow Importance = iota
|
||||
|
||||
// ImportanceNormal indicates a standard level of importance or priority for a Msg.
|
||||
ImportanceNormal
|
||||
|
||||
// ImportanceHigh indicates a high level of importance or priority in a Msg.
|
||||
ImportanceHigh
|
||||
|
||||
// ImportanceNonUrgent indicates a non-urgent level of importance or priority in a Msg.
|
||||
ImportanceNonUrgent
|
||||
|
||||
// ImportanceUrgent indicates an urgent level of importance or priority in a Msg.
|
||||
ImportanceUrgent
|
||||
)
|
||||
|
||||
// NumString returns the importance number string based on the Importance
|
||||
// NumString returns a numerical string representation of the Importance level.
|
||||
//
|
||||
// This method maps ImportanceHigh and ImportanceUrgent to "1", while ImportanceNonUrgent and ImportanceLow
|
||||
// are mapped to "0". Other values return an empty string.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the numerical value of the Importance level ("1" or "0"), or an empty string
|
||||
// if the Importance level is unrecognized.
|
||||
func (i Importance) NumString() string {
|
||||
switch i {
|
||||
case ImportanceNonUrgent:
|
||||
|
@ -142,7 +159,14 @@ func (i Importance) NumString() string {
|
|||
}
|
||||
}
|
||||
|
||||
// XPrioString returns the X-Priority number string based on the Importance
|
||||
// XPrioString returns the X-Priority string representation of the Importance level.
|
||||
//
|
||||
// This method maps ImportanceHigh and ImportanceUrgent to "1", while ImportanceNonUrgent and ImportanceLow
|
||||
// are mapped to "5". Other values return an empty string.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the X-Priority value of the Importance level ("1" or "5"), or an empty string
|
||||
// if the Importance level is unrecognized.
|
||||
func (i Importance) XPrioString() string {
|
||||
switch i {
|
||||
case ImportanceNonUrgent:
|
||||
|
@ -158,7 +182,14 @@ func (i Importance) XPrioString() string {
|
|||
}
|
||||
}
|
||||
|
||||
// String returns the importance string based on the Importance
|
||||
// String satisfies the fmt.Stringer interface for the Importance type and returns the string
|
||||
// representation of the Importance level.
|
||||
//
|
||||
// This method provides a human-readable string for each Importance level.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the Importance level ("non-urgent", "low", "high", or "urgent"), or an empty
|
||||
// string if the Importance level is unrecognized.
|
||||
func (i Importance) String() string {
|
||||
switch i {
|
||||
case ImportanceNonUrgent:
|
||||
|
@ -174,12 +205,20 @@ func (i Importance) String() string {
|
|||
}
|
||||
}
|
||||
|
||||
// String returns the header string based on the given Header
|
||||
// String satisfies the fmt.Stringer interface for the Header type and returns the string
|
||||
// representation of the Header.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the Header.
|
||||
func (h Header) String() string {
|
||||
return string(h)
|
||||
}
|
||||
|
||||
// String returns the address header string based on the given AddrHeader
|
||||
// String satisfies the fmt.Stringer interface for the AddrHeader type and returns the string
|
||||
// representation of the AddrHeader.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the AddrHeader.
|
||||
func (a AddrHeader) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
|
|
@ -12,8 +12,14 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
// WriteToTempFile will create a temporary file and output the Msg to this file
|
||||
// The method will return the filename of the temporary file
|
||||
// WriteToTempFile creates a temporary file and writes the Msg content to this file.
|
||||
//
|
||||
// This method generates a temporary file with a ".eml" extension, writes the Msg to it, and returns the
|
||||
// filename of the created temporary file.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the filename of the temporary file.
|
||||
// - An error if the file creation or writing process fails.
|
||||
func (m *Msg) WriteToTempFile() (string, error) {
|
||||
f, err := os.CreateTemp("", "go-mail_*.eml")
|
||||
if err != nil {
|
||||
|
|
|
@ -12,8 +12,14 @@ import (
|
|||
"io/ioutil"
|
||||
)
|
||||
|
||||
// WriteToTempFile will create a temporary file and output the Msg to this file
|
||||
// The method will return the filename of the temporary file
|
||||
// WriteToTempFile creates a temporary file and writes the Msg content to this file.
|
||||
//
|
||||
// This method generates a temporary file with a ".eml" extension, writes the Msg to it, and returns the
|
||||
// filename of the created temporary file.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the filename of the temporary file.
|
||||
// - An error if the file creation or writing process fails.
|
||||
func (m *Msg) WriteToTempFile() (string, error) {
|
||||
f, err := ioutil.TempFile("", "go-mail_*.eml")
|
||||
if err != nil {
|
||||
|
|
168
msgwriter.go
168
msgwriter.go
|
@ -18,22 +18,39 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// MaxHeaderLength defines the maximum line length for a mail header
|
||||
// RFC 2047 suggests 76 characters
|
||||
const MaxHeaderLength = 76
|
||||
const (
|
||||
// MaxHeaderLength defines the maximum line length for a mail header.
|
||||
//
|
||||
// This constant follows the recommendation of RFC 2047, which suggests a maximum length of 76 characters.
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2047
|
||||
MaxHeaderLength = 76
|
||||
|
||||
// MaxBodyLength defines the maximum line length for the mail body
|
||||
// RFC 2047 suggests 76 characters
|
||||
const MaxBodyLength = 76
|
||||
// MaxBodyLength defines the maximum line length for the mail body.
|
||||
//
|
||||
// This constant follows the recommendation of RFC 2047, which suggests a maximum length of 76 characters.
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2047
|
||||
MaxBodyLength = 76
|
||||
|
||||
// SingleNewLine represents a new line that can be used by the msgWriter to issue a carriage return
|
||||
const SingleNewLine = "\r\n"
|
||||
// SingleNewLine represents a single newline character sequence ("\r\n").
|
||||
//
|
||||
// This constant can be used by the msgWriter to issue a carriage return when writing mail content.
|
||||
SingleNewLine = "\r\n"
|
||||
|
||||
// DoubleNewLine represents a double new line that can be used by the msgWriter to
|
||||
// indicate a new segement of the mail
|
||||
const DoubleNewLine = "\r\n\r\n"
|
||||
// DoubleNewLine represents a double newline character sequence ("\r\n\r\n").
|
||||
//
|
||||
// This constant can be used by the msgWriter to indicate a new segment of the mail when writing mail content.
|
||||
DoubleNewLine = "\r\n\r\n"
|
||||
)
|
||||
|
||||
// msgWriter handles the I/O to the io.WriteCloser of the SMTP client
|
||||
// msgWriter handles the I/O operations for writing to the io.WriteCloser of the SMTP client.
|
||||
//
|
||||
// This struct keeps track of the number of bytes written, the character set used, and the depth of the
|
||||
// current multipart section. It also handles encoding, error tracking, and managing multipart and part
|
||||
// writers for constructing the email message body.
|
||||
type msgWriter struct {
|
||||
bytesWritten int64
|
||||
charset Charset
|
||||
|
@ -45,7 +62,18 @@ type msgWriter struct {
|
|||
writer io.Writer
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface for msgWriter
|
||||
// Write implements the io.Writer interface for msgWriter.
|
||||
//
|
||||
// This method writes the provided payload to the underlying writer. It keeps track of the number of bytes
|
||||
// written and handles any errors encountered during the writing process. If a previous error exists, it
|
||||
// prevents further writing and returns the error.
|
||||
//
|
||||
// Parameters:
|
||||
// - payload: A byte slice containing the data to be written.
|
||||
//
|
||||
// Returns:
|
||||
// - The number of bytes successfully written.
|
||||
// - An error if the writing process fails, or if a previous error was encountered.
|
||||
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)
|
||||
|
@ -57,7 +85,19 @@ func (mw *msgWriter) Write(payload []byte) (int, error) {
|
|||
return n, mw.err
|
||||
}
|
||||
|
||||
// writeMsg formats the message and sends it to its io.Writer
|
||||
// writeMsg formats the message and writes it to the msgWriter's io.Writer.
|
||||
//
|
||||
// This method handles the process of writing the message headers and body content, including handling
|
||||
// multipart structures (e.g., mixed, related, alternative), PGP types, and attachments/embeds. It sets the
|
||||
// required headers (e.g., "From", "To", "Cc") and iterates over the message parts, writing them to the
|
||||
// output writer.
|
||||
//
|
||||
// Parameters:
|
||||
// - msg: A pointer to the Msg struct containing the message data and headers to be written.
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2045 (Multipurpose Internet Mail Extensions - MIME)
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322 (Internet Message Format)
|
||||
func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||
msg.addDefaultHeader()
|
||||
msg.checkUserAgent()
|
||||
|
@ -136,7 +176,13 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
|
|||
}
|
||||
}
|
||||
|
||||
// writeGenHeader writes out all generic headers to the msgWriter
|
||||
// writeGenHeader writes out all generic headers to the msgWriter.
|
||||
//
|
||||
// This function extracts all generic headers from the provided Msg object, sorts them, and writes them
|
||||
// to the msgWriter in alphabetical order.
|
||||
//
|
||||
// Parameters:
|
||||
// - msg: The Msg object containing the headers to be written.
|
||||
func (mw *msgWriter) writeGenHeader(msg *Msg) {
|
||||
keys := make([]string, 0, len(msg.genHeader))
|
||||
for key := range msg.genHeader {
|
||||
|
@ -148,14 +194,32 @@ func (mw *msgWriter) writeGenHeader(msg *Msg) {
|
|||
}
|
||||
}
|
||||
|
||||
// writePreformatedHeader writes out all preformated generic headers to the msgWriter
|
||||
// writePreformattedGenHeader writes out all preformatted generic headers to the msgWriter.
|
||||
//
|
||||
// This function iterates over all preformatted generic headers from the provided Msg object and writes
|
||||
// them to the msgWriter in the format "key: value" followed by a newline.
|
||||
//
|
||||
// Parameters:
|
||||
// - msg: The Msg object containing the preformatted headers to be written.
|
||||
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
|
||||
// startMP writes a multipart beginning.
|
||||
//
|
||||
// This function initializes a multipart writer for the msgWriter using the specified MIME type and
|
||||
// boundary. It sets the Content-Type header to indicate the multipart type and writes the boundary
|
||||
// information. If a boundary is provided, it is set explicitly; otherwise, a default boundary is
|
||||
// generated. It also handles writing a new part when nested multipart structures are used.
|
||||
//
|
||||
// Parameters:
|
||||
// - mimeType: The MIME type of the multipart content (e.g., "mixed", "alternative").
|
||||
// - boundary: The boundary string separating different parts of the multipart message.
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2046
|
||||
func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
|
||||
multiPartWriter := multipart.NewWriter(mw)
|
||||
if boundary != "" {
|
||||
|
@ -175,7 +239,10 @@ func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
|
|||
mw.depth++
|
||||
}
|
||||
|
||||
// stopMP closes the multipart
|
||||
// stopMP closes the multipart.
|
||||
//
|
||||
// This function closes the current multipart writer if there is an active multipart structure.
|
||||
// It decreases the depth level of multipart nesting.
|
||||
func (mw *msgWriter) stopMP() {
|
||||
if mw.depth > 0 {
|
||||
mw.err = mw.multiPartWriter[mw.depth-1].Close()
|
||||
|
@ -183,7 +250,17 @@ func (mw *msgWriter) stopMP() {
|
|||
}
|
||||
}
|
||||
|
||||
// addFiles adds the attachments/embeds file content to the mail body
|
||||
// addFiles adds the attachments/embeds file content to the mail body.
|
||||
//
|
||||
// This function iterates through the list of files, setting necessary headers for each file,
|
||||
// including Content-Type, Content-Transfer-Encoding, Content-Disposition, and Content-ID
|
||||
// (if the file is an embed). It determines the appropriate MIME type for each file based on
|
||||
// its extension or the provided ContentType. It writes file headers and file content
|
||||
// to the mail body using the appropriate encoding.
|
||||
//
|
||||
// Parameters:
|
||||
// - files: A slice of File objects to be added to the mail body.
|
||||
// - isAttachment: A boolean indicating whether the files are attachments (true) or embeds (false).
|
||||
func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||
for _, file := range files {
|
||||
encoding := EncodingB64
|
||||
|
@ -242,12 +319,29 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// newPart creates a new MIME multipart io.Writer and sets the partwriter to it
|
||||
// newPart creates a new MIME multipart io.Writer and sets the partWriter to it.
|
||||
//
|
||||
// This function creates a new MIME part using the provided header information and assigns it
|
||||
// to the partWriter. It interacts with the current multipart writer at the specified depth
|
||||
// to create the part.
|
||||
//
|
||||
// Parameters:
|
||||
// - header: A map containing the header fields and their corresponding values for the new part.
|
||||
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
|
||||
// writePart writes the corresponding part to the Msg body.
|
||||
//
|
||||
// This function writes a MIME part to the message body, setting the appropriate headers such
|
||||
// as Content-Type and Content-Transfer-Encoding. It determines the charset for the part,
|
||||
// either using the part's own charset or a fallback charset if none is specified. If the part
|
||||
// is at the top level (depth 0), headers are written directly. For nested parts, it creates
|
||||
// a new MIME part with the provided headers.
|
||||
//
|
||||
// Parameters:
|
||||
// - part: The Part object containing the data to be written.
|
||||
// - charset: The Charset used as a fallback if the part does not specify one.
|
||||
func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
||||
partCharset := part.charset
|
||||
if partCharset.String() == "" {
|
||||
|
@ -272,7 +366,14 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
|||
mw.writeBody(part.writeFunc, part.encoding)
|
||||
}
|
||||
|
||||
// writeString writes a string into the msgWriter's io.Writer interface
|
||||
// writeString writes a string into the msgWriter's io.Writer interface.
|
||||
//
|
||||
// This function writes the given string to the msgWriter's underlying writer. It checks for
|
||||
// existing errors before performing the write operation. It also tracks the number of bytes
|
||||
// written and updates the bytesWritten field accordingly.
|
||||
//
|
||||
// Parameters:
|
||||
// - s: The string to be written.
|
||||
func (mw *msgWriter) writeString(s string) {
|
||||
if mw.err != nil {
|
||||
return
|
||||
|
@ -282,7 +383,16 @@ func (mw *msgWriter) writeString(s string) {
|
|||
mw.bytesWritten += int64(n)
|
||||
}
|
||||
|
||||
// writeHeader writes a header into the msgWriter's io.Writer
|
||||
// writeHeader writes a header into the msgWriter's io.Writer.
|
||||
//
|
||||
// This function writes a header key and its associated values to the msgWriter. It ensures
|
||||
// proper formatting of long headers by inserting line breaks as needed. The header values
|
||||
// are joined and split into words to ensure compliance with the maximum header length
|
||||
// (MaxHeaderLength). After processing the header, it is written to the underlying writer.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The Header key to be written.
|
||||
// - values: A variadic parameter representing the values associated with the header.
|
||||
func (mw *msgWriter) writeHeader(key Header, values ...string) {
|
||||
buffer := strings.Builder{}
|
||||
charLength := MaxHeaderLength - 2
|
||||
|
@ -317,7 +427,17 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
|
|||
mw.writeString("\r\n")
|
||||
}
|
||||
|
||||
// writeBody writes an io.Reader into an io.Writer using provided Encoding
|
||||
// writeBody writes an io.Reader into an io.Writer using the provided Encoding.
|
||||
//
|
||||
// This function writes data from an io.Reader to the underlying writer using a specified
|
||||
// encoding (quoted-printable, base64, or no encoding). It handles encoding of the content
|
||||
// and manages writing the encoded data to the appropriate writer, depending on the depth
|
||||
// (whether the data is part of a multipart structure or not). It also tracks the number
|
||||
// of bytes written and manages any errors encountered during the process.
|
||||
//
|
||||
// Parameters:
|
||||
// - writeFunc: A function that writes the body content to the given io.Writer.
|
||||
// - encoding: The encoding type to use when writing the content (e.g., base64, quoted-printable).
|
||||
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
|
||||
var writer io.Writer
|
||||
var encodedWriter io.WriteCloser
|
||||
|
|
136
part.go
136
part.go
|
@ -12,7 +12,11 @@ import (
|
|||
// PartOption returns a function that can be used for grouping Part options
|
||||
type PartOption func(*Part)
|
||||
|
||||
// Part is a part of the Msg
|
||||
// Part is a part of the Msg.
|
||||
//
|
||||
// This struct represents a single part of a multipart message. Each part has a content type,
|
||||
// charset, optional description, encoding, and a function to write its content to an io.Writer.
|
||||
// It also includes a flag to mark the part as deleted.
|
||||
type Part struct {
|
||||
contentType ContentType
|
||||
charset Charset
|
||||
|
@ -22,7 +26,14 @@ type Part struct {
|
|||
writeFunc func(io.Writer) (int64, error)
|
||||
}
|
||||
|
||||
// GetContent executes the WriteFunc of the Part and returns the content as byte slice
|
||||
// GetContent executes the WriteFunc of the Part and returns the content as a byte slice.
|
||||
//
|
||||
// This function runs the part's writeFunc to write its content into a buffer and then returns
|
||||
// the content as a byte slice. If an error occurs during the writing process, it is returned.
|
||||
//
|
||||
// Returns:
|
||||
// - A byte slice containing the part's content.
|
||||
// - An error if the writeFunc encounters an issue.
|
||||
func (p *Part) GetContent() ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
if _, err := p.writeFunc(&b); err != nil {
|
||||
|
@ -31,83 +42,172 @@ func (p *Part) GetContent() ([]byte, error) {
|
|||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// GetCharset returns the currently set Charset of the Part
|
||||
// GetCharset returns the currently set Charset of the Part.
|
||||
//
|
||||
// This function returns the Charset that is currently set for the Part.
|
||||
//
|
||||
// Returns:
|
||||
// - The Charset of the Part.
|
||||
func (p *Part) GetCharset() Charset {
|
||||
return p.charset
|
||||
}
|
||||
|
||||
// GetContentType returns the currently set ContentType of the Part
|
||||
// GetContentType returns the currently set ContentType of the Part.
|
||||
//
|
||||
// This function returns the ContentType that is currently set for the Part.
|
||||
//
|
||||
// Returns:
|
||||
// - The ContentType of the Part.
|
||||
func (p *Part) GetContentType() ContentType {
|
||||
return p.contentType
|
||||
}
|
||||
|
||||
// GetEncoding returns the currently set Encoding of the Part
|
||||
// GetEncoding returns the currently set Encoding of the Part.
|
||||
//
|
||||
// This function returns the Encoding that is currently set for the Part.
|
||||
//
|
||||
// Returns:
|
||||
// - The Encoding of the Part.
|
||||
func (p *Part) GetEncoding() Encoding {
|
||||
return p.encoding
|
||||
}
|
||||
|
||||
// GetWriteFunc returns the currently set WriterFunc of the Part
|
||||
// GetWriteFunc returns the currently set WriteFunc of the Part.
|
||||
//
|
||||
// This function returns the WriteFunc that is currently set for the Part, which writes
|
||||
// the part's content to an io.Writer.
|
||||
//
|
||||
// Returns:
|
||||
// - The WriteFunc of the Part, which is a function that takes an io.Writer and returns
|
||||
// the number of bytes written and an error (if any).
|
||||
func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) {
|
||||
return p.writeFunc
|
||||
}
|
||||
|
||||
// GetDescription returns the currently set Content-Description of the Part
|
||||
// GetDescription returns the currently set Content-Description of the Part.
|
||||
//
|
||||
// This function returns the Content-Description that is currently set for the Part.
|
||||
//
|
||||
// Returns:
|
||||
// - The Content-Description of the Part as a string.
|
||||
func (p *Part) GetDescription() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
// SetContent overrides the content of the Part with the given string
|
||||
// SetContent overrides the content of the Part with the given string.
|
||||
//
|
||||
// This function sets the content of the Part by creating a new writeFunc that writes the
|
||||
// provided string content to an io.Writer.
|
||||
//
|
||||
// Parameters:
|
||||
// - content: The string that will replace the current content of the Part.
|
||||
func (p *Part) SetContent(content string) {
|
||||
buffer := bytes.NewBufferString(content)
|
||||
p.writeFunc = writeFuncFromBuffer(buffer)
|
||||
}
|
||||
|
||||
// SetContentType overrides the ContentType of the Part
|
||||
// SetContentType overrides the ContentType of the Part.
|
||||
//
|
||||
// This function sets a new ContentType for the Part, replacing the existing one.
|
||||
//
|
||||
// Parameters:
|
||||
// - contentType: The new ContentType to be set for the Part.
|
||||
func (p *Part) SetContentType(contentType ContentType) {
|
||||
p.contentType = contentType
|
||||
}
|
||||
|
||||
// SetCharset overrides the Charset of the Part
|
||||
// SetCharset overrides the Charset of the Part.
|
||||
//
|
||||
// This function sets a new Charset for the Part, replacing the existing one.
|
||||
//
|
||||
// Parameters:
|
||||
// - charset: The new Charset to be set for the Part.
|
||||
func (p *Part) SetCharset(charset Charset) {
|
||||
p.charset = charset
|
||||
}
|
||||
|
||||
// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message
|
||||
// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message.
|
||||
//
|
||||
// This function sets a new Encoding for the Part, replacing the existing one.
|
||||
//
|
||||
// Parameters:
|
||||
// - encoding: The new Encoding to be set for the Part.
|
||||
func (p *Part) SetEncoding(encoding Encoding) {
|
||||
p.encoding = encoding
|
||||
}
|
||||
|
||||
// SetDescription overrides the Content-Description of the Part
|
||||
// SetDescription overrides the Content-Description of the Part.
|
||||
//
|
||||
// This function sets a new Content-Description for the Part, replacing the existing one.
|
||||
//
|
||||
// Parameters:
|
||||
// - description: The new Content-Description to be set for the Part.
|
||||
func (p *Part) SetDescription(description string) {
|
||||
p.description = description
|
||||
}
|
||||
|
||||
// SetWriteFunc overrides the WriteFunc of the Part
|
||||
// SetWriteFunc overrides the WriteFunc of the Part.
|
||||
//
|
||||
// This function sets a new WriteFunc for the Part, replacing the existing one. The WriteFunc
|
||||
// is responsible for writing the Part's content to an io.Writer.
|
||||
//
|
||||
// Parameters:
|
||||
// - writeFunc: A function that writes the Part's content to an io.Writer and returns
|
||||
// the number of bytes written and an error (if any).
|
||||
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
|
||||
p.writeFunc = writeFunc
|
||||
}
|
||||
|
||||
// Delete removes the current part from the parts list of the Msg by setting the
|
||||
// isDeleted flag to true. The msgWriter will skip it then
|
||||
// Delete removes the current part from the parts list of the Msg by setting the isDeleted flag to true.
|
||||
//
|
||||
// This function marks the Part as deleted by setting the isDeleted flag to true. The msgWriter
|
||||
// will skip over this Part during processing.
|
||||
func (p *Part) Delete() {
|
||||
p.isDeleted = true
|
||||
}
|
||||
|
||||
// WithPartCharset overrides the default Part charset
|
||||
// WithPartCharset overrides the default Part charset.
|
||||
//
|
||||
// This function returns a PartOption that allows the charset of a Part to be overridden
|
||||
// with the specified Charset.
|
||||
//
|
||||
// Parameters:
|
||||
// - charset: The Charset to be set for the Part.
|
||||
//
|
||||
// Returns:
|
||||
// - A PartOption function that sets the Part's charset.
|
||||
func WithPartCharset(charset Charset) PartOption {
|
||||
return func(p *Part) {
|
||||
p.charset = charset
|
||||
}
|
||||
}
|
||||
|
||||
// WithPartEncoding overrides the default Part encoding
|
||||
// WithPartEncoding overrides the default Part encoding.
|
||||
//
|
||||
// This function returns a PartOption that allows the encoding of a Part to be overridden
|
||||
// with the specified Encoding.
|
||||
//
|
||||
// Parameters:
|
||||
// - encoding: The Encoding to be set for the Part.
|
||||
//
|
||||
// Returns:
|
||||
// - A PartOption function that sets the Part's encoding.
|
||||
func WithPartEncoding(encoding Encoding) PartOption {
|
||||
return func(p *Part) {
|
||||
p.encoding = encoding
|
||||
}
|
||||
}
|
||||
|
||||
// WithPartContentDescription overrides the default Part Content-Description
|
||||
// WithPartContentDescription overrides the default Part Content-Description.
|
||||
//
|
||||
// This function returns a PartOption that allows the Content-Description of a Part
|
||||
// to be overridden with the specified description.
|
||||
//
|
||||
// Parameters:
|
||||
// - description: The Content-Description to be set for the Part.
|
||||
//
|
||||
// Returns:
|
||||
// - A PartOption function that sets the Part's Content-Description.
|
||||
func WithPartContentDescription(description string) PartOption {
|
||||
return func(p *Part) {
|
||||
p.description = description
|
||||
|
|
29
random.go
29
random.go
|
@ -14,14 +14,33 @@ import (
|
|||
const cr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
// Bitmask sizes for the string generators (based on 93 chars total)
|
||||
//
|
||||
// These constants define bitmask-related values used for efficient random string generation.
|
||||
// The bitmask operates over 93 possible characters, and the constants help determine the
|
||||
// number of bits and indices used in the process.
|
||||
const (
|
||||
letterIdxBits = 7 // 7 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
// letterIdxBits: Number of bits (7) needed to represent a letter index.
|
||||
letterIdxBits = 7
|
||||
// letterIdxMask: Bitmask to extract letter indices (all 1-bits for letterIdxBits).
|
||||
letterIdxMask = 1<<letterIdxBits - 1
|
||||
// letterIdxMax: The maximum number of letter indices that fit in 63 bits.
|
||||
letterIdxMax = 63 / letterIdxBits
|
||||
)
|
||||
|
||||
// randomStringSecure returns a random, string of length characters. This method uses the
|
||||
// crypto/random package and therfore is cryptographically secure
|
||||
// randomStringSecure returns a random string of the specified length.
|
||||
//
|
||||
// This function generates a cryptographically secure random string of the given length using
|
||||
// the crypto/rand package. It ensures that the randomness is secure and suitable for
|
||||
// cryptographic purposes. The function reads random bytes, converts them to indices within
|
||||
// a character range, and builds the string. If an error occurs while reading from the random
|
||||
// pool, it returns the error.
|
||||
//
|
||||
// Parameters:
|
||||
// - length: The length of the random string to be generated.
|
||||
//
|
||||
// Returns:
|
||||
// - A randomly generated string.
|
||||
// - An error if the random generation fails.
|
||||
func randomStringSecure(length int) (string, error) {
|
||||
randString := strings.Builder{}
|
||||
randString.Grow(length)
|
||||
|
|
|
@ -12,7 +12,17 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// randNum returns a random number with a maximum value of length
|
||||
// randNum returns a random number with a maximum value of maxval.
|
||||
//
|
||||
// This function generates a random integer between 0 and maxval (exclusive). It seeds the
|
||||
// random number generator with the current time in nanoseconds to ensure different results
|
||||
// each time the function is called.
|
||||
//
|
||||
// Parameters:
|
||||
// - maxval: The upper bound for the random number generation (exclusive).
|
||||
//
|
||||
// Returns:
|
||||
// - A random integer between 0 and maxval. If maxval is less than or equal to 0, it returns 0.
|
||||
func randNum(maxval int) int {
|
||||
if maxval <= 0 {
|
||||
return 0
|
||||
|
|
|
@ -11,7 +11,17 @@ import (
|
|||
"math/rand"
|
||||
)
|
||||
|
||||
// randNum returns a random number with a maximum value of length
|
||||
// randNum returns a random number with a maximum value of maxval.
|
||||
//
|
||||
// This function generates a random integer between 0 and maxval (exclusive). If maxval is less
|
||||
// than or equal to 0, it returns 0. The random number generator uses the default seed provided
|
||||
// by the rand package.
|
||||
//
|
||||
// Parameters:
|
||||
// - maxval: The upper bound for the random number generation (exclusive).
|
||||
//
|
||||
// Returns:
|
||||
// - A random integer between 0 and maxval. If maxval is less than or equal to 0, it returns 0.
|
||||
func randNum(maxval int) int {
|
||||
if maxval <= 0 {
|
||||
return 0
|
||||
|
|
|
@ -12,8 +12,16 @@ import (
|
|||
)
|
||||
|
||||
// randNum returns a random number with a maximum value of maxval.
|
||||
// go-mail compiled with Go 1.22+ will make use of the novel math/rand/v2 interface
|
||||
// Older versions of Go will use math/rand
|
||||
//
|
||||
// This function generates a random integer between 0 and maxval (exclusive). It utilizes
|
||||
// the math/rand/v2 interface for Go 1.22+ and will default to math/rand for older Go versions.
|
||||
// If maxval is less than or equal to 0, it returns 0.
|
||||
//
|
||||
// Parameters:
|
||||
// - maxval: The upper bound for the random number generation (exclusive).
|
||||
//
|
||||
// Returns:
|
||||
// - A random integer between 0 and maxval. If maxval is less than or equal to 0, it returns 0.
|
||||
func randNum(maxval int) int {
|
||||
if maxval <= 0 {
|
||||
return 0
|
||||
|
|
40
reader.go
40
reader.go
|
@ -8,19 +8,41 @@ import (
|
|||
"io"
|
||||
)
|
||||
|
||||
// Reader is a type that implements the io.Reader interface for a Msg
|
||||
// Reader is a type that implements the io.Reader interface for a Msg.
|
||||
//
|
||||
// This struct represents a reader that reads from a byte slice buffer. It keeps track of the
|
||||
// current read position (offset) and any initialization error. The buffer holds the data to be
|
||||
// read from the message.
|
||||
type Reader struct {
|
||||
buffer []byte // contents are the bytes buffer[offset : len(buffer)]
|
||||
offset int // read at &buffer[offset], write at &buffer[len(buffer)]
|
||||
err error // initialization error
|
||||
}
|
||||
|
||||
// Error returns an error if the Reader err field is not nil
|
||||
// Error returns an error if the Reader err field is not nil.
|
||||
//
|
||||
// This function checks the Reader's err field and returns it if it is not nil. If no error
|
||||
// occurred during initialization, it returns nil.
|
||||
//
|
||||
// Returns:
|
||||
// - The error stored in the err field, or nil if no error is present.
|
||||
func (r *Reader) Error() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// Read reads the length of p of the Msg buffer to satisfy the io.Reader interface
|
||||
// Read reads the content of the Msg buffer into the provided payload to satisfy the io.Reader interface.
|
||||
//
|
||||
// This function reads data from the Reader's buffer into the provided byte slice (payload).
|
||||
// It checks for errors or an empty buffer and resets the Reader if necessary. If no data is available,
|
||||
// it returns io.EOF. Otherwise, it copies the content from the buffer into the payload and updates
|
||||
// the read offset.
|
||||
//
|
||||
// Parameters:
|
||||
// - payload: A byte slice where the data will be copied.
|
||||
//
|
||||
// Returns:
|
||||
// - n: The number of bytes copied into the payload.
|
||||
// - err: An error if any issues occurred during the read operation or io.EOF if the buffer is empty.
|
||||
func (r *Reader) Read(payload []byte) (n int, err error) {
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
|
@ -37,12 +59,20 @@ func (r *Reader) Read(payload []byte) (n int, err error) {
|
|||
return n, err
|
||||
}
|
||||
|
||||
// Reset resets the Reader buffer to be empty, but it retains the underlying storage
|
||||
// for use by future writes.
|
||||
// Reset resets the Reader buffer to be empty, but it retains the underlying storage for future use.
|
||||
//
|
||||
// This function clears the Reader's buffer by setting its length to 0 and resets the read offset
|
||||
// to the beginning. The underlying storage is retained, allowing future writes to reuse the buffer.
|
||||
func (r *Reader) Reset() {
|
||||
r.buffer = r.buffer[:0]
|
||||
r.offset = 0
|
||||
}
|
||||
|
||||
// empty reports whether the unread portion of the Reader buffer is empty.
|
||||
//
|
||||
// This function checks if the unread portion of the Reader's buffer is empty by comparing
|
||||
// the length of the buffer to the current read offset.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the unread portion is empty, false otherwise.
|
||||
func (r *Reader) empty() bool { return len(r.buffer) <= r.offset }
|
||||
|
|
75
senderror.go
75
senderror.go
|
@ -54,7 +54,11 @@ const (
|
|||
ErrAmbiguous
|
||||
)
|
||||
|
||||
// SendError is an error wrapper for delivery errors of the Msg
|
||||
// SendError is an error wrapper for delivery errors of the Msg.
|
||||
//
|
||||
// This struct represents an error that occurs during the delivery of a message. It holds
|
||||
// details about the affected message, a list of errors, the recipient list, and whether
|
||||
// the error is temporary or permanent. It also includes a reason code for the error.
|
||||
type SendError struct {
|
||||
affectedMsg *Msg
|
||||
errlist []error
|
||||
|
@ -66,7 +70,16 @@ type SendError struct {
|
|||
// SendErrReason represents a comparable reason on why the delivery failed
|
||||
type SendErrReason int
|
||||
|
||||
// Error implements the error interface for the SendError type
|
||||
// Error implements the error interface for the SendError type.
|
||||
//
|
||||
// This function returns a detailed error message string for the SendError, including the
|
||||
// reason for failure, list of errors, affected recipients, and the message ID of the
|
||||
// affected message (if available). If the reason is unknown (greater than 10), it returns
|
||||
// "unknown reason". The error message is built dynamically based on the content of the
|
||||
// error list, recipient list, and message ID.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the error message.
|
||||
func (e *SendError) Error() string {
|
||||
if e.Reason > 10 {
|
||||
return "unknown reason"
|
||||
|
@ -101,7 +114,17 @@ func (e *SendError) Error() string {
|
|||
return errMessage.String()
|
||||
}
|
||||
|
||||
// Is implements the errors.Is functionality and compares the SendErrReason
|
||||
// Is implements the errors.Is functionality and compares the SendErrReason.
|
||||
//
|
||||
// This function allows for comparison between two errors by checking if the provided
|
||||
// error matches the SendError type and, if so, compares the SendErrReason and the
|
||||
// temporary status (isTemp) of both errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - errType: The error to compare against the current SendError.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the errors have the same reason and temporary status, false otherwise.
|
||||
func (e *SendError) Is(errType error) bool {
|
||||
var t *SendError
|
||||
if errors.As(errType, &t) && t != nil {
|
||||
|
@ -110,7 +133,13 @@ func (e *SendError) Is(errType error) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// IsTemp returns true if the delivery error is of temporary nature and can be retried
|
||||
// IsTemp returns true if the delivery error is of a temporary nature and can be retried.
|
||||
//
|
||||
// This function checks whether the SendError indicates a temporary error, which suggests
|
||||
// that the delivery can be retried. If the SendError is nil, it returns false.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the error is temporary, false otherwise.
|
||||
func (e *SendError) IsTemp() bool {
|
||||
if e == nil {
|
||||
return false
|
||||
|
@ -118,8 +147,13 @@ func (e *SendError) IsTemp() bool {
|
|||
return e.isTemp
|
||||
}
|
||||
|
||||
// MessageID returns the message ID of the affected Msg that caused the error
|
||||
// If no message ID was set for the Msg, an empty string will be returned
|
||||
// MessageID returns the message ID of the affected Msg that caused the error.
|
||||
//
|
||||
// This function retrieves the message ID of the Msg associated with the SendError.
|
||||
// If no message ID was set or if the SendError or Msg is nil, it returns an empty string.
|
||||
//
|
||||
// Returns:
|
||||
// - The message ID as a string, or an empty string if no ID is available.
|
||||
func (e *SendError) MessageID() string {
|
||||
if e == nil || e.affectedMsg == nil {
|
||||
return ""
|
||||
|
@ -127,7 +161,13 @@ func (e *SendError) MessageID() string {
|
|||
return e.affectedMsg.GetMessageID()
|
||||
}
|
||||
|
||||
// Msg returns the pointer to the affected message that caused the error
|
||||
// Msg returns the pointer to the affected message that caused the error.
|
||||
//
|
||||
// This function retrieves the Msg associated with the SendError. If the SendError or
|
||||
// the affectedMsg is nil, it returns nil.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the Msg that caused the error, or nil if not available.
|
||||
func (e *SendError) Msg() *Msg {
|
||||
if e == nil || e.affectedMsg == nil {
|
||||
return nil
|
||||
|
@ -135,7 +175,14 @@ func (e *SendError) Msg() *Msg {
|
|||
return e.affectedMsg
|
||||
}
|
||||
|
||||
// String implements the Stringer interface for the SendErrReason
|
||||
// String satisfies the fmt.Stringer interface for the SendErrReason type.
|
||||
//
|
||||
// This function converts the SendErrReason into a human-readable string representation based
|
||||
// on the error type. If the error reason does not match any predefined case, it returns
|
||||
// "unknown reason".
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the SendErrReason.
|
||||
func (r SendErrReason) String() string {
|
||||
switch r {
|
||||
case ErrGetSender:
|
||||
|
@ -164,8 +211,16 @@ func (r SendErrReason) String() string {
|
|||
return "unknown reason"
|
||||
}
|
||||
|
||||
// isTempError checks the given SMTP error and returns true if the given error is of temporary nature
|
||||
// and should be retried
|
||||
// isTempError checks if the given SMTP error is of a temporary nature and should be retried.
|
||||
//
|
||||
// This function inspects the error message and returns true if the first character of the
|
||||
// error message is '4', indicating a temporary SMTP error that can be retried.
|
||||
//
|
||||
// Parameters:
|
||||
// - err: The error to check.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the error is temporary, false otherwise.
|
||||
func isTempError(err error) bool {
|
||||
return err.Error()[0] == '4'
|
||||
}
|
||||
|
|
32
smtp/smtp.go
32
smtp/smtp.go
|
@ -36,7 +36,15 @@ import (
|
|||
"github.com/wneessen/go-mail/log"
|
||||
)
|
||||
|
||||
var ErrNonTLSConnection = errors.New("connection is not using TLS")
|
||||
var (
|
||||
|
||||
// ErrNonTLSConnection is returned when an attempt is made to retrieve TLS state on a non-TLS connection.
|
||||
ErrNonTLSConnection = errors.New("connection is not using TLS")
|
||||
|
||||
// ErrNoConnection is returned when attempting to perform an operation that requires an established
|
||||
// connection but none exists.
|
||||
ErrNoConnection = errors.New("connection is not established")
|
||||
)
|
||||
|
||||
// A Client represents a client connection to an SMTP server.
|
||||
type Client struct {
|
||||
|
@ -67,6 +75,9 @@ type Client struct {
|
|||
// helloError is the error from the hello
|
||||
helloError error
|
||||
|
||||
// isConnected indicates if the Client has an active connection
|
||||
isConnected bool
|
||||
|
||||
// localName is the name to use in HELO/EHLO
|
||||
localName string // the name to use in HELO/EHLO
|
||||
|
||||
|
@ -113,6 +124,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
|||
}
|
||||
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
|
||||
_, c.tls = conn.(*tls.Conn)
|
||||
c.isConnected = true
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
@ -121,6 +133,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
|||
func (c *Client) Close() error {
|
||||
c.mutex.Lock()
|
||||
err := c.Text.Close()
|
||||
c.isConnected = false
|
||||
c.mutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
@ -516,6 +529,7 @@ func (c *Client) Quit() error {
|
|||
}
|
||||
c.mutex.Lock()
|
||||
err = c.Text.Close()
|
||||
c.isConnected = false
|
||||
c.mutex.Unlock()
|
||||
|
||||
return err
|
||||
|
@ -555,15 +569,19 @@ func (c *Client) SetDSNRcptNotifyOption(d string) {
|
|||
// HasConnection checks if the client has an active connection.
|
||||
// Returns true if the `conn` field is not nil, indicating an active connection.
|
||||
func (c *Client) HasConnection() bool {
|
||||
return c.conn != nil
|
||||
c.mutex.RLock()
|
||||
isConn := c.isConnected
|
||||
c.mutex.RUnlock()
|
||||
return isConn
|
||||
}
|
||||
|
||||
// UpdateDeadline sets a new deadline on the SMTP connection with the specified timeout duration.
|
||||
func (c *Client) UpdateDeadline(timeout time.Duration) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return fmt.Errorf("smtp: failed to update deadline: %w", err)
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -573,17 +591,17 @@ func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) {
|
|||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
if !c.isConnected {
|
||||
return nil, ErrNoConnection
|
||||
}
|
||||
if !c.tls {
|
||||
return nil, ErrNonTLSConnection
|
||||
}
|
||||
if c.conn == nil {
|
||||
return nil, errors.New("smtp: connection is not established")
|
||||
}
|
||||
if conn, ok := c.conn.(*tls.Conn); ok {
|
||||
cstate := conn.ConnectionState()
|
||||
return &cstate, nil
|
||||
}
|
||||
return nil, errors.New("smtp: connection is not a TLS connection")
|
||||
return nil, errors.New("unable to retrieve TLS connection state")
|
||||
}
|
||||
|
||||
// debugLog checks if the debug flag is set and if so logs the provided message to
|
||||
|
|
|
@ -1640,6 +1640,357 @@ func TestTLSConnState(t *testing.T) {
|
|||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_GetTLSConnectionState(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
cfg := &tls.Config{ServerName: "example.com"}
|
||||
testHookStartTLS(cfg) // set the RootCAs
|
||||
if err := c.StartTLS(cfg); err != nil {
|
||||
t.Errorf("StartTLS: %v", err)
|
||||
return
|
||||
}
|
||||
cs, err := c.GetTLSConnectionState()
|
||||
if err != nil {
|
||||
t.Errorf("failed to get TLSConnectionState: %s", err)
|
||||
return
|
||||
}
|
||||
if cs.Version == 0 || !cs.HandshakeComplete {
|
||||
t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs)
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_GetTLSConnectionState_noTLS(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
_, err = c.GetTLSConnectionState()
|
||||
if err == nil {
|
||||
t.Error("GetTLSConnectionState: expected error; got nil")
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_GetTLSConnectionState_noConn(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
_ = c.Close()
|
||||
_, err = c.GetTLSConnectionState()
|
||||
if err == nil {
|
||||
t.Error("GetTLSConnectionState: expected error; got nil")
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_GetTLSConnectionState_unableErr(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
c.tls = true
|
||||
_, err = c.GetTLSConnectionState()
|
||||
if err == nil {
|
||||
t.Error("GetTLSConnectionState: expected error; got nil")
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_HasConnection(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
cfg := &tls.Config{ServerName: "example.com"}
|
||||
testHookStartTLS(cfg) // set the RootCAs
|
||||
if err := c.StartTLS(cfg); err != nil {
|
||||
t.Errorf("StartTLS: %v", err)
|
||||
return
|
||||
}
|
||||
if !c.HasConnection() {
|
||||
t.Error("HasConnection: expected true; got false")
|
||||
return
|
||||
}
|
||||
if err = c.Quit(); err != nil {
|
||||
t.Errorf("closing connection failed: %s", err)
|
||||
return
|
||||
}
|
||||
if c.HasConnection() {
|
||||
t.Error("HasConnection: expected false; got true")
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_SetDSNMailReturnOption(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
c.SetDSNMailReturnOption("foo")
|
||||
if c.dsnmrtype != "foo" {
|
||||
t.Errorf("SetDSNMailReturnOption: expected %s; got %s", "foo", c.dsnrntype)
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_SetDSNRcptNotifyOption(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
c.SetDSNRcptNotifyOption("foo")
|
||||
if c.dsnrntype != "foo" {
|
||||
t.Errorf("SetDSNMailReturnOption: expected %s; got %s", "foo", c.dsnrntype)
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_UpdateDeadline(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err = serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if !c.HasConnection() {
|
||||
t.Error("HasConnection: expected true; got false")
|
||||
return
|
||||
}
|
||||
if err = c.UpdateDeadline(time.Millisecond * 20); err != nil {
|
||||
t.Errorf("failed to update deadline: %s", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
if !c.HasConnection() {
|
||||
t.Error("HasConnection: expected true; got false")
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func newLocalListener(t *testing.T) net.Listener {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
|
@ -1685,6 +2036,8 @@ func serverHandle(c net.Conn, t *testing.T) error {
|
|||
}
|
||||
config := &tls.Config{Certificates: []tls.Certificate{keypair}}
|
||||
return tf(config)
|
||||
case "QUIT":
|
||||
return nil
|
||||
default:
|
||||
t.Fatalf("unrecognized command: %q", s.Text())
|
||||
}
|
||||
|
|
17
tls.go
17
tls.go
|
@ -4,25 +4,32 @@
|
|||
|
||||
package mail
|
||||
|
||||
// TLSPolicy type describes a int alias for the different TLS policies we allow
|
||||
// TLSPolicy is a type wrapper for an int type and describes the different TLS policies we allow.
|
||||
type TLSPolicy int
|
||||
|
||||
const (
|
||||
// TLSMandatory requires that the connection to the server is
|
||||
// encrypting using STARTTLS. If the server does not support STARTTLS
|
||||
// the connection will be terminated with an error
|
||||
// the connection will be terminated with an error.
|
||||
TLSMandatory TLSPolicy = iota
|
||||
|
||||
// TLSOpportunistic tries to establish an encrypted connection via the
|
||||
// STARTTLS protocol. If the server does not support this, it will fall
|
||||
// back to non-encrypted plaintext transmission
|
||||
// back to non-encrypted plaintext transmission.
|
||||
TLSOpportunistic
|
||||
|
||||
// NoTLS forces the transaction to be not encrypted
|
||||
// NoTLS forces the transaction to be not encrypted.
|
||||
NoTLS
|
||||
)
|
||||
|
||||
// String is a standard method to convert a TLSPolicy into a printable format
|
||||
// String satisfies the fmt.Stringer interface for the TLSPolicy type.
|
||||
//
|
||||
// This function returns a string representation of the TLSPolicy. It matches the policy
|
||||
// value to predefined constants and returns the corresponding string. If the policy does
|
||||
// not match any known values, it returns "UnknownPolicy".
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the TLSPolicy.
|
||||
func (p TLSPolicy) String() string {
|
||||
switch p {
|
||||
case TLSMandatory:
|
||||
|
|
Loading…
Reference in a new issue