mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 06:10:48 +01:00
Compare commits
18 commits
fbbf17acd0
...
3d5435c138
Author | SHA1 | Date | |
---|---|---|---|
3d5435c138 | |||
1dcdad9da1 | |||
78e2857782 | |||
c186cba2c2 | |||
682f7a6ca5 | |||
cd4c0194dc | |||
a820ba3cee | |||
96466facdd | |||
493f8fc657 | |||
94f47d4369 | |||
476130d6e3 | |||
a0a7f74121 | |||
ecd0bff5ad | |||
5653df373b | |||
869e8db6c5 | |||
fa3c6f956e | |||
159c1bf850 | |||
9163943684 |
15 changed files with 768 additions and 252 deletions
|
@ -52,7 +52,7 @@ Here are some highlights of go-mail's featureset:
|
|||
* [X] Support sending mails via a local sendmail command
|
||||
* [X] Support for requestng MDNs (RFC 8098) and DSNs (RFC 1891)
|
||||
* [X] DKIM signature support via [go-mail-middlware](https://github.com/wneessen/go-mail-middleware)
|
||||
* [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces
|
||||
* [X] Message object satisfies `io.WriterTo` and `io.Reader` interfaces
|
||||
* [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed)
|
||||
* [X] Output to file support which allows storing mail messages as e. g. `.eml` files to disk to open them in a MUA
|
||||
* [X] Debug logging of SMTP traffic
|
||||
|
|
20
auth.go
20
auth.go
|
@ -20,6 +20,7 @@ const (
|
|||
//
|
||||
// It was recommended to deprecate the standard in 20 November 2008. As an alternative it
|
||||
// recommends e.g. SCRAM or SASL Plain protected by TLS instead.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-sasl-crammd5-to-historic-00.html
|
||||
SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"
|
||||
|
||||
|
@ -33,12 +34,14 @@ const (
|
|||
// does not have an official RFC that could be followed. There is a spec by Microsoft and an
|
||||
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
|
||||
// automatically matches the MS spec.
|
||||
// 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
|
||||
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||
// connection.
|
||||
//
|
||||
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
||||
|
||||
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
||||
|
@ -47,11 +50,12 @@ const (
|
|||
SMTPAuthNoAuth SMTPAuthType = ""
|
||||
|
||||
// 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
|
||||
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||
// connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
||||
|
||||
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
||||
|
@ -59,16 +63,16 @@ const (
|
|||
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
||||
|
||||
// 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
|
||||
// of a challenge-response authentication mechanism (as we use it). However, it is generally
|
||||
// recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known
|
||||
// vulnerabilities in other contexts, although it remains effective in HMAC constructions.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc5802
|
||||
SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1"
|
||||
|
||||
// SMTPAuthSCRAMSHA1PLUS 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
|
||||
// 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
|
||||
// recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known
|
||||
// vulnerabilities in other contexts, although it remains effective in HMAC constructions.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc5802
|
||||
SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS"
|
||||
|
||||
// SMTPAuthSCRAMSHA256 is the "SCRAM-SHA-256" SASL authentication mechanism as described in RFC 7677.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||
SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256"
|
||||
|
||||
// SMTPAuthSCRAMSHA256PLUS 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
|
||||
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
|
||||
// process. Therefore we only allow this mechansim over a TLS secured connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
|
||||
)
|
||||
|
||||
|
|
19
client.go
19
client.go
|
@ -44,26 +44,31 @@ const (
|
|||
|
||||
// DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in
|
||||
// a DSN (Delivery Status Notification).
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891#section-5.3
|
||||
DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS"
|
||||
|
||||
// DSNMailReturnFull requests that the entire mail message is returned in any failed DSN
|
||||
// (Delivery Status Notification) issued for this recipient.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.3
|
||||
DSNMailReturnFull DSNMailReturnOption = "FULL"
|
||||
|
||||
// DSNRcptNotifyNever indicates that no DSN (Delivery Status Notifications) should be sent for the
|
||||
// recipient under any condition.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
|
||||
DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER"
|
||||
|
||||
// DSNRcptNotifySuccess indicates that the sender requests a DSN (Delivery Status Notification) if the
|
||||
// message is successfully delivered.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
|
||||
DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS"
|
||||
|
||||
// DSNRcptNotifyFailure requests that a DSN (Delivery Status Notification) is issued if delivery of
|
||||
// a message fails.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
|
||||
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
|
||||
// successful or failure) cannot be determined. The absence of the DELAY keyword in a NOTIFY parameter
|
||||
// requests that a "delayed" DSN NOT be issued under any conditions.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
|
||||
DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY"
|
||||
)
|
||||
|
@ -87,11 +93,13 @@ type (
|
|||
|
||||
// DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested
|
||||
// in a Delivery Status Notification (DSN).
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891/
|
||||
DSNMailReturnOption string
|
||||
|
||||
// DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a
|
||||
// recipient in DSNs.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891/
|
||||
DSNRcptNotifyOption string
|
||||
|
||||
|
@ -167,6 +175,7 @@ type (
|
|||
smtpClient *smtp.Client
|
||||
|
||||
// tlspolicy defines the TLSPolicy configuration the Client uses for the STARTTLS protocol.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc3207#section-2
|
||||
tlspolicy TLSPolicy
|
||||
|
||||
|
@ -180,6 +189,7 @@ type (
|
|||
user string
|
||||
|
||||
// useSSL indicates whether to use SSL/TLS encryption for network communication.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc8314
|
||||
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
|
||||
// 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
|
||||
// and DSNRcptNotifyFailure.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891
|
||||
func WithDSN() Option {
|
||||
return func(c *Client) error {
|
||||
c.requestDSN = true
|
||||
|
@ -439,9 +450,10 @@ func WithDSN() Option {
|
|||
|
||||
// WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in the
|
||||
// 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.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891
|
||||
func WithDSNMailReturnType(option DSNMailReturnOption) Option {
|
||||
return func(c *Client) error {
|
||||
switch option {
|
||||
|
@ -459,9 +471,10 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option {
|
|||
|
||||
// WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in the
|
||||
// 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.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc1891
|
||||
func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
|
||||
return func(c *Client) error {
|
||||
var rcptOpts []string
|
||||
|
|
|
@ -9,7 +9,10 @@ package mail
|
|||
|
||||
import "errors"
|
||||
|
||||
// Send sends out the mail message
|
||||
// Send attempts to send one or more Msg using the Client connection to the SMTP server.
|
||||
// If the Client has no active connection to the server, Send will fail with an error. For each of the
|
||||
// provided Msg it will associate a SendError to the Msg in case there of a transmission or delivery
|
||||
// error.
|
||||
func (c *Client) Send(messages ...*Msg) error {
|
||||
if err := c.checkConn(); err != nil {
|
||||
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
|
|
|
@ -11,7 +11,10 @@ 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) (returnErr error) {
|
||||
if err := c.checkConn(); err != nil {
|
||||
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
|
|
9
doc.go
9
doc.go
|
@ -2,8 +2,13 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package mail provides a simple and easy way to send mails with Go
|
||||
// Package mail provides an easy to use interface for formating and sending mails. go-mail follows idiomatic Go style
|
||||
// and best practice. It has a small dependency footprint by mainly relying on the Go Standard Library and the Go
|
||||
// extended packages. It combines a lot of functionality from the standard library to give easy and convenient access
|
||||
// to mail and SMTP related tasks. It works like a programatic email client and provides lots of methods and
|
||||
// functionalities you would consider standard in a MUA.
|
||||
package mail
|
||||
|
||||
// VERSION is used in the default user agent string
|
||||
// VERSION indicates the current version of the package. It is also attached to the default user
|
||||
// agent string.
|
||||
const VERSION = "0.4.4"
|
||||
|
|
36
eml.go
36
eml.go
|
@ -18,14 +18,13 @@ import (
|
|||
"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) {
|
||||
eb := bytes.NewBufferString(emlString)
|
||||
return EMLToMsgFromReader(eb)
|
||||
}
|
||||
|
||||
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled
|
||||
// Msg pointer
|
||||
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled Msg pointer.
|
||||
func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
||||
msg := &Msg{
|
||||
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
||||
|
@ -46,8 +45,7 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
|||
return msg, nil
|
||||
}
|
||||
|
||||
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a
|
||||
// pre-filled Msg pointer
|
||||
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a pre-filled Msg pointer.
|
||||
func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
||||
msg := &Msg{
|
||||
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
||||
|
@ -68,7 +66,7 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
|||
return msg, nil
|
||||
}
|
||||
|
||||
// parseEML parses the EML's headers and body and inserts the parsed values into the Msg
|
||||
// parseEML parses the EML's headers and body and inserts the parsed values into the Msg.
|
||||
func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||
if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
|
||||
return fmt.Errorf("failed to parse EML headers: %w", err)
|
||||
|
@ -79,7 +77,7 @@ func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// readEML opens an EML file and uses net/mail to parse the header and body
|
||||
// readEML opens an EML file and uses net/mail to parse the header and body.
|
||||
func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
||||
fileHandle, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
|
@ -91,7 +89,7 @@ func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
|||
return readEMLFromReader(fileHandle)
|
||||
}
|
||||
|
||||
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader
|
||||
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader.
|
||||
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
|
||||
parsedMsg, err := netmail.ReadMessage(reader)
|
||||
if err != nil {
|
||||
|
@ -106,8 +104,8 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error
|
|||
return parsedMsg, &buf, nil
|
||||
}
|
||||
|
||||
// parseEMLHeaders will check the EML headers for the most common headers and set the
|
||||
// according settings in the Msg
|
||||
// parseEMLHeaders will check the EML headers for the most common headers and set the according settings
|
||||
// in the Msg.
|
||||
func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
||||
commonHeaders := []Header{
|
||||
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
|
||||
|
@ -175,7 +173,7 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings
|
||||
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings.
|
||||
func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||
// Extract the transfer encoding of the body
|
||||
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
|
||||
|
@ -212,10 +210,11 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseEMLBodyPlain parses the mail body of plain type mails
|
||||
// parseEMLBodyPlain parses the mail body of plain type mails.
|
||||
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String())
|
||||
// According to RFC2045, if no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
|
||||
// If no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.1
|
||||
if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
|
||||
msg.SetEncoding(EncodingUSASCII)
|
||||
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
||||
|
@ -349,7 +348,7 @@ ReadNextPart:
|
|||
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) {
|
||||
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
|
||||
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) {
|
||||
if value := mailHeader.Get(HeaderContentType.String()); 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 {
|
||||
part.SetEncoding(EncodingB64)
|
||||
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
|
||||
|
@ -388,8 +387,7 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as
|
||||
// separate map
|
||||
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as separate map.
|
||||
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
|
||||
optional = make(map[string]string)
|
||||
headerSplit := strings.SplitN(multiPartHeader, ";", 2)
|
||||
|
@ -404,7 +402,7 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s
|
|||
return
|
||||
}
|
||||
|
||||
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed
|
||||
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed.
|
||||
func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error {
|
||||
cdType, optional := parseMultiPartHeader(contentDisposition[0])
|
||||
filename := "generic.attachment"
|
||||
|
|
127
encoding.go
127
encoding.go
|
@ -4,171 +4,198 @@
|
|||
|
||||
package mail
|
||||
|
||||
// Charset represents a character set for the encoding
|
||||
// Charset is a type wrapper for a string representing different character encodings.
|
||||
type Charset string
|
||||
|
||||
// ContentType represents a content type for the Msg
|
||||
// ContentType is a type wrapper for a string and represents the MIME type of the content being handled.
|
||||
type ContentType string
|
||||
|
||||
// Encoding represents a MIME encoding scheme like quoted-printable or Base64.
|
||||
// Encoding is a type wrapper for a string and represents the type of encoding used for email messages
|
||||
// and/or parts.
|
||||
type Encoding string
|
||||
|
||||
// MIMEVersion represents the MIME version for the mail
|
||||
// MIMEVersion is a type wrapper for a string nad represents the MIME version used in email messages.
|
||||
type MIMEVersion string
|
||||
|
||||
// MIMEType represents the MIME type for the mail
|
||||
// MIMEType is a type wrapper for a string and represents the MIME type for the Msg content or parts.
|
||||
type MIMEType string
|
||||
|
||||
// List of supported encodings
|
||||
const (
|
||||
// EncodingB64 represents the Base64 encoding as specified in RFC 2045.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
|
||||
EncodingB64 Encoding = "base64"
|
||||
|
||||
// EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.7
|
||||
EncodingQP Encoding = "quoted-printable"
|
||||
|
||||
// EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit)
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.7
|
||||
EncodingUSASCII Encoding = "7bit"
|
||||
|
||||
// NoEncoding avoids any character encoding (except of the mail headers)
|
||||
// NoEncoding represents 8-bit encoding for email messages as specified in RFC 6152.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.8
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc6152
|
||||
NoEncoding Encoding = "8bit"
|
||||
)
|
||||
|
||||
// List of common charsets
|
||||
const (
|
||||
// CharsetUTF7 represents the "UTF-7" charset
|
||||
// CharsetUTF7 represents the "UTF-7" charset.
|
||||
CharsetUTF7 Charset = "UTF-7"
|
||||
|
||||
// CharsetUTF8 represents the "UTF-8" charset
|
||||
// CharsetUTF8 represents the "UTF-8" charset.
|
||||
CharsetUTF8 Charset = "UTF-8"
|
||||
|
||||
// CharsetASCII represents the "US-ASCII" charset
|
||||
// CharsetASCII represents the "US-ASCII" charset.
|
||||
CharsetASCII Charset = "US-ASCII"
|
||||
|
||||
// CharsetISO88591 represents the "ISO-8859-1" charset
|
||||
// CharsetISO88591 represents the "ISO-8859-1" charset.
|
||||
CharsetISO88591 Charset = "ISO-8859-1"
|
||||
|
||||
// CharsetISO88592 represents the "ISO-8859-2" charset
|
||||
// CharsetISO88592 represents the "ISO-8859-2" charset.
|
||||
CharsetISO88592 Charset = "ISO-8859-2"
|
||||
|
||||
// CharsetISO88593 represents the "ISO-8859-3" charset
|
||||
// CharsetISO88593 represents the "ISO-8859-3" charset.
|
||||
CharsetISO88593 Charset = "ISO-8859-3"
|
||||
|
||||
// CharsetISO88594 represents the "ISO-8859-4" charset
|
||||
// CharsetISO88594 represents the "ISO-8859-4" charset.
|
||||
CharsetISO88594 Charset = "ISO-8859-4"
|
||||
|
||||
// CharsetISO88595 represents the "ISO-8859-5" charset
|
||||
// CharsetISO88595 represents the "ISO-8859-5" charset.
|
||||
CharsetISO88595 Charset = "ISO-8859-5"
|
||||
|
||||
// CharsetISO88596 represents the "ISO-8859-6" charset
|
||||
// CharsetISO88596 represents the "ISO-8859-6" charset.
|
||||
CharsetISO88596 Charset = "ISO-8859-6"
|
||||
|
||||
// CharsetISO88597 represents the "ISO-8859-7" charset
|
||||
// CharsetISO88597 represents the "ISO-8859-7" charset.
|
||||
CharsetISO88597 Charset = "ISO-8859-7"
|
||||
|
||||
// CharsetISO88599 represents the "ISO-8859-9" charset
|
||||
// CharsetISO88599 represents the "ISO-8859-9" charset.
|
||||
CharsetISO88599 Charset = "ISO-8859-9"
|
||||
|
||||
// CharsetISO885913 represents the "ISO-8859-13" charset
|
||||
// CharsetISO885913 represents the "ISO-8859-13" charset.
|
||||
CharsetISO885913 Charset = "ISO-8859-13"
|
||||
|
||||
// CharsetISO885914 represents the "ISO-8859-14" charset
|
||||
// CharsetISO885914 represents the "ISO-8859-14" charset.
|
||||
CharsetISO885914 Charset = "ISO-8859-14"
|
||||
|
||||
// CharsetISO885915 represents the "ISO-8859-15" charset
|
||||
// CharsetISO885915 represents the "ISO-8859-15" charset.
|
||||
CharsetISO885915 Charset = "ISO-8859-15"
|
||||
|
||||
// CharsetISO885916 represents the "ISO-8859-16" charset
|
||||
// CharsetISO885916 represents the "ISO-8859-16" charset.
|
||||
CharsetISO885916 Charset = "ISO-8859-16"
|
||||
|
||||
// CharsetISO2022JP represents the "ISO-2022-JP" charset
|
||||
// CharsetISO2022JP represents the "ISO-2022-JP" charset.
|
||||
CharsetISO2022JP Charset = "ISO-2022-JP"
|
||||
|
||||
// CharsetISO2022KR represents the "ISO-2022-KR" charset
|
||||
// CharsetISO2022KR represents the "ISO-2022-KR" charset.
|
||||
CharsetISO2022KR Charset = "ISO-2022-KR"
|
||||
|
||||
// CharsetWindows1250 represents the "windows-1250" charset
|
||||
// CharsetWindows1250 represents the "windows-1250" charset.
|
||||
CharsetWindows1250 Charset = "windows-1250"
|
||||
|
||||
// CharsetWindows1251 represents the "windows-1251" charset
|
||||
// CharsetWindows1251 represents the "windows-1251" charset.
|
||||
CharsetWindows1251 Charset = "windows-1251"
|
||||
|
||||
// CharsetWindows1252 represents the "windows-1252" charset
|
||||
// CharsetWindows1252 represents the "windows-1252" charset.
|
||||
CharsetWindows1252 Charset = "windows-1252"
|
||||
|
||||
// CharsetWindows1255 represents the "windows-1255" charset
|
||||
// CharsetWindows1255 represents the "windows-1255" charset.
|
||||
CharsetWindows1255 Charset = "windows-1255"
|
||||
|
||||
// CharsetWindows1256 represents the "windows-1256" charset
|
||||
// CharsetWindows1256 represents the "windows-1256" charset.
|
||||
CharsetWindows1256 Charset = "windows-1256"
|
||||
|
||||
// CharsetKOI8R represents the "KOI8-R" charset
|
||||
// CharsetKOI8R represents the "KOI8-R" charset.
|
||||
CharsetKOI8R Charset = "KOI8-R"
|
||||
|
||||
// CharsetKOI8U represents the "KOI8-U" charset
|
||||
// CharsetKOI8U represents the "KOI8-U" charset.
|
||||
CharsetKOI8U Charset = "KOI8-U"
|
||||
|
||||
// CharsetBig5 represents the "Big5" charset
|
||||
// CharsetBig5 represents the "Big5" charset.
|
||||
CharsetBig5 Charset = "Big5"
|
||||
|
||||
// CharsetGB18030 represents the "GB18030" charset
|
||||
// CharsetGB18030 represents the "GB18030" charset.
|
||||
CharsetGB18030 Charset = "GB18030"
|
||||
|
||||
// CharsetGB2312 represents the "GB2312" charset
|
||||
// CharsetGB2312 represents the "GB2312" charset.
|
||||
CharsetGB2312 Charset = "GB2312"
|
||||
|
||||
// CharsetTIS620 represents the "TIS-620" charset
|
||||
// CharsetTIS620 represents the "TIS-620" charset.
|
||||
CharsetTIS620 Charset = "TIS-620"
|
||||
|
||||
// CharsetEUCKR represents the "EUC-KR" charset
|
||||
// CharsetEUCKR represents the "EUC-KR" charset.
|
||||
CharsetEUCKR Charset = "EUC-KR"
|
||||
|
||||
// CharsetShiftJIS represents the "Shift_JIS" charset
|
||||
// CharsetShiftJIS represents the "Shift_JIS" charset.
|
||||
CharsetShiftJIS Charset = "Shift_JIS"
|
||||
|
||||
// CharsetUnknown represents the "Unknown" charset
|
||||
// CharsetUnknown represents the "Unknown" charset.
|
||||
CharsetUnknown Charset = "Unknown"
|
||||
|
||||
// CharsetGBK represents the "GBK" charset
|
||||
// CharsetGBK represents the "GBK" charset.
|
||||
CharsetGBK Charset = "GBK"
|
||||
)
|
||||
|
||||
// List of MIME versions
|
||||
const (
|
||||
// MIME10 is the MIME Version 1.0
|
||||
MIME10 MIMEVersion = "1.0"
|
||||
)
|
||||
// MIME10 represents the MIME version "1.0" used in email messages.
|
||||
const MIME10 MIMEVersion = "1.0"
|
||||
|
||||
// List of common content types
|
||||
const (
|
||||
// TypeAppOctetStream represents the MIME type for arbitrary binary data.
|
||||
TypeAppOctetStream ContentType = "application/octet-stream"
|
||||
|
||||
// TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative
|
||||
// formats.
|
||||
TypeMultipartAlternative ContentType = "multipart/alternative"
|
||||
|
||||
// TypeMultipartMixed represents the MIME type for a multipart message containing different parts.
|
||||
TypeMultipartMixed ContentType = "multipart/mixed"
|
||||
|
||||
// TypeMultipartRelated represents the MIME type for a multipart message where each part is a related file
|
||||
// or resource.
|
||||
TypeMultipartRelated ContentType = "multipart/related"
|
||||
|
||||
// TypePGPSignature represents the MIME type for PGP signed messages.
|
||||
TypePGPSignature ContentType = "application/pgp-signature"
|
||||
|
||||
// TypePGPEncrypted represents the MIME type for PGP encrypted messages.
|
||||
TypePGPEncrypted ContentType = "application/pgp-encrypted"
|
||||
|
||||
// TypeTextHTML represents the MIME type for HTML text content.
|
||||
TypeTextHTML ContentType = "text/html"
|
||||
|
||||
// TypeTextPlain represents the MIME type for plain text content.
|
||||
TypeTextPlain ContentType = "text/plain"
|
||||
)
|
||||
|
||||
// List of MIMETypes
|
||||
const (
|
||||
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
|
||||
MIMEAlternative MIMEType = "alternative"
|
||||
|
||||
// MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content.
|
||||
MIMEMixed MIMEType = "mixed"
|
||||
|
||||
// MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities.
|
||||
MIMERelated MIMEType = "related"
|
||||
)
|
||||
|
||||
// String is a standard method to convert an Charset into a printable format
|
||||
// String satisfies the fmt.Stringer interface for the Charset type. It converts a Charset into a printable format.
|
||||
func (c Charset) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// String is a standard method to convert an ContentType into a printable format
|
||||
// String satisfies the fmt.Stringer interface for the ContentType type. It converts a ContentType into a printable
|
||||
// format.
|
||||
func (c ContentType) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// String is a standard method to convert an Encoding into a printable format
|
||||
// String satisfies the fmt.Stringer interface for the Encoding type. It converts an Encoding into a printable format.
|
||||
func (e Encoding) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
|
38
file.go
38
file.go
|
@ -9,10 +9,11 @@ import (
|
|||
"net/textproto"
|
||||
)
|
||||
|
||||
// FileOption returns a function that can be used for grouping File options
|
||||
// FileOption is a function type used to modify properties of a File
|
||||
type FileOption func(*File)
|
||||
|
||||
// File is an attachment or embedded file of the Msg
|
||||
// File represents a file with properties 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 {
|
||||
ContentType ContentType
|
||||
Desc string
|
||||
|
@ -22,32 +23,35 @@ type File struct {
|
|||
Writer func(w io.Writer) (int64, error)
|
||||
}
|
||||
|
||||
// WithFileContentID sets the Content-ID header for the File
|
||||
// WithFileContentID sets the "Content-ID" header in the File's MIME headers to the specified id.
|
||||
func WithFileContentID(id string) FileOption {
|
||||
return func(f *File) {
|
||||
f.Header.Set(HeaderContentID.String(), id)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFileName sets the filename of the File
|
||||
// WithFileName sets the name of a File to the provided value.
|
||||
func WithFileName(name string) FileOption {
|
||||
return func(f *File) {
|
||||
f.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// WithFileDescription sets an optional file description of the File that will be
|
||||
// added as Content-Description part
|
||||
// WithFileDescription sets an optional file description for the File. The description is used in the
|
||||
// Content-Description header of the MIME output.
|
||||
func WithFileDescription(description string) FileOption {
|
||||
return func(f *File) {
|
||||
f.Desc = description
|
||||
}
|
||||
}
|
||||
|
||||
// WithFileEncoding sets the encoding of the File. By default we should always use
|
||||
// Base64 encoding but there might be exceptions, where this might come handy.
|
||||
// Please note that quoted-printable should never be used for attachments/embeds. If this
|
||||
// is provided as argument, the function will automatically override back to Base64
|
||||
// WithFileEncoding sets the encoding type for a file.
|
||||
//
|
||||
// By default one should always use Base64 encoding for attachments and embeds, but there might be exceptions in
|
||||
// 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 {
|
||||
return func(f *File) {
|
||||
if encoding == EncodingQP {
|
||||
|
@ -58,23 +62,23 @@ func WithFileEncoding(encoding Encoding) FileOption {
|
|||
}
|
||||
|
||||
// WithFileContentType sets the content type of the File.
|
||||
// By default go-mail will try to guess the file type and its corresponding
|
||||
// content type and fall back to application/octet-stream if the file type
|
||||
// could not be guessed. In some cases, however, it might be needed to force
|
||||
// this to a specific type. For such situations this override method can
|
||||
// be used
|
||||
//
|
||||
// By default we will try to guess the file type and its corresponding content type and fall back to
|
||||
// application/octet-stream if the file type, if no matching type could be guessed. This FileOption can
|
||||
// be used to override this type, in case a specific type is required.
|
||||
func WithFileContentType(contentType ContentType) FileOption {
|
||||
return func(f *File) {
|
||||
f.ContentType = contentType
|
||||
}
|
||||
}
|
||||
|
||||
// setHeader sets header fields to a File
|
||||
// setHeader sets the value of a given MIME header field for the File.
|
||||
func (f *File) setHeader(header Header, value string) {
|
||||
f.Header.Set(string(header), value)
|
||||
}
|
||||
|
||||
// getHeader return header fields of a File
|
||||
// getHeader retrieves the value of the specified MIME header field. It returns the header value and a boolean
|
||||
// indicating whether the header was present or not.
|
||||
func (f *File) getHeader(header Header) (string, bool) {
|
||||
v := f.Header.Get(string(header))
|
||||
return v, v != ""
|
||||
|
|
4
go.mod
4
go.mod
|
@ -7,6 +7,6 @@ module github.com/wneessen/go-mail
|
|||
go 1.16
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/text v0.18.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/text v0.19.0
|
||||
)
|
||||
|
|
12
go.sum
12
go.sum
|
@ -5,8 +5,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
|
@ -37,7 +37,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -46,7 +46,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
@ -55,8 +55,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
|
108
header.go
108
header.go
|
@ -4,129 +4,140 @@
|
|||
|
||||
package mail
|
||||
|
||||
// Header represents a generic mail header field name
|
||||
// Header is a type wrapper for a string and represents email header fields in a Msg.
|
||||
type Header string
|
||||
|
||||
// AddrHeader represents a address related mail Header field name
|
||||
// AddrHeader is a type wrapper for a string and represents email address headers fields in a Msg.
|
||||
type AddrHeader string
|
||||
|
||||
// Importance represents a Importance/Priority value string
|
||||
// Importance is a type wrapper for an int and represents the level of importance or priority for a Msg.
|
||||
type Importance int
|
||||
|
||||
// List of common generic header field names
|
||||
const (
|
||||
// HeaderContentDescription is the "Content-Description" header
|
||||
// HeaderContentDescription is the "Content-Description" header.
|
||||
HeaderContentDescription Header = "Content-Description"
|
||||
|
||||
// HeaderContentDisposition is the "Content-Disposition" header
|
||||
// HeaderContentDisposition is the "Content-Disposition" header.
|
||||
HeaderContentDisposition Header = "Content-Disposition"
|
||||
|
||||
// HeaderContentID is the "Content-ID" header
|
||||
// HeaderContentID is the "Content-ID" header.
|
||||
HeaderContentID Header = "Content-ID"
|
||||
|
||||
// HeaderContentLang is the "Content-Language" header
|
||||
// HeaderContentLang is the "Content-Language" header.
|
||||
HeaderContentLang Header = "Content-Language"
|
||||
|
||||
// HeaderContentLocation is the "Content-Location" header (RFC 2110)
|
||||
// HeaderContentLocation is the "Content-Location" header (RFC 2110).
|
||||
// https://datatracker.ietf.org/doc/html/rfc2110#section-4.3
|
||||
HeaderContentLocation Header = "Content-Location"
|
||||
|
||||
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header
|
||||
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header.
|
||||
HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
|
||||
|
||||
// HeaderContentType is the "Content-Type" header
|
||||
// HeaderContentType is the "Content-Type" header.
|
||||
HeaderContentType Header = "Content-Type"
|
||||
|
||||
// HeaderDate represents the "Date" field
|
||||
// See: https://www.rfc-editor.org/rfc/rfc822#section-5.1
|
||||
// HeaderDate represents the "Date" field.
|
||||
// https://datatracker.ietf.org/doc/html/rfc822#section-5.1
|
||||
HeaderDate Header = "Date"
|
||||
|
||||
// HeaderDispositionNotificationTo is the MDN header as described in RFC8098
|
||||
// See: https://www.rfc-editor.org/rfc/rfc8098.html#section-2.1
|
||||
// HeaderDispositionNotificationTo is the MDN header as described in RFC 8098.
|
||||
// https://datatracker.ietf.org/doc/html/rfc8098#section-2.1
|
||||
HeaderDispositionNotificationTo Header = "Disposition-Notification-To"
|
||||
|
||||
// HeaderImportance represents the "Importance" field
|
||||
// HeaderImportance represents the "Importance" field.
|
||||
HeaderImportance Header = "Importance"
|
||||
|
||||
// HeaderInReplyTo represents the "In-Reply-To" field
|
||||
// HeaderInReplyTo represents the "In-Reply-To" field.
|
||||
HeaderInReplyTo Header = "In-Reply-To"
|
||||
|
||||
// HeaderListUnsubscribe is the "List-Unsubscribe" header field
|
||||
// HeaderListUnsubscribe is the "List-Unsubscribe" header field.
|
||||
HeaderListUnsubscribe Header = "List-Unsubscribe"
|
||||
|
||||
// HeaderListUnsubscribePost is the "List-Unsubscribe-Post" header field
|
||||
// HeaderListUnsubscribePost is the "List-Unsubscribe-Post" header field.
|
||||
HeaderListUnsubscribePost Header = "List-Unsubscribe-Post"
|
||||
|
||||
// HeaderMessageID represents the "Message-ID" field for message identification
|
||||
// See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5
|
||||
// HeaderMessageID represents the "Message-ID" field for message identification.
|
||||
// https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.5
|
||||
HeaderMessageID Header = "Message-ID"
|
||||
|
||||
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045
|
||||
// See: https://datatracker.ietf.org/doc/html/rfc2045#section-4
|
||||
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045.
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-4
|
||||
HeaderMIMEVersion Header = "MIME-Version"
|
||||
|
||||
// HeaderOrganization is the "Organization" header field
|
||||
// HeaderOrganization is the "Organization" header field.
|
||||
HeaderOrganization Header = "Organization"
|
||||
|
||||
// HeaderPrecedence is the "Precedence" header field
|
||||
// HeaderPrecedence is the "Precedence" header field.
|
||||
HeaderPrecedence Header = "Precedence"
|
||||
|
||||
// HeaderPriority represents the "Priority" field
|
||||
// HeaderPriority represents the "Priority" field.
|
||||
HeaderPriority Header = "Priority"
|
||||
|
||||
// HeaderReferences is the "References" header field
|
||||
// HeaderReferences is the "References" header field.
|
||||
HeaderReferences Header = "References"
|
||||
|
||||
// HeaderReplyTo is the "Reply-To" header field
|
||||
// HeaderReplyTo is the "Reply-To" header field.
|
||||
HeaderReplyTo Header = "Reply-To"
|
||||
|
||||
// HeaderSubject is the "Subject" header field
|
||||
// HeaderSubject is the "Subject" header field.
|
||||
HeaderSubject Header = "Subject"
|
||||
|
||||
// HeaderUserAgent is the "User-Agent" header field
|
||||
// HeaderUserAgent is the "User-Agent" header field.
|
||||
HeaderUserAgent Header = "User-Agent"
|
||||
|
||||
// HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field
|
||||
// HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field.
|
||||
HeaderXAutoResponseSuppress Header = "X-Auto-Response-Suppress"
|
||||
|
||||
// HeaderXMailer is the "X-Mailer" header field
|
||||
// HeaderXMailer is the "X-Mailer" header field.
|
||||
HeaderXMailer Header = "X-Mailer"
|
||||
|
||||
// HeaderXMSMailPriority is the "X-MSMail-Priority" header field
|
||||
// HeaderXMSMailPriority is the "X-MSMail-Priority" header field.
|
||||
HeaderXMSMailPriority Header = "X-MSMail-Priority"
|
||||
|
||||
// HeaderXPriority is the "X-Priority" header field
|
||||
// HeaderXPriority is the "X-Priority" header field.
|
||||
HeaderXPriority Header = "X-Priority"
|
||||
)
|
||||
|
||||
// List of common address header field names
|
||||
const (
|
||||
// HeaderBcc is the "Blind Carbon Copy" header field
|
||||
// HeaderBcc is the "Blind Carbon Copy" header field.
|
||||
HeaderBcc AddrHeader = "Bcc"
|
||||
|
||||
// HeaderCc is the "Carbon Copy" header field
|
||||
// HeaderCc is the "Carbon Copy" header field.
|
||||
HeaderCc AddrHeader = "Cc"
|
||||
|
||||
// HeaderEnvelopeFrom is the envelope FROM header field
|
||||
// It's not included in the mail body but only used by the Client for the envelope
|
||||
// HeaderEnvelopeFrom is the envelope FROM header field.
|
||||
//
|
||||
// It is generally not included in the mail body but only used by the Client for the communication with the
|
||||
// SMTP server. If the Msg has no "FROM" address set in the mail body, the msgWriter will try to use the
|
||||
// envelope from address, if this has been set for the Msg.
|
||||
HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom"
|
||||
|
||||
// HeaderFrom is the "From" header field
|
||||
// HeaderFrom is the "From" header field.
|
||||
HeaderFrom AddrHeader = "From"
|
||||
|
||||
// HeaderTo is the "Receipient" header field
|
||||
// HeaderTo is the "Receipient" header field.
|
||||
HeaderTo AddrHeader = "To"
|
||||
)
|
||||
|
||||
// List of Importance values
|
||||
const (
|
||||
// ImportanceLow indicates a low level of importance or priority in a Msg.
|
||||
ImportanceLow Importance = iota
|
||||
|
||||
// ImportanceNormal indicates a standard level of importance or priority for a Msg.
|
||||
ImportanceNormal
|
||||
|
||||
// ImportanceHigh indicates a high level of importance or priority in a Msg.
|
||||
ImportanceHigh
|
||||
|
||||
// ImportanceNonUrgent indicates a non-urgent level of importance or priority in a Msg.
|
||||
ImportanceNonUrgent
|
||||
|
||||
// ImportanceUrgent indicates an urgent level of importance or priority in a Msg.
|
||||
ImportanceUrgent
|
||||
)
|
||||
|
||||
// NumString returns the importance number string based on the Importance
|
||||
// NumString returns a numerical string representation of the Importance, mapping ImportanceHigh and
|
||||
// ImportanceUrgent to "1" and others to "0".
|
||||
func (i Importance) NumString() string {
|
||||
switch i {
|
||||
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 {
|
||||
switch i {
|
||||
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 {
|
||||
switch i {
|
||||
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 {
|
||||
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 {
|
||||
return string(a)
|
||||
}
|
||||
|
|
231
msg.go
231
msg.go
|
@ -24,111 +24,134 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// ErrNoFromAddress should be used when a FROM address is requrested but not set
|
||||
// ErrNoFromAddress indicates that the FROM address is not set, which is required.
|
||||
ErrNoFromAddress = errors.New("no FROM address set")
|
||||
|
||||
// ErrNoRcptAddresses should be used when the list of RCPTs is empty
|
||||
// ErrNoRcptAddresses indicates that no recipient addresses have been set.
|
||||
ErrNoRcptAddresses = errors.New("no recipient addresses set")
|
||||
)
|
||||
|
||||
const (
|
||||
// errTplExecuteFailed is issued when the template execution was not successful
|
||||
// errTplExecuteFailed indicates that the execution of a template has failed, including the underlying error.
|
||||
errTplExecuteFailed = "failed to execute template: %w"
|
||||
|
||||
// errTplPointerNil is issued when a template pointer is expected but it is nil
|
||||
// errTplPointerNil indicates that a template pointer is nil, which prevents further template execution or
|
||||
// processing.
|
||||
errTplPointerNil = "template pointer is nil"
|
||||
|
||||
// errParseMailAddr is used when a mail address could not be validated
|
||||
// errParseMailAddr indicates that parsing of a mail address has failed, including the problematic address
|
||||
// and error.
|
||||
errParseMailAddr = "failed to parse mail address %q: %w"
|
||||
)
|
||||
|
||||
const (
|
||||
// NoPGP indicates that a message should not be treated as PGP encrypted
|
||||
// or signed and is the default value for a message
|
||||
// NoPGP indicates that a message should not be treated as PGP encrypted or signed and is the default value
|
||||
// for a message
|
||||
NoPGP PGPType = iota
|
||||
// PGPEncrypt indicates that a message should be treated as PGP encrypted
|
||||
// This works closely together with the corresponding go-mail-middleware
|
||||
// PGPEncrypt indicates that a message should be treated as PGP encrypted. This works closely together with
|
||||
// the corresponding go-mail-middleware.
|
||||
PGPEncrypt
|
||||
// PGPSignature indicates that a message should be treated as PGP signed
|
||||
// This works closely together with the corresponding go-mail-middleware
|
||||
// PGPSignature indicates that a message should be treated as PGP signed. This works closely together with
|
||||
// the corresponding go-mail-middleware.
|
||||
PGPSignature
|
||||
)
|
||||
|
||||
// MiddlewareType is the type description of the Middleware and needs to be returned
|
||||
// in the Middleware interface by the Type method
|
||||
// MiddlewareType is a type wrapper for a string. It describes the type of the Middleware and needs to be
|
||||
// returned by the Middleware.Type method to satisfy the Middleware interface.
|
||||
type MiddlewareType string
|
||||
|
||||
// Middleware is an interface to define a function to apply to Msg before sending
|
||||
// Middleware represents the interface for modifying or handling email messages. A Middleware allows the user to
|
||||
// alter a Msg before it is finally processed. Multiple Middleware can be applied to a Msg.
|
||||
//
|
||||
// Type returns a unique MiddlewareType. It describes the type of Middleware and makes sure that
|
||||
// a Middleware is only applied once.
|
||||
// Handle performs all the processing to the Msg. It always needs to return a Msg back.
|
||||
type Middleware interface {
|
||||
Handle(*Msg) *Msg
|
||||
Type() MiddlewareType
|
||||
}
|
||||
|
||||
// PGPType is a type alias for a int representing a type of PGP encryption
|
||||
// or signature
|
||||
// PGPType is a type wrapper for an int, representing a type of PGP encryption or signature.
|
||||
type PGPType int
|
||||
|
||||
// Msg is the mail message struct
|
||||
// Msg represents an email message with various headers, attachments, and encoding settings.
|
||||
//
|
||||
// The Msg is the central part of go-mail. It provided a lot of methods that you would expect in a mail
|
||||
// user agent (MUA). Msg satisfies the io.WriterTo and io.Reader interfaces.
|
||||
type Msg struct {
|
||||
// addrHeader is a slice of strings that the different mail AddrHeader fields
|
||||
// addrHeader holds a mapping between AddrHeader keys and their corresponding slices of mail.Address pointers.
|
||||
addrHeader map[AddrHeader][]*mail.Address
|
||||
|
||||
// attachments represent the different attachment File of the Msg
|
||||
// attachments holds a list of File pointers that represent files either as attachments or embeds files in
|
||||
// a Msg.
|
||||
attachments []*File
|
||||
|
||||
// boundary is the MIME content boundary
|
||||
// boundary represents the delimiter for separating parts in a multipart message.
|
||||
boundary string
|
||||
|
||||
// charset represents the charset of the mail (defaults to UTF-8)
|
||||
// charset represents the Charset of the Msg.
|
||||
//
|
||||
// By default we set CharsetUTF8 for a Msg unless overridden by a corresponding MsgOption.
|
||||
charset Charset
|
||||
|
||||
// embeds represent the different embedded File of the Msg
|
||||
// embeds contains a slice of File pointers representing the embedded files in a Msg.
|
||||
embeds []*File
|
||||
|
||||
// encoder represents a mime.WordEncoder from the std lib
|
||||
// encoder is a mime.WordEncoder used to encode strings (such as email headers) using a specified
|
||||
// Encoding.
|
||||
encoder mime.WordEncoder
|
||||
|
||||
// encoding represents the message encoding (the encoder will be a corresponding WordEncoder)
|
||||
// encoding specifies the type of Encoding used for email messages and/or parts.
|
||||
encoding Encoding
|
||||
|
||||
// genHeader is a slice of strings that the different generic mail Header fields
|
||||
// genHeader is a map where the keys are email headers (of type Header) and the values are slices of strings
|
||||
// representing header values.
|
||||
genHeader map[Header][]string
|
||||
|
||||
// isDelivered signals if a message has been delivered or not
|
||||
// isDelivered indicates wether the Msg has been delivered.
|
||||
isDelivered bool
|
||||
|
||||
// middlewares is the list of middlewares to apply to the Msg before sending in FIFO order
|
||||
// middlewares is a slice of Middleware used for modifying or handling messages before they are processed.
|
||||
//
|
||||
// middlewares are processed in FIFO order.
|
||||
middlewares []Middleware
|
||||
|
||||
// mimever represents the MIME version
|
||||
// mimever represents the MIME version used in a Msg.
|
||||
mimever MIMEVersion
|
||||
|
||||
// parts represent the different parts of the Msg
|
||||
// parts is a slice that holds pointers to Part structures, which represent different parts of a Msg.
|
||||
parts []*Part
|
||||
|
||||
// preformHeader is a slice of strings that the different generic mail Header fields
|
||||
// of which content is already preformated and will not be affected by the automatic line
|
||||
// breaks
|
||||
// preformHeader maps Header types to their already preformatted string values.
|
||||
//
|
||||
// Preformatted Header values will not be affected by automatic line breaks.
|
||||
preformHeader map[Header]string
|
||||
|
||||
// pgptype indicates that a message has a PGPType assigned and therefore will generate
|
||||
// different Content-Type settings in the msgWriter
|
||||
// different Content-Type settings in the msgWriter.
|
||||
pgptype PGPType
|
||||
|
||||
// sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation
|
||||
// sendError represents an error encountered during the process of sending a Msg during the
|
||||
// Client.Send operation.
|
||||
//
|
||||
// sendError will hold an error of type SendError.
|
||||
sendError error
|
||||
|
||||
// noDefaultUserAgent indicates whether the default User Agent will be excluded for the Msg when it's sent.
|
||||
// noDefaultUserAgent indicates whether the default User-Agent will be omitted for the Msg when it is
|
||||
// being sent.
|
||||
//
|
||||
// This can be useful in scenarios where headers are conditionally passed based on receipt - i. e. SMTP proxies.
|
||||
noDefaultUserAgent bool
|
||||
}
|
||||
|
||||
// SendmailPath is the default system path to the sendmail binary
|
||||
// SendmailPath is the default system path to the sendmail binary - at least on standard Unix-like OS.
|
||||
const SendmailPath = "/usr/sbin/sendmail"
|
||||
|
||||
// MsgOption returns a function that can be used for grouping Msg options
|
||||
// MsgOption is a function type that modifies a Msg instance during its creation or initialization.
|
||||
type MsgOption func(*Msg)
|
||||
|
||||
// NewMsg returns a new Msg pointer
|
||||
// NewMsg creates a new email message with optional MsgOption functions that customize various aspects of the
|
||||
// message.
|
||||
func NewMsg(opts ...MsgOption) *Msg {
|
||||
msg := &Msg{
|
||||
addrHeader: make(map[AddrHeader][]*mail.Address),
|
||||
|
@ -139,7 +162,7 @@ func NewMsg(opts ...MsgOption) *Msg {
|
|||
mimever: MIME10,
|
||||
}
|
||||
|
||||
// Override defaults with optionally provided MsgOption functions
|
||||
// Override defaults with optionally provided MsgOption functions.
|
||||
for _, option := range opts {
|
||||
if option == nil {
|
||||
continue
|
||||
|
@ -153,101 +176,133 @@ func NewMsg(opts ...MsgOption) *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 {
|
||||
return func(m *Msg) {
|
||||
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 {
|
||||
return func(m *Msg) {
|
||||
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 {
|
||||
return func(m *Msg) {
|
||||
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 {
|
||||
return func(m *Msg) {
|
||||
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 {
|
||||
return func(m *Msg) {
|
||||
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 {
|
||||
return func(m *Msg) {
|
||||
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 {
|
||||
return func(m *Msg) {
|
||||
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) {
|
||||
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) {
|
||||
m.encoding = e
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
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 {
|
||||
return m.charset.String()
|
||||
}
|
||||
|
||||
// SetHeader sets a generic header field of the Msg
|
||||
// For adding address headers like "To:" or "From", see SetAddrHeader
|
||||
// SetHeader sets a generic header field of the Msg.
|
||||
//
|
||||
// Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead
|
||||
// Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead.
|
||||
// For adding address headers like "To:" or "From", use SetAddrHeader instead.
|
||||
func (m *Msg) SetHeader(header Header, values ...string) {
|
||||
m.SetGenHeader(header, values...)
|
||||
}
|
||||
|
||||
// SetGenHeader sets a generic header field of the Msg
|
||||
// For adding address headers like "To:" or "From", see SetAddrHeader
|
||||
// SetGenHeader sets a generic header field of the Msg to the provided list of values.
|
||||
//
|
||||
// Note: for adding email address related headers (like "To:" or "From") use SetAddrHeader instead.
|
||||
func (m *Msg) SetGenHeader(header Header, values ...string) {
|
||||
if m.genHeader == nil {
|
||||
m.genHeader = make(map[Header][]string)
|
||||
|
@ -258,26 +313,24 @@ func (m *Msg) SetGenHeader(header Header, values ...string) {
|
|||
m.genHeader[header] = values
|
||||
}
|
||||
|
||||
// SetHeaderPreformatted sets a generic header field of the Msg which content is
|
||||
// already preformated.
|
||||
// SetHeaderPreformatted sets a generic header field of the Msg, which content is already preformatted.
|
||||
//
|
||||
// Deprecated: This method only exists for compatibility reason. Please use
|
||||
// SetGenHeaderPreformatted instead
|
||||
// Deprecated: This method only exists for compatibility reason. Please use SetGenHeaderPreformatted instead.
|
||||
func (m *Msg) SetHeaderPreformatted(header Header, value string) {
|
||||
m.SetGenHeaderPreformatted(header, value)
|
||||
}
|
||||
|
||||
// SetGenHeaderPreformatted sets a generic header field of the Msg which content is
|
||||
// already preformated.
|
||||
// SetGenHeaderPreformatted sets a generic header field of the Msg which content is already preformated.
|
||||
//
|
||||
// This method does not take a slice of values but only a single value. This is
|
||||
// due to the fact, that we do not perform any content alteration and expect the
|
||||
// user has already done so
|
||||
// This method does not take a slice of values but only a single value. The reason for this is that we do not
|
||||
// perform any content alteration on these kind of headers and expect the user to have already taken care of
|
||||
// any kind of formatting required for the header.
|
||||
//
|
||||
// **Please note:** This method should be used only as a last resort. Since the
|
||||
// user is respondible for the formating of the message header, go-mail cannot
|
||||
// guarantee the fully compliance with the RFC 2822. It is recommended to use
|
||||
// SetGenHeader instead.
|
||||
// Note: This method should be used only as a last resort. Since the user is respondible for the formatting of
|
||||
// the message header, we cannot guarantee any compliance with the RFC 2822. It is advised to use SetGenHeader
|
||||
// instead.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822
|
||||
func (m *Msg) SetGenHeaderPreformatted(header Header, value string) {
|
||||
if m.preformHeader == nil {
|
||||
m.preformHeader = make(map[Header]string)
|
||||
|
@ -285,7 +338,13 @@ func (m *Msg) SetGenHeaderPreformatted(header Header, value string) {
|
|||
m.preformHeader[header] = value
|
||||
}
|
||||
|
||||
// SetAddrHeader sets an address related header field of the Msg
|
||||
// SetAddrHeader sets the specified AddrHeader for the Msg to the given values.
|
||||
//
|
||||
// Addresses are parsed according to RFC 5322. If parsing 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 {
|
||||
if m.addrHeader == nil {
|
||||
m.addrHeader = make(map[AddrHeader][]*mail.Address)
|
||||
|
@ -309,8 +368,12 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SetAddrHeaderIgnoreInvalid sets an address related header field of the Msg and ignores invalid address
|
||||
// in the validation process
|
||||
// SetAddrHeaderIgnoreInvalid sets the specified AddrHeader for the Msg to the given values.
|
||||
//
|
||||
// Addresses are parsed according to RFC 5322. If parsing of ANY of the provided values fails,
|
||||
// the error is ignored and the address omiitted from the address list.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
|
||||
func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
||||
var addresses []*mail.Address
|
||||
for _, addrVal := range values {
|
||||
|
@ -323,14 +386,26 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
|||
m.addrHeader[header] = addresses
|
||||
}
|
||||
|
||||
// EnvelopeFrom takes and validates a given mail address and sets it as envelope "FROM"
|
||||
// addrHeader of the Msg
|
||||
// EnvelopeFrom sets the envelope from address for the Msg.
|
||||
//
|
||||
// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the Client for 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 {
|
||||
return m.SetAddrHeader(HeaderEnvelopeFrom, from)
|
||||
}
|
||||
|
||||
// EnvelopeFromFormat takes a name and address, formats them RFC5322 compliant and stores them as
|
||||
// the envelope FROM address header field
|
||||
// EnvelopeFromFormat sets the provided name and mail address as HeaderEnvelopeFrom for the Msg.
|
||||
//
|
||||
// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the Client for 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 {
|
||||
return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, name, addr))
|
||||
}
|
||||
|
|
33
smtp/smtp.go
33
smtp/smtp.go
|
@ -36,7 +36,15 @@ import (
|
|||
"github.com/wneessen/go-mail/log"
|
||||
)
|
||||
|
||||
var ErrNonTLSConnection = errors.New("connection is not using TLS")
|
||||
var (
|
||||
|
||||
// ErrNonTLSConnection is returned when an attempt is made to retrieve TLS state on a non-TLS connection.
|
||||
ErrNonTLSConnection = errors.New("connection is not using TLS")
|
||||
|
||||
// ErrNoConnection is returned when attempting to perform an operation that requires an established
|
||||
// connection but none exists.
|
||||
ErrNoConnection = errors.New("connection is not established")
|
||||
)
|
||||
|
||||
// A Client represents a client connection to an SMTP server.
|
||||
type Client struct {
|
||||
|
@ -67,6 +75,9 @@ type Client struct {
|
|||
// helloError is the error from the hello
|
||||
helloError error
|
||||
|
||||
// isConnected indicates if the Client has an active connection
|
||||
isConnected bool
|
||||
|
||||
// localName is the name to use in HELO/EHLO
|
||||
localName string // the name to use in HELO/EHLO
|
||||
|
||||
|
@ -113,6 +124,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
|||
}
|
||||
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
|
||||
_, c.tls = conn.(*tls.Conn)
|
||||
c.isConnected = true
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
@ -121,6 +133,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
|||
func (c *Client) Close() error {
|
||||
c.mutex.Lock()
|
||||
err := c.Text.Close()
|
||||
c.isConnected = false
|
||||
c.mutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
@ -516,8 +529,7 @@ func (c *Client) Quit() error {
|
|||
}
|
||||
c.mutex.Lock()
|
||||
err = c.Text.Close()
|
||||
c.Text = nil
|
||||
c.conn = nil
|
||||
c.isConnected = false
|
||||
c.mutex.Unlock()
|
||||
|
||||
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.
|
||||
func (c *Client) HasConnection() bool {
|
||||
c.mutex.RLock()
|
||||
conn := c.conn
|
||||
isConn := c.isConnected
|
||||
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 {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return fmt.Errorf("smtp: failed to update deadline: %w", err)
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -578,17 +591,17 @@ func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) {
|
|||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
if !c.isConnected {
|
||||
return nil, ErrNoConnection
|
||||
}
|
||||
if !c.tls {
|
||||
return nil, ErrNonTLSConnection
|
||||
}
|
||||
if c.conn == nil {
|
||||
return nil, errors.New("smtp: connection is not established")
|
||||
}
|
||||
if conn, ok := c.conn.(*tls.Conn); ok {
|
||||
cstate := conn.ConnectionState()
|
||||
return &cstate, nil
|
||||
}
|
||||
return nil, errors.New("smtp: connection is not a TLS connection")
|
||||
return nil, errors.New("unable to retrieve TLS connection state")
|
||||
}
|
||||
|
||||
// debugLog checks if the debug flag is set and if so logs the provided message to
|
||||
|
|
|
@ -1640,6 +1640,357 @@ func TestTLSConnState(t *testing.T) {
|
|||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_GetTLSConnectionState(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
cfg := &tls.Config{ServerName: "example.com"}
|
||||
testHookStartTLS(cfg) // set the RootCAs
|
||||
if err := c.StartTLS(cfg); err != nil {
|
||||
t.Errorf("StartTLS: %v", err)
|
||||
return
|
||||
}
|
||||
cs, err := c.GetTLSConnectionState()
|
||||
if err != nil {
|
||||
t.Errorf("failed to get TLSConnectionState: %s", err)
|
||||
return
|
||||
}
|
||||
if cs.Version == 0 || !cs.HandshakeComplete {
|
||||
t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs)
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_GetTLSConnectionState_noTLS(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
_, err = c.GetTLSConnectionState()
|
||||
if err == nil {
|
||||
t.Error("GetTLSConnectionState: expected error; got nil")
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_GetTLSConnectionState_noConn(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
_ = c.Close()
|
||||
_, err = c.GetTLSConnectionState()
|
||||
if err == nil {
|
||||
t.Error("GetTLSConnectionState: expected error; got nil")
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_GetTLSConnectionState_unableErr(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
c.tls = true
|
||||
_, err = c.GetTLSConnectionState()
|
||||
if err == nil {
|
||||
t.Error("GetTLSConnectionState: expected error; got nil")
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_HasConnection(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
cfg := &tls.Config{ServerName: "example.com"}
|
||||
testHookStartTLS(cfg) // set the RootCAs
|
||||
if err := c.StartTLS(cfg); err != nil {
|
||||
t.Errorf("StartTLS: %v", err)
|
||||
return
|
||||
}
|
||||
if !c.HasConnection() {
|
||||
t.Error("HasConnection: expected true; got false")
|
||||
return
|
||||
}
|
||||
if err = c.Quit(); err != nil {
|
||||
t.Errorf("closing connection failed: %s", err)
|
||||
return
|
||||
}
|
||||
if c.HasConnection() {
|
||||
t.Error("HasConnection: expected false; got true")
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_SetDSNMailReturnOption(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
c.SetDSNMailReturnOption("foo")
|
||||
if c.dsnmrtype != "foo" {
|
||||
t.Errorf("SetDSNMailReturnOption: expected %s; got %s", "foo", c.dsnrntype)
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_SetDSNRcptNotifyOption(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err := serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Quit()
|
||||
}()
|
||||
c.SetDSNRcptNotifyOption("foo")
|
||||
if c.dsnrntype != "foo" {
|
||||
t.Errorf("SetDSNMailReturnOption: expected %s; got %s", "foo", c.dsnrntype)
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestClient_UpdateDeadline(t *testing.T) {
|
||||
ln := newLocalListener(t)
|
||||
defer func() {
|
||||
_ = ln.Close()
|
||||
}()
|
||||
clientDone := make(chan bool)
|
||||
serverDone := make(chan bool)
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err = serverHandle(c, t); err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer close(clientDone)
|
||||
c, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("Client dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if !c.HasConnection() {
|
||||
t.Error("HasConnection: expected true; got false")
|
||||
return
|
||||
}
|
||||
if err = c.UpdateDeadline(time.Millisecond * 20); err != nil {
|
||||
t.Errorf("failed to update deadline: %s", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
if !c.HasConnection() {
|
||||
t.Error("HasConnection: expected true; got false")
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-clientDone
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func newLocalListener(t *testing.T) net.Listener {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
|
@ -1685,6 +2036,8 @@ func serverHandle(c net.Conn, t *testing.T) error {
|
|||
}
|
||||
config := &tls.Config{Certificates: []tls.Certificate{keypair}}
|
||||
return tf(config)
|
||||
case "QUIT":
|
||||
return nil
|
||||
default:
|
||||
t.Fatalf("unrecognized command: %q", s.Text())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue