diff --git a/README.md b/README.md index 8c200c7..3a67d0d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/auth.go b/auth.go index f1dad86..e175a12 100644 --- a/auth.go +++ b/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") ) diff --git a/b64linebreaker.go b/b64linebreaker.go index 088b38e..cc83973 100644 --- a/b64linebreaker.go +++ b/b64linebreaker.go @@ -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]) diff --git a/client.go b/client.go index 4d1b0b1..45fd764 100644 --- a/client.go +++ b/client.go @@ -19,190 +19,241 @@ import ( "github.com/wneessen/go-mail/smtp" ) -// Defaults const ( - // DefaultPort is the default connection port to the SMTP server + // DefaultPort is the default connection port to the SMTP server. DefaultPort = 25 - // DefaultPortSSL is the default connection port for SSL/TLS to the SMTP server + // DefaultPortSSL is the default connection port for SSL/TLS to the SMTP server. DefaultPortSSL = 465 - // DefaultPortTLS is the default connection port for STARTTLS to the SMTP server + // DefaultPortTLS is the default connection port for STARTTLS to the SMTP server. DefaultPortTLS = 587 - // DefaultTimeout is the default connection timeout + // DefaultTimeout is the default connection timeout. DefaultTimeout = time.Second * 15 - // DefaultTLSPolicy is the default STARTTLS policy + // DefaultTLSPolicy specifies the default TLS policy for connections. DefaultTLSPolicy = TLSMandatory - // DefaultTLSMinVersion is the minimum TLS version required for the connection - // Nowadays TLS1.2 should be the sane default + // DefaultTLSMinVersion defines the minimum TLS version to be used for secure connections. + // Nowadays TLS 1.2 is assumed be a sane default. DefaultTLSMinVersion = tls.VersionTLS12 ) -// DSNMailReturnOption is a type to define which MAIL RET option is used when a DSN -// is requested -type DSNMailReturnOption string - -// DSNRcptNotifyOption is a type to define which RCPT NOTIFY option is used when a DSN -// is requested -type DSNRcptNotifyOption string - const ( - // DSNMailReturnHeadersOnly requests that only the headers of the message be returned. - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.3 + // DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in + // a DSN (Delivery Status Notification). + // + // https://datatracker.ietf.org/doc/html/rfc1891#section-5.3 DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS" - // DSNMailReturnFull requests that the entire message be returned in any "failed" - // delivery status notification issued for this recipient - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.3 + // DSNMailReturnFull requests that the entire mail message is returned in any failed DSN + // (Delivery Status Notification) issued for this recipient. + // + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.3 DSNMailReturnFull DSNMailReturnOption = "FULL" - // DSNRcptNotifyNever requests that a DSN not be returned to the sender under - // any conditions. - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1 + // DSNRcptNotifyNever indicates that no DSN (Delivery Status Notifications) should be sent for the + // recipient under any condition. + // + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER" - // DSNRcptNotifySuccess requests that a DSN be issued on successful delivery - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1 + // DSNRcptNotifySuccess indicates that the sender requests a DSN (Delivery Status Notification) if the + // message is successfully delivered. + // + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS" - // DSNRcptNotifyFailure requests that a DSN be issued on delivery failure - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1 + // DSNRcptNotifyFailure requests that a DSN (Delivery Status Notification) is issued if delivery of + // a message fails. + // + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyFailure DSNRcptNotifyOption = "FAILURE" - // DSNRcptNotifyDelay indicates the sender's willingness to receive - // "delayed" DSNs. Delayed DSNs may be issued if delivery of a message has - // been delayed for an unusual amount of time (as determined by the MTA at - // which the message is delayed), but the final delivery status (whether - // successful or failure) cannot be determined. The absence of the DELAY - // keyword in a NOTIFY parameter requests that a "delayed" DSN NOT be - // issued under any conditions. - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1 + // DSNRcptNotifyDelay indicates the sender's willingness to receive "delayed" DSNs. + // + // Delayed DSNs may be issued if delivery of a message has been delayed for an unusual amount of time + // (as determined by the MTA at which the message is delayed), but the final delivery status (whether + // successful or failure) cannot be determined. The absence of the DELAY keyword in a NOTIFY parameter + // requests that a "delayed" DSN NOT be issued under any conditions. + // + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY" ) -// DialContextFunc is a type to define custom DialContext function. -type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error) +type ( + // DialContextFunc defines a function type for establishing a network connection using context, network + // type, and address. It is used to specify custom DialContext function. + // + // By default we use net.Dial or tls.Dial respectively. + DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error) -// Client is the SMTP client struct -type Client struct { - // Timeout for the SMTP server connection - connTimeout time.Duration + // DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested + // in a Delivery Status Notification (DSN). + // + // https://datatracker.ietf.org/doc/html/rfc1891/ + DSNMailReturnOption string - // dialContextFunc is a custom DialContext function to dial target SMTP server - dialContextFunc DialContextFunc + // DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a + // recipient in DSNs. + // + // https://datatracker.ietf.org/doc/html/rfc1891/ + DSNRcptNotifyOption string - // dsn indicates that we want to use DSN for the Client - dsn bool + // Option is a function type that modifies the configuration or behavior of a Client instance. + Option func(*Client) error - // dsnmrtype defines the DSNMailReturnOption in case DSN is enabled - dsnmrtype DSNMailReturnOption + // Client is responsible for connecting and interacting with an SMTP server. + // + // This struct represents the go-mail client, which manages the connection, authentication, and communication + // with an SMTP server. It contains various configuration options, including connection timeouts, encryption + // settings, authentication methods, and Delivery Status Notification (DSN) preferences. + // + // References: + // - https://datatracker.ietf.org/doc/html/rfc3207#section-2 + // - https://datatracker.ietf.org/doc/html/rfc8314 + Client struct { + // connTimeout specifies timeout for the connection to the SMTP server. + connTimeout time.Duration - // dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled - dsnrntype []string + // dialContextFunc is the DialContextFunc that is used by the Client to connect to the SMTP server. + dialContextFunc DialContextFunc - // fallbackPort is used as an alternative port number in case the primary port is unavailable or - // fails to bind. - fallbackPort int + // dsnRcptNotifyType represents the different types of notifications for DSN (Delivery Status Notifications) + // receipts. + dsnRcptNotifyType []string - // HELO/EHLO string for the greeting the target SMTP server - helo string + // dsnReturnType specifies the type of Delivery Status Notification (DSN) that should be requested for an + // email. + dsnReturnType DSNMailReturnOption - // Hostname of the target SMTP server to connect to - host string + // fallbackPort is used as an alternative port number in case the primary port is unavailable or + // fails to bind. + // + // The fallbackPort is only used in combination with SetTLSPortPolicy and SetSSLPort correspondingly. + fallbackPort int - // isEncrypted indicates if a Client connection is encrypted or not - isEncrypted bool + // helo is the hostname used in the HELO/EHLO greeting, that is sent to the target SMTP server. + // + // helo might be different as host. This can be useful in a shared-hosting scenario. + helo string - // logger is a logger that implements the log.Logger interface - logger log.Logger + // host is the hostname of the SMTP server we are connecting to. + host string - // mutex is used to synchronize access to shared resources, ensuring that only one goroutine can - // modify them at a time. - mutex sync.RWMutex + // isEncrypted indicates wether the Client connection is encrypted or not. + isEncrypted bool - // noNoop indicates the Noop is to be skipped - noNoop bool + // logger is a logger that satisfies the log.Logger interface. + logger log.Logger - // pass is the corresponding SMTP AUTH password - pass string + // mutex is used to synchronize access to shared resources, ensuring that only one goroutine can + // modify them at a time. + mutex sync.RWMutex - // port specifies the network port number on which the server listens for incoming connections. - port int + // noNoop indicates that the Client should skip the "NOOP" command during the dial. + // + // This is useful for servers which delay potentially unwanted clients when they perform commands + // other than AUTH. + noNoop bool - // smtpAuth is a pointer to smtp.Auth - smtpAuth smtp.Auth + // pass represents a password or a secret token used for the SMTP authentication. + pass string - // smtpAuthType represents the authentication type for SMTP AUTH - smtpAuthType SMTPAuthType + // port specifies the network port that is used to establish the connection with the SMTP server. + port int - // smtpClient is the smtp.Client that is set up when using the Dial*() methods - smtpClient *smtp.Client + // requestDSN indicates wether we want to request DSN (Delivery Status Notifications). + requestDSN bool - // tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol - tlspolicy TLSPolicy + // smtpAuth is the authentication type that is used to authenticate the user with SMTP server. It + // satisfies the smtp.Auth interface. + // + // Unless you plan to write you own custom authentication method, it is advised to not set this manually. + // You should use one of go-mail's SMTPAuthType, instead. + smtpAuth smtp.Auth - // tlsconfig represents the tls.Config setting for the STARTTLS connection - tlsconfig *tls.Config + // smtpAuthType specifies the authentication type to be used for SMTP authentication. + smtpAuthType SMTPAuthType - // useDebugLog enables the debug logging on the SMTP client - useDebugLog bool + // smtpClient is an instance of smtp.Client used for handling the communication with the SMTP server. + smtpClient *smtp.Client - // user is the SMTP AUTH username - user string + // tlspolicy defines the TLSPolicy configuration the Client uses for the STARTTLS protocol. + // + // https://datatracker.ietf.org/doc/html/rfc3207#section-2 + tlspolicy TLSPolicy - // Use SSL for the connection - useSSL bool -} + // tlsconfig is a pointer to tls.Config that specifies the TLS configuration for the STARTTLS communication. + tlsconfig *tls.Config -// Option returns a function that can be used for grouping Client options -type Option func(*Client) error + // useDebugLog indicates whether debug level logging is enabled for the Client. + useDebugLog bool + + // user represents a username used for the SMTP authentication. + user string + + // useSSL indicates whether to use SSL/TLS encryption for network communication. + // + // https://datatracker.ietf.org/doc/html/rfc8314 + useSSL bool + } +) var ( - // ErrInvalidPort should be used if a port is specified that is not valid + // ErrInvalidPort is returned when the specified port for the SMTP connection is not valid ErrInvalidPort = errors.New("invalid port number") - // ErrInvalidTimeout should be used if a timeout is set that is zero or negative + // ErrInvalidTimeout is returned when the specified timeout is zero or negative. ErrInvalidTimeout = errors.New("timeout cannot be zero or negative") - // ErrInvalidHELO should be used if an empty HELO sting is provided + // ErrInvalidHELO is returned when the HELO/EHLO value is invalid due to being empty. ErrInvalidHELO = errors.New("invalid HELO/EHLO value - must not be empty") - // ErrInvalidTLSConfig should be used if an empty tls.Config is provided + // ErrInvalidTLSConfig is returned when the provided TLS configuration is invalid or nil. ErrInvalidTLSConfig = errors.New("invalid TLS config") - // ErrNoHostname should be used if a Client has no hostname set + // ErrNoHostname is returned when the hostname for the client is not provided or empty. ErrNoHostname = errors.New("hostname for client cannot be empty") - // ErrDeadlineExtendFailed should be used if the extension of the connection deadline fails + // ErrDeadlineExtendFailed is returned when an attempt to extend the connection deadline fails. ErrDeadlineExtendFailed = errors.New("connection deadline extension failed") - // ErrNoActiveConnection should be used when a method is used that requies a server connection - // but is not yet connected + // ErrNoActiveConnection indicates that there is no active connection to the SMTP server. ErrNoActiveConnection = errors.New("not connected to SMTP server") - // ErrServerNoUnencoded should be used when 8BIT encoding is selected for a message, but - // the server does not offer 8BITMIME mode + // ErrServerNoUnencoded indicates that the server does not support 8BITMIME for unencoded 8-bit messages. ErrServerNoUnencoded = errors.New("message is 8bit unencoded, but server does not support 8BITMIME") - // ErrInvalidDSNMailReturnOption should be used when an invalid option is provided for the - // DSNMailReturnOption in WithDSN + // ErrInvalidDSNMailReturnOption is returned when an invalid DSNMailReturnOption is provided as argument + // to the WithDSN Option. ErrInvalidDSNMailReturnOption = errors.New("DSN mail return option can only be HDRS or FULL") - // ErrInvalidDSNRcptNotifyOption should be used when an invalid option is provided for the - // DSNRcptNotifyOption in WithDSN + // ErrInvalidDSNRcptNotifyOption is returned when an invalid DSNRcptNotifyOption is provided as argument + // to the WithDSN Option. ErrInvalidDSNRcptNotifyOption = errors.New("DSN rcpt notify option can only be: NEVER, " + "SUCCESS, FAILURE or DELAY") - // ErrInvalidDSNRcptNotifyCombination should be used when an invalid option is provided for the - // DSNRcptNotifyOption in WithDSN + // ErrInvalidDSNRcptNotifyCombination is returned when an invalid combination of DSNRcptNotifyOption is + // provided as argument to the WithDSN Option. ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " + "combined with any of SUCCESS, FAILURE or DELAY") ) -// NewClient returns a new Session client object +// NewClient creates a new Client instance with the provided host and optional configuration Option functions. +// +// This function initializes a Client with default values, such as connection timeout, port, TLS settings, +// and the HELO/EHLO hostname. Option functions, if provided, can override the default configuration. +// It ensures that essential values, like the host, are set. An error is returned if critical defaults are unset. +// +// Parameters: +// - host: The hostname of the SMTP server to connect to. +// - opts: Optional configuration functions to override default settings. +// +// Returns: +// - A pointer to the initialized Client. +// - An error if any critical default values are missing or options fail to apply. func NewClient(host string, opts ...Option) (*Client, error) { c := &Client{ connTimeout: DefaultTimeout, @@ -235,7 +286,17 @@ func NewClient(host string, opts ...Option) (*Client, error) { return c, nil } -// WithPort overrides the default connection port +// WithPort sets the port number for the Client and overrides the default port. +// +// This function sets the specified port number for the Client, ensuring that the port number is valid +// (between 1 and 65535). If the provided port number is invalid, an error is returned. +// +// Parameters: +// - port: The port number to be used by the Client. Must be between 1 and 65535. +// +// Returns: +// - An Option function that applies the port setting to the Client. +// - An error if the port number is outside the valid range. func WithPort(port int) Option { return func(c *Client) error { if port < 1 || port > 65535 { @@ -246,7 +307,17 @@ func WithPort(port int) Option { } } -// WithTimeout overrides the default connection timeout +// WithTimeout sets the connection timeout for the Client and overrides the default timeout. +// +// This function configures the Client with a specified connection timeout duration. It validates that the +// provided timeout is greater than zero. If the timeout is invalid, an error is returned. +// +// Parameters: +// - timeout: The duration to be set as the connection timeout. Must be greater than zero. +// +// Returns: +// - An Option function that applies the timeout setting to the Client. +// - An error if the timeout duration is invalid. func WithTimeout(timeout time.Duration) Option { return func(c *Client) error { if timeout <= 0 { @@ -257,7 +328,12 @@ func WithTimeout(timeout time.Duration) Option { } } -// WithSSL tells the client to use a SSL/TLS connection +// WithSSL enables implicit SSL/TLS for the Client. +// +// This function configures the Client to use implicit SSL/TLS for secure communication. +// +// Returns: +// - An Option function that enables SSL/TLS for the Client. func WithSSL() Option { return func(c *Client) error { c.useSSL = true @@ -265,16 +341,19 @@ func WithSSL() Option { } } -// WithSSLPort tells the Client wether or not to use SSL and fallback. -// The correct port is automatically set. +// WithSSLPort enables implicit SSL/TLS with an optional fallback for the Client. The correct port is +// automatically set. // -// Port 465 is used when SSL set (true). -// Port 25 is used when SSL is unset (false). -// When the SSL connection fails and fb is set to true, -// the client will attempt to connect on port 25 using plaintext. +// When this option is used with NewClient, the default port 25 is overridden with port 465 for SSL/TLS connections. +// If fallback is set to true and the SSL/TLS connection fails, the Client attempts to connect on port 25 using an +// unencrypted connection. If WithPort has already been used to set a different port, that port takes precedence, +// and the automatic fallback mechanism is skipped. // -// Note: If a different port has already been set otherwise, the port-choosing -// and fallback automatism will be skipped. +// Parameters: +// - fallback: A boolean indicating whether to fall back to port 25 without SSL/TLS if the connection fails. +// +// Returns: +// - An Option function that enables SSL/TLS and configures the fallback mechanism for the Client. func WithSSLPort(fallback bool) Option { return func(c *Client) error { c.SetSSLPort(true, fallback) @@ -282,8 +361,15 @@ func WithSSLPort(fallback bool) Option { } } -// WithDebugLog tells the client to log incoming and outgoing messages of the SMTP client -// to StdErr +// WithDebugLog enables debug logging for the Client. +// +// This function activates debug logging, which logs incoming and outgoing communication between the +// Client and the SMTP server to os.Stderr. Be cautious when using this option, as the logs may include +// unencrypted authentication data, depending on the SMTP authentication method in use, which could +// pose a data protection risk. +// +// Returns: +// - An Option function that enables debug logging for the Client. func WithDebugLog() Option { return func(c *Client) error { c.useDebugLog = true @@ -291,7 +377,16 @@ func WithDebugLog() Option { } } -// WithLogger overrides the default log.Logger that is used for debug logging +// WithLogger defines a custom logger for the Client. +// +// This function sets a custom logger for the Client, which must satisfy the log.Logger interface. The custom +// logger is used only when debug logging is enabled. By default, log.Stdlog is used if no custom logger is provided. +// +// Parameters: +// - logger: A logger that satisfies the log.Logger interface. +// +// Returns: +// - An Option function that sets the custom logger for the Client. func WithLogger(logger log.Logger) Option { return func(c *Client) error { c.logger = logger @@ -299,7 +394,17 @@ func WithLogger(logger log.Logger) Option { } } -// WithHELO tells the client to use the provided string as HELO/EHLO greeting host +// WithHELO sets the HELO/EHLO string used by the Client. +// +// This function configures the HELO/EHLO string sent by the Client when initiating communication +// with the SMTP server. By default, os.Hostname is used to identify the HELO/EHLO string. +// +// Parameters: +// - helo: The string to be used for the HELO/EHLO greeting. Must not be empty. +// +// Returns: +// - An Option function that sets the HELO/EHLO string for the Client. +// - An error if the provided HELO string is empty. func WithHELO(helo string) Option { return func(c *Client) error { if helo == "" { @@ -310,10 +415,19 @@ func WithHELO(helo string) Option { } } -// WithTLSPolicy tells the client to use the provided TLSPolicy +// WithTLSPolicy sets the TLSPolicy of the Client and overrides the DefaultTLSPolicy. // -// Note: To follow best-practices for SMTP TLS connections, it is recommended -// to use WithTLSPortPolicy instead. +// This function configures the Client's TLSPolicy, specifying how the Client handles TLS for SMTP connections. +// It overrides the default policy. For best practices regarding SMTP TLS connections, it is recommended to use +// WithTLSPortPolicy instead. +// +// Parameters: +// - policy: The TLSPolicy to be applied to the Client. +// +// Returns: +// - An Option function that sets the TLSPolicy for the Client. +// +// WithTLSPortPolicy instead. func WithTLSPolicy(policy TLSPolicy) Option { return func(c *Client) error { c.tlspolicy = policy @@ -321,16 +435,20 @@ func WithTLSPolicy(policy TLSPolicy) Option { } } -// WithTLSPortPolicy tells the client to use the provided TLSPolicy, -// The correct port is automatically set. +// WithTLSPortPolicy enables explicit TLS via STARTTLS for the Client using the provided TLSPolicy. The +// correct port is automatically set. // -// Port 587 is used for TLSMandatory and TLSOpportunistic. -// If the connection fails with TLSOpportunistic, -// a plaintext connection is attempted on port 25 as a fallback. -// NoTLS will allways use port 25. +// When TLSMandatory or TLSOpportunistic is provided as the TLSPolicy, port 587 is used for the connection. +// If the connection fails with TLSOpportunistic, the Client attempts to connect on port 25 using an unencrypted +// connection as a fallback. If NoTLS is specified, the Client will always use port 25. +// If WithPort has already been used to set a different port, that port takes precedence, and the automatic fallback +// mechanism is skipped. // -// Note: If a different port has already been set otherwise, the port-choosing -// and fallback automatism will be skipped. +// Parameters: +// - policy: The TLSPolicy to be used for STARTTLS communication. +// +// Returns: +// - An Option function that sets the TLSPortPolicy for the Client. func WithTLSPortPolicy(policy TLSPolicy) Option { return func(c *Client) error { c.SetTLSPortPolicy(policy) @@ -338,7 +456,17 @@ func WithTLSPortPolicy(policy TLSPolicy) Option { } } -// WithTLSConfig tells the client to use the provided *tls.Config +// WithTLSConfig sets the tls.Config for the Client and overrides the default configuration. +// +// This function configures the Client with a custom tls.Config. It overrides the default TLS settings. +// An error is returned if the provided tls.Config is nil or invalid. +// +// Parameters: +// - tlsconfig: A pointer to a tls.Config struct to be used for the Client. Must not be nil. +// +// Returns: +// - An Option function that sets the tls.Config for the Client. +// - An error if the provided tls.Config is invalid. func WithTLSConfig(tlsconfig *tls.Config) Option { return func(c *Client) error { if tlsconfig == nil { @@ -349,7 +477,15 @@ func WithTLSConfig(tlsconfig *tls.Config) Option { } } -// WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication +// WithSMTPAuth configures the Client to use the specified SMTPAuthType for SMTP authentication. +// +// This function sets the Client to use the specified SMTPAuthType for authenticating with the SMTP server. +// +// Parameters: +// - authtype: The SMTPAuthType to be used for SMTP authentication. +// +// Returns: +// - An Option function that configures the Client to use the specified SMTPAuthType. func WithSMTPAuth(authtype SMTPAuthType) Option { return func(c *Client) error { c.smtpAuthType = authtype @@ -357,15 +493,33 @@ func WithSMTPAuth(authtype SMTPAuthType) Option { } } -// WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication +// WithSMTPAuthCustom sets a custom SMTP authentication mechanism for the Client. +// +// This function configures the Client to use a custom SMTP authentication mechanism. The provided +// mechanism must satisfy the smtp.Auth interface. +// +// Parameters: +// - smtpAuth: The custom SMTP authentication mechanism, which must implement the smtp.Auth interface. +// +// Returns: +// - An Option function that sets the custom SMTP authentication for the Client. func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option { return func(c *Client) error { c.smtpAuth = smtpAuth + c.smtpAuthType = SMTPAuthCustom return nil } } -// WithUsername tells the client to use the provided string as username for authentication +// WithUsername sets the username that the Client will use for SMTP authentication. +// +// This function configures the Client with the specified username for SMTP authentication. +// +// Parameters: +// - username: The username to be used for SMTP authentication. +// +// Returns: +// - An Option function that sets the username for the Client. func WithUsername(username string) Option { return func(c *Client) error { c.user = username @@ -373,7 +527,15 @@ func WithUsername(username string) Option { } } -// WithPassword tells the client to use the provided string as password/secret for authentication +// WithPassword sets the password that the Client will use for SMTP authentication. +// +// This function configures the Client with the specified password for SMTP authentication. +// +// Parameters: +// - password: The password to be used for SMTP authentication. +// +// Returns: +// - An Option function that sets the password for the Client. func WithPassword(password string) Option { return func(c *Client) error { c.pass = password @@ -381,23 +543,41 @@ func WithPassword(password string) Option { } } -// WithDSN enables the Client to request DSNs (if the server supports it) -// as described in the RFC 1891 and set defaults for DSNMailReturnOption -// to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess -// and DSNRcptNotifyFailure +// WithDSN enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891. +// +// This function configures the Client to request DSN, which provides status notifications for email delivery. +// DSN is only effective if the SMTP server supports it. By default, DSNMailReturnOption is set to DSNMailReturnFull, +// and DSNRcptNotifyOption is set to DSNRcptNotifySuccess and DSNRcptNotifyFailure. +// +// Returns: +// - An Option function that enables DSN for the Client. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc1891 func WithDSN() Option { return func(c *Client) error { - c.dsn = true - c.dsnmrtype = DSNMailReturnFull - c.dsnrntype = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)} + c.requestDSN = true + c.dsnReturnType = DSNMailReturnFull + c.dsnRcptNotifyType = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)} return nil } } -// WithDSNMailReturnType enables the Client to request DSNs (if the server supports it) -// as described in the RFC 1891 and set the MAIL FROM Return option type to the -// given DSNMailReturnOption -// See: https://www.rfc-editor.org/rfc/rfc1891 +// WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891. +// +// This function configures the Client to request DSN and sets the DSNMailReturnOption to the provided value. +// DSN is only effective if the SMTP server supports it. The provided option must be either DSNMailReturnHeadersOnly +// or DSNMailReturnFull; otherwise, an error is returned. +// +// Parameters: +// - option: The DSNMailReturnOption to be used (DSNMailReturnHeadersOnly or DSNMailReturnFull). +// +// Returns: +// - An Option function that sets the DSNMailReturnOption for the Client. +// - An error if an invalid DSNMailReturnOption is provided. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc1891 func WithDSNMailReturnType(option DSNMailReturnOption) Option { return func(c *Client) error { switch option { @@ -407,15 +587,28 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option { return ErrInvalidDSNMailReturnOption } - c.dsn = true - c.dsnmrtype = option + c.requestDSN = true + c.dsnReturnType = option return nil } } -// WithDSNRcptNotifyType enables the Client to request DSNs as described in the RFC 1891 -// and sets the RCPT TO notify options to the given list of DSNRcptNotifyOption -// See: https://www.rfc-editor.org/rfc/rfc1891 +// WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891. +// +// This function configures the Client to request DSN and sets the DSNRcptNotifyOption to the provided values. +// The provided options must be valid DSNRcptNotifyOption types. If DSNRcptNotifyNever is combined with +// any other notification type (such as DSNRcptNotifySuccess, DSNRcptNotifyFailure, or DSNRcptNotifyDelay), +// an error is returned. +// +// Parameters: +// - opts: A variadic list of DSNRcptNotifyOption values (e.g., DSNRcptNotifySuccess, DSNRcptNotifyFailure). +// +// Returns: +// - An Option function that sets the DSNRcptNotifyOption for the Client. +// - An error if invalid DSNRcptNotifyOption values are provided or incompatible combinations are used. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc1891 func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { return func(c *Client) error { var rcptOpts []string @@ -441,14 +634,19 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { return ErrInvalidDSNRcptNotifyCombination } - c.dsn = true - c.dsnrntype = rcptOpts + c.requestDSN = true + c.dsnRcptNotifyType = rcptOpts return nil } } -// WithoutNoop disables the Client Noop check during connections. This is primarily for servers which delay responses -// to SMTP commands that are not the AUTH command. For example Microsoft Exchange's Tarpit. +// WithoutNoop indicates that the Client should skip the "NOOP" command during the dial. +// +// This option is useful for servers that delay potentially unwanted clients when they perform +// commands other than AUTH, such as Microsoft's Exchange Tarpit. +// +// Returns: +// - An Option function that configures the Client to skip the "NOOP" command. func WithoutNoop() Option { return func(c *Client) error { c.noNoop = true @@ -456,7 +654,16 @@ func WithoutNoop() Option { } } -// WithDialContextFunc overrides the default DialContext for connecting SMTP server +// WithDialContextFunc sets the provided DialContextFunc as the DialContext for connecting to the SMTP server. +// +// This function overrides the default DialContext function used by the Client when establishing a connection +// to the SMTP server with the provided DialContextFunc. +// +// Parameters: +// - dialCtxFunc: The custom DialContextFunc to be used for connecting to the SMTP server. +// +// Returns: +// - An Option function that sets the custom DialContextFunc for the Client. func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { return func(c *Client) error { c.dialContextFunc = dialCtxFunc @@ -464,34 +671,50 @@ func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { } } -// TLSPolicy returns the currently set TLSPolicy as string +// TLSPolicy returns the TLSPolicy that is currently set on the Client as a string. +// +// This method retrieves the current TLSPolicy configured for the Client and returns it as a string representation. +// +// Returns: +// - A string representing the currently set TLSPolicy for the Client. func (c *Client) TLSPolicy() string { return c.tlspolicy.String() } -// ServerAddr returns the currently set combination of hostname and port +// ServerAddr returns the server address that is currently set on the Client in the format "host:port". +// +// This method constructs and returns the server address using the host and port currently configured +// for the Client. +// +// Returns: +// - A string representing the server address in the format "host:port". func (c *Client) ServerAddr() string { return fmt.Sprintf("%s:%d", c.host, c.port) } -// SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value +// SetTLSPolicy sets or overrides the TLSPolicy currently configured on the Client with the given TLSPolicy. // -// Note: To follow best-practices for SMTP TLS connections, it is recommended -// to use SetTLSPortPolicy instead. +// This method allows the user to set a new TLSPolicy for the Client. For best practices regarding +// SMTP TLS connections, it is recommended to use SetTLSPortPolicy instead. +// +// Parameters: +// - policy: The TLSPolicy to be set for the Client. func (c *Client) SetTLSPolicy(policy TLSPolicy) { c.tlspolicy = policy } -// SetTLSPortPolicy overrides the current TLSPolicy with the given TLSPolicy -// value. The correct port is automatically set. +// SetTLSPortPolicy sets or overrides the TLSPolicy currently configured on the Client with the given TLSPolicy. +// The correct port is automatically set based on the specified policy. // -// Port 587 is used for TLSMandatory and TLSOpportunistic. -// If the connection fails with TLSOpportunistic, a plaintext connection is -// attempted on port 25 as a fallback. -// NoTLS will allways use port 25. +// If TLSMandatory or TLSOpportunistic is provided as the TLSPolicy, port 587 will be used for the connection. +// If the connection fails with TLSOpportunistic, the Client will attempt to connect on port 25 using +// an unencrypted connection as a fallback. If NoTLS is provided, the Client will always use port 25. // -// Note: If a different port has already been set otherwise, the port-choosing -// and fallback automatism will be skipped. +// Note: If a different port has already been set using WithPort, that port takes precedence and is used +// to establish the SSL/TLS connection, skipping the automatic fallback mechanism. +// +// Parameters: +// - policy: The TLSPolicy to be set for the Client. func (c *Client) SetTLSPortPolicy(policy TLSPolicy) { if c.port == DefaultPort { c.port = DefaultPortTLS @@ -507,21 +730,29 @@ func (c *Client) SetTLSPortPolicy(policy TLSPolicy) { c.tlspolicy = policy } -// SetSSL tells the Client wether to use SSL or not +// SetSSL sets or overrides whether the Client should use implicit SSL/TLS. +// +// This method configures the Client to either enable or disable implicit SSL/TLS for secure communication. +// +// Parameters: +// - ssl: A boolean value indicating whether to enable (true) or disable (false) implicit SSL/TLS. func (c *Client) SetSSL(ssl bool) { c.useSSL = ssl } -// SetSSLPort tells the Client wether or not to use SSL and fallback. +// SetSSLPort sets or overrides whether the Client should use implicit SSL/TLS with optional fallback. // The correct port is automatically set. // -// Port 465 is used when SSL set (true). -// Port 25 is used when SSL is unset (false). -// When the SSL connection fails and fb is set to true, -// the client will attempt to connect on port 25 using plaintext. +// If ssl is set to true, the default port 25 will be overridden with port 465. If fallback is set to true +// and the SSL/TLS connection fails, the Client will attempt to connect on port 25 using an unencrypted +// connection. // -// Note: If a different port has already been set otherwise, the port-choosing -// and fallback automatism will be skipped. +// Note: If a different port has already been set using WithPort, that port takes precedence and is used +// to establish the SSL/TLS connection, skipping the automatic fallback mechanism. +// +// Parameters: +// - ssl: A boolean value indicating whether to enable implicit SSL/TLS. +// - fallback: A boolean value indicating whether to enable fallback to an unencrypted connection. func (c *Client) SetSSLPort(ssl bool, fallback bool) { if c.port == DefaultPort { if ssl { @@ -537,7 +768,15 @@ func (c *Client) SetSSLPort(ssl bool, fallback bool) { c.useSSL = ssl } -// SetDebugLog tells the Client whether debug logging is enabled or not +// SetDebugLog sets or overrides whether the Client is using debug logging. The debug logger will log incoming +// and outgoing communication between the Client and the server to os.Stderr. +// +// Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using +// SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use +// debug logging with caution. +// +// Parameters: +// - val: A boolean value indicating whether to enable (true) or disable (false) debug logging. func (c *Client) SetDebugLog(val bool) { c.useDebugLog = val if c.smtpClient != nil { @@ -545,7 +784,14 @@ func (c *Client) SetDebugLog(val bool) { } } -// SetLogger tells the Client which log.Logger to use +// SetLogger sets or overrides the custom logger currently used by the Client. The logger must +// satisfy the log.Logger interface and is only utilized when debug logging is enabled on the +// Client. +// +// By default, log.Stdlog is used if no custom logger is provided. +// +// Parameters: +// - logger: A logger that satisfies the log.Logger interface to be set for the Client. func (c *Client) SetLogger(logger log.Logger) { c.logger = logger if c.smtpClient != nil { @@ -553,11 +799,18 @@ func (c *Client) SetLogger(logger log.Logger) { } } -// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value +// SetTLSConfig sets or overrides the tls.Config currently configured for the Client with the +// given value. An error is returned if the provided tls.Config is invalid. +// +// This method ensures that the provided tls.Config is not nil before updating the Client's +// TLS configuration. +// +// Parameters: +// - tlsconfig: A pointer to the tls.Config struct to be set for the Client. Must not be nil. +// +// Returns: +// - An error if the provided tls.Config is invalid or nil. func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { - c.mutex.Lock() - defer c.mutex.Unlock() - if tlsconfig == nil { return ErrInvalidTLSConfig } @@ -565,38 +818,68 @@ func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { return nil } -// SetUsername overrides the current username string with the given value +// SetUsername sets or overrides the username that the Client will use for SMTP authentication. +// +// This method updates the username used by the Client for authenticating with the SMTP server. +// +// Parameters: +// - username: The username to be set for SMTP authentication. func (c *Client) SetUsername(username string) { c.user = username } -// SetPassword overrides the current password string with the given value +// SetPassword sets or overrides the password that the Client will use for SMTP authentication. +// +// This method updates the password used by the Client for authenticating with the SMTP server. +// +// Parameters: +// - password: The password to be set for SMTP authentication. func (c *Client) SetPassword(password string) { c.pass = password } -// SetSMTPAuth overrides the current SMTP AUTH type setting with the given value +// SetSMTPAuth sets or overrides the SMTPAuthType currently configured on the Client for SMTP +// authentication. +// +// This method updates the authentication type used by the Client for authenticating with the +// SMTP server and resets any custom SMTP authentication mechanism. +// +// Parameters: +// - authtype: The SMTPAuthType to be set for the Client. func (c *Client) SetSMTPAuth(authtype SMTPAuthType) { c.smtpAuthType = authtype c.smtpAuth = nil } -// SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth +// SetSMTPAuthCustom sets or overrides the custom SMTP authentication mechanism currently +// configured for the Client. The provided authentication mechanism must satisfy the +// smtp.Auth interface. +// +// This method updates the authentication mechanism used by the Client for authenticating +// with the SMTP server and sets the authentication type to SMTPAuthCustom. +// +// Parameters: +// - smtpAuth: The custom SMTP authentication mechanism to be set for the Client. func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) { c.smtpAuth = smtpAuth + c.smtpAuthType = SMTPAuthCustom } -// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname -func (c *Client) setDefaultHelo() error { - hostname, err := os.Hostname() - if err != nil { - return fmt.Errorf("failed to read local hostname: %w", err) - } - c.helo = hostname - return nil -} - -// DialWithContext establishes a connection to the SMTP server with a given context.Context +// DialWithContext establishes a connection to the server using the provided context.Context. +// +// This function adds a deadline based on the Client's timeout to the provided context.Context +// before connecting to the server. After dialing the defined DialContextFunc and successfully +// establishing the connection, it sends the HELO/EHLO SMTP command, followed by optional +// STARTTLS and SMTP AUTH commands. If debug logging is enabled, it attaches the log.Logger. +// +// After this method is called, the Client will have an active (cancelable) connection to the +// SMTP server. +// +// Parameters: +// - dialCtx: The context.Context used to control the connection timeout and cancellation. +// +// Returns: +// - An error if the connection to the SMTP server fails or any subsequent command fails. func (c *Client) DialWithContext(dialCtx context.Context) error { c.mutex.Lock() defer c.mutex.Unlock() @@ -653,10 +936,18 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { return nil } -// Close closes the Client connection +// Close terminates the connection to the SMTP server, returning an error if the disconnection +// fails. If the connection is already closed, this method is a no-op and disregards any error. +// +// This function checks if the Client's SMTP connection is active. If not, it simply returns +// without any action. If the connection is active, it attempts to gracefully close the +// connection using the Quit method. +// +// Returns: +// - An error if the disconnection fails; otherwise, returns nil. func (c *Client) Close() error { - if err := c.checkConn(); err != nil { - return err + if !c.smtpClient.HasConnection() { + return nil } if err := c.smtpClient.Quit(); err != nil { return fmt.Errorf("failed to close SMTP client: %w", err) @@ -665,7 +956,14 @@ func (c *Client) Close() error { return nil } -// Reset sends the RSET command to the SMTP client +// Reset sends an SMTP RSET command to reset the state of the current SMTP session. +// +// This method checks the connection to the SMTP server and, if the connection is valid, +// it sends an RSET command to reset the session state. If the connection is invalid or +// the command fails, an error is returned. +// +// Returns: +// - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil. func (c *Client) Reset() error { if err := c.checkConn(); err != nil { return err @@ -677,19 +975,46 @@ func (c *Client) Reset() error { return nil } -// DialAndSend establishes a connection to the SMTP server with a -// default context.Background and sends the mail +// DialAndSend establishes a connection to the server and sends out the provided Msg. +// It calls DialAndSendWithContext with an empty Context.Background. +// +// This method simplifies the process of connecting to the SMTP server and sending messages +// by using a default context. It prepares the messages for sending and ensures the connection +// is established before attempting to send them. +// +// Parameters: +// - messages: A variadic list of pointers to Msg objects to be sent. +// +// Returns: +// - An error if the connection fails or if sending the messages fails; otherwise, returns nil. func (c *Client) DialAndSend(messages ...*Msg) error { ctx := context.Background() return c.DialAndSendWithContext(ctx, messages...) } -// DialAndSendWithContext establishes a connection to the SMTP server with a -// custom context and sends the mail +// DialAndSendWithContext establishes a connection to the SMTP server using DialWithContext +// with the provided context.Context, then sends out the given Msg. After successful delivery, +// the Client will close the connection to the server. +// +// This method first attempts to connect to the SMTP server using the provided context. +// Upon successful connection, it sends the specified messages and ensures that the connection +// is closed after the operation, regardless of success or failure in sending the messages. +// +// Parameters: +// - ctx: The context.Context to control the connection timeout and cancellation. +// - messages: A variadic list of pointers to Msg objects to be sent. +// +// Returns: +// - An error if the connection fails, if sending the messages fails, or if closing the +// connection fails; otherwise, returns nil. func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error { if err := c.DialWithContext(ctx); err != nil { return fmt.Errorf("dial failed: %w", err) } + defer func() { + _ = c.Close() + }() + if err := c.Send(messages...); err != nil { return fmt.Errorf("send failed: %w", err) } @@ -699,77 +1024,23 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e return nil } -// checkConn makes sure that a required server connection is available and extends the -// connection deadline -func (c *Client) checkConn() error { - if !c.smtpClient.HasConnection() { - return ErrNoActiveConnection - } - - if !c.noNoop { - if err := c.smtpClient.Noop(); err != nil { - return ErrNoActiveConnection - } - } - - if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil { - return ErrDeadlineExtendFailed - } - return nil -} - -// serverFallbackAddr returns the currently set combination of hostname -// and fallback port. -func (c *Client) serverFallbackAddr() string { - return fmt.Sprintf("%s:%d", c.host, c.fallbackPort) -} - -// tls tries to make sure that the STARTTLS requirements are satisfied -func (c *Client) tls() error { - if !c.smtpClient.HasConnection() { - return ErrNoActiveConnection - } - if !c.useSSL && c.tlspolicy != NoTLS { - hasStartTLS := false - extension, _ := c.smtpClient.Extension("STARTTLS") - if c.tlspolicy == TLSMandatory { - hasStartTLS = true - if !extension { - return fmt.Errorf("STARTTLS mode set to: %q, but target host does not support STARTTLS", - c.tlspolicy) - } - } - if c.tlspolicy == TLSOpportunistic { - if extension { - hasStartTLS = true - } - } - if hasStartTLS { - if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil { - return err - } - } - tlsConnState, err := c.smtpClient.GetTLSConnectionState() - if err != nil { - switch { - case errors.Is(err, smtp.ErrNonTLSConnection): - c.isEncrypted = false - return nil - default: - return fmt.Errorf("failed to get TLS connection state: %w", err) - } - } - c.isEncrypted = tlsConnState.HandshakeComplete - } - return nil -} - -// auth will try to perform SMTP AUTH if requested +// auth attempts to authenticate the client using SMTP AUTH mechanisms. It checks the connection, +// determines the supported authentication methods, and applies the appropriate authentication +// type. An error is returned if authentication fails. +// +// This method first verifies the connection to the SMTP server. If no custom authentication +// mechanism is provided, it checks which authentication methods are supported by the server. +// Based on the configured SMTPAuthType, it sets up the appropriate authentication mechanism. +// Finally, it attempts to authenticate the client using the selected method. +// +// Returns: +// - An error if the connection check fails, if no supported authentication method is found, +// or if the authentication process fails. func (c *Client) auth() error { if err := c.checkConn(); err != nil { return fmt.Errorf("failed to authenticate: %w", err) } - if c.smtpAuth == nil && c.smtpAuthType != "" { + if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthCustom { hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH") if !hasSMTPAuth { return fmt.Errorf("server does not support SMTP AUTH") @@ -837,8 +1108,21 @@ func (c *Client) auth() error { return nil } -// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails. -// It is invoked by the public Send methods +// sendSingleMsg sends out a single message and returns an error if the transmission or +// delivery fails. It is invoked by the public Send methods. +// +// This method handles the process of sending a single email message through the SMTP +// client. It performs several checks and operations, including verifying the encoding, +// retrieving the sender and recipient addresses, and managing delivery status notifications +// (DSN). It attempts to send the message and handles any errors that occur during the +// transmission process, ensuring that any necessary cleanup is performed (such as resetting +// the SMTP client if an error occurs). +// +// Parameters: +// - message: A pointer to the Msg object representing the email message to be sent. +// +// Returns: +// - An error if any part of the sending process fails; otherwise, returns nil. func (c *Client) sendSingleMsg(message *Msg) error { c.mutex.Lock() defer c.mutex.Unlock() @@ -863,9 +1147,9 @@ func (c *Client) sendSingleMsg(message *Msg) error { } } - if c.dsn { - if c.dsnmrtype != "" { - c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) + if c.requestDSN { + if c.dsnReturnType != "" { + c.smtpClient.SetDSNMailReturnOption(string(c.dsnReturnType)) } } if err = c.smtpClient.Mail(from); err != nil { @@ -882,7 +1166,7 @@ func (c *Client) sendSingleMsg(message *Msg) error { rcptSendErr := &SendError{affectedMsg: message} rcptSendErr.errlist = make([]error, 0) rcptSendErr.rcpt = make([]string, 0) - rcptNotifyOpt := strings.Join(c.dsnrntype, ",") + rcptNotifyOpt := strings.Join(c.dsnRcptNotifyType, ",") c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) for _, rcpt := range rcpts { if err = c.smtpClient.Rcpt(rcpt); err != nil { @@ -936,3 +1220,112 @@ func (c *Client) sendSingleMsg(message *Msg) error { } return nil } + +// checkConn ensures that a required server connection is available and extends the connection +// deadline. +// +// This method verifies whether there is an active connection to the SMTP server. If there is no +// connection, it returns an error. If the "noNoop" flag is not set, it sends a NOOP command to +// the server to confirm the connection is still valid. Finally, it updates the connection +// deadline based on the specified timeout value. If any operation fails, the appropriate error +// is returned. +// +// Returns: +// - An error if there is no active connection, if the NOOP command fails, or if extending +// the deadline fails; otherwise, returns nil. +func (c *Client) checkConn() error { + if !c.smtpClient.HasConnection() { + return ErrNoActiveConnection + } + + if !c.noNoop { + if err := c.smtpClient.Noop(); err != nil { + return ErrNoActiveConnection + } + } + + if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil { + return ErrDeadlineExtendFailed + } + return nil +} + +// serverFallbackAddr returns the currently set combination of hostname and fallback port. +// +// This method constructs and returns the server address using the host and fallback port +// currently configured for the Client. It is useful for establishing a connection when +// the primary port is unavailable. +// +// Returns: +// - A string representing the server address in the format "host:fallbackPort". +func (c *Client) serverFallbackAddr() string { + return fmt.Sprintf("%s:%d", c.host, c.fallbackPort) +} + +// setDefaultHelo sets the HELO/EHLO hostname to the local machine's hostname. +// +// This method retrieves the local hostname using the operating system's hostname function +// and sets it as the HELO/EHLO string for the Client. If retrieving the hostname fails, +// an error is returned. +// +// Returns: +// - An error if there is a failure in reading the local hostname; otherwise, returns nil. +func (c *Client) setDefaultHelo() error { + hostname, err := os.Hostname() + if err != nil { + return fmt.Errorf("failed to read local hostname: %w", err) + } + c.helo = hostname + return nil +} + +// tls establishes a TLS connection based on the client's TLS policy and configuration. +// Returns an error if no active connection exists or if a TLS error occurs. +// +// This method first checks if there is an active connection to the SMTP server. If SSL is not +// being used and the TLS policy is not set to NoTLS, it checks for STARTTLS support. Depending +// on the TLS policy (mandatory or opportunistic), it may initiate a TLS connection using the +// StartTLS method. The method also retrieves the TLS connection state to determine if the +// connection is encrypted and returns any errors encountered during these processes. +// +// Returns: +// - An error if there is no active connection, if STARTTLS is required but not supported, +// or if there are issues during the TLS handshake; otherwise, returns nil. +func (c *Client) tls() error { + if !c.smtpClient.HasConnection() { + return ErrNoActiveConnection + } + if !c.useSSL && c.tlspolicy != NoTLS { + hasStartTLS := false + extension, _ := c.smtpClient.Extension("STARTTLS") + if c.tlspolicy == TLSMandatory { + hasStartTLS = true + if !extension { + return fmt.Errorf("STARTTLS mode set to: %q, but target host does not support STARTTLS", + c.tlspolicy) + } + } + if c.tlspolicy == TLSOpportunistic { + if extension { + hasStartTLS = true + } + } + if hasStartTLS { + if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil { + return err + } + } + tlsConnState, err := c.smtpClient.GetTLSConnectionState() + if err != nil { + switch { + case errors.Is(err, smtp.ErrNonTLSConnection): + c.isEncrypted = false + return nil + default: + return fmt.Errorf("failed to get TLS connection state: %w", err) + } + } + c.isEncrypted = tlsConnState.HandshakeComplete + } + return nil +} diff --git a/client_119.go b/client_119.go index 7de5d59..093967e 100644 --- a/client_119.go +++ b/client_119.go @@ -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)} diff --git a/client_120.go b/client_120.go index 4f82aa7..012a4f7 100644 --- a/client_120.go +++ b/client_120.go @@ -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)} diff --git a/client_test.go b/client_test.go index b8d77ed..c767e75 100644 --- a/client_test.go +++ b/client_test.go @@ -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]) } } diff --git a/doc.go b/doc.go index 831a57c..de16f5d 100644 --- a/doc.go +++ b/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 +// VERSION indicates the current version of the package. It is also attached to the default user +// agent string. const VERSION = "0.4.4" diff --git a/eml.go b/eml.go index 7e705f6..35cd90d 100644 --- a/eml.go +++ b/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" diff --git a/encoding.go b/encoding.go index 47213da..9ed5666 100644 --- a/encoding.go +++ b/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) } diff --git a/file.go b/file.go index 45e142a..8866d97 100644 --- a/file.go +++ b/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 != "" diff --git a/go.mod b/go.mod index 1dcef3a..4fb64a8 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 78b6dba..8e6bffc 100644 --- a/go.sum +++ b/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= diff --git a/header.go b/header.go index 9191b7e..217823a 100644 --- a/header.go +++ b/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) } diff --git a/msg.go b/msg.go index b66bffe..fc0bb57 100644 --- a/msg.go +++ b/msg.go @@ -24,111 +24,150 @@ import ( ) var ( - // ErrNoFromAddress should be used when a FROM address is requrested but not set + // ErrNoFromAddress indicates that the FROM address is not set, which is required. ErrNoFromAddress = errors.New("no FROM address set") - // ErrNoRcptAddresses should be used when the list of RCPTs is empty + // ErrNoRcptAddresses indicates that no recipient addresses have been set. ErrNoRcptAddresses = errors.New("no recipient addresses set") ) const ( - // errTplExecuteFailed is issued when the template execution was not successful + // errTplExecuteFailed indicates that the execution of a template has failed, including the underlying error. errTplExecuteFailed = "failed to execute template: %w" - // errTplPointerNil is issued when a template pointer is expected but it is nil + // errTplPointerNil indicates that a template pointer is nil, which prevents further template execution or + // processing. errTplPointerNil = "template pointer is nil" - // errParseMailAddr is used when a mail address could not be validated + // errParseMailAddr indicates that parsing of a mail address has failed, including the problematic address + // and error. errParseMailAddr = "failed to parse mail address %q: %w" ) const ( - // NoPGP indicates that a message should not be treated as PGP encrypted - // or signed and is the default value for a message + // NoPGP indicates that a message should not be treated as PGP encrypted or signed and is the default value + // for a message NoPGP PGPType = iota - // PGPEncrypt indicates that a message should be treated as PGP encrypted - // This works closely together with the corresponding go-mail-middleware + + // PGPEncrypt indicates that a message should be treated as PGP encrypted. This works closely together with + // the corresponding go-mail-middleware. PGPEncrypt - // PGPSignature indicates that a message should be treated as PGP signed - // This works closely together with the corresponding go-mail-middleware + + // PGPSignature indicates that a message should be treated as PGP signed. This works closely together with + // the corresponding go-mail-middleware. PGPSignature ) -// MiddlewareType is the type description of the Middleware and needs to be returned -// in the Middleware interface by the Type method +// MiddlewareType is a type wrapper for a string. It describes the type of the Middleware and needs to be +// returned by the Middleware.Type method to satisfy the Middleware interface. type MiddlewareType string -// Middleware is an interface to define a function to apply to Msg before sending +// Middleware represents the interface for modifying or handling email messages. A Middleware allows the user to +// alter a Msg before it is finally processed. Multiple Middleware can be applied to a Msg. +// +// Type returns a unique MiddlewareType. It describes the type of Middleware and makes sure that +// a Middleware is only applied once. +// Handle performs all the processing to the Msg. It always needs to return a Msg back. type Middleware interface { Handle(*Msg) *Msg Type() MiddlewareType } -// PGPType is a type alias for a int representing a type of PGP encryption -// or signature +// PGPType is a type wrapper for an int, representing a type of PGP encryption or signature. type PGPType int -// Msg is the mail message struct +// Msg represents an email message with various headers, attachments, and encoding settings. +// +// The Msg is the central part of go-mail. It provided a lot of methods that you would expect in a mail +// user agent (MUA). Msg satisfies the io.WriterTo and io.Reader interfaces. type Msg struct { - // addrHeader is a slice of strings that the different mail AddrHeader fields + // addrHeader holds a mapping between AddrHeader keys and their corresponding slices of mail.Address pointers. addrHeader map[AddrHeader][]*mail.Address - // attachments represent the different attachment File of the Msg + // attachments holds a list of File pointers that represent files either as attachments or embeds files in + // a Msg. attachments []*File - // boundary is the MIME content boundary + // boundary represents the delimiter for separating parts in a multipart message. boundary string - // charset represents the charset of the mail (defaults to UTF-8) + // charset represents the Charset of the Msg. + // + // By default we set CharsetUTF8 for a Msg unless overridden by a corresponding MsgOption. charset Charset - // embeds represent the different embedded File of the Msg + // embeds contains a slice of File pointers representing the embedded files in a Msg. embeds []*File - // encoder represents a mime.WordEncoder from the std lib + // encoder is a mime.WordEncoder used to encode strings (such as email headers) using a specified + // Encoding. encoder mime.WordEncoder - // encoding represents the message encoding (the encoder will be a corresponding WordEncoder) + // encoding specifies the type of Encoding used for email messages and/or parts. encoding Encoding - // genHeader is a slice of strings that the different generic mail Header fields + // genHeader is a map where the keys are email headers (of type Header) and the values are slices of strings + // representing header values. genHeader map[Header][]string - // isDelivered signals if a message has been delivered or not + // isDelivered indicates wether the Msg has been delivered. isDelivered bool - // middlewares is the list of middlewares to apply to the Msg before sending in FIFO order + // middlewares is a slice of Middleware used for modifying or handling messages before they are processed. + // + // middlewares are processed in FIFO order. middlewares []Middleware - // mimever represents the MIME version + // mimever represents the MIME version used in a Msg. mimever MIMEVersion - // parts represent the different parts of the Msg + // parts is a slice that holds pointers to Part structures, which represent different parts of a Msg. parts []*Part - // preformHeader is a slice of strings that the different generic mail Header fields - // of which content is already preformated and will not be affected by the automatic line - // breaks + // preformHeader maps Header types to their already preformatted string values. + // + // Preformatted Header values will not be affected by automatic line breaks. preformHeader map[Header]string // pgptype indicates that a message has a PGPType assigned and therefore will generate - // different Content-Type settings in the msgWriter + // different Content-Type settings in the msgWriter. pgptype PGPType - // sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation + // sendError represents an error encountered during the process of sending a Msg during the + // Client.Send operation. + // + // sendError will hold an error of type SendError. sendError error - // noDefaultUserAgent indicates whether the default User Agent will be excluded for the Msg when it's sent. + // noDefaultUserAgent indicates whether the default User-Agent will be omitted for the Msg when it is + // being sent. + // + // This can be useful in scenarios where headers are conditionally passed based on receipt - i. e. SMTP proxies. noDefaultUserAgent bool } -// SendmailPath is the default system path to the sendmail binary +// SendmailPath is the default system path to the sendmail binary - at least on standard Unix-like OS. const SendmailPath = "/usr/sbin/sendmail" -// MsgOption returns a function that can be used for grouping Msg options +// MsgOption is a function type that modifies a Msg instance during its creation or initialization. type MsgOption func(*Msg) -// NewMsg returns a new Msg pointer +// NewMsg creates a new email message with optional MsgOption functions that customize various aspects +// of the message. +// +// This function initializes a new Msg instance with default values for address headers, character set, +// encoding, general headers, and MIME version. It then applies any provided MsgOption functions to +// customize the message according to the user's needs. If an option is nil, it will be ignored. +// After applying the options, the function sets the appropriate MIME WordEncoder for the message. +// +// Parameters: +// - opts: A variadic list of MsgOption functions that can be used to customize the Msg instance. +// +// Returns: +// - A pointer to the newly created Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5321 func NewMsg(opts ...MsgOption) *Msg { msg := &Msg{ addrHeader: make(map[AddrHeader][]*mail.Address), @@ -139,7 +178,7 @@ func NewMsg(opts ...MsgOption) *Msg { mimever: MIME10, } - // Override defaults with optionally provided MsgOption functions + // Override defaults with optionally provided MsgOption functions. for _, option := range opts { if option == nil { continue @@ -153,101 +192,292 @@ func NewMsg(opts ...MsgOption) *Msg { return msg } -// WithCharset overrides the default message charset -func WithCharset(c Charset) MsgOption { +// WithCharset sets the Charset type for a Msg during its creation or initialization. +// +// This MsgOption function allows you to specify the character set to be used in the email message. +// The charset defines how the text in the message is encoded and interpreted by the email client. +// This option should be called when creating a new Msg instance to ensure that the desired charset +// is set correctly. +// +// Parameters: +// - charset: The Charset value that specifies the desired character set for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047#section-5 +func WithCharset(charset Charset) MsgOption { return func(m *Msg) { - m.charset = c + m.charset = charset } } -// WithEncoding overrides the default message encoding -func WithEncoding(e Encoding) MsgOption { +// WithEncoding sets the Encoding type for a Msg during its creation or initialization. +// +// This MsgOption function allows you to specify the encoding type to be used in the email message. +// The encoding defines how the message content is encoded, which affects how it is transmitted +// and decoded by email clients. This option should be called when creating a new Msg instance to +// ensure that the desired encoding is set correctly. +// +// Parameters: +// - encoding: The Encoding value that specifies the desired encoding type for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047#section-6 +func WithEncoding(encoding Encoding) MsgOption { return func(m *Msg) { - m.encoding = e + m.encoding = encoding } } -// WithMIMEVersion overrides the default MIME version -func WithMIMEVersion(mv MIMEVersion) MsgOption { +// WithMIMEVersion sets the MIMEVersion type for a Msg during its creation or initialization. +// +// Note that in the context of email, MIME Version 1.0 is the only officially standardized and +// supported version. While MIME has been updated and extended over time via various RFCs, these +// updates and extensions do not introduce new MIME versions; they refine or add features within +// the framework of MIME 1.0. Therefore, there should be no reason to ever use this MsgOption. +// +// Parameters: +// - version: The MIMEVersion value that specifies the desired MIME version for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc1521 +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2049 +func WithMIMEVersion(version MIMEVersion) MsgOption { return func(m *Msg) { - m.mimever = mv + m.mimever = version } } -// WithBoundary overrides the default MIME boundary -func WithBoundary(b string) MsgOption { +// WithBoundary sets the boundary of a Msg to the provided string value during its creation or +// initialization. +// +// Note that by default, random MIME boundaries are created. This option should only be used if +// a specific boundary is required for the email message. Using a predefined boundary can be +// helpful when constructing multipart messages with specific formatting or content separation. +// +// Parameters: +// - boundary: The string value that specifies the desired boundary for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +func WithBoundary(boundary string) MsgOption { return func(m *Msg) { - m.boundary = b + m.boundary = boundary } } -// WithMiddleware add the given middleware in the end of the list of the client middlewares -func WithMiddleware(mw Middleware) MsgOption { +// WithMiddleware adds the given Middleware to the end of the list of the Client middlewares slice. +// Middleware are processed in FIFO order. +// +// This MsgOption function allows you to specify custom middleware that will be applied during the +// message handling process. Middleware can be used to modify the message, perform logging, or +// implement additional functionality as the message flows through the system. Each middleware +// is executed in the order it was added. +// +// Parameters: +// - middleware: The Middleware to be added to the list for processing. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +func WithMiddleware(middleware Middleware) MsgOption { return func(m *Msg) { - m.middlewares = append(m.middlewares, mw) + m.middlewares = append(m.middlewares, middleware) } } -// WithPGPType overrides the default PGPType of the message -func WithPGPType(pt PGPType) MsgOption { +// WithPGPType sets the PGP type for the Msg during its creation or initialization, determining +// the encryption or signature method. +// +// This MsgOption function allows you to specify the PGP (Pretty Good Privacy) type to be used +// for securing the message. The chosen PGP type influences how the message is encrypted or +// signed, ensuring confidentiality and integrity of the content. This option should be called +// when creating a new Msg instance to set the desired PGP type appropriately. +// +// Parameters: +// - pgptype: The PGPType value that specifies the desired PGP type for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc4880 +func WithPGPType(pgptype PGPType) MsgOption { return func(m *Msg) { - m.pgptype = pt + m.pgptype = pgptype } } -// WithNoDefaultUserAgent configures the Msg to not use the default User Agent +// WithNoDefaultUserAgent disables the inclusion of a default User-Agent header in the Msg during +// its creation or initialization. +// +// This MsgOption function allows you to customize the Msg instance by omitting the default +// User-Agent header, which is typically included to provide information about the software +// sending the email. This option can be useful when you want to have more control over the +// headers included in the message, such as when sending from a custom application or for +// privacy reasons. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. func WithNoDefaultUserAgent() MsgOption { return func(m *Msg) { m.noDefaultUserAgent = true } } -// SetCharset sets the encoding charset of the Msg -func (m *Msg) SetCharset(c Charset) { - m.charset = c +// SetCharset sets or overrides the currently set encoding charset of the Msg. +// +// This method allows you to specify a character set for the email message. The charset is +// important for ensuring that the content of the message is correctly interpreted by +// mail clients. Common charset values include UTF-8, ISO-8859-1, and others. If a charset +// is not explicitly set, CharsetUTF8 is used as default. +// +// Parameters: +// - charset: The Charset value to set for the Msg, determining the encoding used for the message content. +func (m *Msg) SetCharset(charset Charset) { + m.charset = charset } -// SetEncoding sets the encoding of the Msg -func (m *Msg) SetEncoding(e Encoding) { - m.encoding = e +// SetEncoding sets or overrides the currently set Encoding of the Msg. +// +// This method allows you to specify the encoding type for the email message. The encoding +// determines how the message content is represented and can affect the size and compatibility +// of the email. Common encoding types include Base64 and Quoted-Printable. Setting a new +// encoding may also adjust how the message content is processed and transmitted. +// +// Parameters: +// - encoding: The Encoding value to set for the Msg, determining the method used to encode the +// message content. +func (m *Msg) SetEncoding(encoding Encoding) { + m.encoding = encoding m.setEncoder() } -// SetBoundary sets the boundary of the Msg -func (m *Msg) SetBoundary(b string) { - m.boundary = b +// SetBoundary sets or overrides the currently set boundary of the Msg. +// +// This method allows you to specify a custom boundary string for the MIME message. The +// boundary is used to separate different parts of the message, especially when dealing +// with multipart messages. By default, the Msg generates random MIME boundaries. This +// function should only be used if you have a specific boundary requirement for the +// message. Ensure that the boundary value does not conflict with any content within the +// message to avoid parsing errors. +// +// Parameters: +// - boundary: The string value representing the boundary to set for the Msg, used in +// multipart messages to delimit different sections. +func (m *Msg) SetBoundary(boundary string) { + m.boundary = boundary } -// SetMIMEVersion sets the MIME version of the Msg -func (m *Msg) SetMIMEVersion(mv MIMEVersion) { - m.mimever = mv +// SetMIMEVersion sets or overrides the currently set MIME version of the Msg. +// +// In the context of email, MIME Version 1.0 is the only officially standardized and +// supported version. Although MIME has been updated and extended over time through +// various RFCs, these updates do not introduce new MIME versions; they refine or add +// features within the framework of MIME 1.0. Therefore, there is generally no need to +// use this function to set a different MIME version. +// +// Parameters: +// - version: The MIMEVersion value to set for the Msg, which determines the MIME +// version used in the email message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc1521 +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2049 +func (m *Msg) SetMIMEVersion(version MIMEVersion) { + m.mimever = version } -// SetPGPType sets the PGPType of the Msg -func (m *Msg) SetPGPType(t PGPType) { - m.pgptype = t +// SetPGPType sets or overrides the currently set PGP type for the Msg, determining the +// encryption or signature method. +// +// This method allows you to specify the PGP type that will be used when encrypting or +// signing the message. Different PGP types correspond to various encryption and signing +// algorithms, and selecting the appropriate type is essential for ensuring the security +// and integrity of the message content. +// +// Parameters: +// - pgptype: The PGPType value to set for the Msg, which determines the encryption +// or signature method used for the email message. +func (m *Msg) SetPGPType(pgptype PGPType) { + m.pgptype = pgptype } -// Encoding returns the currently set encoding of the Msg +// Encoding returns the currently set Encoding of the Msg as a string. +// +// This method retrieves the encoding type that is currently applied to the message. The +// encoding type determines how the message content is encoded for transmission. Common +// encoding types include quoted-printable and base64, and the returned string will reflect +// the specific encoding method in use. +// +// Returns: +// - A string representation of the current Encoding of the Msg. func (m *Msg) Encoding() string { return m.encoding.String() } -// Charset returns the currently set charset of the Msg +// Charset returns the currently set Charset of the Msg as a string. +// +// This method retrieves the character set that is currently applied to the message. The +// charset defines the encoding for the text content of the message, ensuring that +// characters are displayed correctly across different email clients and platforms. The +// returned string will reflect the specific charset in use, such as UTF-8 or ISO-8859-1. +// +// Returns: +// - A string representation of the current Charset of the Msg. func (m *Msg) Charset() string { return m.charset.String() } -// SetHeader sets a generic header field of the Msg -// For adding address headers like "To:" or "From", see SetAddrHeader +// SetHeader sets a generic header field of the Msg. // -// Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead +// Deprecated: This method only exists for compatibility reasons. Please use SetGenHeader +// instead. For adding address headers like "To:" or "From", use SetAddrHeader instead. +// +// This method allows you to set a header field for the message, providing the header name +// and its corresponding values. However, it is recommended to utilize the newer methods +// for better clarity and functionality. Using SetGenHeader or SetAddrHeader is preferred +// for more specific header types, ensuring proper handling of the message headers. +// +// Parameters: +// - header: The header field to set in the Msg. +// - values: One or more string values to associate with the header field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3 +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) SetHeader(header Header, values ...string) { m.SetGenHeader(header, values...) } -// SetGenHeader sets a generic header field of the Msg -// For adding address headers like "To:" or "From", see SetAddrHeader +// SetGenHeader sets a generic header field of the Msg to the provided list of values. +// +// This method is intended for setting generic headers in the email message. It takes a +// header name and a variadic list of string values, encoding them as necessary before +// storing them in the message's internal header map. +// +// Note: For adding email address-related headers (like "To:", "From", "Cc", etc.), +// use SetAddrHeader instead to ensure proper formatting and validation. +// +// Parameters: +// - header: The header field to set in the Msg. +// - values: One or more string values to associate with the header field. +// +// This method ensures that all values are appropriately encoded for email transmission, +// adhering to the necessary standards. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3 +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) SetGenHeader(header Header, values ...string) { if m.genHeader == nil { m.genHeader = make(map[Header][]string) @@ -258,26 +488,38 @@ func (m *Msg) SetGenHeader(header Header, values ...string) { m.genHeader[header] = values } -// SetHeaderPreformatted sets a generic header field of the Msg which content is -// already preformated. +// SetHeaderPreformatted sets a generic header field of the Msg, which content is already preformatted. // -// Deprecated: This method only exists for compatibility reason. Please use -// SetGenHeaderPreformatted instead +// Deprecated: This method only exists for compatibility reasons. Please use +// SetGenHeaderPreformatted instead for setting preformatted generic header fields. +// +// Parameters: +// - header: The header field to set in the Msg. +// - value: The preformatted string value to associate with the header field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3 +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) SetHeaderPreformatted(header Header, value string) { m.SetGenHeaderPreformatted(header, value) } -// SetGenHeaderPreformatted sets a generic header field of the Msg which content is -// already preformated. +// SetGenHeaderPreformatted sets a generic header field of the Msg which content is already preformatted. // -// This method does not take a slice of values but only a single value. This is -// due to the fact, that we do not perform any content alteration and expect the -// user has already done so +// This method does not take a slice of values but only a single value. The reason for this is that we do not +// perform any content alteration on these kinds of headers and expect the user to have already taken care of +// any kind of formatting required for the header. // -// **Please note:** This method should be used only as a last resort. Since the -// user is respondible for the formating of the message header, go-mail cannot -// guarantee the fully compliance with the RFC 2822. It is recommended to use -// SetGenHeader instead. +// Note: This method should be used only as a last resort. Since the user is responsible for the formatting of +// the message header, we cannot guarantee any compliance with RFC 2822. It is advised to use SetGenHeader +// instead for general header fields. +// +// Parameters: +// - header: The header field to set in the Msg. +// - value: The preformatted string value to associate with the header field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2822 func (m *Msg) SetGenHeaderPreformatted(header Header, value string) { if m.preformHeader == nil { m.preformHeader = make(map[Header]string) @@ -285,7 +527,23 @@ func (m *Msg) SetGenHeaderPreformatted(header Header, value string) { m.preformHeader[header] = value } -// SetAddrHeader sets an address related header field of the Msg +// SetAddrHeader sets the specified AddrHeader for the Msg to the given values. +// +// Addresses are parsed according to RFC 5322. If parsing any of the provided values fails, +// an error is returned. If you cannot guarantee that all provided values are valid, you can +// use SetAddrHeaderIgnoreInvalid instead, which will silently skip any parsing errors. +// +// This method allows you to set address-related headers for the message, ensuring that the +// provided addresses are properly formatted and parsed. Using this method helps maintain the +// integrity of the email addresses within the message. +// +// Parameters: +// - header: The AddrHeader to set in the Msg (e.g., "From", "To", "Cc", "Bcc"). +// - values: One or more string values representing the email addresses to associate with +// the specified header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error { if m.addrHeader == nil { m.addrHeader = make(map[AddrHeader][]*mail.Address) @@ -309,8 +567,21 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error { return nil } -// SetAddrHeaderIgnoreInvalid sets an address related header field of the Msg and ignores invalid address -// in the validation process +// SetAddrHeaderIgnoreInvalid sets the specified AddrHeader for the Msg to the given values. +// +// Addresses are parsed according to RFC 5322. If parsing of any of the provided values fails, +// the error is ignored and the address is omitted from the address list. +// +// This method allows for setting address headers while ignoring invalid addresses. It is useful +// in scenarios where you want to ensure that only valid addresses are included without halting +// execution due to parsing errors. +// +// Parameters: +// - header: The AddrHeader field to set in the Msg. +// - values: One or more string values representing email addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) { var addresses []*mail.Address for _, addrVal := range values { @@ -323,114 +594,341 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) { m.addrHeader[header] = addresses } -// EnvelopeFrom takes and validates a given mail address and sets it as envelope "FROM" -// addrHeader of the Msg +// EnvelopeFrom sets the envelope from address for the Msg. +// +// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the +// Client for 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 it has been set for the Msg. +// The provided address is validated according to RFC 5322 and will return an error if the validation fails. +// +// Parameters: +// - from: The envelope from address to set in the Msg. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) EnvelopeFrom(from string) error { return m.SetAddrHeader(HeaderEnvelopeFrom, from) } -// EnvelopeFromFormat takes a name and address, formats them RFC5322 compliant and stores them as -// the envelope FROM address header field +// EnvelopeFromFormat sets the provided name and mail address as HeaderEnvelopeFrom for the Msg. +// +// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the +// Client for 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 it has been set for the Msg. +// The provided name and address are validated according to RFC 5322 and will return an error if +// the validation fails. +// +// Parameters: +// - name: The name to associate with the envelope from address. +// - addr: The mail address to set as the envelope from address. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) EnvelopeFromFormat(name, addr string) error { return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// From takes and validates a given mail address and sets it as "From" genHeader of the Msg +// From sets the "FROM" address in the mail body for the Msg. +// +// The "FROM" address is included in the mail body and indicates the sender of the message to +// the recipient. This address is visible in the email client and is typically displayed to the +// recipient. If the "FROM" address is not set, the msgWriter may attempt to use the envelope +// from address (if available) for sending. The provided address is validated according to RFC +// 5322 and will return an error if the validation fails. +// +// Parameters: +// - from: The "FROM" address to set in the mail body. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) From(from string) error { return m.SetAddrHeader(HeaderFrom, from) } -// FromFormat takes a name and address, formats them RFC5322 compliant and stores them as -// the From address header field +// FromFormat sets the provided name and mail address as the "FROM" address in the mail body for the Msg. +// +// The "FROM" address is included in the mail body and indicates the sender of the message to +// the recipient, and is visible in the email client. If the "FROM" address is not explicitly +// set, the msgWriter may use the envelope from address (if provided) when sending the message. +// The provided name and address are validated according to RFC 5322 and will return an error +// if the validation fails. +// +// Parameters: +// - name: The name of the sender to include in the "FROM" address. +// - addr: The email address of the sender to include in the "FROM" address. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) FromFormat(name, addr string) error { return m.SetAddrHeader(HeaderFrom, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// To takes and validates a given mail address list sets the To: addresses of the Msg +// To sets one or more "TO" addresses in the mail body for the Msg. +// +// The "TO" address specifies the primary recipient(s) of the message and is included in the mail body. +// This address is visible to the recipient and any other recipients of the message. Multiple "TO" addresses +// can be set by passing them as variadic arguments to this method. Each provided address is validated +// according to RFC 5322, and an error will be returned if ANY validation fails. +// +// Parameters: +// - rcpts: One or more recipient email addresses to include in the "TO" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) To(rcpts ...string) error { return m.SetAddrHeader(HeaderTo, rcpts...) } -// AddTo adds an additional address to the To address header field +// AddTo adds a single "TO" address to the existing list of recipients in the mail body for the Msg. +// +// This method allows you to add a single recipient to the "TO" field without replacing any previously set +// "TO" addresses. The "TO" address specifies the primary recipient(s) of the message and is visible in the mail +// client. The provided address is validated according to RFC 5322, and an error will be returned if the +// validation fails. +// +// Parameters: +// - rcpt: The recipient email address to add to the "TO" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddTo(rcpt string) error { return m.addAddr(HeaderTo, rcpt) } -// AddToFormat takes a name and address, formats them RFC5322 compliant and stores them as -// as additional To address header field +// AddToFormat adds a single "TO" address with the provided name and email to the existing list of recipients +// in the mail body for the Msg. +// +// This method allows you to add a recipient's name and email address to the "TO" field without replacing any +// previously set "TO" addresses. The "TO" address specifies the primary recipient(s) of the message and is +// visible in the mail client. The provided name and address are validated according to RFC 5322, and an error +// will be returned if the validation fails. +// +// Parameters: +// - name: The name of the recipient to add to the "TO" field. +// - addr: The email address of the recipient to add to the "TO" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddToFormat(name, addr string) error { return m.addAddr(HeaderTo, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// ToIgnoreInvalid takes and validates a given mail address list sets the To: addresses of the Msg -// Any provided address that is not RFC5322 compliant, will be ignored +// ToIgnoreInvalid sets one or more "TO" addresses in the mail body for the Msg, ignoring any invalid addresses. +// +// This method allows you to add multiple "TO" recipients to the message body. Unlike the standard `To` method, +// any invalid addresses are ignored, and no error is returned for those addresses. Valid addresses will still be +// included in the "TO" field, which is visible in the recipient's mail client. Use this method with caution if +// address validation is critical. Invalid addresses are determined according to RFC 5322. +// +// Parameters: +// - rcpts: One or more recipient addresses to add to the "TO" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) ToIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderTo, rcpts...) } -// ToFromString takes and validates a given string of comma separted -// mail address and sets them as To: addresses of the Msg +// ToFromString takes a string of comma-separated email addresses, validates each, and sets them as the +// "TO" addresses for the Msg. +// +// This method allows you to pass a single string containing multiple email addresses separated by commas. +// Each address is validated according to RFC 5322 and set as a recipient in the "TO" field. If any validation +// fails, an error will be returned. The addresses are visible in the mail body and displayed to recipients in +// the mail client. Any "TO" address applied previously will be overwritten. +// +// Parameters: +// - rcpts: A string containing multiple recipient addresses separated by commas. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) ToFromString(rcpts string) error { return m.To(strings.Split(rcpts, ",")...) } -// Cc takes and validates a given mail address list sets the Cc: addresses of the Msg +// Cc sets one or more "CC" (carbon copy) addresses in the mail body for the Msg. +// +// The "CC" address specifies secondary recipient(s) of the message, and is included in the mail body. +// These addresses are visible to all recipients, including those listed in the "TO" and other "CC" fields. +// Multiple "CC" addresses can be set by passing them as variadic arguments to this method. Each provided +// address is validated according to RFC 5322, and an error will be returned if ANY validation fails. +// +// Parameters: +// - rcpts: One or more recipient addresses to be included in the "CC" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) Cc(rcpts ...string) error { return m.SetAddrHeader(HeaderCc, rcpts...) } -// AddCc adds an additional address to the Cc address header field +// AddCc adds a single "CC" (carbon copy) address to the existing list of "CC" recipients in the mail body +// for the Msg. +// +// This method allows you to add a single recipient to the "CC" field without replacing any previously set "CC" +// addresses. The "CC" address specifies secondary recipient(s) and is visible to all recipients, including those +// in the "TO" field. The provided address is validated according to RFC 5322, and an error will be returned if +// the validation fails. +// +// Parameters: +// - rcpt: The recipient address to be added to the "CC" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddCc(rcpt string) error { return m.addAddr(HeaderCc, rcpt) } -// AddCcFormat takes a name and address, formats them RFC5322 compliant and stores them as -// as additional Cc address header field +// AddCcFormat adds a single "CC" (carbon copy) address with the provided name and email to the existing list +// of "CC" recipients in the mail body for the Msg. +// +// This method allows you to add a recipient's name and email address to the "CC" field without replacing any +// previously set "CC" addresses. The "CC" address specifies secondary recipient(s) and is visible to all +// recipients, including those in the "TO" field. The provided name and address are validated according to +// RFC 5322, and an error will be returned if the validation fails. +// +// Parameters: +// - name: The name of the recipient to be added to the "CC" field. +// - addr: The email address of the recipient to be added to the "CC" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddCcFormat(name, addr string) error { return m.addAddr(HeaderCc, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// CcIgnoreInvalid takes and validates a given mail address list sets the Cc: addresses of the Msg -// Any provided address that is not RFC5322 compliant, will be ignored +// CcIgnoreInvalid sets one or more "CC" (carbon copy) addresses in the mail body for the Msg, ignoring any +// invalid addresses. +// +// This method allows you to add multiple "CC" recipients to the message body. Unlike the standard `Cc` method, +// any invalid addresses are ignored, and no error is returned for those addresses. Valid addresses will still +// be included in the "CC" field, which is visible to all recipients in the mail client. Use this method with +// caution if address validation is critical, as invalid addresses are determined according to RFC 5322. +// +// Parameters: +// - rcpts: One or more recipient email addresses to be added to the "CC" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) CcIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderCc, rcpts...) } -// CcFromString takes and validates a given string of comma separted -// mail address and sets them as Cc: addresses of the Msg +// CcFromString takes a string of comma-separated email addresses, validates each, and sets them as the "CC" +// addresses for the Msg. +// +// This method allows you to pass a single string containing multiple email addresses separated by commas. +// Each address is validated according to RFC 5322 and set as a recipient in the "CC" field. If any validation +// fails, an error will be returned. The addresses are visible in the mail body and displayed to recipients +// in the mail client. +// +// Parameters: +// - rcpts: A string containing multiple email addresses separated by commas. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) CcFromString(rcpts string) error { return m.Cc(strings.Split(rcpts, ",")...) } -// Bcc takes and validates a given mail address list sets the Bcc: addresses of the Msg +// Bcc sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg. +// +// The "BCC" address specifies recipient(s) of the message who will receive a copy without other recipients +// being aware of it. These addresses are not visible in the mail body or to any other recipients, ensuring +// the privacy of BCC'd recipients. Multiple "BCC" addresses can be set by passing them as variadic arguments +// to this method. Each provided address is validated according to RFC 5322, and an error will be returned +// if ANY validation fails. +// +// Parameters: +// - rcpts: One or more string values representing the BCC addresses to set in the Msg. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) Bcc(rcpts ...string) error { return m.SetAddrHeader(HeaderBcc, rcpts...) } -// AddBcc adds an additional address to the Bcc address header field +// AddBcc adds a single "BCC" (blind carbon copy) address to the existing list of "BCC" recipients in the mail +// body for the Msg. +// +// This method allows you to add a single recipient to the "BCC" field without replacing any previously set +// "BCC" addresses. The "BCC" address specifies recipient(s) of the message who will receive a copy without other +// recipients being aware of it. The provided address is validated according to RFC 5322, and an error will be +// returned if the validation fails. +// +// Parameters: +// - rcpt: The BCC address to add to the existing list of recipients in the Msg. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddBcc(rcpt string) error { return m.addAddr(HeaderBcc, rcpt) } -// AddBccFormat takes a name and address, formats them RFC5322 compliant and stores them as -// as additional Bcc address header field +// AddBccFormat adds a single "BCC" (blind carbon copy) address with the provided name and email to the existing +// list of "BCC" recipients in the mail body for the Msg. +// +// This method allows you to add a recipient's name and email address to the "BCC" field without replacing +// any previously set "BCC" addresses. The "BCC" address specifies recipient(s) of the message who will receive +// a copy without other recipients being aware of it. The provided name and address are validated according to +// RFC 5322, and an error will be returned if the validation fails. +// +// Parameters: +// - name: The name of the recipient to add to the BCC field. +// - addr: The email address of the recipient to add to the BCC field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddBccFormat(name, addr string) error { return m.addAddr(HeaderBcc, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// BccIgnoreInvalid takes and validates a given mail address list sets the Bcc: addresses of the Msg -// Any provided address that is not RFC5322 compliant, will be ignored +// BccIgnoreInvalid sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg, +// ignoring any invalid addresses. +// +// This method allows you to add multiple "BCC" recipients to the message body. Unlike the standard `Bcc` +// method, any invalid addresses are ignored, and no error is returned for those addresses. Valid addresses +// will still be included in the "BCC" field, which ensures the privacy of the BCC'd recipients. Use this method +// with caution if address validation is critical, as invalid addresses are determined according to RFC 5322. +// +// Parameters: +// - rcpts: One or more string values representing the BCC email addresses to set. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) BccIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderBcc, rcpts...) } -// BccFromString takes and validates a given string of comma separted -// mail address and sets them as Bcc: addresses of the Msg +// BccFromString takes a string of comma-separated email addresses, validates each, and sets them as the "BCC" +// addresses for the Msg. +// +// This method allows you to pass a single string containing multiple email addresses separated by commas. +// Each address is validated according to RFC 5322 and set as a recipient in the "BCC" field. If any validation +// fails, an error will be returned. The addresses are not visible in the mail body and ensure the privacy of +// BCC'd recipients. +// +// Parameters: +// - rcpts: A string of comma-separated email addresses to set as BCC recipients. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) BccFromString(rcpts string) error { return m.Bcc(strings.Split(rcpts, ",")...) } -// ReplyTo takes and validates a given mail address and sets it as "Reply-To" addrHeader of the Msg +// ReplyTo sets the "Reply-To" address for the Msg, specifying where replies should be sent. +// +// This method takes a single email address as input and attempts to parse it. If the address is valid, it sets +// the "Reply-To" header in the message. The "Reply-To" address can be different from the "From" address, +// allowing the sender to specify an alternate address for responses. If the provided address cannot be parsed, +// an error will be returned, indicating the parsing failure. +// +// Parameters: +// - addr: The email address to set as the "Reply-To" address. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) ReplyTo(addr string) error { replyTo, err := mail.ParseAddress(addr) if err != nil { @@ -440,28 +938,49 @@ func (m *Msg) ReplyTo(addr string) error { return nil } -// ReplyToFormat takes a name and address, formats them RFC5322 compliant and stores them as -// the Reply-To header field +// ReplyToFormat sets the "Reply-To" address for the Msg using the provided name and email address, specifying +// where replies should be sent. +// +// This method formats the name and email address into a single "Reply-To" header. If the formatted address is valid, +// it sets the "Reply-To" header in the message. This allows the sender to specify a display name along with the +// reply address, providing clarity for recipients. If the constructed address cannot be parsed, an error will +// be returned, indicating the parsing failure. +// +// Parameters: +// - name: The display name associated with the reply address. +// - addr: The email address to set as the "Reply-To" address. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) ReplyToFormat(name, addr string) error { return m.ReplyTo(fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// addAddr adds an additional address to the given addrHeader of the Msg -func (m *Msg) addAddr(header AddrHeader, addr string) error { - var addresses []string - for _, address := range m.addrHeader[header] { - addresses = append(addresses, address.String()) - } - addresses = append(addresses, addr) - return m.SetAddrHeader(header, addresses...) -} - -// Subject sets the "Subject" header field of the Msg +// Subject sets the "Subject" header for the Msg, specifying the topic of the message. +// +// This method takes a single string as input and sets it as the "Subject" of the email. The subject line provides +// a brief summary of the content of the message, allowing recipients to quickly understand its purpose. +// +// Parameters: +// - subj: The subject line of the email. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.5 func (m *Msg) Subject(subj string) { m.SetGenHeader(HeaderSubject, subj) } -// SetMessageID generates a random message id for the mail +// SetMessageID generates and sets a unique "Message-ID" header for the Msg. +// +// This method creates a "Message-ID" string using the current process ID, random numbers, and the hostname +// of the machine. The generated ID helps uniquely identify the message in email systems, facilitating tracking +// and preventing duplication. If the hostname cannot be retrieved, it defaults to "localhost.localdomain". +// +// The generated Message-ID follows the format +// "". +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) SetMessageID() { hostname, err := os.Hostname() if err != nil { @@ -476,8 +995,15 @@ func (m *Msg) SetMessageID() { m.SetMessageIDWithValue(messageID) } -// GetMessageID returns the message ID of the Msg as string value. If no message ID -// is set, an empty string will be returned +// GetMessageID retrieves the "Message-ID" header from the Msg. +// +// This method checks if a "Message-ID" has been set in the message's generated headers. If a valid "Message-ID" +// exists in the Msg, it returns the first occurrence of the header. If the "Message-ID" has not been set or +// is empty, it returns an empty string. This allows other components to access the unique identifier for the +// message, which is useful for tracking and referencing in email systems. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) GetMessageID() string { if msgidheader, ok := m.genHeader[HeaderMessageID]; ok { if len(msgidheader) > 0 { @@ -487,32 +1013,80 @@ func (m *Msg) GetMessageID() string { return "" } -// SetMessageIDWithValue sets the message id for the mail +// SetMessageIDWithValue sets the "Message-ID" header for the Msg using the provided messageID string. +// +// This method formats the input messageID by enclosing it in angle brackets ("<>") and sets it as the "Message-ID" +// header in the message. The "Message-ID" is a unique identifier for the email, helping email clients and servers +// to track and reference the message. There are no validations performed on the input messageID, so it should +// be in a suitable format for use as a Message-ID. +// +// Parameters: +// - messageID: The string to set as the "Message-ID" in the message header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) SetMessageIDWithValue(messageID string) { m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID)) } -// SetBulk sets the "Precedence: bulk" and "X-Auto-Response-Suppress: All" genHeaders which are -// recommended for automated mails like OOO replies -// See: https://www.rfc-editor.org/rfc/rfc2076#section-3.9 -// See also: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/ced68690-498a-4567-9d14-5c01f974d8b1#Appendix_A_Target_51 +// SetBulk sets the "Precedence: bulk" and "X-Auto-Response-Suppress: All" headers for the Msg, +// which are recommended for automated emails such as out-of-office replies. +// +// The "Precedence: bulk" header indicates that the message is a bulk email, and the "X-Auto-Response-Suppress: All" +// header instructs mail servers and clients to suppress automatic responses to this message. +// This is particularly useful for reducing unnecessary replies to automated notifications or replies. +// +// References: +// - https://www.rfc-editor.org/rfc/rfc2076#section-3.9 +// - https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/ced68690-498a-4567-9d14-5c01f974d8b1#Appendix_A_Target_51 func (m *Msg) SetBulk() { m.SetGenHeader(HeaderPrecedence, "bulk") m.SetGenHeader(HeaderXAutoResponseSuppress, "All") } -// SetDate sets the Date genHeader field to the current time in a valid format +// SetDate sets the "Date" header for the Msg to the current time in a valid RFC 1123 format. +// +// This method retrieves the current time and formats it according to RFC 1123, ensuring that the "Date" +// header is compliant with email standards. The "Date" header indicates when the message was created, +// providing recipients with context for the timing of the email. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 +// - https://datatracker.ietf.org/doc/html/rfc1123 func (m *Msg) SetDate() { now := time.Now().Format(time.RFC1123Z) m.SetGenHeader(HeaderDate, now) } -// SetDateWithValue sets the Date genHeader field to the provided time in a valid format +// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format. +// +// This method takes a `time.Time` value as input and formats it according to RFC 1123, ensuring that the "Date" +// header is compliant with email standards. The "Date" header indicates when the message was created, +// providing recipients with context for the timing of the email. This allows for setting a custom date +// rather than using the current time. +// +// Parameters: +// - timeVal: The time value used to set the "Date" header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 +// - https://datatracker.ietf.org/doc/html/rfc1123 func (m *Msg) SetDateWithValue(timeVal time.Time) { m.SetGenHeader(HeaderDate, timeVal.Format(time.RFC1123Z)) } -// SetImportance sets the Msg Importance/Priority header to given Importance +// SetImportance sets the "Importance" and "Priority" headers for the Msg to the specified Importance level. +// +// This method adjusts the email's importance based on the provided Importance value. If the importance level +// is set to `ImportanceNormal`, no headers are modified. Otherwise, it sets the "Importance", "Priority", +// "X-Priority", and "X-MSMail-Priority" headers accordingly, providing email clients with information on +// how to prioritize the message. This allows the sender to indicate the significance of the email to recipients. +// +// Parameters: +// - importance: The Importance value that determines the priority of the email message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2156 func (m *Msg) SetImportance(importance Importance) { if importance == ImportanceNormal { return @@ -523,26 +1097,63 @@ func (m *Msg) SetImportance(importance Importance) { m.SetGenHeader(HeaderXMSMailPriority, importance.NumString()) } -// SetOrganization sets the provided string as Organization header for the Msg +// SetOrganization sets the "Organization" header for the Msg to the specified organization string. +// +// This method allows you to specify the organization associated with the email sender. The "Organization" +// header provides recipients with information about the organization that is sending the message. +// This can help establish context and credibility for the email communication. +// +// Parameters: +// - org: The name of the organization to be set in the "Organization" header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetOrganization(org string) { m.SetGenHeader(HeaderOrganization, org) } -// SetUserAgent sets the User-Agent/X-Mailer header for the Msg +// SetUserAgent sets the "User-Agent" and "X-Mailer" headers for the Msg to the specified user agent string. +// +// This method allows you to specify the user agent or mailer software used to send the email. +// The "User-Agent" and "X-Mailer" headers provide recipients with information about the email client +// or application that generated the message. This can be useful for identifying the source of the email, +// particularly for troubleshooting or filtering purposes. +// +// Parameters: +// - userAgent: The user agent or mailer software to be set in the "User-Agent" and "X-Mailer" headers. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.7 func (m *Msg) SetUserAgent(userAgent string) { m.SetGenHeader(HeaderUserAgent, userAgent) m.SetGenHeader(HeaderXMailer, userAgent) } -// IsDelivered will return true if the Msg has been successfully delivered +// IsDelivered indicates whether the Msg has been delivered. +// +// This method checks the internal state of the message to determine if it has been successfully +// delivered. It returns true if the message is marked as delivered and false otherwise. +// This can be useful for tracking the status of the email communication. +// +// Returns: +// - A boolean value indicating the delivery status of the message (true if delivered, false otherwise). func (m *Msg) IsDelivered() bool { return m.isDelivered } -// RequestMDNTo adds the Disposition-Notification-To header to request a MDN from the receiving end -// as described in RFC8098. It allows to provide a list recipient addresses. -// Address validation is performed -// See: https://www.rfc-editor.org/rfc/rfc8098.html +// RequestMDNTo adds the "Disposition-Notification-To" header to the Msg to request a Message Disposition +// Notification (MDN) from the receiving end, as specified in RFC 8098. +// +// This method allows you to provide a list of recipient addresses to receive the MDN. +// Each address is validated according to RFC 5322 standards. If ANY address is invalid, an error +// will be returned indicating the parsing failure. If the "Disposition-Notification-To" header +// is already set, it will be updated with the new list of addresses. +// +// Parameters: +// - rcpts: One or more recipient email addresses to request the MDN from. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNTo(rcpts ...string) error { var addresses []string for _, addrVal := range rcpts { @@ -558,15 +1169,35 @@ func (m *Msg) RequestMDNTo(rcpts ...string) error { return nil } -// RequestMDNToFormat adds the Disposition-Notification-To header to request a MDN from the receiving end -// as described in RFC8098. It allows to provide a recipient address with name and address and will format -// accordingly. Address validation is performed -// See: https://www.rfc-editor.org/rfc/rfc8098.html +// RequestMDNToFormat adds the "Disposition-Notification-To" header to the Msg to request a Message Disposition +// Notification (MDN) from the receiving end, as specified in RFC 8098. +// +// This method allows you to provide a recipient address along with a name, formatting it appropriately. +// Address validation is performed according to RFC 5322 standards. If the provided address is invalid, +// an error will be returned. This method internally calls RequestMDNTo to handle the actual setting of the header. +// +// Parameters: +// - name: The name of the recipient for the MDN request. +// - addr: The email address of the recipient for the MDN request. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNToFormat(name, addr string) error { return m.RequestMDNTo(fmt.Sprintf(`%s <%s>`, name, addr)) } -// RequestMDNAddTo adds an additional recipient to the recipient list of the MDN +// RequestMDNAddTo adds an additional recipient to the "Disposition-Notification-To" header for the Msg. +// +// This method allows you to append a new recipient address to the existing list of recipients for the +// MDN. The provided address is validated according to RFC 5322 standards. If the address is invalid, +// an error will be returned indicating the parsing failure. If the "Disposition-Notification-To" +// header is already set, the new recipient will be added to the existing list. +// +// Parameters: +// - rcpt: The recipient email address to add to the "Disposition-Notification-To" header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNAddTo(rcpt string) error { address, err := mail.ParseAddress(rcpt) if err != nil { @@ -581,14 +1212,41 @@ func (m *Msg) RequestMDNAddTo(rcpt string) error { return nil } -// RequestMDNAddToFormat adds an additional formated recipient to the recipient list of the MDN +// RequestMDNAddToFormat adds an additional formatted recipient to the "Disposition-Notification-To" +// header for the Msg. +// +// This method allows you to specify a recipient address along with a name, formatting it appropriately +// before adding it to the existing list of recipients for the MDN. The formatted address is validated +// according to RFC 5322 standards. If the provided address is invalid, an error will be returned. +// This method internally calls RequestMDNAddTo to handle the actual addition of the recipient. +// +// Parameters: +// - name: The name of the recipient to add to the "Disposition-Notification-To" header. +// - addr: The email address of the recipient to add to the "Disposition-Notification-To" header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNAddToFormat(name, addr string) error { return m.RequestMDNAddTo(fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// GetSender returns the currently set envelope FROM address. If no envelope FROM is set it will use -// the first mail body FROM address. If useFullAddr is true, it will return the full address string -// including the address name, if set +// GetSender returns the currently set envelope "FROM" address for the Msg. If no envelope +// "FROM" address is set, it will use the first "FROM" address from the mail body. If the +// useFullAddr parameter is true, it will return the full address string, including the name +// if it is set. +// +// If neither the envelope "FROM" nor the body "FROM" addresses are available, it will return +// an error indicating that no "FROM" address is present. +// +// Parameters: +// - useFullAddr: A boolean indicating whether to return the full address string (including +// the name) or just the email address. +// +// Returns: +// - The sender's address as a string and an error if applicable. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) GetSender(useFullAddr bool) (string, error) { from, ok := m.addrHeader[HeaderEnvelopeFrom] if !ok || len(from) == 0 { @@ -603,7 +1261,19 @@ func (m *Msg) GetSender(useFullAddr bool) (string, error) { return from[0].Address, nil } -// GetRecipients returns a list of the currently set TO/CC/BCC addresses. +// GetRecipients returns a list of the currently set "TO", "CC", and "BCC" addresses for the Msg. +// +// This method aggregates recipients from the "TO", "CC", and "BCC" headers and returns them as a +// slice of strings. If no recipients are found in these headers, it will return an error indicating +// that no recipient addresses are present. +// +// Returns: +// - A slice of strings containing the recipients' addresses and an error if applicable. +// - If there are no recipient addresses set, it will return an error indicating no recipient +// addresses are available. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetRecipients() ([]string, error) { var rcpts []string for _, addressType := range []AddrHeader{HeaderTo, HeaderCc, HeaderBcc} { @@ -621,12 +1291,42 @@ func (m *Msg) GetRecipients() ([]string, error) { return rcpts, nil } -// GetAddrHeader returns the content of the requested address header of the Msg +// GetAddrHeader returns the content of the requested address header for the Msg. +// +// This method retrieves the addresses associated with the specified address header. It returns a +// slice of pointers to mail.Address structures representing the addresses found in the header. +// If the requested header does not exist or contains no addresses, it will return nil. +// +// Parameters: +// - header: The AddrHeader enum value indicating which address header to retrieve (e.g., "TO", +// "CC", "BCC", etc.). +// +// Returns: +// - A slice of pointers to mail.Address structures containing the addresses from the specified +// header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 func (m *Msg) GetAddrHeader(header AddrHeader) []*mail.Address { return m.addrHeader[header] } -// GetAddrHeaderString returns the address string of the requested address header of the Msg +// GetAddrHeaderString returns the address strings of the requested address header for the Msg. +// +// This method retrieves the addresses associated with the specified address header and returns them +// as a slice of strings. Each address is formatted as a string, which includes both the name (if +// available) and the email address. If the requested header does not exist or contains no addresses, +// it will return an empty slice. +// +// Parameters: +// - header: The AddrHeader enum value indicating which address header to retrieve (e.g., "TO", +// "CC", "BCC", etc.). +// +// Returns: +// - A slice of strings containing the formatted addresses from the specified header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 func (m *Msg) GetAddrHeaderString(header AddrHeader) []string { var addresses []string for _, mh := range m.addrHeader[header] { @@ -635,67 +1335,179 @@ func (m *Msg) GetAddrHeaderString(header AddrHeader) []string { return addresses } -// GetFrom returns the content of the From address header of the Msg +// GetFrom returns the content of the "From" address header of the Msg. +// +// This method retrieves the list of email addresses set in the "From" header of the message. +// It returns a slice of pointers to `mail.Address` objects representing the sender(s) of the email. +// +// Returns: +// - A slice of `*mail.Address` containing the "From" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) GetFrom() []*mail.Address { return m.GetAddrHeader(HeaderFrom) } -// GetFromString returns the content of the From address header of the Msg as string slice +// GetFromString returns the content of the "From" address header of the Msg as a string slice. +// +// This method retrieves the list of email addresses set in the "From" header of the message +// and returns them as a slice of strings, with each entry representing a formatted email address. +// +// Returns: +// - A slice of strings containing the "From" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) GetFromString() []string { return m.GetAddrHeaderString(HeaderFrom) } -// GetTo returns the content of the To address header of the Msg +// GetTo returns the content of the "To" address header of the Msg. +// +// This method retrieves the list of email addresses set in the "To" header of the message. +// It returns a slice of pointers to `mail.Address` objects representing the primary recipient(s) of the email. +// +// Returns: +// - A slice of `*mail.Address` containing the "To" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetTo() []*mail.Address { return m.GetAddrHeader(HeaderTo) } -// GetToString returns the content of the To address header of the Msg as string slice +// GetToString returns the content of the "To" address header of the Msg as a string slice. +// +// This method retrieves the list of email addresses set in the "To" header of the message +// and returns them as a slice of strings, with each entry representing a formatted email address. +// +// Returns: +// - A slice of strings containing the "To" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetToString() []string { return m.GetAddrHeaderString(HeaderTo) } -// GetCc returns the content of the Cc address header of the Msg +// GetCc returns the content of the "Cc" address header of the Msg. +// +// This method retrieves the list of email addresses set in the "Cc" (carbon copy) header of the message. +// It returns a slice of pointers to `mail.Address` objects representing the secondary recipient(s) of the email. +// +// Returns: +// - A slice of `*mail.Address` containing the "Cc" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetCc() []*mail.Address { return m.GetAddrHeader(HeaderCc) } -// GetCcString returns the content of the Cc address header of the Msg as string slice +// GetCcString returns the content of the "Cc" address header of the Msg as a string slice. +// +// This method retrieves the list of email addresses set in the "Cc" (carbon copy) header of the message +// and returns them as a slice of strings, with each entry representing a formatted email address. +// +// Returns: +// - A slice of strings containing the "Cc" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetCcString() []string { return m.GetAddrHeaderString(HeaderCc) } -// GetBcc returns the content of the Bcc address header of the Msg +// GetBcc returns the content of the "Bcc" address header of the Msg. +// +// This method retrieves the list of email addresses set in the "Bcc" (blind carbon copy) header of the message. +// It returns a slice of pointers to `mail.Address` objects representing the Bcc recipient(s) of the email. +// +// Returns: +// - A slice of `*mail.Address` containing the "Bcc" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetBcc() []*mail.Address { return m.GetAddrHeader(HeaderBcc) } -// GetBccString returns the content of the Bcc address header of the Msg as string slice +// GetBccString returns the content of the "Bcc" address header of the Msg as a string slice. +// +// This method retrieves the list of email addresses set in the "Bcc" (blind carbon copy) header of the message +// and returns them as a slice of strings, with each entry representing a formatted email address. +// +// Returns: +// - A slice of strings containing the "Bcc" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetBccString() []string { return m.GetAddrHeaderString(HeaderBcc) } -// GetGenHeader returns the content of the requested generic header of the Msg +// GetGenHeader returns the content of the requested generic header of the Msg. +// +// This method retrieves the list of string values associated with the specified generic header of the message. +// It returns a slice of strings representing the header's values. +// +// Parameters: +// - header: The Header field whose values are being retrieved. +// +// Returns: +// - A slice of strings containing the values of the specified generic header. func (m *Msg) GetGenHeader(header Header) []string { return m.genHeader[header] } -// GetParts returns the message parts of the Msg +// GetParts returns the message parts of the Msg. +// +// This method retrieves the list of parts that make up the email message. Each part may represent +// a different section of the email, such as a plain text body, HTML body, or attachments. +// +// Returns: +// - A slice of Part pointers representing the message parts of the email. func (m *Msg) GetParts() []*Part { return m.parts } -// GetAttachments returns the attachments of the Msg +// GetAttachments returns the attachments of the Msg. +// +// This method retrieves the list of files that have been attached to the email message. +// Each attachment includes details about the file, such as its name, content type, and data. +// +// Returns: +// - A slice of File pointers representing the attachments of the email. func (m *Msg) GetAttachments() []*File { return m.attachments } -// GetBoundary returns the boundary of the Msg +// GetBoundary returns the boundary of the Msg. +// +// This method retrieves the MIME boundary that is used to separate different parts of the message, +// particularly in multipart emails. The boundary helps to differentiate between various sections +// such as plain text, HTML content, and attachments. +// +// Returns: +// - A string representing the boundary of the message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1 func (m *Msg) GetBoundary() string { return m.boundary } // SetAttachments sets the attachments of the message. +// +// This method allows you to specify the attachments for the message by providing a slice of File pointers. +// Each file represents an attachment that will be included in the email. +// +// Parameters: +// - files: A slice of pointers to File structures representing the attachments to set for the message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) SetAttachments(files []*File) { m.attachments = files } @@ -707,33 +1519,83 @@ func (m *Msg) SetAttachements(files []*File) { m.SetAttachments(files) } -// UnsetAllAttachments unset the attachments of the message. +// UnsetAllAttachments unsets the attachments of the message. +// +// This method removes all attachments from the message by setting the attachments to nil, effectively +// clearing any previously set attachments. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) UnsetAllAttachments() { m.attachments = nil } -// GetEmbeds returns the embeds of the Msg +// GetEmbeds returns the embedded files of the Msg. +// +// This method retrieves the list of files that have been embedded in the message. Embeds are typically +// images or other media files that are referenced directly in the content of the email, such as inline +// images in HTML emails. +// +// Returns: +// - A slice of pointers to File structures representing the embedded files in the message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) GetEmbeds() []*File { return m.embeds } -// SetEmbeds sets the embeds of the message. +// SetEmbeds sets the embedded files of the message. +// +// This method allows you to specify the files to be embedded in the message by providing a slice of File pointers. +// Embedded files, such as images or media, are typically used for inline content in HTML emails. +// +// Parameters: +// - files: A slice of pointers to File structures representing the embedded files to set for the message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) SetEmbeds(files []*File) { m.embeds = files } -// UnsetAllEmbeds unset the embeds of the message. +// UnsetAllEmbeds unsets the embedded files of the message. +// +// This method removes all embedded files from the message by setting the embeds to nil, effectively +// clearing any previously set embedded files. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) UnsetAllEmbeds() { m.embeds = nil } -// UnsetAllParts unset the embeds and attachments of the message. +// UnsetAllParts unsets the embeds and attachments of the message. +// +// This method removes all embedded files and attachments from the message by unsetting both the +// embeds and attachments, effectively clearing all previously set message parts. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) UnsetAllParts() { m.UnsetAllAttachments() m.UnsetAllEmbeds() } // SetBodyString sets the body of the message. +// +// This method sets the body of the message using the provided content type and string content. The body can +// be set as plain text, HTML, or other formats based on the specified content type. Optional part settings +// can be passed through PartOption to customize the message body further. +// +// Parameters: +// - contentType: The ContentType of the body (e.g., plain text, HTML). +// - content: The string content to set as the body of the message. +// - opts: Optional parameters for customizing the body part. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) SetBodyString(contentType ContentType, content string, opts ...PartOption) { buffer := bytes.NewBufferString(content) writeFunc := writeFuncFromBuffer(buffer) @@ -741,6 +1603,20 @@ func (m *Msg) SetBodyString(contentType ContentType, content string, opts ...Par } // SetBodyWriter sets the body of the message. +// +// This method sets the body of the message using a write function, allowing content to be written +// directly to the body. The content type determines the format (e.g., plain text, HTML). +// Optional part settings can be provided via PartOption to customize the body further. +// +// Parameters: +// - contentType: The ContentType of the body (e.g., plain text, HTML). +// - writeFunc: A function that writes content to an io.Writer and returns the number of bytes written +// and an error, if any. +// - opts: Optional parameters for customizing the body part. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) SetBodyWriter( contentType ContentType, writeFunc func(io.Writer) (int64, error), opts ...PartOption, @@ -750,8 +1626,24 @@ func (m *Msg) SetBodyWriter( m.parts = []*Part{p} } -// SetBodyHTMLTemplate sets the body of the message from a given html/template.Template pointer -// The content type will be set to text/html automatically +// SetBodyHTMLTemplate sets the body of the message from a given html/template.Template pointer. +// +// This method sets the body of the message using the provided HTML template and data. The content type +// will be set to "text/html" automatically. The method executes the template with the provided data +// and writes the output to the message body. If the template is nil or fails to execute, an error will +// be returned. +// +// Parameters: +// - tpl: A pointer to the html/template.Template to be used for the message body. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the body part. +// +// Returns: +// - An error if the template is nil or fails to execute, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error { if tpl == nil { return errors.New(errTplPointerNil) @@ -765,8 +1657,24 @@ func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...Pa return nil } -// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer -// The content type will be set to text/plain automatically +// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer. +// +// This method sets the body of the message using the provided text template and data. The content type +// will be set to "text/plain" automatically. The method executes the template with the provided data +// and writes the output to the message body. If the template is nil or fails to execute, an error will +// be returned. +// +// Parameters: +// - tpl: A pointer to the text/template.Template to be used for the message body. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the body part. +// +// Returns: +// - An error if the template is nil or fails to execute, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error { if tpl == nil { return errors.New(errTplPointerNil) @@ -781,13 +1689,40 @@ func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...Pa } // AddAlternativeString sets the alternative body of the message. +// +// This method adds an alternative representation of the message body using the specified content type +// and string content. This is typically used to provide both plain text and HTML versions of the email. +// Optional part settings can be provided via PartOption to further customize the message. +// +// Parameters: +// - contentType: The content type of the alternative body (e.g., plain text, HTML). +// - content: The string content to set as the alternative body. +// - opts: Optional parameters for customizing the alternative body part. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) AddAlternativeString(contentType ContentType, content string, opts ...PartOption) { buffer := bytes.NewBufferString(content) writeFunc := writeFuncFromBuffer(buffer) m.AddAlternativeWriter(contentType, writeFunc, opts...) } -// AddAlternativeWriter sets the body of the message. +// AddAlternativeWriter sets the alternative body of the message. +// +// This method adds an alternative representation of the message body using a write function, allowing +// content to be written directly to the body. This is typically used to provide different formats, such +// as plain text and HTML. Optional part settings can be provided via PartOption to customize the message part. +// +// Parameters: +// - contentType: The content type of the alternative body (e.g., plain text, HTML). +// - writeFunc: A function that writes content to an io.Writer and returns the number of bytes written and +// an error, if any. +// - opts: Optional parameters for customizing the alternative body part. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) AddAlternativeWriter( contentType ContentType, writeFunc func(io.Writer) (int64, error), opts ...PartOption, @@ -797,8 +1732,23 @@ func (m *Msg) AddAlternativeWriter( m.parts = append(m.parts, part) } -// AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output -// The content type will be set to text/html automatically +// AddAlternativeHTMLTemplate sets the alternative body of the message to an html/template.Template output. +// +// The content type will be set to "text/html" automatically. This method executes the provided HTML template +// with the given data and adds the result as an alternative version of the message body. If the template +// is nil or fails to execute, an error will be returned. +// +// Parameters: +// - tpl: A pointer to the html/template.Template to be used for the alternative body. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the alternative body part. +// +// Returns: +// - An error if the template is nil or fails to execute, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error { if tpl == nil { return errors.New(errTplPointerNil) @@ -812,8 +1762,23 @@ func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opt return nil } -// AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output -// The content type will be set to text/plain automatically +// AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output. +// +// The content type will be set to "text/plain" automatically. This method executes the provided text template +// with the given data and adds the result as an alternative version of the message body. If the template +// is nil or fails to execute, an error will be returned. +// +// Parameters: +// - tpl: A pointer to the text/template.Template to be used for the alternative body. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the alternative body part. +// +// Returns: +// - An error if the template is nil or fails to execute, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error { if tpl == nil { return errors.New(errTplPointerNil) @@ -827,7 +1792,18 @@ func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opt return nil } -// AttachFile adds an attachment File to the Msg +// AttachFile adds an attachment File to the Msg. +// +// This method attaches a file to the message by specifying the file name. The file is retrieved from the +// filesystem and added to the list of attachments. Optional FileOption parameters can be provided to customize +// the attachment, such as setting its content type or encoding. +// +// Parameters: +// - name: The name of the file to be attached. +// - opts: Optional parameters for customizing the attachment. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachFile(name string, opts ...FileOption) { file := fileFromFS(name) if file == nil { @@ -836,12 +1812,22 @@ func (m *Msg) AttachFile(name string, opts ...FileOption) { m.attachments = m.appendFile(m.attachments, file, opts...) } -// AttachReader adds an attachment File via io.Reader to the Msg +// AttachReader adds an attachment File via io.Reader to the Msg. // -// CAVEAT: For AttachReader to work it has to read all data of the io.Reader -// into memory first, so it can seek through it. Using larger amounts of -// data on the io.Reader should be avoided. For such, it is recommended to -// either use AttachFile or AttachReadSeeker instead +// This method allows you to attach a file to the message using an io.Reader. It reads all data from the +// io.Reader into memory before attaching the file, which may not be suitable for large data sources. +// For larger files, it is recommended to use AttachFile or AttachReadSeeker instead. +// +// Parameters: +// - name: The name of the file to be attached. +// - reader: The io.Reader providing the file data to be attached. +// - opts: Optional parameters for customizing the attachment. +// +// Returns: +// - An error if the file could not be read from the io.Reader, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachReader(name string, reader io.Reader, opts ...FileOption) error { file, err := fileFromReader(name, reader) if err != nil { @@ -851,13 +1837,41 @@ func (m *Msg) AttachReader(name string, reader io.Reader, opts ...FileOption) er return nil } -// AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg +// AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg. +// +// This method allows you to attach a file to the message using an io.ReadSeeker, which is more efficient +// for larger files compared to AttachReader, as it allows for seeking through the data without needing +// to load the entire content into memory. +// +// Parameters: +// - name: The name of the file to be attached. +// - reader: The io.ReadSeeker providing the file data to be attached. +// - opts: Optional parameters for customizing the attachment. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachReadSeeker(name string, reader io.ReadSeeker, opts ...FileOption) { file := fileFromReadSeeker(name, reader) m.attachments = m.appendFile(m.attachments, file, opts...) } -// AttachHTMLTemplate adds the output of a html/template.Template pointer as File attachment to the Msg +// AttachHTMLTemplate adds the output of a html/template.Template pointer as a File attachment to the Msg. +// +// This method allows you to attach the rendered output of an HTML template as a file to the message. +// The template is executed with the provided data, and its output is attached as a file. If the template +// fails to execute, an error will be returned. +// +// Parameters: +// - name: The name of the file to be attached. +// - tpl: A pointer to the html/template.Template to be executed for the attachment. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the attachment. +// +// Returns: +// - An error if the template fails to execute or cannot be attached, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachHTMLTemplate( name string, tpl *ht.Template, data interface{}, opts ...FileOption, ) error { @@ -869,7 +1883,23 @@ func (m *Msg) AttachHTMLTemplate( return nil } -// AttachTextTemplate adds the output of a text/template.Template pointer as File attachment to the Msg +// AttachTextTemplate adds the output of a text/template.Template pointer as a File attachment to the Msg. +// +// This method allows you to attach the rendered output of a text template as a file to the message. +// The template is executed with the provided data, and its output is attached as a file. If the template +// fails to execute, an error will be returned. +// +// Parameters: +// - name: The name of the file to be attached. +// - tpl: A pointer to the text/template.Template to be executed for the attachment. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the attachment. +// +// Returns: +// - An error if the template fails to execute or cannot be attached, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachTextTemplate( name string, tpl *tt.Template, data interface{}, opts ...FileOption, ) error { @@ -881,7 +1911,22 @@ func (m *Msg) AttachTextTemplate( return nil } -// AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg +// AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg. +// +// This method allows you to attach a file from an embedded filesystem (embed.FS) to the message. +// The file is retrieved from the provided embed.FS and attached to the email. If the embedded filesystem +// is nil or the file cannot be retrieved, an error will be returned. +// +// Parameters: +// - name: The name of the file to be attached. +// - fs: A pointer to the embed.FS from which the file will be retrieved. +// - opts: Optional parameters for customizing the attachment. +// +// Returns: +// - An error if the embed.FS is nil or the file cannot be retrieved, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error { if fs == nil { return fmt.Errorf("embed.FS must not be nil") @@ -894,7 +1939,18 @@ func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) e return nil } -// EmbedFile adds an embedded File to the Msg +// EmbedFile adds an embedded File to the Msg. +// +// This method embeds a file from the filesystem directly into the email message. The embedded file, +// typically an image or media file, can be referenced within the email's content (such as inline in HTML). +// If the file is not found or cannot be loaded, it will not be added. +// +// Parameters: +// - name: The name of the file to be embedded. +// - opts: Optional parameters for customizing the embedded file. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedFile(name string, opts ...FileOption) { file := fileFromFS(name) if file == nil { @@ -903,12 +1959,22 @@ func (m *Msg) EmbedFile(name string, opts ...FileOption) { m.embeds = m.appendFile(m.embeds, file, opts...) } -// EmbedReader adds an embedded File from an io.Reader to the Msg +// EmbedReader adds an embedded File from an io.Reader to the Msg. // -// CAVEAT: For EmbedReader to work it has to read all data of the io.Reader -// into memory first, so it can seek through it. Using larger amounts of -// data on the io.Reader should be avoided. For such, it is recommended to -// either use EmbedFile or EmbedReadSeeker instead +// This method embeds a file into the email message by reading its content from an io.Reader. +// It reads all data into memory before embedding the file, which may not be efficient for large data sources. +// For larger files, it is recommended to use EmbedFile or EmbedReadSeeker instead. +// +// Parameters: +// - name: The name of the file to be embedded. +// - reader: The io.Reader providing the file data to be embedded. +// - opts: Optional parameters for customizing the embedded file. +// +// Returns: +// - An error if the file could not be read from the io.Reader, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedReader(name string, reader io.Reader, opts ...FileOption) error { file, err := fileFromReader(name, reader) if err != nil { @@ -918,13 +1984,41 @@ func (m *Msg) EmbedReader(name string, reader io.Reader, opts ...FileOption) err return nil } -// EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg +// EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg. +// +// This method embeds a file into the email message by reading its content from an io.ReadSeeker. +// Using io.ReadSeeker allows for more efficient handling of large files since it can seek through the data +// without loading the entire content into memory. +// +// Parameters: +// - name: The name of the file to be embedded. +// - reader: The io.ReadSeeker providing the file data to be embedded. +// - opts: Optional parameters for customizing the embedded file. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedReadSeeker(name string, reader io.ReadSeeker, opts ...FileOption) { file := fileFromReadSeeker(name, reader) m.embeds = m.appendFile(m.embeds, file, opts...) } -// EmbedHTMLTemplate adds the output of a html/template.Template pointer as embedded File to the Msg +// EmbedHTMLTemplate adds the output of a html/template.Template pointer as an embedded File to the Msg. +// +// This method embeds the rendered output of an HTML template into the email message. The template is +// executed with the provided data, and its output is embedded as a file. If the template fails to execute, +// an error will be returned. +// +// Parameters: +// - name: The name of the embedded file. +// - tpl: A pointer to the html/template.Template to be executed for the embedded content. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the embedded file. +// +// Returns: +// - An error if the template fails to execute or cannot be embedded, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedHTMLTemplate( name string, tpl *ht.Template, data interface{}, opts ...FileOption, ) error { @@ -936,7 +2030,23 @@ func (m *Msg) EmbedHTMLTemplate( return nil } -// EmbedTextTemplate adds the output of a text/template.Template pointer as embedded File to the Msg +// EmbedTextTemplate adds the output of a text/template.Template pointer as an embedded File to the Msg. +// +// This method embeds the rendered output of a text template into the email message. The template is +// executed with the provided data, and its output is embedded as a file. If the template fails to execute, +// an error will be returned. +// +// Parameters: +// - name: The name of the embedded file. +// - tpl: A pointer to the text/template.Template to be executed for the embedded content. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the embedded file. +// +// Returns: +// - An error if the template fails to execute or cannot be embedded, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedTextTemplate( name string, tpl *tt.Template, data interface{}, opts ...FileOption, ) error { @@ -948,7 +2058,21 @@ func (m *Msg) EmbedTextTemplate( return nil } -// EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg +// EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg. +// +// This method embeds a file from an embedded filesystem (embed.FS) into the email message. If the +// embedded filesystem is nil or the file cannot be retrieved, an error will be returned. +// +// Parameters: +// - name: The name of the file to be embedded. +// - fs: A pointer to the embed.FS from which the file will be retrieved. +// - opts: Optional parameters for customizing the embedded file. +// +// Returns: +// - An error if the embed.FS is nil or the file cannot be retrieved, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error { if fs == nil { return fmt.Errorf("embed.FS must not be nil") @@ -961,8 +2085,14 @@ func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) er return nil } -// Reset resets all headers, body parts and attachments/embeds of the Msg -// It leaves already set encodings, charsets, boundaries, etc. as is +// Reset resets all headers, body parts, attachments, and embeds of the Msg. +// +// This method clears all address headers, attachments, embeds, generic headers, and body parts of the message. +// However, it preserves the existing encoding, charset, boundary, and other message-level settings. +// Use this method to reset the message content while keeping certain configurations intact. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) Reset() { m.addrHeader = make(map[AddrHeader][]*mail.Address) m.attachments = nil @@ -971,7 +2101,17 @@ func (m *Msg) Reset() { m.parts = nil } -// ApplyMiddlewares apply the list of middlewares to a Msg +// ApplyMiddlewares applies the list of middlewares to a Msg. +// +// This method sequentially applies each middleware function in the list to the message (in FIFO order). +// The middleware functions can modify the message, such as adding headers or altering its content. +// The message is passed through each middleware in order, and the modified message is returned. +// +// Parameters: +// - msg: The Msg object to which the middlewares will be applied. +// +// Returns: +// - The modified Msg after all middleware functions have been applied. func (m *Msg) applyMiddlewares(msg *Msg) *Msg { for _, middleware := range m.middlewares { msg = middleware.Handle(msg) @@ -979,15 +2119,44 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg { return msg } -// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface +// WriteTo writes the formatted Msg into the given io.Writer and satisfies the io.WriterTo interface. +// +// This method writes the email message, including its headers, body, and attachments, to the provided +// io.Writer. It applies any middlewares to the message before writing it. The total number of bytes +// written and any error encountered during the writing process are returned. +// +// Parameters: +// - writer: The io.Writer to which the formatted message will be written. +// +// Returns: +// - The total number of bytes written. +// - An error if any occurred during the writing process, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) WriteTo(writer io.Writer) (int64, error) { mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder} mw.writeMsg(m.applyMiddlewares(m)) return mw.bytesWritten, mw.err } -// WriteToSkipMiddleware writes the formated Msg into a give io.Writer and satisfies -// the io.WriteTo interface but will skip the given Middleware +// WriteToSkipMiddleware writes the formatted Msg into the given io.Writer, but skips the specified +// middleware type. +// +// This method writes the email message to the provided io.Writer after applying all middlewares, +// except for the specified middleware type, which will be skipped. It temporarily removes the +// middleware of the given type, writes the message, and then restores the original middleware list. +// +// Parameters: +// - writer: The io.Writer to which the formatted message will be written. +// - middleWareType: The MiddlewareType that should be skipped during the writing process. +// +// Returns: +// - The total number of bytes written. +// - An error if any occurred during the writing process, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) WriteToSkipMiddleware(writer io.Writer, middleWareType MiddlewareType) (int64, error) { var origMiddlewares, middlewares []Middleware origMiddlewares = m.middlewares @@ -1004,30 +2173,39 @@ func (m *Msg) WriteToSkipMiddleware(writer io.Writer, middleWareType MiddlewareT return mw.bytesWritten, mw.err } -// Write is an alias method to WriteTo due to compatibility reasons +// Write is an alias method to WriteTo for compatibility reasons. +// +// This method provides a backward-compatible way to write the formatted Msg to the provided io.Writer +// by calling the WriteTo method. It writes the email message, including headers, body, and attachments, +// to the io.Writer and returns the number of bytes written and any error encountered. +// +// Parameters: +// - writer: The io.Writer to which the formatted message will be written. +// +// Returns: +// - The total number of bytes written. +// - An error if any occurred during the writing process, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) Write(writer io.Writer) (int64, error) { return m.WriteTo(writer) } -// appendFile adds a File to the Msg (as attachment or embed) -func (m *Msg) appendFile(files []*File, file *File, opts ...FileOption) []*File { - // Override defaults with optionally provided FileOption functions - for _, opt := range opts { - if opt == nil { - continue - } - opt(file) - } - - if files == nil { - return []*File{file} - } - - return append(files, file) -} - -// WriteToFile stores the Msg as file on disk. It will try to create the given filename -// Already existing files will be overwritten +// WriteToFile stores the Msg as a file on disk. It will try to create the given filename, +// and if the file already exists, it will be overwritten. +// +// This method writes the email message, including its headers, body, and attachments, to a file on disk. +// If the file cannot be created or an error occurs during writing, an error is returned. +// +// Parameters: +// - name: The name of the file to be created or overwritten. +// +// Returns: +// - An error if the file cannot be created or if writing to the file fails, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) WriteToFile(name string) error { file, err := os.Create(name) if err != nil { @@ -1041,22 +2219,58 @@ func (m *Msg) WriteToFile(name string) error { return file.Close() } -// WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path +// WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path. +// +// This method sends the email message using the default sendmail path. It calls WriteToSendmailWithCommand +// using the standard SendmailPath. If sending via sendmail fails, an error is returned. +// +// Returns: +// - An error if sending the message via sendmail fails, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5321 func (m *Msg) WriteToSendmail() error { return m.WriteToSendmailWithCommand(SendmailPath) } // WriteToSendmailWithCommand returns WriteToSendmailWithContext with a default timeout -// of 5 seconds and a given sendmail path +// of 5 seconds and a given sendmail path. +// +// This method sends the email message using the provided sendmail path, with a default timeout of 5 seconds. +// It creates a context with the specified timeout and then calls WriteToSendmailWithContext to send the message. +// +// Parameters: +// - sendmailPath: The path to the sendmail executable to be used for sending the message. +// +// Returns: +// - An error if sending the message via sendmail fails, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5321 func (m *Msg) WriteToSendmailWithCommand(sendmailPath string) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() return m.WriteToSendmailWithContext(ctx, sendmailPath) } -// WriteToSendmailWithContext opens an pipe to the local sendmail binary and tries to send the -// mail though that. It takes a context.Context, the path to the sendmail binary and additional -// arguments for the sendmail binary as parameters +// WriteToSendmailWithContext opens a pipe to the local sendmail binary and tries to send the +// email through it. It takes a context.Context, the path to the sendmail binary, and additional +// arguments for the sendmail binary as parameters. +// +// This method establishes a pipe to the sendmail executable using the provided context and arguments. +// It writes the email message to the sendmail process via STDIN. If any errors occur during the +// communication with the sendmail binary, they will be captured and returned. +// +// Parameters: +// - ctx: The context to control the timeout and cancellation of the sendmail process. +// - sendmailPath: The path to the sendmail executable. +// - args: Additional arguments for the sendmail binary. +// +// Returns: +// - An error if sending the message via sendmail fails, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5321 func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath string, args ...string) error { cmdCtx := exec.CommandContext(ctx, sendmailPath) cmdCtx.Args = append(cmdCtx.Args, "-oi", "-t") @@ -1109,10 +2323,19 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath strin // NewReader returns a Reader type that satisfies the io.Reader interface. // -// IMPORTANT: when creating a new Reader, the current state of the Msg is taken, as -// basis for the Reader. If you perform changes on Msg after creating the Reader, these -// changes will not be reflected in the Reader. You will have to use Msg.UpdateReader -// first to update the Reader's buffer with the current Msg content +// This method creates a new Reader for the Msg, capturing the current state of the message. +// Any subsequent changes made to the Msg after creating the Reader will not be reflected in the Reader's buffer. +// To reflect these changes in the Reader, you must call Msg.UpdateReader to update the Reader's content with +// the current state of the Msg. +// +// Returns: +// - A pointer to a Reader, which allows the Msg to be read as a stream of bytes. +// +// IMPORTANT: Any changes made to the Msg after creating the Reader will not be reflected in the Reader unless +// Msg.UpdateReader is called. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) NewReader() *Reader { reader := &Reader{} buffer := bytes.Buffer{} @@ -1124,8 +2347,17 @@ func (m *Msg) NewReader() *Reader { return reader } -// UpdateReader will update a Reader with the content of the given Msg and reset the -// Reader position to the start +// UpdateReader updates a Reader with the current content of the Msg and resets the +// Reader's position to the start. +// +// This method rewrites the content of the provided Reader to reflect any changes made to the Msg. +// It resets the Reader's position to the beginning and updates the buffer with the latest message content. +// +// Parameters: +// - reader: A pointer to the Reader that will be updated with the Msg's current content. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) UpdateReader(reader *Reader) { buffer := bytes.Buffer{} _, err := m.Write(&buffer) @@ -1134,14 +2366,27 @@ func (m *Msg) UpdateReader(reader *Reader) { reader.err = err } -// HasSendError returns true if the Msg experienced an error during the message delivery and the -// sendError field of the Msg is not nil +// HasSendError returns true if the Msg experienced an error during message delivery +// and the sendError field of the Msg is not nil. +// +// This method checks whether the message has encountered a delivery error by verifying if the +// sendError field is populated. +// +// Returns: +// - A boolean value indicating whether a send error occurred (true if an error is present). func (m *Msg) HasSendError() bool { return m.sendError != nil } -// SendErrorIsTemp returns true if the Msg experienced an error during the message delivery and the -// corresponding error was of temporary nature and should be retried later +// SendErrorIsTemp returns true if the Msg experienced a delivery error, and the corresponding +// error was of a temporary nature, meaning it can be retried later. +// +// This method checks whether the encountered sendError is a temporary error that can be retried. +// It uses the errors.As function to determine if the error is of type SendError and checks if +// the error is marked as temporary. +// +// Returns: +// - A boolean value indicating whether the send error is temporary (true if the error is temporary). func (m *Msg) SendErrorIsTemp() bool { var err *SendError if errors.As(m.sendError, &err) && err != nil { @@ -1150,18 +2395,104 @@ func (m *Msg) SendErrorIsTemp() bool { return false } -// SendError returns the sendError field of the Msg +// SendError returns the sendError field of the Msg. +// +// This method retrieves the error that occurred during the message delivery process, if any. +// It returns the sendError field, which holds the error encountered during sending. +// +// Returns: +// - The error encountered during message delivery, or nil if no error occurred. func (m *Msg) SendError() error { return m.sendError } +// addAddr adds an additional address to the given addrHeader of the Msg. +// +// This method appends an email address to the specified address header (such as "To", "Cc", or "Bcc") +// without overwriting existing addresses. It first collects the current addresses in the header, then +// adds the new address and updates the header. +// +// Parameters: +// - header: The AddrHeader (e.g., HeaderTo, HeaderCc) to which the address will be added. +// - addr: The email address to add to the specified header. +// +// Returns: +// - An error if the address cannot be added, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 +func (m *Msg) addAddr(header AddrHeader, addr string) error { + var addresses []string + for _, address := range m.addrHeader[header] { + addresses = append(addresses, address.String()) + } + addresses = append(addresses, addr) + return m.SetAddrHeader(header, addresses...) +} + +// appendFile adds a File to the Msg, either as an attachment or an embed. +// +// This method appends a File to the list of files (attachments or embeds) for the message. It applies +// optional FileOption functions to customize the file properties before adding it. If no files are +// already present, a new list is created. +// +// Parameters: +// - files: The current list of files (either attachments or embeds). +// - file: The File to be added. +// - opts: Optional FileOption functions to customize the file. +// +// Returns: +// - A slice of File pointers representing the updated list of files. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 +func (m *Msg) appendFile(files []*File, file *File, opts ...FileOption) []*File { + // Override defaults with optionally provided FileOption functions + for _, opt := range opts { + if opt == nil { + continue + } + opt(file) + } + + if files == nil { + return []*File{file} + } + + return append(files, file) +} + // encodeString encodes a string based on the configured message encoder and the corresponding -// charset for the Msg +// charset for the Msg. +// +// This method encodes the provided string using the message's charset and encoder settings. +// The encoding ensures that the string is properly formatted according to the message's +// character encoding (e.g., UTF-8, ISO-8859-1). +// +// Parameters: +// - str: The string to be encoded. +// +// Returns: +// - The encoded string based on the message's charset and encoder. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) encodeString(str string) string { return m.encoder.Encode(string(m.charset), str) } -// hasAlt returns true if the Msg has more than one part +// hasAlt returns true if the Msg has more than one part. +// +// This method checks whether the message contains more than one part, indicating that +// the message has alternative content (e.g., both plain text and HTML parts). It ignores +// any parts marked as deleted and returns true only if more than one valid part exists +// and no PGP type is set. +// +// Returns: +// - A boolean value indicating whether the message has multiple parts (true if more than one part exists). +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) hasAlt() bool { count := 0 for _, part := range m.parts { @@ -1172,22 +2503,66 @@ func (m *Msg) hasAlt() bool { return count > 1 && m.pgptype == 0 } -// hasMixed returns true if the Msg has mixed parts +// hasMixed returns true if the Msg has mixed parts. +// +// This method checks whether the message contains mixed content, such as attachments along with +// message parts (e.g., text or HTML). A message is considered to have mixed parts if there are both +// attachments and message parts, or if there are multiple attachments. +// +// Returns: +// - A boolean value indicating whether the message has mixed parts. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.3 func (m *Msg) hasMixed() bool { return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1) } -// hasRelated returns true if the Msg has related parts +// hasRelated returns true if the Msg has related parts. +// +// This method checks whether the message contains related parts, such as inline embedded files +// (e.g., images) that are referenced within the message body. A message is considered to have +// related parts if there are both message parts and embedded files, or if there are multiple embedded files. +// +// Returns: +// - A boolean value indicating whether the message has related parts. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2387 func (m *Msg) hasRelated() bool { return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1) } -// hasPGPType returns true if the Msg should be treated as PGP encoded message +// hasPGPType returns true if the Msg should be treated as a PGP-encoded message. +// +// This method checks whether the message is configured to be treated as a PGP-encoded message by examining +// the pgptype field. If the PGP type is set to a value greater than 0, the message is considered PGP-encoded. +// +// Returns: +// - A boolean value indicating whether the message is PGP-encoded. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc4880 func (m *Msg) hasPGPType() bool { return m.pgptype > 0 } -// newPart returns a new Part for the Msg +// newPart returns a new Part for the Msg. +// +// This method creates a new Part for the message with the specified content type, +// using the message's current charset and encoding settings. Optional PartOption +// functions can be applied to customize the Part further. +// +// Parameters: +// - contentType: The content type for the new Part (e.g., text/plain, text/html). +// - opts: Optional PartOption functions to customize the Part. +// +// Returns: +// - A pointer to the newly created Part structure. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) newPart(contentType ContentType, opts ...PartOption) *Part { p := &Part{ contentType: contentType, @@ -1206,13 +2581,26 @@ func (m *Msg) newPart(contentType ContentType, opts ...PartOption) *Part { return p } -// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message +// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message. +// +// This method sets the message's encoder by creating a new mime.WordEncoder that matches the +// current encoding setting (e.g., quoted-printable or base64). The encoder is used to encode +// message headers and body content appropriately. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) setEncoder() { m.encoder = getEncoder(m.encoding) } -// checkUserAgent checks if a useragent/x-mailer is set and if not will set a default -// version string +// checkUserAgent checks if a User-Agent or X-Mailer header is set, and if not, sets a default version string. +// +// This method ensures that the message includes a User-Agent and X-Mailer header, unless the noDefaultUserAgent +// flag is set. If neither of these headers is present, a default User-Agent string with the current library +// version is added. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.7 func (m *Msg) checkUserAgent() { if m.noDefaultUserAgent { return @@ -1225,7 +2613,16 @@ func (m *Msg) checkUserAgent() { } } -// addDefaultHeader sets some default headers, if they haven't been set before +// addDefaultHeader sets default headers if they haven't been set before. +// +// This method ensures that essential headers such as "Date", "Message-ID", and "MIME-Version" are set +// in the message. If these headers are not already present, they will be set to default values. +// The "Date" and "Message-ID" headers are generated, and the "MIME-Version" is set to the message's current setting. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.1 (Date) +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 (Message-ID) +// - https://datatracker.ietf.org/doc/html/rfc2045#section-4 (MIME-Version) func (m *Msg) addDefaultHeader() { if _, ok := m.genHeader[HeaderDate]; !ok { m.SetDate() @@ -1236,7 +2633,22 @@ func (m *Msg) addDefaultHeader() { m.SetGenHeader(HeaderMIMEVersion, string(m.mimever)) } -// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS +// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS. +// +// This method retrieves a file from the embedded filesystem (embed.FS) and returns a File structure +// that can be used as an attachment or embed in the email message. The file's content is read when +// writing to an io.Writer, and the file is identified by its base name. +// +// Parameters: +// - name: The name of the file to retrieve from the embedded filesystem. +// - fs: A pointer to the embed.FS from which the file will be opened. +// +// Returns: +// - A pointer to the File structure representing the embedded file. +// - An error if the file cannot be opened or read from the embedded filesystem. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) { _, err := fs.Open(name) if err != nil { @@ -1260,7 +2672,21 @@ func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) { }, nil } -// fileFromFS returns a File pointer from a given file in the system's file system +// fileFromFS returns a File pointer from a given file in the system's file system. +// +// This method retrieves a file from the system's file system and returns a File structure +// that can be used as an attachment or embed in the email message. The file is identified +// by its base name, and its content is read when writing to an io.Writer. +// +// Parameters: +// - name: The name of the file to retrieve from the system's file system. +// +// Returns: +// - A pointer to the File structure representing the file from the system's file system. +// - Nil if the file does not exist or cannot be accessed. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromFS(name string) *File { _, err := os.Stat(name) if err != nil { @@ -1285,7 +2711,22 @@ func fileFromFS(name string) *File { } } -// fileFromReader returns a File pointer from a given io.Reader +// fileFromReader returns a File pointer from a given io.Reader. +// +// This method reads all data from the provided io.Reader and creates a File structure +// that can be used as an attachment or embed in the email message. The file's content +// is stored in memory and written to an io.Writer when needed. +// +// Parameters: +// - name: The name of the file to be represented by the reader's content. +// - reader: The io.Reader from which the file content will be read. +// +// Returns: +// - A pointer to the File structure representing the content of the io.Reader. +// - An error if the content cannot be read from the io.Reader. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromReader(name string, reader io.Reader) (*File, error) { d, err := io.ReadAll(reader) if err != nil { @@ -1306,7 +2747,21 @@ func fileFromReader(name string, reader io.Reader) (*File, error) { }, nil } -// fileFromReadSeeker returns a File pointer from a given io.ReadSeeker +// fileFromReadSeeker returns a File pointer from a given io.ReadSeeker. +// +// This method creates a File structure from an io.ReadSeeker, allowing efficient handling of file content +// by seeking and reading from the source without fully loading it into memory. The content is written +// to an io.Writer when needed, and the reader's position is reset to the start after writing. +// +// Parameters: +// - name: The name of the file to be represented by the io.ReadSeeker. +// - reader: The io.ReadSeeker from which the file content will be read. +// +// Returns: +// - A pointer to the File structure representing the content of the io.ReadSeeker. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromReadSeeker(name string, reader io.ReadSeeker) *File { return &File{ Name: name, @@ -1322,7 +2777,23 @@ func fileFromReadSeeker(name string, reader io.ReadSeeker) *File { } } -// fileFromHTMLTemplate returns a File pointer form a given html/template.Template +// fileFromHTMLTemplate returns a File pointer from a given html/template.Template. +// +// This method executes the provided HTML template with the given data and creates a File structure +// representing the output. The rendered template content is stored in a buffer and then processed +// as a file attachment or embed. +// +// Parameters: +// - name: The name of the file to be created from the template output. +// - tpl: A pointer to the html/template.Template to be executed. +// - data: The data to populate the template. +// +// Returns: +// - A pointer to the File structure representing the rendered template. +// - An error if the template is nil or if it fails to execute. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromHTMLTemplate(name string, tpl *ht.Template, data interface{}) (*File, error) { if tpl == nil { return nil, errors.New(errTplPointerNil) @@ -1334,7 +2805,23 @@ func fileFromHTMLTemplate(name string, tpl *ht.Template, data interface{}) (*Fil return fileFromReader(name, &buffer) } -// fileFromTextTemplate returns a File pointer form a given text/template.Template +// fileFromTextTemplate returns a File pointer from a given text/template.Template. +// +// This method executes the provided text template with the given data and creates a File structure +// representing the output. The rendered template content is stored in a buffer and then processed +// as a file attachment or embed. +// +// Parameters: +// - name: The name of the file to be created from the template output. +// - tpl: A pointer to the text/template.Template to be executed. +// - data: The data to populate the template. +// +// Returns: +// - A pointer to the File structure representing the rendered template. +// - An error if the template is nil or if it fails to execute. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromTextTemplate(name string, tpl *tt.Template, data interface{}) (*File, error) { if tpl == nil { return nil, errors.New(errTplPointerNil) @@ -1346,7 +2833,19 @@ func fileFromTextTemplate(name string, tpl *tt.Template, data interface{}) (*Fil return fileFromReader(name, &buffer) } -// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message +// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message. +// +// This function returns a mime.WordEncoder based on the specified encoding (e.g., quoted-printable or base64). +// The encoder is used for encoding message headers and body content according to the chosen encoding standard. +// +// Parameters: +// - enc: The Encoding type for the message (e.g., EncodingQP for quoted-printable or EncodingB64 for base64). +// +// Returns: +// - A mime.WordEncoder based on the encoding setting. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047 func getEncoder(enc Encoding) mime.WordEncoder { switch enc { case EncodingQP: @@ -1358,8 +2857,21 @@ func getEncoder(enc Encoding) mime.WordEncoder { } } -// writeFuncFromBuffer is a common method to convert a byte buffer into a writeFunc as -// often required by this library +// writeFuncFromBuffer converts a byte buffer into a writeFunc, which is commonly required by go-mail. +// +// This function wraps a byte buffer into a write function that can be used to write the buffer's content +// to an io.Writer. It returns a function that writes the buffer's content to the given writer and returns +// the number of bytes written and any error that occurred during writing. +// +// Parameters: +// - buffer: A pointer to the bytes.Buffer containing the data to be written. +// +// Returns: +// - A function that writes the buffer's content to an io.Writer, returning the number of bytes written +// and any error encountered during the write operation. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func writeFuncFromBuffer(buffer *bytes.Buffer) func(io.Writer) (int64, error) { writeFunc := func(w io.Writer) (int64, error) { numBytes, err := w.Write(buffer.Bytes()) diff --git a/msg_totmpfile.go b/msg_totmpfile.go index 44c427d..b7d2ada 100644 --- a/msg_totmpfile.go +++ b/msg_totmpfile.go @@ -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 { diff --git a/msg_totmpfile_116.go b/msg_totmpfile_116.go index 4611106..bb93411 100644 --- a/msg_totmpfile_116.go +++ b/msg_totmpfile_116.go @@ -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 { diff --git a/msgwriter.go b/msgwriter.go index ff1b47b..ed2b41e 100644 --- a/msgwriter.go +++ b/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 diff --git a/part.go b/part.go index 7c76b7d..5be8059 100644 --- a/part.go +++ b/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 diff --git a/random.go b/random.go index 3a3f16b..2478c75 100644 --- a/random.go +++ b/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< 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' } diff --git a/smtp/smtp.go b/smtp/smtp.go index f9961c9..d713f8c 100644 --- a/smtp/smtp.go +++ b/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 diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 451a349..4fd32eb 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -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()) } diff --git a/tls.go b/tls.go index bb7ad14..3adc0cc 100644 --- a/tls.go +++ b/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: