Compare commits

..

18 commits

Author SHA1 Message Date
3d5435c138
Update EnvelopeFromFormat documentation in msg.go
Expanded the documentation for the EnvelopeFromFormat method to clarify its purpose, usage, and compliance with RFC 5322. Added details on how the envelope from address is used in SMTP communication and validation requirements.
2024-10-05 13:59:24 +02:00
1dcdad9da1
Enhance address header methods with detailed documentation
Updated comments in `header.go` and `msg.go` to provide more detailed explanations and references to RFC 5322. This improves clarity on how headers are set and utilized, and the conditions under which they operate.
2024-10-05 13:56:47 +02:00
78e2857782
Update method comments to include additional context and RFC references
Enhanced the comments for various methods in `msg.go`, `client.go`, `auth.go`, and `encoding.go` to provide more detailed explanations, context, and relevant RFC references. This improves the clarity and maintainability of the code by providing developers with a deeper understanding of each method's purpose and usage.
2024-10-05 13:39:21 +02:00
c186cba2c2
Refactor MsgOption comments for clarity
Revised the comments for MsgOption functions to provide clearer explanations of their purpose and usage. Added detailed descriptions for options such as WithMIMEVersion and WithBoundary to clarify their contexts and constraints.
2024-10-05 12:48:09 +02:00
682f7a6ca5
Clarify Msg struct field comments
Updated comments for all fields in the Msg struct to provide clearer and more detailed explanations. This includes specifying the data type for each field and the role they play within the Msg struct, making the code easier to understand and maintain.
2024-10-05 12:35:15 +02:00
cd4c0194dc
Refactor comments for clarity and detail
Enhanced the descriptive comments for error variables, constants, and types to provide clearer and more detailed explanations. This improves code readability and ensures that the purpose and usage of different elements are better understood by developers.
2024-10-05 12:15:01 +02:00
a820ba3cee
Correct typo in README.md
Fixed a typo in the README where `io.WriteTo` was incorrectly spelled instead of `io.WriterTo`. This ensures the documentation correctly reflects the interfaces implemented by the Message object.
2024-10-05 12:14:00 +02:00
96466facdd
Refactor and document header types and importance levels.
Updated type declarations for headers and importance to clarify their roles in the Msg package. Added detailed inline comments and RFC links for better documentation and understanding. Enhanced string representation methods to explicitly handle importance levels and header types.
2024-10-05 12:00:10 +02:00
493f8fc657
Add periods to charset comments
Updated the comment lines for various charset constants to include ending periods for consistency and better readability. This change enhances code documentation quality without altering any functional behavior.
2024-10-05 11:54:37 +02:00
94f47d4369
Update crypto and text libraries
Upgraded golang.org/x/crypto to v0.28.0 and golang.org/x/text to v0.19.0. These updates improve security and compatibility with recent changes in Go modules. Ensure to run `go mod tidy` to clean up any unused dependencies.
2024-10-05 11:47:15 +02:00
476130d6e3
Fumpt files to make golangci-lint happy 2024-10-05 11:43:50 +02:00
a0a7f74121
Refactor file.go comments for clarity and detail
Improved comments for better readability and understanding. Enhanced descriptions for File, FileOption, and various methods, providing more context and precision. Added notes on default behaviors and specific use cases for methods where applicable.
2024-10-05 11:42:21 +02:00
ecd0bff5ad
Update comments for better clarity and add RFC references
Revised the comments to provide more detailed descriptions and context for each type and constant. Additionally, included relevant RFC document references where applicable to improve understanding of encoding and MIME types.
2024-10-05 11:23:44 +02:00
5653df373b
Add periods to end of go doc comments
Ensure all go doc comments in eml.go have consistent punctuation by adding periods to the end of each comment. This improves code readability and maintains uniformity in the documentation style.
2024-10-05 11:11:17 +02:00
869e8db6c5
Improve package documentation and description
Expand the package mail description to highlight ease of use, reliance on the Go Standard Library, and added functionalities. Clarify the role of VERSION in the user agent string.
2024-10-05 11:06:46 +02:00
fa3c6f956e
Update Send function documentation for better clarity
Enhanced the documentation of the `Send` function to explicitly describe the behavior when the Client has no active connection and the handling of multiple message transmissions. This ensures developers understand the error handling mechanism and the association of `SendError` with each message.
2024-10-05 11:02:26 +02:00
159c1bf850
Add tests for new tls and connection handling methods
This commit introduces tests for various TLS-related methods such as GetTLSConnectionState, HasConnection, SetDSNMailReturnOption, SetDSNRcptNotifyOption, and UpdateDeadline. It also modifies the error handling logic in smtp.go to include new error types and improves the mutex handling in UpdateDeadline.
2024-10-05 10:55:25 +02:00
9163943684
Add isConnected flag to track active connection state
Introduced the isConnected boolean flag in the Client struct to clearly indicate whether there is an active connection. Updated relevant methods to set this flag accordingly, ensuring consistent state management across the Client's lifecycle.
2024-10-05 10:15:43 +02:00
15 changed files with 768 additions and 252 deletions

View file

@ -52,7 +52,7 @@ Here are some highlights of go-mail's featureset:
* [X] Support sending mails via a local sendmail command * [X] Support sending mails via a local sendmail command
* [X] Support for requestng MDNs (RFC 8098) and DSNs (RFC 1891) * [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] 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] 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] 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 * [X] Debug logging of SMTP traffic

20
auth.go
View file

@ -20,6 +20,7 @@ const (
// //
// It was recommended to deprecate the standard in 20 November 2008. As an alternative it // 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. // 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 // https://datatracker.ietf.org/doc/html/draft-ietf-sasl-crammd5-to-historic-00.html
SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5" SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"
@ -33,12 +34,14 @@ const (
// does not have an official RFC that could be followed. There is a spec by Microsoft and an // 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 // IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
// automatically matches the MS spec. // automatically matches the MS spec.
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
// //
// Since the "LOGIN" SASL authentication mechansim transmits the username and password in // 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 // plaintext over the internet connection, we only allow this mechanism over a TLS secured
// connection. // 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" SMTPAuthLogin SMTPAuthType = "LOGIN"
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience // SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
@ -47,11 +50,12 @@ const (
SMTPAuthNoAuth SMTPAuthType = "" SMTPAuthNoAuth SMTPAuthType = ""
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616. // SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
// https://datatracker.ietf.org/doc/html/rfc4616/
// //
// Since the "PLAIN" SASL authentication mechansim transmits the username and password in // 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 // plaintext over the internet connection, we only allow this mechanism over a TLS secured
// connection. // connection.
//
// https://datatracker.ietf.org/doc/html/rfc4616/
SMTPAuthPlain SMTPAuthType = "PLAIN" SMTPAuthPlain SMTPAuthType = "PLAIN"
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism. // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
@ -59,16 +63,16 @@ const (
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
// SMTPAuthSCRAMSHA1 is the "SCRAM-SHA-1" SASL authentication mechanism as described in RFC 5802. // SMTPAuthSCRAMSHA1 is the "SCRAM-SHA-1" SASL authentication mechanism as described in RFC 5802.
// https://datatracker.ietf.org/doc/html/rfc5802
// //
// SCRAM-SHA-1 is still considered secure for certain applications, particularly when used as part // 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 // 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 // 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. // vulnerabilities in other contexts, although it remains effective in HMAC constructions.
//
// https://datatracker.ietf.org/doc/html/rfc5802
SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1"
// SMTPAuthSCRAMSHA1PLUS is the "SCRAM-SHA-1-PLUS" SASL authentication mechanism as described in RFC 5802. // SMTPAuthSCRAMSHA1PLUS is the "SCRAM-SHA-1-PLUS" SASL authentication mechanism as described in RFC 5802.
// https://datatracker.ietf.org/doc/html/rfc5802
// //
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and // 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 // to guarantee that the integrity of the transport layer is preserved throughout the authentication
@ -78,18 +82,22 @@ const (
// of a challenge-response authentication mechanism (as we use it). However, it is generally // 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 // 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. // vulnerabilities in other contexts, although it remains effective in HMAC constructions.
//
// https://datatracker.ietf.org/doc/html/rfc5802
SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS"
// SMTPAuthSCRAMSHA256 is the "SCRAM-SHA-256" SASL authentication mechanism as described in RFC 7677. // SMTPAuthSCRAMSHA256 is the "SCRAM-SHA-256" SASL authentication mechanism as described in RFC 7677.
//
// https://datatracker.ietf.org/doc/html/rfc7677 // https://datatracker.ietf.org/doc/html/rfc7677
SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256"
// SMTPAuthSCRAMSHA256PLUS is the "SCRAM-SHA-256-PLUS" SASL authentication mechanism as described in RFC 7677. // SMTPAuthSCRAMSHA256PLUS is the "SCRAM-SHA-256-PLUS" SASL authentication mechanism as described in RFC 7677.
// https://datatracker.ietf.org/doc/html/rfc7677
// //
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and // 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 // 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. // 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" SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
) )

View file

@ -44,26 +44,31 @@ const (
// DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in // DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in
// a DSN (Delivery Status Notification). // a DSN (Delivery Status Notification).
//
// https://datatracker.ietf.org/doc/html/rfc1891#section-5.3 // https://datatracker.ietf.org/doc/html/rfc1891#section-5.3
DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS" DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS"
// DSNMailReturnFull requests that the entire mail message is returned in any failed DSN // DSNMailReturnFull requests that the entire mail message is returned in any failed DSN
// (Delivery Status Notification) issued for this recipient. // (Delivery Status Notification) issued for this recipient.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.3 // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.3
DSNMailReturnFull DSNMailReturnOption = "FULL" DSNMailReturnFull DSNMailReturnOption = "FULL"
// DSNRcptNotifyNever indicates that no DSN (Delivery Status Notifications) should be sent for the // DSNRcptNotifyNever indicates that no DSN (Delivery Status Notifications) should be sent for the
// recipient under any condition. // recipient under any condition.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER" DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER"
// DSNRcptNotifySuccess indicates that the sender requests a DSN (Delivery Status Notification) if the // DSNRcptNotifySuccess indicates that the sender requests a DSN (Delivery Status Notification) if the
// message is successfully delivered. // message is successfully delivered.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS" DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS"
// DSNRcptNotifyFailure requests that a DSN (Delivery Status Notification) is issued if delivery of // DSNRcptNotifyFailure requests that a DSN (Delivery Status Notification) is issued if delivery of
// a message fails. // a message fails.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyFailure DSNRcptNotifyOption = "FAILURE" DSNRcptNotifyFailure DSNRcptNotifyOption = "FAILURE"
@ -73,6 +78,7 @@ const (
// (as determined by the MTA at which the message is delayed), but the final delivery status (whether // (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 // 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. // requests that a "delayed" DSN NOT be issued under any conditions.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY" DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY"
) )
@ -87,11 +93,13 @@ type (
// DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested // DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested
// in a Delivery Status Notification (DSN). // in a Delivery Status Notification (DSN).
//
// https://datatracker.ietf.org/doc/html/rfc1891/ // https://datatracker.ietf.org/doc/html/rfc1891/
DSNMailReturnOption string DSNMailReturnOption string
// DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a // DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a
// recipient in DSNs. // recipient in DSNs.
//
// https://datatracker.ietf.org/doc/html/rfc1891/ // https://datatracker.ietf.org/doc/html/rfc1891/
DSNRcptNotifyOption string DSNRcptNotifyOption string
@ -167,6 +175,7 @@ type (
smtpClient *smtp.Client smtpClient *smtp.Client
// tlspolicy defines the TLSPolicy configuration the Client uses for the STARTTLS protocol. // tlspolicy defines the TLSPolicy configuration the Client uses for the STARTTLS protocol.
//
// https://datatracker.ietf.org/doc/html/rfc3207#section-2 // https://datatracker.ietf.org/doc/html/rfc3207#section-2
tlspolicy TLSPolicy tlspolicy TLSPolicy
@ -180,6 +189,7 @@ type (
user string user string
// useSSL indicates whether to use SSL/TLS encryption for network communication. // useSSL indicates whether to use SSL/TLS encryption for network communication.
//
// https://datatracker.ietf.org/doc/html/rfc8314 // https://datatracker.ietf.org/doc/html/rfc8314
useSSL bool useSSL bool
} }
@ -424,10 +434,11 @@ func WithPassword(password string) Option {
// WithDSN enables DSN (Delivery Status Notifications) for the Client as described in the RFC 1891. DSN // WithDSN enables DSN (Delivery Status Notifications) for the Client as described in the RFC 1891. DSN
// only work if the server supports them. // only work if the server supports them.
// https://datatracker.ietf.org/doc/html/rfc1891
// //
// By default we set DSNMailReturnOption to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess // By default we set DSNMailReturnOption to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess
// and DSNRcptNotifyFailure. // and DSNRcptNotifyFailure.
//
// https://datatracker.ietf.org/doc/html/rfc1891
func WithDSN() Option { func WithDSN() Option {
return func(c *Client) error { return func(c *Client) error {
c.requestDSN = true c.requestDSN = true
@ -439,9 +450,10 @@ func WithDSN() Option {
// WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in the // WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in the
// RFC 1891. DSN only work if the server supports them. // RFC 1891. DSN only work if the server supports them.
// https://datatracker.ietf.org/doc/html/rfc1891
// //
// It will set the DSNMailReturnOption to the provided value. // It will set the DSNMailReturnOption to the provided value.
//
// https://datatracker.ietf.org/doc/html/rfc1891
func WithDSNMailReturnType(option DSNMailReturnOption) Option { func WithDSNMailReturnType(option DSNMailReturnOption) Option {
return func(c *Client) error { return func(c *Client) error {
switch option { switch option {
@ -459,9 +471,10 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option {
// WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in the // WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in the
// RFC 1891. DSN only work if the server supports them. // RFC 1891. DSN only work if the server supports them.
// https://datatracker.ietf.org/doc/html/rfc1891
// //
// It will set the DSNRcptNotifyOption to the provided values. // It will set the DSNRcptNotifyOption to the provided values.
//
// https://datatracker.ietf.org/doc/html/rfc1891
func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
return func(c *Client) error { return func(c *Client) error {
var rcptOpts []string var rcptOpts []string

View file

@ -9,7 +9,10 @@ package mail
import "errors" 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 to the Msg in case there of a transmission or delivery
// error.
func (c *Client) Send(messages ...*Msg) error { func (c *Client) Send(messages ...*Msg) error {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}

View file

@ -11,7 +11,10 @@ import (
"errors" "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 to the Msg in case there of a transmission or delivery
// error.
func (c *Client) Send(messages ...*Msg) (returnErr error) { func (c *Client) Send(messages ...*Msg) (returnErr error) {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}

9
doc.go
View file

@ -2,8 +2,13 @@
// //
// SPDX-License-Identifier: MIT // 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 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" const VERSION = "0.4.4"

36
eml.go
View file

@ -18,14 +18,13 @@ import (
"strings" "strings"
) )
// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer // EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer.
func EMLToMsgFromString(emlString string) (*Msg, error) { func EMLToMsgFromString(emlString string) (*Msg, error) {
eb := bytes.NewBufferString(emlString) eb := bytes.NewBufferString(emlString)
return EMLToMsgFromReader(eb) return EMLToMsgFromReader(eb)
} }
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled // EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled Msg pointer.
// Msg pointer
func EMLToMsgFromReader(reader io.Reader) (*Msg, error) { func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
msg := &Msg{ msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address), addrHeader: make(map[AddrHeader][]*netmail.Address),
@ -46,8 +45,7 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
return msg, nil return msg, nil
} }
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a // EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a pre-filled Msg pointer.
// pre-filled Msg pointer
func EMLToMsgFromFile(filePath string) (*Msg, error) { func EMLToMsgFromFile(filePath string) (*Msg, error) {
msg := &Msg{ msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address), addrHeader: make(map[AddrHeader][]*netmail.Address),
@ -68,7 +66,7 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
return msg, nil 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.
func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil { if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
return fmt.Errorf("failed to parse EML headers: %w", err) return fmt.Errorf("failed to parse EML headers: %w", err)
@ -79,7 +77,7 @@ func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error
return nil 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.
func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) { func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
fileHandle, err := os.Open(filePath) fileHandle, err := os.Open(filePath)
if err != nil { if err != nil {
@ -91,7 +89,7 @@ func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
return readEMLFromReader(fileHandle) 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.
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) { func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
parsedMsg, err := netmail.ReadMessage(reader) parsedMsg, err := netmail.ReadMessage(reader)
if err != nil { if err != nil {
@ -106,8 +104,8 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error
return parsedMsg, &buf, nil return parsedMsg, &buf, nil
} }
// parseEMLHeaders will check the EML headers for the most common headers and set the // parseEMLHeaders will check the EML headers for the most common headers and set the according settings
// according settings in the Msg // in the Msg.
func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
commonHeaders := []Header{ commonHeaders := []Header{
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
@ -175,7 +173,7 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
return nil return nil
} }
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings // parseEMLBodyParts parses the body of a EML based on the different content types and encodings.
func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
// Extract the transfer encoding of the body // Extract the transfer encoding of the body
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String())) mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
@ -212,10 +210,11 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
return nil return nil
} }
// parseEMLBodyPlain parses the mail body of plain type mails // parseEMLBodyPlain parses the mail body of plain type mails.
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String()) 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()) { if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
msg.SetEncoding(EncodingUSASCII) msg.SetEncoding(EncodingUSASCII)
msg.SetBodyString(ContentType(mediatype), bodybuf.String()) msg.SetBodyString(ContentType(mediatype), bodybuf.String())
@ -349,7 +348,7 @@ ReadNextPart:
return nil return nil
} }
// parseEMLEncoding parses and determines the encoding of the message // parseEMLEncoding parses and determines the encoding of the message.
func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) { func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
switch { switch {
@ -363,7 +362,7 @@ 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.
func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) { func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
if value := mailHeader.Get(HeaderContentType.String()); value != "" { if value := mailHeader.Get(HeaderContentType.String()); value != "" {
contentType, optional := parseMultiPartHeader(value) contentType, optional := parseMultiPartHeader(value)
@ -377,7 +376,7 @@ 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.
func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
part.SetEncoding(EncodingB64) part.SetEncoding(EncodingB64)
content, err := base64.StdEncoding.DecodeString(string(multiPartData)) content, err := base64.StdEncoding.DecodeString(string(multiPartData))
@ -388,8 +387,7 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
return nil return nil
} }
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as // parseMultiPartHeader parses a multipart header and returns the value and optional parts as separate map.
// separate map
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) { func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
optional = make(map[string]string) optional = make(map[string]string)
headerSplit := strings.SplitN(multiPartHeader, ";", 2) headerSplit := strings.SplitN(multiPartHeader, ";", 2)
@ -404,7 +402,7 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s
return return
} }
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed // parseEMLAttachmentEmbed parses a multipart that is an attachment or embed.
func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error { func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error {
cdType, optional := parseMultiPartHeader(contentDisposition[0]) cdType, optional := parseMultiPartHeader(contentDisposition[0])
filename := "generic.attachment" filename := "generic.attachment"

View file

@ -4,171 +4,198 @@
package mail 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 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 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 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 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 type MIMEType string
// List of supported encodings
const ( const (
// EncodingB64 represents the Base64 encoding as specified in RFC 2045. // EncodingB64 represents the Base64 encoding as specified in RFC 2045.
//
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
EncodingB64 Encoding = "base64" EncodingB64 Encoding = "base64"
// EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045. // 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" EncodingQP Encoding = "quoted-printable"
// EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit) // EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit)
//
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.7
EncodingUSASCII Encoding = "7bit" 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" NoEncoding Encoding = "8bit"
) )
// List of common charsets
const ( const (
// CharsetUTF7 represents the "UTF-7" charset // CharsetUTF7 represents the "UTF-7" charset.
CharsetUTF7 Charset = "UTF-7" CharsetUTF7 Charset = "UTF-7"
// CharsetUTF8 represents the "UTF-8" charset // CharsetUTF8 represents the "UTF-8" charset.
CharsetUTF8 Charset = "UTF-8" CharsetUTF8 Charset = "UTF-8"
// CharsetASCII represents the "US-ASCII" charset // CharsetASCII represents the "US-ASCII" charset.
CharsetASCII Charset = "US-ASCII" CharsetASCII Charset = "US-ASCII"
// CharsetISO88591 represents the "ISO-8859-1" charset // CharsetISO88591 represents the "ISO-8859-1" charset.
CharsetISO88591 Charset = "ISO-8859-1" CharsetISO88591 Charset = "ISO-8859-1"
// CharsetISO88592 represents the "ISO-8859-2" charset // CharsetISO88592 represents the "ISO-8859-2" charset.
CharsetISO88592 Charset = "ISO-8859-2" CharsetISO88592 Charset = "ISO-8859-2"
// CharsetISO88593 represents the "ISO-8859-3" charset // CharsetISO88593 represents the "ISO-8859-3" charset.
CharsetISO88593 Charset = "ISO-8859-3" CharsetISO88593 Charset = "ISO-8859-3"
// CharsetISO88594 represents the "ISO-8859-4" charset // CharsetISO88594 represents the "ISO-8859-4" charset.
CharsetISO88594 Charset = "ISO-8859-4" CharsetISO88594 Charset = "ISO-8859-4"
// CharsetISO88595 represents the "ISO-8859-5" charset // CharsetISO88595 represents the "ISO-8859-5" charset.
CharsetISO88595 Charset = "ISO-8859-5" CharsetISO88595 Charset = "ISO-8859-5"
// CharsetISO88596 represents the "ISO-8859-6" charset // CharsetISO88596 represents the "ISO-8859-6" charset.
CharsetISO88596 Charset = "ISO-8859-6" CharsetISO88596 Charset = "ISO-8859-6"
// CharsetISO88597 represents the "ISO-8859-7" charset // CharsetISO88597 represents the "ISO-8859-7" charset.
CharsetISO88597 Charset = "ISO-8859-7" CharsetISO88597 Charset = "ISO-8859-7"
// CharsetISO88599 represents the "ISO-8859-9" charset // CharsetISO88599 represents the "ISO-8859-9" charset.
CharsetISO88599 Charset = "ISO-8859-9" CharsetISO88599 Charset = "ISO-8859-9"
// CharsetISO885913 represents the "ISO-8859-13" charset // CharsetISO885913 represents the "ISO-8859-13" charset.
CharsetISO885913 Charset = "ISO-8859-13" CharsetISO885913 Charset = "ISO-8859-13"
// CharsetISO885914 represents the "ISO-8859-14" charset // CharsetISO885914 represents the "ISO-8859-14" charset.
CharsetISO885914 Charset = "ISO-8859-14" CharsetISO885914 Charset = "ISO-8859-14"
// CharsetISO885915 represents the "ISO-8859-15" charset // CharsetISO885915 represents the "ISO-8859-15" charset.
CharsetISO885915 Charset = "ISO-8859-15" CharsetISO885915 Charset = "ISO-8859-15"
// CharsetISO885916 represents the "ISO-8859-16" charset // CharsetISO885916 represents the "ISO-8859-16" charset.
CharsetISO885916 Charset = "ISO-8859-16" CharsetISO885916 Charset = "ISO-8859-16"
// CharsetISO2022JP represents the "ISO-2022-JP" charset // CharsetISO2022JP represents the "ISO-2022-JP" charset.
CharsetISO2022JP Charset = "ISO-2022-JP" CharsetISO2022JP Charset = "ISO-2022-JP"
// CharsetISO2022KR represents the "ISO-2022-KR" charset // CharsetISO2022KR represents the "ISO-2022-KR" charset.
CharsetISO2022KR Charset = "ISO-2022-KR" CharsetISO2022KR Charset = "ISO-2022-KR"
// CharsetWindows1250 represents the "windows-1250" charset // CharsetWindows1250 represents the "windows-1250" charset.
CharsetWindows1250 Charset = "windows-1250" CharsetWindows1250 Charset = "windows-1250"
// CharsetWindows1251 represents the "windows-1251" charset // CharsetWindows1251 represents the "windows-1251" charset.
CharsetWindows1251 Charset = "windows-1251" CharsetWindows1251 Charset = "windows-1251"
// CharsetWindows1252 represents the "windows-1252" charset // CharsetWindows1252 represents the "windows-1252" charset.
CharsetWindows1252 Charset = "windows-1252" CharsetWindows1252 Charset = "windows-1252"
// CharsetWindows1255 represents the "windows-1255" charset // CharsetWindows1255 represents the "windows-1255" charset.
CharsetWindows1255 Charset = "windows-1255" CharsetWindows1255 Charset = "windows-1255"
// CharsetWindows1256 represents the "windows-1256" charset // CharsetWindows1256 represents the "windows-1256" charset.
CharsetWindows1256 Charset = "windows-1256" CharsetWindows1256 Charset = "windows-1256"
// CharsetKOI8R represents the "KOI8-R" charset // CharsetKOI8R represents the "KOI8-R" charset.
CharsetKOI8R Charset = "KOI8-R" CharsetKOI8R Charset = "KOI8-R"
// CharsetKOI8U represents the "KOI8-U" charset // CharsetKOI8U represents the "KOI8-U" charset.
CharsetKOI8U Charset = "KOI8-U" CharsetKOI8U Charset = "KOI8-U"
// CharsetBig5 represents the "Big5" charset // CharsetBig5 represents the "Big5" charset.
CharsetBig5 Charset = "Big5" CharsetBig5 Charset = "Big5"
// CharsetGB18030 represents the "GB18030" charset // CharsetGB18030 represents the "GB18030" charset.
CharsetGB18030 Charset = "GB18030" CharsetGB18030 Charset = "GB18030"
// CharsetGB2312 represents the "GB2312" charset // CharsetGB2312 represents the "GB2312" charset.
CharsetGB2312 Charset = "GB2312" CharsetGB2312 Charset = "GB2312"
// CharsetTIS620 represents the "TIS-620" charset // CharsetTIS620 represents the "TIS-620" charset.
CharsetTIS620 Charset = "TIS-620" CharsetTIS620 Charset = "TIS-620"
// CharsetEUCKR represents the "EUC-KR" charset // CharsetEUCKR represents the "EUC-KR" charset.
CharsetEUCKR Charset = "EUC-KR" CharsetEUCKR Charset = "EUC-KR"
// CharsetShiftJIS represents the "Shift_JIS" charset // CharsetShiftJIS represents the "Shift_JIS" charset.
CharsetShiftJIS Charset = "Shift_JIS" CharsetShiftJIS Charset = "Shift_JIS"
// CharsetUnknown represents the "Unknown" charset // CharsetUnknown represents the "Unknown" charset.
CharsetUnknown Charset = "Unknown" CharsetUnknown Charset = "Unknown"
// CharsetGBK represents the "GBK" charset // CharsetGBK represents the "GBK" charset.
CharsetGBK Charset = "GBK" CharsetGBK Charset = "GBK"
) )
// List of MIME versions // MIME10 represents the MIME version "1.0" used in email messages.
const ( const MIME10 MIMEVersion = "1.0"
// MIME10 is the MIME Version 1.0
MIME10 MIMEVersion = "1.0"
)
// List of common content types
const ( const (
// TypeAppOctetStream represents the MIME type for arbitrary binary data.
TypeAppOctetStream ContentType = "application/octet-stream" TypeAppOctetStream ContentType = "application/octet-stream"
// TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative
// formats.
TypeMultipartAlternative ContentType = "multipart/alternative" TypeMultipartAlternative ContentType = "multipart/alternative"
// TypeMultipartMixed represents the MIME type for a multipart message containing different parts.
TypeMultipartMixed ContentType = "multipart/mixed" 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" TypeMultipartRelated ContentType = "multipart/related"
// TypePGPSignature represents the MIME type for PGP signed messages.
TypePGPSignature ContentType = "application/pgp-signature" TypePGPSignature ContentType = "application/pgp-signature"
// TypePGPEncrypted represents the MIME type for PGP encrypted messages.
TypePGPEncrypted ContentType = "application/pgp-encrypted" TypePGPEncrypted ContentType = "application/pgp-encrypted"
// TypeTextHTML represents the MIME type for HTML text content.
TypeTextHTML ContentType = "text/html" TypeTextHTML ContentType = "text/html"
// TypeTextPlain represents the MIME type for plain text content.
TypeTextPlain ContentType = "text/plain" TypeTextPlain ContentType = "text/plain"
) )
// List of MIMETypes
const ( const (
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
MIMEAlternative MIMEType = "alternative" MIMEAlternative MIMEType = "alternative"
// MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content.
MIMEMixed MIMEType = "mixed" MIMEMixed MIMEType = "mixed"
// MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities.
MIMERelated MIMEType = "related" 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.
func (c Charset) String() string { func (c Charset) String() string {
return string(c) 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.
func (c ContentType) String() string { func (c ContentType) String() string {
return string(c) 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.
func (e Encoding) String() string { func (e Encoding) String() string {
return string(e) return string(e)
} }

38
file.go
View file

@ -9,10 +9,11 @@ import (
"net/textproto" "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) type FileOption func(*File)
// File is an attachment or embedded file of the Msg // File represents a file with properties like content type, description, encoding, headers, name, and
// writer function. This can either be an attachment or an embedded file for a Msg.
type File struct { type File struct {
ContentType ContentType ContentType ContentType
Desc string Desc string
@ -22,32 +23,35 @@ type File struct {
Writer func(w io.Writer) (int64, error) 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.
func WithFileContentID(id string) FileOption { func WithFileContentID(id string) FileOption {
return func(f *File) { return func(f *File) {
f.Header.Set(HeaderContentID.String(), id) f.Header.Set(HeaderContentID.String(), id)
} }
} }
// WithFileName sets the filename of the File // WithFileName sets the name of a File to the provided value.
func WithFileName(name string) FileOption { func WithFileName(name string) FileOption {
return func(f *File) { return func(f *File) {
f.Name = name f.Name = name
} }
} }
// WithFileDescription sets an optional file description of the File that will be // WithFileDescription sets an optional file description for the File. The description is used in the
// added as Content-Description part // Content-Description header of the MIME output.
func WithFileDescription(description string) FileOption { func WithFileDescription(description string) FileOption {
return func(f *File) { return func(f *File) {
f.Desc = description f.Desc = description
} }
} }
// WithFileEncoding sets the encoding of the File. By default we should always use // WithFileEncoding sets the encoding type for a file.
// 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 // By default one should always use Base64 encoding for attachments and embeds, but there might be exceptions in
// is provided as argument, the function will automatically override back to Base64 // which this might come handy.
//
// Note: that quoted-printable must never be used for attachments or embeds. If EncodingQP is provided as encoding
// to this method, it will be automatically overwritten with EncodingB64.
func WithFileEncoding(encoding Encoding) FileOption { func WithFileEncoding(encoding Encoding) FileOption {
return func(f *File) { return func(f *File) {
if encoding == EncodingQP { if encoding == EncodingQP {
@ -58,23 +62,23 @@ func WithFileEncoding(encoding Encoding) FileOption {
} }
// WithFileContentType sets the content type of the File. // 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 // By default we will try to guess the file type and its corresponding content type and fall back to
// could not be guessed. In some cases, however, it might be needed to force // application/octet-stream if the file type, if no matching type could be guessed. This FileOption can
// this to a specific type. For such situations this override method can // be used to override this type, in case a specific type is required.
// be used
func WithFileContentType(contentType ContentType) FileOption { func WithFileContentType(contentType ContentType) FileOption {
return func(f *File) { return func(f *File) {
f.ContentType = contentType f.ContentType = contentType
} }
} }
// setHeader sets header fields to a File // setHeader sets the value of a given MIME header field for the File.
func (f *File) setHeader(header Header, value string) { func (f *File) setHeader(header Header, value string) {
f.Header.Set(string(header), value) f.Header.Set(string(header), value)
} }
// getHeader return header fields of a File // getHeader retrieves the value of the specified MIME header field. It returns the header value and a boolean
// indicating whether the header was present or not.
func (f *File) getHeader(header Header) (string, bool) { func (f *File) getHeader(header Header) (string, bool) {
v := f.Header.Get(string(header)) v := f.Header.Get(string(header))
return v, v != "" return v, v != ""

4
go.mod
View file

@ -7,6 +7,6 @@ module github.com/wneessen/go-mail
go 1.16 go 1.16
require ( require (
golang.org/x/crypto v0.27.0 golang.org/x/crypto v0.28.0
golang.org/x/text v0.18.0 golang.org/x/text v0.19.0
) )

12
go.sum
View file

@ -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.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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.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.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.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.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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

108
header.go
View file

@ -4,129 +4,140 @@
package mail 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 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 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 type Importance int
// List of common generic header field names
const ( const (
// HeaderContentDescription is the "Content-Description" header // HeaderContentDescription is the "Content-Description" header.
HeaderContentDescription Header = "Content-Description" HeaderContentDescription Header = "Content-Description"
// HeaderContentDisposition is the "Content-Disposition" header // HeaderContentDisposition is the "Content-Disposition" header.
HeaderContentDisposition Header = "Content-Disposition" HeaderContentDisposition Header = "Content-Disposition"
// HeaderContentID is the "Content-ID" header // HeaderContentID is the "Content-ID" header.
HeaderContentID Header = "Content-ID" HeaderContentID Header = "Content-ID"
// HeaderContentLang is the "Content-Language" header // HeaderContentLang is the "Content-Language" header.
HeaderContentLang Header = "Content-Language" 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" HeaderContentLocation Header = "Content-Location"
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header // HeaderContentTransferEnc is the "Content-Transfer-Encoding" header.
HeaderContentTransferEnc Header = "Content-Transfer-Encoding" HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
// HeaderContentType is the "Content-Type" header // HeaderContentType is the "Content-Type" header.
HeaderContentType Header = "Content-Type" HeaderContentType Header = "Content-Type"
// HeaderDate represents the "Date" field // HeaderDate represents the "Date" field.
// See: https://www.rfc-editor.org/rfc/rfc822#section-5.1 // https://datatracker.ietf.org/doc/html/rfc822#section-5.1
HeaderDate Header = "Date" HeaderDate Header = "Date"
// HeaderDispositionNotificationTo is the MDN header as described in RFC8098 // HeaderDispositionNotificationTo is the MDN header as described in RFC 8098.
// See: https://www.rfc-editor.org/rfc/rfc8098.html#section-2.1 // https://datatracker.ietf.org/doc/html/rfc8098#section-2.1
HeaderDispositionNotificationTo Header = "Disposition-Notification-To" HeaderDispositionNotificationTo Header = "Disposition-Notification-To"
// HeaderImportance represents the "Importance" field // HeaderImportance represents the "Importance" field.
HeaderImportance Header = "Importance" HeaderImportance Header = "Importance"
// HeaderInReplyTo represents the "In-Reply-To" field // HeaderInReplyTo represents the "In-Reply-To" field.
HeaderInReplyTo Header = "In-Reply-To" HeaderInReplyTo Header = "In-Reply-To"
// HeaderListUnsubscribe is the "List-Unsubscribe" header field // HeaderListUnsubscribe is the "List-Unsubscribe" header field.
HeaderListUnsubscribe Header = "List-Unsubscribe" 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" HeaderListUnsubscribePost Header = "List-Unsubscribe-Post"
// HeaderMessageID represents the "Message-ID" field for message identification // HeaderMessageID represents the "Message-ID" field for message identification.
// See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5 // https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.5
HeaderMessageID Header = "Message-ID" HeaderMessageID Header = "Message-ID"
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045 // HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045.
// See: https://datatracker.ietf.org/doc/html/rfc2045#section-4 // https://datatracker.ietf.org/doc/html/rfc2045#section-4
HeaderMIMEVersion Header = "MIME-Version" HeaderMIMEVersion Header = "MIME-Version"
// HeaderOrganization is the "Organization" header field // HeaderOrganization is the "Organization" header field.
HeaderOrganization Header = "Organization" HeaderOrganization Header = "Organization"
// HeaderPrecedence is the "Precedence" header field // HeaderPrecedence is the "Precedence" header field.
HeaderPrecedence Header = "Precedence" HeaderPrecedence Header = "Precedence"
// HeaderPriority represents the "Priority" field // HeaderPriority represents the "Priority" field.
HeaderPriority Header = "Priority" HeaderPriority Header = "Priority"
// HeaderReferences is the "References" header field // HeaderReferences is the "References" header field.
HeaderReferences Header = "References" HeaderReferences Header = "References"
// HeaderReplyTo is the "Reply-To" header field // HeaderReplyTo is the "Reply-To" header field.
HeaderReplyTo Header = "Reply-To" HeaderReplyTo Header = "Reply-To"
// HeaderSubject is the "Subject" header field // HeaderSubject is the "Subject" header field.
HeaderSubject Header = "Subject" HeaderSubject Header = "Subject"
// HeaderUserAgent is the "User-Agent" header field // HeaderUserAgent is the "User-Agent" header field.
HeaderUserAgent Header = "User-Agent" 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" HeaderXAutoResponseSuppress Header = "X-Auto-Response-Suppress"
// HeaderXMailer is the "X-Mailer" header field // HeaderXMailer is the "X-Mailer" header field.
HeaderXMailer Header = "X-Mailer" 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" HeaderXMSMailPriority Header = "X-MSMail-Priority"
// HeaderXPriority is the "X-Priority" header field // HeaderXPriority is the "X-Priority" header field.
HeaderXPriority Header = "X-Priority" HeaderXPriority Header = "X-Priority"
) )
// List of common address header field names
const ( const (
// HeaderBcc is the "Blind Carbon Copy" header field // HeaderBcc is the "Blind Carbon Copy" header field.
HeaderBcc AddrHeader = "Bcc" HeaderBcc AddrHeader = "Bcc"
// HeaderCc is the "Carbon Copy" header field // HeaderCc is the "Carbon Copy" header field.
HeaderCc AddrHeader = "Cc" HeaderCc AddrHeader = "Cc"
// HeaderEnvelopeFrom is the envelope FROM header field // HeaderEnvelopeFrom is the envelope FROM header field.
// It's not included in the mail body but only used by the Client for the envelope //
// 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" HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom"
// HeaderFrom is the "From" header field // HeaderFrom is the "From" header field.
HeaderFrom AddrHeader = "From" HeaderFrom AddrHeader = "From"
// HeaderTo is the "Receipient" header field // HeaderTo is the "Receipient" header field.
HeaderTo AddrHeader = "To" HeaderTo AddrHeader = "To"
) )
// List of Importance values
const ( const (
// ImportanceLow indicates a low level of importance or priority in a Msg.
ImportanceLow Importance = iota ImportanceLow Importance = iota
// ImportanceNormal indicates a standard level of importance or priority for a Msg.
ImportanceNormal ImportanceNormal
// ImportanceHigh indicates a high level of importance or priority in a Msg.
ImportanceHigh ImportanceHigh
// ImportanceNonUrgent indicates a non-urgent level of importance or priority in a Msg.
ImportanceNonUrgent ImportanceNonUrgent
// ImportanceUrgent indicates an urgent level of importance or priority in a Msg.
ImportanceUrgent ImportanceUrgent
) )
// NumString returns the importance number string based on the Importance // NumString returns a numerical string representation of the Importance, mapping ImportanceHigh and
// ImportanceUrgent to "1" and others to "0".
func (i Importance) NumString() string { func (i Importance) NumString() string {
switch i { switch i {
case ImportanceNonUrgent: case ImportanceNonUrgent:
@ -142,7 +153,8 @@ 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, mapping ImportanceHigh and
// ImportanceUrgent to "1" and others to "5".
func (i Importance) XPrioString() string { func (i Importance) XPrioString() string {
switch i { switch i {
case ImportanceNonUrgent: case ImportanceNonUrgent:
@ -158,7 +170,8 @@ 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.
func (i Importance) String() string { func (i Importance) String() string {
switch i { switch i {
case ImportanceNonUrgent: case ImportanceNonUrgent:
@ -174,12 +187,13 @@ 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.
func (h Header) String() string { func (h Header) String() string {
return string(h) 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.
func (a AddrHeader) String() string { func (a AddrHeader) String() string {
return string(a) return string(a)
} }

231
msg.go
View file

@ -24,111 +24,134 @@ import (
) )
var ( 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") 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") ErrNoRcptAddresses = errors.New("no recipient addresses set")
) )
const ( 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" 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" 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" errParseMailAddr = "failed to parse mail address %q: %w"
) )
const ( const (
// NoPGP indicates that a message should not be treated as PGP encrypted // NoPGP indicates that a message should not be treated as PGP encrypted or signed and is the default value
// or signed and is the default value for a message // for a message
NoPGP PGPType = iota NoPGP PGPType = iota
// PGPEncrypt indicates that a message should be treated as PGP encrypted // PGPEncrypt indicates that a message should be treated as PGP encrypted. This works closely together with
// This works closely together with the corresponding go-mail-middleware // the corresponding go-mail-middleware.
PGPEncrypt PGPEncrypt
// PGPSignature indicates that a message should be treated as PGP signed // PGPSignature indicates that a message should be treated as PGP signed. This works closely together with
// This works closely together with the corresponding go-mail-middleware // the corresponding go-mail-middleware.
PGPSignature PGPSignature
) )
// MiddlewareType is the type description of the Middleware and needs to be returned // MiddlewareType is a type wrapper for a string. It describes the type of the Middleware and needs to be
// in the Middleware interface by the Type method // returned by the Middleware.Type method to satisfy the Middleware interface.
type MiddlewareType string 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 { type Middleware interface {
Handle(*Msg) *Msg Handle(*Msg) *Msg
Type() MiddlewareType Type() MiddlewareType
} }
// PGPType is a type alias for a int representing a type of PGP encryption // PGPType is a type wrapper for an int, representing a type of PGP encryption or signature.
// or signature
type PGPType int 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 { 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 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 attachments []*File
// boundary is the MIME content boundary // boundary represents the delimiter for separating parts in a multipart message.
boundary string 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 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 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 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 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 genHeader map[Header][]string
// isDelivered signals if a message has been delivered or not // isDelivered indicates wether the Msg has been delivered.
isDelivered bool 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 middlewares []Middleware
// mimever represents the MIME version // mimever represents the MIME version used in a Msg.
mimever MIMEVersion 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 parts []*Part
// preformHeader is a slice of strings that the different generic mail Header fields // preformHeader maps Header types to their already preformatted string values.
// of which content is already preformated and will not be affected by the automatic line //
// breaks // Preformatted Header values will not be affected by automatic line breaks.
preformHeader map[Header]string preformHeader map[Header]string
// pgptype indicates that a message has a PGPType assigned and therefore will generate // 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 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 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 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" 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) 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.
func NewMsg(opts ...MsgOption) *Msg { func NewMsg(opts ...MsgOption) *Msg {
msg := &Msg{ msg := &Msg{
addrHeader: make(map[AddrHeader][]*mail.Address), addrHeader: make(map[AddrHeader][]*mail.Address),
@ -139,7 +162,7 @@ func NewMsg(opts ...MsgOption) *Msg {
mimever: MIME10, mimever: MIME10,
} }
// Override defaults with optionally provided MsgOption functions // Override defaults with optionally provided MsgOption functions.
for _, option := range opts { for _, option := range opts {
if option == nil { if option == nil {
continue continue
@ -153,101 +176,133 @@ func NewMsg(opts ...MsgOption) *Msg {
return msg return msg
} }
// WithCharset overrides the default message charset // WithCharset sets the Charset type for a Msg during its creation or initialization.
func WithCharset(c Charset) MsgOption { func WithCharset(c Charset) MsgOption {
return func(m *Msg) { return func(m *Msg) {
m.charset = c m.charset = c
} }
} }
// WithEncoding overrides the default message encoding // WithEncoding sets the Encoding type for a Msg during its creation or initialization.
func WithEncoding(e Encoding) MsgOption { func WithEncoding(e Encoding) MsgOption {
return func(m *Msg) { return func(m *Msg) {
m.encoding = e m.encoding = e
} }
} }
// WithMIMEVersion overrides the default MIME version // 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.
// https://datatracker.ietf.org/doc/html/rfc1521
//
// https://datatracker.ietf.org/doc/html/rfc2045
//
// https://datatracker.ietf.org/doc/html/rfc2049
func WithMIMEVersion(mv MIMEVersion) MsgOption { func WithMIMEVersion(mv MIMEVersion) MsgOption {
return func(m *Msg) { return func(m *Msg) {
m.mimever = mv m.mimever = mv
} }
} }
// WithBoundary overrides the default MIME boundary // WithBoundary sets the boundary of a Msg to the provided string value during its creation or initialization.
//
// Note that by default we create random MIME boundaries. This should only be used if a specific boundary is
// required.
func WithBoundary(b string) MsgOption { func WithBoundary(b string) MsgOption {
return func(m *Msg) { return func(m *Msg) {
m.boundary = b m.boundary = b
} }
} }
// WithMiddleware add the given middleware in the end of the list of the client middlewares // WithMiddleware adds the given Middleware to the end of the list of the Client middlewares slice. Middleware
// are processed in FIFO order.
func WithMiddleware(mw Middleware) MsgOption { func WithMiddleware(mw Middleware) MsgOption {
return func(m *Msg) { return func(m *Msg) {
m.middlewares = append(m.middlewares, mw) m.middlewares = append(m.middlewares, mw)
} }
} }
// WithPGPType overrides the default PGPType of the message // WithPGPType sets the PGP type for the Msg during its creation or initialization, determining the encryption or
// signature method.
func WithPGPType(pt PGPType) MsgOption { func WithPGPType(pt PGPType) MsgOption {
return func(m *Msg) { return func(m *Msg) {
m.pgptype = pt m.pgptype = pt
} }
} }
// 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.
func WithNoDefaultUserAgent() MsgOption { func WithNoDefaultUserAgent() MsgOption {
return func(m *Msg) { return func(m *Msg) {
m.noDefaultUserAgent = true m.noDefaultUserAgent = true
} }
} }
// SetCharset sets the encoding charset of the Msg // SetCharset sets or overrides the currently set encoding charset of the Msg.
func (m *Msg) SetCharset(c Charset) { func (m *Msg) SetCharset(c Charset) {
m.charset = c m.charset = c
} }
// SetEncoding sets the encoding of the Msg // SetEncoding sets or overrides the currently set Encoding of the Msg.
func (m *Msg) SetEncoding(e Encoding) { func (m *Msg) SetEncoding(e Encoding) {
m.encoding = e m.encoding = e
m.setEncoder() m.setEncoder()
} }
// SetBoundary sets the boundary of the Msg // SetBoundary sets or overrides the currently set boundary of the Msg.
//
// Note that by default we create random MIME boundaries. This should only be used if a specific boundary is
// required.
func (m *Msg) SetBoundary(b string) { func (m *Msg) SetBoundary(b string) {
m.boundary = b m.boundary = b
} }
// SetMIMEVersion sets the MIME version of the Msg // SetMIMEVersion sets or overrides the currently set MIME version of the Msg.
//
// 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.
//
// 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(mv MIMEVersion) { func (m *Msg) SetMIMEVersion(mv MIMEVersion) {
m.mimever = mv m.mimever = mv
} }
// SetPGPType sets the PGPType of the Msg // SetPGPType sets or overrides the currently set PGP type for the Msg, determining the encryption or
// signature method.
func (m *Msg) SetPGPType(t PGPType) { func (m *Msg) SetPGPType(t PGPType) {
m.pgptype = t m.pgptype = t
} }
// Encoding returns the currently set encoding of the Msg // Encoding returns the currently set Encoding of the Msg as string.
func (m *Msg) Encoding() string { func (m *Msg) Encoding() string {
return m.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 string.
func (m *Msg) Charset() string { func (m *Msg) Charset() string {
return m.charset.String() return m.charset.String()
} }
// SetHeader sets a generic header field of the Msg // SetHeader sets a generic header field of the Msg.
// For adding address headers like "To:" or "From", see SetAddrHeader
// //
// Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead // Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead.
// For adding address headers like "To:" or "From", use SetAddrHeader instead.
func (m *Msg) SetHeader(header Header, values ...string) { func (m *Msg) SetHeader(header Header, values ...string) {
m.SetGenHeader(header, values...) m.SetGenHeader(header, values...)
} }
// SetGenHeader sets a generic header field of the Msg // SetGenHeader sets a generic header field of the Msg to the provided list of values.
// For adding address headers like "To:" or "From", see SetAddrHeader //
// Note: for adding email address related headers (like "To:" or "From") use SetAddrHeader instead.
func (m *Msg) SetGenHeader(header Header, values ...string) { func (m *Msg) SetGenHeader(header Header, values ...string) {
if m.genHeader == nil { if m.genHeader == nil {
m.genHeader = make(map[Header][]string) m.genHeader = make(map[Header][]string)
@ -258,26 +313,24 @@ func (m *Msg) SetGenHeader(header Header, values ...string) {
m.genHeader[header] = values m.genHeader[header] = values
} }
// SetHeaderPreformatted sets a generic header field of the Msg which content is // SetHeaderPreformatted sets a generic header field of the Msg, which content is already preformatted.
// already preformated.
// //
// Deprecated: This method only exists for compatibility reason. Please use // Deprecated: This method only exists for compatibility reason. Please use SetGenHeaderPreformatted instead.
// SetGenHeaderPreformatted instead
func (m *Msg) SetHeaderPreformatted(header Header, value string) { func (m *Msg) SetHeaderPreformatted(header Header, value string) {
m.SetGenHeaderPreformatted(header, value) m.SetGenHeaderPreformatted(header, value)
} }
// SetGenHeaderPreformatted sets a generic header field of the Msg which content is // SetGenHeaderPreformatted sets a generic header field of the Msg which content is already preformated.
// already preformated.
// //
// This method does not take a slice of values but only a single value. This is // This method does not take a slice of values but only a single value. The reason for this is that we do not
// due to the fact, that we do not perform any content alteration and expect the // perform any content alteration on these kind of headers and expect the user to have already taken care of
// user has already done so // any kind of formatting required for the header.
// //
// **Please note:** This method should be used only as a last resort. Since the // Note: This method should be used only as a last resort. Since the user is respondible for the formatting of
// user is respondible for the formating of the message header, go-mail cannot // the message header, we cannot guarantee any compliance with the RFC 2822. It is advised to use SetGenHeader
// guarantee the fully compliance with the RFC 2822. It is recommended to use // instead.
// SetGenHeader instead. //
// https://datatracker.ietf.org/doc/html/rfc2822
func (m *Msg) SetGenHeaderPreformatted(header Header, value string) { func (m *Msg) SetGenHeaderPreformatted(header Header, value string) {
if m.preformHeader == nil { if m.preformHeader == nil {
m.preformHeader = make(map[Header]string) m.preformHeader = make(map[Header]string)
@ -285,7 +338,13 @@ func (m *Msg) SetGenHeaderPreformatted(header Header, value string) {
m.preformHeader[header] = value 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 of ANY of the provided values fails,
// and error is returned. If you cannot guarantee that all provided values are valid, you can
// use SetAddrHeaderIgnoreInvalid instead, which will skip any parsing error silently.
//
// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error { func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error {
if m.addrHeader == nil { if m.addrHeader == nil {
m.addrHeader = make(map[AddrHeader][]*mail.Address) m.addrHeader = make(map[AddrHeader][]*mail.Address)
@ -309,8 +368,12 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error {
return nil return nil
} }
// SetAddrHeaderIgnoreInvalid sets an address related header field of the Msg and ignores invalid address // SetAddrHeaderIgnoreInvalid sets the specified AddrHeader for the Msg to the given values.
// in the validation process //
// Addresses are parsed according to RFC 5322. If parsing of ANY of the provided values fails,
// the error is ignored and the address omiitted from the address list.
//
// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) { func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
var addresses []*mail.Address var addresses []*mail.Address
for _, addrVal := range values { for _, addrVal := range values {
@ -323,14 +386,26 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
m.addrHeader[header] = addresses m.addrHeader[header] = addresses
} }
// EnvelopeFrom takes and validates a given mail address and sets it as envelope "FROM" // EnvelopeFrom sets the envelope from address for the Msg.
// addrHeader of the Msg //
// The HeaderEnvelopeFrom address 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. The provided address is validated
// according to RFC 5322 and will return an error if the validation fails.
//
// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
func (m *Msg) EnvelopeFrom(from string) error { func (m *Msg) EnvelopeFrom(from string) error {
return m.SetAddrHeader(HeaderEnvelopeFrom, from) return m.SetAddrHeader(HeaderEnvelopeFrom, from)
} }
// EnvelopeFromFormat takes a name and address, formats them RFC5322 compliant and stores them as // EnvelopeFromFormat sets the provided name and mail address as HeaderEnvelopeFrom for the Msg.
// the envelope FROM address header field //
// The HeaderEnvelopeFrom address 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. The provided name and address adre
// validated according to RFC 5322 and will return an error if the validation fails.
//
// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
func (m *Msg) EnvelopeFromFormat(name, addr string) error { func (m *Msg) EnvelopeFromFormat(name, addr string) error {
return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, name, addr)) return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, name, addr))
} }

View file

@ -36,7 +36,15 @@ import (
"github.com/wneessen/go-mail/log" "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. // A Client represents a client connection to an SMTP server.
type Client struct { type Client struct {
@ -67,6 +75,9 @@ type Client struct {
// helloError is the error from the hello // helloError is the error from the hello
helloError error helloError error
// isConnected indicates if the Client has an active connection
isConnected bool
// localName is the name to use in HELO/EHLO // localName is the name to use in HELO/EHLO
localName string // 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 := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
_, c.tls = conn.(*tls.Conn) _, c.tls = conn.(*tls.Conn)
c.isConnected = true
return c, nil return c, nil
} }
@ -121,6 +133,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
func (c *Client) Close() error { func (c *Client) Close() error {
c.mutex.Lock() c.mutex.Lock()
err := c.Text.Close() err := c.Text.Close()
c.isConnected = false
c.mutex.Unlock() c.mutex.Unlock()
return err return err
} }
@ -516,8 +529,7 @@ func (c *Client) Quit() error {
} }
c.mutex.Lock() c.mutex.Lock()
err = c.Text.Close() err = c.Text.Close()
c.Text = nil c.isConnected = false
c.conn = nil
c.mutex.Unlock() c.mutex.Unlock()
return err return err
@ -558,17 +570,18 @@ func (c *Client) SetDSNRcptNotifyOption(d string) {
// Returns true if the `conn` field is not nil, indicating an active connection. // Returns true if the `conn` field is not nil, indicating an active connection.
func (c *Client) HasConnection() bool { func (c *Client) HasConnection() bool {
c.mutex.RLock() c.mutex.RLock()
conn := c.conn isConn := c.isConnected
c.mutex.RUnlock() c.mutex.RUnlock()
return conn != nil return isConn
} }
// UpdateDeadline sets a new deadline on the SMTP connection with the specified timeout duration.
func (c *Client) UpdateDeadline(timeout time.Duration) error { func (c *Client) UpdateDeadline(timeout time.Duration) error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock()
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil { if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
return fmt.Errorf("smtp: failed to update deadline: %w", err) return fmt.Errorf("smtp: failed to update deadline: %w", err)
} }
c.mutex.Unlock()
return nil return nil
} }
@ -578,17 +591,17 @@ func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) {
c.mutex.RLock() c.mutex.RLock()
defer c.mutex.RUnlock() defer c.mutex.RUnlock()
if !c.isConnected {
return nil, ErrNoConnection
}
if !c.tls { if !c.tls {
return nil, ErrNonTLSConnection return nil, ErrNonTLSConnection
} }
if c.conn == nil {
return nil, errors.New("smtp: connection is not established")
}
if conn, ok := c.conn.(*tls.Conn); ok { if conn, ok := c.conn.(*tls.Conn); ok {
cstate := conn.ConnectionState() cstate := conn.ConnectionState()
return &cstate, nil 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 // debugLog checks if the debug flag is set and if so logs the provided message to

View file

@ -1640,6 +1640,357 @@ func TestTLSConnState(t *testing.T) {
<-serverDone <-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 { func newLocalListener(t *testing.T) net.Listener {
ln, err := net.Listen("tcp", "127.0.0.1:0") ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
@ -1685,6 +2036,8 @@ func serverHandle(c net.Conn, t *testing.T) error {
} }
config := &tls.Config{Certificates: []tls.Certificate{keypair}} config := &tls.Config{Certificates: []tls.Certificate{keypair}}
return tf(config) return tf(config)
case "QUIT":
return nil
default: default:
t.Fatalf("unrecognized command: %q", s.Text()) t.Fatalf("unrecognized command: %q", s.Text())
} }